From 7f149f93c13682e9710473e3c08a6eb899bfe6a3 Mon Sep 17 00:00:00 2001 From: Samuel Hawksby-Robinson Date: Wed, 15 Jun 2022 15:49:31 +0100 Subject: [PATCH] Get preferred network IP and refactor server package to increase reusability (#2626) * Added function to get preffered network IP Also done some refactor work oon server package to make a lot more reusable * Added server.Option and simplified handler funcs * Added serial number deterministically generated from pk * Debugging TLS server connection * Implemented configurable server ip When accessing over the network the server needs to listen on the network port and not localhost or 127.0.0.1 . Also the cert can now have a dedicated IP * Refactor of URL funcs to use the url package * Removed redundant Options pattern in favour of config param * Added full server test using GetOutboundIP * Remove references and usage of Server.port The application does not need to set the port, we rely on the net.Listener to pick a port. * Version bump * Added ToECDSA func and improved cert testing * Added error check in test * Split Server types, embedding raw Server funcs into specialised server types * localhost * Implemented DNS and IP based cert gen ios doesn't allow for restricted ip addresses to be used in a valid tls cert * Replace listener handling with original port store Also added handlers as a parameter of the Server --- VERSION | 2 +- node/get_status_node.go | 6 +- protocol/common/message.go | 17 +-- protocol/messenger.go | 20 ++- protocol/messenger_config.go | 4 +- protocol/messenger_images.go | 4 +- server/certs.go | 110 +++++++++++++-- server/certs_test.go | 93 +++++++++++++ server/handlers.go | 135 ++++++++++++++++++ server/ips.go | 22 +++ server/ips_test.go | 82 +++++++++++ server/server.go | 257 +++++++---------------------------- server/server_media.go | 75 ++++++++++ server/server_pairing.go | 22 +++ server/server_test.go | 74 ++++++++++ services/ext/service.go | 14 +- services/stickers/api.go | 7 +- services/stickers/service.go | 4 +- 18 files changed, 686 insertions(+), 262 deletions(-) create mode 100644 server/certs_test.go create mode 100644 server/handlers.go create mode 100644 server/ips.go create mode 100644 server/ips_test.go create mode 100644 server/server_media.go create mode 100644 server/server_pairing.go create mode 100644 server/server_test.go diff --git a/VERSION b/VERSION index ef0aff387..5d6a8337a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.101.1 +0.101.2 diff --git a/node/get_status_node.go b/node/get_status_node.go index ead6dc3da..25b14e3e4 100644 --- a/node/get_status_node.go +++ b/node/get_status_node.go @@ -82,7 +82,7 @@ type StatusNode struct { rpcClient *rpc.Client // reference to an RPC client downloader *ipfs.Downloader - httpServer *server.Server + httpServer *server.MediaServer discovery discovery.Discovery register *peers.Register @@ -152,7 +152,7 @@ func (n *StatusNode) GethNode() *node.Node { return n.gethNode } -func (n *StatusNode) HTTPServer() *server.Server { +func (n *StatusNode) HTTPServer() *server.MediaServer { n.mu.RLock() defer n.mu.RUnlock() @@ -238,7 +238,7 @@ func (n *StatusNode) startWithDB(config *params.NodeConfig, accs *accounts.Manag n.downloader = ipfs.NewDownloader(config.RootDataDir) - httpServer, err := server.NewServer(n.appDB, n.downloader) + httpServer, err := server.NewMediaServer(n.appDB, n.downloader) if err != nil { return err } diff --git a/protocol/common/message.go b/protocol/common/message.go index 9956e3d8b..7b84f5713 100644 --- a/protocol/common/message.go +++ b/protocol/common/message.go @@ -19,6 +19,7 @@ import ( "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/images" "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/server" ) // QuotedMessage contains the original text of the message replied to @@ -35,10 +36,6 @@ type QuotedMessage struct { CommunityID string `json:"communityId,omitempty"` } -func (m *QuotedMessage) PrepareImageURL(port int) { - m.ImageLocalURL = fmt.Sprintf("https://localhost:%d/messages/images?messageId=%s", port, m.ID) -} - type CommandState int const ( @@ -177,20 +174,20 @@ type Message struct { ContactRequestState ContactRequestState `json:"contactRequestState,omitempty"` } -func (m *Message) PrepareServerURLs(port int) { - m.Identicon = fmt.Sprintf("https://localhost:%d/messages/identicons?publicKey=%s", port, m.From) +func (m *Message) PrepareServerURLs(s *server.MediaServer) { + m.Identicon = s.MakeIdenticonURL(m.From) if m.QuotedMessage != nil && m.QuotedMessage.ContentType == int64(protobuf.ChatMessage_IMAGE) { - m.QuotedMessage.PrepareImageURL(port) + m.QuotedMessage.ImageLocalURL = s.MakeImageURL(m.QuotedMessage.ID) } if m.ContentType == protobuf.ChatMessage_IMAGE { - m.ImageLocalURL = fmt.Sprintf("https://localhost:%d/messages/images?messageId=%s", port, m.ID) + m.ImageLocalURL = s.MakeImageURL(m.ID) } if m.ContentType == protobuf.ChatMessage_AUDIO { - m.AudioLocalURL = fmt.Sprintf("https://localhost:%d/messages/audio?messageId=%s", port, m.ID) + m.AudioLocalURL = s.MakeAudioURL(m.ID) } if m.ContentType == protobuf.ChatMessage_STICKER { - m.StickerLocalURL = fmt.Sprintf("https://localhost:%d/ipfs?hash=%s", port, m.GetSticker().Hash) + m.StickerLocalURL = s.MakeStickerURL(m.GetSticker().Hash) } } diff --git a/protocol/messenger.go b/protocol/messenger.go index f37429823..df21941ba 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -15,22 +15,18 @@ import ( "sync" "time" - "github.com/status-im/status-go/contracts" - "github.com/status-im/status-go/services/browsers" - + "github.com/davecgh/go-spew/spew" + "github.com/golang/protobuf/proto" "github.com/pkg/errors" "go.uber.org/zap" - "github.com/ethereum/go-ethereum/event" - - "github.com/davecgh/go-spew/spew" - "github.com/golang/protobuf/proto" - gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/p2p" "github.com/status-im/status-go/appdatabase" "github.com/status-im/status-go/appmetrics" "github.com/status-im/status-go/connection" + "github.com/status-im/status-go/contracts" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" userimage "github.com/status-im/status-go/images" @@ -56,9 +52,9 @@ import ( "github.com/status-im/status-go/protocol/transport" v1protocol "github.com/status-im/status-go/protocol/v1" "github.com/status-im/status-go/server" + "github.com/status-im/status-go/services/browsers" "github.com/status-im/status-go/services/ext/mailservers" mailserversDB "github.com/status-im/status-go/services/mailservers" - "github.com/status-im/status-go/telemetry" ) @@ -128,7 +124,7 @@ type Messenger struct { account *multiaccounts.Account mailserversDatabase *mailserversDB.Database browserDatabase *browsers.Database - httpServer *server.Server + httpServer *server.MediaServer quit chan struct{} requestedCommunitiesLock sync.RWMutex @@ -4220,7 +4216,7 @@ func (m *Messenger) MessageByChatID(chatID, cursor string, limit int) ([]*common } if m.httpServer != nil { for idx := range msgs { - msgs[idx].PrepareServerURLs(m.httpServer.Port) + msgs[idx].PrepareServerURLs(m.httpServer) } } @@ -4230,7 +4226,7 @@ func (m *Messenger) MessageByChatID(chatID, cursor string, limit int) ([]*common func (m *Messenger) prepareMessages(messages map[string]*common.Message) { if m.httpServer != nil { for idx := range messages { - messages[idx].PrepareServerURLs(m.httpServer.Port) + messages[idx].PrepareServerURLs(m.httpServer) } } } diff --git a/protocol/messenger_config.go b/protocol/messenger_config.go index e6b2ba71b..b0431e032 100644 --- a/protocol/messenger_config.go +++ b/protocol/messenger_config.go @@ -70,7 +70,7 @@ type config struct { clusterConfig params.ClusterConfig browserDatabase *browsers.Database torrentConfig *params.TorrentConfig - httpServer *server.Server + httpServer *server.MediaServer rpcClient *rpc.Client verifyTransactionClient EthClient @@ -280,7 +280,7 @@ func WithTorrentConfig(tc *params.TorrentConfig) Option { } } -func WithHTTPServer(s *server.Server) Option { +func WithHTTPServer(s *server.MediaServer) Option { return func(c *config) error { c.httpServer = s return nil diff --git a/protocol/messenger_images.go b/protocol/messenger_images.go index e54b6bb63..b288c6136 100644 --- a/protocol/messenger_images.go +++ b/protocol/messenger_images.go @@ -1,7 +1,5 @@ package protocol -import "fmt" - func (m *Messenger) ImageServerURL() string { - return fmt.Sprintf("https://localhost:%d/messages/", m.httpServer.Port) + return m.httpServer.MakeImageServerURL() } diff --git a/server/certs.go b/server/certs.go index 30eb683b1..3b34a213a 100644 --- a/server/certs.go +++ b/server/certs.go @@ -2,35 +2,53 @@ package server import ( "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" + "crypto/sha256" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" + "net" "time" ) -func GenerateX509Cert(from, to time.Time) (*x509.Certificate, error) { +var globalCertificate *tls.Certificate = nil +var globalPem string + +func makeRandomSerialNumber() (*big.Int, error) { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + return rand.Int(rand.Reader, serialNumberLimit) +} - if err != nil { - return nil, err - } +func makeSerialNumberFromKey(pk *ecdsa.PrivateKey) *big.Int { + h := sha256.New() + h.Write(append(pk.D.Bytes(), append(pk.Y.Bytes(), pk.X.Bytes()...)...)) - template := &x509.Certificate{ - SerialNumber: serialNumber, + return new(big.Int).SetBytes(h.Sum(nil)) +} + +func GenerateX509Cert(sn *big.Int, from, to time.Time, hostname string) *x509.Certificate { + c := &x509.Certificate{ + SerialNumber: sn, Subject: pkix.Name{Organization: []string{"Self-signed cert"}}, NotBefore: from, NotAfter: to, - DNSNames: []string{"localhost"}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true, } - return template, nil + ip := net.ParseIP(hostname) + if ip != nil { + c.IPAddresses = []net.IP{ip} + } else { + c.DNSNames = []string{hostname} + } + + return c } func GenerateX509PEMs(cert *x509.Certificate, key *ecdsa.PrivateKey) (certPem, keyPem []byte, err error) { @@ -48,3 +66,77 @@ func GenerateX509PEMs(cert *x509.Certificate, key *ecdsa.PrivateKey) (certPem, k return } + +func generateTLSCert() error { + if globalCertificate != nil { + return nil + } + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + + notBefore := time.Now() + notAfter := notBefore.Add(365 * 24 * time.Hour) + + sn, err := makeRandomSerialNumber() + if err != nil { + return err + } + + cert := GenerateX509Cert(sn, notBefore, notAfter, localhost) + certPem, keyPem, err := GenerateX509PEMs(cert, priv) + if err != nil { + return err + } + + finalCert, err := tls.X509KeyPair(certPem, keyPem) + if err != nil { + return err + } + + globalCertificate = &finalCert + globalPem = string(certPem) + return nil +} + +func PublicTLSCert() (string, error) { + err := generateTLSCert() + if err != nil { + return "", err + } + + return globalPem, nil +} + +func GenerateCertFromKey(pk *ecdsa.PrivateKey, ttl time.Duration, hostname string) (tls.Certificate, []byte, error) { + // TODO fix, this isn't deterministic, + + notBefore := time.Now() + notAfter := notBefore.Add(ttl) + + cert := GenerateX509Cert(makeSerialNumberFromKey(pk), notBefore, notAfter, hostname) + certPem, keyPem, err := GenerateX509PEMs(cert, pk) + if err != nil { + return tls.Certificate{}, nil, err + } + + tlsCert, err := tls.X509KeyPair(certPem, keyPem) + if err != nil { + return tls.Certificate{}, nil, err + } + + return tlsCert, certPem, nil +} + +// ToECDSA takes a []byte of D and uses it to create an ecdsa.PublicKey on the elliptic.P256 curve +// this function is basically a P256 curve version of eth-node/crypto.ToECDSA without all the nice validation +func ToECDSA(d []byte) *ecdsa.PrivateKey { + k := new(ecdsa.PrivateKey) + k.D = new(big.Int).SetBytes(d) + k.PublicKey.Curve = elliptic.P256() + + k.PublicKey.X, k.PublicKey.Y = k.PublicKey.Curve.ScalarBaseMult(d) + return k +} diff --git a/server/certs_test.go b/server/certs_test.go new file mode 100644 index 000000000..d40b5d460 --- /dev/null +++ b/server/certs_test.go @@ -0,0 +1,93 @@ +package server + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "math/big" + "testing" + "time" + + "github.com/btcsuite/btcutil/base58" + "github.com/stretchr/testify/suite" +) + +func TestCerts(t *testing.T) { + suite.Run(t, new(CertsSuite)) +} + +const ( + X = "7744735542292224619198421067303535767629647588258222392379329927711683109548" + Y = "6855516769916529066379811647277920115118980625614889267697023742462401590771" + D = "38564357061962143106230288374146033267100509055924181407058066820384455255240" + DB58 = "6jpbvo2ucrtrnpXXF4DQYuysh697isH9ppd2aT8uSRDh" + SN = "91849736469742262272885892667727604096707836853856473239722372976236128900962" +) + +type CertsSuite struct { + suite.Suite + + X *big.Int + Y *big.Int + D *big.Int + DBytes []byte + SN *big.Int +} + +func (s *CertsSuite) SetupSuite() { + var ok bool + + s.X, ok = new(big.Int).SetString(X, 10) + s.Require().True(ok) + + s.Y, ok = new(big.Int).SetString(Y, 10) + s.Require().True(ok) + + s.D, ok = new(big.Int).SetString(D, 10) + s.Require().True(ok) + + s.DBytes = base58.Decode(DB58) + s.Require().Exactly(s.D.Bytes(), s.DBytes) + + s.SN, ok = new(big.Int).SetString(SN, 10) + s.Require().True(ok) +} + +func (s *CertsSuite) Test_makeSerialNumberFromKey() { + pk := &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: s.X, + Y: s.Y, + }, + D: s.D, + } + + s.Require().Zero(makeSerialNumberFromKey(pk).Cmp(s.SN)) +} + +func (s *CertsSuite) TestToECDSA() { + k := ToECDSA(base58.Decode(DB58)) + s.Require().NotNil(k.PublicKey.X) + s.Require().NotNil(k.PublicKey.Y) + + s.Require().Zero(k.PublicKey.X.Cmp(s.X)) + s.Require().Zero(k.PublicKey.Y.Cmp(s.Y)) + s.Require().Zero(k.D.Cmp(s.D)) + + b58 := base58.Encode(s.D.Bytes()) + s.Require().Equal(DB58, b58) +} + +func (s *CertsSuite) TestGenerateX509Cert() { + notBefore := time.Now() + notAfter := notBefore.Add(time.Hour) + + c1 := GenerateX509Cert(s.SN, notBefore, notAfter, localhost) + s.Require().Exactly([]string{localhost}, c1.DNSNames) + s.Require().Nil(c1.IPAddresses) + + c2 := GenerateX509Cert(s.SN, notBefore, notAfter, defaultIP.String()) + s.Require().Len(c2.IPAddresses, 1) + s.Require().Equal(defaultIP.String(), c2.IPAddresses[0].String()) + s.Require().Nil(c2.DNSNames) +} diff --git a/server/handlers.go b/server/handlers.go new file mode 100644 index 000000000..0862f3ea5 --- /dev/null +++ b/server/handlers.go @@ -0,0 +1,135 @@ +package server + +import ( + "database/sql" + "net/http" + "time" + + "go.uber.org/zap" + + "github.com/status-im/status-go/ipfs" + "github.com/status-im/status-go/protocol/identity/identicon" + "github.com/status-im/status-go/protocol/images" +) + +const ( + basePath = "/messages" + identiconsPath = basePath + "/identicons" + imagesPath = basePath + "/images" + audioPath = basePath + "/audio" + ipfsPath = "/ipfs" +) + +type HandlerPatternMap map[string]http.HandlerFunc + +func handleIdenticon(logger *zap.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pks, ok := r.URL.Query()["publicKey"] + if !ok || len(pks) == 0 { + logger.Error("no publicKey") + return + } + pk := pks[0] + image, err := identicon.Generate(pk) + if err != nil { + logger.Error("could not generate identicon") + } + + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Cache-Control", "max-age:290304000, public") + w.Header().Set("Expires", time.Now().AddDate(60, 0, 0).Format(http.TimeFormat)) + + _, err = w.Write(image) + if err != nil { + logger.Error("failed to write image", zap.Error(err)) + } + } +} + +func handleImage(db *sql.DB, logger *zap.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + messageIDs, ok := r.URL.Query()["messageId"] + if !ok || len(messageIDs) == 0 { + logger.Error("no messageID") + return + } + messageID := messageIDs[0] + var image []byte + err := db.QueryRow(`SELECT image_payload FROM user_messages WHERE id = ?`, messageID).Scan(&image) + if err != nil { + logger.Error("failed to find image", zap.Error(err)) + return + } + if len(image) == 0 { + logger.Error("empty image") + return + } + mime, err := images.ImageMime(image) + if err != nil { + logger.Error("failed to get mime", zap.Error(err)) + } + + w.Header().Set("Content-Type", mime) + w.Header().Set("Cache-Control", "no-store") + + _, err = w.Write(image) + if err != nil { + logger.Error("failed to write image", zap.Error(err)) + } + } +} + +func handleAudio(db *sql.DB, logger *zap.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + messageIDs, ok := r.URL.Query()["messageId"] + if !ok || len(messageIDs) == 0 { + logger.Error("no messageID") + return + } + messageID := messageIDs[0] + var audio []byte + err := db.QueryRow(`SELECT audio_payload FROM user_messages WHERE id = ?`, messageID).Scan(&audio) + if err != nil { + logger.Error("failed to find image", zap.Error(err)) + return + } + if len(audio) == 0 { + logger.Error("empty audio") + return + } + + w.Header().Set("Content-Type", "audio/aac") + w.Header().Set("Cache-Control", "no-store") + + _, err = w.Write(audio) + if err != nil { + logger.Error("failed to write audio", zap.Error(err)) + } + } +} + +func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + hashes, ok := r.URL.Query()["hash"] + if !ok || len(hashes) == 0 { + logger.Error("no hash") + return + } + + _, download := r.URL.Query()["download"] + + content, err := downloader.Get(hashes[0], download) + if err != nil { + logger.Error("could not download hash", zap.Error(err)) + return + } + + w.Header().Set("Cache-Control", "max-age:290304000, public") + w.Header().Set("Expires", time.Now().AddDate(60, 0, 0).Format(http.TimeFormat)) + + _, err = w.Write(content) + if err != nil { + logger.Error("failed to write ipfs resource", zap.Error(err)) + } + } +} diff --git a/server/ips.go b/server/ips.go new file mode 100644 index 000000000..48c2a9604 --- /dev/null +++ b/server/ips.go @@ -0,0 +1,22 @@ +package server + +import ( + "net" +) + +var ( + defaultIP = net.IP{127, 0, 0, 1} + localhost = "localhost" +) + +func GetOutboundIP() (net.IP, error) { + conn, err := net.Dial("udp", "255.255.255.255:8080") + if err != nil { + return nil, err + } + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.UDPAddr) + + return localAddr.IP, nil +} diff --git a/server/ips_test.go b/server/ips_test.go new file mode 100644 index 000000000..67d6fdb72 --- /dev/null +++ b/server/ips_test.go @@ -0,0 +1,82 @@ +package server + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "io/ioutil" + "net/http" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" +) + +func testHandler(t *testing.T) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + say, ok := r.URL.Query()["say"] + if !ok || len(say) == 0 { + say = append(say, "nothing") + } + + _, err := w.Write([]byte("Hello I like to be a tls server. You said: `" + say[0] + "` " + time.Now().String())) + if err != nil { + require.NoError(t, err) + } + } +} + +func TestGetOutboundIPWithFullServerE2e(t *testing.T) { + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + ip, err := GetOutboundIP() + require.NoError(t, err) + + cert, certPem, err := GenerateCertFromKey(pk, time.Hour, ip.String()) + require.NoError(t, err) + + s := NewPairingServer(&Config{&cert, ip.String()}) + + s.SetHandlers(HandlerPatternMap{"/hello": testHandler(t)}) + + err = s.Start() + require.NoError(t, err) + + // Give time for the sever to be ready, hacky I know + time.Sleep(100 * time.Millisecond) + spew.Dump(s.MakeBaseURL().String()) + + rootCAs, err := x509.SystemCertPool() + require.NoError(t, err) + + ok := rootCAs.AppendCertsFromPEM(certPem) + require.True(t, ok) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: false, // MUST BE FALSE, or the test is meaningless + RootCAs: rootCAs, + }, + } + client := &http.Client{Transport: tr} + + b := make([]byte, 32) + _, err = rand.Read(b) + require.NoError(t, err) + thing := hex.EncodeToString(b) + + response, err := client.Get(s.MakeBaseURL().String() + "/hello?say=" + thing) + require.NoError(t, err) + + defer response.Body.Close() + + content, err := ioutil.ReadAll(response.Body) + require.NoError(t, err) + require.Equal(t, "Hello I like to be a tls server. You said: `"+thing+"`", string(content[:109])) +} diff --git a/server/server.go b/server/server.go index f4549d18c..1295b0a19 100644 --- a/server/server.go +++ b/server/server.go @@ -2,225 +2,44 @@ package server import ( "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" "crypto/tls" - "database/sql" "fmt" "net" "net/http" - "time" + "net/url" "go.uber.org/zap" - "github.com/status-im/status-go/ipfs" "github.com/status-im/status-go/logutils" - "github.com/status-im/status-go/protocol/identity/identicon" - "github.com/status-im/status-go/protocol/images" ) -var globalCertificate *tls.Certificate = nil -var globalPem string - -func generateTLSCert() error { - if globalCertificate != nil { - return nil - } - - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return err - } - - notBefore := time.Now() - notAfter := notBefore.Add(365 * 24 * time.Hour) - - cert, err := GenerateX509Cert(notBefore, notAfter) - if err != nil { - return err - } - - certPem, keyPem, err := GenerateX509PEMs(cert, priv) - if err != nil { - return err - } - - finalCert, err := tls.X509KeyPair(certPem, keyPem) - if err != nil { - return err - } - - globalCertificate = &finalCert - globalPem = string(certPem) - return nil -} - -func PublicTLSCert() (string, error) { - err := generateTLSCert() - - if err != nil { - return "", err - } - - return globalPem, nil -} - -type imageHandler struct { - db *sql.DB - logger *zap.Logger -} - -type audioHandler struct { - db *sql.DB - logger *zap.Logger -} - -type identiconHandler struct { - logger *zap.Logger -} - -type ipfsHandler struct { - logger *zap.Logger - downloader *ipfs.Downloader -} - -func (s *identiconHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - pks, ok := r.URL.Query()["publicKey"] - if !ok || len(pks) == 0 { - s.logger.Error("no publicKey") - return - } - pk := pks[0] - image, err := identicon.Generate(pk) - if err != nil { - s.logger.Error("could not generate identicon") - } - - w.Header().Set("Content-Type", "image/png") - w.Header().Set("Cache-Control", "max-age:290304000, public") - w.Header().Set("Expires", time.Now().AddDate(60, 0, 0).Format(http.TimeFormat)) - - _, err = w.Write(image) - if err != nil { - s.logger.Error("failed to write image", zap.Error(err)) - } -} - -func (s *imageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - - messageIDs, ok := r.URL.Query()["messageId"] - if !ok || len(messageIDs) == 0 { - s.logger.Error("no messageID") - return - } - messageID := messageIDs[0] - var image []byte - err := s.db.QueryRow(`SELECT image_payload FROM user_messages WHERE id = ?`, messageID).Scan(&image) - if err != nil { - s.logger.Error("failed to find image", zap.Error(err)) - return - } - if len(image) == 0 { - s.logger.Error("empty image") - return - } - mime, err := images.ImageMime(image) - if err != nil { - s.logger.Error("failed to get mime", zap.Error(err)) - } - - w.Header().Set("Content-Type", mime) - w.Header().Set("Cache-Control", "no-store") - - _, err = w.Write(image) - if err != nil { - s.logger.Error("failed to write image", zap.Error(err)) - } -} - -func (s *audioHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - - messageIDs, ok := r.URL.Query()["messageId"] - if !ok || len(messageIDs) == 0 { - s.logger.Error("no messageID") - return - } - messageID := messageIDs[0] - var audio []byte - err := s.db.QueryRow(`SELECT audio_payload FROM user_messages WHERE id = ?`, messageID).Scan(&audio) - if err != nil { - s.logger.Error("failed to find image", zap.Error(err)) - return - } - if len(audio) == 0 { - s.logger.Error("empty audio") - return - } - - w.Header().Set("Content-Type", "audio/aac") - w.Header().Set("Cache-Control", "no-store") - - _, err = w.Write(audio) - if err != nil { - s.logger.Error("failed to write audio", zap.Error(err)) - } -} - -func (s *ipfsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - hashes, ok := r.URL.Query()["hash"] - if !ok || len(hashes) == 0 { - s.logger.Error("no hash") - return - } - - _, download := r.URL.Query()["download"] - - content, err := s.downloader.Get(hashes[0], download) - if err != nil { - s.logger.Error("could not download hash", zap.Error(err)) - return - } - - w.Header().Set("Cache-Control", "max-age:290304000, public") - w.Header().Set("Expires", time.Now().AddDate(60, 0, 0).Format(http.TimeFormat)) - - _, err = w.Write(content) - if err != nil { - s.logger.Error("failed to write ipfs resource", zap.Error(err)) - } -} - type Server struct { - Port int - run bool - server *http.Server - logger *zap.Logger - db *sql.DB - cert *tls.Certificate - downloader *ipfs.Downloader + run bool + server *http.Server + logger *zap.Logger + cert *tls.Certificate + hostname string + port int + handlers HandlerPatternMap } -func NewServer(db *sql.DB, downloader *ipfs.Downloader) (*Server, error) { - err := generateTLSCert() +func NewServer(cert *tls.Certificate, hostname string) Server { + return Server{logger: logutils.ZapLogger(), cert: cert, hostname: hostname} +} - if err != nil { - return nil, err - } - - return &Server{db: db, logger: logutils.ZapLogger(), cert: globalCertificate, Port: 0, downloader: downloader}, nil +func (s *Server) getHost() string { + // TODO consider returning an error if s.getPort returns `0`, as this means that the listener is not ready + return fmt.Sprintf("%s:%d", s.hostname, s.port) } func (s *Server) listenAndServe() { - cfg := &tls.Config{Certificates: []tls.Certificate{*s.cert}, ServerName: "localhost", MinVersion: tls.VersionTLS12} + cfg := &tls.Config{Certificates: []tls.Certificate{*s.cert}, ServerName: s.hostname, MinVersion: tls.VersionTLS12} // in case of restart, we should use the same port as the first start in order not to break existing links - addr := fmt.Sprintf("localhost:%d", s.Port) - - listener, err := tls.Listen("tcp", addr, cfg) + listener, err := tls.Listen("tcp", s.getHost(), cfg) if err != nil { s.logger.Error("failed to start server, retrying", zap.Error(err)) - s.Port = 0 + s.port = 0 err = s.Start() if err != nil { s.logger.Error("server start failed, giving up", zap.Error(err)) @@ -228,8 +47,9 @@ func (s *Server) listenAndServe() { return } - s.Port = listener.Addr().(*net.TCPAddr).Port + s.port = listener.Addr().(*net.TCPAddr).Port s.run = true + err = s.server.Serve(listener) if err != http.ErrServerClosed { s.logger.Error("server failed unexpectedly, restarting", zap.Error(err)) @@ -243,17 +63,27 @@ func (s *Server) listenAndServe() { s.run = false } +func (s *Server) resetServer() { + s.server = new(http.Server) +} + +func (s *Server) applyHandlers() { + if s.server == nil { + s.server = new(http.Server) + } + mux := http.NewServeMux() + + for p, h := range s.handlers { + mux.HandleFunc(p, h) + } + s.server.Handler = mux +} + func (s *Server) Start() error { - handler := http.NewServeMux() - handler.Handle("/messages/images", &imageHandler{db: s.db, logger: s.logger}) - handler.Handle("/messages/audio", &audioHandler{db: s.db, logger: s.logger}) - handler.Handle("/messages/identicons", &identiconHandler{logger: s.logger}) - handler.Handle("/ipfs", &ipfsHandler{logger: s.logger, downloader: s.downloader}) - - s.server = &http.Server{Handler: handler} - + // Once Shutdown has been called on a server, it may not be reused; + s.resetServer() + s.applyHandlers() go s.listenAndServe() - return nil } @@ -282,3 +112,14 @@ func (s *Server) ToBackground() { } } } + +func (s *Server) SetHandlers(handlers HandlerPatternMap) { + s.handlers = handlers +} + +func (s *Server) MakeBaseURL() *url.URL { + return &url.URL{ + Scheme: "https", + Host: s.getHost(), + } +} diff --git a/server/server_media.go b/server/server_media.go new file mode 100644 index 000000000..5e9c99b05 --- /dev/null +++ b/server/server_media.go @@ -0,0 +1,75 @@ +package server + +import ( + "database/sql" + "net/url" + + "github.com/status-im/status-go/ipfs" +) + +type MediaServer struct { + Server + + db *sql.DB + downloader *ipfs.Downloader +} + +// NewMediaServer returns a *MediaServer +func NewMediaServer(db *sql.DB, downloader *ipfs.Downloader) (*MediaServer, error) { + err := generateTLSCert() + if err != nil { + return nil, err + } + + s := &MediaServer{ + Server: NewServer(globalCertificate, localhost), + db: db, + downloader: downloader, + } + s.SetHandlers(HandlerPatternMap{ + imagesPath: handleImage(s.db, s.logger), + audioPath: handleAudio(s.db, s.logger), + identiconsPath: handleIdenticon(s.logger), + ipfsPath: handleIPFS(s.downloader, s.logger), + }) + + return s, nil +} + +func (s *MediaServer) MakeImageServerURL() string { + u := s.MakeBaseURL() + u.Path = basePath + "/" + return u.String() +} + +func (s *MediaServer) MakeIdenticonURL(from string) string { + u := s.MakeBaseURL() + u.Path = identiconsPath + u.RawQuery = url.Values{"publicKey": {from}}.Encode() + + return u.String() +} + +func (s *MediaServer) MakeImageURL(id string) string { + u := s.MakeBaseURL() + u.Path = imagesPath + u.RawQuery = url.Values{"messageId": {id}}.Encode() + + return u.String() +} + +func (s *MediaServer) MakeAudioURL(id string) string { + u := s.MakeBaseURL() + u.Path = audioPath + u.RawQuery = url.Values{"messageId": {id}}.Encode() + + return u.String() +} + +func (s *MediaServer) MakeStickerURL(stickerHash string) string { + u := s.MakeBaseURL() + u.Path = ipfsPath + u.RawQuery = url.Values{"hash": {stickerHash}}.Encode() + + return u.String() +} diff --git a/server/server_pairing.go b/server/server_pairing.go new file mode 100644 index 000000000..85d53a97d --- /dev/null +++ b/server/server_pairing.go @@ -0,0 +1,22 @@ +package server + +import ( + "crypto/tls" +) + +type PairingServer struct { + Server +} + +type Config struct { + Cert *tls.Certificate + Hostname string +} + +// NewPairingServer returns a *NewPairingServer init from the given *Config +func NewPairingServer(config *Config) *PairingServer { + return &PairingServer{Server: NewServer( + config.Cert, + config.Hostname, + )} +} diff --git a/server/server_test.go b/server/server_test.go new file mode 100644 index 000000000..3c5c8a76b --- /dev/null +++ b/server/server_test.go @@ -0,0 +1,74 @@ +package server + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +func TestServerURLSuite(t *testing.T) { + suite.Run(t, new(ServerURLSuite)) +} + +type ServerURLSuite struct { + suite.Suite + + server *MediaServer + serverNoPort *MediaServer +} + +func (s *ServerURLSuite) SetupSuite() { + s.server = &MediaServer{Server: Server{ + hostname: defaultIP.String(), + port: 1337, + }} + s.serverNoPort = &MediaServer{Server: Server{ + hostname: defaultIP.String(), + }} +} + +func (s *ServerURLSuite) TestServer_MakeBaseURL() { + s.Require().Equal("https://127.0.0.1:1337", s.server.MakeBaseURL().String()) + s.Require().Equal("https://127.0.0.1:0", s.serverNoPort.MakeBaseURL().String()) +} + +func (s *ServerURLSuite) TestServer_MakeImageServerURL() { + s.Require().Equal("https://127.0.0.1:1337/messages/", s.server.MakeImageServerURL()) + s.Require().Equal("https://127.0.0.1:0/messages/", s.serverNoPort.MakeImageServerURL()) +} + +func (s *ServerURLSuite) TestServer_MakeIdenticonURL() { + s.Require().Equal( + "https://127.0.0.1:1337/messages/identicons?publicKey=0xdaff0d11decade", + s.server.MakeIdenticonURL("0xdaff0d11decade")) + s.Require().Equal( + "https://127.0.0.1:0/messages/identicons?publicKey=0xdaff0d11decade", + s.serverNoPort.MakeIdenticonURL("0xdaff0d11decade")) +} + +func (s *ServerURLSuite) TestServer_MakeImageURL() { + s.Require().Equal( + "https://127.0.0.1:1337/messages/images?messageId=0x10aded70ffee", + s.server.MakeImageURL("0x10aded70ffee")) + s.Require().Equal( + "https://127.0.0.1:0/messages/images?messageId=0x10aded70ffee", + s.serverNoPort.MakeImageURL("0x10aded70ffee")) +} + +func (s *ServerURLSuite) TestServer_MakeAudioURL() { + s.Require().Equal( + "https://127.0.0.1:1337/messages/audio?messageId=0xde1e7ebee71e", + s.server.MakeAudioURL("0xde1e7ebee71e")) + s.Require().Equal( + "https://127.0.0.1:0/messages/audio?messageId=0xde1e7ebee71e", + s.serverNoPort.MakeAudioURL("0xde1e7ebee71e")) +} + +func (s *ServerURLSuite) TestServer_MakeStickerURL() { + s.Require().Equal( + "https://127.0.0.1:1337/ipfs?hash=0xdeadbeef4ac0", + s.server.MakeStickerURL("0xdeadbeef4ac0")) + s.Require().Equal( + "https://127.0.0.1:0/ipfs?hash=0xdeadbeef4ac0", + s.serverNoPort.MakeStickerURL("0xdeadbeef4ac0")) +} diff --git a/services/ext/service.go b/services/ext/service.go index c3d82c3e6..60f016e64 100644 --- a/services/ext/service.go +++ b/services/ext/service.go @@ -11,10 +11,8 @@ import ( "path/filepath" "time" - "github.com/status-im/status-go/server" - "github.com/status-im/status-go/services/browsers" - "github.com/syndtr/goleveldb/leveldb" + "go.uber.org/zap" commongethtypes "github.com/ethereum/go-ethereum/common" gethtypes "github.com/ethereum/go-ethereum/core/types" @@ -24,7 +22,6 @@ import ( "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/enode" gethrpc "github.com/ethereum/go-ethereum/rpc" - "github.com/status-im/status-go/rpc" "github.com/status-im/status-go/connection" "github.com/status-im/status-go/db" @@ -39,12 +36,13 @@ import ( "github.com/status-im/status-go/protocol/pushnotificationclient" "github.com/status-im/status-go/protocol/pushnotificationserver" "github.com/status-im/status-go/protocol/transport" + "github.com/status-im/status-go/rpc" + "github.com/status-im/status-go/server" + "github.com/status-im/status-go/services/browsers" "github.com/status-im/status-go/services/ext/mailservers" localnotifications "github.com/status-im/status-go/services/local-notifications" mailserversDB "github.com/status-im/status-go/services/mailservers" "github.com/status-im/status-go/services/wallet/transfer" - - "go.uber.org/zap" ) // EnvelopeEventsHandler used for two different event types. @@ -109,7 +107,7 @@ func (s *Service) GetPeer(rawURL string) (*enode.Node, error) { return enode.ParseV4(rawURL) } -func (s *Service) InitProtocol(nodeName string, identity *ecdsa.PrivateKey, db *sql.DB, httpServer *server.Server, multiAccountDb *multiaccounts.Database, acc *multiaccounts.Account, logger *zap.Logger) error { +func (s *Service) InitProtocol(nodeName string, identity *ecdsa.PrivateKey, db *sql.DB, httpServer *server.MediaServer, multiAccountDb *multiaccounts.Database, acc *multiaccounts.Account, logger *zap.Logger) error { var err error if !s.config.ShhextConfig.PFSEnabled { return nil @@ -394,7 +392,7 @@ func buildMessengerOptions( config params.NodeConfig, identity *ecdsa.PrivateKey, db *sql.DB, - httpServer *server.Server, + httpServer *server.MediaServer, rpcClient *rpc.Client, multiAccounts *multiaccounts.Database, account *multiaccounts.Account, diff --git a/services/stickers/api.go b/services/stickers/api.go index f72fff650..759495bd7 100644 --- a/services/stickers/api.go +++ b/services/stickers/api.go @@ -2,7 +2,6 @@ package stickers import ( "context" - "fmt" "math/big" "github.com/zenthangplus/goccm" @@ -44,7 +43,7 @@ type API struct { keyStoreDir string downloader *ipfs.Downloader - httpServer *server.Server + httpServer *server.MediaServer ctx context.Context } @@ -85,7 +84,7 @@ type ednStickerPackInfo struct { Meta ednStickerPack } -func NewAPI(ctx context.Context, acc *accounts.Database, rpcClient *rpc.Client, accountsManager *account.GethManager, rpcFiltersSrvc *rpcfilters.Service, keyStoreDir string, downloader *ipfs.Downloader, httpServer *server.Server) *API { +func NewAPI(ctx context.Context, acc *accounts.Database, rpcClient *rpc.Client, accountsManager *account.GethManager, rpcFiltersSrvc *rpcfilters.Service, keyStoreDir string, downloader *ipfs.Downloader, httpServer *server.MediaServer) *API { result := &API{ contractMaker: &contracts.ContractMaker{ RPCClient: rpcClient, @@ -327,7 +326,7 @@ func (api *API) downloadPackData(stickerPack *StickerPack, contentHash []byte, t } func (api *API) hashToURL(hash string) string { - return fmt.Sprintf("https://localhost:%d/ipfs?hash=%s", api.httpServer.Port, hash) + return api.httpServer.MakeStickerURL(hash) } func (api *API) populateStickerPackAttributes(stickerPack *StickerPack, ednSource []byte, translateHashes bool) error { diff --git a/services/stickers/service.go b/services/stickers/service.go index 065d96445..59b9ff4d5 100644 --- a/services/stickers/service.go +++ b/services/stickers/service.go @@ -15,7 +15,7 @@ import ( ) // NewService initializes service instance. -func NewService(acc *accounts.Database, rpcClient *rpc.Client, accountsManager *account.GethManager, rpcFiltersSrvc *rpcfilters.Service, config *params.NodeConfig, downloader *ipfs.Downloader, httpServer *server.Server) *Service { +func NewService(acc *accounts.Database, rpcClient *rpc.Client, accountsManager *account.GethManager, rpcFiltersSrvc *rpcfilters.Service, config *params.NodeConfig, downloader *ipfs.Downloader, httpServer *server.MediaServer) *Service { ctx, cancel := context.WithCancel(context.Background()) return &Service{ @@ -39,7 +39,7 @@ type Service struct { rpcFiltersSrvc *rpcfilters.Service downloader *ipfs.Downloader keyStoreDir string - httpServer *server.Server + httpServer *server.MediaServer ctx context.Context cancel context.CancelFunc }