http server for images (#2418)

Serve images over HTTPS

Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
Co-authored-by: Michele Balistreri <michele@bitgamma.com>
This commit is contained in:
flexsurfer 2022-02-10 18:19:34 +01:00 committed by GitHub
parent 54b35b0510
commit 5925b3b7cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 382 additions and 24 deletions

View File

@ -1 +1 @@
0.93.5
0.94.0

View File

@ -1026,6 +1026,23 @@ func (b *GethStatusBackend) AppStateChange(state string) {
b.log.Info("App State changed", "new-state", s)
b.appState = s
if b.statusNode != nil {
wakuext := b.statusNode.WakuExtService()
if wakuext != nil {
messenger := wakuext.Messenger()
if messenger != nil {
if s == appStateForeground {
messenger.ToForeground()
} else {
messenger.ToBackground()
}
}
}
}
// TODO: put node in low-power mode if the app is in background (or inactive)
// and normal mode if the app is in foreground.
}

View File

@ -23,6 +23,7 @@ import (
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/profiling"
protocol "github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/images"
"github.com/status-im/status-go/services/personal"
"github.com/status-im/status-go/services/typeddata"
"github.com/status-im/status-go/signal"
@ -726,3 +727,13 @@ func ConvertToKeycardAccount(keyStoreDir, accountData, settingsJSON, password, n
}
return makeJSONResponse(nil)
}
func ImageServerTLSCert() string {
cert, err := images.PublicTLSCert()
if err != nil {
return makeJSONResponse(err)
}
return cert
}

View File

@ -131,6 +131,8 @@ type Message struct {
Base64Audio string `json:"audio,omitempty"`
// AudioPath is the path of the audio to be sent
AudioPath string `json:"audioPath,omitempty"`
// ImageLocalURL is the local url of the image
ImageLocalURL string `json:"imageLocalUrl,omitempty"`
// CommunityID is the id of the community to advertise
CommunityID string `json:"communityId,omitempty"`
@ -158,6 +160,11 @@ type Message struct {
Deleted bool `json:"deleted"`
}
func (m *Message) PrepareImageURL(port int) {
m.ImageLocalURL = fmt.Sprintf("https://localhost:%d/messages/images?messageId=%s", port, m.ID)
m.Identicon = fmt.Sprintf("https://localhost:%d/messages/identicons?publicKey=%s", port, m.From)
}
func (m *Message) MarshalJSON() ([]byte, error) {
type StickerAlias struct {
Hash string `json:"hash"`
@ -218,7 +225,7 @@ func (m *Message) MarshalJSON() ([]byte, error) {
ResponseTo: m.ResponseTo,
New: m.New,
EnsName: m.EnsName,
Image: m.Base64Image,
Image: m.ImageLocalURL,
Audio: m.Base64Audio,
CommunityID: m.CommunityID,
Timestamp: m.Timestamp,

View File

@ -10,22 +10,11 @@ import (
)
func renderBase64(id Identicon) (string, error) {
img := image.NewRGBA(image.Rect(0, 0, 50, 50))
var buff bytes.Buffer
setBackgroundTransparent(img)
for i, v := range id.bitmap {
if v == 1 {
drawRect(img, i, id.color)
}
}
if err := png.Encode(&buff, img); err != nil {
img, err := render(id)
if err != nil {
return "", err
}
encodedString := base64.StdEncoding.EncodeToString(buff.Bytes())
encodedString := base64.StdEncoding.EncodeToString(img)
image := "data:image/png;base64," + encodedString
return image, nil
}
@ -48,8 +37,32 @@ func drawRect(rgba *image.RGBA, i int, c color.Color) {
draw.Draw(rgba, r, &image.Uniform{C: c}, image.Point{}, draw.Src)
}
func render(id Identicon) ([]byte, error) {
img := image.NewRGBA(image.Rect(0, 0, 50, 50))
var buff bytes.Buffer
setBackgroundTransparent(img)
for i, v := range id.bitmap {
if v == 1 {
drawRect(img, i, id.color)
}
}
if err := png.Encode(&buff, img); err != nil {
return nil, err
}
return buff.Bytes(), nil
}
// GenerateBase64 generates an identicon in base64 png format given a string
func GenerateBase64(id string) (string, error) {
i := generate(id)
return renderBase64(i)
}
func Generate(id string) ([]byte, error) {
i := generate(id)
return render(i)
}

244
protocol/images/server.go Normal file
View File

@ -0,0 +1,244 @@
package images
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"database/sql"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/http"
"time"
"go.uber.org/zap"
"github.com/status-im/status-go/protocol/identity/identicon"
)
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)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return err
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{Organization: []string{"Self-signed cert"}},
NotBefore: notBefore,
NotAfter: notAfter,
DNSNames: []string{"localhost"},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return err
}
certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return err
}
keyPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
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 messageHandler struct {
db *sql.DB
logger *zap.Logger
}
type identiconHandler struct {
logger *zap.Logger
}
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 *messageHandler) 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 := 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))
}
}
type Server struct {
Port int
run bool
server *http.Server
logger *zap.Logger
db *sql.DB
cert *tls.Certificate
}
func NewServer(db *sql.DB, logger *zap.Logger) (*Server, error) {
err := generateTLSCert()
if err != nil {
return nil, err
}
return &Server{db: db, logger: logger, cert: globalCertificate, Port: 0}, nil
}
func (s *Server) listenAndServe() {
cfg := &tls.Config{Certificates: []tls.Certificate{*s.cert}, ServerName: "localhost", 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)
if err != nil {
s.logger.Error("failed to start server, retrying", zap.Error(err))
s.Port = 0
err = s.Start()
if err != nil {
s.logger.Error("server start failed, giving up", zap.Error(err))
}
return
}
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))
err = s.Start()
if err != nil {
s.logger.Error("server start failed, giving up", zap.Error(err))
}
return
}
s.run = false
}
func (s *Server) Start() error {
handler := http.NewServeMux()
handler.Handle("/messages/images", &messageHandler{db: s.db, logger: s.logger})
handler.Handle("/messages/identicons", &identiconHandler{logger: s.logger})
s.server = &http.Server{Handler: handler}
go s.listenAndServe()
return nil
}
func (s *Server) Stop() error {
if s.server != nil {
return s.server.Shutdown(context.Background())
}
return nil
}
func (s *Server) ToForeground() {
if !s.run && (s.server != nil) {
err := s.Start()
if err != nil {
s.logger.Error("server start failed during foreground transition", zap.Error(err))
}
}
}
func (s *Server) ToBackground() {
if s.run {
err := s.Stop()
if err != nil {
s.logger.Error("server stop failed during background transition", zap.Error(err))
}
}
}

View File

@ -1,6 +1,8 @@
package images
import (
"errors"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/protocol/protobuf"
)
@ -19,3 +21,18 @@ func ImageType(buf []byte) protobuf.ImageType {
return protobuf.ImageType_UNKNOWN_IMAGE_TYPE
}
}
func ImageMime(buf []byte) (string, error) {
switch images.GetType(buf) {
case images.JPEG:
return "image/jpeg", nil
case images.PNG:
return "image/png", nil
case images.GIF:
return "image/gif", nil
case images.WEBP:
return "image/webp", nil
default:
return "", errors.New("mime type not found")
}
}

View File

@ -74,7 +74,6 @@ func (db sqlitePersistence) tableUserMessagesAllFieldsJoin() string {
m1.parsed_text,
m1.sticker_pack,
m1.sticker_hash,
m1.image_base64,
COALESCE(m1.audio_duration_ms,0),
m1.audio_base64,
m1.community_id,
@ -100,7 +99,6 @@ func (db sqlitePersistence) tableUserMessagesAllFieldsJoin() string {
m2.source,
m2.text,
m2.parsed_text,
m2.image_base64,
m2.audio_duration_ms,
m2.audio_base64,
m2.community_id,
@ -120,7 +118,6 @@ func (db sqlitePersistence) tableUserMessagesScanAllFields(row scanner, message
var quotedText sql.NullString
var quotedParsedText []byte
var quotedFrom sql.NullString
var quotedImage sql.NullString
var quotedAudio sql.NullString
var quotedAudioDuration sql.NullInt64
var quotedCommunityID sql.NullString
@ -155,7 +152,6 @@ func (db sqlitePersistence) tableUserMessagesScanAllFields(row scanner, message
&message.ParsedText,
&sticker.Pack,
&sticker.Hash,
&message.Base64Image,
&audio.DurationMs,
&message.Base64Audio,
&communityID,
@ -181,7 +177,6 @@ func (db sqlitePersistence) tableUserMessagesScanAllFields(row scanner, message
&quotedFrom,
&quotedText,
&quotedParsedText,
&quotedImage,
&quotedAudioDuration,
&quotedAudio,
&quotedCommunityID,
@ -206,7 +201,6 @@ func (db sqlitePersistence) tableUserMessagesScanAllFields(row scanner, message
From: quotedFrom.String,
Text: quotedText.String,
ParsedText: quotedParsedText,
Base64Image: quotedImage.String,
AudioDurationMs: uint64(quotedAudioDuration.Int64),
Base64Audio: quotedAudio.String,
CommunityID: quotedCommunityID.String,

View File

@ -120,6 +120,7 @@ type Messenger struct {
settings *accounts.Database
account *multiaccounts.Account
mailserversDatabase *mailserversDB.Database
imageServer *images.Server
quit chan struct{}
requestedCommunities map[string]*transport.Filter
connectionState connection.State
@ -379,7 +380,14 @@ func NewMessenger(
return nil, err
}
settings := accounts.NewDB(database)
mailservers := mailserversDB.NewDB(database)
imageServer, err := images.NewServer(database, logger)
if err != nil {
return nil, err
}
messenger = &Messenger{
config: &c,
node: node,
@ -415,12 +423,14 @@ func NewMessenger(
account: c.account,
quit: make(chan struct{}),
requestedCommunities: make(map[string]*transport.Filter),
imageServer: imageServer,
shutdownTasks: []func() error{
ensVerifier.Stop,
pushNotificationClient.Stop,
communitiesManager.Stop,
encryptionProtocol.Stop,
transp.ResetFilters,
imageServer.Stop,
transp.Stop,
func() error { sender.Stop(); return nil },
// Currently this often fails, seems like it's safe to ignore them
@ -545,6 +555,18 @@ func (m *Messenger) resendExpiredMessages() error {
return nil
}
func (m *Messenger) ToForeground() {
if m.imageServer != nil {
m.imageServer.ToForeground()
}
}
func (m *Messenger) ToBackground() {
if m.imageServer != nil {
m.imageServer.ToBackground()
}
}
func (m *Messenger) Start() (*MessengerResponse, error) {
m.logger.Info("starting messenger", zap.String("identity", types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))))
// Start push notification server
@ -630,6 +652,11 @@ func (m *Messenger) Start() (*MessengerResponse, error) {
}
}
err = m.imageServer.Start()
if err != nil {
return nil, err
}
return response, nil
}
@ -2380,10 +2407,13 @@ func (m *Messenger) sendChatMessage(ctx context.Context, message *common.Message
if err != nil {
return nil, err
}
response.SetMessages(msg)
response.AddChat(chat)
m.logger.Debug("sent message", zap.String("id", message.ID))
m.prepareMessages(response.messages)
return &response, m.saveChat(chat)
}
@ -3561,6 +3591,8 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte
return nil, err
}
m.prepareMessages(messageState.Response.messages)
for _, message := range messageState.Response.messages {
if _, ok := newMessagesIds[message.ID]; ok {
message.New = true
@ -3621,6 +3653,9 @@ func (m *Messenger) MessageByChatID(chatID, cursor string, limit int) ([]*common
return nil, "", ErrChatNotFound
}
var msgs []*common.Message
var nextCursor string
if chat.Timeline() {
var chatIDs = []string{"@" + contactIDFromPublicKey(&m.identity.PublicKey)}
m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
@ -3629,9 +3664,29 @@ func (m *Messenger) MessageByChatID(chatID, cursor string, limit int) ([]*common
}
return true
})
return m.persistence.MessageByChatIDs(chatIDs, cursor, limit)
msgs, nextCursor, err = m.persistence.MessageByChatIDs(chatIDs, cursor, limit)
if err != nil {
return nil, "", err
}
} else {
msgs, nextCursor, err = m.persistence.MessageByChatID(chatID, cursor, limit)
if err != nil {
return nil, "", err
}
}
return m.persistence.MessageByChatID(chatID, cursor, limit)
for idx := range msgs {
msgs[idx].PrepareImageURL(m.imageServer.Port)
}
return msgs, nextCursor, nil
}
func (m *Messenger) prepareMessages(messages map[string]*common.Message) {
for idx := range messages {
messages[idx].PrepareImageURL(m.imageServer.Port)
}
}
func (m *Messenger) AllMessageByChatIDWhichMatchTerm(chatID string, searchTerm string, caseSensitive bool) ([]*common.Message, error) {