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
This commit is contained in:
Samuel Hawksby-Robinson 2022-06-15 15:49:31 +01:00 committed by GitHub
parent efa14805bd
commit 7f149f93c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 686 additions and 262 deletions

View File

@ -1 +1 @@
0.101.1 0.101.2

View File

@ -82,7 +82,7 @@ type StatusNode struct {
rpcClient *rpc.Client // reference to an RPC client rpcClient *rpc.Client // reference to an RPC client
downloader *ipfs.Downloader downloader *ipfs.Downloader
httpServer *server.Server httpServer *server.MediaServer
discovery discovery.Discovery discovery discovery.Discovery
register *peers.Register register *peers.Register
@ -152,7 +152,7 @@ func (n *StatusNode) GethNode() *node.Node {
return n.gethNode return n.gethNode
} }
func (n *StatusNode) HTTPServer() *server.Server { func (n *StatusNode) HTTPServer() *server.MediaServer {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
@ -238,7 +238,7 @@ func (n *StatusNode) startWithDB(config *params.NodeConfig, accs *accounts.Manag
n.downloader = ipfs.NewDownloader(config.RootDataDir) 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 { if err != nil {
return err return err
} }

View File

@ -19,6 +19,7 @@ import (
"github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/images" "github.com/status-im/status-go/images"
"github.com/status-im/status-go/protocol/protobuf" "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 // QuotedMessage contains the original text of the message replied to
@ -35,10 +36,6 @@ type QuotedMessage struct {
CommunityID string `json:"communityId,omitempty"` 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 type CommandState int
const ( const (
@ -177,20 +174,20 @@ type Message struct {
ContactRequestState ContactRequestState `json:"contactRequestState,omitempty"` ContactRequestState ContactRequestState `json:"contactRequestState,omitempty"`
} }
func (m *Message) PrepareServerURLs(port int) { func (m *Message) PrepareServerURLs(s *server.MediaServer) {
m.Identicon = fmt.Sprintf("https://localhost:%d/messages/identicons?publicKey=%s", port, m.From) m.Identicon = s.MakeIdenticonURL(m.From)
if m.QuotedMessage != nil && m.QuotedMessage.ContentType == int64(protobuf.ChatMessage_IMAGE) { 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 { 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 { 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 { 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)
} }
} }

View File

@ -15,22 +15,18 @@ import (
"sync" "sync"
"time" "time"
"github.com/status-im/status-go/contracts" "github.com/davecgh/go-spew/spew"
"github.com/status-im/status-go/services/browsers" "github.com/golang/protobuf/proto"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap" "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" gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/status-im/status-go/appdatabase" "github.com/status-im/status-go/appdatabase"
"github.com/status-im/status-go/appmetrics" "github.com/status-im/status-go/appmetrics"
"github.com/status-im/status-go/connection" "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/crypto"
"github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/eth-node/types"
userimage "github.com/status-im/status-go/images" userimage "github.com/status-im/status-go/images"
@ -56,9 +52,9 @@ import (
"github.com/status-im/status-go/protocol/transport" "github.com/status-im/status-go/protocol/transport"
v1protocol "github.com/status-im/status-go/protocol/v1" v1protocol "github.com/status-im/status-go/protocol/v1"
"github.com/status-im/status-go/server" "github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/browsers"
"github.com/status-im/status-go/services/ext/mailservers" "github.com/status-im/status-go/services/ext/mailservers"
mailserversDB "github.com/status-im/status-go/services/mailservers" mailserversDB "github.com/status-im/status-go/services/mailservers"
"github.com/status-im/status-go/telemetry" "github.com/status-im/status-go/telemetry"
) )
@ -128,7 +124,7 @@ type Messenger struct {
account *multiaccounts.Account account *multiaccounts.Account
mailserversDatabase *mailserversDB.Database mailserversDatabase *mailserversDB.Database
browserDatabase *browsers.Database browserDatabase *browsers.Database
httpServer *server.Server httpServer *server.MediaServer
quit chan struct{} quit chan struct{}
requestedCommunitiesLock sync.RWMutex requestedCommunitiesLock sync.RWMutex
@ -4220,7 +4216,7 @@ func (m *Messenger) MessageByChatID(chatID, cursor string, limit int) ([]*common
} }
if m.httpServer != nil { if m.httpServer != nil {
for idx := range msgs { 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) { func (m *Messenger) prepareMessages(messages map[string]*common.Message) {
if m.httpServer != nil { if m.httpServer != nil {
for idx := range messages { for idx := range messages {
messages[idx].PrepareServerURLs(m.httpServer.Port) messages[idx].PrepareServerURLs(m.httpServer)
} }
} }
} }

View File

@ -70,7 +70,7 @@ type config struct {
clusterConfig params.ClusterConfig clusterConfig params.ClusterConfig
browserDatabase *browsers.Database browserDatabase *browsers.Database
torrentConfig *params.TorrentConfig torrentConfig *params.TorrentConfig
httpServer *server.Server httpServer *server.MediaServer
rpcClient *rpc.Client rpcClient *rpc.Client
verifyTransactionClient EthClient 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 { return func(c *config) error {
c.httpServer = s c.httpServer = s
return nil return nil

View File

@ -1,7 +1,5 @@
package protocol package protocol
import "fmt"
func (m *Messenger) ImageServerURL() string { func (m *Messenger) ImageServerURL() string {
return fmt.Sprintf("https://localhost:%d/messages/", m.httpServer.Port) return m.httpServer.MakeImageServerURL()
} }

View File

@ -2,35 +2,53 @@ package server
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/pem" "encoding/pem"
"math/big" "math/big"
"net"
"time" "time"
) )
func GenerateX509Cert(from, to time.Time) (*x509.Certificate, error) { var globalCertificate *tls.Certificate = nil
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) var globalPem string
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil { func makeRandomSerialNumber() (*big.Int, error) {
return nil, err serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
return rand.Int(rand.Reader, serialNumberLimit)
} }
template := &x509.Certificate{ func makeSerialNumberFromKey(pk *ecdsa.PrivateKey) *big.Int {
SerialNumber: serialNumber, h := sha256.New()
h.Write(append(pk.D.Bytes(), append(pk.Y.Bytes(), pk.X.Bytes()...)...))
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"}}, Subject: pkix.Name{Organization: []string{"Self-signed cert"}},
NotBefore: from, NotBefore: from,
NotAfter: to, NotAfter: to,
DNSNames: []string{"localhost"},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true, BasicConstraintsValid: true,
IsCA: 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) { 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 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
}

93
server/certs_test.go Normal file
View File

@ -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)
}

135
server/handlers.go Normal file
View File

@ -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))
}
}
}

22
server/ips.go Normal file
View File

@ -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
}

82
server/ips_test.go Normal file
View File

@ -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]))
}

View File

@ -2,225 +2,44 @@ package server
import ( import (
"context" "context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls" "crypto/tls"
"database/sql"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"time" "net/url"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/status-im/status-go/ipfs"
"github.com/status-im/status-go/logutils" "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 { type Server struct {
Port int
run bool run bool
server *http.Server server *http.Server
logger *zap.Logger logger *zap.Logger
db *sql.DB
cert *tls.Certificate cert *tls.Certificate
downloader *ipfs.Downloader hostname string
port int
handlers HandlerPatternMap
} }
func NewServer(db *sql.DB, downloader *ipfs.Downloader) (*Server, error) { func NewServer(cert *tls.Certificate, hostname string) Server {
err := generateTLSCert() 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() { 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 // 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", s.getHost(), cfg)
listener, err := tls.Listen("tcp", addr, cfg)
if err != nil { if err != nil {
s.logger.Error("failed to start server, retrying", zap.Error(err)) s.logger.Error("failed to start server, retrying", zap.Error(err))
s.Port = 0 s.port = 0
err = s.Start() err = s.Start()
if err != nil { if err != nil {
s.logger.Error("server start failed, giving up", zap.Error(err)) s.logger.Error("server start failed, giving up", zap.Error(err))
@ -228,8 +47,9 @@ func (s *Server) listenAndServe() {
return return
} }
s.Port = listener.Addr().(*net.TCPAddr).Port s.port = listener.Addr().(*net.TCPAddr).Port
s.run = true s.run = true
err = s.server.Serve(listener) err = s.server.Serve(listener)
if err != http.ErrServerClosed { if err != http.ErrServerClosed {
s.logger.Error("server failed unexpectedly, restarting", zap.Error(err)) s.logger.Error("server failed unexpectedly, restarting", zap.Error(err))
@ -243,17 +63,27 @@ func (s *Server) listenAndServe() {
s.run = false 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 { func (s *Server) Start() error {
handler := http.NewServeMux() // Once Shutdown has been called on a server, it may not be reused;
handler.Handle("/messages/images", &imageHandler{db: s.db, logger: s.logger}) s.resetServer()
handler.Handle("/messages/audio", &audioHandler{db: s.db, logger: s.logger}) s.applyHandlers()
handler.Handle("/messages/identicons", &identiconHandler{logger: s.logger})
handler.Handle("/ipfs", &ipfsHandler{logger: s.logger, downloader: s.downloader})
s.server = &http.Server{Handler: handler}
go s.listenAndServe() go s.listenAndServe()
return nil 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(),
}
}

75
server/server_media.go Normal file
View File

@ -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()
}

22
server/server_pairing.go Normal file
View File

@ -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,
)}
}

74
server/server_test.go Normal file
View File

@ -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"))
}

View File

@ -11,10 +11,8 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/browsers"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
"go.uber.org/zap"
commongethtypes "github.com/ethereum/go-ethereum/common" commongethtypes "github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types" 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"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
gethrpc "github.com/ethereum/go-ethereum/rpc" 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/connection"
"github.com/status-im/status-go/db" "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/pushnotificationclient"
"github.com/status-im/status-go/protocol/pushnotificationserver" "github.com/status-im/status-go/protocol/pushnotificationserver"
"github.com/status-im/status-go/protocol/transport" "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" "github.com/status-im/status-go/services/ext/mailservers"
localnotifications "github.com/status-im/status-go/services/local-notifications" localnotifications "github.com/status-im/status-go/services/local-notifications"
mailserversDB "github.com/status-im/status-go/services/mailservers" mailserversDB "github.com/status-im/status-go/services/mailservers"
"github.com/status-im/status-go/services/wallet/transfer" "github.com/status-im/status-go/services/wallet/transfer"
"go.uber.org/zap"
) )
// EnvelopeEventsHandler used for two different event types. // EnvelopeEventsHandler used for two different event types.
@ -109,7 +107,7 @@ func (s *Service) GetPeer(rawURL string) (*enode.Node, error) {
return enode.ParseV4(rawURL) 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 var err error
if !s.config.ShhextConfig.PFSEnabled { if !s.config.ShhextConfig.PFSEnabled {
return nil return nil
@ -394,7 +392,7 @@ func buildMessengerOptions(
config params.NodeConfig, config params.NodeConfig,
identity *ecdsa.PrivateKey, identity *ecdsa.PrivateKey,
db *sql.DB, db *sql.DB,
httpServer *server.Server, httpServer *server.MediaServer,
rpcClient *rpc.Client, rpcClient *rpc.Client,
multiAccounts *multiaccounts.Database, multiAccounts *multiaccounts.Database,
account *multiaccounts.Account, account *multiaccounts.Account,

View File

@ -2,7 +2,6 @@ package stickers
import ( import (
"context" "context"
"fmt"
"math/big" "math/big"
"github.com/zenthangplus/goccm" "github.com/zenthangplus/goccm"
@ -44,7 +43,7 @@ type API struct {
keyStoreDir string keyStoreDir string
downloader *ipfs.Downloader downloader *ipfs.Downloader
httpServer *server.Server httpServer *server.MediaServer
ctx context.Context ctx context.Context
} }
@ -85,7 +84,7 @@ type ednStickerPackInfo struct {
Meta ednStickerPack 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{ result := &API{
contractMaker: &contracts.ContractMaker{ contractMaker: &contracts.ContractMaker{
RPCClient: rpcClient, RPCClient: rpcClient,
@ -327,7 +326,7 @@ func (api *API) downloadPackData(stickerPack *StickerPack, contentHash []byte, t
} }
func (api *API) hashToURL(hash string) string { 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 { func (api *API) populateStickerPackAttributes(stickerPack *StickerPack, ednSource []byte, translateHashes bool) error {

View File

@ -15,7 +15,7 @@ import (
) )
// NewService initializes service instance. // 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()) ctx, cancel := context.WithCancel(context.Background())
return &Service{ return &Service{
@ -39,7 +39,7 @@ type Service struct {
rpcFiltersSrvc *rpcfilters.Service rpcFiltersSrvc *rpcfilters.Service
downloader *ipfs.Downloader downloader *ipfs.Downloader
keyStoreDir string keyStoreDir string
httpServer *server.Server httpServer *server.MediaServer
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
} }