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:
parent
54b35b0510
commit
5925b3b7cc
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|||
"edFrom,
|
||||
"edText,
|
||||
"edParsedText,
|
||||
"edImage,
|
||||
"edAudioDuration,
|
||||
"edAudio,
|
||||
"edCommunityID,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue