status-go/server/handlers.go

530 lines
14 KiB
Go

package server
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"image"
"net/http"
"net/url"
"strconv"
"time"
"go.uber.org/zap"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/ipfs"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/protocol/identity/colorhash"
"github.com/status-im/status-go/protocol/identity/identicon"
"github.com/status-im/status-go/protocol/identity/ring"
"github.com/status-im/status-go/protocol/protobuf"
)
const (
basePath = "/messages"
identiconsPath = basePath + "/identicons"
imagesPath = basePath + "/images"
audioPath = basePath + "/audio"
ipfsPath = "/ipfs"
discordAuthorsPath = "/discord/authors"
discordAttachmentsPath = basePath + "/discord/attachments"
LinkPreviewThumbnailPath = "/link-preview/thumbnail"
// Handler routes for pairing
accountImagesPath = "/accountImages"
contactImagesPath = "/contactImages"
generateQRCode = "/GenerateQRCode"
)
type HandlerPatternMap map[string]http.HandlerFunc
func handleRequestDBMissing(logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger.Error("can't handle media request without appdb")
}
}
func handleRequestDownloaderMissing(logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger.Error("can't handle media request without ipfs downloader")
}
}
func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
keyUids, ok := params["keyUid"]
if !ok || len(keyUids) == 0 {
logger.Error("no keyUid")
return
}
imageNames, ok := params["imageName"]
if !ok || len(imageNames) == 0 {
logger.Error("no imageName")
return
}
identityImage, err := multiaccountsDB.GetIdentityImage(keyUids[0], imageNames[0])
if err != nil {
logger.Error("handleAccountImages: failed to load image.", zap.String("keyUid", keyUids[0]), zap.String("imageName", imageNames[0]), zap.Error(err))
return
}
var payload = identityImage.Payload
if ringEnabled(params) {
account, err := multiaccountsDB.GetAccount(keyUids[0])
if err != nil {
logger.Error("handleAccountImages: failed to GetAccount .", zap.String("keyUid", keyUids[0]), zap.Error(err))
return
}
accColorHash := account.ColorHash
if accColorHash == nil {
pks, ok := params["publicKey"]
if !ok || len(pks) == 0 {
logger.Error("no publicKey")
return
}
accColorHash, err = colorhash.GenerateFor(pks[0])
if err != nil {
logger.Error("could not generate color hash")
return
}
}
var theme = getTheme(params, logger)
payload, err = ring.DrawRing(&ring.DrawRingParam{
Theme: theme, ColorHash: accColorHash, ImageBytes: identityImage.Payload, Height: identityImage.Height, Width: identityImage.Width,
})
if err != nil {
logger.Error("failed to draw ring for account identity", zap.Error(err))
return
}
}
if len(payload) == 0 {
logger.Error("empty image")
return
}
mime, err := images.GetProtobufImageMime(payload)
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(payload)
if err != nil {
logger.Error("failed to write image", zap.Error(err))
}
}
}
func handleContactImages(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
if db == nil {
return handleRequestDBMissing(logger)
}
return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
pks, ok := params["publicKey"]
if !ok || len(pks) == 0 {
logger.Error("no publicKey")
return
}
imageNames, ok := params["imageName"]
if !ok || len(imageNames) == 0 {
logger.Error("no imageName")
return
}
var payload []byte
err := db.QueryRow(`SELECT payload FROM chat_identity_contacts WHERE contact_id = ? and image_type = ?`, pks[0], imageNames[0]).Scan(&payload)
if err != nil {
logger.Error("failed to load image.", zap.String("contact id", pks[0]), zap.String("image type", imageNames[0]), zap.Error(err))
return
}
if ringEnabled(params) {
colorHash, err := colorhash.GenerateFor(pks[0])
if err != nil {
logger.Error("could not generate color hash")
return
}
var theme = getTheme(params, logger)
config, _, err := image.DecodeConfig(bytes.NewReader(payload))
if err != nil {
logger.Error("failed to decode config.", zap.String("contact id", pks[0]), zap.String("image type", imageNames[0]), zap.Error(err))
return
}
payload, err = ring.DrawRing(&ring.DrawRingParam{
Theme: theme, ColorHash: colorHash, ImageBytes: payload, Height: config.Height, Width: config.Width,
})
if err != nil {
logger.Error("failed to draw ring for contact image.", zap.Error(err))
return
}
}
if len(payload) == 0 {
logger.Error("empty image")
return
}
mime, err := images.GetProtobufImageMime(payload)
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(payload)
if err != nil {
logger.Error("failed to write image", zap.Error(err))
}
}
}
func ringEnabled(params url.Values) bool {
addRings, ok := params["addRing"]
return ok && len(addRings) == 1 && addRings[0] == "1"
}
func getTheme(params url.Values, logger *zap.Logger) ring.Theme {
theme := ring.LightTheme // default
themes, ok := params["theme"]
if ok && len(themes) > 0 {
t, err := strconv.Atoi(themes[0])
if err != nil {
logger.Error("invalid param[theme], value: " + themes[0])
} else {
theme = ring.Theme(t)
}
}
return theme
}
func handleIdenticon(logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
pks, ok := params["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")
}
if image != nil && ringEnabled(params) {
colorHash, err := colorhash.GenerateFor(pk)
if err != nil {
logger.Error("could not generate color hash")
return
}
theme := getTheme(params, logger)
image, err = ring.DrawRing(&ring.DrawRingParam{
Theme: theme, ColorHash: colorHash, ImageBytes: image, Height: identicon.Height, Width: identicon.Width,
})
if err != nil {
logger.Error("failed to draw ring", zap.Error(err))
}
}
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 handleDiscordAuthorAvatar(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
if db == nil {
return handleRequestDBMissing(logger)
}
return func(w http.ResponseWriter, r *http.Request) {
authorIDs, ok := r.URL.Query()["authorId"]
if !ok || len(authorIDs) == 0 {
logger.Error("no authorIDs")
return
}
authorID := authorIDs[0]
var image []byte
err := db.QueryRow(`SELECT avatar_image_payload FROM discord_message_authors WHERE id = ?`, authorID).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.GetProtobufImageMime(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 handleDiscordAttachment(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
if db == nil {
return handleRequestDBMissing(logger)
}
return func(w http.ResponseWriter, r *http.Request) {
messageIDs, ok := r.URL.Query()["messageId"]
if !ok || len(messageIDs) == 0 {
logger.Error("no messageID")
return
}
attachmentIDs, ok := r.URL.Query()["attachmentId"]
if !ok || len(attachmentIDs) == 0 {
logger.Error("no attachmentID")
return
}
messageID := messageIDs[0]
attachmentID := attachmentIDs[0]
var image []byte
err := db.QueryRow(`SELECT payload FROM discord_message_attachments WHERE discord_message_id = ? AND id = ?`, messageID, attachmentID).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.GetProtobufImageMime(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 handleImage(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
if db == nil {
return handleRequestDBMissing(logger)
}
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.GetProtobufImageMime(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) http.HandlerFunc {
if db == nil {
return handleRequestDBMissing(logger)
}
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) http.HandlerFunc {
if downloader == nil {
return handleRequestDownloaderMissing(logger)
}
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))
}
}
}
func handleQRCodeGeneration(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
payload := generateQRBytes(params, logger, multiaccountsDB)
mime, err := images.GetProtobufImageMime(payload)
if err != nil {
logger.Error("could not generate image from payload", zap.Error(err))
}
w.Header().Set("Content-Type", mime)
w.Header().Set("Cache-Control", "no-store")
_, err = w.Write(payload)
if err != nil {
logger.Error("failed to write image", zap.Error(err))
}
}
}
func getThumbnailPayload(db *sql.DB, logger *zap.Logger, msgID string, thumbnailURL string) ([]byte, error) {
var payload []byte
var result []byte
err := db.QueryRow(`SELECT unfurled_links FROM user_messages WHERE id = ?`, msgID).Scan(&result)
if err != nil {
return payload, fmt.Errorf("could not find message with message-id '%s': %w", msgID, err)
}
var links []*protobuf.UnfurledLink
err = json.Unmarshal(result, &links)
if err != nil {
return payload, fmt.Errorf("failed to unmarshal protobuf.UrlPreview: %w", err)
}
for _, p := range links {
if p.Url == thumbnailURL {
payload = p.ThumbnailPayload
break
}
}
return payload, nil
}
func handleLinkPreviewThumbnail(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query()
paramID, ok := queryParams["message-id"]
if !ok || len(paramID) == 0 {
http.Error(w, "missing query parameter 'message-id'", http.StatusBadRequest)
return
}
paramURL, ok := queryParams["url"]
if !ok || len(paramURL) == 0 {
http.Error(w, "missing query parameter 'url'", http.StatusBadRequest)
return
}
msgID := paramID[0]
thumbnailURL := paramURL[0]
thumbnail, err := getThumbnailPayload(db, logger, msgID, thumbnailURL)
if err != nil {
logger.Error("failed to get thumbnail", zap.String("msgID", msgID))
http.Error(w, "failed to get thumbnail", http.StatusInternalServerError)
return
}
mimeType, err := images.GetMimeType(thumbnail)
if err != nil {
http.Error(w, "mime type not supported", http.StatusNotImplemented)
return
}
w.Header().Set("Content-Type", "image/"+mimeType)
w.Header().Set("Cache-Control", "no-store")
_, err = w.Write(thumbnail)
if err != nil {
logger.Error("failed to write response", zap.Error(err))
}
}
}