Compare commits

...

51 commits
main ... 2.5.x

Author SHA1 Message Date
Nicola Murino
501a870e9d
docker: download plugins compatible with SFTPGo 2.5.x
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-15 10:37:44 +02:00
Nicola Murino
25a498a153
improve error wrapping
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-14 20:45:25 +02:00
Nicola Murino
76fddc126d
switch to Go 1.21 for CI and docker images
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-14 20:36:57 +02:00
Nicola Murino
b3ce596385
update certificates in test cases
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-10 17:38:19 +02:00
Nicola Murino
a73c6569f9
fix the error message for errors that occur during file transfers
we should special case path errors and replace the fs path with the
virtual path.

Thanks to @nezzzumi for reporting this issue

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-10 16:04:44 +02:00
Nicola Murino
a6a92f0d69
set version to 2.5.6
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-12-18 18:35:18 +01:00
Nicola Murino
a3d6d9cd33
portable mode: fix panic while validating TLS certificates
Fixes #1480

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-12-12 18:19:56 +01:00
Nicola Murino
8812e5e450
S3: fix compatibility with newer SDK versions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-12-12 18:16:53 +01:00
Nicola Murino
a132a21a38
keyboard interactive auth: respect hook disabled setting
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-11-18 11:26:37 +01:00
Nicola Murino
5d9cda9d34
CI: set Go version to 1.20.11
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-11-10 18:49:03 +01:00
Nicola Murino
14d79e052c
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-11-10 18:12:46 +01:00
Nicola Murino
b81f819b3e
httpd: fixed logging of refused requests due to rate limiting/blocklisting
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-11-08 19:25:45 +01:00
Nicola Murino
5c1c7e4fa3
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-11-01 10:55:51 +01:00
Nicola Murino
ebec3042e9
loaddata: do not reveal the existence of the files in error messages
return a generic error message

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-11-01 10:52:32 +01:00
Nicola Murino
50cae4ee7d
httpd: add database based token manager
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-10-31 19:56:59 +01:00
Nicola Murino
a4009c8894
events page: fix dismissable alert
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-10-29 08:18:06 +01:00
Nicola Murino
c50d2c15e8
httpd request logger: set log level based on the status code
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-10-28 10:39:09 +02:00
CUI Hao
cd953e6794
webadmin: fix typo on webpages (#1438)
Signed-off-by: CUI Hao <cuihao.leo@gmail.com>
2023-10-23 09:58:12 +02:00
Nicola Murino
f5d64a1a8a
docker: upgrade also build environment before build
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-10-13 12:51:25 +02:00
Nicola Murino
9a9d16292a
docker: upgrade packages
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-10-12 22:04:58 +02:00
Nicola Murino
1c579d73f8
suppress lint warning
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-10-10 20:54:10 +02:00
Nicola Murino
904ad2f691
sshd: skip host keys with invalid algorithms
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-10-10 19:59:22 +02:00
Nicola Murino
bc6bdb2f05
backports from main
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-10-10 19:22:52 +02:00
Nicola Murino
d9ac1a5631
WebClient: fix icon for 0 byte files
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-10-04 19:39:46 +02:00
Nicola Murino
f37b57884f
editfiles: fix label
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-09-17 17:38:20 +02:00
Nicola Murino
d6e31ce8e2
web UIs: fix dismissable alerts
alerts can now be shown again after the user dismissal

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-09-17 17:31:40 +02:00
Nicola Murino
cf1cc25a48
SQL providers: make sure we don't exceed the allowed placeholders
Fixes #1415

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-09-12 19:16:54 +02:00
Nicola Murino
9906caefd5
httpd: disable directory index for static files
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-09-08 19:56:20 +02:00
Nicola Murino
bef0e10d1e
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-09-08 19:19:25 +02:00
Nicola Murino
e8df1b6e4c
validate API key scope
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-09-08 18:54:57 +02:00
Nicola Murino
991739d47a
WebUIs: update the css to hide the theme hard coded background image
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-09-08 18:54:52 +02:00
Nicola Murino
1508fc9253
External/plugin auth: check for password change after empty response
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-08-26 12:07:06 +02:00
Nicola Murino
520e22b63d
backports from main branch
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-08-20 17:22:03 +02:00
Nicola Murino
d6b584e064
shares: respect password strength
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-16 16:53:43 +02:00
Nicola Murino
cc381443be
set version to 2.5.4
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-14 20:35:45 +02:00
Nicola Murino
89a251d640
update pgx to the latest commit
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-09 11:23:26 +02:00
Nicola Murino
dbbae3129d
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-08 17:21:55 +02:00
Nicola Murino
c457538280
file patterns: fix denied except rules
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-08 17:09:44 +02:00
Nicola Murino
7f65aa1fa4
set version to 2.5.3-dev
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-04 19:43:00 +02:00
Nicola Murino
abac3cfc8d
revert pgx to an older version
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-04 13:41:32 +02:00
Nicola Murino
a805a930e8
set version to 2.5.3
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-29 12:21:13 +02:00
Nicola Murino
de72495092
Windows setup: add PrepareToInstall event function
so the service is stopped before the installation starts and
we avoid the force close app warning

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-29 12:15:38 +02:00
Nicola Murino
7c845f07d5
config: fix loading commands args from env vars
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-25 21:32:37 +02:00
Nicola Murino
b9ace46180
add auth plugin
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-25 18:58:13 +02:00
Nicola Murino
e446e3392d
check second factor after plugin authentication
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-25 07:18:42 +02:00
Nicola Murino
a503feaab6
set version to 2.5.2
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-17 18:33:56 +02:00
Nicola Murino
cba894987c
WebClient: show user quota
Also remove per-source data transfer limits. This was an
oversight

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-16 21:31:15 +02:00
Nicola Murino
1d120bdd26
WebAdmin: don't show hidden deny policy for allowed patterns
The deny policy only applies to denied patterns, showing an allowed
pattern as hidden will confuse users

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-14 19:01:22 +02:00
Nicola Murino
7245710b31
CI: fix MariaDB initialization
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-12 20:04:48 +02:00
Nicola Murino
3a3df5670d
WebAdmin: relax key prefix validation
try to automatically fix leading and trailing slashes

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-12 19:15:02 +02:00
Nicola Murino
97bbf37af4
branch 2.5.x
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-09 19:47:17 +02:00
88 changed files with 2513 additions and 4281 deletions

View file

@ -2,7 +2,7 @@ name: CI
on: on:
push: push:
branches: [main] branches: [2.5.x]
pull_request: pull_request:
jobs: jobs:
@ -11,11 +11,11 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go: ['1.20'] go: ['1.21']
os: [ubuntu-latest, macos-latest] os: [ubuntu-latest, macos-latest]
upload-coverage: [true] upload-coverage: [true]
include: include:
- go: '1.20' - go: '1.21'
os: windows-latest os: windows-latest
upload-coverage: false upload-coverage: false
@ -232,7 +232,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: '1.20' go-version: '1.21'
- name: Build - name: Build
run: | run: |
@ -268,7 +268,7 @@ jobs:
MYSQL_USER: sftpgo MYSQL_USER: sftpgo
MYSQL_PASSWORD: sftpgo MYSQL_PASSWORD: sftpgo
options: >- options: >-
--health-cmd "mysqladmin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD" --health-cmd "mariadb-admin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD"
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
--health-retries 6 --health-retries 6
@ -296,7 +296,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: '1.20' go-version: '1.21'
- name: Build - name: Build
run: | run: |
@ -409,7 +409,7 @@ jobs:
echo 'apt-get install -q -y curl gcc' >> build.sh echo 'apt-get install -q -y curl gcc' >> build.sh
if [ ${{ matrix.go }} == 'latest' ] if [ ${{ matrix.go }} == 'latest' ]
then then
echo 'GO_VERSION=$(curl -L https://go.dev/VERSION?m=text)' >> build.sh echo 'GO_VERSION=$(curl -L https://go.dev/VERSION?m=text | head -n 1)' >> build.sh
else else
echo 'GO_VERSION=${{ matrix.go }}' >> build.sh echo 'GO_VERSION=${{ matrix.go }}' >> build.sh
fi fi
@ -452,7 +452,7 @@ jobs:
apt-get install -q -y curl gcc apt-get install -q -y curl gcc
if [ ${{ matrix.go }} == 'latest' ] if [ ${{ matrix.go }} == 'latest' ]
then then
GO_VERSION=$(curl -L https://go.dev/VERSION?m=text) GO_VERSION=$(curl -L https://go.dev/VERSION?m=text | head -n 1)
else else
GO_VERSION=${{ matrix.go }} GO_VERSION=${{ matrix.go }}
fi fi
@ -517,9 +517,9 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: '1.20' go-version: '1.21'
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Run golangci-lint - name: Run golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
version: latest version: v1.54.2

View file

@ -5,7 +5,7 @@ on:
# - cron: '0 4 * * *' # everyday at 4:00 AM UTC # - cron: '0 4 * * *' # everyday at 4:00 AM UTC
push: push:
branches: branches:
- main - 2.5.x
tags: tags:
- v* - v*
pull_request: pull_request:

View file

@ -5,7 +5,7 @@ on:
tags: 'v*' tags: 'v*'
env: env:
GO_VERSION: 1.20.5 GO_VERSION: 1.21.10
jobs: jobs:
prepare-sources-with-deps: prepare-sources-with-deps:

View file

@ -1,7 +1,9 @@
FROM golang:1.20-bullseye as builder FROM golang:1.21-bullseye as builder
ENV GOFLAGS="-mod=readonly" ENV GOFLAGS="-mod=readonly"
RUN apt-get update && apt-get -y upgrade && apt-get install --no-install-recommends -y openssh-server && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /workspace RUN mkdir -p /workspace
WORKDIR /workspace WORKDIR /workspace
@ -28,14 +30,12 @@ ARG DOWNLOAD_PLUGINS=false
RUN if [ "${DOWNLOAD_PLUGINS}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y curl && ./docker/scripts/download-plugins.sh; fi RUN if [ "${DOWNLOAD_PLUGINS}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y curl && ./docker/scripts/download-plugins.sh; fi
RUN apt-get update && apt-get install --no-install-recommends -y openssh-server && rm -rf /var/lib/apt/lists/*
FROM debian:bullseye-slim FROM debian:bullseye-slim
# Set to "true" to install jq and the optional git and rsync dependencies # Set to "true" to install jq and the optional git and rsync dependencies
ARG INSTALL_OPTIONAL_PACKAGES=false ARG INSTALL_OPTIONAL_PACKAGES=false
RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates media-types && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get -y upgrade && apt-get install --no-install-recommends -y ca-certificates media-types && rm -rf /var/lib/apt/lists/*
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y jq git rsync && rm -rf /var/lib/apt/lists/*; fi RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y jq git rsync && rm -rf /var/lib/apt/lists/*; fi

View file

@ -1,8 +1,8 @@
FROM golang:1.20-alpine3.18 AS builder FROM golang:1.21-alpine3.18 AS builder
ENV GOFLAGS="-mod=readonly" ENV GOFLAGS="-mod=readonly"
RUN apk add --update --no-cache bash ca-certificates curl git gcc g++ RUN apk -U upgrade --no-cache && apk add --update --no-cache bash ca-certificates curl git gcc g++
RUN mkdir -p /workspace RUN mkdir -p /workspace
WORKDIR /workspace WORKDIR /workspace
@ -32,7 +32,7 @@ FROM alpine:3.18
# Set to "true" to install jq and the optional git and rsync dependencies # Set to "true" to install jq and the optional git and rsync dependencies
ARG INSTALL_OPTIONAL_PACKAGES=false ARG INSTALL_OPTIONAL_PACKAGES=false
RUN apk add --update --no-cache ca-certificates tzdata mailcap RUN apk -U upgrade --no-cache && apk add --update --no-cache ca-certificates tzdata mailcap
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq git rsync; fi RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq git rsync; fi

View file

@ -22,7 +22,7 @@ I'd like to make SFTPGo into a sustainable long term project and would not like
If you use SFTPGo, it is in your best interest to ensure that the project you rely on stays healthy and well maintained. If you use SFTPGo, it is in your best interest to ensure that the project you rely on stays healthy and well maintained.
This can only happen with your donations and [sponsorships](https://github.com/sponsors/drakkan) :heart: This can only happen with your donations and [sponsorships](https://github.com/sponsors/drakkan) :heart:
You can also purchase support plans from the [SFTPGo website](https://sftpgo.com/#pricing). You can also purchase, using many payment methods, support plans from the [SFTPGo website](https://sftpgo.com/#pricing).
With sponsorships/donations or support plans we establish a channel for reciprocal access, ensuring better outcomes for both you and the project. With sponsorships/donations or support plans we establish a channel for reciprocal access, ensuring better outcomes for both you and the project.
@ -67,6 +67,7 @@ If you report an invalid issue or ask for step-by-step support, your issue will
- Partial authentication. You can configure multi-step authentication requiring, for example, the user password after successful public key authentication. - Partial authentication. You can configure multi-step authentication requiring, for example, the user password after successful public key authentication.
- Per-user authentication methods. - Per-user authentication methods.
- [Two-factor authentication](./docs/howto/two-factor-authentication.md) based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator, Microsoft Authenticator and other compatible apps. - [Two-factor authentication](./docs/howto/two-factor-authentication.md) based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator, Microsoft Authenticator and other compatible apps.
- LDAP/Active Directory authentication using a [plugin](https://github.com/sftpgo/sftpgo-plugin-auth).
- Simplified user administrations using [groups](./docs/groups.md). - Simplified user administrations using [groups](./docs/groups.md).
- [Roles](./docs/roles.md) allow to create limited administrators who can only create and manage users with their role. - [Roles](./docs/roles.md) allow to create limited administrators who can only create and manage users with their role.
- Custom authentication via [external programs/HTTP API](./docs/external-auth.md). - Custom authentication via [external programs/HTTP API](./docs/external-auth.md).

View file

@ -4,11 +4,11 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
## Supported tags and respective Dockerfile links ## Supported tags and respective Dockerfile links
- [v2.5.1, v2.5, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile) - [v2.5.6, v2.5, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile)
- [v2.5.1-plugins, v2.5-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile) - [v2.5.6-plugins, v2.5-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile)
- [v2.5.1-alpine, v2.5-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile.alpine) - [v2.5.6-alpine, v2.5-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile.alpine)
- [v2.5.1-slim, v2.5-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile) - [v2.5.6-slim, v2.5-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile)
- [v2.5.1-alpine-slim, v2.5-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile.alpine) - [v2.5.6-alpine-slim, v2.5-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile.alpine)
- [edge](../Dockerfile) - [edge](../Dockerfile)
- [edge-plugins](../Dockerfile) - [edge-plugins](../Dockerfile)
- [edge-alpine](../Dockerfile.alpine) - [edge-alpine](../Dockerfile.alpine)

View file

@ -17,9 +17,23 @@ esac
echo "download plugins for arch ${SUFFIX}" echo "download plugins for arch ${SUFFIX}"
for PLUGIN in geoipfilter kms pubsub eventstore eventsearch metadata curl -L "https://github.com/sftpgo/sftpgo-plugin-geoipfilter/releases/download/v1.0.7/sftpgo-plugin-geoipfilter-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-geoipfilter"
do chmod 755 "/usr/local/bin/sftpgo-plugin-geoipfilter"
echo "download plugin from https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}"
curl -L "https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-${PLUGIN}" curl -L "https://github.com/sftpgo/sftpgo-plugin-kms/releases/download/v1.0.10/sftpgo-plugin-kms-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-kms"
chmod 755 "/usr/local/bin/sftpgo-plugin-${PLUGIN}" chmod 755 "/usr/local/bin/sftpgo-plugin-kms"
done
curl -L "https://github.com/sftpgo/sftpgo-plugin-pubsub/releases/download/v1.0.11/sftpgo-plugin-pubsub-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-pubsub"
chmod 755 "/usr/local/bin/sftpgo-plugin-pubsub"
curl -L "https://github.com/sftpgo/sftpgo-plugin-eventstore/releases/download/v1.0.15/sftpgo-plugin-eventstore-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-eventstore"
chmod 755 "/usr/local/bin/sftpgo-plugin-eventstore"
curl -L "https://github.com/sftpgo/sftpgo-plugin-eventsearch/releases/download/v1.0.15/sftpgo-plugin-eventsearch-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-eventsearch"
chmod 755 "/usr/local/bin/sftpgo-plugin-eventsearch"
curl -L "https://github.com/sftpgo/sftpgo-plugin-metadata/releases/download/v1.0.12/sftpgo-plugin-metadata-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-metadata"
chmod 755 "/usr/local/bin/sftpgo-plugin-metadata"
curl -L "https://github.com/sftpgo/sftpgo-plugin-auth/releases/download/v1.0.5/sftpgo-plugin-auth-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-auth"
chmod 755 "/usr/local/bin/sftpgo-plugin-auth"

View file

@ -138,9 +138,9 @@ The configuration file contains the following sections:
- `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default `SFTPGo_<version>`, for example `SSH-2.0-SFTPGo_0.9.5` - `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default `SFTPGo_<version>`, for example `SSH-2.0-SFTPGo_0.9.5`
- `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty, the daemon will search or try to generate `id_rsa`, `id_ecdsa` and `id_ed25519` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa`, `id_ecdsa` and/or `id_ed25519` then SFTPGo will try to generate these keys using the default settings. - `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty, the daemon will search or try to generate `id_rsa`, `id_ecdsa` and `id_ed25519` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa`, `id_ecdsa` and/or `id_ed25519` then SFTPGo will try to generate these keys using the default settings.
- `host_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty. - `host_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty.
- `host_key_algorithms`, list of strings. Public key algorithms that the server will accept for host key authentication. The supported values are: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ssh-rsa-cert-v01@openssh.com`, `ssh-dss-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`. Default values: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-ed25519`. - `host_key_algorithms`, list of strings. Public key algorithms that the server will accept for host key authentication. The supported values are: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ssh-rsa-cert-v01@openssh.com`, `ssh-dss-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`. Certificate algorithms are listed for backward compatibility purposes only, they are not used. Default values: `rsa-sha2-512`, `rsa-sha2-256`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `ssh-ed25519`.
- `moduli`, list of strings. Diffie-Hellman moduli files. Each moduli file can be defined as a path relative to the configuration directory or an absolute one. If set and valid, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` KEX algorithms will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you don't explicitly set KEXs. Invalid moduli file will be silently ignored. Default: empty. - `moduli`, list of strings. Diffie-Hellman moduli files. Each moduli file can be defined as a path relative to the configuration directory or an absolute one. If set and valid, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` KEX algorithms will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you don't explicitly set KEXs. Invalid moduli file will be silently ignored. Default: empty.
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group18-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. SHA512 based KEXs are disabled by default because they are slow. If you set one or more moduli files, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` will be available. - `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. SHA512 based KEXs are disabled by default because they are slow. If you set one or more moduli files, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` will be available.
- `ciphers`, list of strings. Allowed ciphers in preference order. Leave empty to use default values. The supported values are: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`, `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `3des-cbc`, `arcfour256`, `arcfour128`, `arcfour`. Default values: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`. Please note that the ciphers disabled by default are insecure, you should expect that an active attacker can recover plaintext if you enable them. - `ciphers`, list of strings. Allowed ciphers in preference order. Leave empty to use default values. The supported values are: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`, `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `3des-cbc`, `arcfour256`, `arcfour128`, `arcfour`. Default values: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`. Please note that the ciphers disabled by default are insecure, you should expect that an active attacker can recover plaintext if you enable them.
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values are: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`, `hmac-sha2-512-etm@openssh.com`, `hmac-sha2-512`, `hmac-sha1`, `hmac-sha1-96`. Default values: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`. - `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values are: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`, `hmac-sha2-512-etm@openssh.com`, `hmac-sha2-512`, `hmac-sha1`, `hmac-sha1-96`. Default values: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`.
- `trusted_user_ca_keys`, list of public keys paths of certificate authorities that are trusted to sign user certificates for authentication. The paths can be absolute or relative to the configuration directory. - `trusted_user_ca_keys`, list of public keys paths of certificate authorities that are trusted to sign user certificates for authentication. The paths can be absolute or relative to the configuration directory.

225
go.mod
View file

@ -3,168 +3,181 @@ module github.com/drakkan/sftpgo/v2
go 1.20 go 1.20
require ( require (
cloud.google.com/go/storage v1.30.1 cloud.google.com/go/storage v1.36.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736 github.com/alexedwards/argon2id v1.0.0
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
github.com/aws/aws-sdk-go-v2 v1.18.0 github.com/aws/aws-sdk-go-v2 v1.24.0
github.com/aws/aws-sdk-go-v2/config v1.18.25 github.com/aws/aws-sdk-go-v2/config v1.26.1
github.com/aws/aws-sdk-go-v2/credentials v1.13.24 github.com/aws/aws-sdk-go-v2/credentials v1.16.12
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.11 github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.5
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.19.8 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.25.5
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 github.com/aws/aws-sdk-go-v2/service/sts v1.26.5
github.com/bmatcuk/doublestar/v4 v4.6.0 github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/cockroachdb/cockroach-go/v2 v2.3.4 github.com/cockroachdb/cockroach-go/v2 v2.3.5
github.com/coreos/go-oidc/v3 v3.6.0 github.com/coreos/go-oidc/v3 v3.9.0
github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8 github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.21.0 github.com/fclairamb/ftpserverlib v0.22.0
github.com/fclairamb/go-log v0.4.1 github.com/fclairamb/go-log v0.4.1
github.com/go-acme/lego/v4 v4.12.1 github.com/go-acme/lego/v4 v4.14.2
github.com/go-chi/chi/v5 v5.0.9-0.20230502103705-7f280968675b github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/jwtauth/v5 v5.1.0 github.com/go-chi/jwtauth/v5 v5.3.0
github.com/go-chi/render v1.0.2 github.com/go-chi/render v1.0.3
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.3.0 github.com/google/uuid v1.5.0
github.com/hashicorp/go-hclog v1.5.0 github.com/hashicorp/go-hclog v1.6.2
github.com/hashicorp/go-plugin v1.4.10 github.com/hashicorp/go-plugin v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.4 github.com/hashicorp/go-retryablehttp v0.7.5
github.com/jackc/pgx/v5 v5.3.2-0.20230603125928-d9560c78b8e6 github.com/jackc/pgx/v5 v5.4.3
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.16.5 github.com/klauspost/compress v1.17.4
github.com/lestrrat-go/jwx/v2 v2.0.9 github.com/lestrrat-go/jwx/v2 v2.0.18
github.com/lithammer/shortuuid/v3 v3.0.7 github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mattn/go-sqlite3 v1.14.17 github.com/mattn/go-sqlite3 v1.14.19
github.com/mhale/smtpd v0.8.0 github.com/mhale/smtpd v0.8.1
github.com/minio/sio v0.3.1 github.com/minio/sio v0.3.1
github.com/otiai10/copy v1.11.0 github.com/otiai10/copy v1.14.0
github.com/pires/go-proxyproto v0.7.0 github.com/pires/go-proxyproto v0.7.0
github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6 github.com/pkg/sftp v1.13.6
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.15.1 github.com/prometheus/client_golang v1.17.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/rs/cors v1.9.0 github.com/rs/cors v1.10.1
github.com/rs/xid v1.5.0 github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.29.1 github.com/rs/zerolog v1.31.0
github.com/sftpgo/sdk v0.1.5-0.20230524172149-afb96ebee860 github.com/sftpgo/sdk v0.1.6
github.com/shirou/gopsutil/v3 v3.23.5 github.com/shirou/gopsutil/v3 v3.23.11
github.com/spf13/afero v1.9.5 github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.16.0 github.com/spf13/viper v1.18.1
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2 github.com/studio-b12/gowebdav v0.9.0
github.com/subosito/gotenv v1.4.2 github.com/subosito/gotenv v1.6.0
github.com/unrolled/secure v1.13.0 github.com/unrolled/secure v1.13.0
github.com/wagslane/go-password-validator v0.3.0 github.com/wagslane/go-password-validator v0.3.0
github.com/wneessen/go-mail v0.3.10-0.20230531074101-4100fef083df github.com/wneessen/go-mail v0.4.1-0.20230823094700-0bd5390e370d
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
go.etcd.io/bbolt v1.3.7 go.etcd.io/bbolt v1.3.8
go.uber.org/automaxprocs v1.5.2 go.uber.org/automaxprocs v1.5.3
gocloud.dev v0.29.0 gocloud.dev v0.35.0
golang.org/x/crypto v0.9.0 golang.org/x/crypto v0.17.0
golang.org/x/net v0.10.0 golang.org/x/net v0.19.0
golang.org/x/oauth2 v0.8.0 golang.org/x/oauth2 v0.15.0
golang.org/x/sys v0.8.0 golang.org/x/sys v0.15.0
golang.org/x/term v0.8.0 golang.org/x/term v0.15.0
golang.org/x/time v0.3.0 golang.org/x/time v0.5.0
google.golang.org/api v0.125.0 google.golang.org/api v0.154.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
require ( require (
cloud.google.com/go v0.110.2 // indirect cloud.google.com/go v0.111.0 // indirect
cloud.google.com/go/compute v1.20.0 // indirect cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.0 // indirect cloud.google.com/go/iam v1.1.5 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect
github.com/aws/smithy-go v1.13.5 // indirect github.com/aws/smithy-go v1.19.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/fatih/color v1.15.0 // indirect github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-logr/logr v1.3.0 // 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.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/s2a-go v0.1.7 // indirect
github.com/google/s2a-go v0.1.4 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/googleapis/gax-go/v2 v2.10.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kr/fs v0.1.0 // indirect github.com/kr/fs v0.1.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/miekg/dns v1.1.54 // indirect github.com/miekg/dns v1.1.57 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/run v1.1.0 // indirect github.com/oklog/run v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect github.com/prometheus/procfs v0.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.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 github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spf13/cast v1.5.1 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.7.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
golang.org/x/mod v0.10.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
golang.org/x/text v0.9.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
golang.org/x/tools v0.9.3 // indirect go.opentelemetry.io/otel v1.21.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect
google.golang.org/appengine v1.6.7 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect go.uber.org/multierr v1.11.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect golang.org/x/mod v0.14.0 // indirect
google.golang.org/grpc v1.55.0 // indirect golang.org/x/sync v0.5.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.16.1 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
google.golang.org/grpc v1.60.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
@ -172,5 +185,5 @@ require (
replace ( replace (
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 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-20230608154636-e9d673c2a1a8 golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20231218163640-d096410108fe
) )

2987
go.sum

File diff suppressed because it is too large Load diff

View file

@ -1644,6 +1644,112 @@ func TestIPList(t *testing.T) {
} }
} }
func TestSQLPlaceholderLimits(t *testing.T) {
numGroups := 120
numUsers := 120
var groupMapping []sdk.GroupMapping
folder := vfs.BaseVirtualFolder{
Name: "testfolder",
MappedPath: filepath.Join(os.TempDir(), "folder"),
}
err := dataprovider.AddFolder(&folder, "", "", "")
assert.NoError(t, err)
for i := 0; i < numGroups; i++ {
group := dataprovider.Group{
BaseGroup: sdk.BaseGroup{
Name: fmt.Sprintf("testgroup%d", i),
},
UserSettings: dataprovider.GroupUserSettings{
BaseGroupUserSettings: sdk.BaseGroupUserSettings{
Permissions: map[string][]string{
fmt.Sprintf("/dir%d", i): {dataprovider.PermAny},
},
},
},
}
group.VirtualFolders = append(group.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: folder,
VirtualPath: "/vdir",
})
err := dataprovider.AddGroup(&group, "", "", "")
assert.NoError(t, err)
groupMapping = append(groupMapping, sdk.GroupMapping{
Name: group.Name,
Type: sdk.GroupTypeSecondary,
})
}
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: "testusername",
HomeDir: filepath.Join(os.TempDir(), "testhome"),
Status: 1,
Permissions: map[string][]string{
"/": {dataprovider.PermAny},
},
},
Groups: groupMapping,
}
err = dataprovider.AddUser(&user, "", "", "")
assert.NoError(t, err)
users, err := dataprovider.GetUsersForQuotaCheck(map[string]bool{user.Username: true})
assert.NoError(t, err)
if assert.Len(t, users, 1) {
for i := 0; i < numGroups; i++ {
_, ok := users[0].Permissions[fmt.Sprintf("/dir%d", i)]
assert.True(t, ok)
}
}
err = dataprovider.DeleteUser(user.Username, "", "", "")
assert.NoError(t, err)
for i := 0; i < numUsers; i++ {
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: fmt.Sprintf("testusername%d", i),
HomeDir: filepath.Join(os.TempDir()),
Status: 1,
Permissions: map[string][]string{
"/": {dataprovider.PermAny},
},
},
Groups: []sdk.GroupMapping{
{
Name: "testgroup0",
Type: sdk.GroupTypePrimary,
},
},
}
err := dataprovider.AddUser(&user, "", "", "")
assert.NoError(t, err)
}
time.Sleep(100 * time.Millisecond)
err = dataprovider.DeleteFolder(folder.Name, "", "", "")
assert.NoError(t, err)
for i := 0; i < numUsers; i++ {
username := fmt.Sprintf("testusername%d", i)
user, err := dataprovider.UserExists(username, "")
assert.NoError(t, err)
assert.Greater(t, user.UpdatedAt, user.CreatedAt)
err = dataprovider.DeleteUser(username, "", "", "")
assert.NoError(t, err)
}
for i := 0; i < numGroups; i++ {
groupName := fmt.Sprintf("testgroup%d", i)
err = dataprovider.DeleteGroup(groupName, "", "", "")
assert.NoError(t, err)
}
}
func BenchmarkBcryptHashing(b *testing.B) { func BenchmarkBcryptHashing(b *testing.B) {
bcryptPassword := "bcryptpassword" bcryptPassword := "bcryptpassword"
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {

View file

@ -18,6 +18,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"path" "path"
"strings" "strings"
@ -1316,8 +1317,7 @@ func (c *BaseConnection) GetTransferQuota() dataprovider.TransferQuota {
} }
func (c *BaseConnection) checkUserQuota() (dataprovider.TransferQuota, int, int64) { func (c *BaseConnection) checkUserQuota() (dataprovider.TransferQuota, int, int64) {
clientIP := c.GetRemoteIP() ul, dl, total := c.User.GetDataTransferLimits()
ul, dl, total := c.User.GetDataTransferLimits(clientIP)
result := dataprovider.TransferQuota{ result := dataprovider.TransferQuota{
ULSize: ul, ULSize: ul,
DLSize: dl, DLSize: dl,
@ -1610,7 +1610,7 @@ func (c *BaseConnection) GetOpUnsupportedError() error {
func getQuotaExceededError(protocol string) error { func getQuotaExceededError(protocol string) error {
switch protocol { switch protocol {
case ProtocolSFTP: case ProtocolSFTP:
return fmt.Errorf("%w: %v", sftp.ErrSSHFxFailure, ErrQuotaExceeded.Error()) return fmt.Errorf("%w: %w", sftp.ErrSSHFxFailure, ErrQuotaExceeded)
case ProtocolFTP: case ProtocolFTP:
return ftpserver.ErrStorageExceeded return ftpserver.ErrStorageExceeded
default: default:
@ -1621,7 +1621,7 @@ func getQuotaExceededError(protocol string) error {
func getReadQuotaExceededError(protocol string) error { func getReadQuotaExceededError(protocol string) error {
switch protocol { switch protocol {
case ProtocolSFTP: case ProtocolSFTP:
return fmt.Errorf("%w: %v", sftp.ErrSSHFxFailure, ErrReadQuotaExceeded.Error()) return fmt.Errorf("%w: %w", sftp.ErrSSHFxFailure, ErrReadQuotaExceeded)
default: default:
return ErrReadQuotaExceeded return ErrReadQuotaExceeded
} }
@ -1655,29 +1655,33 @@ func (c *BaseConnection) IsQuotaExceededError(err error) bool {
} }
} }
func isSFTPGoError(err error) bool {
return errors.Is(err, ErrPermissionDenied) || errors.Is(err, ErrNotExist) || errors.Is(err, ErrOpUnsupported) ||
errors.Is(err, ErrQuotaExceeded) || errors.Is(err, ErrReadQuotaExceeded) ||
errors.Is(err, vfs.ErrStorageSizeUnavailable) || errors.Is(err, ErrShuttingDown)
}
// GetGenericError returns an appropriate generic error for the connection protocol // GetGenericError returns an appropriate generic error for the connection protocol
func (c *BaseConnection) GetGenericError(err error) error { func (c *BaseConnection) GetGenericError(err error) error {
switch c.protocol { switch c.protocol {
case ProtocolSFTP: case ProtocolSFTP:
if err == vfs.ErrStorageSizeUnavailable { if errors.Is(err, vfs.ErrStorageSizeUnavailable) || errors.Is(err, ErrOpUnsupported) || errors.Is(err, sftp.ErrSSHFxOpUnsupported) {
return fmt.Errorf("%w: %v", sftp.ErrSSHFxOpUnsupported, err.Error()) return fmt.Errorf("%w: %w", sftp.ErrSSHFxOpUnsupported, err)
} }
if err == ErrShuttingDown { if isSFTPGoError(err) {
return fmt.Errorf("%w: %v", sftp.ErrSSHFxFailure, err.Error()) return fmt.Errorf("%w: %w", sftp.ErrSSHFxFailure, err)
} }
if err != nil { if err != nil {
if e, ok := err.(*os.PathError); ok { var pathError *fs.PathError
c.Log(logger.LevelError, "generic path error: %+v", e) if errors.As(err, &pathError) {
return fmt.Errorf("%w: %v %v", sftp.ErrSSHFxFailure, e.Op, e.Err.Error()) c.Log(logger.LevelError, "generic path error: %+v", pathError)
return fmt.Errorf("%w: %v %v", sftp.ErrSSHFxFailure, pathError.Op, pathError.Err.Error())
} }
c.Log(logger.LevelError, "generic error: %+v", err) c.Log(logger.LevelError, "generic error: %+v", err)
return fmt.Errorf("%w: %v", sftp.ErrSSHFxFailure, ErrGenericFailure.Error())
} }
return sftp.ErrSSHFxFailure return sftp.ErrSSHFxFailure
default: default:
if err == ErrPermissionDenied || err == ErrNotExist || err == ErrOpUnsupported || if isSFTPGoError(err) {
err == ErrQuotaExceeded || err == ErrReadQuotaExceeded || err == vfs.ErrStorageSizeUnavailable ||
err == ErrShuttingDown {
return err return err
} }
c.Log(logger.LevelError, "generic error: %+v", err) c.Log(logger.LevelError, "generic error: %+v", err)

View file

@ -28,6 +28,7 @@ import (
"github.com/rs/xid" "github.com/rs/xid"
"github.com/sftpgo/sdk" "github.com/sftpgo/sdk"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/v2/internal/dataprovider" "github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/kms" "github.com/drakkan/sftpgo/v2/internal/kms"
@ -647,7 +648,7 @@ func TestFsFileCopier(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
} }
func TestFilterListDirs(t *testing.T) { func TestFilePatterns(t *testing.T) {
filters := dataprovider.UserFilters{ filters := dataprovider.UserFilters{
BaseUserFilters: sdk.BaseUserFilters{ BaseUserFilters: sdk.BaseUserFilters{
FilePatterns: []sdk.PatternsFilter{ FilePatterns: []sdk.PatternsFilter{
@ -661,6 +662,16 @@ func TestFilterListDirs(t *testing.T) {
DenyPolicy: sdk.DenyPolicyHide, DenyPolicy: sdk.DenyPolicyHide,
AllowedPatterns: []string{"*.jpg"}, AllowedPatterns: []string{"*.jpg"},
}, },
{
Path: "/dir3",
DenyPolicy: sdk.DenyPolicyDefault,
DeniedPatterns: []string{"*.jpg"},
},
{
Path: "/dir4",
DenyPolicy: sdk.DenyPolicyHide,
DeniedPatterns: []string{"*"},
},
}, },
}, },
} }
@ -693,13 +704,90 @@ func TestFilterListDirs(t *testing.T) {
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false), vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false), vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
} }
// dirContents are modified in place, we need to redefine them each time
filtered := user.FilterListDir(dirContents, "/dir1") filtered := user.FilterListDir(dirContents, "/dir1")
assert.Len(t, filtered, 5) assert.Len(t, filtered, 5)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir1/vdir1")
assert.Len(t, filtered, 2)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir2/vdir2")
require.Len(t, filtered, 1)
assert.Equal(t, "file1.jpg", filtered[0].Name())
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir2/vdir2/sub")
require.Len(t, filtered, 1)
assert.Equal(t, "file1.jpg", filtered[0].Name())
res, _ := user.IsFileAllowed("/dir1/vdir1/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir1/vdir1/sub/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir1/vdir1/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir1/vdir1/sub/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/dir1/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/dir1/sub/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir4/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir4/dir1/sub/file.jpg")
assert.False(t, res)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir4")
require.Len(t, filtered, 0)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir4/vdir2/sub")
require.Len(t, filtered, 0)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir2") filtered = user.FilterListDir(dirContents, "/dir2")
assert.Len(t, filtered, 2) assert.Len(t, filtered, 2)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir4")
assert.Len(t, filtered, 0)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir4/sub")
assert.Len(t, filtered, 0)
dirContents = []os.FileInfo{ dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false), vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false), vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
@ -708,11 +796,6 @@ func TestFilterListDirs(t *testing.T) {
filtered = user.FilterListDir(dirContents, "/dir1") filtered = user.FilterListDir(dirContents, "/dir1")
assert.Len(t, filtered, 5) assert.Len(t, filtered, 5)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir2") filtered = user.FilterListDir(dirContents, "/dir2")
if assert.Len(t, filtered, 1) { if assert.Len(t, filtered, 1) {
assert.True(t, filtered[0].IsDir()) assert.True(t, filtered[0].IsDir())
@ -799,7 +882,155 @@ func TestFilterListDirs(t *testing.T) {
dirContents = append(dirContents, vfs.NewFileInfo("file.jpg", false, 123, time.Now(), false)) dirContents = append(dirContents, vfs.NewFileInfo("file.jpg", false, 123, time.Now(), false))
filtered = user.FilterListDir(dirContents, "/dir3") filtered = user.FilterListDir(dirContents, "/dir3")
if assert.Len(t, filtered, 1) { require.Len(t, filtered, 1)
assert.Equal(t, "ic35", filtered[0].Name()) assert.Equal(t, "ic35", filtered[0].Name())
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
} }
filtered = user.FilterListDir(dirContents, "/dir3/ic36")
require.Len(t, filtered, 0)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35")
require.Len(t, filtered, 3)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub")
require.Len(t, filtered, 3)
res, _ = user.IsFileAllowed("/dir3/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35a")
assert.False(t, res)
res, policy := user.IsFileAllowed("/dir3/ic35a/file")
assert.False(t, res)
assert.Equal(t, sdk.DenyPolicyHide, policy)
res, _ = user.IsFileAllowed("/dir3/ic35")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub/file.txt")
assert.True(t, res)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub")
require.Len(t, filtered, 3)
user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
Path: "/dir3/ic35/sub1",
AllowedPatterns: []string{"*.jpg"},
DenyPolicy: sdk.DenyPolicyDefault,
})
user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
Path: "/dir3/ic35/sub2",
DeniedPatterns: []string{"*.jpg"},
DenyPolicy: sdk.DenyPolicyHide,
})
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub1")
require.Len(t, filtered, 3)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub2")
require.Len(t, filtered, 2)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub2/sub1")
require.Len(t, filtered, 2)
res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub/dir/file.txt")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub/dir/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/sub/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/sub2/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.txt")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub1/file.txt")
assert.True(t, res)
user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
Path: "/dir3/ic35",
DeniedPatterns: []string{"*.txt"},
DenyPolicy: sdk.DenyPolicyHide,
})
res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/adir/sub/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/adir/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.txt")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub1/file.txt")
assert.True(t, res)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35")
require.Len(t, filtered, 1)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/abc")
require.Len(t, filtered, 1)
} }

View file

@ -1302,7 +1302,8 @@ func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *stri
} else { } else {
h.Set("Content-Disposition", h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
multipartQuoteEscaper.Replace(part.Name), multipartQuoteEscaper.Replace(path.Base(part.Filepath)))) multipartQuoteEscaper.Replace(part.Name),
multipartQuoteEscaper.Replace((path.Base(replaceWithReplacer(part.Filepath, replacer))))))
contentType := mime.TypeByExtension(path.Ext(part.Filepath)) contentType := mime.TypeByExtension(path.Ext(part.Filepath))
if contentType == "" { if contentType == "" {
contentType = "application/octet-stream" contentType = "application/octet-stream"
@ -1525,7 +1526,6 @@ func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
user.Filters.DisableFsChecks = false user.Filters.DisableFsChecks = false
user.Filters.FilePatterns = nil user.Filters.FilePatterns = nil
user.Filters.BandwidthLimits = nil user.Filters.BandwidthLimits = nil
user.Filters.DataTransferLimits = nil
for k := range user.Permissions { for k := range user.Permissions {
user.Permissions[k] = []string{dataprovider.PermAny} user.Permissions[k] = []string{dataprovider.PermAny}
} }

View file

@ -1548,12 +1548,6 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {
func TestQuotaRenameOverwrite(t *testing.T) { func TestQuotaRenameOverwrite(t *testing.T) {
u := getTestUser() u := getTestUser()
u.QuotaFiles = 100 u.QuotaFiles = 100
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
{
Sources: []string{"10.8.0.0/8"},
TotalDataTransfer: 1,
},
}
user, _, err := httpdtest.AddUser(u, http.StatusCreated) user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
conn, client, err := getSftpClient(user) conn, client, err := getSftpClient(user)

View file

@ -299,6 +299,8 @@ func NewCertManager(keyPairs []TLSKeyPair, configDir, logSender string) (*CertMa
} }
randSecs := rand.Intn(59) randSecs := rand.Intn(59)
manager.monitor() manager.monitor()
_, err = eventScheduler.AddFunc(fmt.Sprintf("@every 8h0m%ds", randSecs), manager.monitor) if eventScheduler != nil {
_, err = eventScheduler.AddFunc(fmt.Sprintf("@every 8h0m%ds", randSecs), manager.monitor)
}
return manager, err return manager, err
} }

View file

@ -16,6 +16,8 @@ package common
import ( import (
"errors" "errors"
"fmt"
"io/fs"
"path" "path"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -215,12 +217,11 @@ func (t *BaseTransfer) SetCancelFn(cancelFn func()) {
// converts it into a more understandable form for the client if it is a // converts it into a more understandable form for the client if it is a
// well-known type of error // well-known type of error
func (t *BaseTransfer) ConvertError(err error) error { func (t *BaseTransfer) ConvertError(err error) error {
if t.Fs.IsNotExist(err) { var pathError *fs.PathError
return t.Connection.GetNotExistError() if errors.As(err, &pathError) {
} else if t.Fs.IsPermission(err) { return fmt.Errorf("%s %s: %s", pathError.Op, t.GetVirtualPath(), pathError.Err.Error())
return t.Connection.GetPermissionDeniedError()
} }
return err return t.Connection.GetFsError(t.Fs, err)
} }
// CheckRead returns an error if read if not allowed // CheckRead returns an error if read if not allowed

View file

@ -16,6 +16,7 @@ package common
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -226,6 +227,13 @@ func TestTransferErrors(t *testing.T) {
conn := NewBaseConnection("id", ProtocolSFTP, "", "", u) conn := NewBaseConnection("id", ProtocolSFTP, "", "", u)
transfer := NewBaseTransfer(file, conn, nil, testFile, testFile, "/transfer_test_file", TransferUpload, transfer := NewBaseTransfer(file, conn, nil, testFile, testFile, "/transfer_test_file", TransferUpload,
0, 0, 0, 0, true, fs, dataprovider.TransferQuota{}) 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})
pathError := &os.PathError{
Op: "test",
Path: testFile,
Err: os.ErrInvalid,
}
err = transfer.ConvertError(pathError)
assert.EqualError(t, err, fmt.Sprintf("%s %s: %s", pathError.Op, "/transfer_test_file", pathError.Err.Error()))
err = transfer.ConvertError(os.ErrNotExist) err = transfer.ConvertError(os.ErrNotExist)
assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile) assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
err = transfer.ConvertError(os.ErrPermission) err = transfer.ConvertError(os.ErrPermission)
@ -332,41 +340,22 @@ func TestFTPMode(t *testing.T) {
func TestTransferQuota(t *testing.T) { func TestTransferQuota(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
BaseUser: sdk.BaseUser{ BaseUser: sdk.BaseUser{
TotalDataTransfer: -1, TotalDataTransfer: 3,
UploadDataTransfer: -1, UploadDataTransfer: 2,
DownloadDataTransfer: -1, DownloadDataTransfer: 1,
}, },
} }
user.Filters.DataTransferLimits = []sdk.DataTransferLimit{ ul, dl, total := user.GetDataTransferLimits()
{ assert.Equal(t, int64(2*1048576), ul)
Sources: []string{"127.0.0.1/32", "192.168.1.0/24"}, assert.Equal(t, int64(1*1048576), dl)
TotalDataTransfer: 100, assert.Equal(t, int64(3*1048576), total)
UploadDataTransfer: 0, user.TotalDataTransfer = -1
DownloadDataTransfer: 0, user.UploadDataTransfer = -1
}, user.DownloadDataTransfer = -1
{ ul, dl, total = user.GetDataTransferLimits()
Sources: []string{"172.16.0.0/24"},
TotalDataTransfer: 0,
UploadDataTransfer: 120,
DownloadDataTransfer: 150,
},
}
ul, dl, total := user.GetDataTransferLimits("127.0.1.1")
assert.Equal(t, int64(0), ul) assert.Equal(t, int64(0), ul)
assert.Equal(t, int64(0), dl) assert.Equal(t, int64(0), dl)
assert.Equal(t, int64(0), total) assert.Equal(t, int64(0), total)
ul, dl, total = user.GetDataTransferLimits("127.0.0.1")
assert.Equal(t, int64(0), ul)
assert.Equal(t, int64(0), dl)
assert.Equal(t, int64(100*1048576), total)
ul, dl, total = user.GetDataTransferLimits("192.168.1.4")
assert.Equal(t, int64(0), ul)
assert.Equal(t, int64(0), dl)
assert.Equal(t, int64(100*1048576), total)
ul, dl, total = user.GetDataTransferLimits("172.16.0.2")
assert.Equal(t, int64(120*1048576), ul)
assert.Equal(t, int64(150*1048576), dl)
assert.Equal(t, int64(0), total)
transferQuota := dataprovider.TransferQuota{} transferQuota := dataprovider.TransferQuota{}
assert.True(t, transferQuota.HasDownloadSpace()) assert.True(t, transferQuota.HasDownloadSpace())
assert.True(t, transferQuota.HasUploadSpace()) assert.True(t, transferQuota.HasUploadSpace())

View file

@ -61,7 +61,7 @@ type baseTransferChecker struct {
func (t *baseTransferChecker) isDataTransferExceeded(user dataprovider.User, transfer dataprovider.ActiveTransfer, ulSize, func (t *baseTransferChecker) isDataTransferExceeded(user dataprovider.User, transfer dataprovider.ActiveTransfer, ulSize,
dlSize int64, dlSize int64,
) bool { ) bool {
ulQuota, dlQuota, totalQuota := user.GetDataTransferLimits(transfer.IP) ulQuota, dlQuota, totalQuota := user.GetDataTransferLimits()
if totalQuota > 0 { if totalQuota > 0 {
allowedSize := totalQuota - (user.UsedUploadDataTransfer + user.UsedDownloadDataTransfer) allowedSize := totalQuota - (user.UsedUploadDataTransfer + user.UsedDownloadDataTransfer)
if ulSize+dlSize > allowedSize { if ulSize+dlSize > allowedSize {

View file

@ -592,7 +592,7 @@ func TestDataTransferExceeded(t *testing.T) {
func TestGetUsersForQuotaCheck(t *testing.T) { func TestGetUsersForQuotaCheck(t *testing.T) {
usersToFetch := make(map[string]bool) usersToFetch := make(map[string]bool)
for i := 0; i < 50; i++ { for i := 0; i < 70; i++ {
usersToFetch[fmt.Sprintf("user%v", i)] = i%2 == 0 usersToFetch[fmt.Sprintf("user%v", i)] = i%2 == 0
} }
@ -600,7 +600,7 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, users, 0) assert.Len(t, users, 0)
for i := 0; i < 40; i++ { for i := 0; i < 60; i++ {
user := dataprovider.User{ user := dataprovider.User{
BaseUser: sdk.BaseUser{ BaseUser: sdk.BaseUser{
Username: fmt.Sprintf("user%v", i), Username: fmt.Sprintf("user%v", i),
@ -622,17 +622,6 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
QuotaSize: 100, QuotaSize: 100,
}, },
}, },
Filters: dataprovider.UserFilters{
BaseUserFilters: sdk.BaseUserFilters{
DataTransferLimits: []sdk.DataTransferLimit{
{
Sources: []string{"172.16.0.0/16"},
UploadDataTransfer: 50,
DownloadDataTransfer: 80,
},
},
},
},
} }
err = dataprovider.AddUser(&user, "", "", "") err = dataprovider.AddUser(&user, "", "", "")
assert.NoError(t, err) assert.NoError(t, err)
@ -642,7 +631,7 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
users, err = dataprovider.GetUsersForQuotaCheck(usersToFetch) users, err = dataprovider.GetUsersForQuotaCheck(usersToFetch)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, users, 40) assert.Len(t, users, 60)
for _, user := range users { for _, user := range users {
userIdxStr := strings.Replace(user.Username, "user", "", 1) userIdxStr := strings.Replace(user.Username, "user", "", 1)
@ -660,17 +649,13 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
assert.Len(t, user.VirtualFolders, 0, user.Username) assert.Len(t, user.VirtualFolders, 0, user.Username)
} }
} }
ul, dl, total := user.GetDataTransferLimits("127.1.1.1") ul, dl, total := user.GetDataTransferLimits()
assert.Equal(t, int64(0), ul) assert.Equal(t, int64(0), ul)
assert.Equal(t, int64(0), dl) assert.Equal(t, int64(0), dl)
assert.Equal(t, int64(0), total) assert.Equal(t, int64(0), total)
ul, dl, total = user.GetDataTransferLimits("172.16.2.3")
assert.Equal(t, int64(50*1024*1024), ul)
assert.Equal(t, int64(80*1024*1024), dl)
assert.Equal(t, int64(0), total)
} }
for i := 0; i < 40; i++ { for i := 0; i < 60; i++ {
err = dataprovider.DeleteUser(fmt.Sprintf("user%v", i), "", "", "") err = dataprovider.DeleteUser(fmt.Sprintf("user%v", i), "", "", "")
assert.NoError(t, err) assert.NoError(t, err)
err = dataprovider.DeleteFolder(fmt.Sprintf("f%v", i), "", "", "") err = dataprovider.DeleteFolder(fmt.Sprintf("f%v", i), "", "", "")

View file

@ -1961,6 +1961,11 @@ func getCommandConfigsFromEnv(idx int) {
cfg.Env = env cfg.Env = env
} }
args, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__ARGS", idx))
if ok {
cfg.Args = args
}
if cfg.Path != "" { if cfg.Path != "" {
if len(globalConf.CommandConfig.Commands) > idx { if len(globalConf.CommandConfig.Commands) > idx {
globalConf.CommandConfig.Commands[idx] = cfg globalConf.CommandConfig.Commands[idx] = cfg

View file

@ -924,13 +924,17 @@ func TestCommandsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__PATH", "cmd2") os.Setenv("SFTPGO_COMMAND__COMMANDS__1__PATH", "cmd2")
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__TIMEOUT", "20") os.Setenv("SFTPGO_COMMAND__COMMANDS__1__TIMEOUT", "20")
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ENV", "e=f") os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ENV", "e=f")
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ARGS", "arg1, arg2")
t.Cleanup(func() { t.Cleanup(func() {
os.Unsetenv("SFTPGO_COMMAND__TIMEOUT") os.Unsetenv("SFTPGO_COMMAND__TIMEOUT")
os.Unsetenv("SFTPGO_COMMAND__ENV") os.Unsetenv("SFTPGO_COMMAND__ENV")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__PATH") os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__PATH")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__TIMEOUT") os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__TIMEOUT")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__ENV") os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__PATH")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__TIMEOUT")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__ENV")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__ARGS")
}) })
err = config.LoadConfig(configDir, confName) err = config.LoadConfig(configDir, confName)
@ -945,6 +949,7 @@ func TestCommandsFromEnv(t *testing.T) {
require.Equal(t, "cmd2", commandConfig.Commands[1].Path) require.Equal(t, "cmd2", commandConfig.Commands[1].Path)
require.Equal(t, 20, commandConfig.Commands[1].Timeout) require.Equal(t, 20, commandConfig.Commands[1].Timeout)
require.Equal(t, []string{"e=f"}, commandConfig.Commands[1].Env) require.Equal(t, []string{"e=f"}, commandConfig.Commands[1].Env)
require.Equal(t, []string{"arg1", "arg2"}, commandConfig.Commands[1].Args)
err = os.Remove(configFilePath) err = os.Remove(configFilePath)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -797,6 +797,7 @@ func (p *BoltProvider) updateUserPassword(username, password string) error {
return err return err
} }
user.Password = password user.Password = password
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(user) buf, err := json.Marshal(user)
if err != nil { if err != nil {
return err return err

View file

@ -28,10 +28,9 @@ import (
// Supported values for host keys, KEXs, ciphers, MACs // Supported values for host keys, KEXs, ciphers, MACs
var ( var (
supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01} supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA}
supportedKexAlgos = []string{ supportedKexAlgos = []string{
"diffie-hellman-group16-sha512", "diffie-hellman-group18-sha512", "diffie-hellman-group16-sha512", "diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
"diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha256", "diffie-hellman-group-exchange-sha1", "diffie-hellman-group-exchange-sha256", "diffie-hellman-group-exchange-sha1",
} }
supportedCiphers = []string{ supportedCiphers = []string{
@ -98,16 +97,28 @@ func (c *SFTPDConfigs) GetModuliAsString() string {
} }
func (c *SFTPDConfigs) validate() error { func (c *SFTPDConfigs) validate() error {
var hostKeyAlgos []string
for _, algo := range c.HostKeyAlgos { for _, algo := range c.HostKeyAlgos {
if algo == ssh.CertAlgoRSAv01 {
continue
}
if !util.Contains(supportedHostKeyAlgos, algo) { if !util.Contains(supportedHostKeyAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo)) return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
} }
hostKeyAlgos = append(hostKeyAlgos, algo)
} }
c.HostKeyAlgos = hostKeyAlgos
var kexAlgos []string
for _, algo := range c.KexAlgorithms { for _, algo := range c.KexAlgorithms {
if algo == "diffie-hellman-group18-sha512" {
continue
}
if !util.Contains(supportedKexAlgos, algo) { if !util.Contains(supportedKexAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo)) return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
} }
kexAlgos = append(kexAlgos, algo)
} }
c.KexAlgorithms = kexAlgos
for _, cipher := range c.Ciphers { for _, cipher := range c.Ciphers {
if !util.Contains(supportedCiphers, cipher) { if !util.Contains(supportedCiphers, cipher) {
return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher)) return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))

View file

@ -2585,18 +2585,6 @@ func copyBaseUserFilters(in sdk.BaseUserFilters) sdk.BaseUserFilters {
copy(bwLimit.Sources, limit.Sources) copy(bwLimit.Sources, limit.Sources)
filters.BandwidthLimits = append(filters.BandwidthLimits, bwLimit) filters.BandwidthLimits = append(filters.BandwidthLimits, bwLimit)
} }
filters.DataTransferLimits = make([]sdk.DataTransferLimit, 0, len(in.DataTransferLimits))
for _, limit := range in.DataTransferLimits {
dtLimit := sdk.DataTransferLimit{
UploadDataTransfer: limit.UploadDataTransfer,
DownloadDataTransfer: limit.DownloadDataTransfer,
TotalDataTransfer: limit.TotalDataTransfer,
Sources: make([]string, 0, len(limit.Sources)),
}
dtLimit.Sources = make([]string, len(limit.Sources))
copy(dtLimit.Sources, limit.Sources)
filters.DataTransferLimits = append(filters.DataTransferLimits, dtLimit)
}
return filters return filters
} }
@ -2943,26 +2931,6 @@ func validateBandwidthLimitsFilter(filters *sdk.BaseUserFilters) error {
return nil return nil
} }
func validateTransferLimitsFilter(filters *sdk.BaseUserFilters) error {
for idx, limit := range filters.DataTransferLimits {
filters.DataTransferLimits[idx].Sources = util.RemoveDuplicates(limit.Sources, false)
if len(limit.Sources) == 0 {
return util.NewValidationError("no data transfer limit source specified")
}
for _, source := range limit.Sources {
_, _, err := net.ParseCIDR(source)
if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse data transfer limit source %q: %v", source, err))
}
}
if limit.TotalDataTransfer > 0 {
filters.DataTransferLimits[idx].UploadDataTransfer = 0
filters.DataTransferLimits[idx].DownloadDataTransfer = 0
}
}
return nil
}
func updateFiltersValues(filters *sdk.BaseUserFilters) { func updateFiltersValues(filters *sdk.BaseUserFilters) {
if filters.StartDirectory != "" { if filters.StartDirectory != "" {
filters.StartDirectory = util.CleanPath(filters.StartDirectory) filters.StartDirectory = util.CleanPath(filters.StartDirectory)
@ -2998,9 +2966,6 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
if err := validateBandwidthLimitsFilter(filters); err != nil { if err := validateBandwidthLimitsFilter(filters); err != nil {
return err return err
} }
if err := validateTransferLimitsFilter(filters); err != nil {
return err
}
if len(filters.DeniedLoginMethods) >= len(ValidLoginMethods) { if len(filters.DeniedLoginMethods) >= len(ValidLoginMethods) {
return util.NewValidationError("invalid denied_login_methods") return util.NewValidationError("invalid denied_login_methods")
} }
@ -3525,7 +3490,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
return 0, err return 0, err
} }
if len(answers) != 1 { if len(answers) != 1 {
return 0, fmt.Errorf("unexpected number of answers: %v", len(answers)) return 0, fmt.Errorf("unexpected number of answers: %d", len(answers))
} }
err = user.LoadAndApplyGroupSettings() err = user.LoadAndApplyGroupSettings()
if err != nil { if err != nil {
@ -3535,16 +3500,20 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
if err != nil { if err != nil {
return 0, err return 0, err
} }
return checkKeyboardInteractiveSecondFactor(user, client, protocol)
}
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 || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
return 1, nil return 1, nil
} }
err = user.Filters.TOTPConfig.Secret.TryDecrypt() err := user.Filters.TOTPConfig.Secret.TryDecrypt()
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v", providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
user.Username, protocol, err) user.Username, protocol, err)
return 0, err return 0, err
} }
answers, err = client("", "", []string{"Authentication code: "}, []bool{false}) answers, err := client("", "", []string{"Authentication code: "}, []bool{false})
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -3773,15 +3742,25 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
} }
func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) { func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) {
if err := user.LoadAndApplyGroupSettings(); err != nil {
return *user, err
}
var authResult int var authResult int
var err error var err error
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) { if !user.Filters.Hooks.ExternalAuthDisabled {
authResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol) if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
} else if authHook != "" { authResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol)
if strings.HasPrefix(authHook, "http") { if authResult == 1 && err == nil {
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol) authResult, err = checkKeyboardInteractiveSecondFactor(user, client, protocol)
}
} else if authHook != "" {
if strings.HasPrefix(authHook, "http") {
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol)
} else {
authResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip, protocol)
}
} else { } else {
authResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip, protocol) authResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol)
} }
} else { } else {
authResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol) authResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol)
@ -3792,10 +3771,6 @@ func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardI
if authResult != 1 { if authResult != 1 {
return *user, fmt.Errorf("keyboard interactive auth failed, result: %v", authResult) return *user, fmt.Errorf("keyboard interactive auth failed, result: %v", authResult)
} }
err = user.LoadAndApplyGroupSettings()
if err != nil {
return *user, err
}
err = user.CheckLoginConditions() err = user.CheckLoginConditions()
if err != nil { if err != nil {
return *user, err return *user, err
@ -4145,6 +4120,35 @@ func updateUserFromExtAuthResponse(user *User, password, pkey string) {
user.LastPasswordChange = 0 user.LastPasswordChange = 0
} }
func checkPasswordAfterEmptyExtAuthResponse(user *User, plainPwd, protocol string) error {
if plainPwd == "" {
return nil
}
match, err := isPasswordOK(user, plainPwd)
if match && err == nil {
return nil
}
hashedPwd, err := hashPlainPassword(plainPwd)
if err != nil {
providerLog(logger.LevelError, "unable to hash password for user %q after empty external response: %v",
user.Username, err)
return err
}
err = provider.updateUserPassword(user.Username, hashedPwd)
if err != nil {
providerLog(logger.LevelError, "unable to update password for user %q after empty external response: %v",
user.Username, err)
}
user.Password = hashedPwd
cachedUserPasswords.Add(user.Username, plainPwd, user.Password)
if protocol != protocolWebDAV {
webDAVUsersCache.swap(user, plainPwd)
}
providerLog(logger.LevelDebug, "updated password for user %q after empty external auth response", user.Username)
return nil
}
func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string, func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string,
tlsCert *x509.Certificate, tlsCert *x509.Certificate,
) (User, error) { ) (User, error) {
@ -4176,7 +4180,8 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
if u.ID == 0 { if u.ID == 0 {
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username)) return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
} }
return u, nil err = checkPasswordAfterEmptyExtAuthResponse(&u, password, protocol)
return u, err
} }
err = json.Unmarshal(out, &user) err = json.Unmarshal(out, &user)
if err != nil { if err != nil {
@ -4249,18 +4254,19 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
out, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON) out, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON)
if err != nil { if err != nil {
return user, fmt.Errorf("plugin auth error for user %q: %v, elapsed: %v, auth scope: %v", return user, fmt.Errorf("plugin auth error for user %q: %v, elapsed: %v, auth scope: %d",
username, err, time.Since(startTime), authScope) username, err, time.Since(startTime), authScope)
} }
providerLog(logger.LevelDebug, "plugin auth completed for user %q, elapsed: %v,auth scope: %v", providerLog(logger.LevelDebug, "plugin auth completed for user %q, elapsed: %v, auth scope: %d",
username, time.Since(startTime), authScope) username, time.Since(startTime), authScope)
if util.IsByteArrayEmpty(out) { if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %q id: %v", providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %q id: %d, auth scope: %d",
username, u.ID) username, u.ID, authScope)
if u.ID == 0 { if u.ID == 0 {
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username)) return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
} }
return u, nil err = checkPasswordAfterEmptyExtAuthResponse(&u, password, protocol)
return u, err
} }
err = json.Unmarshal(out, &user) err = json.Unmarshal(out, &user)
if err != nil { if err != nil {

View file

@ -472,6 +472,7 @@ func (p *MemoryProvider) updateUserPassword(username, password string) error {
return err return err
} }
user.Password = password user.Password = password
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
p.dbHandle.users[username] = user p.dbHandle.users[username] = user
return nil return nil
} }

View file

@ -28,6 +28,7 @@ const (
SessionTypeOIDCToken SessionTypeOIDCToken
SessionTypeResetCode SessionTypeResetCode
SessionTypeOAuth2Auth SessionTypeOAuth2Auth
SessionTypeInvalidToken
) )
// Session defines a shared session persisted in the data provider // Session defines a shared session persisted in the data provider
@ -42,7 +43,7 @@ func (s *Session) validate() error {
if s.Key == "" { if s.Key == "" {
return errors.New("unable to save a session with an empty key") return errors.New("unable to save a session with an empty key")
} }
if s.Type < SessionTypeOIDCAuth || s.Type > SessionTypeOAuth2Auth { if s.Type < SessionTypeOIDCAuth || s.Type > SessionTypeInvalidToken {
return fmt.Errorf("invalid session type: %v", s.Type) return fmt.Errorf("invalid session type: %v", s.Type)
} }
return nil return nil

View file

@ -22,6 +22,7 @@ import (
"time" "time"
"github.com/alexedwards/argon2id" "github.com/alexedwards/argon2id"
passwordvalidator "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"github.com/drakkan/sftpgo/v2/internal/logger" "github.com/drakkan/sftpgo/v2/internal/logger"
@ -178,6 +179,15 @@ func (s *Share) HasRedactedPassword() bool {
func (s *Share) hashPassword() error { func (s *Share) hashPassword() error {
if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) { if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) {
user, err := UserExists(s.Username, "")
if err != nil {
return util.NewGenericError(fmt.Sprintf("unable to validate user: %v", err))
}
if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {
if err := passwordvalidator.Validate(s.Password, minEntropy); err != nil {
return util.NewValidationError(err.Error())
}
}
if config.PasswordHashing.Algo == HashingAlgoBcrypt { if config.PasswordHashing.Algo == HashingAlgoBcrypt {
hashed, err := bcrypt.GenerateFromPassword([]byte(s.Password), config.PasswordHashing.BcryptOptions.Cost) hashed, err := bcrypt.GenerateFromPassword([]byte(s.Password), config.PasswordHashing.BcryptOptions.Cost)
if err != nil { if err != nil {
@ -198,8 +208,7 @@ func (s *Share) hashPassword() error {
func (s *Share) validatePaths() error { func (s *Share) validatePaths() error {
var paths []string var paths []string
for _, p := range s.Paths { for _, p := range s.Paths {
p = strings.TrimSpace(p) if strings.TrimSpace(p) != "" {
if p != "" {
paths = append(paths, p) paths = append(paths, p)
} }
} }

View file

@ -958,63 +958,86 @@ func sqlCommonGetUsersInGroups(names []string, dbHandle sqlQuerier) ([]string, e
if len(names) == 0 { if len(names) == 0 {
return nil, nil return nil, nil
} }
maxNames := len(sqlPlaceholders)
usernames := make([]string, 0, len(names))
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel() defer cancel()
q := getUsersInGroupsQuery(len(names)) for len(names) > 0 {
args := make([]any, 0, len(names)) if maxNames > len(names) {
for _, name := range names { maxNames = len(names)
args = append(args, name) }
}
usernames := make([]string, 0, len(names)) q := getUsersInGroupsQuery(maxNames)
rows, err := dbHandle.QueryContext(ctx, q, args...) args := make([]any, 0, maxNames)
if err != nil { for _, name := range names[:maxNames] {
return nil, err args = append(args, name)
} }
defer rows.Close()
for rows.Next() { rows, err := dbHandle.QueryContext(ctx, q, args...)
var username string if err != nil {
err = rows.Scan(&username) return nil, err
}
defer rows.Close()
for rows.Next() {
var username string
err = rows.Scan(&username)
if err != nil {
return usernames, err
}
usernames = append(usernames, username)
}
err = rows.Err()
if err != nil { if err != nil {
return usernames, err return usernames, err
} }
usernames = append(usernames, username) names = names[maxNames:]
} }
return usernames, rows.Err() return usernames, nil
} }
func sqlCommonGetGroupsWithNames(names []string, dbHandle sqlQuerier) ([]Group, error) { func sqlCommonGetGroupsWithNames(names []string, dbHandle sqlQuerier) ([]Group, error) {
if len(names) == 0 { if len(names) == 0 {
return nil, nil return nil, nil
} }
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) maxNames := len(sqlPlaceholders)
defer cancel()
q := getGroupsWithNamesQuery(len(names))
args := make([]any, 0, len(names))
for _, name := range names {
args = append(args, name)
}
groups := make([]Group, 0, len(names)) groups := make([]Group, 0, len(names))
rows, err := dbHandle.QueryContext(ctx, q, args...) for len(names) > 0 {
if err != nil { if maxNames > len(names) {
return groups, err maxNames = len(names)
} }
defer rows.Close() ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
for rows.Next() { q := getGroupsWithNamesQuery(maxNames)
group, err := getGroupFromDbRow(rows) args := make([]any, 0, maxNames)
for _, name := range names[:maxNames] {
args = append(args, name)
}
rows, err := dbHandle.QueryContext(ctx, q, args...)
if err != nil { if err != nil {
return groups, err return groups, err
} }
groups = append(groups, group) defer rows.Close()
}
err = rows.Err() for rows.Next() {
if err != nil { group, err := getGroupFromDbRow(rows)
return groups, err if err != nil {
return groups, err
}
groups = append(groups, group)
}
err = rows.Err()
if err != nil {
return groups, err
}
names = names[maxNames:]
} }
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return getGroupsWithVirtualFolders(ctx, groups, dbHandle) return getGroupsWithVirtualFolders(ctx, groups, dbHandle)
} }
@ -1386,7 +1409,7 @@ func sqlCommonUpdateUserPassword(username, password string, dbHandle *sql.DB) er
defer cancel() defer cancel()
q := getUpdateUserPasswordQuery() q := getUpdateUserPasswordQuery()
res, err := dbHandle.ExecContext(ctx, q, password, username) res, err := dbHandle.ExecContext(ctx, q, password, util.GetTimeAsMsSinceEpoch(time.Now()), username)
if err != nil { if err != nil {
return err return err
} }
@ -1535,6 +1558,9 @@ func sqlCommonGetRecentlyUpdatedUsers(after int64, dbHandle sqlQuerier) ([]User,
} }
} }
groupNames = util.RemoveDuplicates(groupNames, false) groupNames = util.RemoveDuplicates(groupNames, false)
if len(groupNames) == 0 {
return users, nil
}
groups, err := sqlCommonGetGroupsWithNames(groupNames, dbHandle) groups, err := sqlCommonGetGroupsWithNames(groupNames, dbHandle)
if err != nil { if err != nil {
return users, err return users, err
@ -1553,15 +1579,23 @@ func sqlCommonGetRecentlyUpdatedUsers(after int64, dbHandle sqlQuerier) ([]User,
return users, nil return users, nil
} }
func sqlGetMaxUsersForQuotaCheckRange() int {
maxUsers := 50
if maxUsers > len(sqlPlaceholders) {
maxUsers = len(sqlPlaceholders)
}
return maxUsers
}
func sqlCommonGetUsersForQuotaCheck(toFetch map[string]bool, dbHandle sqlQuerier) ([]User, error) { func sqlCommonGetUsersForQuotaCheck(toFetch map[string]bool, dbHandle sqlQuerier) ([]User, error) {
users := make([]User, 0, 30) maxUsers := sqlGetMaxUsersForQuotaCheckRange()
users := make([]User, 0, maxUsers)
usernames := make([]string, 0, len(toFetch)) usernames := make([]string, 0, len(toFetch))
for k := range toFetch { for k := range toFetch {
usernames = append(usernames, k) usernames = append(usernames, k)
} }
maxUsers := 30
for len(usernames) > 0 { for len(usernames) > 0 {
if maxUsers > len(usernames) { if maxUsers > len(usernames) {
maxUsers = len(usernames) maxUsers = len(usernames)

View file

@ -42,7 +42,7 @@ const (
func getSQLPlaceholders() []string { func getSQLPlaceholders() []string {
var placeholders []string var placeholders []string
for i := 1; i <= 50; i++ { for i := 1; i <= 100; i++ {
if config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName { if config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName {
placeholders = append(placeholders, fmt.Sprintf("$%d", i)) placeholders = append(placeholders, fmt.Sprintf("$%d", i))
} else { } else {
@ -400,7 +400,7 @@ func getUsersInGroupsQuery(numArgs int) string {
} else { } else {
sb.WriteString("('')") sb.WriteString("('')")
} }
return fmt.Sprintf(`SELECT username FROM %s WHERE id IN (SELECT user_id from %s WHERE group_id IN (SELECT id FROM %s WHERE name IN (%s)))`, return fmt.Sprintf(`SELECT username FROM %s WHERE id IN (SELECT user_id from %s WHERE group_id IN (SELECT id FROM %s WHERE name IN %s))`,
sqlTableUsers, sqlTableUsersGroupsMapping, getSQLQuotedName(sqlTableGroups), sb.String()) sqlTableUsers, sqlTableUsersGroupsMapping, getSQLQuotedName(sqlTableGroups), sb.String())
} }
@ -716,8 +716,8 @@ func getUpdateUserQuery(role string) string {
} }
func getUpdateUserPasswordQuery() string { func getUpdateUserPasswordQuery() string {
return fmt.Sprintf(`UPDATE %s SET password=%s WHERE username = %s`, return fmt.Sprintf(`UPDATE %s SET password=%s,updated_at=%s WHERE username = %s`,
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1]) sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])
} }
func getDeleteUserQuery(softDelete bool) string { func getDeleteUserQuery(softDelete bool) string {

View file

@ -981,9 +981,14 @@ func (u *User) getPatternsFilterForPath(virtualPath string) sdk.PatternsFilter {
return filter return filter
} }
dirsForPath := util.GetDirsForVirtualPath(virtualPath) dirsForPath := util.GetDirsForVirtualPath(virtualPath)
for _, dir := range dirsForPath { for idx, dir := range dirsForPath {
for _, f := range u.Filters.FilePatterns { for _, f := range u.Filters.FilePatterns {
if f.Path == dir { if f.Path == dir {
if idx > 0 && len(f.AllowedPatterns) > 0 && len(f.DeniedPatterns) > 0 && f.DeniedPatterns[0] == "*" {
if f.CheckAllowed(path.Base(dirsForPath[idx-1])) {
return filter
}
}
filter = f filter = f
break break
} }
@ -1004,7 +1009,7 @@ func (u *User) isDirHidden(virtualPath string) bool {
return false return false
} }
filter := u.getPatternsFilterForPath(dirPath) filter := u.getPatternsFilterForPath(dirPath)
if filter.DenyPolicy == sdk.DenyPolicyHide { if filter.DenyPolicy == sdk.DenyPolicyHide && filter.Path != dirPath {
if !filter.CheckAllowed(path.Base(dirPath)) { if !filter.CheckAllowed(path.Base(dirPath)) {
return true return true
} }
@ -1173,7 +1178,7 @@ func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
return false return false
} }
// GetSignature returns a signature for this admin. // GetSignature returns a signature for this user.
// It will change after an update // It will change after an update
func (u *User) GetSignature() string { func (u *User) GetSignature() string {
return strconv.FormatInt(u.UpdatedAt, 10) return strconv.FormatInt(u.UpdatedAt, 10)
@ -1289,39 +1294,12 @@ func (u *User) HasQuotaRestrictions() bool {
// HasTransferQuotaRestrictions returns true if there are any data transfer restrictions // HasTransferQuotaRestrictions returns true if there are any data transfer restrictions
func (u *User) HasTransferQuotaRestrictions() bool { func (u *User) HasTransferQuotaRestrictions() bool {
if len(u.Filters.DataTransferLimits) > 0 {
return true
}
return u.UploadDataTransfer > 0 || u.TotalDataTransfer > 0 || u.DownloadDataTransfer > 0 return u.UploadDataTransfer > 0 || u.TotalDataTransfer > 0 || u.DownloadDataTransfer > 0
} }
// GetDataTransferLimits returns upload, download and total data transfer limits // GetDataTransferLimits returns upload, download and total data transfer limits
func (u *User) GetDataTransferLimits(clientIP string) (int64, int64, int64) { func (u *User) GetDataTransferLimits() (int64, int64, int64) {
var total, ul, dl int64 var total, ul, dl int64
if len(u.Filters.DataTransferLimits) > 0 {
ip := net.ParseIP(clientIP)
if ip != nil {
for _, limit := range u.Filters.DataTransferLimits {
for _, source := range limit.Sources {
_, ipNet, err := net.ParseCIDR(source)
if err == nil {
if ipNet.Contains(ip) {
if limit.TotalDataTransfer > 0 {
total = limit.TotalDataTransfer * 1048576
}
if limit.DownloadDataTransfer > 0 {
dl = limit.DownloadDataTransfer * 1048576
}
if limit.UploadDataTransfer > 0 {
ul = limit.UploadDataTransfer * 1048576
}
return ul, dl, total
}
}
}
}
}
}
if u.TotalDataTransfer > 0 { if u.TotalDataTransfer > 0 {
total = u.TotalDataTransfer * 1048576 total = u.TotalDataTransfer * 1048576
} }
@ -1825,7 +1803,6 @@ func (u *User) mergeAdditiveProperties(group *Group, groupType int, replacer *st
u.mergePermissions(group, groupType, replacer) u.mergePermissions(group, groupType, replacer)
u.mergeFilePatterns(group, groupType, replacer) u.mergeFilePatterns(group, groupType, replacer)
u.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...) u.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...)
u.Filters.DataTransferLimits = append(u.Filters.DataTransferLimits, group.UserSettings.Filters.DataTransferLimits...)
u.Filters.AllowedIP = append(u.Filters.AllowedIP, group.UserSettings.Filters.AllowedIP...) u.Filters.AllowedIP = append(u.Filters.AllowedIP, group.UserSettings.Filters.AllowedIP...)
u.Filters.DeniedIP = append(u.Filters.DeniedIP, group.UserSettings.Filters.DeniedIP...) u.Filters.DeniedIP = append(u.Filters.DeniedIP, group.UserSettings.Filters.DeniedIP...)
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, group.UserSettings.Filters.DeniedLoginMethods...) u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, group.UserSettings.Filters.DeniedLoginMethods...)
@ -1855,6 +1832,9 @@ func (u *User) mergeVirtualFolders(group *Group, groupType int, replacer *string
} }
func (u *User) mergePermissions(group *Group, groupType int, replacer *strings.Replacer) { func (u *User) mergePermissions(group *Group, groupType int, replacer *strings.Replacer) {
if u.Permissions == nil {
u.Permissions = make(map[string][]string)
}
for k, v := range group.UserSettings.Permissions { for k, v := range group.UserSettings.Permissions {
if k == "/" { if k == "/" {
if groupType == sdk.GroupTypePrimary { if groupType == sdk.GroupTypePrimary {

View file

@ -90,154 +90,154 @@ CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
-----END EC PRIVATE KEY-----` -----END EC PRIVATE KEY-----`
caCRT = `-----BEGIN CERTIFICATE----- caCRT = `-----BEGIN CERTIFICATE-----
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0 MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
QXV0aDAeFw0yMjA3MDQxNTQzMTFaFw0yNDAxMDQxNTUzMDhaMBMxETAPBgNVBAMT QXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4eyDJkmW CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m
D4OVYo7ddgiZkd6QQdPyLcsa31Wc9jdR2/peEabyNT8jSWteS6ouY84GRlnhfFeZ fi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1
mpXgbaUJu/Z8Y/8riPxwL8XF4vCScQDMywpQnVUd6E9x2/+/uaD4p/BBswgKqKPe v03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh
uDcHZn7MkD4QlquUhMElDrBUi1Dv/AVHnQ6iP4vd5Jlv0F+40jdq/8Wa7yhW7Pu5 yzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL
iNvPwCk8HjENBKVur/re+Acif8A2TlbCsuOnVduSQNmnWH+iZmB9upyBZtUszGS0 CdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U
JhUwtSnwUX/JapF70Pwte/PV3RK8cJ5FjuAPNeTyJvSuMTELFSAyCeiNynFGgyhW 4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb
cqbEiPu6BURLculyVkmh4dOrhTrYZv/n3UJAhyxkdYrbh3INHmTa4izvclcuwoEo KF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo
lFlJp3l77D0lIi+pbtcBV6ys7reyuxUAkBNwnpt2pWfCQoi4QYKcNbHm47c2phOb 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
WwSa13hh6yjoDQ== kK8u1YiiVenmoQ==
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
caCRL = `-----BEGIN X509 CRL----- caCRL = `-----BEGIN X509 CRL-----
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
MjIwNzA0MTU1MzU4WhcNMjQwNzAzMTU1MzU4WjAkMCICEQDZo5Q3lhxFuDUsxGNm MjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg
794YFw0yMjA3MDQxNTUzNThaoCMwITAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8Qvn ZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r
bGzjmOULwDANBgkqhkiG9w0BAQsFAAOCAgEA1lK6g8qmhyY6myx8342dDuaauY03 rl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV
0iojkxpasuYcytK6XRm96YqjZK9EETxsHHViVU0vCXES60D6wJ9gw4fTWn3WxEdx +jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199
nIwbGyjUGHh2y+R3uQsfvwxsdYvDsTLAnOLwOo68dAHWmMDZRmgTuGNoYFxVQRGR XSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE
Cn90ZR7LPLpCScclWM8FE/W1B90x3ZE8EhJiCI/WyyTh3EgshmB7A5GoDrFZfmvR jF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF
dzoTKO+F9p2XjtmgfiBE3czWQysfATmbutZUbG/ZRb89u+ZEUyPoC94mg8fhNWoX DouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD
1d5G9QAkZFHp957/5QHLq9OHNfnWXoohhebjF4VWqZH7w+RtLc8t0PIog2lX4t1o MSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4
5N/xFk9akvuoyNGg/fYuJBmN162Q0MdeYfYKDGWdXxf6fpHxVr5v2JrIx6gOwubb iYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE
cIKP22ZBv/PYOeFsAZ755lTl4OTFUjU5ZJEPD6pUc1daaIqfxsxu8gDZP92FZjsB ypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4
zaalMbh30n2OhagSMBzSLg5rE6WmBzlQX0ZN8YrW4l2Vq6twnnFHY+UyblRZS+d4 hVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94
oHBaoOaxPEkLxNZ8ulzJS4B6c4D1CXOaBEf++snVzRRUOEdX3x7TvkkrLvIsm06R cp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67
ux0L1zJb9LbZ/1rhuv70z/kIlD55sqYuRqu3RpgTgZuTERU//rYIqWd03Y5Qon8i sn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn
VoC6Yp9DPldQJrk= w7bXJGfJcIMrrKs=
-----END X509 CRL-----` -----END X509 CRL-----`
client1Crt = `-----BEGIN CERTIFICATE----- client1Crt = `-----BEGIN CERTIFICATE-----
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
7LEmxYg= MU3Bo0A=
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
client1Key = `-----BEGIN RSA PRIVATE KEY----- client1Key = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----` -----END RSA PRIVATE KEY-----`
// client 2 crt is revoked // client 2 crt is revoked
client2Crt = `-----BEGIN CERTIFICATE----- client2Crt = `-----BEGIN CERTIFICATE-----
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
enqNrFI= E0+r2+4=
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
client2Key = `-----BEGIN RSA PRIVATE KEY----- client2Key = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----` -----END RSA PRIVATE KEY-----`
testFileName = "test_file_ftp.dat" testFileName = "test_file_ftp.dat"
testDLFileName = "test_download_ftp.dat" testDLFileName = "test_download_ftp.dat"

View file

@ -170,7 +170,7 @@ func searchFsEvents(w http.ResponseWriter, r *http.Request) {
return return
} }
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json")
w.Write(data) //nolint:errcheck w.Write(data) //nolint:errcheck
} }
@ -205,7 +205,7 @@ func searchProviderEvents(w http.ResponseWriter, r *http.Request) {
return return
} }
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json")
w.Write(data) //nolint:errcheck w.Write(data) //nolint:errcheck
} }
@ -238,7 +238,7 @@ func searchLogEvents(w http.ResponseWriter, r *http.Request) {
return return
} }
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json")
w.Write(data) //nolint:errcheck w.Write(data) //nolint:errcheck
} }

View file

@ -160,7 +160,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
} }
fi, err := os.Stat(inputFile) fi, err := os.Stat(inputFile)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, fmt.Errorf("invalid input_file %q", inputFile), "", http.StatusBadRequest)
return return
} }
if fi.Size() > MaxRestoreSize { if fi.Size() > MaxRestoreSize {
@ -171,7 +171,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
content, err := os.ReadFile(inputFile) content, err := os.ReadFile(inputFile)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, fmt.Errorf("invalid input_file %q", inputFile), "", http.StatusBadRequest)
return return
} }
if err := restoreBackup(content, inputFile, scanQuota, mode, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil { if err := restoreBackup(content, inputFile, scanQuota, mode, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {
@ -184,7 +184,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
func restoreBackup(content []byte, inputFile string, scanQuota, mode int, executor, ipAddress, role string) error { func restoreBackup(content []byte, inputFile string, scanQuota, mode int, executor, ipAddress, role string) error {
dump, err := dataprovider.ParseDumpData(content) dump, err := dataprovider.ParseDumpData(content)
if err != nil { if err != nil {
return util.NewValidationError(fmt.Sprintf("unable to parse backup content: %v", err)) return util.NewValidationError(fmt.Sprintf("invalid input_file %q", inputFile))
} }
if err = RestoreConfigs(dump.Configs, mode, executor, ipAddress, role); err != nil { if err = RestoreConfigs(dump.Configs, mode, executor, ipAddress, role); err != nil {

View file

@ -331,7 +331,7 @@ func isTokenInvalidated(r *http.Request) bool {
token := fn(r) token := fn(r)
if token != "" { if token != "" {
isTokenFound = true isTokenFound = true
if _, ok := invalidatedJWTTokens.Load(token); ok { if invalidatedJWTTokens.Get(token) {
return true return true
} }
} }
@ -343,11 +343,11 @@ func isTokenInvalidated(r *http.Request) bool {
func invalidateToken(r *http.Request) { func invalidateToken(r *http.Request) {
tokenString := jwtauth.TokenFromHeader(r) tokenString := jwtauth.TokenFromHeader(r)
if tokenString != "" { if tokenString != "" {
invalidatedJWTTokens.Store(tokenString, time.Now().Add(tokenDuration).UTC()) invalidatedJWTTokens.Add(tokenString, time.Now().Add(tokenDuration).UTC())
} }
tokenString = jwtauth.TokenFromCookie(r) tokenString = jwtauth.TokenFromCookie(r)
if tokenString != "" { if tokenString != "" {
invalidatedJWTTokens.Store(tokenString, time.Now().Add(tokenDuration).UTC()) invalidatedJWTTokens.Add(tokenString, time.Now().Add(tokenDuration).UTC())
} }
} }

View file

@ -28,7 +28,6 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -196,7 +195,7 @@ var (
certMgr *common.CertManager certMgr *common.CertManager
cleanupTicker *time.Ticker cleanupTicker *time.Ticker
cleanupDone chan bool cleanupDone chan bool
invalidatedJWTTokens sync.Map invalidatedJWTTokens tokenManager
csrfTokenAuth *jwtauth.JWTAuth csrfTokenAuth *jwtauth.JWTAuth
webRootPath string webRootPath string
webBasePath string webBasePath string
@ -921,6 +920,7 @@ func (c *Conf) Initialize(configDir string, isShared int) error {
} }
logger.Info(logSender, "", "initializing HTTP server with config %+v", c.getRedacted()) logger.Info(logSender, "", "initializing HTTP server with config %+v", c.getRedacted())
configurationDir = configDir configurationDir = configDir
invalidatedJWTTokens = newTokenManager(isShared)
resetCodesMgr = newResetCodeManager(isShared) resetCodesMgr = newResetCodeManager(isShared)
oidcMgr = newOIDCManager(isShared) oidcMgr = newOIDCManager(isShared)
oauth2Mgr = newOAuth2Manager(isShared) oauth2Mgr = newOAuth2Manager(isShared)
@ -1047,7 +1047,7 @@ func getServicesStatus() *ServicesStatus {
return status return status
} }
func fileServer(r chi.Router, path string, root http.FileSystem) { func fileServer(r chi.Router, path string, root http.FileSystem, disableDirectoryIndex bool) {
if path != "/" && path[len(path)-1] != '/' { if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP) r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
path += "/" path += "/"
@ -1057,7 +1057,11 @@ func fileServer(r chi.Router, path string, root http.FileSystem) {
r.Get(path, func(w http.ResponseWriter, r *http.Request) { r.Get(path, func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context()) rctx := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*") pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
fs := http.StripPrefix(pathPrefix, http.FileServer(root)) handler := http.FileServer(root)
if disableDirectoryIndex {
handler = neuter(handler)
}
fs := http.StripPrefix(pathPrefix, handler)
fs.ServeHTTP(w, r) fs.ServeHTTP(w, r)
}) })
} }
@ -1179,7 +1183,7 @@ func startCleanupTicker(duration time.Duration) {
return return
case <-cleanupTicker.C: case <-cleanupTicker.C:
counter++ counter++
cleanupExpiredJWTTokens() invalidatedJWTTokens.Cleanup()
resetCodesMgr.Cleanup() resetCodesMgr.Cleanup()
if counter%2 == 0 { if counter%2 == 0 {
oidcMgr.cleanup() oidcMgr.cleanup()
@ -1198,16 +1202,6 @@ func stopCleanupTicker() {
} }
} }
func cleanupExpiredJWTTokens() {
invalidatedJWTTokens.Range(func(key, value any) bool {
exp, ok := value.(time.Time)
if !ok || exp.Before(time.Now().UTC()) {
invalidatedJWTTokens.Delete(key)
}
return true
})
}
func getSigningKey(signingPassphrase string) []byte { func getSigningKey(signingPassphrase string) []byte {
if signingPassphrase != "" { if signingPassphrase != "" {
sk := sha256.Sum256([]byte(signingPassphrase)) sk := sha256.Sum256([]byte(signingPassphrase))

View file

@ -2627,103 +2627,6 @@ func TestEventRuleValidation(t *testing.T) {
assert.Contains(t, string(resp), "invalid Identity Provider login event") assert.Contains(t, string(resp), "invalid Identity Provider login event")
} }
func TestUserTransferLimits(t *testing.T) {
u := getTestUser()
u.TotalDataTransfer = 100
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
{
Sources: nil,
},
}
_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err, string(resp))
assert.Contains(t, string(resp), "Validation error: no data transfer limit source specified")
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
{
Sources: []string{"a"},
},
}
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err, string(resp))
assert.Contains(t, string(resp), "Validation error: could not parse data transfer limit source")
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
{
Sources: []string{"127.0.0.1/32"},
UploadDataTransfer: 120,
DownloadDataTransfer: 140,
},
{
Sources: []string{"192.168.0.0/24", "192.168.1.0/24"},
TotalDataTransfer: 400,
},
{
Sources: []string{"10.0.0.0/8"},
},
}
user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err, string(resp))
assert.Len(t, user.Filters.DataTransferLimits, 3)
assert.Equal(t, u.Filters.DataTransferLimits, user.Filters.DataTransferLimits)
up, down, total := user.GetDataTransferLimits("1.1.1.1")
assert.Equal(t, user.TotalDataTransfer*1024*1024, total)
assert.Equal(t, user.UploadDataTransfer*1024*1024, up)
assert.Equal(t, user.DownloadDataTransfer*1024*1024, down)
up, down, total = user.GetDataTransferLimits("127.0.0.1")
assert.Equal(t, user.Filters.DataTransferLimits[0].TotalDataTransfer*1024*1024, total)
assert.Equal(t, user.Filters.DataTransferLimits[0].UploadDataTransfer*1024*1024, up)
assert.Equal(t, user.Filters.DataTransferLimits[0].DownloadDataTransfer*1024*1024, down)
up, down, total = user.GetDataTransferLimits("192.168.1.6")
assert.Equal(t, user.Filters.DataTransferLimits[1].TotalDataTransfer*1024*1024, total)
assert.Equal(t, user.Filters.DataTransferLimits[1].UploadDataTransfer*1024*1024, up)
assert.Equal(t, user.Filters.DataTransferLimits[1].DownloadDataTransfer*1024*1024, down)
up, down, total = user.GetDataTransferLimits("10.1.2.3")
assert.Equal(t, user.Filters.DataTransferLimits[2].TotalDataTransfer*1024*1024, total)
assert.Equal(t, user.Filters.DataTransferLimits[2].UploadDataTransfer*1024*1024, up)
assert.Equal(t, user.Filters.DataTransferLimits[2].DownloadDataTransfer*1024*1024, down)
connID := xid.New().String()
localAddr := "::1"
conn := common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "1.1.1.2", user)
transferQuota := conn.GetTransferQuota()
assert.Equal(t, user.TotalDataTransfer*1024*1024, transferQuota.AllowedTotalSize)
assert.Equal(t, user.UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
assert.Equal(t, user.DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "127.0.0.1", user)
transferQuota = conn.GetTransferQuota()
assert.Equal(t, user.Filters.DataTransferLimits[0].TotalDataTransfer*1024*1024, transferQuota.AllowedTotalSize)
assert.Equal(t, user.Filters.DataTransferLimits[0].UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
assert.Equal(t, user.Filters.DataTransferLimits[0].DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "192.168.1.5", user)
transferQuota = conn.GetTransferQuota()
assert.Equal(t, user.Filters.DataTransferLimits[1].TotalDataTransfer*1024*1024, transferQuota.AllowedTotalSize)
assert.Equal(t, user.Filters.DataTransferLimits[1].UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
assert.Equal(t, user.Filters.DataTransferLimits[1].DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
u.UsedDownloadDataTransfer = 10 * 1024 * 1024
u.UsedUploadDataTransfer = 5 * 1024 * 1024
_, err = httpdtest.UpdateTransferQuotaUsage(u, "", http.StatusOK)
assert.NoError(t, err)
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "192.168.1.6", user)
transferQuota = conn.GetTransferQuota()
assert.Equal(t, (user.Filters.DataTransferLimits[1].TotalDataTransfer-15)*1024*1024, transferQuota.AllowedTotalSize)
assert.Equal(t, user.Filters.DataTransferLimits[1].UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
assert.Equal(t, user.Filters.DataTransferLimits[1].DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "10.8.3.4", user)
transferQuota = conn.GetTransferQuota()
assert.Equal(t, int64(0), transferQuota.AllowedTotalSize)
assert.Equal(t, int64(0), transferQuota.AllowedULSize)
assert.Equal(t, int64(0), transferQuota.AllowedDLSize)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}
func TestUserBandwidthLimits(t *testing.T) { func TestUserBandwidthLimits(t *testing.T) {
u := getTestUser() u := getTestUser()
u.UploadBandwidth = 128 u.UploadBandwidth = 128
@ -8038,7 +7941,7 @@ func TestLoaddata(t *testing.T) {
if runtime.GOOS != osWindows { if runtime.GOOS != osWindows {
err = os.Chmod(backupFilePath, 0111) err = os.Chmod(backupFilePath, 0111)
assert.NoError(t, err) assert.NoError(t, err)
_, _, err = httpdtest.Loaddata(backupFilePath, "1", "", http.StatusForbidden) _, _, err = httpdtest.Loaddata(backupFilePath, "1", "", http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
err = os.Chmod(backupFilePath, 0644) err = os.Chmod(backupFilePath, 0644)
assert.NoError(t, err) assert.NoError(t, err)
@ -8052,7 +7955,7 @@ func TestLoaddata(t *testing.T) {
configsGet, err := dataprovider.GetConfigs() configsGet, err := dataprovider.GetConfigs()
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, configs.SMTP, configsGet.SMTP) assert.Equal(t, configs.SMTP, configsGet.SMTP)
assert.Equal(t, configs.SFTPD.HostKeyAlgos, configsGet.SFTPD.HostKeyAlgos) assert.Equal(t, []string{ssh.KeyAlgoRSA}, configsGet.SFTPD.HostKeyAlgos)
assert.Len(t, configsGet.SFTPD.Moduli, 0) assert.Len(t, configsGet.SFTPD.Moduli, 0)
assert.Len(t, configsGet.SFTPD.KexAlgorithms, 0) assert.Len(t, configsGet.SFTPD.KexAlgorithms, 0)
assert.Len(t, configsGet.SFTPD.Ciphers, 0) assert.Len(t, configsGet.SFTPD.Ciphers, 0)
@ -12631,7 +12534,7 @@ func TestDefender(t *testing.T) {
req.RemoteAddr = remoteAddr req.RemoteAddr = remoteAddr
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "your IP address is banned") assert.Contains(t, rr.Body.String(), "your IP address is blocked")
req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil) req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil)
req.RequestURI = webUsersPath req.RequestURI = webUsersPath
@ -12639,7 +12542,7 @@ func TestDefender(t *testing.T) {
req.RemoteAddr = remoteAddr req.RemoteAddr = remoteAddr
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "your IP address is banned") assert.Contains(t, rr.Body.String(), "your IP address is blocked")
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil) req, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)
req.Header.Set("X-Real-IP", "127.0.0.1:2345") req.Header.Set("X-Real-IP", "127.0.0.1:2345")
@ -12647,7 +12550,7 @@ func TestDefender(t *testing.T) {
req.RemoteAddr = remoteAddr req.RemoteAddr = remoteAddr
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "your IP address is banned") assert.Contains(t, rr.Body.String(), "your IP address is blocked")
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -12870,6 +12773,8 @@ func TestWebConfigsMock(t *testing.T) {
assert.Contains(t, rr.Body.String(), ssh.CertAlgoDSAv01) // invalid algo assert.Contains(t, rr.Body.String(), ssh.CertAlgoDSAv01) // invalid algo
form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA) form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
form.Add("sftp_host_key_algos", ssh.CertAlgoRSAv01) form.Add("sftp_host_key_algos", ssh.CertAlgoRSAv01)
form.Set("sftp_kex_algos", "diffie-hellman-group18-sha512")
form.Add("sftp_kex_algos", "diffie-hellman-group16-sha512")
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
@ -12880,12 +12785,13 @@ func TestWebConfigsMock(t *testing.T) {
// check SFTP configs // check SFTP configs
configs, err := dataprovider.GetConfigs() configs, err := dataprovider.GetConfigs()
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2) assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA) assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
assert.Len(t, configs.SFTPD.Moduli, 2) assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Contains(t, configs.SFTPD.Moduli, "path 1") assert.Contains(t, configs.SFTPD.Moduli, "path 1")
assert.Contains(t, configs.SFTPD.Moduli, "path 2") assert.Contains(t, configs.SFTPD.Moduli, "path 2")
assert.Len(t, configs.SFTPD.KexAlgorithms, 1)
assert.Contains(t, configs.SFTPD.KexAlgorithms, "diffie-hellman-group16-sha512")
// invalid form action // invalid form action
form.Set("form_action", "") form.Set("form_action", "")
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
@ -12927,9 +12833,8 @@ func TestWebConfigsMock(t *testing.T) {
// check // check
configs, err = dataprovider.GetConfigs() configs, err = dataprovider.GetConfigs()
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2) assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA) assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
assert.Len(t, configs.SFTPD.Moduli, 2) assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Equal(t, "mail.example.net", configs.SMTP.Host) assert.Equal(t, "mail.example.net", configs.SMTP.Host)
assert.Equal(t, 587, configs.SMTP.Port) assert.Equal(t, 587, configs.SMTP.Port)
@ -12998,9 +12903,8 @@ func TestWebConfigsMock(t *testing.T) {
// check // check
configs, err = dataprovider.GetConfigs() configs, err = dataprovider.GetConfigs()
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2) assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA) assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
assert.Len(t, configs.SFTPD.Moduli, 2) assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Equal(t, 80, configs.ACME.HTTP01Challenge.Port) assert.Equal(t, 80, configs.ACME.HTTP01Challenge.Port)
assert.Equal(t, 7, configs.ACME.Protocols) assert.Equal(t, 7, configs.ACME.Protocols)
@ -13031,7 +12935,7 @@ func TestWebConfigsMock(t *testing.T) {
assert.Contains(t, rr.Body.String(), "Configurations updated") assert.Contains(t, rr.Body.String(), "Configurations updated")
configs, err = dataprovider.GetConfigs() configs, err = dataprovider.GetConfigs()
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2) assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
assert.Equal(t, 402, configs.ACME.HTTP01Challenge.Port) assert.Equal(t, 402, configs.ACME.HTTP01Challenge.Port)
assert.Equal(t, 1, configs.ACME.Protocols) assert.Equal(t, 1, configs.ACME.Protocols)
assert.Equal(t, domain, configs.ACME.Domain) assert.Equal(t, domain, configs.ACME.Domain)
@ -13993,7 +13897,8 @@ func TestShareUploadSingle(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
req.SetBasicAuth(defaultUsername, defaultPassword) req.SetBasicAuth(defaultUsername, defaultPassword)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "operation unsupported")
err = os.MkdirAll(filepath.Join(user.GetHomeDir(), "dir"), os.ModePerm) err = os.MkdirAll(filepath.Join(user.GetHomeDir(), "dir"), os.ModePerm)
assert.NoError(t, err) assert.NoError(t, err)
@ -14004,6 +13909,13 @@ func TestShareUploadSingle(t *testing.T) {
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "operation unsupported") assert.Contains(t, rr.Body.String(), "operation unsupported")
// only uploads to the share root dir are allowed
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "dir", "file.dat"), bytes.NewBuffer(content))
assert.NoError(t, err)
req.SetBasicAuth(defaultUsername, defaultPassword)
rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr)
share, err = dataprovider.ShareExists(objectID, user.Username) share, err = dataprovider.ShareExists(objectID, user.Username)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 2, share.UsedTokens) assert.Equal(t, 2, share.UsedTokens)
@ -14986,9 +14898,11 @@ func TestUserAPIShares(t *testing.T) {
func TestUsersAPISharesNoPasswordDisabled(t *testing.T) { func TestUsersAPISharesNoPasswordDisabled(t *testing.T) {
u := getTestUser() u := getTestUser()
u.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled} u.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled}
u.Filters.PasswordStrength = 70
u.Password = "ahpoo8baa6EeZieshies"
user, _, err := httpdtest.AddUser(u, http.StatusCreated) user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword) token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, u.Password)
assert.NoError(t, err) assert.NoError(t, err)
share := dataprovider.Share{ share := dataprovider.Share{
@ -15012,6 +14926,15 @@ func TestUsersAPISharesNoPasswordDisabled(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
setBearerForReq(req, token) setBearerForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
share.Password = "vi5eiJoovee5ya9yahpi"
asJSON, err = json.Marshal(share)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr) checkResponseCode(t, http.StatusCreated, rr)
location := rr.Header().Get("Location") location := rr.Header().Get("Location")
assert.NotEmpty(t, location) assert.NotEmpty(t, location)
@ -15051,6 +14974,13 @@ func TestUserAPIKey(t *testing.T) {
apiKey, _, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated) apiKey, _, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
adminAPIKey := dataprovider.APIKey{
Name: "testadminkey",
Scope: dataprovider.APIKeyScopeAdmin,
}
adminAPIKey, _, err = httpdtest.AddAPIKey(adminAPIKey, http.StatusCreated)
assert.NoError(t, err)
body := new(bytes.Buffer) body := new(bytes.Buffer)
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("filenames", "filenametest") part, err := writer.CreateFormFile("filenames", "filenametest")
@ -15079,6 +15009,12 @@ func TestUserAPIKey(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, dirEntries, 1) assert.Len(t, dirEntries, 1)
req, err = http.NewRequest(http.MethodGet, userDirsPath, nil)
assert.NoError(t, err)
setAPIKeyForReq(req, adminAPIKey.Key, user.Username)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
user.Status = 0 user.Status = 0
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
@ -15155,6 +15091,9 @@ func TestUserAPIKey(t *testing.T) {
_, err = httpdtest.RemoveAPIKey(apiKeyNew, http.StatusOK) _, err = httpdtest.RemoveAPIKey(apiKeyNew, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
_, err = httpdtest.RemoveAPIKey(adminAPIKey, http.StatusOK)
assert.NoError(t, err)
} }
func TestWebClientViewPDF(t *testing.T) { func TestWebClientViewPDF(t *testing.T) {
@ -19686,49 +19625,6 @@ func TestWebUserAddMock(t *testing.T) {
assert.Contains(t, rr.Body.String(), "Validation error: could not parse bandwidth limit source") assert.Contains(t, rr.Body.String(), "Validation error: could not parse bandwidth limit source")
form.Set("bandwidth_limit_sources1", "127.0.0.1/32") form.Set("bandwidth_limit_sources1", "127.0.0.1/32")
form.Set("upload_bandwidth_source1", "-1") form.Set("upload_bandwidth_source1", "-1")
form.Set("data_transfer_limit_sources0", "127.0.1.1")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "could not parse data transfer limit source")
form.Set("data_transfer_limit_sources0", "127.0.1.1/32")
form.Set("upload_data_transfer_source0", "a")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "invalid upload_data_transfer_source")
form.Set("upload_data_transfer_source0", "0")
form.Set("download_data_transfer_source0", "a")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "invalid download_data_transfer_source")
form.Set("download_data_transfer_source0", "0")
form.Set("total_data_transfer_source0", "a")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "invalid total_data_transfer_source")
form.Set("total_data_transfer_source0", "0")
form.Set("data_transfer_limit_sources10", "192.168.5.0/24, 10.8.0.0/16")
form.Set("download_data_transfer_source10", "100")
form.Set("upload_data_transfer_source10", "120")
form.Set("data_transfer_limit_sources12", "192.168.3.0/24, 10.8.2.0/24,::1/64")
form.Set("download_data_transfer_source12", "100")
form.Set("upload_data_transfer_source12", "120")
form.Set("total_data_transfer_source12", "200")
// invalid external auth cache size // invalid external auth cache size
form.Set("external_auth_cache_time", "a") form.Set("external_auth_cache_time", "a")
b, contentType, _ = getMultipartFormData(form, "", "") b, contentType, _ = getMultipartFormData(form, "", "")
@ -19850,30 +19746,6 @@ func TestWebUserAddMock(t *testing.T) {
} }
} }
} }
if assert.Len(t, newUser.Filters.DataTransferLimits, 3) {
for _, dtLimit := range newUser.Filters.DataTransferLimits {
switch len(dtLimit.Sources) {
case 3:
assert.Equal(t, "192.168.3.0/24", dtLimit.Sources[0])
assert.Equal(t, "10.8.2.0/24", dtLimit.Sources[1])
assert.Equal(t, "::1/64", dtLimit.Sources[2])
assert.Equal(t, int64(0), dtLimit.UploadDataTransfer)
assert.Equal(t, int64(0), dtLimit.DownloadDataTransfer)
assert.Equal(t, int64(200), dtLimit.TotalDataTransfer)
case 2:
assert.Equal(t, "192.168.5.0/24", dtLimit.Sources[0])
assert.Equal(t, "10.8.0.0/16", dtLimit.Sources[1])
assert.Equal(t, int64(120), dtLimit.UploadDataTransfer)
assert.Equal(t, int64(100), dtLimit.DownloadDataTransfer)
assert.Equal(t, int64(0), dtLimit.TotalDataTransfer)
case 1:
assert.Equal(t, "127.0.1.1/32", dtLimit.Sources[0])
assert.Equal(t, int64(0), dtLimit.UploadDataTransfer)
assert.Equal(t, int64(0), dtLimit.DownloadDataTransfer)
assert.Equal(t, int64(0), dtLimit.TotalDataTransfer)
}
}
}
assert.Len(t, newUser.Groups, 3) assert.Len(t, newUser.Groups, 3)
assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername) assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil) req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)
@ -22886,6 +22758,72 @@ func TestWebRole(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestNameParamSingleSlash(t *testing.T) {
err := dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
providerConf.NamingRules = 5
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
assert.NoError(t, err)
group := getTestGroup()
group.Name = "/"
form := make(url.Values)
form.Set("name", group.Name)
form.Set("description", group.Description)
form.Set("max_sessions", "0")
form.Set("quota_files", "0")
form.Set("quota_size", "0")
form.Set("upload_bandwidth", "0")
form.Set("download_bandwidth", "0")
form.Set("upload_data_transfer", "0")
form.Set("download_data_transfer", "0")
form.Set("total_data_transfer", "0")
form.Set("max_upload_file_size", "0")
form.Set("default_shares_expiration", "0")
form.Set("max_shares_expiration", "0")
form.Set("password_expiration", "0")
form.Set("password_strength", "0")
form.Set("expires_in", "0")
form.Set("external_auth_cache_time", "0")
form.Set(csrfFormToken, csrfToken)
b, contentType, err := getMultipartFormData(form, "", "")
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodPost, webGroupPath, &b)
assert.NoError(t, err)
req.Header.Set("Content-Type", contentType)
setJWTCookieForReq(req, webToken)
rr := executeRequest(req)
checkResponseCode(t, http.StatusSeeOther, rr)
groupGet, _, err := httpdtest.GetGroupByName(group.Name, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, "/", groupGet.Name)
// cleanup
req, err = http.NewRequest(http.MethodDelete, groupPath+"/"+url.PathEscape(group.Name), nil)
assert.NoError(t, err)
setBearerForReq(req, apiToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf = config.GetProviderConf()
providerConf.BackupsPath = backupsPath
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
}
func TestAddWebGroup(t *testing.T) { func TestAddWebGroup(t *testing.T) {
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err) assert.NoError(t, err)
@ -24406,7 +24344,7 @@ func TestStaticFilesMock(t *testing.T) {
req, err = http.NewRequest(http.MethodGet, location, nil) req, err = http.NewRequest(http.MethodGet, location, nil)
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusForbidden, rr)
req, err = http.NewRequest(http.MethodGet, "/openapi", nil) req, err = http.NewRequest(http.MethodGet, "/openapi", nil)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -1991,13 +1991,38 @@ func TestJWTTokenCleanup(t *testing.T) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
invalidatedJWTTokens.Store(token, time.Now().Add(-tokenDuration).UTC()) invalidatedJWTTokens.Add(token, time.Now().Add(-tokenDuration).UTC())
require.True(t, isTokenInvalidated(req)) require.True(t, isTokenInvalidated(req))
startCleanupTicker(100 * time.Millisecond) startCleanupTicker(100 * time.Millisecond)
assert.Eventually(t, func() bool { return !isTokenInvalidated(req) }, 1*time.Second, 200*time.Millisecond) assert.Eventually(t, func() bool { return !isTokenInvalidated(req) }, 1*time.Second, 200*time.Millisecond)
stopCleanupTicker() stopCleanupTicker()
} }
func TestDbTokenManager(t *testing.T) {
if !isSharedProviderSupported() {
t.Skip("this test it is not available with this provider")
}
mgr := newTokenManager(1)
dbTokenManager := mgr.(*dbTokenManager)
testToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiV2ViQWRtaW4iLCI6OjEiXSwiZXhwIjoxNjk4NjYwMDM4LCJqdGkiOiJja3ZuazVrYjF1aHUzZXRmZmhyZyIsIm5iZiI6MTY5ODY1ODgwOCwicGVybWlzc2lvbnMiOlsiKiJdLCJzdWIiOiIxNjk3ODIwNDM3NTMyIiwidXNlcm5hbWUiOiJhZG1pbiJ9.LXuFFksvnSuzHqHat6r70yR0jEulNRju7m7SaWrOfy8; csrftoken=mP0C7DqjwpAXsptO2gGCaYBkYw3oNMWB"
key := dbTokenManager.getKey(testToken)
require.Len(t, key, 64)
dbTokenManager.Add(testToken, time.Now().Add(-tokenDuration).UTC())
isInvalidated := dbTokenManager.Get(testToken)
assert.True(t, isInvalidated)
dbTokenManager.Cleanup()
isInvalidated = dbTokenManager.Get(testToken)
assert.False(t, isInvalidated)
dbTokenManager.Add(testToken, time.Now().Add(tokenDuration).UTC())
isInvalidated = dbTokenManager.Get(testToken)
assert.True(t, isInvalidated)
dbTokenManager.Cleanup()
isInvalidated = dbTokenManager.Get(testToken)
assert.True(t, isInvalidated)
err := dataprovider.DeleteSharedSession(key)
assert.NoError(t, err)
}
func TestAllowedProxyUnixDomainSocket(t *testing.T) { func TestAllowedProxyUnixDomainSocket(t *testing.T) {
b := Binding{ b := Binding{
Address: filepath.Join(os.TempDir(), "sock"), Address: filepath.Join(os.TempDir(), "sock"),
@ -3365,6 +3390,92 @@ func TestGetLogEventString(t *testing.T) {
assert.Empty(t, getLogEventString(0)) assert.Empty(t, getLogEventString(0))
} }
func TestUserQuotaUsage(t *testing.T) {
usage := userQuotaUsage{
QuotaSize: 100,
}
require.True(t, usage.HasQuotaInfo())
require.NotEmpty(t, usage.GetQuotaSize())
providerConf := dataprovider.GetProviderConfig()
quotaTracking := dataprovider.GetQuotaTracking()
providerConf.TrackQuota = 0
err := dataprovider.Close()
assert.NoError(t, err)
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
err = dataprovider.Close()
assert.NoError(t, err)
assert.False(t, usage.HasQuotaInfo())
providerConf.TrackQuota = quotaTracking
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
usage.QuotaSize = 0
assert.False(t, usage.HasQuotaInfo())
assert.Empty(t, usage.GetQuotaSize())
assert.Equal(t, 0, usage.GetQuotaSizePercentage())
assert.False(t, usage.IsQuotaSizeLow())
assert.False(t, usage.IsDiskQuotaLow())
assert.False(t, usage.IsQuotaLow())
usage.UsedQuotaSize = 9
assert.NotEmpty(t, usage.GetQuotaSize())
usage.QuotaSize = 10
assert.True(t, usage.IsQuotaSizeLow())
assert.True(t, usage.IsDiskQuotaLow())
assert.True(t, usage.IsQuotaLow())
usage.DownloadDataTransfer = 1
assert.True(t, usage.HasQuotaInfo())
assert.True(t, usage.HasTranferQuota())
assert.Empty(t, usage.GetQuotaFiles())
assert.Equal(t, 0, usage.GetQuotaFilesPercentage())
usage.QuotaFiles = 1
assert.NotEmpty(t, usage.GetQuotaFiles())
usage.QuotaFiles = 0
usage.UsedQuotaFiles = 9
assert.NotEmpty(t, usage.GetQuotaFiles())
usage.QuotaFiles = 10
usage.DownloadDataTransfer = 0
assert.True(t, usage.IsQuotaFilesLow())
assert.True(t, usage.IsDiskQuotaLow())
assert.False(t, usage.IsTotalTransferQuotaLow())
assert.False(t, usage.IsUploadTransferQuotaLow())
assert.False(t, usage.IsDownloadTransferQuotaLow())
assert.Equal(t, 0, usage.GetTotalTransferQuotaPercentage())
assert.Equal(t, 0, usage.GetUploadTransferQuotaPercentage())
assert.Equal(t, 0, usage.GetDownloadTransferQuotaPercentage())
assert.Empty(t, usage.GetTotalTransferQuota())
assert.Empty(t, usage.GetUploadTransferQuota())
assert.Empty(t, usage.GetDownloadTransferQuota())
usage.TotalDataTransfer = 3
usage.UsedUploadDataTransfer = 1 * 1048576
assert.NotEmpty(t, usage.GetTotalTransferQuota())
usage.TotalDataTransfer = 0
assert.NotEmpty(t, usage.GetTotalTransferQuota())
assert.NotEmpty(t, usage.GetUploadTransferQuota())
usage.UploadDataTransfer = 2
assert.NotEmpty(t, usage.GetUploadTransferQuota())
usage.UsedDownloadDataTransfer = 1 * 1048576
assert.NotEmpty(t, usage.GetDownloadTransferQuota())
usage.DownloadDataTransfer = 2
assert.NotEmpty(t, usage.GetDownloadTransferQuota())
assert.False(t, usage.IsTransferQuotaLow())
usage.UsedDownloadDataTransfer = 8 * 1048576
usage.TotalDataTransfer = 10
assert.True(t, usage.IsTotalTransferQuotaLow())
assert.True(t, usage.IsTransferQuotaLow())
usage.TotalDataTransfer = 0
usage.UploadDataTransfer = 0
usage.DownloadDataTransfer = 0
assert.False(t, usage.IsTransferQuotaLow())
usage.UploadDataTransfer = 10
usage.UsedUploadDataTransfer = 9 * 1048576
assert.True(t, usage.IsUploadTransferQuotaLow())
assert.True(t, usage.IsTransferQuotaLow())
usage.DownloadDataTransfer = 10
usage.UsedDownloadDataTransfer = 9 * 1048576
assert.True(t, usage.IsDownloadTransferQuotaLow())
assert.True(t, usage.IsTransferQuotaLow())
}
func isSharedProviderSupported() bool { func isSharedProviderSupported() bool {
// SQLite shares the implementation with other SQL-based provider but it makes no sense // SQLite shares the implementation with other SQL-based provider but it makes no sense
// to use it outside test cases // to use it outside test cases

View file

@ -384,6 +384,13 @@ func checkAPIKeyAuth(tokenAuth *jwtauth.JWTAuth, scope dataprovider.APIKeyScope)
sendAPIResponse(w, r, errors.New("the provided api key is not valid"), "", http.StatusBadRequest) sendAPIResponse(w, r, errors.New("the provided api key is not valid"), "", http.StatusBadRequest)
return return
} }
if k.Scope != scope {
handleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), dataprovider.ErrInvalidCredentials) //nolint:errcheck
logger.Debug(logSender, "", "unable to authenticate api key %q: invalid scope: got %d, wanted: %d",
apiKey, k.Scope, scope)
sendAPIResponse(w, r, fmt.Errorf("the provided api key is invalid for this request"), "", http.StatusForbidden)
return
}
if err := k.Authenticate(key); err != nil { if err := k.Authenticate(key); err != nil {
handleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), dataprovider.ErrInvalidCredentials) //nolint:errcheck handleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), dataprovider.ErrInvalidCredentials) //nolint:errcheck
logger.Debug(logSender, "", "unable to authenticate api key %q: %v", apiKey, err) logger.Debug(logSender, "", "unable to authenticate api key %q: %v", apiKey, err)
@ -546,3 +553,14 @@ func checkPartialAuth(w http.ResponseWriter, r *http.Request, audience string, t
} }
return nil return nil
} }
func neuter(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}

View file

@ -105,7 +105,6 @@ func (o *memoryOAuth2Manager) getPendingAuth(state string) (oauth2PendingAuth, e
} }
func (o *memoryOAuth2Manager) cleanup() { func (o *memoryOAuth2Manager) cleanup() {
logger.Debug(logSender, "", "oauth2 manager cleanup")
o.mu.Lock() o.mu.Lock()
defer o.mu.Unlock() defer o.mu.Unlock()
@ -165,6 +164,5 @@ func (o *dbOAuth2Manager) decodePendingAuthData(data any) (oauth2PendingAuth, er
} }
func (o *dbOAuth2Manager) cleanup() { func (o *dbOAuth2Manager) cleanup() {
logger.Debug(logSender, "", "oauth2 manager cleanup")
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOAuth2Auth, time.Now()) //nolint:errcheck dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOAuth2Auth, time.Now()) //nolint:errcheck
} }

View file

@ -124,7 +124,6 @@ func (o *memoryOIDCManager) updateTokenUsage(token oidcToken) {
} }
func (o *memoryOIDCManager) cleanup() { func (o *memoryOIDCManager) cleanup() {
logger.Debug(logSender, "", "oidc manager cleanup")
o.cleanupAuthRequests() o.cleanupAuthRequests()
o.cleanupTokens() o.cleanupTokens()
} }
@ -238,7 +237,6 @@ func (o *dbOIDCManager) decodeTokenData(data any) (oidcToken, error) {
} }
func (o *dbOIDCManager) cleanup() { func (o *dbOIDCManager) cleanup() {
logger.Debug(logSender, "", "oidc manager cleanup")
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCAuth, time.Now()) //nolint:errcheck dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCAuth, time.Now()) //nolint:errcheck
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCToken, time.Now()) //nolint:errcheck dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCToken, time.Now()) //nolint:errcheck
} }

View file

@ -23,6 +23,6 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
func serveStaticDir(router chi.Router, path, fsDirPath string) { func serveStaticDir(router chi.Router, path, fsDirPath string, disableDirectoryIndex bool) {
fileServer(router, path, http.Dir(fsDirPath)) fileServer(router, path, http.Dir(fsDirPath), disableDirectoryIndex)
} }

View file

@ -18,16 +18,20 @@
package httpd package httpd
import ( import (
"net/http"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/drakkan/sftpgo/v2/internal/bundle" "github.com/drakkan/sftpgo/v2/internal/bundle"
) )
func serveStaticDir(router chi.Router, path, _ string) { func serveStaticDir(router chi.Router, path, fsDirPath string, disableDirectoryIndex bool) {
switch path { switch path {
case webStaticFilesPath: case webStaticFilesPath:
fileServer(router, path, bundle.GetStaticFs()) fileServer(router, path, bundle.GetStaticFs(), disableDirectoryIndex)
case webOpenAPIPath: case webOpenAPIPath:
fileServer(router, path, bundle.GetOpenAPIFs()) fileServer(router, path, bundle.GetOpenAPIFs(), disableDirectoryIndex)
default:
fileServer(router, path, http.Dir(fsDirPath), disableDirectoryIndex)
} }
} }

View file

@ -1046,7 +1046,7 @@ func (s *httpdServer) updateContextFromCookie(r *http.Request) *http.Request {
return r return r
} }
func (s *httpdServer) checkConnection(next http.Handler) http.Handler { func (s *httpdServer) parseHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
var ip net.IP var ip net.IP
@ -1078,6 +1078,13 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
} }
} }
next.ServeHTTP(w, r)
})
}
func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
common.Connections.AddClientConnection(ipAddr) common.Connections.AddClientConnection(ipAddr)
defer common.Connections.RemoveClientConnection(ipAddr) defer common.Connections.RemoveClientConnection(ipAddr)
@ -1087,7 +1094,7 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
return return
} }
if common.IsBanned(ipAddr, common.ProtocolHTTP) { if common.IsBanned(ipAddr, common.ProtocolHTTP) {
s.sendForbiddenResponse(w, r, "your IP address is banned") s.sendForbiddenResponse(w, r, "your IP address is blocked")
return return
} }
if delay, err := common.LimitRate(common.ProtocolHTTP, ipAddr); err != nil { if delay, err := common.LimitRate(common.ProtocolHTTP, ipAddr); err != nil {
@ -1165,7 +1172,9 @@ func (s *httpdServer) redirectToWebPath(w http.ResponseWriter, r *http.Request,
} }
} }
func (s *httpdServer) isStaticFileURL(r *http.Request) bool { // The StripSlashes causes infinite redirects at the root path if used with http.FileServer.
// We also don't strip paths with more than one trailing slash, see #1434
func (s *httpdServer) mustStripSlash(r *http.Request) bool {
var urlPath string var urlPath string
rctx := chi.RouteContext(r.Context()) rctx := chi.RouteContext(r.Context())
if rctx != nil && rctx.RoutePath != "" { if rctx != nil && rctx.RoutePath != "" {
@ -1173,7 +1182,8 @@ func (s *httpdServer) isStaticFileURL(r *http.Request) bool {
} else { } else {
urlPath = r.URL.Path urlPath = r.URL.Path
} }
return !strings.HasPrefix(urlPath, webOpenAPIPath) && !strings.HasPrefix(urlPath, webStaticFilesPath) return !strings.HasSuffix(urlPath, "//") && !strings.HasPrefix(urlPath, webOpenAPIPath) &&
!strings.HasPrefix(urlPath, webStaticFilesPath) && !strings.HasPrefix(urlPath, acmeChallengeURI)
} }
func (s *httpdServer) initializeRouter() { func (s *httpdServer) initializeRouter() {
@ -1182,9 +1192,10 @@ func (s *httpdServer) initializeRouter() {
s.router = chi.NewRouter() s.router = chi.NewRouter()
s.router.Use(middleware.RequestID) s.router.Use(middleware.RequestID)
s.router.Use(s.checkConnection) s.router.Use(s.parseHeaders)
s.router.Use(logger.NewStructuredLogger(logger.GetLogger())) s.router.Use(logger.NewStructuredLogger(logger.GetLogger()))
s.router.Use(middleware.Recoverer) s.router.Use(middleware.Recoverer)
s.router.Use(s.checkConnection)
if s.binding.Security.Enabled { if s.binding.Security.Enabled {
secureMiddleware := secure.New(secure.Options{ secureMiddleware := secure.New(secure.Options{
AllowedHosts: s.binding.Security.AllowedHosts, AllowedHosts: s.binding.Security.AllowedHosts,
@ -1223,7 +1234,7 @@ func (s *httpdServer) initializeRouter() {
} }
s.router.Use(middleware.GetHead) s.router.Use(middleware.GetHead)
// StripSlashes causes infinite redirects at the root path if used with http.FileServer // StripSlashes causes infinite redirects at the root path if used with http.FileServer
s.router.Use(middleware.Maybe(middleware.StripSlashes, s.isStaticFileURL)) s.router.Use(middleware.Maybe(middleware.StripSlashes, s.mustStripSlash))
s.router.NotFound(s.notFoundHandler) s.router.NotFound(s.notFoundHandler)
@ -1237,7 +1248,7 @@ func (s *httpdServer) initializeRouter() {
if hasHTTPSRedirect { if hasHTTPSRedirect {
if p := acme.GetHTTP01WebRoot(); p != "" { if p := acme.GetHTTP01WebRoot(); p != "" {
serveStaticDir(s.router, acmeChallengeURI, p) serveStaticDir(s.router, acmeChallengeURI, p, true)
} }
} }
@ -1434,7 +1445,7 @@ func (s *httpdServer) initializeRouter() {
if s.renderOpenAPI { if s.renderOpenAPI {
s.router.Group(func(router chi.Router) { s.router.Group(func(router chi.Router) {
router.Use(compressor.Handler) router.Use(compressor.Handler)
serveStaticDir(router, webOpenAPIPath, s.openAPIPath) serveStaticDir(router, webOpenAPIPath, s.openAPIPath, false)
}) })
} }
} }
@ -1442,7 +1453,7 @@ func (s *httpdServer) initializeRouter() {
if s.enableWebAdmin || s.enableWebClient { if s.enableWebAdmin || s.enableWebClient {
s.router.Group(func(router chi.Router) { s.router.Group(func(router chi.Router) {
router.Use(compressor.Handler) router.Use(compressor.Handler)
serveStaticDir(router, webStaticFilesPath, s.staticFilesPath) serveStaticDir(router, webStaticFilesPath, s.staticFilesPath, true)
}) })
if s.binding.OIDC.isEnabled() { if s.binding.OIDC.isEnabled() {
s.router.Get(webOIDCRedirectPath, s.handleOIDCRedirect) s.router.Get(webOIDCRedirectPath, s.handleOIDCRedirect)

95
internal/httpd/token.go Normal file
View file

@ -0,0 +1,95 @@
// Copyright (C) 2019-2023 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// 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/>.
package httpd
import (
"crypto/sha256"
"encoding/hex"
"sync"
"time"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/util"
)
func newTokenManager(isShared int) tokenManager {
if isShared == 1 {
logger.Info(logSender, "", "using provider token manager")
return &dbTokenManager{}
}
logger.Info(logSender, "", "using memory token manager")
return &memoryTokenManager{}
}
type tokenManager interface {
Add(token string, expiresAt time.Time)
Get(token string) bool
Cleanup()
}
type memoryTokenManager struct {
invalidatedJWTTokens sync.Map
}
func (m *memoryTokenManager) Add(token string, expiresAt time.Time) {
m.invalidatedJWTTokens.Store(token, expiresAt)
}
func (m *memoryTokenManager) Get(token string) bool {
_, ok := m.invalidatedJWTTokens.Load(token)
return ok
}
func (m *memoryTokenManager) Cleanup() {
m.invalidatedJWTTokens.Range(func(key, value any) bool {
exp, ok := value.(time.Time)
if !ok || exp.Before(time.Now().UTC()) {
m.invalidatedJWTTokens.Delete(key)
}
return true
})
}
type dbTokenManager struct{}
func (m *dbTokenManager) getKey(token string) string {
digest := sha256.Sum256([]byte(token))
return hex.EncodeToString(digest[:])
}
func (m *dbTokenManager) Add(token string, expiresAt time.Time) {
key := m.getKey(token)
data := map[string]string{
"jwt": token,
}
session := dataprovider.Session{
Key: key,
Data: data,
Type: dataprovider.SessionTypeInvalidToken,
Timestamp: util.GetTimeAsMsSinceEpoch(expiresAt),
}
dataprovider.AddSharedSession(session) //nolint:errcheck
}
func (m *dbTokenManager) Get(token string) bool {
key := m.getKey(token)
_, err := dataprovider.GetSharedSession(key)
return err == nil
}
func (m *dbTokenManager) Cleanup() {
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeInvalidToken, time.Now()) //nolint:errcheck
}

View file

@ -122,7 +122,7 @@ const (
pageForgotPwdTitle = "SFTPGo Admin - Forgot password" pageForgotPwdTitle = "SFTPGo Admin - Forgot password"
pageResetPwdTitle = "SFTPGo Admin - Reset password" pageResetPwdTitle = "SFTPGo Admin - Reset password"
pageSetupTitle = "Create first admin user" pageSetupTitle = "Create first admin user"
defaultQueryLimit = 500 defaultQueryLimit = 1000
inversePatternType = "inverse" inversePatternType = "inverse"
) )
@ -1353,50 +1353,6 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
return permissions return permissions
} }
func getDataTransferLimitsFromPostFields(r *http.Request) ([]sdk.DataTransferLimit, error) {
var result []sdk.DataTransferLimit
for k := range r.Form {
if strings.HasPrefix(k, "data_transfer_limit_sources") {
sources := getSliceFromDelimitedValues(r.Form.Get(k), ",")
if len(sources) > 0 {
dtLimit := sdk.DataTransferLimit{
Sources: sources,
}
idx := strings.TrimPrefix(k, "data_transfer_limit_sources")
ul := r.Form.Get(fmt.Sprintf("upload_data_transfer_source%v", idx))
dl := r.Form.Get(fmt.Sprintf("download_data_transfer_source%v", idx))
total := r.Form.Get(fmt.Sprintf("total_data_transfer_source%v", idx))
if ul != "" {
dataUL, err := strconv.ParseInt(ul, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid upload_data_transfer_source%v %q: %w", idx, ul, err)
}
dtLimit.UploadDataTransfer = dataUL
}
if dl != "" {
dataDL, err := strconv.ParseInt(dl, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid download_data_transfer_source%v %q: %w", idx, dl, err)
}
dtLimit.DownloadDataTransfer = dataDL
}
if total != "" {
dataTotal, err := strconv.ParseInt(total, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid total_data_transfer_source%v %q: %w", idx, total, err)
}
dtLimit.TotalDataTransfer = dataTotal
}
result = append(result, dtLimit)
}
}
}
return result, nil
}
func getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) { func getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) {
var result []sdk.BandwidthLimit var result []sdk.BandwidthLimit
@ -1534,10 +1490,6 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
if err != nil { if err != nil {
return filters, err return filters, err
} }
dtLimits, err := getDataTransferLimitsFromPostFields(r)
if err != nil {
return filters, err
}
maxFileSize, err := util.ParseBytes(r.Form.Get("max_upload_file_size")) maxFileSize, err := util.ParseBytes(r.Form.Get("max_upload_file_size"))
if err != nil { if err != nil {
return filters, fmt.Errorf("invalid max upload file size: %w", err) return filters, fmt.Errorf("invalid max upload file size: %w", err)
@ -1558,7 +1510,6 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
filters.FTPSecurity = 1 filters.FTPSecurity = 1
} }
filters.BandwidthLimits = bwLimits filters.BandwidthLimits = bwLimits
filters.DataTransferLimits = dtLimits
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",") filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",") filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
filters.DeniedLoginMethods = r.Form["denied_login_methods"] filters.DeniedLoginMethods = r.Form["denied_login_methods"]
@ -1614,7 +1565,7 @@ func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
config.Endpoint = strings.TrimSpace(r.Form.Get("s3_endpoint")) config.Endpoint = strings.TrimSpace(r.Form.Get("s3_endpoint"))
config.StorageClass = strings.TrimSpace(r.Form.Get("s3_storage_class")) config.StorageClass = strings.TrimSpace(r.Form.Get("s3_storage_class"))
config.ACL = strings.TrimSpace(r.Form.Get("s3_acl")) config.ACL = strings.TrimSpace(r.Form.Get("s3_acl"))
config.KeyPrefix = strings.TrimSpace(r.Form.Get("s3_key_prefix")) config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("s3_key_prefix"), "/"))
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64) config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
if err != nil { if err != nil {
return config, fmt.Errorf("invalid s3 upload part size: %w", err) return config, fmt.Errorf("invalid s3 upload part size: %w", err)
@ -1650,7 +1601,7 @@ func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
config.Bucket = strings.TrimSpace(r.Form.Get("gcs_bucket")) config.Bucket = strings.TrimSpace(r.Form.Get("gcs_bucket"))
config.StorageClass = strings.TrimSpace(r.Form.Get("gcs_storage_class")) config.StorageClass = strings.TrimSpace(r.Form.Get("gcs_storage_class"))
config.ACL = strings.TrimSpace(r.Form.Get("gcs_acl")) config.ACL = strings.TrimSpace(r.Form.Get("gcs_acl"))
config.KeyPrefix = strings.TrimSpace(r.Form.Get("gcs_key_prefix")) config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("gcs_key_prefix"), "/"))
uploadPartSize, err := strconv.ParseInt(r.Form.Get("gcs_upload_part_size"), 10, 64) uploadPartSize, err := strconv.ParseInt(r.Form.Get("gcs_upload_part_size"), 10, 64)
if err == nil { if err == nil {
config.UploadPartSize = uploadPartSize config.UploadPartSize = uploadPartSize
@ -1732,7 +1683,7 @@ func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
config.AccountKey = getSecretFromFormField(r, "az_account_key") config.AccountKey = getSecretFromFormField(r, "az_account_key")
config.SASURL = getSecretFromFormField(r, "az_sas_url") config.SASURL = getSecretFromFormField(r, "az_sas_url")
config.Endpoint = strings.TrimSpace(r.Form.Get("az_endpoint")) config.Endpoint = strings.TrimSpace(r.Form.Get("az_endpoint"))
config.KeyPrefix = strings.TrimSpace(r.Form.Get("az_key_prefix")) config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("az_key_prefix"), "/"))
config.AccessTier = strings.TrimSpace(r.Form.Get("az_access_tier")) config.AccessTier = strings.TrimSpace(r.Form.Get("az_access_tier"))
config.UseEmulator = r.Form.Get("az_use_emulator") != "" config.UseEmulator = r.Form.Get("az_use_emulator") != ""
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64) config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
@ -4218,7 +4169,7 @@ func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.R
errTxt := "the OAuth2 provider returned an empty token. " + errTxt := "the OAuth2 provider returned an empty token. " +
"Some providers only return the token when the user first authorizes. " + "Some providers only return the token when the user first authorizes. " +
"If you have already registered SFTPGo with this user in the past, revoke access and try again. " + "If you have already registered SFTPGo with this user in the past, revoke access and try again. " +
"This way you will invalidate the previous token." "This way you will invalidate the previous token"
s.renderMessagePage(w, r, errorTitle, "Unable to get token:", http.StatusBadRequest, errors.New(errTxt), "") s.renderMessagePage(w, r, errorTitle, "Unable to get token:", http.StatusBadRequest, errors.New(errTxt), "")
return return
} }

View file

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"math"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -155,6 +156,7 @@ type filesPage struct {
Error string Error string
Paths []dirMapping Paths []dirMapping
HasIntegrations bool HasIntegrations bool
QuotaUsage *userQuotaUsage
} }
type shareLoginPage struct { type shareLoginPage struct {
@ -229,6 +231,185 @@ type clientSharePage struct {
IsAdd bool IsAdd bool
} }
type userQuotaUsage struct {
QuotaSize int64
QuotaFiles int
UsedQuotaSize int64
UsedQuotaFiles int
UploadDataTransfer int64
DownloadDataTransfer int64
TotalDataTransfer int64
UsedUploadDataTransfer int64
UsedDownloadDataTransfer int64
}
func (u *userQuotaUsage) HasQuotaInfo() bool {
if dataprovider.GetQuotaTracking() == 0 {
return false
}
if u.HasDiskQuota() {
return true
}
return u.HasTranferQuota()
}
func (u *userQuotaUsage) HasDiskQuota() bool {
if u.QuotaSize > 0 || u.UsedQuotaSize > 0 {
return true
}
return u.QuotaFiles > 0 || u.UsedQuotaFiles > 0
}
func (u *userQuotaUsage) HasTranferQuota() bool {
if u.TotalDataTransfer > 0 || u.UploadDataTransfer > 0 || u.DownloadDataTransfer > 0 {
return true
}
return u.UsedDownloadDataTransfer > 0 || u.UsedUploadDataTransfer > 0
}
func (u *userQuotaUsage) GetQuotaSize() string {
if u.QuotaSize > 0 {
return fmt.Sprintf("%s/%s", util.ByteCountIEC(u.UsedQuotaSize), util.ByteCountIEC(u.QuotaSize))
}
if u.UsedQuotaSize > 0 {
return util.ByteCountIEC(u.UsedQuotaSize)
}
return ""
}
func (u *userQuotaUsage) GetQuotaFiles() string {
if u.QuotaFiles > 0 {
return fmt.Sprintf("%d/%d", u.UsedQuotaFiles, u.QuotaFiles)
}
if u.UsedQuotaFiles > 0 {
return strconv.FormatInt(int64(u.UsedQuotaFiles), 10)
}
return ""
}
func (u *userQuotaUsage) GetQuotaSizePercentage() int {
if u.QuotaSize > 0 {
return int(math.Round(100 * float64(u.UsedQuotaSize) / float64(u.QuotaSize)))
}
return 0
}
func (u *userQuotaUsage) GetQuotaFilesPercentage() int {
if u.QuotaFiles > 0 {
return int(math.Round(100 * float64(u.UsedQuotaFiles) / float64(u.QuotaFiles)))
}
return 0
}
func (u *userQuotaUsage) IsQuotaSizeLow() bool {
return u.GetQuotaSizePercentage() > 85
}
func (u *userQuotaUsage) IsQuotaFilesLow() bool {
return u.GetQuotaFilesPercentage() > 85
}
func (u *userQuotaUsage) IsDiskQuotaLow() bool {
return u.IsQuotaSizeLow() || u.IsQuotaFilesLow()
}
func (u *userQuotaUsage) GetTotalTransferQuota() string {
total := u.UsedUploadDataTransfer + u.UsedDownloadDataTransfer
if u.TotalDataTransfer > 0 {
return fmt.Sprintf("%s/%s", util.ByteCountIEC(total), util.ByteCountIEC(u.TotalDataTransfer*1048576))
}
if total > 0 {
return util.ByteCountIEC(total)
}
return ""
}
func (u *userQuotaUsage) GetUploadTransferQuota() string {
if u.UploadDataTransfer > 0 {
return fmt.Sprintf("%s/%s", util.ByteCountIEC(u.UsedUploadDataTransfer),
util.ByteCountIEC(u.UploadDataTransfer*1048576))
}
if u.UsedUploadDataTransfer > 0 {
return util.ByteCountIEC(u.UsedUploadDataTransfer)
}
return ""
}
func (u *userQuotaUsage) GetDownloadTransferQuota() string {
if u.DownloadDataTransfer > 0 {
return fmt.Sprintf("%s/%s", util.ByteCountIEC(u.UsedDownloadDataTransfer),
util.ByteCountIEC(u.DownloadDataTransfer*1048576))
}
if u.UsedDownloadDataTransfer > 0 {
return util.ByteCountIEC(u.UsedDownloadDataTransfer)
}
return ""
}
func (u *userQuotaUsage) GetTotalTransferQuotaPercentage() int {
if u.TotalDataTransfer > 0 {
return int(math.Round(100 * float64(u.UsedDownloadDataTransfer+u.UsedUploadDataTransfer) / float64(u.TotalDataTransfer*1048576)))
}
return 0
}
func (u *userQuotaUsage) GetUploadTransferQuotaPercentage() int {
if u.UploadDataTransfer > 0 {
return int(math.Round(100 * float64(u.UsedUploadDataTransfer) / float64(u.UploadDataTransfer*1048576)))
}
return 0
}
func (u *userQuotaUsage) GetDownloadTransferQuotaPercentage() int {
if u.DownloadDataTransfer > 0 {
return int(math.Round(100 * float64(u.UsedDownloadDataTransfer) / float64(u.DownloadDataTransfer*1048576)))
}
return 0
}
func (u *userQuotaUsage) IsTotalTransferQuotaLow() bool {
if u.TotalDataTransfer > 0 {
return u.GetTotalTransferQuotaPercentage() > 85
}
return false
}
func (u *userQuotaUsage) IsUploadTransferQuotaLow() bool {
if u.UploadDataTransfer > 0 {
return u.GetUploadTransferQuotaPercentage() > 85
}
return false
}
func (u *userQuotaUsage) IsDownloadTransferQuotaLow() bool {
if u.DownloadDataTransfer > 0 {
return u.GetDownloadTransferQuotaPercentage() > 85
}
return false
}
func (u *userQuotaUsage) IsTransferQuotaLow() bool {
return u.IsTotalTransferQuotaLow() || u.IsUploadTransferQuotaLow() || u.IsDownloadTransferQuotaLow()
}
func (u *userQuotaUsage) IsQuotaLow() bool {
return u.IsDiskQuotaLow() || u.IsTransferQuotaLow()
}
func newUserQuotaUsage(u *dataprovider.User) *userQuotaUsage {
return &userQuotaUsage{
QuotaSize: u.QuotaSize,
QuotaFiles: u.QuotaFiles,
UsedQuotaSize: u.UsedQuotaSize,
UsedQuotaFiles: u.UsedQuotaFiles,
TotalDataTransfer: u.TotalDataTransfer,
UploadDataTransfer: u.UploadDataTransfer,
DownloadDataTransfer: u.DownloadDataTransfer,
UsedUploadDataTransfer: u.UsedUploadDataTransfer,
UsedDownloadDataTransfer: u.UsedDownloadDataTransfer,
}
}
func getFileObjectURL(baseDir, name, baseWebPath string) string { func getFileObjectURL(baseDir, name, baseWebPath string) string {
return fmt.Sprintf("%v?path=%v&_=%v", baseWebPath, url.QueryEscape(path.Join(baseDir, name)), time.Now().UTC().Unix()) return fmt.Sprintf("%v?path=%v&_=%v", baseWebPath, url.QueryEscape(path.Join(baseDir, name)), time.Now().UTC().Unix())
} }
@ -595,7 +776,7 @@ func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Req
renderClientTemplate(w, templateUploadToShare, data) renderClientTemplate(w, templateUploadToShare, data)
} }
func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user dataprovider.User, func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user *dataprovider.User,
hasIntegrations bool, hasIntegrations bool,
) { ) {
data := filesPage{ data := filesPage{
@ -615,6 +796,7 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
CanShare: user.CanManageShares(), CanShare: user.CanManageShares(),
HasIntegrations: hasIntegrations, HasIntegrations: hasIntegrations,
Paths: getDirMapping(dirName, webClientFilesPath), Paths: getDirMapping(dirName, webClientFilesPath),
QuotaUsage: newUserQuotaUsage(user),
} }
renderClientTemplate(w, templateClientFiles, data) renderClientTemplate(w, templateClientFiles, data)
} }
@ -964,11 +1146,11 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
} }
if err != nil { if err != nil {
s.renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %q: %v", name, err), s.renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %q: %v", name, err),
user, len(s.binding.WebClientIntegrations) > 0) &user, len(s.binding.WebClientIntegrations) > 0)
return return
} }
if info.IsDir() { if info.IsDir() {
s.renderFilesPage(w, r, name, "", user, len(s.binding.WebClientIntegrations) > 0) s.renderFilesPage(w, r, name, "", &user, len(s.binding.WebClientIntegrations) > 0)
return return
} }
if status, err := downloadFile(w, r, connection, name, info, false, nil); err != nil && status != 0 { if status, err := downloadFile(w, r, connection, name, info, false, nil); err != nil && status != 0 {
@ -977,7 +1159,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
s.renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "") s.renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
return return
} }
s.renderFilesPage(w, r, path.Dir(name), err.Error(), user, len(s.binding.WebClientIntegrations) > 0) s.renderFilesPage(w, r, path.Dir(name), err.Error(), &user, len(s.binding.WebClientIntegrations) > 0)
} }
} }
} }
@ -1049,7 +1231,7 @@ func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Reques
return return
} }
s.renderEditFilePage(w, r, name, b.String(), util.Contains(user.Filters.WebClient, sdk.WebClientWriteDisabled)) s.renderEditFilePage(w, r, name, b.String(), !user.CanAddFilesFromWeb(path.Dir(name)))
} }
func (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
@ -1303,8 +1485,7 @@ func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
share.Name = strings.TrimSpace(r.Form.Get("name")) share.Name = strings.TrimSpace(r.Form.Get("name"))
share.Description = r.Form.Get("description") share.Description = r.Form.Get("description")
for _, p := range r.Form["paths"] { for _, p := range r.Form["paths"] {
p = strings.TrimSpace(p) if strings.TrimSpace(p) != "" {
if p != "" {
share.Paths = append(share.Paths, p) share.Paths = append(share.Paths, p)
} }
} }

View file

@ -2487,9 +2487,6 @@ func compareUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters
if err := compareUserBandwidthLimitFilters(expected, actual); err != nil { if err := compareUserBandwidthLimitFilters(expected, actual); err != nil {
return err return err
} }
if err := compareUserDataTransferLimitFilters(expected, actual); err != nil {
return err
}
return compareUserFilePatternsFilters(expected, actual) return compareUserFilePatternsFilters(expected, actual)
} }
@ -2505,30 +2502,6 @@ func checkFilterMatch(expected []string, actual []string) bool {
return true return true
} }
func compareUserDataTransferLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
if len(expected.DataTransferLimits) != len(actual.DataTransferLimits) {
return errors.New("data transfer limits filters mismatch")
}
for idx, l := range expected.DataTransferLimits {
if actual.DataTransferLimits[idx].UploadDataTransfer != l.UploadDataTransfer {
return errors.New("data transfer limit upload_data_transfer mismatch")
}
if actual.DataTransferLimits[idx].DownloadDataTransfer != l.DownloadDataTransfer {
return errors.New("data transfer limit download_data_transfer mismatch")
}
if actual.DataTransferLimits[idx].TotalDataTransfer != l.TotalDataTransfer {
return errors.New("data transfer limit total_data_transfer mismatch")
}
for _, source := range actual.DataTransferLimits[idx].Sources {
if !util.Contains(l.Sources, source) {
return errors.New("data transfer limit source mismatch")
}
}
}
return nil
}
func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error { func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
if len(expected.BandwidthLimits) != len(actual.BandwidthLimits) { if len(expected.BandwidthLimits) != len(actual.BandwidthLimits) {
return errors.New("bandwidth limits filters mismatch") return errors.New("bandwidth limits filters mismatch")

View file

@ -14,6 +14,12 @@
package logger package logger
import (
"fmt"
"github.com/wneessen/go-mail/log"
)
const ( const (
mailLogSender = "smtpclient" mailLogSender = "smtpclient"
) )
@ -24,25 +30,37 @@ type MailAdapter struct {
} }
// Errorf emits a log at Error level // Errorf emits a log at Error level
func (l *MailAdapter) Errorf(format string, v ...any) { func (l *MailAdapter) Errorf(logMsg log.Log) {
ErrorToConsole(format, v...) format := l.getFormatString(&logMsg)
Log(LevelError, mailLogSender, l.ConnectionID, format, v...) ErrorToConsole(format, logMsg.Messages...)
Log(LevelError, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
} }
// Warnf emits a log at Warn level // Warnf emits a log at Warn level
func (l *MailAdapter) Warnf(format string, v ...any) { func (l *MailAdapter) Warnf(logMsg log.Log) {
WarnToConsole(format, v...) format := l.getFormatString(&logMsg)
Log(LevelWarn, mailLogSender, l.ConnectionID, format, v...) WarnToConsole(format, logMsg.Messages...)
Log(LevelWarn, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
} }
// Infof emits a log at Info level // Infof emits a log at Info level
func (l *MailAdapter) Infof(format string, v ...any) { func (l *MailAdapter) Infof(logMsg log.Log) {
InfoToConsole(format, v...) format := l.getFormatString(&logMsg)
Log(LevelInfo, mailLogSender, l.ConnectionID, format, v...) InfoToConsole(format, logMsg.Messages...)
Log(LevelInfo, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
} }
// Debugf emits a log at Debug level // Debugf emits a log at Debug level
func (l *MailAdapter) Debugf(format string, v ...any) { func (l *MailAdapter) Debugf(logMsg log.Log) {
DebugToConsole(format, v...) format := l.getFormatString(&logMsg)
Log(LevelDebug, mailLogSender, l.ConnectionID, format, v...) DebugToConsole(format, logMsg.Messages...)
Log(LevelDebug, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
}
func (*MailAdapter) getFormatString(logMsg *log.Log) string {
p := "C <-- S:"
if logMsg.Direction == log.DirClientToServer {
p = "C --> S:"
}
return fmt.Sprintf("%s %s", p, logMsg.Format)
} }

View file

@ -73,7 +73,15 @@ func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
// Write logs a new entry at the end of the HTTP request // Write logs a new entry at the end of the HTTP request
func (l *StructuredLoggerEntry) Write(status, bytes int, _ http.Header, elapsed time.Duration, _ any) { func (l *StructuredLoggerEntry) Write(status, bytes int, _ http.Header, elapsed time.Duration, _ any) {
metric.HTTPRequestServed(status) metric.HTTPRequestServed(status)
l.Logger.Info(). var ev *zerolog.Event
if status >= http.StatusInternalServerError {
ev = l.Logger.Error()
} else if status >= http.StatusBadRequest {
ev = l.Logger.Warn()
} else {
ev = l.Logger.Debug()
}
ev.
Timestamp(). Timestamp().
Str("sender", "httpd"). Str("sender", "httpd").
Fields(l.fields). Fields(l.fields).

View file

@ -136,7 +136,7 @@ func (p *authPlugin) initialize() error {
}) })
rpcClient, err := client.Client() rpcClient, err := client.Client()
if err != nil { if err != nil {
logger.Debug(logSender, "", "unable to get rpc client for kms plugin %q: %v", p.config.Cmd, err) logger.Debug(logSender, "", "unable to get rpc client for auth plugin %q: %v", p.config.Cmd, err)
return err return err
} }
raw, err := rpcClient.Dispense(auth.PluginName) raw, err := rpcClient.Dispense(auth.PluginName)

View file

@ -1722,7 +1722,7 @@ func TestSCPUploadFiledata(t *testing.T) {
transfer.Connection.AddTransfer(transfer) transfer.Connection.AddTransfer(transfer)
err = scpCommand.getUploadFileData(2, transfer) err = scpCommand.getUploadFileData(2, transfer)
assert.True(t, errors.Is(err, os.ErrClosed)) assert.ErrorContains(t, err, os.ErrClosed.Error())
err = os.Remove(testfile) err = os.Remove(testfile)
assert.NoError(t, err) assert.NoError(t, err)
@ -1794,7 +1794,7 @@ func TestTransferFailingReader(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
buf := make([]byte, 32) buf := make([]byte, 32)
_, err = transfer.ReadAt(buf, 0) _, err = transfer.ReadAt(buf, 0)
assert.EqualError(t, err, sftp.ErrSSHFxOpUnsupported.Error()) assert.ErrorIs(t, err, sftp.ErrSSHFxOpUnsupported)
if c, ok := transfer.(io.Closer); ok { if c, ok := transfer.(io.Closer); ok {
err = c.Close() err = c.Close()
assert.NoError(t, err) assert.NoError(t, err)
@ -1809,14 +1809,14 @@ func TestTransferFailingReader(t *testing.T) {
errRead := errors.New("read is not allowed") errRead := errors.New("read is not allowed")
tr := newTransfer(baseTransfer, nil, r, errRead) tr := newTransfer(baseTransfer, nil, r, errRead)
_, err = tr.ReadAt(buf, 0) _, err = tr.ReadAt(buf, 0)
assert.EqualError(t, err, errRead.Error()) assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
err = tr.Close() err = tr.Close()
assert.NoError(t, err) assert.NoError(t, err)
tr = newTransfer(baseTransfer, nil, nil, errRead) tr = newTransfer(baseTransfer, nil, nil, errRead)
_, err = tr.ReadAt(buf, 0) _, err = tr.ReadAt(buf, 0)
assert.EqualError(t, err, errRead.Error()) assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
err = tr.Close() err = tr.Close()
assert.NoError(t, err) assert.NoError(t, err)
@ -1984,6 +1984,15 @@ func TestLoadHostKeys(t *testing.T) {
c.HostKeys = []string{nonDefaultKeyName, rsaKeyName, ecdsaKeyName, ed25519KeyName} c.HostKeys = []string{nonDefaultKeyName, rsaKeyName, ecdsaKeyName, ed25519KeyName}
err = c.checkAndLoadHostKeys(configDir, serverConfig) err = c.checkAndLoadHostKeys(configDir, serverConfig)
assert.Error(t, err) assert.Error(t, err)
c.HostKeyAlgorithms = []string{ssh.KeyAlgoRSASHA256}
c.HostKeys = []string{ecdsaKeyName}
err = c.checkAndLoadHostKeys(configDir, serverConfig)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "server has no host keys")
}
c.HostKeyAlgorithms = preferredHostKeyAlgos
err = c.checkAndLoadHostKeys(configDir, serverConfig)
assert.NoError(t, err)
assert.FileExists(t, rsaKeyName) assert.FileExists(t, rsaKeyName)
assert.FileExists(t, ecdsaKeyName) assert.FileExists(t, ecdsaKeyName)
assert.FileExists(t, ed25519KeyName) assert.FileExists(t, ed25519KeyName)

View file

@ -65,11 +65,8 @@ var (
ssh.KeyAlgoED25519, ssh.KeyAlgoED25519,
} }
preferredHostKeyAlgos = []string{ preferredHostKeyAlgos = []string{
ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSASHA256v01, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512,
ssh.CertAlgoECDSA256v01,
ssh.CertAlgoECDSA384v01, ssh.CertAlgoECDSA521v01, ssh.CertAlgoED25519v01,
ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521, ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSASHA256,
ssh.KeyAlgoED25519, ssh.KeyAlgoED25519,
} }
supportedKexAlgos = []string{ supportedKexAlgos = []string{
@ -369,15 +366,6 @@ func (c *Configuration) Initialize(configDir string) error {
return common.ErrNoBinding return common.ErrNoBinding
} }
if err := c.checkAndLoadHostKeys(configDir, serverConfig); err != nil {
serviceStatus.HostKeys = nil
return err
}
if err := c.initializeCertChecker(configDir); err != nil {
return err
}
c.loadModuli(configDir) c.loadModuli(configDir)
sftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck // we configure valid SFTP Extensions so we cannot get an error sftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck // we configure valid SFTP Extensions so we cannot get an error
@ -385,6 +373,13 @@ func (c *Configuration) Initialize(configDir string) error {
if err := c.configureSecurityOptions(serverConfig); err != nil { if err := c.configureSecurityOptions(serverConfig); err != nil {
return err return err
} }
if err := c.checkAndLoadHostKeys(configDir, serverConfig); err != nil {
serviceStatus.HostKeys = nil
return err
}
if err := c.initializeCertChecker(configDir); err != nil {
return err
}
c.configureKeyboardInteractiveAuth(serverConfig) c.configureKeyboardInteractiveAuth(serverConfig)
c.configureLoginBanner(serverConfig, configDir) c.configureLoginBanner(serverConfig, configDir)
c.checkSSHCommands() c.checkSSHCommands()
@ -471,8 +466,6 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
return fmt.Errorf("unsupported host key algorithm %q", hostKeyAlgo) return fmt.Errorf("unsupported host key algorithm %q", hostKeyAlgo)
} }
} }
serverConfig.HostKeyAlgorithms = c.HostKeyAlgorithms
serviceStatus.HostKeyAlgos = c.HostKeyAlgorithms
if len(c.KexAlgorithms) > 0 { if len(c.KexAlgorithms) > 0 {
hasDHGroupKEX := util.Contains(supportedKexAlgos, kexDHGroupExchangeSHA256) hasDHGroupKEX := util.Contains(supportedKexAlgos, kexDHGroupExchangeSHA256)
@ -989,8 +982,18 @@ func (c *Configuration) loadModuli(configDir string) {
} }
} }
func (c *Configuration) getHostKeyAlgorithms(keyFormat string) []string {
var algos []string
for _, algo := range algorithmsForKeyFormat(keyFormat) {
if util.Contains(c.HostKeyAlgorithms, algo) {
algos = append(algos, algo)
}
}
return algos
}
// If no host keys are defined we try to use or generate the default ones. // If no host keys are defined we try to use or generate the default ones.
func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh.ServerConfig) error { func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh.ServerConfig) error { //nolint:gocyclo
if err := c.checkHostKeyAutoGeneration(configDir); err != nil { if err := c.checkHostKeyAutoGeneration(configDir); err != nil {
return err return err
} }
@ -1023,22 +1026,45 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
k := HostKey{ k := HostKey{
Path: hostKey, Path: hostKey,
Fingerprint: ssh.FingerprintSHA256(private.PublicKey()), Fingerprint: ssh.FingerprintSHA256(private.PublicKey()),
Algorithms: c.getHostKeyAlgorithms(private.PublicKey().Type()),
}
mas, err := ssh.NewSignerWithAlgorithms(private.(ssh.AlgorithmSigner), k.Algorithms)
if err != nil {
logger.Warn(logSender, "", "could not create signer for key %q with algorithms %+v: %v", k.Path, k.Algorithms, err)
logger.WarnToConsole("could not create signer for key %q with algorithms %+v: %v", k.Path, k.Algorithms, err)
continue
} }
serviceStatus.HostKeys = append(serviceStatus.HostKeys, k) serviceStatus.HostKeys = append(serviceStatus.HostKeys, k)
logger.Info(logSender, "", "Host key %q loaded, type %q, fingerprint %q", hostKey, logger.Info(logSender, "", "Host key %q loaded, type %q, fingerprint %q, algorithms %+v", hostKey,
private.PublicKey().Type(), k.Fingerprint) private.PublicKey().Type(), k.Fingerprint, k.Algorithms)
// Add private key to the server configuration. // Add private key to the server configuration.
serverConfig.AddHostKey(private) serverConfig.AddHostKey(mas)
for _, cert := range hostCertificates { for _, cert := range hostCertificates {
signer, err := ssh.NewCertSigner(cert, private) signer, err := ssh.NewCertSigner(cert.Certificate, mas)
if err == nil { if err == nil {
var algos []string
for _, algo := range algorithmsForKeyFormat(signer.PublicKey().Type()) {
if underlyingAlgo, ok := certKeyAlgoNames[algo]; ok {
if util.Contains(mas.Algorithms(), underlyingAlgo) {
algos = append(algos, algo)
}
}
}
serviceStatus.HostKeys = append(serviceStatus.HostKeys, HostKey{
Path: cert.Path,
Fingerprint: ssh.FingerprintSHA256(signer.PublicKey()),
Algorithms: algos,
})
serverConfig.AddHostKey(signer) serverConfig.AddHostKey(signer)
logger.Info(logSender, "", "Host certificate loaded for host key %q, fingerprint %q", logger.Info(logSender, "", "Host certificate loaded for host key %q, fingerprint %q, algorithms %+v",
hostKey, ssh.FingerprintSHA256(signer.PublicKey())) hostKey, ssh.FingerprintSHA256(signer.PublicKey()), algos)
} }
} }
} }
if len(serviceStatus.HostKeys) == 0 {
return errors.New("ssh: server has no host keys")
}
var fp []string var fp []string
for idx := range serviceStatus.HostKeys { for idx := range serviceStatus.HostKeys {
h := &serviceStatus.HostKeys[idx] h := &serviceStatus.HostKeys[idx]
@ -1048,8 +1074,8 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
return nil return nil
} }
func (c *Configuration) loadHostCertificates(configDir string) ([]*ssh.Certificate, error) { func (c *Configuration) loadHostCertificates(configDir string) ([]hostCertificate, error) {
var certs []*ssh.Certificate var certs []hostCertificate
for _, certPath := range c.HostCertificates { for _, certPath := range c.HostCertificates {
certPath = strings.TrimSpace(certPath) certPath = strings.TrimSpace(certPath)
if !util.IsFileInputValid(certPath) { if !util.IsFileInputValid(certPath) {
@ -1075,7 +1101,10 @@ func (c *Configuration) loadHostCertificates(configDir string) ([]*ssh.Certifica
if cert.CertType != ssh.HostCert { if cert.CertType != ssh.HostCert {
return nil, fmt.Errorf("the file %q is not an host certificate", certPath) return nil, fmt.Errorf("the file %q is not an host certificate", certPath)
} }
certs = append(certs, cert) certs = append(certs, hostCertificate{
Path: certPath,
Certificate: cert,
})
} }
return certs, nil return certs, nil
} }
@ -1312,3 +1341,14 @@ func (r *revokedCertificates) isRevoked(fp string) bool {
func Reload() error { func Reload() error {
return revokedCertManager.load() return revokedCertManager.load()
} }
func algorithmsForKeyFormat(keyFormat string) []string {
switch keyFormat {
case ssh.KeyAlgoRSA:
return []string{ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSA}
case ssh.CertAlgoRSAv01:
return []string{ssh.CertAlgoRSASHA256v01, ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSAv01}
default:
return []string{keyFormat}
}
}

View file

@ -20,6 +20,8 @@ package sftpd
import ( import (
"strings" "strings"
"time" "time"
"golang.org/x/crypto/ssh"
) )
const ( const (
@ -34,6 +36,18 @@ var (
sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"} sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
systemCommands = []string{"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"} systemCommands = []string{"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"}
serviceStatus ServiceStatus serviceStatus ServiceStatus
certKeyAlgoNames = map[string]string{
ssh.CertAlgoRSAv01: ssh.KeyAlgoRSA,
ssh.CertAlgoRSASHA256v01: ssh.KeyAlgoRSASHA256,
ssh.CertAlgoRSASHA512v01: ssh.KeyAlgoRSASHA512,
ssh.CertAlgoDSAv01: ssh.KeyAlgoDSA,
ssh.CertAlgoECDSA256v01: ssh.KeyAlgoECDSA256,
ssh.CertAlgoECDSA384v01: ssh.KeyAlgoECDSA384,
ssh.CertAlgoECDSA521v01: ssh.KeyAlgoECDSA521,
ssh.CertAlgoSKECDSA256v01: ssh.KeyAlgoSKECDSA256,
ssh.CertAlgoED25519v01: ssh.KeyAlgoED25519,
ssh.CertAlgoSKED25519v01: ssh.KeyAlgoSKED25519,
}
) )
type sshSubsystemExitStatus struct { type sshSubsystemExitStatus struct {
@ -44,10 +58,21 @@ type sshSubsystemExecMsg struct {
Command string Command string
} }
type hostCertificate struct {
Certificate *ssh.Certificate
Path string
}
// HostKey defines the details for a used host key // HostKey defines the details for a used host key
type HostKey struct { type HostKey struct {
Path string `json:"path"` Path string `json:"path"`
Fingerprint string `json:"fingerprint"` Fingerprint string `json:"fingerprint"`
Algorithms []string `json:"algorithms"`
}
// GetAlgosAsString returns the host key algorithms as comma separated string
func (h *HostKey) GetAlgosAsString() string {
return strings.Join(h.Algorithms, ", ")
} }
// ServiceStatus defines the service status // ServiceStatus defines the service status
@ -57,7 +82,6 @@ type ServiceStatus struct {
SSHCommands []string `json:"ssh_commands"` SSHCommands []string `json:"ssh_commands"`
HostKeys []HostKey `json:"host_keys"` HostKeys []HostKey `json:"host_keys"`
Authentications []string `json:"authentications"` Authentications []string `json:"authentications"`
HostKeyAlgos []string `json:"host_key_algos"`
MACs []string `json:"macs"` MACs []string `json:"macs"`
KexAlgorithms []string `json:"kex_algorithms"` KexAlgorithms []string `json:"kex_algorithms"`
Ciphers []string `json:"ciphers"` Ciphers []string `json:"ciphers"`
@ -73,11 +97,6 @@ func (s *ServiceStatus) GetSupportedAuthsAsString() string {
return strings.Join(s.Authentications, ", ") return strings.Join(s.Authentications, ", ")
} }
// GetHostKeyAlgosAsString returns the enabled host keys algorithms as comma separated string
func (s *ServiceStatus) GetHostKeyAlgosAsString() string {
return strings.Join(s.HostKeyAlgos, ", ")
}
// GetMACsAsString returns the enabled MAC algorithms as comma separated string // GetMACsAsString returns the enabled MAC algorithms as comma separated string
func (s *ServiceStatus) GetMACsAsString() string { func (s *ServiceStatus) GetMACsAsString() string {
return strings.Join(s.MACs, ", ") return strings.Join(s.MACs, ", ")

View file

@ -573,7 +573,7 @@ func TestBasicSFTPHandling(t *testing.T) {
assert.NotEmpty(t, sshCommands) assert.NotEmpty(t, sshCommands)
sshAuths := status.GetSupportedAuthsAsString() sshAuths := status.GetSupportedAuthsAsString()
assert.NotEmpty(t, sshAuths) assert.NotEmpty(t, sshAuths)
assert.NotEmpty(t, status.GetHostKeyAlgosAsString()) assert.NotEmpty(t, status.HostKeys[0].GetAlgosAsString())
assert.NotEmpty(t, status.GetMACsAsString()) assert.NotEmpty(t, status.GetMACsAsString())
assert.NotEmpty(t, status.GetKEXsAsString()) assert.NotEmpty(t, status.GetKEXsAsString())
assert.NotEmpty(t, status.GetCiphersAsString()) assert.NotEmpty(t, status.GetCiphersAsString())
@ -3786,6 +3786,17 @@ func TestExternalAuthEmptyResponse(t *testing.T) {
assert.Equal(t, 10, user.MaxSessions) assert.Equal(t, 10, user.MaxSessions)
assert.Equal(t, 100, user.QuotaFiles) assert.Equal(t, 100, user.QuotaFiles)
// the auth script accepts any password and returns an empty response, the
// user password must be updated
u.Password = defaultUsername
conn, client, err = getSftpClient(u, usePubKey)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
err = checkBasicSFTP(client)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir()) err = os.RemoveAll(user.GetHomeDir())
@ -4745,7 +4756,9 @@ func TestQuotaLimits(t *testing.T) {
err = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client) err = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)
if assert.Error(t, err) { if assert.Error(t, err) {
assert.Contains(t, err.Error(), "SSH_FX_FAILURE") assert.Contains(t, err.Error(), "SSH_FX_FAILURE")
assert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error()) if user.Username == localUser.Username {
assert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())
}
} }
_, err = client.Stat(testFileName1) _, err = client.Stat(testFileName1)
assert.Error(t, err) assert.Error(t, err)
@ -8386,6 +8399,35 @@ func TestWildcardPermissions(t *testing.T) {
assert.True(t, user.HasPerm(dataprovider.PermListItems, "/abc/a/a/a/b")) assert.True(t, user.HasPerm(dataprovider.PermListItems, "/abc/a/a/a/b"))
} }
func TestRootWildcardPerms(t *testing.T) {
user := getTestUser(true)
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermListItems}
user.Permissions["/*"] = []string{dataprovider.PermDelete}
user.Permissions["/p/*"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}
user.Permissions["/p/2"] = []string{dataprovider.PermCreateDirs}
user.Permissions["/pa"] = []string{dataprovider.PermChmod}
user.Permissions["/p/3/4"] = []string{dataprovider.PermChtimes}
assert.True(t, user.HasPerm(dataprovider.PermListItems, "/"))
assert.True(t, user.HasPerm(dataprovider.PermDelete, "/p1"))
assert.True(t, user.HasPerm(dataprovider.PermDelete, "/ppppp"))
assert.False(t, user.HasPerm(dataprovider.PermDelete, "/pa"))
assert.True(t, user.HasPerm(dataprovider.PermChmod, "/pa"))
assert.True(t, user.HasPerm(dataprovider.PermUpload, "/p/1"))
assert.True(t, user.HasPerm(dataprovider.PermUpload, "/p/p"))
assert.False(t, user.HasPerm(dataprovider.PermUpload, "/p/2"))
assert.True(t, user.HasPerm(dataprovider.PermCreateDirs, "/p/2"))
assert.True(t, user.HasPerm(dataprovider.PermCreateDirs, "/p/2/a"))
assert.True(t, user.HasPerm(dataprovider.PermDownload, "/p/3"))
assert.True(t, user.HasPerm(dataprovider.PermDownload, "/p/a/a/a"))
assert.False(t, user.HasPerm(dataprovider.PermDownload, "/p/3/4"))
assert.True(t, user.HasPerm(dataprovider.PermChtimes, "/p/3/4"))
assert.True(t, user.HasPerm(dataprovider.PermDelete, "/pb/a/a/a"))
assert.True(t, user.HasPerm(dataprovider.PermDelete, "/abc/a/a/a"))
assert.False(t, user.HasPerm(dataprovider.PermListItems, "/abc/a/a/a/b"))
assert.True(t, user.HasPerm(dataprovider.PermDelete, "/abc/a/a/a/b"))
}
func TestFilterFilePatterns(t *testing.T) { func TestFilterFilePatterns(t *testing.T) {
user := getTestUser(true) user := getTestUser(true)
pattern := sdk.PatternsFilter{ pattern := sdk.PatternsFilter{
@ -11504,7 +11546,7 @@ func getExtAuthScriptContent(user dataprovider.User, nonJSONResponse, emptyRespo
return extAuthContent return extAuthContent
} }
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%v\"; then\n", user.Username))...) extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%v\"; then\n", user.Username))...)
if len(username) > 0 { if username != "" {
user.Username = username user.Username = username
} }
u, _ := json.Marshal(user) u, _ := json.Marshal(user)

View file

@ -336,7 +336,7 @@ func GetIntFromPointer(val *int64) int64 {
// GetTimeFromPointer returns the time value or now // GetTimeFromPointer returns the time value or now
func GetTimeFromPointer(val *time.Time) time.Time { func GetTimeFromPointer(val *time.Time) time.Time {
if val == nil { if val == nil {
return time.Now() return time.Unix(0, 0)
} }
return *val return *val
} }

View file

@ -17,7 +17,7 @@ package version
import "strings" import "strings"
const version = "2.5.1-dev" const version = "2.5.7-dev"
var ( var (
commit = "" commit = ""

View file

@ -115,19 +115,6 @@ func NewS3Fs(connectionID, localTempDir, mountPath string, s3Config S3FsConfig)
awsConfig.Credentials = aws.NewCredentialsCache( awsConfig.Credentials = aws.NewCredentialsCache(
credentials.NewStaticCredentialsProvider(fs.config.AccessKey, fs.config.AccessSecret.GetPayload(), "")) credentials.NewStaticCredentialsProvider(fs.config.AccessKey, fs.config.AccessSecret.GetPayload(), ""))
} }
if fs.config.Endpoint != "" {
endpointResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...any) (aws.Endpoint, error) {
return aws.Endpoint{
URL: fs.config.Endpoint,
HostnameImmutable: fs.config.ForcePathStyle,
PartitionID: "aws",
SigningRegion: fs.config.Region,
Source: aws.EndpointSourceCustom,
}, nil
})
awsConfig.EndpointResolverWithOptions = endpointResolver
}
fs.setConfigDefaults() fs.setConfigDefaults()
if fs.config.RoleARN != "" { if fs.config.RoleARN != "" {
@ -137,6 +124,9 @@ func NewS3Fs(connectionID, localTempDir, mountPath string, s3Config S3FsConfig)
} }
fs.svc = s3.NewFromConfig(awsConfig, func(o *s3.Options) { fs.svc = s3.NewFromConfig(awsConfig, func(o *s3.Options) {
o.UsePathStyle = fs.config.ForcePathStyle o.UsePathStyle = fs.config.ForcePathStyle
if fs.config.Endpoint != "" {
o.BaseEndpoint = aws.String(fs.config.Endpoint)
}
}) })
return fs, nil return fs, nil
} }
@ -165,11 +155,11 @@ func (fs *S3Fs) Stat(name string) (os.FileInfo, error) {
// Some S3 providers (like SeaweedFS) remove the trailing '/' from object keys. // 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". // So we check some common content types to detect if this is a "directory".
isDir := util.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType)) isDir := util.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
if obj.ContentLength == 0 && !isDir { if util.GetIntFromPointer(obj.ContentLength) == 0 && !isDir {
_, err = fs.headObject(name + "/") _, err = fs.headObject(name + "/")
isDir = err == nil isDir = err == nil
} }
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, isDir, obj.ContentLength, return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, isDir, util.GetIntFromPointer(obj.ContentLength),
util.GetTimeFromPointer(obj.LastModified), false)) util.GetTimeFromPointer(obj.LastModified), false))
} }
if !fs.IsNotExist(err) { if !fs.IsNotExist(err) {
@ -195,7 +185,7 @@ func (fs *S3Fs) getStatForDir(name string) (os.FileInfo, error) {
if err != nil { if err != nil {
return result, err return result, err
} }
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, true, obj.ContentLength, return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, true, util.GetIntFromPointer(obj.ContentLength),
util.GetTimeFromPointer(obj.LastModified), false)) util.GetTimeFromPointer(obj.LastModified), false))
} }
@ -433,6 +423,7 @@ func (fs *S3Fs) ReadDir(dirname string) ([]os.FileInfo, error) {
} }
for _, fileObject := range page.Contents { for _, fileObject := range page.Contents {
objectModTime := util.GetTimeFromPointer(fileObject.LastModified) objectModTime := util.GetTimeFromPointer(fileObject.LastModified)
objectSize := util.GetIntFromPointer(fileObject.Size)
name, isDir := fs.resolve(fileObject.Key, prefix) name, isDir := fs.resolve(fileObject.Key, prefix)
if name == "" || name == "/" { if name == "" || name == "/" {
continue continue
@ -446,7 +437,7 @@ func (fs *S3Fs) ReadDir(dirname string) ([]os.FileInfo, error) {
if t, ok := modTimes[name]; ok { if t, ok := modTimes[name]; ok {
objectModTime = util.GetTimeFromMsecSinceEpoch(t) objectModTime = util.GetTimeFromMsecSinceEpoch(t)
} }
result = append(result, NewFileInfo(name, (isDir && fileObject.Size == 0), fileObject.Size, result = append(result, NewFileInfo(name, (isDir && objectSize == 0), objectSize,
objectModTime, false)) objectModTime, false))
} }
} }
@ -588,11 +579,12 @@ func (fs *S3Fs) GetDirSize(dirname string) (int, int64, error) {
} }
for _, fileObject := range page.Contents { for _, fileObject := range page.Contents {
isDir := strings.HasSuffix(util.GetStringFromPointer(fileObject.Key), "/") isDir := strings.HasSuffix(util.GetStringFromPointer(fileObject.Key), "/")
if isDir && fileObject.Size == 0 { objectSize := util.GetIntFromPointer(fileObject.Size)
if isDir && objectSize == 0 {
continue continue
} }
numFiles++ numFiles++
size += fileObject.Size size += objectSize
if numFiles%1000 == 0 { if numFiles%1000 == 0 {
fsLog(fs, logger.LevelDebug, "dirname %q scan in progress, files: %d, size: %d", dirname, numFiles, size) fsLog(fs, logger.LevelDebug, "dirname %q scan in progress, files: %d, size: %d", dirname, numFiles, size)
} }
@ -657,7 +649,8 @@ func (fs *S3Fs) Walk(root string, walkFn filepath.WalkFunc) error {
continue continue
} }
err := walkFn(util.GetStringFromPointer(fileObject.Key), err := walkFn(util.GetStringFromPointer(fileObject.Key),
NewFileInfo(name, isDir, fileObject.Size, util.GetTimeFromPointer(fileObject.LastModified), false), nil) NewFileInfo(name, isDir, util.GetIntFromPointer(fileObject.Size),
util.GetTimeFromPointer(fileObject.LastModified), false), nil)
if err != nil { if err != nil {
return err return err
} }
@ -826,10 +819,11 @@ func (fs *S3Fs) mkdirInternal(name string) error {
func (fs *S3Fs) hasContents(name string) (bool, error) { func (fs *S3Fs) hasContents(name string) (bool, error) {
prefix := fs.getPrefix(name) prefix := fs.getPrefix(name)
maxKeys := int32(2)
paginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{ paginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{
Bucket: aws.String(fs.config.Bucket), Bucket: aws.String(fs.config.Bucket),
Prefix: aws.String(prefix), Prefix: aws.String(prefix),
MaxKeys: 2, MaxKeys: &maxKeys,
}) })
if paginator.HasMorePages() { if paginator.HasMorePages() {
@ -923,7 +917,7 @@ func (fs *S3Fs) doMultipartCopy(source, target, contentType string, fileSize int
Bucket: aws.String(fs.config.Bucket), Bucket: aws.String(fs.config.Bucket),
CopySource: aws.String(source), CopySource: aws.String(source),
Key: aws.String(target), Key: aws.String(target),
PartNumber: partNum, PartNumber: &partNum,
UploadId: aws.String(uploadID), UploadId: aws.String(uploadID),
CopySourceRange: aws.String(fmt.Sprintf("bytes=%d-%d", partStart, partEnd-1)), CopySourceRange: aws.String(fmt.Sprintf("bytes=%d-%d", partStart, partEnd-1)),
}) })
@ -952,7 +946,7 @@ func (fs *S3Fs) doMultipartCopy(source, target, contentType string, fileSize int
partMutex.Lock() partMutex.Lock()
completedParts = append(completedParts, types.CompletedPart{ completedParts = append(completedParts, types.CompletedPart{
ETag: partResp.CopyPartResult.ETag, ETag: partResp.CopyPartResult.ETag,
PartNumber: partNum, PartNumber: &partNum,
}) })
partMutex.Unlock() partMutex.Unlock()
}(partNumber, start, end) }(partNumber, start, end)
@ -965,7 +959,14 @@ func (fs *S3Fs) doMultipartCopy(source, target, contentType string, fileSize int
return copyError return copyError
} }
sort.Slice(completedParts, func(i, j int) bool { sort.Slice(completedParts, func(i, j int) bool {
return completedParts[i].PartNumber < completedParts[j].PartNumber getPartNumber := func(number *int32) int32 {
if number == nil {
return 0
}
return *number
}
return getPartNumber(completedParts[i].PartNumber) < getPartNumber(completedParts[j].PartNumber)
}) })
completeCtx, completeCancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout)) completeCtx, completeCancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))

View file

@ -92,154 +92,154 @@ CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
-----END EC PRIVATE KEY-----` -----END EC PRIVATE KEY-----`
caCRT = `-----BEGIN CERTIFICATE----- caCRT = `-----BEGIN CERTIFICATE-----
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0 MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
QXV0aDAeFw0yMjA3MDQxNTQzMTFaFw0yNDAxMDQxNTUzMDhaMBMxETAPBgNVBAMT QXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4eyDJkmW CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m
D4OVYo7ddgiZkd6QQdPyLcsa31Wc9jdR2/peEabyNT8jSWteS6ouY84GRlnhfFeZ fi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1
mpXgbaUJu/Z8Y/8riPxwL8XF4vCScQDMywpQnVUd6E9x2/+/uaD4p/BBswgKqKPe v03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh
uDcHZn7MkD4QlquUhMElDrBUi1Dv/AVHnQ6iP4vd5Jlv0F+40jdq/8Wa7yhW7Pu5 yzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL
iNvPwCk8HjENBKVur/re+Acif8A2TlbCsuOnVduSQNmnWH+iZmB9upyBZtUszGS0 CdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U
JhUwtSnwUX/JapF70Pwte/PV3RK8cJ5FjuAPNeTyJvSuMTELFSAyCeiNynFGgyhW 4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb
cqbEiPu6BURLculyVkmh4dOrhTrYZv/n3UJAhyxkdYrbh3INHmTa4izvclcuwoEo KF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo
lFlJp3l77D0lIi+pbtcBV6ys7reyuxUAkBNwnpt2pWfCQoi4QYKcNbHm47c2phOb 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
WwSa13hh6yjoDQ== kK8u1YiiVenmoQ==
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
caCRL = `-----BEGIN X509 CRL----- caCRL = `-----BEGIN X509 CRL-----
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
MjIwNzA0MTU1MzU4WhcNMjQwNzAzMTU1MzU4WjAkMCICEQDZo5Q3lhxFuDUsxGNm MjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg
794YFw0yMjA3MDQxNTUzNThaoCMwITAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8Qvn ZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r
bGzjmOULwDANBgkqhkiG9w0BAQsFAAOCAgEA1lK6g8qmhyY6myx8342dDuaauY03 rl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV
0iojkxpasuYcytK6XRm96YqjZK9EETxsHHViVU0vCXES60D6wJ9gw4fTWn3WxEdx +jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199
nIwbGyjUGHh2y+R3uQsfvwxsdYvDsTLAnOLwOo68dAHWmMDZRmgTuGNoYFxVQRGR XSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE
Cn90ZR7LPLpCScclWM8FE/W1B90x3ZE8EhJiCI/WyyTh3EgshmB7A5GoDrFZfmvR jF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF
dzoTKO+F9p2XjtmgfiBE3czWQysfATmbutZUbG/ZRb89u+ZEUyPoC94mg8fhNWoX DouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD
1d5G9QAkZFHp957/5QHLq9OHNfnWXoohhebjF4VWqZH7w+RtLc8t0PIog2lX4t1o MSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4
5N/xFk9akvuoyNGg/fYuJBmN162Q0MdeYfYKDGWdXxf6fpHxVr5v2JrIx6gOwubb iYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE
cIKP22ZBv/PYOeFsAZ755lTl4OTFUjU5ZJEPD6pUc1daaIqfxsxu8gDZP92FZjsB ypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4
zaalMbh30n2OhagSMBzSLg5rE6WmBzlQX0ZN8YrW4l2Vq6twnnFHY+UyblRZS+d4 hVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94
oHBaoOaxPEkLxNZ8ulzJS4B6c4D1CXOaBEf++snVzRRUOEdX3x7TvkkrLvIsm06R cp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67
ux0L1zJb9LbZ/1rhuv70z/kIlD55sqYuRqu3RpgTgZuTERU//rYIqWd03Y5Qon8i sn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn
VoC6Yp9DPldQJrk= w7bXJGfJcIMrrKs=
-----END X509 CRL-----` -----END X509 CRL-----`
client1Crt = `-----BEGIN CERTIFICATE----- client1Crt = `-----BEGIN CERTIFICATE-----
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
7LEmxYg= MU3Bo0A=
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
client1Key = `-----BEGIN RSA PRIVATE KEY----- client1Key = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----` -----END RSA PRIVATE KEY-----`
// client 2 crt is revoked // client 2 crt is revoked
client2Crt = `-----BEGIN CERTIFICATE----- client2Crt = `-----BEGIN CERTIFICATE-----
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
enqNrFI= E0+r2+4=
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
client2Key = `-----BEGIN RSA PRIVATE KEY----- client2Key = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----` -----END RSA PRIVATE KEY-----`
testFileName = "test_file_dav.dat" testFileName = "test_file_dav.dat"
testDLFileName = "test_download_dav.dat" testDLFileName = "test_download_dav.dat"

View file

@ -33,7 +33,7 @@ paths:
200: 200:
description: successful operation description: successful operation
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/FileInfo' $ref: '#/components/schemas/FileInfo'
401: 401:
@ -350,7 +350,7 @@ paths:
200: 200:
description: successful operation description: successful operation
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
type: array type: array
items: items:
@ -384,7 +384,7 @@ paths:
200: 200:
description: successful operation description: successful operation
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
type: object type: object
properties: properties:
@ -424,7 +424,7 @@ paths:
200: 200:
description: successful operation description: successful operation
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
type: object type: object
properties: properties:
@ -459,7 +459,7 @@ paths:
200: 200:
description: successful operation description: successful operation
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/StatVFS' $ref: '#/components/schemas/StatVFS'
401: 401:
@ -479,61 +479,61 @@ components:
OKResponse: OKResponse:
description: successful operation description: successful operation
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
BadRequest: BadRequest:
description: Bad Request description: Bad Request
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
Unauthorized: Unauthorized:
description: Unauthorized description: Unauthorized
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
Forbidden: Forbidden:
description: Forbidden description: Forbidden
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
NotFound: NotFound:
description: Not Found description: Not Found
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
NotImplemented: NotImplemented:
description: Not Implemented description: Not Implemented
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
Conflict: Conflict:
description: Conflict description: Conflict
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
RequestEntityTooLarge: RequestEntityTooLarge:
description: Request Entity Too Large, max allowed size exceeded description: Request Entity Too Large, max allowed size exceeded
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
InternalServerError: InternalServerError:
description: Internal Server Error description: Internal Server Error
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
DefaultResponse: DefaultResponse:
description: Unexpected Error description: Unexpected Error
content: content:
application/json; charset=utf-8: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
schemas: schemas:

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
NFPM_VERSION=2.30.1 NFPM_VERSION=2.32.0
NFPM_ARCH=${NFPM_ARCH:-amd64} NFPM_ARCH=${NFPM_ARCH:-amd64}
if [ -z ${SFTPGO_VERSION} ] if [ -z ${SFTPGO_VERSION} ]
then then

View file

@ -45,10 +45,10 @@
} }
.bg-login-image { .bg-login-image {
background-image: url('{{.StaticURL}}{{.Branding.LoginImagePath}}'); background: url('{{.StaticURL}}{{.Branding.LoginImagePath}}');
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
padding: 1em; background-position: center;
} }
.row.login-image { .row.login-image {

View file

@ -28,12 +28,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">

View file

@ -459,7 +459,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify(data), data: JSON.stringify(data),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
$('#spinnerModal').modal('hide'); $('#spinnerModal').modal('hide');
@ -526,7 +526,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify(data), data: JSON.stringify(data),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
$('#spinnerModal').modal('hide'); $('#spinnerModal').modal('hide');

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage auto blocklist</h6> <h6 class="m-0 font-weight-bold text-primary">View and manage auto blocklist</h6>
@ -66,7 +71,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body">Do you want to remoce the selected entry?</div> <div class="modal-body">Do you want to remove the selected entry?</div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal"> <button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel Cancel

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage event actions</h6> <h6 class="m-0 font-weight-bold text-primary">View and manage event actions</h6>

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;"> <div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body"></div> <div id="successTxt" class="card-body"></div>
</div> </div>

View file

@ -28,12 +28,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Search logs</h6> <h6 class="m-0 font-weight-bold text-primary">Search logs</h6>

View file

@ -27,12 +27,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;"> <div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body"></div> <div id="successTxt" class="card-body"></div>

View file

@ -216,7 +216,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input type="text" class="form-control" id="idS3KeyPrefix" name="s3_key_prefix" placeholder="" <input type="text" class="form-control" id="idS3KeyPrefix" name="s3_key_prefix" placeholder=""
value="{{.S3Config.KeyPrefix}}" aria-describedby="S3KeyPrefixHelpBlock"> value="{{.S3Config.KeyPrefix}}" aria-describedby="S3KeyPrefixHelpBlock">
<small id="S3KeyPrefixHelpBlock" class="form-text text-muted"> <small id="S3KeyPrefixHelpBlock" class="form-text text-muted">
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". Similar to a chroot for local filesystem. Example: "somedir/subdir/".
</small> </small>
</div> </div>
</div> </div>
@ -298,7 +298,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input type="text" class="form-control" id="idGCSKeyPrefix" name="gcs_key_prefix" placeholder="" <input type="text" class="form-control" id="idGCSKeyPrefix" name="gcs_key_prefix" placeholder=""
value="{{.GCSConfig.KeyPrefix}}" aria-describedby="GCSKeyPrefixHelpBlock"> value="{{.GCSConfig.KeyPrefix}}" aria-describedby="GCSKeyPrefixHelpBlock">
<small id="GCSKeyPrefixHelpBlock" class="form-text text-muted"> <small id="GCSKeyPrefixHelpBlock" class="form-text text-muted">
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". Similar to a chroot for local filesystem. Example: "somedir/subdir/".
</small> </small>
</div> </div>
</div> </div>
@ -420,7 +420,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input type="text" class="form-control" id="idAzKeyPrefix" name="az_key_prefix" placeholder="" <input type="text" class="form-control" id="idAzKeyPrefix" name="az_key_prefix" placeholder=""
value="{{.AzBlobConfig.KeyPrefix}}" aria-describedby="AzKeyPrefixHelpBlock"> value="{{.AzBlobConfig.KeyPrefix}}" aria-describedby="AzKeyPrefixHelpBlock">
<small id="AzKeyPrefixHelpBlock" class="form-text text-muted"> <small id="AzKeyPrefixHelpBlock" class="form-text text-muted">
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". Similar to a chroot for local filesystem. Example: "somedir/subdir/".
</small> </small>
</div> </div>
</div> </div>

View file

@ -222,7 +222,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
<div class="card-body"> <div class="card-body">
<h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6> <h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
<p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories</p> <p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories. Setting a denied pattern as "*" and allowed pattern/s for the same directory you can create denied except rules, but note that if you allow a directory, everything in it will be allowed unless more specific patterns/permissions are defined.</p>
<div class="form-group row"> <div class="form-group row">
<div class="col-md-12 form_field_patterns_outer"> <div class="col-md-12 form_field_patterns_outer">
{{range $idx, $pattern := .Group.UserSettings.Filters.GetFlatFilePatterns -}} {{range $idx, $pattern := .Group.UserSettings.Filters.GetFlatFilePatterns -}}
@ -554,105 +554,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
</div> </div>
<div class="card bg-light mb-3">
<div class="card-header">
<b>Per-source data transfer limits</b>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-12 form_field_dtlimits_outer">
{{range $idx, $dtLimit := .Group.UserSettings.Filters.DataTransferLimits -}}
<div class="row form_field_dtlimits_outer_row">
<div class="form-group col-md-5">
<textarea class="form-control" id="idDataTransferLimitSources{{$idx}}" name="data_transfer_limit_sources{{$idx}}" rows="4" placeholder=""
aria-describedby="dtLimitSourcesHelpBlock{{$idx}}">{{$dtLimit.GetSourcesAsString}}</textarea>
<small id="dtLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadTransferSource{{$idx}}" name="upload_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.UploadDataTransfer}}" min="0" aria-describedby="ulDtHelpBlock{{$idx}}">
<small id="ulDtHelpBlock{{$idx}}" class="form-text text-muted">
UL (MB). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadTransferSource{{$idx}}" name="download_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.DownloadDataTransfer}}" min="0" aria-describedby="dlDtHelpBlock{{$idx}}">
<small id="dlDtHelpBlock{{$idx}}" class="form-text text-muted">
DL (MB). 0 means no limit
</small>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idTotalTransferSource{{$idx}}" name="total_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.TotalDataTransfer}}" min="0" aria-describedby="totalDtHelpBlock{{$idx}}">
<small id="totalDtHelpBlock{{$idx}}" class="form-text text-muted">
Total (MB). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_dtlimits_outer_row">
<div class="form-group col-md-5">
<textarea class="form-control" id="idDataTransferLimitSources0" name="data_transfer_limit_sources0" rows="4" placeholder=""
aria-describedby="dtLimitSourcesHelpBlock0"></textarea>
<small id="dtLimitSourcesHelpBlock0" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadTransferSource0" name="upload_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock0">
<small id="ulDtHelpBlock0" class="form-text text-muted">
UL (MB). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadTransferSource0" name="download_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock0">
<small id="dlDtHelpBlock0" class="form-text text-muted">
DL (MB). 0 means no limit
</small>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idTotalTransferSource0" name="total_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock0">
<small id="totalDtHelpBlock0" class="form-text text-muted">
Total (MB). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_dtlimit_field_btn">
<i class="fas fa-plus"></i> Add new data transfer limit
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">

View file

@ -27,12 +27,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage IP Lists</h6> <h6 class="m-0 font-weight-bold text-primary">View and manage IP Lists</h6>
@ -114,7 +119,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body">Do you want to remoce the selected entry?</div> <div class="modal-body">Do you want to remove the selected entry?</div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal"> <button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel Cancel

View file

@ -31,12 +31,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div id="successTOTPMsg" class="card mb-4 border-left-success" style="display: none;"> <div id="successTOTPMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTOTPTxt" class="card-body"></div> <div id="successTOTPTxt" class="card-body"></div>
</div> </div>
<div id="errorTOTPMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorTOTPMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTOTPTxt"></span> <span id="errorTOTPTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorTOTPMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorTOTPMsg(){
$('#errorTOTPMsg').hide();
}
</script>
<div> <div>
<p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p> <p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p>
</div> </div>
@ -93,12 +98,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div id="successRecCodesMsg" class="card mb-4 border-left-success" style="display: none;"> <div id="successRecCodesMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successRecCodesTxt" class="card-body"></div> <div id="successRecCodesTxt" class="card-body"></div>
</div> </div>
<div id="errorRecCodesMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorRecCodesMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorRecCodesTxt"></span> <span id="errorRecCodesTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorRecCodesMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorRecCodesMsg(){
$('#errorRecCodesMsg').hide();
}
</script>
<div> <div>
<p>Recovery codes are a set of one time use codes that can be used in place of the TOTP to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate TOTP configuration.</p> <p>Recovery codes are a set of one time use codes that can be used in place of the TOTP to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate TOTP configuration.</p>
<p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p> <p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>
@ -166,7 +176,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}), data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
$('.totpDisable').hide(); $('.totpDisable').hide();
@ -209,7 +219,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}), data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
totpSave(); totpSave();
@ -242,7 +252,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "secret": {"status": "Plain", "payload": $('#idSecret').text()}}), data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
$('#successTOTPTxt').text("Configuration saved"); $('#successTOTPTxt').text("Configuration saved");
@ -284,7 +294,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": false}), data: JSON.stringify({"enabled": false}),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
location.reload(); location.reload();
@ -357,7 +367,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
type: 'POST', type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
$('.viewRecoveryCodes').hide(); $('.viewRecoveryCodes').hide();

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">

View file

@ -137,58 +137,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
$(this).closest(".form_field_bwlimits_outer_row").remove(); $(this).closest(".form_field_bwlimits_outer_row").remove();
}); });
$("body").on("click", ".add_new_dtlimit_field_btn", function () {
let index = $(".form_field_dtlimits_outer").find(".form_field_dtlimits_outer_row").length;
while (document.getElementById("idDataTransferLimitSources"+index) != null){
index++;
}
$(".form_field_dtlimits_outer").append(`
<div class="row form_field_dtlimits_outer_row">
<div class="form-group col-md-5">
<textarea class="form-control" id="idDataTransferLimitSources${index}" name="data_transfer_limit_sources${index}" rows="4" placeholder=""
aria-describedby="dtLimitSourcesHelpBlock${index}"></textarea>
<small id="dtLimitSourcesHelpBlock${index}" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadTransferSource${index}" name="upload_data_transfer_source${index}"
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock${index}">
<small id="ulDtHelpBlock${index}" class="form-text text-muted">
UL (MB). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadTransferSource${index}" name="download_data_transfer_source${index}"
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock${index}">
<small id="dlDtHelpBlock${index}" class="form-text text-muted">
DL (MB). 0 means no limit
</small>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idTotalTransferSource${index}" name="total_data_transfer_source${index}"
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock${index}">
<small id="totalDtHelpBlock${index}" class="form-text text-muted">
Total (MB). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
});
$("body").on("click", ".remove_dtlimit_btn_frm_field", function () {
$(this).closest(".form_field_dtlimits_outer_row").remove();
});
$("body").on("click", ".add_new_pattern_field_btn", function () { $("body").on("click", ".add_new_pattern_field_btn", function () {
let index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length; let index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
while (document.getElementById("idPatternPath"+index) != null){ while (document.getElementById("idPatternPath"+index) != null){

View file

@ -46,10 +46,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<br> <br>
Fingerprint: "{{.Fingerprint}}" Fingerprint: "{{.Fingerprint}}"
<br> <br>
Algorithms: "{{.GetAlgosAsString}}"
<br>
{{end}} {{end}}
<br> <br>
Host Key algorithms: "{{.Status.SSH.GetHostKeyAlgosAsString}}"
<br><br>
MAC algorithms: "{{.Status.SSH.GetMACsAsString}}" MAC algorithms: "{{.Status.SSH.GetMACsAsString}}"
<br><br> <br><br>
KEX algorithms: "{{.Status.SSH.GetKEXsAsString}}" KEX algorithms: "{{.Status.SSH.GetKEXsAsString}}"

View file

@ -502,7 +502,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
<div class="card-body"> <div class="card-body">
<h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6> <h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
<p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories</p> <p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories. Setting a denied pattern as "*" and allowed pattern/s for the same directory you can create denied except rules, but note that if you allow a directory, everything in it will be allowed unless more specific patterns/permissions are defined.</p>
<div class="form-group row"> <div class="form-group row">
<div class="col-md-12 form_field_patterns_outer"> <div class="col-md-12 form_field_patterns_outer">
{{range $idx, $pattern := .User.Filters.GetFlatFilePatterns -}} {{range $idx, $pattern := .User.Filters.GetFlatFilePatterns -}}
@ -834,105 +834,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
</div> </div>
<div class="card bg-light mb-3">
<div class="card-header">
<b>Per-source data transfer limits</b>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-12 form_field_dtlimits_outer">
{{range $idx, $dtLimit := .User.Filters.DataTransferLimits -}}
<div class="row form_field_dtlimits_outer_row">
<div class="form-group col-md-5">
<textarea class="form-control" id="idDataTransferLimitSources{{$idx}}" name="data_transfer_limit_sources{{$idx}}" rows="4" placeholder=""
aria-describedby="dtLimitSourcesHelpBlock{{$idx}}">{{$dtLimit.GetSourcesAsString}}</textarea>
<small id="dtLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadTransferSource{{$idx}}" name="upload_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.UploadDataTransfer}}" min="0" aria-describedby="ulDtHelpBlock{{$idx}}">
<small id="ulDtHelpBlock{{$idx}}" class="form-text text-muted">
UL (MB). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadTransferSource{{$idx}}" name="download_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.DownloadDataTransfer}}" min="0" aria-describedby="dlDtHelpBlock{{$idx}}">
<small id="dlDtHelpBlock{{$idx}}" class="form-text text-muted">
DL (MB). 0 means no limit
</small>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idTotalTransferSource{{$idx}}" name="total_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.TotalDataTransfer}}" min="0" aria-describedby="totalDtHelpBlock{{$idx}}">
<small id="totalDtHelpBlock{{$idx}}" class="form-text text-muted">
Total (MB). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_dtlimits_outer_row">
<div class="form-group col-md-5">
<textarea class="form-control" id="idDataTransferLimitSources0" name="data_transfer_limit_sources0" rows="4" placeholder=""
aria-describedby="dtLimitSourcesHelpBlock0"></textarea>
<small id="dtLimitSourcesHelpBlock0" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadTransferSource0" name="upload_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock0">
<small id="ulDtHelpBlock0" class="form-text text-muted">
UL (MB). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadTransferSource0" name="download_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock0">
<small id="dlDtHelpBlock0" class="form-text text-muted">
DL (MB). 0 means no limit
</small>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idTotalTransferSource0" name="total_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock0">
<small id="totalDtHelpBlock0" class="form-text text-muted">
Total (MB). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_dtlimit_field_btn">
<i class="fas fa-plus"></i> Add new data transfer limit
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -28,12 +28,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;"> <div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body"></div> <div id="successTxt" class="card-body"></div>

View file

@ -42,16 +42,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header"> <div class="card-header">
<h6 class="d-flex justify-content-between align-items-center"> <h6 class="d-flex justify-content-between align-items-center">
<span class="font-weight-bold text-primary">Edit file "{{.Path}}"</span> <span class="font-weight-bold text-primary">{{- if .ReadOnly}}View{{- else}}Edit{{- end}} file "{{.Path}}"</span>
<span class="btn-toolbar"> <span class="btn-toolbar">
<a id="idBack" class="btn btn-secondary mx-1 my-1" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a> <a id="idBack" class="btn btn-secondary mx-1 my-1" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a>
{{if not .ReadOnly}} {{if not .ReadOnly}}

View file

@ -34,6 +34,82 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</style> </style>
{{end}} {{end}}
{{define "additionalnavitems"}}
{{if .QuotaUsage.HasQuotaInfo}}
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="quotaDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">Quota</span>
{{if .QuotaUsage.IsQuotaLow}}
<i class="fas fa-exclamation-triangle fa-fw"></i>
{{else}}
<i class="fas fa-info fa-fw"></i>
{{end}}
</a>
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
ria-labelledby="alertsDropdown">
<h6 class="dropdown-header">
Quota usage
</h6>
{{ if .QuotaUsage.HasDiskQuota}}
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle {{if .QuotaUsage.IsDiskQuotaLow}}bg-warning{{else}}bg-success{{end}}">
<i class="fas fa-hdd text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">Disk quota</div>
{{$size := .QuotaUsage.GetQuotaSize}}
{{$files := .QuotaUsage.GetQuotaFiles}}
{{if $size}}
{{$percentage := .QuotaUsage.GetQuotaSizePercentage}}
<span class="font-weight-bold {{if .QuotaUsage.IsQuotaSizeLow}}text-warning{{end}}">Size: {{$size}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
{{if $files}}<br>{{end}}
{{end}}
{{if $files}}
{{$percentage := .QuotaUsage.GetQuotaFilesPercentage}}
<span class="font-weight-bold {{if .QuotaUsage.IsQuotaFilesLow}}text-warning{{end}}">Files: {{$files}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
{{end}}
</div>
</a>
{{end}}
{{ if .QuotaUsage.HasTranferQuota}}
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle {{if .QuotaUsage.IsTransferQuotaLow}}bg-warning{{else}}bg-success{{end}}">
<i class="fas fa-exchange-alt text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">Transfer quota</div>
{{$total := .QuotaUsage.GetTotalTransferQuota}}
{{$upload := .QuotaUsage.GetUploadTransferQuota}}
{{$download := .QuotaUsage.GetDownloadTransferQuota}}
{{if $total}}
{{$percentage := .QuotaUsage.GetTotalTransferQuotaPercentage}}
<span class="font-weight-bold {{if .QuotaUsage.IsTotalTransferQuotaLow}}text-warning{{end}}">Total: {{$total}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
{{if or $upload $download}}<br>{{end}}
{{end}}
{{if $download}}
{{$percentage := .QuotaUsage.GetDownloadTransferQuotaPercentage}}
<span class="font-weight-bold {{if .QuotaUsage.IsDownloadTransferQuotaLow}}text-warning{{end}}">Download: {{$download}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
{{if $upload}}<br>{{end}}
{{end}}
{{if $upload}}
{{$percentage := .QuotaUsage.GetUploadTransferQuotaPercentage}}
<span class="font-weight-bold {{if .QuotaUsage.IsUploadTransferQuotaLow}}text-warning{{end}}">Upload: {{$upload}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
{{end}}
</div>
</a>
{{end}}
</div>
</li>
<div class="topbar-divider d-none d-sm-block"></div>
{{end}}
{{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
@ -1182,7 +1258,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
if (row["type"] == "1") { if (row["type"] == "1") {
return `<i class="fas fa-folder"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`; return `<i class="fas fa-folder"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
} }
if (row["size"] == "") { if (row["size"] === "") {
return `<i class="fas fa-external-link-alt"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`; return `<i class="fas fa-external-link-alt"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
} }
let icon = getIconForFile(data); let icon = getIconForFile(data);
@ -1195,7 +1271,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"data": "size", "data": "size",
"render": function (data, type, row) { "render": function (data, type, row) {
if (type === 'display') { if (type === 'display') {
if (data){ if (data || data === 0){
return fileSizeIEC(data); return fileSizeIEC(data);
} }
return ""; return "";

View file

@ -31,12 +31,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div id="successTOTPMsg" class="card mb-4 border-left-success" style="display: none;"> <div id="successTOTPMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTOTPTxt" class="card-body"></div> <div id="successTOTPTxt" class="card-body"></div>
</div> </div>
<div id="errorTOTPMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorTOTPMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTOTPTxt"></span> <span id="errorTOTPTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorTOTPMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorTOTPMsg(){
$('#errorTOTPMsg').hide();
}
</script>
<div> <div>
<p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p> <p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p>
</div> </div>
@ -118,12 +123,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div id="successRecCodesMsg" class="card mb-4 border-left-success" style="display: none;"> <div id="successRecCodesMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successRecCodesTxt" class="card-body"></div> <div id="successRecCodesTxt" class="card-body"></div>
</div> </div>
<div id="errorRecCodesMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorRecCodesMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorRecCodesTxt"></span> <span id="errorRecCodesTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorRecCodesMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorRecCodesMsg(){
$('#errorRecCodesMsg').hide();
}
</script>
<div> <div>
<p>Recovery codes are a set of one time use codes that can be used in place of the TOTP to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate TOTP configuration.</p> <p>Recovery codes are a set of one time use codes that can be used in place of the TOTP to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate TOTP configuration.</p>
<p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p> <p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>
@ -192,7 +202,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}), data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
$('.totpDisable').hide(); $('.totpDisable').hide();
@ -236,7 +246,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}), data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
totpSave(); totpSave();
@ -273,7 +283,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "protocols": protocolsArray, "secret": {"status": "Plain", "payload": $('#idSecret').text()}}), data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "protocols": protocolsArray, "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
$('#successTOTPTxt').text("Configuration saved"); $('#successTOTPTxt').text("Configuration saved");
@ -318,7 +328,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"protocols": protocolsArray}), data: JSON.stringify({"protocols": protocolsArray}),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
$('#successTOTPTxt').text("Protocols updated"); $('#successTOTPTxt').text("Protocols updated");
@ -356,7 +366,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": false}), data: JSON.stringify({"enabled": false}),
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
location.reload(); location.reload();
@ -429,7 +439,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
type: 'POST', type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=utf-8', contentType: 'application/json',
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
$('.viewRecoveryCodes').hide(); $('.viewRecoveryCodes').hide();

View file

@ -46,12 +46,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</button> </button>
</div> </div>
{{end}} {{end}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div id="tableContainer" class="table-responsive"> <div id="tableContainer" class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0"> <table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<thead> <thead>
@ -485,7 +490,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
if (row["type"] == "1") { if (row["type"] == "1") {
return `<i class="fas fa-folder"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`; return `<i class="fas fa-folder"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
} }
if (row["size"] == "") { if (row["size"] === "") {
return `<i class="fas fa-external-link-alt"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`; return `<i class="fas fa-external-link-alt"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
} }
var icon = getIconForFile(data); var icon = getIconForFile(data);
@ -498,7 +503,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"data": "size", "data": "size",
"render": function (data, type, row) { "render": function (data, type, row) {
if (type === 'display') { if (type === 'display') {
if (data){ if (data || data === 0){
return fileSizeIEC(data); return fileSizeIEC(data);
} }
return ""; return "";

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}} {{end}}
{{define "page_body"}} {{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert"> <div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span> <span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">

View file

@ -98,4 +98,16 @@ Filename: "{app}\{#MyAppExeName}"; Parameters: "service uninstall"; Flags: runhi
Filename: "netsh"; Parameters: "advfirewall firewall delete rule name=""SFTPGo Service"""; Flags: runhidden; RunOnceId: "Remove SFTPGo firewall rule" Filename: "netsh"; Parameters: "advfirewall firewall delete rule name=""SFTPGo Service"""; Flags: runhidden; RunOnceId: "Remove SFTPGo firewall rule"
[Messages] [Messages]
FinishedLabel=Setup has finished installing SFTPGo on your computer. SFTPGo should already be running as a Windows service, it uses TCP port 8080 for HTTP service and TCP port 2022 for SFTP service by default, make sure the configured ports are not used by other services or edit the configuration according to your needs. FinishedLabel=Setup has finished installing SFTPGo on your computer. SFTPGo should already be running as a Windows service, it uses TCP port 8080 for HTTP service and TCP port 2022 for SFTP service by default, make sure the configured ports are not used by other services or edit the configuration according to your needs.
[Code]
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
Code: Integer;
begin
if (FileExists(ExpandConstant('{app}\{#MyAppExeName}'))) then
begin
Exec(ExpandConstant('{app}\{#MyAppExeName}'), 'service stop', '', SW_HIDE, ewWaitUntilTerminated, Code);
end
end;