diff --git a/api/s5/errors.go b/api/s5/errors.go new file mode 100644 index 0000000..04643be --- /dev/null +++ b/api/s5/errors.go @@ -0,0 +1,122 @@ +package s5 + +import ( + "fmt" + "net/http" +) + +// S5-specific error keys +const ( + // File-related errors + ErrKeyFileUploadFailed = "ErrFileUploadFailed" + ErrKeyFileDownloadFailed = "ErrFileDownloadFailed" + ErrKeyMetadataFetchFailed = "ErrMetadataFetchFailed" + ErrKeyInvalidFileFormat = "ErrInvalidFileFormat" + ErrKeyUnsupportedFileType = "ErrUnsupportedFileType" + ErrKeyFileProcessingFailed = "ErrFileProcessingFailed" + + // Storage and data handling errors + ErrKeyStorageOperationFailed = "ErrStorageOperationFailed" + ErrKeyResourceNotFound = "ErrResourceNotFound" + ErrKeyResourceLimitExceeded = "ErrResourceLimitExceeded" + ErrKeyDataIntegrityError = "ErrDataIntegrityError" + + // User and permission errors + ErrKeyPermissionDenied = "ErrPermissionDenied" + ErrKeyInvalidOperation = "ErrInvalidOperation" + ErrKeyAuthenticationFailed = "ErrAuthenticationFailed" + ErrKeyAuthorizationFailed = "ErrAuthorizationFailed" + + // Network and communication errors + ErrKeyNetworkError = "ErrNetworkError" + ErrKeyServiceUnavailable = "ErrServiceUnavailable" + + // General errors + ErrKeyInternalError = "ErrInternalError" + ErrKeyConfigurationError = "ErrConfigurationError" + ErrKeyOperationTimeout = "ErrOperationTimeout" +) + +// Default error messages for S5-specific errors +var defaultErrorMessages = map[string]string{ + ErrKeyFileUploadFailed: "File upload failed due to an internal error.", + ErrKeyFileDownloadFailed: "File download failed.", + ErrKeyMetadataFetchFailed: "Failed to fetch metadata for the resource.", + ErrKeyInvalidFileFormat: "Invalid file format provided.", + ErrKeyUnsupportedFileType: "Unsupported file type.", + ErrKeyFileProcessingFailed: "Failed to process the file.", + ErrKeyStorageOperationFailed: "Storage operation failed unexpectedly.", + ErrKeyResourceNotFound: "The specified resource was not found.", + ErrKeyResourceLimitExceeded: "The operation exceeded the resource limit.", + ErrKeyDataIntegrityError: "Data integrity check failed.", + ErrKeyPermissionDenied: "Permission denied for the requested operation.", + ErrKeyInvalidOperation: "Invalid or unsupported operation requested.", + ErrKeyAuthenticationFailed: "Authentication failed.", + ErrKeyAuthorizationFailed: "Authorization failed or insufficient permissions.", + ErrKeyNetworkError: "Network error or connectivity issue.", + ErrKeyServiceUnavailable: "The requested service is temporarily unavailable.", + ErrKeyInternalError: "An internal server error occurred.", + ErrKeyConfigurationError: "Configuration error or misconfiguration detected.", + ErrKeyOperationTimeout: "The operation timed out.", +} + +// Mapping of S5-specific error keys to HTTP status codes +var errorCodeToHttpStatus = map[string]int{ + ErrKeyFileUploadFailed: http.StatusInternalServerError, + ErrKeyFileDownloadFailed: http.StatusInternalServerError, + ErrKeyMetadataFetchFailed: http.StatusInternalServerError, + ErrKeyInvalidFileFormat: http.StatusBadRequest, + ErrKeyUnsupportedFileType: http.StatusBadRequest, + ErrKeyFileProcessingFailed: http.StatusInternalServerError, + ErrKeyStorageOperationFailed: http.StatusInternalServerError, + ErrKeyResourceNotFound: http.StatusNotFound, + ErrKeyResourceLimitExceeded: http.StatusForbidden, + ErrKeyDataIntegrityError: http.StatusInternalServerError, + ErrKeyPermissionDenied: http.StatusForbidden, + ErrKeyInvalidOperation: http.StatusBadRequest, + ErrKeyAuthenticationFailed: http.StatusUnauthorized, + ErrKeyAuthorizationFailed: http.StatusUnauthorized, + ErrKeyNetworkError: http.StatusBadGateway, + ErrKeyServiceUnavailable: http.StatusServiceUnavailable, + ErrKeyInternalError: http.StatusInternalServerError, + ErrKeyConfigurationError: http.StatusInternalServerError, + ErrKeyOperationTimeout: http.StatusRequestTimeout, +} + +// S5Error struct for representing S5-specific errors +type S5Error struct { + Key string + Message string + Err error +} + +// Error method to implement the error interface +func (e *S5Error) Error() string { + if e.Err != nil { + return fmt.Sprintf("%s: %v", e.Message, e.Err) + } + return e.Message +} + +func (e *S5Error) HttpStatus() int { + if code, exists := errorCodeToHttpStatus[e.Key]; exists { + return code + } + return http.StatusInternalServerError +} + +func NewS5Error(key string, err error, customMessage ...string) *S5Error { + message, exists := defaultErrorMessages[key] + if !exists { + message = "An unknown error occurred" + } + if len(customMessage) > 0 { + message = customMessage[0] + } + + return &S5Error{ + Key: key, + Message: message, + Err: err, + } +} diff --git a/api/s5/http.go b/api/s5/http.go index ab3ecea..f86dd09 100644 --- a/api/s5/http.go +++ b/api/s5/http.go @@ -9,8 +9,17 @@ import ( "encoding/hex" "errors" "fmt" - "git.lumeweb.com/LumeWeb/libs5-go/encoding" + "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" @@ -29,53 +38,16 @@ import ( "go.uber.org/fx" "go.uber.org/zap" "gorm.io/gorm" - "io" - "math" - "mime/multipart" - "net/http" "nhooyr.io/websocket" - "strconv" - "strings" - "time" ) -const ( - errMultiformParse = "Error parsing multipart form" - errRetrievingFile = "Error retrieving the file" - errReadFile = "Error reading the file" - errClosingStream = "Error closing the stream" - errUploadingFile = "Error uploading the file" - errAccountGenerateChallenge = "Error generating challenge" - errAccountRegister = "Error registering account" - errAccountLogin = "Error logging in account" - errFailedToGetPins = "Failed to get pins" - errFailedToDelPin = "Failed to delete pin" - errFailedToAddPin = "Failed to add pin" - errorNotMultiform = "Not a multipart form" - errFetchingUrls = "Error fetching urls" - errDownloadingFile = "Error downloading file" -) +type readSeekNopCloser struct { + *bytes.Reader +} -var ( - errUploadingFileErr = errors.New(errUploadingFile) - errAccountGenerateChallengeErr = errors.New(errAccountGenerateChallenge) - errAccountRegisterErr = errors.New(errAccountRegister) - errInvalidChallengeErr = errors.New("Invalid challenge") - errInvalidSignatureErr = errors.New("Invalid signature") - errPubkeyNotSupported = errors.New("Only ed25519 keys are supported") - errInvalidEmail = errors.New("Invalid email") - errEmailAlreadyExists = errors.New("Email already exists") - errGeneratingPassword = errors.New("Error generating password") - errPubkeyAlreadyExists = errors.New("Pubkey already exists") - errPubkeyNotExist = errors.New("Pubkey does not exist") - errAccountLoginErr = errors.New(errAccountLogin) - errFailedToGetPinsErr = errors.New(errFailedToGetPins) - errFailedToDelPinErr = errors.New(errFailedToDelPin) - errFailedToAddPinErr = errors.New(errFailedToAddPin) - errNotMultiformErr = errors.New(errorNotMultiform) - errFetchingUrlsErr = errors.New(errFetchingUrls) - errDownloadingFileErr = errors.New(errDownloadingFile) -) +func (rsnc readSeekNopCloser) Close() error { + return nil +} type HttpHandler struct { config *viper.Viper @@ -117,154 +89,73 @@ func NewHttpHandler(params HttpHandlerParams) (HttpHandlerResult, error) { } func (h *HttpHandler) smallFileUpload(jc jape.Context) { - var rs io.ReadSeeker - var bufferSize int64 - - r := jc.Request - contentType := r.Header.Get("Content-Type") user := middleware.GetUserFromContext(jc.Request.Context()) - if strings.HasPrefix(contentType, "multipart/form-data") { - // Parse the multipart form - err := r.ParseMultipartForm(h.config.GetInt64("core.post-upload-limit")) - - if jc.Check(errMultiformParse, err) != nil { - h.logger.Error(errMultiformParse, zap.Error(err)) - return - } - - // Retrieve the file from the form data - file, _, err := r.FormFile("file") - if jc.Check(errRetrievingFile, err) != nil { - h.logger.Error(errRetrievingFile, zap.Error(err)) - return - } - defer func(file multipart.File) { - err := file.Close() - if err != nil { - h.logger.Error(errClosingStream, zap.Error(err)) - } - }(file) - - rs = file - } else { - data, err := io.ReadAll(r.Body) - if jc.Check(errReadFile, err) != nil { - h.logger.Error(errReadFile, zap.Error(err)) - return - } - - buffer := bytes.NewReader(data) - bufferSize = int64(buffer.Len()) - rs = buffer - - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - h.logger.Error(errClosingStream, zap.Error(err)) - } - }(r.Body) - } - - hash, err := h.storage.GetHashSmall(rs) - _, err = rs.Seek(0, io.SeekStart) + file, err := h.prepareFileUpload(jc) if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) + h.sendErrorResponse(jc, err) return } - - if exists, upload := h.storage.FileExists(hash.Hash); exists { - cid, err := encoding.CIDFromHash(hash, upload.Size, types.CIDTypeRaw, types.HashTypeBlake3) + defer func(file io.ReadSeekCloser) { + err := file.Close() if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) - return - } - cidStr, err := cid.ToString() - if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) - return + h.logger.Error("Error closing file", zap.Error(err)) } + }(file) - err = h.accounts.PinByID(upload.ID, user) - if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) - return - } - - jc.Encode(&SmallUploadResponse{ - CID: cidStr, - }) + // 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 } - hash, err = h.storage.PutFileSmall(rs, "s5") - - if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) + cid, err2 := encoding.CIDFromHash(newUpload.Hash, newUpload.Size, types.CIDTypeRaw, types.HashTypeBlake3) + if err2 != nil { + h.sendErrorResponse(jc, NewS5Error(ErrKeyFileUploadFailed, err2)) return } - h.logger.Info("Hash", zap.String("hash", hex.EncodeToString(hash.Hash))) - - cid, err := encoding.CIDFromHash(hash, uint64(bufferSize), types.CIDTypeRaw, types.HashTypeBlake3) - - if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) + cidStr, err2 := cid.ToString() + if err2 != nil { + h.sendErrorResponse(jc, NewS5Error(ErrKeyFileUploadFailed, err2)) return } - cidStr, err := cid.ToString() - - if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) - return - } - - h.logger.Info("CID", zap.String("cidStr", cidStr)) - - _, err = rs.Seek(0, io.SeekStart) - if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) - return - - } - - var mimeBytes [512]byte - - _, err = rs.Read(mimeBytes[:]) - if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) - return - } - - mimeType := http.DetectContentType(mimeBytes[:]) - - upload, err := h.storage.CreateUpload(hash.Hash, mimeType, user, jc.Request.RemoteAddr, uint64(bufferSize), "s5") - if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) - } - - err = h.accounts.PinByID(upload.ID, user) - if err != nil { - _ = jc.Error(errUploadingFileErr, http.StatusInternalServerError) - h.logger.Error(errUploadingFile, zap.Error(err)) - } - 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 { @@ -272,25 +163,20 @@ func (h *HttpHandler) accountRegisterChallenge(jc jape.Context) { } challenge := make([]byte, 32) - _, err := rand.Read(challenge) if err != nil { - _ = jc.Error(errAccountGenerateChallengeErr, http.StatusInternalServerError) - h.logger.Error(errAccountGenerateChallenge, zap.Error(err)) + h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err)) return } decodedKey, err := base64.RawURLEncoding.DecodeString(pubkey) - if err != nil { - _ = jc.Error(errAccountGenerateChallengeErr, http.StatusInternalServerError) - h.logger.Error(errAccountGenerateChallenge, zap.Error(err)) + h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) return } - if len(decodedKey) != 33 && int(decodedKey[0]) != int(types.HashTypeEd25519) { - _ = jc.Error(errAccountGenerateChallengeErr, http.StatusInternalServerError) - h.logger.Error(errAccountGenerateChallenge, zap.Error(err)) + if len(decodedKey) != 33 || int(decodedKey[0]) != int(types.HashTypeEd25519) { + h.sendErrorResponse(jc, NewS5Error(ErrKeyDataIntegrityError, fmt.Errorf("invalid public key format"))) return } @@ -301,8 +187,7 @@ func (h *HttpHandler) accountRegisterChallenge(jc jape.Context) { }) if result.Error != nil { - _ = jc.Error(errAccountGenerateChallengeErr, http.StatusInternalServerError) - h.logger.Error(errAccountGenerateChallenge, zap.Error(err)) + h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) return } @@ -313,75 +198,41 @@ func (h *HttpHandler) accountRegisterChallenge(jc jape.Context) { func (h *HttpHandler) accountRegister(jc jape.Context) { var request AccountRegisterRequest - if jc.Decode(&request) != nil { return } - errored := func(err error) { - _ = jc.Error(errAccountRegisterErr, http.StatusInternalServerError) - h.logger.Error(errAccountRegister, zap.Error(err)) - } - decodedKey, err := base64.RawURLEncoding.DecodeString(request.Pubkey) - - if err != nil { - errored(err) + if err != nil || len(decodedKey) != 33 || int(decodedKey[0]) != int(types.HashTypeEd25519) { + h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) return } - if len(decodedKey) != 33 { - errored(err) - return + challenge := models.S5Challenge{ + Pubkey: request.Pubkey, + Type: "register", } - var challenge models.S5Challenge - - result := h.db.Model(&models.S5Challenge{}).Where(&models.S5Challenge{Pubkey: request.Pubkey, Type: "register"}).First(&challenge) - - if result.RowsAffected == 0 || result.Error != nil { - errored(err) + 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 { - errored(errInvalidChallengeErr) - return - } - - if len(decodedResponse) != 65 { - errored(errInvalidChallengeErr) + if err != nil || len(decodedResponse) != 65 { + h.sendErrorResponse(jc, NewS5Error(ErrKeyDataIntegrityError, err)) return } decodedChallenge, err := base64.RawURLEncoding.DecodeString(challenge.Challenge) - - if err != nil { - errored(errInvalidChallengeErr) - return - } - - if !bytes.Equal(decodedResponse[1:33], decodedChallenge) { - errored(errInvalidChallengeErr) - return - } - - if int(decodedKey[0]) != int(types.HashTypeEd25519) { - errored(errPubkeyNotSupported) + 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 { - errored(err) - return - } - - if !ed25519.Verify(decodedKey[1:], decodedResponse, decodedSignature) { - errored(errInvalidSignatureErr) + if err != nil || !ed25519.Verify(decodedKey[1:], decodedResponse, decodedSignature) { + h.sendErrorResponse(jc, NewS5Error(ErrKeyAuthorizationFailed, err)) return } @@ -389,48 +240,42 @@ func (h *HttpHandler) accountRegister(jc jape.Context) { request.Email = fmt.Sprintf("%s@%s", hex.EncodeToString(decodedKey[1:]), "example.com") } - accountExists, _, _ := h.accounts.EmailExists(request.Email) - - if accountExists { - errored(errEmailAlreadyExists) + if accountExists, _, _ := h.accounts.EmailExists(request.Email); accountExists { + h.sendErrorResponse(jc, NewS5Error(ErrKeyResourceLimitExceeded, fmt.Errorf("email already exists"))) return } - pubkeyExists, _, _ := h.accounts.PubkeyExists(hex.EncodeToString(decodedKey[1:])) - - if pubkeyExists { - errored(errPubkeyAlreadyExists) + 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) - - _, err = rand.Read(passwd) + 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 { - errored(errAccountRegisterErr) + h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) return } rawPubkey := hex.EncodeToString(decodedKey[1:]) - - err = h.accounts.AddPubkeyToAccount(*newAccount, rawPubkey) - if err != nil { - errored(errAccountRegisterErr) + 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 { - errored(errAccountRegisterErr) + h.sendErrorResponse(jc, NewS5Error(ErrKeyAuthenticationFailed, err)) return } - result = h.db.Delete(&challenge) - - if result.Error != nil { - errored(errAccountRegisterErr) + if result := h.db.Delete(&challenge); result.Error != nil { + h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) return } @@ -443,46 +288,38 @@ func (h *HttpHandler) accountLoginChallenge(jc jape.Context) { return } - errored := func(err error) { - _ = jc.Error(errAccountLoginErr, http.StatusInternalServerError) - h.logger.Error(errAccountLogin, zap.Error(err)) - } - challenge := make([]byte, 32) - _, err := rand.Read(challenge) if err != nil { - _ = jc.Error(errAccountGenerateChallengeErr, http.StatusInternalServerError) - h.logger.Error(errAccountGenerateChallenge, zap.Error(err)) + h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err)) return } decodedKey, err := base64.RawURLEncoding.DecodeString(pubkey) - if err != nil { - errored(errAccountGenerateChallengeErr) + h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) return } - if len(decodedKey) != 33 && int(decodedKey[0]) != int(types.HashTypeEd25519) { - errored(errPubkeyNotSupported) + 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 { - errored(errPubkeyNotExist) + 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 { - errored(result.Error) + h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) return } @@ -493,88 +330,54 @@ func (h *HttpHandler) accountLoginChallenge(jc jape.Context) { func (h *HttpHandler) accountLogin(jc jape.Context) { var request AccountLoginRequest - if jc.Decode(&request) != nil { return } - errored := func(err error) { - _ = jc.Error(errAccountLoginErr, http.StatusInternalServerError) - h.logger.Error(errAccountLogin, zap.Error(err)) - } - decodedKey, err := base64.RawURLEncoding.DecodeString(request.Pubkey) - if err != nil { - errored(err) - return - } - - if len(decodedKey) != 32 { - errored(err) - return - } - - var challenge models.S5Challenge - - result := h.db.Model(&models.S5Challenge{}).Where(&models.S5Challenge{Pubkey: request.Pubkey, Type: "login"}).First(&challenge) - - if result.RowsAffected == 0 || result.Error != nil { - errored(err) - return - } - - decodedResponse, err := base64.RawURLEncoding.DecodeString(request.Response) - - if err != nil { - errored(err) - return - } - - if len(decodedResponse) != 65 { - errored(err) - return - } - - decodedChallenge, err := base64.RawURLEncoding.DecodeString(challenge.Challenge) - - if err != nil { - errored(err) - return - } - - if !bytes.Equal(decodedResponse[1:33], decodedChallenge) { - errored(errInvalidChallengeErr) + if err != nil || len(decodedKey) != 32 { + h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err)) return } if int(decodedKey[0]) != int(types.HashTypeEd25519) { - errored(errPubkeyNotSupported) + 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 { - errored(err) + h.sendErrorResponse(jc, NewS5Error(ErrKeyAuthenticationFailed, err)) return } - if !ed25519.Verify(decodedKey[1:], decodedResponse, decodedSignature) { - errored(errInvalidSignatureErr) - return - } - - jwt, err := h.accounts.LoginPubkey(request.Pubkey) - - if err != nil { - errored(errAccountLoginErr) - return - } - - result = h.db.Delete(&challenge) - - if result.Error != nil { - errored(errAccountLoginErr) + if result := h.db.Delete(&challenge); result.Error != nil { + h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error)) return } @@ -632,66 +435,51 @@ func (h *HttpHandler) accountStats(jc jape.Context) { func (h *HttpHandler) accountPins(jc jape.Context) { var cursor uint64 - - if jc.DecodeForm("cursor", &cursor) != nil { + 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()) - errored := func(err error) { - _ = jc.Error(errFailedToGetPinsErr, http.StatusInternalServerError) - h.logger.Error(errFailedToGetPins, zap.Error(err)) - } - pins, err := h.accounts.AccountPins(userID, cursor) - if err != nil { - errored(err) + h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) return } pinResponse := &AccountPinResponse{Cursor: cursor, Pins: pins} - - result, err := msgpack.Marshal(pinResponse) - - if err != nil { - errored(err) + result, err2 := msgpack.Marshal(pinResponse) + if err2 != nil { + h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err2)) return } - jc.Custom(jc.Request, result) - + jc.ResponseWriter.Header().Set("Content-Type", "application/msgpack") jc.ResponseWriter.WriteHeader(http.StatusOK) - _, _ = jc.ResponseWriter.Write(result) + 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 jc.DecodeParam("cid", &cid) != nil { + if err := jc.DecodeParam("cid", &cid); err != nil { return } user := middleware.GetUserFromContext(jc.Request.Context()) - errored := func(err error) { - _ = jc.Error(errFailedToDelPinErr, http.StatusInternalServerError) - h.logger.Error(errFailedToDelPin, zap.Error(err)) - } - decodedCid, err := encoding.CIDFromString(cid) - if err != nil { - errored(err) + h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) return } hash := hex.EncodeToString(decodedCid.Hash.HashBytes()) - - err = h.accounts.DeletePinByHash(hash, user) - - if err != nil { - errored(err) + if err := h.accounts.DeletePinByHash(hash, user); err != nil { + h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err)) + return } jc.ResponseWriter.WriteHeader(http.StatusNoContent) @@ -699,40 +487,30 @@ func (h *HttpHandler) accountPinDelete(jc jape.Context) { func (h *HttpHandler) accountPin(jc jape.Context) { var cid string - if jc.DecodeParam("cid", &cid) != nil { + if err := jc.DecodeParam("cid", &cid); err != nil { return } userID := middleware.GetUserFromContext(jc.Request.Context()) - errored := func(err error) { - _ = jc.Error(errFailedToAddPinErr, http.StatusInternalServerError) - h.logger.Error(errFailedToAddPin, zap.Error(err)) - } - decodedCid, err := encoding.CIDFromString(cid) - if err != nil { - errored(err) + h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err)) return } - h.logger.Info("CID", zap.String("cidStr", cid)) - h.logger.Info("hash", zap.String("hash", hex.EncodeToString(decodedCid.Hash.HashBytes()))) - hash := hex.EncodeToString(decodedCid.Hash.HashBytes()) + h.logger.Info("Processing pin request", zap.String("cid", cid), zap.String("hash", hash)) - err = h.accounts.PinByHash(hash, userID) - - if err != nil { - errored(err) + 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) { +/*func (h *HttpHandler) directoryUpload(jc jape.Context) { var tryFiles []string var errorPages map[int]string var name string @@ -928,36 +706,163 @@ func (h *HttpHandler) directoryUpload(jc jape.Context) { jc.Encode(&AppUploadResponse{CID: cidStr}) } +*/ + +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 jc.DecodeParam("cid", &cid) != nil { + if err := jc.DecodeParam("cid", &cid); err != nil { return } decodedCid, err := encoding.CIDFromString(cid) - if err != nil { - _ = jc.Error(errFetchingUrlsErr, http.StatusInternalServerError) - h.logger.Error(errFetchingUrls, zap.Error(err)) + 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) - err = dlUriProvider.Start() - if err != nil { - _ = jc.Error(errFetchingUrlsErr, http.StatusInternalServerError) - h.logger.Error(errFetchingUrls, zap.Error(err)) - return - } - - _, err = dlUriProvider.Next() - if err != nil { - _ = jc.Error(errFetchingUrlsErr, http.StatusInternalServerError) - h.logger.Error(errFetchingUrls, zap.Error(err)) + if err := dlUriProvider.Start(); err != nil { + h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err, "Failed to start URI provider")) return } @@ -965,20 +870,17 @@ func (h *HttpHandler) debugDownloadUrls(jc jape.Context) { types.StorageLocationTypeFull, types.StorageLocationTypeFile, types.StorageLocationTypeBridge, }) if err != nil { - _ = jc.Error(errFetchingUrlsErr, http.StatusInternalServerError) - h.logger.Error(errFetchingUrls, zap.Error(err)) + 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 { - _ = jc.Error(errFetchingUrlsErr, http.StatusInternalServerError) - h.logger.Error(errFetchingUrls, zap.Error(err)) + h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err, "Failed to decode node ID")) return } availableNodesIds[i] = nodeId @@ -986,45 +888,45 @@ func (h *HttpHandler) debugDownloadUrls(jc jape.Context) { sorted, err := node.Services().P2P().SortNodesByScore(availableNodesIds) if err != nil { - return - } - - if err != nil { - _ = jc.Error(errFetchingUrlsErr, http.StatusInternalServerError) - h.logger.Error(errFetchingUrls, zap.Error(err)) + 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 { - _ = jc.Error(errFetchingUrlsErr, http.StatusInternalServerError) - h.logger.Error(errFetchingUrls, zap.Error(err)) + h.sendErrorResponse(jc, NewS5Error(ErrKeyInternalError, err, "Failed to convert node ID to string")) return } output[i] = locations[nodeIdStr].BytesURL() } jc.ResponseWriter.WriteHeader(http.StatusOK) - _, _ = jc.ResponseWriter.Write([]byte(strings.Join(output, "\n"))) + _, 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 jc.DecodeForm("pk", &pk) != nil { + if err := jc.DecodeForm("pk", &pk); err != nil { return } pkBytes, err := base64.RawURLEncoding.DecodeString(pk) - if jc.Check("error decoding pk", err) != nil { + if err != nil { + s5Err := NewS5Error(ErrKeyInvalidFileFormat, err) + h.sendErrorResponse(jc, s5Err) return } entry, err := h.getNode().Services().Registry().Get(pkBytes) - if jc.Check("error getting registry entry", err) != nil { + if err != nil { + s5ErrKey := ErrKeyStorageOperationFailed + s5Err := NewS5Error(s5ErrKey, err) + h.sendErrorResponse(jc, s5Err) return } @@ -1033,44 +935,48 @@ func (h *HttpHandler) registryQuery(jc jape.Context) { return } - jc.Encode(&RegistryQueryResponse{ + 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 jc.Decode(&request) != nil { + if err := jc.Decode(&request); err != nil { return } pk, err := base64.RawURLEncoding.DecodeString(request.Pk) - if jc.Check("error decoding pk", err) != nil { + if err != nil { + h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding public key")) return } data, err := base64.RawURLEncoding.DecodeString(request.Data) - if jc.Check("error decoding data", err) != nil { + if err != nil { + h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding data")) return } signature, err := base64.RawURLEncoding.DecodeString(request.Signature) - if jc.Check("error decoding signature", err) != nil { + 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 jc.Check("error setting registry entry", err) != 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()) @@ -1084,20 +990,20 @@ func (h *HttpHandler) registrySubscription(jc jape.Context) { h.logger.Error("error accepting websocket connection", zap.Error(err)) return } - defer func(c *websocket.Conn, code websocket.StatusCode, reason string) { - err := c.Close(code, reason) + 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() } - }(c, websocket.StatusNormalClosure, "connection closed") + }() // Main loop for reading messages for { - // Read a message (the actual reading and unpacking is skipped here) _, data, err := c.Read(ctx) if err != nil { if websocket.CloseStatus(err) == websocket.StatusNormalClosure { @@ -1112,25 +1018,25 @@ func (h *HttpHandler) registrySubscription(jc jape.Context) { 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)) - break + continue } if method != 2 { h.logger.Error("invalid method", zap.Int64("method", int64(method))) - break + continue } sre, err := decoder.DecodeBytes() - if err != nil { h.logger.Error("error decoding sre", zap.Error(err)) - break + 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 { @@ -1138,18 +1044,17 @@ func (h *HttpHandler) registrySubscription(jc jape.Context) { return } - err = c.Write(ctx, websocket.MessageBinary, encoded) - - if err != nil { + // 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 listening to registry", zap.Error(err)) + h.logger.Error("error setting up listener for registry", zap.Error(err)) break } - listeners = append(listeners, off) + listeners = append(listeners, off) // Add the listener's cleanup function to the list } } @@ -1387,6 +1292,27 @@ func (h *HttpHandler) downloadFile(jc jape.Context) { 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(),