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:
push:
branches: [main]
branches: [2.5.x]
pull_request:
jobs:
@ -11,11 +11,11 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
go: ['1.20']
go: ['1.21']
os: [ubuntu-latest, macos-latest]
upload-coverage: [true]
include:
- go: '1.20'
- go: '1.21'
os: windows-latest
upload-coverage: false
@ -232,7 +232,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.21'
- name: Build
run: |
@ -268,7 +268,7 @@ jobs:
MYSQL_USER: sftpgo
MYSQL_PASSWORD: sftpgo
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-timeout 5s
--health-retries 6
@ -296,7 +296,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.21'
- name: Build
run: |
@ -409,7 +409,7 @@ jobs:
echo 'apt-get install -q -y curl gcc' >> build.sh
if [ ${{ matrix.go }} == 'latest' ]
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
echo 'GO_VERSION=${{ matrix.go }}' >> build.sh
fi
@ -452,7 +452,7 @@ jobs:
apt-get install -q -y curl gcc
if [ ${{ matrix.go }} == 'latest' ]
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
GO_VERSION=${{ matrix.go }}
fi
@ -517,9 +517,9 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.21'
- uses: actions/checkout@v3
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
version: v1.54.2

View file

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

View file

@ -5,7 +5,7 @@ on:
tags: 'v*'
env:
GO_VERSION: 1.20.5
GO_VERSION: 1.21.10
jobs:
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"
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
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 apt-get update && apt-get install --no-install-recommends -y openssh-server && rm -rf /var/lib/apt/lists/*
FROM debian:bullseye-slim
# Set to "true" to install jq and the optional git and rsync dependencies
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

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"
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
WORKDIR /workspace
@ -32,7 +32,7 @@ FROM alpine:3.18
# Set to "true" to install jq and the optional git and rsync dependencies
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

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.
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.
@ -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.
- 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.
- LDAP/Active Directory authentication using a [plugin](https://github.com/sftpgo/sftpgo-plugin-auth).
- 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.
- 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
- [v2.5.1, v2.5, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile)
- [v2.5.1-plugins, v2.5-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile)
- [v2.5.1-alpine, v2.5-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile.alpine)
- [v2.5.1-slim, v2.5-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.5.1/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, v2.5, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile)
- [v2.5.6-plugins, v2.5-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile)
- [v2.5.6-alpine, v2.5-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile.alpine)
- [v2.5.6-slim, v2.5-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile)
- [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-plugins](../Dockerfile)
- [edge-alpine](../Dockerfile.alpine)

View file

@ -17,9 +17,23 @@ esac
echo "download plugins for arch ${SUFFIX}"
for PLUGIN in geoipfilter kms pubsub eventstore eventsearch metadata
do
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}"
chmod 755 "/usr/local/bin/sftpgo-plugin-${PLUGIN}"
done
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"
chmod 755 "/usr/local/bin/sftpgo-plugin-geoipfilter"
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-kms"
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`
- `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_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.
- `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.
- `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.

225
go.mod
View file

@ -3,168 +3,181 @@ module github.com/drakkan/sftpgo/v2
go 1.20
require (
cloud.google.com/go/storage v1.30.1
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0
cloud.google.com/go/storage v1.36.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1
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/aws/aws-sdk-go-v2 v1.18.0
github.com/aws/aws-sdk-go-v2/config v1.18.25
github.com/aws/aws-sdk-go-v2/credentials v1.13.24
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.19.8
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0
github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/cockroachdb/cockroach-go/v2 v2.3.4
github.com/coreos/go-oidc/v3 v3.6.0
github.com/aws/aws-sdk-go-v2 v1.24.0
github.com/aws/aws-sdk-go-v2/config v1.26.1
github.com/aws/aws-sdk-go-v2/credentials v1.16.12
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.5
github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.25.5
github.com/aws/aws-sdk-go-v2/service/sts v1.26.5
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/cockroachdb/cockroach-go/v2 v2.3.5
github.com/coreos/go-oidc/v3 v3.9.0
github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8
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/go-acme/lego/v4 v4.12.1
github.com/go-chi/chi/v5 v5.0.9-0.20230502103705-7f280968675b
github.com/go-chi/jwtauth/v5 v5.1.0
github.com/go-chi/render v1.0.2
github.com/go-acme/lego/v4 v4.14.2
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/jwtauth/v5 v5.3.0
github.com/go-chi/render v1.0.3
github.com/go-sql-driver/mysql v1.7.1
github.com/golang/mock v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.3.0
github.com/hashicorp/go-hclog v1.5.0
github.com/hashicorp/go-plugin v1.4.10
github.com/hashicorp/go-retryablehttp v0.7.4
github.com/jackc/pgx/v5 v5.3.2-0.20230603125928-d9560c78b8e6
github.com/google/uuid v1.5.0
github.com/hashicorp/go-hclog v1.6.2
github.com/hashicorp/go-plugin v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.5
github.com/jackc/pgx/v5 v5.4.3
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.16.5
github.com/lestrrat-go/jwx/v2 v2.0.9
github.com/klauspost/compress v1.17.4
github.com/lestrrat-go/jwx/v2 v2.0.18
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mattn/go-sqlite3 v1.14.17
github.com/mhale/smtpd v0.8.0
github.com/mattn/go-sqlite3 v1.14.19
github.com/mhale/smtpd v0.8.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/pkg/sftp v1.13.6-0.20230213180117-971c283182b6
github.com/pkg/sftp v1.13.6
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/rs/cors v1.9.0
github.com/rs/cors v1.10.1
github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.29.1
github.com/sftpgo/sdk v0.1.5-0.20230524172149-afb96ebee860
github.com/shirou/gopsutil/v3 v3.23.5
github.com/spf13/afero v1.9.5
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.16.0
github.com/rs/zerolog v1.31.0
github.com/sftpgo/sdk v0.1.6
github.com/shirou/gopsutil/v3 v3.23.11
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.1
github.com/stretchr/testify v1.8.4
github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2
github.com/subosito/gotenv v1.4.2
github.com/studio-b12/gowebdav v0.9.0
github.com/subosito/gotenv v1.6.0
github.com/unrolled/secure v1.13.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
go.etcd.io/bbolt v1.3.7
go.uber.org/automaxprocs v1.5.2
gocloud.dev v0.29.0
golang.org/x/crypto v0.9.0
golang.org/x/net v0.10.0
golang.org/x/oauth2 v0.8.0
golang.org/x/sys v0.8.0
golang.org/x/term v0.8.0
golang.org/x/time v0.3.0
google.golang.org/api v0.125.0
go.etcd.io/bbolt v1.3.8
go.uber.org/automaxprocs v1.5.3
gocloud.dev v0.35.0
golang.org/x/crypto v0.17.0
golang.org/x/net v0.19.0
golang.org/x/oauth2 v0.15.0
golang.org/x/sys v0.15.0
golang.org/x/term v0.15.0
golang.org/x/time v0.5.0
google.golang.org/api v0.154.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
cloud.google.com/go v0.110.2 // indirect
cloud.google.com/go/compute v1.20.0 // indirect
cloud.google.com/go v0.111.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/iam v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
cloud.google.com/go/iam v1.1.5 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal 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/internal/configsources v1.1.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // 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/checksum v1.1.28 // 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/s3shared v1.14.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
github.com/aws/smithy-go v1.13.5 // 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.2.9 // 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.7.2 // 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.10.4 // 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.10.9 // 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.18.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // 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/fatih/color v1.15.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // 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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // 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.4 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect
github.com/googleapis/gax-go/v2 v2.10.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // 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/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/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/miekg/dns v1.1.54 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // 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/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.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/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.9.3 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sync v0.5.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/yaml.v3 v3.0.1 // indirect
)
@ -172,5 +185,5 @@ require (
replace (
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
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) {
bcryptPassword := "bcryptpassword"
for i := 0; i < b.N; i++ {

View file

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

View file

@ -28,6 +28,7 @@ import (
"github.com/rs/xid"
"github.com/sftpgo/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/kms"
@ -647,7 +648,7 @@ func TestFsFileCopier(t *testing.T) {
assert.True(t, ok)
}
func TestFilterListDirs(t *testing.T) {
func TestFilePatterns(t *testing.T) {
filters := dataprovider.UserFilters{
BaseUserFilters: sdk.BaseUserFilters{
FilePatterns: []sdk.PatternsFilter{
@ -661,6 +662,16 @@ func TestFilterListDirs(t *testing.T) {
DenyPolicy: sdk.DenyPolicyHide,
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.jpg", false, 123, time.Now(), false),
}
// dirContents are modified in place, we need to redefine them each time
filtered := user.FilterListDir(dirContents, "/dir1")
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")
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{
vfs.NewFileInfo("file1.txt", 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")
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")
if assert.Len(t, filtered, 1) {
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))
filtered = user.FilterListDir(dirContents, "/dir3")
if assert.Len(t, filtered, 1) {
assert.Equal(t, "ic35", filtered[0].Name())
require.Len(t, filtered, 1)
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 {
h.Set("Content-Disposition",
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))
if contentType == "" {
contentType = "application/octet-stream"
@ -1525,7 +1526,6 @@ func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
user.Filters.DisableFsChecks = false
user.Filters.FilePatterns = nil
user.Filters.BandwidthLimits = nil
user.Filters.DataTransferLimits = nil
for k := range user.Permissions {
user.Permissions[k] = []string{dataprovider.PermAny}
}

View file

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

View file

@ -299,6 +299,8 @@ func NewCertManager(keyPairs []TLSKeyPair, configDir, logSender string) (*CertMa
}
randSecs := rand.Intn(59)
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
}

View file

@ -16,6 +16,8 @@ package common
import (
"errors"
"fmt"
"io/fs"
"path"
"sync"
"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
// well-known type of error
func (t *BaseTransfer) ConvertError(err error) error {
if t.Fs.IsNotExist(err) {
return t.Connection.GetNotExistError()
} else if t.Fs.IsPermission(err) {
return t.Connection.GetPermissionDeniedError()
var pathError *fs.PathError
if errors.As(err, &pathError) {
return fmt.Errorf("%s %s: %s", pathError.Op, t.GetVirtualPath(), pathError.Err.Error())
}
return err
return t.Connection.GetFsError(t.Fs, err)
}
// CheckRead returns an error if read if not allowed

View file

@ -16,6 +16,7 @@ package common
import (
"errors"
"fmt"
"os"
"path/filepath"
"testing"
@ -226,6 +227,13 @@ func TestTransferErrors(t *testing.T) {
conn := NewBaseConnection("id", ProtocolSFTP, "", "", u)
transfer := NewBaseTransfer(file, conn, nil, testFile, testFile, "/transfer_test_file", TransferUpload,
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)
assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
err = transfer.ConvertError(os.ErrPermission)
@ -332,41 +340,22 @@ func TestFTPMode(t *testing.T) {
func TestTransferQuota(t *testing.T) {
user := dataprovider.User{
BaseUser: sdk.BaseUser{
TotalDataTransfer: -1,
UploadDataTransfer: -1,
DownloadDataTransfer: -1,
TotalDataTransfer: 3,
UploadDataTransfer: 2,
DownloadDataTransfer: 1,
},
}
user.Filters.DataTransferLimits = []sdk.DataTransferLimit{
{
Sources: []string{"127.0.0.1/32", "192.168.1.0/24"},
TotalDataTransfer: 100,
UploadDataTransfer: 0,
DownloadDataTransfer: 0,
},
{
Sources: []string{"172.16.0.0/24"},
TotalDataTransfer: 0,
UploadDataTransfer: 120,
DownloadDataTransfer: 150,
},
}
ul, dl, total := user.GetDataTransferLimits("127.0.1.1")
ul, dl, total := user.GetDataTransferLimits()
assert.Equal(t, int64(2*1048576), ul)
assert.Equal(t, int64(1*1048576), dl)
assert.Equal(t, int64(3*1048576), total)
user.TotalDataTransfer = -1
user.UploadDataTransfer = -1
user.DownloadDataTransfer = -1
ul, dl, total = user.GetDataTransferLimits()
assert.Equal(t, int64(0), ul)
assert.Equal(t, int64(0), dl)
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{}
assert.True(t, transferQuota.HasDownloadSpace())
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,
dlSize int64,
) bool {
ulQuota, dlQuota, totalQuota := user.GetDataTransferLimits(transfer.IP)
ulQuota, dlQuota, totalQuota := user.GetDataTransferLimits()
if totalQuota > 0 {
allowedSize := totalQuota - (user.UsedUploadDataTransfer + user.UsedDownloadDataTransfer)
if ulSize+dlSize > allowedSize {

View file

@ -592,7 +592,7 @@ func TestDataTransferExceeded(t *testing.T) {
func TestGetUsersForQuotaCheck(t *testing.T) {
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
}
@ -600,7 +600,7 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, users, 0)
for i := 0; i < 40; i++ {
for i := 0; i < 60; i++ {
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: fmt.Sprintf("user%v", i),
@ -622,17 +622,6 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
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, "", "", "")
assert.NoError(t, err)
@ -642,7 +631,7 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
users, err = dataprovider.GetUsersForQuotaCheck(usersToFetch)
assert.NoError(t, err)
assert.Len(t, users, 40)
assert.Len(t, users, 60)
for _, user := range users {
userIdxStr := strings.Replace(user.Username, "user", "", 1)
@ -660,17 +649,13 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
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), dl)
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), "", "", "")
assert.NoError(t, err)
err = dataprovider.DeleteFolder(fmt.Sprintf("f%v", i), "", "", "")

View file

@ -1961,6 +1961,11 @@ func getCommandConfigsFromEnv(idx int) {
cfg.Env = env
}
args, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__ARGS", idx))
if ok {
cfg.Args = args
}
if cfg.Path != "" {
if len(globalConf.CommandConfig.Commands) > idx {
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__TIMEOUT", "20")
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ENV", "e=f")
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ARGS", "arg1, arg2")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_COMMAND__TIMEOUT")
os.Unsetenv("SFTPGO_COMMAND__ENV")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__PATH")
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)
@ -945,6 +949,7 @@ func TestCommandsFromEnv(t *testing.T) {
require.Equal(t, "cmd2", commandConfig.Commands[1].Path)
require.Equal(t, 20, commandConfig.Commands[1].Timeout)
require.Equal(t, []string{"e=f"}, commandConfig.Commands[1].Env)
require.Equal(t, []string{"arg1", "arg2"}, commandConfig.Commands[1].Args)
err = os.Remove(configFilePath)
assert.NoError(t, err)

View file

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

View file

@ -28,10 +28,9 @@ import (
// Supported values for host keys, KEXs, ciphers, MACs
var (
supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01}
supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA}
supportedKexAlgos = []string{
"diffie-hellman-group16-sha512", "diffie-hellman-group18-sha512",
"diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
"diffie-hellman-group16-sha512", "diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha256", "diffie-hellman-group-exchange-sha1",
}
supportedCiphers = []string{
@ -98,16 +97,28 @@ func (c *SFTPDConfigs) GetModuliAsString() string {
}
func (c *SFTPDConfigs) validate() error {
var hostKeyAlgos []string
for _, algo := range c.HostKeyAlgos {
if algo == ssh.CertAlgoRSAv01 {
continue
}
if !util.Contains(supportedHostKeyAlgos, 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 {
if algo == "diffie-hellman-group18-sha512" {
continue
}
if !util.Contains(supportedKexAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
}
kexAlgos = append(kexAlgos, algo)
}
c.KexAlgorithms = kexAlgos
for _, cipher := range c.Ciphers {
if !util.Contains(supportedCiphers, cipher) {
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)
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
}
@ -2943,26 +2931,6 @@ func validateBandwidthLimitsFilter(filters *sdk.BaseUserFilters) error {
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) {
if filters.StartDirectory != "" {
filters.StartDirectory = util.CleanPath(filters.StartDirectory)
@ -2998,9 +2966,6 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
if err := validateBandwidthLimitsFilter(filters); err != nil {
return err
}
if err := validateTransferLimitsFilter(filters); err != nil {
return err
}
if len(filters.DeniedLoginMethods) >= len(ValidLoginMethods) {
return util.NewValidationError("invalid denied_login_methods")
}
@ -3525,7 +3490,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
return 0, err
}
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()
if err != nil {
@ -3535,16 +3500,20 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
if err != nil {
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) {
return 1, nil
}
err = user.Filters.TOTPConfig.Secret.TryDecrypt()
err := user.Filters.TOTPConfig.Secret.TryDecrypt()
if err != nil {
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
user.Username, protocol, err)
return 0, err
}
answers, err = client("", "", []string{"Authentication code: "}, []bool{false})
answers, err := client("", "", []string{"Authentication code: "}, []bool{false})
if err != nil {
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) {
if err := user.LoadAndApplyGroupSettings(); err != nil {
return *user, err
}
var authResult int
var err error
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
authResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol)
} else if authHook != "" {
if strings.HasPrefix(authHook, "http") {
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol)
if !user.Filters.Hooks.ExternalAuthDisabled {
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
authResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol)
if authResult == 1 && err == nil {
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 {
authResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip, protocol)
authResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol)
}
} else {
authResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol)
@ -3792,10 +3771,6 @@ func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardI
if authResult != 1 {
return *user, fmt.Errorf("keyboard interactive auth failed, result: %v", authResult)
}
err = user.LoadAndApplyGroupSettings()
if err != nil {
return *user, err
}
err = user.CheckLoginConditions()
if err != nil {
return *user, err
@ -4145,6 +4120,35 @@ func updateUserFromExtAuthResponse(user *User, password, pkey string) {
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,
tlsCert *x509.Certificate,
) (User, error) {
@ -4176,7 +4180,8 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
if u.ID == 0 {
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)
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)
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)
}
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)
if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %q id: %v",
username, u.ID)
providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %q id: %d, auth scope: %d",
username, u.ID, authScope)
if u.ID == 0 {
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)
if err != nil {

View file

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

View file

@ -28,6 +28,7 @@ const (
SessionTypeOIDCToken
SessionTypeResetCode
SessionTypeOAuth2Auth
SessionTypeInvalidToken
)
// Session defines a shared session persisted in the data provider
@ -42,7 +43,7 @@ func (s *Session) validate() error {
if s.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 nil

View file

@ -22,6 +22,7 @@ import (
"time"
"github.com/alexedwards/argon2id"
passwordvalidator "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/bcrypt"
"github.com/drakkan/sftpgo/v2/internal/logger"
@ -178,6 +179,15 @@ func (s *Share) HasRedactedPassword() bool {
func (s *Share) hashPassword() error {
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 {
hashed, err := bcrypt.GenerateFromPassword([]byte(s.Password), config.PasswordHashing.BcryptOptions.Cost)
if err != nil {
@ -198,8 +208,7 @@ func (s *Share) hashPassword() error {
func (s *Share) validatePaths() error {
var paths []string
for _, p := range s.Paths {
p = strings.TrimSpace(p)
if p != "" {
if strings.TrimSpace(p) != "" {
paths = append(paths, p)
}
}

View file

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

View file

@ -42,7 +42,7 @@ const (
func getSQLPlaceholders() []string {
var placeholders []string
for i := 1; i <= 50; i++ {
for i := 1; i <= 100; i++ {
if config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName {
placeholders = append(placeholders, fmt.Sprintf("$%d", i))
} else {
@ -400,7 +400,7 @@ func getUsersInGroupsQuery(numArgs int) string {
} else {
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())
}
@ -716,8 +716,8 @@ func getUpdateUserQuery(role string) string {
}
func getUpdateUserPasswordQuery() string {
return fmt.Sprintf(`UPDATE %s SET password=%s WHERE username = %s`,
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
return fmt.Sprintf(`UPDATE %s SET password=%s,updated_at=%s WHERE username = %s`,
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])
}
func getDeleteUserQuery(softDelete bool) string {

View file

@ -981,9 +981,14 @@ func (u *User) getPatternsFilterForPath(virtualPath string) sdk.PatternsFilter {
return filter
}
dirsForPath := util.GetDirsForVirtualPath(virtualPath)
for _, dir := range dirsForPath {
for idx, dir := range dirsForPath {
for _, f := range u.Filters.FilePatterns {
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
break
}
@ -1004,7 +1009,7 @@ func (u *User) isDirHidden(virtualPath string) bool {
return false
}
filter := u.getPatternsFilterForPath(dirPath)
if filter.DenyPolicy == sdk.DenyPolicyHide {
if filter.DenyPolicy == sdk.DenyPolicyHide && filter.Path != dirPath {
if !filter.CheckAllowed(path.Base(dirPath)) {
return true
}
@ -1173,7 +1178,7 @@ func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
return false
}
// GetSignature returns a signature for this admin.
// GetSignature returns a signature for this user.
// It will change after an update
func (u *User) GetSignature() string {
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
func (u *User) HasTransferQuotaRestrictions() bool {
if len(u.Filters.DataTransferLimits) > 0 {
return true
}
return u.UploadDataTransfer > 0 || u.TotalDataTransfer > 0 || u.DownloadDataTransfer > 0
}
// 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
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 {
total = u.TotalDataTransfer * 1048576
}
@ -1825,7 +1803,6 @@ func (u *User) mergeAdditiveProperties(group *Group, groupType int, replacer *st
u.mergePermissions(group, groupType, replacer)
u.mergeFilePatterns(group, groupType, replacer)
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.DeniedIP = append(u.Filters.DeniedIP, group.UserSettings.Filters.DeniedIP...)
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) {
if u.Permissions == nil {
u.Permissions = make(map[string][]string)
}
for k, v := range group.UserSettings.Permissions {
if k == "/" {
if groupType == sdk.GroupTypePrimary {

View file

@ -90,154 +90,154 @@ CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
-----END EC PRIVATE KEY-----`
caCRT = `-----BEGIN CERTIFICATE-----
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
QXV0aDAeFw0yMjA3MDQxNTQzMTFaFw0yNDAxMDQxNTUzMDhaMBMxETAPBgNVBAMT
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4eyDJkmW
D4OVYo7ddgiZkd6QQdPyLcsa31Wc9jdR2/peEabyNT8jSWteS6ouY84GRlnhfFeZ
mpXgbaUJu/Z8Y/8riPxwL8XF4vCScQDMywpQnVUd6E9x2/+/uaD4p/BBswgKqKPe
uDcHZn7MkD4QlquUhMElDrBUi1Dv/AVHnQ6iP4vd5Jlv0F+40jdq/8Wa7yhW7Pu5
iNvPwCk8HjENBKVur/re+Acif8A2TlbCsuOnVduSQNmnWH+iZmB9upyBZtUszGS0
JhUwtSnwUX/JapF70Pwte/PV3RK8cJ5FjuAPNeTyJvSuMTELFSAyCeiNynFGgyhW
cqbEiPu6BURLculyVkmh4dOrhTrYZv/n3UJAhyxkdYrbh3INHmTa4izvclcuwoEo
lFlJp3l77D0lIi+pbtcBV6ys7reyuxUAkBNwnpt2pWfCQoi4QYKcNbHm47c2phOb
QSojQ8SsNU5bnlY2MDzkKo5DPav/i4d0HpndphUpx4f8hA0KylLevDRkMz9TAH7H
uDssn0CxFOGHiveEAGGbn+doHjNWM339x/cdLbK0vuieDKby8YYcBY1JML57Dl9f
rs52ySnDZbMqOb9zF66mQpC2FZoAj713xSkDSnSCUekrqgck1EA1ifxAviHt+p26
JwaEDL7Lk01EEdYN4csSd1fezbCqTrG8ffUCAwEAAaNFMEMwDgYDVR0PAQH/BAQD
AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFPirPBPO01zUuf7xC+ds
bOOY5QvAMA0GCSqGSIb3DQEBCwUAA4ICAQBUYa+ydfTPKjTN4lXyEZgchZQ+juny
aMy1xosLz6Evj0us2Bwczmy6X2Zvaw/KteFlgKaU1Ex2UkU7FfAlaH0HtwTLFMVM
p9nB7ZzStvg0n8zFM29SEkOFwZ9FRonxx4sY3FdvI4QvAWyDyqgOl8+Eedg0kC4+
M7hxarTFmZZ7POZl8Hio592yx3asMmSCcmb7oUCKVI98qsf9fuL+LIZSpn4fE7av
AiNBcOqCZ10CRnl4VSgAW2LH4oqROYdUv+me1u1YRwh7fCF/R7VjOLuaDzv0mp/g
hzG9U+Yso3WV4b28MsctwUmGTK8Zc5QaANKgmI3ulkta37wN5KjrUuescHC7MqZg
vN9n60801be1EoUL83KUx57Bix95YZR02Zge0gYdYTb+E2bwaZ4GMlf7cs6qmC6A
ZPLR7Tffw2J4dPTcfEx3rPZ91s3MkAdPzYYGdGlbKp8RCFnezZ7rw2z57rnT0zDr
LuL3Q6ADBfothoos/EBIC5ekXb9czp8gig+nJXLC6jlqcQpCLrV88oS3+8zACmx1
d6tje9uuAqPgiQGddKZj4b4BlHmAMXq0PufQsZVoyzboTewZiLVCtTR9/iF7Cepg
6EVv57p61pFhPu8lNRAi0aH/po9yt+7435FGpn2kan6k9aDIVdaqeuxxITwsqJ4R
WwSa13hh6yjoDQ==
QXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m
fi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1
v03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh
yzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL
CdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U
4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb
KF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo
NlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLqR/BJTbbyXUB0imne1u00fuzb
S7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb78x7ivdyXSF5LVQJ1JvhhWu6i
M6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpBP8d2jcRZVUVrSXGc2mAGuGOY
/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2ZxugCXULtRWJ9p4C9zUl40HEy
OQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD
AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNoJhIvDZQrEf/VQbWuu
XgNnt2m5MA0GCSqGSIb3DQEBCwUAA4ICAQCYhT5SRqk19hGrQ09hVSZOzynXAa5F
sYkEWJzFyLg9azhnTPE1bFM18FScnkd+dal6mt+bQiJvdh24NaVkDghVB7GkmXki
pAiZwEDHMqtbhiPxY8LtSeCBAz5JqXVU2Q0TpAgNSH4W7FbGWNThhxcJVOoIrXKE
jbzhwl1Etcaf0DBKWliUbdlxQQs65DLy+rNBYtOeK0pzhzn1vpehUlJ4eTFzP9KX
y2Mksuq9AspPbqnqpWW645MdTxMb5T57MCrY3GDKw63z5z3kz88LWJF3nOxZmgQy
WFUhbLmZm7x6N5eiu6Wk8/B4yJ/n5UArD4cEP1i7nqu+mbbM/SZlq1wnGpg/sbRV
oUF+a7pRcSbfxEttle4pLFhS+ErKatjGcNEab2OlU3bX5UoBs+TYodnCWGKOuBKV
L/CYc65QyeYZ+JiwYn9wC8YkzOnnVIQjiCEkLgSL30h9dxpnTZDLrdAA8ItelDn5
DvjuQq58CGDsaVqpSobiSC1DMXYWot4Ets1wwovUNEq1l0MERB+2olE+JU/8E23E
eL1/aA7Kw/JibkWz1IyzClpFDKXf6kR2onJyxerdwUL+is7tqYFLysiHxZDL1bli
SXbW8hMa5gvo0IilFP9Rznn8PplIfCsvBDVv6xsRr5nTAFtwKaMBVgznE2ghs69w
kK8u1YiiVenmoQ==
-----END CERTIFICATE-----`
caCRL = `-----BEGIN X509 CRL-----
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
MjIwNzA0MTU1MzU4WhcNMjQwNzAzMTU1MzU4WjAkMCICEQDZo5Q3lhxFuDUsxGNm
794YFw0yMjA3MDQxNTUzNThaoCMwITAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8Qvn
bGzjmOULwDANBgkqhkiG9w0BAQsFAAOCAgEA1lK6g8qmhyY6myx8342dDuaauY03
0iojkxpasuYcytK6XRm96YqjZK9EETxsHHViVU0vCXES60D6wJ9gw4fTWn3WxEdx
nIwbGyjUGHh2y+R3uQsfvwxsdYvDsTLAnOLwOo68dAHWmMDZRmgTuGNoYFxVQRGR
Cn90ZR7LPLpCScclWM8FE/W1B90x3ZE8EhJiCI/WyyTh3EgshmB7A5GoDrFZfmvR
dzoTKO+F9p2XjtmgfiBE3czWQysfATmbutZUbG/ZRb89u+ZEUyPoC94mg8fhNWoX
1d5G9QAkZFHp957/5QHLq9OHNfnWXoohhebjF4VWqZH7w+RtLc8t0PIog2lX4t1o
5N/xFk9akvuoyNGg/fYuJBmN162Q0MdeYfYKDGWdXxf6fpHxVr5v2JrIx6gOwubb
cIKP22ZBv/PYOeFsAZ755lTl4OTFUjU5ZJEPD6pUc1daaIqfxsxu8gDZP92FZjsB
zaalMbh30n2OhagSMBzSLg5rE6WmBzlQX0ZN8YrW4l2Vq6twnnFHY+UyblRZS+d4
oHBaoOaxPEkLxNZ8ulzJS4B6c4D1CXOaBEf++snVzRRUOEdX3x7TvkkrLvIsm06R
ux0L1zJb9LbZ/1rhuv70z/kIlD55sqYuRqu3RpgTgZuTERU//rYIqWd03Y5Qon8i
VoC6Yp9DPldQJrk=
MjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg
ZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r
rl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV
+jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199
XSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE
jF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF
DouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD
MSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4
iYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE
ypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4
hVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94
cp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67
sn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn
w7bXJGfJcIMrrKs=
-----END X509 CRL-----`
client1Crt = `-----BEGIN CERTIFICATE-----
MIIEITCCAgmgAwIBAgIRAJla/m/UkZMifNwG+DxFr2MwDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjIwNzA0MTU0MzM3WhcNMjQwMTA0MTU1
MzA3WjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA8xM5v+2QfdzfwnNT5cl+6oEy2fZoI2YG6L6c25rG0pr+yl1IHKdM
Zcvn93uat7hlbzxeOLfJRM7+QK1lLaxuppq9p+gT+1x9eG3E4X7e0pdbjrpJGbvN
ji0hwDBLDWD8mHNq/SCk9FKtGnfZqrNB5BLw2uIKjJzVGXVlsjN6geBDm2hVjTSm
zMr39CfLUdtvMaZhpIPJzbH+sNfp1zKavFIpmwCd77p/z0QAiQ9NaIvzv4PZDDEE
MUHzmVAU6bUjD8GToXaMbRiz694SU8aAwvvcdjGexdbHnfSAfLOl2wTPPxvePncR
aa656ZeZWxY9pRCItP+v43nm7d4sAyRD4QIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQbwDqF
aja3ifZHm6mtSeTK9IHc+zAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8QvnbGzjmOUL
wDANBgkqhkiG9w0BAQsFAAOCAgEAprE/zV6u8UIH8g4Jb73wtUD/eIL3iBJ7mNYa
lqwCyJrWH7/F9fcovJnF9WO1QPTeHxhoD9rlQK70GitUAeboYw611yNWDS4tDlaL
sjpJKykUxBgBR7QSLZCrPtQ3fP2WvlZzLGqB28rASTLphShqTuGp4gJaxGHfbCU7
mlV9QYi+InQxOICJJPebXUOwx5wYkFQWJ9qE1AK3QrWPi8QYFznJvHgkNAaMBEmI
jAlggOzpveVvy8f4z3QG9o29LIwp7JvtJQs7QXL80FZK98/8US/3gONwTrBz2Imx
28ywvwCq7fpMyPgxX4sXtxphCNim+vuHcqDn2CvLS9p/6L6zzqbFNxpmMkJDLrOc
YqtHE4TLWIaXpb5JNrYJgNCZyJuYDICVTbivtMacHpSwYtXQ4iuzY2nIr0+4y9i9
MNpqv3W47xnvgUQa5vbTbIqo2NSY24A84mF5EyjhaNgNtDlN56+qTQ6HLZNVr6pv
eUCCWnY4GkaZUEU1M8/uNtKaZKv1WA7gJxZDQHj8+R110mPtzm1C5jqg7jSjGy9C
8PhAwBqIXkVLNayFEtyZZobTxMH5qY1yFkI3sic7S9ZyXt3quY1Q1UT3liRteIm/
sZHC5zEoidsHObkTeU44hqZVPkbvrfmgW01xTJjddnMPBH+yqjCCc94yCbW79j/2
7LEmxYg=
MIIEITCCAgmgAwIBAgIRAJr32nHRlhyPiS7IfZ/ZWYowDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjM3WhcNMzQwMTEwMTgy
MTUzWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+kiJ3X6
HUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi85xE
OkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLWeYl7
Qie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4ZdRf
XlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dybbhO
c9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRUh5Xo
Gzjh6iReaPSOgGatqOw9bDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp
uTANBgkqhkiG9w0BAQsFAAOCAgEAyAK7cOTWqjyLgFM0kyyx1fNPvm2GwKep3MuU
OrSnLuWjoxzb7WcbKNVMlnvnmSUAWuErxsY0PUJNfcuqWiGmEp4d/SWfWPigG6DC
sDej35BlSfX8FCufYrfC74VNk4yBS2LVYmIqcpqUrfay0I2oZA8+ToLEpdUvEv2I
l59eOhJO2jsC3JbOyZZmK2Kv7d94fR+1tg2Rq1Wbnmc9AZKq7KDReAlIJh4u2KHb
BbtF79idusMwZyP777tqSQ4THBMa+VAEc2UrzdZqTIAwqlKQOvO2fRz2P+ARR+Tz
MYJMdCdmPZ9qAc8U1OcFBG6qDDltO8wf/Nu/PsSI5LGCIhIuPPIuKfm0rRfTqCG7
QPQPWjRoXtGGhwjdIuWbX9fIB+c+NpAEKHgLtV+Rxj8s5IVxqG9a5TtU9VkfVXJz
J20naoz/G+vDsVINpd3kH0ziNvdrKfGRM5UgtnUOPCXB22fVmkIsMH2knI10CKK+
offI56NTkLRu00xvg98/wdukhkwIAxg6PQI/BHY5mdvoacEHHHdOhMq+GSAh7DDX
G8+HdbABM1ExkPnZLat15q706ztiuUpQv1C2DI8YviUVkMqCslj4cD4F8EFPo4kr
kvme0Cuc9Qlf7N5rjdV3cjwavhFx44dyXj9aesft2Q1okPiIqbGNpcjHcIRlj4Au
MU3Bo0A=
-----END CERTIFICATE-----`
client1Key = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----`
// client 2 crt is revoked
client2Crt = `-----BEGIN CERTIFICATE-----
MIIEITCCAgmgAwIBAgIRANmjlDeWHEW4NSzEY2bv3hgwDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjIwNzA0MTU0MzUxWhcNMjQwMTA0MTU1
MzA3WjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAzNl7q7yS8MSaQs6zRbuqrsUuwEJ5ZH85vf7zHZKgOW3zNniXLOmH
JdtQ3jKZQ1BCIsJFvez2GxGIMWbXaSPw4bL0J3vl5oItChsjGg34IvqcDxWuIk2a
muRdMh7r1ryVs2ir2cQ5YHzI59BEpUWKQg3bD4yragdkb6BRc7lVgzCbrM1Eq758
HHbaLwlsfpqOvheaum4IG113CeD/HHrw42W6g/qQWL+FHlYqV3plHZ8Bj+bhcZI5
jdU4paGEzeY0a0NlnyH4gXGPjLKvPKFZHy4D6RiRlLHvHeiRyDtTu4wFkAiXxzGs
E4UBbykmYUB85zgwpjaktOaoe36IM1T8CQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRdYIEk
gxh+vTaMpAbqaPGRKGGBpTAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8QvnbGzjmOUL
wDANBgkqhkiG9w0BAQsFAAOCAgEABSR/PbPfiNZ6FOrt91/I0g6LviwICDcuXhfr
re4UsWp1kxXeS3CB2G71qXv3hswN8phG2hdsij0/FBEGUTLS3FTCmLmqmcVqPj3/
677PMFDoACBKgT5iIwpnNvdD+4ROM8JFjUwy7aTWx85a5yoPFGnB+ORMfLCYjr2S
D02KFvKuSXWCjXphqJ41cFGne4oeh/JMkN0RNArm7wTT8yWCGgO1k4OON8dphuTV
48Wm6I9UBSWuLk1vcIlgb/8YWVwy9rBNmjOBDGuroL6PSmfZD+e9Etii0X2znZ+t
qDpXJB7V5U0DbsBCtGM/dHaFz/LCoBYX9z6th1iPUHksUTM3RzN9L24r9/28dY/a
shBpn5rK3ui/2mPBpO26wX14Kl/DUkdKUV9dJllSlmwo8Z0RluY9S4xnCrna/ODH
FbhWmlTSs+odCZl6Lc0nuw+WQ2HnlTVJYBSFAGfsGQQ3pzk4DC5VynnxY0UniUgD
WYPR8JEYa+BpH3rIQ9jmnOKWLtyc7lFPB9ab63pQBBiwRvWo+tZ2vybqjeHPuu5N
BuKvvtu3RKKdSCnIo5Rs5zw4JYCjvlx/NVk9jtpa1lIHYHilvBmCcRX5DkE/yH/x
IjEKhCOQpGR6D5Kkca9xNL7zNcat3bzLn+d7Wo4m09uWi9ifPdchxed0w5d9ihx1
enqNrFI=
MIIEITCCAgmgAwIBAgIRAM5p4duNjikSPxYGaqBm5FEwDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjUyWhcNMzQwMTEwMTgy
MTUzWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImVjm/b
Qe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TPkZua
eq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEKh4LQ
cr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81PQAmT
A0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu4Ic0
6tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBR5mf0f
Zjf8ZCGXqU2+45th7VkkLDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp
uTANBgkqhkiG9w0BAQsFAAOCAgEARhFxNAouwbpEfN1M90+ao5rwyxEewerSoCCz
PQzeUZ66MA/FkS/tFUGgGGG+wERN+WLbe1cN6q/XFr0FSMLuUxLXDNV02oUL/FnY
xcyNLaZUZ0pP7sA+Hmx2AdTA6baIwQbyIY9RLAaz6hzo1YbI8yeis645F1bxgL2D
EP5kXa3Obv0tqWByMZtrmJPv3p0W5GJKXVDn51GR/E5KI7pliZX2e0LmMX9mxfPB
4sXFUggMHXxWMMSAmXPVsxC2KX6gMnajO7JUraTwuGm+6V371FzEX+UKXHI+xSvO
78TseTIYsBGLjeiA8UjkKlD3T9qsQm2mb2PlKyqjvIm4i2ilM0E2w4JZmd45b925
7q/QLV3NZ/zZMi6AMyULu28DWKfAx3RLKwnHWSFcR4lVkxQrbDhEUMhAhLAX+2+e
qc7qZm3dTabi7ZJiiOvYK/yNgFHa/XtZp5uKPB5tigPIa+34hbZF7s2/ty5X3O1N
f5Ardz7KNsxJjZIt6HvB28E/PPOvBqCKJc1Y08J9JbZi8p6QS1uarGoR7l7rT1Hv
/ZXkNTw2bw1VpcWdzDBLLVHYNnJmS14189LVk11PcJJpSmubwCqg+ZZULdgtVr3S
ANas2dgMPVwXhnAalgkcc+lb2QqaEz06axfbRGBsgnyqR5/koKCg1Hr0+vThHSsR
E0+r2+4=
-----END CERTIFICATE-----`
client2Key = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAzNl7q7yS8MSaQs6zRbuqrsUuwEJ5ZH85vf7zHZKgOW3zNniX
LOmHJdtQ3jKZQ1BCIsJFvez2GxGIMWbXaSPw4bL0J3vl5oItChsjGg34IvqcDxWu
Ik2amuRdMh7r1ryVs2ir2cQ5YHzI59BEpUWKQg3bD4yragdkb6BRc7lVgzCbrM1E
q758HHbaLwlsfpqOvheaum4IG113CeD/HHrw42W6g/qQWL+FHlYqV3plHZ8Bj+bh
cZI5jdU4paGEzeY0a0NlnyH4gXGPjLKvPKFZHy4D6RiRlLHvHeiRyDtTu4wFkAiX
xzGsE4UBbykmYUB85zgwpjaktOaoe36IM1T8CQIDAQABAoIBAETHMJK0udFE8VZE
+EQNgn0zj0LWDtQDM2vrUc04Ebu2gtZjHr7hmZLIVBqGepbzN4FcIPZnvSnRdRzB
HsoaWyIsZ3VqUAJY6q5d9iclUY7M/eDCsripvaML0Y6meyCaKNkX57sx+uG+g+Xx
M1saQhVzeX17CYKMANjJxw9HxsJI0aBPyiBbILHMwfRfsJU8Ou72HH1sIQuPdH2H
/c9ru8YZAno6oVq1zuC/pCis+h50U9HzTnt3/4NNS6cWG/y2YLztCvm9uGo4MTd/
mA9s4cxVhvQW6gCDHgGn6zj661OL/d2rpak1eWizhZvZ8jsIN/sM87b0AJeVT4zH
6xA3egECgYEA1nI5EsCetQbFBp7tDovSp3fbitwoQtdtHtLn2u4DfvmbLrgSoq0Z
L+9N13xML/l8lzWai2gI69uA3c2+y1O64LkaiSeDqbeBp9b6fKMlmwIVbklEke1w
XVTIWOYTTF5/8+tUOlsgme5BhLAWnQ7+SoitzHtl5e1vEYaAGamE2DECgYEA9Is2
BbTk2YCqkcsB7D9q95JbY0SZpecvTv0rLR+acz3T8JrAASdmvqdBOlPWc+0ZaEdS
PcJaOEw3yxYJ33cR/nLBaR2/Uu5qQebyPALs3B2pjjTFdGvcpeFxO55fowwsfR/e
0H+HeiFj5Y4S+kFWT+3FRmJ6GUB828LJYaVhQ1kCgYEA1bdsTdYN1Vfzz89fbZnH
zQLUl6UlssfDhm6mhzeh4E+eaocke1+LtIwHxfOocj9v/bp8VObPzU8rNOIxfa3q
lr+jRIFO5DtwSfckGEb32W3QMeNvJQe/biRqrr5NCVU8q7kibi4XZZFfVn+vacNh
hqKEoz9vpCBnCs5CqFCbhmECgYAG8qWYR+lwnI08Ey58zdh2LDxYd6x94DGh5uOB
JrK2r30ECwGFht8Ob6YUyCkBpizgn5YglxMFInU7Webx6GokdpI0MFotOwTd1nfv
aI3eOyGEHs+1XRMpy1vyO6+v7DqfW3ZzKgxpVeWGsiCr54tSPgkq1MVvTju96qza
D17SEQKBgCKC0GjDjnt/JvujdzHuBt1sWdOtb+B6kQvA09qVmuDF/Dq36jiaHDjg
XMf5HU3ThYqYn3bYypZZ8nQ7BXVh4LqGNqG29wR4v6l+dLO6odXnLzfApGD9e+d4
2tmlLP54LaN35hQxRjhT8lCN0BkrNF44+bh8frwm/kuxSd8wT2S+
MIIEowIBAAKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImV
jm/bQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TP
kZuaeq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEK
h4LQcr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81P
QAmTA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu
4Ic06tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABAoIBAQCMnEeg9uXQmdvq
op4qi6bV+ZcDWvvkLwvHikFMnYpIaheYBpF2ZMKzdmO4xgCSWeFCQ4Hah8KxfHCM
qLuWvw2bBBE5J8yQ/JaPyeLbec7RX41GQ2YhPoxDdP0PdErREdpWo4imiFhH/Ewt
Rvq7ufRdpdLoS8dzzwnvX3r+H2MkHoC/QANW2AOuVoZK5qyCH5N8yEAAbWKaQaeL
VBhAYEVKbAkWEtXw7bYXzxRR7WIM3f45v3ncRusDIG+Hf75ZjatoH0lF1gHQNofO
qkCVZVzjkLFuzDic2KZqsNORglNs4J6t5Dahb9v3hnoK963YMnVSUjFvqQ+/RZZy
VILFShilAoGBANucwZU61eJ0tLKBYEwmRY/K7Gu1MvvcYJIOoX8/BL3zNmNO0CLl
NiABtNt9WOVwZxDsxJXdo1zvMtAegNqS6W11R1VAZbL6mQ/krScbLDE6JKA5DmA7
4nNi1gJOW1ziAfdBAfhe4cLbQOb94xkOK5xM1YpO0xgDJLwrZbehDMmPAoGBAMAl
/owPDAvcXz7JFynT0ieYVc64MSFiwGYJcsmxSAnbEgQ+TR5FtkHYe91OSqauZcCd
aoKXQNyrYKIhyounRPFTdYQrlx6KtEs7LU9wOxuphhpJtGjRnhmA7IqvX703wNvu
khrEavn86G5boH8R80371SrN0Rh9UeAlQGuNBdvfAoGAEAmokW9Ug08miwqrr6Pz
3IZjMZJwALidTM1IufQuMnj6ddIhnQrEIx48yPKkdUz6GeBQkuk2rujA+zXfDxc/
eMDhzrX/N0zZtLFse7ieR5IJbrH7/MciyG5lVpHGVkgjAJ18uVikgAhm+vd7iC7i
vG1YAtuyysQgAKXircBTIL0CgYAHeTLWVbt9NpwJwB6DhPaWjalAug9HIiUjktiB
GcEYiQnBWn77X3DATOA8clAa/Yt9m2HKJIHkU1IV3ESZe+8Fh955PozJJlHu3yVb
Ap157PUHTriSnxyMF2Sb3EhX/rQkmbnbCqqygHC14iBy8MrKzLG00X6BelZV5n0D
8d85dwKBgGWY2nsaemPH/TiTVF6kW1IKSQoIyJChkngc+Xj/2aCCkkmAEn8eqncl
RKjnkiEZeG4+G91Xu7+HmcBLwV86k5I+tXK9O1Okomr6Zry8oqVcxU5TB6VRS+rA
ubwF00Drdvk2+kDZfxIM137nBiy7wgCJi2Ksm5ihN3dUF6Q0oNPl
-----END RSA PRIVATE KEY-----`
testFileName = "test_file_ftp.dat"
testDLFileName = "test_download_ftp.dat"

View file

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

View file

@ -160,7 +160,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
}
fi, err := os.Stat(inputFile)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
sendAPIResponse(w, r, fmt.Errorf("invalid input_file %q", inputFile), "", http.StatusBadRequest)
return
}
if fi.Size() > MaxRestoreSize {
@ -171,7 +171,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
content, err := os.ReadFile(inputFile)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
sendAPIResponse(w, r, fmt.Errorf("invalid input_file %q", inputFile), "", http.StatusBadRequest)
return
}
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 {
dump, err := dataprovider.ParseDumpData(content)
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 {

View file

@ -331,7 +331,7 @@ func isTokenInvalidated(r *http.Request) bool {
token := fn(r)
if token != "" {
isTokenFound = true
if _, ok := invalidatedJWTTokens.Load(token); ok {
if invalidatedJWTTokens.Get(token) {
return true
}
}
@ -343,11 +343,11 @@ func isTokenInvalidated(r *http.Request) bool {
func invalidateToken(r *http.Request) {
tokenString := jwtauth.TokenFromHeader(r)
if tokenString != "" {
invalidatedJWTTokens.Store(tokenString, time.Now().Add(tokenDuration).UTC())
invalidatedJWTTokens.Add(tokenString, time.Now().Add(tokenDuration).UTC())
}
tokenString = jwtauth.TokenFromCookie(r)
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"
"runtime"
"strings"
"sync"
"time"
"github.com/go-chi/chi/v5"
@ -196,7 +195,7 @@ var (
certMgr *common.CertManager
cleanupTicker *time.Ticker
cleanupDone chan bool
invalidatedJWTTokens sync.Map
invalidatedJWTTokens tokenManager
csrfTokenAuth *jwtauth.JWTAuth
webRootPath 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())
configurationDir = configDir
invalidatedJWTTokens = newTokenManager(isShared)
resetCodesMgr = newResetCodeManager(isShared)
oidcMgr = newOIDCManager(isShared)
oauth2Mgr = newOAuth2Manager(isShared)
@ -1047,7 +1047,7 @@ func getServicesStatus() *ServicesStatus {
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] != '/' {
r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
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) {
rctx := chi.RouteContext(r.Context())
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)
})
}
@ -1179,7 +1183,7 @@ func startCleanupTicker(duration time.Duration) {
return
case <-cleanupTicker.C:
counter++
cleanupExpiredJWTTokens()
invalidatedJWTTokens.Cleanup()
resetCodesMgr.Cleanup()
if counter%2 == 0 {
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 {
if 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")
}
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) {
u := getTestUser()
u.UploadBandwidth = 128
@ -8038,7 +7941,7 @@ func TestLoaddata(t *testing.T) {
if runtime.GOOS != osWindows {
err = os.Chmod(backupFilePath, 0111)
assert.NoError(t, err)
_, _, err = httpdtest.Loaddata(backupFilePath, "1", "", http.StatusForbidden)
_, _, err = httpdtest.Loaddata(backupFilePath, "1", "", http.StatusBadRequest)
assert.NoError(t, err)
err = os.Chmod(backupFilePath, 0644)
assert.NoError(t, err)
@ -8052,7 +7955,7 @@ func TestLoaddata(t *testing.T) {
configsGet, err := dataprovider.GetConfigs()
assert.NoError(t, err)
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.KexAlgorithms, 0)
assert.Len(t, configsGet.SFTPD.Ciphers, 0)
@ -12631,7 +12534,7 @@ func TestDefender(t *testing.T) {
req.RemoteAddr = remoteAddr
rr = executeRequest(req)
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.RequestURI = webUsersPath
@ -12639,7 +12542,7 @@ func TestDefender(t *testing.T) {
req.RemoteAddr = remoteAddr
rr = executeRequest(req)
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.Header.Set("X-Real-IP", "127.0.0.1:2345")
@ -12647,7 +12550,7 @@ func TestDefender(t *testing.T) {
req.RemoteAddr = remoteAddr
rr = executeRequest(req)
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)
assert.NoError(t, err)
@ -12870,6 +12773,8 @@ func TestWebConfigsMock(t *testing.T) {
assert.Contains(t, rr.Body.String(), ssh.CertAlgoDSAv01) // invalid algo
form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
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())))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
@ -12880,12 +12785,13 @@ func TestWebConfigsMock(t *testing.T) {
// check SFTP configs
configs, err := dataprovider.GetConfigs()
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.CertAlgoRSAv01)
assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Contains(t, configs.SFTPD.Moduli, "path 1")
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
form.Set("form_action", "")
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
@ -12927,9 +12833,8 @@ func TestWebConfigsMock(t *testing.T) {
// check
configs, err = dataprovider.GetConfigs()
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.CertAlgoRSAv01)
assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Equal(t, "mail.example.net", configs.SMTP.Host)
assert.Equal(t, 587, configs.SMTP.Port)
@ -12998,9 +12903,8 @@ func TestWebConfigsMock(t *testing.T) {
// check
configs, err = dataprovider.GetConfigs()
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.CertAlgoRSAv01)
assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Equal(t, 80, configs.ACME.HTTP01Challenge.Port)
assert.Equal(t, 7, configs.ACME.Protocols)
@ -13031,7 +12935,7 @@ func TestWebConfigsMock(t *testing.T) {
assert.Contains(t, rr.Body.String(), "Configurations updated")
configs, err = dataprovider.GetConfigs()
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, 1, configs.ACME.Protocols)
assert.Equal(t, domain, configs.ACME.Domain)
@ -13993,7 +13897,8 @@ func TestShareUploadSingle(t *testing.T) {
assert.NoError(t, err)
req.SetBasicAuth(defaultUsername, defaultPassword)
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)
assert.NoError(t, err)
@ -14004,6 +13909,13 @@ func TestShareUploadSingle(t *testing.T) {
checkResponseCode(t, http.StatusBadRequest, rr)
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)
assert.NoError(t, err)
assert.Equal(t, 2, share.UsedTokens)
@ -14986,9 +14898,11 @@ func TestUserAPIShares(t *testing.T) {
func TestUsersAPISharesNoPasswordDisabled(t *testing.T) {
u := getTestUser()
u.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled}
u.Filters.PasswordStrength = 70
u.Password = "ahpoo8baa6EeZieshies"
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, u.Password)
assert.NoError(t, err)
share := dataprovider.Share{
@ -15012,6 +14926,15 @@ func TestUsersAPISharesNoPasswordDisabled(t *testing.T) {
assert.NoError(t, err)
setBearerForReq(req, token)
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)
location := rr.Header().Get("Location")
assert.NotEmpty(t, location)
@ -15051,6 +14974,13 @@ func TestUserAPIKey(t *testing.T) {
apiKey, _, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)
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)
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("filenames", "filenametest")
@ -15079,6 +15009,12 @@ func TestUserAPIKey(t *testing.T) {
assert.NoError(t, err)
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, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
@ -15155,6 +15091,9 @@ func TestUserAPIKey(t *testing.T) {
_, err = httpdtest.RemoveAPIKey(apiKeyNew, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveAPIKey(adminAPIKey, http.StatusOK)
assert.NoError(t, err)
}
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")
form.Set("bandwidth_limit_sources1", "127.0.0.1/32")
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
form.Set("external_auth_cache_time", "a")
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.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)
@ -22886,6 +22758,72 @@ func TestWebRole(t *testing.T) {
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) {
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
@ -24406,7 +24344,7 @@ func TestStaticFilesMock(t *testing.T) {
req, err = http.NewRequest(http.MethodGet, location, nil)
assert.NoError(t, err)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
checkResponseCode(t, http.StatusForbidden, rr)
req, err = http.NewRequest(http.MethodGet, "/openapi", nil)
assert.NoError(t, err)

View file

@ -1991,13 +1991,38 @@ func TestJWTTokenCleanup(t *testing.T) {
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))
startCleanupTicker(100 * time.Millisecond)
assert.Eventually(t, func() bool { return !isTokenInvalidated(req) }, 1*time.Second, 200*time.Millisecond)
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) {
b := Binding{
Address: filepath.Join(os.TempDir(), "sock"),
@ -3365,6 +3390,92 @@ func TestGetLogEventString(t *testing.T) {
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 {
// SQLite shares the implementation with other SQL-based provider but it makes no sense
// 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)
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 {
handleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), dataprovider.ErrInvalidCredentials) //nolint:errcheck
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
}
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() {
logger.Debug(logSender, "", "oauth2 manager cleanup")
o.mu.Lock()
defer o.mu.Unlock()
@ -165,6 +164,5 @@ func (o *dbOAuth2Manager) decodePendingAuthData(data any) (oauth2PendingAuth, er
}
func (o *dbOAuth2Manager) cleanup() {
logger.Debug(logSender, "", "oauth2 manager cleanup")
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOAuth2Auth, time.Now()) //nolint:errcheck
}

View file

@ -124,7 +124,6 @@ func (o *memoryOIDCManager) updateTokenUsage(token oidcToken) {
}
func (o *memoryOIDCManager) cleanup() {
logger.Debug(logSender, "", "oidc manager cleanup")
o.cleanupAuthRequests()
o.cleanupTokens()
}
@ -238,7 +237,6 @@ func (o *dbOIDCManager) decodeTokenData(data any) (oidcToken, error) {
}
func (o *dbOIDCManager) cleanup() {
logger.Debug(logSender, "", "oidc manager cleanup")
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCAuth, 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"
)
func serveStaticDir(router chi.Router, path, fsDirPath string) {
fileServer(router, path, http.Dir(fsDirPath))
func serveStaticDir(router chi.Router, path, fsDirPath string, disableDirectoryIndex bool) {
fileServer(router, path, http.Dir(fsDirPath), disableDirectoryIndex)
}

View file

@ -18,16 +18,20 @@
package httpd
import (
"net/http"
"github.com/go-chi/chi/v5"
"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 {
case webStaticFilesPath:
fileServer(router, path, bundle.GetStaticFs())
fileServer(router, path, bundle.GetStaticFs(), disableDirectoryIndex)
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
}
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) {
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
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)
defer common.Connections.RemoveClientConnection(ipAddr)
@ -1087,7 +1094,7 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
return
}
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
}
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
rctx := chi.RouteContext(r.Context())
if rctx != nil && rctx.RoutePath != "" {
@ -1173,7 +1182,8 @@ func (s *httpdServer) isStaticFileURL(r *http.Request) bool {
} else {
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() {
@ -1182,9 +1192,10 @@ func (s *httpdServer) initializeRouter() {
s.router = chi.NewRouter()
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(middleware.Recoverer)
s.router.Use(s.checkConnection)
if s.binding.Security.Enabled {
secureMiddleware := secure.New(secure.Options{
AllowedHosts: s.binding.Security.AllowedHosts,
@ -1223,7 +1234,7 @@ func (s *httpdServer) initializeRouter() {
}
s.router.Use(middleware.GetHead)
// 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)
@ -1237,7 +1248,7 @@ func (s *httpdServer) initializeRouter() {
if hasHTTPSRedirect {
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 {
s.router.Group(func(router chi.Router) {
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 {
s.router.Group(func(router chi.Router) {
router.Use(compressor.Handler)
serveStaticDir(router, webStaticFilesPath, s.staticFilesPath)
serveStaticDir(router, webStaticFilesPath, s.staticFilesPath, true)
})
if s.binding.OIDC.isEnabled() {
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"
pageResetPwdTitle = "SFTPGo Admin - Reset password"
pageSetupTitle = "Create first admin user"
defaultQueryLimit = 500
defaultQueryLimit = 1000
inversePatternType = "inverse"
)
@ -1353,50 +1353,6 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
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) {
var result []sdk.BandwidthLimit
@ -1534,10 +1490,6 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
if err != nil {
return filters, err
}
dtLimits, err := getDataTransferLimitsFromPostFields(r)
if err != nil {
return filters, err
}
maxFileSize, err := util.ParseBytes(r.Form.Get("max_upload_file_size"))
if err != nil {
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.BandwidthLimits = bwLimits
filters.DataTransferLimits = dtLimits
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
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.StorageClass = strings.TrimSpace(r.Form.Get("s3_storage_class"))
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)
if err != nil {
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.StorageClass = strings.TrimSpace(r.Form.Get("gcs_storage_class"))
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)
if err == nil {
config.UploadPartSize = uploadPartSize
@ -1732,7 +1683,7 @@ func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
config.AccountKey = getSecretFromFormField(r, "az_account_key")
config.SASURL = getSecretFromFormField(r, "az_sas_url")
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.UseEmulator = r.Form.Get("az_use_emulator") != ""
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. " +
"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. " +
"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), "")
return
}

View file

@ -21,6 +21,7 @@ import (
"fmt"
"html/template"
"io"
"math"
"net/http"
"net/url"
"os"
@ -155,6 +156,7 @@ type filesPage struct {
Error string
Paths []dirMapping
HasIntegrations bool
QuotaUsage *userQuotaUsage
}
type shareLoginPage struct {
@ -229,6 +231,185 @@ type clientSharePage struct {
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 {
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)
}
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,
) {
data := filesPage{
@ -615,6 +796,7 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
CanShare: user.CanManageShares(),
HasIntegrations: hasIntegrations,
Paths: getDirMapping(dirName, webClientFilesPath),
QuotaUsage: newUserQuotaUsage(user),
}
renderClientTemplate(w, templateClientFiles, data)
}
@ -964,11 +1146,11 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
}
if err != nil {
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
}
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
}
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, "")
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
}
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) {
@ -1303,8 +1485,7 @@ func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
share.Name = strings.TrimSpace(r.Form.Get("name"))
share.Description = r.Form.Get("description")
for _, p := range r.Form["paths"] {
p = strings.TrimSpace(p)
if p != "" {
if strings.TrimSpace(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 {
return err
}
if err := compareUserDataTransferLimitFilters(expected, actual); err != nil {
return err
}
return compareUserFilePatternsFilters(expected, actual)
}
@ -2505,30 +2502,6 @@ func checkFilterMatch(expected []string, actual []string) bool {
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 {
if len(expected.BandwidthLimits) != len(actual.BandwidthLimits) {
return errors.New("bandwidth limits filters mismatch")

View file

@ -14,6 +14,12 @@
package logger
import (
"fmt"
"github.com/wneessen/go-mail/log"
)
const (
mailLogSender = "smtpclient"
)
@ -24,25 +30,37 @@ type MailAdapter struct {
}
// Errorf emits a log at Error level
func (l *MailAdapter) Errorf(format string, v ...any) {
ErrorToConsole(format, v...)
Log(LevelError, mailLogSender, l.ConnectionID, format, v...)
func (l *MailAdapter) Errorf(logMsg log.Log) {
format := l.getFormatString(&logMsg)
ErrorToConsole(format, logMsg.Messages...)
Log(LevelError, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
}
// Warnf emits a log at Warn level
func (l *MailAdapter) Warnf(format string, v ...any) {
WarnToConsole(format, v...)
Log(LevelWarn, mailLogSender, l.ConnectionID, format, v...)
func (l *MailAdapter) Warnf(logMsg log.Log) {
format := l.getFormatString(&logMsg)
WarnToConsole(format, logMsg.Messages...)
Log(LevelWarn, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
}
// Infof emits a log at Info level
func (l *MailAdapter) Infof(format string, v ...any) {
InfoToConsole(format, v...)
Log(LevelInfo, mailLogSender, l.ConnectionID, format, v...)
func (l *MailAdapter) Infof(logMsg log.Log) {
format := l.getFormatString(&logMsg)
InfoToConsole(format, logMsg.Messages...)
Log(LevelInfo, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
}
// Debugf emits a log at Debug level
func (l *MailAdapter) Debugf(format string, v ...any) {
DebugToConsole(format, v...)
Log(LevelDebug, mailLogSender, l.ConnectionID, format, v...)
func (l *MailAdapter) Debugf(logMsg log.Log) {
format := l.getFormatString(&logMsg)
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
func (l *StructuredLoggerEntry) Write(status, bytes int, _ http.Header, elapsed time.Duration, _ any) {
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().
Str("sender", "httpd").
Fields(l.fields).

View file

@ -136,7 +136,7 @@ func (p *authPlugin) initialize() error {
})
rpcClient, err := client.Client()
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
}
raw, err := rpcClient.Dispense(auth.PluginName)

View file

@ -1722,7 +1722,7 @@ func TestSCPUploadFiledata(t *testing.T) {
transfer.Connection.AddTransfer(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)
assert.NoError(t, err)
@ -1794,7 +1794,7 @@ func TestTransferFailingReader(t *testing.T) {
require.NoError(t, err)
buf := make([]byte, 32)
_, 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 {
err = c.Close()
assert.NoError(t, err)
@ -1809,14 +1809,14 @@ func TestTransferFailingReader(t *testing.T) {
errRead := errors.New("read is not allowed")
tr := newTransfer(baseTransfer, nil, r, errRead)
_, err = tr.ReadAt(buf, 0)
assert.EqualError(t, err, errRead.Error())
assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
err = tr.Close()
assert.NoError(t, err)
tr = newTransfer(baseTransfer, nil, nil, errRead)
_, err = tr.ReadAt(buf, 0)
assert.EqualError(t, err, errRead.Error())
assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
err = tr.Close()
assert.NoError(t, err)
@ -1984,6 +1984,15 @@ func TestLoadHostKeys(t *testing.T) {
c.HostKeys = []string{nonDefaultKeyName, rsaKeyName, ecdsaKeyName, ed25519KeyName}
err = c.checkAndLoadHostKeys(configDir, serverConfig)
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, ecdsaKeyName)
assert.FileExists(t, ed25519KeyName)

View file

@ -65,11 +65,8 @@ var (
ssh.KeyAlgoED25519,
}
preferredHostKeyAlgos = []string{
ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSASHA256v01,
ssh.CertAlgoECDSA256v01,
ssh.CertAlgoECDSA384v01, ssh.CertAlgoECDSA521v01, ssh.CertAlgoED25519v01,
ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512,
ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSASHA256,
ssh.KeyAlgoED25519,
}
supportedKexAlgos = []string{
@ -369,15 +366,6 @@ func (c *Configuration) Initialize(configDir string) error {
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)
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 {
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.configureLoginBanner(serverConfig, configDir)
c.checkSSHCommands()
@ -471,8 +466,6 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
return fmt.Errorf("unsupported host key algorithm %q", hostKeyAlgo)
}
}
serverConfig.HostKeyAlgorithms = c.HostKeyAlgorithms
serviceStatus.HostKeyAlgos = c.HostKeyAlgorithms
if len(c.KexAlgorithms) > 0 {
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.
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 {
return err
}
@ -1023,22 +1026,45 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
k := HostKey{
Path: hostKey,
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)
logger.Info(logSender, "", "Host key %q loaded, type %q, fingerprint %q", hostKey,
private.PublicKey().Type(), k.Fingerprint)
logger.Info(logSender, "", "Host key %q loaded, type %q, fingerprint %q, algorithms %+v", hostKey,
private.PublicKey().Type(), k.Fingerprint, k.Algorithms)
// Add private key to the server configuration.
serverConfig.AddHostKey(private)
serverConfig.AddHostKey(mas)
for _, cert := range hostCertificates {
signer, err := ssh.NewCertSigner(cert, private)
signer, err := ssh.NewCertSigner(cert.Certificate, mas)
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)
logger.Info(logSender, "", "Host certificate loaded for host key %q, fingerprint %q",
hostKey, ssh.FingerprintSHA256(signer.PublicKey()))
logger.Info(logSender, "", "Host certificate loaded for host key %q, fingerprint %q, algorithms %+v",
hostKey, ssh.FingerprintSHA256(signer.PublicKey()), algos)
}
}
}
if len(serviceStatus.HostKeys) == 0 {
return errors.New("ssh: server has no host keys")
}
var fp []string
for idx := range serviceStatus.HostKeys {
h := &serviceStatus.HostKeys[idx]
@ -1048,8 +1074,8 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
return nil
}
func (c *Configuration) loadHostCertificates(configDir string) ([]*ssh.Certificate, error) {
var certs []*ssh.Certificate
func (c *Configuration) loadHostCertificates(configDir string) ([]hostCertificate, error) {
var certs []hostCertificate
for _, certPath := range c.HostCertificates {
certPath = strings.TrimSpace(certPath)
if !util.IsFileInputValid(certPath) {
@ -1075,7 +1101,10 @@ func (c *Configuration) loadHostCertificates(configDir string) ([]*ssh.Certifica
if cert.CertType != ssh.HostCert {
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
}
@ -1312,3 +1341,14 @@ func (r *revokedCertificates) isRevoked(fp string) bool {
func Reload() error {
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 (
"strings"
"time"
"golang.org/x/crypto/ssh"
)
const (
@ -34,6 +36,18 @@ var (
sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
systemCommands = []string{"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"}
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 {
@ -44,10 +58,21 @@ type sshSubsystemExecMsg struct {
Command string
}
type hostCertificate struct {
Certificate *ssh.Certificate
Path string
}
// HostKey defines the details for a used host key
type HostKey struct {
Path string `json:"path"`
Fingerprint string `json:"fingerprint"`
Path string `json:"path"`
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
@ -57,7 +82,6 @@ type ServiceStatus struct {
SSHCommands []string `json:"ssh_commands"`
HostKeys []HostKey `json:"host_keys"`
Authentications []string `json:"authentications"`
HostKeyAlgos []string `json:"host_key_algos"`
MACs []string `json:"macs"`
KexAlgorithms []string `json:"kex_algorithms"`
Ciphers []string `json:"ciphers"`
@ -73,11 +97,6 @@ func (s *ServiceStatus) GetSupportedAuthsAsString() string {
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
func (s *ServiceStatus) GetMACsAsString() string {
return strings.Join(s.MACs, ", ")

View file

@ -573,7 +573,7 @@ func TestBasicSFTPHandling(t *testing.T) {
assert.NotEmpty(t, sshCommands)
sshAuths := status.GetSupportedAuthsAsString()
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.GetKEXsAsString())
assert.NotEmpty(t, status.GetCiphersAsString())
@ -3786,6 +3786,17 @@ func TestExternalAuthEmptyResponse(t *testing.T) {
assert.Equal(t, 10, user.MaxSessions)
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)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
@ -4745,7 +4756,9 @@ func TestQuotaLimits(t *testing.T) {
err = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)
if assert.Error(t, err) {
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)
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"))
}
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) {
user := getTestUser(true)
pattern := sdk.PatternsFilter{
@ -11504,7 +11546,7 @@ func getExtAuthScriptContent(user dataprovider.User, nonJSONResponse, emptyRespo
return extAuthContent
}
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
}
u, _ := json.Marshal(user)

View file

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

View file

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

View file

@ -115,19 +115,6 @@ func NewS3Fs(connectionID, localTempDir, mountPath string, s3Config S3FsConfig)
awsConfig.Credentials = aws.NewCredentialsCache(
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()
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) {
o.UsePathStyle = fs.config.ForcePathStyle
if fs.config.Endpoint != "" {
o.BaseEndpoint = aws.String(fs.config.Endpoint)
}
})
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.
// So we check some common content types to detect if this is a "directory".
isDir := util.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
if obj.ContentLength == 0 && !isDir {
if util.GetIntFromPointer(obj.ContentLength) == 0 && !isDir {
_, err = fs.headObject(name + "/")
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))
}
if !fs.IsNotExist(err) {
@ -195,7 +185,7 @@ func (fs *S3Fs) getStatForDir(name string) (os.FileInfo, error) {
if err != nil {
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))
}
@ -433,6 +423,7 @@ func (fs *S3Fs) ReadDir(dirname string) ([]os.FileInfo, error) {
}
for _, fileObject := range page.Contents {
objectModTime := util.GetTimeFromPointer(fileObject.LastModified)
objectSize := util.GetIntFromPointer(fileObject.Size)
name, isDir := fs.resolve(fileObject.Key, prefix)
if name == "" || name == "/" {
continue
@ -446,7 +437,7 @@ func (fs *S3Fs) ReadDir(dirname string) ([]os.FileInfo, error) {
if t, ok := modTimes[name]; ok {
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))
}
}
@ -588,11 +579,12 @@ func (fs *S3Fs) GetDirSize(dirname string) (int, int64, error) {
}
for _, fileObject := range page.Contents {
isDir := strings.HasSuffix(util.GetStringFromPointer(fileObject.Key), "/")
if isDir && fileObject.Size == 0 {
objectSize := util.GetIntFromPointer(fileObject.Size)
if isDir && objectSize == 0 {
continue
}
numFiles++
size += fileObject.Size
size += objectSize
if numFiles%1000 == 0 {
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
}
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 {
return err
}
@ -826,10 +819,11 @@ func (fs *S3Fs) mkdirInternal(name string) error {
func (fs *S3Fs) hasContents(name string) (bool, error) {
prefix := fs.getPrefix(name)
maxKeys := int32(2)
paginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{
Bucket: aws.String(fs.config.Bucket),
Prefix: aws.String(prefix),
MaxKeys: 2,
MaxKeys: &maxKeys,
})
if paginator.HasMorePages() {
@ -923,7 +917,7 @@ func (fs *S3Fs) doMultipartCopy(source, target, contentType string, fileSize int
Bucket: aws.String(fs.config.Bucket),
CopySource: aws.String(source),
Key: aws.String(target),
PartNumber: partNum,
PartNumber: &partNum,
UploadId: aws.String(uploadID),
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()
completedParts = append(completedParts, types.CompletedPart{
ETag: partResp.CopyPartResult.ETag,
PartNumber: partNum,
PartNumber: &partNum,
})
partMutex.Unlock()
}(partNumber, start, end)
@ -965,7 +959,14 @@ func (fs *S3Fs) doMultipartCopy(source, target, contentType string, fileSize int
return copyError
}
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))

View file

@ -92,154 +92,154 @@ CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
-----END EC PRIVATE KEY-----`
caCRT = `-----BEGIN CERTIFICATE-----
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
QXV0aDAeFw0yMjA3MDQxNTQzMTFaFw0yNDAxMDQxNTUzMDhaMBMxETAPBgNVBAMT
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4eyDJkmW
D4OVYo7ddgiZkd6QQdPyLcsa31Wc9jdR2/peEabyNT8jSWteS6ouY84GRlnhfFeZ
mpXgbaUJu/Z8Y/8riPxwL8XF4vCScQDMywpQnVUd6E9x2/+/uaD4p/BBswgKqKPe
uDcHZn7MkD4QlquUhMElDrBUi1Dv/AVHnQ6iP4vd5Jlv0F+40jdq/8Wa7yhW7Pu5
iNvPwCk8HjENBKVur/re+Acif8A2TlbCsuOnVduSQNmnWH+iZmB9upyBZtUszGS0
JhUwtSnwUX/JapF70Pwte/PV3RK8cJ5FjuAPNeTyJvSuMTELFSAyCeiNynFGgyhW
cqbEiPu6BURLculyVkmh4dOrhTrYZv/n3UJAhyxkdYrbh3INHmTa4izvclcuwoEo
lFlJp3l77D0lIi+pbtcBV6ys7reyuxUAkBNwnpt2pWfCQoi4QYKcNbHm47c2phOb
QSojQ8SsNU5bnlY2MDzkKo5DPav/i4d0HpndphUpx4f8hA0KylLevDRkMz9TAH7H
uDssn0CxFOGHiveEAGGbn+doHjNWM339x/cdLbK0vuieDKby8YYcBY1JML57Dl9f
rs52ySnDZbMqOb9zF66mQpC2FZoAj713xSkDSnSCUekrqgck1EA1ifxAviHt+p26
JwaEDL7Lk01EEdYN4csSd1fezbCqTrG8ffUCAwEAAaNFMEMwDgYDVR0PAQH/BAQD
AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFPirPBPO01zUuf7xC+ds
bOOY5QvAMA0GCSqGSIb3DQEBCwUAA4ICAQBUYa+ydfTPKjTN4lXyEZgchZQ+juny
aMy1xosLz6Evj0us2Bwczmy6X2Zvaw/KteFlgKaU1Ex2UkU7FfAlaH0HtwTLFMVM
p9nB7ZzStvg0n8zFM29SEkOFwZ9FRonxx4sY3FdvI4QvAWyDyqgOl8+Eedg0kC4+
M7hxarTFmZZ7POZl8Hio592yx3asMmSCcmb7oUCKVI98qsf9fuL+LIZSpn4fE7av
AiNBcOqCZ10CRnl4VSgAW2LH4oqROYdUv+me1u1YRwh7fCF/R7VjOLuaDzv0mp/g
hzG9U+Yso3WV4b28MsctwUmGTK8Zc5QaANKgmI3ulkta37wN5KjrUuescHC7MqZg
vN9n60801be1EoUL83KUx57Bix95YZR02Zge0gYdYTb+E2bwaZ4GMlf7cs6qmC6A
ZPLR7Tffw2J4dPTcfEx3rPZ91s3MkAdPzYYGdGlbKp8RCFnezZ7rw2z57rnT0zDr
LuL3Q6ADBfothoos/EBIC5ekXb9czp8gig+nJXLC6jlqcQpCLrV88oS3+8zACmx1
d6tje9uuAqPgiQGddKZj4b4BlHmAMXq0PufQsZVoyzboTewZiLVCtTR9/iF7Cepg
6EVv57p61pFhPu8lNRAi0aH/po9yt+7435FGpn2kan6k9aDIVdaqeuxxITwsqJ4R
WwSa13hh6yjoDQ==
QXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m
fi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1
v03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh
yzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL
CdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U
4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb
KF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo
NlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLqR/BJTbbyXUB0imne1u00fuzb
S7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb78x7ivdyXSF5LVQJ1JvhhWu6i
M6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpBP8d2jcRZVUVrSXGc2mAGuGOY
/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2ZxugCXULtRWJ9p4C9zUl40HEy
OQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD
AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNoJhIvDZQrEf/VQbWuu
XgNnt2m5MA0GCSqGSIb3DQEBCwUAA4ICAQCYhT5SRqk19hGrQ09hVSZOzynXAa5F
sYkEWJzFyLg9azhnTPE1bFM18FScnkd+dal6mt+bQiJvdh24NaVkDghVB7GkmXki
pAiZwEDHMqtbhiPxY8LtSeCBAz5JqXVU2Q0TpAgNSH4W7FbGWNThhxcJVOoIrXKE
jbzhwl1Etcaf0DBKWliUbdlxQQs65DLy+rNBYtOeK0pzhzn1vpehUlJ4eTFzP9KX
y2Mksuq9AspPbqnqpWW645MdTxMb5T57MCrY3GDKw63z5z3kz88LWJF3nOxZmgQy
WFUhbLmZm7x6N5eiu6Wk8/B4yJ/n5UArD4cEP1i7nqu+mbbM/SZlq1wnGpg/sbRV
oUF+a7pRcSbfxEttle4pLFhS+ErKatjGcNEab2OlU3bX5UoBs+TYodnCWGKOuBKV
L/CYc65QyeYZ+JiwYn9wC8YkzOnnVIQjiCEkLgSL30h9dxpnTZDLrdAA8ItelDn5
DvjuQq58CGDsaVqpSobiSC1DMXYWot4Ets1wwovUNEq1l0MERB+2olE+JU/8E23E
eL1/aA7Kw/JibkWz1IyzClpFDKXf6kR2onJyxerdwUL+is7tqYFLysiHxZDL1bli
SXbW8hMa5gvo0IilFP9Rznn8PplIfCsvBDVv6xsRr5nTAFtwKaMBVgznE2ghs69w
kK8u1YiiVenmoQ==
-----END CERTIFICATE-----`
caCRL = `-----BEGIN X509 CRL-----
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
MjIwNzA0MTU1MzU4WhcNMjQwNzAzMTU1MzU4WjAkMCICEQDZo5Q3lhxFuDUsxGNm
794YFw0yMjA3MDQxNTUzNThaoCMwITAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8Qvn
bGzjmOULwDANBgkqhkiG9w0BAQsFAAOCAgEA1lK6g8qmhyY6myx8342dDuaauY03
0iojkxpasuYcytK6XRm96YqjZK9EETxsHHViVU0vCXES60D6wJ9gw4fTWn3WxEdx
nIwbGyjUGHh2y+R3uQsfvwxsdYvDsTLAnOLwOo68dAHWmMDZRmgTuGNoYFxVQRGR
Cn90ZR7LPLpCScclWM8FE/W1B90x3ZE8EhJiCI/WyyTh3EgshmB7A5GoDrFZfmvR
dzoTKO+F9p2XjtmgfiBE3czWQysfATmbutZUbG/ZRb89u+ZEUyPoC94mg8fhNWoX
1d5G9QAkZFHp957/5QHLq9OHNfnWXoohhebjF4VWqZH7w+RtLc8t0PIog2lX4t1o
5N/xFk9akvuoyNGg/fYuJBmN162Q0MdeYfYKDGWdXxf6fpHxVr5v2JrIx6gOwubb
cIKP22ZBv/PYOeFsAZ755lTl4OTFUjU5ZJEPD6pUc1daaIqfxsxu8gDZP92FZjsB
zaalMbh30n2OhagSMBzSLg5rE6WmBzlQX0ZN8YrW4l2Vq6twnnFHY+UyblRZS+d4
oHBaoOaxPEkLxNZ8ulzJS4B6c4D1CXOaBEf++snVzRRUOEdX3x7TvkkrLvIsm06R
ux0L1zJb9LbZ/1rhuv70z/kIlD55sqYuRqu3RpgTgZuTERU//rYIqWd03Y5Qon8i
VoC6Yp9DPldQJrk=
MjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg
ZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r
rl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV
+jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199
XSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE
jF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF
DouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD
MSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4
iYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE
ypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4
hVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94
cp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67
sn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn
w7bXJGfJcIMrrKs=
-----END X509 CRL-----`
client1Crt = `-----BEGIN CERTIFICATE-----
MIIEITCCAgmgAwIBAgIRAJla/m/UkZMifNwG+DxFr2MwDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjIwNzA0MTU0MzM3WhcNMjQwMTA0MTU1
MzA3WjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA8xM5v+2QfdzfwnNT5cl+6oEy2fZoI2YG6L6c25rG0pr+yl1IHKdM
Zcvn93uat7hlbzxeOLfJRM7+QK1lLaxuppq9p+gT+1x9eG3E4X7e0pdbjrpJGbvN
ji0hwDBLDWD8mHNq/SCk9FKtGnfZqrNB5BLw2uIKjJzVGXVlsjN6geBDm2hVjTSm
zMr39CfLUdtvMaZhpIPJzbH+sNfp1zKavFIpmwCd77p/z0QAiQ9NaIvzv4PZDDEE
MUHzmVAU6bUjD8GToXaMbRiz694SU8aAwvvcdjGexdbHnfSAfLOl2wTPPxvePncR
aa656ZeZWxY9pRCItP+v43nm7d4sAyRD4QIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQbwDqF
aja3ifZHm6mtSeTK9IHc+zAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8QvnbGzjmOUL
wDANBgkqhkiG9w0BAQsFAAOCAgEAprE/zV6u8UIH8g4Jb73wtUD/eIL3iBJ7mNYa
lqwCyJrWH7/F9fcovJnF9WO1QPTeHxhoD9rlQK70GitUAeboYw611yNWDS4tDlaL
sjpJKykUxBgBR7QSLZCrPtQ3fP2WvlZzLGqB28rASTLphShqTuGp4gJaxGHfbCU7
mlV9QYi+InQxOICJJPebXUOwx5wYkFQWJ9qE1AK3QrWPi8QYFznJvHgkNAaMBEmI
jAlggOzpveVvy8f4z3QG9o29LIwp7JvtJQs7QXL80FZK98/8US/3gONwTrBz2Imx
28ywvwCq7fpMyPgxX4sXtxphCNim+vuHcqDn2CvLS9p/6L6zzqbFNxpmMkJDLrOc
YqtHE4TLWIaXpb5JNrYJgNCZyJuYDICVTbivtMacHpSwYtXQ4iuzY2nIr0+4y9i9
MNpqv3W47xnvgUQa5vbTbIqo2NSY24A84mF5EyjhaNgNtDlN56+qTQ6HLZNVr6pv
eUCCWnY4GkaZUEU1M8/uNtKaZKv1WA7gJxZDQHj8+R110mPtzm1C5jqg7jSjGy9C
8PhAwBqIXkVLNayFEtyZZobTxMH5qY1yFkI3sic7S9ZyXt3quY1Q1UT3liRteIm/
sZHC5zEoidsHObkTeU44hqZVPkbvrfmgW01xTJjddnMPBH+yqjCCc94yCbW79j/2
7LEmxYg=
MIIEITCCAgmgAwIBAgIRAJr32nHRlhyPiS7IfZ/ZWYowDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjM3WhcNMzQwMTEwMTgy
MTUzWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+kiJ3X6
HUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi85xE
OkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLWeYl7
Qie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4ZdRf
XlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dybbhO
c9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRUh5Xo
Gzjh6iReaPSOgGatqOw9bDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp
uTANBgkqhkiG9w0BAQsFAAOCAgEAyAK7cOTWqjyLgFM0kyyx1fNPvm2GwKep3MuU
OrSnLuWjoxzb7WcbKNVMlnvnmSUAWuErxsY0PUJNfcuqWiGmEp4d/SWfWPigG6DC
sDej35BlSfX8FCufYrfC74VNk4yBS2LVYmIqcpqUrfay0I2oZA8+ToLEpdUvEv2I
l59eOhJO2jsC3JbOyZZmK2Kv7d94fR+1tg2Rq1Wbnmc9AZKq7KDReAlIJh4u2KHb
BbtF79idusMwZyP777tqSQ4THBMa+VAEc2UrzdZqTIAwqlKQOvO2fRz2P+ARR+Tz
MYJMdCdmPZ9qAc8U1OcFBG6qDDltO8wf/Nu/PsSI5LGCIhIuPPIuKfm0rRfTqCG7
QPQPWjRoXtGGhwjdIuWbX9fIB+c+NpAEKHgLtV+Rxj8s5IVxqG9a5TtU9VkfVXJz
J20naoz/G+vDsVINpd3kH0ziNvdrKfGRM5UgtnUOPCXB22fVmkIsMH2knI10CKK+
offI56NTkLRu00xvg98/wdukhkwIAxg6PQI/BHY5mdvoacEHHHdOhMq+GSAh7DDX
G8+HdbABM1ExkPnZLat15q706ztiuUpQv1C2DI8YviUVkMqCslj4cD4F8EFPo4kr
kvme0Cuc9Qlf7N5rjdV3cjwavhFx44dyXj9aesft2Q1okPiIqbGNpcjHcIRlj4Au
MU3Bo0A=
-----END CERTIFICATE-----`
client1Key = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----`
// client 2 crt is revoked
client2Crt = `-----BEGIN CERTIFICATE-----
MIIEITCCAgmgAwIBAgIRANmjlDeWHEW4NSzEY2bv3hgwDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjIwNzA0MTU0MzUxWhcNMjQwMTA0MTU1
MzA3WjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAzNl7q7yS8MSaQs6zRbuqrsUuwEJ5ZH85vf7zHZKgOW3zNniXLOmH
JdtQ3jKZQ1BCIsJFvez2GxGIMWbXaSPw4bL0J3vl5oItChsjGg34IvqcDxWuIk2a
muRdMh7r1ryVs2ir2cQ5YHzI59BEpUWKQg3bD4yragdkb6BRc7lVgzCbrM1Eq758
HHbaLwlsfpqOvheaum4IG113CeD/HHrw42W6g/qQWL+FHlYqV3plHZ8Bj+bhcZI5
jdU4paGEzeY0a0NlnyH4gXGPjLKvPKFZHy4D6RiRlLHvHeiRyDtTu4wFkAiXxzGs
E4UBbykmYUB85zgwpjaktOaoe36IM1T8CQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRdYIEk
gxh+vTaMpAbqaPGRKGGBpTAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8QvnbGzjmOUL
wDANBgkqhkiG9w0BAQsFAAOCAgEABSR/PbPfiNZ6FOrt91/I0g6LviwICDcuXhfr
re4UsWp1kxXeS3CB2G71qXv3hswN8phG2hdsij0/FBEGUTLS3FTCmLmqmcVqPj3/
677PMFDoACBKgT5iIwpnNvdD+4ROM8JFjUwy7aTWx85a5yoPFGnB+ORMfLCYjr2S
D02KFvKuSXWCjXphqJ41cFGne4oeh/JMkN0RNArm7wTT8yWCGgO1k4OON8dphuTV
48Wm6I9UBSWuLk1vcIlgb/8YWVwy9rBNmjOBDGuroL6PSmfZD+e9Etii0X2znZ+t
qDpXJB7V5U0DbsBCtGM/dHaFz/LCoBYX9z6th1iPUHksUTM3RzN9L24r9/28dY/a
shBpn5rK3ui/2mPBpO26wX14Kl/DUkdKUV9dJllSlmwo8Z0RluY9S4xnCrna/ODH
FbhWmlTSs+odCZl6Lc0nuw+WQ2HnlTVJYBSFAGfsGQQ3pzk4DC5VynnxY0UniUgD
WYPR8JEYa+BpH3rIQ9jmnOKWLtyc7lFPB9ab63pQBBiwRvWo+tZ2vybqjeHPuu5N
BuKvvtu3RKKdSCnIo5Rs5zw4JYCjvlx/NVk9jtpa1lIHYHilvBmCcRX5DkE/yH/x
IjEKhCOQpGR6D5Kkca9xNL7zNcat3bzLn+d7Wo4m09uWi9ifPdchxed0w5d9ihx1
enqNrFI=
MIIEITCCAgmgAwIBAgIRAM5p4duNjikSPxYGaqBm5FEwDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjUyWhcNMzQwMTEwMTgy
MTUzWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImVjm/b
Qe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TPkZua
eq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEKh4LQ
cr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81PQAmT
A0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu4Ic0
6tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBR5mf0f
Zjf8ZCGXqU2+45th7VkkLDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp
uTANBgkqhkiG9w0BAQsFAAOCAgEARhFxNAouwbpEfN1M90+ao5rwyxEewerSoCCz
PQzeUZ66MA/FkS/tFUGgGGG+wERN+WLbe1cN6q/XFr0FSMLuUxLXDNV02oUL/FnY
xcyNLaZUZ0pP7sA+Hmx2AdTA6baIwQbyIY9RLAaz6hzo1YbI8yeis645F1bxgL2D
EP5kXa3Obv0tqWByMZtrmJPv3p0W5GJKXVDn51GR/E5KI7pliZX2e0LmMX9mxfPB
4sXFUggMHXxWMMSAmXPVsxC2KX6gMnajO7JUraTwuGm+6V371FzEX+UKXHI+xSvO
78TseTIYsBGLjeiA8UjkKlD3T9qsQm2mb2PlKyqjvIm4i2ilM0E2w4JZmd45b925
7q/QLV3NZ/zZMi6AMyULu28DWKfAx3RLKwnHWSFcR4lVkxQrbDhEUMhAhLAX+2+e
qc7qZm3dTabi7ZJiiOvYK/yNgFHa/XtZp5uKPB5tigPIa+34hbZF7s2/ty5X3O1N
f5Ardz7KNsxJjZIt6HvB28E/PPOvBqCKJc1Y08J9JbZi8p6QS1uarGoR7l7rT1Hv
/ZXkNTw2bw1VpcWdzDBLLVHYNnJmS14189LVk11PcJJpSmubwCqg+ZZULdgtVr3S
ANas2dgMPVwXhnAalgkcc+lb2QqaEz06axfbRGBsgnyqR5/koKCg1Hr0+vThHSsR
E0+r2+4=
-----END CERTIFICATE-----`
client2Key = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----`
testFileName = "test_file_dav.dat"
testDLFileName = "test_download_dav.dat"

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -28,12 +28,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<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}}'},
data: JSON.stringify(data),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('#spinnerModal').modal('hide');
@ -526,7 +526,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify(data),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('#spinnerModal').modal('hide');

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}}
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<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}}
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<div class="card-header py-3">
<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>
</button>
</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">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}}
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<div class="card-header py-3">
<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}}
{{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>
<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>
</button>
</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="successTxt" class="card-body"></div>
</div>

View file

@ -28,12 +28,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}}
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<div class="card-header py-3">
<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}}
{{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>
<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>
</button>
</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="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=""
value="{{.S3Config.KeyPrefix}}" aria-describedby="S3KeyPrefixHelpBlock">
<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>
</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=""
value="{{.GCSConfig.KeyPrefix}}" aria-describedby="GCSKeyPrefixHelpBlock">
<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>
</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=""
value="{{.AzBlobConfig.KeyPrefix}}" aria-describedby="AzKeyPrefixHelpBlock">
<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>
</div>
</div>

View file

@ -222,7 +222,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<div class="card-body">
<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="col-md-12 form_field_patterns_outer">
{{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 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>

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}}
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<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}}
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<div class="card-header py-3">
<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>
</button>
</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">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
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="successTOTPTxt" class="card-body"></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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorTOTPMsg(){
$('#errorTOTPMsg').hide();
}
</script>
<div>
<p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p>
</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="successRecCodesTxt" class="card-body"></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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorRecCodesMsg(){
$('#errorRecCodesMsg').hide();
}
</script>
<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>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}}'},
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('.totpDisable').hide();
@ -209,7 +219,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
totpSave();
@ -242,7 +252,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('#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}}'},
data: JSON.stringify({"enabled": false}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
location.reload();
@ -357,7 +367,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('.viewRecoveryCodes').hide();

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}}
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<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();
});
$("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 () {
let index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
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>
Fingerprint: "{{.Fingerprint}}"
<br>
Algorithms: "{{.GetAlgosAsString}}"
<br>
{{end}}
<br>
Host Key algorithms: "{{.Status.SSH.GetHostKeyAlgosAsString}}"
<br><br>
MAC algorithms: "{{.Status.SSH.GetMACsAsString}}"
<br><br>
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 class="card-body">
<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="col-md-12 form_field_patterns_outer">
{{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 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>

View file

@ -28,12 +28,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{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>
<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>
</button>
</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="successTxt" class="card-body"></div>

View file

@ -42,16 +42,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}}
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<div class="card-header">
<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">
<a id="idBack" class="btn btn-secondary mx-1 my-1" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a>
{{if not .ReadOnly}}

View file

@ -34,6 +34,82 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</style>
{{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"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
@ -1182,7 +1258,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
if (row["type"] == "1") {
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>`;
}
let icon = getIconForFile(data);
@ -1195,7 +1271,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"data": "size",
"render": function (data, type, row) {
if (type === 'display') {
if (data){
if (data || data === 0){
return fileSizeIEC(data);
}
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="successTOTPTxt" class="card-body"></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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorTOTPMsg(){
$('#errorTOTPMsg').hide();
}
</script>
<div>
<p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p>
</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="successRecCodesTxt" class="card-body"></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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorRecCodesMsg(){
$('#errorRecCodesMsg').hide();
}
</script>
<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>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}}'},
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('.totpDisable').hide();
@ -236,7 +246,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
totpSave();
@ -273,7 +283,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "protocols": protocolsArray, "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('#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}}'},
data: JSON.stringify({"protocols": protocolsArray}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('#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}}'},
data: JSON.stringify({"enabled": false}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
location.reload();
@ -429,7 +439,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('.viewRecoveryCodes').hide();

View file

@ -46,12 +46,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</button>
</div>
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div id="tableContainer" class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<thead>
@ -485,7 +490,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
if (row["type"] == "1") {
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>`;
}
var icon = getIconForFile(data);
@ -498,7 +503,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"data": "size",
"render": function (data, type, row) {
if (type === 'display') {
if (data){
if (data || data === 0){
return fileSizeIEC(data);
}
return "";

View file

@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}}
{{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>
<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>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<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"
[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;