From 7834471b84aa7b114ac86bc2a78a1b4905f69295 Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Fri, 16 Feb 2024 08:49:19 -0500 Subject: [PATCH] refactor: merge http handler back to s5 api struct --- api/s5/http.go | 1142 --------------------------------------------- api/s5/s5.go | 1193 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 1161 insertions(+), 1174 deletions(-) delete mode 100644 api/s5/http.go diff --git a/api/s5/http.go b/api/s5/http.go deleted file mode 100644 index 0e44831..0000000 --- a/api/s5/http.go +++ /dev/null @@ -1,1142 +0,0 @@ -package s5 - -import ( - "bytes" - "context" - "crypto/ed25519" - "crypto/rand" - "encoding/base64" - "encoding/hex" - "errors" - "fmt" - "io" - "math" - "mime/multipart" - "net/http" - "strconv" - "strings" - "time" - - "git.lumeweb.com/LumeWeb/libs5-go/metadata" - - "git.lumeweb.com/LumeWeb/libs5-go/encoding" - libs5node "git.lumeweb.com/LumeWeb/libs5-go/node" - libs5protocol "git.lumeweb.com/LumeWeb/libs5-go/protocol" - libs5service "git.lumeweb.com/LumeWeb/libs5-go/service" - libs5storage "git.lumeweb.com/LumeWeb/libs5-go/storage" - libs5storageProvider "git.lumeweb.com/LumeWeb/libs5-go/storage/provider" - "git.lumeweb.com/LumeWeb/libs5-go/types" - "git.lumeweb.com/LumeWeb/portal/account" - "git.lumeweb.com/LumeWeb/portal/api/middleware" - "git.lumeweb.com/LumeWeb/portal/db/models" - "git.lumeweb.com/LumeWeb/portal/protocols/s5" - "git.lumeweb.com/LumeWeb/portal/storage" - "github.com/samber/lo" - "github.com/spf13/viper" - "github.com/vmihailenco/msgpack/v5" - "go.sia.tech/jape" - "go.uber.org/fx" - "go.uber.org/zap" - "gorm.io/gorm" - "nhooyr.io/websocket" -) - -type readSeekNopCloser struct { - *bytes.Reader -} - -func (rsnc readSeekNopCloser) Close() error { - return nil -} - -type HttpHandler struct { - config *viper.Viper - logger *zap.Logger - storage *storage.StorageServiceDefault - db *gorm.DB - accounts *account.AccountServiceDefault - protocol *s5.S5Protocol -} - -type HttpHandlerParams struct { - fx.In - - Config *viper.Viper - Logger *zap.Logger - Storage *storage.StorageServiceDefault - Db *gorm.DB - Accounts *account.AccountServiceDefault - Protocol *s5.S5Protocol -} - -type HttpHandlerResult struct { - fx.Out - - HttpHandler HttpHandler -} - -func NewHttpHandler(params HttpHandlerParams) (HttpHandlerResult, error) { - return HttpHandlerResult{ - HttpHandler: HttpHandler{ - config: params.Config, - logger: params.Logger, - storage: params.Storage, - db: params.Db, - accounts: params.Accounts, - protocol: params.Protocol, - }, - }, nil -} - -func (h *HttpHandler) smallFileUpload(jc jape.Context) { - user := middleware.GetUserFromContext(jc.Request.Context()) - - file, err := h.prepareFileUpload(jc) - if err != nil { - h.sendErrorResponse(jc, err) - return - } - defer func(file io.ReadSeekCloser) { - err := file.Close() - if err != nil { - h.logger.Error("Error closing file", zap.Error(err)) - } - }(file) - - // Use PutFileSmall for the actual file upload - newUpload, err2 := h.storage.PutFileSmall(file, "s5", user, jc.Request.RemoteAddr) - if err2 != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyFileUploadFailed, err2)) - return - } - - cid, err2 := encoding.CIDFromHash(newUpload.Hash, newUpload.Size, types.CIDTypeRaw, types.HashTypeBlake3) - if err2 != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyFileUploadFailed, err2)) - return - } - - cidStr, err2 := cid.ToString() - if err2 != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyFileUploadFailed, err2)) - return - } - - jc.Encode(&SmallUploadResponse{ - CID: cidStr, - }) -} - -func (h *HttpHandler) prepareFileUpload(jc jape.Context) (file io.ReadSeekCloser, s5Err *S5Error) { - r := jc.Request - contentType := r.Header.Get("Content-Type") - - // Handle multipart form data uploads - if strings.HasPrefix(contentType, "multipart/form-data") { - if err := r.ParseMultipartForm(h.config.GetInt64("core.post-upload-limit")); err != nil { - return nil, NewS5Error(ErrKeyFileUploadFailed, err) - } - - multipartFile, _, err := r.FormFile("file") - if err != nil { - return nil, NewS5Error(ErrKeyFileUploadFailed, err) - } - - return multipartFile, nil - } - - // Handle raw body uploads - data, err := io.ReadAll(r.Body) - if err != nil { - return nil, NewS5Error(ErrKeyFileUploadFailed, err) - } - - buffer := readSeekNopCloser{bytes.NewReader(data)} - - return buffer, nil -} - -func (h *HttpHandler) accountRegisterChallenge(jc jape.Context) { - var pubkey string - if jc.DecodeForm("pubKey", &pubkey) != nil { - return - } - - challenge := make([]byte, 32) - _, err := rand.Read(challenge) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err)) - return - } - - decodedKey, err := base64.RawURLEncoding.DecodeString(pubkey) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) - return - } - - if len(decodedKey) != 33 || int(decodedKey[0]) != int(types.HashTypeEd25519) { - h.sendErrorResponse(jc, NewS5Error(ErrKeyDataIntegrityError, fmt.Errorf("invalid public key format"))) - return - } - - result := h.db.Create(&models.S5Challenge{ - Pubkey: pubkey, - Challenge: base64.RawURLEncoding.EncodeToString(challenge), - Type: "register", - }) - - if result.Error != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) - return - } - - jc.Encode(&AccountRegisterChallengeResponse{ - Challenge: base64.RawURLEncoding.EncodeToString(challenge), - }) -} - -func (h *HttpHandler) accountRegister(jc jape.Context) { - var request AccountRegisterRequest - if jc.Decode(&request) != nil { - return - } - - decodedKey, err := base64.RawURLEncoding.DecodeString(request.Pubkey) - if err != nil || len(decodedKey) != 33 || int(decodedKey[0]) != int(types.HashTypeEd25519) { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) - return - } - - challenge := models.S5Challenge{ - Pubkey: request.Pubkey, - Type: "register", - } - - if result := h.db.Where(&challenge).First(&challenge); result.RowsAffected == 0 || result.Error != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyResourceNotFound, result.Error)) - return - } - - decodedResponse, err := base64.RawURLEncoding.DecodeString(request.Response) - if err != nil || len(decodedResponse) != 65 { - h.sendErrorResponse(jc, NewS5Error(ErrKeyDataIntegrityError, err)) - return - } - - decodedChallenge, err := base64.RawURLEncoding.DecodeString(challenge.Challenge) - if err != nil || !bytes.Equal(decodedResponse[1:33], decodedChallenge) { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) - return - } - - decodedSignature, err := base64.RawURLEncoding.DecodeString(request.Signature) - if err != nil || !ed25519.Verify(decodedKey[1:], decodedResponse, decodedSignature) { - h.sendErrorResponse(jc, NewS5Error(ErrKeyAuthorizationFailed, err)) - return - } - - if request.Email == "" { - request.Email = fmt.Sprintf("%s@%s", hex.EncodeToString(decodedKey[1:]), "example.com") - } - - if accountExists, _, _ := h.accounts.EmailExists(request.Email); accountExists { - h.sendErrorResponse(jc, NewS5Error(ErrKeyResourceLimitExceeded, fmt.Errorf("email already exists"))) - return - } - - if pubkeyExists, _, _ := h.accounts.PubkeyExists(hex.EncodeToString(decodedKey[1:])); pubkeyExists { - h.sendErrorResponse(jc, NewS5Error(ErrKeyResourceLimitExceeded, fmt.Errorf("pubkey already exists"))) - return - } - - passwd := make([]byte, 32) - if _, err = rand.Read(passwd); err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err)) - return - } - - newAccount, err := h.accounts.CreateAccount(request.Email, string(passwd)) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) - return - } - - rawPubkey := hex.EncodeToString(decodedKey[1:]) - if err = h.accounts.AddPubkeyToAccount(*newAccount, rawPubkey); err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) - return - } - - jwt, err := h.accounts.LoginPubkey(rawPubkey) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyAuthenticationFailed, err)) - return - } - - if result := h.db.Delete(&challenge); result.Error != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) - return - } - - setAuthCookie(jwt, jc) -} - -func (h *HttpHandler) accountLoginChallenge(jc jape.Context) { - var pubkey string - if jc.DecodeForm("pubKey", &pubkey) != nil { - return - } - - challenge := make([]byte, 32) - _, err := rand.Read(challenge) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err)) - return - } - - decodedKey, err := base64.RawURLEncoding.DecodeString(pubkey) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) - return - } - - if len(decodedKey) != 33 || int(decodedKey[0]) != int(types.HashTypeEd25519) { - h.sendErrorResponse(jc, NewS5Error(ErrKeyUnsupportedFileType, fmt.Errorf("public key not supported"))) - return - } - - pubkeyExists, _, _ := h.accounts.PubkeyExists(hex.EncodeToString(decodedKey[1:])) - if !pubkeyExists { - h.sendErrorResponse(jc, NewS5Error(ErrKeyResourceNotFound, fmt.Errorf("public key does not exist"))) - return - } - - result := h.db.Create(&models.S5Challenge{ - Pubkey: pubkey, - Challenge: base64.RawURLEncoding.EncodeToString(challenge), - Type: "login", - }) - - if result.Error != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) - return - } - - jc.Encode(&AccountLoginChallengeResponse{ - Challenge: base64.RawURLEncoding.EncodeToString(challenge), - }) -} - -func (h *HttpHandler) accountLogin(jc jape.Context) { - var request AccountLoginRequest - if jc.Decode(&request) != nil { - return - } - - decodedKey, err := base64.RawURLEncoding.DecodeString(request.Pubkey) - if err != nil || len(decodedKey) != 32 { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) - return - } - - if int(decodedKey[0]) != int(types.HashTypeEd25519) { - h.sendErrorResponse(jc, NewS5Error(ErrKeyUnsupportedFileType, fmt.Errorf("public key type not supported"))) - return - } - - var challenge models.S5Challenge - result := h.db.Where(&models.S5Challenge{Pubkey: request.Pubkey, Type: "login"}).First(&challenge) - if result.RowsAffected == 0 || result.Error != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyResourceNotFound, result.Error)) - return - } - - decodedResponse, err := base64.RawURLEncoding.DecodeString(request.Response) - if err != nil || len(decodedResponse) != 65 { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) - return - } - - decodedChallenge, err := base64.RawURLEncoding.DecodeString(challenge.Challenge) - if err != nil || !bytes.Equal(decodedResponse[1:33], decodedChallenge) { - h.sendErrorResponse(jc, NewS5Error(ErrKeyDataIntegrityError, err)) - return - } - - decodedSignature, err := base64.RawURLEncoding.DecodeString(request.Signature) - if err != nil || !ed25519.Verify(decodedKey[1:], decodedResponse, decodedSignature) { - h.sendErrorResponse(jc, NewS5Error(ErrKeyAuthorizationFailed, err)) - return - } - - jwt, err := h.accounts.LoginPubkey(hex.EncodeToString(decodedKey[1:])) // Adjust based on how LoginPubkey is implemented - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyAuthenticationFailed, err)) - return - } - - if result := h.db.Delete(&challenge); result.Error != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) - return - } - - setAuthCookie(jwt, jc) -} - -func (h *HttpHandler) accountInfo(jc jape.Context) { - userID := middleware.GetUserFromContext(jc.Request.Context()) - _, user, _ := h.accounts.AccountExists(userID) - - info := &AccountInfoResponse{ - Email: user.Email, - QuotaExceeded: false, - EmailConfirmed: false, - IsRestricted: false, - Tier: AccountTier{ - Id: 1, - Name: "default", - UploadBandwidth: math.MaxUint32, - StorageLimit: math.MaxUint32, - Scopes: []interface{}{}, - }, - } - - jc.Encode(info) -} - -func (h *HttpHandler) accountStats(jc jape.Context) { - userID := middleware.GetUserFromContext(jc.Request.Context()) - _, user, _ := h.accounts.AccountExists(userID) - - info := &AccountStatsResponse{ - AccountInfoResponse: AccountInfoResponse{ - Email: user.Email, - QuotaExceeded: false, - EmailConfirmed: false, - IsRestricted: false, - Tier: AccountTier{ - Id: 1, - Name: "default", - UploadBandwidth: math.MaxUint32, - StorageLimit: math.MaxUint32, - Scopes: []interface{}{}, - }, - }, - Stats: AccountStats{ - Total: AccountStatsTotal{ - UsedStorage: 0, - }, - }, - } - - jc.Encode(info) -} - -func (h *HttpHandler) accountPins(jc jape.Context) { - var cursor uint64 - if err := jc.DecodeForm("cursor", &cursor); err != nil { - // Assuming jc.DecodeForm sends out its own error, so no need for further action here - return - } - - userID := middleware.GetUserFromContext(jc.Request.Context()) - - pins, err := h.accounts.AccountPins(userID, cursor) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) - return - } - - pinResponse := &AccountPinResponse{Cursor: cursor, Pins: pins} - result, err2 := msgpack.Marshal(pinResponse) - if err2 != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err2)) - return - } - - jc.ResponseWriter.Header().Set("Content-Type", "application/msgpack") - jc.ResponseWriter.WriteHeader(http.StatusOK) - if _, err := jc.ResponseWriter.Write(result); err != nil { - h.logger.Error("failed to write account pins response", zap.Error(err)) - } -} - -func (h *HttpHandler) accountPinDelete(jc jape.Context) { - var cid string - if err := jc.DecodeParam("cid", &cid); err != nil { - return - } - - user := middleware.GetUserFromContext(jc.Request.Context()) - - decodedCid, err := encoding.CIDFromString(cid) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) - return - } - - hash := hex.EncodeToString(decodedCid.Hash.HashBytes()) - if err := h.accounts.DeletePinByHash(hash, user); err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) - return - } - - jc.ResponseWriter.WriteHeader(http.StatusNoContent) -} - -func (h *HttpHandler) accountPin(jc jape.Context) { - var cid string - if err := jc.DecodeParam("cid", &cid); err != nil { - return - } - - userID := middleware.GetUserFromContext(jc.Request.Context()) - - decodedCid, err := encoding.CIDFromString(cid) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) - return - } - - hash := hex.EncodeToString(decodedCid.Hash.HashBytes()) - h.logger.Info("Processing pin request", zap.String("cid", cid), zap.String("hash", hash)) - - if err := h.accounts.PinByHash(hash, userID); err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) - return - } - - jc.ResponseWriter.WriteHeader(http.StatusNoContent) -} - -func (h *HttpHandler) directoryUpload(jc jape.Context) { - // Decode form fields - var ( - tryFiles []string - errorPages map[int]string - name string - ) - - if err := jc.DecodeForm("tryFiles", &tryFiles); err != nil || jc.DecodeForm("errorPages", &errorPages) != nil || jc.DecodeForm("name", &name) != nil { - } - - // Verify content type - if contentType := jc.Request.Header.Get("Content-Type"); !strings.HasPrefix(contentType, "multipart/form-data") { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, fmt.Errorf("expected multipart/form-data content type, got %s", contentType))) - return - } - - // Parse multipart form with size limit from config - if err := jc.Request.ParseMultipartForm(h.config.GetInt64("core.post-upload-limit")); err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) - return - } - - user := middleware.GetUserFromContext(jc.Request.Context()) - uploads, err := h.processMultipartFiles(jc.Request, user) - if err != nil { - h.sendErrorResponse(jc, err) // processMultipartFiles should return a properly wrapped S5Error - return - } - - // Generate metadata for the directory upload - app, err := h.createAppMetadata(name, tryFiles, errorPages, uploads) - if err != nil { - h.sendErrorResponse(jc, err) // createAppMetadata should return a properly wrapped S5Error - return - } - - // Upload the metadata - cidStr, err := h.uploadAppMetadata(app, user, jc.Request) - if err != nil { - h.sendErrorResponse(jc, err) // uploadAppMetadata should return a properly wrapped S5Error - return - } - - jc.Encode(&AppUploadResponse{CID: cidStr}) -} - -func (h *HttpHandler) processMultipartFiles(r *http.Request, user uint) (map[string]*models.Upload, error) { - uploadMap := make(map[string]*models.Upload) - - for _, files := range r.MultipartForm.File { - for _, fileHeader := range files { - file, err := fileHeader.Open() - if err != nil { - return nil, NewS5Error(ErrKeyStorageOperationFailed, err) - } - defer func(file multipart.File) { - err := file.Close() - if err != nil { - h.logger.Error("Error closing file", zap.Error(err)) - } - }(file) - - upload, err := h.storage.PutFileSmall(file, "s5", user, r.RemoteAddr) - if err != nil { - return nil, NewS5Error(ErrKeyStorageOperationFailed, err) - } - - uploadMap[fileHeader.Filename] = upload - } - } - - return uploadMap, nil -} - -func (h *HttpHandler) createAppMetadata(name string, tryFiles []string, errorPages map[int]string, uploads map[string]*models.Upload) (*metadata.WebAppMetadata, error) { - filesMap := make(map[string]metadata.WebAppMetadataFileReference, len(uploads)) - - for filename, upload := range uploads { - hashDecoded, err := hex.DecodeString(upload.Hash) - if err != nil { - return nil, NewS5Error(ErrKeyInternalError, err, "Failed to decode hash for file: "+filename) - } - - cid, err := encoding.CIDFromHash(hashDecoded, upload.Size, types.CIDTypeRaw, types.HashTypeBlake3) - if err != nil { - return nil, NewS5Error(ErrKeyInternalError, err, "Failed to create CID for file: "+filename) - } - filesMap[filename] = metadata.WebAppMetadataFileReference{ - Cid: cid, - ContentType: upload.MimeType, - } - } - - extraMetadataMap := make(map[int]interface{}) - for statusCode, page := range errorPages { - extraMetadataMap[statusCode] = page - } - - extraMetadata := metadata.NewExtraMetadata(extraMetadataMap) - // Create the web app metadata object - app := metadata.NewWebAppMetadata( - name, - tryFiles, - *extraMetadata, - errorPages, - filesMap, - ) - - return app, nil -} - -func (h *HttpHandler) uploadAppMetadata(appData *metadata.WebAppMetadata, userId uint, r *http.Request) (string, error) { - appDataRaw, err := msgpack.Marshal(appData) - if err != nil { - return "", NewS5Error(ErrKeyInternalError, err, "Failed to marshal app metadata") - } - - file := bytes.NewReader(appDataRaw) - - upload, err := h.storage.PutFileSmall(file, "s5", userId, r.RemoteAddr) - if err != nil { - return "", NewS5Error(ErrKeyStorageOperationFailed, err) - } - - // Construct the CID for the newly uploaded metadata - cid, err := encoding.CIDFromHash(upload.Hash, uint64(len(appDataRaw)), types.CIDTypeMetadataWebapp, types.HashTypeBlake3) - if err != nil { - return "", NewS5Error(ErrKeyInternalError, err, "Failed to create CID for new app metadata") - } - cidStr, err := cid.ToString() - if err != nil { - return "", NewS5Error(ErrKeyInternalError, err, "Failed to convert CID to string for new app metadata") - } - - return cidStr, nil -} - -func (h *HttpHandler) debugDownloadUrls(jc jape.Context) { - var cid string - if err := jc.DecodeParam("cid", &cid); err != nil { - return - } - - decodedCid, err := encoding.CIDFromString(cid) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err, "Failed to decode CID")) - return - } - - node := h.getNode() - dlUriProvider := h.newStorageLocationProvider(&decodedCid.Hash, types.StorageLocationTypeFull, types.StorageLocationTypeFile, types.StorageLocationTypeBridge) - - if err := dlUriProvider.Start(); err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err, "Failed to start URI provider")) - return - } - - locations, err := node.Services().Storage().GetCachedStorageLocations(&decodedCid.Hash, []types.StorageLocationType{ - types.StorageLocationTypeFull, types.StorageLocationTypeFile, types.StorageLocationTypeBridge, - }) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err, "Failed to get cached storage locations")) - return - } - - availableNodes := lo.Keys[string, libs5storage.StorageLocation](locations) - availableNodesIds := make([]*encoding.NodeId, len(availableNodes)) - - for i, nodeIdStr := range availableNodes { - nodeId, err := encoding.DecodeNodeId(nodeIdStr) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err, "Failed to decode node ID")) - return - } - availableNodesIds[i] = nodeId - } - - sorted, err := node.Services().P2P().SortNodesByScore(availableNodesIds) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyNetworkError, err, "Failed to sort nodes by score")) - return - } - - output := make([]string, len(sorted)) - for i, nodeId := range sorted { - nodeIdStr, err := nodeId.ToString() - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err, "Failed to convert node ID to string")) - return - } - output[i] = locations[nodeIdStr].BytesURL() - } - - jc.ResponseWriter.WriteHeader(http.StatusOK) - _, err = jc.ResponseWriter.Write([]byte(strings.Join(output, "\n"))) - if err != nil { - h.logger.Error("Failed to write response", zap.Error(err)) - } -} - -func (h *HttpHandler) registryQuery(jc jape.Context) { - var pk string - if err := jc.DecodeForm("pk", &pk); err != nil { - return - } - - pkBytes, err := base64.RawURLEncoding.DecodeString(pk) - if err != nil { - s5Err := NewS5Error(ErrKeyInvalidFileFormat, err) - h.sendErrorResponse(jc, s5Err) - return - } - - entry, err := h.getNode().Services().Registry().Get(pkBytes) - if err != nil { - s5ErrKey := ErrKeyStorageOperationFailed - s5Err := NewS5Error(s5ErrKey, err) - h.sendErrorResponse(jc, s5Err) - return - } - - if entry == nil { - jc.ResponseWriter.WriteHeader(http.StatusNotFound) - return - } - - response := RegistryQueryResponse{ - Pk: base64.RawURLEncoding.EncodeToString(entry.PK()), - Revision: entry.Revision(), - Data: base64.RawURLEncoding.EncodeToString(entry.Data()), - Signature: base64.RawURLEncoding.EncodeToString(entry.Signature()), - } - jc.Encode(response) -} - -func (h *HttpHandler) registrySet(jc jape.Context) { - var request RegistrySetRequest - - if err := jc.Decode(&request); err != nil { - return - } - - pk, err := base64.RawURLEncoding.DecodeString(request.Pk) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding public key")) - return - } - - data, err := base64.RawURLEncoding.DecodeString(request.Data) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding data")) - return - } - - signature, err := base64.RawURLEncoding.DecodeString(request.Signature) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding signature")) - return - } - - entry := libs5protocol.NewSignedRegistryEntry(pk, request.Revision, data, signature) - - err = h.getNode().Services().Registry().Set(entry, false, nil) - if err != nil { - h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err, "Error setting registry entry")) - return - } -} -func (h *HttpHandler) registrySubscription(jc jape.Context) { - // Create a context for the WebSocket operations - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - var listeners []func() - - // Accept the WebSocket connection - c, err := websocket.Accept(jc.ResponseWriter, jc.Request, nil) - if err != nil { - h.logger.Error("error accepting websocket connection", zap.Error(err)) - return - } - defer func() { - // Close the WebSocket connection gracefully - err := c.Close(websocket.StatusNormalClosure, "connection closed") - if err != nil { - h.logger.Error("error closing websocket connection", zap.Error(err)) - } - // Clean up all listeners when the connection is closed - for _, listener := range listeners { - listener() - } - }() - - // Main loop for reading messages - for { - _, data, err := c.Read(ctx) - if err != nil { - if websocket.CloseStatus(err) == websocket.StatusNormalClosure { - // Normal closure - h.logger.Info("websocket connection closed normally") - } else { - // Handle different types of errors - h.logger.Error("error in websocket connection", zap.Error(err)) - } - break - } - - decoder := msgpack.NewDecoder(bytes.NewReader(data)) - - // Assuming method indicates the type of operation, validate it - method, err := decoder.DecodeInt() - if err != nil { - h.logger.Error("error decoding method", zap.Error(err)) - continue - } - - if method != 2 { - h.logger.Error("invalid method", zap.Int64("method", int64(method))) - continue - } - - sre, err := decoder.DecodeBytes() - if err != nil { - h.logger.Error("error decoding sre", zap.Error(err)) - continue - } - - // Listen for updates on the registry entry and send updates via WebSocket - off, err := h.getNode().Services().Registry().Listen(sre, func(entry libs5protocol.SignedRegistryEntry) { - encoded, err := msgpack.Marshal(entry) - if err != nil { - h.logger.Error("error encoding entry", zap.Error(err)) - return - } - - // Write updates to the WebSocket connection - if err := c.Write(ctx, websocket.MessageBinary, encoded); err != nil { - h.logger.Error("error writing to websocket", zap.Error(err)) - } - }) - if err != nil { - h.logger.Error("error setting up listener for registry", zap.Error(err)) - break - } - - listeners = append(listeners, off) // Add the listener's cleanup function to the list - } -} - -func (h *HttpHandler) getNode() *libs5node.Node { - return h.protocol.Node() -} - -func (h *HttpHandler) downloadBlob(jc jape.Context) { - var cid string - - if jc.DecodeParam("cid", &cid) != nil { - return - } - - cid = strings.Split(cid, ".")[0] - - cidDecoded, err := encoding.CIDFromString(cid) - if jc.Check("error decoding cid", err) != nil { - return - } - - dlUriProvider := h.newStorageLocationProvider(&cidDecoded.Hash, types.StorageLocationTypeFull, types.StorageLocationTypeFile, types.StorageLocationTypeBridge) - - err = dlUriProvider.Start() - - if jc.Check("error starting search", err) != nil { - return - } - - next, err := dlUriProvider.Next() - if jc.Check("error fetching blob", err) != nil { - return - } - - http.Redirect(jc.ResponseWriter, jc.Request, next.Location().BytesURL(), http.StatusFound) -} - -func (h *HttpHandler) debugStorageLocations(jc jape.Context) { - var hash string - - if jc.DecodeParam("hash", &hash) != nil { - return - } - - var kinds string - - if jc.DecodeForm("kinds", &kinds) != nil { - return - } - - decodedHash, err := encoding.MultihashFromBase64Url(hash) - if jc.Check("error decoding hash", err) != nil { - return - } - - typeList := strings.Split(kinds, ",") - typeIntList := make([]types.StorageLocationType, 0) - - for _, typeStr := range typeList { - typeInt, err := strconv.Atoi(typeStr) - if err != nil { - continue - } - typeIntList = append(typeIntList, types.StorageLocationType(typeInt)) - } - - if len(typeIntList) == 0 { - typeIntList = []types.StorageLocationType{ - types.StorageLocationTypeFull, - types.StorageLocationTypeFile, - types.StorageLocationTypeBridge, - types.StorageLocationTypeArchive, - } - } - - dlUriProvider := h.newStorageLocationProvider(decodedHash, typeIntList...) - - err = dlUriProvider.Start() - if jc.Check("error starting search", err) != nil { - return - } - - _, err = dlUriProvider.Next() - if jc.Check("error fetching locations", err) != nil { - return - } - - locations, err := h.getNode().Services().Storage().GetCachedStorageLocations(decodedHash, typeIntList) - if jc.Check("error getting cached locations", err) != nil { - return - } - - availableNodes := lo.Keys[string, libs5storage.StorageLocation](locations) - availableNodesIds := make([]*encoding.NodeId, len(availableNodes)) - - for i, nodeIdStr := range availableNodes { - nodeId, err := encoding.DecodeNodeId(nodeIdStr) - if jc.Check("error decoding node id", err) != nil { - return - } - availableNodesIds[i] = nodeId - } - - availableNodesIds, err = h.getNode().Services().P2P().SortNodesByScore(availableNodesIds) - - if jc.Check("error sorting nodes", err) != nil { - return - } - - debugLocations := make([]DebugStorageLocation, len(availableNodes)) - - for i, nodeId := range availableNodesIds { - nodeIdStr, err := nodeId.ToBase58() - if jc.Check("error encoding node id", err) != nil { - return - } - - score, err := h.getNode().Services().P2P().GetNodeScore(nodeId) - - if jc.Check("error getting node score", err) != nil { - return - } - - debugLocations[i] = DebugStorageLocation{ - Type: locations[nodeIdStr].Type(), - Parts: locations[nodeIdStr].Parts(), - Expiry: locations[nodeIdStr].Expiry(), - NodeId: nodeIdStr, - Score: score, - } - } - - jc.Encode(&DebugStorageLocationsResponse{ - Locations: debugLocations, - }) -} - -func (h *HttpHandler) downloadMetadata(jc jape.Context) { - var cid string - - if jc.DecodeParam("cid", &cid) != nil { - return - } - - cidDecoded, err := encoding.CIDFromString(cid) - if jc.Check("error decoding cid", err) != nil { - h.logger.Error("error decoding cid", zap.Error(err)) - return - } - - switch cidDecoded.Type { - case types.CIDTypeRaw: - _ = jc.Error(errors.New("Raw CIDs do not have metadata"), http.StatusBadRequest) - return - - case types.CIDTypeResolver: - _ = jc.Error(errors.New("Resolver CIDs not yet supported"), http.StatusBadRequest) - return - } - - meta, err := h.getNode().Services().Storage().GetMetadataByCID(cidDecoded) - - if jc.Check("error getting metadata", err) != nil { - h.logger.Error("error getting metadata", zap.Error(err)) - return - } - - if cidDecoded.Type != types.CIDTypeBridge { - jc.ResponseWriter.Header().Set("Cache-Control", "public, max-age=31536000") - } else { - jc.ResponseWriter.Header().Set("Cache-Control", "public, max-age=60") - } - - jc.Encode(&meta) - -} - -func (h *HttpHandler) downloadFile(jc jape.Context) { - var cid string - - if jc.DecodeParam("cid", &cid) != nil { - return - } - - var hashBytes []byte - isProof := false - - if strings.HasSuffix(cid, ".obao") { - isProof = true - cid = strings.TrimSuffix(cid, ".obao") - } - - cidDecoded, err := encoding.CIDFromString(cid) - - if err != nil { - hashDecoded, err := encoding.MultihashFromBase64Url(cid) - - if jc.Check("error decoding as cid or hash", err) != nil { - return - } - - hashBytes = hashDecoded.HashBytes() - } else { - hashBytes = cidDecoded.Hash.HashBytes() - } - - file := h.storage.NewFile(hashBytes) - - if !file.Exists() { - jc.ResponseWriter.WriteHeader(http.StatusNotFound) - return - } - - defer func(file io.ReadCloser) { - err := file.Close() - if err != nil { - h.logger.Error("error closing file", zap.Error(err)) - } - }(file) - - if isProof { - proof, err := file.Proof() - - if jc.Check("error getting proof", err) != nil { - return - } - - jc.ResponseWriter.Header().Set("Content-Type", "application/octet-stream") - http.ServeContent(jc.ResponseWriter, jc.Request, fmt.Sprintf("%.obao", file.Name()), file.Modtime(), bytes.NewReader(proof)) - return - } - - jc.ResponseWriter.Header().Set("Content-Type", file.Mime()) - - http.ServeContent(jc.ResponseWriter, jc.Request, file.Name(), file.Modtime(), file) -} - -func (h *HttpHandler) sendErrorResponse(jc jape.Context, err error) { - var statusCode int - - switch e := err.(type) { - case *S5Error: - statusCode = e.HttpStatus() - case *account.AccountError: - mappedCode, ok := account.ErrorCodeToHttpStatus[e.Key] - if !ok { - statusCode = http.StatusInternalServerError - } else { - statusCode = mappedCode - } - default: - statusCode = http.StatusInternalServerError - err = errors.New("An internal server error occurred.") - } - - _ = jc.Error(err, statusCode) -} - -func (h *HttpHandler) newStorageLocationProvider(hash *encoding.Multihash, types ...types.StorageLocationType) libs5storage.StorageLocationProvider { - return libs5storageProvider.NewStorageLocationProvider(libs5storageProvider.StorageLocationProviderParams{ - Services: h.getNode().Services(), - Hash: hash, - LocationTypes: types, - ServiceParams: libs5service.ServiceParams{ - Logger: h.logger, - Config: h.getNode().Config(), - Db: h.getNode().Db(), - }, - }) -} - -func setAuthCookie(jwt string, jc jape.Context) { - authCookie := http.Cookie{ - Name: "s5-auth-token", - Value: jwt, - Path: "/", - HttpOnly: true, - MaxAge: int(time.Hour.Seconds() * 24), - Secure: true, - } - - http.SetCookie(jc.ResponseWriter, &authCookie) -} diff --git a/api/s5/s5.go b/api/s5/s5.go index 78dd0dd..d027c77 100644 --- a/api/s5/s5.go +++ b/api/s5/s5.go @@ -1,14 +1,40 @@ package s5 import ( + "bytes" "context" "crypto/ed25519" + "crypto/rand" "embed" _ "embed" + "encoding/base64" + "encoding/hex" + "errors" "fmt" + "io" "io/fs" + "math" + "mime/multipart" "net/http" "net/url" + "strconv" + "strings" + "time" + + "git.lumeweb.com/LumeWeb/libs5-go/encoding" + "git.lumeweb.com/LumeWeb/libs5-go/metadata" + "git.lumeweb.com/LumeWeb/libs5-go/node" + "git.lumeweb.com/LumeWeb/libs5-go/protocol" + "git.lumeweb.com/LumeWeb/libs5-go/service" + storage2 "git.lumeweb.com/LumeWeb/libs5-go/storage" + "git.lumeweb.com/LumeWeb/libs5-go/storage/provider" + "git.lumeweb.com/LumeWeb/libs5-go/types" + "git.lumeweb.com/LumeWeb/portal/db/models" + "github.com/samber/lo" + "github.com/vmihailenco/msgpack/v5" + "go.uber.org/zap" + "gorm.io/gorm" + "nhooyr.io/websocket" "github.com/julienschmidt/httprouter" @@ -42,19 +68,22 @@ type S5API struct { identity ed25519.PrivateKey accounts *account.AccountServiceDefault storage *storage.StorageServiceDefault + db *gorm.DB protocols []protoRegistry.Protocol httpHandler HttpHandler protocol *s5.S5Protocol + logger *zap.Logger } type APIParams struct { fx.In - Config *viper.Viper - Identity ed25519.PrivateKey - Accounts *account.AccountServiceDefault - Storage *storage.StorageServiceDefault - Protocols []protoRegistry.Protocol `group:"protocol"` - HttpHandler HttpHandler + Config *viper.Viper + Identity ed25519.PrivateKey + Accounts *account.AccountServiceDefault + Storage *storage.StorageServiceDefault + Db *gorm.DB + Protocols []protoRegistry.Protocol `group:"protocol"` + Logger *zap.Logger } type S5ApiResult struct { @@ -65,12 +94,13 @@ type S5ApiResult struct { func NewS5(params APIParams) (S5ApiResult, error) { api := &S5API{ - config: params.Config, - identity: params.Identity, - accounts: params.Accounts, - storage: params.Storage, - protocols: params.Protocols, - httpHandler: params.HttpHandler, + config: params.Config, + identity: params.Identity, + accounts: params.Accounts, + storage: params.Storage, + db: params.Db, + protocols: params.Protocols, + logger: params.Logger, } return S5ApiResult{ API: api, @@ -80,7 +110,6 @@ func NewS5(params APIParams) (S5ApiResult, error) { var Module = fx.Module("s5_api", fx.Provide(NewS5), - fx.Provide(NewHttpHandler), ) func (s *S5API) Init() error { @@ -159,17 +188,17 @@ func (s *S5API) Routes() *httprouter.Router { routes := map[string]jape.Handler{ // Account API - "GET /s5/account/register": s.httpHandler.accountRegisterChallenge, - "POST /s5/account/register": s.httpHandler.accountRegister, - "GET /s5/account/login": s.httpHandler.accountLoginChallenge, - "POST /s5/account/login": s.httpHandler.accountLogin, - "GET /s5/account": middleware.ApplyMiddlewares(s.httpHandler.accountInfo, authMw), - "GET /s5/account/stats": middleware.ApplyMiddlewares(s.httpHandler.accountStats, authMw), - "GET /s5/account/pins.bin": middleware.ApplyMiddlewares(s.httpHandler.accountPins, authMw), + "GET /s5/account/register": s.accountRegisterChallenge, + "POST /s5/account/register": s.accountRegister, + "GET /s5/account/login": s.accountLoginChallenge, + "POST /s5/account/login": s.accountLogin, + "GET /s5/account": middleware.ApplyMiddlewares(s.accountInfo, authMw), + "GET /s5/account/stats": middleware.ApplyMiddlewares(s.accountStats, authMw), + "GET /s5/account/pins.bin": middleware.ApplyMiddlewares(s.accountPins, authMw), // Upload API - "POST /s5/upload": middleware.ApplyMiddlewares(s.httpHandler.smallFileUpload, authMw), - "POST /s5/upload/directory": middleware.ApplyMiddlewares(s.httpHandler.directoryUpload, authMw), + "POST /s5/upload": middleware.ApplyMiddlewares(s.smallFileUpload, authMw), + "POST /s5/upload/directory": middleware.ApplyMiddlewares(s.directoryUpload, authMw), // Tus API "POST /s5/upload/tus": tusHandler, @@ -180,22 +209,22 @@ func (s *S5API) Routes() *httprouter.Router { "OPTIONS /s5/upload/tus/:id": wrappedTusHandler, // Download API - "GET /s5/blob/:cid": middleware.ApplyMiddlewares(s.httpHandler.downloadBlob, authMw), - "GET /s5/metadata/:cid": s.httpHandler.downloadMetadata, - "GET /s5/download/:cid": middleware.ApplyMiddlewares(s.httpHandler.downloadFile, cors.Default().Handler), + "GET /s5/blob/:cid": middleware.ApplyMiddlewares(s.downloadBlob, authMw), + "GET /s5/metadata/:cid": s.downloadMetadata, + "GET /s5/download/:cid": middleware.ApplyMiddlewares(s.downloadFile, cors.Default().Handler), // Pins API - "POST /s5/pin/:cid": middleware.ApplyMiddlewares(s.httpHandler.accountPin, authMw), - "DELETE /s5/delete/:cid": middleware.ApplyMiddlewares(s.httpHandler.accountPinDelete, authMw), + "POST /s5/pin/:cid": middleware.ApplyMiddlewares(s.accountPin, authMw), + "DELETE /s5/delete/:cid": middleware.ApplyMiddlewares(s.accountPinDelete, authMw), // Debug API - "GET /s5/debug/download_urls/:cid": middleware.ApplyMiddlewares(s.httpHandler.debugDownloadUrls, authMw), - "GET /s5/debug/storage_locations/:hash": middleware.ApplyMiddlewares(s.httpHandler.debugStorageLocations, authMw), + "GET /s5/debug/download_urls/:cid": middleware.ApplyMiddlewares(s.debugDownloadUrls, authMw), + "GET /s5/debug/storage_locations/:hash": middleware.ApplyMiddlewares(s.debugStorageLocations, authMw), // Registry API - "GET /s5/registry": middleware.ApplyMiddlewares(s.httpHandler.registryQuery, authMw), - "POST /s5/registry": middleware.ApplyMiddlewares(s.httpHandler.registrySet, authMw), - "GET /s5/registry/subscription": middleware.ApplyMiddlewares(s.httpHandler.registrySubscription, authMw), + "GET /s5/registry": middleware.ApplyMiddlewares(s.registryQuery, authMw), + "POST /s5/registry": middleware.ApplyMiddlewares(s.registrySet, authMw), + "GET /s5/registry/subscription": middleware.ApplyMiddlewares(s.registrySubscription, authMw), "GET /swagger.json": byteHandler(jsonDoc), "GET /swagger": swaggerRedirect, @@ -296,3 +325,1103 @@ func BuildS5TusApi(authMw middleware.HttpMiddlewareFunc, storage *storage.Storag return tusHandler } + +type readSeekNopCloser struct { + *bytes.Reader +} + +func (rsnc readSeekNopCloser) Close() error { + return nil +} + +type HttpHandler struct { + config *viper.Viper + logger *zap.Logger + storage *storage.StorageServiceDefault + db *gorm.DB + accounts *account.AccountServiceDefault + protocol *s5.S5Protocol +} + +type HttpHandlerParams struct { + fx.In + + Config *viper.Viper + Logger *zap.Logger + Storage *storage.StorageServiceDefault + Db *gorm.DB + Accounts *account.AccountServiceDefault + Protocol *s5.S5Protocol +} + +type HttpHandlerResult struct { + fx.Out + + HttpHandler HttpHandler +} + +func NewHttpHandler(params HttpHandlerParams) (HttpHandlerResult, error) { + return HttpHandlerResult{ + HttpHandler: HttpHandler{ + config: params.Config, + logger: params.Logger, + storage: params.Storage, + db: params.Db, + accounts: params.Accounts, + protocol: params.Protocol, + }, + }, nil +} + +func (s *S5API) smallFileUpload(jc jape.Context) { + user := middleware.GetUserFromContext(jc.Request.Context()) + + file, err := s.prepareFileUpload(jc) + if err != nil { + s.sendErrorResponse(jc, err) + return + } + defer func(file io.ReadSeekCloser) { + err := file.Close() + if err != nil { + s.logger.Error("Error closing file", zap.Error(err)) + } + }(file) + + // Use PutFileSmall for the actual file upload + newUpload, err2 := s.storage.PutFileSmall(file, "s5", user, jc.Request.RemoteAddr) + if err2 != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyFileUploadFailed, err2)) + return + } + + cid, err2 := encoding.CIDFromHash(newUpload.Hash, newUpload.Size, types.CIDTypeRaw, types.HashTypeBlake3) + if err2 != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyFileUploadFailed, err2)) + return + } + + cidStr, err2 := cid.ToString() + if err2 != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyFileUploadFailed, err2)) + return + } + + jc.Encode(&SmallUploadResponse{ + CID: cidStr, + }) +} + +func (s *S5API) prepareFileUpload(jc jape.Context) (file io.ReadSeekCloser, s5Err *S5Error) { + r := jc.Request + contentType := r.Header.Get("Content-Type") + + // Handle multipart form data uploads + if strings.HasPrefix(contentType, "multipart/form-data") { + if err := r.ParseMultipartForm(s.config.GetInt64("core.post-upload-limit")); err != nil { + return nil, NewS5Error(ErrKeyFileUploadFailed, err) + } + + multipartFile, _, err := r.FormFile("file") + if err != nil { + return nil, NewS5Error(ErrKeyFileUploadFailed, err) + } + + return multipartFile, nil + } + + // Handle raw body uploads + data, err := io.ReadAll(r.Body) + if err != nil { + return nil, NewS5Error(ErrKeyFileUploadFailed, err) + } + + buffer := readSeekNopCloser{bytes.NewReader(data)} + + return buffer, nil +} + +func (s *S5API) accountRegisterChallenge(jc jape.Context) { + var pubkey string + if jc.DecodeForm("pubKey", &pubkey) != nil { + return + } + + challenge := make([]byte, 32) + _, err := rand.Read(challenge) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err)) + return + } + + decodedKey, err := base64.RawURLEncoding.DecodeString(pubkey) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) + return + } + + if len(decodedKey) != 33 || int(decodedKey[0]) != int(types.HashTypeEd25519) { + s.sendErrorResponse(jc, NewS5Error(ErrKeyDataIntegrityError, fmt.Errorf("invalid public key format"))) + return + } + + result := s.db.Create(&models.S5Challenge{ + Pubkey: pubkey, + Challenge: base64.RawURLEncoding.EncodeToString(challenge), + Type: "register", + }) + + if result.Error != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) + return + } + + jc.Encode(&AccountRegisterChallengeResponse{ + Challenge: base64.RawURLEncoding.EncodeToString(challenge), + }) +} + +func (s *S5API) accountRegister(jc jape.Context) { + var request AccountRegisterRequest + if jc.Decode(&request) != nil { + return + } + + decodedKey, err := base64.RawURLEncoding.DecodeString(request.Pubkey) + if err != nil || len(decodedKey) != 33 || int(decodedKey[0]) != int(types.HashTypeEd25519) { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) + return + } + + challenge := models.S5Challenge{ + Pubkey: request.Pubkey, + Type: "register", + } + + if result := s.db.Where(&challenge).First(&challenge); result.RowsAffected == 0 || result.Error != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyResourceNotFound, result.Error)) + return + } + + decodedResponse, err := base64.RawURLEncoding.DecodeString(request.Response) + if err != nil || len(decodedResponse) != 65 { + s.sendErrorResponse(jc, NewS5Error(ErrKeyDataIntegrityError, err)) + return + } + + decodedChallenge, err := base64.RawURLEncoding.DecodeString(challenge.Challenge) + if err != nil || !bytes.Equal(decodedResponse[1:33], decodedChallenge) { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) + return + } + + decodedSignature, err := base64.RawURLEncoding.DecodeString(request.Signature) + if err != nil || !ed25519.Verify(decodedKey[1:], decodedResponse, decodedSignature) { + s.sendErrorResponse(jc, NewS5Error(ErrKeyAuthorizationFailed, err)) + return + } + + if request.Email == "" { + request.Email = fmt.Sprintf("%s@%s", hex.EncodeToString(decodedKey[1:]), "example.com") + } + + if accountExists, _, _ := s.accounts.EmailExists(request.Email); accountExists { + s.sendErrorResponse(jc, NewS5Error(ErrKeyResourceLimitExceeded, fmt.Errorf("email already exists"))) + return + } + + if pubkeyExists, _, _ := s.accounts.PubkeyExists(hex.EncodeToString(decodedKey[1:])); pubkeyExists { + s.sendErrorResponse(jc, NewS5Error(ErrKeyResourceLimitExceeded, fmt.Errorf("pubkey already exists"))) + return + } + + passwd := make([]byte, 32) + if _, err = rand.Read(passwd); err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err)) + return + } + + newAccount, err := s.accounts.CreateAccount(request.Email, string(passwd)) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) + return + } + + rawPubkey := hex.EncodeToString(decodedKey[1:]) + if err = s.accounts.AddPubkeyToAccount(*newAccount, rawPubkey); err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) + return + } + + jwt, err := s.accounts.LoginPubkey(rawPubkey) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyAuthenticationFailed, err)) + return + } + + if result := s.db.Delete(&challenge); result.Error != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) + return + } + + setAuthCookie(jwt, jc) +} + +func (s *S5API) accountLoginChallenge(jc jape.Context) { + var pubkey string + if jc.DecodeForm("pubKey", &pubkey) != nil { + return + } + + challenge := make([]byte, 32) + _, err := rand.Read(challenge) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err)) + return + } + + decodedKey, err := base64.RawURLEncoding.DecodeString(pubkey) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) + return + } + + if len(decodedKey) != 33 || int(decodedKey[0]) != int(types.HashTypeEd25519) { + s.sendErrorResponse(jc, NewS5Error(ErrKeyUnsupportedFileType, fmt.Errorf("public key not supported"))) + return + } + + pubkeyExists, _, _ := s.accounts.PubkeyExists(hex.EncodeToString(decodedKey[1:])) + if !pubkeyExists { + s.sendErrorResponse(jc, NewS5Error(ErrKeyResourceNotFound, fmt.Errorf("public key does not exist"))) + return + } + + result := s.db.Create(&models.S5Challenge{ + Pubkey: pubkey, + Challenge: base64.RawURLEncoding.EncodeToString(challenge), + Type: "login", + }) + + if result.Error != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) + return + } + + jc.Encode(&AccountLoginChallengeResponse{ + Challenge: base64.RawURLEncoding.EncodeToString(challenge), + }) +} + +func (s *S5API) accountLogin(jc jape.Context) { + var request AccountLoginRequest + if jc.Decode(&request) != nil { + return + } + + decodedKey, err := base64.RawURLEncoding.DecodeString(request.Pubkey) + if err != nil || len(decodedKey) != 32 { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) + return + } + + if int(decodedKey[0]) != int(types.HashTypeEd25519) { + s.sendErrorResponse(jc, NewS5Error(ErrKeyUnsupportedFileType, fmt.Errorf("public key type not supported"))) + return + } + + var challenge models.S5Challenge + result := s.db.Where(&models.S5Challenge{Pubkey: request.Pubkey, Type: "login"}).First(&challenge) + if result.RowsAffected == 0 || result.Error != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyResourceNotFound, result.Error)) + return + } + + decodedResponse, err := base64.RawURLEncoding.DecodeString(request.Response) + if err != nil || len(decodedResponse) != 65 { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) + return + } + + decodedChallenge, err := base64.RawURLEncoding.DecodeString(challenge.Challenge) + if err != nil || !bytes.Equal(decodedResponse[1:33], decodedChallenge) { + s.sendErrorResponse(jc, NewS5Error(ErrKeyDataIntegrityError, err)) + return + } + + decodedSignature, err := base64.RawURLEncoding.DecodeString(request.Signature) + if err != nil || !ed25519.Verify(decodedKey[1:], decodedResponse, decodedSignature) { + s.sendErrorResponse(jc, NewS5Error(ErrKeyAuthorizationFailed, err)) + return + } + + jwt, err := s.accounts.LoginPubkey(hex.EncodeToString(decodedKey[1:])) // Adjust based on how LoginPubkey is implemented + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyAuthenticationFailed, err)) + return + } + + if result := s.db.Delete(&challenge); result.Error != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) + return + } + + setAuthCookie(jwt, jc) +} + +func (s *S5API) accountInfo(jc jape.Context) { + userID := middleware.GetUserFromContext(jc.Request.Context()) + _, user, _ := s.accounts.AccountExists(userID) + + info := &AccountInfoResponse{ + Email: user.Email, + QuotaExceeded: false, + EmailConfirmed: false, + IsRestricted: false, + Tier: AccountTier{ + Id: 1, + Name: "default", + UploadBandwidth: math.MaxUint32, + StorageLimit: math.MaxUint32, + Scopes: []interface{}{}, + }, + } + + jc.Encode(info) +} + +func (s *S5API) accountStats(jc jape.Context) { + userID := middleware.GetUserFromContext(jc.Request.Context()) + _, user, _ := s.accounts.AccountExists(userID) + + info := &AccountStatsResponse{ + AccountInfoResponse: AccountInfoResponse{ + Email: user.Email, + QuotaExceeded: false, + EmailConfirmed: false, + IsRestricted: false, + Tier: AccountTier{ + Id: 1, + Name: "default", + UploadBandwidth: math.MaxUint32, + StorageLimit: math.MaxUint32, + Scopes: []interface{}{}, + }, + }, + Stats: AccountStats{ + Total: AccountStatsTotal{ + UsedStorage: 0, + }, + }, + } + + jc.Encode(info) +} + +func (s *S5API) accountPins(jc jape.Context) { + var cursor uint64 + if err := jc.DecodeForm("cursor", &cursor); err != nil { + // Assuming jc.DecodeForm sends out its own error, so no need for further action here + return + } + + userID := middleware.GetUserFromContext(jc.Request.Context()) + + pins, err := s.accounts.AccountPins(userID, cursor) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) + return + } + + pinResponse := &AccountPinResponse{Cursor: cursor, Pins: pins} + result, err2 := msgpack.Marshal(pinResponse) + if err2 != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err2)) + return + } + + jc.ResponseWriter.Header().Set("Content-Type", "application/msgpack") + jc.ResponseWriter.WriteHeader(http.StatusOK) + if _, err := jc.ResponseWriter.Write(result); err != nil { + s.logger.Error("failed to write account pins response", zap.Error(err)) + } +} + +func (s *S5API) accountPinDelete(jc jape.Context) { + var cid string + if err := jc.DecodeParam("cid", &cid); err != nil { + return + } + + user := middleware.GetUserFromContext(jc.Request.Context()) + + decodedCid, err := encoding.CIDFromString(cid) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) + return + } + + hash := hex.EncodeToString(decodedCid.Hash.HashBytes()) + if err := s.accounts.DeletePinByHash(hash, user); err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) + return + } + + jc.ResponseWriter.WriteHeader(http.StatusNoContent) +} + +func (s *S5API) accountPin(jc jape.Context) { + var cid string + if err := jc.DecodeParam("cid", &cid); err != nil { + return + } + + userID := middleware.GetUserFromContext(jc.Request.Context()) + + decodedCid, err := encoding.CIDFromString(cid) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) + return + } + + hash := hex.EncodeToString(decodedCid.Hash.HashBytes()) + s.logger.Info("Processing pin request", zap.String("cid", cid), zap.String("hash", hash)) + + if err := s.accounts.PinByHash(hash, userID); err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) + return + } + + jc.ResponseWriter.WriteHeader(http.StatusNoContent) +} + +func (s *S5API) directoryUpload(jc jape.Context) { + // Decode form fields + var ( + tryFiles []string + errorPages map[int]string + name string + ) + + if err := jc.DecodeForm("tryFiles", &tryFiles); err != nil || jc.DecodeForm("errorPages", &errorPages) != nil || jc.DecodeForm("name", &name) != nil { + } + + // Verify content type + if contentType := jc.Request.Header.Get("Content-Type"); !strings.HasPrefix(contentType, "multipart/form-data") { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, fmt.Errorf("expected multipart/form-data content type, got %s", contentType))) + return + } + + // Parse multipart form with size limit from config + if err := jc.Request.ParseMultipartForm(s.config.GetInt64("core.post-upload-limit")); err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) + return + } + + user := middleware.GetUserFromContext(jc.Request.Context()) + uploads, err := s.processMultipartFiles(jc.Request, user) + if err != nil { + s.sendErrorResponse(jc, err) // processMultipartFiles should return a properly wrapped S5Error + return + } + + // Generate metadata for the directory upload + app, err := s.createAppMetadata(name, tryFiles, errorPages, uploads) + if err != nil { + s.sendErrorResponse(jc, err) // createAppMetadata should return a properly wrapped S5Error + return + } + + // Upload the metadata + cidStr, err := s.uploadAppMetadata(app, user, jc.Request) + if err != nil { + s.sendErrorResponse(jc, err) // uploadAppMetadata should return a properly wrapped S5Error + return + } + + jc.Encode(&AppUploadResponse{CID: cidStr}) +} + +func (s *S5API) processMultipartFiles(r *http.Request, user uint) (map[string]*models.Upload, error) { + uploadMap := make(map[string]*models.Upload) + + for _, files := range r.MultipartForm.File { + for _, fileHeader := range files { + file, err := fileHeader.Open() + if err != nil { + return nil, NewS5Error(ErrKeyStorageOperationFailed, err) + } + defer func(file multipart.File) { + err := file.Close() + if err != nil { + s.logger.Error("Error closing file", zap.Error(err)) + } + }(file) + + upload, err := s.storage.PutFileSmall(file, "s5", user, r.RemoteAddr) + if err != nil { + return nil, NewS5Error(ErrKeyStorageOperationFailed, err) + } + + uploadMap[fileHeader.Filename] = upload + } + } + + return uploadMap, nil +} + +func (s *S5API) createAppMetadata(name string, tryFiles []string, errorPages map[int]string, uploads map[string]*models.Upload) (*metadata.WebAppMetadata, error) { + filesMap := make(map[string]metadata.WebAppMetadataFileReference, len(uploads)) + + for filename, upload := range uploads { + hashDecoded, err := hex.DecodeString(upload.Hash) + if err != nil { + return nil, NewS5Error(ErrKeyInternalError, err, "Failed to decode hash for file: "+filename) + } + + cid, err := encoding.CIDFromHash(hashDecoded, upload.Size, types.CIDTypeRaw, types.HashTypeBlake3) + if err != nil { + return nil, NewS5Error(ErrKeyInternalError, err, "Failed to create CID for file: "+filename) + } + filesMap[filename] = metadata.WebAppMetadataFileReference{ + Cid: cid, + ContentType: upload.MimeType, + } + } + + extraMetadataMap := make(map[int]interface{}) + for statusCode, page := range errorPages { + extraMetadataMap[statusCode] = page + } + + extraMetadata := metadata.NewExtraMetadata(extraMetadataMap) + // Create the web app metadata object + app := metadata.NewWebAppMetadata( + name, + tryFiles, + *extraMetadata, + errorPages, + filesMap, + ) + + return app, nil +} + +func (s *S5API) uploadAppMetadata(appData *metadata.WebAppMetadata, userId uint, r *http.Request) (string, error) { + appDataRaw, err := msgpack.Marshal(appData) + if err != nil { + return "", NewS5Error(ErrKeyInternalError, err, "Failed to marshal app metadata") + } + + file := bytes.NewReader(appDataRaw) + + upload, err := s.storage.PutFileSmall(file, "s5", userId, r.RemoteAddr) + if err != nil { + return "", NewS5Error(ErrKeyStorageOperationFailed, err) + } + + // Construct the CID for the newly uploaded metadata + cid, err := encoding.CIDFromHash(upload.Hash, uint64(len(appDataRaw)), types.CIDTypeMetadataWebapp, types.HashTypeBlake3) + if err != nil { + return "", NewS5Error(ErrKeyInternalError, err, "Failed to create CID for new app metadata") + } + cidStr, err := cid.ToString() + if err != nil { + return "", NewS5Error(ErrKeyInternalError, err, "Failed to convert CID to string for new app metadata") + } + + return cidStr, nil +} + +func (s *S5API) debugDownloadUrls(jc jape.Context) { + var cid string + if err := jc.DecodeParam("cid", &cid); err != nil { + return + } + + decodedCid, err := encoding.CIDFromString(cid) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err, "Failed to decode CID")) + return + } + + node := s.getNode() + dlUriProvider := s.newStorageLocationProvider(&decodedCid.Hash, types.StorageLocationTypeFull, types.StorageLocationTypeFile, types.StorageLocationTypeBridge) + + if err := dlUriProvider.Start(); err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err, "Failed to start URI provider")) + return + } + + locations, err := node.Services().Storage().GetCachedStorageLocations(&decodedCid.Hash, []types.StorageLocationType{ + types.StorageLocationTypeFull, types.StorageLocationTypeFile, types.StorageLocationTypeBridge, + }) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err, "Failed to get cached storage locations")) + return + } + + availableNodes := lo.Keys[string, storage2.StorageLocation](locations) + availableNodesIds := make([]*encoding.NodeId, len(availableNodes)) + + for i, nodeIdStr := range availableNodes { + nodeId, err := encoding.DecodeNodeId(nodeIdStr) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err, "Failed to decode node ID")) + return + } + availableNodesIds[i] = nodeId + } + + sorted, err := node.Services().P2P().SortNodesByScore(availableNodesIds) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyNetworkError, err, "Failed to sort nodes by score")) + return + } + + output := make([]string, len(sorted)) + for i, nodeId := range sorted { + nodeIdStr, err := nodeId.ToString() + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err, "Failed to convert node ID to string")) + return + } + output[i] = locations[nodeIdStr].BytesURL() + } + + jc.ResponseWriter.WriteHeader(http.StatusOK) + _, err = jc.ResponseWriter.Write([]byte(strings.Join(output, "\n"))) + if err != nil { + s.logger.Error("Failed to write response", zap.Error(err)) + } +} + +func (s *S5API) registryQuery(jc jape.Context) { + var pk string + if err := jc.DecodeForm("pk", &pk); err != nil { + return + } + + pkBytes, err := base64.RawURLEncoding.DecodeString(pk) + if err != nil { + s5Err := NewS5Error(ErrKeyInvalidFileFormat, err) + s.sendErrorResponse(jc, s5Err) + return + } + + entry, err := s.getNode().Services().Registry().Get(pkBytes) + if err != nil { + s5ErrKey := ErrKeyStorageOperationFailed + s5Err := NewS5Error(s5ErrKey, err) + s.sendErrorResponse(jc, s5Err) + return + } + + if entry == nil { + jc.ResponseWriter.WriteHeader(http.StatusNotFound) + return + } + + response := RegistryQueryResponse{ + Pk: base64.RawURLEncoding.EncodeToString(entry.PK()), + Revision: entry.Revision(), + Data: base64.RawURLEncoding.EncodeToString(entry.Data()), + Signature: base64.RawURLEncoding.EncodeToString(entry.Signature()), + } + jc.Encode(response) +} + +func (s *S5API) registrySet(jc jape.Context) { + var request RegistrySetRequest + + if err := jc.Decode(&request); err != nil { + return + } + + pk, err := base64.RawURLEncoding.DecodeString(request.Pk) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding public key")) + return + } + + data, err := base64.RawURLEncoding.DecodeString(request.Data) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding data")) + return + } + + signature, err := base64.RawURLEncoding.DecodeString(request.Signature) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding signature")) + return + } + + entry := protocol.NewSignedRegistryEntry(pk, request.Revision, data, signature) + + err = s.getNode().Services().Registry().Set(entry, false, nil) + if err != nil { + s.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err, "Error setting registry entry")) + return + } +} +func (s *S5API) registrySubscription(jc jape.Context) { + // Create a context for the WebSocket operations + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var listeners []func() + + // Accept the WebSocket connection + c, err := websocket.Accept(jc.ResponseWriter, jc.Request, nil) + if err != nil { + s.logger.Error("error accepting websocket connection", zap.Error(err)) + return + } + defer func() { + // Close the WebSocket connection gracefully + err := c.Close(websocket.StatusNormalClosure, "connection closed") + if err != nil { + s.logger.Error("error closing websocket connection", zap.Error(err)) + } + // Clean up all listeners when the connection is closed + for _, listener := range listeners { + listener() + } + }() + + // Main loop for reading messages + for { + _, data, err := c.Read(ctx) + if err != nil { + if websocket.CloseStatus(err) == websocket.StatusNormalClosure { + // Normal closure + s.logger.Info("websocket connection closed normally") + } else { + // Handle different types of errors + s.logger.Error("error in websocket connection", zap.Error(err)) + } + break + } + + decoder := msgpack.NewDecoder(bytes.NewReader(data)) + + // Assuming method indicates the type of operation, validate it + method, err := decoder.DecodeInt() + if err != nil { + s.logger.Error("error decoding method", zap.Error(err)) + continue + } + + if method != 2 { + s.logger.Error("invalid method", zap.Int64("method", int64(method))) + continue + } + + sre, err := decoder.DecodeBytes() + if err != nil { + s.logger.Error("error decoding sre", zap.Error(err)) + continue + } + + // Listen for updates on the registry entry and send updates via WebSocket + off, err := s.getNode().Services().Registry().Listen(sre, func(entry protocol.SignedRegistryEntry) { + encoded, err := msgpack.Marshal(entry) + if err != nil { + s.logger.Error("error encoding entry", zap.Error(err)) + return + } + + // Write updates to the WebSocket connection + if err := c.Write(ctx, websocket.MessageBinary, encoded); err != nil { + s.logger.Error("error writing to websocket", zap.Error(err)) + } + }) + if err != nil { + s.logger.Error("error setting up listener for registry", zap.Error(err)) + break + } + + listeners = append(listeners, off) // Add the listener's cleanup function to the list + } +} + +func (s *S5API) getNode() *node.Node { + return s.protocol.Node() +} + +func (s *S5API) downloadBlob(jc jape.Context) { + var cid string + + if jc.DecodeParam("cid", &cid) != nil { + return + } + + cid = strings.Split(cid, ".")[0] + + cidDecoded, err := encoding.CIDFromString(cid) + if jc.Check("error decoding cid", err) != nil { + return + } + + dlUriProvider := s.newStorageLocationProvider(&cidDecoded.Hash, types.StorageLocationTypeFull, types.StorageLocationTypeFile, types.StorageLocationTypeBridge) + + err = dlUriProvider.Start() + + if jc.Check("error starting search", err) != nil { + return + } + + next, err := dlUriProvider.Next() + if jc.Check("error fetching blob", err) != nil { + return + } + + http.Redirect(jc.ResponseWriter, jc.Request, next.Location().BytesURL(), http.StatusFound) +} + +func (s *S5API) debugStorageLocations(jc jape.Context) { + var hash string + + if jc.DecodeParam("hash", &hash) != nil { + return + } + + var kinds string + + if jc.DecodeForm("kinds", &kinds) != nil { + return + } + + decodedHash, err := encoding.MultihashFromBase64Url(hash) + if jc.Check("error decoding hash", err) != nil { + return + } + + typeList := strings.Split(kinds, ",") + typeIntList := make([]types.StorageLocationType, 0) + + for _, typeStr := range typeList { + typeInt, err := strconv.Atoi(typeStr) + if err != nil { + continue + } + typeIntList = append(typeIntList, types.StorageLocationType(typeInt)) + } + + if len(typeIntList) == 0 { + typeIntList = []types.StorageLocationType{ + types.StorageLocationTypeFull, + types.StorageLocationTypeFile, + types.StorageLocationTypeBridge, + types.StorageLocationTypeArchive, + } + } + + dlUriProvider := s.newStorageLocationProvider(decodedHash, typeIntList...) + + err = dlUriProvider.Start() + if jc.Check("error starting search", err) != nil { + return + } + + _, err = dlUriProvider.Next() + if jc.Check("error fetching locations", err) != nil { + return + } + + locations, err := s.getNode().Services().Storage().GetCachedStorageLocations(decodedHash, typeIntList) + if jc.Check("error getting cached locations", err) != nil { + return + } + + availableNodes := lo.Keys[string, storage2.StorageLocation](locations) + availableNodesIds := make([]*encoding.NodeId, len(availableNodes)) + + for i, nodeIdStr := range availableNodes { + nodeId, err := encoding.DecodeNodeId(nodeIdStr) + if jc.Check("error decoding node id", err) != nil { + return + } + availableNodesIds[i] = nodeId + } + + availableNodesIds, err = s.getNode().Services().P2P().SortNodesByScore(availableNodesIds) + + if jc.Check("error sorting nodes", err) != nil { + return + } + + debugLocations := make([]DebugStorageLocation, len(availableNodes)) + + for i, nodeId := range availableNodesIds { + nodeIdStr, err := nodeId.ToBase58() + if jc.Check("error encoding node id", err) != nil { + return + } + + score, err := s.getNode().Services().P2P().GetNodeScore(nodeId) + + if jc.Check("error getting node score", err) != nil { + return + } + + debugLocations[i] = DebugStorageLocation{ + Type: locations[nodeIdStr].Type(), + Parts: locations[nodeIdStr].Parts(), + Expiry: locations[nodeIdStr].Expiry(), + NodeId: nodeIdStr, + Score: score, + } + } + + jc.Encode(&DebugStorageLocationsResponse{ + Locations: debugLocations, + }) +} + +func (s *S5API) downloadMetadata(jc jape.Context) { + var cid string + + if jc.DecodeParam("cid", &cid) != nil { + return + } + + cidDecoded, err := encoding.CIDFromString(cid) + if jc.Check("error decoding cid", err) != nil { + s.logger.Error("error decoding cid", zap.Error(err)) + return + } + + switch cidDecoded.Type { + case types.CIDTypeRaw: + _ = jc.Error(errors.New("Raw CIDs do not have metadata"), http.StatusBadRequest) + return + + case types.CIDTypeResolver: + _ = jc.Error(errors.New("Resolver CIDs not yet supported"), http.StatusBadRequest) + return + } + + meta, err := s.getNode().Services().Storage().GetMetadataByCID(cidDecoded) + + if jc.Check("error getting metadata", err) != nil { + s.logger.Error("error getting metadata", zap.Error(err)) + return + } + + if cidDecoded.Type != types.CIDTypeBridge { + jc.ResponseWriter.Header().Set("Cache-Control", "public, max-age=31536000") + } else { + jc.ResponseWriter.Header().Set("Cache-Control", "public, max-age=60") + } + + jc.Encode(&meta) + +} + +func (s *S5API) downloadFile(jc jape.Context) { + var cid string + + if jc.DecodeParam("cid", &cid) != nil { + return + } + + var hashBytes []byte + isProof := false + + if strings.HasSuffix(cid, ".obao") { + isProof = true + cid = strings.TrimSuffix(cid, ".obao") + } + + cidDecoded, err := encoding.CIDFromString(cid) + + if err != nil { + hashDecoded, err := encoding.MultihashFromBase64Url(cid) + + if jc.Check("error decoding as cid or hash", err) != nil { + return + } + + hashBytes = hashDecoded.HashBytes() + } else { + hashBytes = cidDecoded.Hash.HashBytes() + } + + file := s.storage.NewFile(hashBytes) + + if !file.Exists() { + jc.ResponseWriter.WriteHeader(http.StatusNotFound) + return + } + + defer func(file io.ReadCloser) { + err := file.Close() + if err != nil { + s.logger.Error("error closing file", zap.Error(err)) + } + }(file) + + if isProof { + proof, err := file.Proof() + + if jc.Check("error getting proof", err) != nil { + return + } + + jc.ResponseWriter.Header().Set("Content-Type", "application/octet-stream") + http.ServeContent(jc.ResponseWriter, jc.Request, fmt.Sprintf("%.obao", file.Name()), file.Modtime(), bytes.NewReader(proof)) + return + } + + jc.ResponseWriter.Header().Set("Content-Type", file.Mime()) + + http.ServeContent(jc.ResponseWriter, jc.Request, file.Name(), file.Modtime(), file) +} + +func (s *S5API) sendErrorResponse(jc jape.Context, err error) { + var statusCode int + + switch e := err.(type) { + case *S5Error: + statusCode = e.HttpStatus() + case *account.AccountError: + mappedCode, ok := account.ErrorCodeToHttpStatus[e.Key] + if !ok { + statusCode = http.StatusInternalServerError + } else { + statusCode = mappedCode + } + default: + statusCode = http.StatusInternalServerError + err = errors.New("An internal server error occurred.") + } + + _ = jc.Error(err, statusCode) +} + +func (s *S5API) newStorageLocationProvider(hash *encoding.Multihash, types ...types.StorageLocationType) storage2.StorageLocationProvider { + return provider.NewStorageLocationProvider(provider.StorageLocationProviderParams{ + Services: s.getNode().Services(), + Hash: hash, + LocationTypes: types, + ServiceParams: service.ServiceParams{ + Logger: s.logger, + Config: s.getNode().Config(), + Db: s.getNode().Db(), + }, + }) +} + +func setAuthCookie(jwt string, jc jape.Context) { + authCookie := http.Cookie{ + Name: "s5-auth-token", + Value: jwt, + Path: "/", + HttpOnly: true, + MaxAge: int(time.Hour.Seconds() * 24), + Secure: true, + } + + http.SetCookie(jc.ResponseWriter, &authCookie) +}