Compare commits
68 commits
Author | SHA1 | Date | |
---|---|---|---|
|
00155eaaf6 | ||
|
d94f80c8da | ||
|
bd5eb03d9c | ||
|
6ba1198c47 | ||
|
b5c821795a | ||
|
b2926377b7 | ||
|
99f47ca4e7 | ||
|
fef388d8cb | ||
|
92849ca473 | ||
|
0952887157 | ||
|
d010b26e1c | ||
|
58de410850 | ||
|
54bc3ea87d | ||
|
64a2f7aa4f | ||
|
55be9f0b9c | ||
|
97ffa0394f | ||
|
dc91ec2056 | ||
|
356795f8b0 | ||
|
3efcd94e14 | ||
|
34bc21b3b7 | ||
|
37845c2936 | ||
|
47924716c1 | ||
|
1d60505629 | ||
|
9daf0ba767 | ||
|
bdae378569 | ||
|
363770ab84 | ||
|
8bc08b25dc | ||
|
e0c1b974c9 | ||
|
39cf9f6943 | ||
|
d650defa08 | ||
|
c5c42f072b | ||
|
bd5b32101f | ||
|
8208ac817d | ||
|
a99c4879de | ||
|
01b666a78f | ||
|
8294952474 | ||
|
7fb5b1b996 | ||
|
2749a98f26 | ||
|
08526da153 | ||
|
8269adf176 | ||
|
0cddcba5a7 | ||
|
3bd1eeacc1 | ||
|
1698ec2eb3 | ||
|
07710ad98d | ||
|
f63bf7093c | ||
|
0597bf1047 | ||
|
5bde4b92a2 | ||
|
faa994e3b3 | ||
|
68cc1a8e2c | ||
|
9c775e2213 | ||
|
6c94173ca1 | ||
|
d1e0560d28 | ||
|
52a94b2593 | ||
|
9550fd2921 | ||
|
a6549b08f9 | ||
|
ba3e2ecb5f | ||
|
2bd3b46e3f | ||
|
7831ddaede | ||
|
613f2f1c24 | ||
|
525f33a07a | ||
|
3f2604d33f | ||
|
b823bb04d2 | ||
|
9ba92d9495 | ||
|
0127fc188b | ||
|
3c7a651d27 | ||
|
50a3c0d911 | ||
|
b2bea85add | ||
|
61bc0065f9 |
130 changed files with 3909 additions and 1809 deletions
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
|
@ -1,11 +1,11 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 2
|
||||
#- package-ecosystem: "gomod"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
# open-pull-requests-limit: 2
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
|
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
|
@ -163,7 +163,7 @@ jobs:
|
|||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
builder: ${{ steps.builder.outputs.name }}
|
||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -5,7 +5,7 @@ on:
|
|||
tags: 'v*'
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.22.3
|
||||
GO_VERSION: 1.22.4
|
||||
|
||||
jobs:
|
||||
prepare-sources-with-deps:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.22-alpine3.19 AS builder
|
||||
FROM golang:1.22-alpine3.20 AS builder
|
||||
|
||||
ENV GOFLAGS="-mod=readonly"
|
||||
|
||||
|
@ -25,7 +25,7 @@ RUN set -xe && \
|
|||
export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --abbrev=8 --dirty)} && \
|
||||
go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -trimpath -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -v -o sftpgo
|
||||
|
||||
FROM alpine:3.19
|
||||
FROM alpine:3.20
|
||||
|
||||
# Set to "true" to install jq and the optional git and rsync dependencies
|
||||
ARG INSTALL_OPTIONAL_PACKAGES=false
|
||||
|
|
|
@ -25,7 +25,7 @@ The open source license grant you freedom but not assurance of help. So why woul
|
|||
|
||||
Supporting the project benefit businesses and the community because if the project is financially sustainable, using this business model, we don't have to restrict features and/or switch to an [Open-core](https://en.wikipedia.org/wiki/Open-core_model) model. The technology stays truly open source. Everyone wins.
|
||||
|
||||
You should support the project for its ongoing maintenance, even if you don't have any questions or need new features. If SFTPGo is no longer maintained you will have troubles and your company will lose money: bugs and security vulnerabilities will no longer be fixed, new algorithms will not be added to support newer clients, and so on. You will be forced to switch to a similar proprietary product and pay for its license and the migration cost.
|
||||
It is important to understand that you should support SFTPGo and any other Open Source project you rely on for ongoing maintenance, even if you don't have any questions or need new features, to mitigate the business risk of a project you depend on going unmaintained, with its security and development velocity implications.
|
||||
|
||||
### Thank you to our sponsors
|
||||
|
||||
|
@ -53,7 +53,7 @@ You can use SFTPGo for free, respecting the obligations of the Open Source licen
|
|||
|
||||
Use [discussions](https://github.com/drakkan/sftpgo/discussions) to ask questions and get support from the community.
|
||||
|
||||
If you report an invalid issue and/or ask for step-by-step support, your issue will be closed as invalid without further explanation. Invalid bug reports left open may confuse other users. Thanks for understanding.
|
||||
If you report an invalid issue and/or ask for step-by-step support, your issue will be closed as invalid without further explanation and/or the "support request" label will be added. Invalid bug reports may confuse other users. Thanks for understanding.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
@ -61,7 +61,7 @@ You can read more about supported features and documentation at [sftpgo.github.i
|
|||
|
||||
## Release Cadence
|
||||
|
||||
SFTPGo releases are feature-driven, we don't have a fixed time based schedule. As a rough estimate, you can expect 1 or 2 new releases per year.
|
||||
SFTPGo releases are feature-driven, we don't have a fixed time based schedule. As a rough estimate, you can expect 1 or 2 new major releases per year and several bug fix releases.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
|
164
go.mod
164
go.mod
|
@ -3,30 +3,30 @@ module github.com/drakkan/sftpgo/v2
|
|||
go 1.22.2
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.41.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2
|
||||
cloud.google.com/go/storage v1.43.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.13
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.13
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.18
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.5
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.54.0
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.7
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.9
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.23.3
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.8
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
github.com/fclairamb/ftpserverlib v0.24.0
|
||||
github.com/fclairamb/ftpserverlib v0.24.1
|
||||
github.com/fclairamb/go-log v0.5.0
|
||||
github.com/go-acme/lego/v4 v4.16.1
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-acme/lego/v4 v4.17.4
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
|
@ -34,15 +34,15 @@ require (
|
|||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-hclog v1.6.3
|
||||
github.com/hashicorp/go-plugin v1.6.1
|
||||
github.com/hashicorp/go-retryablehttp v0.7.6
|
||||
github.com/jackc/pgx/v5 v5.5.5
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
github.com/jlaffaye/ftp v0.2.0
|
||||
github.com/klauspost/compress v1.17.8
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.21
|
||||
github.com/klauspost/compress v1.17.9
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.0
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/mhale/smtpd v0.8.3
|
||||
github.com/minio/sio v0.3.1
|
||||
github.com/minio/sio v0.4.0
|
||||
github.com/otiai10/copy v1.14.0
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317
|
||||
|
@ -51,55 +51,55 @@ require (
|
|||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/cors v1.11.0
|
||||
github.com/rs/xid v1.5.0
|
||||
github.com/rs/zerolog v1.32.0
|
||||
github.com/sftpgo/sdk v0.1.7
|
||||
github.com/shirou/gopsutil/v3 v3.24.4
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/sftpgo/sdk v0.1.8
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/studio-b12/gowebdav v0.9.0
|
||||
github.com/subosito/gotenv v1.6.0
|
||||
github.com/unrolled/secure v1.14.0
|
||||
github.com/unrolled/secure v1.15.0
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
github.com/wneessen/go-mail v0.4.1
|
||||
github.com/wneessen/go-mail v0.4.2
|
||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
||||
go.etcd.io/bbolt v1.3.10
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
gocloud.dev v0.37.0
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/oauth2 v0.20.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/term v0.20.0
|
||||
gocloud.dev v0.38.0
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/net v0.27.0
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/sys v0.22.0
|
||||
golang.org/x/term v0.22.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/api v0.180.0
|
||||
google.golang.org/api v0.189.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.113.0 // indirect
|
||||
cloud.google.com/go/auth v0.4.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.8 // indirect
|
||||
cloud.google.com/go v0.115.0 // indirect
|
||||
cloud.google.com/go/auth v0.7.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.12 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 // indirect
|
||||
github.com/aws/smithy-go v1.20.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
|
@ -109,16 +109,16 @@ require (
|
|||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.3 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
|
@ -126,31 +126,31 @@ require (
|
|||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.5 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.59 // indirect
|
||||
github.com/miekg/dns v1.1.61 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.53.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.0 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
|
@ -161,30 +161,30 @@ require (
|
|||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
|
||||
go.opentelemetry.io/otel v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.26.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/mod v0.19.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/tools v0.21.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240725223205-93522f1f2a9f // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20240510125431-4617586dfa1c
|
||||
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20240603150004-6a8f643fbf2e
|
||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f
|
||||
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20240509175024-33071fb6437f
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20240726170110-f4e4a4627441
|
||||
)
|
||||
|
|
364
go.sum
364
go.sum
|
@ -1,34 +1,33 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.113.0 h1:g3C70mn3lWfckKBiCVsAshabrDg01pQ0pnX1MNtnMkA=
|
||||
cloud.google.com/go v0.113.0/go.mod h1:glEqlogERKYeePz6ZdkcLJ28Q2I6aERgDDErBg9GzO8=
|
||||
cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
|
||||
cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
|
||||
cloud.google.com/go/compute v1.26.0 h1:uHf0NN2nvxl1Gh4QO83yRCOdMK4zivtMS5gv0dEX0hg=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
|
||||
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
|
||||
cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY=
|
||||
cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc=
|
||||
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
|
||||
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
|
||||
cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0=
|
||||
cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80=
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
||||
cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
|
||||
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
cloud.google.com/go/iam v1.1.12 h1:JixGLimRrNGcxvJEQ8+clfLxPlbeZA6MuRJ+qJNQ5Xw=
|
||||
cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg=
|
||||
cloud.google.com/go/kms v1.18.3 h1:8+Z2S4bQDSCdghB5ZA5dVDDJTLmnkRlowtFiXqMFd74=
|
||||
cloud.google.com/go/kms v1.18.3/go.mod h1:y/Lcf6fyhbdn7MrG1VaDqXxM8rhOBc5rWcWAhcvZjQU=
|
||||
cloud.google.com/go/longrunning v0.5.10 h1:eB/BniENNRKhjz/xgiillrdcH3G74TGSl3BXinGlI7E=
|
||||
cloud.google.com/go/longrunning v0.5.10/go.mod h1:tljz5guTr5oc/qhlUjBlk7UAIFMOGuPNxkNDZXlLics=
|
||||
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
|
||||
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 h1:YUUxeiOWgdAQE3pXt2H7QXzZs0q8UBjgRbl56qo8GYM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2/go.mod h1:dmXQgZuiSubAecswZE+Sm8jkvEa7kQgTPVRvwL/nd0E=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0 h1:Be6KInmFEKV81c0pOAEbRYehLMwmmGI1exuFj248AMk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0/go.mod h1:WCPBHsOXfBVnivScjs2ypRfimjEW0qPVLGgJkZlrIOA=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
|
@ -40,59 +39,55 @@ github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHc
|
|||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4=
|
||||
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.17 h1:9b1Os1s11mF5qTIKLgSsyPG810di2+ySSLIIt9bwe9I=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.17/go.mod h1:9Wp7tDOMhv0+sb/FTRAkbHNQ7abYDnoJRzm5AAtCnTc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.18 h1:fUHit8Pe+2dWEHtxpOVDTOSQR257iH24HjT17DAz6qs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.18/go.mod h1:IX1n1o870YYxzqN56w26s7FrO5Zaw/hdatxhJDiEf2U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.9 h1:TC2vjvaAv1VNl9A0rm+SeuBjrzXnrlwk6Yop+gKRi38=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.9/go.mod h1:WPv2FRnkIOoDv/8j2gSUsI4qDc7392w5anFB/I89GZ8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.5 h1:p2PxN+OO28p2bCCXE79sJfFBaSohwxa24bQdjuyPZCs=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.5/go.mod h1:Q01yJLephuOzv6IYzcknrpVAriOqB66+qtGnpqgw9UE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.2 h1:rq2hglTQM3yHZvOPVMtNvLS5x6hijx7JvRDgKiTNDGQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.2/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.54.0 h1:Ls94RY3P6HtB88JkzXo1lHrXzonHPpNR//OSAV63mSE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.54.0/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.7 h1:4cziOtpDwtgcb+wTYRzz8C+GoH1XySy0p7j4oBbqPQE=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.7/go.mod h1:3Ba++UwWd154xtP4FRX5pUK3Gt4up5sDHCve6kVfE+g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
|
||||
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.23.3 h1:ZkaFS2PmZFk710zqw7Yki2douIA6fL5JVvy7rP4q9qg=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.23.3/go.mod h1:ZK5KBD+u8g1Frfqe1atGaH19dSnY9SbHuSUimYv1cy0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 h1:sZXIzO38GZOU+O0C+INqbH7C2yALwfMWpd64tONS/NE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4 h1:NgRFYyFpiMD62y4VPXh4DosPFbZd4vdMVBWKk0VmWXc=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4/go.mod h1:TKKN7IQoM7uTnyuFm9bm9cw5P//ZYTl4m3htBWQ1G/c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
|
||||
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
||||
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
|
@ -104,11 +99,10 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.8 h1:53yoUo4+EtrC1NrAEgnnad4AS3ntNvGup1PAXZ7UmpE=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.8/go.mod h1:9uH5jK4yQ3ZQUT9IXe4I2fHzMIF5+JC/oOdzTRgJYJk=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -119,12 +113,12 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnN
|
|||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 h1:EW9gIJRmt9lzk66Fhh4S8VEtURA6QHZqGeSRE9Nb2/U=
|
||||
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/drakkan/crypto v0.0.0-20240509175024-33071fb6437f h1:4+0I7deWH0/8dTS1xVgFrNSq7aaNvKrfaqLlfFBNV64=
|
||||
github.com/drakkan/crypto v0.0.0-20240509175024-33071fb6437f/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
github.com/drakkan/crypto v0.0.0-20240726170110-f4e4a4627441 h1:1iNKXQ0IOEUADDah6knbVh2SBhDH0Bu0kkrOXpTkXvA=
|
||||
github.com/drakkan/crypto v0.0.0-20240726170110-f4e4a4627441/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f h1:S9JUlrOzjK58UKoLqqb40YLyVlt0bcIFtYrvnanV3zc=
|
||||
github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f/go.mod h1:4p8lUl4vQ80L598CygL+3IFtm+3nggvvW/palOlViwE=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20240510125431-4617586dfa1c h1:cO3eqB2Bjv8WM8HUDfajAt3bFFGj6FUQ2eIxsxVvyC8=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20240510125431-4617586dfa1c/go.mod h1:+9afJRWESpCq4/O8Vr00Q2jfinRxP6PiCpXph6CgGuc=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20240603150004-6a8f643fbf2e h1:VBpqQeChkGXSV1FXCtvd3BJTyB+DcMgiu7SfkpsGuKw=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20240603150004-6a8f643fbf2e/go.mod h1:aAwyOAC6IIe+IZeeGD1QjuE3GGDzqW/c5Xtn+Dp0JUM=
|
||||
github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb h1:067/Uo8cfeY7QC0yzWCr/RImuNcM0rLWAsBUyMks59o=
|
||||
github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb/go.mod h1:zOVb1QDhwwqWn2L2qZ0U3swMSO4GTSNyIwXCGO/UGWE=
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
|
||||
|
@ -144,23 +138,23 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
|||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-acme/lego/v4 v4.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ=
|
||||
github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-acme/lego/v4 v4.17.4 h1:h0nePd3ObP6o7kAkndtpTzCw8shOZuWckNYeUQwo36Q=
|
||||
github.com/go-acme/lego/v4 v4.17.4/go.mod h1:dU94SvPNqimEeb7EVilGGSnS0nU1O5Exir0pQ4QFL4U=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-jose/go-jose/v4 v4.0.3 h1:o8aphO8Hv6RPmH+GfzVuyf7YXSBibp+8YyHdOoDESGo=
|
||||
github.com/go-jose/go-jose/v4 v4.0.3/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
|
@ -168,8 +162,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
|||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
|
@ -197,14 +191,12 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
||||
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
@ -215,8 +207,8 @@ github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
|||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
@ -228,8 +220,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
|||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI=
|
||||
github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
|
@ -238,21 +230,16 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
|
@ -265,19 +252,18 @@ github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N
|
|||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
|
||||
github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
|
||||
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM=
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.0 h1:0zs7Ya6+39qoit7gwAf+cYm1zzgS3fceIdo7RmQ5lkw=
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.0/go.mod h1:Xpw9QIaUGiIUD1Wx0NcY1sIHwFf8lDuZn/cmxtXYRys=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
|
@ -296,14 +282,16 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
|
|||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8=
|
||||
github.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
|
||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/minio/sio v0.3.1 h1:d59r5RTHb1OsQaSl1EaTWurzMMDRLA5fgNmjzD4eVu4=
|
||||
github.com/minio/sio v0.3.1/go.mod h1:S0ovgVgc+sTlQyhiXA1ppBLv7REM7TYi5yyq2qL/Y6o=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/minio/sio v0.4.0 h1:u4SWVEm5lXSqU42ZWawV0D9I5AZ5YMmo2RXpEQ/kRhc=
|
||||
github.com/minio/sio v0.4.0/go.mod h1:oBSjJeGbBdRMZZwna07sX9EFzZy+ywu5aofRiV1g79I=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||
|
@ -322,7 +310,6 @@ github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317/go.mod h1:KMKI0t3T6hfA
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
|
@ -334,32 +321,32 @@ github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJL
|
|||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
|
||||
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
|
||||
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
|
||||
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sftpgo/sdk v0.1.7 h1:lzOKBDnKb1PpSMlskqCPxBYKxVWz34uMBhT78r/13iA=
|
||||
github.com/sftpgo/sdk v0.1.7/go.mod h1:ler/KG6kMLlsOs/8s6dVN3oom+z+NkbXBVWO//Cv/WA=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
||||
github.com/sftpgo/sdk v0.1.8 h1:HAywJl9jZnigFGztA/CWLieOW+R+HH6js6o6/qYvuSY=
|
||||
github.com/sftpgo/sdk v0.1.8/go.mod h1:Isl0IEzS/Muvh8Fr4X+NWFsOS/fZQHRD4oPQpoY7C4g=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
|
@ -370,12 +357,12 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
|||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
@ -394,18 +381,16 @@ github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4
|
|||
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/unrolled/secure v1.14.0 h1:u9vJTU/pR4Bny0ntLUMxdfLtmIRGvQf2sEFuA0TG9AE=
|
||||
github.com/unrolled/secure v1.14.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/unrolled/secure v1.15.0 h1:q7x+pdp8jAHnbzxu6UheP8fRlG/rwYTb8TPuQ3rn9Og=
|
||||
github.com/unrolled/secure v1.15.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
||||
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||
github.com/wneessen/go-mail v0.4.1 h1:m2rSg/sc8FZQCdtrV5M8ymHYOFrC6KJAQAIcgrXvqoo=
|
||||
github.com/wneessen/go-mail v0.4.1/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
|
||||
github.com/wneessen/go-mail v0.4.2 h1:wISuU9LOGqrA7pxy7OipRtwoExXTzuGKmAjb8gYwc00=
|
||||
github.com/wneessen/go-mail v0.4.2/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
|
||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a h1:XfF01GyP+0eWCaVp0y6rNN+kFp7pt9Da4UUYrJ5XPWA=
|
||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a/go.mod h1:aXb8yZQEWo1XHGMf1qQfnb83GR/EJ2EBlwtUgAaNBoE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
|
@ -415,34 +400,37 @@ go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
|||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
|
||||
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
|
||||
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
|
||||
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
|
||||
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
|
||||
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro=
|
||||
gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco=
|
||||
gocloud.dev v0.38.0 h1:SpxfaOc/Fp4PeO8ui7wRcCZV0EgXZ+IWcVSLn6ZMSw0=
|
||||
gocloud.dev v0.38.0/go.mod h1:3XjKvd2E5iVNu/xFImRzjN0d/fkNHe4s0RiKidpEUMQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -451,17 +439,21 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
||||
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -482,25 +474,28 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -511,34 +506,34 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
|
|||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4=
|
||||
google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
|
||||
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8 h1:XpH03M6PDRKTo1oGfZBXu2SzwcbfxUokgobVinuUZoU=
|
||||
google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8/go.mod h1:OLh2Ylz+WlYAJaSBRpJIJLP8iQP+8da+fpxbwNEAV/o=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM=
|
||||
google.golang.org/genproto v0.0.0-20240725223205-93522f1f2a9f h1:htT2I9bZvGm+110zq8bIErMX+WgBWxCzV3ChwbvnKnc=
|
||||
google.golang.org/genproto v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:Sk3mLpoDFTAp6R4OvlcUgaG4ISTspKeFsIAXMn9Bm4Y=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f h1:b1Ln/PG8orm0SsBbHZWke8dDp2lrCD4jSmfglFpTZbk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f h1:RARaIm8pxYuxyNPbBQf5igT7XdOyCNtat1qAT2ZxjU4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
|
||||
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
@ -548,8 +543,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
|||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
@ -558,9 +553,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -249,7 +250,7 @@ func (c *Configuration) Initialize(configDir string) error {
|
|||
if c.RenewDays < 1 {
|
||||
return fmt.Errorf("invalid number of days remaining before renewal: %d", c.RenewDays)
|
||||
}
|
||||
if !util.Contains(supportedKeyTypes, c.KeyType) {
|
||||
if !slices.Contains(supportedKeyTypes, c.KeyType) {
|
||||
return fmt.Errorf("invalid key type %q", c.KeyType)
|
||||
}
|
||||
caURL, err := url.Parse(c.CAEndpoint)
|
||||
|
|
|
@ -40,8 +40,8 @@ Please take a look at the usage below to customize the options.`,
|
|||
Run: func(_ *cobra.Command, _ []string) {
|
||||
logger.DisableLogger()
|
||||
logger.EnableConsoleLogger(zerolog.DebugLevel)
|
||||
if revertProviderTargetVersion != 28 {
|
||||
logger.WarnToConsole("Unsupported target version, 28 is the only supported one")
|
||||
if revertProviderTargetVersion != 29 {
|
||||
logger.WarnToConsole("Unsupported target version, 29 is the only supported one")
|
||||
os.Exit(1)
|
||||
}
|
||||
configDir = util.CleanDirInput(configDir)
|
||||
|
@ -71,7 +71,7 @@ Please take a look at the usage below to customize the options.`,
|
|||
|
||||
func init() {
|
||||
addConfigFlags(revertProviderCmd)
|
||||
revertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, "to-version", 28, `28 means the version supported in v2.5.x`)
|
||||
revertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, "to-version", 29, `29 means the version supported in v2.6.x`)
|
||||
|
||||
rootCmd.AddCommand(revertProviderCmd)
|
||||
}
|
||||
|
|
|
@ -16,13 +16,21 @@ package cmd
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/subosito/gotenv"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/service"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
envFileMaxSize = 1048576
|
||||
)
|
||||
|
||||
var (
|
||||
serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
|
@ -34,9 +42,11 @@ $ sftpgo serve
|
|||
|
||||
Please take a look at the usage below to customize the startup options`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
configDir := util.CleanDirInput(configDir)
|
||||
checkServeParamsFromEnvFiles(configDir)
|
||||
service.SetGraceTime(graceTime)
|
||||
service := service.Service{
|
||||
ConfigDir: util.CleanDirInput(configDir),
|
||||
ConfigDir: configDir,
|
||||
ConfigFile: configFile,
|
||||
LogFilePath: logFilePath,
|
||||
LogMaxSize: logMaxSize,
|
||||
|
@ -62,6 +72,75 @@ Please take a look at the usage below to customize the startup options`,
|
|||
}
|
||||
)
|
||||
|
||||
func setIntFromEnv(receiver *int, val string) {
|
||||
converted, err := strconv.Atoi(val)
|
||||
if err == nil {
|
||||
*receiver = converted
|
||||
}
|
||||
}
|
||||
|
||||
func setBoolFromEnv(receiver *bool, val string) {
|
||||
converted, err := strconv.ParseBool(strings.TrimSpace(val))
|
||||
if err == nil {
|
||||
*receiver = converted
|
||||
}
|
||||
}
|
||||
|
||||
func checkServeParamsFromEnvFiles(configDir string) { //nolint:gocyclo
|
||||
// The logger is not yet initialized here, we have no way to report errors.
|
||||
envd := filepath.Join(configDir, "env.d")
|
||||
entries, err := os.ReadDir(envd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
info, err := entry.Info()
|
||||
if err == nil && info.Mode().IsRegular() {
|
||||
envFile := filepath.Join(envd, entry.Name())
|
||||
if info.Size() > envFileMaxSize {
|
||||
continue
|
||||
}
|
||||
envVars, err := gotenv.Read(envFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for k, v := range envVars {
|
||||
if _, isSet := os.LookupEnv(k); isSet {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case "SFTPGO_LOG_FILE_PATH":
|
||||
logFilePath = v
|
||||
case "SFTPGO_LOG_MAX_SIZE":
|
||||
setIntFromEnv(&logMaxSize, v)
|
||||
case "SFTPGO_LOG_MAX_BACKUPS":
|
||||
setIntFromEnv(&logMaxBackups, v)
|
||||
case "SFTPGO_LOG_MAX_AGE":
|
||||
setIntFromEnv(&logMaxAge, v)
|
||||
case "SFTPGO_LOG_COMPRESS":
|
||||
setBoolFromEnv(&logCompress, v)
|
||||
case "SFTPGO_LOG_LEVEL":
|
||||
logLevel = v
|
||||
case "SFTPGO_LOG_UTC_TIME":
|
||||
setBoolFromEnv(&logUTCTime, v)
|
||||
case "SFTPGO_CONFIG_FILE":
|
||||
configFile = v
|
||||
case "SFTPGO_LOADDATA_FROM":
|
||||
loadDataFrom = v
|
||||
case "SFTPGO_LOADDATA_MODE":
|
||||
setIntFromEnv(&loadDataMode, v)
|
||||
case "SFTPGO_LOADDATA_CLEAN":
|
||||
setBoolFromEnv(&loadDataClean, v)
|
||||
case "SFTPGO_LOADDATA_QUOTA_SCAN":
|
||||
setIntFromEnv(&loadDataQuotaScan, v)
|
||||
case "SFTPGO_GRACE_TIME":
|
||||
setIntFromEnv(&graceTime, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
addServeFlags(serveCmd)
|
||||
|
|
|
@ -17,7 +17,6 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
@ -31,9 +30,7 @@ var (
|
|||
Short: "Start the SFTPGo Windows Service",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
configDir = util.CleanDirInput(configDir)
|
||||
if !filepath.IsAbs(logFilePath) && util.IsFileInputValid(logFilePath) {
|
||||
logFilePath = filepath.Join(configDir, logFilePath)
|
||||
}
|
||||
checkServeParamsFromEnvFiles(configDir)
|
||||
service.SetGraceTime(graceTime)
|
||||
s := service.Service{
|
||||
ConfigDir: configDir,
|
||||
|
@ -45,6 +42,10 @@ var (
|
|||
LogCompress: logCompress,
|
||||
LogLevel: logLevel,
|
||||
LogUTCTime: logUTCTime,
|
||||
LoadDataFrom: loadDataFrom,
|
||||
LoadDataMode: loadDataMode,
|
||||
LoadDataQuotaScan: loadDataQuotaScan,
|
||||
LoadDataClean: loadDataClean,
|
||||
Shutdown: make(chan bool),
|
||||
}
|
||||
winService := service.WindowsService{
|
||||
|
|
|
@ -17,10 +17,9 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -117,7 +116,7 @@ func (c Config) Initialize() error {
|
|||
}
|
||||
// don't validate args, we allow to pass empty arguments
|
||||
if cmd.Hook != "" {
|
||||
if !util.Contains(supportedHooks, cmd.Hook) {
|
||||
if !slices.Contains(supportedHooks, cmd.Hook) {
|
||||
return fmt.Errorf("invalid hook name %q, supported values: %+v", cmd.Hook, supportedHooks)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
@ -86,7 +87,7 @@ func InitializeActionHandler(handler ActionHandler) {
|
|||
func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) (int, error) {
|
||||
var event *notifier.FsEvent
|
||||
hasNotifiersPlugin := plugin.Handler.HasNotifiers()
|
||||
hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
|
||||
hasHook := slices.Contains(Config.Actions.ExecuteOn, operation)
|
||||
hasRules := eventManager.hasFsRules()
|
||||
if !hasHook && !hasNotifiersPlugin && !hasRules {
|
||||
return 0, nil
|
||||
|
@ -132,7 +133,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
|
|||
fileSize int64, err error, elapsed int64, metadata map[string]string,
|
||||
) error {
|
||||
hasNotifiersPlugin := plugin.Handler.HasNotifiers()
|
||||
hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
|
||||
hasHook := slices.Contains(Config.Actions.ExecuteOn, operation)
|
||||
hasRules := eventManager.hasFsRules()
|
||||
if !hasHook && !hasNotifiersPlugin && !hasRules {
|
||||
return nil
|
||||
|
@ -173,7 +174,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
|
|||
}
|
||||
}
|
||||
if hasHook {
|
||||
if util.Contains(Config.Actions.ExecuteSync, operation) {
|
||||
if slices.Contains(Config.Actions.ExecuteSync, operation) {
|
||||
_, err := actionHandler.Handle(notification)
|
||||
return err
|
||||
}
|
||||
|
@ -247,7 +248,7 @@ func newActionNotification(
|
|||
type defaultActionHandler struct{}
|
||||
|
||||
func (h *defaultActionHandler) Handle(event *notifier.FsEvent) (int, error) {
|
||||
if !util.Contains(Config.Actions.ExecuteOn, event.Action) {
|
||||
if !slices.Contains(Config.Actions.ExecuteOn, event.Action) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -163,13 +164,20 @@ var (
|
|||
rateLimiters map[string][]*rateLimiter
|
||||
isShuttingDown atomic.Bool
|
||||
ftpLoginCommands = []string{"PASS", "USER"}
|
||||
fnUpdateBranding func(*dataprovider.BrandingConfigs)
|
||||
)
|
||||
|
||||
// SetUpdateBrandingFn sets the function to call to update branding configs.
|
||||
func SetUpdateBrandingFn(fn func(*dataprovider.BrandingConfigs)) {
|
||||
fnUpdateBranding = fn
|
||||
}
|
||||
|
||||
// Initialize sets the common configuration
|
||||
func Initialize(c Configuration, isShared int) error {
|
||||
isShuttingDown.Store(false)
|
||||
util.SetUmask(c.Umask)
|
||||
version.SetConfig(c.ServerVersion)
|
||||
dataprovider.SetTZ(c.TZ)
|
||||
Config = c
|
||||
Config.Actions.ExecuteOn = util.RemoveDuplicates(Config.Actions.ExecuteOn, true)
|
||||
Config.Actions.ExecuteSync = util.RemoveDuplicates(Config.Actions.ExecuteSync, true)
|
||||
|
@ -200,7 +208,7 @@ func Initialize(c Configuration, isShared int) error {
|
|||
Config.rateLimitersList = rateLimitersList
|
||||
}
|
||||
if c.DefenderConfig.Enabled {
|
||||
if !util.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
|
||||
if !slices.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
|
||||
return fmt.Errorf("unsupported defender driver %q", c.DefenderConfig.Driver)
|
||||
}
|
||||
var defender Defender
|
||||
|
@ -327,6 +335,13 @@ func Reload() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// DelayLogin applies the configured login delay
|
||||
func DelayLogin(err error) {
|
||||
if Config.defender != nil {
|
||||
Config.defender.DelayLogin(err)
|
||||
}
|
||||
}
|
||||
|
||||
// IsBanned returns true if the specified IP address is banned
|
||||
func IsBanned(ip, protocol string) bool {
|
||||
if plugin.Handler.IsIPBanned(ip, protocol) {
|
||||
|
@ -395,6 +410,23 @@ func AddDefenderEvent(ip, protocol string, event HostEvent) bool {
|
|||
return Config.defender.AddEvent(ip, protocol, event)
|
||||
}
|
||||
|
||||
func reloadProviderConfigs() {
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
if err != nil {
|
||||
logger.Error(logSender, "", "unable to load config from provider: %v", err)
|
||||
return
|
||||
}
|
||||
configs.SetNilsToEmpty()
|
||||
if fnUpdateBranding != nil {
|
||||
fnUpdateBranding(configs.Branding)
|
||||
}
|
||||
if err := configs.SMTP.TryDecrypt(); err != nil {
|
||||
logger.Error(logSender, "", "unable to decrypt smtp config: %v", err)
|
||||
return
|
||||
}
|
||||
smtp.Activate(configs.SMTP)
|
||||
}
|
||||
|
||||
func startPeriodicChecks(duration time.Duration, isShared int) {
|
||||
startEventScheduler()
|
||||
spec := fmt.Sprintf("@every %s", duration)
|
||||
|
@ -403,7 +435,7 @@ func startPeriodicChecks(duration time.Duration, isShared int) {
|
|||
logger.Info(logSender, "", "scheduled overquota transfers check, schedule %q", spec)
|
||||
if isShared == 1 {
|
||||
logger.Info(logSender, "", "add reload configs task")
|
||||
_, err := eventScheduler.AddFunc("@every 10m", smtp.ReloadProviderConf)
|
||||
_, err := eventScheduler.AddFunc("@every 10m", reloadProviderConfigs)
|
||||
util.PanicOnError(err)
|
||||
}
|
||||
if Config.IdleTimeout > 0 {
|
||||
|
@ -581,6 +613,10 @@ type Configuration struct {
|
|||
Umask string `json:"umask" mapstructure:"umask"`
|
||||
// Defines the server version
|
||||
ServerVersion string `json:"server_version" mapstructure:"server_version"`
|
||||
// TZ defines the time zone to use for the EventManager scheduler and to
|
||||
// control time-based access restrictions. Set to "local" to use the
|
||||
// server's local time, otherwise UTC will be used.
|
||||
TZ string `json:"tz" mapstructure:"tz"`
|
||||
// Metadata configuration
|
||||
Metadata MetadataConfig `json:"metadata" mapstructure:"metadata"`
|
||||
idleTimeoutAsDuration time.Duration
|
||||
|
@ -742,7 +778,7 @@ func (c *Configuration) checkPostDisconnectHook(remoteAddr, protocol, username,
|
|||
if c.PostDisconnectHook == "" {
|
||||
return
|
||||
}
|
||||
if !util.Contains(disconnHookProtocols, protocol) {
|
||||
if !slices.Contains(disconnHookProtocols, protocol) {
|
||||
return
|
||||
}
|
||||
go c.executePostDisconnectHook(remoteAddr, protocol, username, connID, connectionTime)
|
||||
|
@ -803,7 +839,8 @@ func getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy)
|
|||
return func(upstream net.Addr) (proxyproto.Policy, error) {
|
||||
upstreamIP, err := util.GetIPFromNetAddr(upstream)
|
||||
if err != nil {
|
||||
// something is wrong with the source IP, better reject the connection
|
||||
// Something is wrong with the source IP, better reject the
|
||||
// connection if a proxy header is found.
|
||||
return proxyproto.REJECT, err
|
||||
}
|
||||
|
||||
|
@ -822,6 +859,9 @@ func getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy)
|
|||
}
|
||||
}
|
||||
|
||||
if def == proxyproto.REQUIRE {
|
||||
return proxyproto.REJECT, nil
|
||||
}
|
||||
return def, nil
|
||||
}
|
||||
}
|
||||
|
@ -980,7 +1020,7 @@ func (conns *ActiveConnections) Remove(connectionID string) {
|
|||
metric.UpdateActiveConnectionsSize(lastIdx)
|
||||
logger.Debug(conn.GetProtocol(), conn.GetID(), "connection removed, local address %q, remote address %q close fs error: %v, num open connections: %d",
|
||||
conn.GetLocalAddress(), conn.GetRemoteAddress(), err, lastIdx)
|
||||
if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !util.Contains(ftpLoginCommands, conn.GetCommand()) {
|
||||
if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !slices.Contains(ftpLoginCommands, conn.GetCommand()) {
|
||||
ip := util.GetIPFromRemoteAddress(conn.GetRemoteAddress())
|
||||
logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTried, ProtocolFTP,
|
||||
dataprovider.ErrNoAuthTried.Error())
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -419,6 +420,9 @@ func TestDefenderIntegration(t *testing.T) {
|
|||
ObservationTime: 15,
|
||||
EntriesSoftLimit: 100,
|
||||
EntriesHardLimit: 150,
|
||||
LoginDelay: LoginDelay{
|
||||
PasswordFailed: 200,
|
||||
},
|
||||
}
|
||||
err = Initialize(Config, 0)
|
||||
// ScoreInvalid cannot be greater than threshold
|
||||
|
@ -477,6 +481,16 @@ func TestDefenderIntegration(t *testing.T) {
|
|||
assert.Nil(t, banTime)
|
||||
assert.False(t, DeleteDefenderHost(ip))
|
||||
|
||||
startTime := time.Now()
|
||||
DelayLogin(nil)
|
||||
elapsed := time.Since(startTime)
|
||||
assert.Less(t, elapsed, time.Millisecond*50)
|
||||
|
||||
startTime = time.Now()
|
||||
DelayLogin(ErrInternalFailure)
|
||||
elapsed = time.Since(startTime)
|
||||
assert.Greater(t, elapsed, time.Millisecond*150)
|
||||
|
||||
Config = configCopy
|
||||
}
|
||||
|
||||
|
@ -1064,7 +1078,7 @@ func TestProxyPolicy(t *testing.T) {
|
|||
assert.Equal(t, proxyproto.SKIP, policy)
|
||||
policy, err = p(&net.TCPAddr{IP: net.ParseIP("10.8.1.5")})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proxyproto.REQUIRE, policy)
|
||||
assert.Equal(t, proxyproto.REJECT, policy)
|
||||
}
|
||||
|
||||
func TestProxyProtocolVersion(t *testing.T) {
|
||||
|
@ -1213,8 +1227,8 @@ func TestFolderCopy(t *testing.T) {
|
|||
folder.ID = 2
|
||||
folder.Users = []string{"user3"}
|
||||
require.Len(t, folderCopy.Users, 2)
|
||||
require.True(t, util.Contains(folderCopy.Users, "user1"))
|
||||
require.True(t, util.Contains(folderCopy.Users, "user2"))
|
||||
require.True(t, slices.Contains(folderCopy.Users, "user1"))
|
||||
require.True(t, slices.Contains(folderCopy.Users, "user2"))
|
||||
require.Equal(t, int64(1), folderCopy.ID)
|
||||
require.Equal(t, folder.Name, folderCopy.Name)
|
||||
require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
|
||||
|
@ -1230,7 +1244,7 @@ func TestFolderCopy(t *testing.T) {
|
|||
folderCopy = folder.GetACopy()
|
||||
folder.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
|
||||
require.Len(t, folderCopy.Users, 1)
|
||||
require.True(t, util.Contains(folderCopy.Users, "user3"))
|
||||
require.True(t, slices.Contains(folderCopy.Users, "user3"))
|
||||
require.Equal(t, int64(2), folderCopy.ID)
|
||||
require.Equal(t, folder.Name, folderCopy.Name)
|
||||
require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
@ -62,7 +63,7 @@ type BaseConnection struct {
|
|||
// NewBaseConnection returns a new BaseConnection
|
||||
func NewBaseConnection(id, protocol, localAddr, remoteAddr string, user dataprovider.User) *BaseConnection {
|
||||
connID := id
|
||||
if util.Contains(supportedProtocols, protocol) {
|
||||
if slices.Contains(supportedProtocols, protocol) {
|
||||
connID = fmt.Sprintf("%s_%s", protocol, id)
|
||||
}
|
||||
user.UploadBandwidth, user.DownloadBandwidth = user.GetBandwidthForIP(util.GetIPFromRemoteAddress(remoteAddr), connID)
|
||||
|
@ -131,7 +132,7 @@ func (c *BaseConnection) GetRemoteIP() string {
|
|||
// SetProtocol sets the protocol for this connection
|
||||
func (c *BaseConnection) SetProtocol(protocol string) {
|
||||
c.protocol = protocol
|
||||
if util.Contains(supportedProtocols, c.protocol) {
|
||||
if slices.Contains(supportedProtocols, c.protocol) {
|
||||
c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID)
|
||||
}
|
||||
}
|
||||
|
@ -449,10 +450,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info
|
|||
if updateQuota && info.Mode()&os.ModeSymlink == 0 {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, -1, -size, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, -1, -size, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &c.User, -1, -size, false)
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&c.User, -1, -size, false) //nolint:errcheck
|
||||
}
|
||||
|
@ -1121,10 +1119,7 @@ func (c *BaseConnection) truncateFile(fs vfs.Fs, fsPath, virtualPath string, siz
|
|||
sizeDiff := initialSize - size
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -sizeDiff, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -sizeDiff, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -sizeDiff, false)
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -sizeDiff, false) //nolint:errcheck
|
||||
}
|
||||
|
@ -1518,61 +1513,40 @@ func (c *BaseConnection) updateQuotaMoveBetweenVFolders(sourceFolder, dstFolder
|
|||
if sourceFolder.Name == dstFolder.Name {
|
||||
// both files are inside the same virtual folder
|
||||
if initialSize != -1 {
|
||||
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, -numFiles, -initialSize, false) //nolint:errcheck
|
||||
if dstFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, -numFiles, -initialSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, -numFiles, -initialSize, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
// files are inside different virtual folders
|
||||
dataprovider.UpdateVirtualFolderQuota(&sourceFolder.BaseVirtualFolder, -numFiles, -filesSize, false) //nolint:errcheck
|
||||
if sourceFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, -numFiles, -filesSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(sourceFolder, &c.User, -numFiles, -filesSize, false)
|
||||
if initialSize == -1 {
|
||||
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, numFiles, filesSize, false) //nolint:errcheck
|
||||
if dstFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, numFiles, filesSize, false) //nolint:errcheck
|
||||
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, numFiles, filesSize, false)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// we cannot have a directory here, initialSize != -1 only for files
|
||||
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
if dstFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, 0, filesSize-initialSize, false)
|
||||
}
|
||||
|
||||
func (c *BaseConnection) updateQuotaMoveFromVFolder(sourceFolder *vfs.VirtualFolder, initialSize, filesSize int64, numFiles int) {
|
||||
// move between a virtual folder and the user home dir
|
||||
dataprovider.UpdateVirtualFolderQuota(&sourceFolder.BaseVirtualFolder, -numFiles, -filesSize, false) //nolint:errcheck
|
||||
if sourceFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, -numFiles, -filesSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(sourceFolder, &c.User, -numFiles, -filesSize, false)
|
||||
if initialSize == -1 {
|
||||
dataprovider.UpdateUserQuota(&c.User, numFiles, filesSize, false) //nolint:errcheck
|
||||
} else {
|
||||
return
|
||||
}
|
||||
// we cannot have a directory here, initialSize != -1 only for files
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BaseConnection) updateQuotaMoveToVFolder(dstFolder *vfs.VirtualFolder, initialSize, filesSize int64, numFiles int) {
|
||||
// move between the user home dir and a virtual folder
|
||||
dataprovider.UpdateUserQuota(&c.User, -numFiles, -filesSize, false) //nolint:errcheck
|
||||
if initialSize == -1 {
|
||||
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, numFiles, filesSize, false) //nolint:errcheck
|
||||
if dstFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, numFiles, filesSize, false) //nolint:errcheck
|
||||
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, numFiles, filesSize, false)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// we cannot have a directory here, initialSize != -1 only for files
|
||||
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
if dstFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, 0, filesSize-initialSize, false)
|
||||
}
|
||||
|
||||
func (c *BaseConnection) updateQuotaAfterRename(fs vfs.Fs, virtualSourcePath, virtualTargetPath, targetPath string,
|
||||
|
@ -1822,12 +1796,12 @@ type DirListerAt struct {
|
|||
lister vfs.DirLister
|
||||
}
|
||||
|
||||
// Add adds the given os.FileInfo to the internal cache
|
||||
func (l *DirListerAt) Add(fi os.FileInfo) {
|
||||
// Prepend adds the given os.FileInfo as first element of the internal cache
|
||||
func (l *DirListerAt) Prepend(fi os.FileInfo) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.info = append(l.info, fi)
|
||||
l.info = slices.Insert(l.info, 0, fi)
|
||||
}
|
||||
|
||||
// ListAt implements sftp.ListerAt
|
||||
|
@ -1840,10 +1814,10 @@ func (l *DirListerAt) ListAt(f []os.FileInfo, _ int64) (int, error) {
|
|||
}
|
||||
if len(f) <= len(l.info) {
|
||||
files := make([]os.FileInfo, 0, len(f))
|
||||
for idx := len(l.info) - 1; idx >= 0; idx-- {
|
||||
for idx := range l.info {
|
||||
files = append(files, l.info[idx])
|
||||
if len(files) == len(f) {
|
||||
l.info = l.info[:idx]
|
||||
l.info = l.info[idx+1:]
|
||||
n := copy(f, files)
|
||||
return n, nil
|
||||
}
|
||||
|
@ -1865,9 +1839,7 @@ func (l *DirListerAt) Next(limit int) ([]os.FileInfo, error) {
|
|||
}
|
||||
files = l.user.FilterListDir(files, l.virtualPath)
|
||||
if len(l.info) > 0 {
|
||||
for _, fi := range l.info {
|
||||
files = util.PrependFileInfo(files, fi)
|
||||
}
|
||||
files = slices.Concat(l.info, files)
|
||||
l.info = nil
|
||||
}
|
||||
if err != nil || len(files) > 0 {
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -389,7 +390,7 @@ func TestErrorsMapping(t *testing.T) {
|
|||
err := conn.GetFsError(fs, os.ErrNotExist)
|
||||
if protocol == ProtocolSFTP {
|
||||
assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
|
||||
} else if util.Contains(osErrorsProtocols, protocol) {
|
||||
} else if slices.Contains(osErrorsProtocols, protocol) {
|
||||
assert.EqualError(t, err, os.ErrNotExist.Error())
|
||||
} else {
|
||||
assert.EqualError(t, err, ErrNotExist.Error())
|
||||
|
@ -1134,8 +1135,8 @@ func TestListerAt(t *testing.T) {
|
|||
require.Equal(t, 0, n)
|
||||
lister, err = conn.ListDir("/")
|
||||
require.NoError(t, err)
|
||||
lister.Add(vfs.NewFileInfo("..", true, 0, time.Unix(0, 0), false))
|
||||
lister.Add(vfs.NewFileInfo(".", true, 0, time.Unix(0, 0), false))
|
||||
lister.Prepend(vfs.NewFileInfo("..", true, 0, time.Unix(0, 0), false))
|
||||
lister.Prepend(vfs.NewFileInfo(".", true, 0, time.Unix(0, 0), false))
|
||||
files = make([]os.FileInfo, 1)
|
||||
n, err = lister.ListAt(files, 0)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -53,6 +53,7 @@ type Defender interface {
|
|||
GetBanTime(ip string) (*time.Time, error)
|
||||
GetScore(ip string) (int, error)
|
||||
DeleteHost(ip string) bool
|
||||
DelayLogin(err error)
|
||||
}
|
||||
|
||||
// DefenderConfig defines the "defender" configuration
|
||||
|
@ -90,6 +91,16 @@ type DefenderConfig struct {
|
|||
// to return when you request for the entire host list from the defender
|
||||
EntriesSoftLimit int `json:"entries_soft_limit" mapstructure:"entries_soft_limit"`
|
||||
EntriesHardLimit int `json:"entries_hard_limit" mapstructure:"entries_hard_limit"`
|
||||
// Configuration to impose a delay between login attempts
|
||||
LoginDelay LoginDelay `json:"login_delay" mapstructure:"login_delay"`
|
||||
}
|
||||
|
||||
// LoginDelay defines the delays to impose between login attempts.
|
||||
type LoginDelay struct {
|
||||
// The number of milliseconds to pause prior to allowing a successful login
|
||||
Success int `json:"success" mapstructure:"success"`
|
||||
// The number of milliseconds to pause prior to reporting a failed login
|
||||
PasswordFailed int `json:"password_failed" mapstructure:"password_failed"`
|
||||
}
|
||||
|
||||
type baseDefender struct {
|
||||
|
@ -163,6 +174,19 @@ func (d *baseDefender) logBan(ip, protocol string) {
|
|||
Send()
|
||||
}
|
||||
|
||||
// DelayLogin applies the configured login delay.
|
||||
func (d *baseDefender) DelayLogin(err error) {
|
||||
if err == nil {
|
||||
if d.config.LoginDelay.Success > 0 {
|
||||
time.Sleep(time.Duration(d.config.LoginDelay.Success) * time.Millisecond)
|
||||
}
|
||||
return
|
||||
}
|
||||
if d.config.LoginDelay.PasswordFailed > 0 {
|
||||
time.Sleep(time.Duration(d.config.LoginDelay.PasswordFailed) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
type hostEvent struct {
|
||||
dateTime time.Time
|
||||
score int
|
||||
|
|
|
@ -435,6 +435,31 @@ func TestDefenderCleanup(t *testing.T) {
|
|||
assert.Equal(t, 0, score)
|
||||
}
|
||||
|
||||
func TestDefenderDelay(t *testing.T) {
|
||||
d := memoryDefender{
|
||||
baseDefender: baseDefender{
|
||||
config: &DefenderConfig{
|
||||
ObservationTime: 1,
|
||||
EntriesSoftLimit: 2,
|
||||
EntriesHardLimit: 3,
|
||||
LoginDelay: LoginDelay{
|
||||
Success: 50,
|
||||
PasswordFailed: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
startTime := time.Now()
|
||||
d.DelayLogin(nil)
|
||||
elapsed := time.Since(startTime)
|
||||
assert.Less(t, elapsed, time.Millisecond*100)
|
||||
|
||||
startTime = time.Now()
|
||||
d.DelayLogin(ErrInternalFailure)
|
||||
elapsed = time.Since(startTime)
|
||||
assert.Greater(t, elapsed, time.Millisecond*150)
|
||||
}
|
||||
|
||||
func TestDefenderConfig(t *testing.T) {
|
||||
c := DefenderConfig{}
|
||||
err := c.validate()
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -307,7 +308,7 @@ func (*eventRulesContainer) checkIPDLoginEventMatch(conditions *dataprovider.Eve
|
|||
}
|
||||
|
||||
func (*eventRulesContainer) checkProviderEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
|
||||
if !util.Contains(conditions.ProviderEvents, params.Event) {
|
||||
if !slices.Contains(conditions.ProviderEvents, params.Event) {
|
||||
return false
|
||||
}
|
||||
if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
|
||||
|
@ -316,14 +317,14 @@ func (*eventRulesContainer) checkProviderEventMatch(conditions *dataprovider.Eve
|
|||
if !checkEventConditionPatterns(params.Role, conditions.Options.RoleNames) {
|
||||
return false
|
||||
}
|
||||
if len(conditions.Options.ProviderObjects) > 0 && !util.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
|
||||
if len(conditions.Options.ProviderObjects) > 0 && !slices.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
|
||||
if !util.Contains(conditions.FsEvents, params.Event) {
|
||||
if !slices.Contains(conditions.FsEvents, params.Event) {
|
||||
return false
|
||||
}
|
||||
if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
|
||||
|
@ -338,7 +339,7 @@ func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventCond
|
|||
if !checkEventConditionPatterns(params.VirtualPath, conditions.Options.FsPaths) {
|
||||
return false
|
||||
}
|
||||
if len(conditions.Options.Protocols) > 0 && !util.Contains(conditions.Options.Protocols, params.Protocol) {
|
||||
if len(conditions.Options.Protocols) > 0 && !slices.Contains(conditions.Options.Protocols, params.Protocol) {
|
||||
return false
|
||||
}
|
||||
if params.Event == operationUpload || params.Event == operationDownload {
|
||||
|
@ -908,10 +909,7 @@ func updateUserQuotaAfterFileWrite(conn *BaseConnection, virtualPath string, num
|
|||
dataprovider.UpdateUserQuota(&conn.User, numFiles, fileSize, false) //nolint:errcheck
|
||||
return
|
||||
}
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, numFiles, fileSize, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&conn.User, numFiles, fileSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &conn.User, numFiles, fileSize, false)
|
||||
}
|
||||
|
||||
func checkWriterPermsAndQuota(conn *BaseConnection, virtualPath string, numFiles int, expectedSize, truncatedSize int64) error {
|
||||
|
@ -2551,7 +2549,7 @@ func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *
|
|||
return &u, err
|
||||
}
|
||||
|
||||
func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
|
||||
func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams, //nolint:gocyclo
|
||||
conditions dataprovider.ConditionOptions,
|
||||
) error {
|
||||
var err error
|
||||
|
@ -2585,6 +2583,8 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
|
|||
err = executeUserExpirationCheckRuleAction(conditions, params)
|
||||
case dataprovider.ActionTypeUserInactivityCheck:
|
||||
err = executeUserInactivityCheckRuleAction(action.Options.UserInactivityConfig, conditions, params, time.Now())
|
||||
case dataprovider.ActionTypeRotateLogs:
|
||||
err = logger.RotateLogFile()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported action type: %d", action.Type)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import (
|
|||
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
|
@ -36,7 +38,15 @@ func stopEventScheduler() {
|
|||
func startEventScheduler() {
|
||||
stopEventScheduler()
|
||||
|
||||
eventScheduler = cron.New(cron.WithLocation(time.UTC), cron.WithLogger(cron.DiscardLogger))
|
||||
options := []cron.Option{
|
||||
cron.WithLogger(cron.DiscardLogger),
|
||||
}
|
||||
if !dataprovider.UseLocalTime() {
|
||||
eventManagerLog(logger.LevelDebug, "use UTC time for the scheduler")
|
||||
options = append(options, cron.WithLocation(time.UTC))
|
||||
}
|
||||
|
||||
eventScheduler = cron.New(options...)
|
||||
eventManager.loadRules()
|
||||
_, err := eventScheduler.AddFunc("@every 10m", eventManager.loadRules)
|
||||
util.PanicOnError(err)
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
@ -1457,15 +1458,15 @@ func TestTruncateQuotaLimits(t *testing.T) {
|
|||
expectedQuotaSize := int64(3)
|
||||
fold, _, err := httpdtest.GetFolderByName(folder2.Name, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaSize, fold.UsedQuotaSize)
|
||||
assert.Equal(t, expectedQuotaFiles, fold.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), fold.UsedQuotaSize)
|
||||
assert.Equal(t, 0, fold.UsedQuotaFiles)
|
||||
err = f.Close()
|
||||
assert.NoError(t, err)
|
||||
expectedQuotaFiles = 1
|
||||
fold, _, err = httpdtest.GetFolderByName(folder2.Name, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaSize, fold.UsedQuotaSize)
|
||||
assert.Equal(t, expectedQuotaFiles, fold.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), fold.UsedQuotaSize)
|
||||
assert.Equal(t, 0, fold.UsedQuotaFiles)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
|
@ -1777,8 +1778,8 @@ func TestVirtualFoldersQuotaValues(t *testing.T) {
|
|||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
|
@ -1885,8 +1886,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -1910,8 +1911,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
// rename a file inside vdir2, it isn't included inside user quota, so we have:
|
||||
// - vdir1/dir1/testFileName.rename
|
||||
// - vdir1/dir2/testFileName1
|
||||
|
@ -1929,8 +1930,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
// rename a file inside vdir2 overwriting an existing, we now have:
|
||||
// - vdir1/dir1/testFileName.rename
|
||||
// - vdir1/dir2/testFileName1
|
||||
|
@ -1947,8 +1948,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
// rename a file inside vdir1 overwriting an existing, we now have:
|
||||
// - vdir1/dir1/testFileName.rename (initial testFileName1)
|
||||
// - vdir2/dir1/testFileName.rename (initial testFileName1)
|
||||
|
@ -1960,8 +1961,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -1981,8 +1982,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -2087,8 +2088,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1+testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -2106,8 +2107,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize*2, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize*2, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1*2, f.UsedQuotaSize)
|
||||
|
@ -2124,8 +2125,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1+testFileSize, f.UsedQuotaSize)
|
||||
|
@ -2141,8 +2142,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
|
@ -2172,8 +2173,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize1*3+testFileSize*2, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1*3+testFileSize*2, f.UsedQuotaSize)
|
||||
assert.Equal(t, 5, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
|
@ -2187,8 +2188,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize1*2+testFileSize, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1*2+testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 3, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -2293,8 +2294,8 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -2312,8 +2313,8 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
|
@ -2376,8 +2377,8 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize*3+testFileSize1*3, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
|
@ -2497,8 +2498,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
// rename a file from user home dir to vdir2, vdir2 is not included in user quota so we have:
|
||||
// - /vdir2/dir1/testFileName
|
||||
// - /vdir1/dir1/testFileName1
|
||||
|
@ -2537,8 +2538,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
|
@ -2554,8 +2555,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -2577,8 +2578,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -2595,8 +2596,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize*2+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 3, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -2621,8 +2622,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize*2+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 3, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1*2+testFileSize, f.UsedQuotaSize)
|
||||
|
@ -3978,9 +3979,9 @@ func TestEventRule(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 3)
|
||||
assert.True(t, util.Contains(email.To, "test1@example.com"))
|
||||
assert.True(t, util.Contains(email.To, "test2@example.com"))
|
||||
assert.True(t, util.Contains(email.To, "test3@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test1@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test2@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test3@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "upload" from "%s" status OK`, user.Username))
|
||||
// test the failure action, we download a file that exceeds the transfer quota limit
|
||||
err = writeSFTPFileNoCheck(path.Join("subdir1", testFileName), 1*1024*1024+65535, client)
|
||||
|
@ -3999,9 +4000,9 @@ func TestEventRule(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email = lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 3)
|
||||
assert.True(t, util.Contains(email.To, "test1@example.com"))
|
||||
assert.True(t, util.Contains(email.To, "test2@example.com"))
|
||||
assert.True(t, util.Contains(email.To, "test3@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test1@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test2@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test3@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s" status KO`, user.Username))
|
||||
assert.Contains(t, email.Data, `"download" failed`)
|
||||
assert.Contains(t, email.Data, common.ErrReadQuotaExceeded.Error())
|
||||
|
@ -4019,7 +4020,7 @@ func TestEventRule(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email = lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "failure@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "failure@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: Failed "upload" from "%s"`, user.Username))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`action %q failed`, action1.Name))
|
||||
// now test the download rule
|
||||
|
@ -4036,9 +4037,9 @@ func TestEventRule(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email = lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 3)
|
||||
assert.True(t, util.Contains(email.To, "test1@example.com"))
|
||||
assert.True(t, util.Contains(email.To, "test2@example.com"))
|
||||
assert.True(t, util.Contains(email.To, "test3@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test1@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test2@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test3@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s"`, user.Username))
|
||||
}
|
||||
// test upload action command with arguments
|
||||
|
@ -4079,9 +4080,9 @@ func TestEventRule(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 3)
|
||||
assert.True(t, util.Contains(email.To, "test1@example.com"))
|
||||
assert.True(t, util.Contains(email.To, "test2@example.com"))
|
||||
assert.True(t, util.Contains(email.To, "test3@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test1@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test2@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test3@example.com"))
|
||||
assert.Contains(t, email.Data, `Subject: New "delete" from "admin"`)
|
||||
_, err = httpdtest.RemoveEventRule(rule3, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -4236,7 +4237,7 @@ func TestEventRuleProviderEvents(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test3@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test3@example.com"))
|
||||
assert.Contains(t, email.Data, `Subject: New "update" from "admin"`)
|
||||
}
|
||||
// now delete the script to generate an error
|
||||
|
@ -4251,7 +4252,7 @@ func TestEventRuleProviderEvents(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "failure@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "failure@example.com"))
|
||||
assert.Contains(t, email.Data, `Subject: Failed "update" from "admin"`)
|
||||
assert.Contains(t, email.Data, fmt.Sprintf("Object name: %s object type: folder", folder.Name))
|
||||
lastReceivedEmail.reset()
|
||||
|
@ -5306,7 +5307,7 @@ func TestBackupAsAttachment(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
|
||||
assert.Contains(t, email.Data, `Domain: example.com`)
|
||||
assert.Contains(t, email.Data, "Content-Type: application/json")
|
||||
|
@ -5676,7 +5677,7 @@ func TestEventActionCompressQuotaErrors(t *testing.T) {
|
|||
}, 3*time.Second, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, `Subject: "Compress failed"`)
|
||||
assert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())
|
||||
// update quota size so the user is already overquota
|
||||
|
@ -5691,7 +5692,7 @@ func TestEventActionCompressQuotaErrors(t *testing.T) {
|
|||
}, 3*time.Second, 100*time.Millisecond)
|
||||
email = lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, `Subject: "Compress failed"`)
|
||||
assert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())
|
||||
// remove the path to compress to trigger an error for size estimation
|
||||
|
@ -5705,7 +5706,7 @@ func TestEventActionCompressQuotaErrors(t *testing.T) {
|
|||
}, 3*time.Second, 100*time.Millisecond)
|
||||
email = lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, `Subject: "Compress failed"`)
|
||||
assert.Contains(t, email.Data, "unable to estimate archive size")
|
||||
}
|
||||
|
@ -5834,8 +5835,8 @@ func TestEventActionCompressQuotaFolder(t *testing.T) {
|
|||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
vfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, vfolder.UsedQuotaFiles)
|
||||
assert.Equal(t, info.Size()+int64(len(testFileContent)), vfolder.UsedQuotaSize)
|
||||
assert.Equal(t, 0, vfolder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), vfolder.UsedQuotaSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6041,7 +6042,7 @@ func TestEventActionEmailAttachments(t *testing.T) {
|
|||
}, 1500*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, `Subject: "upload" from`)
|
||||
assert.Contains(t, email.Data, "Content-Disposition: attachment")
|
||||
}
|
||||
|
@ -6218,7 +6219,7 @@ func TestEventActionsRetentionReports(t *testing.T) {
|
|||
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "upload" from "%s"`, user.Username))
|
||||
assert.Contains(t, email.Data, "Content-Disposition: attachment")
|
||||
_, err = client.Stat(testDir)
|
||||
|
@ -6391,7 +6392,7 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) {
|
|||
}, 1500*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-upload" from "%s"`, user.Username))
|
||||
lastReceivedEmail.reset()
|
||||
// a new upload will not produce a new notification
|
||||
|
@ -6414,7 +6415,7 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) {
|
|||
}, 1500*time.Millisecond, 100*time.Millisecond)
|
||||
email = lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-download" from "%s"`, user.Username))
|
||||
// download again
|
||||
lastReceivedEmail.reset()
|
||||
|
@ -6510,7 +6511,7 @@ func TestEventRuleRenameEvent(t *testing.T) {
|
|||
}, 1500*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "rename" from "%s"`, user.Username))
|
||||
assert.Contains(t, email.Data, "Content-Type: text/html")
|
||||
assert.Contains(t, email.Data, fmt.Sprintf("Target path %q", path.Join("/subdir", testFileName)))
|
||||
|
@ -6644,7 +6645,7 @@ func TestEventRuleIDPLogin(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, common.IDPLoginUser))
|
||||
assert.Contains(t, email.Data, username)
|
||||
assert.Contains(t, email.Data, custom1)
|
||||
|
@ -6708,7 +6709,7 @@ func TestEventRuleIDPLogin(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email = lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, common.IDPLoginAdmin))
|
||||
assert.Contains(t, email.Data, username)
|
||||
assert.Contains(t, email.Data, custom1)
|
||||
|
@ -6900,7 +6901,7 @@ func TestEventRuleEmailField(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, user.Email))
|
||||
assert.True(t, slices.Contains(email.To, user.Email))
|
||||
assert.Contains(t, email.Data, `Subject: "add" from "admin"`)
|
||||
|
||||
// if we add a user without email the notification will fail
|
||||
|
@ -6914,7 +6915,7 @@ func TestEventRuleEmailField(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email = lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "failure@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "failure@example.com"))
|
||||
assert.Contains(t, email.Data, `no recipient addresses set`)
|
||||
|
||||
conn, client, err := getSftpClient(user)
|
||||
|
@ -6931,7 +6932,7 @@ func TestEventRuleEmailField(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, user.Email))
|
||||
assert.True(t, slices.Contains(email.To, user.Email))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "mkdir" from "%s"`, user.Username))
|
||||
}
|
||||
|
||||
|
@ -7038,7 +7039,7 @@ func TestEventRuleCertificate(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
|
||||
assert.Contains(t, email.Data, "Content-Type: text/plain")
|
||||
assert.Contains(t, email.Data, `Domain: example.com Timestamp`)
|
||||
|
@ -7058,7 +7059,7 @@ func TestEventRuleCertificate(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email = lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s KO"`, renewalEvent))
|
||||
assert.Contains(t, email.Data, `Domain: example.com Timestamp`)
|
||||
assert.Contains(t, email.Data, errRenew.Error())
|
||||
|
@ -7184,8 +7185,8 @@ func TestEventRuleIPBlocked(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 2)
|
||||
assert.True(t, util.Contains(email.To, "test3@example.com"))
|
||||
assert.True(t, util.Contains(email.To, "test4@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test3@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "test4@example.com"))
|
||||
assert.Contains(t, email.Data, `Subject: New "IP Blocked"`)
|
||||
|
||||
err = dataprovider.DeleteEventRule(rule1.Name, "", "", "")
|
||||
|
@ -7209,6 +7210,103 @@ func TestEventRuleIPBlocked(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventRuleRotateLog(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 2525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
a1 := dataprovider.BaseEventAction{
|
||||
Name: "a1",
|
||||
Type: dataprovider.ActionTypeRotateLogs,
|
||||
}
|
||||
action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
a2 := dataprovider.BaseEventAction{
|
||||
Name: "a2",
|
||||
Type: dataprovider.ActionTypeEmail,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
Recipients: []string{"success@example.net"},
|
||||
Subject: `OK`,
|
||||
Body: "OK action",
|
||||
},
|
||||
},
|
||||
}
|
||||
action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
r1 := dataprovider.EventRule{
|
||||
Name: "rule1",
|
||||
Status: 1,
|
||||
Trigger: dataprovider.EventTriggerFsEvent,
|
||||
Conditions: dataprovider.EventConditions{
|
||||
FsEvents: []string{"mkdir"},
|
||||
Options: dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: user.Username,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action1.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action2.Name,
|
||||
},
|
||||
Order: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
rule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated)
|
||||
assert.NoError(t, err, string(resp))
|
||||
conn, client, err := getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
lastReceivedEmail.reset()
|
||||
err := client.Mkdir("just a test dir")
|
||||
assert.NoError(t, err)
|
||||
// just check that the action is executed
|
||||
assert.Eventually(t, func() bool {
|
||||
return lastReceivedEmail.get().From != ""
|
||||
}, 1500*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.Contains(t, email.To, "success@example.net")
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
||||
smtpCfg = smtp.Config{}
|
||||
err = smtpCfg.Initialize(configDir, true)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventRuleInactivityCheck(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
|
@ -8260,7 +8358,7 @@ func TestSFTPLoopError(t *testing.T) {
|
|||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "failure@example.com"))
|
||||
assert.True(t, slices.Contains(email.To, "failure@example.com"))
|
||||
assert.Contains(t, email.Data, `Subject: Failed action`)
|
||||
|
||||
user1.VirtualFolders[0].FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)
|
||||
|
@ -8936,7 +9034,7 @@ func TestProxyProtocol(t *testing.T) {
|
|||
resp, err := httpclient.Get(fmt.Sprintf("http://%v", httpProxyAddr))
|
||||
if assert.NoError(t, err) {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ package common
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
@ -94,7 +95,7 @@ func (r *RateLimiterConfig) validate() error {
|
|||
}
|
||||
r.Protocols = util.RemoveDuplicates(r.Protocols, true)
|
||||
for _, protocol := range r.Protocols {
|
||||
if !util.Contains(rateLimiterProtocolValues, protocol) {
|
||||
if !slices.Contains(rateLimiterProtocolValues, protocol) {
|
||||
return fmt.Errorf("invalid protocol %q", protocol)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
|
@ -96,7 +97,7 @@ func (m *CertManager) loadCertificates() error {
|
|||
}
|
||||
logger.Debug(m.logSender, "", "TLS certificate %q successfully loaded, id %v", keyPair.Cert, keyPair.ID)
|
||||
certs[keyPair.ID] = &newCert
|
||||
if !util.Contains(m.monitorList, keyPair.Cert) {
|
||||
if !slices.Contains(m.monitorList, keyPair.Cert) {
|
||||
m.monitorList = append(m.monitorList, keyPair.Cert)
|
||||
}
|
||||
}
|
||||
|
@ -190,7 +191,7 @@ func (m *CertManager) LoadCRLs() error {
|
|||
|
||||
logger.Debug(m.logSender, "", "CRL %q successfully loaded", revocationList)
|
||||
crls = append(crls, crl)
|
||||
if !util.Contains(m.monitorList, revocationList) {
|
||||
if !slices.Contains(m.monitorList, revocationList) {
|
||||
m.monitorList = append(m.monitorList, revocationList)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -352,6 +352,9 @@ func (t *BaseTransfer) checkUploadOutsideHomeDir(err error) int {
|
|||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if t.ErrTransfer == nil {
|
||||
t.ErrTransfer = err
|
||||
}
|
||||
if Config.TempPath == "" {
|
||||
return 0
|
||||
}
|
||||
|
@ -410,7 +413,8 @@ func (t *BaseTransfer) Close() error {
|
|||
var uploadFileSize int64
|
||||
if t.transferType == TransferDownload {
|
||||
logger.TransferLog(downloadLogSender, t.fsPath, elapsed, t.BytesSent.Load(), t.Connection.User.Username,
|
||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
|
||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode,
|
||||
t.ErrTransfer)
|
||||
ExecuteActionNotification(t.Connection, operationDownload, t.fsPath, t.requestPath, "", "", "", //nolint:errcheck
|
||||
t.BytesSent.Load(), t.ErrTransfer, elapsed, t.metadata)
|
||||
} else {
|
||||
|
@ -431,7 +435,8 @@ func (t *BaseTransfer) Close() error {
|
|||
t.updateQuota(numFiles, uploadFileSize)
|
||||
t.updateTimes()
|
||||
logger.TransferLog(uploadLogSender, t.fsPath, elapsed, t.BytesReceived.Load(), t.Connection.User.Username,
|
||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
|
||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode,
|
||||
t.ErrTransfer)
|
||||
}
|
||||
if t.ErrTransfer != nil {
|
||||
t.Connection.Log(logger.LevelError, "transfer error: %v, path: %q", t.ErrTransfer, t.fsPath)
|
||||
|
@ -516,11 +521,8 @@ func (t *BaseTransfer) updateQuota(numFiles int, fileSize int64) bool {
|
|||
if t.transferType == TransferUpload && (numFiles != 0 || sizeDiff != 0) {
|
||||
vfolder, err := t.Connection.User.GetVirtualFolderForPath(path.Dir(t.requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, numFiles, //nolint:errcheck
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &t.Connection.User, numFiles,
|
||||
sizeDiff, false)
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&t.Connection.User, numFiles, sizeDiff, false) //nolint:errcheck
|
||||
}
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&t.Connection.User, numFiles, sizeDiff, false) //nolint:errcheck
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -226,10 +227,15 @@ func Init() {
|
|||
ObservationTime: 30,
|
||||
EntriesSoftLimit: 100,
|
||||
EntriesHardLimit: 150,
|
||||
LoginDelay: common.LoginDelay{
|
||||
Success: 0,
|
||||
PasswordFailed: 1000,
|
||||
},
|
||||
},
|
||||
RateLimitersConfig: []common.RateLimiterConfig{defaultRateLimiter},
|
||||
Umask: "",
|
||||
ServerVersion: "",
|
||||
TZ: "",
|
||||
Metadata: common.MetadataConfig{
|
||||
Read: 0,
|
||||
},
|
||||
|
@ -257,6 +263,7 @@ func Init() {
|
|||
HostCertificates: []string{},
|
||||
HostKeyAlgorithms: []string{},
|
||||
KexAlgorithms: []string{},
|
||||
MinDHGroupExchangeKeySize: 2048,
|
||||
Ciphers: []string{},
|
||||
MACs: []string{},
|
||||
PublicKeyAlgorithms: []string{},
|
||||
|
@ -631,6 +638,15 @@ func getRedactedGlobalConf() globalConfig {
|
|||
binding.OIDC.ClientSecret = getRedactedPassword(binding.OIDC.ClientSecret)
|
||||
conf.HTTPDConfig.Bindings = append(conf.HTTPDConfig.Bindings, binding)
|
||||
}
|
||||
conf.PluginsConfig = nil
|
||||
for _, plugin := range globalConf.PluginsConfig {
|
||||
var args []string
|
||||
for _, arg := range plugin.Args {
|
||||
args = append(args, getRedactedPassword(arg))
|
||||
}
|
||||
plugin.Args = args
|
||||
conf.PluginsConfig = append(conf.PluginsConfig, plugin)
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
|
@ -701,7 +717,7 @@ func checkOverrideDefaultSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
if util.Contains(viper.AllKeys(), "mfa.totp") {
|
||||
if slices.Contains(viper.AllKeys(), "mfa.totp") {
|
||||
globalConf.MFAConfig.TOTP = nil
|
||||
}
|
||||
}
|
||||
|
@ -1645,12 +1661,6 @@ func getHTTPDUIBrandingFromEnv(prefix string, branding httpd.UIBranding) (httpd.
|
|||
isSet = true
|
||||
}
|
||||
|
||||
loginImagePath, ok := os.LookupEnv(fmt.Sprintf("%s__LOGIN_IMAGE_PATH", prefix))
|
||||
if ok {
|
||||
branding.LoginImagePath = loginImagePath
|
||||
isSet = true
|
||||
}
|
||||
|
||||
disclaimerName, ok := os.LookupEnv(fmt.Sprintf("%s__DISCLAIMER_NAME", prefix))
|
||||
if ok {
|
||||
branding.DisclaimerName = disclaimerName
|
||||
|
@ -1995,8 +2005,11 @@ func setViperDefaults() {
|
|||
viper.SetDefault("common.defender.observation_time", globalConf.Common.DefenderConfig.ObservationTime)
|
||||
viper.SetDefault("common.defender.entries_soft_limit", globalConf.Common.DefenderConfig.EntriesSoftLimit)
|
||||
viper.SetDefault("common.defender.entries_hard_limit", globalConf.Common.DefenderConfig.EntriesHardLimit)
|
||||
viper.SetDefault("common.defender.login_delay.success", globalConf.Common.DefenderConfig.LoginDelay.Success)
|
||||
viper.SetDefault("common.defender.login_delay.password_failed", globalConf.Common.DefenderConfig.LoginDelay.PasswordFailed)
|
||||
viper.SetDefault("common.umask", globalConf.Common.Umask)
|
||||
viper.SetDefault("common.server_version", globalConf.Common.ServerVersion)
|
||||
viper.SetDefault("common.tz", globalConf.Common.TZ)
|
||||
viper.SetDefault("common.metadata.read", globalConf.Common.Metadata.Read)
|
||||
viper.SetDefault("acme.email", globalConf.ACME.Email)
|
||||
viper.SetDefault("acme.key_type", globalConf.ACME.KeyType)
|
||||
|
@ -2013,6 +2026,7 @@ func setViperDefaults() {
|
|||
viper.SetDefault("sftpd.host_certificates", globalConf.SFTPD.HostCertificates)
|
||||
viper.SetDefault("sftpd.host_key_algorithms", globalConf.SFTPD.HostKeyAlgorithms)
|
||||
viper.SetDefault("sftpd.kex_algorithms", globalConf.SFTPD.KexAlgorithms)
|
||||
viper.SetDefault("sftpd.min_dh_group_exchange_key_size", globalConf.SFTPD.MinDHGroupExchangeKeySize)
|
||||
viper.SetDefault("sftpd.ciphers", globalConf.SFTPD.Ciphers)
|
||||
viper.SetDefault("sftpd.macs", globalConf.SFTPD.MACs)
|
||||
viper.SetDefault("sftpd.public_key_algorithms", globalConf.SFTPD.PublicKeyAlgorithms)
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/sftpgo/sdk/kms"
|
||||
|
@ -36,7 +37,6 @@ import (
|
|||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/internal/sftpd"
|
||||
"github.com/drakkan/sftpgo/v2/internal/smtp"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
"github.com/drakkan/sftpgo/v2/internal/webdavd"
|
||||
)
|
||||
|
||||
|
@ -679,8 +679,8 @@ func TestPluginsFromEnv(t *testing.T) {
|
|||
pluginConf := pluginsConf[0]
|
||||
require.Equal(t, "notifier", pluginConf.Type)
|
||||
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
|
||||
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
|
||||
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
|
||||
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
|
||||
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
|
||||
require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
|
||||
require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
|
||||
require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
|
||||
|
@ -729,8 +729,8 @@ func TestPluginsFromEnv(t *testing.T) {
|
|||
pluginConf = pluginsConf[0]
|
||||
require.Equal(t, "notifier", pluginConf.Type)
|
||||
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
|
||||
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
|
||||
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
|
||||
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
|
||||
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
|
||||
require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
|
||||
require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
|
||||
require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
|
||||
|
@ -787,8 +787,8 @@ func TestRateLimitersFromEnv(t *testing.T) {
|
|||
require.Equal(t, 2, limiters[0].Type)
|
||||
protocols := limiters[0].Protocols
|
||||
require.Len(t, protocols, 2)
|
||||
require.True(t, util.Contains(protocols, common.ProtocolFTP))
|
||||
require.True(t, util.Contains(protocols, common.ProtocolSSH))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolFTP))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolSSH))
|
||||
require.True(t, limiters[0].GenerateDefenderEvents)
|
||||
require.Equal(t, 50, limiters[0].EntriesSoftLimit)
|
||||
require.Equal(t, 100, limiters[0].EntriesHardLimit)
|
||||
|
@ -799,10 +799,10 @@ func TestRateLimitersFromEnv(t *testing.T) {
|
|||
require.Equal(t, 2, limiters[1].Type)
|
||||
protocols = limiters[1].Protocols
|
||||
require.Len(t, protocols, 4)
|
||||
require.True(t, util.Contains(protocols, common.ProtocolFTP))
|
||||
require.True(t, util.Contains(protocols, common.ProtocolSSH))
|
||||
require.True(t, util.Contains(protocols, common.ProtocolWebDAV))
|
||||
require.True(t, util.Contains(protocols, common.ProtocolHTTP))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolFTP))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolSSH))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolWebDAV))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolHTTP))
|
||||
require.False(t, limiters[1].GenerateDefenderEvents)
|
||||
require.Equal(t, 100, limiters[1].EntriesSoftLimit)
|
||||
require.Equal(t, 150, limiters[1].EntriesHardLimit)
|
||||
|
@ -1207,7 +1207,6 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH", "path2")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH", "favicon.ico")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH", "logo.png")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__LOGIN_IMAGE_PATH", "login_image.png")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME", "disclaimer")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH", "disclaimer.html")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS", "default.css")
|
||||
|
@ -1272,7 +1271,6 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__LOGIN_IMAGE_PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS")
|
||||
|
@ -1380,7 +1378,6 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, "same-origin", bindings[2].Security.CrossOriginOpenerPolicy)
|
||||
require.Equal(t, "favicon.ico", bindings[2].Branding.WebAdmin.FaviconPath)
|
||||
require.Equal(t, "logo.png", bindings[2].Branding.WebClient.LogoPath)
|
||||
require.Equal(t, "login_image.png", bindings[2].Branding.WebAdmin.LoginImagePath)
|
||||
require.Equal(t, "disclaimer", bindings[2].Branding.WebClient.DisclaimerName)
|
||||
require.Equal(t, "disclaimer.html", bindings[2].Branding.WebAdmin.DisclaimerPath)
|
||||
require.Equal(t, []string{"default.css"}, bindings[2].Branding.WebClient.DefaultCSS)
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"net/url"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -78,8 +79,8 @@ func executeAction(operation, executor, ip, objectType, objectName, role string,
|
|||
if config.Actions.Hook == "" {
|
||||
return
|
||||
}
|
||||
if !util.Contains(config.Actions.ExecuteOn, operation) ||
|
||||
!util.Contains(config.Actions.ExecuteFor, objectType) {
|
||||
if !slices.Contains(config.Actions.ExecuteOn, operation) ||
|
||||
!slices.Contains(config.Actions.ExecuteFor, objectType) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -96,7 +97,7 @@ func (c *AdminTOTPConfig) validate(username string) error {
|
|||
if c.ConfigName == "" {
|
||||
return util.NewValidationError("totp: config name is mandatory")
|
||||
}
|
||||
if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
|
||||
if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
|
||||
return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
|
||||
}
|
||||
if c.Secret.IsEmpty() {
|
||||
|
@ -337,15 +338,15 @@ func (a *Admin) validatePermissions() error {
|
|||
util.I18nErrorPermissionsRequired,
|
||||
)
|
||||
}
|
||||
if util.Contains(a.Permissions, PermAdminAny) {
|
||||
if slices.Contains(a.Permissions, PermAdminAny) {
|
||||
a.Permissions = []string{PermAdminAny}
|
||||
}
|
||||
for _, perm := range a.Permissions {
|
||||
if !util.Contains(validAdminPerms, perm) {
|
||||
if !slices.Contains(validAdminPerms, perm) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid permission: %q", perm))
|
||||
}
|
||||
if a.Role != "" {
|
||||
if util.Contains(forbiddenPermsForRoleAdmins, perm) {
|
||||
if slices.Contains(forbiddenPermsForRoleAdmins, perm) {
|
||||
deniedPerms := strings.Join(forbiddenPermsForRoleAdmins, ",")
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("a role admin cannot have the following permissions: %q", deniedPerms)),
|
||||
|
@ -559,10 +560,10 @@ func (a *Admin) SetNilSecretsIfEmpty() {
|
|||
|
||||
// HasPermission returns true if the admin has the specified permission
|
||||
func (a *Admin) HasPermission(perm string) bool {
|
||||
if util.Contains(a.Permissions, PermAdminAny) {
|
||||
if slices.Contains(a.Permissions, PermAdminAny) {
|
||||
return true
|
||||
}
|
||||
return util.Contains(a.Permissions, perm)
|
||||
return slices.Contains(a.Permissions, perm)
|
||||
}
|
||||
|
||||
// GetAllowedIPAsString returns the allowed IP as comma separated string
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"fmt"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
|
@ -3134,15 +3135,11 @@ func (p *BoltProvider) migrateDatabase() error {
|
|||
case version == boltDatabaseVersion:
|
||||
providerLog(logger.LevelDebug, "bolt database is up to date, current version: %d", version)
|
||||
return ErrNoInitRequired
|
||||
case version < 28:
|
||||
case version < 29:
|
||||
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
|
||||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 28:
|
||||
logger.InfoToConsole("updating database schema version: %d -> 29", version)
|
||||
providerLog(logger.LevelInfo, "updating database schema version: %d -> 29", version)
|
||||
return updateBoltDatabaseVersion(p.dbHandle, 29)
|
||||
default:
|
||||
if version > boltDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||
|
@ -3164,10 +3161,6 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error { //nolint:gocycl
|
|||
return errors.New("current version match target version, nothing to do")
|
||||
}
|
||||
switch dbVersion.Version {
|
||||
case 29:
|
||||
logger.InfoToConsole("downgrading database schema version: %d -> 28", dbVersion.Version)
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 28", dbVersion.Version)
|
||||
return updateBoltDatabaseVersion(p.dbHandle, 28)
|
||||
default:
|
||||
return fmt.Errorf("database schema version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
|
@ -3328,7 +3321,7 @@ func (p *BoltProvider) addAdminToRole(username, roleName string, bucket *bolt.Bu
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(role.Admins, username) {
|
||||
if !slices.Contains(role.Admins, username) {
|
||||
role.Admins = append(role.Admins, username)
|
||||
buf, err := json.Marshal(role)
|
||||
if err != nil {
|
||||
|
@ -3353,7 +3346,7 @@ func (p *BoltProvider) removeAdminFromRole(username, roleName string, bucket *bo
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if util.Contains(role.Admins, username) {
|
||||
if slices.Contains(role.Admins, username) {
|
||||
var admins []string
|
||||
for _, admin := range role.Admins {
|
||||
if admin != username {
|
||||
|
@ -3383,7 +3376,7 @@ func (p *BoltProvider) addUserToRole(username, roleName string, bucket *bolt.Buc
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(role.Users, username) {
|
||||
if !slices.Contains(role.Users, username) {
|
||||
role.Users = append(role.Users, username)
|
||||
buf, err := json.Marshal(role)
|
||||
if err != nil {
|
||||
|
@ -3408,7 +3401,7 @@ func (p *BoltProvider) removeUserFromRole(username, roleName string, bucket *bol
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if util.Contains(role.Users, username) {
|
||||
if slices.Contains(role.Users, username) {
|
||||
var users []string
|
||||
for _, user := range role.Users {
|
||||
if user != username {
|
||||
|
@ -3436,7 +3429,7 @@ func (p *BoltProvider) addRuleToActionMapping(ruleName, actionName string, bucke
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(action.Rules, ruleName) {
|
||||
if !slices.Contains(action.Rules, ruleName) {
|
||||
action.Rules = append(action.Rules, ruleName)
|
||||
buf, err := json.Marshal(action)
|
||||
if err != nil {
|
||||
|
@ -3458,7 +3451,7 @@ func (p *BoltProvider) removeRuleFromActionMapping(ruleName, actionName string,
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if util.Contains(action.Rules, ruleName) {
|
||||
if slices.Contains(action.Rules, ruleName) {
|
||||
var rules []string
|
||||
for _, r := range action.Rules {
|
||||
if r != ruleName {
|
||||
|
@ -3485,7 +3478,7 @@ func (p *BoltProvider) addUserToGroupMapping(username, groupname string, bucket
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(group.Users, username) {
|
||||
if !slices.Contains(group.Users, username) {
|
||||
group.Users = append(group.Users, username)
|
||||
buf, err := json.Marshal(group)
|
||||
if err != nil {
|
||||
|
@ -3530,7 +3523,7 @@ func (p *BoltProvider) addAdminToGroupMapping(username, groupname string, bucket
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(group.Admins, username) {
|
||||
if !slices.Contains(group.Admins, username) {
|
||||
group.Admins = append(group.Admins, username)
|
||||
buf, err := json.Marshal(group)
|
||||
if err != nil {
|
||||
|
@ -3601,11 +3594,11 @@ func (p *BoltProvider) addRelationToFolderMapping(folderName string, user *User,
|
|||
return err
|
||||
}
|
||||
updated := false
|
||||
if user != nil && !util.Contains(folder.Users, user.Username) {
|
||||
if user != nil && !slices.Contains(folder.Users, user.Username) {
|
||||
folder.Users = append(folder.Users, user.Username)
|
||||
updated = true
|
||||
}
|
||||
if group != nil && !util.Contains(folder.Groups, group.Name) {
|
||||
if group != nil && !slices.Contains(folder.Groups, group.Name) {
|
||||
folder.Groups = append(folder.Groups, group.Name)
|
||||
updated = true
|
||||
}
|
||||
|
@ -3899,7 +3892,7 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
|
|||
v := bucket.Get(dbVersionKey)
|
||||
if v == nil {
|
||||
dbVersion = schemaVersion{
|
||||
Version: 28,
|
||||
Version: 29,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -3908,7 +3901,7 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
|
|||
return dbVersion, err
|
||||
}
|
||||
|
||||
func updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error {
|
||||
/*func updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error {
|
||||
err := dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket(dbVersionBucket)
|
||||
if bucket == nil {
|
||||
|
@ -3924,4 +3917,4 @@ func updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error {
|
|||
return bucket.Put(dbVersionKey, buf)
|
||||
})
|
||||
return err
|
||||
}
|
||||
}*/
|
||||
|
|
|
@ -15,8 +15,12 @@
|
|||
package dataprovider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"net/url"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
|
@ -102,7 +106,7 @@ func (c *SFTPDConfigs) validate() error {
|
|||
if algo == ssh.CertAlgoRSAv01 {
|
||||
continue
|
||||
}
|
||||
if !util.Contains(supportedHostKeyAlgos, algo) {
|
||||
if !slices.Contains(supportedHostKeyAlgos, algo) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
|
||||
}
|
||||
hostKeyAlgos = append(hostKeyAlgos, algo)
|
||||
|
@ -113,24 +117,24 @@ func (c *SFTPDConfigs) validate() error {
|
|||
if algo == "diffie-hellman-group18-sha512" || algo == ssh.KeyExchangeDHGEXSHA256 {
|
||||
continue
|
||||
}
|
||||
if !util.Contains(supportedKexAlgos, algo) {
|
||||
if !slices.Contains(supportedKexAlgos, algo) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
|
||||
}
|
||||
kexAlgos = append(kexAlgos, algo)
|
||||
}
|
||||
c.KexAlgorithms = kexAlgos
|
||||
for _, cipher := range c.Ciphers {
|
||||
if !util.Contains(supportedCiphers, cipher) {
|
||||
if !slices.Contains(supportedCiphers, cipher) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
|
||||
}
|
||||
}
|
||||
for _, mac := range c.MACs {
|
||||
if !util.Contains(supportedMACs, mac) {
|
||||
if !slices.Contains(supportedMACs, mac) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
|
||||
}
|
||||
}
|
||||
for _, algo := range c.PublicKeyAlgos {
|
||||
if !util.Contains(supportedPublicKeyAlgos, algo) {
|
||||
if !slices.Contains(supportedPublicKeyAlgos, algo) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported public key algorithm %q", algo))
|
||||
}
|
||||
}
|
||||
|
@ -305,6 +309,27 @@ func (c *SMTPConfigs) TryDecrypt() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *SMTPConfigs) prepareForRendering() {
|
||||
if c.Password != nil {
|
||||
c.Password.Hide()
|
||||
if c.Password.IsEmpty() {
|
||||
c.Password = nil
|
||||
}
|
||||
}
|
||||
if c.OAuth2.ClientSecret != nil {
|
||||
c.OAuth2.ClientSecret.Hide()
|
||||
if c.OAuth2.ClientSecret.IsEmpty() {
|
||||
c.OAuth2.ClientSecret = nil
|
||||
}
|
||||
}
|
||||
if c.OAuth2.RefreshToken != nil {
|
||||
c.OAuth2.RefreshToken.Hide()
|
||||
if c.OAuth2.RefreshToken.IsEmpty() {
|
||||
c.OAuth2.RefreshToken = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SMTPConfigs) getACopy() *SMTPConfigs {
|
||||
var password *kms.Secret
|
||||
if c.Password != nil {
|
||||
|
@ -387,12 +412,136 @@ func (c *ACMEConfigs) getACopy() *ACMEConfigs {
|
|||
}
|
||||
}
|
||||
|
||||
// BrandingConfig defines the branding configuration
|
||||
type BrandingConfig struct {
|
||||
Name string `json:"name"`
|
||||
ShortName string `json:"short_name"`
|
||||
Logo []byte `json:"logo"`
|
||||
Favicon []byte `json:"favicon"`
|
||||
DisclaimerName string `json:"disclaimer_name"`
|
||||
DisclaimerURL string `json:"disclaimer_url"`
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) isEmpty() bool {
|
||||
if c.Name != "" {
|
||||
return false
|
||||
}
|
||||
if c.ShortName != "" {
|
||||
return false
|
||||
}
|
||||
if len(c.Logo) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(c.Favicon) > 0 {
|
||||
return false
|
||||
}
|
||||
if c.DisclaimerName != "" && c.DisclaimerURL != "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (*BrandingConfig) validatePNG(b []byte, maxWidth, maxHeight int) error {
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
// DecodeConfig is more efficient, but I'm not sure if this would lead to
|
||||
// accepting invalid images in some edge cases and performance does not
|
||||
// matter here.
|
||||
img, err := png.Decode(bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid PNG image"),
|
||||
util.I18nErrorInvalidPNG,
|
||||
)
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
if bounds.Dx() > maxWidth || bounds.Dy() > maxHeight {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid PNG image size"),
|
||||
util.I18nErrorInvalidPNGSize,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) validateDisclaimerURL() error {
|
||||
if c.DisclaimerURL == "" {
|
||||
return nil
|
||||
}
|
||||
u, err := url.Parse(c.DisclaimerURL)
|
||||
if err != nil {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid disclaimer URL"),
|
||||
util.I18nErrorInvalidDisclaimerURL,
|
||||
)
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid disclaimer URL scheme"),
|
||||
util.I18nErrorInvalidDisclaimerURL,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) validate() error {
|
||||
if err := c.validateDisclaimerURL(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.validatePNG(c.Logo, 512, 512); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.validatePNG(c.Favicon, 256, 256)
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) getACopy() BrandingConfig {
|
||||
logo := make([]byte, len(c.Logo))
|
||||
copy(logo, c.Logo)
|
||||
favicon := make([]byte, len(c.Favicon))
|
||||
copy(favicon, c.Favicon)
|
||||
|
||||
return BrandingConfig{
|
||||
Name: c.Name,
|
||||
ShortName: c.ShortName,
|
||||
Logo: logo,
|
||||
Favicon: favicon,
|
||||
DisclaimerName: c.DisclaimerName,
|
||||
DisclaimerURL: c.DisclaimerURL,
|
||||
}
|
||||
}
|
||||
|
||||
// BrandingConfigs defines the branding configuration for WebAdmin and WebClient UI
|
||||
type BrandingConfigs struct {
|
||||
WebAdmin BrandingConfig
|
||||
WebClient BrandingConfig
|
||||
}
|
||||
|
||||
func (c *BrandingConfigs) isEmpty() bool {
|
||||
return c.WebAdmin.isEmpty() && c.WebClient.isEmpty()
|
||||
}
|
||||
|
||||
func (c *BrandingConfigs) validate() error {
|
||||
if err := c.WebAdmin.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.WebClient.validate()
|
||||
}
|
||||
|
||||
func (c *BrandingConfigs) getACopy() *BrandingConfigs {
|
||||
return &BrandingConfigs{
|
||||
WebAdmin: c.WebAdmin.getACopy(),
|
||||
WebClient: c.WebClient.getACopy(),
|
||||
}
|
||||
}
|
||||
|
||||
// Configs allows to set configuration keys disabled by default without
|
||||
// modifying the config file or setting env vars
|
||||
type Configs struct {
|
||||
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
|
||||
SMTP *SMTPConfigs `json:"smtp,omitempty"`
|
||||
ACME *ACMEConfigs `json:"acme,omitempty"`
|
||||
Branding *BrandingConfigs `json:"branding,omitempty"`
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -412,6 +561,11 @@ func (c *Configs) validate() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if c.Branding != nil {
|
||||
if err := c.Branding.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -428,25 +582,11 @@ func (c *Configs) PrepareForRendering() {
|
|||
if c.ACME != nil && c.ACME.isEmpty() {
|
||||
c.ACME = nil
|
||||
}
|
||||
if c.Branding != nil && c.Branding.isEmpty() {
|
||||
c.Branding = nil
|
||||
}
|
||||
if c.SMTP != nil {
|
||||
if c.SMTP.Password != nil {
|
||||
c.SMTP.Password.Hide()
|
||||
if c.SMTP.Password.IsEmpty() {
|
||||
c.SMTP.Password = nil
|
||||
}
|
||||
}
|
||||
if c.SMTP.OAuth2.ClientSecret != nil {
|
||||
c.SMTP.OAuth2.ClientSecret.Hide()
|
||||
if c.SMTP.OAuth2.ClientSecret.IsEmpty() {
|
||||
c.SMTP.OAuth2.ClientSecret = nil
|
||||
}
|
||||
}
|
||||
if c.SMTP.OAuth2.RefreshToken != nil {
|
||||
c.SMTP.OAuth2.RefreshToken.Hide()
|
||||
if c.SMTP.OAuth2.RefreshToken.IsEmpty() {
|
||||
c.SMTP.OAuth2.RefreshToken = nil
|
||||
}
|
||||
}
|
||||
c.SMTP.prepareForRendering()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -470,6 +610,9 @@ func (c *Configs) SetNilsToEmpty() {
|
|||
if c.ACME == nil {
|
||||
c.ACME = &ACMEConfigs{}
|
||||
}
|
||||
if c.Branding == nil {
|
||||
c.Branding = &BrandingConfigs{}
|
||||
}
|
||||
}
|
||||
|
||||
// RenderAsJSON implements the renderer interface used within plugins
|
||||
|
@ -498,6 +641,9 @@ func (c *Configs) getACopy() Configs {
|
|||
if c.ACME != nil {
|
||||
result.ACME = c.ACME.getACopy()
|
||||
}
|
||||
if c.Branding != nil {
|
||||
result.Branding = c.Branding.getACopy()
|
||||
}
|
||||
result.UpdatedAt = c.UpdatedAt
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -187,6 +188,7 @@ var (
|
|||
ErrDuplicatedKey = errors.New("duplicated key not allowed")
|
||||
// ErrForeignKeyViolated occurs when there is a foreign key constraint violation
|
||||
ErrForeignKeyViolated = errors.New("violates foreign key constraint")
|
||||
tz = ""
|
||||
isAdminCreated atomic.Bool
|
||||
validTLSUsernames = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)}
|
||||
config Config
|
||||
|
@ -518,7 +520,7 @@ type Config struct {
|
|||
// GetShared returns the provider share mode.
|
||||
// This method is called before the provider is initialized
|
||||
func (c *Config) GetShared() int {
|
||||
if !util.Contains(sharedProviders, c.Driver) {
|
||||
if !slices.Contains(sharedProviders, c.Driver) {
|
||||
return 0
|
||||
}
|
||||
return c.IsShared
|
||||
|
@ -590,6 +592,16 @@ func (c *Config) doBackup() (string, error) {
|
|||
return outputFile, nil
|
||||
}
|
||||
|
||||
// SetTZ sets the configured timezone.
|
||||
func SetTZ(val string) {
|
||||
tz = val
|
||||
}
|
||||
|
||||
// UseLocalTime returns true if local time should be used instead of UTC.
|
||||
func UseLocalTime() bool {
|
||||
return tz == "local"
|
||||
}
|
||||
|
||||
// ExecuteBackup executes a backup
|
||||
func ExecuteBackup() (string, error) {
|
||||
return config.doBackup()
|
||||
|
@ -874,7 +886,7 @@ func SetTempPath(fsPath string) {
|
|||
}
|
||||
|
||||
func checkSharedMode() {
|
||||
if !util.Contains(sharedProviders, config.Driver) {
|
||||
if !slices.Contains(sharedProviders, config.Driver) {
|
||||
config.IsShared = 0
|
||||
}
|
||||
}
|
||||
|
@ -929,12 +941,13 @@ func checkDatabase(checkAdmins bool) error {
|
|||
if config.UpdateMode == 0 {
|
||||
err := provider.initializeDatabase()
|
||||
if err != nil && err != ErrNoInitRequired {
|
||||
logger.WarnToConsole("Unable to initialize data provider: %v", err)
|
||||
providerLog(logger.LevelError, "Unable to initialize data provider: %v", err)
|
||||
logger.WarnToConsole("unable to initialize data provider: %v", err)
|
||||
providerLog(logger.LevelError, "unable to initialize data provider: %v", err)
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
logger.DebugToConsole("Data provider successfully initialized")
|
||||
logger.DebugToConsole("data provider successfully initialized")
|
||||
providerLog(logger.LevelInfo, "data provider successfully initialized")
|
||||
}
|
||||
err = provider.migrateDatabase()
|
||||
if err != nil && err != ErrNoInitRequired {
|
||||
|
@ -1503,6 +1516,15 @@ func UpdateUserQuota(user *User, filesAdd int, sizeAdd int64, reset bool) error
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserFolderQuota updates the quota for the given user and virtual folder.
|
||||
func UpdateUserFolderQuota(folder *vfs.VirtualFolder, user *User, filesAdd int, sizeAdd int64, reset bool) {
|
||||
if folder.IsIncludedInUserQuota() {
|
||||
UpdateUserQuota(user, filesAdd, sizeAdd, reset) //nolint:errcheck
|
||||
return
|
||||
}
|
||||
UpdateVirtualFolderQuota(&folder.BaseVirtualFolder, filesAdd, sizeAdd, reset) //nolint:errcheck
|
||||
}
|
||||
|
||||
// UpdateVirtualFolderQuota updates the quota for the given virtual folder adding filesAdd and sizeAdd.
|
||||
// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.
|
||||
func UpdateVirtualFolderQuota(vfolder *vfs.BaseVirtualFolder, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
|
@ -1693,7 +1715,7 @@ func IPListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error)
|
|||
|
||||
// GetIPListEntries returns the IP list entries applying the specified criteria and search limit
|
||||
func GetIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {
|
||||
if !util.Contains(supportedIPListType, listType) {
|
||||
if !slices.Contains(supportedIPListType, listType) {
|
||||
return nil, util.NewValidationError(fmt.Sprintf("invalid list type %d", listType))
|
||||
}
|
||||
return provider.getIPListEntries(listType, filter, from, order, limit)
|
||||
|
@ -2352,7 +2374,7 @@ func GetFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtua
|
|||
}
|
||||
|
||||
func dumpUsers(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeUsers) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeUsers) {
|
||||
users, err := provider.dumpUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2363,7 +2385,7 @@ func dumpUsers(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpFolders(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeFolders) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeFolders) {
|
||||
folders, err := provider.dumpFolders()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2374,7 +2396,7 @@ func dumpFolders(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpGroups(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeGroups) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeGroups) {
|
||||
groups, err := provider.dumpGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2385,7 +2407,7 @@ func dumpGroups(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpAdmins(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeAdmins) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeAdmins) {
|
||||
admins, err := provider.dumpAdmins()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2396,7 +2418,7 @@ func dumpAdmins(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpAPIKeys(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeAPIKeys) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeAPIKeys) {
|
||||
apiKeys, err := provider.dumpAPIKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2407,7 +2429,7 @@ func dumpAPIKeys(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpShares(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeShares) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeShares) {
|
||||
shares, err := provider.dumpShares()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2418,7 +2440,7 @@ func dumpShares(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpActions(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeActions) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeActions) {
|
||||
actions, err := provider.dumpEventActions()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2429,7 +2451,7 @@ func dumpActions(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpRules(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeRules) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeRules) {
|
||||
rules, err := provider.dumpEventRules()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2440,7 +2462,7 @@ func dumpRules(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpRoles(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeRoles) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeRoles) {
|
||||
roles, err := provider.dumpRoles()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2451,7 +2473,7 @@ func dumpRoles(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpIPLists(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeIPLists) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeIPLists) {
|
||||
ipLists, err := provider.dumpIPListEntries()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2462,7 +2484,7 @@ func dumpIPLists(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpConfigs(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeConfigs) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeConfigs) {
|
||||
configs, err := provider.getConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2766,7 +2788,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
|
|||
if c.ConfigName == "" {
|
||||
return util.NewValidationError("totp: config name is mandatory")
|
||||
}
|
||||
if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
|
||||
if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
|
||||
return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
|
||||
}
|
||||
if c.Secret.IsEmpty() {
|
||||
|
@ -2782,7 +2804,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
|
|||
return util.NewValidationError("totp: specify at least one protocol")
|
||||
}
|
||||
for _, protocol := range c.Protocols {
|
||||
if !util.Contains(MFAProtocols, protocol) {
|
||||
if !slices.Contains(MFAProtocols, protocol) {
|
||||
return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %q", protocol))
|
||||
}
|
||||
}
|
||||
|
@ -2815,7 +2837,7 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
|
|||
return permissions, util.NewValidationError("invalid permissions")
|
||||
}
|
||||
for _, p := range perms {
|
||||
if !util.Contains(ValidPerms, p) {
|
||||
if !slices.Contains(ValidPerms, p) {
|
||||
return permissions, util.NewValidationError(fmt.Sprintf("invalid permission: %q", p))
|
||||
}
|
||||
}
|
||||
|
@ -2829,7 +2851,7 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
|
|||
if dir != cleanedDir && cleanedDir == "/" {
|
||||
return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %q is an alias for \"/\"", dir))
|
||||
}
|
||||
if util.Contains(perms, PermAny) {
|
||||
if slices.Contains(perms, PermAny) {
|
||||
permissions[cleanedDir] = []string{PermAny}
|
||||
} else {
|
||||
permissions[cleanedDir] = util.RemoveDuplicates(perms, false)
|
||||
|
@ -2905,7 +2927,7 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
|
|||
util.I18nErrorFilePatternPathInvalid,
|
||||
)
|
||||
}
|
||||
if util.Contains(filteredPaths, cleanedPath) {
|
||||
if slices.Contains(filteredPaths, cleanedPath) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path)),
|
||||
util.I18nErrorFilePatternDuplicated,
|
||||
|
@ -3024,13 +3046,13 @@ func validateFilterProtocols(filters *sdk.BaseUserFilters) error {
|
|||
return util.NewValidationError("invalid denied_protocols")
|
||||
}
|
||||
for _, p := range filters.DeniedProtocols {
|
||||
if !util.Contains(ValidProtocols, p) {
|
||||
if !slices.Contains(ValidProtocols, p) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid denied protocol %q", p))
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range filters.TwoFactorAuthProtocols {
|
||||
if !util.Contains(MFAProtocols, p) {
|
||||
if !slices.Contains(MFAProtocols, p) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %q", p))
|
||||
}
|
||||
}
|
||||
|
@ -3086,7 +3108,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
|
|||
return util.NewValidationError("invalid denied_login_methods")
|
||||
}
|
||||
for _, loginMethod := range filters.DeniedLoginMethods {
|
||||
if !util.Contains(ValidLoginMethods, loginMethod) {
|
||||
if !slices.Contains(ValidLoginMethods, loginMethod) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid login method: %q", loginMethod))
|
||||
}
|
||||
}
|
||||
|
@ -3094,7 +3116,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
|
|||
return err
|
||||
}
|
||||
if filters.TLSUsername != "" {
|
||||
if !util.Contains(validTLSUsernames, string(filters.TLSUsername)) {
|
||||
if !slices.Contains(validTLSUsernames, string(filters.TLSUsername)) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid TLS username: %q", filters.TLSUsername))
|
||||
}
|
||||
}
|
||||
|
@ -3104,7 +3126,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
|
|||
}
|
||||
filters.TLSCerts = certs
|
||||
for _, opts := range filters.WebClient {
|
||||
if !util.Contains(sdk.WebClientOptions, opts) {
|
||||
if !slices.Contains(sdk.WebClientOptions, opts) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts))
|
||||
}
|
||||
}
|
||||
|
@ -3172,19 +3194,19 @@ func validateAccessTimeFilters(filters *sdk.BaseUserFilters) error {
|
|||
}
|
||||
|
||||
func validateCombinedUserFilters(user *User) error {
|
||||
if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration"),
|
||||
util.I18nErrorDisableActive2FA,
|
||||
)
|
||||
}
|
||||
if user.Filters.RequirePasswordChange && util.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
|
||||
if user.Filters.RequirePasswordChange && slices.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("you cannot require password change and at the same time disallow it"),
|
||||
util.I18nErrorPwdChangeConflict,
|
||||
)
|
||||
}
|
||||
if len(user.Filters.TwoFactorAuthProtocols) > 0 && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
if len(user.Filters.TwoFactorAuthProtocols) > 0 && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("you cannot require two-factor authentication and at the same time disallow it"),
|
||||
util.I18nError2FAConflict,
|
||||
|
@ -3505,7 +3527,7 @@ func checkUserPasscode(user *User, password, protocol string) (string, error) {
|
|||
if user.Filters.TOTPConfig.Enabled {
|
||||
switch protocol {
|
||||
case protocolFTP:
|
||||
if util.Contains(user.Filters.TOTPConfig.Protocols, protocol) {
|
||||
if slices.Contains(user.Filters.TOTPConfig.Protocols, protocol) {
|
||||
// the TOTP passcode has six digits
|
||||
pwdLen := len(password)
|
||||
if pwdLen < 7 {
|
||||
|
@ -3711,7 +3733,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
|
|||
if err := user.LoadAndApplyGroupSettings(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
hasSecondFactor := user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)
|
||||
hasSecondFactor := user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)
|
||||
if !isPartialAuth || !hasSecondFactor {
|
||||
answers, err := client("", "", []string{"Password: "}, []bool{false})
|
||||
if err != nil {
|
||||
|
@ -3729,7 +3751,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
|
|||
}
|
||||
|
||||
func checkKeyboardInteractiveSecondFactor(user *User, client ssh.KeyboardInteractiveChallenge, protocol string) (int, error) {
|
||||
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
|
||||
if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
|
||||
return 1, nil
|
||||
}
|
||||
err := user.Filters.TOTPConfig.Secret.TryDecrypt()
|
||||
|
@ -3853,7 +3875,7 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
|
|||
}
|
||||
if len(answers) == 1 && response.CheckPwd > 0 {
|
||||
if response.CheckPwd == 2 {
|
||||
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
|
||||
if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
|
||||
providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %q",
|
||||
user.Username)
|
||||
return answers, errors.New("TOTP not enabled for SSH protocol")
|
||||
|
@ -4442,7 +4464,7 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
|
|||
// preserve TOTP config and recovery codes
|
||||
user.Filters.TOTPConfig = u.Filters.TOTPConfig
|
||||
user.Filters.RecoveryCodes = u.Filters.RecoveryCodes
|
||||
err = provider.updateUser(&user)
|
||||
user, err = updateUserAfterExternalAuth(&user)
|
||||
if err == nil {
|
||||
if protocol != protocolWebDAV {
|
||||
webDAVUsersCache.swap(&user, password)
|
||||
|
@ -4514,7 +4536,7 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
|||
// preserve TOTP config and recovery codes
|
||||
user.Filters.TOTPConfig = u.Filters.TOTPConfig
|
||||
user.Filters.RecoveryCodes = u.Filters.RecoveryCodes
|
||||
err = provider.updateUser(&user)
|
||||
user, err = updateUserAfterExternalAuth(&user)
|
||||
if err == nil {
|
||||
if protocol != protocolWebDAV {
|
||||
webDAVUsersCache.swap(&user, password)
|
||||
|
@ -4530,6 +4552,13 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
|||
return provider.userExists(user.Username, "")
|
||||
}
|
||||
|
||||
func updateUserAfterExternalAuth(user *User) (User, error) {
|
||||
if err := provider.updateUser(user); err != nil {
|
||||
return *user, err
|
||||
}
|
||||
return provider.userExists(user.Username, "")
|
||||
}
|
||||
|
||||
func getUserForHook(username string, oidcTokenFields *map[string]any) (User, User, error) {
|
||||
u, err := provider.userExists(username, "")
|
||||
if err != nil {
|
||||
|
@ -4612,7 +4641,7 @@ func getConfigPath(name, configDir string) string {
|
|||
}
|
||||
|
||||
func checkReservedUsernames(username string) error {
|
||||
if util.Contains(reservedUsers, username) {
|
||||
if slices.Contains(reservedUsers, username) {
|
||||
return util.NewValidationError("this username is reserved")
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -49,17 +50,18 @@ const (
|
|||
ActionTypeUserExpirationCheck
|
||||
ActionTypeIDPAccountCheck
|
||||
ActionTypeUserInactivityCheck
|
||||
ActionTypeRotateLogs
|
||||
)
|
||||
|
||||
var (
|
||||
supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
|
||||
ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
|
||||
ActionTypeDataRetentionCheck, ActionTypePasswordExpirationCheck,
|
||||
ActionTypeUserExpirationCheck, ActionTypeUserInactivityCheck, ActionTypeIDPAccountCheck}
|
||||
ActionTypeDataRetentionCheck, ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck,
|
||||
ActionTypeUserInactivityCheck, ActionTypeIDPAccountCheck, ActionTypeRotateLogs}
|
||||
)
|
||||
|
||||
func isActionTypeValid(action int) bool {
|
||||
return util.Contains(supportedEventActions, action)
|
||||
return slices.Contains(supportedEventActions, action)
|
||||
}
|
||||
|
||||
func getActionTypeAsString(action int) string {
|
||||
|
@ -88,6 +90,8 @@ func getActionTypeAsString(action int) string {
|
|||
return util.I18nActionTypeUserInactivityCheck
|
||||
case ActionTypeIDPAccountCheck:
|
||||
return util.I18nActionTypeIDPCheck
|
||||
case ActionTypeRotateLogs:
|
||||
return util.I18nActionTypeRotateLogs
|
||||
default:
|
||||
return util.I18nActionTypeCommand
|
||||
}
|
||||
|
@ -112,7 +116,7 @@ var (
|
|||
)
|
||||
|
||||
func isEventTriggerValid(trigger int) bool {
|
||||
return util.Contains(supportedEventTriggers, trigger)
|
||||
return slices.Contains(supportedEventTriggers, trigger)
|
||||
}
|
||||
|
||||
func getTriggerTypeAsString(trigger int) string {
|
||||
|
@ -166,7 +170,7 @@ var (
|
|||
)
|
||||
|
||||
func isFilesystemActionValid(value int) bool {
|
||||
return util.Contains(supportedFsActions, value)
|
||||
return slices.Contains(supportedFsActions, value)
|
||||
}
|
||||
|
||||
func getFsActionTypeAsString(value int) string {
|
||||
|
@ -377,7 +381,7 @@ func (c *EventActionHTTPConfig) validate(additionalData string) error {
|
|||
return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP password: %v", err))
|
||||
}
|
||||
}
|
||||
if !util.Contains(SupportedHTTPActionMethods, c.Method) {
|
||||
if !slices.Contains(SupportedHTTPActionMethods, c.Method) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported HTTP method: %s", c.Method))
|
||||
}
|
||||
for _, kv := range c.QueryParameters {
|
||||
|
@ -398,11 +402,11 @@ func (c *EventActionHTTPConfig) GetContext() (context.Context, context.CancelFun
|
|||
|
||||
// HasObjectData returns true if the {{ObjectData}} placeholder is defined
|
||||
func (c *EventActionHTTPConfig) HasObjectData() bool {
|
||||
if strings.Contains(c.Body, "{{ObjectData}}") {
|
||||
if strings.Contains(c.Body, "{{ObjectData}}") || strings.Contains(c.Body, "{{ObjectDataString}}") {
|
||||
return true
|
||||
}
|
||||
for _, part := range c.Parts {
|
||||
if strings.Contains(part.Body, "{{ObjectData}}") {
|
||||
if strings.Contains(part.Body, "{{ObjectData}}") || strings.Contains(part.Body, "{{ObjectDataString}}") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -1277,7 +1281,7 @@ func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error
|
|||
}
|
||||
if trigger == EventTriggerFsEvent {
|
||||
for _, ev := range fsEvents {
|
||||
if !util.Contains(allowedSyncFsEvents, ev) {
|
||||
if !slices.Contains(allowedSyncFsEvents, ev) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("sync execution is only supported for upload and pre-* events"),
|
||||
util.I18nErrorEvSyncUnsupportedFs,
|
||||
|
@ -1358,12 +1362,12 @@ func (f *ConditionOptions) validate() error {
|
|||
}
|
||||
|
||||
for _, p := range f.Protocols {
|
||||
if !util.Contains(SupportedRuleConditionProtocols, p) {
|
||||
if !slices.Contains(SupportedRuleConditionProtocols, p) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported rule condition protocol: %q", p))
|
||||
}
|
||||
}
|
||||
for _, p := range f.ProviderObjects {
|
||||
if !util.Contains(SupporteRuleConditionProviderObjects, p) {
|
||||
if !slices.Contains(SupporteRuleConditionProviderObjects, p) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported provider object: %q", p))
|
||||
}
|
||||
}
|
||||
|
@ -1465,7 +1469,7 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
)
|
||||
}
|
||||
for _, ev := range c.FsEvents {
|
||||
if !util.Contains(SupportedFsEvents, ev) {
|
||||
if !slices.Contains(SupportedFsEvents, ev) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported fs event: %q", ev))
|
||||
}
|
||||
}
|
||||
|
@ -1485,7 +1489,7 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
)
|
||||
}
|
||||
for _, ev := range c.ProviderEvents {
|
||||
if !util.Contains(SupportedProviderEvents, ev) {
|
||||
if !slices.Contains(SupportedProviderEvents, ev) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported provider event: %q", ev))
|
||||
}
|
||||
}
|
||||
|
@ -1534,7 +1538,7 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
c.Options.MinFileSize = 0
|
||||
c.Options.MaxFileSize = 0
|
||||
c.Schedules = nil
|
||||
if !util.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
|
||||
if !slices.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid Identity Provider login event %d", c.IDPLoginEvent))
|
||||
}
|
||||
default:
|
||||
|
@ -1687,7 +1691,7 @@ func (r *EventRule) validateMandatorySyncActions() error {
|
|||
return nil
|
||||
}
|
||||
for _, ev := range r.Conditions.FsEvents {
|
||||
if util.Contains(mandatorySyncFsEvents, ev) {
|
||||
if slices.Contains(mandatorySyncFsEvents, ev) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev)),
|
||||
util.I18nErrorRuleSyncActionRequired,
|
||||
|
@ -1705,7 +1709,7 @@ func (r *EventRule) checkIPBlockedAndCertificateActions() error {
|
|||
ActionTypeDataRetentionCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck,
|
||||
ActionTypeUserExpirationCheck}
|
||||
for _, action := range r.Actions {
|
||||
if util.Contains(unavailableActions, action.Type) {
|
||||
if slices.Contains(unavailableActions, action.Type) {
|
||||
return fmt.Errorf("action %q, type %q is not supported for event trigger %q",
|
||||
action.Name, getActionTypeAsString(action.Type), getTriggerTypeAsString(r.Trigger))
|
||||
}
|
||||
|
@ -1721,7 +1725,7 @@ func (r *EventRule) checkProviderEventActions(providerObjectType string) error {
|
|||
ActionTypeDataRetentionCheck, ActionTypeFilesystem,
|
||||
ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck}
|
||||
for _, action := range r.Actions {
|
||||
if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
|
||||
if slices.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
|
||||
return fmt.Errorf("action %q, type %q is only supported for provider user events",
|
||||
action.Name, getActionTypeAsString(action.Type))
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
@ -85,7 +86,7 @@ var (
|
|||
|
||||
// CheckIPListType returns an error if the provided IP list type is not valid
|
||||
func CheckIPListType(t IPListType) error {
|
||||
if !util.Contains(supportedIPListType, t) {
|
||||
if !slices.Contains(supportedIPListType, t) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid list type %d", t))
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -1210,7 +1211,7 @@ func (p *MemoryProvider) addRuleToActionMapping(ruleName, actionName string) err
|
|||
if err != nil {
|
||||
return util.NewGenericError(fmt.Sprintf("action %q does not exist", actionName))
|
||||
}
|
||||
if !util.Contains(a.Rules, ruleName) {
|
||||
if !slices.Contains(a.Rules, ruleName) {
|
||||
a.Rules = append(a.Rules, ruleName)
|
||||
p.dbHandle.actions[actionName] = a
|
||||
}
|
||||
|
@ -1223,7 +1224,7 @@ func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string
|
|||
providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName)
|
||||
return
|
||||
}
|
||||
if util.Contains(a.Rules, ruleName) {
|
||||
if slices.Contains(a.Rules, ruleName) {
|
||||
var rules []string
|
||||
for _, r := range a.Rules {
|
||||
if r != ruleName {
|
||||
|
@ -1240,7 +1241,7 @@ func (p *MemoryProvider) addAdminToGroupMapping(username, groupname string) erro
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(g.Admins, username) {
|
||||
if !slices.Contains(g.Admins, username) {
|
||||
g.Admins = append(g.Admins, username)
|
||||
p.dbHandle.groups[groupname] = g
|
||||
}
|
||||
|
@ -1283,7 +1284,7 @@ func (p *MemoryProvider) addUserToGroupMapping(username, groupname string) error
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(g.Users, username) {
|
||||
if !slices.Contains(g.Users, username) {
|
||||
g.Users = append(g.Users, username)
|
||||
p.dbHandle.groups[groupname] = g
|
||||
}
|
||||
|
@ -1313,7 +1314,7 @@ func (p *MemoryProvider) addAdminToRole(username, role string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
|
||||
}
|
||||
if !util.Contains(r.Admins, username) {
|
||||
if !slices.Contains(r.Admins, username) {
|
||||
r.Admins = append(r.Admins, username)
|
||||
p.dbHandle.roles[role] = r
|
||||
}
|
||||
|
@ -1347,7 +1348,7 @@ func (p *MemoryProvider) addUserToRole(username, role string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
|
||||
}
|
||||
if !util.Contains(r.Users, username) {
|
||||
if !slices.Contains(r.Users, username) {
|
||||
r.Users = append(r.Users, username)
|
||||
p.dbHandle.roles[role] = r
|
||||
}
|
||||
|
@ -1378,7 +1379,7 @@ func (p *MemoryProvider) addUserToFolderMapping(username, foldername string) err
|
|||
if err != nil {
|
||||
return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
|
||||
}
|
||||
if !util.Contains(f.Users, username) {
|
||||
if !slices.Contains(f.Users, username) {
|
||||
f.Users = append(f.Users, username)
|
||||
p.dbHandle.vfolders[foldername] = f
|
||||
}
|
||||
|
@ -1390,7 +1391,7 @@ func (p *MemoryProvider) addGroupToFolderMapping(name, foldername string) error
|
|||
if err != nil {
|
||||
return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
|
||||
}
|
||||
if !util.Contains(f.Groups, name) {
|
||||
if !slices.Contains(f.Groups, name) {
|
||||
f.Groups = append(f.Groups, name)
|
||||
p.dbHandle.vfolders[foldername] = f
|
||||
}
|
||||
|
|
|
@ -95,8 +95,8 @@ const (
|
|||
"`last_login` bigint NOT NULL, `filters` longtext NULL, `filesystem` longtext NULL, `additional_info` longtext NULL, " +
|
||||
"`created_at` bigint NOT NULL, `updated_at` bigint NOT NULL, `email` varchar(255) NULL, " +
|
||||
"`upload_data_transfer` integer NOT NULL, `download_data_transfer` integer NOT NULL, " +
|
||||
"`total_data_transfer` integer NOT NULL, `used_upload_data_transfer` integer NOT NULL, " +
|
||||
"`used_download_data_transfer` integer NOT NULL, `deleted_at` bigint NOT NULL, `first_download` bigint NOT NULL, " +
|
||||
"`total_data_transfer` integer NOT NULL, `used_upload_data_transfer` bigint NOT NULL, " +
|
||||
"`used_download_data_transfer` bigint NOT NULL, `deleted_at` bigint NOT NULL, `first_download` bigint NOT NULL, " +
|
||||
"`first_upload` bigint NOT NULL, `last_password_change` bigint NOT NULL, `role_id` integer NULL);" +
|
||||
"CREATE TABLE `{{groups_folders_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
|
||||
"`group_id` integer NOT NULL, `folder_id` integer NOT NULL, " +
|
||||
|
@ -193,11 +193,7 @@ const (
|
|||
"CREATE INDEX `{{prefix}}ip_lists_updated_at_idx` ON `{{ip_lists}}` (`updated_at`);" +
|
||||
"CREATE INDEX `{{prefix}}ip_lists_deleted_at_idx` ON `{{ip_lists}}` (`deleted_at`);" +
|
||||
"CREATE INDEX `{{prefix}}ip_lists_first_last_idx` ON `{{ip_lists}}` (`first`, `last`);" +
|
||||
"INSERT INTO {{schema_version}} (version) VALUES (28);"
|
||||
mysqlV29SQL = "ALTER TABLE `{{users}}` MODIFY `used_download_data_transfer` bigint NOT NULL;" +
|
||||
"ALTER TABLE `{{users}}` MODIFY `used_upload_data_transfer` bigint NOT NULL;"
|
||||
mysqlV29DownSQL = "ALTER TABLE `{{users}}` MODIFY `used_upload_data_transfer` integer NOT NULL;" +
|
||||
"ALTER TABLE `{{users}}` MODIFY `used_download_data_transfer` integer NOT NULL;"
|
||||
"INSERT INTO {{schema_version}} (version) VALUES (29);"
|
||||
)
|
||||
|
||||
// MySQLProvider defines the auth provider for MySQL/MariaDB database
|
||||
|
@ -776,11 +772,11 @@ func (p *MySQLProvider) initializeDatabase() error {
|
|||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errSchemaVersionEmpty
|
||||
}
|
||||
logger.InfoToConsole("creating initial database schema, version 28")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 28")
|
||||
logger.InfoToConsole("creating initial database schema, version 29")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 29")
|
||||
initialSQL := sqlReplaceAll(mysqlInitialSQL)
|
||||
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(initialSQL, ";"), 28, true)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(initialSQL, ";"), 29, true)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) migrateDatabase() error {
|
||||
|
@ -793,13 +789,11 @@ func (p *MySQLProvider) migrateDatabase() error {
|
|||
case version == sqlDatabaseVersion:
|
||||
providerLog(logger.LevelDebug, "sql database is up to date, current version: %d", version)
|
||||
return ErrNoInitRequired
|
||||
case version < 28:
|
||||
case version < 29:
|
||||
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
|
||||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 28:
|
||||
return updateMySQLDatabaseFrom28To29(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||
|
@ -822,8 +816,6 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
|
|||
}
|
||||
|
||||
switch dbVersion.Version {
|
||||
case 29:
|
||||
return downgradeMySQLDatabaseFrom29To28(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
||||
}
|
||||
|
@ -861,19 +853,3 @@ func (p *MySQLProvider) normalizeError(err error, fieldType int) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFrom28To29(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database schema version: 28 -> 29")
|
||||
providerLog(logger.LevelInfo, "updating database schema version: 28 -> 29")
|
||||
|
||||
sql := strings.ReplaceAll(mysqlV29SQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 29, true)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFrom29To28(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database schema version: 29 -> 28")
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: 29 -> 28")
|
||||
|
||||
sql := strings.ReplaceAll(mysqlV29DownSQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 28, false)
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -95,7 +96,7 @@ CREATE TABLE "{{users}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS
|
|||
"download_bandwidth" integer NOT NULL, "last_login" bigint NOT NULL, "filters" text NULL, "filesystem" text NULL,
|
||||
"additional_info" text NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "email" varchar(255) NULL,
|
||||
"upload_data_transfer" integer NOT NULL, "download_data_transfer" integer NOT NULL, "total_data_transfer" integer NOT NULL,
|
||||
"used_upload_data_transfer" integer NOT NULL, "used_download_data_transfer" integer NOT NULL, "deleted_at" bigint NOT NULL,
|
||||
"used_upload_data_transfer" bigint NOT NULL, "used_download_data_transfer" bigint NOT NULL, "deleted_at" bigint NOT NULL,
|
||||
"first_download" bigint NOT NULL, "first_upload" bigint NOT NULL, "last_password_change" bigint NOT NULL, "role_id" integer NULL);
|
||||
CREATE TABLE "{{groups_folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "group_id" integer NOT NULL,
|
||||
"folder_id" integer NOT NULL, "virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL);
|
||||
|
@ -205,16 +206,10 @@ CREATE INDEX "{{prefix}}ip_lists_ipornet_idx" ON "{{ip_lists}}" ("ipornet");
|
|||
CREATE INDEX "{{prefix}}ip_lists_updated_at_idx" ON "{{ip_lists}}" ("updated_at");
|
||||
CREATE INDEX "{{prefix}}ip_lists_deleted_at_idx" ON "{{ip_lists}}" ("deleted_at");
|
||||
CREATE INDEX "{{prefix}}ip_lists_first_last_idx" ON "{{ip_lists}}" ("first", "last");
|
||||
INSERT INTO {{schema_version}} (version) VALUES (28);
|
||||
INSERT INTO {{schema_version}} (version) VALUES (29);
|
||||
`
|
||||
// not supported in CockroachDB
|
||||
ipListsLikeIndex = `CREATE INDEX "{{prefix}}ip_lists_ipornet_like_idx" ON "{{ip_lists}}" ("ipornet" varchar_pattern_ops);`
|
||||
pgsqlV29SQL = `ALTER TABLE "{{users}}" ALTER COLUMN "used_download_data_transfer" TYPE bigint;
|
||||
ALTER TABLE "{{users}}" ALTER COLUMN "used_upload_data_transfer" TYPE bigint;
|
||||
`
|
||||
pgsqlV29DownSQL = `ALTER TABLE "{{users}}" ALTER COLUMN "used_upload_data_transfer" TYPE integer;
|
||||
ALTER TABLE "{{users}}" ALTER COLUMN "used_download_data_transfer" TYPE integer;
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -311,7 +306,7 @@ func getPGSQLConnectionString(redactedPwd bool) string {
|
|||
if config.DisableSNI {
|
||||
connectionString += " sslsni=0"
|
||||
}
|
||||
if util.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {
|
||||
if slices.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {
|
||||
connectionString += fmt.Sprintf(" target_session_attrs='%s'", config.TargetSessionAttrs)
|
||||
}
|
||||
} else {
|
||||
|
@ -795,8 +790,8 @@ func (p *PGSQLProvider) initializeDatabase() error {
|
|||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errSchemaVersionEmpty
|
||||
}
|
||||
logger.InfoToConsole("creating initial database schema, version 28")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 28")
|
||||
logger.InfoToConsole("creating initial database schema, version 29")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 29")
|
||||
var initialSQL string
|
||||
if config.Driver == CockroachDataProviderName {
|
||||
initialSQL = sqlReplaceAll(pgsqlInitial)
|
||||
|
@ -805,7 +800,7 @@ func (p *PGSQLProvider) initializeDatabase() error {
|
|||
initialSQL = sqlReplaceAll(pgsqlInitial + ipListsLikeIndex)
|
||||
}
|
||||
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{initialSQL}, 28, true)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{initialSQL}, 29, true)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
|
||||
|
@ -818,13 +813,11 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
|
|||
case version == sqlDatabaseVersion:
|
||||
providerLog(logger.LevelDebug, "sql database is up to date, current version: %d", version)
|
||||
return ErrNoInitRequired
|
||||
case version < 28:
|
||||
case version < 29:
|
||||
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
|
||||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 28:
|
||||
return updatePGSQLDatabaseFrom28To29(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||
|
@ -847,8 +840,6 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
|
|||
}
|
||||
|
||||
switch dbVersion.Version {
|
||||
case 29:
|
||||
return downgradePGSQLDatabaseFrom29To28(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
||||
}
|
||||
|
@ -886,19 +877,3 @@ func (p *PGSQLProvider) normalizeError(err error, fieldType int) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFrom28To29(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database schema version: 28 -> 29")
|
||||
providerLog(logger.LevelInfo, "updating database schema version: 28 -> 29")
|
||||
|
||||
sql := strings.ReplaceAll(pgsqlV29SQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 29, true)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFrom29To28(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database schema version: 29 -> 28")
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: 29 -> 28")
|
||||
|
||||
sql := strings.ReplaceAll(pgsqlV29DownSQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 28, false)
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !nosqlite
|
||||
// +build !nosqlite
|
||||
//go:build !nosqlite && cgo
|
||||
// +build !nosqlite,cgo
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
@ -178,7 +178,7 @@ CREATE INDEX "{{prefix}}ip_lists_ip_type_idx" ON "{{ip_lists}}" ("ip_type");
|
|||
CREATE INDEX "{{prefix}}ip_lists_ip_updated_at_idx" ON "{{ip_lists}}" ("updated_at");
|
||||
CREATE INDEX "{{prefix}}ip_lists_ip_deleted_at_idx" ON "{{ip_lists}}" ("deleted_at");
|
||||
CREATE INDEX "{{prefix}}ip_lists_first_last_idx" ON "{{ip_lists}}" ("first", "last");
|
||||
INSERT INTO {{schema_version}} (version) VALUES (28);
|
||||
INSERT INTO {{schema_version}} (version) VALUES (29);
|
||||
`
|
||||
)
|
||||
|
||||
|
@ -693,10 +693,10 @@ func (p *SQLiteProvider) initializeDatabase() error {
|
|||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errSchemaVersionEmpty
|
||||
}
|
||||
logger.InfoToConsole("creating initial database schema, version 28")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 28")
|
||||
logger.InfoToConsole("creating initial database schema, version 29")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 29")
|
||||
sql := sqlReplaceAll(sqliteInitialSQL)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 28, true)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 29, true)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
|
||||
|
@ -709,13 +709,11 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
|
|||
case version == sqlDatabaseVersion:
|
||||
providerLog(logger.LevelDebug, "sql database is up to date, current version: %d", version)
|
||||
return ErrNoInitRequired
|
||||
case version < 28:
|
||||
case version < 29:
|
||||
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
|
||||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 28:
|
||||
return updateSQLiteDatabaseFrom28To29(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||
|
@ -738,8 +736,6 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
|
|||
}
|
||||
|
||||
switch dbVersion.Version {
|
||||
case 29:
|
||||
return downgradeSQLiteDatabaseFrom29To28(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
||||
}
|
||||
|
@ -777,26 +773,6 @@ func (p *SQLiteProvider) normalizeError(err error, fieldType int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFrom28To29(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database schema version: 28 -> 29")
|
||||
providerLog(logger.LevelInfo, "updating database schema version: 28 -> 29")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
return sqlCommonUpdateDatabaseVersion(ctx, dbHandle, 29)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFrom29To28(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database schema version: 29 -> 28")
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: 29 -> 28")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
return sqlCommonUpdateDatabaseVersion(ctx, dbHandle, 28)
|
||||
}
|
||||
|
||||
/*func setPragmaFK(dbHandle *sql.DB, value string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build nosqlite
|
||||
// +build nosqlite
|
||||
//go:build nosqlite || !cgo
|
||||
// +build nosqlite !cgo
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build unixcrypt
|
||||
// +build unixcrypt
|
||||
//go:build unixcrypt && cgo
|
||||
// +build unixcrypt,cgo
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !unixcrypt
|
||||
// +build !unixcrypt
|
||||
//go:build !unixcrypt || !cgo
|
||||
// +build !unixcrypt !cgo
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -342,7 +343,11 @@ func (u *User) isTimeBasedAccessAllowed(when time.Time) bool {
|
|||
if when.IsZero() {
|
||||
when = time.Now()
|
||||
}
|
||||
if UseLocalTime() {
|
||||
when = when.Local()
|
||||
} else {
|
||||
when = when.UTC()
|
||||
}
|
||||
weekDay := when.Weekday()
|
||||
hhMM := when.Format("15:04")
|
||||
for _, p := range u.Filters.AccessTime {
|
||||
|
@ -840,20 +845,20 @@ func (u *User) HasPermissionsInside(virtualPath string) bool {
|
|||
// HasPerm returns true if the user has the given permission or any permission
|
||||
func (u *User) HasPerm(permission, path string) bool {
|
||||
perms := u.GetPermissionsForPath(path)
|
||||
if util.Contains(perms, PermAny) {
|
||||
if slices.Contains(perms, PermAny) {
|
||||
return true
|
||||
}
|
||||
return util.Contains(perms, permission)
|
||||
return slices.Contains(perms, permission)
|
||||
}
|
||||
|
||||
// HasAnyPerm returns true if the user has at least one of the given permissions
|
||||
func (u *User) HasAnyPerm(permissions []string, path string) bool {
|
||||
perms := u.GetPermissionsForPath(path)
|
||||
if util.Contains(perms, PermAny) {
|
||||
if slices.Contains(perms, PermAny) {
|
||||
return true
|
||||
}
|
||||
for _, permission := range permissions {
|
||||
if util.Contains(perms, permission) {
|
||||
if slices.Contains(perms, permission) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -863,11 +868,11 @@ func (u *User) HasAnyPerm(permissions []string, path string) bool {
|
|||
// HasPerms returns true if the user has all the given permissions
|
||||
func (u *User) HasPerms(permissions []string, path string) bool {
|
||||
perms := u.GetPermissionsForPath(path)
|
||||
if util.Contains(perms, PermAny) {
|
||||
if slices.Contains(perms, PermAny) {
|
||||
return true
|
||||
}
|
||||
for _, permission := range permissions {
|
||||
if !util.Contains(perms, permission) {
|
||||
if !slices.Contains(perms, permission) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -927,11 +932,11 @@ func (u *User) IsLoginMethodAllowed(loginMethod, protocol string) bool {
|
|||
if len(u.Filters.DeniedLoginMethods) == 0 {
|
||||
return true
|
||||
}
|
||||
if util.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
|
||||
if slices.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
|
||||
return false
|
||||
}
|
||||
if protocol == protocolSSH && loginMethod == LoginMethodPassword {
|
||||
if util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
|
||||
if slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -965,10 +970,10 @@ func (u *User) IsPartialAuth() bool {
|
|||
method == SSHLoginMethodPassword {
|
||||
continue
|
||||
}
|
||||
if method == LoginMethodPassword && util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
|
||||
if method == LoginMethodPassword && slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
|
||||
continue
|
||||
}
|
||||
if !util.Contains(SSHMultiStepsLoginMethods, method) {
|
||||
if !slices.Contains(SSHMultiStepsLoginMethods, method) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -982,7 +987,7 @@ func (u *User) GetAllowedLoginMethods() []string {
|
|||
if method == SSHLoginMethodPassword {
|
||||
continue
|
||||
}
|
||||
if !util.Contains(u.Filters.DeniedLoginMethods, method) {
|
||||
if !slices.Contains(u.Filters.DeniedLoginMethods, method) {
|
||||
allowedMethods = append(allowedMethods, method)
|
||||
}
|
||||
}
|
||||
|
@ -1052,7 +1057,7 @@ func (u *User) IsFileAllowed(virtualPath string) (bool, int) {
|
|||
|
||||
// CanManageMFA returns true if the user can add a multi-factor authentication configuration
|
||||
func (u *User) CanManageMFA() bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
return false
|
||||
}
|
||||
return len(mfa.GetAvailableTOTPConfigs()) > 0
|
||||
|
@ -1073,39 +1078,39 @@ func (u *User) skipExternalAuth() bool {
|
|||
|
||||
// CanManageShares returns true if the user can add, update and list shares
|
||||
func (u *User) CanManageShares() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
|
||||
}
|
||||
|
||||
// CanResetPassword returns true if this user is allowed to reset its password
|
||||
func (u *User) CanResetPassword() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
|
||||
}
|
||||
|
||||
// CanChangePassword returns true if this user is allowed to change its password
|
||||
func (u *User) CanChangePassword() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
|
||||
}
|
||||
|
||||
// CanChangeAPIKeyAuth returns true if this user is allowed to enable/disable API key authentication
|
||||
func (u *User) CanChangeAPIKeyAuth() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
|
||||
}
|
||||
|
||||
// CanChangeInfo returns true if this user is allowed to change its info such as email and description
|
||||
func (u *User) CanChangeInfo() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
|
||||
}
|
||||
|
||||
// CanManagePublicKeys returns true if this user is allowed to manage public keys
|
||||
// from the WebClient. Used in WebClient UI
|
||||
func (u *User) CanManagePublicKeys() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
|
||||
}
|
||||
|
||||
// CanManageTLSCerts returns true if this user is allowed to manage TLS certificates
|
||||
// from the WebClient. Used in WebClient UI
|
||||
func (u *User) CanManageTLSCerts() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)
|
||||
}
|
||||
|
||||
// CanUpdateProfile returns true if the user is allowed to update the profile.
|
||||
|
@ -1117,7 +1122,7 @@ func (u *User) CanUpdateProfile() bool {
|
|||
// CanAddFilesFromWeb returns true if the client can add files from the web UI.
|
||||
// The specified target is the directory where the files must be uploaded
|
||||
func (u *User) CanAddFilesFromWeb(target string) bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
return false
|
||||
}
|
||||
return u.HasPerm(PermUpload, target) || u.HasPerm(PermOverwrite, target)
|
||||
|
@ -1126,7 +1131,7 @@ func (u *User) CanAddFilesFromWeb(target string) bool {
|
|||
// CanAddDirsFromWeb returns true if the client can add directories from the web UI.
|
||||
// The specified target is the directory where the new directory must be created
|
||||
func (u *User) CanAddDirsFromWeb(target string) bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
return false
|
||||
}
|
||||
return u.HasPerm(PermCreateDirs, target)
|
||||
|
@ -1135,7 +1140,7 @@ func (u *User) CanAddDirsFromWeb(target string) bool {
|
|||
// CanRenameFromWeb returns true if the client can rename objects from the web UI.
|
||||
// The specified src and dest are the source and target directories for the rename.
|
||||
func (u *User) CanRenameFromWeb(src, dest string) bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
return false
|
||||
}
|
||||
return u.HasAnyPerm(permsRenameAny, src) && u.HasAnyPerm(permsRenameAny, dest)
|
||||
|
@ -1144,7 +1149,7 @@ func (u *User) CanRenameFromWeb(src, dest string) bool {
|
|||
// CanDeleteFromWeb returns true if the client can delete objects from the web UI.
|
||||
// The specified target is the parent directory for the object to delete
|
||||
func (u *User) CanDeleteFromWeb(target string) bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
return false
|
||||
}
|
||||
return u.HasAnyPerm(permsDeleteAny, target)
|
||||
|
@ -1153,7 +1158,7 @@ func (u *User) CanDeleteFromWeb(target string) bool {
|
|||
// CanCopyFromWeb returns true if the client can copy objects from the web UI.
|
||||
// The specified src and dest are the source and target directories for the copy.
|
||||
func (u *User) CanCopyFromWeb(src, dest string) bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
return false
|
||||
}
|
||||
if !u.HasPerm(PermListItems, src) {
|
||||
|
@ -1213,7 +1218,7 @@ func (u *User) MustSetSecondFactor() bool {
|
|||
return true
|
||||
}
|
||||
for _, p := range u.Filters.TwoFactorAuthProtocols {
|
||||
if !util.Contains(u.Filters.TOTPConfig.Protocols, p) {
|
||||
if !slices.Contains(u.Filters.TOTPConfig.Protocols, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -1224,11 +1229,11 @@ func (u *User) MustSetSecondFactor() bool {
|
|||
// MustSetSecondFactorForProtocol returns true if the user must set a second factor authentication
|
||||
// for the specified protocol
|
||||
func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
|
||||
if util.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
|
||||
if slices.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
|
||||
if !u.Filters.TOTPConfig.Enabled {
|
||||
return true
|
||||
}
|
||||
if !util.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
|
||||
if !slices.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2718,6 +2718,7 @@ func TestStat(t *testing.T) {
|
|||
|
||||
func TestUploadOverwriteVfolder(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.QuotaFiles = 1000
|
||||
vdir := "/vdir"
|
||||
mappedPath := filepath.Join(os.TempDir(), "vdir")
|
||||
folderName := filepath.Base(mappedPath)
|
||||
|
@ -2749,14 +2750,24 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
folder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, folder.UsedQuotaSize)
|
||||
assert.Equal(t, 1, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), folder.UsedQuotaSize)
|
||||
assert.Equal(t, 0, folder.UsedQuotaFiles)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, user.UsedQuotaSize)
|
||||
assert.Equal(t, 1, user.UsedQuotaFiles)
|
||||
|
||||
err = ftpUploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client, 0)
|
||||
assert.NoError(t, err)
|
||||
folder, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, folder.UsedQuotaSize)
|
||||
assert.Equal(t, 1, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), folder.UsedQuotaSize)
|
||||
assert.Equal(t, 0, folder.UsedQuotaFiles)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, user.UsedQuotaSize)
|
||||
assert.Equal(t, 1, user.UsedQuotaFiles)
|
||||
|
||||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
|
|
|
@ -493,10 +493,7 @@ func (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolve
|
|||
if vfs.HasTruncateSupport(fs) {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -fileSize, false)
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck
|
||||
}
|
||||
|
|
|
@ -885,7 +885,6 @@ func TestTransferErrors(t *testing.T) {
|
|||
fs := newMockOsFs(nil, nil, false, connID, user.GetHomeDir())
|
||||
connection := &Connection{
|
||||
BaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, "", "", user),
|
||||
clientContext: mockCC,
|
||||
}
|
||||
baseTransfer := common.NewBaseTransfer(file, connection.BaseConnection, nil, file.Name(), file.Name(), testfile,
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
ftpserver "github.com/fclairamb/ftpserverlib"
|
||||
"github.com/sftpgo/sdk/plugin/notifier"
|
||||
|
@ -361,7 +362,7 @@ func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext
|
|||
user.Username, user.HomeDir)
|
||||
return nil, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
|
||||
}
|
||||
if util.Contains(user.Filters.DeniedProtocols, common.ProtocolFTP) {
|
||||
if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolFTP) {
|
||||
logger.Info(logSender, connectionID, "cannot login user %q, protocol FTP is not allowed", user.Username)
|
||||
return nil, fmt.Errorf("protocol FTP is not allowed for user %q", user.Username)
|
||||
}
|
||||
|
@ -420,9 +421,9 @@ func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err err
|
|||
metric.AddLoginAttempt(loginMethod)
|
||||
if err == nil {
|
||||
plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolFTP, user.Username, ip, "", nil)
|
||||
common.DelayLogin(nil)
|
||||
} else if err != common.ErrInternalFailure {
|
||||
logger.ConnectionFailedLog(user.Username, ip, loginMethod,
|
||||
common.ProtocolFTP, err.Error())
|
||||
logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolFTP, err.Error())
|
||||
event := common.HostEventLoginFailed
|
||||
logEv := notifier.LogEventTypeLoginFailed
|
||||
if errors.Is(err, util.ErrNotFound) {
|
||||
|
@ -431,6 +432,9 @@ func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err err
|
|||
}
|
||||
common.AddDefenderEvent(ip, common.ProtocolFTP, event)
|
||||
plugin.Handler.NotifyLogEvent(logEv, common.ProtocolFTP, user.Username, ip, "", err)
|
||||
if loginMethod != dataprovider.LoginMethodTLSCertificate {
|
||||
common.DelayLogin(err)
|
||||
}
|
||||
}
|
||||
metric.AddLoginResult(loginMethod, err)
|
||||
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolFTP, err)
|
||||
|
|
|
@ -297,6 +297,7 @@ func changeAdminPassword(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
invalidateToken(r, false)
|
||||
sendAPIResponse(w, r, err, "Password updated", http.StatusOK)
|
||||
}
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ type oauth2TokenRequest struct {
|
|||
BaseRedirectURL string `json:"base_redirect_url"`
|
||||
}
|
||||
|
||||
func handleSMTPOAuth2TokenRequestPost(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleSMTPOAuth2TokenRequestPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
var req oauth2TokenRequest
|
||||
|
@ -115,7 +115,7 @@ func handleSMTPOAuth2TokenRequestPost(w http.ResponseWriter, r *http.Request) {
|
|||
clientSecret.SetAdditionalData(xid.New().String())
|
||||
pendingAuth := newOAuth2PendingAuth(req.Provider, cfg.RedirectURL, cfg.ClientID, clientSecret)
|
||||
oauth2Mgr.addPendingAuth(pendingAuth)
|
||||
stateToken := createOAuth2Token(pendingAuth.State, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
stateToken := createOAuth2Token(s.csrfTokenAuth, pendingAuth.State, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if stateToken == "" {
|
||||
sendAPIResponse(w, r, nil, "unable to create state token", http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -531,6 +531,7 @@ func changeUserPassword(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
invalidateToken(r, false)
|
||||
sendAPIResponse(w, r, err, "Password updated", http.StatusOK)
|
||||
}
|
||||
|
||||
|
|
|
@ -551,9 +551,10 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
|
|||
return fmt.Errorf("unable to restore user %q: %w", user.Username, err)
|
||||
}
|
||||
if scanQuota == 1 || (scanQuota == 2 && user.HasQuotaRestrictions()) {
|
||||
if common.QuotaScans.AddUserQuotaScan(user.Username, user.Role) {
|
||||
user, err = dataprovider.GetUserWithGroupSettings(user.Username, "")
|
||||
if err == nil && common.QuotaScans.AddUserQuotaScan(user.Username, user.Role) {
|
||||
logger.Debug(logSender, "", "starting quota scan for restored user: %q", user.Username)
|
||||
go doUserQuotaScan(user) //nolint:errcheck
|
||||
go doUserQuotaScan(&user) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -138,8 +139,7 @@ func saveTOTPConfig(w http.ResponseWriter, r *http.Request) {
|
|||
if claims.MustSetTwoFactorAuth {
|
||||
// force logout
|
||||
defer func() {
|
||||
c := jwtTokenClaims{}
|
||||
c.removeCookie(w, r, baseURL)
|
||||
removeCookie(w, r, baseURL)
|
||||
}()
|
||||
}
|
||||
|
||||
|
@ -276,7 +276,7 @@ func saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []datapr
|
|||
return util.NewValidationError("two-factor authentication must be enabled")
|
||||
}
|
||||
for _, p := range userMerged.Filters.TwoFactorAuthProtocols {
|
||||
if !util.Contains(user.Filters.TOTPConfig.Protocols, p) {
|
||||
if !slices.Contains(user.Filters.TOTPConfig.Protocols, p) {
|
||||
return util.NewValidationError(fmt.Sprintf("totp: the following protocols are required: %q",
|
||||
strings.Join(userMerged.Filters.TwoFactorAuthProtocols, ", ")))
|
||||
}
|
||||
|
|
|
@ -219,7 +219,7 @@ func doStartUserQuotaScan(w http.ResponseWriter, r *http.Request, username strin
|
|||
http.StatusConflict)
|
||||
return
|
||||
}
|
||||
go doUserQuotaScan(user) //nolint:errcheck
|
||||
go doUserQuotaScan(&user) //nolint:errcheck
|
||||
sendAPIResponse(w, r, err, "Scan started", http.StatusAccepted)
|
||||
}
|
||||
|
||||
|
@ -242,14 +242,14 @@ func doStartFolderQuotaScan(w http.ResponseWriter, r *http.Request, name string)
|
|||
sendAPIResponse(w, r, err, "Scan started", http.StatusAccepted)
|
||||
}
|
||||
|
||||
func doUserQuotaScan(user dataprovider.User) error {
|
||||
func doUserQuotaScan(user *dataprovider.User) error {
|
||||
defer common.QuotaScans.RemoveUserQuotaScan(user.Username)
|
||||
numFiles, size, err := user.ScanQuota()
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "error scanning user quota %q: %v", user.Username, err)
|
||||
return err
|
||||
}
|
||||
err = dataprovider.UpdateUserQuota(&user, numFiles, size, true)
|
||||
err = dataprovider.UpdateUserQuota(user, numFiles, size, true)
|
||||
logger.Debug(logSender, "", "user quota scanned, user: %q, error: %v", user.Username, err)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -107,7 +108,7 @@ func addShare(w http.ResponseWriter, r *http.Request) {
|
|||
share.Name = share.ShareID
|
||||
}
|
||||
if share.Password == "" {
|
||||
if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
|
||||
if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
|
||||
sendAPIResponse(w, r, nil, "You are not authorized to share files/folders without a password",
|
||||
http.StatusForbidden)
|
||||
return
|
||||
|
@ -155,7 +156,7 @@ func updateShare(w http.ResponseWriter, r *http.Request) {
|
|||
updatedShare.Password = share.Password
|
||||
}
|
||||
if updatedShare.Password == "" {
|
||||
if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
|
||||
if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
|
||||
sendAPIResponse(w, r, nil, "You are not authorized to share files/folders without a password",
|
||||
http.StatusForbidden)
|
||||
return
|
||||
|
@ -425,36 +426,42 @@ func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
}
|
||||
|
||||
func (s *httpdServer) getShareClaims(r *http.Request, shareID string) (*jwtTokenClaims, error) {
|
||||
token, err := jwtauth.VerifyRequest(s.tokenAuth, r, jwtauth.TokenFromCookie)
|
||||
if err != nil || token == nil {
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
tokenString := jwtauth.TokenFromCookie(r)
|
||||
if tokenString == "" || invalidatedJWTTokens.Get(tokenString) {
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
if !slices.Contains(token.Audience(), tokenAudienceWebShare) {
|
||||
logger.Debug(logSender, "", "invalid token audience for share %q", shareID)
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := validateIPForToken(token, ipAddr); err != nil {
|
||||
logger.Debug(logSender, "", "token for share %q is not valid for the ip address %q", shareID, ipAddr)
|
||||
return nil, err
|
||||
}
|
||||
ctx := jwtauth.NewContext(r.Context(), token, nil)
|
||||
claims, err := getTokenClaims(r.WithContext(ctx))
|
||||
if err != nil || claims.Username != shareID {
|
||||
logger.Debug(logSender, "", "token not valid for share %q", shareID)
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
func (s *httpdServer) checkWebClientShareCredentials(w http.ResponseWriter, r *http.Request, share *dataprovider.Share) error {
|
||||
doRedirect := func() {
|
||||
redirectURL := path.Join(webClientPubSharesPath, share.ShareID, fmt.Sprintf("login?next=%s", url.QueryEscape(r.RequestURI)))
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
}
|
||||
|
||||
token, err := jwtauth.VerifyRequest(s.tokenAuth, r, jwtauth.TokenFromCookie)
|
||||
if err != nil || token == nil {
|
||||
if _, err := s.getShareClaims(r, share.ShareID); err != nil {
|
||||
doRedirect()
|
||||
return errInvalidToken
|
||||
}
|
||||
if !util.Contains(token.Audience(), tokenAudienceWebShare) {
|
||||
logger.Debug(logSender, "", "invalid token audience for share %q", share.ShareID)
|
||||
doRedirect()
|
||||
return errInvalidToken
|
||||
}
|
||||
if tokenValidationMode != tokenValidationNoIPMatch {
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if !util.Contains(token.Audience(), ipAddr) {
|
||||
logger.Debug(logSender, "", "token for share %q is not valid for the ip address %q", share.ShareID, ipAddr)
|
||||
doRedirect()
|
||||
return errInvalidToken
|
||||
}
|
||||
}
|
||||
ctx := jwtauth.NewContext(r.Context(), token, nil)
|
||||
claims, err := getTokenClaims(r.WithContext(ctx))
|
||||
if err != nil || claims.Username != share.ShareID {
|
||||
logger.Debug(logSender, "", "token not valid for share %q", share.ShareID)
|
||||
doRedirect()
|
||||
return errInvalidToken
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -480,7 +487,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, v
|
|||
renderError(err, "", statusCode)
|
||||
return share, nil, err
|
||||
}
|
||||
if !util.Contains(validScopes, share.Scope) {
|
||||
if !slices.Contains(validScopes, share.Scope) {
|
||||
err := errors.New("invalid share scope")
|
||||
renderError(util.NewI18nError(err, util.I18nErrorShareScope), "", http.StatusForbidden)
|
||||
return share, nil, err
|
||||
|
@ -512,6 +519,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, v
|
|||
return share, nil, dataprovider.ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
common.DelayLogin(nil)
|
||||
}
|
||||
user, err := getUserForShare(share)
|
||||
if err != nil {
|
||||
|
@ -536,7 +544,7 @@ func getUserForShare(share dataprovider.Share) (dataprovider.User, error) {
|
|||
if !user.CanManageShares() {
|
||||
return user, util.NewI18nError(util.NewRecordNotFoundError("this share does not exist"), util.I18nError404Message)
|
||||
}
|
||||
if share.Password == "" && util.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) {
|
||||
if share.Password == "" && slices.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) {
|
||||
return user, util.NewI18nError(
|
||||
fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission),
|
||||
util.I18nError403Message,
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -36,6 +37,7 @@ import (
|
|||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk/plugin/notifier"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
|
@ -362,6 +364,16 @@ func streamJSONArray(w http.ResponseWriter, chunkSize int, dataGetter func(limit
|
|||
streamData(w, []byte("]"))
|
||||
}
|
||||
|
||||
func renderPNGImage(w http.ResponseWriter, r *http.Request, b []byte) {
|
||||
if len(b) == 0 {
|
||||
ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusNotFound)
|
||||
render.PlainText(w, r.WithContext(ctx), http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
streamData(w, b)
|
||||
}
|
||||
|
||||
func getCompressedFileName(username string, files []string) string {
|
||||
if len(files) == 1 {
|
||||
name := path.Base(files[0])
|
||||
|
@ -686,6 +698,7 @@ func handleDefenderEventLoginFailed(ipAddr string, err error) error {
|
|||
err = dataprovider.ErrInvalidCredentials
|
||||
}
|
||||
common.AddDefenderEvent(ipAddr, common.ProtocolHTTP, event)
|
||||
common.DelayLogin(err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -700,6 +713,7 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err
|
|||
}
|
||||
if err == nil {
|
||||
plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, protocol, user.Username, ip, "", nil)
|
||||
common.DelayLogin(nil)
|
||||
} else if err != common.ErrInternalFailure && err != common.ErrNoCredentials {
|
||||
logger.ConnectionFailedLog(user.Username, ip, loginMethod, protocol, err.Error())
|
||||
err = handleDefenderEventLoginFailed(ip, err)
|
||||
|
@ -714,7 +728,7 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err
|
|||
}
|
||||
|
||||
func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string, checkSessions bool) error {
|
||||
if util.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
|
||||
if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
|
||||
logger.Info(logSender, connectionID, "cannot login user %q, protocol HTTP is not allowed", user.Username)
|
||||
return util.NewI18nError(
|
||||
fmt.Errorf("protocol HTTP is not allowed for user %q", user.Username),
|
||||
|
@ -746,6 +760,31 @@ func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID
|
|||
return nil
|
||||
}
|
||||
|
||||
func getActiveAdmin(username, ipAddr string) (dataprovider.Admin, error) {
|
||||
admin, err := dataprovider.AdminExists(username)
|
||||
if err != nil {
|
||||
return admin, err
|
||||
}
|
||||
if err := admin.CanLogin(ipAddr); err != nil {
|
||||
return admin, util.NewRecordNotFoundError(fmt.Sprintf("admin %q cannot login: %v", username, err))
|
||||
}
|
||||
return admin, nil
|
||||
}
|
||||
|
||||
func getActiveUser(username string, r *http.Request) (dataprovider.User, error) {
|
||||
user, err := dataprovider.GetUserWithGroupSettings(username, "")
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
if err := user.CheckLoginConditions(); err != nil {
|
||||
return user, util.NewRecordNotFoundError(fmt.Sprintf("user %q cannot login: %v", username, err))
|
||||
}
|
||||
if err := checkHTTPClientUser(&user, r, xid.New().String(), false); err != nil {
|
||||
return user, util.NewRecordNotFoundError(fmt.Sprintf("user %q cannot login: %v", username, err))
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func handleForgotPassword(r *http.Request, username string, isAdmin bool) error {
|
||||
var email, subject string
|
||||
var err error
|
||||
|
@ -756,11 +795,11 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
|
|||
return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
|
||||
}
|
||||
if isAdmin {
|
||||
admin, err = dataprovider.AdminExists(username)
|
||||
admin, err = getActiveAdmin(username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
email = admin.Email
|
||||
subject = fmt.Sprintf("Email Verification Code for admin %q", username)
|
||||
} else {
|
||||
user, err = dataprovider.GetUserWithGroupSettings(username, "")
|
||||
user, err = getActiveUser(username, r)
|
||||
email = user.Email
|
||||
subject = fmt.Sprintf("Email Verification Code for user %q", username)
|
||||
if err == nil {
|
||||
|
@ -775,8 +814,9 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
|
|||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotFound) {
|
||||
handleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), err) //nolint:errcheck
|
||||
logger.Debug(logSender, middleware.GetReqID(r.Context()), "username %q does not exists, reset password request silently ignored, is admin? %v",
|
||||
username, isAdmin)
|
||||
logger.Debug(logSender, middleware.GetReqID(r.Context()),
|
||||
"username %q does not exists or cannot login, reset password request silently ignored, is admin? %t, err: %v",
|
||||
username, isAdmin, err)
|
||||
return nil
|
||||
}
|
||||
return util.NewI18nError(util.NewGenericError("Error retrieving your account, please try again later"), util.I18nErrorGetUser)
|
||||
|
@ -836,7 +876,7 @@ func handleResetPassword(r *http.Request, code, newPassword, confirmPassword str
|
|||
return &admin, &user, util.NewValidationError("invalid confirmation code")
|
||||
}
|
||||
if isAdmin {
|
||||
admin, err = dataprovider.AdminExists(resetCode.Username)
|
||||
admin, err = getActiveAdmin(resetCode.Username, ipAddr)
|
||||
if err != nil {
|
||||
return &admin, &user, util.NewValidationError("unable to associate the confirmation code with an existing admin")
|
||||
}
|
||||
|
@ -849,7 +889,7 @@ func handleResetPassword(r *http.Request, code, newPassword, confirmPassword str
|
|||
err = resetCodesMgr.Delete(code)
|
||||
return &admin, &user, err
|
||||
}
|
||||
user, err = dataprovider.GetUserWithGroupSettings(resetCode.Username, "")
|
||||
user, err = getActiveUser(resetCode.Username, r)
|
||||
if err != nil {
|
||||
return &admin, &user, util.NewValidationError("Unable to associate the confirmation code with an existing user")
|
||||
}
|
||||
|
@ -873,7 +913,7 @@ func isUserAllowedToResetPassword(r *http.Request, user *dataprovider.User) bool
|
|||
if !user.CanResetPassword() {
|
||||
return false
|
||||
}
|
||||
if util.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
|
||||
if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
|
||||
return false
|
||||
}
|
||||
if !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
|
@ -41,6 +42,7 @@ const (
|
|||
tokenAudienceAPIUser tokenAudience = "APIUser"
|
||||
tokenAudienceCSRF tokenAudience = "CSRF"
|
||||
tokenAudienceOAuth2 tokenAudience = "OAuth2"
|
||||
tokenAudienceWebLogin tokenAudience = "WebLogin"
|
||||
)
|
||||
|
||||
type tokenValidation = int
|
||||
|
@ -60,16 +62,17 @@ const (
|
|||
claimMustSetSecondFactorKey = "2fa_required"
|
||||
claimRequiredTwoFactorProtocols = "2fa_protos"
|
||||
claimHideUserPageSection = "hus"
|
||||
claimRef = "ref"
|
||||
basicRealm = "Basic realm=\"SFTPGo\""
|
||||
jwtCookieKey = "jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenDuration = 20 * time.Minute
|
||||
shareTokenDuration = 12 * time.Hour
|
||||
shareTokenDuration = 2 * time.Hour
|
||||
// csrf token duration is greater than normal token duration to reduce issues
|
||||
// with the login form
|
||||
csrfTokenDuration = 6 * time.Hour
|
||||
csrfTokenDuration = 4 * time.Hour
|
||||
tokenRefreshThreshold = 10 * time.Minute
|
||||
tokenValidationMode = tokenValidationFull
|
||||
)
|
||||
|
@ -86,6 +89,8 @@ type jwtTokenClaims struct {
|
|||
MustChangePassword bool
|
||||
RequiredTwoFactorProtocols []string
|
||||
HideUserPageSections int
|
||||
JwtID string
|
||||
Ref string
|
||||
}
|
||||
|
||||
func (c *jwtTokenClaims) hasUserAudience() bool {
|
||||
|
@ -103,6 +108,12 @@ func (c *jwtTokenClaims) asMap() map[string]any {
|
|||
|
||||
claims[claimUsernameKey] = c.Username
|
||||
claims[claimPermissionsKey] = c.Permissions
|
||||
if c.JwtID != "" {
|
||||
claims[jwt.JwtIDKey] = c.JwtID
|
||||
}
|
||||
if c.Ref != "" {
|
||||
claims[claimRef] = c.Ref
|
||||
}
|
||||
if c.Role != "" {
|
||||
claims[claimRole] = c.Role
|
||||
}
|
||||
|
@ -169,6 +180,7 @@ func (c *jwtTokenClaims) Decode(token map[string]any) {
|
|||
c.Permissions = nil
|
||||
c.Username = c.decodeString(token[claimUsernameKey])
|
||||
c.Signature = c.decodeString(token[jwt.SubjectKey])
|
||||
c.JwtID = c.decodeString(token[jwt.JwtIDKey])
|
||||
|
||||
audience := token[jwt.AudienceKey]
|
||||
switch v := audience.(type) {
|
||||
|
@ -176,6 +188,10 @@ func (c *jwtTokenClaims) Decode(token map[string]any) {
|
|||
c.Audience = v
|
||||
}
|
||||
|
||||
if val, ok := token[claimRef]; ok {
|
||||
c.Ref = c.decodeString(val)
|
||||
}
|
||||
|
||||
if val, ok := token[claimAPIKey]; ok {
|
||||
c.APIKeyID = c.decodeString(val)
|
||||
}
|
||||
|
@ -212,33 +228,39 @@ func (c *jwtTokenClaims) Decode(token map[string]any) {
|
|||
}
|
||||
|
||||
func (c *jwtTokenClaims) isCriticalPermRemoved(permissions []string) bool {
|
||||
if util.Contains(permissions, dataprovider.PermAdminAny) {
|
||||
if slices.Contains(permissions, dataprovider.PermAdminAny) {
|
||||
return false
|
||||
}
|
||||
if (util.Contains(c.Permissions, dataprovider.PermAdminManageAdmins) ||
|
||||
util.Contains(c.Permissions, dataprovider.PermAdminAny)) &&
|
||||
!util.Contains(permissions, dataprovider.PermAdminManageAdmins) &&
|
||||
!util.Contains(permissions, dataprovider.PermAdminAny) {
|
||||
if (slices.Contains(c.Permissions, dataprovider.PermAdminManageAdmins) ||
|
||||
slices.Contains(c.Permissions, dataprovider.PermAdminAny)) &&
|
||||
!slices.Contains(permissions, dataprovider.PermAdminManageAdmins) &&
|
||||
!slices.Contains(permissions, dataprovider.PermAdminAny) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *jwtTokenClaims) hasPerm(perm string) bool {
|
||||
if util.Contains(c.Permissions, dataprovider.PermAdminAny) {
|
||||
if slices.Contains(c.Permissions, dataprovider.PermAdminAny) {
|
||||
return true
|
||||
}
|
||||
|
||||
return util.Contains(c.Permissions, perm)
|
||||
return slices.Contains(c.Permissions, perm)
|
||||
}
|
||||
|
||||
func (c *jwtTokenClaims) createToken(tokenAuth *jwtauth.JWTAuth, audience tokenAudience, ip string) (jwt.Token, string, error) {
|
||||
claims := c.asMap()
|
||||
now := time.Now().UTC()
|
||||
|
||||
if _, ok := claims[jwt.JwtIDKey]; !ok {
|
||||
claims[jwt.JwtIDKey] = xid.New().String()
|
||||
}
|
||||
claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
|
||||
if audience == tokenAudienceWebLogin {
|
||||
claims[jwt.ExpirationKey] = now.Add(csrfTokenDuration)
|
||||
} else {
|
||||
claims[jwt.ExpirationKey] = now.Add(tokenDuration)
|
||||
}
|
||||
claims[jwt.AudienceKey] = []string{audience, ip}
|
||||
|
||||
return tokenAuth.Encode(claims)
|
||||
|
@ -274,21 +296,25 @@ func (c *jwtTokenClaims) createAndSetCookie(w http.ResponseWriter, r *http.Reque
|
|||
if audience == tokenAudienceWebShare {
|
||||
duration = shareTokenDuration
|
||||
}
|
||||
setCookie(w, r, basePath, resp["access_token"].(string), duration)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCookie(w http.ResponseWriter, r *http.Request, cookiePath, cookieValue string, duration time.Duration) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: jwtCookieKey,
|
||||
Value: resp["access_token"].(string),
|
||||
Path: basePath,
|
||||
Value: cookieValue,
|
||||
Path: cookiePath,
|
||||
Expires: time.Now().Add(duration),
|
||||
MaxAge: int(duration / time.Second),
|
||||
HttpOnly: true,
|
||||
Secure: isTLS(r),
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *jwtTokenClaims) removeCookie(w http.ResponseWriter, r *http.Request, cookiePath string) {
|
||||
func removeCookie(w http.ResponseWriter, r *http.Request, cookiePath string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: jwtCookieKey,
|
||||
Value: "",
|
||||
|
@ -300,10 +326,10 @@ func (c *jwtTokenClaims) removeCookie(w http.ResponseWriter, r *http.Request, co
|
|||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`)
|
||||
invalidateToken(r)
|
||||
invalidateToken(r, false)
|
||||
}
|
||||
|
||||
func tokenFromContext(r *http.Request) string {
|
||||
func oidcTokenFromContext(r *http.Request) string {
|
||||
if token, ok := r.Context().Value(oidcGeneratedToken).(string); ok {
|
||||
return token
|
||||
}
|
||||
|
@ -324,7 +350,7 @@ func isTokenInvalidated(r *http.Request) bool {
|
|||
var findTokenFns []func(r *http.Request) string
|
||||
findTokenFns = append(findTokenFns, jwtauth.TokenFromHeader)
|
||||
findTokenFns = append(findTokenFns, jwtauth.TokenFromCookie)
|
||||
findTokenFns = append(findTokenFns, tokenFromContext)
|
||||
findTokenFns = append(findTokenFns, oidcTokenFromContext)
|
||||
|
||||
isTokenFound := false
|
||||
for _, fn := range findTokenFns {
|
||||
|
@ -340,14 +366,18 @@ func isTokenInvalidated(r *http.Request) bool {
|
|||
return !isTokenFound
|
||||
}
|
||||
|
||||
func invalidateToken(r *http.Request) {
|
||||
func invalidateToken(r *http.Request, isLoginToken bool) {
|
||||
duration := tokenDuration
|
||||
if isLoginToken {
|
||||
duration = csrfTokenDuration
|
||||
}
|
||||
tokenString := jwtauth.TokenFromHeader(r)
|
||||
if tokenString != "" {
|
||||
invalidatedJWTTokens.Add(tokenString, time.Now().Add(tokenDuration).UTC())
|
||||
invalidatedJWTTokens.Add(tokenString, time.Now().Add(duration).UTC())
|
||||
}
|
||||
tokenString = jwtauth.TokenFromCookie(r)
|
||||
if tokenString != "" {
|
||||
invalidatedJWTTokens.Add(tokenString, time.Now().Add(tokenDuration).UTC())
|
||||
invalidatedJWTTokens.Add(tokenString, time.Now().Add(duration).UTC())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -380,7 +410,22 @@ func getAdminFromToken(r *http.Request) *dataprovider.Admin {
|
|||
return admin
|
||||
}
|
||||
|
||||
func createCSRFToken(ip string) string {
|
||||
func createLoginCookie(w http.ResponseWriter, r *http.Request, csrfTokenAuth *jwtauth.JWTAuth, tokenID, basePath, ip string,
|
||||
) {
|
||||
c := jwtTokenClaims{
|
||||
JwtID: tokenID,
|
||||
}
|
||||
resp, err := c.createTokenResponse(csrfTokenAuth, tokenAudienceWebLogin, ip)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
setCookie(w, r, basePath, resp["access_token"].(string), csrfTokenDuration)
|
||||
}
|
||||
|
||||
func createCSRFToken(w http.ResponseWriter, r *http.Request, csrfTokenAuth *jwtauth.JWTAuth, tokenID,
|
||||
basePath string,
|
||||
) string {
|
||||
ip := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
claims := make(map[string]any)
|
||||
now := time.Now().UTC()
|
||||
|
||||
|
@ -388,7 +433,16 @@ func createCSRFToken(ip string) string {
|
|||
claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
|
||||
claims[jwt.ExpirationKey] = now.Add(csrfTokenDuration)
|
||||
claims[jwt.AudienceKey] = []string{tokenAudienceCSRF, ip}
|
||||
|
||||
if tokenID != "" {
|
||||
createLoginCookie(w, r, csrfTokenAuth, tokenID, basePath, ip)
|
||||
claims[claimRef] = tokenID
|
||||
} else {
|
||||
if c, err := getTokenClaims(r); err == nil {
|
||||
claims[claimRef] = c.JwtID
|
||||
} else {
|
||||
logger.Error(logSender, "", "unable to add reference to CSRF token: %v", err)
|
||||
}
|
||||
}
|
||||
_, tokenString, err := csrfTokenAuth.Encode(claims)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "unable to create CSRF token: %v", err)
|
||||
|
@ -397,29 +451,73 @@ func createCSRFToken(ip string) string {
|
|||
return tokenString
|
||||
}
|
||||
|
||||
func verifyCSRFToken(tokenString, ip string) error {
|
||||
func verifyCSRFToken(r *http.Request, csrfTokenAuth *jwtauth.JWTAuth) error {
|
||||
tokenString := r.Form.Get(csrfFormToken)
|
||||
token, err := jwtauth.VerifyToken(csrfTokenAuth, tokenString)
|
||||
if err != nil || token == nil {
|
||||
logger.Debug(logSender, "", "error validating CSRF token %q: %v", tokenString, err)
|
||||
return fmt.Errorf("unable to verify form token: %v", err)
|
||||
}
|
||||
|
||||
if !util.Contains(token.Audience(), tokenAudienceCSRF) {
|
||||
if !slices.Contains(token.Audience(), tokenAudienceCSRF) {
|
||||
logger.Debug(logSender, "", "error validating CSRF token audience")
|
||||
return errors.New("the form token is not valid")
|
||||
}
|
||||
|
||||
if tokenValidationMode != tokenValidationNoIPMatch {
|
||||
if !util.Contains(token.Audience(), ip) {
|
||||
if err := validateIPForToken(token, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||
logger.Debug(logSender, "", "error validating CSRF token IP audience")
|
||||
return errors.New("the form token is not valid")
|
||||
}
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "error getting token claims for CSRF validation: %v", err)
|
||||
return err
|
||||
}
|
||||
ref, ok := token.Get(claimRef)
|
||||
if !ok {
|
||||
logger.Debug(logSender, "", "error validating CSRF token, missing reference")
|
||||
return errors.New("the form token is not valid")
|
||||
}
|
||||
if claims.JwtID == "" || claims.JwtID != ref.(string) {
|
||||
logger.Debug(logSender, "", "error validating CSRF reference, id %q, reference %q", claims.JwtID, ref)
|
||||
return errors.New("unexpected form token")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createOAuth2Token(state, ip string) string {
|
||||
func verifyLoginCookie(r *http.Request) error {
|
||||
token, _, err := jwtauth.FromContext(r.Context())
|
||||
if err != nil || token == nil {
|
||||
logger.Debug(logSender, "", "error getting login token: %v", err)
|
||||
return errInvalidToken
|
||||
}
|
||||
if isTokenInvalidated(r) {
|
||||
logger.Debug(logSender, "", "the login token has been invalidated")
|
||||
return errInvalidToken
|
||||
}
|
||||
if !slices.Contains(token.Audience(), tokenAudienceWebLogin) {
|
||||
logger.Debug(logSender, "", "the token with id %q is not valid for audience %q", token.JwtID(), tokenAudienceWebLogin)
|
||||
return errInvalidToken
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := validateIPForToken(token, ipAddr); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyLoginCookieAndCSRFToken(r *http.Request, csrfTokenAuth *jwtauth.JWTAuth) error {
|
||||
if err := verifyLoginCookie(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := verifyCSRFToken(r, csrfTokenAuth); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createOAuth2Token(csrfTokenAuth *jwtauth.JWTAuth, state, ip string) string {
|
||||
claims := make(map[string]any)
|
||||
now := time.Now().UTC()
|
||||
|
||||
|
@ -436,7 +534,7 @@ func createOAuth2Token(state, ip string) string {
|
|||
return tokenString
|
||||
}
|
||||
|
||||
func verifyOAuth2Token(tokenString, ip string) (string, error) {
|
||||
func verifyOAuth2Token(csrfTokenAuth *jwtauth.JWTAuth, tokenString, ip string) (string, error) {
|
||||
token, err := jwtauth.VerifyToken(csrfTokenAuth, tokenString)
|
||||
if err != nil || token == nil {
|
||||
logger.Debug(logSender, "", "error validating OAuth2 token %q: %v", tokenString, err)
|
||||
|
@ -446,17 +544,15 @@ func verifyOAuth2Token(tokenString, ip string) (string, error) {
|
|||
)
|
||||
}
|
||||
|
||||
if !util.Contains(token.Audience(), tokenAudienceOAuth2) {
|
||||
if !slices.Contains(token.Audience(), tokenAudienceOAuth2) {
|
||||
logger.Debug(logSender, "", "error validating OAuth2 token audience")
|
||||
return "", util.NewI18nError(errors.New("invalid OAuth2 state"), util.I18nOAuth2InvalidState)
|
||||
}
|
||||
|
||||
if tokenValidationMode != tokenValidationNoIPMatch {
|
||||
if !util.Contains(token.Audience(), ip) {
|
||||
if err := validateIPForToken(token, ip); err != nil {
|
||||
logger.Debug(logSender, "", "error validating OAuth2 token IP audience")
|
||||
return "", util.NewI18nError(errors.New("invalid OAuth2 state"), util.I18nOAuth2InvalidState)
|
||||
}
|
||||
}
|
||||
if val, ok := token.Get(jwt.JwtIDKey); ok {
|
||||
if state, ok := val.(string); ok {
|
||||
return state, nil
|
||||
|
@ -465,3 +561,12 @@ func verifyOAuth2Token(tokenString, ip string) (string, error) {
|
|||
logger.Debug(logSender, "", "jti not found in OAuth2 token")
|
||||
return "", util.NewI18nError(errors.New("invalid OAuth2 state"), util.I18nOAuth2InvalidState)
|
||||
}
|
||||
|
||||
func validateIPForToken(token jwt.Token, ip string) error {
|
||||
if tokenValidationMode != tokenValidationNoIPMatch {
|
||||
if !slices.Contains(token.Audience(), ip) {
|
||||
return errInvalidToken
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -213,10 +213,7 @@ func (c *Connection) handleUploadFile(fs vfs.Fs, resolvedPath, filePath, request
|
|||
if vfs.HasTruncateSupport(fs) {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -fileSize, false)
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck
|
||||
}
|
||||
|
|
|
@ -28,11 +28,10 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/acme"
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
|
@ -196,7 +195,6 @@ var (
|
|||
cleanupTicker *time.Ticker
|
||||
cleanupDone chan bool
|
||||
invalidatedJWTTokens tokenManager
|
||||
csrfTokenAuth *jwtauth.JWTAuth
|
||||
webRootPath string
|
||||
webBasePath string
|
||||
webBaseAdminPath string
|
||||
|
@ -288,12 +286,88 @@ var (
|
|||
installationCodeHint string
|
||||
fnInstallationCodeResolver FnInstallationCodeResolver
|
||||
configurationDir string
|
||||
dbBrandingConfig brandingCache
|
||||
)
|
||||
|
||||
func init() {
|
||||
updateWebAdminURLs("")
|
||||
updateWebClientURLs("")
|
||||
acme.SetReloadHTTPDCertsFn(ReloadCertificateMgr)
|
||||
common.SetUpdateBrandingFn(dbBrandingConfig.Set)
|
||||
}
|
||||
|
||||
type brandingCache struct {
|
||||
mu sync.RWMutex
|
||||
configs *dataprovider.BrandingConfigs
|
||||
}
|
||||
|
||||
func (b *brandingCache) Set(configs *dataprovider.BrandingConfigs) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.configs = configs
|
||||
}
|
||||
|
||||
func (b *brandingCache) getWebAdminLogo() []byte {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
return b.configs.WebAdmin.Logo
|
||||
}
|
||||
|
||||
func (b *brandingCache) getWebAdminFavicon() []byte {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
return b.configs.WebAdmin.Favicon
|
||||
}
|
||||
|
||||
func (b *brandingCache) getWebClientLogo() []byte {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
return b.configs.WebClient.Logo
|
||||
}
|
||||
|
||||
func (b *brandingCache) getWebClientFavicon() []byte {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
return b.configs.WebClient.Favicon
|
||||
}
|
||||
|
||||
func (b *brandingCache) mergeBrandingConfig(branding UIBranding, isWebClient bool) UIBranding {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
var urlPrefix string
|
||||
var cfg dataprovider.BrandingConfig
|
||||
if isWebClient {
|
||||
cfg = b.configs.WebClient
|
||||
urlPrefix = "webclient"
|
||||
} else {
|
||||
cfg = b.configs.WebAdmin
|
||||
urlPrefix = "webadmin"
|
||||
}
|
||||
if cfg.Name != "" {
|
||||
branding.Name = cfg.Name
|
||||
}
|
||||
if cfg.ShortName != "" {
|
||||
branding.ShortName = cfg.ShortName
|
||||
}
|
||||
if cfg.DisclaimerName != "" {
|
||||
branding.DisclaimerName = cfg.DisclaimerName
|
||||
}
|
||||
if cfg.DisclaimerURL != "" {
|
||||
branding.DisclaimerPath = cfg.DisclaimerURL
|
||||
}
|
||||
if len(cfg.Logo) > 0 {
|
||||
branding.LogoPath = path.Join("/", "branding", urlPrefix, "logo.png")
|
||||
}
|
||||
if len(cfg.Favicon) > 0 {
|
||||
branding.FaviconPath = path.Join("/", "branding", urlPrefix, "favicon.png")
|
||||
}
|
||||
return branding
|
||||
}
|
||||
|
||||
// FnInstallationCodeResolver defines a method to get the installation code.
|
||||
|
@ -398,8 +472,6 @@ type UIBranding struct {
|
|||
// For example, if you create a directory named "branding" inside the static dir and
|
||||
// put the "mylogo.png" file in it, you must set "/branding/mylogo.png" as logo path.
|
||||
LogoPath string `json:"logo_path" mapstructure:"logo_path"`
|
||||
// Path to the image to show on the login screen relative to "static_files_path"
|
||||
LoginImagePath string `json:"login_image_path" mapstructure:"login_image_path"`
|
||||
// Path to your favicon relative to "static_files_path"
|
||||
FaviconPath string `json:"favicon_path" mapstructure:"favicon_path"`
|
||||
// DisclaimerName defines the name for the link to your optional disclaimer
|
||||
|
@ -412,23 +484,22 @@ type UIBranding struct {
|
|||
DefaultCSS []string `json:"default_css" mapstructure:"default_css"`
|
||||
// Additional CSS file paths, relative to "static_files_path", to include
|
||||
ExtraCSS []string `json:"extra_css" mapstructure:"extra_css"`
|
||||
DefaultLogoPath string `json:"-" mapstructure:"-"`
|
||||
DefaultFaviconPath string `json:"-" mapstructure:"-"`
|
||||
}
|
||||
|
||||
func (b *UIBranding) check() {
|
||||
b.DefaultLogoPath = "/img/logo.png"
|
||||
b.DefaultFaviconPath = "/favicon.png"
|
||||
if b.LogoPath != "" {
|
||||
b.LogoPath = util.CleanPath(b.LogoPath)
|
||||
} else {
|
||||
b.LogoPath = "/img/logo.png"
|
||||
}
|
||||
if b.LoginImagePath != "" {
|
||||
b.LoginImagePath = util.CleanPath(b.LoginImagePath)
|
||||
} else {
|
||||
b.LoginImagePath = "/img/login_image.png"
|
||||
b.LogoPath = b.DefaultLogoPath
|
||||
}
|
||||
if b.FaviconPath != "" {
|
||||
b.FaviconPath = util.CleanPath(b.FaviconPath)
|
||||
} else {
|
||||
b.FaviconPath = "/favicon.ico"
|
||||
b.FaviconPath = b.DefaultFaviconPath
|
||||
}
|
||||
if b.DisclaimerPath != "" {
|
||||
if !strings.HasPrefix(b.DisclaimerPath, "https://") && !strings.HasPrefix(b.DisclaimerPath, "http://") {
|
||||
|
@ -558,6 +629,14 @@ func (b *Binding) checkBranding() {
|
|||
}
|
||||
}
|
||||
|
||||
func (b *Binding) webAdminBranding() UIBranding {
|
||||
return dbBrandingConfig.mergeBrandingConfig(b.Branding.WebAdmin, false)
|
||||
}
|
||||
|
||||
func (b *Binding) webClientBranding() UIBranding {
|
||||
return dbBrandingConfig.mergeBrandingConfig(b.Branding.WebClient, true)
|
||||
}
|
||||
|
||||
func (b *Binding) parseAllowedProxy() error {
|
||||
if filepath.IsAbs(b.Address) && len(b.ProxyAllowed) > 0 {
|
||||
// unix domain socket
|
||||
|
@ -671,6 +750,10 @@ func (b *Binding) showClientLoginURL() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (b *Binding) isMutualTLSEnabled() bool {
|
||||
return b.ClientAuthType == 1
|
||||
}
|
||||
|
||||
type defenderStatus struct {
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
@ -885,6 +968,7 @@ func (c *Conf) loadFromProvider() error {
|
|||
return fmt.Errorf("unable to load config from provider: %w", err)
|
||||
}
|
||||
configs.SetNilsToEmpty()
|
||||
dbBrandingConfig.Set(configs.Branding)
|
||||
if configs.ACME.Domain == "" || !configs.ACME.HasProtocol(common.ProtocolHTTP) {
|
||||
return nil
|
||||
}
|
||||
|
@ -970,7 +1054,6 @@ func (c *Conf) Initialize(configDir string, isShared int) error {
|
|||
c.SigningPassphrase = passphrase
|
||||
}
|
||||
|
||||
csrfTokenAuth = jwtauth.New(jwa.HS256.String(), getSigningKey(c.SigningPassphrase), nil)
|
||||
hideSupportLink = c.HideSupportLink
|
||||
|
||||
exitChannel := make(chan error, 1)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -331,7 +331,6 @@ func TestBrandingValidation(t *testing.T) {
|
|||
Branding: Branding{
|
||||
WebAdmin: UIBranding{
|
||||
LogoPath: "path1",
|
||||
LoginImagePath: "login1.png",
|
||||
DefaultCSS: []string{"my.css"},
|
||||
},
|
||||
WebClient: UIBranding{
|
||||
|
@ -342,14 +341,12 @@ func TestBrandingValidation(t *testing.T) {
|
|||
},
|
||||
}
|
||||
b.checkBranding()
|
||||
assert.Equal(t, "/favicon.ico", b.Branding.WebAdmin.FaviconPath)
|
||||
assert.Equal(t, "/favicon.png", b.Branding.WebAdmin.FaviconPath)
|
||||
assert.Equal(t, "/path1", b.Branding.WebAdmin.LogoPath)
|
||||
assert.Equal(t, "/login1.png", b.Branding.WebAdmin.LoginImagePath)
|
||||
assert.Equal(t, []string{"/my.css"}, b.Branding.WebAdmin.DefaultCSS)
|
||||
assert.Len(t, b.Branding.WebAdmin.ExtraCSS, 0)
|
||||
assert.Equal(t, "/favicon1.ico", b.Branding.WebClient.FaviconPath)
|
||||
assert.Equal(t, path.Join(webStaticFilesPath, "/path2"), b.Branding.WebClient.DisclaimerPath)
|
||||
assert.Equal(t, "/img/login_image.png", b.Branding.WebClient.LoginImagePath)
|
||||
if assert.Len(t, b.Branding.WebClient.ExtraCSS, 1) {
|
||||
assert.Equal(t, "/1.css", b.Branding.WebClient.ExtraCSS[0])
|
||||
}
|
||||
|
@ -415,6 +412,45 @@ func TestGCSWebInvalidFormFile(t *testing.T) {
|
|||
assert.EqualError(t, err, http.ErrNotMultipart.Error())
|
||||
}
|
||||
|
||||
func TestBrandingInvalidFormFile(t *testing.T) {
|
||||
form := make(url.Values)
|
||||
req, _ := http.NewRequest(http.MethodPost, webConfigsPath, strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
err := req.ParseForm()
|
||||
assert.NoError(t, err)
|
||||
_, err = getBrandingConfigFromPostFields(req, &dataprovider.BrandingConfigs{})
|
||||
assert.EqualError(t, err, http.ErrNotMultipart.Error())
|
||||
}
|
||||
|
||||
func TestVerifyCSRFToken(t *testing.T) {
|
||||
server := httpdServer{}
|
||||
server.initializeRouter()
|
||||
req, err := http.NewRequest(http.MethodPost, webAdminEventActionPath, nil)
|
||||
require.NoError(t, err)
|
||||
req = req.WithContext(context.WithValue(req.Context(), jwtauth.ErrorCtxKey, fs.ErrPermission))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
tokenString := createCSRFToken(rr, req, server.csrfTokenAuth, "", webBaseAdminPath)
|
||||
assert.NotEmpty(t, tokenString)
|
||||
|
||||
token, err := server.csrfTokenAuth.Decode(tokenString)
|
||||
require.NoError(t, err)
|
||||
_, ok := token.Get(claimRef)
|
||||
assert.False(t, ok)
|
||||
|
||||
req.Form = url.Values{}
|
||||
req.Form.Set(csrfFormToken, tokenString)
|
||||
err = verifyCSRFToken(req, server.csrfTokenAuth)
|
||||
assert.ErrorIs(t, err, fs.ErrPermission)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, webAdminEventActionPath, nil)
|
||||
require.NoError(t, err)
|
||||
req.Form = url.Values{}
|
||||
req.Form.Set(csrfFormToken, tokenString)
|
||||
err = verifyCSRFToken(req, server.csrfTokenAuth)
|
||||
assert.ErrorContains(t, err, "the form token is not valid")
|
||||
}
|
||||
|
||||
func TestInvalidToken(t *testing.T) {
|
||||
server := httpdServer{}
|
||||
server.initializeRouter()
|
||||
|
@ -926,13 +962,24 @@ func TestUpdateWebAdminInvalidClaims(t *testing.T) {
|
|||
token, err := c.createTokenResponse(server.tokenAuth, tokenAudienceWebAdmin, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, webAdminPath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
parsedToken, err := jwtauth.VerifyRequest(server.tokenAuth, req, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx := req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, createCSRFToken(""))
|
||||
form.Set(csrfFormToken, createCSRFToken(rr, req, server.csrfTokenAuth, "", webBaseAdminPath))
|
||||
form.Set("status", "1")
|
||||
form.Set("default_users_expiration", "30")
|
||||
req, _ := http.NewRequest(http.MethodPost, path.Join(webAdminPath, "admin"), bytes.NewBuffer([]byte(form.Encode())))
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webAdminPath, "admin"), bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("username", "admin")
|
||||
req = req.WithContext(ctx)
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
|
@ -1031,7 +1078,7 @@ func TestOAuth2Redirect(t *testing.T) {
|
|||
assert.Contains(t, rr.Body.String(), util.I18nOAuth2ErrorTitle)
|
||||
|
||||
ip := "127.1.1.4"
|
||||
tokenString := createOAuth2Token(xid.New().String(), ip)
|
||||
tokenString := createOAuth2Token(server.csrfTokenAuth, xid.New().String(), ip)
|
||||
rr = httptest.NewRecorder()
|
||||
req, err = http.NewRequest(http.MethodGet, webOAuth2RedirectPath+"?state="+tokenString, nil) //nolint:goconst
|
||||
assert.NoError(t, err)
|
||||
|
@ -1042,8 +1089,10 @@ func TestOAuth2Redirect(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOAuth2Token(t *testing.T) {
|
||||
server := httpdServer{}
|
||||
server.initializeRouter()
|
||||
// invalid token
|
||||
_, err := verifyOAuth2Token("token", "")
|
||||
_, err := verifyOAuth2Token(server.csrfTokenAuth, "token", "")
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "unable to verify OAuth2 state")
|
||||
}
|
||||
|
@ -1056,22 +1105,22 @@ func TestOAuth2Token(t *testing.T) {
|
|||
claims[jwt.ExpirationKey] = now.Add(tokenDuration)
|
||||
claims[jwt.AudienceKey] = []string{tokenAudienceAPI}
|
||||
|
||||
_, tokenString, err := csrfTokenAuth.Encode(claims)
|
||||
_, tokenString, err := server.csrfTokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
_, err = verifyOAuth2Token(tokenString, "")
|
||||
_, err = verifyOAuth2Token(server.csrfTokenAuth, tokenString, "")
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "invalid OAuth2 state")
|
||||
}
|
||||
// bad IP
|
||||
tokenString = createOAuth2Token("state", "127.1.1.1")
|
||||
_, err = verifyOAuth2Token(tokenString, "127.1.1.2")
|
||||
tokenString = createOAuth2Token(server.csrfTokenAuth, "state", "127.1.1.1")
|
||||
_, err = verifyOAuth2Token(server.csrfTokenAuth, tokenString, "127.1.1.2")
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "invalid OAuth2 state")
|
||||
}
|
||||
// ok
|
||||
state := xid.New().String()
|
||||
tokenString = createOAuth2Token(state, "127.1.1.3")
|
||||
s, err := verifyOAuth2Token(tokenString, "127.1.1.3")
|
||||
tokenString = createOAuth2Token(server.csrfTokenAuth, state, "127.1.1.3")
|
||||
s, err := verifyOAuth2Token(server.csrfTokenAuth, tokenString, "127.1.1.3")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, state, s)
|
||||
// no jti
|
||||
|
@ -1080,19 +1129,17 @@ func TestOAuth2Token(t *testing.T) {
|
|||
claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
|
||||
claims[jwt.ExpirationKey] = now.Add(tokenDuration)
|
||||
claims[jwt.AudienceKey] = []string{tokenAudienceOAuth2, "127.1.1.4"}
|
||||
_, tokenString, err = csrfTokenAuth.Encode(claims)
|
||||
_, tokenString, err = server.csrfTokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
_, err = verifyOAuth2Token(tokenString, "127.1.1.4")
|
||||
_, err = verifyOAuth2Token(server.csrfTokenAuth, tokenString, "127.1.1.4")
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "invalid OAuth2 state")
|
||||
}
|
||||
// encode error
|
||||
csrfTokenAuth = jwtauth.New("HT256", util.GenerateRandomBytes(32), nil)
|
||||
tokenString = createOAuth2Token(xid.New().String(), "")
|
||||
server.csrfTokenAuth = jwtauth.New("HT256", util.GenerateRandomBytes(32), nil)
|
||||
tokenString = createOAuth2Token(server.csrfTokenAuth, xid.New().String(), "")
|
||||
assert.Empty(t, tokenString)
|
||||
|
||||
server := httpdServer{}
|
||||
server.initializeRouter()
|
||||
rr := httptest.NewRecorder()
|
||||
testReq := make(map[string]any)
|
||||
testReq["base_redirect_url"] = "http://localhost:8082"
|
||||
|
@ -1100,16 +1147,17 @@ func TestOAuth2Token(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPost, webOAuth2TokenPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
handleSMTPOAuth2TokenRequestPost(rr, req)
|
||||
server.handleSMTPOAuth2TokenRequestPost(rr, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "unable to create state token")
|
||||
|
||||
csrfTokenAuth = jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil)
|
||||
}
|
||||
|
||||
func TestCSRFToken(t *testing.T) {
|
||||
server := httpdServer{}
|
||||
server.initializeRouter()
|
||||
// invalid token
|
||||
err := verifyCSRFToken("token", "")
|
||||
req := &http.Request{}
|
||||
err := verifyCSRFToken(req, server.csrfTokenAuth)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "unable to verify form token")
|
||||
}
|
||||
|
@ -1122,16 +1170,23 @@ func TestCSRFToken(t *testing.T) {
|
|||
claims[jwt.ExpirationKey] = now.Add(tokenDuration)
|
||||
claims[jwt.AudienceKey] = []string{tokenAudienceAPI}
|
||||
|
||||
_, tokenString, err := csrfTokenAuth.Encode(claims)
|
||||
_, tokenString, err := server.csrfTokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
err = verifyCSRFToken(tokenString, "")
|
||||
values := url.Values{}
|
||||
values.Set(csrfFormToken, tokenString)
|
||||
req.Form = values
|
||||
err = verifyCSRFToken(req, server.csrfTokenAuth)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "form token is not valid")
|
||||
}
|
||||
|
||||
// bad IP
|
||||
tokenString = createCSRFToken("127.1.1.1")
|
||||
err = verifyCSRFToken(tokenString, "127.1.1.2")
|
||||
req.RemoteAddr = "127.1.1.1"
|
||||
tokenString = createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, "", webBaseAdminPath)
|
||||
values.Set(csrfFormToken, tokenString)
|
||||
req.Form = values
|
||||
req.RemoteAddr = "127.1.1.2"
|
||||
err = verifyCSRFToken(req, server.csrfTokenAuth)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "form token is not valid")
|
||||
}
|
||||
|
@ -1140,8 +1195,9 @@ func TestCSRFToken(t *testing.T) {
|
|||
claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
|
||||
claims[jwt.ExpirationKey] = now.Add(tokenDuration)
|
||||
claims[jwt.AudienceKey] = []string{tokenAudienceAPI}
|
||||
_, tokenString, err = csrfTokenAuth.Encode(claims)
|
||||
_, tokenString, err = server.csrfTokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, tokenString)
|
||||
|
||||
r := GetHTTPRouter(Binding{
|
||||
Address: "",
|
||||
|
@ -1151,9 +1207,9 @@ func TestCSRFToken(t *testing.T) {
|
|||
EnableRESTAPI: true,
|
||||
RenderOpenAPI: true,
|
||||
})
|
||||
fn := verifyCSRFHeader(r)
|
||||
fn := server.verifyCSRFHeader(r)
|
||||
rr := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodDelete, path.Join(userPath, "username"), nil)
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, "username"), nil)
|
||||
fn.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token")
|
||||
|
@ -1166,18 +1222,20 @@ func TestCSRFToken(t *testing.T) {
|
|||
assert.Contains(t, rr.Body.String(), "the token is not valid")
|
||||
|
||||
// invalid IP
|
||||
tokenString = createCSRFToken("172.16.1.2")
|
||||
tokenString = createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, "", webBaseAdminPath)
|
||||
req.Header.Set(csrfHeaderToken, tokenString)
|
||||
req.RemoteAddr = "172.16.1.2"
|
||||
rr = httptest.NewRecorder()
|
||||
fn.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "the token is not valid")
|
||||
|
||||
csrfTokenAuth = jwtauth.New("PS256", util.GenerateRandomBytes(32), nil)
|
||||
tokenString = createCSRFToken("")
|
||||
csrfTokenAuth := jwtauth.New("PS256", util.GenerateRandomBytes(32), nil)
|
||||
tokenString = createCSRFToken(httptest.NewRecorder(), req, csrfTokenAuth, "", webBaseAdminPath)
|
||||
assert.Empty(t, tokenString)
|
||||
|
||||
csrfTokenAuth = jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil)
|
||||
rr = httptest.NewRecorder()
|
||||
createLoginCookie(rr, req, csrfTokenAuth, "", webBaseAdminPath, req.RemoteAddr)
|
||||
assert.Empty(t, rr.Header().Get("Set-Cookie"))
|
||||
}
|
||||
|
||||
func TestCreateShareCookieError(t *testing.T) {
|
||||
|
@ -1209,18 +1267,37 @@ func TestCreateShareCookieError(t *testing.T) {
|
|||
|
||||
server := httpdServer{
|
||||
tokenAuth: jwtauth.New("TS256", util.GenerateRandomBytes(32), nil),
|
||||
csrfTokenAuth: jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil),
|
||||
}
|
||||
|
||||
c := jwtTokenClaims{
|
||||
JwtID: xid.New().String(),
|
||||
}
|
||||
resp, err := c.createTokenResponse(server.csrfTokenAuth, tokenAudienceWebLogin, "127.0.0.1")
|
||||
assert.NoError(t, err)
|
||||
parsedToken, err := jwtauth.VerifyToken(server.csrfTokenAuth, resp["access_token"].(string))
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, share.ShareID, "login"), nil)
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = "127.0.0.1:4567"
|
||||
ctx := req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
form := make(url.Values)
|
||||
form.Set("share_password", pwd)
|
||||
form.Set(csrfFormToken, createCSRFToken("127.0.0.1"))
|
||||
form.Set(csrfFormToken, createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, "", webBaseClientPath))
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("id", share.ShareID)
|
||||
rr := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, share.ShareID, "login"),
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, share.ShareID, "login"),
|
||||
bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = "127.0.0.1:2345"
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", resp["access_token"]))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req = req.WithContext(ctx)
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||
server.handleClientShareLoginPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
|
@ -1233,6 +1310,7 @@ func TestCreateShareCookieError(t *testing.T) {
|
|||
func TestCreateTokenError(t *testing.T) {
|
||||
server := httpdServer{
|
||||
tokenAuth: jwtauth.New("PS256", util.GenerateRandomBytes(32), nil),
|
||||
csrfTokenAuth: jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil),
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
admin := dataprovider.Admin{
|
||||
|
@ -1256,14 +1334,36 @@ func TestCreateTokenError(t *testing.T) {
|
|||
server.generateAndSendUserToken(rr, req, "", user)
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
|
||||
c := jwtTokenClaims{
|
||||
JwtID: xid.New().String(),
|
||||
}
|
||||
token, err := c.createTokenResponse(server.csrfTokenAuth, tokenAudienceWebLogin, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
parsedToken, err := jwtauth.VerifyRequest(server.csrfTokenAuth, req, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx := req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
form := make(url.Values)
|
||||
form.Set("username", admin.Username)
|
||||
form.Set("password", admin.Password)
|
||||
form.Set(csrfFormToken, createCSRFToken("127.0.0.1"))
|
||||
form.Set(csrfFormToken, createCSRFToken(rr, req, server.csrfTokenAuth, xid.New().String(), webBaseAdminPath))
|
||||
cookie := rr.Header().Get("Set-Cookie")
|
||||
assert.NotEmpty(t, cookie)
|
||||
req, _ = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.RemoteAddr = "127.0.0.1:1234"
|
||||
req.Header.Set("Cookie", cookie)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
parsedToken, err = jwtauth.VerifyRequest(server.csrfTokenAuth, req, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx = req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
server.handleWebAdminLoginPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
// req with no content type
|
||||
|
@ -1290,7 +1390,7 @@ func TestCreateTokenError(t *testing.T) {
|
|||
|
||||
req, _ = http.NewRequest(http.MethodGet, webAdminLoginPath+"?a=a%C3%A2%G3", nil)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
_, err := getAdminFromPostFields(req)
|
||||
_, err = getAdminFromPostFields(req)
|
||||
assert.Error(t, err)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodPost, webAdminEventActionPath+"?a=a%C3%A2%GG", nil)
|
||||
|
@ -1424,13 +1524,21 @@ func TestCreateTokenError(t *testing.T) {
|
|||
err = dataprovider.AddUser(&user, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webClientLoginPath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
parsedToken, err = jwtauth.VerifyRequest(server.csrfTokenAuth, req, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx = req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
form = make(url.Values)
|
||||
form.Set("username", user.Username)
|
||||
form.Set("password", "clientpwd")
|
||||
form.Set(csrfFormToken, createCSRFToken("127.0.0.1"))
|
||||
form.Set(csrfFormToken, createCSRFToken(rr, req, server.csrfTokenAuth, "", webBaseClientPath))
|
||||
req, _ = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.RemoteAddr = "127.0.0.1:4567"
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
server.handleWebClientLoginPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
|
@ -1619,6 +1727,7 @@ func TestCookieExpiration(t *testing.T) {
|
|||
claims = make(map[string]any)
|
||||
claims[claimUsernameKey] = admin.Username
|
||||
claims[claimPermissionsKey] = admin.Permissions
|
||||
claims[jwt.JwtIDKey] = xid.New().String()
|
||||
claims[jwt.SubjectKey] = admin.GetSignature()
|
||||
claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
|
||||
claims[jwt.AudienceKey] = []string{tokenAudienceAPI}
|
||||
|
@ -1651,9 +1760,11 @@ func TestCookieExpiration(t *testing.T) {
|
|||
|
||||
admin, err = dataprovider.AdminExists(admin.Username)
|
||||
assert.NoError(t, err)
|
||||
tokenID := xid.New().String()
|
||||
claims = make(map[string]any)
|
||||
claims[claimUsernameKey] = admin.Username
|
||||
claims[claimPermissionsKey] = admin.Permissions
|
||||
claims[jwt.JwtIDKey] = tokenID
|
||||
claims[jwt.SubjectKey] = admin.GetSignature()
|
||||
claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
|
||||
claims[jwt.AudienceKey] = []string{tokenAudienceAPI}
|
||||
|
@ -1672,6 +1783,11 @@ func TestCookieExpiration(t *testing.T) {
|
|||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.True(t, strings.HasPrefix(cookie, "jwt="))
|
||||
req.Header.Set("Cookie", cookie)
|
||||
token, err = jwtauth.VerifyRequest(server.tokenAuth, req, jwtauth.TokenFromCookie)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, tokenID, token.JwtID())
|
||||
}
|
||||
|
||||
err = dataprovider.DeleteAdmin(admin.Username, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
@ -1692,6 +1808,7 @@ func TestCookieExpiration(t *testing.T) {
|
|||
claims = make(map[string]any)
|
||||
claims[claimUsernameKey] = user.Username
|
||||
claims[claimPermissionsKey] = user.Filters.WebClient
|
||||
claims[jwt.JwtIDKey] = tokenID
|
||||
claims[jwt.SubjectKey] = user.GetSignature()
|
||||
claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
|
||||
claims[jwt.AudienceKey] = []string{tokenAudienceWebClient}
|
||||
|
@ -1724,6 +1841,7 @@ func TestCookieExpiration(t *testing.T) {
|
|||
claims = make(map[string]any)
|
||||
claims[claimUsernameKey] = user.Username
|
||||
claims[claimPermissionsKey] = user.Filters.WebClient
|
||||
claims[jwt.JwtIDKey] = tokenID
|
||||
claims[jwt.SubjectKey] = user.GetSignature()
|
||||
claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
|
||||
claims[jwt.AudienceKey] = []string{tokenAudienceWebClient}
|
||||
|
@ -1743,6 +1861,35 @@ func TestCookieExpiration(t *testing.T) {
|
|||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.NotEmpty(t, cookie)
|
||||
req.Header.Set("Cookie", cookie)
|
||||
token, err = jwtauth.VerifyRequest(server.tokenAuth, req, jwtauth.TokenFromCookie)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, tokenID, token.JwtID())
|
||||
}
|
||||
|
||||
// test a disabled user
|
||||
user.Status = 0
|
||||
err = dataprovider.UpdateUser(&user, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
user, err = dataprovider.UserExists(user.Username, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
claims = make(map[string]any)
|
||||
claims[claimUsernameKey] = user.Username
|
||||
claims[claimPermissionsKey] = user.Filters.WebClient
|
||||
claims[jwt.JwtIDKey] = tokenID
|
||||
claims[jwt.SubjectKey] = user.GetSignature()
|
||||
claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
|
||||
claims[jwt.AudienceKey] = []string{tokenAudienceWebClient}
|
||||
token, _, err = server.tokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)
|
||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.Empty(t, cookie)
|
||||
|
||||
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
@ -1814,7 +1961,7 @@ func TestRenderInvalidTemplate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestQuotaScanInvalidFs(t *testing.T) {
|
||||
user := dataprovider.User{
|
||||
user := &dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: "test",
|
||||
HomeDir: os.TempDir(),
|
||||
|
@ -2107,34 +2254,95 @@ func TestProxyHeaders(t *testing.T) {
|
|||
testServer.Config.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = testIP
|
||||
rr = httptest.NewRecorder()
|
||||
testServer.Config.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
cookie := rr.Header().Get("Set-Cookie")
|
||||
assert.NotEmpty(t, cookie)
|
||||
req.Header.Set("Cookie", cookie)
|
||||
parsedToken, err := jwtauth.VerifyRequest(server.csrfTokenAuth, req, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx := req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
form := make(url.Values)
|
||||
form.Set("username", username)
|
||||
form.Set("password", password)
|
||||
form.Set(csrfFormToken, createCSRFToken(testIP))
|
||||
form.Set(csrfFormToken, createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, "", webBaseAdminPath))
|
||||
req, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = testIP
|
||||
req.Header.Set("Cookie", cookie)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = httptest.NewRecorder()
|
||||
testServer.Config.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
|
||||
|
||||
form.Set(csrfFormToken, createCSRFToken(validForwardedFor))
|
||||
req, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = validForwardedFor
|
||||
rr = httptest.NewRecorder()
|
||||
testServer.Config.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
loginCookie := rr.Header().Get("Set-Cookie")
|
||||
assert.NotEmpty(t, loginCookie)
|
||||
req.Header.Set("Cookie", loginCookie)
|
||||
parsedToken, err = jwtauth.VerifyRequest(server.csrfTokenAuth, req, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx = req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
form.Set(csrfFormToken, createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, "", webBaseAdminPath))
|
||||
req, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = testIP
|
||||
req.Header.Set("Cookie", loginCookie)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("X-Forwarded-For", validForwardedFor)
|
||||
rr = httptest.NewRecorder()
|
||||
testServer.Config.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusFound, rr.Code, rr.Body.String())
|
||||
cookie := rr.Header().Get("Set-Cookie")
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.NotContains(t, cookie, "Secure")
|
||||
|
||||
// The login cookie is invalidated after a successful login, the same request will fail
|
||||
req, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = testIP
|
||||
req.Header.Set("Cookie", loginCookie)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("X-Forwarded-For", validForwardedFor)
|
||||
rr = httptest.NewRecorder()
|
||||
testServer.Config.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = validForwardedFor
|
||||
rr = httptest.NewRecorder()
|
||||
testServer.Config.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
loginCookie = rr.Header().Get("Set-Cookie")
|
||||
assert.NotEmpty(t, loginCookie)
|
||||
req.Header.Set("Cookie", loginCookie)
|
||||
parsedToken, err = jwtauth.VerifyRequest(server.csrfTokenAuth, req, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx = req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
form.Set(csrfFormToken, createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, "", webBaseAdminPath))
|
||||
req, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = testIP
|
||||
req.Header.Set("Cookie", loginCookie)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("X-Forwarded-For", validForwardedFor)
|
||||
req.Header.Set(xForwardedProto, "https")
|
||||
|
@ -2144,9 +2352,26 @@ func TestProxyHeaders(t *testing.T) {
|
|||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.Contains(t, cookie, "Secure")
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = validForwardedFor
|
||||
rr = httptest.NewRecorder()
|
||||
testServer.Config.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
loginCookie = rr.Header().Get("Set-Cookie")
|
||||
assert.NotEmpty(t, loginCookie)
|
||||
req.Header.Set("Cookie", loginCookie)
|
||||
parsedToken, err = jwtauth.VerifyRequest(server.csrfTokenAuth, req, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx = req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
form.Set(csrfFormToken, createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, "", webBaseAdminPath))
|
||||
req, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = testIP
|
||||
req.Header.Set("Cookie", loginCookie)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("X-Forwarded-For", validForwardedFor)
|
||||
req.Header.Set(xForwardedProto, "http")
|
||||
|
@ -2554,7 +2779,6 @@ func TestHTTPDFile(t *testing.T) {
|
|||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||
connection := &Connection{
|
||||
BaseConnection: common.NewBaseConnection(xid.New().String(), common.ProtocolHTTP, "", "", user),
|
||||
request: nil,
|
||||
}
|
||||
|
||||
fs, err := user.GetFilesystem("")
|
||||
|
@ -2718,10 +2942,22 @@ func TestInvalidClaims(t *testing.T) {
|
|||
}
|
||||
token, err := c.createTokenResponse(server.tokenAuth, tokenAudienceWebClient, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, webClientProfilePath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
parsedToken, err := jwtauth.VerifyRequest(server.tokenAuth, req, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx := req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, createCSRFToken(""))
|
||||
form.Set(csrfFormToken, createCSRFToken(rr, req, server.csrfTokenAuth, "", webBaseClientPath))
|
||||
form.Set("public_keys", "")
|
||||
req, _ := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req, err = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
server.handleWebClientProfilePost(rr, req)
|
||||
|
@ -2738,14 +2974,27 @@ func TestInvalidClaims(t *testing.T) {
|
|||
}
|
||||
token, err = c.createTokenResponse(server.tokenAuth, tokenAudienceWebAdmin, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webAdminProfilePath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
parsedToken, err = jwtauth.VerifyRequest(server.tokenAuth, req, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx = req.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
form = make(url.Values)
|
||||
form.Set(csrfFormToken, createCSRFToken(""))
|
||||
form.Set(csrfFormToken, createCSRFToken(rr, req, server.csrfTokenAuth, "", webBaseAdminPath))
|
||||
form.Set("allow_api_key_auth", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req, err = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
server.handleWebAdminProfilePost(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
|
||||
}
|
||||
|
||||
func TestTLSReq(t *testing.T) {
|
||||
|
@ -3044,24 +3293,31 @@ func TestWebAdminSetupWithInstallCode(t *testing.T) {
|
|||
}
|
||||
server.initializeRouter()
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
r, err := http.NewRequest(http.MethodGet, webAdminSetupPath, nil)
|
||||
assert.NoError(t, err)
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
for _, webURL := range []string{"/", webBasePath, webBaseAdminPath, webAdminLoginPath, webClientLoginPath} {
|
||||
rr = httptest.NewRecorder()
|
||||
r, err = http.NewRequest(http.MethodGet, webURL, nil)
|
||||
rr := httptest.NewRecorder()
|
||||
r, err := http.NewRequest(http.MethodGet, webURL, nil)
|
||||
assert.NoError(t, err)
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
assert.Equal(t, webAdminSetupPath, rr.Header().Get("Location"))
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
r, err := http.NewRequest(http.MethodGet, webAdminSetupPath, nil)
|
||||
assert.NoError(t, err)
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
cookie := rr.Header().Get("Set-Cookie")
|
||||
r.Header.Set("Cookie", cookie)
|
||||
parsedToken, err := jwtauth.VerifyRequest(server.csrfTokenAuth, r, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx := r.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
form := make(url.Values)
|
||||
csrfToken := createCSRFToken("")
|
||||
form.Set("_form_token", csrfToken)
|
||||
csrfToken := createCSRFToken(rr, r, server.csrfTokenAuth, "", webBaseAdminPath)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("install_code", installationCode+"5")
|
||||
form.Set("username", defaultAdminUsername)
|
||||
form.Set("password", "password")
|
||||
|
@ -3069,6 +3325,8 @@ func TestWebAdminSetupWithInstallCode(t *testing.T) {
|
|||
rr = httptest.NewRecorder()
|
||||
r, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
r = r.WithContext(ctx)
|
||||
r.Header.Set("Cookie", cookie)
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
@ -3080,6 +3338,8 @@ func TestWebAdminSetupWithInstallCode(t *testing.T) {
|
|||
rr = httptest.NewRecorder()
|
||||
r, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
r = r.WithContext(ctx)
|
||||
r.Header.Set("Cookie", cookie)
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
|
@ -3101,12 +3361,6 @@ func TestWebAdminSetupWithInstallCode(t *testing.T) {
|
|||
return "5678"
|
||||
})
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
r, err = http.NewRequest(http.MethodGet, webAdminSetupPath, nil)
|
||||
assert.NoError(t, err)
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
for _, webURL := range []string{"/", webBasePath, webBaseAdminPath, webAdminLoginPath, webClientLoginPath} {
|
||||
rr = httptest.NewRecorder()
|
||||
r, err = http.NewRequest(http.MethodGet, webURL, nil)
|
||||
|
@ -3116,9 +3370,22 @@ func TestWebAdminSetupWithInstallCode(t *testing.T) {
|
|||
assert.Equal(t, webAdminSetupPath, rr.Header().Get("Location"))
|
||||
}
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
r, err = http.NewRequest(http.MethodGet, webAdminSetupPath, nil)
|
||||
assert.NoError(t, err)
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
r.Header.Set("Cookie", cookie)
|
||||
parsedToken, err = jwtauth.VerifyRequest(server.csrfTokenAuth, r, jwtauth.TokenFromCookie)
|
||||
assert.NoError(t, err)
|
||||
ctx = r.Context()
|
||||
ctx = jwtauth.NewContext(ctx, parsedToken, err)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
form = make(url.Values)
|
||||
csrfToken = createCSRFToken("")
|
||||
form.Set("_form_token", csrfToken)
|
||||
csrfToken = createCSRFToken(rr, r, server.csrfTokenAuth, "", webBaseAdminPath)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("install_code", installationCode)
|
||||
form.Set("username", defaultAdminUsername)
|
||||
form.Set("password", "password")
|
||||
|
@ -3126,6 +3393,8 @@ func TestWebAdminSetupWithInstallCode(t *testing.T) {
|
|||
rr = httptest.NewRecorder()
|
||||
r, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
r = r.WithContext(ctx)
|
||||
r.Header.Set("Cookie", cookie)
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
@ -3137,6 +3406,8 @@ func TestWebAdminSetupWithInstallCode(t *testing.T) {
|
|||
rr = httptest.NewRecorder()
|
||||
r, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
r = r.WithContext(ctx)
|
||||
r.Header.Set("Cookie", cookie)
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
|
@ -3202,6 +3473,7 @@ func TestDecodeToken(t *testing.T) {
|
|||
claimNodeID: nodeID,
|
||||
claimMustChangePasswordKey: false,
|
||||
claimMustSetSecondFactorKey: true,
|
||||
claimRef: "ref",
|
||||
}
|
||||
c := jwtTokenClaims{}
|
||||
c.Decode(token)
|
||||
|
@ -3209,6 +3481,11 @@ func TestDecodeToken(t *testing.T) {
|
|||
assert.Equal(t, nodeID, c.NodeID)
|
||||
assert.False(t, c.MustChangePassword)
|
||||
assert.True(t, c.MustSetTwoFactorAuth)
|
||||
assert.Equal(t, "ref", c.Ref)
|
||||
|
||||
asMap := c.asMap()
|
||||
asMap[claimMustChangePasswordKey] = false
|
||||
assert.Equal(t, token, asMap)
|
||||
|
||||
token[claimMustChangePasswordKey] = 10
|
||||
c = jwtTokenClaims{}
|
||||
|
|
|
@ -20,10 +20,10 @@ import (
|
|||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
|
||||
|
@ -75,12 +75,6 @@ func validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudi
|
|||
return errInvalidToken
|
||||
}
|
||||
|
||||
err = jwt.Validate(token)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "error validating jwt token: %v", err)
|
||||
doRedirect(http.StatusText(http.StatusUnauthorized), err)
|
||||
return errInvalidToken
|
||||
}
|
||||
if isTokenInvalidated(r) {
|
||||
logger.Debug(logSender, "", "the token has been invalidated")
|
||||
doRedirect("Your token is no longer valid", nil)
|
||||
|
@ -90,18 +84,16 @@ func validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudi
|
|||
if err := checkPartialAuth(w, r, audience, token.Audience()); err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(token.Audience(), audience) {
|
||||
if !slices.Contains(token.Audience(), audience) {
|
||||
logger.Debug(logSender, "", "the token is not valid for audience %q", audience)
|
||||
doRedirect("Your token audience is not valid", nil)
|
||||
return errInvalidToken
|
||||
}
|
||||
if tokenValidationMode != tokenValidationNoIPMatch {
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if !util.Contains(token.Audience(), ipAddr) {
|
||||
if err := validateIPForToken(token, ipAddr); err != nil {
|
||||
logger.Debug(logSender, "", "the token with id %q is not valid for the ip address %q", token.JwtID(), ipAddr)
|
||||
doRedirect("Your token is not valid", nil)
|
||||
return errInvalidToken
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -114,7 +106,7 @@ func (s *httpdServer) validateJWTPartialToken(w http.ResponseWriter, r *http.Req
|
|||
} else {
|
||||
notFoundFunc = s.renderClientNotFoundPage
|
||||
}
|
||||
if err != nil || token == nil || jwt.Validate(token) != nil {
|
||||
if err != nil || token == nil {
|
||||
notFoundFunc(w, r, nil)
|
||||
return errInvalidToken
|
||||
}
|
||||
|
@ -122,11 +114,17 @@ func (s *httpdServer) validateJWTPartialToken(w http.ResponseWriter, r *http.Req
|
|||
notFoundFunc(w, r, nil)
|
||||
return errInvalidToken
|
||||
}
|
||||
if !util.Contains(token.Audience(), audience) {
|
||||
logger.Debug(logSender, "", "the token is not valid for audience %q", audience)
|
||||
if !slices.Contains(token.Audience(), audience) {
|
||||
logger.Debug(logSender, "", "the partial token with id %q is not valid for audience %q", token.JwtID(), audience)
|
||||
notFoundFunc(w, r, nil)
|
||||
return errInvalidToken
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := validateIPForToken(token, ipAddr); err != nil {
|
||||
logger.Debug(logSender, "", "the partial token with id %q is not valid for the ip address %q", token.JwtID(), ipAddr)
|
||||
notFoundFunc(w, r, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -324,29 +322,27 @@ func (s *httpdServer) checkPerm(perm string) func(next http.Handler) http.Handle
|
|||
}
|
||||
}
|
||||
|
||||
func verifyCSRFHeader(next http.Handler) http.Handler {
|
||||
func (s *httpdServer) verifyCSRFHeader(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenString := r.Header.Get(csrfHeaderToken)
|
||||
token, err := jwtauth.VerifyToken(csrfTokenAuth, tokenString)
|
||||
token, err := jwtauth.VerifyToken(s.csrfTokenAuth, tokenString)
|
||||
if err != nil || token == nil {
|
||||
logger.Debug(logSender, "", "error validating CSRF header: %v", err)
|
||||
sendAPIResponse(w, r, err, "Invalid token", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !util.Contains(token.Audience(), tokenAudienceCSRF) {
|
||||
if !slices.Contains(token.Audience(), tokenAudienceCSRF) {
|
||||
logger.Debug(logSender, "", "error validating CSRF header token audience")
|
||||
sendAPIResponse(w, r, errors.New("the token is not valid"), "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenValidationMode != tokenValidationNoIPMatch {
|
||||
if !util.Contains(token.Audience(), util.GetIPFromRemoteAddress(r.RemoteAddr)) {
|
||||
if err := validateIPForToken(token, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||
logger.Debug(logSender, "", "error validating CSRF header IP audience")
|
||||
sendAPIResponse(w, r, errors.New("the token is not valid"), "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
|
@ -440,6 +436,7 @@ func checkAPIKeyAuth(tokenAuth *jwtauth.JWTAuth, scope dataprovider.APIKeyScope)
|
|||
"", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
common.DelayLogin(nil)
|
||||
} else {
|
||||
if k.User != "" {
|
||||
apiUser = k.User
|
||||
|
@ -512,6 +509,7 @@ func authenticateAdminWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTA
|
|||
}
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", resp["access_token"]))
|
||||
dataprovider.UpdateAdminLastLogin(&admin)
|
||||
common.DelayLogin(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -574,11 +572,11 @@ func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAu
|
|||
}
|
||||
|
||||
func checkPartialAuth(w http.ResponseWriter, r *http.Request, audience string, tokenAudience []string) error {
|
||||
if audience == tokenAudienceWebAdmin && util.Contains(tokenAudience, tokenAudienceWebAdminPartial) {
|
||||
if audience == tokenAudienceWebAdmin && slices.Contains(tokenAudience, tokenAudienceWebAdminPartial) {
|
||||
http.Redirect(w, r, webAdminTwoFactorPath, http.StatusFound)
|
||||
return errInvalidToken
|
||||
}
|
||||
if audience == tokenAudienceWebClient && util.Contains(tokenAudience, tokenAudienceWebClientPartial) {
|
||||
if audience == tokenAudienceWebClient && slices.Contains(tokenAudience, tokenAudienceWebClientPartial) {
|
||||
http.Redirect(w, r, webClientTwoFactorPath, http.StatusFound)
|
||||
return errInvalidToken
|
||||
}
|
||||
|
|
|
@ -16,10 +16,12 @@ package httpd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -142,7 +144,7 @@ func (o *OIDC) initialize() error {
|
|||
if o.RedirectBaseURL == "" {
|
||||
return errors.New("oidc: redirect base URL cannot be empty")
|
||||
}
|
||||
if !util.Contains(o.Scopes, oidc.ScopeOpenID) {
|
||||
if !slices.Contains(o.Scopes, oidc.ScopeOpenID) {
|
||||
return fmt.Errorf("oidc: required scope %q is not set", oidc.ScopeOpenID)
|
||||
}
|
||||
if o.ClientSecretFile != "" {
|
||||
|
@ -203,7 +205,7 @@ type oidcPendingAuth struct {
|
|||
func newOIDCPendingAuth(audience tokenAudience) oidcPendingAuth {
|
||||
return oidcPendingAuth{
|
||||
State: xid.New().String(),
|
||||
Nonce: xid.New().String(),
|
||||
Nonce: hex.EncodeToString(util.GenerateRandomBytes(20)),
|
||||
Audience: audience,
|
||||
IssuedAt: util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
}
|
||||
|
@ -345,14 +347,15 @@ func (t *oidcToken) refresh(ctx context.Context, config OAuth2Config, verifier O
|
|||
logger.Debug(logSender, "", "unable to verify refreshed id token for cookie %q: %v", t.Cookie, err)
|
||||
return err
|
||||
}
|
||||
if idToken.Nonce != t.Nonce {
|
||||
logger.Debug(logSender, "", "unable to verify refreshed id token for cookie %q: nonce mismatch", t.Cookie)
|
||||
if idToken.Nonce != "" && idToken.Nonce != t.Nonce {
|
||||
logger.Warn(logSender, "", "unable to verify refreshed id token for cookie %q: nonce mismatch, expected: %q, actual: %q",
|
||||
t.Cookie, t.Nonce, idToken.Nonce)
|
||||
return errors.New("the refreshed token nonce mismatch")
|
||||
}
|
||||
claims := make(map[string]any)
|
||||
err = idToken.Claims(&claims)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "unable to get refreshed id token claims for cookie %q: %v", t.Cookie, err)
|
||||
logger.Warn(logSender, "", "unable to get refreshed id token claims for cookie %q: %v", t.Cookie, err)
|
||||
return err
|
||||
}
|
||||
sid, ok := claims["sid"].(string)
|
||||
|
@ -428,6 +431,7 @@ func (t *oidcToken) getUser(r *http.Request) error {
|
|||
t.TokenRole = admin.Role
|
||||
t.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
|
||||
dataprovider.UpdateAdminLastLogin(admin)
|
||||
common.DelayLogin(nil)
|
||||
return nil
|
||||
}
|
||||
params.Event = common.IDPLoginUser
|
||||
|
@ -540,6 +544,7 @@ func (s *httpdServer) oidcTokenAuthenticator(audience tokenAudience) func(next h
|
|||
return
|
||||
}
|
||||
jwtTokenClaims := jwtTokenClaims{
|
||||
JwtID: token.Cookie,
|
||||
Username: token.Username,
|
||||
Permissions: token.Permissions,
|
||||
Role: token.TokenRole,
|
||||
|
@ -593,6 +598,7 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
|
|||
authReq, err := oidcMgr.getPendingAuth(state)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "oidc authentication state did not match")
|
||||
oidcMgr.removePendingAuth(state)
|
||||
s.renderClientMessagePage(w, r, util.I18nInvalidAuthReqTitle, http.StatusBadRequest,
|
||||
util.NewI18nError(err, util.I18nInvalidAuth), "")
|
||||
return
|
||||
|
|
|
@ -33,7 +33,6 @@ import (
|
|||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -627,7 +626,9 @@ func TestOIDCRefreshToken(t *testing.T) {
|
|||
},
|
||||
}
|
||||
verifier = mockOIDCVerifier{
|
||||
token: &oidc.IDToken{},
|
||||
token: &oidc.IDToken{
|
||||
Nonce: xid.New().String(), // nonce is different from the expected one
|
||||
},
|
||||
}
|
||||
err = token.refresh(context.Background(), &config, &verifier, r)
|
||||
if assert.Error(t, err) {
|
||||
|
@ -635,7 +636,7 @@ func TestOIDCRefreshToken(t *testing.T) {
|
|||
}
|
||||
verifier = mockOIDCVerifier{
|
||||
token: &oidc.IDToken{
|
||||
Nonce: token.Nonce,
|
||||
Nonce: "", // empty token is fine on refresh but claims are not set
|
||||
},
|
||||
}
|
||||
err = token.refresh(context.Background(), &config, &verifier, r)
|
||||
|
@ -1584,12 +1585,9 @@ func TestOIDCWithLoginFormsDisabled(t *testing.T) {
|
|||
tokenCookie = k
|
||||
}
|
||||
// we should be able to create admins without setting a password
|
||||
if csrfTokenAuth == nil {
|
||||
csrfTokenAuth = jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil)
|
||||
}
|
||||
adminUsername := "testAdmin"
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, createCSRFToken(""))
|
||||
form.Set(csrfFormToken, createCSRFToken(rr, r, server.csrfTokenAuth, tokenCookie, webBaseAdminPath))
|
||||
form.Set("username", adminUsername)
|
||||
form.Set("password", "")
|
||||
form.Set("status", "1")
|
||||
|
|
|
@ -24,7 +24,9 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -68,6 +70,7 @@ type httpdServer struct {
|
|||
isShared int
|
||||
router *chi.Mux
|
||||
tokenAuth *jwtauth.JWTAuth
|
||||
csrfTokenAuth *jwtauth.JWTAuth
|
||||
signingPassphrase string
|
||||
cors CorsConfig
|
||||
}
|
||||
|
@ -120,7 +123,7 @@ func (s *httpdServer) listenAndServe() error {
|
|||
httpServer.TLSConfig = config
|
||||
logger.Debug(logSender, "", "configured TLS cipher suites for binding %q: %v, certID: %v",
|
||||
s.binding.GetAddress(), httpServer.TLSConfig.CipherSuites, certID)
|
||||
if s.binding.ClientAuthType == 1 {
|
||||
if s.binding.isMutualTLSEnabled() {
|
||||
httpServer.TLSConfig.ClientCAs = certMgr.GetRootCAs()
|
||||
httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
httpServer.TLSConfig.VerifyConnection = s.verifyTLSConnection
|
||||
|
@ -164,14 +167,14 @@ func (s *httpdServer) refreshCookie(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := loginPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
Title: util.I18nLoginTitle,
|
||||
CurrentURL: webClientLoginPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseClientPath),
|
||||
Branding: s.binding.webClientBranding(),
|
||||
FormDisabled: s.binding.isWebClientLoginFormDisabled(),
|
||||
CheckRedirect: true,
|
||||
}
|
||||
|
@ -180,7 +183,7 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
if s.binding.showAdminLoginURL() {
|
||||
data.AltLoginURL = webAdminLoginPath
|
||||
data.AltLoginName = s.binding.Branding.WebAdmin.ShortName
|
||||
data.AltLoginName = s.binding.webAdminBranding().ShortName
|
||||
}
|
||||
if smtp.IsEnabled() && !data.FormDisabled {
|
||||
data.ForgotPwdURL = webClientForgotPwdPath
|
||||
|
@ -193,8 +196,7 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Reque
|
|||
|
||||
func (s *httpdServer) handleWebClientLogout(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
c := jwtTokenClaims{}
|
||||
c.removeCookie(w, r, webBaseClientPath)
|
||||
removeCookie(w, r, webBaseClientPath)
|
||||
s.logoutOIDCUser(w, r)
|
||||
|
||||
http.Redirect(w, r, webClientLoginPath, http.StatusFound)
|
||||
|
@ -206,7 +208,7 @@ func (s *httpdServer) handleWebClientChangePwdPost(w http.ResponseWriter, r *htt
|
|||
s.renderClientChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -226,7 +228,7 @@ func (s *httpdServer) handleClientWebLogin(w http.ResponseWriter, r *http.Reques
|
|||
return
|
||||
}
|
||||
msg := getFlashMessage(w, r)
|
||||
s.renderClientLoginPage(w, r, msg.getI18nError(), util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderClientLoginPage(w, r, msg.getI18nError())
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -234,7 +236,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
|
|||
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
protocol := common.ProtocolHTTP
|
||||
|
@ -244,20 +246,19 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
|
|||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials)
|
||||
s.renderClientLoginPage(w, r,
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
|
||||
return
|
||||
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
}
|
||||
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nError403Message), ipAddr)
|
||||
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nError403Message))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -265,13 +266,13 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
|
|||
if err != nil {
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
s.renderClientLoginPage(w, r,
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, xid.New().String())
|
||||
if err := checkHTTPClientUser(&user, r, connectionID, true); err != nil {
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nError403Message), ipAddr)
|
||||
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nError403Message))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -280,7 +281,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
|
|||
if err != nil {
|
||||
logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
|
||||
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorFsGeneric), ipAddr)
|
||||
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorFsGeneric))
|
||||
return
|
||||
}
|
||||
s.loginUser(w, r, &user, connectionID, ipAddr, false, s.renderClientLoginPage)
|
||||
|
@ -292,10 +293,10 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
|
|||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -304,12 +305,12 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
|
|||
_, user, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")),
|
||||
newPassword, confirmPassword, false)
|
||||
if err != nil {
|
||||
s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric), ipAddr)
|
||||
s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric))
|
||||
return
|
||||
}
|
||||
connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
|
||||
if err := checkHTTPClientUser(user, r, connectionID, true); err != nil {
|
||||
s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorDirList403), ipAddr)
|
||||
s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorLoginAfterReset))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -317,7 +318,7 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
|
|||
err = user.CheckFsRoot(connectionID)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
|
||||
s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorLoginAfterReset), ipAddr)
|
||||
s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorLoginAfterReset))
|
||||
return
|
||||
}
|
||||
s.loginUser(w, r, user, connectionID, ipAddr, false, s.renderClientResetPwdPage)
|
||||
|
@ -332,18 +333,18 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
|
|||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.renderClientTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderClientTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
username := claims.Username
|
||||
recoveryCode := strings.TrimSpace(r.Form.Get("recovery_code"))
|
||||
if username == "" || recoveryCode == "" {
|
||||
s.renderClientTwoFactorRecoveryPage(w, r,
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
s.renderClientTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderClientTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
user, userMerged, err := dataprovider.GetUserVariants(username, "")
|
||||
|
@ -352,12 +353,12 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
|
|||
handleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck
|
||||
}
|
||||
s.renderClientTwoFactorRecoveryPage(w, r,
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
if !userMerged.Filters.TOTPConfig.Enabled || !util.Contains(userMerged.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
|
||||
if !userMerged.Filters.TOTPConfig.Enabled || !slices.Contains(userMerged.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
|
||||
s.renderClientTwoFactorPage(w, r, util.NewI18nError(
|
||||
util.NewValidationError("two factory authentication is not enabled"), util.I18n2FADisabled), ipAddr)
|
||||
util.NewValidationError("two factory authentication is not enabled"), util.I18n2FADisabled))
|
||||
return
|
||||
}
|
||||
for idx, code := range user.Filters.RecoveryCodes {
|
||||
|
@ -368,7 +369,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
|
|||
if code.Secret.GetPayload() == recoveryCode {
|
||||
if code.Used {
|
||||
s.renderClientTwoFactorRecoveryPage(w, r,
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
user.Filters.RecoveryCodes[idx].Used = true
|
||||
|
@ -386,7 +387,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
|
|||
}
|
||||
handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck
|
||||
s.renderClientTwoFactorRecoveryPage(w, r,
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -398,7 +399,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
|
|||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
username := claims.Username
|
||||
|
@ -407,25 +408,25 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
|
|||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials)
|
||||
s.renderClientTwoFactorPage(w, r,
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
|
||||
s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserWithGroupSettings(username, "")
|
||||
if err != nil {
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
|
||||
if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
|
||||
s.renderClientTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled), ipAddr)
|
||||
s.renderClientTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled))
|
||||
return
|
||||
}
|
||||
err = user.Filters.TOTPConfig.Secret.Decrypt()
|
||||
|
@ -439,7 +440,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
|
|||
if !match || err != nil {
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, dataprovider.ErrInvalidCredentials)
|
||||
s.renderClientTwoFactorPage(w, r,
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", getProtocolFromRequest(r), xid.New().String())
|
||||
|
@ -456,18 +457,17 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
|
|||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
username := claims.Username
|
||||
recoveryCode := strings.TrimSpace(r.Form.Get("recovery_code"))
|
||||
if username == "" || recoveryCode == "" {
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
|
||||
ipAddr)
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
admin, err := dataprovider.AdminExists(username)
|
||||
|
@ -475,12 +475,11 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
|
|||
if errors.Is(err, util.ErrNotFound) {
|
||||
handleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck
|
||||
}
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
|
||||
ipAddr)
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
if !admin.Filters.TOTPConfig.Enabled {
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(util.NewValidationError("two factory authentication is not enabled"), util.I18n2FADisabled), ipAddr)
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(util.NewValidationError("two factory authentication is not enabled"), util.I18n2FADisabled))
|
||||
return
|
||||
}
|
||||
for idx, code := range admin.Filters.RecoveryCodes {
|
||||
|
@ -491,7 +490,7 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
|
|||
if code.Secret.GetPayload() == recoveryCode {
|
||||
if code.Used {
|
||||
s.renderTwoFactorRecoveryPage(w, r,
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
admin.Filters.RecoveryCodes[idx].Used = true
|
||||
|
@ -506,8 +505,7 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
|
|||
}
|
||||
}
|
||||
handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
|
||||
ipAddr)
|
||||
s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -519,19 +517,18 @@ func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http
|
|||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
username := claims.Username
|
||||
passcode := strings.TrimSpace(r.Form.Get("passcode"))
|
||||
if username == "" || passcode == "" {
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
|
||||
ipAddr)
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
err = handleDefenderEventLoginFailed(ipAddr, err)
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
admin, err := dataprovider.AdminExists(username)
|
||||
|
@ -539,11 +536,11 @@ func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http
|
|||
if errors.Is(err, util.ErrNotFound) {
|
||||
handleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck
|
||||
}
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
if !admin.Filters.TOTPConfig.Enabled {
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled), ipAddr)
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled))
|
||||
return
|
||||
}
|
||||
err = admin.Filters.TOTPConfig.Secret.Decrypt()
|
||||
|
@ -555,8 +552,7 @@ func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http
|
|||
admin.Filters.TOTPConfig.Secret.GetPayload())
|
||||
if !match || err != nil {
|
||||
handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
|
||||
ipAddr)
|
||||
s.renderTwoFactorPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
s.loginAdmin(w, r, &admin, true, s.renderTwoFactorPage, ipAddr)
|
||||
|
@ -567,44 +563,42 @@ func (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Req
|
|||
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.renderAdminLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderAdminLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
username := strings.TrimSpace(r.Form.Get("username"))
|
||||
password := strings.TrimSpace(r.Form.Get("password"))
|
||||
if username == "" || password == "" {
|
||||
s.renderAdminLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
|
||||
ipAddr)
|
||||
s.renderAdminLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
s.renderAdminLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
|
||||
if err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderAdminLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
admin, err := dataprovider.CheckAdminAndPass(username, password, ipAddr)
|
||||
if err != nil {
|
||||
handleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck
|
||||
s.renderAdminLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
|
||||
ipAddr)
|
||||
s.renderAdminLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
s.loginAdmin(w, r, &admin, false, s.renderAdminLoginPage, ipAddr)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := loginPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
Title: util.I18nLoginTitle,
|
||||
CurrentURL: webAdminLoginPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseAdminPath),
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
FormDisabled: s.binding.isWebAdminLoginFormDisabled(),
|
||||
CheckRedirect: false,
|
||||
}
|
||||
if s.binding.showClientLoginURL() {
|
||||
data.AltLoginURL = webClientLoginPath
|
||||
data.AltLoginName = s.binding.Branding.WebClient.ShortName
|
||||
data.AltLoginName = s.binding.webClientBranding().ShortName
|
||||
}
|
||||
if smtp.IsEnabled() && !data.FormDisabled {
|
||||
data.ForgotPwdURL = webAdminForgotPwdPath
|
||||
|
@ -622,13 +616,12 @@ func (s *httpdServer) handleWebAdminLogin(w http.ResponseWriter, r *http.Request
|
|||
return
|
||||
}
|
||||
msg := getFlashMessage(w, r)
|
||||
s.renderAdminLoginPage(w, r, msg.getI18nError(), util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderAdminLoginPage(w, r, msg.getI18nError())
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAdminLogout(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
c := jwtTokenClaims{}
|
||||
c.removeCookie(w, r, webBaseAdminPath)
|
||||
removeCookie(w, r, webBaseAdminPath)
|
||||
s.logoutOIDCUser(w, r)
|
||||
|
||||
http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
|
||||
|
@ -641,7 +634,7 @@ func (s *httpdServer) handleWebAdminChangePwdPost(w http.ResponseWriter, r *http
|
|||
s.renderChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -660,10 +653,10 @@ func (s *httpdServer) handleWebAdminPasswordResetPost(w http.ResponseWriter, r *
|
|||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
s.renderResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -672,7 +665,7 @@ func (s *httpdServer) handleWebAdminPasswordResetPost(w http.ResponseWriter, r *
|
|||
admin, _, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")),
|
||||
newPassword, confirmPassword, true)
|
||||
if err != nil {
|
||||
s.renderResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric), ipAddr)
|
||||
s.renderResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -688,10 +681,10 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
|
|||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
s.renderAdminSetupPage(w, r, "", ipAddr, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
s.renderAdminSetupPage(w, r, "", util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -700,7 +693,7 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
|
|||
confirmPassword := strings.TrimSpace(r.Form.Get("confirm_password"))
|
||||
installCode := strings.TrimSpace(r.Form.Get("install_code"))
|
||||
if installationCode != "" && installCode != resolveInstallationCode() {
|
||||
s.renderAdminSetupPage(w, r, username, ipAddr,
|
||||
s.renderAdminSetupPage(w, r, username,
|
||||
util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("%v mismatch", installationCodeHint)),
|
||||
util.I18nErrorSetupInstallCode),
|
||||
|
@ -708,17 +701,17 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
|
|||
return
|
||||
}
|
||||
if username == "" {
|
||||
s.renderAdminSetupPage(w, r, username, ipAddr,
|
||||
s.renderAdminSetupPage(w, r, username,
|
||||
util.NewI18nError(util.NewValidationError("please set a username"), util.I18nError500Message))
|
||||
return
|
||||
}
|
||||
if password == "" {
|
||||
s.renderAdminSetupPage(w, r, username, ipAddr,
|
||||
s.renderAdminSetupPage(w, r, username,
|
||||
util.NewI18nError(util.NewValidationError("please set a password"), util.I18nError500Message))
|
||||
return
|
||||
}
|
||||
if password != confirmPassword {
|
||||
s.renderAdminSetupPage(w, r, username, ipAddr,
|
||||
s.renderAdminSetupPage(w, r, username,
|
||||
util.NewI18nError(errors.New("the two password fields do not match"), util.I18nErrorChangePwdNoMatch))
|
||||
return
|
||||
}
|
||||
|
@ -730,7 +723,7 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
|
|||
}
|
||||
err = dataprovider.AddAdmin(&admin, username, ipAddr, "")
|
||||
if err != nil {
|
||||
s.renderAdminSetupPage(w, r, username, ipAddr, util.NewI18nError(err, util.I18nError500Message))
|
||||
s.renderAdminSetupPage(w, r, username, util.NewI18nError(err, util.I18nError500Message))
|
||||
return
|
||||
}
|
||||
s.loginAdmin(w, r, &admin, false, nil, ipAddr)
|
||||
|
@ -738,7 +731,7 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
|
|||
|
||||
func (s *httpdServer) loginUser(
|
||||
w http.ResponseWriter, r *http.Request, user *dataprovider.User, connectionID, ipAddr string,
|
||||
isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string),
|
||||
isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, r *http.Request, err *util.I18nError),
|
||||
) {
|
||||
c := jwtTokenClaims{
|
||||
Username: user.Username,
|
||||
|
@ -751,7 +744,7 @@ func (s *httpdServer) loginUser(
|
|||
}
|
||||
|
||||
audience := tokenAudienceWebClient
|
||||
if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) &&
|
||||
if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) &&
|
||||
user.CanManageMFA() && !isSecondFactorAuth {
|
||||
audience = tokenAudienceWebClientPartial
|
||||
}
|
||||
|
@ -760,12 +753,10 @@ func (s *httpdServer) loginUser(
|
|||
if err != nil {
|
||||
logger.Warn(logSender, connectionID, "unable to set user login cookie %v", err)
|
||||
updateLoginMetrics(user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
|
||||
errorFunc(w, r, util.NewI18nError(err, util.I18nError500Message), ipAddr)
|
||||
errorFunc(w, r, util.NewI18nError(err, util.I18nError500Message))
|
||||
return
|
||||
}
|
||||
if isSecondFactorAuth {
|
||||
invalidateToken(r)
|
||||
}
|
||||
invalidateToken(r, !isSecondFactorAuth)
|
||||
if audience == tokenAudienceWebClientPartial {
|
||||
redirectPath := webClientTwoFactorPath
|
||||
if next := r.URL.Query().Get("next"); strings.HasPrefix(next, webClientFilesPath) {
|
||||
|
@ -785,7 +776,7 @@ func (s *httpdServer) loginUser(
|
|||
|
||||
func (s *httpdServer) loginAdmin(
|
||||
w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
|
||||
isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string),
|
||||
isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, r *http.Request, err *util.I18nError),
|
||||
ipAddr string,
|
||||
) {
|
||||
c := jwtTokenClaims{
|
||||
|
@ -807,20 +798,19 @@ func (s *httpdServer) loginAdmin(
|
|||
if err != nil {
|
||||
logger.Warn(logSender, "", "unable to set admin login cookie %v", err)
|
||||
if errorFunc == nil {
|
||||
s.renderAdminSetupPage(w, r, admin.Username, ipAddr, util.NewI18nError(err, util.I18nError500Message))
|
||||
s.renderAdminSetupPage(w, r, admin.Username, util.NewI18nError(err, util.I18nError500Message))
|
||||
return
|
||||
}
|
||||
errorFunc(w, r, util.NewI18nError(err, util.I18nError500Message), ipAddr)
|
||||
errorFunc(w, r, util.NewI18nError(err, util.I18nError500Message))
|
||||
return
|
||||
}
|
||||
if isSecondFactorAuth {
|
||||
invalidateToken(r)
|
||||
}
|
||||
invalidateToken(r, !isSecondFactorAuth)
|
||||
if audience == tokenAudienceWebAdminPartial {
|
||||
http.Redirect(w, r, webAdminTwoFactorPath, http.StatusFound)
|
||||
return
|
||||
}
|
||||
dataprovider.UpdateAdminLastLogin(admin)
|
||||
common.DelayLogin(nil)
|
||||
redirectURL := webUsersPath
|
||||
if errorFunc == nil {
|
||||
redirectURL = webAdminMFAPath
|
||||
|
@ -830,7 +820,7 @@ func (s *httpdServer) loginAdmin(
|
|||
|
||||
func (s *httpdServer) logout(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
invalidateToken(r)
|
||||
invalidateToken(r, false)
|
||||
sendAPIResponse(w, r, nil, "Your token has been invalidated", http.StatusOK)
|
||||
}
|
||||
|
||||
|
@ -874,7 +864,7 @@ func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
|
||||
if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
|
||||
passcode := r.Header.Get(otpHeaderCode)
|
||||
if passcode == "" {
|
||||
logger.Debug(logSender, "", "TOTP enabled for user %q and not passcode provided, authentication refused", user.Username)
|
||||
|
@ -1000,6 +990,7 @@ func (s *httpdServer) generateAndSendToken(w http.ResponseWriter, r *http.Reques
|
|||
}
|
||||
|
||||
dataprovider.UpdateAdminLastLogin(&admin)
|
||||
common.DelayLogin(nil)
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
|
@ -1019,14 +1010,14 @@ func (s *httpdServer) checkCookieExpiration(w http.ResponseWriter, r *http.Reque
|
|||
if time.Until(token.Expiration()) > tokenRefreshThreshold {
|
||||
return
|
||||
}
|
||||
if util.Contains(token.Audience(), tokenAudienceWebClient) {
|
||||
s.refreshClientToken(w, r, tokenClaims)
|
||||
if slices.Contains(token.Audience(), tokenAudienceWebClient) {
|
||||
s.refreshClientToken(w, r, &tokenClaims)
|
||||
} else {
|
||||
s.refreshAdminToken(w, r, tokenClaims)
|
||||
s.refreshAdminToken(w, r, &tokenClaims)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *httpdServer) refreshClientToken(w http.ResponseWriter, r *http.Request, tokenClaims jwtTokenClaims) {
|
||||
func (s *httpdServer) refreshClientToken(w http.ResponseWriter, r *http.Request, tokenClaims *jwtTokenClaims) {
|
||||
user, err := dataprovider.GetUserWithGroupSettings(tokenClaims.Username, "")
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -1035,6 +1026,10 @@ func (s *httpdServer) refreshClientToken(w http.ResponseWriter, r *http.Request,
|
|||
logger.Debug(logSender, "", "signature mismatch for user %q, unable to refresh cookie", user.Username)
|
||||
return
|
||||
}
|
||||
if err := user.CheckLoginConditions(); err != nil {
|
||||
logger.Debug(logSender, "", "unable to refresh cookie for user %q: %v", user.Username, err)
|
||||
return
|
||||
}
|
||||
if err := checkHTTPClientUser(&user, r, xid.New().String(), true); err != nil {
|
||||
logger.Debug(logSender, "", "unable to refresh cookie for user %q: %v", user.Username, err)
|
||||
return
|
||||
|
@ -1046,22 +1041,18 @@ func (s *httpdServer) refreshClientToken(w http.ResponseWriter, r *http.Request,
|
|||
tokenClaims.createAndSetCookie(w, r, s.tokenAuth, tokenAudienceWebClient, util.GetIPFromRemoteAddress(r.RemoteAddr)) //nolint:errcheck
|
||||
}
|
||||
|
||||
func (s *httpdServer) refreshAdminToken(w http.ResponseWriter, r *http.Request, tokenClaims jwtTokenClaims) {
|
||||
func (s *httpdServer) refreshAdminToken(w http.ResponseWriter, r *http.Request, tokenClaims *jwtTokenClaims) {
|
||||
admin, err := dataprovider.AdminExists(tokenClaims.Username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if admin.Status != 1 {
|
||||
logger.Debug(logSender, "", "admin %q is disabled, unable to refresh cookie", admin.Username)
|
||||
return
|
||||
}
|
||||
if admin.GetSignature() != tokenClaims.Signature {
|
||||
logger.Debug(logSender, "", "signature mismatch for admin %q, unable to refresh cookie", admin.Username)
|
||||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if !admin.CanLoginFromIP(ipAddr) {
|
||||
logger.Debug(logSender, "", "admin %q cannot login from %v, unable to refresh cookie", admin.Username, r.RemoteAddr)
|
||||
if err := admin.CanLogin(ipAddr); err != nil {
|
||||
logger.Debug(logSender, "", "unable to refresh cookie for admin %q, err: %v", admin.Username, err)
|
||||
return
|
||||
}
|
||||
tokenClaims.Permissions = admin.Permissions
|
||||
|
@ -1234,6 +1225,7 @@ func (s *httpdServer) mustCheckPath(r *http.Request) bool {
|
|||
func (s *httpdServer) initializeRouter() {
|
||||
var hasHTTPSRedirect bool
|
||||
s.tokenAuth = jwtauth.New(jwa.HS256.String(), getSigningKey(s.signingPassphrase), nil)
|
||||
s.csrfTokenAuth = jwtauth.New(jwa.HS256.String(), getSigningKey(s.signingPassphrase), nil)
|
||||
s.router = chi.NewRouter()
|
||||
|
||||
s.router.Use(middleware.RequestID)
|
||||
|
@ -1530,16 +1522,30 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
http.Redirect(w, r, webClientLoginPath, http.StatusFound)
|
||||
})
|
||||
s.router.Get(path.Join(webStaticFilesPath, "branding/webclient/logo.png"),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderPNGImage(w, r, dbBrandingConfig.getWebClientLogo())
|
||||
})
|
||||
s.router.Get(path.Join(webStaticFilesPath, "branding/webclient/favicon.png"),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderPNGImage(w, r, dbBrandingConfig.getWebClientFavicon())
|
||||
})
|
||||
s.router.Get(webClientLoginPath, s.handleClientWebLogin)
|
||||
if s.binding.OIDC.isEnabled() && !s.binding.isWebClientOIDCLoginDisabled() {
|
||||
s.router.Get(webClientOIDCLoginPath, s.handleWebClientOIDCLogin)
|
||||
}
|
||||
if !s.binding.isWebClientLoginFormDisabled() {
|
||||
s.router.Post(webClientLoginPath, s.handleWebClientLoginPost)
|
||||
s.router.With(jwtauth.Verify(s.csrfTokenAuth, jwtauth.TokenFromCookie)).
|
||||
Post(webClientLoginPath, s.handleWebClientLoginPost)
|
||||
s.router.Get(webClientForgotPwdPath, s.handleWebClientForgotPwd)
|
||||
s.router.Post(webClientForgotPwdPath, s.handleWebClientForgotPwdPost)
|
||||
s.router.Get(webClientResetPwdPath, s.handleWebClientPasswordReset)
|
||||
s.router.Post(webClientResetPwdPath, s.handleWebClientPasswordResetPost)
|
||||
s.router.With(jwtauth.Verify(s.csrfTokenAuth, jwtauth.TokenFromCookie)).
|
||||
Post(webClientForgotPwdPath, s.handleWebClientForgotPwdPost)
|
||||
s.router.With(jwtauth.Verify(s.csrfTokenAuth, jwtauth.TokenFromCookie)).
|
||||
Get(webClientResetPwdPath, s.handleWebClientPasswordReset)
|
||||
s.router.With(jwtauth.Verify(s.csrfTokenAuth, jwtauth.TokenFromCookie)).
|
||||
Post(webClientResetPwdPath, s.handleWebClientPasswordResetPost)
|
||||
s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
|
||||
s.jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).
|
||||
Get(webClientTwoFactorPath, s.handleWebClientTwoFactor)
|
||||
|
@ -1555,7 +1561,9 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
}
|
||||
// share routes available to external users
|
||||
s.router.Get(webClientPubSharesPath+"/{id}/login", s.handleClientShareLoginGet)
|
||||
s.router.Post(webClientPubSharesPath+"/{id}/login", s.handleClientShareLoginPost)
|
||||
s.router.With(jwtauth.Verify(s.csrfTokenAuth, jwtauth.TokenFromCookie)).
|
||||
Post(webClientPubSharesPath+"/{id}/login", s.handleClientShareLoginPost)
|
||||
s.router.Get(webClientPubSharesPath+"/{id}/logout", s.handleClientShareLogout)
|
||||
s.router.Get(webClientPubSharesPath+"/{id}", s.downloadFromShare)
|
||||
s.router.Post(webClientPubSharesPath+"/{id}/partial", s.handleClientSharePartialDownload)
|
||||
s.router.Get(webClientPubSharesPath+"/{id}/browse", s.handleShareGetFiles)
|
||||
|
@ -1572,32 +1580,32 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
if s.binding.OIDC.isEnabled() {
|
||||
router.Use(s.oidcTokenAuthenticator(tokenAudienceWebClient))
|
||||
}
|
||||
router.Use(jwtauth.Verify(s.tokenAuth, tokenFromContext, jwtauth.TokenFromCookie))
|
||||
router.Use(jwtauth.Verify(s.tokenAuth, oidcTokenFromContext, jwtauth.TokenFromCookie))
|
||||
router.Use(jwtAuthenticatorWebClient)
|
||||
|
||||
router.Get(webClientLogoutPath, s.handleWebClientLogout)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientFilesPath, s.handleClientGetFiles)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientViewPDFPath, s.handleClientViewPDF)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientGetPDFPath, s.handleClientGetPDF)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie, verifyCSRFHeader).Get(webClientFilePath, getUserFile)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie, verifyCSRFHeader).Get(webClientTasksPath+"/{id}",
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie, s.verifyCSRFHeader).Get(webClientFilePath, getUserFile)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie, s.verifyCSRFHeader).Get(webClientTasksPath+"/{id}",
|
||||
getWebTask)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).
|
||||
Post(webClientFilePath, uploadUserFile)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).
|
||||
Post(webClientExistPath, s.handleClientCheckExist)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientEditFilePath, s.handleClientEditFile)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).
|
||||
Delete(webClientFilesPath, deleteUserFile)
|
||||
router.With(s.checkAuthRequirements, compressor.Handler, s.refreshCookie).
|
||||
Get(webClientDirsPath, s.handleClientGetDirContents)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).
|
||||
Post(webClientDirsPath, createUserDir)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).
|
||||
Delete(webClientDirsPath, taskDeleteDir)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).
|
||||
Post(webClientFileActionsPath+"/move", taskRenameFsEntry)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).
|
||||
Post(webClientFileActionsPath+"/copy", taskCopyFsEntry)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).
|
||||
Post(webClientDownloadZipPath, s.handleWebClientDownloadZip)
|
||||
|
@ -1613,15 +1621,15 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
Get(webClientMFAPath, s.handleWebClientMFA)
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).
|
||||
Get(webClientMFAPath+"/qrcode", getQRCode)
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.verifyCSRFHeader).
|
||||
Post(webClientTOTPGeneratePath, generateTOTPSecret)
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.verifyCSRFHeader).
|
||||
Post(webClientTOTPValidatePath, validateTOTPPasscode)
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.verifyCSRFHeader).
|
||||
Post(webClientTOTPSavePath, saveTOTPConfig)
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader, s.refreshCookie).
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.verifyCSRFHeader, s.refreshCookie).
|
||||
Get(webClientRecoveryCodesPath, getRecoveryCodes)
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.verifyCSRFHeader).
|
||||
Post(webClientRecoveryCodesPath, generateRecoveryCodes)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), compressor.Handler, s.refreshCookie).
|
||||
Get(webClientSharesPath+jsonAPISuffix, getAllShares)
|
||||
|
@ -1635,7 +1643,7 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
Get(webClientSharePath+"/{id}", s.handleClientUpdateShareGet)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
|
||||
Post(webClientSharePath+"/{id}", s.handleClientUpdateSharePost)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), verifyCSRFHeader).
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.verifyCSRFHeader).
|
||||
Delete(webClientSharePath+"/{id}", deleteShare)
|
||||
})
|
||||
}
|
||||
|
@ -1647,15 +1655,27 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
s.redirectToWebPath(w, r, webAdminLoginPath)
|
||||
})
|
||||
s.router.Get(path.Join(webStaticFilesPath, "branding/webadmin/logo.png"),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderPNGImage(w, r, dbBrandingConfig.getWebAdminLogo())
|
||||
})
|
||||
s.router.Get(path.Join(webStaticFilesPath, "branding/webadmin/favicon.png"),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderPNGImage(w, r, dbBrandingConfig.getWebAdminFavicon())
|
||||
})
|
||||
s.router.Get(webAdminLoginPath, s.handleWebAdminLogin)
|
||||
if s.binding.OIDC.hasRoles() && !s.binding.isWebAdminOIDCLoginDisabled() {
|
||||
s.router.Get(webAdminOIDCLoginPath, s.handleWebAdminOIDCLogin)
|
||||
}
|
||||
s.router.Get(webOAuth2RedirectPath, s.handleOAuth2TokenRedirect)
|
||||
s.router.Get(webAdminSetupPath, s.handleWebAdminSetupGet)
|
||||
s.router.Post(webAdminSetupPath, s.handleWebAdminSetupPost)
|
||||
s.router.With(jwtauth.Verify(s.csrfTokenAuth, jwtauth.TokenFromCookie)).
|
||||
Post(webAdminSetupPath, s.handleWebAdminSetupPost)
|
||||
if !s.binding.isWebAdminLoginFormDisabled() {
|
||||
s.router.Post(webAdminLoginPath, s.handleWebAdminLoginPost)
|
||||
s.router.With(jwtauth.Verify(s.csrfTokenAuth, jwtauth.TokenFromCookie)).
|
||||
Post(webAdminLoginPath, s.handleWebAdminLoginPost)
|
||||
s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
|
||||
s.jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).
|
||||
Get(webAdminTwoFactorPath, s.handleWebAdminTwoFactor)
|
||||
|
@ -1669,16 +1689,19 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
s.jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).
|
||||
Post(webAdminTwoFactorRecoveryPath, s.handleWebAdminTwoFactorRecoveryPost)
|
||||
s.router.Get(webAdminForgotPwdPath, s.handleWebAdminForgotPwd)
|
||||
s.router.Post(webAdminForgotPwdPath, s.handleWebAdminForgotPwdPost)
|
||||
s.router.Get(webAdminResetPwdPath, s.handleWebAdminPasswordReset)
|
||||
s.router.Post(webAdminResetPwdPath, s.handleWebAdminPasswordResetPost)
|
||||
s.router.With(jwtauth.Verify(s.csrfTokenAuth, jwtauth.TokenFromCookie)).
|
||||
Post(webAdminForgotPwdPath, s.handleWebAdminForgotPwdPost)
|
||||
s.router.With(jwtauth.Verify(s.csrfTokenAuth, jwtauth.TokenFromCookie)).
|
||||
Get(webAdminResetPwdPath, s.handleWebAdminPasswordReset)
|
||||
s.router.With(jwtauth.Verify(s.csrfTokenAuth, jwtauth.TokenFromCookie)).
|
||||
Post(webAdminResetPwdPath, s.handleWebAdminPasswordResetPost)
|
||||
}
|
||||
|
||||
s.router.Group(func(router chi.Router) {
|
||||
if s.binding.OIDC.isEnabled() {
|
||||
router.Use(s.oidcTokenAuthenticator(tokenAudienceWebAdmin))
|
||||
}
|
||||
router.Use(jwtauth.Verify(s.tokenAuth, tokenFromContext, jwtauth.TokenFromCookie))
|
||||
router.Use(jwtauth.Verify(s.tokenAuth, oidcTokenFromContext, jwtauth.TokenFromCookie))
|
||||
router.Use(jwtAuthenticatorWebAdmin)
|
||||
|
||||
router.Get(webLogoutPath, s.handleWebAdminLogout)
|
||||
|
@ -1690,12 +1713,12 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
|
||||
router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webAdminMFAPath, s.handleWebAdminMFA)
|
||||
router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webAdminMFAPath+"/qrcode", getQRCode)
|
||||
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPGeneratePath, generateTOTPSecret)
|
||||
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPValidatePath, validateTOTPPasscode)
|
||||
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPSavePath, saveTOTPConfig)
|
||||
router.With(verifyCSRFHeader, s.requireBuiltinLogin, s.refreshCookie).Get(webAdminRecoveryCodesPath,
|
||||
router.With(s.verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPGeneratePath, generateTOTPSecret)
|
||||
router.With(s.verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPValidatePath, validateTOTPPasscode)
|
||||
router.With(s.verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPSavePath, saveTOTPConfig)
|
||||
router.With(s.verifyCSRFHeader, s.requireBuiltinLogin, s.refreshCookie).Get(webAdminRecoveryCodesPath,
|
||||
getRecoveryCodes)
|
||||
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminRecoveryCodesPath, generateRecoveryCodes)
|
||||
router.With(s.verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminRecoveryCodesPath, generateRecoveryCodes)
|
||||
|
||||
router.Group(func(router chi.Router) {
|
||||
router.Use(s.checkAuthRequirements)
|
||||
|
@ -1722,7 +1745,7 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
Get(webGroupPath+"/{name}", s.handleWebUpdateGroupGet)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath+"/{name}",
|
||||
s.handleWebUpdateGroupPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.verifyCSRFHeader).
|
||||
Delete(webGroupPath+"/{name}", deleteGroup)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
|
||||
Get(webConnectionsPath, s.handleWebGetConnections)
|
||||
|
@ -1748,25 +1771,25 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, s.handleWebAddAdminPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}",
|
||||
s.handleWebUpdateAdminPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.verifyCSRFHeader).
|
||||
Delete(webAdminPath+"/{username}", deleteAdmin)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminDisableMFA), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminDisableMFA), s.verifyCSRFHeader).
|
||||
Put(webAdminPath+"/{username}/2fa/disable", disableAdmin2FA)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminCloseConnections), s.verifyCSRFHeader).
|
||||
Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
|
||||
Get(webFolderPath+"/{name}", s.handleWebUpdateFolderGet)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath+"/{name}",
|
||||
s.handleWebUpdateFolderPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.verifyCSRFHeader).
|
||||
Delete(webFolderPath+"/{name}", deleteFolder)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), s.verifyCSRFHeader).
|
||||
Post(webScanVFolderPath+"/{name}", startFolderQuotaScan)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), s.verifyCSRFHeader).
|
||||
Delete(webUserPath+"/{username}", deleteUser)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminDisableMFA), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminDisableMFA), s.verifyCSRFHeader).
|
||||
Put(webUserPath+"/{username}/2fa/disable", disableUser2FA)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), s.verifyCSRFHeader).
|
||||
Post(webQuotaScanPath+"/{username}", startUserQuotaScan)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, s.handleWebMaintenance)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData)
|
||||
|
@ -1793,7 +1816,7 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
Get(webAdminEventActionPath+"/{name}", s.handleWebUpdateEventActionGet)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath+"/{name}",
|
||||
s.handleWebUpdateEventActionPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.verifyCSRFHeader).
|
||||
Delete(webAdminEventActionPath+"/{name}", deleteEventAction)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie).
|
||||
Get(webAdminEventRulesPath+jsonAPISuffix, getAllRules)
|
||||
|
@ -1807,9 +1830,9 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
Get(webAdminEventRulePath+"/{name}", s.handleWebUpdateEventRuleGet)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath+"/{name}",
|
||||
s.handleWebUpdateEventRulePost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.verifyCSRFHeader).
|
||||
Delete(webAdminEventRulePath+"/{name}", deleteEventRule)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.verifyCSRFHeader).
|
||||
Post(webAdminEventRulePath+"/run/{name}", runOnDemandRule)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
|
||||
Get(webAdminRolesPath, s.handleWebGetRoles)
|
||||
|
@ -1822,7 +1845,7 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
Get(webAdminRolePath+"/{name}", s.handleWebUpdateRoleGet)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath+"/{name}",
|
||||
s.handleWebUpdateRolePost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageRoles), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.verifyCSRFHeader).
|
||||
Delete(webAdminRolePath+"/{name}", deleteRole)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminViewEvents), s.refreshCookie).Get(webEventsPath,
|
||||
s.handleWebGetEvents)
|
||||
|
@ -1843,14 +1866,14 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
s.handleWebUpdateIPListEntryGet)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}/{ipornet}",
|
||||
s.handleWebUpdateIPListEntryPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), verifyCSRFHeader).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.verifyCSRFHeader).
|
||||
Delete(webIPListPath+"/{type}/{ipornet}", deleteIPListEntry)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).Get(webConfigsPath, s.handleWebConfigs)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webConfigsPath, s.handleWebConfigsPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie).
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.verifyCSRFHeader, s.refreshCookie).
|
||||
Post(webConfigsPath+"/smtp/test", testSMTPConfig)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie).
|
||||
Post(webOAuth2TokenPath, handleSMTPOAuth2TokenRequestPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.verifyCSRFHeader, s.refreshCookie).
|
||||
Post(webOAuth2TokenPath, s.handleSMTPOAuth2TokenRequestPost)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -25,12 +25,14 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
sdkkms "github.com/sftpgo/sdk/kms"
|
||||
|
||||
|
@ -150,6 +152,7 @@ type basePage struct {
|
|||
HasSearcher bool
|
||||
HasExternalLogin bool
|
||||
LoggedUser *dataprovider.Admin
|
||||
IsLoggedToShare bool
|
||||
Branding UIBranding
|
||||
}
|
||||
|
||||
|
@ -327,6 +330,7 @@ type configsPage struct {
|
|||
RedactedSecret string
|
||||
OAuth2TokenURL string
|
||||
OAuth2RedirectURL string
|
||||
WebClientBranding UIBranding
|
||||
Error *util.I18nError
|
||||
}
|
||||
|
||||
|
@ -612,10 +616,10 @@ func isServerManagerResource(currentURL string) bool {
|
|||
currentURL == webConfigsPath
|
||||
}
|
||||
|
||||
func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) basePage {
|
||||
func (s *httpdServer) getBasePageData(title, currentURL string, w http.ResponseWriter, r *http.Request) basePage {
|
||||
var csrfToken string
|
||||
if currentURL != "" {
|
||||
csrfToken = createCSRFToken(util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
csrfToken = createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseAdminPath)
|
||||
}
|
||||
return basePage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
|
@ -660,7 +664,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
|
|||
HasSearcher: plugin.Handler.HasSearcher(),
|
||||
HasExternalLogin: isLoggedInWithOIDC(r),
|
||||
CSRFToken: csrfToken,
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -675,7 +679,7 @@ func (s *httpdServer) renderMessagePageWithString(w http.ResponseWriter, r *http
|
|||
err error, message, text string,
|
||||
) {
|
||||
data := messagePage{
|
||||
basePage: s.getBasePageData(title, "", r),
|
||||
basePage: s.getBasePageData(title, "", w, r),
|
||||
Error: getI18nError(err),
|
||||
Success: message,
|
||||
Text: text,
|
||||
|
@ -710,60 +714,60 @@ func (s *httpdServer) renderNotFoundPage(w http.ResponseWriter, r *http.Request,
|
|||
util.NewI18nError(err, util.I18nError404Message), "")
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := forgotPwdPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
CurrentURL: webAdminForgotPwdPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseAdminPath),
|
||||
LoginURL: webAdminLoginPath,
|
||||
Title: util.I18nForgotPwdTitle,
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
renderAdminTemplate(w, templateForgotPassword, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := resetPwdPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
CurrentURL: webAdminResetPwdPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseAdminPath),
|
||||
LoginURL: webAdminLoginPath,
|
||||
Title: util.I18nResetPwdTitle,
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
renderAdminTemplate(w, templateResetPassword, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := twoFactorPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
Title: pageTwoFactorTitle,
|
||||
CurrentURL: webAdminTwoFactorPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseAdminPath),
|
||||
RecoveryURL: webAdminTwoFactorRecoveryPath,
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
renderAdminTemplate(w, templateTwoFactor, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := twoFactorPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
Title: pageTwoFactorRecoveryTitle,
|
||||
CurrentURL: webAdminTwoFactorRecoveryPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseAdminPath),
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
renderAdminTemplate(w, templateTwoFactorRecovery, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderMFAPage(w http.ResponseWriter, r *http.Request) {
|
||||
data := mfaPage{
|
||||
basePage: s.getBasePageData(pageMFATitle, webAdminMFAPath, r),
|
||||
basePage: s.getBasePageData(pageMFATitle, webAdminMFAPath, w, r),
|
||||
TOTPConfigs: mfa.GetAvailableTOTPConfigNames(),
|
||||
GenerateTOTPURL: webAdminTOTPGeneratePath,
|
||||
ValidateTOTPURL: webAdminTOTPValidatePath,
|
||||
|
@ -782,7 +786,7 @@ func (s *httpdServer) renderMFAPage(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func (s *httpdServer) renderProfilePage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
data := profilePage{
|
||||
basePage: s.getBasePageData(util.I18nProfileTitle, webAdminProfilePath, r),
|
||||
basePage: s.getBasePageData(util.I18nProfileTitle, webAdminProfilePath, w, r),
|
||||
Error: getI18nError(err),
|
||||
}
|
||||
admin, err := dataprovider.AdminExists(data.LoggedUser.Username)
|
||||
|
@ -799,7 +803,7 @@ func (s *httpdServer) renderProfilePage(w http.ResponseWriter, r *http.Request,
|
|||
|
||||
func (s *httpdServer) renderChangePasswordPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := changePasswordPage{
|
||||
basePage: s.getBasePageData(util.I18nChangePwdTitle, webChangeAdminPwdPath, r),
|
||||
basePage: s.getBasePageData(util.I18nChangePwdTitle, webChangeAdminPwdPath, w, r),
|
||||
Error: err,
|
||||
}
|
||||
|
||||
|
@ -808,7 +812,7 @@ func (s *httpdServer) renderChangePasswordPage(w http.ResponseWriter, r *http.Re
|
|||
|
||||
func (s *httpdServer) renderMaintenancePage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
data := maintenancePage{
|
||||
basePage: s.getBasePageData(util.I18nMaintenanceTitle, webMaintenancePath, r),
|
||||
basePage: s.getBasePageData(util.I18nMaintenanceTitle, webMaintenancePath, w, r),
|
||||
BackupPath: webBackupPath,
|
||||
RestorePath: webRestorePath,
|
||||
Error: getI18nError(err),
|
||||
|
@ -830,30 +834,31 @@ func (s *httpdServer) renderConfigsPage(w http.ResponseWriter, r *http.Request,
|
|||
configs.ACME.HTTP01Challenge.Port = 80
|
||||
}
|
||||
data := configsPage{
|
||||
basePage: s.getBasePageData(util.I18nConfigsTitle, webConfigsPath, r),
|
||||
basePage: s.getBasePageData(util.I18nConfigsTitle, webConfigsPath, w, r),
|
||||
Configs: configs,
|
||||
ConfigSection: section,
|
||||
RedactedSecret: redactedSecret,
|
||||
OAuth2TokenURL: webOAuth2TokenPath,
|
||||
OAuth2RedirectURL: webOAuth2RedirectPath,
|
||||
WebClientBranding: s.binding.webClientBranding(),
|
||||
Error: getI18nError(err),
|
||||
}
|
||||
|
||||
renderAdminTemplate(w, templateConfigs, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, ip string, err *util.I18nError) {
|
||||
func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username string, err *util.I18nError) {
|
||||
data := setupPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
Title: util.I18nSetupTitle,
|
||||
CurrentURL: webAdminSetupPath,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseAdminPath),
|
||||
Username: username,
|
||||
HasInstallationCode: installationCode != "",
|
||||
InstallationCodeHint: installationCodeHint,
|
||||
HideSupportLink: hideSupportLink,
|
||||
Error: err,
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
|
||||
renderAdminTemplate(w, templateSetup, data)
|
||||
|
@ -876,7 +881,7 @@ func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Re
|
|||
title = util.I18nUpdateAdminTitle
|
||||
}
|
||||
data := adminPage{
|
||||
basePage: s.getBasePageData(title, currentURL, r),
|
||||
basePage: s.getBasePageData(title, currentURL, w, r),
|
||||
Admin: admin,
|
||||
Groups: groups,
|
||||
Roles: roles,
|
||||
|
@ -917,7 +922,7 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
|
|||
}
|
||||
}
|
||||
user.FsConfig.RedactedSecret = redactedSecret
|
||||
basePage := s.getBasePageData(title, currentURL, r)
|
||||
basePage := s.getBasePageData(title, currentURL, w, r)
|
||||
if (mode == userPageModeAdd || mode == userPageModeTemplate) && len(user.Groups) == 0 && admin != nil {
|
||||
for _, group := range admin.Groups {
|
||||
user.Groups = append(user.Groups, sdk.GroupMapping{
|
||||
|
@ -982,7 +987,7 @@ func (s *httpdServer) renderIPListPage(w http.ResponseWriter, r *http.Request, e
|
|||
currentURL = fmt.Sprintf("%s/%d/%s", webIPListPath, entry.Type, url.PathEscape(entry.IPOrNet))
|
||||
}
|
||||
data := ipListPage{
|
||||
basePage: s.getBasePageData(title, currentURL, r),
|
||||
basePage: s.getBasePageData(title, currentURL, w, r),
|
||||
Error: getI18nError(err),
|
||||
Entry: &entry,
|
||||
Mode: mode,
|
||||
|
@ -1003,7 +1008,7 @@ func (s *httpdServer) renderRolePage(w http.ResponseWriter, r *http.Request, rol
|
|||
currentURL = fmt.Sprintf("%s/%s", webAdminRolePath, url.PathEscape(role.Name))
|
||||
}
|
||||
data := rolePage{
|
||||
basePage: s.getBasePageData(title, currentURL, r),
|
||||
basePage: s.getBasePageData(title, currentURL, w, r),
|
||||
Error: getI18nError(err),
|
||||
Role: &role,
|
||||
Mode: mode,
|
||||
|
@ -1033,7 +1038,7 @@ func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, gr
|
|||
group.UserSettings.FsConfig.SetEmptySecretsIfNil()
|
||||
|
||||
data := groupPage{
|
||||
basePage: s.getBasePageData(title, currentURL, r),
|
||||
basePage: s.getBasePageData(title, currentURL, w, r),
|
||||
Error: getI18nError(err),
|
||||
Group: &group,
|
||||
Mode: mode,
|
||||
|
@ -1078,7 +1083,7 @@ func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
|
||||
data := eventActionPage{
|
||||
basePage: s.getBasePageData(title, currentURL, r),
|
||||
basePage: s.getBasePageData(title, currentURL, w, r),
|
||||
Action: action,
|
||||
ActionTypes: dataprovider.EventActionTypes,
|
||||
FsActions: dataprovider.FsActionTypes,
|
||||
|
@ -1108,7 +1113,7 @@ func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request
|
|||
}
|
||||
|
||||
data := eventRulePage{
|
||||
basePage: s.getBasePageData(title, currentURL, r),
|
||||
basePage: s.getBasePageData(title, currentURL, w, r),
|
||||
Rule: rule,
|
||||
TriggerTypes: dataprovider.EventTriggerTypes,
|
||||
Actions: actions,
|
||||
|
@ -1142,7 +1147,7 @@ func (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, f
|
|||
folder.FsConfig.SetEmptySecretsIfNil()
|
||||
|
||||
data := folderPage{
|
||||
basePage: s.getBasePageData(title, currentURL, r),
|
||||
basePage: s.getBasePageData(title, currentURL, w, r),
|
||||
Error: getI18nError(err),
|
||||
Folder: folder,
|
||||
Mode: mode,
|
||||
|
@ -1484,13 +1489,13 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
|||
filters.PasswordStrength = passwordStrength
|
||||
filters.AccessTime = getAccessTimeRestrictionsFromPostFields(r)
|
||||
hooks := r.Form["hooks"]
|
||||
if util.Contains(hooks, "external_auth_disabled") {
|
||||
if slices.Contains(hooks, "external_auth_disabled") {
|
||||
filters.Hooks.ExternalAuthDisabled = true
|
||||
}
|
||||
if util.Contains(hooks, "pre_login_disabled") {
|
||||
if slices.Contains(hooks, "pre_login_disabled") {
|
||||
filters.Hooks.PreLoginDisabled = true
|
||||
}
|
||||
if util.Contains(hooks, "check_password_disabled") {
|
||||
if slices.Contains(hooks, "check_password_disabled") {
|
||||
filters.Hooks.CheckPasswordDisabled = true
|
||||
}
|
||||
filters.IsAnonymous = r.Form.Get("is_anonymous") != ""
|
||||
|
@ -1580,7 +1585,7 @@ func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
|
|||
config.AutomaticCredentials = 0
|
||||
}
|
||||
credentials, _, err := r.FormFile("gcs_credential_file")
|
||||
if err == http.ErrMissingFile {
|
||||
if errors.Is(err, http.ErrMissingFile) {
|
||||
return config, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -2211,7 +2216,7 @@ func getFoldersRetentionFromPostFields(r *http.Request) ([]dataprovider.FolderRe
|
|||
res = append(res, dataprovider.FolderRetention{
|
||||
Path: p,
|
||||
Retention: retention,
|
||||
DeleteEmptyDirs: util.Contains(opts, "1"),
|
||||
DeleteEmptyDirs: slices.Contains(opts, "1"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -2553,9 +2558,9 @@ func getEventRuleActionsFromPostFields(r *http.Request) []dataprovider.EventActi
|
|||
},
|
||||
Order: order + 1,
|
||||
Options: dataprovider.EventActionOptions{
|
||||
IsFailureAction: util.Contains(options, "1"),
|
||||
StopOnFailure: util.Contains(options, "2"),
|
||||
ExecuteSync: util.Contains(options, "3"),
|
||||
IsFailureAction: slices.Contains(options, "1"),
|
||||
StopOnFailure: slices.Contains(options, "2"),
|
||||
ExecuteSync: slices.Contains(options, "3"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -2758,31 +2763,95 @@ func getSMTPConfigsFromPostFields(r *http.Request) *dataprovider.SMTPConfigs {
|
|||
}
|
||||
}
|
||||
|
||||
func getImageInputBytes(r *http.Request, fieldName, removeFieldName string, defaultVal []byte) ([]byte, error) {
|
||||
var result []byte
|
||||
remove := r.Form.Get(removeFieldName)
|
||||
if remove == "" || remove == "0" {
|
||||
result = defaultVal
|
||||
}
|
||||
f, _, err := r.FormFile(fieldName)
|
||||
if err != nil {
|
||||
if errors.Is(err, http.ErrMissingFile) {
|
||||
return result, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
func getBrandingConfigFromPostFields(r *http.Request, config *dataprovider.BrandingConfigs) (
|
||||
*dataprovider.BrandingConfigs, error,
|
||||
) {
|
||||
if config == nil {
|
||||
config = &dataprovider.BrandingConfigs{}
|
||||
}
|
||||
adminLogo, err := getImageInputBytes(r, "branding_webadmin_logo", "branding_webadmin_logo_remove", config.WebAdmin.Logo)
|
||||
if err != nil {
|
||||
return nil, util.NewI18nError(err, util.I18nErrorInvalidForm)
|
||||
}
|
||||
adminFavicon, err := getImageInputBytes(r, "branding_webadmin_favicon", "branding_webadmin_favicon_remove",
|
||||
config.WebAdmin.Favicon)
|
||||
if err != nil {
|
||||
return nil, util.NewI18nError(err, util.I18nErrorInvalidForm)
|
||||
}
|
||||
clientLogo, err := getImageInputBytes(r, "branding_webclient_logo", "branding_webclient_logo_remove",
|
||||
config.WebClient.Logo)
|
||||
if err != nil {
|
||||
return nil, util.NewI18nError(err, util.I18nErrorInvalidForm)
|
||||
}
|
||||
clientFavicon, err := getImageInputBytes(r, "branding_webclient_favicon", "branding_webclient_favicon_remove",
|
||||
config.WebClient.Favicon)
|
||||
if err != nil {
|
||||
return nil, util.NewI18nError(err, util.I18nErrorInvalidForm)
|
||||
}
|
||||
|
||||
branding := &dataprovider.BrandingConfigs{
|
||||
WebAdmin: dataprovider.BrandingConfig{
|
||||
Name: strings.TrimSpace(r.Form.Get("branding_webadmin_name")),
|
||||
ShortName: strings.TrimSpace(r.Form.Get("branding_webadmin_short_name")),
|
||||
Logo: adminLogo,
|
||||
Favicon: adminFavicon,
|
||||
DisclaimerName: strings.TrimSpace(r.Form.Get("branding_webadmin_disclaimer_name")),
|
||||
DisclaimerURL: strings.TrimSpace(r.Form.Get("branding_webadmin_disclaimer_url")),
|
||||
},
|
||||
WebClient: dataprovider.BrandingConfig{
|
||||
Name: strings.TrimSpace(r.Form.Get("branding_webclient_name")),
|
||||
ShortName: strings.TrimSpace(r.Form.Get("branding_webclient_short_name")),
|
||||
Logo: clientLogo,
|
||||
Favicon: clientFavicon,
|
||||
DisclaimerName: strings.TrimSpace(r.Form.Get("branding_webclient_disclaimer_name")),
|
||||
DisclaimerURL: strings.TrimSpace(r.Form.Get("branding_webclient_disclaimer_url")),
|
||||
},
|
||||
}
|
||||
return branding, nil
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
if !smtp.IsEnabled() {
|
||||
s.renderNotFoundPage(w, r, errors.New("this page does not exist"))
|
||||
return
|
||||
}
|
||||
s.renderForgotPwdPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderForgotPwdPage(w, r, nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
s.renderForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
err = handleForgotPassword(r, r.Form.Get("username"), true)
|
||||
if err != nil {
|
||||
s.renderForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorPwdResetGeneric), ipAddr)
|
||||
s.renderForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorPwdResetGeneric))
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webAdminResetPwdPath, http.StatusFound)
|
||||
|
@ -2794,17 +2863,17 @@ func (s *httpdServer) handleWebAdminPasswordReset(w http.ResponseWriter, r *http
|
|||
s.renderNotFoundPage(w, r, errors.New("this page does not exist"))
|
||||
return
|
||||
}
|
||||
s.renderResetPwdPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderResetPwdPage(w, r, nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAdminTwoFactor(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
s.renderTwoFactorPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderTwoFactorPage(w, r, nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAdminTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
s.renderTwoFactorRecoveryPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderTwoFactorRecoveryPage(w, r, nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAdminMFA(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -2830,7 +2899,7 @@ func (s *httpdServer) handleWebAdminProfilePost(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -2875,7 +2944,7 @@ func (s *httpdServer) handleWebRestore(w http.ResponseWriter, r *http.Request) {
|
|||
defer r.MultipartForm.RemoveAll() //nolint:errcheck
|
||||
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -2936,7 +3005,7 @@ func getAllAdmins(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *httpdServer) handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := s.getBasePageData(util.I18nAdminsTitle, webAdminsPath, r)
|
||||
data := s.getBasePageData(util.I18nAdminsTitle, webAdminsPath, w, r)
|
||||
renderAdminTemplate(w, templateAdmins, data)
|
||||
}
|
||||
|
||||
|
@ -2946,7 +3015,7 @@ func (s *httpdServer) handleWebAdminSetupGet(w http.ResponseWriter, r *http.Requ
|
|||
http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
|
||||
return
|
||||
}
|
||||
s.renderAdminSetupPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr), nil)
|
||||
s.renderAdminSetupPage(w, r, "", nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -2987,7 +3056,7 @@ func (s *httpdServer) handleWebAddAdminPost(w http.ResponseWriter, r *http.Reque
|
|||
admin.Password = util.GenerateUniqueID()
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3018,7 +3087,7 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3071,7 +3140,7 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re
|
|||
func (s *httpdServer) handleWebDefenderPage(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
data := defenderHostsPage{
|
||||
basePage: s.getBasePageData(util.I18nDefenderTitle, webDefenderPath, r),
|
||||
basePage: s.getBasePageData(util.I18nDefenderTitle, webDefenderPath, w, r),
|
||||
DefenderHostsURL: webDefenderHostsPath,
|
||||
}
|
||||
|
||||
|
@ -3105,7 +3174,7 @@ func (s *httpdServer) handleGetWebUsers(w http.ResponseWriter, r *http.Request)
|
|||
s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
|
||||
return
|
||||
}
|
||||
data := s.getBasePageData(util.I18nUsersTitle, webUsersPath, r)
|
||||
data := s.getBasePageData(util.I18nUsersTitle, webUsersPath, w, r)
|
||||
renderAdminTemplate(w, templateUsers, data)
|
||||
}
|
||||
|
||||
|
@ -3144,7 +3213,7 @@ func (s *httpdServer) handleWebTemplateFolderPost(w http.ResponseWriter, r *http
|
|||
defer r.MultipartForm.RemoveAll() //nolint:errcheck
|
||||
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3244,7 +3313,7 @@ func (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3341,7 +3410,7 @@ func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Reques
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3387,7 +3456,7 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3425,7 +3494,7 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
|
|||
func (s *httpdServer) handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
data := statusPage{
|
||||
basePage: s.getBasePageData(util.I18nStatusTitle, webStatusPath, r),
|
||||
basePage: s.getBasePageData(util.I18nStatusTitle, webStatusPath, w, r),
|
||||
Status: getServicesStatus(),
|
||||
}
|
||||
renderAdminTemplate(w, templateStatus, data)
|
||||
|
@ -3439,7 +3508,7 @@ func (s *httpdServer) handleWebGetConnections(w http.ResponseWriter, r *http.Req
|
|||
return
|
||||
}
|
||||
|
||||
data := s.getBasePageData(util.I18nSessionsTitle, webConnectionsPath, r)
|
||||
data := s.getBasePageData(util.I18nSessionsTitle, webConnectionsPath, w, r)
|
||||
renderAdminTemplate(w, templateConnections, data)
|
||||
}
|
||||
|
||||
|
@ -3464,7 +3533,7 @@ func (s *httpdServer) handleWebAddFolderPost(w http.ResponseWriter, r *http.Requ
|
|||
defer r.MultipartForm.RemoveAll() //nolint:errcheck
|
||||
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3525,7 +3594,7 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R
|
|||
defer r.MultipartForm.RemoveAll() //nolint:errcheck
|
||||
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3588,7 +3657,7 @@ func getAllFolders(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *httpdServer) handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := s.getBasePageData(util.I18nFoldersTitle, webFoldersPath, r)
|
||||
data := s.getBasePageData(util.I18nFoldersTitle, webFoldersPath, w, r)
|
||||
renderAdminTemplate(w, templateFolders, data)
|
||||
}
|
||||
|
||||
|
@ -3626,7 +3695,7 @@ func getAllGroups(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *httpdServer) handleWebGetGroups(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := s.getBasePageData(util.I18nGroupsTitle, webGroupsPath, r)
|
||||
data := s.getBasePageData(util.I18nGroupsTitle, webGroupsPath, w, r)
|
||||
renderAdminTemplate(w, templateGroups, data)
|
||||
}
|
||||
|
||||
|
@ -3648,7 +3717,7 @@ func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3695,7 +3764,7 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3748,7 +3817,7 @@ func getAllActions(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *httpdServer) handleWebGetEventActions(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := s.getBasePageData(util.I18nActionsTitle, webAdminEventActionsPath, r)
|
||||
data := s.getBasePageData(util.I18nActionsTitle, webAdminEventActionsPath, w, r)
|
||||
renderAdminTemplate(w, templateEventActions, data)
|
||||
}
|
||||
|
||||
|
@ -3773,7 +3842,7 @@ func (s *httpdServer) handleWebAddEventActionPost(w http.ResponseWriter, r *http
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3819,7 +3888,7 @@ func (s *httpdServer) handleWebUpdateEventActionPost(w http.ResponseWriter, r *h
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3858,7 +3927,7 @@ func getAllRules(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := s.getBasePageData(util.I18nRulesTitle, webAdminEventRulesPath, r)
|
||||
data := s.getBasePageData(util.I18nRulesTitle, webAdminEventRulesPath, w, r)
|
||||
renderAdminTemplate(w, templateEventRules, data)
|
||||
}
|
||||
|
||||
|
@ -3884,7 +3953,7 @@ func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
err = verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr)
|
||||
err = verifyCSRFToken(r, s.csrfTokenAuth)
|
||||
if err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
|
@ -3931,7 +4000,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -3978,7 +4047,7 @@ func getAllRoles(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func (s *httpdServer) handleWebGetRoles(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
data := s.getBasePageData(util.I18nRolesTitle, webAdminRolesPath, r)
|
||||
data := s.getBasePageData(util.I18nRolesTitle, webAdminRolesPath, w, r)
|
||||
|
||||
renderAdminTemplate(w, templateRoles, data)
|
||||
}
|
||||
|
@ -4001,7 +4070,7 @@ func (s *httpdServer) handleWebAddRolePost(w http.ResponseWriter, r *http.Reques
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -4047,7 +4116,7 @@ func (s *httpdServer) handleWebUpdateRolePost(w http.ResponseWriter, r *http.Req
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -4065,7 +4134,7 @@ func (s *httpdServer) handleWebGetEvents(w http.ResponseWriter, r *http.Request)
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := eventsPage{
|
||||
basePage: s.getBasePageData(util.I18nEventsTitle, webEventsPath, r),
|
||||
basePage: s.getBasePageData(util.I18nEventsTitle, webEventsPath, w, r),
|
||||
FsEventsSearchURL: webEventsFsSearchPath,
|
||||
ProviderEventsSearchURL: webEventsProviderSearchPath,
|
||||
LogEventsSearchURL: webEventsLogSearchPath,
|
||||
|
@ -4077,7 +4146,7 @@ func (s *httpdServer) handleWebIPListsPage(w http.ResponseWriter, r *http.Reques
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
rtlStatus, rtlProtocols := common.Config.GetRateLimitersStatus()
|
||||
data := ipListsPage{
|
||||
basePage: s.getBasePageData(util.I18nIPListsTitle, webIPListsPath, r),
|
||||
basePage: s.getBasePageData(util.I18nIPListsTitle, webIPListsPath, w, r),
|
||||
RateLimitersStatus: rtlStatus,
|
||||
RateLimitersProtocols: strings.Join(rtlProtocols, ", "),
|
||||
IsAllowListEnabled: common.Config.IsAllowListEnabled(),
|
||||
|
@ -4115,7 +4184,7 @@ func (s *httpdServer) handleWebAddIPListEntryPost(w http.ResponseWriter, r *http
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -4170,7 +4239,7 @@ func (s *httpdServer) handleWebUpdateIPListEntryPost(w http.ResponseWriter, r *h
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -4206,13 +4275,15 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques
|
|||
s.renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
err = r.ParseForm()
|
||||
err = r.ParseMultipartForm(maxRequestSize)
|
||||
if err != nil {
|
||||
s.renderBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
defer r.MultipartForm.RemoveAll() //nolint:errcheck
|
||||
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -4236,6 +4307,15 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques
|
|||
smtpConfigs := getSMTPConfigsFromPostFields(r)
|
||||
updateSMTPSecrets(smtpConfigs, configs.SMTP)
|
||||
configs.SMTP = smtpConfigs
|
||||
case "branding_submit":
|
||||
configSection = 4
|
||||
brandingConfigs, err := getBrandingConfigFromPostFields(r, configs.Branding)
|
||||
if err != nil {
|
||||
logger.Info(logSender, "", "unable to get branding config: %v", err)
|
||||
s.renderConfigsPage(w, r, configs, err, configSection)
|
||||
return
|
||||
}
|
||||
configs.Branding = brandingConfigs
|
||||
default:
|
||||
s.renderBadRequestPage(w, r, errors.New("unsupported form action"))
|
||||
return
|
||||
|
@ -4246,15 +4326,22 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques
|
|||
s.renderConfigsPage(w, r, configs, err, configSection)
|
||||
return
|
||||
}
|
||||
if configSection == 3 {
|
||||
postConfigsUpdate(configSection, configs)
|
||||
s.renderMessagePage(w, r, util.I18nConfigsTitle, http.StatusOK, nil, util.I18nConfigsOK)
|
||||
}
|
||||
|
||||
func postConfigsUpdate(section int, configs dataprovider.Configs) {
|
||||
switch section {
|
||||
case 3:
|
||||
err := configs.SMTP.TryDecrypt()
|
||||
if err == nil {
|
||||
smtp.Activate(configs.SMTP)
|
||||
} else {
|
||||
logger.Error(logSender, "", "unable to decrypt SMTP configuration, cannot activate configuration: %v", err)
|
||||
}
|
||||
case 4:
|
||||
dbBrandingConfig.Set(configs.Branding)
|
||||
}
|
||||
s.renderMessagePage(w, r, util.I18nConfigsTitle, http.StatusOK, nil, util.I18nConfigsOK)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -4262,20 +4349,21 @@ func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.R
|
|||
|
||||
stateToken := r.URL.Query().Get("state")
|
||||
|
||||
state, err := verifyOAuth2Token(stateToken, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
state, err := verifyOAuth2Token(s.csrfTokenAuth, stateToken, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusBadRequest, err, "")
|
||||
return
|
||||
}
|
||||
|
||||
defer oauth2Mgr.removePendingAuth(state)
|
||||
|
||||
pendingAuth, err := oauth2Mgr.getPendingAuth(state)
|
||||
if err != nil {
|
||||
oauth2Mgr.removePendingAuth(state)
|
||||
s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusInternalServerError,
|
||||
util.NewI18nError(err, util.I18nOAuth2ErrorValidateState), "")
|
||||
return
|
||||
}
|
||||
oauth2Mgr.removePendingAuth(state)
|
||||
|
||||
oauth2Config := smtp.OAuth2Config{
|
||||
Provider: pendingAuth.Provider,
|
||||
ClientID: pendingAuth.ClientID,
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -95,6 +96,7 @@ type baseClientPage struct {
|
|||
MFAURL string
|
||||
CSRFToken string
|
||||
LoggedUser *dataprovider.User
|
||||
IsLoggedToShare bool
|
||||
Branding UIBranding
|
||||
}
|
||||
|
||||
|
@ -523,10 +525,10 @@ func loadClientTemplates(templatesPath string) {
|
|||
clientTemplates[templateShareDownload] = shareDownloadTmpl
|
||||
}
|
||||
|
||||
func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
|
||||
func (s *httpdServer) getBaseClientPageData(title, currentURL string, w http.ResponseWriter, r *http.Request) baseClientPage {
|
||||
var csrfToken string
|
||||
if currentURL != "" {
|
||||
csrfToken = createCSRFToken(util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
csrfToken = createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseClientPath)
|
||||
}
|
||||
|
||||
data := baseClientPage{
|
||||
|
@ -544,7 +546,8 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Re
|
|||
MFAURL: webClientMFAPath,
|
||||
CSRFToken: csrfToken,
|
||||
LoggedUser: getUserFromToken(r),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
IsLoggedToShare: false,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
if !strings.HasPrefix(r.RequestURI, webClientPubSharesPath) {
|
||||
data.LoginURL = webClientLoginPath
|
||||
|
@ -552,40 +555,40 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Re
|
|||
return data
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := forgotPwdPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
CurrentURL: webClientForgotPwdPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseClientPath),
|
||||
LoginURL: webClientLoginPath,
|
||||
Title: util.I18nForgotPwdTitle,
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateForgotPassword, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := resetPwdPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
CurrentURL: webClientResetPwdPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseClientPath),
|
||||
LoginURL: webClientLoginPath,
|
||||
Title: util.I18nResetPwdTitle,
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateResetPassword, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderShareLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderShareLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := shareLoginPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
Title: util.I18nShareLoginTitle,
|
||||
CurrentURL: r.RequestURI,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseClientPath),
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateShareLogin, data)
|
||||
}
|
||||
|
@ -599,7 +602,7 @@ func renderClientTemplate(w http.ResponseWriter, tmplName string, data any) {
|
|||
|
||||
func (s *httpdServer) renderClientMessagePage(w http.ResponseWriter, r *http.Request, title string, statusCode int, err error, message string) {
|
||||
data := clientMessagePage{
|
||||
baseClientPage: s.getBaseClientPageData(title, "", r),
|
||||
baseClientPage: s.getBaseClientPageData(title, "", w, r),
|
||||
Error: getI18nError(err),
|
||||
Success: message,
|
||||
}
|
||||
|
@ -627,15 +630,15 @@ func (s *httpdServer) renderClientNotFoundPage(w http.ResponseWriter, r *http.Re
|
|||
util.NewI18nError(err, util.I18nError404Message), "")
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := twoFactorPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
Title: pageTwoFactorTitle,
|
||||
CurrentURL: webClientTwoFactorPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseClientPath),
|
||||
RecoveryURL: webClientTwoFactorRecoveryPath,
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
if next := r.URL.Query().Get("next"); strings.HasPrefix(next, webClientFilesPath) {
|
||||
data.CurrentURL += "?next=" + url.QueryEscape(next)
|
||||
|
@ -643,21 +646,21 @@ func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.R
|
|||
renderClientTemplate(w, templateTwoFactor, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
|
||||
func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := twoFactorPage{
|
||||
commonBasePage: getCommonBasePage(r),
|
||||
Title: pageTwoFactorRecoveryTitle,
|
||||
CurrentURL: webClientTwoFactorRecoveryPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(ip),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseClientPath),
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateTwoFactorRecovery, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request) {
|
||||
data := clientMFAPage{
|
||||
baseClientPage: s.getBaseClientPageData(util.I18n2FATitle, webClientMFAPath, r),
|
||||
baseClientPage: s.getBaseClientPageData(util.I18n2FATitle, webClientMFAPath, w, r),
|
||||
TOTPConfigs: mfa.GetAvailableTOTPConfigNames(),
|
||||
GenerateTOTPURL: webClientTOTPGeneratePath,
|
||||
ValidateTOTPURL: webClientTOTPValidatePath,
|
||||
|
@ -681,7 +684,7 @@ func (s *httpdServer) renderEditFilePage(w http.ResponseWriter, r *http.Request,
|
|||
title = util.I18nEditFileTitle
|
||||
}
|
||||
data := editFilePage{
|
||||
baseClientPage: s.getBaseClientPageData(title, webClientEditFilePath, r),
|
||||
baseClientPage: s.getBaseClientPageData(title, webClientEditFilePath, w, r),
|
||||
Path: fileName,
|
||||
Name: path.Base(fileName),
|
||||
CurrentDir: path.Dir(fileName),
|
||||
|
@ -702,7 +705,7 @@ func (s *httpdServer) renderAddUpdateSharePage(w http.ResponseWriter, r *http.Re
|
|||
title = util.I18nShareUpdateTitle
|
||||
}
|
||||
data := clientSharePage{
|
||||
baseClientPage: s.getBaseClientPageData(title, currentURL, r),
|
||||
baseClientPage: s.getBaseClientPageData(title, currentURL, w, r),
|
||||
Share: share,
|
||||
Error: err,
|
||||
IsAdd: isAdd,
|
||||
|
@ -736,9 +739,11 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque
|
|||
err *util.I18nError, share dataprovider.Share,
|
||||
) {
|
||||
currentURL := path.Join(webClientPubSharesPath, share.ShareID, "browse")
|
||||
baseData := s.getBaseClientPageData(util.I18nSharedFilesTitle, currentURL, r)
|
||||
baseData := s.getBaseClientPageData(util.I18nSharedFilesTitle, currentURL, w, r)
|
||||
baseData.FilesURL = currentURL
|
||||
baseSharePath := path.Join(webClientPubSharesPath, share.ShareID)
|
||||
baseData.LogoutURL = path.Join(webClientPubSharesPath, share.ShareID, "logout")
|
||||
baseData.IsLoggedToShare = share.Password != ""
|
||||
|
||||
data := filesPage{
|
||||
baseClientPage: baseData,
|
||||
|
@ -766,28 +771,39 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque
|
|||
renderClientTemplate(w, templateClientFiles, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderShareDownloadPage(w http.ResponseWriter, r *http.Request, downloadLink string) {
|
||||
func (s *httpdServer) renderShareDownloadPage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share,
|
||||
downloadLink string,
|
||||
) {
|
||||
data := shareDownloadPage{
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nShareDownloadTitle, "", r),
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nShareDownloadTitle, "", w, r),
|
||||
DownloadLink: downloadLink,
|
||||
}
|
||||
data.LogoutURL = ""
|
||||
if share.Password != "" {
|
||||
data.LogoutURL = path.Join(webClientPubSharesPath, share.ShareID, "logout")
|
||||
}
|
||||
|
||||
renderClientTemplate(w, templateShareDownload, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share dataprovider.Share) {
|
||||
func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share) {
|
||||
currentURL := path.Join(webClientPubSharesPath, share.ShareID, "upload")
|
||||
data := shareUploadPage{
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nShareUploadTitle, currentURL, r),
|
||||
Share: &share,
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nShareUploadTitle, currentURL, w, r),
|
||||
Share: share,
|
||||
UploadBasePath: path.Join(webClientPubSharesPath, share.ShareID),
|
||||
}
|
||||
data.LogoutURL = ""
|
||||
if share.Password != "" {
|
||||
data.LogoutURL = path.Join(webClientPubSharesPath, share.ShareID, "logout")
|
||||
}
|
||||
renderClientTemplate(w, templateUploadToShare, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName string,
|
||||
err *util.I18nError, user *dataprovider.User) {
|
||||
data := filesPage{
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nFilesTitle, webClientFilesPath, r),
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nFilesTitle, webClientFilesPath, w, r),
|
||||
Error: err,
|
||||
CurrentDir: url.QueryEscape(dirName),
|
||||
DownloadURL: webClientDownloadZipPath,
|
||||
|
@ -813,7 +829,7 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
|
|||
|
||||
func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := clientProfilePage{
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nProfileTitle, webClientProfilePath, r),
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nProfileTitle, webClientProfilePath, w, r),
|
||||
Error: err,
|
||||
}
|
||||
user, userMerged, errUser := dataprovider.GetUserVariants(data.LoggedUser.Username, "")
|
||||
|
@ -832,7 +848,7 @@ func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Req
|
|||
|
||||
func (s *httpdServer) renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
|
||||
data := changeClientPasswordPage{
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nChangePwdTitle, webChangeClientPwdPath, r),
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nChangePwdTitle, webChangeClientPwdPath, w, r),
|
||||
Error: err,
|
||||
}
|
||||
|
||||
|
@ -850,8 +866,7 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http.
|
|||
s.renderClientBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -1023,7 +1038,7 @@ func (s *httpdServer) handleClientUploadToShare(w http.ResponseWriter, r *http.R
|
|||
http.Redirect(w, r, path.Join(webClientPubSharesPath, share.ShareID, "browse"), http.StatusFound)
|
||||
return
|
||||
}
|
||||
s.renderUploadToSharePage(w, r, share)
|
||||
s.renderUploadToSharePage(w, r, &share)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -1088,7 +1103,7 @@ func (s *httpdServer) handleShareViewPDF(w http.ResponseWriter, r *http.Request)
|
|||
Title: path.Base(name),
|
||||
URL: fmt.Sprintf("%s?path=%s&_=%d", path.Join(webClientPubSharesPath, share.ShareID, "getpdf"),
|
||||
url.QueryEscape(name), time.Now().UTC().Unix()),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateClientViewPDF, data)
|
||||
}
|
||||
|
@ -1440,7 +1455,7 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -1449,7 +1464,7 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
|
|||
share.LastUseAt = 0
|
||||
share.Username = claims.Username
|
||||
if share.Password == "" {
|
||||
if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
|
||||
if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
|
||||
s.renderAddUpdateSharePage(w, r, share,
|
||||
util.NewI18nError(util.NewValidationError("You are not allowed to share files/folders without password"), util.I18nErrorShareNoPwd),
|
||||
true)
|
||||
|
@ -1508,7 +1523,7 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -1518,7 +1533,7 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
|
|||
updatedShare.Password = share.Password
|
||||
}
|
||||
if updatedShare.Password == "" {
|
||||
if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
|
||||
if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
|
||||
s.renderAddUpdateSharePage(w, r, updatedShare,
|
||||
util.NewI18nError(util.NewValidationError("You are not allowed to share files/folders without password"), util.I18nErrorShareNoPwd),
|
||||
false)
|
||||
|
@ -1579,7 +1594,7 @@ func (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Reque
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := clientSharesPage{
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nSharesTitle, webClientSharesPath, r),
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nSharesTitle, webClientSharesPath, w, r),
|
||||
BasePublicSharesURL: webClientPubSharesPath,
|
||||
}
|
||||
renderClientTemplate(w, templateClientShares, data)
|
||||
|
@ -1603,7 +1618,7 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
|
@ -1662,12 +1677,12 @@ func (s *httpdServer) handleWebClientMFA(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
func (s *httpdServer) handleWebClientTwoFactor(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
s.renderClientTwoFactorPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderClientTwoFactorPage(w, r, nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebClientTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
s.renderClientTwoFactorRecoveryPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderClientTwoFactorRecoveryPage(w, r, nil)
|
||||
}
|
||||
|
||||
func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
|
||||
|
@ -1719,26 +1734,25 @@ func (s *httpdServer) handleWebClientForgotPwd(w http.ResponseWriter, r *http.Re
|
|||
s.renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
|
||||
return
|
||||
}
|
||||
s.renderClientForgotPwdPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderClientForgotPwdPage(w, r, nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
s.renderClientForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderClientForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
if err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
username := strings.TrimSpace(r.Form.Get("username"))
|
||||
err = handleForgotPassword(r, username, false)
|
||||
if err != nil {
|
||||
s.renderClientForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorPwdResetGeneric), ipAddr)
|
||||
s.renderClientForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorPwdResetGeneric))
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webClientResetPwdPath, http.StatusFound)
|
||||
|
@ -1750,7 +1764,7 @@ func (s *httpdServer) handleWebClientPasswordReset(w http.ResponseWriter, r *htt
|
|||
s.renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
|
||||
return
|
||||
}
|
||||
s.renderClientResetPwdPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderClientResetPwdPage(w, r, nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -1765,7 +1779,7 @@ func (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request
|
|||
commonBasePage: getCommonBasePage(r),
|
||||
Title: path.Base(name),
|
||||
URL: fmt.Sprintf("%s?path=%s&_=%d", webClientGetPDFPath, url.QueryEscape(name), time.Now().UTC().Unix()),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateClientViewPDF, data)
|
||||
}
|
||||
|
@ -1853,43 +1867,46 @@ func (s *httpdServer) ensurePDF(w http.ResponseWriter, r *http.Request, name str
|
|||
|
||||
func (s *httpdServer) handleClientShareLoginGet(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
s.renderShareLoginPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
s.renderShareLoginPage(w, r, nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
|
||||
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
|
||||
if err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
return
|
||||
}
|
||||
invalidateToken(r, true)
|
||||
shareID := getURLParam(r, "id")
|
||||
share, err := dataprovider.ShareExists(shareID, "")
|
||||
if err != nil {
|
||||
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials), ipAddr)
|
||||
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
match, err := share.CheckCredentials(strings.TrimSpace(r.Form.Get("share_password")))
|
||||
if !match || err != nil {
|
||||
s.renderShareLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
|
||||
ipAddr)
|
||||
return
|
||||
}
|
||||
c := jwtTokenClaims{
|
||||
Username: shareID,
|
||||
}
|
||||
err = c.createAndSetCookie(w, r, s.tokenAuth, tokenAudienceWebShare, ipAddr)
|
||||
if err != nil {
|
||||
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nError500Message), ipAddr)
|
||||
s.renderShareLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
|
||||
return
|
||||
}
|
||||
next := path.Clean(r.URL.Query().Get("next"))
|
||||
baseShareURL := path.Join(webClientPubSharesPath, share.ShareID)
|
||||
isRedirect, redirectTo := checkShareRedirectURL(next, baseShareURL)
|
||||
c := jwtTokenClaims{
|
||||
Username: shareID,
|
||||
}
|
||||
if isRedirect {
|
||||
c.Ref = next
|
||||
}
|
||||
err = c.createAndSetCookie(w, r, s.tokenAuth, tokenAudienceWebShare, ipAddr)
|
||||
if err != nil {
|
||||
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nError500Message))
|
||||
return
|
||||
}
|
||||
if isRedirect {
|
||||
http.Redirect(w, r, redirectTo, http.StatusFound)
|
||||
return
|
||||
|
@ -1897,6 +1914,22 @@ func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http.
|
|||
s.renderClientMessagePage(w, r, util.I18nSharedFilesTitle, http.StatusOK, nil, util.I18nShareLoginOK)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleClientShareLogout(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
|
||||
shareID := getURLParam(r, "id")
|
||||
claims, err := s.getShareClaims(r, shareID)
|
||||
if err != nil {
|
||||
s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, http.StatusForbidden,
|
||||
util.NewI18nError(err, util.I18nErrorInvalidToken), "")
|
||||
return
|
||||
}
|
||||
removeCookie(w, r, webBaseClientPath)
|
||||
|
||||
redirectURL := path.Join(webClientPubSharesPath, shareID, fmt.Sprintf("login?next=%s", url.QueryEscape(claims.Ref)))
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleClientSharedFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead}
|
||||
|
@ -1908,7 +1941,7 @@ func (s *httpdServer) handleClientSharedFile(w http.ResponseWriter, r *http.Requ
|
|||
if r.URL.RawQuery != "" {
|
||||
query = "?" + r.URL.RawQuery
|
||||
}
|
||||
s.renderShareDownloadPage(w, r, path.Join(webClientPubSharesPath, share.ShareID)+query)
|
||||
s.renderShareDownloadPage(w, r, &share, path.Join(webClientPubSharesPath, share.ShareID)+query)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleClientCheckExist(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -1983,7 +2016,7 @@ func doCheckExist(w http.ResponseWriter, r *http.Request, connection *Connection
|
|||
}
|
||||
existing := make([]map[string]any, 0)
|
||||
for _, info := range contents {
|
||||
if util.Contains(filesList.Files, info.Name()) {
|
||||
if slices.Contains(filesList.Files, info.Name()) {
|
||||
res := make(map[string]any)
|
||||
res["name"] = info.Name()
|
||||
if info.IsDir() {
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -36,7 +37,6 @@ import (
|
|||
"github.com/drakkan/sftpgo/v2/internal/httpclient"
|
||||
"github.com/drakkan/sftpgo/v2/internal/httpd"
|
||||
"github.com/drakkan/sftpgo/v2/internal/kms"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
"github.com/drakkan/sftpgo/v2/internal/version"
|
||||
"github.com/drakkan/sftpgo/v2/internal/vfs"
|
||||
)
|
||||
|
@ -1679,7 +1679,7 @@ func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions)
|
|||
return errors.New("condition protocols mismatch")
|
||||
}
|
||||
for _, v := range expected.Protocols {
|
||||
if !util.Contains(actual.Protocols, v) {
|
||||
if !slices.Contains(actual.Protocols, v) {
|
||||
return errors.New("condition protocols content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -1687,7 +1687,7 @@ func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions)
|
|||
return errors.New("condition provider objects mismatch")
|
||||
}
|
||||
for _, v := range expected.ProviderObjects {
|
||||
if !util.Contains(actual.ProviderObjects, v) {
|
||||
if !slices.Contains(actual.ProviderObjects, v) {
|
||||
return errors.New("condition provider objects content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -1705,7 +1705,7 @@ func checkEventConditions(expected, actual dataprovider.EventConditions) error {
|
|||
return errors.New("fs events mismatch")
|
||||
}
|
||||
for _, v := range expected.FsEvents {
|
||||
if !util.Contains(actual.FsEvents, v) {
|
||||
if !slices.Contains(actual.FsEvents, v) {
|
||||
return errors.New("fs events content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -1713,7 +1713,7 @@ func checkEventConditions(expected, actual dataprovider.EventConditions) error {
|
|||
return errors.New("provider events mismatch")
|
||||
}
|
||||
for _, v := range expected.ProviderEvents {
|
||||
if !util.Contains(actual.ProviderEvents, v) {
|
||||
if !slices.Contains(actual.ProviderEvents, v) {
|
||||
return errors.New("provider events content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -1948,7 +1948,7 @@ func checkAdmin(expected, actual *dataprovider.Admin) error {
|
|||
return errors.New("permissions mismatch")
|
||||
}
|
||||
for _, p := range expected.Permissions {
|
||||
if !util.Contains(actual.Permissions, p) {
|
||||
if !slices.Contains(actual.Permissions, p) {
|
||||
return errors.New("permissions content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -1966,7 +1966,7 @@ func compareAdminFilters(expected, actual dataprovider.AdminFilters) error {
|
|||
return errors.New("allow list mismatch")
|
||||
}
|
||||
for _, v := range expected.AllowList {
|
||||
if !util.Contains(actual.AllowList, v) {
|
||||
if !slices.Contains(actual.AllowList, v) {
|
||||
return errors.New("allow list content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2057,7 +2057,7 @@ func compareUserPermissions(expected map[string][]string, actual map[string][]st
|
|||
for dir, perms := range expected {
|
||||
if actualPerms, ok := actual[dir]; ok {
|
||||
for _, v := range actualPerms {
|
||||
if !util.Contains(perms, v) {
|
||||
if !slices.Contains(perms, v) {
|
||||
return errors.New("permissions contents mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2310,7 +2310,7 @@ func compareSFTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error
|
|||
return errors.New("SFTPFs fingerprints mismatch")
|
||||
}
|
||||
for _, value := range actual.SFTPConfig.Fingerprints {
|
||||
if !util.Contains(expected.SFTPConfig.Fingerprints, value) {
|
||||
if !slices.Contains(expected.SFTPConfig.Fingerprints, value) {
|
||||
return errors.New("SFTPFs fingerprints mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2401,27 +2401,27 @@ func checkEncryptedSecret(expected, actual *kms.Secret) error {
|
|||
|
||||
func compareUserFilterSubStructs(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
||||
for _, IPMask := range expected.AllowedIP {
|
||||
if !util.Contains(actual.AllowedIP, IPMask) {
|
||||
if !slices.Contains(actual.AllowedIP, IPMask) {
|
||||
return errors.New("allowed IP contents mismatch")
|
||||
}
|
||||
}
|
||||
for _, IPMask := range expected.DeniedIP {
|
||||
if !util.Contains(actual.DeniedIP, IPMask) {
|
||||
if !slices.Contains(actual.DeniedIP, IPMask) {
|
||||
return errors.New("denied IP contents mismatch")
|
||||
}
|
||||
}
|
||||
for _, method := range expected.DeniedLoginMethods {
|
||||
if !util.Contains(actual.DeniedLoginMethods, method) {
|
||||
if !slices.Contains(actual.DeniedLoginMethods, method) {
|
||||
return errors.New("denied login methods contents mismatch")
|
||||
}
|
||||
}
|
||||
for _, protocol := range expected.DeniedProtocols {
|
||||
if !util.Contains(actual.DeniedProtocols, protocol) {
|
||||
if !slices.Contains(actual.DeniedProtocols, protocol) {
|
||||
return errors.New("denied protocols contents mismatch")
|
||||
}
|
||||
}
|
||||
for _, options := range expected.WebClient {
|
||||
if !util.Contains(actual.WebClient, options) {
|
||||
if !slices.Contains(actual.WebClient, options) {
|
||||
return errors.New("web client options contents mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2430,7 +2430,7 @@ func compareUserFilterSubStructs(expected sdk.BaseUserFilters, actual sdk.BaseUs
|
|||
return errors.New("TLS certs mismatch")
|
||||
}
|
||||
for _, cert := range expected.TLSCerts {
|
||||
if !util.Contains(actual.TLSCerts, cert) {
|
||||
if !slices.Contains(actual.TLSCerts, cert) {
|
||||
return errors.New("TLS certs content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2527,7 +2527,7 @@ func checkFilterMatch(expected []string, actual []string) bool {
|
|||
return false
|
||||
}
|
||||
for _, e := range expected {
|
||||
if !util.Contains(actual, strings.ToLower(e)) {
|
||||
if !slices.Contains(actual, strings.ToLower(e)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -2570,7 +2570,7 @@ func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.B
|
|||
return errors.New("bandwidth filters sources mismatch")
|
||||
}
|
||||
for _, source := range actual.BandwidthLimits[idx].Sources {
|
||||
if !util.Contains(l.Sources, source) {
|
||||
if !slices.Contains(l.Sources, source) {
|
||||
return errors.New("bandwidth filters source mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2680,7 +2680,7 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
|
|||
return errors.New("email recipients mismatch")
|
||||
}
|
||||
for _, v := range expected.Recipients {
|
||||
if !util.Contains(actual.Recipients, v) {
|
||||
if !slices.Contains(actual.Recipients, v) {
|
||||
return errors.New("email recipients content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2688,7 +2688,7 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
|
|||
return errors.New("email bcc mismatch")
|
||||
}
|
||||
for _, v := range expected.Bcc {
|
||||
if !util.Contains(actual.Bcc, v) {
|
||||
if !slices.Contains(actual.Bcc, v) {
|
||||
return errors.New("email bcc content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2705,7 +2705,7 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
|
|||
return errors.New("email attachments mismatch")
|
||||
}
|
||||
for _, v := range expected.Attachments {
|
||||
if !util.Contains(actual.Attachments, v) {
|
||||
if !slices.Contains(actual.Attachments, v) {
|
||||
return errors.New("email attachments content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2720,7 +2720,7 @@ func compareEventActionFsCompressFields(expected, actual dataprovider.EventActio
|
|||
return errors.New("fs compress paths mismatch")
|
||||
}
|
||||
for _, v := range expected.Paths {
|
||||
if !util.Contains(actual.Paths, v) {
|
||||
if !slices.Contains(actual.Paths, v) {
|
||||
return errors.New("fs compress paths content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2741,7 +2741,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
|
|||
return errors.New("fs deletes mismatch")
|
||||
}
|
||||
for _, v := range expected.Deletes {
|
||||
if !util.Contains(actual.Deletes, v) {
|
||||
if !slices.Contains(actual.Deletes, v) {
|
||||
return errors.New("fs deletes content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2749,7 +2749,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
|
|||
return errors.New("fs mkdirs mismatch")
|
||||
}
|
||||
for _, v := range expected.MkDirs {
|
||||
if !util.Contains(actual.MkDirs, v) {
|
||||
if !slices.Contains(actual.MkDirs, v) {
|
||||
return errors.New("fs mkdir content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2757,7 +2757,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
|
|||
return errors.New("fs exist mismatch")
|
||||
}
|
||||
for _, v := range expected.Exist {
|
||||
if !util.Contains(actual.Exist, v) {
|
||||
if !slices.Contains(actual.Exist, v) {
|
||||
return errors.New("fs exist content mismatch")
|
||||
}
|
||||
}
|
||||
|
@ -2788,7 +2788,7 @@ func compareEventActionCmdConfigFields(expected, actual dataprovider.EventAction
|
|||
return errors.New("cmd args mismatch")
|
||||
}
|
||||
for _, v := range expected.Args {
|
||||
if !util.Contains(actual.Args, v) {
|
||||
if !slices.Contains(actual.Args, v) {
|
||||
return errors.New("cmd args content mismatch")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@ type HCLogAdapter struct {
|
|||
|
||||
// Log emits a message and key/value pairs at a provided log level
|
||||
func (l *HCLogAdapter) Log(level hclog.Level, msg string, args ...any) {
|
||||
// Workaround to avoid logging plugin arguments that may contain sensitive data.
|
||||
// Check everytime we update go-plugin library.
|
||||
if msg == "starting plugin" {
|
||||
return
|
||||
}
|
||||
var ev *zerolog.Event
|
||||
switch level {
|
||||
case hclog.Info:
|
||||
|
|
|
@ -203,7 +203,7 @@ func ErrorToConsole(format string, v ...any) {
|
|||
|
||||
// TransferLog logs uploads or downloads
|
||||
func TransferLog(operation, path string, elapsed int64, size int64, user, connectionID, protocol, localAddr,
|
||||
remoteAddr, ftpMode string,
|
||||
remoteAddr, ftpMode string, err error,
|
||||
) {
|
||||
ev := logger.Info().
|
||||
Timestamp().
|
||||
|
@ -219,7 +219,7 @@ func TransferLog(operation, path string, elapsed int64, size int64, user, connec
|
|||
if ftpMode != "" {
|
||||
ev.Str("ftp_mode", ftpMode)
|
||||
}
|
||||
ev.Send()
|
||||
ev.AnErr("error", err).Send()
|
||||
}
|
||||
|
||||
// CommandLog logs an SFTP/SCP/SSH command
|
||||
|
|
|
@ -17,6 +17,7 @@ package plugin
|
|||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
|
@ -25,7 +26,6 @@ import (
|
|||
|
||||
"github.com/drakkan/sftpgo/v2/internal/kms"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -41,10 +41,10 @@ type KMSConfig struct {
|
|||
}
|
||||
|
||||
func (c *KMSConfig) validate() error {
|
||||
if !util.Contains(validKMSSchemes, c.Scheme) {
|
||||
if !slices.Contains(validKMSSchemes, c.Scheme) {
|
||||
return fmt.Errorf("invalid kms scheme: %v", c.Scheme)
|
||||
}
|
||||
if !util.Contains(validKMSEncryptedStatuses, c.EncryptedStatus) {
|
||||
if !slices.Contains(validKMSEncryptedStatuses, c.EncryptedStatus) {
|
||||
return fmt.Errorf("invalid kms encrypted status: %v", c.EncryptedStatus)
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -16,6 +16,7 @@ package plugin
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -24,7 +25,6 @@ import (
|
|||
"github.com/sftpgo/sdk/plugin/notifier"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
// NotifierConfig defines configuration parameters for notifiers plugins
|
||||
|
@ -220,7 +220,7 @@ func (p *notifierPlugin) canQueueEvent(timestamp int64) bool {
|
|||
}
|
||||
|
||||
func (p *notifierPlugin) notifyFsAction(event *notifier.FsEvent) {
|
||||
if !util.Contains(p.config.NotifierOptions.FsEvents, event.Action) {
|
||||
if !slices.Contains(p.config.NotifierOptions.FsEvents, event.Action) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -233,8 +233,8 @@ func (p *notifierPlugin) notifyFsAction(event *notifier.FsEvent) {
|
|||
}
|
||||
|
||||
func (p *notifierPlugin) notifyProviderAction(event *notifier.ProviderEvent, object Renderer) {
|
||||
if !util.Contains(p.config.NotifierOptions.ProviderEvents, event.Action) ||
|
||||
!util.Contains(p.config.NotifierOptions.ProviderObjects, event.ObjectType) {
|
||||
if !slices.Contains(p.config.NotifierOptions.ProviderEvents, event.Action) ||
|
||||
!slices.Contains(p.config.NotifierOptions.ProviderObjects, event.ObjectType) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
@ -118,7 +119,9 @@ func (c *Config) getEnvVarPrefix() string {
|
|||
return c.EnvPrefix
|
||||
}
|
||||
|
||||
prefix := strings.ToUpper(filepath.Base(c.Cmd)) + "_"
|
||||
baseName := filepath.Base(c.Cmd)
|
||||
name := strings.TrimSuffix(baseName, filepath.Ext(baseName))
|
||||
prefix := strings.ToUpper(name) + "_"
|
||||
return strings.ReplaceAll(prefix, "-", "_")
|
||||
}
|
||||
|
||||
|
@ -334,7 +337,7 @@ func (m *Manager) NotifyLogEvent(event notifier.LogEventType, protocol, username
|
|||
var e *notifier.LogEvent
|
||||
|
||||
for _, n := range m.notifiers {
|
||||
if util.Contains(n.config.NotifierOptions.LogEvents, int(event)) {
|
||||
if slices.Contains(n.config.NotifierOptions.LogEvents, int(event)) {
|
||||
if e == nil {
|
||||
message := ""
|
||||
if err != nil {
|
||||
|
|
|
@ -20,6 +20,7 @@ package service
|
|||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/sftpgo/sdk"
|
||||
|
@ -211,7 +212,7 @@ func configurePortableSFTPService(port int, enabledSSHCommands []string) {
|
|||
} else {
|
||||
sftpdConf.Bindings[0].Port = 0
|
||||
}
|
||||
if util.Contains(enabledSSHCommands, "*") {
|
||||
if slices.Contains(enabledSSHCommands, "*") {
|
||||
sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands()
|
||||
} else {
|
||||
sftpdConf.EnabledSSHCommands = enabledSSHCommands
|
||||
|
|
|
@ -41,7 +41,6 @@ type Connection struct {
|
|||
LocalAddr net.Addr
|
||||
channel io.ReadWriteCloser
|
||||
command string
|
||||
folderPrefix string
|
||||
}
|
||||
|
||||
// GetClientVersion returns the connected client's version
|
||||
|
@ -221,10 +220,10 @@ func (c *Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
|||
return nil, err
|
||||
}
|
||||
modTime := time.Unix(0, 0)
|
||||
if request.Filepath != "/" || c.folderPrefix != "" {
|
||||
lister.Add(vfs.NewFileInfo("..", true, 0, modTime, false))
|
||||
if request.Filepath != "/" {
|
||||
lister.Prepend(vfs.NewFileInfo("..", true, 0, modTime, false))
|
||||
}
|
||||
lister.Add(vfs.NewFileInfo(".", true, 0, modTime, false))
|
||||
lister.Prepend(vfs.NewFileInfo(".", true, 0, modTime, false))
|
||||
return lister, nil
|
||||
case "Stat":
|
||||
if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(request.Filepath)) {
|
||||
|
@ -560,14 +559,11 @@ func (c *Connection) getStatVFSFromQuotaResult(fs vfs.Fs, name string, quotaResu
|
|||
func (c *Connection) updateQuotaAfterTruncate(requestPath string, fileSize int64) {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -fileSize, false)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
func getOSOpenFlags(requestFlags sftp.FileOpenFlags) (flags int) {
|
||||
var osFlags int
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -418,7 +419,7 @@ func TestSupportedSSHCommands(t *testing.T) {
|
|||
assert.Equal(t, len(supportedSSHCommands), len(cmds))
|
||||
|
||||
for _, c := range cmds {
|
||||
assert.True(t, util.Contains(supportedSSHCommands, c))
|
||||
assert.True(t, slices.Contains(supportedSSHCommands, c))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -842,7 +843,7 @@ func TestRsyncOptions(t *testing.T) {
|
|||
}
|
||||
cmd, err := sshCmd.getSystemCommand()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, util.Contains(cmd.cmd.Args, "--safe-links"),
|
||||
assert.True(t, slices.Contains(cmd.cmd.Args, "--safe-links"),
|
||||
"--safe-links must be added if the user has the create symlinks permission")
|
||||
|
||||
permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs,
|
||||
|
@ -859,7 +860,7 @@ func TestRsyncOptions(t *testing.T) {
|
|||
}
|
||||
cmd, err = sshCmd.getSystemCommand()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, util.Contains(cmd.cmd.Args, "--munge-links"),
|
||||
assert.True(t, slices.Contains(cmd.cmd.Args, "--munge-links"),
|
||||
"--munge-links must be added if the user has the create symlinks permission")
|
||||
|
||||
sshCmd.connection.User.VirtualFolders = append(sshCmd.connection.User.VirtualFolders, vfs.VirtualFolder{
|
||||
|
|
|
@ -258,10 +258,7 @@ func (c *scpCommand) handleUploadFile(fs vfs.Fs, resolvedPath, filePath string,
|
|||
if vfs.HasTruncateSupport(fs) {
|
||||
vfolder, err := c.connection.User.GetVirtualFolderForPath(path.Dir(requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.connection.User, 0, -fileSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &c.connection.User, 0, -fileSize, false)
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&c.connection.User, 0, -fileSize, false) //nolint:errcheck
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -128,6 +129,10 @@ type Configuration struct {
|
|||
// KexAlgorithms specifies the available KEX (Key Exchange) algorithms in
|
||||
// preference order.
|
||||
KexAlgorithms []string `json:"kex_algorithms" mapstructure:"kex_algorithms"`
|
||||
// MinDHGroupExchangeKeySize defines the minimum key size to allow for the
|
||||
// key exchanges when using diffie-ellman-group-exchange-sha1 or sha256 key
|
||||
// exchange algorithms.
|
||||
MinDHGroupExchangeKeySize int `json:"min_dh_group_exchange_key_size" mapstructure:"min_dh_group_exchange_key_size"`
|
||||
// Ciphers specifies the ciphers allowed
|
||||
Ciphers []string `json:"ciphers" mapstructure:"ciphers"`
|
||||
// MACs Specifies the available MAC (message authentication code) algorithms
|
||||
|
@ -259,13 +264,13 @@ func (c *Configuration) getServerConfig() *ssh.ServerConfig {
|
|||
func (c *Configuration) updateSupportedAuthentications() {
|
||||
serviceStatus.Authentications = util.RemoveDuplicates(serviceStatus.Authentications, false)
|
||||
|
||||
if util.Contains(serviceStatus.Authentications, dataprovider.LoginMethodPassword) &&
|
||||
util.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
|
||||
if slices.Contains(serviceStatus.Authentications, dataprovider.LoginMethodPassword) &&
|
||||
slices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
|
||||
serviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyAndPassword)
|
||||
}
|
||||
|
||||
if util.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyboardInteractive) &&
|
||||
util.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
|
||||
if slices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyboardInteractive) &&
|
||||
slices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
|
||||
serviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyAndKeyboardInt)
|
||||
}
|
||||
}
|
||||
|
@ -321,8 +326,11 @@ func (c *Configuration) Initialize(configDir string) error {
|
|||
return common.ErrNoBinding
|
||||
}
|
||||
|
||||
ssh.SetDHKexServerMinBits(uint32(c.MinDHGroupExchangeKeySize))
|
||||
logger.Debug(logSender, "", "minimum key size allowed for diffie-ellman-group-exchange: %d",
|
||||
ssh.GetDHKexServerMinBits())
|
||||
sftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck // we configure valid SFTP Extensions so we cannot get an error
|
||||
sftp.MaxFilelist = vfs.ListerBatchSize
|
||||
sftp.MaxFilelist = 250
|
||||
|
||||
if err := c.configureSecurityOptions(serverConfig); err != nil {
|
||||
return err
|
||||
|
@ -415,7 +423,7 @@ func (c *Configuration) configureKeyAlgos(serverConfig *ssh.ServerConfig) error
|
|||
c.HostKeyAlgorithms = util.RemoveDuplicates(c.HostKeyAlgorithms, true)
|
||||
}
|
||||
for _, hostKeyAlgo := range c.HostKeyAlgorithms {
|
||||
if !util.Contains(supportedHostKeyAlgos, hostKeyAlgo) {
|
||||
if !slices.Contains(supportedHostKeyAlgos, hostKeyAlgo) {
|
||||
return fmt.Errorf("unsupported host key algorithm %q", hostKeyAlgo)
|
||||
}
|
||||
}
|
||||
|
@ -423,7 +431,7 @@ func (c *Configuration) configureKeyAlgos(serverConfig *ssh.ServerConfig) error
|
|||
if len(c.PublicKeyAlgorithms) > 0 {
|
||||
c.PublicKeyAlgorithms = util.RemoveDuplicates(c.PublicKeyAlgorithms, true)
|
||||
for _, algo := range c.PublicKeyAlgorithms {
|
||||
if !util.Contains(supportedPublicKeyAlgos, algo) {
|
||||
if !slices.Contains(supportedPublicKeyAlgos, algo) {
|
||||
return fmt.Errorf("unsupported public key authentication algorithm %q", algo)
|
||||
}
|
||||
}
|
||||
|
@ -465,7 +473,7 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
|
|||
if kex == keyExchangeCurve25519SHA256LibSSH {
|
||||
continue
|
||||
}
|
||||
if !util.Contains(supportedKexAlgos, kex) {
|
||||
if !slices.Contains(supportedKexAlgos, kex) {
|
||||
return fmt.Errorf("unsupported key-exchange algorithm %q", kex)
|
||||
}
|
||||
}
|
||||
|
@ -479,7 +487,7 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
|
|||
if len(c.Ciphers) > 0 {
|
||||
c.Ciphers = util.RemoveDuplicates(c.Ciphers, true)
|
||||
for _, cipher := range c.Ciphers {
|
||||
if !util.Contains(supportedCiphers, cipher) {
|
||||
if !slices.Contains(supportedCiphers, cipher) {
|
||||
return fmt.Errorf("unsupported cipher %q", cipher)
|
||||
}
|
||||
}
|
||||
|
@ -492,7 +500,7 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
|
|||
if len(c.MACs) > 0 {
|
||||
c.MACs = util.RemoveDuplicates(c.MACs, true)
|
||||
for _, mac := range c.MACs {
|
||||
if !util.Contains(supportedMACs, mac) {
|
||||
if !slices.Contains(supportedMACs, mac) {
|
||||
return fmt.Errorf("unsupported MAC algorithm %q", mac)
|
||||
}
|
||||
}
|
||||
|
@ -778,7 +786,7 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
|
|||
user.Username, user.HomeDir)
|
||||
return nil, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
|
||||
}
|
||||
if util.Contains(user.Filters.DeniedProtocols, common.ProtocolSSH) {
|
||||
if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolSSH) {
|
||||
logger.Info(logSender, connectionID, "cannot login user %q, protocol SSH is not allowed", user.Username)
|
||||
return nil, fmt.Errorf("protocol SSH is not allowed for user %q", user.Username)
|
||||
}
|
||||
|
@ -823,14 +831,14 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
|
|||
}
|
||||
|
||||
func (c *Configuration) checkSSHCommands() {
|
||||
if util.Contains(c.EnabledSSHCommands, "*") {
|
||||
if slices.Contains(c.EnabledSSHCommands, "*") {
|
||||
c.EnabledSSHCommands = GetSupportedSSHCommands()
|
||||
return
|
||||
}
|
||||
sshCommands := []string{}
|
||||
for _, command := range c.EnabledSSHCommands {
|
||||
command = strings.TrimSpace(command)
|
||||
if util.Contains(supportedSSHCommands, command) {
|
||||
if slices.Contains(supportedSSHCommands, command) {
|
||||
sshCommands = append(sshCommands, command)
|
||||
} else {
|
||||
logger.Warn(logSender, "", "unsupported ssh command: %q ignored", command)
|
||||
|
@ -920,7 +928,7 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
|
|||
func (c *Configuration) getHostKeyAlgorithms(keyFormat string) []string {
|
||||
var algos []string
|
||||
for _, algo := range algorithmsForKeyFormat(keyFormat) {
|
||||
if util.Contains(c.HostKeyAlgorithms, algo) {
|
||||
if slices.Contains(c.HostKeyAlgorithms, algo) {
|
||||
algos = append(algos, algo)
|
||||
}
|
||||
}
|
||||
|
@ -979,7 +987,7 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
|
|||
var algos []string
|
||||
for _, algo := range algorithmsForKeyFormat(signer.PublicKey().Type()) {
|
||||
if underlyingAlgo, ok := certKeyAlgoNames[algo]; ok {
|
||||
if util.Contains(mas.Algorithms(), underlyingAlgo) {
|
||||
if slices.Contains(mas.Algorithms(), underlyingAlgo) {
|
||||
algos = append(algos, algo)
|
||||
}
|
||||
}
|
||||
|
@ -1091,12 +1099,12 @@ func (c *Configuration) initializeCertChecker(configDir string) error {
|
|||
|
||||
func (c *Configuration) getPartialSuccessError(nextAuthMethods []string) error {
|
||||
err := &ssh.PartialSuccessError{}
|
||||
if c.PasswordAuthentication && util.Contains(nextAuthMethods, dataprovider.LoginMethodPassword) {
|
||||
if c.PasswordAuthentication && slices.Contains(nextAuthMethods, dataprovider.LoginMethodPassword) {
|
||||
err.Next.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
return c.validatePasswordCredentials(conn, password, dataprovider.SSHLoginMethodKeyAndPassword)
|
||||
}
|
||||
}
|
||||
if c.KeyboardInteractiveAuthentication && util.Contains(nextAuthMethods, dataprovider.SSHLoginMethodKeyboardInteractive) {
|
||||
if c.KeyboardInteractiveAuthentication && slices.Contains(nextAuthMethods, dataprovider.SSHLoginMethodKeyboardInteractive) {
|
||||
err.Next.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
|
||||
return c.validateKeyboardInteractiveCredentials(conn, client, dataprovider.SSHLoginMethodKeyAndKeyboardInt, true)
|
||||
}
|
||||
|
@ -1216,6 +1224,7 @@ func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) {
|
|||
metric.AddLoginAttempt(method)
|
||||
if err == nil {
|
||||
plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolSSH, user.Username, ip, "", err)
|
||||
common.DelayLogin(nil)
|
||||
} else {
|
||||
logger.ConnectionFailedLog(user.Username, ip, method, common.ProtocolSSH, err.Error())
|
||||
if method != dataprovider.SSHLoginMethodPublicKey {
|
||||
|
@ -1230,6 +1239,9 @@ func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) {
|
|||
}
|
||||
common.AddDefenderEvent(ip, common.ProtocolSSH, event)
|
||||
plugin.Handler.NotifyLogEvent(logEv, common.ProtocolSSH, user.Username, ip, "", err)
|
||||
if method != dataprovider.SSHLoginMethodPublicKey {
|
||||
common.DelayLogin(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
metric.AddLoginResult(method, err)
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -37,6 +38,7 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -782,6 +784,34 @@ func TestSFTPFsEscapeHomeDir(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestReadDirLongNames(t *testing.T) {
|
||||
usePubKey := true
|
||||
user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
conn, client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
numFiles := 1000
|
||||
for i := 0; i < 1000; i++ {
|
||||
fPath := filepath.Join(user.GetHomeDir(), hex.EncodeToString(util.GenerateRandomBytes(127)))
|
||||
err = os.WriteFile(fPath, util.GenerateRandomBytes(30), 0666)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
entries, err := client.ReadDir("/")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, entries, numFiles)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGroupSettingsOverride(t *testing.T) {
|
||||
usePubKey := true
|
||||
g := getTestGroup()
|
||||
|
@ -1197,9 +1227,9 @@ func TestProxyProtocol(t *testing.T) {
|
|||
assert.NoError(t, checkBasicSFTP(client))
|
||||
}
|
||||
conn, client, err = getSftpClientWithAddr(user, usePubKey, "127.0.0.1:2224")
|
||||
if !assert.Error(t, err) {
|
||||
client.Close()
|
||||
conn.Close()
|
||||
if assert.NoError(t, err) {
|
||||
defer client.Close()
|
||||
defer conn.Close()
|
||||
}
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -5496,13 +5526,13 @@ func TestNestedVirtualFolders(t *testing.T) {
|
|||
|
||||
folderGet, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(18769), folderGet.UsedQuotaSize)
|
||||
assert.Equal(t, 1, folderGet.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), folderGet.UsedQuotaSize)
|
||||
assert.Equal(t, 0, folderGet.UsedQuotaFiles)
|
||||
|
||||
folderGet, _, err = httpdtest.GetFolderByName(folderNameNested, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(27658), folderGet.UsedQuotaSize)
|
||||
assert.Equal(t, 1, folderGet.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), folderGet.UsedQuotaSize)
|
||||
assert.Equal(t, 0, folderGet.UsedQuotaFiles)
|
||||
|
||||
files, err := client.ReadDir("/")
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -6169,8 +6199,8 @@ func TestVirtualFoldersQuotaValues(t *testing.T) {
|
|||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
|
@ -6289,8 +6319,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -6314,8 +6344,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
// rename a file inside vdir2, it isn't included inside user quota, so we have:
|
||||
// - vdir1/dir1/testFileName.rename
|
||||
// - vdir1/dir2/testFileName1
|
||||
|
@ -6333,8 +6363,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
// rename a file inside vdir2 overwriting an existing, we now have:
|
||||
// - vdir1/dir1/testFileName.rename
|
||||
// - vdir1/dir2/testFileName1
|
||||
|
@ -6351,8 +6381,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
// rename a file inside vdir1 overwriting an existing, we now have:
|
||||
// - vdir1/dir1/testFileName.rename (initial testFileName1)
|
||||
// - vdir2/dir1/testFileName.rename (initial testFileName1)
|
||||
|
@ -6364,8 +6394,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -6385,8 +6415,8 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -6507,8 +6537,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1+testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -6526,8 +6556,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize*2, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize*2, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1*2, f.UsedQuotaSize)
|
||||
|
@ -6544,8 +6574,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1+testFileSize, f.UsedQuotaSize)
|
||||
|
@ -6561,8 +6591,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
|
@ -6592,8 +6622,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize1*3+testFileSize*2, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1*3+testFileSize*2, f.UsedQuotaSize)
|
||||
assert.Equal(t, 5, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
|
@ -6607,8 +6637,8 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize1*2+testFileSize, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1*2+testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 3, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -6729,8 +6759,8 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -6748,8 +6778,8 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
|
@ -6812,8 +6842,8 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize*3+testFileSize1*3, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
|
@ -6946,8 +6976,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
// rename a file from user home dir to vdir2, vdir2 is not included in user quota so we have:
|
||||
// - /vdir2/dir1/testFileName
|
||||
// - /vdir1/dir1/testFileName1
|
||||
|
@ -6986,8 +7016,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
|
@ -7003,8 +7033,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -7026,8 +7056,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -7044,8 +7074,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize*2+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 3, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -7070,8 +7100,8 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
|
|||
assert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize*2+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 3, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize1*2+testFileSize, f.UsedQuotaSize)
|
||||
|
@ -7339,8 +7369,8 @@ func TestVFolderQuotaSize(t *testing.T) {
|
|||
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, 1, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, f.UsedQuotaSize)
|
||||
|
@ -8610,8 +8640,8 @@ func TestUserAllowedLoginMethods(t *testing.T) {
|
|||
allowedMethods = user.GetAllowedLoginMethods()
|
||||
assert.Equal(t, 4, len(allowedMethods))
|
||||
|
||||
assert.True(t, util.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndKeyboardInt))
|
||||
assert.True(t, util.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndPassword))
|
||||
assert.True(t, slices.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndKeyboardInt))
|
||||
assert.True(t, slices.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndPassword))
|
||||
}
|
||||
|
||||
func TestUserPartialAuth(t *testing.T) {
|
||||
|
@ -9118,8 +9148,8 @@ func TestSSHCopy(t *testing.T) {
|
|||
assert.Equal(t, 2*testFileSize+2*testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
|
@ -9197,8 +9227,8 @@ func TestSSHCopy(t *testing.T) {
|
|||
assert.Equal(t, 5*testFileSize+4*testFileSize1, user.UsedQuotaSize)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2*testFileSize+2*testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 4, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
}
|
||||
// cross folder copy
|
||||
newDir := "newdir"
|
||||
|
@ -9894,8 +9924,8 @@ func TestGitIncludedVirtualFolders(t *testing.T) {
|
|||
|
||||
folder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.UsedQuotaFiles, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, user.UsedQuotaSize, folder.UsedQuotaSize)
|
||||
assert.Equal(t, 0, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), folder.UsedQuotaSize)
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -10680,8 +10710,8 @@ func TestSCPVirtualFoldersQuota(t *testing.T) {
|
|||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
f, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaSize, f.UsedQuotaSize)
|
||||
assert.Equal(t, expectedQuotaFiles, f.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), f.UsedQuotaSize)
|
||||
assert.Equal(t, 0, f.UsedQuotaFiles)
|
||||
f, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaSize, f.UsedQuotaSize)
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"os/exec"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -91,7 +92,7 @@ func processSSHCommand(payload []byte, connection *Connection, enabledSSHCommand
|
|||
name, args, err := parseCommandPayload(msg.Command)
|
||||
connection.Log(logger.LevelDebug, "new ssh command: %q args: %v num args: %d user: %s, error: %v",
|
||||
name, args, len(args), connection.User.Username, err)
|
||||
if err == nil && util.Contains(enabledSSHCommands, name) {
|
||||
if err == nil && slices.Contains(enabledSSHCommands, name) {
|
||||
connection.command = msg.Command
|
||||
if name == scpCmdName && len(args) >= 2 {
|
||||
connection.SetProtocol(common.ProtocolSCP)
|
||||
|
@ -139,9 +140,9 @@ func (c *sshCommand) handle() (err error) {
|
|||
defer common.Connections.Remove(c.connection.GetID())
|
||||
|
||||
c.connection.UpdateLastActivity()
|
||||
if util.Contains(sshHashCommands, c.command) {
|
||||
if slices.Contains(sshHashCommands, c.command) {
|
||||
return c.handleHashCommands()
|
||||
} else if util.Contains(systemCommands, c.command) {
|
||||
} else if slices.Contains(systemCommands, c.command) {
|
||||
command, err := c.getSystemCommand()
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
|
@ -192,14 +193,11 @@ func (c *sshCommand) handleSFTPGoRemove() error {
|
|||
func (c *sshCommand) updateQuota(sshDestPath string, filesNum int, filesSize int64) {
|
||||
vfolder, err := c.connection.User.GetVirtualFolderForPath(sshDestPath)
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, filesNum, filesSize, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.connection.User, filesNum, filesSize, false) //nolint:errcheck
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &c.connection.User, filesNum, filesSize, false)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&c.connection.User, filesNum, filesSize, false) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sshCommand) handleHashCommands() error {
|
||||
var h hash.Hash
|
||||
|
@ -432,11 +430,11 @@ func (c *sshCommand) getSystemCommand() (systemCommand, error) {
|
|||
// If the user cannot create symlinks we add the option --munge-links, if it is not
|
||||
// already set. This should make symlinks unusable (but manually recoverable)
|
||||
if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, c.getDestPath()) {
|
||||
if !util.Contains(args, "--safe-links") {
|
||||
if !slices.Contains(args, "--safe-links") {
|
||||
args = append([]string{"--safe-links"}, args...)
|
||||
}
|
||||
} else {
|
||||
if !util.Contains(args, "--munge-links") {
|
||||
if !slices.Contains(args, "--munge-links") {
|
||||
args = append([]string{"--munge-links"}, args...)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -27,7 +28,6 @@ import (
|
|||
"golang.org/x/oauth2/microsoft"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
// Supported OAuth2 providers
|
||||
|
@ -56,7 +56,7 @@ type OAuth2Config struct {
|
|||
|
||||
// Validate validates and initializes the configuration
|
||||
func (c *OAuth2Config) Validate() error {
|
||||
if !util.Contains(supportedOAuth2Providers, c.Provider) {
|
||||
if !slices.Contains(supportedOAuth2Providers, c.Provider) {
|
||||
return fmt.Errorf("smtp oauth2: unsupported provider %d", c.Provider)
|
||||
}
|
||||
if c.ClientID == "" {
|
||||
|
|
|
@ -279,15 +279,15 @@ func (c *Config) Initialize(configDir string, isService bool) error {
|
|||
}
|
||||
|
||||
func (c *Config) getMailClientOptions() []mail.Option {
|
||||
options := []mail.Option{mail.WithoutNoop()}
|
||||
options := []mail.Option{mail.WithPort(c.Port), mail.WithoutNoop()}
|
||||
|
||||
switch c.Encryption {
|
||||
case 1:
|
||||
options = append(options, mail.WithSSLPort(false))
|
||||
options = append(options, mail.WithSSL())
|
||||
case 2:
|
||||
options = append(options, mail.WithTLSPortPolicy(mail.TLSMandatory))
|
||||
options = append(options, mail.WithTLSPolicy(mail.TLSMandatory))
|
||||
default:
|
||||
options = append(options, mail.WithTLSPortPolicy(mail.NoTLS))
|
||||
options = append(options, mail.WithTLSPolicy(mail.NoTLS))
|
||||
}
|
||||
if c.User != "" {
|
||||
options = append(options, mail.WithUsername(c.User))
|
||||
|
@ -317,14 +317,13 @@ func (c *Config) getMailClientOptions() []mail.Option {
|
|||
}),
|
||||
mail.WithDebugLog())
|
||||
}
|
||||
options = append(options, mail.WithPort(c.Port))
|
||||
return options
|
||||
}
|
||||
|
||||
func (c *Config) getSMTPClientAndMsg(to, bcc []string, subject, body string, contentType EmailContentType,
|
||||
attachments ...*mail.File) (*mail.Client, *mail.Msg, error) {
|
||||
msg := mail.NewMsg()
|
||||
msg.SetUserAgent(version.GetServerVersion(" ", true))
|
||||
msg.SetUserAgent(version.GetServerVersion(" ", false))
|
||||
|
||||
var from string
|
||||
if c.From != "" {
|
||||
|
@ -416,12 +415,6 @@ func SendEmail(to, bcc []string, subject, body string, contentType EmailContentT
|
|||
return config.sendEmail(to, bcc, subject, body, contentType, attachments...)
|
||||
}
|
||||
|
||||
// ReloadProviderConf reloads the configuration from the provider
|
||||
// and apply it if different from the active one
|
||||
func ReloadProviderConf() {
|
||||
loadConfigFromProvider() //nolint:errcheck
|
||||
}
|
||||
|
||||
func loadConfigFromProvider() error {
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
if err != nil {
|
||||
|
|
|
@ -274,6 +274,7 @@ const (
|
|||
I18nActionTypeUserInactivityCheck = "actions.types.user_inactivity_check"
|
||||
I18nActionTypeIDPCheck = "actions.types.idp_check"
|
||||
I18nActionTypeCommand = "actions.types.command"
|
||||
I18nActionTypeRotateLogs = "actions.types.rotate_logs"
|
||||
I18nActionFsTypeRename = "actions.fs_types.rename"
|
||||
I18nActionFsTypeDelete = "actions.fs_types.delete"
|
||||
I18nActionFsTypePathExists = "actions.fs_types.path_exists"
|
||||
|
@ -302,6 +303,9 @@ const (
|
|||
I18nErrorEvSyncUnsupportedFs = "rules.sync_unsupported_fs_event"
|
||||
I18nErrorRuleFailureActionsOnly = "rules.only_failure_actions"
|
||||
I18nErrorRuleSyncActionRequired = "rules.sync_action_required"
|
||||
I18nErrorInvalidPNG = "branding.invalid_png"
|
||||
I18nErrorInvalidPNGSize = "branding.invalid_png_size"
|
||||
I18nErrorInvalidDisclaimerURL = "branding.invalid_disclaimer_url"
|
||||
)
|
||||
|
||||
// NewI18nError returns a I18nError wrappring the provided error
|
||||
|
|
|
@ -128,16 +128,6 @@ var bytesSizeTable = map[string]uint64{
|
|||
"e": eByte,
|
||||
}
|
||||
|
||||
// Contains reports whether v is present in elems.
|
||||
func Contains[T comparable](elems []T, v T) bool {
|
||||
for _, s := range elems {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove removes an element from a string slice and
|
||||
// returns the modified slice
|
||||
func Remove(elems []string, val string) []string {
|
||||
|
@ -380,7 +370,7 @@ func GenerateRSAKeys(file string) error {
|
|||
if err := createDirPathIfMissing(file, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
key, err := rsa.GenerateKey(rand.Reader, 3072)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -809,15 +799,6 @@ func GetRedactedURL(rawurl string) string {
|
|||
return u.Redacted()
|
||||
}
|
||||
|
||||
// PrependFileInfo prepends a file info to a slice in an efficient way.
|
||||
// We, optimistically, assume that the slice has enough capacity
|
||||
func PrependFileInfo(files []os.FileInfo, info os.FileInfo) []os.FileInfo {
|
||||
files = append(files, nil)
|
||||
copy(files[1:], files)
|
||||
files[0] = info
|
||||
return files
|
||||
}
|
||||
|
||||
// GetTLSVersion returns the TLS version for integer:
|
||||
// - 12 means TLS 1.2
|
||||
// - 13 means TLS 1.3
|
||||
|
|
|
@ -18,7 +18,7 @@ package version
|
|||
import "strings"
|
||||
|
||||
const (
|
||||
version = "2.6.0"
|
||||
version = "2.6.99-dev"
|
||||
appName = "SFTPGo"
|
||||
)
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -34,7 +35,6 @@ import (
|
|||
"github.com/sftpgo/sdk"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -475,7 +475,7 @@ func (fs *OsFs) findNonexistentDirs(filePath string) ([]string, error) {
|
|||
for fs.IsNotExist(err) {
|
||||
results = append(results, parent)
|
||||
parent = filepath.Dir(parent)
|
||||
if util.Contains(results, parent) {
|
||||
if slices.Contains(results, parent) {
|
||||
break
|
||||
}
|
||||
_, err = os.Stat(parent)
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -161,7 +162,7 @@ func (fs *S3Fs) Stat(name string) (os.FileInfo, error) {
|
|||
if err == nil {
|
||||
// Some S3 providers (like SeaweedFS) remove the trailing '/' from object keys.
|
||||
// So we check some common content types to detect if this is a "directory".
|
||||
isDir := util.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
|
||||
isDir := slices.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
|
||||
if util.GetIntFromPointer(obj.ContentLength) == 0 && !isDir {
|
||||
_, err = fs.headObject(name + "/")
|
||||
isDir = err == nil
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -69,6 +70,17 @@ type SFTPFsConfig struct {
|
|||
forbiddenSelfUsernames []string `json:"-"`
|
||||
}
|
||||
|
||||
func (c *SFTPFsConfig) getKeySigner() (ssh.Signer, error) {
|
||||
privPayload := c.PrivateKey.GetPayload()
|
||||
if privPayload == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if key := c.KeyPassphrase.GetPayload(); key != "" {
|
||||
return ssh.ParsePrivateKeyWithPassphrase([]byte(privPayload), []byte(key))
|
||||
}
|
||||
return ssh.ParsePrivateKey([]byte(privPayload))
|
||||
}
|
||||
|
||||
// HideConfidentialData hides confidential data
|
||||
func (c *SFTPFsConfig) HideConfidentialData() {
|
||||
if c.Password != nil {
|
||||
|
@ -114,7 +126,7 @@ func (c *SFTPFsConfig) isEqual(other SFTPFsConfig) bool {
|
|||
return false
|
||||
}
|
||||
for _, fp := range c.Fingerprints {
|
||||
if !util.Contains(other.Fingerprints, fp) {
|
||||
if !slices.Contains(other.Fingerprints, fp) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -185,17 +197,11 @@ func (c *SFTPFsConfig) validate() error {
|
|||
|
||||
func (c *SFTPFsConfig) validatePrivateKey() error {
|
||||
if c.PrivateKey.IsPlain() {
|
||||
var signer ssh.Signer
|
||||
var err error
|
||||
if c.KeyPassphrase.IsPlain() {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(c.PrivateKey.GetPayload()),
|
||||
[]byte(c.KeyPassphrase.GetPayload()))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey([]byte(c.PrivateKey.GetPayload()))
|
||||
}
|
||||
signer, err := c.getKeySigner()
|
||||
if err != nil {
|
||||
return util.NewI18nError(fmt.Errorf("invalid private key: %w", err), util.I18nErrorPrivKeyInvalid)
|
||||
}
|
||||
if signer != nil {
|
||||
if key, ok := signer.PublicKey().(ssh.CryptoPublicKey); ok {
|
||||
cryptoKey := key.CryptoPublicKey()
|
||||
if rsaKey, ok := cryptoKey.(*rsa.PublicKey); ok {
|
||||
|
@ -208,6 +214,7 @@ func (c *SFTPFsConfig) validatePrivateKey() error {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -331,15 +338,19 @@ func NewSFTPFs(connectionID, mountPath, localTempDir string, forbiddenSelfUserna
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
conn, err := sftpConnsCache.Get(&config, connectionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.forbiddenSelfUsernames = forbiddenSelfUsernames
|
||||
sftpFs := &SFTPFs{
|
||||
connectionID: connectionID,
|
||||
mountPath: getMountPath(mountPath),
|
||||
localTempDir: localTempDir,
|
||||
config: &config,
|
||||
conn: sftpConnsCache.Get(&config, connectionID),
|
||||
conn: conn,
|
||||
}
|
||||
err := sftpFs.createConnection()
|
||||
err = sftpFs.createConnection()
|
||||
if err != nil {
|
||||
sftpFs.Close() //nolint:errcheck
|
||||
}
|
||||
|
@ -910,6 +921,7 @@ type sftpConnection struct {
|
|||
isConnected bool
|
||||
sessions map[string]bool
|
||||
lastActivity time.Time
|
||||
signer ssh.Signer
|
||||
}
|
||||
|
||||
func newSFTPConnection(config *SFTPFsConfig, sessionID string) *sftpConnection {
|
||||
|
@ -919,6 +931,7 @@ func newSFTPConnection(config *SFTPFsConfig, sessionID string) *sftpConnection {
|
|||
isConnected: false,
|
||||
sessions: map[string]bool{},
|
||||
lastActivity: time.Now().UTC(),
|
||||
signer: nil,
|
||||
}
|
||||
c.sessions[sessionID] = true
|
||||
return c
|
||||
|
@ -931,17 +944,6 @@ func (c *sftpConnection) OpenConnection() error {
|
|||
return c.openConnNoLock()
|
||||
}
|
||||
|
||||
func (c *sftpConnection) getKeySigner() (ssh.Signer, error) {
|
||||
privPayload := c.config.PrivateKey.GetPayload()
|
||||
if privPayload == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if key := c.config.KeyPassphrase.GetPayload(); key != "" {
|
||||
return ssh.ParsePrivateKeyWithPassphrase([]byte(privPayload), []byte(key))
|
||||
}
|
||||
return ssh.ParsePrivateKey([]byte(privPayload))
|
||||
}
|
||||
|
||||
func (c *sftpConnection) openConnNoLock() error {
|
||||
if c.isConnected {
|
||||
logger.Debug(c.logSender, "", "reusing connection")
|
||||
|
@ -953,12 +955,12 @@ func (c *sftpConnection) openConnNoLock() error {
|
|||
User: c.config.Username,
|
||||
HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
|
||||
fp := ssh.FingerprintSHA256(key)
|
||||
if util.Contains(sftpFingerprints, fp) {
|
||||
if slices.Contains(sftpFingerprints, fp) {
|
||||
if allowSelfConnections == 0 {
|
||||
logger.Log(logger.LevelError, c.logSender, "", "SFTP self connections not allowed")
|
||||
return ErrSFTPLoop
|
||||
}
|
||||
if util.Contains(c.config.forbiddenSelfUsernames, c.config.Username) {
|
||||
if slices.Contains(c.config.forbiddenSelfUsernames, c.config.Username) {
|
||||
logger.Log(logger.LevelError, c.logSender, "",
|
||||
"SFTP loop or nested local SFTP folders detected, username %q, forbidden usernames: %+v",
|
||||
c.config.Username, c.config.forbiddenSelfUsernames)
|
||||
|
@ -979,12 +981,8 @@ func (c *sftpConnection) openConnNoLock() error {
|
|||
Timeout: 15 * time.Second,
|
||||
ClientVersion: fmt.Sprintf("SSH-2.0-%s", version.GetServerVersion("_", false)),
|
||||
}
|
||||
signer, err := c.getKeySigner()
|
||||
if err != nil {
|
||||
return fmt.Errorf("sftpfs: unable to parse the private key: %w", err)
|
||||
}
|
||||
if signer != nil {
|
||||
clientConfig.Auth = append(clientConfig.Auth, ssh.PublicKeys(signer))
|
||||
if c.signer != nil {
|
||||
clientConfig.Auth = append(clientConfig.Auth, ssh.PublicKeys(c.signer))
|
||||
}
|
||||
if pwd := c.config.Password.GetPayload(); pwd != "" {
|
||||
clientConfig.Auth = append(clientConfig.Auth, ssh.Password(pwd))
|
||||
|
@ -1156,7 +1154,7 @@ func newSFTPConnectionCache() *sftpConnectionsCache {
|
|||
return c
|
||||
}
|
||||
|
||||
func (c *sftpConnectionsCache) Get(config *SFTPFsConfig, sessionID string) *sftpConnection {
|
||||
func (c *sftpConnectionsCache) Get(config *SFTPFsConfig, sessionID string) (*sftpConnection, error) {
|
||||
partition := 0
|
||||
key := config.getUniqueID(partition)
|
||||
|
||||
|
@ -1172,7 +1170,7 @@ func (c *sftpConnectionsCache) Get(config *SFTPFsConfig, sessionID string) *sftp
|
|||
"reusing connection for session ID %q, key: %d, active sessions %d, active connections: %d",
|
||||
sessionID, key, activeSessions+1, len(c.items))
|
||||
val.AddSession(sessionID)
|
||||
return val
|
||||
return val, nil
|
||||
}
|
||||
partition++
|
||||
oldKey = key
|
||||
|
@ -1182,11 +1180,16 @@ func (c *sftpConnectionsCache) Get(config *SFTPFsConfig, sessionID string) *sftp
|
|||
partition, activeSessions, oldKey, key)
|
||||
} else {
|
||||
conn := newSFTPConnection(config, sessionID)
|
||||
signer, err := config.getKeySigner()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sftpfs: unable to parse the private key: %w", err)
|
||||
}
|
||||
conn.signer = signer
|
||||
c.items[key] = conn
|
||||
logger.Debug(logSenderSFTPCache, "",
|
||||
"adding new connection for session ID %q, partition: %d, key: %d, active connections: %d",
|
||||
sessionID, partition, key, len(c.items))
|
||||
return conn
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -764,7 +765,7 @@ func (c *AzBlobFsConfig) validate() error {
|
|||
if err := c.checkPartSizeAndConcurrency(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(validAzAccessTier, c.AccessTier) {
|
||||
if !slices.Contains(validAzAccessTier, c.AccessTier) {
|
||||
return fmt.Errorf("invalid access tier %q, valid values: \"''%v\"", c.AccessTier, strings.Join(validAzAccessTier, ", "))
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
|
@ -447,7 +448,7 @@ func (f *webDavFile) Patch(patches []webdav.Proppatch) ([]webdav.Propstat, error
|
|||
pstat := webdav.Propstat{}
|
||||
for _, p := range patch.Props {
|
||||
if status == http.StatusForbidden && !hasError {
|
||||
if !patch.Remove && util.Contains(lastModifiedProps, p.XMLName.Local) {
|
||||
if !patch.Remove && slices.Contains(lastModifiedProps, p.XMLName.Local) {
|
||||
parsed, err := parseTime(util.BytesToString(p.InnerXML))
|
||||
if err != nil {
|
||||
f.Connection.Log(logger.LevelWarn, "unsupported last modification time: %q, err: %v",
|
||||
|
|
|
@ -272,10 +272,7 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat
|
|||
if vfs.HasTruncateSupport(fs) {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -fileSize, false)
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -346,7 +347,7 @@ func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request, lo
|
|||
user.Username, user.HomeDir)
|
||||
return connID, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
|
||||
}
|
||||
if util.Contains(user.Filters.DeniedProtocols, common.ProtocolWebDAV) {
|
||||
if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolWebDAV) {
|
||||
logger.Info(logSender, connectionID, "cannot login user %q, protocol DAV is not allowed", user.Username)
|
||||
return connID, fmt.Errorf("protocol DAV is not allowed for user %q", user.Username)
|
||||
}
|
||||
|
@ -426,6 +427,7 @@ func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err err
|
|||
metric.AddLoginAttempt(loginMethod)
|
||||
if err == nil {
|
||||
plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolWebDAV, user.Username, ip, "", nil)
|
||||
common.DelayLogin(nil)
|
||||
} else if err != common.ErrInternalFailure && err != common.ErrNoCredentials {
|
||||
logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())
|
||||
event := common.HostEventLoginFailed
|
||||
|
@ -436,6 +438,9 @@ func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err err
|
|||
}
|
||||
common.AddDefenderEvent(ip, common.ProtocolWebDAV, event)
|
||||
plugin.Handler.NotifyLogEvent(logEv, common.ProtocolWebDAV, user.Username, ip, "", err)
|
||||
if loginMethod != dataprovider.LoginMethodTLSCertificate {
|
||||
common.DelayLogin(err)
|
||||
}
|
||||
}
|
||||
metric.AddLoginResult(loginMethod, err)
|
||||
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolWebDAV, err)
|
||||
|
|
|
@ -667,6 +667,8 @@ func TestBasicHandlingCryptFs(t *testing.T) {
|
|||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = downloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
|
@ -2537,6 +2539,7 @@ func TestStat(t *testing.T) {
|
|||
|
||||
func TestUploadOverwriteVfolder(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.QuotaFiles = 1000
|
||||
vdir := "/vdir"
|
||||
mappedPath := filepath.Join(os.TempDir(), "mappedDir")
|
||||
folderName := filepath.Base(mappedPath)
|
||||
|
@ -2583,15 +2586,25 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
folder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, folder.UsedQuotaSize)
|
||||
assert.Equal(t, 1, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), folder.UsedQuotaSize)
|
||||
assert.Equal(t, 0, folder.UsedQuotaFiles)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, user.UsedQuotaSize)
|
||||
assert.Equal(t, 1, user.UsedQuotaFiles)
|
||||
|
||||
err = uploadFileWithRawClient(testFilePath, path.Join(vdir, testFileName), user.Username,
|
||||
defaultPassword, true, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
folder, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, folder.UsedQuotaSize)
|
||||
assert.Equal(t, 1, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), folder.UsedQuotaSize)
|
||||
assert.Equal(t, 0, folder.UsedQuotaFiles)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testFileSize, user.UsedQuotaSize)
|
||||
assert.Equal(t, 1, user.UsedQuotaFiles)
|
||||
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
|
|
|
@ -29,7 +29,7 @@ info:
|
|||
SFTPGo supports groups to simplify the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
|
||||
The SFTPGo WebClient allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Authy, Google Authenticator and other compatible apps.
|
||||
From the WebClient each authorized user can also create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.
|
||||
version: 2.6.0
|
||||
version: 2.6.99-dev
|
||||
contact:
|
||||
name: API support
|
||||
url: 'https://github.com/drakkan/sftpgo'
|
||||
|
@ -5008,6 +5008,7 @@ components:
|
|||
- 12
|
||||
- 13
|
||||
- 14
|
||||
- 15
|
||||
description: |
|
||||
Supported event action types:
|
||||
* `1` - HTTP
|
||||
|
@ -5023,6 +5024,7 @@ components:
|
|||
* `12` - User expiration check
|
||||
* `13` - Identity Provider account check
|
||||
* `14` - User inactivity check
|
||||
* `15` - Rotate log file
|
||||
FilesystemActionTypes:
|
||||
type: integer
|
||||
enum:
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
NFPM_VERSION=2.37.1
|
||||
NFPM_VERSION=2.38.0
|
||||
NFPM_ARCH=${NFPM_ARCH:-amd64}
|
||||
if [ -z ${SFTPGO_VERSION} ]
|
||||
then
|
||||
|
|
|
@ -3,17 +3,17 @@
|
|||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>sftpgo</id>
|
||||
<version>2.5.6</version>
|
||||
<version>2.6.2</version>
|
||||
<packageSourceUrl>https://github.com/drakkan/sftpgo/tree/main/pkgs/choco</packageSourceUrl>
|
||||
<owners>asheroto</owners>
|
||||
<title>SFTPGo</title>
|
||||
<authors>Nicola Murino</authors>
|
||||
<projectUrl>https://github.com/drakkan/sftpgo</projectUrl>
|
||||
<iconUrl>https://cdn.statically.io/gh/drakkan/sftpgo/v2.5.6/static/img/logo.png</iconUrl>
|
||||
<iconUrl>https://cdn.statically.io/gh/drakkan/sftpgo/v2.6.2/static/img/logo.png</iconUrl>
|
||||
<licenseUrl>https://github.com/drakkan/sftpgo/blob/main/LICENSE</licenseUrl>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<projectSourceUrl>https://github.com/drakkan/sftpgo</projectSourceUrl>
|
||||
<docsUrl>https://github.com/drakkan/sftpgo/tree/v2.5.6/docs</docsUrl>
|
||||
<docsUrl>https://sftpgo.github.io/2.6/</docsUrl>
|
||||
<bugTrackerUrl>https://github.com/drakkan/sftpgo/issues</bugTrackerUrl>
|
||||
<tags>sftp sftp-server ftp webdav s3 azure-blob google-cloud-storage cloud-storage scp data-at-rest-encryption multi-factor-authentication multi-step-authentication</tags>
|
||||
<summary>Full-featured and highly configurable file transfer server: SFTP, HTTP/S,FTP/S, WebDAV.</summary>
|
||||
|
@ -26,13 +26,13 @@ SFTPGo allows to create HTTP/S links to externally share files and folders secur
|
|||
|
||||
SFTPGo is highly customizable and extensible to suit your needs.
|
||||
|
||||
You can find more info [here](https://github.com/drakkan/sftpgo).
|
||||
You can find more info [here](https://sftpgo.github.io/2.6/).
|
||||
|
||||
### Notes
|
||||
|
||||
* This package installs SFTPGo as Windows Service.
|
||||
* After the first installation please take a look at the [Getting Started Guide](https://sftpgo.github.io/latest/initial-configuration/).</description>
|
||||
<releaseNotes>https://github.com/drakkan/sftpgo/releases/tag/v2.5.6</releaseNotes>
|
||||
* After the first installation please take a look at the [Getting Started Guide](https://sftpgo.github.io/2.6/initial-configuration/).</description>
|
||||
<releaseNotes>https://github.com/drakkan/sftpgo/releases/tag/v2.6.2</releaseNotes>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="**" exclude="**\*.md;**\icon.png;**\icon.jpg;**\icon.svg" />
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
$ErrorActionPreference = 'Stop'
|
||||
$packageName = 'sftpgo'
|
||||
$softwareName = 'SFTPGo'
|
||||
$url = 'https://github.com/drakkan/sftpgo/releases/download/v2.5.6/sftpgo_v2.5.6_windows_x86_64.exe'
|
||||
$checksum = 'C20BB051D3EA2ACBF05231ECB94410D01648E15DE304CA6240D37FDC34007DBE'
|
||||
$url = 'https://github.com/drakkan/sftpgo/releases/download/v2.6.2/sftpgo_v2.6.2_windows_x86_64.exe'
|
||||
$checksum = '40905AED44A7189C5DF6164631DB07BCBB53FAB7A63503A6BE7AD1328D9986D5'
|
||||
$silentArgs = '/VERYSILENT'
|
||||
$validExitCodes = @(0)
|
||||
|
||||
|
@ -48,5 +48,7 @@ Write-Output "General information (README) location:"
|
|||
Write-Output "`thttps://github.com/drakkan/sftpgo"
|
||||
Write-Output "Documentation location:"
|
||||
Write-Output "`thttps://sftpgo.github.io/"
|
||||
Write-Output "Commercial support:"
|
||||
Write-Output "`thttps://sftpgo.com/#pricing"
|
||||
Write-Output ""
|
||||
Write-Output "---------------------------"
|
|
@ -1,3 +1,21 @@
|
|||
sftpgo (2.6.2-1ppa1) bionic; urgency=medium
|
||||
|
||||
* New upstream release
|
||||
|
||||
-- Nicola Murino <nicola.murino@gmail.com> Fri, 21 Jun 2024 19:47:58 +0200
|
||||
|
||||
sftpgo (2.6.1-1ppa1) bionic; urgency=medium
|
||||
|
||||
* New upstream release
|
||||
|
||||
-- Nicola Murino <nicola.murino@gmail.com> Wed, 19 Jun 2024 10:17:49 +0200
|
||||
|
||||
sftpgo (2.6.0-1ppa1) bionic; urgency=medium
|
||||
|
||||
* New upstream release
|
||||
|
||||
-- Nicola Murino <nicola.murino@gmail.com> Wed, 15 May 2024 19:12:01 +0200
|
||||
|
||||
sftpgo (2.5.6-1ppa1) bionic; urgency=medium
|
||||
|
||||
* New upstream release
|
||||
|
|
|
@ -3,7 +3,7 @@ Upstream-Name: SFTPGo
|
|||
Source: https://github.com/drakkan/sftpgo
|
||||
|
||||
Files: *
|
||||
Copyright: 2019-2023 Nicola Murino <nicola.murino@gmail.com>
|
||||
Copyright: 2019 Nicola Murino <nicola.murino@gmail.com>
|
||||
License: AGPL-3
|
||||
|
||||
License: AGPL-3
|
||||
|
|
|
@ -2,7 +2,7 @@ Index: sftpgo/sftpgo.json
|
|||
===================================================================
|
||||
--- sftpgo.orig/sftpgo.json
|
||||
+++ sftpgo/sftpgo.json
|
||||
@@ -57,7 +57,7 @@
|
||||
@@ -67,7 +67,7 @@
|
||||
"domains": [],
|
||||
"email": "",
|
||||
"key_type": "4096",
|
||||
|
@ -11,7 +11,7 @@ Index: sftpgo/sftpgo.json
|
|||
"ca_endpoint": "https://acme-v02.api.letsencrypt.org/directory",
|
||||
"renew_days": 30,
|
||||
"http01_challenge": {
|
||||
@@ -186,7 +186,7 @@
|
||||
@@ -198,7 +198,7 @@
|
||||
},
|
||||
"data_provider": {
|
||||
"driver": "sqlite",
|
||||
|
@ -20,7 +20,7 @@ Index: sftpgo/sftpgo.json
|
|||
"host": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
@@ -202,7 +202,7 @@
|
||||
@@ -214,7 +214,7 @@
|
||||
"track_quota": 2,
|
||||
"delayed_quota_update": 0,
|
||||
"pool_size": 0,
|
||||
|
@ -29,7 +29,7 @@ Index: sftpgo/sftpgo.json
|
|||
"actions": {
|
||||
"execute_on": [],
|
||||
"execute_for": [],
|
||||
@@ -244,7 +244,7 @@
|
||||
@@ -256,7 +256,7 @@
|
||||
"port": 0,
|
||||
"proto": "http"
|
||||
},
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue