diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..fa21ccb78d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git/ +.github/ +.idea/ +.DS_Store +build/ +dist/ +*.db +*.bin +*.test +*.out +/tmp/ +/log/ +/lang/ +/daemon/ +/public/dist/* +!/public/dist/README.md diff --git a/.railwayignore b/.railwayignore new file mode 100644 index 0000000000..fa21ccb78d --- /dev/null +++ b/.railwayignore @@ -0,0 +1,16 @@ +.git/ +.github/ +.idea/ +.DS_Store +build/ +dist/ +*.db +*.bin +*.test +*.out +/tmp/ +/log/ +/lang/ +/daemon/ +/public/dist/* +!/public/dist/README.md diff --git a/Dockerfile.railway b/Dockerfile.railway new file mode 100644 index 0000000000..34bafd59ac --- /dev/null +++ b/Dockerfile.railway @@ -0,0 +1,36 @@ +FROM alpine:edge AS builder +LABEL stage=go-builder +WORKDIR /app/ +RUN apk add --no-cache bash curl jq gcc git go musl-dev +COPY go.mod go.sum ./ +RUN go mod download +COPY ./ ./ +RUN bash build.sh release docker + +FROM alpine:edge +LABEL MAINTAINER="OpenList" +ARG INSTALL_FFMPEG=false +ARG INSTALL_ARIA2=false +ARG USER=openlist +ARG UID=1001 +ARG GID=1001 + +WORKDIR /opt/openlist/ + +RUN addgroup -g ${GID} ${USER} && \ + adduser -D -u ${UID} -G ${USER} ${USER} && \ + mkdir -p /opt/openlist/data && \ + apk add --no-cache ca-certificates tzdata && \ + if [ "${INSTALL_FFMPEG}" = "true" ]; then apk add --no-cache ffmpeg; fi && \ + if [ "${INSTALL_ARIA2}" = "true" ]; then apk add --no-cache aria2; fi + +COPY --from=builder --chmod=755 --chown=${UID}:${GID} /app/bin/openlist ./ +COPY --chmod=755 --chown=${UID}:${GID} entrypoint.sh /entrypoint.sh + +USER ${USER} +RUN /entrypoint.sh version + +ENV UMASK=022 RUN_ARIA2=${INSTALL_ARIA2} +VOLUME /opt/openlist/data/ +EXPOSE 5244 5245 +CMD [ "/entrypoint.sh" ] diff --git a/RAILWAY_SETUP.md b/RAILWAY_SETUP.md new file mode 100644 index 0000000000..a727410012 --- /dev/null +++ b/RAILWAY_SETUP.md @@ -0,0 +1,169 @@ +# Railway Deployment Guide + +Deploy OpenList on Railway with persistent storage and automatic HTTPS. + +## Prerequisites + +- A [Railway](https://railway.app) account +- This repository connected to Railway (or a new project from this repo) + +## Quick Start + +### 1. Create a New Project + +1. Go to [Railway Dashboard](https://railway.app/dashboard) +2. Click **"New Project"** +3. Select **"Deploy from GitHub repo"** (or upload this repo) +4. Choose this repository + +### 2. Railway will Auto-Detect + +Railway will automatically detect the `railway.json` or `railway.toml` in the repo root and use `Dockerfile.railway` for building. + +### 3. Add a Volume (Persistent Storage) + +OpenList stores data (database, uploads, configs) in `/opt/openlist/data`. You need a persistent volume: + +1. In your Railway project, click **"New"** → **"Volume"** +2. Name it `openlist-data` +3. Mount it to `/opt/openlist/data` in your service + +### 4. Configure Environment Variables + +Go to your service → **Variables** tab and add: + +| Variable | Value | Description | +|----------|-------|-------------| +| `UMASK` | `022` | File permissions mask | +| `TZ` | `Asia/Shanghai` | Timezone (optional) | +| `RUN_ARIA2` | `false` | Enable aria2 (optional, increases build time) | +| `INSTALL_FFMPEG` | `false` | Install ffmpeg (optional, increases build time) | +| `INSTALL_ARIA2` | `false` | Install aria2 (optional, increases build time) | +| `GITHUB_TOKEN` | `` | GitHub token to avoid API rate limits when downloading the frontend (optional) | + +> **Note:** Railway's dynamic `PORT` is automatically mapped to `HTTP_PORT` by the entrypoint script. You do **not** need to manually set `HTTP_PORT=$PORT`. + +### 5. Deploy + +1. Railway will automatically build and deploy +2. Wait for the build to complete (first build takes ~3-5 minutes) +3. Check the **Deployments** tab for build logs + +### 6. Get Admin Credentials + +On first startup, OpenList generates a random admin password. Find it in the deployment logs: + +1. Go to **Deployments** → click on the latest deployment +2. View **Logs** +3. Look for: `Successfully created the admin user and the initial password is: XXXXXXXX` + +Alternatively, you can set a fixed admin password by running: +``` +openlist admin set YOUR_PASSWORD +``` + +### 7. Access OpenList + +Once deployed, Railway will provide a public URL like: +``` +https://your-project.up.railway.app +``` + +Open this URL in your browser and log in with the admin credentials. + +## Configuration + +### Database + +By default, OpenList uses SQLite and stores the database in the volume at `/opt/openlist/data/data.db`. + +For better performance, you can use Railway's PostgreSQL: + +1. Add a **PostgreSQL** database to your Railway project +2. Set these environment variables: + - `DB_TYPE=postgresql` + - `DB_HOST=${{Postgres.HOSTNAME}}` + - `DB_PORT=${{Postgres.PORT}}` + - `DB_USER=${{Postgres.USERNAME}}` + - `DB_PASS=${{Postgres.PASSWORD}}` + - `DB_NAME=${{Postgres.DATABASE}}` + +### Custom Domain + +1. Go to **Settings** → **Domains** +2. Add your custom domain +3. Railway will automatically provision an SSL certificate + +### Build Options + +Edit `railway.json` or `railway.toml` to customize: + +- `restartPolicyType`: `"on-failure"` or `"always"` +- `healthcheckPath`: Health check endpoint +- `INSTALL_FFMPEG`: Set to `true` to enable ffmpeg +- `INSTALL_ARIA2`: Set to `true` to enable aria2 + +## Troubleshooting + +### Build Fails + +- Check build logs for missing dependencies +- Ensure `go.mod` and `go.sum` are present +- Try rebuilding with `INSTALL_FFMPEG=false` and `INSTALL_ARIA2=false` +- If the build fails while downloading the frontend, add a `GITHUB_TOKEN` variable with a GitHub personal access token to avoid API rate limits +- As a fallback, you can change `bash build.sh release docker` to `bash build.sh dev docker` in `Dockerfile.railway` to skip frontend version pinning + +### Port Not Accessible + +- The entrypoint automatically maps Railway's `PORT` to `HTTP_PORT` +- Check that the service is listening on `0.0.0.0` (default) +- Ensure no other process is using the assigned port + +### Data Lost After Redeploy + +- Ensure the volume is mounted to `/opt/openlist/data` +- Check volume status in Railway dashboard + +### Cannot Login + +- Check deployment logs for admin password +- Ensure database file exists in volume + +## Manual Railway CLI Deployment + +If you prefer using the Railway CLI: + +```bash +# Install Railway CLI +npm i -g @railway/cli + +# Login +railway login + +# Initialize project +railway init + +# Link to existing project +railway link + +# Deploy +railway up +``` + +## Architecture + +``` +Railway Project +├── Service: OpenList +│ ├── Build: Dockerfile.railway +│ ├── Port: $PORT (mapped to HTTP_PORT) +│ └── Volume: /opt/openlist/data +└── (Optional) PostgreSQL Database +``` + +## Files Added + +- `railway.json` - Railway project configuration +- `railway.toml` - Alternative Railway configuration +- `Dockerfile.railway` - Self-contained Dockerfile for Railway +- `.railwayignore` - Files excluded from Railway builds diff --git a/drivers/189/help.go b/drivers/189/help.go index 50748a35c6..1b7dbe9307 100644 --- a/drivers/189/help.go +++ b/drivers/189/help.go @@ -29,11 +29,22 @@ func random() string { func RsaEncode(origData []byte, j_rsakey string, hex bool) string { publicKey := []byte("-----BEGIN PUBLIC KEY-----\n" + j_rsakey + "\n-----END PUBLIC KEY-----") block, _ := pem.Decode(publicKey) - pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes) - pub := pubInterface.(*rsa.PublicKey) + if block == nil { + return "" + } + pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + log.Errorf("err: %s", err.Error()) + return "" + } + pub, ok := pubInterface.(*rsa.PublicKey) + if !ok { + return "" + } b, err := rsa.EncryptPKCS1v15(rand.Reader, pub, origData) if err != nil { log.Errorf("err: %s", err.Error()) + return "" } res := base64.StdEncoding.EncodeToString(b) if hex { diff --git a/drivers/189pc/help.go b/drivers/189pc/help.go index 6f6c59f305..229fec5b28 100644 --- a/drivers/189pc/help.go +++ b/drivers/189pc/help.go @@ -47,8 +47,21 @@ func signatureOfHmac(sessionSecret, sessionKey, operate, fullUrl, dateOfGmt, par // RAS 加密用户名密码 func RsaEncrypt(publicKey, origData string) string { block, _ := pem.Decode([]byte(publicKey)) - pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes) - data, _ := rsa.EncryptPKCS1v15(rand.Reader, pubInterface.(*rsa.PublicKey), []byte(origData)) + if block == nil { + return "" + } + pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return "" + } + pub, ok := pubInterface.(*rsa.PublicKey) + if !ok { + return "" + } + data, err := rsa.EncryptPKCS1v15(rand.Reader, pub, []byte(origData)) + if err != nil { + return "" + } return strings.ToUpper(hex.EncodeToString(data)) } diff --git a/drivers/cloudreve_v4/util.go b/drivers/cloudreve_v4/util.go index 9cf8d98a81..426fc01746 100644 --- a/drivers/cloudreve_v4/util.go +++ b/drivers/cloudreve_v4/util.go @@ -236,7 +236,9 @@ func (d *CloudreveV4) refreshToken() error { if d.canLogin() { return d.login() } - d.GetStorage().SetStatus(fmt.Sprintf("Invalid RefreshToken: %s", err.Error())) + if storage := d.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("Invalid RefreshToken: %s", err.Error())) + } op.MustSaveDriverStorage(d) return fmt.Errorf("invalid refresh token: %w", err) } @@ -254,7 +256,9 @@ func (d *CloudreveV4) refreshToken() error { // try to login again return d.login() } - d.GetStorage().SetStatus("This session is no longer valid") + if storage := d.GetStorage(); storage != nil { + storage.SetStatus("This session is no longer valid") + } op.MustSaveDriverStorage(d) return ErrorIssueToken } @@ -322,7 +326,9 @@ func (d *CloudreveV4) isTokenExpired() bool { var jwt AccessJWT err = d.parseJWT(d.AccessToken, &jwt) if err != nil { - d.GetStorage().SetStatus(fmt.Sprintf("Invalid AccessToken: %s", err.Error())) + if storage := d.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("Invalid AccessToken: %s", err.Error())) + } op.MustSaveDriverStorage(d) return false } @@ -345,7 +351,9 @@ func (d *CloudreveV4) isTokenExpired() bool { // try to login again return true } - d.GetStorage().SetStatus("This session is no longer valid") + if storage := d.GetStorage(); storage != nil { + storage.SetStatus("This session is no longer valid") + } op.MustSaveDriverStorage(d) return false } diff --git a/drivers/google_drive/util.go b/drivers/google_drive/util.go index 042abafa44..54ac0e27e0 100644 --- a/drivers/google_drive/util.go +++ b/drivers/google_drive/util.go @@ -143,7 +143,13 @@ func (d *GoogleDrive) refreshToken() error { // load private key from string privateKeyPem, _ := pem.Decode([]byte(jsonData.PrivateKey)) - privateKey, _ := x509.ParsePKCS8PrivateKey(privateKeyPem.Bytes) + if privateKeyPem == nil { + return fmt.Errorf("failed to decode PEM block containing private key") + } + privateKey, err := x509.ParsePKCS8PrivateKey(privateKeyPem.Bytes) + if err != nil { + return err + } jwtToken := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ diff --git a/drivers/halalcloud/driver.go b/drivers/halalcloud/driver.go index 93783dcd55..36912d2867 100644 --- a/drivers/halalcloud/driver.go +++ b/drivers/halalcloud/driver.go @@ -88,7 +88,9 @@ func (d *HalalCloud) Init(ctx context.Context) error { if d.Addition.RefreshToken == "" || !d.IsLogin() { as, err := d.NewAuthServiceWithOauth() if err != nil { - d.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) + if storage := d.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("%+v", err.Error())) + } return err } d.HalalCommon.AuthService = as @@ -215,7 +217,11 @@ func (d *HalalCloud) getLink(ctx context.Context, file model.Obj, args model.Lin ctx1, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - result, err := client.ParseFileSlice(ctx1, (*pubUserFile.File)(file.(*Files))) + files, ok := file.(*Files) + if !ok { + return nil, fmt.Errorf("unsupported file type: %T", file) + } + result, err := client.ParseFileSlice(ctx1, (*pubUserFile.File)(files)) if err != nil { return nil, err } @@ -370,7 +376,10 @@ func (d *HalalCloud) put(ctx context.Context, dstDir model.Obj, fileStream model if err != nil { return nil, err } - u, _ := url.Parse(result.Endpoint) + u, err := url.Parse(result.Endpoint) + if err != nil { + return nil, fmt.Errorf("failed to parse endpoint: %w", err) + } u.Host = "s3." + u.Host result.Endpoint = u.String() s, err := session.NewSession(&aws.Config{ diff --git a/drivers/netease_music/crypto.go b/drivers/netease_music/crypto.go index 65b3df4bf3..ad82f4a580 100644 --- a/drivers/netease_music/crypto.go +++ b/drivers/netease_music/crypto.go @@ -88,8 +88,17 @@ func rsaEncrypt(buffer, key []byte) []byte { buffers := make([]byte, 128-16, 128) buffers = append(buffers, buffer...) block, _ := pem.Decode(key) - pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes) - pub := pubInterface.(*rsa.PublicKey) + if block == nil { + return nil + } + pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil + } + pub, ok := pubInterface.(*rsa.PublicKey) + if !ok { + return nil + } c := new(big.Int).SetBytes([]byte(buffers)) return c.Exp(c, big.NewInt(int64(pub.E)), pub.N).Bytes() } diff --git a/drivers/thunder/driver.go b/drivers/thunder/driver.go index df4113c71d..0022415977 100644 --- a/drivers/thunder/driver.go +++ b/drivers/thunder/driver.go @@ -82,7 +82,9 @@ func (x *Thunder) Init(ctx context.Context) (err error) { // 重新登录 token, err = x.Login(x.Username, x.Password) if err != nil { - x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) + if storage := x.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("%+v", err.Error())) + } op.MustSaveDriverStorage(x) } // 清空 信任密钥 @@ -217,7 +219,9 @@ func (x *ThunderExpert) Init(ctx context.Context) (err error) { x.SetRefreshTokenFunc(func() error { token, err := x.XunLeiCommon.RefreshToken(x.TokenResp.RefreshToken) if err != nil { - x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) + if storage := x.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("%+v", err.Error())) + } } x.SetTokenResp(token) op.MustSaveDriverStorage(x) @@ -237,7 +241,9 @@ func (x *ThunderExpert) Init(ctx context.Context) (err error) { if err != nil { token, err = x.Login(x.Username, x.Password) if err != nil { - x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) + if storage := x.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("%+v", err.Error())) + } } // 清空 信任密钥 x.ExpertAddition.CreditKey = "" diff --git a/drivers/thunder_browser/driver.go b/drivers/thunder_browser/driver.go index da1844afa7..94c51f8a91 100644 --- a/drivers/thunder_browser/driver.go +++ b/drivers/thunder_browser/driver.go @@ -82,7 +82,9 @@ func (x *ThunderBrowser) Init(ctx context.Context) (err error) { // 重新登录 token, err = x.Login(x.Username, x.Password) if err != nil { - x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) + if storage := x.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("%+v", err.Error())) + } op.MustSaveDriverStorage(x) } // 清空 信任密钥 @@ -264,7 +266,9 @@ func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) { x.SetRefreshTokenFunc(func() error { token, err := x.XunLeiBrowserCommon.RefreshToken(x.TokenResp.RefreshToken) if err != nil { - x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) + if storage := x.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("%+v", err.Error())) + } } x.SetTokenResp(token) op.MustSaveDriverStorage(x) @@ -290,7 +294,9 @@ func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) { if err != nil { token, err = x.Login(x.Username, x.Password) if err != nil { - x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) + if storage := x.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("%+v", err.Error())) + } } // 清空 信任密钥 x.ExpertAddition.CreditKey = "" diff --git a/drivers/thunderx/driver.go b/drivers/thunderx/driver.go index 457a9c5367..6bd8c5009a 100644 --- a/drivers/thunderx/driver.go +++ b/drivers/thunderx/driver.go @@ -68,7 +68,9 @@ func (x *ThunderX) Init(ctx context.Context) (err error) { // 重新登录 token, err = x.Login(x.Username, x.Password) if err != nil { - x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) + if storage := x.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("%+v", err.Error())) + } if token != nil && token.UserID != "" { x.SetUserID(token.UserID) x.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, token.UserID) @@ -213,7 +215,9 @@ func (x *ThunderXExpert) Init(ctx context.Context) (err error) { x.SetRefreshTokenFunc(func() error { token, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken) if err != nil { - x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) + if storage := x.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("%+v", err.Error())) + } } x.SetTokenResp(token) op.MustSaveDriverStorage(x) @@ -231,7 +235,9 @@ func (x *ThunderXExpert) Init(ctx context.Context) (err error) { if err != nil { token, err = x.Login(x.Username, x.Password) if err != nil { - x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) + if storage := x.GetStorage(); storage != nil { + storage.SetStatus(fmt.Sprintf("%+v", err.Error())) + } } } x.SetTokenResp(token) diff --git a/entrypoint.sh b/entrypoint.sh index 9e759294f4..39e051b714 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -5,6 +5,16 @@ umask ${UMASK} if [ "$1" = "version" ]; then ./openlist version else + # Railway provides a dynamic PORT; map it to OpenList's HTTP_PORT when needed. + if [ -z "$HTTP_PORT" ] && [ -n "$PORT" ]; then + export HTTP_PORT="$PORT" + fi + + # Ensure data directory exists + if [ ! -d ./data ]; then + mkdir -p ./data + fi + # Check file of /opt/openlist/data permissions for current user # 检查当前用户是否有当前目录的写和执行权限 if [ -d ./data ]; then diff --git a/internal/db/user.go b/internal/db/user.go index 4b9c67eceb..bbc0b1befa 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -72,7 +72,10 @@ func RegisterAuthn(u *model.User, credential *webauthn.Credential) error { if u == nil { return errors.New("user is nil") } - exists := u.WebAuthnCredentials() + exists, err := u.WebAuthnCredentials() + if err != nil { + return err + } if credential != nil { exists = append(exists, *credential) } @@ -84,7 +87,10 @@ func RegisterAuthn(u *model.User, credential *webauthn.Credential) error { } func RemoveAuthn(u *model.User, id string) error { - exists := u.WebAuthnCredentials() + exists, err := u.WebAuthnCredentials() + if err != nil { + return err + } for i := 0; i < len(exists); i++ { idEncoded := base64.StdEncoding.EncodeToString(exists[i].ID) if idEncoded == id { diff --git a/internal/model/user.go b/internal/model/user.go index 2dacd752eb..73e7dfcd1a 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -258,13 +258,12 @@ func (u *User) WebAuthnDisplayName() string { return u.Username } -func (u *User) WebAuthnCredentials() []webauthn.Credential { +func (u *User) WebAuthnCredentials() ([]webauthn.Credential, error) { var res []webauthn.Credential - err := json.Unmarshal([]byte(u.Authn), &res) - if err != nil { - fmt.Println(err) + if err := json.Unmarshal([]byte(u.Authn), &res); err != nil { + return nil, err } - return res + return res, nil } func (u *User) WebAuthnIcon() string { diff --git a/pkg/utils/file.go b/pkg/utils/file.go index 4fa58d397c..0225833f96 100644 --- a/pkg/utils/file.go +++ b/pkg/utils/file.go @@ -1,7 +1,6 @@ package utils import ( - "fmt" "io" "mime" "os" @@ -62,11 +61,11 @@ func CopyDir(src, dst string) error { if fd.IsDir() { if err = CopyDir(srcfp, dstfp); err != nil { - fmt.Println(err) + return err } } else { if err = CopyFile(srcfp, dstfp); err != nil { - fmt.Println(err) + return err } } } diff --git a/railway.toml b/railway.toml new file mode 100644 index 0000000000..f5941662c2 --- /dev/null +++ b/railway.toml @@ -0,0 +1,9 @@ +[build] +builder = "dockerfile" +dockerfilePath = "./Dockerfile.railway" + +[deploy] +restartPolicyType = "on-failure" +restartPolicyMaxRetries = 10 +healthcheckPath = "/api/public/settings" +healthcheckTimeout = 300 diff --git a/server/ftp/afero.go b/server/ftp/afero.go index 02c956a509..590cda745e 100644 --- a/server/ftp/afero.go +++ b/server/ftp/afero.go @@ -94,7 +94,10 @@ func (a *AferoAdapter) GetHandle(name string, flags int, offset int64) (ftpserve if (flags & os.O_APPEND) != 0 { return nil, errs.NotSupport } - user := a.ctx.Value(conf.UserKey).(*model.User) + user, ok := a.ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return nil, errs.PermissionDenied + } path, err := user.JoinPath(name) if err != nil { return nil, err diff --git a/server/ftp/fsmanage.go b/server/ftp/fsmanage.go index 3e98d6d14d..e36a094cdb 100644 --- a/server/ftp/fsmanage.go +++ b/server/ftp/fsmanage.go @@ -14,7 +14,10 @@ import ( ) func Mkdir(ctx context.Context, path string) error { - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return errs.PermissionDenied + } if !user.CanFTPManage() { return errs.PermissionDenied } @@ -37,7 +40,10 @@ func Mkdir(ctx context.Context, path string) error { } func Remove(ctx context.Context, path string) error { - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return errs.PermissionDenied + } if !user.CanRemove() || !user.CanFTPManage() { return errs.PermissionDenied } @@ -59,7 +65,10 @@ func Remove(ctx context.Context, path string) error { } func Rename(ctx context.Context, oldPath, newPath string) error { - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return errs.PermissionDenied + } srcPath, err := user.JoinPath(oldPath) if err != nil { return err diff --git a/server/ftp/fsread.go b/server/ftp/fsread.go index 54a3de8f2c..ccddb842b4 100644 --- a/server/ftp/fsread.go +++ b/server/ftp/fsread.go @@ -25,13 +25,17 @@ type FileDownloadProxy struct { } func OpenDownload(ctx context.Context, reqPath string, offset int64) (*FileDownloadProxy, error) { - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return nil, errs.PermissionDenied + } meta, err := op.GetNearestMeta(reqPath) if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) { return nil, err } ctx = context.WithValue(ctx, conf.MetaKey, meta) - if !common.CanAccess(user, meta, reqPath, ctx.Value(conf.MetaPassKey).(string)) { + metaPass, _ := ctx.Value(conf.MetaPassKey).(string) + if !common.CanAccess(user, meta, reqPath, metaPass) { return nil, errs.PermissionDenied } @@ -113,7 +117,10 @@ func (o *OsFileInfoAdapter) Sys() any { } func Stat(ctx context.Context, path string) (os.FileInfo, error) { - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return nil, errs.PermissionDenied + } reqPath, err := user.JoinPath(path) if err != nil { return nil, err @@ -123,7 +130,8 @@ func Stat(ctx context.Context, path string) (os.FileInfo, error) { return nil, err } ctx = context.WithValue(ctx, conf.MetaKey, meta) - if !common.CanAccess(user, meta, reqPath, ctx.Value(conf.MetaPassKey).(string)) { + metaPass, _ := ctx.Value(conf.MetaPassKey).(string) + if !common.CanAccess(user, meta, reqPath, metaPass) { return nil, errs.PermissionDenied } if ret, err := StatStage(reqPath); !errors.Is(err, errs.ObjectNotFound) { @@ -137,7 +145,10 @@ func Stat(ctx context.Context, path string) (os.FileInfo, error) { } func List(ctx context.Context, path string) ([]os.FileInfo, error) { - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return nil, errs.PermissionDenied + } reqPath, err := user.JoinPath(path) if err != nil { return nil, err @@ -147,7 +158,8 @@ func List(ctx context.Context, path string) ([]os.FileInfo, error) { return nil, err } ctx = context.WithValue(ctx, conf.MetaKey, meta) - if !common.CanAccess(user, meta, reqPath, ctx.Value(conf.MetaPassKey).(string)) { + metaPass, _ := ctx.Value(conf.MetaPassKey).(string) + if !common.CanAccess(user, meta, reqPath, metaPass) { return nil, errs.PermissionDenied } objs, err := fs.List(ctx, reqPath, &fs.ListArgs{}) diff --git a/server/ftp/fsup.go b/server/ftp/fsup.go index 7a96a4f655..895210cf55 100644 --- a/server/ftp/fsup.go +++ b/server/ftp/fsup.go @@ -32,7 +32,10 @@ type FileUploadProxy struct { } func uploadAuth(ctx context.Context, path string) error { - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return errs.PermissionDenied + } if !user.CanFTPManage() { return errs.PermissionDenied } @@ -101,7 +104,10 @@ func (f *FileUploadProxy) Close() error { if _, err := f.buffer.Seek(0, io.SeekStart); err != nil { return err } - user := f.ctx.Value(conf.UserKey).(*model.User) + user, ok := f.ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return errs.PermissionDenied + } sf, borrow, err := MakeStage(f.ctx, f.buffer, size, f.path, func(target string) { ctx := context.WithValue(context.Background(), conf.UserKey, user) dstDir, dstBase := stdpath.Split(target) @@ -121,7 +127,9 @@ func (f *FileUploadProxy) Close() error { return fmt.Errorf("failed make stage for [%s]: %+v", f.path, err) } if f.trunc { - _ = fs.Remove(f.ctx, f.path) + if err := fs.Remove(f.ctx, f.path); err != nil { + return err + } } s := &stream.FileStream{ Obj: &model.Object{ @@ -242,8 +250,12 @@ func (f *FileUploadWithLengthProxy) Close() error { if err != nil { return err } - err = <-f.errChan - return err + select { + case err = <-f.errChan: + return err + case <-time.After(5 * time.Minute): + return context.DeadlineExceeded + } } else { data := f.first512Bytes[:f.pFirst] contentType := http.DetectContentType(data) diff --git a/server/handles/webauthn.go b/server/handles/webauthn.go index 6848020fd0..7256fc8892 100644 --- a/server/handles/webauthn.go +++ b/server/handles/webauthn.go @@ -231,7 +231,11 @@ func GetAuthnCredentials(c *gin.Context) { FingerPrint string `json:"fingerprint"` } user := c.Request.Context().Value(conf.UserKey).(*model.User) - credentials := user.WebAuthnCredentials() + credentials, err := user.WebAuthnCredentials() + if err != nil { + common.FailResp(c, err) + return + } res := make([]WebAuthnCredentials, 0, len(credentials)) for _, v := range credentials { credential := WebAuthnCredentials{ diff --git a/server/webdav/file.go b/server/webdav/file.go index ea60997359..07598dddb0 100644 --- a/server/webdav/file.go +++ b/server/webdav/file.go @@ -37,7 +37,10 @@ func moveFiles(ctx context.Context, src, dst string, overwrite bool) (status int dstDir := path.Dir(dst) srcName := path.Base(src) dstName := path.Base(dst) - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } if srcDir != dstDir && !user.CanMove() { return http.StatusForbidden, nil } @@ -80,7 +83,10 @@ func moveFiles(ctx context.Context, src, dst string, overwrite bool) (status int func copyFiles(ctx context.Context, src, dst string, overwrite bool) (status int, err error) { srcDir := path.Dir(src) dstDir := path.Dir(dst) - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } if !user.CanCopy() { return http.StatusForbidden, nil } @@ -141,16 +147,10 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn for _, fileInfo := range objs { filename := path.Join(name, fileInfo.GetName()) - if err != nil { - if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { - return err - } - } else { - err = walkFS(ctx, depth, filename, fileInfo, walkFn) - if err != nil { - if !fileInfo.IsDir() || err != filepath.SkipDir { - return err - } + walkErr := walkFS(ctx, depth, filename, fileInfo, walkFn) + if walkErr != nil { + if !fileInfo.IsDir() || walkErr != filepath.SkipDir { + return walkErr } } } diff --git a/server/webdav/webdav.go b/server/webdav/webdav.go index 06d1431ac3..b4068a1024 100644 --- a/server/webdav/webdav.go +++ b/server/webdav/webdav.go @@ -198,7 +198,10 @@ func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status return status, err } ctx := r.Context() - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } reqPath, err = user.JoinPath(reqPath) if err != nil { return http.StatusForbidden, err @@ -226,7 +229,10 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta } // TODO: check locks for read-only access?? ctx := r.Context() - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } password, _ := ctx.Value(conf.MetaPassKey).(string) reqPath, err = user.JoinPath(reqPath) if err != nil { @@ -252,7 +258,10 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta return http.StatusMethodNotAllowed, nil } // Let ServeContent determine the Content-Type header. - storage, _ := fs.GetStorage(reqPath, &fs.GetStoragesArgs{}) + storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{}) + if err != nil || storage == nil { + return http.StatusInternalServerError, err + } if storage.GetStorage().Webdav302() { link, _, err := fs.Link(ctx, reqPath, model.LinkArgs{IP: utils.ClientIP(r), Header: r.Header, Redirect: true}) if err != nil { @@ -302,7 +311,10 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status i defer release() ctx := r.Context() - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } if !user.CanRemove() { return http.StatusForbidden, nil } @@ -358,7 +370,10 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, // TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz' // comments in http.checkEtag. ctx := r.Context() - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } reqPath, err = user.JoinPath(reqPath) if err != nil { return http.StatusForbidden, err @@ -435,7 +450,10 @@ func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status in defer release() ctx := r.Context() - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } reqPath, err = user.JoinPath(reqPath) if err != nil { return http.StatusForbidden, err @@ -509,7 +527,10 @@ func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status } ctx := r.Context() - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } src, err = user.JoinPath(src) if err != nil { return http.StatusForbidden, err @@ -573,7 +594,10 @@ func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus } ctx := r.Context() - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } token, ld, now, created := "", LockDetails{}, time.Now(), false if li == (lockInfo{}) { // An empty lockInfo means to refresh the lock. @@ -677,13 +701,19 @@ func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status i return http.StatusBadRequest, errInvalidLockToken } t = t[1 : len(t)-1] + if t == "" { + return http.StatusBadRequest, errInvalidLockToken + } reqPath, status, err := h.stripPrefix(r.URL.Path) if err != nil { return status, err } ctx := r.Context() - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } reqPath, err = user.JoinPath(reqPath) if err != nil { return http.StatusForbidden, err @@ -718,7 +748,10 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status ctx := r.Context() userAgent := r.Header.Get("User-Agent") ctx = context.WithValue(ctx, conf.UserAgentKey, userAgent) - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } password, _ := ctx.Value(conf.MetaPassKey).(string) reqPath, err = user.JoinPath(reqPath) if err != nil { @@ -805,7 +838,10 @@ func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (statu defer release() ctx := r.Context() - user := ctx.Value(conf.UserKey).(*model.User) + user, ok := ctx.Value(conf.UserKey).(*model.User) + if !ok || user == nil { + return http.StatusForbidden, errs.PermissionDenied + } reqPath, err = user.JoinPath(reqPath) if err != nil { return http.StatusForbidden, err