package server import ( "bytes" "crypto/rand" "database/sql" "io/ioutil" "net/http" "time" "github.com/btcsuite/btcutil/base58" "go.uber.org/zap" "github.com/status-im/status-go/ipfs" "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/identity/identicon" "github.com/status-im/status-go/protocol/images" "github.com/status-im/status-go/signal" ) const ( basePath = "/messages" identiconsPath = basePath + "/identicons" imagesPath = basePath + "/images" audioPath = basePath + "/audio" ipfsPath = "/ipfs" // Handler routes for pairing pairingBase = "/pairing" pairingSend = pairingBase + "/send" pairingReceive = pairingBase + "/receive" pairingChallenge = pairingBase + "/challenge" // Session names sessionChallenge = "challenge" sessionBlocked = "blocked" ) type HandlerPatternMap map[string]http.HandlerFunc func handleIdenticon(logger *zap.Logger) http.HandlerFunc { 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) http.HandlerFunc { 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) http.HandlerFunc { 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 { 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 handlePairingReceive(ps *PairingServer) http.HandlerFunc { signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess}) return func(w http.ResponseWriter, r *http.Request) { payload, err := ioutil.ReadAll(r.Body) if err != nil { signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err}) ps.logger.Error("ioutil.ReadAll(r.Body)", zap.Error(err)) } signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess}) err = ps.PayloadManager.Receive(payload) if err != nil { signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err}) ps.logger.Error("ps.PayloadManager.Receive(payload)", zap.Error(err)) } signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess}) } } func handlePairingSend(ps *PairingServer) http.HandlerFunc { signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess}) // TODO lock sending after one successful transfer, perhaps perform the lock on the PayloadManager level return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/octet-stream") _, err := w.Write(ps.PayloadManager.ToSend()) if err != nil { signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err}) ps.logger.Error("w.Write(ps.PayloadManager.ToSend())", zap.Error(err)) } signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess}) } } func challengeMiddleware(ps *PairingServer, next http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { s, err := ps.cookieStore.Get(r, sessionChallenge) if err != nil { ps.logger.Error("ps.cookieStore.Get(r, pairingStoreChallenge)", zap.Error(err)) http.Error(w, "error", http.StatusInternalServerError) } blocked, ok := s.Values[sessionBlocked].(bool) if ok && blocked { http.Error(w, "forbidden", http.StatusForbidden) return } // If the request header doesn't include a challenge don't punish the client, just throw a 403 pc := r.Header.Get(sessionChallenge) if pc == "" { http.Error(w, "forbidden", http.StatusForbidden) return } c, err := common.Decrypt(base58.Decode(pc), ps.ek) if err != nil { ps.logger.Error("c, err := common.Decrypt(rc, ps.ek)", zap.Error(err)) http.Error(w, "error", http.StatusInternalServerError) return } // If the challenge is not in the session store don't punish the client, just throw a 403 challenge, ok := s.Values[sessionChallenge].([]byte) if !ok { http.Error(w, "forbidden", http.StatusForbidden) return } // Only if we have both a challenge in the session store and in the request header // do we entertain blocking the client. Because then we know someone is trying to be sneaky. if bytes.Compare(c, challenge) != 0 { s.Values[sessionBlocked] = true err = s.Save(r, w) if err != nil { ps.logger.Error("err = s.Save(r, w)", zap.Error(err)) } http.Error(w, "forbidden", http.StatusForbidden) return } next.ServeHTTP(w, r) } } func handlePairingChallenge(ps *PairingServer) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { s, err := ps.cookieStore.Get(r, sessionChallenge) if err != nil { ps.logger.Error("ps.cookieStore.Get(r, pairingStoreChallenge)", zap.Error(err)) } var challenge []byte challenge, ok := s.Values[sessionChallenge].([]byte) if !ok { challenge = make([]byte, 64) _, err = rand.Read(challenge) if err != nil { ps.logger.Error("_, err = rand.Read(auth)", zap.Error(err)) } s.Values[sessionChallenge] = challenge err = s.Save(r, w) if err != nil { ps.logger.Error("err = s.Save(r, w)", zap.Error(err)) } } w.Header().Set("Content-Type", "application/octet-stream") _, err = w.Write(challenge) if err != nil { ps.logger.Error("_, err = w.Write(challenge)", zap.Error(err)) } } }