refactor: Prune old code base to prepare for rewrite
This commit is contained in:
parent
d1b0aa5139
commit
c7bce2ff23
31
bao/bao.go
31
bao/bao.go
|
@ -1,31 +0,0 @@
|
|||
package bao
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
_ "embed"
|
||||
"io"
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
func ComputeTree(reader io.Reader, size int64) ([]byte, [32]byte, error) {
|
||||
bufSize := blake3.BaoEncodedSize(int(size), true)
|
||||
buf := bufferAt{buf: make([]byte, bufSize)}
|
||||
|
||||
hash, err := blake3.BaoEncode(&buf, bufio.NewReader(reader), size, true)
|
||||
if err != nil {
|
||||
return nil, [32]byte{}, err
|
||||
}
|
||||
|
||||
return buf.buf, hash, nil
|
||||
}
|
||||
|
||||
type bufferAt struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (b *bufferAt) WriteAt(p []byte, off int64) (int, error) {
|
||||
if copy(b.buf[off:], p) != len(p) {
|
||||
panic("bad buffer size")
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
100
cid/cid.go
100
cid/cid.go
|
@ -1,100 +0,0 @@
|
|||
package cid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"github.com/multiformats/go-multibase"
|
||||
)
|
||||
|
||||
var MAGIC_BYTES = []byte{0x26, 0x1f}
|
||||
|
||||
var (
|
||||
ErrMissingEmptySize = errors.New("Missing or empty size")
|
||||
ErrInvalidCIDMagic = errors.New("CID magic bytes missing or invalid")
|
||||
)
|
||||
|
||||
type CID struct {
|
||||
Hash [32]byte
|
||||
Size uint64
|
||||
}
|
||||
|
||||
func (c CID) StringHash() string {
|
||||
return hex.EncodeToString(c.Hash[:])
|
||||
}
|
||||
|
||||
func Encode(hash []byte, size uint64) (string, error) {
|
||||
var hashBytes [32]byte
|
||||
copy(hashBytes[:], hash)
|
||||
|
||||
return EncodeFixed(hashBytes, size)
|
||||
}
|
||||
|
||||
func EncodeFixed(hash [32]byte, size uint64) (string, error) {
|
||||
sizeBytes := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(sizeBytes, size)
|
||||
|
||||
prefixedHash := append(MAGIC_BYTES, hash[:]...)
|
||||
prefixedHash = append(prefixedHash, sizeBytes...)
|
||||
|
||||
return multibase.Encode(multibase.Base58BTC, prefixedHash)
|
||||
}
|
||||
|
||||
func EncodeString(hash string, size uint64) (string, error) {
|
||||
hashBytes, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return Encode(hashBytes, size)
|
||||
}
|
||||
|
||||
func Valid(cid string) (bool, error) {
|
||||
_, err := maybeDecode(cid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
}
|
||||
|
||||
func Decode(cid string) (*CID, error) {
|
||||
data, err := maybeDecode(cid)
|
||||
if err != nil {
|
||||
return &CID{}, err
|
||||
}
|
||||
|
||||
data = data[len(MAGIC_BYTES):]
|
||||
var hash [32]byte
|
||||
copy(hash[:], data[:])
|
||||
size := binary.LittleEndian.Uint64(data[32:])
|
||||
|
||||
return &CID{Hash: hash, Size: size}, nil
|
||||
}
|
||||
|
||||
func maybeDecode(cid string) ([]byte, error) {
|
||||
_, data, err := multibase.Decode(cid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes.Compare(data[0:len(MAGIC_BYTES)], MAGIC_BYTES) != 0 {
|
||||
return nil, ErrInvalidCIDMagic
|
||||
}
|
||||
|
||||
sizeBytes := data[len(MAGIC_BYTES)+32:]
|
||||
|
||||
if len(sizeBytes) == 0 {
|
||||
return nil, ErrMissingEmptySize
|
||||
}
|
||||
|
||||
size := binary.LittleEndian.Uint64(sizeBytes)
|
||||
|
||||
if size == 0 {
|
||||
return nil, ErrMissingEmptySize
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
)
|
||||
|
||||
var (
|
||||
ConfigFilePaths = []string{
|
||||
"/etc/lumeweb/portal/",
|
||||
"$HOME/.lumeweb/portal/",
|
||||
".",
|
||||
}
|
||||
)
|
||||
|
||||
func Init() {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("json")
|
||||
|
||||
for _, path := range ConfigFilePaths {
|
||||
viper.AddConfigPath(path)
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix("LUME_WEB_PORTAL")
|
||||
viper.AutomaticEnv()
|
||||
|
||||
pflag.String("database.type", "sqlite", "Database type")
|
||||
pflag.String("database.host", "localhost", "Database host")
|
||||
pflag.Int("database.port", 3306, "Database port")
|
||||
pflag.String("database.user", "root", "Database user")
|
||||
pflag.String("database.password", "", "Database password")
|
||||
pflag.String("database.name", "lumeweb_portal", "Database name")
|
||||
pflag.String("database.path", "./db.sqlite", "Database path for SQLite")
|
||||
pflag.String("renterd-api-password", ".", "admin password for renterd")
|
||||
pflag.Bool("debug", false, "enable debug mode")
|
||||
pflag.Parse()
|
||||
|
||||
err := viper.BindPFlags(pflag.CommandLine)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Fatal error arguments: %s \n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = viper.ReadInConfig()
|
||||
if err != nil {
|
||||
if errors.As(err, &viper.ConfigFileNotFoundError{}) {
|
||||
// Config file not found, this is not an error.
|
||||
fmt.Println("Config file not found, using default settings.")
|
||||
} else {
|
||||
// Other error, panic.
|
||||
panic(fmt.Errorf("Fatal error config file: %s \n", err))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/request"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/account"
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
type AccountController struct {
|
||||
Controller
|
||||
}
|
||||
|
||||
func (a *AccountController) PostRegister() {
|
||||
ri, success := tryParseRequest(request.RegisterRequest{}, a.Ctx)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := ri.(*request.RegisterRequest)
|
||||
|
||||
err := account.Register(r.Email, r.Password, r.Pubkey)
|
||||
|
||||
if err != nil {
|
||||
if err == account.ErrQueryingAcct || err == account.ErrFailedCreateAccount {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Return a success response to the client.
|
||||
a.Ctx.StatusCode(iris.StatusCreated)
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/request"
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/response"
|
||||
"git.lumeweb.com/LumeWeb/portal/middleware"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/auth"
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
type AuthController struct {
|
||||
Controller
|
||||
}
|
||||
|
||||
// PostLogin handles the POST /api/auth/login request to authenticate a user and return a JWT token.
|
||||
func (a *AuthController) PostLogin() {
|
||||
ri, success := tryParseRequest(request.LoginRequest{}, a.Ctx)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := ri.(*request.LoginRequest)
|
||||
|
||||
token, err := auth.LoginWithPassword(r.Email, r.Password)
|
||||
|
||||
if err != nil {
|
||||
if err == auth.ErrFailedGenerateToken {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
a.respondJSON(&response.LoginResponse{Token: token})
|
||||
}
|
||||
|
||||
// PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key.
|
||||
func (a *AuthController) PostPubkeyChallenge() {
|
||||
ri, success := tryParseRequest(request.PubkeyChallengeRequest{}, a.Ctx)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := (ri).(*request.PubkeyChallengeRequest)
|
||||
|
||||
challenge, err := auth.GeneratePubkeyChallenge(r.Pubkey)
|
||||
if err != nil {
|
||||
if err == auth.ErrFailedGenerateKeyChallenge {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
a.respondJSON(&response.ChallengeResponse{Challenge: challenge})
|
||||
}
|
||||
|
||||
// PostKeyLogin handles the POST /api/auth/pubkey/login request to authenticate a user using a public key challenge and return a JWT token.
|
||||
func (a *AuthController) PostPubkeyLogin() {
|
||||
ri, success := tryParseRequest(request.PubkeyLoginRequest{}, a.Ctx)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := ri.(*request.PubkeyLoginRequest)
|
||||
|
||||
token, err := auth.LoginWithPubkey(r.Pubkey, r.Challenge, r.Signature)
|
||||
|
||||
if err != nil {
|
||||
if err == auth.ErrFailedGenerateKeyChallenge || err == auth.ErrFailedGenerateToken || err == auth.ErrFailedSaveToken {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
a.respondJSON(&response.LoginResponse{Token: token})
|
||||
|
||||
}
|
||||
|
||||
// PostLogout handles the POST /api/auth/logout request to invalidate a JWT token.
|
||||
func (a *AuthController) PostLogout() {
|
||||
ri, success := tryParseRequest(request.LogoutRequest{}, a.Ctx)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := ri.(*request.LogoutRequest)
|
||||
|
||||
err := auth.Logout(r.Token)
|
||||
|
||||
if err != nil {
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return a success response to the client.
|
||||
a.Ctx.StatusCode(iris.StatusNoContent)
|
||||
}
|
||||
|
||||
func (a *AuthController) GetStatus() {
|
||||
middleware.VerifyJwt(a.Ctx)
|
||||
|
||||
if a.Ctx.IsStopped() {
|
||||
return
|
||||
}
|
||||
|
||||
a.respondJSON(&response.AuthStatusResponse{Status: true})
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"github.com/kataras/iris/v12"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func tryParseRequest(r interface{}, ctx iris.Context) (interface{}, bool) {
|
||||
v, ok := r.(validators.Validatable)
|
||||
|
||||
if !ok {
|
||||
return r, true
|
||||
}
|
||||
|
||||
var d map[string]interface{}
|
||||
|
||||
// Read the logout request from the client.
|
||||
if err := ctx.ReadJSON(&d); err != nil {
|
||||
logger.Get().Debug("failed to parse request", zap.Error(err))
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
data, err := v.Import(d)
|
||||
if err != nil {
|
||||
logger.Get().Debug("failed to parse request", zap.Error(err))
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if err := data.Validate(); err != nil {
|
||||
logger.Get().Debug("failed to parse request", zap.Error(err))
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return data, true
|
||||
}
|
||||
|
||||
func sendErrorCustom(ctx iris.Context, err error, customError error, irisError int) bool {
|
||||
if err != nil {
|
||||
if customError != nil {
|
||||
err = customError
|
||||
}
|
||||
ctx.StopWithError(irisError, err)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
func InternalError(ctx iris.Context, err error) bool {
|
||||
return sendErrorCustom(ctx, err, nil, iris.StatusInternalServerError)
|
||||
}
|
||||
func internalErrorCustom(ctx iris.Context, err error, customError error) bool {
|
||||
return sendErrorCustom(ctx, err, customError, iris.StatusInternalServerError)
|
||||
}
|
||||
func SendError(ctx iris.Context, err error, irisError int) bool {
|
||||
return sendErrorCustom(ctx, err, nil, irisError)
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
Ctx iris.Context
|
||||
}
|
||||
|
||||
func (c Controller) respondJSON(data interface{}) {
|
||||
err := c.Ctx.JSON(data)
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to generate response", zap.Error(err))
|
||||
}
|
||||
}
|
|
@ -1,213 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/cid"
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/response"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/middleware"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/auth"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/files"
|
||||
"github.com/kataras/iris/v12"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
)
|
||||
|
||||
var ErrStreamDone = errors.New("done")
|
||||
|
||||
type FilesController struct {
|
||||
Controller
|
||||
}
|
||||
|
||||
func (f *FilesController) BeginRequest(ctx iris.Context) {
|
||||
middleware.VerifyJwt(ctx)
|
||||
}
|
||||
func (f *FilesController) EndRequest(ctx iris.Context) {
|
||||
}
|
||||
|
||||
func (f *FilesController) PostUpload() {
|
||||
ctx := f.Ctx
|
||||
|
||||
file, meta, err := f.Ctx.FormFile("file")
|
||||
if internalErrorCustom(ctx, err, errors.New("invalid file data")) {
|
||||
logger.Get().Debug("invalid file data", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
upload, err := files.Upload(file, meta.Size, nil, auth.GetCurrentUserId(ctx))
|
||||
|
||||
if InternalError(ctx, err) {
|
||||
logger.Get().Debug("failed uploading file", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = files.Pin(upload.Hash, upload.AccountID)
|
||||
|
||||
if InternalError(ctx, err) {
|
||||
logger.Get().Debug("failed pinning file", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
cidString, err := cid.EncodeString(upload.Hash, uint64(meta.Size))
|
||||
|
||||
if InternalError(ctx, err) {
|
||||
logger.Get().Debug("failed creating cid", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.JSON(&response.UploadResponse{Cid: cidString})
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to create response", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FilesController) GetDownloadBy(cidString string) {
|
||||
ctx := f.Ctx
|
||||
|
||||
hashHex, valid := ValidateCid(cidString, true, ctx)
|
||||
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
download, err := files.Download(hashHex)
|
||||
if InternalError(ctx, err) {
|
||||
logger.Get().Debug("failed fetching file", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = PassThroughStream(download, ctx)
|
||||
if err != ErrStreamDone && InternalError(ctx, err) {
|
||||
logger.Get().Debug("failed streaming file", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FilesController) GetProofBy(cidString string) {
|
||||
ctx := f.Ctx
|
||||
|
||||
hashHex, valid := ValidateCid(cidString, true, ctx)
|
||||
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
proof, err := files.DownloadProof(hashHex)
|
||||
if InternalError(ctx, err) {
|
||||
logger.Get().Debug("failed fetching file proof", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = PassThroughStream(proof, ctx)
|
||||
if InternalError(ctx, err) {
|
||||
logger.Get().Debug("failed streaming file proof", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FilesController) GetStatusBy(cidString string) {
|
||||
ctx := f.Ctx
|
||||
|
||||
hashHex, valid := ValidateCid(cidString, false, ctx)
|
||||
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
status := files.Status(hashHex)
|
||||
|
||||
var statusCode string
|
||||
|
||||
switch status {
|
||||
case files.STATUS_UPLOADED:
|
||||
statusCode = "uploaded"
|
||||
break
|
||||
case files.STATUS_UPLOADING:
|
||||
statusCode = "uploading"
|
||||
break
|
||||
case files.STATUS_NOT_FOUND:
|
||||
statusCode = "not_found"
|
||||
break
|
||||
}
|
||||
|
||||
f.respondJSON(&response.FileStatusResponse{Status: statusCode})
|
||||
|
||||
}
|
||||
|
||||
func (f *FilesController) PostPinBy(cidString string) {
|
||||
ctx := f.Ctx
|
||||
|
||||
hashHex, valid := ValidateCid(cidString, true, ctx)
|
||||
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
err := files.Pin(hashHex, auth.GetCurrentUserId(ctx))
|
||||
if InternalError(ctx, err) {
|
||||
logger.Get().Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
f.Ctx.StatusCode(iris.StatusCreated)
|
||||
}
|
||||
|
||||
func (f *FilesController) GetUploadLimit() {
|
||||
f.respondJSON(&response.UploadLimit{Limit: f.Ctx.Application().ConfigurationReadOnly().GetPostMaxMemory()})
|
||||
}
|
||||
|
||||
func ValidateCid(cidString string, validateStatus bool, ctx iris.Context) (string, bool) {
|
||||
_, err := cid.Valid(cidString)
|
||||
if SendError(ctx, err, iris.StatusBadRequest) {
|
||||
logger.Get().Debug("invalid cid", zap.Error(err))
|
||||
return "", false
|
||||
}
|
||||
|
||||
cidObject, _ := cid.Decode(cidString)
|
||||
hashHex := cidObject.StringHash()
|
||||
|
||||
if validateStatus {
|
||||
status := files.Status(hashHex)
|
||||
|
||||
if status == files.STATUS_NOT_FOUND {
|
||||
err := errors.New("cid not found")
|
||||
SendError(ctx, errors.New("cid not found"), iris.StatusNotFound)
|
||||
logger.Get().Debug("cid not found", zap.Error(err))
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
return hashHex, true
|
||||
}
|
||||
|
||||
func PassThroughStream(stream io.Reader, ctx iris.Context) error {
|
||||
closed := false
|
||||
|
||||
err := ctx.StreamWriter(func(w io.Writer) error {
|
||||
if closed {
|
||||
return ErrStreamDone
|
||||
}
|
||||
|
||||
count, err := io.CopyN(w, stream, 1024)
|
||||
if count == 0 || err == io.EOF {
|
||||
err = stream.(io.Closer).Close()
|
||||
if err != nil {
|
||||
logger.Get().Error("failed closing stream", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == ErrStreamDone {
|
||||
err = nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
validatable validators.ValidatableImpl
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (r LoginRequest) Validate() error {
|
||||
return validation.ValidateStruct(&r,
|
||||
validation.Field(&r.Email, is.EmailFormat, validation.Required),
|
||||
validation.Field(&r.Password, validation.Required),
|
||||
)
|
||||
}
|
||||
func (r LoginRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
|
||||
return r.validatable.Import(d, r)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
type LogoutRequest struct {
|
||||
validatable validators.ValidatableImpl
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (r LogoutRequest) Validate() error {
|
||||
return validation.ValidateStruct(&r, validation.Field(&r.Token, validation.Required))
|
||||
}
|
||||
|
||||
func (r LogoutRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
|
||||
return r.validatable.Import(d, r)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
type PubkeyChallengeRequest struct {
|
||||
validatable validators.ValidatableImpl
|
||||
Pubkey string `json:"pubkey"`
|
||||
}
|
||||
|
||||
func (r PubkeyChallengeRequest) Validate() error {
|
||||
return validation.ValidateStruct(&r,
|
||||
validation.Field(&r.Pubkey, validation.Required, validation.By(validators.CheckPubkeyValidator)),
|
||||
)
|
||||
}
|
||||
|
||||
func (r PubkeyChallengeRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
|
||||
return r.validatable.Import(d, r)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
type PubkeyLoginRequest struct {
|
||||
validatable validators.ValidatableImpl
|
||||
Pubkey string `json:"pubkey"`
|
||||
Challenge string `json:"challenge"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
func (r PubkeyLoginRequest) Validate() error {
|
||||
return validation.ValidateStruct(&r,
|
||||
validation.Field(&r.Pubkey, validation.Required, validation.By(validators.CheckPubkeyValidator)),
|
||||
validation.Field(&r.Challenge, validation.Required),
|
||||
validation.Field(&r.Signature, validation.Required, validation.Length(128, 128)),
|
||||
)
|
||||
}
|
||||
|
||||
func (r PubkeyLoginRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
|
||||
return r.validatable.Import(d, r)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
)
|
||||
|
||||
type RegisterRequest struct {
|
||||
validatable validators.ValidatableImpl
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
}
|
||||
|
||||
func (r RegisterRequest) Validate() error {
|
||||
return validation.ValidateStruct(&r,
|
||||
validation.Field(&r.Email, validation.Required, is.EmailFormat),
|
||||
validation.Field(&r.Pubkey, validation.When(len(r.Password) == 0, validation.Required, validation.By(validators.CheckPubkeyValidator))),
|
||||
validation.Field(&r.Password, validation.When(len(r.Pubkey) == 0, validation.Required)),
|
||||
)
|
||||
}
|
||||
func (r RegisterRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
|
||||
return r.validatable.Import(d, r)
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type AuthStatusResponse struct {
|
||||
Status bool `json:"status"`
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type ChallengeResponse struct {
|
||||
Challenge string `json:"challenge"`
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type FileStatusResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type UploadResponse struct {
|
||||
Cid string `json:"cid"`
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type UploadLimit struct {
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package validators
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/imdario/mergo"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func CheckPubkeyValidator(value interface{}) error {
|
||||
p, _ := value.(string)
|
||||
pubkeyBytes, err := hex.DecodeString(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pubkeyBytes) != ed25519.PublicKeySize {
|
||||
return errors.New(fmt.Sprintf("pubkey must be %d bytes in hexadecimal format", ed25519.PublicKeySize))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Validatable interface {
|
||||
validation.Validatable
|
||||
Import(d map[string]interface{}) (Validatable, error)
|
||||
}
|
||||
|
||||
type ValidatableImpl struct {
|
||||
}
|
||||
|
||||
func (v ValidatableImpl) Import(d map[string]interface{}, destType Validatable) (Validatable, error) {
|
||||
instance := reflect.New(reflect.TypeOf(destType)).Interface().(Validatable)
|
||||
// Perform the import logic
|
||||
if err := mergo.Map(instance, d, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
71
db/db.go
71
db/db.go
|
@ -1,71 +0,0 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Declare a global variable to hold the database connection.
|
||||
var db *gorm.DB
|
||||
|
||||
// Init initializes the database connection based on the app's configuration settings.
|
||||
func Init() {
|
||||
// If the database connection has already been initialized, panic.
|
||||
if db != nil {
|
||||
panic("DB already initialized")
|
||||
}
|
||||
|
||||
// Retrieve database connection settings from the app's configuration using the viper library.
|
||||
dbType := viper.GetString("database.type")
|
||||
dbHost := viper.GetString("database.host")
|
||||
dbPort := viper.GetInt("database.port")
|
||||
dbSocket := viper.GetString("database.socket")
|
||||
dbUser := viper.GetString("database.user")
|
||||
dbPassword := viper.GetString("database.password")
|
||||
dbName := viper.GetString("database.name")
|
||||
dbPath := viper.GetString("database.path")
|
||||
|
||||
var err error
|
||||
var dsn string
|
||||
switch dbType {
|
||||
// Connect to a MySQL database.
|
||||
case "mysql":
|
||||
if dbSocket != "" {
|
||||
dsn = fmt.Sprintf("%s:%s@unix(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbUser, dbPassword, dbSocket, dbName)
|
||||
} else {
|
||||
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbUser, dbPassword, dbHost, dbPort, dbName)
|
||||
}
|
||||
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
// Connect to a SQLite database.
|
||||
case "sqlite":
|
||||
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
// If the database type is unsupported, panic.
|
||||
default:
|
||||
panic(fmt.Errorf("Unsupported database type: %s \n", dbType))
|
||||
}
|
||||
// If there was an error connecting to the database, panic.
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Failed to connect to database: %s \n", err))
|
||||
}
|
||||
|
||||
// Automatically migrate the database schema based on the model definitions.
|
||||
err = db.Migrator().AutoMigrate(&model.Account{}, &model.Key{}, &model.KeyChallenge{}, &model.LoginSession{}, &model.Upload{}, &model.Pin{}, &model.Tus{}, &model.Dnslink{})
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Database setup failed database type: %s \n", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the database connection instance.
|
||||
func Get() *gorm.DB {
|
||||
return db
|
||||
}
|
||||
func Close() error {
|
||||
|
||||
instance, _ := db.DB()
|
||||
|
||||
return instance.Close()
|
||||
}
|
|
@ -1,227 +0,0 @@
|
|||
package dnslink
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/cid"
|
||||
"git.lumeweb.com/LumeWeb/portal/controller"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/files"
|
||||
dnslink "github.com/dnslink-std/go"
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/context"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedReadAppManifest = errors.New("failed to read app manifest")
|
||||
ErrInvalidAppManifest = errors.New("invalid app manifest")
|
||||
)
|
||||
|
||||
type CID string
|
||||
type ExtraMetadata map[string]interface{}
|
||||
|
||||
type WebAppMetadata struct {
|
||||
Schema string `msgpack:"$schema,omitempty"`
|
||||
Type string `msgpack:"type"`
|
||||
Name string `msgpack:"name,omitempty"`
|
||||
TryFiles []string `msgpack:"tryFiles,omitempty"`
|
||||
ErrorPages map[string]string `msgpack:"errorPages,omitempty"`
|
||||
Paths map[string]PathContent `msgpack:"paths"`
|
||||
ExtraMetadata ExtraMetadata `msgpack:"extraMetadata,omitempty"`
|
||||
}
|
||||
|
||||
type PathContent struct {
|
||||
CID CID `msgpack:"cid"`
|
||||
ContentType string `msgpack:"contentType,omitempty"`
|
||||
}
|
||||
|
||||
func Handler(ctx *context.Context) {
|
||||
record := model.Dnslink{}
|
||||
|
||||
domain := ctx.Request().Host
|
||||
|
||||
if err := db.Get().Model(&model.Dnslink{Domain: domain}).First(&record).Error; err != nil {
|
||||
ctx.StopWithStatus(iris.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ret, err := dnslink.Resolve(domain)
|
||||
if err != nil {
|
||||
switch e := err.(type) {
|
||||
default:
|
||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||
return
|
||||
case dnslink.DNSRCodeError:
|
||||
if e.DNSRCode == 3 {
|
||||
ctx.StopWithStatus(iris.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ret.Links["sia"] == nil || len(ret.Links["sia"]) == 0 {
|
||||
ctx.StopWithStatus(iris.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
appManifest := ret.Links["sia"][0]
|
||||
|
||||
decodedCid, valid := controller.ValidateCid(appManifest.Identifier, true, ctx)
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
manifest := fetchManifest(ctx, decodedCid)
|
||||
if manifest == nil {
|
||||
return
|
||||
}
|
||||
|
||||
path := ctx.Path()
|
||||
|
||||
if strings.HasSuffix(path, "/") || filepath.Ext(path) == "" {
|
||||
var directoryIndex *PathContent
|
||||
for _, indexFile := range manifest.TryFiles {
|
||||
path, exists := manifest.Paths[indexFile]
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := cid.Valid(string(manifest.Paths[indexFile].CID))
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
cidObject, _ := cid.Decode(string(path.CID))
|
||||
hashHex := cidObject.StringHash()
|
||||
|
||||
status := files.Status(hashHex)
|
||||
|
||||
if status == files.STATUS_NOT_FOUND {
|
||||
continue
|
||||
}
|
||||
if status == files.STATUS_UPLOADED {
|
||||
directoryIndex = &path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if directoryIndex == nil {
|
||||
ctx.StopWithStatus(iris.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := fetchFile(directoryIndex)
|
||||
if maybeHandleFileError(err, ctx) {
|
||||
return
|
||||
}
|
||||
ctx.Header("Content-Type", directoryIndex.ContentType)
|
||||
streamFile(file, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
path = strings.TrimLeft(path, "/")
|
||||
|
||||
requestedPath, exists := manifest.Paths[path]
|
||||
|
||||
if !exists {
|
||||
ctx.StopWithStatus(iris.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := fetchFile(&requestedPath)
|
||||
if maybeHandleFileError(err, ctx) {
|
||||
return
|
||||
}
|
||||
ctx.Header("Content-Type", requestedPath.ContentType)
|
||||
streamFile(file, ctx)
|
||||
}
|
||||
|
||||
func maybeHandleFileError(err error, ctx *context.Context) bool {
|
||||
if err != nil {
|
||||
if err == files.ErrInvalidFile {
|
||||
controller.SendError(ctx, err, iris.StatusNotFound)
|
||||
return true
|
||||
}
|
||||
controller.SendError(ctx, err, iris.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return err != nil
|
||||
}
|
||||
|
||||
func streamFile(stream io.Reader, ctx *context.Context) {
|
||||
err := controller.PassThroughStream(stream, ctx)
|
||||
if err != controller.ErrStreamDone && controller.InternalError(ctx, err) {
|
||||
logger.Get().Debug("failed streaming file", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func fetchFile(path *PathContent) (io.Reader, error) {
|
||||
_, err := cid.Valid(string(path.CID))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cidObject, _ := cid.Decode(string(path.CID))
|
||||
hashHex := cidObject.StringHash()
|
||||
|
||||
status := files.Status(hashHex)
|
||||
|
||||
if status == files.STATUS_NOT_FOUND {
|
||||
return nil, errors.New("cid not found")
|
||||
}
|
||||
if status == files.STATUS_UPLOADED {
|
||||
stream, err := files.Download(hashHex)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("cid not found")
|
||||
}
|
||||
|
||||
func fetchManifest(ctx iris.Context, hash string) *WebAppMetadata {
|
||||
stream, err := files.Download(hash)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, files.ErrInvalidFile) {
|
||||
controller.SendError(ctx, err, iris.StatusNotFound)
|
||||
return nil
|
||||
}
|
||||
controller.SendError(ctx, err, iris.StatusInternalServerError)
|
||||
}
|
||||
var metadata WebAppMetadata
|
||||
|
||||
data, err := io.ReadAll(stream)
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Debug(ErrFailedReadAppManifest.Error(), zap.Error(err))
|
||||
controller.SendError(ctx, ErrFailedReadAppManifest, iris.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = msgpack.Unmarshal(data, &metadata)
|
||||
if err != nil {
|
||||
logger.Get().Debug(ErrFailedReadAppManifest.Error(), zap.Error(err))
|
||||
controller.SendError(ctx, ErrFailedReadAppManifest, iris.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
if metadata.Type != "web_app" {
|
||||
logger.Get().Debug(ErrInvalidAppManifest.Error())
|
||||
controller.SendError(ctx, ErrInvalidAppManifest, iris.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &metadata
|
||||
}
|
127
go.mod
127
go.mod
|
@ -1,128 +1,3 @@
|
|||
module git.lumeweb.com/LumeWeb/portal
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/dnslink-std/go v0.6.0
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
github.com/golang-queue/queue v0.1.3
|
||||
github.com/huandu/go-clone v1.6.0
|
||||
github.com/imdario/mergo v0.3.16
|
||||
github.com/iris-contrib/swagger v0.0.0-20230531125653-f4ee631290a7
|
||||
github.com/kataras/iris/v12 v12.2.0
|
||||
github.com/kataras/jwt v0.1.8
|
||||
github.com/multiformats/go-multibase v0.2.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/swaggo/swag v1.16.1
|
||||
github.com/tus/tusd v1.11.0
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/crypto v0.10.0
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
|
||||
gorm.io/driver/mysql v1.5.1
|
||||
gorm.io/driver/sqlite v1.5.2
|
||||
gorm.io/gorm v1.25.2
|
||||
lukechampine.com/blake3 v1.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
|
||||
github.com/Joker/jade v1.1.3 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f // indirect
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/flosch/pongo2/v4 v4.0.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.9 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible // indirect
|
||||
github.com/iris-contrib/schema v0.0.6 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kataras/blocks v0.0.7 // indirect
|
||||
github.com/kataras/golog v0.1.9 // indirect
|
||||
github.com/kataras/neffos v0.0.21 // indirect
|
||||
github.com/kataras/pio v0.0.12 // indirect
|
||||
github.com/kataras/sitemap v0.0.6 // indirect
|
||||
github.com/kataras/tunnel v0.0.4 // indirect
|
||||
github.com/klauspost/compress v1.16.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailgun/raymond/v2 v2.0.48 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mediocregopher/radix/v3 v3.8.1 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.24 // indirect
|
||||
github.com/miekg/dns v1.1.43 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/nats-io/nats.go v1.27.1 // indirect
|
||||
github.com/nats-io/nkeys v0.4.4 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.12.7 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.6.6 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/yosssi/ace v0.0.5 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/net v0.11.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.10.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace go.uber.org/multierr => go.uber.org/multierr v1.9.0
|
||||
|
||||
replace (
|
||||
github.com/tus/tusd => git.lumeweb.com/LumeWeb/tusd v1.11.1-0.20230629085530-7b20ce6a9ae5
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.39.0
|
||||
go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.14.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry => go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.12.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace => go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.12.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.12.0
|
||||
go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v0.37.0
|
||||
go.opentelemetry.io/otel/sdk => go.opentelemetry.io/otel/sdk v1.12.0
|
||||
go.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v1.14.0
|
||||
go.opentelemetry.io/proto/otlp => go.opentelemetry.io/proto/otlp v0.19.0
|
||||
)
|
||||
go 1.20
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
"log"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
|
||||
func Init() {
|
||||
var newLogger *zap.Logger
|
||||
var err error
|
||||
|
||||
if viper.GetBool("debug") {
|
||||
newLogger, err = zap.NewDevelopment()
|
||||
} else {
|
||||
newLogger, err = zap.NewProduction()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
logger = newLogger
|
||||
}
|
||||
|
||||
func Get() *zap.Logger {
|
||||
return logger
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/service/account"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/auth"
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
func VerifyJwt(ctx iris.Context) {
|
||||
token := auth.GetRequestAuthCode(ctx)
|
||||
|
||||
if len(token) == 0 {
|
||||
ctx.StopWithError(iris.StatusUnauthorized, auth.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
acct, err := auth.VerifyLoginToken(token)
|
||||
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusUnauthorized, auth.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.SetUser(account.NewUser(acct))
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primaryKey" gorm:"AUTO_INCREMENT"`
|
||||
Email string `gorm:"uniqueIndex"`
|
||||
Password *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
LoginTokens []LoginSession
|
||||
Keys []Key
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Dnslink struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primaryKey" gorm:"AUTO_INCREMENT"`
|
||||
Domain string `gorm:"uniqueIndex"`
|
||||
}
|
16
model/key.go
16
model/key.go
|
@ -1,16 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Key struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primaryKey" gorm:"AUTO_INCREMENT"`
|
||||
AccountID uint
|
||||
Account Account
|
||||
Pubkey string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type KeyChallenge struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primaryKey" gorm:"AUTO_INCREMENT"`
|
||||
AccountID uint
|
||||
Account Account
|
||||
Challenge string `gorm:"not null"`
|
||||
Expiration time.Time
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LoginSession struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primaryKey" gorm:"AUTO_INCREMENT"`
|
||||
AccountID uint
|
||||
Account Account
|
||||
Token string `gorm:"uniqueIndex"`
|
||||
Expiration time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (s *LoginSession) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
s.Expiration = time.Now().Add(time.Hour * 24)
|
||||
return
|
||||
}
|
12
model/pin.go
12
model/pin.go
|
@ -1,12 +0,0 @@
|
|||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Pin struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primaryKey" gorm:"AUTO_INCREMENT"`
|
||||
AccountID uint `gorm:"uniqueIndex:idx_account_upload"`
|
||||
UploadID uint `gorm:"uniqueIndex:idx_account_upload"`
|
||||
Account Account
|
||||
Upload Upload
|
||||
}
|
15
model/tus.go
15
model/tus.go
|
@ -1,15 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Tus struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primaryKey" gorm:"AUTO_INCREMENT"`
|
||||
UploadID string
|
||||
Hash string
|
||||
Info string
|
||||
AccountID uint
|
||||
Account Account
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Upload struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primaryKey" gorm:"AUTO_INCREMENT"`
|
||||
AccountID uint `gorm:"index"`
|
||||
Account Account
|
||||
Hash string `gorm:"uniqueIndex"`
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmailExists = errors.New("Account with email already exists")
|
||||
ErrPubkeyExists = errors.New("Account with pubkey already exists")
|
||||
ErrQueryingAcct = errors.New("Error querying accounts")
|
||||
ErrFailedHashPassword = errors.New("Failed to hash password")
|
||||
ErrFailedCreateAccount = errors.New("Failed to create account")
|
||||
)
|
||||
|
||||
func Register(email string, password string, pubkey string) error {
|
||||
err := db.Get().Transaction(func(tx *gorm.DB) error {
|
||||
existingAccount := model.Account{}
|
||||
err := tx.Where("email = ?", email).First(&existingAccount).Error
|
||||
if err == nil {
|
||||
return ErrEmailExists
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pubkey) > 0 {
|
||||
var count int64
|
||||
err := tx.Model(&model.Key{}).Where("pubkey = ?", pubkey).Count(&count).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
// An account with the same pubkey already exists.
|
||||
// Return an error response to the client.
|
||||
return ErrPubkeyExists
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new Account model with the provided email and hashed password.
|
||||
account := model.Account{
|
||||
Email: email,
|
||||
}
|
||||
|
||||
// Hash the password before saving it to the database.
|
||||
if len(password) > 0 {
|
||||
hashedPassword, err := hashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.Password = &hashedPassword
|
||||
}
|
||||
|
||||
if err := tx.Create(&account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pubkey) > 0 {
|
||||
if err := tx.Create(&model.Key{Account: account, Pubkey: pubkey}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedCreateAccount.Error(), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"github.com/kataras/iris/v12/context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
context.User
|
||||
account *model.Account
|
||||
}
|
||||
|
||||
func (u User) GetID() (string, error) {
|
||||
return strconv.Itoa(int(u.account.ID)), nil
|
||||
}
|
||||
|
||||
func NewUser(account *model.Account) *User {
|
||||
return &User{account: account}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
// Generate a new bcrypt hash from the provided password.
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedHashPassword.Error(), zap.Error(err))
|
||||
return "", ErrFailedHashPassword
|
||||
}
|
||||
|
||||
// Convert the hashed password to a string and return it.
|
||||
return string(hashedPassword), nil
|
||||
}
|
|
@ -1,264 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"github.com/kataras/jwt"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var jwtKey = ed25519.PrivateKey{}
|
||||
|
||||
var blocklist *jwt.Blocklist
|
||||
|
||||
var (
|
||||
ErrInvalidEmailPassword = errors.New("Invalid email or password")
|
||||
ErrPubkeyOnly = errors.New("Only pubkey login is supported")
|
||||
ErrFailedGenerateToken = errors.New("Failed to generate token")
|
||||
ErrFailedGenerateKeyChallenge = errors.New("Failed to generate key challenge")
|
||||
ErrFailedSignJwt = errors.New("Failed to sign jwt")
|
||||
ErrFailedSaveToken = errors.New("Failed to sign token")
|
||||
ErrFailedDeleteKeyChallenge = errors.New("Failed to delete key challenge")
|
||||
ErrFailedInvalidateToken = errors.New("Failed to invalidate token")
|
||||
ErrInvalidKeyChallenge = errors.New("Invalid key challenge")
|
||||
ErrInvalidPubkey = errors.New("Invalid pubkey")
|
||||
ErrInvalidSignature = errors.New("Invalid signature")
|
||||
ErrInvalidToken = errors.New("Invalid token")
|
||||
)
|
||||
|
||||
func Init() {
|
||||
blocklist = jwt.NewBlocklist(0)
|
||||
|
||||
configFile := viper.ConfigFileUsed()
|
||||
|
||||
var jwtPemPath string
|
||||
jwtPemName := "jwt.pem"
|
||||
|
||||
if configFile == "" {
|
||||
jwtPemPath = path.Join(config.ConfigFilePaths[0], jwtPemName)
|
||||
} else {
|
||||
jwtPemPath = path.Join(filepath.Dir(configFile), jwtPemName)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(jwtPemPath); err != nil {
|
||||
_, private, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
logger.Get().Fatal("Failed to compute JWT private key", zap.Error(err))
|
||||
}
|
||||
|
||||
privateBytes, err := x509.MarshalPKCS8PrivateKey(private)
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Fatal("Failed to create marshal private key", zap.Error(err))
|
||||
}
|
||||
|
||||
var pemPrivateBlock = &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: privateBytes,
|
||||
}
|
||||
|
||||
pemPrivateFile, err := os.Create(jwtPemPath)
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Fatal("Failed to create empty file for JWT private PEM", zap.Error(err))
|
||||
}
|
||||
|
||||
err = pem.Encode(pemPrivateFile, pemPrivateBlock)
|
||||
if err != nil {
|
||||
logger.Get().Fatal("Failed to write JWT private PEM", zap.Error(err))
|
||||
}
|
||||
|
||||
jwtKey = private
|
||||
} else {
|
||||
data, err := os.ReadFile(jwtPemPath)
|
||||
if err != nil {
|
||||
logger.Get().Fatal("Failed to read JWT private PEM", zap.Error(err))
|
||||
}
|
||||
|
||||
pemBlock, _ := pem.Decode(data)
|
||||
if err != nil {
|
||||
logger.Get().Fatal("Failed to decode JWT private PEM", zap.Error(err))
|
||||
}
|
||||
|
||||
privateBytes, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Fatal("Failed to unmarshal JWT private PEM", zap.Error(err))
|
||||
}
|
||||
|
||||
jwtKey = privateBytes.(ed25519.PrivateKey)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func LoginWithPassword(email string, password string) (string, error) {
|
||||
// Retrieve the account for the given email.
|
||||
account := model.Account{}
|
||||
if err := db.Get().Model(&account).Where("email = ?", email).First(&account).Error; err != nil {
|
||||
logger.Get().Debug(ErrInvalidEmailPassword.Error(), zap.String("email", email))
|
||||
return "", ErrInvalidEmailPassword
|
||||
}
|
||||
|
||||
if account.Password == nil || len(*account.Password) == 0 {
|
||||
logger.Get().Debug(ErrPubkeyOnly.Error(), zap.String("email", email))
|
||||
return "", ErrPubkeyOnly
|
||||
}
|
||||
|
||||
// Verify the provided password against the hashed password stored in the database.
|
||||
if err := verifyPassword(*account.Password, password); err != nil {
|
||||
logger.Get().Debug(ErrPubkeyOnly.Error(), zap.String("email", email))
|
||||
return "", ErrInvalidEmailPassword
|
||||
}
|
||||
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateAndSaveLoginToken(account.ID, 24*time.Hour)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func LoginWithPubkey(pubkey string, challenge string, signature string) (string, error) {
|
||||
pubkey = strings.ToLower(pubkey)
|
||||
signature = strings.ToLower(signature)
|
||||
|
||||
// Retrieve the key challenge for the given challenge.
|
||||
challengeObj := model.KeyChallenge{}
|
||||
if err := db.Get().Model(challengeObj).Where("challenge = ?", challenge).First(&challengeObj).Error; err != nil {
|
||||
logger.Get().Debug(ErrInvalidKeyChallenge.Error(), zap.Error(err), zap.String("challenge", challenge))
|
||||
return "", ErrInvalidKeyChallenge
|
||||
}
|
||||
|
||||
verifiedToken, err := jwt.Verify(jwt.EdDSA, jwtKey, []byte(challenge), blocklist)
|
||||
if err != nil {
|
||||
logger.Get().Debug(ErrInvalidKeyChallenge.Error(), zap.Error(err), zap.String("challenge", challenge))
|
||||
return "", ErrInvalidKeyChallenge
|
||||
}
|
||||
|
||||
rawPubKey, err := hex.DecodeString(pubkey)
|
||||
if err != nil {
|
||||
logger.Get().Debug(ErrInvalidPubkey.Error(), zap.Error(err), zap.String("pubkey", pubkey))
|
||||
return "", ErrInvalidPubkey
|
||||
}
|
||||
|
||||
rawSignature, err := hex.DecodeString(signature)
|
||||
if err != nil {
|
||||
logger.Get().Debug(ErrInvalidPubkey.Error(), zap.Error(err), zap.String("signature", pubkey))
|
||||
return "", ErrInvalidSignature
|
||||
}
|
||||
|
||||
publicKeyDecoded := ed25519.PublicKey(rawPubKey)
|
||||
|
||||
// Verify the challenge signature.
|
||||
if !ed25519.Verify(publicKeyDecoded, []byte(challenge), rawSignature) {
|
||||
logger.Get().Debug(ErrInvalidKeyChallenge.Error(), zap.Error(err), zap.String("challenge", challenge))
|
||||
return "", ErrInvalidKeyChallenge
|
||||
}
|
||||
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateAndSaveLoginToken(challengeObj.AccountID, 24*time.Hour)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedInvalidateToken.Error(), zap.Error(err), zap.String("pubkey", pubkey), zap.ByteString("token", verifiedToken.Token), zap.String("challenge", challenge))
|
||||
return "", ErrFailedInvalidateToken
|
||||
}
|
||||
|
||||
if err := db.Get().Delete(&challengeObj).Error; err != nil {
|
||||
logger.Get().Debug(ErrFailedDeleteKeyChallenge.Error(), zap.Error(err))
|
||||
return "", ErrFailedDeleteKeyChallenge
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func GeneratePubkeyChallenge(pubkey string) (string, error) {
|
||||
pubkey = strings.ToLower(pubkey)
|
||||
|
||||
// Retrieve the account for the given email.
|
||||
account := model.Key{}
|
||||
if err := db.Get().Where("pubkey = ?", pubkey).First(&account).Error; err != nil {
|
||||
logger.Get().Debug("failed to query pubkey", zap.Error(err))
|
||||
return "", errors.New("invalid pubkey")
|
||||
}
|
||||
|
||||
// Generate a random challenge string.
|
||||
challenge, err := generateAndSaveChallengeToken(account.AccountID, time.Minute)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedGenerateKeyChallenge.Error())
|
||||
return "", ErrFailedGenerateKeyChallenge
|
||||
}
|
||||
|
||||
return challenge, nil
|
||||
}
|
||||
|
||||
func Logout(token string) error {
|
||||
// Verify the provided token.
|
||||
claims, err := jwt.Verify(jwt.EdDSA, jwtKey, []byte(token), blocklist)
|
||||
if err != nil {
|
||||
logger.Get().Debug(ErrInvalidToken.Error(), zap.Error(err))
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
err = blocklist.InvalidateToken(claims.Token, claims.StandardClaims)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedInvalidateToken.Error(), zap.Error(err), zap.String("token", token))
|
||||
return ErrFailedInvalidateToken
|
||||
}
|
||||
|
||||
// Retrieve the key challenge for the given challenge.
|
||||
session := model.LoginSession{}
|
||||
if err := db.Get().Model(session).Where("token = ?", token).First(&session).Error; err != nil {
|
||||
logger.Get().Debug(ErrFailedInvalidateToken.Error(), zap.Error(err), zap.String("token", token))
|
||||
return ErrFailedInvalidateToken
|
||||
}
|
||||
|
||||
db.Get().Delete(&session)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func VerifyLoginToken(token string) (*model.Account, error) {
|
||||
uvt, err := jwt.Decode([]byte(token))
|
||||
if err != nil {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
var claim jwt.Claims
|
||||
|
||||
err = uvt.Claims(&claim)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
session := model.LoginSession{}
|
||||
if err := db.Get().Model(session).Preload("Account").Where("token = ?", token).First(&session).Error; err != nil {
|
||||
logger.Get().Debug(ErrInvalidToken.Error(), zap.Error(err), zap.String("token", token))
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
_, err = jwt.Verify(jwt.EdDSA, jwtKey, []byte(token), blocklist)
|
||||
if err != nil {
|
||||
db.Get().Delete(&session)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &session.Account, nil
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/jwt"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// verifyPassword compares the provided plaintext password with a hashed password and returns an error if they don't match.
|
||||
func verifyPassword(hashedPassword, password string) error {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
if err != nil {
|
||||
return errors.New("invalid email or password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateToken generates a JWT token for the given account ID.
|
||||
func generateToken(maxAge time.Duration, ttype string) (string, error) {
|
||||
// Define the JWT claims.
|
||||
claim := jwt.Claims{
|
||||
Expiry: time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours.
|
||||
IssuedAt: time.Now().Unix(),
|
||||
Audience: []string{ttype},
|
||||
}
|
||||
|
||||
token, err := jwt.Sign(jwt.EdDSA, jwtKey, claim, jwt.MaxAge(maxAge))
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedSignJwt.Error(), zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
func generateAndSaveLoginToken(accountID uint, maxAge time.Duration) (string, error) {
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateToken(maxAge, "auth")
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedGenerateToken.Error())
|
||||
return "", ErrFailedGenerateToken
|
||||
}
|
||||
|
||||
verifiedToken, _ := jwt.Verify(jwt.EdDSA, jwtKey, []byte(token), blocklist)
|
||||
var claim *jwt.Claims
|
||||
|
||||
_ = verifiedToken.Claims(&claim)
|
||||
|
||||
// Save the token to the database.
|
||||
session := model.LoginSession{
|
||||
Account: model.Account{ID: accountID},
|
||||
Token: token,
|
||||
Expiration: claim.ExpiresAt(),
|
||||
}
|
||||
|
||||
existingSession := model.LoginSession{}
|
||||
|
||||
err = db.Get().Where("token = ?", token).First(&existingSession).Error
|
||||
if err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
if err := db.Get().Create(&session).Error; err != nil {
|
||||
if strings.Contains(err.Error(), "Duplicate entry") {
|
||||
return token, nil
|
||||
}
|
||||
logger.Get().Error(ErrFailedSaveToken.Error(), zap.Uint("account_id", accountID), zap.Duration("max_age", maxAge))
|
||||
return "", ErrFailedSaveToken
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func generateAndSaveChallengeToken(accountID uint, maxAge time.Duration) (string, error) {
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateToken(maxAge, "challenge")
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedGenerateToken.Error(), zap.Error(err))
|
||||
return "", ErrFailedGenerateToken
|
||||
}
|
||||
|
||||
verifiedToken, err := jwt.Verify(jwt.EdDSA, jwtKey, []byte(token), blocklist)
|
||||
|
||||
if err != nil {
|
||||
return "", ErrFailedGenerateToken
|
||||
}
|
||||
|
||||
var claim *jwt.Claims
|
||||
|
||||
_ = verifiedToken.Claims(&claim)
|
||||
|
||||
// Save the token to the database.
|
||||
keyChallenge := model.KeyChallenge{
|
||||
AccountID: accountID,
|
||||
Challenge: token,
|
||||
Expiration: claim.ExpiresAt(),
|
||||
}
|
||||
|
||||
if err := db.Get().Create(&keyChallenge).Error; err != nil {
|
||||
logger.Get().Error(ErrFailedSaveToken.Error(), zap.Error(err))
|
||||
return "", ErrFailedSaveToken
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func GetRequestAuthCode(ctx iris.Context) string {
|
||||
authHeader := ctx.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// pure check: authorization header format must be Bearer {token}
|
||||
authHeaderParts := strings.Split(authHeader, " ")
|
||||
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return authHeaderParts[1]
|
||||
}
|
||||
|
||||
func GetCurrentUserId(ctx iris.Context) uint {
|
||||
usr := ctx.User()
|
||||
|
||||
if usr == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
sid, _ := usr.GetID()
|
||||
userID, _ := strconv.Atoi(sid)
|
||||
|
||||
return uint(userID)
|
||||
}
|
|
@ -1,316 +0,0 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/portal/bao"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"git.lumeweb.com/LumeWeb/portal/shared"
|
||||
"git.lumeweb.com/LumeWeb/portal/tusstore"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/spf13/viper"
|
||||
_ "github.com/tus/tusd/pkg/handler"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
STATUS_UPLOADED = iota
|
||||
STATUS_UPLOADING = iota
|
||||
STATUS_NOT_FOUND = iota
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAlreadyExists = errors.New("Upload already exists")
|
||||
ErrFailedFetchObject = errors.New("Failed fetching object")
|
||||
ErrFailedFetchObjectProof = errors.New("Failed fetching object proof")
|
||||
ErrFailedFetchTusObject = errors.New("Failed fetching tus object")
|
||||
ErrFailedHashFile = errors.New("Failed to hash file")
|
||||
ErrFailedQueryTusUpload = errors.New("Failed to query tus uploads")
|
||||
ErrFailedQueryUpload = errors.New("Failed to query uploads")
|
||||
ErrFailedQueryPins = errors.New("Failed to query pins")
|
||||
ErrFailedSaveUpload = errors.New("Failed saving upload to db")
|
||||
ErrFailedSavePin = errors.New("Failed saving pin to db")
|
||||
ErrFailedUpload = errors.New("Failed uploading object")
|
||||
ErrFailedUploadProof = errors.New("Failed uploading object proof")
|
||||
ErrFileExistsOutOfSync = errors.New("File already exists in network, but missing in database")
|
||||
ErrFileHashMismatch = errors.New("File hash does not match provided file hash")
|
||||
ErrInvalidFile = errors.New("Invalid file")
|
||||
)
|
||||
|
||||
var client *resty.Client
|
||||
|
||||
func Init() {
|
||||
client = resty.New()
|
||||
client.SetBaseURL("http://localhost:9980/api")
|
||||
client.SetBasicAuth("", viper.GetString("renterd-api-password"))
|
||||
client.SetDisableWarn(true)
|
||||
client.SetCloseConnection(true)
|
||||
}
|
||||
|
||||
func Upload(r io.ReadSeeker, size int64, hash []byte, accountID uint) (model.Upload, error) {
|
||||
var upload model.Upload
|
||||
|
||||
tree, hashBytes, err := bao.ComputeTree(r, size)
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedHashFile.Error(), zap.Error(err))
|
||||
return upload, ErrFailedHashFile
|
||||
}
|
||||
|
||||
if hash != nil {
|
||||
if bytes.Compare(hashBytes[:], hash) != 0 {
|
||||
logger.Get().Error(ErrFileHashMismatch.Error())
|
||||
return upload, ErrFileHashMismatch
|
||||
}
|
||||
}
|
||||
|
||||
hashHex := hex.EncodeToString(hashBytes[:])
|
||||
|
||||
_, err = r.Seek(0, io.SeekStart)
|
||||
|
||||
if err != nil {
|
||||
return upload, err
|
||||
}
|
||||
|
||||
result := db.Get().Where(&model.Upload{Hash: hashHex}).First(&upload)
|
||||
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedQueryUpload.Error(), zap.Error(err))
|
||||
return upload, ErrFailedQueryUpload
|
||||
}
|
||||
|
||||
logger.Get().Info(ErrAlreadyExists.Error())
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
objectExistsResult, err := client.R().Get(getBusObjectUrl(hashHex))
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedQueryUpload.Error(), zap.Error(err))
|
||||
return upload, ErrFailedQueryUpload
|
||||
}
|
||||
|
||||
objectStatusCode := objectExistsResult.StatusCode()
|
||||
|
||||
if objectStatusCode == 500 {
|
||||
bodyErr := objectExistsResult.String()
|
||||
if !strings.Contains(bodyErr, "no slabs found") {
|
||||
logger.Get().Error(ErrFailedFetchObject.Error(), zap.String("error", objectExistsResult.String()))
|
||||
return upload, ErrFailedFetchObject
|
||||
}
|
||||
|
||||
objectStatusCode = 404
|
||||
}
|
||||
|
||||
proofExistsResult, err := client.R().Get(getBusProofUrl(hashHex))
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedFetchObjectProof.Error(), zap.Error(err))
|
||||
return upload, ErrFailedFetchObjectProof
|
||||
}
|
||||
|
||||
proofStatusCode := proofExistsResult.StatusCode()
|
||||
|
||||
if proofStatusCode == 500 {
|
||||
bodyErr := proofExistsResult.String()
|
||||
if !strings.Contains(bodyErr, "no slabs found") {
|
||||
logger.Get().Error(ErrFailedFetchObjectProof.Error(), zap.String("error", proofExistsResult.String()))
|
||||
return upload, ErrFailedFetchObjectProof
|
||||
}
|
||||
|
||||
objectStatusCode = 404
|
||||
}
|
||||
|
||||
if objectStatusCode != 404 && proofStatusCode != 404 {
|
||||
logger.Get().Error(ErrFileExistsOutOfSync.Error(), zap.String("hash", hashHex))
|
||||
return upload, ErrFileExistsOutOfSync
|
||||
}
|
||||
|
||||
ret, err := client.R().SetBody(r).Put(getWorkerObjectUrl(hashHex))
|
||||
if ret.StatusCode() != 200 {
|
||||
logger.Get().Error(ErrFailedUpload.Error(), zap.String("error", ret.String()))
|
||||
return upload, ErrFailedUpload
|
||||
}
|
||||
|
||||
ret, err = client.R().SetBody(tree).Put(getWorkerProofUrl(hashHex))
|
||||
if ret.StatusCode() != 200 {
|
||||
logger.Get().Error(ErrFailedUploadProof.Error(), zap.String("error", ret.String()))
|
||||
return upload, ErrFailedUpload
|
||||
}
|
||||
|
||||
upload = model.Upload{
|
||||
Hash: hashHex,
|
||||
AccountID: accountID,
|
||||
}
|
||||
|
||||
if err = db.Get().Create(&upload).Error; err != nil {
|
||||
logger.Get().Error(ErrFailedSaveUpload.Error(), zap.Error(err))
|
||||
return upload, ErrFailedSaveUpload
|
||||
}
|
||||
|
||||
return upload, nil
|
||||
}
|
||||
func Download(hash string) (io.Reader, error) {
|
||||
uploadItem := db.Get().Table("uploads").Where(&model.Upload{Hash: hash}).Row()
|
||||
tusItem := db.Get().Table("tus").Where(&model.Tus{Hash: hash}).Row()
|
||||
|
||||
if uploadItem.Err() == nil {
|
||||
fetch, err := client.R().SetDoNotParseResponse(true).Get(getWorkerObjectUrl(hash))
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedFetchObject.Error(), zap.Error(err))
|
||||
return nil, ErrFailedFetchObject
|
||||
}
|
||||
|
||||
return fetch.RawBody(), nil
|
||||
} else if tusItem.Err() == nil {
|
||||
var tusData model.Tus
|
||||
err := tusItem.Scan(&tusData)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedQueryUpload.Error(), zap.Error(err))
|
||||
return nil, ErrFailedQueryUpload
|
||||
}
|
||||
|
||||
upload, err := getStore().GetUpload(context.Background(), tusData.UploadID)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedQueryTusUpload.Error(), zap.Error(err))
|
||||
return nil, ErrFailedQueryTusUpload
|
||||
}
|
||||
|
||||
reader, err := upload.GetReader(context.Background())
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedFetchTusObject.Error(), zap.Error(err))
|
||||
return nil, ErrFailedFetchTusObject
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
} else {
|
||||
logger.Get().Error(ErrInvalidFile.Error(), zap.String("hash", hash))
|
||||
return nil, ErrInvalidFile
|
||||
}
|
||||
}
|
||||
|
||||
func DownloadProof(hash string) (io.Reader, error) {
|
||||
uploadItem := db.Get().Model(&model.Upload{}).Where(&model.Upload{Hash: hash}).Row()
|
||||
|
||||
if uploadItem.Err() != nil {
|
||||
logger.Get().Debug(ErrInvalidFile.Error(), zap.String("hash", hash))
|
||||
return nil, ErrInvalidFile
|
||||
}
|
||||
fetch, err := client.R().SetDoNotParseResponse(true).Get(getWorkerProofUrl(hash))
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedFetchObject.Error(), zap.Error(err))
|
||||
return nil, ErrFailedFetchObject
|
||||
}
|
||||
|
||||
return fetch.RawBody(), nil
|
||||
}
|
||||
|
||||
func Status(hash string) int {
|
||||
var count int64
|
||||
|
||||
uploadItem := db.Get().Table("uploads").Where(&model.Upload{Hash: hash}).Count(&count)
|
||||
|
||||
if uploadItem.Error != nil && !errors.Is(uploadItem.Error, gorm.ErrRecordNotFound) {
|
||||
logger.Get().Error(ErrFailedQueryUpload.Error(), zap.Error(uploadItem.Error))
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return STATUS_UPLOADED
|
||||
}
|
||||
|
||||
tusItem := db.Get().Table("tus").Where(&model.Tus{Hash: hash}).Count(&count)
|
||||
|
||||
if tusItem.Error != nil && !errors.Is(tusItem.Error, gorm.ErrRecordNotFound) {
|
||||
logger.Get().Error(ErrFailedQueryUpload.Error(), zap.Error(tusItem.Error))
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return STATUS_UPLOADING
|
||||
}
|
||||
|
||||
return STATUS_NOT_FOUND
|
||||
}
|
||||
|
||||
func objectUrlBuilder(hash string, bus bool, proof bool) string {
|
||||
path := []string{}
|
||||
if bus {
|
||||
path = append(path, "bus")
|
||||
} else {
|
||||
path = append(path, "worker")
|
||||
}
|
||||
|
||||
path = append(path, "objects")
|
||||
|
||||
name := "%s"
|
||||
|
||||
if proof {
|
||||
name = name + ".obao"
|
||||
}
|
||||
|
||||
path = append(path, name)
|
||||
|
||||
return fmt.Sprintf(strings.Join(path, "/"), hash)
|
||||
}
|
||||
|
||||
func getBusObjectUrl(hash string) string {
|
||||
return objectUrlBuilder(hash, true, false)
|
||||
}
|
||||
func getWorkerObjectUrl(hash string) string {
|
||||
return objectUrlBuilder(hash, false, false)
|
||||
}
|
||||
func getWorkerProofUrl(hash string) string {
|
||||
return objectUrlBuilder(hash, false, true)
|
||||
}
|
||||
func getBusProofUrl(hash string) string {
|
||||
return objectUrlBuilder(hash, true, true)
|
||||
}
|
||||
|
||||
func getStore() *tusstore.DbFileStore {
|
||||
ret := shared.GetTusStore()
|
||||
return (*ret).(*tusstore.DbFileStore)
|
||||
}
|
||||
|
||||
func Pin(hash string, accountID uint) error {
|
||||
var upload model.Upload
|
||||
|
||||
if result := db.Get().Model(&upload).Where("hash = ?", hash).First(&upload); result.Error != nil {
|
||||
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
logger.Get().Error(ErrFailedQueryUpload.Error(), zap.Error(result.Error))
|
||||
}
|
||||
return ErrFailedQueryUpload
|
||||
}
|
||||
|
||||
var pin model.Pin
|
||||
|
||||
result := db.Get().Model(&pin).Where(&model.Pin{Upload: upload, AccountID: accountID}).First(&pin)
|
||||
|
||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
logger.Get().Error(ErrFailedQueryPins.Error(), zap.Error(result.Error))
|
||||
return ErrFailedQueryPins
|
||||
}
|
||||
|
||||
if result.Error == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pin.AccountID = upload.AccountID
|
||||
pin.Upload = upload
|
||||
|
||||
result = db.Get().Save(&pin)
|
||||
|
||||
if result.Error != nil {
|
||||
logger.Get().Error(ErrFailedSavePin.Error(), zap.Error(result.Error))
|
||||
|
||||
return ErrFailedSavePin
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
_ "go.uber.org/zap"
|
||||
)
|
||||
|
||||
type TusFunc func(upload *tusd.Upload) error
|
||||
|
||||
var tusQueue *interface{}
|
||||
var tusStore *interface{}
|
||||
var tusComposer *interface{}
|
||||
var tusWorker TusFunc
|
||||
|
||||
type tusRequestContextKey int
|
||||
|
||||
const (
|
||||
TusRequestContextKey tusRequestContextKey = iota
|
||||
)
|
||||
|
||||
func SetTusQueue(q interface{}) {
|
||||
tusQueue = &q
|
||||
}
|
||||
|
||||
func GetTusQueue() *interface{} {
|
||||
return tusQueue
|
||||
}
|
||||
|
||||
func SetTusStore(s interface{}) {
|
||||
tusStore = &s
|
||||
}
|
||||
|
||||
func GetTusStore() *interface{} {
|
||||
return tusStore
|
||||
}
|
||||
|
||||
func SetTusComposer(c interface{}) {
|
||||
tusComposer = &c
|
||||
}
|
||||
|
||||
func GetTusComposer() *interface{} {
|
||||
return tusComposer
|
||||
}
|
||||
|
||||
func SetTusWorker(w TusFunc) {
|
||||
tusWorker = w
|
||||
}
|
||||
|
||||
func GetTusWorker() TusFunc {
|
||||
return tusWorker
|
||||
}
|
222
tus/tus.go
222
tus/tus.go
|
@ -1,222 +0,0 @@
|
|||
package tus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/cid"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/files"
|
||||
"git.lumeweb.com/LumeWeb/portal/shared"
|
||||
"git.lumeweb.com/LumeWeb/portal/tusstore"
|
||||
"github.com/golang-queue/queue"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
"github.com/tus/tusd/pkg/memorylocker"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/exp/slices"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const TUS_API_PATH = "/files/tus"
|
||||
|
||||
const HASH_META_HEADER = "hash"
|
||||
|
||||
func Init() *tusd.Handler {
|
||||
store := &tusstore.DbFileStore{
|
||||
Path: "/tmp",
|
||||
}
|
||||
|
||||
shared.SetTusStore(store)
|
||||
|
||||
composer := tusd.NewStoreComposer()
|
||||
composer.UseCore(store)
|
||||
composer.UseConcater(store)
|
||||
composer.UseLocker(memorylocker.New())
|
||||
composer.UseTerminater(store)
|
||||
shared.SetTusComposer(composer)
|
||||
|
||||
handler, err := tusd.NewHandler(tusd.Config{
|
||||
BasePath: "/api/v1" + TUS_API_PATH,
|
||||
StoreComposer: composer,
|
||||
PreUploadCreateCallback: func(hook tusd.HookEvent) error {
|
||||
hash := hook.Upload.MetaData[HASH_META_HEADER]
|
||||
|
||||
if len(hash) == 0 {
|
||||
msg := "missing hash metadata"
|
||||
logger.Get().Debug(msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
var upload model.Upload
|
||||
result := db.Get().Where(&model.Upload{Hash: hash}).First(&upload)
|
||||
if (result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound)) || result.RowsAffected > 0 {
|
||||
hashBytes, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
logger.Get().Debug("invalid hash", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
cidString, err := cid.Encode(hashBytes, uint64(hook.Upload.Size))
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Debug("failed to create cid", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := json.Marshal(UploadResponse{Cid: cidString})
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to create response", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return tusd.NewHTTPError(errors.New(string(resp)), 304)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pool := queue.NewPool(5)
|
||||
|
||||
shared.SetTusQueue(pool)
|
||||
shared.SetTusWorker(tusWorker)
|
||||
|
||||
go tusStartup()
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func tusStartup() {
|
||||
tusQueue := getQueue()
|
||||
store := getStore()
|
||||
|
||||
rows, err := db.Get().Model(&model.Tus{}).Rows()
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to query tus uploads", zap.Error(err))
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
processedHashes := make([]string, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var tusUpload model.Tus
|
||||
err := db.Get().ScanRows(rows, &tusUpload)
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to scan tus records", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
upload, err := store.GetUpload(nil, tusUpload.UploadID)
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to query tus upload", zap.Error(err))
|
||||
db.Get().Delete(&tusUpload)
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Contains(processedHashes, tusUpload.Hash) {
|
||||
err := terminateUpload(upload)
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to terminate tus upload", zap.Error(err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := tusQueue.QueueTask(func(ctx context.Context) error {
|
||||
return tusWorker(&upload)
|
||||
}); err != nil {
|
||||
logger.Get().Error("failed to queue tus upload", zap.Error(err))
|
||||
} else {
|
||||
processedHashes = append(processedHashes, tusUpload.Hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tusWorker(upload *tusd.Upload) error {
|
||||
info, err := (*upload).GetInfo(context.Background())
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to query tus upload metadata", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
file, err := (*upload).GetReader(context.Background())
|
||||
if err != nil {
|
||||
logger.Get().Error("failed reading upload", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
hashHex := info.MetaData[HASH_META_HEADER]
|
||||
|
||||
hashBytes, err := hex.DecodeString(hashHex)
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error("failed decoding hash", zap.Error(err))
|
||||
tErr := terminateUpload(*upload)
|
||||
|
||||
if tErr != nil {
|
||||
return tErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
uploader, _ := strconv.Atoi(info.Storage["uploader"])
|
||||
|
||||
newUpload, err := files.Upload(file.(io.ReadSeeker), info.Size, hashBytes, uint(uploader))
|
||||
tErr := terminateUpload(*upload)
|
||||
|
||||
if tErr != nil {
|
||||
return tErr
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = files.Pin(newUpload.Hash, newUpload.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func terminateUpload(upload tusd.Upload) error {
|
||||
err := getComposer().Terminater.AsTerminatableUpload(upload).Terminate(context.Background())
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error("failed deleting tus upload", zap.Error(err))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UploadResponse struct {
|
||||
Cid string `json:"cid"`
|
||||
}
|
||||
|
||||
func getQueue() *queue.Queue {
|
||||
ret := shared.GetTusQueue()
|
||||
return (*ret).(*queue.Queue)
|
||||
}
|
||||
|
||||
func getStore() *tusstore.DbFileStore {
|
||||
ret := shared.GetTusStore()
|
||||
return (*ret).(*tusstore.DbFileStore)
|
||||
}
|
||||
func getComposer() *tusd.StoreComposer {
|
||||
ret := shared.GetTusComposer()
|
||||
return (*ret).(*tusd.StoreComposer)
|
||||
}
|
|
@ -1,316 +0,0 @@
|
|||
package tusstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/auth"
|
||||
"git.lumeweb.com/LumeWeb/portal/shared"
|
||||
"github.com/golang-queue/queue"
|
||||
clone "github.com/huandu/go-clone"
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/tus/tusd/pkg/handler"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var defaultFilePerm = os.FileMode(0664)
|
||||
|
||||
type DbFileStore struct {
|
||||
// Relative or absolute path to store files in. DbFileStore does not check
|
||||
// whether the path exists, use os.MkdirAll in this case on your own.
|
||||
Path string
|
||||
}
|
||||
|
||||
func (store DbFileStore) UseIn(composer *handler.StoreComposer) {
|
||||
composer.UseCore(store)
|
||||
composer.UseTerminater(store)
|
||||
composer.UseConcater(store)
|
||||
composer.UseLengthDeferrer(store)
|
||||
}
|
||||
|
||||
func (store DbFileStore) NewUpload(ctx context.Context, info handler.FileInfo) (handler.Upload, error) {
|
||||
if info.ID == "" {
|
||||
info.ID = uid()
|
||||
}
|
||||
binPath := store.binPath(info.ID)
|
||||
info.Storage = map[string]string{
|
||||
"Type": "dbstore",
|
||||
"Path": binPath,
|
||||
}
|
||||
|
||||
// Create binary file with no content
|
||||
file, err := os.OpenFile(binPath, os.O_CREATE|os.O_WRONLY, defaultFilePerm)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = fmt.Errorf("upload directory does not exist: %s", store.Path)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
irisContext := ctx.Value(shared.TusRequestContextKey).(iris.Context)
|
||||
|
||||
upload := &fileUpload{
|
||||
info: info,
|
||||
binPath: binPath,
|
||||
hash: info.MetaData["hash"],
|
||||
uploader: auth.GetCurrentUserId(irisContext),
|
||||
}
|
||||
|
||||
// writeInfo creates the file by itself if necessary
|
||||
err = upload.writeInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
func (store DbFileStore) GetUpload(ctx context.Context, id string) (handler.Upload, error) {
|
||||
info := handler.FileInfo{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
fUpload := &fileUpload{info: info}
|
||||
|
||||
record, is404, err := fUpload.getInfo()
|
||||
if err != nil {
|
||||
if is404 {
|
||||
// Interpret os.ErrNotExist as 404 Not Found
|
||||
err = handler.ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(record.Info), &info); err != nil {
|
||||
logger.Get().Error("fail to parse upload meta", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fUpload.info = info
|
||||
|
||||
fUpload.hash = record.Hash
|
||||
binPath := store.binPath(id)
|
||||
stat, err := os.Stat(binPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Interpret os.ErrNotExist as 404 Not Found
|
||||
err = handler.ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.Offset = stat.Size()
|
||||
|
||||
fUpload.binPath = binPath
|
||||
|
||||
return fUpload, nil
|
||||
}
|
||||
|
||||
func (store DbFileStore) AsTerminatableUpload(upload handler.Upload) handler.TerminatableUpload {
|
||||
return upload.(*fileUpload)
|
||||
}
|
||||
|
||||
func (store DbFileStore) AsLengthDeclarableUpload(upload handler.Upload) handler.LengthDeclarableUpload {
|
||||
return upload.(*fileUpload)
|
||||
}
|
||||
|
||||
func (store DbFileStore) AsConcatableUpload(upload handler.Upload) handler.ConcatableUpload {
|
||||
return upload.(*fileUpload)
|
||||
}
|
||||
|
||||
// binPath returns the path to the file storing the binary data.
|
||||
func (store DbFileStore) binPath(id string) string {
|
||||
return filepath.Join(store.Path, id)
|
||||
}
|
||||
|
||||
type fileUpload struct {
|
||||
// info stores the current information about the upload
|
||||
info handler.FileInfo
|
||||
// binPath is the path to the binary file (which has no extension)
|
||||
binPath string
|
||||
hash string
|
||||
uploader uint
|
||||
}
|
||||
|
||||
func (upload *fileUpload) GetInfo(ctx context.Context) (handler.FileInfo, error) {
|
||||
info := clone.Clone(upload.info).(handler.FileInfo)
|
||||
info.Storage["uploader"] = strconv.Itoa(int(upload.uploader))
|
||||
|
||||
return upload.info, nil
|
||||
}
|
||||
|
||||
func (upload *fileUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) {
|
||||
file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
n, err := io.Copy(file, src)
|
||||
|
||||
upload.info.Offset += n
|
||||
|
||||
err = upload.writeInfo()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (upload *fileUpload) GetReader(ctx context.Context) (io.Reader, error) {
|
||||
return os.Open(upload.binPath)
|
||||
}
|
||||
|
||||
func (upload *fileUpload) Terminate(ctx context.Context) error {
|
||||
tusUpload := &model.Tus{
|
||||
UploadID: upload.info.ID,
|
||||
}
|
||||
|
||||
ret := db.Get().Where(&tusUpload).Delete(&tusUpload)
|
||||
|
||||
if ret.Error != nil {
|
||||
logger.Get().Error("failed to delete tus entry", zap.Error(ret.Error))
|
||||
}
|
||||
|
||||
if err := os.Remove(upload.binPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (upload *fileUpload) ConcatUploads(ctx context.Context, uploads []handler.Upload) (err error) {
|
||||
file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for _, partialUpload := range uploads {
|
||||
fileUpload := partialUpload.(*fileUpload)
|
||||
|
||||
src, err := os.Open(fileUpload.binPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(file, src); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (upload *fileUpload) DeclareLength(ctx context.Context, length int64) error {
|
||||
upload.info.Size = length
|
||||
upload.info.SizeIsDeferred = false
|
||||
return upload.writeInfo()
|
||||
}
|
||||
|
||||
// writeInfo updates the entire information. Everything will be overwritten.
|
||||
func (upload *fileUpload) writeInfo() error {
|
||||
data, err := json.Marshal(upload.info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tusRecord, is404, err := upload.getInfo()
|
||||
|
||||
if err != nil && !is404 {
|
||||
return err
|
||||
}
|
||||
|
||||
if tusRecord != nil {
|
||||
tusRecord.Info = string(data)
|
||||
if ret := db.Get().Save(&tusRecord); ret.Error != nil {
|
||||
logger.Get().Error("failed to update tus entry", zap.Error(ret.Error))
|
||||
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
tusRecord = &model.Tus{UploadID: upload.info.ID, Hash: upload.hash, Info: string(data), AccountID: upload.uploader}
|
||||
|
||||
if ret := db.Get().Create(&tusRecord); ret.Error != nil {
|
||||
logger.Get().Error("failed to create tus entry", zap.Error(ret.Error))
|
||||
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (upload *fileUpload) getInfo() (*model.Tus, bool, error) {
|
||||
var tusRecord model.Tus
|
||||
|
||||
result := db.Get().Where(&model.Tus{UploadID: upload.info.ID}).First(&tusRecord)
|
||||
|
||||
if result.Error != nil && result.Error.Error() != "record not found" {
|
||||
logger.Get().Error("failed to query tus entry", zap.Error(result.Error))
|
||||
return nil, false, result.Error
|
||||
}
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, true, result.Error
|
||||
}
|
||||
|
||||
return &tusRecord, false, nil
|
||||
}
|
||||
|
||||
func (upload *fileUpload) FinishUpload(ctx context.Context) error {
|
||||
if err := getQueue().QueueTask(func(ctx context.Context) error {
|
||||
upload, err := getStore().GetUpload(nil, upload.info.ID)
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to query tus upload", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
return shared.GetTusWorker()(&upload)
|
||||
}); err != nil {
|
||||
logger.Get().Error("failed to queue tus upload", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uid() string {
|
||||
id := make([]byte, 16)
|
||||
_, err := io.ReadFull(rand.Reader, id)
|
||||
if err != nil {
|
||||
// This is probably an appropriate way to handle errors from our source
|
||||
// for random bits.
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(id)
|
||||
}
|
||||
func getQueue() *queue.Queue {
|
||||
ret := shared.GetTusQueue()
|
||||
return (*ret).(*queue.Queue)
|
||||
}
|
||||
|
||||
func getStore() *DbFileStore {
|
||||
ret := shared.GetTusStore()
|
||||
return (*ret).(*DbFileStore)
|
||||
}
|
Loading…
Reference in New Issue