portal/api/s5/http.go

1341 lines
36 KiB
Go
Raw Normal View History

2024-01-15 04:54:43 +00:00
package s5
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
2024-01-16 07:01:18 +00:00
"encoding/hex"
2024-01-15 04:54:43 +00:00
"errors"
"fmt"
"io"
"math"
"mime/multipart"
"net/http"
"strconv"
"strings"
"time"
2024-01-17 19:46:37 +00:00
"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"
2024-01-30 05:33:57 +00:00
libs5storageProvider "git.lumeweb.com/LumeWeb/libs5-go/storage/provider"
2024-01-15 19:41:24 +00:00
"git.lumeweb.com/LumeWeb/libs5-go/types"
"git.lumeweb.com/LumeWeb/portal/account"
"git.lumeweb.com/LumeWeb/portal/api/middleware"
2024-01-16 07:01:18 +00:00
"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"
2024-01-15 04:54:43 +00:00
"go.sia.tech/jape"
"go.uber.org/fx"
2024-01-15 04:54:43 +00:00
"go.uber.org/zap"
"gorm.io/gorm"
"nhooyr.io/websocket"
2024-01-15 04:54:43 +00:00
)
type readSeekNopCloser struct {
*bytes.Reader
}
2024-01-15 04:54:43 +00:00
func (rsnc readSeekNopCloser) Close() error {
return nil
}
2024-01-15 04:54:43 +00:00
2024-01-16 16:42:50 +00:00
type HttpHandler struct {
config *viper.Viper
logger *zap.Logger
storage *storage.StorageServiceDefault
db *gorm.DB
accounts *account.AccountServiceDefault
protocol *s5.S5Protocol
2024-01-15 04:54:43 +00:00
}
type HttpHandlerParams struct {
fx.In
Config *viper.Viper
Logger *zap.Logger
Storage *storage.StorageServiceDefault
Db *gorm.DB
Accounts *account.AccountServiceDefault
Protocol *s5.S5Protocol
}
2024-01-28 09:18:32 +00:00
type HttpHandlerResult struct {
fx.Out
2024-01-28 09:22:49 +00:00
HttpHandler HttpHandler
2024-01-28 09:18:32 +00:00
}
func NewHttpHandler(params HttpHandlerParams) (HttpHandlerResult, error) {
return HttpHandlerResult{
2024-01-28 09:22:49 +00:00
HttpHandler: HttpHandler{
2024-01-28 09:18:32 +00:00
config: params.Config,
logger: params.Logger,
storage: params.Storage,
db: params.Db,
accounts: params.Accounts,
protocol: params.Protocol,
},
}, nil
2024-01-15 04:54:43 +00:00
}
func (h *HttpHandler) smallFileUpload(jc jape.Context) {
2024-02-14 04:29:48 +00:00
user := middleware.GetUserFromContext(jc.Request.Context())
file, err := h.prepareFileUpload(jc)
2024-01-17 16:04:24 +00:00
if err != nil {
h.sendErrorResponse(jc, err)
2024-01-17 16:04:24 +00:00
return
}
defer func(file io.ReadSeekCloser) {
err := file.Close()
2024-01-17 22:21:15 +00:00
if err != nil {
h.logger.Error("Error closing file", zap.Error(err))
2024-01-17 22:21:15 +00:00
}
}(file)
2024-01-17 22:21:15 +00:00
// 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))
2024-01-15 04:54:43 +00:00
return
}
cid, err2 := encoding.CIDFromHash(newUpload.Hash, newUpload.Size, types.CIDTypeRaw, types.HashTypeBlake3)
if err2 != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyFileUploadFailed, err2))
2024-01-15 04:54:43 +00:00
return
}
cidStr, err2 := cid.ToString()
if err2 != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyFileUploadFailed, err2))
2024-01-15 04:54:43 +00:00
return
}
jc.Encode(&SmallUploadResponse{
CID: cidStr,
})
}
2024-01-18 19:28:32 +00:00
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)}
2024-01-16 07:01:18 +00:00
return buffer, nil
2024-01-15 04:54:43 +00:00
}
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),
2024-01-16 18:51:03 +00:00
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
}
2024-01-16 18:38:10 +00:00
setAuthCookie(jwt, jc)
}
func (h *HttpHandler) accountLoginChallenge(jc jape.Context) {
2024-01-16 18:51:03 +00:00
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))
2024-01-16 18:51:03 +00:00
return
}
decodedKey, err := base64.RawURLEncoding.DecodeString(pubkey)
if err != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err))
2024-01-16 18:51:03 +00:00
return
}
if len(decodedKey) != 33 || int(decodedKey[0]) != int(types.HashTypeEd25519) {
h.sendErrorResponse(jc, NewS5Error(ErrKeyUnsupportedFileType, fmt.Errorf("public key not supported")))
2024-01-16 18:51:03 +00:00
return
}
pubkeyExists, _, _ := h.accounts.PubkeyExists(hex.EncodeToString(decodedKey[1:]))
if !pubkeyExists {
h.sendErrorResponse(jc, NewS5Error(ErrKeyResourceNotFound, fmt.Errorf("public key does not exist")))
2024-01-16 21:05:28 +00:00
return
}
result := h.db.Create(&models.S5Challenge{
Pubkey: pubkey,
Challenge: base64.RawURLEncoding.EncodeToString(challenge),
2024-01-16 18:51:03 +00:00
Type: "login",
})
if result.Error != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error))
2024-01-16 18:51:03 +00:00
return
}
jc.Encode(&AccountLoginChallengeResponse{
Challenge: base64.RawURLEncoding.EncodeToString(challenge),
})
}
func (h *HttpHandler) accountLogin(jc jape.Context) {
2024-01-16 18:56:25 +00:00
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))
2024-01-16 18:56:25 +00:00
return
}
if int(decodedKey[0]) != int(types.HashTypeEd25519) {
h.sendErrorResponse(jc, NewS5Error(ErrKeyUnsupportedFileType, fmt.Errorf("public key type not supported")))
2024-01-16 18:56:25 +00:00
return
}
var challenge models.S5Challenge
result := h.db.Where(&models.S5Challenge{Pubkey: request.Pubkey, Type: "login"}).First(&challenge)
2024-01-16 18:56:25 +00:00
if result.RowsAffected == 0 || result.Error != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyResourceNotFound, result.Error))
2024-01-16 18:56:25 +00:00
return
}
decodedResponse, err := base64.RawURLEncoding.DecodeString(request.Response)
if err != nil || len(decodedResponse) != 65 {
h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err))
2024-01-16 18:56:25 +00:00
return
}
decodedChallenge, err := base64.RawURLEncoding.DecodeString(challenge.Challenge)
if err != nil || !bytes.Equal(decodedResponse[1:33], decodedChallenge) {
h.sendErrorResponse(jc, NewS5Error(ErrKeyDataIntegrityError, err))
2024-01-16 18:56:25 +00:00
return
}
decodedSignature, err := base64.RawURLEncoding.DecodeString(request.Signature)
if err != nil || !ed25519.Verify(decodedKey[1:], decodedResponse, decodedSignature) {
h.sendErrorResponse(jc, NewS5Error(ErrKeyAuthorizationFailed, err))
2024-01-16 18:56:25 +00:00
return
}
jwt, err := h.accounts.LoginPubkey(hex.EncodeToString(decodedKey[1:])) // Adjust based on how LoginPubkey is implemented
2024-01-16 18:56:25 +00:00
if err != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyAuthenticationFailed, err))
2024-01-16 18:56:25 +00:00
return
}
if result := h.db.Delete(&challenge); result.Error != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, result.Error))
return
}
2024-01-16 18:56:25 +00:00
setAuthCookie(jwt, jc)
}
2024-01-16 18:38:10 +00:00
func (h *HttpHandler) accountInfo(jc jape.Context) {
2024-02-14 04:29:48 +00:00
userID := middleware.GetUserFromContext(jc.Request.Context())
_, user, _ := h.accounts.AccountExists(userID)
2024-01-17 16:52:54 +00:00
info := &AccountInfoResponse{
Email: user.Email,
QuotaExceeded: false,
EmailConfirmed: false,
IsRestricted: false,
2024-01-17 17:38:52 +00:00
Tier: AccountTier{
Id: 1,
Name: "default",
2024-01-24 17:51:19 +00:00
UploadBandwidth: math.MaxUint32,
StorageLimit: math.MaxUint32,
2024-01-17 17:38:52 +00:00
Scopes: []interface{}{},
},
2024-01-17 16:52:54 +00:00
}
jc.Encode(info)
}
func (h *HttpHandler) accountStats(jc jape.Context) {
2024-02-14 04:29:48 +00:00
userID := middleware.GetUserFromContext(jc.Request.Context())
_, user, _ := h.accounts.AccountExists(userID)
2024-01-17 17:03:08 +00:00
info := &AccountStatsResponse{
AccountInfoResponse: AccountInfoResponse{
Email: user.Email,
QuotaExceeded: false,
EmailConfirmed: false,
IsRestricted: false,
2024-01-17 17:38:52 +00:00
Tier: AccountTier{
Id: 1,
Name: "default",
2024-01-24 17:51:19 +00:00
UploadBandwidth: math.MaxUint32,
StorageLimit: math.MaxUint32,
2024-01-17 17:38:52 +00:00
Scopes: []interface{}{},
},
2024-01-17 17:03:08 +00:00
},
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
}
2024-02-14 04:29:48 +00:00
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) {
2024-01-17 18:04:32 +00:00
var cid string
if err := jc.DecodeParam("cid", &cid); err != nil {
2024-01-17 18:04:32 +00:00
return
}
2024-02-14 04:29:48 +00:00
user := middleware.GetUserFromContext(jc.Request.Context())
2024-01-17 18:04:32 +00:00
decodedCid, err := encoding.CIDFromString(cid)
if err != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err))
2024-01-17 18:04:32 +00:00
return
}
hash := hex.EncodeToString(decodedCid.Hash.HashBytes())
if err := h.accounts.DeletePinByHash(hash, user); err != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err))
return
2024-01-17 18:04:32 +00:00
}
jc.ResponseWriter.WriteHeader(http.StatusNoContent)
2024-01-17 18:13:37 +00:00
}
2024-01-17 18:04:32 +00:00
func (h *HttpHandler) accountPin(jc jape.Context) {
2024-01-17 18:13:37 +00:00
var cid string
if err := jc.DecodeParam("cid", &cid); err != nil {
2024-01-17 18:13:37 +00:00
return
}
2024-02-14 04:29:48 +00:00
userID := middleware.GetUserFromContext(jc.Request.Context())
2024-01-17 18:13:37 +00:00
decodedCid, err := encoding.CIDFromString(cid)
if err != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidOperation, err))
2024-01-17 18:13:37 +00:00
return
}
hash := hex.EncodeToString(decodedCid.Hash.HashBytes())
h.logger.Info("Processing pin request", zap.String("cid", cid), zap.String("hash", hash))
2024-01-17 18:13:37 +00:00
if err := h.accounts.PinByHash(hash, userID); err != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err))
2024-01-17 18:13:37 +00:00
return
}
jc.ResponseWriter.WriteHeader(http.StatusNoContent)
2024-01-17 18:04:32 +00:00
}
/*func (h *HttpHandler) directoryUpload(jc jape.Context) {
2024-01-17 19:46:37 +00:00
var tryFiles []string
var errorPages map[int]string
var name string
if jc.DecodeForm("tryFiles", &tryFiles) != nil {
return
}
if jc.DecodeForm("errorPages", &errorPages) != nil {
return
}
if jc.DecodeForm("name", &name) != nil {
return
}
r := jc.Request
contentType := r.Header.Get("Content-Type")
2024-02-14 04:29:48 +00:00
user := middleware.GetUserFromContext(jc.Request.Context())
2024-01-17 19:46:37 +00:00
errored := func(err error) {
_ = jc.Error(errUploadingFileErr, http.StatusInternalServerError)
h.logger.Error(errUploadingFile, zap.Error(err))
2024-01-17 19:46:37 +00:00
}
if !strings.HasPrefix(contentType, "multipart/form-data") {
_ = jc.Error(errNotMultiformErr, http.StatusBadRequest)
h.logger.Error(errorNotMultiform)
2024-01-17 19:46:37 +00:00
return
}
err := r.ParseMultipartForm(h.config.GetInt64("core.post-upload-limit"))
2024-01-17 19:46:37 +00:00
if jc.Check(errMultiformParse, err) != nil {
h.logger.Error(errMultiformParse, zap.Error(err))
2024-01-17 19:46:37 +00:00
return
}
uploadMap := make(map[string]models.Upload, len(r.MultipartForm.File))
mimeMap := make(map[string]string, len(r.MultipartForm.File))
for _, files := range r.MultipartForm.File {
for _, fileHeader := range files {
// Open the file.
file, err := fileHeader.Open()
if err != nil {
errored(err)
return
}
defer func(file multipart.File) {
err := file.Close()
if err != nil {
h.logger.Error(errClosingStream, zap.Error(err))
2024-01-17 19:46:37 +00:00
}
}(file)
var rs io.ReadSeeker
hash, err := h.storage.GetHashSmall(rs)
2024-01-17 19:46:37 +00:00
_, err = rs.Seek(0, io.SeekStart)
if err != nil {
_ = jc.Error(errUploadingFileErr, http.StatusInternalServerError)
h.logger.Error(errUploadingFile, zap.Error(err))
2024-01-17 19:46:37 +00:00
return
}
if exists, upload := h.storage.FileExists(hash.Hash); exists {
2024-01-17 19:46:37 +00:00
uploadMap[fileHeader.Filename] = upload
continue
}
hash, err = h.storage.PutFileSmall(rs, "s5")
2024-01-17 19:46:37 +00:00
if err != nil {
errored(err)
return
}
_, err = rs.Seek(0, io.SeekStart)
if err != nil {
return
}
2024-01-17 19:46:37 +00:00
if err != nil {
errored(err)
return
}
var mimeBytes [512]byte
if _, err := file.Read(mimeBytes[:]); err != nil {
2024-01-17 19:46:37 +00:00
errored(err)
return
}
mimeType := http.DetectContentType(mimeBytes[:])
2024-01-17 19:46:37 +00:00
2024-02-14 04:29:48 +00:00
upload, err := h.storage.CreateUpload(hash.Hash, mimeType, user, jc.Request.RemoteAddr, uint64(fileHeader.Size), "s5")
if err != nil {
2024-01-17 19:46:37 +00:00
errored(err)
return
}
// Reset the read pointer back to the start of the file.
if _, err := file.Seek(0, io.SeekStart); err != nil {
2024-01-17 19:46:37 +00:00
errored(err)
return
}
// Read a snippet of the file to determine its MIME type.
buffer := make([]byte, 512) // 512 bytes should be enough for http.DetectContentType to determine the type
if _, err := file.Read(buffer); err != nil {
errored(err)
return
}
2024-01-17 19:46:37 +00:00
uploadMap[fileHeader.Filename] = *upload
mimeMap[fileHeader.Filename] = mimeType
}
}
filesMap := make(map[string]metadata.WebAppMetadataFileReference, len(uploadMap))
for name, file := range uploadMap {
hashDecoded, err := hex.DecodeString(file.Hash)
if err != nil {
errored(err)
return
}
cid, err := encoding.CIDFromHash(hashDecoded, file.Size, types.CIDTypeRaw, types.HashTypeBlake3)
if err != nil {
errored(err)
return
}
filesMap[name] = *metadata.NewWebAppMetadataFileReference(cid, mimeMap[name])
}
app := metadata.NewWebAppMetadata(name, tryFiles, *metadata.NewExtraMetadata(map[int]interface{}{}), errorPages, filesMap)
appData, err := msgpack.Marshal(app)
if err != nil {
errored(err)
return
}
var rs = bytes.NewReader(appData)
hash, err := h.storage.GetHashSmall(rs)
2024-01-17 19:46:37 +00:00
_, err = rs.Seek(0, io.SeekStart)
if err != nil {
_ = jc.Error(errUploadingFileErr, http.StatusInternalServerError)
h.logger.Error(errUploadingFile, zap.Error(err))
2024-01-17 19:46:37 +00:00
return
}
if exists, upload := h.storage.FileExists(hash.Hash); exists {
2024-01-17 19:46:37 +00:00
cid, err := encoding.CIDFromHash(hash, upload.Size, types.CIDTypeMetadataWebapp, types.HashTypeBlake3)
if err != nil {
_ = jc.Error(errUploadingFileErr, http.StatusInternalServerError)
h.logger.Error(errUploadingFile, zap.Error(err))
2024-01-17 19:46:37 +00:00
return
}
cidStr, err := cid.ToString()
if err != nil {
_ = jc.Error(errUploadingFileErr, http.StatusInternalServerError)
h.logger.Error(errUploadingFile, zap.Error(err))
2024-01-17 19:46:37 +00:00
return
}
jc.Encode(map[string]string{"hash": cidStr})
return
}
hash, err = h.storage.PutFileSmall(rs, "s5")
2024-01-17 19:46:37 +00:00
if err != nil {
errored(err)
return
}
cid, err := encoding.CIDFromHash(hash, uint64(len(appData)), types.CIDTypeRaw, types.HashTypeBlake3)
if err != nil {
errored(err)
return
}
cidStr, err := cid.ToString()
if err != nil {
errored(err)
return
}
jc.Encode(&AppUploadResponse{CID: cidStr})
}
*/
2024-01-17 19:46:37 +00:00
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) {
2024-01-17 21:05:11 +00:00
var pk string
if err := jc.DecodeForm("pk", &pk); err != nil {
2024-01-17 21:05:11 +00:00
return
}
pkBytes, err := base64.RawURLEncoding.DecodeString(pk)
if err != nil {
s5Err := NewS5Error(ErrKeyInvalidFileFormat, err)
h.sendErrorResponse(jc, s5Err)
2024-01-17 21:05:11 +00:00
return
}
entry, err := h.getNode().Services().Registry().Get(pkBytes)
if err != nil {
s5ErrKey := ErrKeyStorageOperationFailed
s5Err := NewS5Error(s5ErrKey, err)
h.sendErrorResponse(jc, s5Err)
2024-01-17 21:05:11 +00:00
return
}
if entry == nil {
jc.ResponseWriter.WriteHeader(http.StatusNotFound)
return
}
response := RegistryQueryResponse{
2024-01-17 21:05:11 +00:00
Pk: base64.RawURLEncoding.EncodeToString(entry.PK()),
Revision: entry.Revision(),
Data: base64.RawURLEncoding.EncodeToString(entry.Data()),
Signature: base64.RawURLEncoding.EncodeToString(entry.Signature()),
}
jc.Encode(response)
2024-01-17 21:05:11 +00:00
}
func (h *HttpHandler) registrySet(jc jape.Context) {
2024-01-17 21:20:51 +00:00
var request RegistrySetRequest
if err := jc.Decode(&request); err != nil {
2024-01-17 21:20:51 +00:00
return
}
pk, err := base64.RawURLEncoding.DecodeString(request.Pk)
if err != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding public key"))
2024-01-17 21:20:51 +00:00
return
}
data, err := base64.RawURLEncoding.DecodeString(request.Data)
if err != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding data"))
2024-01-17 21:20:51 +00:00
return
}
signature, err := base64.RawURLEncoding.DecodeString(request.Signature)
if err != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyInvalidFileFormat, err, "Error decoding signature"))
2024-01-17 21:20:51 +00:00
return
}
entry := libs5protocol.NewSignedRegistryEntry(pk, request.Revision, data, signature)
2024-01-17 21:20:51 +00:00
err = h.getNode().Services().Registry().Set(entry, false, nil)
if err != nil {
h.sendErrorResponse(jc, NewS5Error(ErrKeyStorageOperationFailed, err, "Error setting registry entry"))
2024-01-17 21:20:51 +00:00
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) {
2024-01-18 02:23:33 +00:00
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)
2024-01-18 02:23:33 +00:00
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) {
2024-01-18 03:16:04 +00:00
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))
2024-01-18 03:16:04 +00:00
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)
2024-01-18 03:16:04 +00:00
if jc.Check("error getting metadata", err) != nil {
h.logger.Error("error getting metadata", zap.Error(err))
2024-01-18 03:16:04 +00:00
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) {
2024-01-24 06:27:05 +00:00
var cid string
if jc.DecodeParam("cid", &cid) != nil {
return
}
var hashBytes []byte
isProof := false
2024-02-09 20:58:15 +00:00
if strings.HasSuffix(cid, ".obao") {
isProof = true
2024-02-09 20:58:15 +00:00
cid = strings.TrimSuffix(cid, ".obao")
}
2024-01-24 06:27:05 +00:00
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()
2024-01-25 00:10:19 +00:00
} else {
hashBytes = cidDecoded.Hash.HashBytes()
}
file := h.storage.NewFile(hashBytes)
if !file.Exists() {
jc.ResponseWriter.WriteHeader(http.StatusNotFound)
2024-01-24 06:27:05 +00:00
return
}
2024-01-24 06:27:05 +00:00
defer func(file io.ReadCloser) {
err := file.Close()
if err != nil {
h.logger.Error("error closing file", zap.Error(err))
2024-01-24 06:27:05 +00:00
}
}(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")
2024-02-09 20:58:15 +00:00
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())
2024-01-25 00:23:42 +00:00
http.ServeContent(jc.ResponseWriter, jc.Request, file.Name(), file.Modtime(), file)
2024-01-24 06:27:05 +00:00
}
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 {
2024-01-30 05:33:57 +00:00
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(),
},
})
}
2024-01-16 18:38:10 +00:00
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)
}