Compare commits
51 commits
Author | SHA1 | Date | |
---|---|---|---|
|
501a870e9d | ||
|
25a498a153 | ||
|
76fddc126d | ||
|
b3ce596385 | ||
|
a73c6569f9 | ||
|
a6a92f0d69 | ||
|
a3d6d9cd33 | ||
|
8812e5e450 | ||
|
a132a21a38 | ||
|
5d9cda9d34 | ||
|
14d79e052c | ||
|
b81f819b3e | ||
|
5c1c7e4fa3 | ||
|
ebec3042e9 | ||
|
50cae4ee7d | ||
|
a4009c8894 | ||
|
c50d2c15e8 | ||
|
cd953e6794 | ||
|
f5d64a1a8a | ||
|
9a9d16292a | ||
|
1c579d73f8 | ||
|
904ad2f691 | ||
|
bc6bdb2f05 | ||
|
d9ac1a5631 | ||
|
f37b57884f | ||
|
d6e31ce8e2 | ||
|
cf1cc25a48 | ||
|
9906caefd5 | ||
|
bef0e10d1e | ||
|
e8df1b6e4c | ||
|
991739d47a | ||
|
1508fc9253 | ||
|
520e22b63d | ||
|
d6b584e064 | ||
|
cc381443be | ||
|
89a251d640 | ||
|
dbbae3129d | ||
|
c457538280 | ||
|
7f65aa1fa4 | ||
|
abac3cfc8d | ||
|
a805a930e8 | ||
|
de72495092 | ||
|
7c845f07d5 | ||
|
b9ace46180 | ||
|
e446e3392d | ||
|
a503feaab6 | ||
|
cba894987c | ||
|
1d120bdd26 | ||
|
7245710b31 | ||
|
3a3df5670d | ||
|
97bbf37af4 |
88 changed files with 2513 additions and 4281 deletions
20
.github/workflows/development.yml
vendored
20
.github/workflows/development.yml
vendored
|
@ -2,7 +2,7 @@ name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [2.5.x]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -11,11 +11,11 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go: ['1.20']
|
go: ['1.21']
|
||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
upload-coverage: [true]
|
upload-coverage: [true]
|
||||||
include:
|
include:
|
||||||
- go: '1.20'
|
- go: '1.21'
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
upload-coverage: false
|
upload-coverage: false
|
||||||
|
|
||||||
|
@ -232,7 +232,7 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.21'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
@ -268,7 +268,7 @@ jobs:
|
||||||
MYSQL_USER: sftpgo
|
MYSQL_USER: sftpgo
|
||||||
MYSQL_PASSWORD: sftpgo
|
MYSQL_PASSWORD: sftpgo
|
||||||
options: >-
|
options: >-
|
||||||
--health-cmd "mysqladmin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD"
|
--health-cmd "mariadb-admin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD"
|
||||||
--health-interval 10s
|
--health-interval 10s
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 6
|
--health-retries 6
|
||||||
|
@ -296,7 +296,7 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.21'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
@ -409,7 +409,7 @@ jobs:
|
||||||
echo 'apt-get install -q -y curl gcc' >> build.sh
|
echo 'apt-get install -q -y curl gcc' >> build.sh
|
||||||
if [ ${{ matrix.go }} == 'latest' ]
|
if [ ${{ matrix.go }} == 'latest' ]
|
||||||
then
|
then
|
||||||
echo 'GO_VERSION=$(curl -L https://go.dev/VERSION?m=text)' >> build.sh
|
echo 'GO_VERSION=$(curl -L https://go.dev/VERSION?m=text | head -n 1)' >> build.sh
|
||||||
else
|
else
|
||||||
echo 'GO_VERSION=${{ matrix.go }}' >> build.sh
|
echo 'GO_VERSION=${{ matrix.go }}' >> build.sh
|
||||||
fi
|
fi
|
||||||
|
@ -452,7 +452,7 @@ jobs:
|
||||||
apt-get install -q -y curl gcc
|
apt-get install -q -y curl gcc
|
||||||
if [ ${{ matrix.go }} == 'latest' ]
|
if [ ${{ matrix.go }} == 'latest' ]
|
||||||
then
|
then
|
||||||
GO_VERSION=$(curl -L https://go.dev/VERSION?m=text)
|
GO_VERSION=$(curl -L https://go.dev/VERSION?m=text | head -n 1)
|
||||||
else
|
else
|
||||||
GO_VERSION=${{ matrix.go }}
|
GO_VERSION=${{ matrix.go }}
|
||||||
fi
|
fi
|
||||||
|
@ -517,9 +517,9 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.21'
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Run golangci-lint
|
- name: Run golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v3
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: v1.54.2
|
||||||
|
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
|
@ -5,7 +5,7 @@ on:
|
||||||
# - cron: '0 4 * * *' # everyday at 4:00 AM UTC
|
# - cron: '0 4 * * *' # everyday at 4:00 AM UTC
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- 2.5.x
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -5,7 +5,7 @@ on:
|
||||||
tags: 'v*'
|
tags: 'v*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.20.5
|
GO_VERSION: 1.21.10
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare-sources-with-deps:
|
prepare-sources-with-deps:
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
FROM golang:1.20-bullseye as builder
|
FROM golang:1.21-bullseye as builder
|
||||||
|
|
||||||
ENV GOFLAGS="-mod=readonly"
|
ENV GOFLAGS="-mod=readonly"
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get -y upgrade && apt-get install --no-install-recommends -y openssh-server && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN mkdir -p /workspace
|
RUN mkdir -p /workspace
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
@ -28,14 +30,12 @@ ARG DOWNLOAD_PLUGINS=false
|
||||||
|
|
||||||
RUN if [ "${DOWNLOAD_PLUGINS}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y curl && ./docker/scripts/download-plugins.sh; fi
|
RUN if [ "${DOWNLOAD_PLUGINS}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y curl && ./docker/scripts/download-plugins.sh; fi
|
||||||
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y openssh-server && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
FROM debian:bullseye-slim
|
FROM debian:bullseye-slim
|
||||||
|
|
||||||
# Set to "true" to install jq and the optional git and rsync dependencies
|
# Set to "true" to install jq and the optional git and rsync dependencies
|
||||||
ARG INSTALL_OPTIONAL_PACKAGES=false
|
ARG INSTALL_OPTIONAL_PACKAGES=false
|
||||||
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates media-types && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get -y upgrade && apt-get install --no-install-recommends -y ca-certificates media-types && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y jq git rsync && rm -rf /var/lib/apt/lists/*; fi
|
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y jq git rsync && rm -rf /var/lib/apt/lists/*; fi
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
FROM golang:1.20-alpine3.18 AS builder
|
FROM golang:1.21-alpine3.18 AS builder
|
||||||
|
|
||||||
ENV GOFLAGS="-mod=readonly"
|
ENV GOFLAGS="-mod=readonly"
|
||||||
|
|
||||||
RUN apk add --update --no-cache bash ca-certificates curl git gcc g++
|
RUN apk -U upgrade --no-cache && apk add --update --no-cache bash ca-certificates curl git gcc g++
|
||||||
|
|
||||||
RUN mkdir -p /workspace
|
RUN mkdir -p /workspace
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
@ -32,7 +32,7 @@ FROM alpine:3.18
|
||||||
# Set to "true" to install jq and the optional git and rsync dependencies
|
# Set to "true" to install jq and the optional git and rsync dependencies
|
||||||
ARG INSTALL_OPTIONAL_PACKAGES=false
|
ARG INSTALL_OPTIONAL_PACKAGES=false
|
||||||
|
|
||||||
RUN apk add --update --no-cache ca-certificates tzdata mailcap
|
RUN apk -U upgrade --no-cache && apk add --update --no-cache ca-certificates tzdata mailcap
|
||||||
|
|
||||||
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq git rsync; fi
|
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq git rsync; fi
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ I'd like to make SFTPGo into a sustainable long term project and would not like
|
||||||
If you use SFTPGo, it is in your best interest to ensure that the project you rely on stays healthy and well maintained.
|
If you use SFTPGo, it is in your best interest to ensure that the project you rely on stays healthy and well maintained.
|
||||||
This can only happen with your donations and [sponsorships](https://github.com/sponsors/drakkan) :heart:
|
This can only happen with your donations and [sponsorships](https://github.com/sponsors/drakkan) :heart:
|
||||||
|
|
||||||
You can also purchase support plans from the [SFTPGo website](https://sftpgo.com/#pricing).
|
You can also purchase, using many payment methods, support plans from the [SFTPGo website](https://sftpgo.com/#pricing).
|
||||||
|
|
||||||
With sponsorships/donations or support plans we establish a channel for reciprocal access, ensuring better outcomes for both you and the project.
|
With sponsorships/donations or support plans we establish a channel for reciprocal access, ensuring better outcomes for both you and the project.
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ If you report an invalid issue or ask for step-by-step support, your issue will
|
||||||
- Partial authentication. You can configure multi-step authentication requiring, for example, the user password after successful public key authentication.
|
- Partial authentication. You can configure multi-step authentication requiring, for example, the user password after successful public key authentication.
|
||||||
- Per-user authentication methods.
|
- Per-user authentication methods.
|
||||||
- [Two-factor authentication](./docs/howto/two-factor-authentication.md) based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator, Microsoft Authenticator and other compatible apps.
|
- [Two-factor authentication](./docs/howto/two-factor-authentication.md) based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator, Microsoft Authenticator and other compatible apps.
|
||||||
|
- LDAP/Active Directory authentication using a [plugin](https://github.com/sftpgo/sftpgo-plugin-auth).
|
||||||
- Simplified user administrations using [groups](./docs/groups.md).
|
- Simplified user administrations using [groups](./docs/groups.md).
|
||||||
- [Roles](./docs/roles.md) allow to create limited administrators who can only create and manage users with their role.
|
- [Roles](./docs/roles.md) allow to create limited administrators who can only create and manage users with their role.
|
||||||
- Custom authentication via [external programs/HTTP API](./docs/external-auth.md).
|
- Custom authentication via [external programs/HTTP API](./docs/external-auth.md).
|
||||||
|
|
|
@ -4,11 +4,11 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
|
||||||
|
|
||||||
## Supported tags and respective Dockerfile links
|
## Supported tags and respective Dockerfile links
|
||||||
|
|
||||||
- [v2.5.1, v2.5, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile)
|
- [v2.5.6, v2.5, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile)
|
||||||
- [v2.5.1-plugins, v2.5-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile)
|
- [v2.5.6-plugins, v2.5-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile)
|
||||||
- [v2.5.1-alpine, v2.5-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile.alpine)
|
- [v2.5.6-alpine, v2.5-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile.alpine)
|
||||||
- [v2.5.1-slim, v2.5-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile)
|
- [v2.5.6-slim, v2.5-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile)
|
||||||
- [v2.5.1-alpine-slim, v2.5-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile.alpine)
|
- [v2.5.6-alpine-slim, v2.5-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.5.6/Dockerfile.alpine)
|
||||||
- [edge](../Dockerfile)
|
- [edge](../Dockerfile)
|
||||||
- [edge-plugins](../Dockerfile)
|
- [edge-plugins](../Dockerfile)
|
||||||
- [edge-alpine](../Dockerfile.alpine)
|
- [edge-alpine](../Dockerfile.alpine)
|
||||||
|
|
|
@ -17,9 +17,23 @@ esac
|
||||||
|
|
||||||
echo "download plugins for arch ${SUFFIX}"
|
echo "download plugins for arch ${SUFFIX}"
|
||||||
|
|
||||||
for PLUGIN in geoipfilter kms pubsub eventstore eventsearch metadata
|
curl -L "https://github.com/sftpgo/sftpgo-plugin-geoipfilter/releases/download/v1.0.7/sftpgo-plugin-geoipfilter-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-geoipfilter"
|
||||||
do
|
chmod 755 "/usr/local/bin/sftpgo-plugin-geoipfilter"
|
||||||
echo "download plugin from https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}"
|
|
||||||
curl -L "https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-${PLUGIN}"
|
curl -L "https://github.com/sftpgo/sftpgo-plugin-kms/releases/download/v1.0.10/sftpgo-plugin-kms-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-kms"
|
||||||
chmod 755 "/usr/local/bin/sftpgo-plugin-${PLUGIN}"
|
chmod 755 "/usr/local/bin/sftpgo-plugin-kms"
|
||||||
done
|
|
||||||
|
curl -L "https://github.com/sftpgo/sftpgo-plugin-pubsub/releases/download/v1.0.11/sftpgo-plugin-pubsub-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-pubsub"
|
||||||
|
chmod 755 "/usr/local/bin/sftpgo-plugin-pubsub"
|
||||||
|
|
||||||
|
curl -L "https://github.com/sftpgo/sftpgo-plugin-eventstore/releases/download/v1.0.15/sftpgo-plugin-eventstore-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-eventstore"
|
||||||
|
chmod 755 "/usr/local/bin/sftpgo-plugin-eventstore"
|
||||||
|
|
||||||
|
curl -L "https://github.com/sftpgo/sftpgo-plugin-eventsearch/releases/download/v1.0.15/sftpgo-plugin-eventsearch-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-eventsearch"
|
||||||
|
chmod 755 "/usr/local/bin/sftpgo-plugin-eventsearch"
|
||||||
|
|
||||||
|
curl -L "https://github.com/sftpgo/sftpgo-plugin-metadata/releases/download/v1.0.12/sftpgo-plugin-metadata-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-metadata"
|
||||||
|
chmod 755 "/usr/local/bin/sftpgo-plugin-metadata"
|
||||||
|
|
||||||
|
curl -L "https://github.com/sftpgo/sftpgo-plugin-auth/releases/download/v1.0.5/sftpgo-plugin-auth-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-auth"
|
||||||
|
chmod 755 "/usr/local/bin/sftpgo-plugin-auth"
|
||||||
|
|
|
@ -138,9 +138,9 @@ The configuration file contains the following sections:
|
||||||
- `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default `SFTPGo_<version>`, for example `SSH-2.0-SFTPGo_0.9.5`
|
- `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default `SFTPGo_<version>`, for example `SSH-2.0-SFTPGo_0.9.5`
|
||||||
- `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty, the daemon will search or try to generate `id_rsa`, `id_ecdsa` and `id_ed25519` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa`, `id_ecdsa` and/or `id_ed25519` then SFTPGo will try to generate these keys using the default settings.
|
- `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty, the daemon will search or try to generate `id_rsa`, `id_ecdsa` and `id_ed25519` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa`, `id_ecdsa` and/or `id_ed25519` then SFTPGo will try to generate these keys using the default settings.
|
||||||
- `host_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty.
|
- `host_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty.
|
||||||
- `host_key_algorithms`, list of strings. Public key algorithms that the server will accept for host key authentication. The supported values are: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ssh-rsa-cert-v01@openssh.com`, `ssh-dss-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`. Default values: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-ed25519`.
|
- `host_key_algorithms`, list of strings. Public key algorithms that the server will accept for host key authentication. The supported values are: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ssh-rsa-cert-v01@openssh.com`, `ssh-dss-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`. Certificate algorithms are listed for backward compatibility purposes only, they are not used. Default values: `rsa-sha2-512`, `rsa-sha2-256`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `ssh-ed25519`.
|
||||||
- `moduli`, list of strings. Diffie-Hellman moduli files. Each moduli file can be defined as a path relative to the configuration directory or an absolute one. If set and valid, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` KEX algorithms will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you don't explicitly set KEXs. Invalid moduli file will be silently ignored. Default: empty.
|
- `moduli`, list of strings. Diffie-Hellman moduli files. Each moduli file can be defined as a path relative to the configuration directory or an absolute one. If set and valid, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` KEX algorithms will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you don't explicitly set KEXs. Invalid moduli file will be silently ignored. Default: empty.
|
||||||
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group18-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. SHA512 based KEXs are disabled by default because they are slow. If you set one or more moduli files, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` will be available.
|
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. SHA512 based KEXs are disabled by default because they are slow. If you set one or more moduli files, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` will be available.
|
||||||
- `ciphers`, list of strings. Allowed ciphers in preference order. Leave empty to use default values. The supported values are: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`, `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `3des-cbc`, `arcfour256`, `arcfour128`, `arcfour`. Default values: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`. Please note that the ciphers disabled by default are insecure, you should expect that an active attacker can recover plaintext if you enable them.
|
- `ciphers`, list of strings. Allowed ciphers in preference order. Leave empty to use default values. The supported values are: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`, `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `3des-cbc`, `arcfour256`, `arcfour128`, `arcfour`. Default values: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`. Please note that the ciphers disabled by default are insecure, you should expect that an active attacker can recover plaintext if you enable them.
|
||||||
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values are: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`, `hmac-sha2-512-etm@openssh.com`, `hmac-sha2-512`, `hmac-sha1`, `hmac-sha1-96`. Default values: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`.
|
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values are: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`, `hmac-sha2-512-etm@openssh.com`, `hmac-sha2-512`, `hmac-sha1`, `hmac-sha1-96`. Default values: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`.
|
||||||
- `trusted_user_ca_keys`, list of public keys paths of certificate authorities that are trusted to sign user certificates for authentication. The paths can be absolute or relative to the configuration directory.
|
- `trusted_user_ca_keys`, list of public keys paths of certificate authorities that are trusted to sign user certificates for authentication. The paths can be absolute or relative to the configuration directory.
|
||||||
|
|
225
go.mod
225
go.mod
|
@ -3,168 +3,181 @@ module github.com/drakkan/sftpgo/v2
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/storage v1.30.1
|
cloud.google.com/go/storage v1.36.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1
|
||||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
|
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
|
||||||
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736
|
github.com/alexedwards/argon2id v1.0.0
|
||||||
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
|
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
|
||||||
github.com/aws/aws-sdk-go-v2 v1.18.0
|
github.com/aws/aws-sdk-go-v2 v1.24.0
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.18.25
|
github.com/aws/aws-sdk-go-v2/config v1.26.1
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.24
|
github.com/aws/aws-sdk-go-v2/credentials v1.16.12
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7
|
||||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.11
|
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.5
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.19.8
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.25.5
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0
|
github.com/aws/aws-sdk-go-v2/service/sts v1.26.5
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.0
|
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||||
github.com/cockroachdb/cockroach-go/v2 v2.3.4
|
github.com/cockroachdb/cockroach-go/v2 v2.3.5
|
||||||
github.com/coreos/go-oidc/v3 v3.6.0
|
github.com/coreos/go-oidc/v3 v3.9.0
|
||||||
github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8
|
github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8
|
||||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||||
github.com/fclairamb/ftpserverlib v0.21.0
|
github.com/fclairamb/ftpserverlib v0.22.0
|
||||||
github.com/fclairamb/go-log v0.4.1
|
github.com/fclairamb/go-log v0.4.1
|
||||||
github.com/go-acme/lego/v4 v4.12.1
|
github.com/go-acme/lego/v4 v4.14.2
|
||||||
github.com/go-chi/chi/v5 v5.0.9-0.20230502103705-7f280968675b
|
github.com/go-chi/chi/v5 v5.0.10
|
||||||
github.com/go-chi/jwtauth/v5 v5.1.0
|
github.com/go-chi/jwtauth/v5 v5.3.0
|
||||||
github.com/go-chi/render v1.0.2
|
github.com/go-chi/render v1.0.3
|
||||||
github.com/go-sql-driver/mysql v1.7.1
|
github.com/go-sql-driver/mysql v1.7.1
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.5.0
|
||||||
github.com/hashicorp/go-hclog v1.5.0
|
github.com/hashicorp/go-hclog v1.6.2
|
||||||
github.com/hashicorp/go-plugin v1.4.10
|
github.com/hashicorp/go-plugin v1.6.0
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.4
|
github.com/hashicorp/go-retryablehttp v0.7.5
|
||||||
github.com/jackc/pgx/v5 v5.3.2-0.20230603125928-d9560c78b8e6
|
github.com/jackc/pgx/v5 v5.4.3
|
||||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||||
github.com/klauspost/compress v1.16.5
|
github.com/klauspost/compress v1.17.4
|
||||||
github.com/lestrrat-go/jwx/v2 v2.0.9
|
github.com/lestrrat-go/jwx/v2 v2.0.18
|
||||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||||
github.com/mattn/go-sqlite3 v1.14.17
|
github.com/mattn/go-sqlite3 v1.14.19
|
||||||
github.com/mhale/smtpd v0.8.0
|
github.com/mhale/smtpd v0.8.1
|
||||||
github.com/minio/sio v0.3.1
|
github.com/minio/sio v0.3.1
|
||||||
github.com/otiai10/copy v1.11.0
|
github.com/otiai10/copy v1.14.0
|
||||||
github.com/pires/go-proxyproto v0.7.0
|
github.com/pires/go-proxyproto v0.7.0
|
||||||
github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6
|
github.com/pkg/sftp v1.13.6
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/prometheus/client_golang v1.15.1
|
github.com/prometheus/client_golang v1.17.0
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/rs/cors v1.9.0
|
github.com/rs/cors v1.10.1
|
||||||
github.com/rs/xid v1.5.0
|
github.com/rs/xid v1.5.0
|
||||||
github.com/rs/zerolog v1.29.1
|
github.com/rs/zerolog v1.31.0
|
||||||
github.com/sftpgo/sdk v0.1.5-0.20230524172149-afb96ebee860
|
github.com/sftpgo/sdk v0.1.6
|
||||||
github.com/shirou/gopsutil/v3 v3.23.5
|
github.com/shirou/gopsutil/v3 v3.23.11
|
||||||
github.com/spf13/afero v1.9.5
|
github.com/spf13/afero v1.11.0
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/spf13/viper v1.16.0
|
github.com/spf13/viper v1.18.1
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2
|
github.com/studio-b12/gowebdav v0.9.0
|
||||||
github.com/subosito/gotenv v1.4.2
|
github.com/subosito/gotenv v1.6.0
|
||||||
github.com/unrolled/secure v1.13.0
|
github.com/unrolled/secure v1.13.0
|
||||||
github.com/wagslane/go-password-validator v0.3.0
|
github.com/wagslane/go-password-validator v0.3.0
|
||||||
github.com/wneessen/go-mail v0.3.10-0.20230531074101-4100fef083df
|
github.com/wneessen/go-mail v0.4.1-0.20230823094700-0bd5390e370d
|
||||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
||||||
go.etcd.io/bbolt v1.3.7
|
go.etcd.io/bbolt v1.3.8
|
||||||
go.uber.org/automaxprocs v1.5.2
|
go.uber.org/automaxprocs v1.5.3
|
||||||
gocloud.dev v0.29.0
|
gocloud.dev v0.35.0
|
||||||
golang.org/x/crypto v0.9.0
|
golang.org/x/crypto v0.17.0
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.19.0
|
||||||
golang.org/x/oauth2 v0.8.0
|
golang.org/x/oauth2 v0.15.0
|
||||||
golang.org/x/sys v0.8.0
|
golang.org/x/sys v0.15.0
|
||||||
golang.org/x/term v0.8.0
|
golang.org/x/term v0.15.0
|
||||||
golang.org/x/time v0.3.0
|
golang.org/x/time v0.5.0
|
||||||
google.golang.org/api v0.125.0
|
google.golang.org/api v0.154.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.110.2 // indirect
|
cloud.google.com/go v0.111.0 // indirect
|
||||||
cloud.google.com/go/compute v1.20.0 // indirect
|
cloud.google.com/go/compute v1.23.3 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
cloud.google.com/go/iam v1.1.0 // indirect
|
cloud.google.com/go/iam v1.1.5 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect
|
||||||
github.com/aws/smithy-go v1.13.5 // indirect
|
github.com/aws/smithy-go v1.19.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/boombuler/barcode v1.0.1 // indirect
|
github.com/boombuler/barcode v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
github.com/fatih/color v1.15.0 // indirect
|
github.com/fatih/color v1.16.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.3.0 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/s2a-go v0.1.7 // indirect
|
||||||
github.com/google/s2a-go v0.1.4 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect
|
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.10.0 // indirect
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
|
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||||
github.com/miekg/dns v1.1.54 // indirect
|
github.com/miekg/dns v1.1.57 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/oklog/run v1.1.0 // indirect
|
github.com/oklog/run v1.1.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||||
github.com/prometheus/client_model v0.4.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.44.0 // indirect
|
github.com/prometheus/common v0.45.0 // indirect
|
||||||
github.com/prometheus/procfs v0.10.1 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
github.com/spf13/cast v1.5.1 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
golang.org/x/mod v0.10.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
|
||||||
golang.org/x/tools v0.9.3 // indirect
|
go.opentelemetry.io/otel v1.21.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
go.opentelemetry.io/otel/metric v1.21.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
go.opentelemetry.io/otel/trace v1.21.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
golang.org/x/mod v0.14.0 // indirect
|
||||||
google.golang.org/grpc v1.55.0 // indirect
|
golang.org/x/sync v0.5.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
golang.org/x/tools v0.16.1 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||||
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
|
||||||
|
google.golang.org/grpc v1.60.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
@ -172,5 +185,5 @@ require (
|
||||||
replace (
|
replace (
|
||||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||||
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
|
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
|
||||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20230608154636-e9d673c2a1a8
|
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20231218163640-d096410108fe
|
||||||
)
|
)
|
||||||
|
|
|
@ -1644,6 +1644,112 @@ func TestIPList(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSQLPlaceholderLimits(t *testing.T) {
|
||||||
|
numGroups := 120
|
||||||
|
numUsers := 120
|
||||||
|
var groupMapping []sdk.GroupMapping
|
||||||
|
|
||||||
|
folder := vfs.BaseVirtualFolder{
|
||||||
|
Name: "testfolder",
|
||||||
|
MappedPath: filepath.Join(os.TempDir(), "folder"),
|
||||||
|
}
|
||||||
|
err := dataprovider.AddFolder(&folder, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for i := 0; i < numGroups; i++ {
|
||||||
|
group := dataprovider.Group{
|
||||||
|
BaseGroup: sdk.BaseGroup{
|
||||||
|
Name: fmt.Sprintf("testgroup%d", i),
|
||||||
|
},
|
||||||
|
UserSettings: dataprovider.GroupUserSettings{
|
||||||
|
BaseGroupUserSettings: sdk.BaseGroupUserSettings{
|
||||||
|
Permissions: map[string][]string{
|
||||||
|
fmt.Sprintf("/dir%d", i): {dataprovider.PermAny},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
group.VirtualFolders = append(group.VirtualFolders, vfs.VirtualFolder{
|
||||||
|
BaseVirtualFolder: folder,
|
||||||
|
VirtualPath: "/vdir",
|
||||||
|
})
|
||||||
|
err := dataprovider.AddGroup(&group, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
groupMapping = append(groupMapping, sdk.GroupMapping{
|
||||||
|
Name: group.Name,
|
||||||
|
Type: sdk.GroupTypeSecondary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
user := dataprovider.User{
|
||||||
|
BaseUser: sdk.BaseUser{
|
||||||
|
Username: "testusername",
|
||||||
|
HomeDir: filepath.Join(os.TempDir(), "testhome"),
|
||||||
|
Status: 1,
|
||||||
|
Permissions: map[string][]string{
|
||||||
|
"/": {dataprovider.PermAny},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: groupMapping,
|
||||||
|
}
|
||||||
|
err = dataprovider.AddUser(&user, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
users, err := dataprovider.GetUsersForQuotaCheck(map[string]bool{user.Username: true})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.Len(t, users, 1) {
|
||||||
|
for i := 0; i < numGroups; i++ {
|
||||||
|
_, ok := users[0].Permissions[fmt.Sprintf("/dir%d", i)]
|
||||||
|
assert.True(t, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for i := 0; i < numUsers; i++ {
|
||||||
|
user := dataprovider.User{
|
||||||
|
BaseUser: sdk.BaseUser{
|
||||||
|
Username: fmt.Sprintf("testusername%d", i),
|
||||||
|
HomeDir: filepath.Join(os.TempDir()),
|
||||||
|
Status: 1,
|
||||||
|
Permissions: map[string][]string{
|
||||||
|
"/": {dataprovider.PermAny},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: []sdk.GroupMapping{
|
||||||
|
{
|
||||||
|
Name: "testgroup0",
|
||||||
|
Type: sdk.GroupTypePrimary,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := dataprovider.AddUser(&user, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
err = dataprovider.DeleteFolder(folder.Name, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for i := 0; i < numUsers; i++ {
|
||||||
|
username := fmt.Sprintf("testusername%d", i)
|
||||||
|
user, err := dataprovider.UserExists(username, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, user.UpdatedAt, user.CreatedAt)
|
||||||
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < numGroups; i++ {
|
||||||
|
groupName := fmt.Sprintf("testgroup%d", i)
|
||||||
|
err = dataprovider.DeleteGroup(groupName, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkBcryptHashing(b *testing.B) {
|
func BenchmarkBcryptHashing(b *testing.B) {
|
||||||
bcryptPassword := "bcryptpassword"
|
bcryptPassword := "bcryptpassword"
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -1316,8 +1317,7 @@ func (c *BaseConnection) GetTransferQuota() dataprovider.TransferQuota {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *BaseConnection) checkUserQuota() (dataprovider.TransferQuota, int, int64) {
|
func (c *BaseConnection) checkUserQuota() (dataprovider.TransferQuota, int, int64) {
|
||||||
clientIP := c.GetRemoteIP()
|
ul, dl, total := c.User.GetDataTransferLimits()
|
||||||
ul, dl, total := c.User.GetDataTransferLimits(clientIP)
|
|
||||||
result := dataprovider.TransferQuota{
|
result := dataprovider.TransferQuota{
|
||||||
ULSize: ul,
|
ULSize: ul,
|
||||||
DLSize: dl,
|
DLSize: dl,
|
||||||
|
@ -1610,7 +1610,7 @@ func (c *BaseConnection) GetOpUnsupportedError() error {
|
||||||
func getQuotaExceededError(protocol string) error {
|
func getQuotaExceededError(protocol string) error {
|
||||||
switch protocol {
|
switch protocol {
|
||||||
case ProtocolSFTP:
|
case ProtocolSFTP:
|
||||||
return fmt.Errorf("%w: %v", sftp.ErrSSHFxFailure, ErrQuotaExceeded.Error())
|
return fmt.Errorf("%w: %w", sftp.ErrSSHFxFailure, ErrQuotaExceeded)
|
||||||
case ProtocolFTP:
|
case ProtocolFTP:
|
||||||
return ftpserver.ErrStorageExceeded
|
return ftpserver.ErrStorageExceeded
|
||||||
default:
|
default:
|
||||||
|
@ -1621,7 +1621,7 @@ func getQuotaExceededError(protocol string) error {
|
||||||
func getReadQuotaExceededError(protocol string) error {
|
func getReadQuotaExceededError(protocol string) error {
|
||||||
switch protocol {
|
switch protocol {
|
||||||
case ProtocolSFTP:
|
case ProtocolSFTP:
|
||||||
return fmt.Errorf("%w: %v", sftp.ErrSSHFxFailure, ErrReadQuotaExceeded.Error())
|
return fmt.Errorf("%w: %w", sftp.ErrSSHFxFailure, ErrReadQuotaExceeded)
|
||||||
default:
|
default:
|
||||||
return ErrReadQuotaExceeded
|
return ErrReadQuotaExceeded
|
||||||
}
|
}
|
||||||
|
@ -1655,29 +1655,33 @@ func (c *BaseConnection) IsQuotaExceededError(err error) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isSFTPGoError(err error) bool {
|
||||||
|
return errors.Is(err, ErrPermissionDenied) || errors.Is(err, ErrNotExist) || errors.Is(err, ErrOpUnsupported) ||
|
||||||
|
errors.Is(err, ErrQuotaExceeded) || errors.Is(err, ErrReadQuotaExceeded) ||
|
||||||
|
errors.Is(err, vfs.ErrStorageSizeUnavailable) || errors.Is(err, ErrShuttingDown)
|
||||||
|
}
|
||||||
|
|
||||||
// GetGenericError returns an appropriate generic error for the connection protocol
|
// GetGenericError returns an appropriate generic error for the connection protocol
|
||||||
func (c *BaseConnection) GetGenericError(err error) error {
|
func (c *BaseConnection) GetGenericError(err error) error {
|
||||||
switch c.protocol {
|
switch c.protocol {
|
||||||
case ProtocolSFTP:
|
case ProtocolSFTP:
|
||||||
if err == vfs.ErrStorageSizeUnavailable {
|
if errors.Is(err, vfs.ErrStorageSizeUnavailable) || errors.Is(err, ErrOpUnsupported) || errors.Is(err, sftp.ErrSSHFxOpUnsupported) {
|
||||||
return fmt.Errorf("%w: %v", sftp.ErrSSHFxOpUnsupported, err.Error())
|
return fmt.Errorf("%w: %w", sftp.ErrSSHFxOpUnsupported, err)
|
||||||
}
|
}
|
||||||
if err == ErrShuttingDown {
|
if isSFTPGoError(err) {
|
||||||
return fmt.Errorf("%w: %v", sftp.ErrSSHFxFailure, err.Error())
|
return fmt.Errorf("%w: %w", sftp.ErrSSHFxFailure, err)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if e, ok := err.(*os.PathError); ok {
|
var pathError *fs.PathError
|
||||||
c.Log(logger.LevelError, "generic path error: %+v", e)
|
if errors.As(err, &pathError) {
|
||||||
return fmt.Errorf("%w: %v %v", sftp.ErrSSHFxFailure, e.Op, e.Err.Error())
|
c.Log(logger.LevelError, "generic path error: %+v", pathError)
|
||||||
|
return fmt.Errorf("%w: %v %v", sftp.ErrSSHFxFailure, pathError.Op, pathError.Err.Error())
|
||||||
}
|
}
|
||||||
c.Log(logger.LevelError, "generic error: %+v", err)
|
c.Log(logger.LevelError, "generic error: %+v", err)
|
||||||
return fmt.Errorf("%w: %v", sftp.ErrSSHFxFailure, ErrGenericFailure.Error())
|
|
||||||
}
|
}
|
||||||
return sftp.ErrSSHFxFailure
|
return sftp.ErrSSHFxFailure
|
||||||
default:
|
default:
|
||||||
if err == ErrPermissionDenied || err == ErrNotExist || err == ErrOpUnsupported ||
|
if isSFTPGoError(err) {
|
||||||
err == ErrQuotaExceeded || err == ErrReadQuotaExceeded || err == vfs.ErrStorageSizeUnavailable ||
|
|
||||||
err == ErrShuttingDown {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.Log(logger.LevelError, "generic error: %+v", err)
|
c.Log(logger.LevelError, "generic error: %+v", err)
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
"github.com/sftpgo/sdk"
|
"github.com/sftpgo/sdk"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/kms"
|
"github.com/drakkan/sftpgo/v2/internal/kms"
|
||||||
|
@ -647,7 +648,7 @@ func TestFsFileCopier(t *testing.T) {
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterListDirs(t *testing.T) {
|
func TestFilePatterns(t *testing.T) {
|
||||||
filters := dataprovider.UserFilters{
|
filters := dataprovider.UserFilters{
|
||||||
BaseUserFilters: sdk.BaseUserFilters{
|
BaseUserFilters: sdk.BaseUserFilters{
|
||||||
FilePatterns: []sdk.PatternsFilter{
|
FilePatterns: []sdk.PatternsFilter{
|
||||||
|
@ -661,6 +662,16 @@ func TestFilterListDirs(t *testing.T) {
|
||||||
DenyPolicy: sdk.DenyPolicyHide,
|
DenyPolicy: sdk.DenyPolicyHide,
|
||||||
AllowedPatterns: []string{"*.jpg"},
|
AllowedPatterns: []string{"*.jpg"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Path: "/dir3",
|
||||||
|
DenyPolicy: sdk.DenyPolicyDefault,
|
||||||
|
DeniedPatterns: []string{"*.jpg"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/dir4",
|
||||||
|
DenyPolicy: sdk.DenyPolicyHide,
|
||||||
|
DeniedPatterns: []string{"*"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -693,13 +704,90 @@ func TestFilterListDirs(t *testing.T) {
|
||||||
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
}
|
}
|
||||||
|
// dirContents are modified in place, we need to redefine them each time
|
||||||
filtered := user.FilterListDir(dirContents, "/dir1")
|
filtered := user.FilterListDir(dirContents, "/dir1")
|
||||||
assert.Len(t, filtered, 5)
|
assert.Len(t, filtered, 5)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir1/vdir1")
|
||||||
|
assert.Len(t, filtered, 2)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir2/vdir2")
|
||||||
|
require.Len(t, filtered, 1)
|
||||||
|
assert.Equal(t, "file1.jpg", filtered[0].Name())
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir2/vdir2/sub")
|
||||||
|
require.Len(t, filtered, 1)
|
||||||
|
assert.Equal(t, "file1.jpg", filtered[0].Name())
|
||||||
|
|
||||||
|
res, _ := user.IsFileAllowed("/dir1/vdir1/file.txt")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir1/vdir1/sub/file.txt")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir1/vdir1/file.jpg")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir1/vdir1/sub/file.jpg")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/file.jpg")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/dir1/file.jpg")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/dir1/sub/file.jpg")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir4/file.jpg")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir4/dir1/sub/file.jpg")
|
||||||
|
assert.False(t, res)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir4")
|
||||||
|
require.Len(t, filtered, 0)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir4/vdir2/sub")
|
||||||
|
require.Len(t, filtered, 0)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
|
||||||
filtered = user.FilterListDir(dirContents, "/dir2")
|
filtered = user.FilterListDir(dirContents, "/dir2")
|
||||||
assert.Len(t, filtered, 2)
|
assert.Len(t, filtered, 2)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir4")
|
||||||
|
assert.Len(t, filtered, 0)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir4/sub")
|
||||||
|
assert.Len(t, filtered, 0)
|
||||||
|
|
||||||
dirContents = []os.FileInfo{
|
dirContents = []os.FileInfo{
|
||||||
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
|
vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
|
||||||
|
@ -708,11 +796,6 @@ func TestFilterListDirs(t *testing.T) {
|
||||||
filtered = user.FilterListDir(dirContents, "/dir1")
|
filtered = user.FilterListDir(dirContents, "/dir1")
|
||||||
assert.Len(t, filtered, 5)
|
assert.Len(t, filtered, 5)
|
||||||
|
|
||||||
dirContents = []os.FileInfo{
|
|
||||||
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
|
||||||
vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered = user.FilterListDir(dirContents, "/dir2")
|
filtered = user.FilterListDir(dirContents, "/dir2")
|
||||||
if assert.Len(t, filtered, 1) {
|
if assert.Len(t, filtered, 1) {
|
||||||
assert.True(t, filtered[0].IsDir())
|
assert.True(t, filtered[0].IsDir())
|
||||||
|
@ -799,7 +882,155 @@ func TestFilterListDirs(t *testing.T) {
|
||||||
dirContents = append(dirContents, vfs.NewFileInfo("file.jpg", false, 123, time.Now(), false))
|
dirContents = append(dirContents, vfs.NewFileInfo("file.jpg", false, 123, time.Now(), false))
|
||||||
|
|
||||||
filtered = user.FilterListDir(dirContents, "/dir3")
|
filtered = user.FilterListDir(dirContents, "/dir3")
|
||||||
if assert.Len(t, filtered, 1) {
|
require.Len(t, filtered, 1)
|
||||||
assert.Equal(t, "ic35", filtered[0].Name())
|
assert.Equal(t, "ic35", filtered[0].Name())
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
|
||||||
}
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir3/ic36")
|
||||||
|
require.Len(t, filtered, 0)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir3/ic35")
|
||||||
|
require.Len(t, filtered, 3)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub")
|
||||||
|
require.Len(t, filtered, 3)
|
||||||
|
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/file.txt")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35a")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, policy := user.IsFileAllowed("/dir3/ic35a/file")
|
||||||
|
assert.False(t, res)
|
||||||
|
assert.Equal(t, sdk.DenyPolicyHide, policy)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub/file.txt")
|
||||||
|
assert.True(t, res)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub")
|
||||||
|
require.Len(t, filtered, 3)
|
||||||
|
|
||||||
|
user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
|
||||||
|
Path: "/dir3/ic35/sub1",
|
||||||
|
AllowedPatterns: []string{"*.jpg"},
|
||||||
|
DenyPolicy: sdk.DenyPolicyDefault,
|
||||||
|
})
|
||||||
|
user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
|
||||||
|
Path: "/dir3/ic35/sub2",
|
||||||
|
DeniedPatterns: []string{"*.jpg"},
|
||||||
|
DenyPolicy: sdk.DenyPolicyHide,
|
||||||
|
})
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub1")
|
||||||
|
require.Len(t, filtered, 3)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub2")
|
||||||
|
require.Len(t, filtered, 2)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub2/sub1")
|
||||||
|
require.Len(t, filtered, 2)
|
||||||
|
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub/dir/file.txt")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub/dir/file.jpg")
|
||||||
|
assert.True(t, res)
|
||||||
|
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/file.jpg")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/file.txt")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/sub/file.jpg")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/sub2/file.txt")
|
||||||
|
assert.False(t, res)
|
||||||
|
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.jpg")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.txt")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub/file.jpg")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub1/file.txt")
|
||||||
|
assert.True(t, res)
|
||||||
|
|
||||||
|
user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
|
||||||
|
Path: "/dir3/ic35",
|
||||||
|
DeniedPatterns: []string{"*.txt"},
|
||||||
|
DenyPolicy: sdk.DenyPolicyHide,
|
||||||
|
})
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/adir/sub/file.jpg")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/adir/file.txt")
|
||||||
|
assert.False(t, res)
|
||||||
|
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.jpg")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.txt")
|
||||||
|
assert.True(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub/file.jpg")
|
||||||
|
assert.False(t, res)
|
||||||
|
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub1/file.txt")
|
||||||
|
assert.True(t, res)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir3/ic35")
|
||||||
|
require.Len(t, filtered, 1)
|
||||||
|
|
||||||
|
dirContents = []os.FileInfo{
|
||||||
|
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
|
||||||
|
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
|
||||||
|
}
|
||||||
|
filtered = user.FilterListDir(dirContents, "/dir3/ic35/abc")
|
||||||
|
require.Len(t, filtered, 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1302,7 +1302,8 @@ func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *stri
|
||||||
} else {
|
} else {
|
||||||
h.Set("Content-Disposition",
|
h.Set("Content-Disposition",
|
||||||
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
|
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
|
||||||
multipartQuoteEscaper.Replace(part.Name), multipartQuoteEscaper.Replace(path.Base(part.Filepath))))
|
multipartQuoteEscaper.Replace(part.Name),
|
||||||
|
multipartQuoteEscaper.Replace((path.Base(replaceWithReplacer(part.Filepath, replacer))))))
|
||||||
contentType := mime.TypeByExtension(path.Ext(part.Filepath))
|
contentType := mime.TypeByExtension(path.Ext(part.Filepath))
|
||||||
if contentType == "" {
|
if contentType == "" {
|
||||||
contentType = "application/octet-stream"
|
contentType = "application/octet-stream"
|
||||||
|
@ -1525,7 +1526,6 @@ func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
|
||||||
user.Filters.DisableFsChecks = false
|
user.Filters.DisableFsChecks = false
|
||||||
user.Filters.FilePatterns = nil
|
user.Filters.FilePatterns = nil
|
||||||
user.Filters.BandwidthLimits = nil
|
user.Filters.BandwidthLimits = nil
|
||||||
user.Filters.DataTransferLimits = nil
|
|
||||||
for k := range user.Permissions {
|
for k := range user.Permissions {
|
||||||
user.Permissions[k] = []string{dataprovider.PermAny}
|
user.Permissions[k] = []string{dataprovider.PermAny}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1548,12 +1548,6 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {
|
||||||
func TestQuotaRenameOverwrite(t *testing.T) {
|
func TestQuotaRenameOverwrite(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.QuotaFiles = 100
|
u.QuotaFiles = 100
|
||||||
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
|
|
||||||
{
|
|
||||||
Sources: []string{"10.8.0.0/8"},
|
|
||||||
TotalDataTransfer: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
conn, client, err := getSftpClient(user)
|
conn, client, err := getSftpClient(user)
|
||||||
|
|
|
@ -299,6 +299,8 @@ func NewCertManager(keyPairs []TLSKeyPair, configDir, logSender string) (*CertMa
|
||||||
}
|
}
|
||||||
randSecs := rand.Intn(59)
|
randSecs := rand.Intn(59)
|
||||||
manager.monitor()
|
manager.monitor()
|
||||||
_, err = eventScheduler.AddFunc(fmt.Sprintf("@every 8h0m%ds", randSecs), manager.monitor)
|
if eventScheduler != nil {
|
||||||
|
_, err = eventScheduler.AddFunc(fmt.Sprintf("@every 8h0m%ds", randSecs), manager.monitor)
|
||||||
|
}
|
||||||
return manager, err
|
return manager, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
@ -215,12 +217,11 @@ func (t *BaseTransfer) SetCancelFn(cancelFn func()) {
|
||||||
// converts it into a more understandable form for the client if it is a
|
// converts it into a more understandable form for the client if it is a
|
||||||
// well-known type of error
|
// well-known type of error
|
||||||
func (t *BaseTransfer) ConvertError(err error) error {
|
func (t *BaseTransfer) ConvertError(err error) error {
|
||||||
if t.Fs.IsNotExist(err) {
|
var pathError *fs.PathError
|
||||||
return t.Connection.GetNotExistError()
|
if errors.As(err, &pathError) {
|
||||||
} else if t.Fs.IsPermission(err) {
|
return fmt.Errorf("%s %s: %s", pathError.Op, t.GetVirtualPath(), pathError.Err.Error())
|
||||||
return t.Connection.GetPermissionDeniedError()
|
|
||||||
}
|
}
|
||||||
return err
|
return t.Connection.GetFsError(t.Fs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckRead returns an error if read if not allowed
|
// CheckRead returns an error if read if not allowed
|
||||||
|
|
|
@ -16,6 +16,7 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -226,6 +227,13 @@ func TestTransferErrors(t *testing.T) {
|
||||||
conn := NewBaseConnection("id", ProtocolSFTP, "", "", u)
|
conn := NewBaseConnection("id", ProtocolSFTP, "", "", u)
|
||||||
transfer := NewBaseTransfer(file, conn, nil, testFile, testFile, "/transfer_test_file", TransferUpload,
|
transfer := NewBaseTransfer(file, conn, nil, testFile, testFile, "/transfer_test_file", TransferUpload,
|
||||||
0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})
|
0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})
|
||||||
|
pathError := &os.PathError{
|
||||||
|
Op: "test",
|
||||||
|
Path: testFile,
|
||||||
|
Err: os.ErrInvalid,
|
||||||
|
}
|
||||||
|
err = transfer.ConvertError(pathError)
|
||||||
|
assert.EqualError(t, err, fmt.Sprintf("%s %s: %s", pathError.Op, "/transfer_test_file", pathError.Err.Error()))
|
||||||
err = transfer.ConvertError(os.ErrNotExist)
|
err = transfer.ConvertError(os.ErrNotExist)
|
||||||
assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
|
assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
|
||||||
err = transfer.ConvertError(os.ErrPermission)
|
err = transfer.ConvertError(os.ErrPermission)
|
||||||
|
@ -332,41 +340,22 @@ func TestFTPMode(t *testing.T) {
|
||||||
func TestTransferQuota(t *testing.T) {
|
func TestTransferQuota(t *testing.T) {
|
||||||
user := dataprovider.User{
|
user := dataprovider.User{
|
||||||
BaseUser: sdk.BaseUser{
|
BaseUser: sdk.BaseUser{
|
||||||
TotalDataTransfer: -1,
|
TotalDataTransfer: 3,
|
||||||
UploadDataTransfer: -1,
|
UploadDataTransfer: 2,
|
||||||
DownloadDataTransfer: -1,
|
DownloadDataTransfer: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
user.Filters.DataTransferLimits = []sdk.DataTransferLimit{
|
ul, dl, total := user.GetDataTransferLimits()
|
||||||
{
|
assert.Equal(t, int64(2*1048576), ul)
|
||||||
Sources: []string{"127.0.0.1/32", "192.168.1.0/24"},
|
assert.Equal(t, int64(1*1048576), dl)
|
||||||
TotalDataTransfer: 100,
|
assert.Equal(t, int64(3*1048576), total)
|
||||||
UploadDataTransfer: 0,
|
user.TotalDataTransfer = -1
|
||||||
DownloadDataTransfer: 0,
|
user.UploadDataTransfer = -1
|
||||||
},
|
user.DownloadDataTransfer = -1
|
||||||
{
|
ul, dl, total = user.GetDataTransferLimits()
|
||||||
Sources: []string{"172.16.0.0/24"},
|
|
||||||
TotalDataTransfer: 0,
|
|
||||||
UploadDataTransfer: 120,
|
|
||||||
DownloadDataTransfer: 150,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ul, dl, total := user.GetDataTransferLimits("127.0.1.1")
|
|
||||||
assert.Equal(t, int64(0), ul)
|
assert.Equal(t, int64(0), ul)
|
||||||
assert.Equal(t, int64(0), dl)
|
assert.Equal(t, int64(0), dl)
|
||||||
assert.Equal(t, int64(0), total)
|
assert.Equal(t, int64(0), total)
|
||||||
ul, dl, total = user.GetDataTransferLimits("127.0.0.1")
|
|
||||||
assert.Equal(t, int64(0), ul)
|
|
||||||
assert.Equal(t, int64(0), dl)
|
|
||||||
assert.Equal(t, int64(100*1048576), total)
|
|
||||||
ul, dl, total = user.GetDataTransferLimits("192.168.1.4")
|
|
||||||
assert.Equal(t, int64(0), ul)
|
|
||||||
assert.Equal(t, int64(0), dl)
|
|
||||||
assert.Equal(t, int64(100*1048576), total)
|
|
||||||
ul, dl, total = user.GetDataTransferLimits("172.16.0.2")
|
|
||||||
assert.Equal(t, int64(120*1048576), ul)
|
|
||||||
assert.Equal(t, int64(150*1048576), dl)
|
|
||||||
assert.Equal(t, int64(0), total)
|
|
||||||
transferQuota := dataprovider.TransferQuota{}
|
transferQuota := dataprovider.TransferQuota{}
|
||||||
assert.True(t, transferQuota.HasDownloadSpace())
|
assert.True(t, transferQuota.HasDownloadSpace())
|
||||||
assert.True(t, transferQuota.HasUploadSpace())
|
assert.True(t, transferQuota.HasUploadSpace())
|
||||||
|
|
|
@ -61,7 +61,7 @@ type baseTransferChecker struct {
|
||||||
func (t *baseTransferChecker) isDataTransferExceeded(user dataprovider.User, transfer dataprovider.ActiveTransfer, ulSize,
|
func (t *baseTransferChecker) isDataTransferExceeded(user dataprovider.User, transfer dataprovider.ActiveTransfer, ulSize,
|
||||||
dlSize int64,
|
dlSize int64,
|
||||||
) bool {
|
) bool {
|
||||||
ulQuota, dlQuota, totalQuota := user.GetDataTransferLimits(transfer.IP)
|
ulQuota, dlQuota, totalQuota := user.GetDataTransferLimits()
|
||||||
if totalQuota > 0 {
|
if totalQuota > 0 {
|
||||||
allowedSize := totalQuota - (user.UsedUploadDataTransfer + user.UsedDownloadDataTransfer)
|
allowedSize := totalQuota - (user.UsedUploadDataTransfer + user.UsedDownloadDataTransfer)
|
||||||
if ulSize+dlSize > allowedSize {
|
if ulSize+dlSize > allowedSize {
|
||||||
|
|
|
@ -592,7 +592,7 @@ func TestDataTransferExceeded(t *testing.T) {
|
||||||
|
|
||||||
func TestGetUsersForQuotaCheck(t *testing.T) {
|
func TestGetUsersForQuotaCheck(t *testing.T) {
|
||||||
usersToFetch := make(map[string]bool)
|
usersToFetch := make(map[string]bool)
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 70; i++ {
|
||||||
usersToFetch[fmt.Sprintf("user%v", i)] = i%2 == 0
|
usersToFetch[fmt.Sprintf("user%v", i)] = i%2 == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -600,7 +600,7 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, users, 0)
|
assert.Len(t, users, 0)
|
||||||
|
|
||||||
for i := 0; i < 40; i++ {
|
for i := 0; i < 60; i++ {
|
||||||
user := dataprovider.User{
|
user := dataprovider.User{
|
||||||
BaseUser: sdk.BaseUser{
|
BaseUser: sdk.BaseUser{
|
||||||
Username: fmt.Sprintf("user%v", i),
|
Username: fmt.Sprintf("user%v", i),
|
||||||
|
@ -622,17 +622,6 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
|
||||||
QuotaSize: 100,
|
QuotaSize: 100,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Filters: dataprovider.UserFilters{
|
|
||||||
BaseUserFilters: sdk.BaseUserFilters{
|
|
||||||
DataTransferLimits: []sdk.DataTransferLimit{
|
|
||||||
{
|
|
||||||
Sources: []string{"172.16.0.0/16"},
|
|
||||||
UploadDataTransfer: 50,
|
|
||||||
DownloadDataTransfer: 80,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
err = dataprovider.AddUser(&user, "", "", "")
|
err = dataprovider.AddUser(&user, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -642,7 +631,7 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
|
||||||
|
|
||||||
users, err = dataprovider.GetUsersForQuotaCheck(usersToFetch)
|
users, err = dataprovider.GetUsersForQuotaCheck(usersToFetch)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, users, 40)
|
assert.Len(t, users, 60)
|
||||||
|
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
userIdxStr := strings.Replace(user.Username, "user", "", 1)
|
userIdxStr := strings.Replace(user.Username, "user", "", 1)
|
||||||
|
@ -660,17 +649,13 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
|
||||||
assert.Len(t, user.VirtualFolders, 0, user.Username)
|
assert.Len(t, user.VirtualFolders, 0, user.Username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ul, dl, total := user.GetDataTransferLimits("127.1.1.1")
|
ul, dl, total := user.GetDataTransferLimits()
|
||||||
assert.Equal(t, int64(0), ul)
|
assert.Equal(t, int64(0), ul)
|
||||||
assert.Equal(t, int64(0), dl)
|
assert.Equal(t, int64(0), dl)
|
||||||
assert.Equal(t, int64(0), total)
|
assert.Equal(t, int64(0), total)
|
||||||
ul, dl, total = user.GetDataTransferLimits("172.16.2.3")
|
|
||||||
assert.Equal(t, int64(50*1024*1024), ul)
|
|
||||||
assert.Equal(t, int64(80*1024*1024), dl)
|
|
||||||
assert.Equal(t, int64(0), total)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 40; i++ {
|
for i := 0; i < 60; i++ {
|
||||||
err = dataprovider.DeleteUser(fmt.Sprintf("user%v", i), "", "", "")
|
err = dataprovider.DeleteUser(fmt.Sprintf("user%v", i), "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteFolder(fmt.Sprintf("f%v", i), "", "", "")
|
err = dataprovider.DeleteFolder(fmt.Sprintf("f%v", i), "", "", "")
|
||||||
|
|
|
@ -1961,6 +1961,11 @@ func getCommandConfigsFromEnv(idx int) {
|
||||||
cfg.Env = env
|
cfg.Env = env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__ARGS", idx))
|
||||||
|
if ok {
|
||||||
|
cfg.Args = args
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Path != "" {
|
if cfg.Path != "" {
|
||||||
if len(globalConf.CommandConfig.Commands) > idx {
|
if len(globalConf.CommandConfig.Commands) > idx {
|
||||||
globalConf.CommandConfig.Commands[idx] = cfg
|
globalConf.CommandConfig.Commands[idx] = cfg
|
||||||
|
|
|
@ -924,13 +924,17 @@ func TestCommandsFromEnv(t *testing.T) {
|
||||||
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__PATH", "cmd2")
|
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__PATH", "cmd2")
|
||||||
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__TIMEOUT", "20")
|
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__TIMEOUT", "20")
|
||||||
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ENV", "e=f")
|
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ENV", "e=f")
|
||||||
|
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ARGS", "arg1, arg2")
|
||||||
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
os.Unsetenv("SFTPGO_COMMAND__TIMEOUT")
|
os.Unsetenv("SFTPGO_COMMAND__TIMEOUT")
|
||||||
os.Unsetenv("SFTPGO_COMMAND__ENV")
|
os.Unsetenv("SFTPGO_COMMAND__ENV")
|
||||||
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__PATH")
|
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__PATH")
|
||||||
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__TIMEOUT")
|
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__TIMEOUT")
|
||||||
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__ENV")
|
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__PATH")
|
||||||
|
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__TIMEOUT")
|
||||||
|
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__ENV")
|
||||||
|
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__ARGS")
|
||||||
})
|
})
|
||||||
|
|
||||||
err = config.LoadConfig(configDir, confName)
|
err = config.LoadConfig(configDir, confName)
|
||||||
|
@ -945,6 +949,7 @@ func TestCommandsFromEnv(t *testing.T) {
|
||||||
require.Equal(t, "cmd2", commandConfig.Commands[1].Path)
|
require.Equal(t, "cmd2", commandConfig.Commands[1].Path)
|
||||||
require.Equal(t, 20, commandConfig.Commands[1].Timeout)
|
require.Equal(t, 20, commandConfig.Commands[1].Timeout)
|
||||||
require.Equal(t, []string{"e=f"}, commandConfig.Commands[1].Env)
|
require.Equal(t, []string{"e=f"}, commandConfig.Commands[1].Env)
|
||||||
|
require.Equal(t, []string{"arg1", "arg2"}, commandConfig.Commands[1].Args)
|
||||||
|
|
||||||
err = os.Remove(configFilePath)
|
err = os.Remove(configFilePath)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -797,6 +797,7 @@ func (p *BoltProvider) updateUserPassword(username, password string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
user.Password = password
|
user.Password = password
|
||||||
|
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
buf, err := json.Marshal(user)
|
buf, err := json.Marshal(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -28,10 +28,9 @@ import (
|
||||||
|
|
||||||
// Supported values for host keys, KEXs, ciphers, MACs
|
// Supported values for host keys, KEXs, ciphers, MACs
|
||||||
var (
|
var (
|
||||||
supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01}
|
supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA}
|
||||||
supportedKexAlgos = []string{
|
supportedKexAlgos = []string{
|
||||||
"diffie-hellman-group16-sha512", "diffie-hellman-group18-sha512",
|
"diffie-hellman-group16-sha512", "diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
|
||||||
"diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
|
|
||||||
"diffie-hellman-group-exchange-sha256", "diffie-hellman-group-exchange-sha1",
|
"diffie-hellman-group-exchange-sha256", "diffie-hellman-group-exchange-sha1",
|
||||||
}
|
}
|
||||||
supportedCiphers = []string{
|
supportedCiphers = []string{
|
||||||
|
@ -98,16 +97,28 @@ func (c *SFTPDConfigs) GetModuliAsString() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SFTPDConfigs) validate() error {
|
func (c *SFTPDConfigs) validate() error {
|
||||||
|
var hostKeyAlgos []string
|
||||||
for _, algo := range c.HostKeyAlgos {
|
for _, algo := range c.HostKeyAlgos {
|
||||||
|
if algo == ssh.CertAlgoRSAv01 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !util.Contains(supportedHostKeyAlgos, algo) {
|
if !util.Contains(supportedHostKeyAlgos, algo) {
|
||||||
return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
|
return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
|
||||||
}
|
}
|
||||||
|
hostKeyAlgos = append(hostKeyAlgos, algo)
|
||||||
}
|
}
|
||||||
|
c.HostKeyAlgos = hostKeyAlgos
|
||||||
|
var kexAlgos []string
|
||||||
for _, algo := range c.KexAlgorithms {
|
for _, algo := range c.KexAlgorithms {
|
||||||
|
if algo == "diffie-hellman-group18-sha512" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !util.Contains(supportedKexAlgos, algo) {
|
if !util.Contains(supportedKexAlgos, algo) {
|
||||||
return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
|
return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
|
||||||
}
|
}
|
||||||
|
kexAlgos = append(kexAlgos, algo)
|
||||||
}
|
}
|
||||||
|
c.KexAlgorithms = kexAlgos
|
||||||
for _, cipher := range c.Ciphers {
|
for _, cipher := range c.Ciphers {
|
||||||
if !util.Contains(supportedCiphers, cipher) {
|
if !util.Contains(supportedCiphers, cipher) {
|
||||||
return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
|
return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
|
||||||
|
|
|
@ -2585,18 +2585,6 @@ func copyBaseUserFilters(in sdk.BaseUserFilters) sdk.BaseUserFilters {
|
||||||
copy(bwLimit.Sources, limit.Sources)
|
copy(bwLimit.Sources, limit.Sources)
|
||||||
filters.BandwidthLimits = append(filters.BandwidthLimits, bwLimit)
|
filters.BandwidthLimits = append(filters.BandwidthLimits, bwLimit)
|
||||||
}
|
}
|
||||||
filters.DataTransferLimits = make([]sdk.DataTransferLimit, 0, len(in.DataTransferLimits))
|
|
||||||
for _, limit := range in.DataTransferLimits {
|
|
||||||
dtLimit := sdk.DataTransferLimit{
|
|
||||||
UploadDataTransfer: limit.UploadDataTransfer,
|
|
||||||
DownloadDataTransfer: limit.DownloadDataTransfer,
|
|
||||||
TotalDataTransfer: limit.TotalDataTransfer,
|
|
||||||
Sources: make([]string, 0, len(limit.Sources)),
|
|
||||||
}
|
|
||||||
dtLimit.Sources = make([]string, len(limit.Sources))
|
|
||||||
copy(dtLimit.Sources, limit.Sources)
|
|
||||||
filters.DataTransferLimits = append(filters.DataTransferLimits, dtLimit)
|
|
||||||
}
|
|
||||||
return filters
|
return filters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2943,26 +2931,6 @@ func validateBandwidthLimitsFilter(filters *sdk.BaseUserFilters) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateTransferLimitsFilter(filters *sdk.BaseUserFilters) error {
|
|
||||||
for idx, limit := range filters.DataTransferLimits {
|
|
||||||
filters.DataTransferLimits[idx].Sources = util.RemoveDuplicates(limit.Sources, false)
|
|
||||||
if len(limit.Sources) == 0 {
|
|
||||||
return util.NewValidationError("no data transfer limit source specified")
|
|
||||||
}
|
|
||||||
for _, source := range limit.Sources {
|
|
||||||
_, _, err := net.ParseCIDR(source)
|
|
||||||
if err != nil {
|
|
||||||
return util.NewValidationError(fmt.Sprintf("could not parse data transfer limit source %q: %v", source, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if limit.TotalDataTransfer > 0 {
|
|
||||||
filters.DataTransferLimits[idx].UploadDataTransfer = 0
|
|
||||||
filters.DataTransferLimits[idx].DownloadDataTransfer = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateFiltersValues(filters *sdk.BaseUserFilters) {
|
func updateFiltersValues(filters *sdk.BaseUserFilters) {
|
||||||
if filters.StartDirectory != "" {
|
if filters.StartDirectory != "" {
|
||||||
filters.StartDirectory = util.CleanPath(filters.StartDirectory)
|
filters.StartDirectory = util.CleanPath(filters.StartDirectory)
|
||||||
|
@ -2998,9 +2966,6 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
|
||||||
if err := validateBandwidthLimitsFilter(filters); err != nil {
|
if err := validateBandwidthLimitsFilter(filters); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := validateTransferLimitsFilter(filters); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(filters.DeniedLoginMethods) >= len(ValidLoginMethods) {
|
if len(filters.DeniedLoginMethods) >= len(ValidLoginMethods) {
|
||||||
return util.NewValidationError("invalid denied_login_methods")
|
return util.NewValidationError("invalid denied_login_methods")
|
||||||
}
|
}
|
||||||
|
@ -3525,7 +3490,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if len(answers) != 1 {
|
if len(answers) != 1 {
|
||||||
return 0, fmt.Errorf("unexpected number of answers: %v", len(answers))
|
return 0, fmt.Errorf("unexpected number of answers: %d", len(answers))
|
||||||
}
|
}
|
||||||
err = user.LoadAndApplyGroupSettings()
|
err = user.LoadAndApplyGroupSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3535,16 +3500,20 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
return checkKeyboardInteractiveSecondFactor(user, client, protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkKeyboardInteractiveSecondFactor(user *User, client ssh.KeyboardInteractiveChallenge, protocol string) (int, error) {
|
||||||
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
|
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
err = user.Filters.TOTPConfig.Secret.TryDecrypt()
|
err := user.Filters.TOTPConfig.Secret.TryDecrypt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
|
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
|
||||||
user.Username, protocol, err)
|
user.Username, protocol, err)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
answers, err = client("", "", []string{"Authentication code: "}, []bool{false})
|
answers, err := client("", "", []string{"Authentication code: "}, []bool{false})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
@ -3773,15 +3742,25 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
|
||||||
}
|
}
|
||||||
|
|
||||||
func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) {
|
func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) {
|
||||||
|
if err := user.LoadAndApplyGroupSettings(); err != nil {
|
||||||
|
return *user, err
|
||||||
|
}
|
||||||
var authResult int
|
var authResult int
|
||||||
var err error
|
var err error
|
||||||
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
|
if !user.Filters.Hooks.ExternalAuthDisabled {
|
||||||
authResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol)
|
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
|
||||||
} else if authHook != "" {
|
authResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol)
|
||||||
if strings.HasPrefix(authHook, "http") {
|
if authResult == 1 && err == nil {
|
||||||
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol)
|
authResult, err = checkKeyboardInteractiveSecondFactor(user, client, protocol)
|
||||||
|
}
|
||||||
|
} else if authHook != "" {
|
||||||
|
if strings.HasPrefix(authHook, "http") {
|
||||||
|
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol)
|
||||||
|
} else {
|
||||||
|
authResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip, protocol)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
authResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip, protocol)
|
authResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
authResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol)
|
authResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol)
|
||||||
|
@ -3792,10 +3771,6 @@ func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardI
|
||||||
if authResult != 1 {
|
if authResult != 1 {
|
||||||
return *user, fmt.Errorf("keyboard interactive auth failed, result: %v", authResult)
|
return *user, fmt.Errorf("keyboard interactive auth failed, result: %v", authResult)
|
||||||
}
|
}
|
||||||
err = user.LoadAndApplyGroupSettings()
|
|
||||||
if err != nil {
|
|
||||||
return *user, err
|
|
||||||
}
|
|
||||||
err = user.CheckLoginConditions()
|
err = user.CheckLoginConditions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return *user, err
|
return *user, err
|
||||||
|
@ -4145,6 +4120,35 @@ func updateUserFromExtAuthResponse(user *User, password, pkey string) {
|
||||||
user.LastPasswordChange = 0
|
user.LastPasswordChange = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkPasswordAfterEmptyExtAuthResponse(user *User, plainPwd, protocol string) error {
|
||||||
|
if plainPwd == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
match, err := isPasswordOK(user, plainPwd)
|
||||||
|
if match && err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPwd, err := hashPlainPassword(plainPwd)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelError, "unable to hash password for user %q after empty external response: %v",
|
||||||
|
user.Username, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = provider.updateUserPassword(user.Username, hashedPwd)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelError, "unable to update password for user %q after empty external response: %v",
|
||||||
|
user.Username, err)
|
||||||
|
}
|
||||||
|
user.Password = hashedPwd
|
||||||
|
cachedUserPasswords.Add(user.Username, plainPwd, user.Password)
|
||||||
|
if protocol != protocolWebDAV {
|
||||||
|
webDAVUsersCache.swap(user, plainPwd)
|
||||||
|
}
|
||||||
|
providerLog(logger.LevelDebug, "updated password for user %q after empty external auth response", user.Username)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string,
|
func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string,
|
||||||
tlsCert *x509.Certificate,
|
tlsCert *x509.Certificate,
|
||||||
) (User, error) {
|
) (User, error) {
|
||||||
|
@ -4176,7 +4180,8 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
|
||||||
if u.ID == 0 {
|
if u.ID == 0 {
|
||||||
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
|
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
|
||||||
}
|
}
|
||||||
return u, nil
|
err = checkPasswordAfterEmptyExtAuthResponse(&u, password, protocol)
|
||||||
|
return u, err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(out, &user)
|
err = json.Unmarshal(out, &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -4249,18 +4254,19 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
||||||
|
|
||||||
out, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON)
|
out, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, fmt.Errorf("plugin auth error for user %q: %v, elapsed: %v, auth scope: %v",
|
return user, fmt.Errorf("plugin auth error for user %q: %v, elapsed: %v, auth scope: %d",
|
||||||
username, err, time.Since(startTime), authScope)
|
username, err, time.Since(startTime), authScope)
|
||||||
}
|
}
|
||||||
providerLog(logger.LevelDebug, "plugin auth completed for user %q, elapsed: %v,auth scope: %v",
|
providerLog(logger.LevelDebug, "plugin auth completed for user %q, elapsed: %v, auth scope: %d",
|
||||||
username, time.Since(startTime), authScope)
|
username, time.Since(startTime), authScope)
|
||||||
if util.IsByteArrayEmpty(out) {
|
if util.IsByteArrayEmpty(out) {
|
||||||
providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %q id: %v",
|
providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %q id: %d, auth scope: %d",
|
||||||
username, u.ID)
|
username, u.ID, authScope)
|
||||||
if u.ID == 0 {
|
if u.ID == 0 {
|
||||||
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
|
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
|
||||||
}
|
}
|
||||||
return u, nil
|
err = checkPasswordAfterEmptyExtAuthResponse(&u, password, protocol)
|
||||||
|
return u, err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(out, &user)
|
err = json.Unmarshal(out, &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -472,6 +472,7 @@ func (p *MemoryProvider) updateUserPassword(username, password string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
user.Password = password
|
user.Password = password
|
||||||
|
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
p.dbHandle.users[username] = user
|
p.dbHandle.users[username] = user
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ const (
|
||||||
SessionTypeOIDCToken
|
SessionTypeOIDCToken
|
||||||
SessionTypeResetCode
|
SessionTypeResetCode
|
||||||
SessionTypeOAuth2Auth
|
SessionTypeOAuth2Auth
|
||||||
|
SessionTypeInvalidToken
|
||||||
)
|
)
|
||||||
|
|
||||||
// Session defines a shared session persisted in the data provider
|
// Session defines a shared session persisted in the data provider
|
||||||
|
@ -42,7 +43,7 @@ func (s *Session) validate() error {
|
||||||
if s.Key == "" {
|
if s.Key == "" {
|
||||||
return errors.New("unable to save a session with an empty key")
|
return errors.New("unable to save a session with an empty key")
|
||||||
}
|
}
|
||||||
if s.Type < SessionTypeOIDCAuth || s.Type > SessionTypeOAuth2Auth {
|
if s.Type < SessionTypeOIDCAuth || s.Type > SessionTypeInvalidToken {
|
||||||
return fmt.Errorf("invalid session type: %v", s.Type)
|
return fmt.Errorf("invalid session type: %v", s.Type)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alexedwards/argon2id"
|
"github.com/alexedwards/argon2id"
|
||||||
|
passwordvalidator "github.com/wagslane/go-password-validator"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||||
|
@ -178,6 +179,15 @@ func (s *Share) HasRedactedPassword() bool {
|
||||||
|
|
||||||
func (s *Share) hashPassword() error {
|
func (s *Share) hashPassword() error {
|
||||||
if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) {
|
if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) {
|
||||||
|
user, err := UserExists(s.Username, "")
|
||||||
|
if err != nil {
|
||||||
|
return util.NewGenericError(fmt.Sprintf("unable to validate user: %v", err))
|
||||||
|
}
|
||||||
|
if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {
|
||||||
|
if err := passwordvalidator.Validate(s.Password, minEntropy); err != nil {
|
||||||
|
return util.NewValidationError(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
||||||
hashed, err := bcrypt.GenerateFromPassword([]byte(s.Password), config.PasswordHashing.BcryptOptions.Cost)
|
hashed, err := bcrypt.GenerateFromPassword([]byte(s.Password), config.PasswordHashing.BcryptOptions.Cost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -198,8 +208,7 @@ func (s *Share) hashPassword() error {
|
||||||
func (s *Share) validatePaths() error {
|
func (s *Share) validatePaths() error {
|
||||||
var paths []string
|
var paths []string
|
||||||
for _, p := range s.Paths {
|
for _, p := range s.Paths {
|
||||||
p = strings.TrimSpace(p)
|
if strings.TrimSpace(p) != "" {
|
||||||
if p != "" {
|
|
||||||
paths = append(paths, p)
|
paths = append(paths, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -958,63 +958,86 @@ func sqlCommonGetUsersInGroups(names []string, dbHandle sqlQuerier) ([]string, e
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
maxNames := len(sqlPlaceholders)
|
||||||
|
usernames := make([]string, 0, len(names))
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
q := getUsersInGroupsQuery(len(names))
|
for len(names) > 0 {
|
||||||
args := make([]any, 0, len(names))
|
if maxNames > len(names) {
|
||||||
for _, name := range names {
|
maxNames = len(names)
|
||||||
args = append(args, name)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
usernames := make([]string, 0, len(names))
|
q := getUsersInGroupsQuery(maxNames)
|
||||||
rows, err := dbHandle.QueryContext(ctx, q, args...)
|
args := make([]any, 0, maxNames)
|
||||||
if err != nil {
|
for _, name := range names[:maxNames] {
|
||||||
return nil, err
|
args = append(args, name)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
rows, err := dbHandle.QueryContext(ctx, q, args...)
|
||||||
var username string
|
if err != nil {
|
||||||
err = rows.Scan(&username)
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var username string
|
||||||
|
err = rows.Scan(&username)
|
||||||
|
if err != nil {
|
||||||
|
return usernames, err
|
||||||
|
}
|
||||||
|
usernames = append(usernames, username)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return usernames, err
|
return usernames, err
|
||||||
}
|
}
|
||||||
usernames = append(usernames, username)
|
names = names[maxNames:]
|
||||||
}
|
}
|
||||||
return usernames, rows.Err()
|
return usernames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sqlCommonGetGroupsWithNames(names []string, dbHandle sqlQuerier) ([]Group, error) {
|
func sqlCommonGetGroupsWithNames(names []string, dbHandle sqlQuerier) ([]Group, error) {
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
maxNames := len(sqlPlaceholders)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
q := getGroupsWithNamesQuery(len(names))
|
|
||||||
args := make([]any, 0, len(names))
|
|
||||||
for _, name := range names {
|
|
||||||
args = append(args, name)
|
|
||||||
}
|
|
||||||
groups := make([]Group, 0, len(names))
|
groups := make([]Group, 0, len(names))
|
||||||
rows, err := dbHandle.QueryContext(ctx, q, args...)
|
for len(names) > 0 {
|
||||||
if err != nil {
|
if maxNames > len(names) {
|
||||||
return groups, err
|
maxNames = len(names)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
for rows.Next() {
|
q := getGroupsWithNamesQuery(maxNames)
|
||||||
group, err := getGroupFromDbRow(rows)
|
args := make([]any, 0, maxNames)
|
||||||
|
for _, name := range names[:maxNames] {
|
||||||
|
args = append(args, name)
|
||||||
|
}
|
||||||
|
rows, err := dbHandle.QueryContext(ctx, q, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return groups, err
|
return groups, err
|
||||||
}
|
}
|
||||||
groups = append(groups, group)
|
defer rows.Close()
|
||||||
}
|
|
||||||
err = rows.Err()
|
for rows.Next() {
|
||||||
if err != nil {
|
group, err := getGroupFromDbRow(rows)
|
||||||
return groups, err
|
if err != nil {
|
||||||
|
return groups, err
|
||||||
|
}
|
||||||
|
groups = append(groups, group)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return groups, err
|
||||||
|
}
|
||||||
|
names = names[maxNames:]
|
||||||
}
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
return getGroupsWithVirtualFolders(ctx, groups, dbHandle)
|
return getGroupsWithVirtualFolders(ctx, groups, dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1386,7 +1409,7 @@ func sqlCommonUpdateUserPassword(username, password string, dbHandle *sql.DB) er
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
q := getUpdateUserPasswordQuery()
|
q := getUpdateUserPasswordQuery()
|
||||||
res, err := dbHandle.ExecContext(ctx, q, password, username)
|
res, err := dbHandle.ExecContext(ctx, q, password, util.GetTimeAsMsSinceEpoch(time.Now()), username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1535,6 +1558,9 @@ func sqlCommonGetRecentlyUpdatedUsers(after int64, dbHandle sqlQuerier) ([]User,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
groupNames = util.RemoveDuplicates(groupNames, false)
|
groupNames = util.RemoveDuplicates(groupNames, false)
|
||||||
|
if len(groupNames) == 0 {
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
groups, err := sqlCommonGetGroupsWithNames(groupNames, dbHandle)
|
groups, err := sqlCommonGetGroupsWithNames(groupNames, dbHandle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return users, err
|
return users, err
|
||||||
|
@ -1553,15 +1579,23 @@ func sqlCommonGetRecentlyUpdatedUsers(after int64, dbHandle sqlQuerier) ([]User,
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sqlGetMaxUsersForQuotaCheckRange() int {
|
||||||
|
maxUsers := 50
|
||||||
|
if maxUsers > len(sqlPlaceholders) {
|
||||||
|
maxUsers = len(sqlPlaceholders)
|
||||||
|
}
|
||||||
|
return maxUsers
|
||||||
|
}
|
||||||
|
|
||||||
func sqlCommonGetUsersForQuotaCheck(toFetch map[string]bool, dbHandle sqlQuerier) ([]User, error) {
|
func sqlCommonGetUsersForQuotaCheck(toFetch map[string]bool, dbHandle sqlQuerier) ([]User, error) {
|
||||||
users := make([]User, 0, 30)
|
maxUsers := sqlGetMaxUsersForQuotaCheckRange()
|
||||||
|
users := make([]User, 0, maxUsers)
|
||||||
|
|
||||||
usernames := make([]string, 0, len(toFetch))
|
usernames := make([]string, 0, len(toFetch))
|
||||||
for k := range toFetch {
|
for k := range toFetch {
|
||||||
usernames = append(usernames, k)
|
usernames = append(usernames, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
maxUsers := 30
|
|
||||||
for len(usernames) > 0 {
|
for len(usernames) > 0 {
|
||||||
if maxUsers > len(usernames) {
|
if maxUsers > len(usernames) {
|
||||||
maxUsers = len(usernames)
|
maxUsers = len(usernames)
|
||||||
|
|
|
@ -42,7 +42,7 @@ const (
|
||||||
|
|
||||||
func getSQLPlaceholders() []string {
|
func getSQLPlaceholders() []string {
|
||||||
var placeholders []string
|
var placeholders []string
|
||||||
for i := 1; i <= 50; i++ {
|
for i := 1; i <= 100; i++ {
|
||||||
if config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName {
|
if config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName {
|
||||||
placeholders = append(placeholders, fmt.Sprintf("$%d", i))
|
placeholders = append(placeholders, fmt.Sprintf("$%d", i))
|
||||||
} else {
|
} else {
|
||||||
|
@ -400,7 +400,7 @@ func getUsersInGroupsQuery(numArgs int) string {
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString("('')")
|
sb.WriteString("('')")
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(`SELECT username FROM %s WHERE id IN (SELECT user_id from %s WHERE group_id IN (SELECT id FROM %s WHERE name IN (%s)))`,
|
return fmt.Sprintf(`SELECT username FROM %s WHERE id IN (SELECT user_id from %s WHERE group_id IN (SELECT id FROM %s WHERE name IN %s))`,
|
||||||
sqlTableUsers, sqlTableUsersGroupsMapping, getSQLQuotedName(sqlTableGroups), sb.String())
|
sqlTableUsers, sqlTableUsersGroupsMapping, getSQLQuotedName(sqlTableGroups), sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -716,8 +716,8 @@ func getUpdateUserQuery(role string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUpdateUserPasswordQuery() string {
|
func getUpdateUserPasswordQuery() string {
|
||||||
return fmt.Sprintf(`UPDATE %s SET password=%s WHERE username = %s`,
|
return fmt.Sprintf(`UPDATE %s SET password=%s,updated_at=%s WHERE username = %s`,
|
||||||
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
|
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDeleteUserQuery(softDelete bool) string {
|
func getDeleteUserQuery(softDelete bool) string {
|
||||||
|
|
|
@ -981,9 +981,14 @@ func (u *User) getPatternsFilterForPath(virtualPath string) sdk.PatternsFilter {
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
dirsForPath := util.GetDirsForVirtualPath(virtualPath)
|
dirsForPath := util.GetDirsForVirtualPath(virtualPath)
|
||||||
for _, dir := range dirsForPath {
|
for idx, dir := range dirsForPath {
|
||||||
for _, f := range u.Filters.FilePatterns {
|
for _, f := range u.Filters.FilePatterns {
|
||||||
if f.Path == dir {
|
if f.Path == dir {
|
||||||
|
if idx > 0 && len(f.AllowedPatterns) > 0 && len(f.DeniedPatterns) > 0 && f.DeniedPatterns[0] == "*" {
|
||||||
|
if f.CheckAllowed(path.Base(dirsForPath[idx-1])) {
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
}
|
||||||
filter = f
|
filter = f
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -1004,7 +1009,7 @@ func (u *User) isDirHidden(virtualPath string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
filter := u.getPatternsFilterForPath(dirPath)
|
filter := u.getPatternsFilterForPath(dirPath)
|
||||||
if filter.DenyPolicy == sdk.DenyPolicyHide {
|
if filter.DenyPolicy == sdk.DenyPolicyHide && filter.Path != dirPath {
|
||||||
if !filter.CheckAllowed(path.Base(dirPath)) {
|
if !filter.CheckAllowed(path.Base(dirPath)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -1173,7 +1178,7 @@ func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSignature returns a signature for this admin.
|
// GetSignature returns a signature for this user.
|
||||||
// It will change after an update
|
// It will change after an update
|
||||||
func (u *User) GetSignature() string {
|
func (u *User) GetSignature() string {
|
||||||
return strconv.FormatInt(u.UpdatedAt, 10)
|
return strconv.FormatInt(u.UpdatedAt, 10)
|
||||||
|
@ -1289,39 +1294,12 @@ func (u *User) HasQuotaRestrictions() bool {
|
||||||
|
|
||||||
// HasTransferQuotaRestrictions returns true if there are any data transfer restrictions
|
// HasTransferQuotaRestrictions returns true if there are any data transfer restrictions
|
||||||
func (u *User) HasTransferQuotaRestrictions() bool {
|
func (u *User) HasTransferQuotaRestrictions() bool {
|
||||||
if len(u.Filters.DataTransferLimits) > 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return u.UploadDataTransfer > 0 || u.TotalDataTransfer > 0 || u.DownloadDataTransfer > 0
|
return u.UploadDataTransfer > 0 || u.TotalDataTransfer > 0 || u.DownloadDataTransfer > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDataTransferLimits returns upload, download and total data transfer limits
|
// GetDataTransferLimits returns upload, download and total data transfer limits
|
||||||
func (u *User) GetDataTransferLimits(clientIP string) (int64, int64, int64) {
|
func (u *User) GetDataTransferLimits() (int64, int64, int64) {
|
||||||
var total, ul, dl int64
|
var total, ul, dl int64
|
||||||
if len(u.Filters.DataTransferLimits) > 0 {
|
|
||||||
ip := net.ParseIP(clientIP)
|
|
||||||
if ip != nil {
|
|
||||||
for _, limit := range u.Filters.DataTransferLimits {
|
|
||||||
for _, source := range limit.Sources {
|
|
||||||
_, ipNet, err := net.ParseCIDR(source)
|
|
||||||
if err == nil {
|
|
||||||
if ipNet.Contains(ip) {
|
|
||||||
if limit.TotalDataTransfer > 0 {
|
|
||||||
total = limit.TotalDataTransfer * 1048576
|
|
||||||
}
|
|
||||||
if limit.DownloadDataTransfer > 0 {
|
|
||||||
dl = limit.DownloadDataTransfer * 1048576
|
|
||||||
}
|
|
||||||
if limit.UploadDataTransfer > 0 {
|
|
||||||
ul = limit.UploadDataTransfer * 1048576
|
|
||||||
}
|
|
||||||
return ul, dl, total
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if u.TotalDataTransfer > 0 {
|
if u.TotalDataTransfer > 0 {
|
||||||
total = u.TotalDataTransfer * 1048576
|
total = u.TotalDataTransfer * 1048576
|
||||||
}
|
}
|
||||||
|
@ -1825,7 +1803,6 @@ func (u *User) mergeAdditiveProperties(group *Group, groupType int, replacer *st
|
||||||
u.mergePermissions(group, groupType, replacer)
|
u.mergePermissions(group, groupType, replacer)
|
||||||
u.mergeFilePatterns(group, groupType, replacer)
|
u.mergeFilePatterns(group, groupType, replacer)
|
||||||
u.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...)
|
u.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...)
|
||||||
u.Filters.DataTransferLimits = append(u.Filters.DataTransferLimits, group.UserSettings.Filters.DataTransferLimits...)
|
|
||||||
u.Filters.AllowedIP = append(u.Filters.AllowedIP, group.UserSettings.Filters.AllowedIP...)
|
u.Filters.AllowedIP = append(u.Filters.AllowedIP, group.UserSettings.Filters.AllowedIP...)
|
||||||
u.Filters.DeniedIP = append(u.Filters.DeniedIP, group.UserSettings.Filters.DeniedIP...)
|
u.Filters.DeniedIP = append(u.Filters.DeniedIP, group.UserSettings.Filters.DeniedIP...)
|
||||||
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, group.UserSettings.Filters.DeniedLoginMethods...)
|
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, group.UserSettings.Filters.DeniedLoginMethods...)
|
||||||
|
@ -1855,6 +1832,9 @@ func (u *User) mergeVirtualFolders(group *Group, groupType int, replacer *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) mergePermissions(group *Group, groupType int, replacer *strings.Replacer) {
|
func (u *User) mergePermissions(group *Group, groupType int, replacer *strings.Replacer) {
|
||||||
|
if u.Permissions == nil {
|
||||||
|
u.Permissions = make(map[string][]string)
|
||||||
|
}
|
||||||
for k, v := range group.UserSettings.Permissions {
|
for k, v := range group.UserSettings.Permissions {
|
||||||
if k == "/" {
|
if k == "/" {
|
||||||
if groupType == sdk.GroupTypePrimary {
|
if groupType == sdk.GroupTypePrimary {
|
||||||
|
|
|
@ -90,154 +90,154 @@ CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
|
||||||
-----END EC PRIVATE KEY-----`
|
-----END EC PRIVATE KEY-----`
|
||||||
caCRT = `-----BEGIN CERTIFICATE-----
|
caCRT = `-----BEGIN CERTIFICATE-----
|
||||||
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
|
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
|
||||||
QXV0aDAeFw0yMjA3MDQxNTQzMTFaFw0yNDAxMDQxNTUzMDhaMBMxETAPBgNVBAMT
|
QXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT
|
||||||
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4eyDJkmW
|
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m
|
||||||
D4OVYo7ddgiZkd6QQdPyLcsa31Wc9jdR2/peEabyNT8jSWteS6ouY84GRlnhfFeZ
|
fi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1
|
||||||
mpXgbaUJu/Z8Y/8riPxwL8XF4vCScQDMywpQnVUd6E9x2/+/uaD4p/BBswgKqKPe
|
v03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh
|
||||||
uDcHZn7MkD4QlquUhMElDrBUi1Dv/AVHnQ6iP4vd5Jlv0F+40jdq/8Wa7yhW7Pu5
|
yzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL
|
||||||
iNvPwCk8HjENBKVur/re+Acif8A2TlbCsuOnVduSQNmnWH+iZmB9upyBZtUszGS0
|
CdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U
|
||||||
JhUwtSnwUX/JapF70Pwte/PV3RK8cJ5FjuAPNeTyJvSuMTELFSAyCeiNynFGgyhW
|
4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb
|
||||||
cqbEiPu6BURLculyVkmh4dOrhTrYZv/n3UJAhyxkdYrbh3INHmTa4izvclcuwoEo
|
KF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo
|
||||||
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
|
||||||
WwSa13hh6yjoDQ==
|
kK8u1YiiVenmoQ==
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
caCRL = `-----BEGIN X509 CRL-----
|
caCRL = `-----BEGIN X509 CRL-----
|
||||||
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
|
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
|
||||||
MjIwNzA0MTU1MzU4WhcNMjQwNzAzMTU1MzU4WjAkMCICEQDZo5Q3lhxFuDUsxGNm
|
MjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg
|
||||||
794YFw0yMjA3MDQxNTUzNThaoCMwITAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8Qvn
|
ZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r
|
||||||
bGzjmOULwDANBgkqhkiG9w0BAQsFAAOCAgEA1lK6g8qmhyY6myx8342dDuaauY03
|
rl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV
|
||||||
0iojkxpasuYcytK6XRm96YqjZK9EETxsHHViVU0vCXES60D6wJ9gw4fTWn3WxEdx
|
+jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199
|
||||||
nIwbGyjUGHh2y+R3uQsfvwxsdYvDsTLAnOLwOo68dAHWmMDZRmgTuGNoYFxVQRGR
|
XSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE
|
||||||
Cn90ZR7LPLpCScclWM8FE/W1B90x3ZE8EhJiCI/WyyTh3EgshmB7A5GoDrFZfmvR
|
jF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF
|
||||||
dzoTKO+F9p2XjtmgfiBE3czWQysfATmbutZUbG/ZRb89u+ZEUyPoC94mg8fhNWoX
|
DouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD
|
||||||
1d5G9QAkZFHp957/5QHLq9OHNfnWXoohhebjF4VWqZH7w+RtLc8t0PIog2lX4t1o
|
MSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4
|
||||||
5N/xFk9akvuoyNGg/fYuJBmN162Q0MdeYfYKDGWdXxf6fpHxVr5v2JrIx6gOwubb
|
iYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE
|
||||||
cIKP22ZBv/PYOeFsAZ755lTl4OTFUjU5ZJEPD6pUc1daaIqfxsxu8gDZP92FZjsB
|
ypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4
|
||||||
zaalMbh30n2OhagSMBzSLg5rE6WmBzlQX0ZN8YrW4l2Vq6twnnFHY+UyblRZS+d4
|
hVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94
|
||||||
oHBaoOaxPEkLxNZ8ulzJS4B6c4D1CXOaBEf++snVzRRUOEdX3x7TvkkrLvIsm06R
|
cp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67
|
||||||
ux0L1zJb9LbZ/1rhuv70z/kIlD55sqYuRqu3RpgTgZuTERU//rYIqWd03Y5Qon8i
|
sn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn
|
||||||
VoC6Yp9DPldQJrk=
|
w7bXJGfJcIMrrKs=
|
||||||
-----END X509 CRL-----`
|
-----END X509 CRL-----`
|
||||||
client1Crt = `-----BEGIN CERTIFICATE-----
|
client1Crt = `-----BEGIN CERTIFICATE-----
|
||||||
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
|
||||||
7LEmxYg=
|
MU3Bo0A=
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
client1Key = `-----BEGIN RSA PRIVATE KEY-----
|
client1Key = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
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
|
||||||
-----END RSA PRIVATE KEY-----`
|
-----END RSA PRIVATE KEY-----`
|
||||||
// client 2 crt is revoked
|
// client 2 crt is revoked
|
||||||
client2Crt = `-----BEGIN CERTIFICATE-----
|
client2Crt = `-----BEGIN CERTIFICATE-----
|
||||||
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
|
||||||
enqNrFI=
|
E0+r2+4=
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
client2Key = `-----BEGIN RSA PRIVATE KEY-----
|
client2Key = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
MIIEowIBAAKCAQEAzNl7q7yS8MSaQs6zRbuqrsUuwEJ5ZH85vf7zHZKgOW3zNniX
|
MIIEowIBAAKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImV
|
||||||
LOmHJdtQ3jKZQ1BCIsJFvez2GxGIMWbXaSPw4bL0J3vl5oItChsjGg34IvqcDxWu
|
jm/bQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TP
|
||||||
Ik2amuRdMh7r1ryVs2ir2cQ5YHzI59BEpUWKQg3bD4yragdkb6BRc7lVgzCbrM1E
|
kZuaeq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEK
|
||||||
q758HHbaLwlsfpqOvheaum4IG113CeD/HHrw42W6g/qQWL+FHlYqV3plHZ8Bj+bh
|
h4LQcr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81P
|
||||||
cZI5jdU4paGEzeY0a0NlnyH4gXGPjLKvPKFZHy4D6RiRlLHvHeiRyDtTu4wFkAiX
|
QAmTA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu
|
||||||
xzGsE4UBbykmYUB85zgwpjaktOaoe36IM1T8CQIDAQABAoIBAETHMJK0udFE8VZE
|
4Ic06tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABAoIBAQCMnEeg9uXQmdvq
|
||||||
+EQNgn0zj0LWDtQDM2vrUc04Ebu2gtZjHr7hmZLIVBqGepbzN4FcIPZnvSnRdRzB
|
op4qi6bV+ZcDWvvkLwvHikFMnYpIaheYBpF2ZMKzdmO4xgCSWeFCQ4Hah8KxfHCM
|
||||||
HsoaWyIsZ3VqUAJY6q5d9iclUY7M/eDCsripvaML0Y6meyCaKNkX57sx+uG+g+Xx
|
qLuWvw2bBBE5J8yQ/JaPyeLbec7RX41GQ2YhPoxDdP0PdErREdpWo4imiFhH/Ewt
|
||||||
M1saQhVzeX17CYKMANjJxw9HxsJI0aBPyiBbILHMwfRfsJU8Ou72HH1sIQuPdH2H
|
Rvq7ufRdpdLoS8dzzwnvX3r+H2MkHoC/QANW2AOuVoZK5qyCH5N8yEAAbWKaQaeL
|
||||||
/c9ru8YZAno6oVq1zuC/pCis+h50U9HzTnt3/4NNS6cWG/y2YLztCvm9uGo4MTd/
|
VBhAYEVKbAkWEtXw7bYXzxRR7WIM3f45v3ncRusDIG+Hf75ZjatoH0lF1gHQNofO
|
||||||
mA9s4cxVhvQW6gCDHgGn6zj661OL/d2rpak1eWizhZvZ8jsIN/sM87b0AJeVT4zH
|
qkCVZVzjkLFuzDic2KZqsNORglNs4J6t5Dahb9v3hnoK963YMnVSUjFvqQ+/RZZy
|
||||||
6xA3egECgYEA1nI5EsCetQbFBp7tDovSp3fbitwoQtdtHtLn2u4DfvmbLrgSoq0Z
|
VILFShilAoGBANucwZU61eJ0tLKBYEwmRY/K7Gu1MvvcYJIOoX8/BL3zNmNO0CLl
|
||||||
L+9N13xML/l8lzWai2gI69uA3c2+y1O64LkaiSeDqbeBp9b6fKMlmwIVbklEke1w
|
NiABtNt9WOVwZxDsxJXdo1zvMtAegNqS6W11R1VAZbL6mQ/krScbLDE6JKA5DmA7
|
||||||
XVTIWOYTTF5/8+tUOlsgme5BhLAWnQ7+SoitzHtl5e1vEYaAGamE2DECgYEA9Is2
|
4nNi1gJOW1ziAfdBAfhe4cLbQOb94xkOK5xM1YpO0xgDJLwrZbehDMmPAoGBAMAl
|
||||||
BbTk2YCqkcsB7D9q95JbY0SZpecvTv0rLR+acz3T8JrAASdmvqdBOlPWc+0ZaEdS
|
/owPDAvcXz7JFynT0ieYVc64MSFiwGYJcsmxSAnbEgQ+TR5FtkHYe91OSqauZcCd
|
||||||
PcJaOEw3yxYJ33cR/nLBaR2/Uu5qQebyPALs3B2pjjTFdGvcpeFxO55fowwsfR/e
|
aoKXQNyrYKIhyounRPFTdYQrlx6KtEs7LU9wOxuphhpJtGjRnhmA7IqvX703wNvu
|
||||||
0H+HeiFj5Y4S+kFWT+3FRmJ6GUB828LJYaVhQ1kCgYEA1bdsTdYN1Vfzz89fbZnH
|
khrEavn86G5boH8R80371SrN0Rh9UeAlQGuNBdvfAoGAEAmokW9Ug08miwqrr6Pz
|
||||||
zQLUl6UlssfDhm6mhzeh4E+eaocke1+LtIwHxfOocj9v/bp8VObPzU8rNOIxfa3q
|
3IZjMZJwALidTM1IufQuMnj6ddIhnQrEIx48yPKkdUz6GeBQkuk2rujA+zXfDxc/
|
||||||
lr+jRIFO5DtwSfckGEb32W3QMeNvJQe/biRqrr5NCVU8q7kibi4XZZFfVn+vacNh
|
eMDhzrX/N0zZtLFse7ieR5IJbrH7/MciyG5lVpHGVkgjAJ18uVikgAhm+vd7iC7i
|
||||||
hqKEoz9vpCBnCs5CqFCbhmECgYAG8qWYR+lwnI08Ey58zdh2LDxYd6x94DGh5uOB
|
vG1YAtuyysQgAKXircBTIL0CgYAHeTLWVbt9NpwJwB6DhPaWjalAug9HIiUjktiB
|
||||||
JrK2r30ECwGFht8Ob6YUyCkBpizgn5YglxMFInU7Webx6GokdpI0MFotOwTd1nfv
|
GcEYiQnBWn77X3DATOA8clAa/Yt9m2HKJIHkU1IV3ESZe+8Fh955PozJJlHu3yVb
|
||||||
aI3eOyGEHs+1XRMpy1vyO6+v7DqfW3ZzKgxpVeWGsiCr54tSPgkq1MVvTju96qza
|
Ap157PUHTriSnxyMF2Sb3EhX/rQkmbnbCqqygHC14iBy8MrKzLG00X6BelZV5n0D
|
||||||
D17SEQKBgCKC0GjDjnt/JvujdzHuBt1sWdOtb+B6kQvA09qVmuDF/Dq36jiaHDjg
|
8d85dwKBgGWY2nsaemPH/TiTVF6kW1IKSQoIyJChkngc+Xj/2aCCkkmAEn8eqncl
|
||||||
XMf5HU3ThYqYn3bYypZZ8nQ7BXVh4LqGNqG29wR4v6l+dLO6odXnLzfApGD9e+d4
|
RKjnkiEZeG4+G91Xu7+HmcBLwV86k5I+tXK9O1Okomr6Zry8oqVcxU5TB6VRS+rA
|
||||||
2tmlLP54LaN35hQxRjhT8lCN0BkrNF44+bh8frwm/kuxSd8wT2S+
|
ubwF00Drdvk2+kDZfxIM137nBiy7wgCJi2Ksm5ihN3dUF6Q0oNPl
|
||||||
-----END RSA PRIVATE KEY-----`
|
-----END RSA PRIVATE KEY-----`
|
||||||
testFileName = "test_file_ftp.dat"
|
testFileName = "test_file_ftp.dat"
|
||||||
testDLFileName = "test_download_ftp.dat"
|
testDLFileName = "test_download_ftp.dat"
|
||||||
|
|
|
@ -170,7 +170,7 @@ func searchFsEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write(data) //nolint:errcheck
|
w.Write(data) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +205,7 @@ func searchProviderEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write(data) //nolint:errcheck
|
w.Write(data) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +238,7 @@ func searchLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write(data) //nolint:errcheck
|
w.Write(data) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -160,7 +160,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
fi, err := os.Stat(inputFile)
|
fi, err := os.Stat(inputFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, fmt.Errorf("invalid input_file %q", inputFile), "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if fi.Size() > MaxRestoreSize {
|
if fi.Size() > MaxRestoreSize {
|
||||||
|
@ -171,7 +171,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
content, err := os.ReadFile(inputFile)
|
content, err := os.ReadFile(inputFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, fmt.Errorf("invalid input_file %q", inputFile), "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := restoreBackup(content, inputFile, scanQuota, mode, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {
|
if err := restoreBackup(content, inputFile, scanQuota, mode, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {
|
||||||
|
@ -184,7 +184,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
|
||||||
func restoreBackup(content []byte, inputFile string, scanQuota, mode int, executor, ipAddress, role string) error {
|
func restoreBackup(content []byte, inputFile string, scanQuota, mode int, executor, ipAddress, role string) error {
|
||||||
dump, err := dataprovider.ParseDumpData(content)
|
dump, err := dataprovider.ParseDumpData(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.NewValidationError(fmt.Sprintf("unable to parse backup content: %v", err))
|
return util.NewValidationError(fmt.Sprintf("invalid input_file %q", inputFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = RestoreConfigs(dump.Configs, mode, executor, ipAddress, role); err != nil {
|
if err = RestoreConfigs(dump.Configs, mode, executor, ipAddress, role); err != nil {
|
||||||
|
|
|
@ -331,7 +331,7 @@ func isTokenInvalidated(r *http.Request) bool {
|
||||||
token := fn(r)
|
token := fn(r)
|
||||||
if token != "" {
|
if token != "" {
|
||||||
isTokenFound = true
|
isTokenFound = true
|
||||||
if _, ok := invalidatedJWTTokens.Load(token); ok {
|
if invalidatedJWTTokens.Get(token) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -343,11 +343,11 @@ func isTokenInvalidated(r *http.Request) bool {
|
||||||
func invalidateToken(r *http.Request) {
|
func invalidateToken(r *http.Request) {
|
||||||
tokenString := jwtauth.TokenFromHeader(r)
|
tokenString := jwtauth.TokenFromHeader(r)
|
||||||
if tokenString != "" {
|
if tokenString != "" {
|
||||||
invalidatedJWTTokens.Store(tokenString, time.Now().Add(tokenDuration).UTC())
|
invalidatedJWTTokens.Add(tokenString, time.Now().Add(tokenDuration).UTC())
|
||||||
}
|
}
|
||||||
tokenString = jwtauth.TokenFromCookie(r)
|
tokenString = jwtauth.TokenFromCookie(r)
|
||||||
if tokenString != "" {
|
if tokenString != "" {
|
||||||
invalidatedJWTTokens.Store(tokenString, time.Now().Add(tokenDuration).UTC())
|
invalidatedJWTTokens.Add(tokenString, time.Now().Add(tokenDuration).UTC())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
@ -196,7 +195,7 @@ var (
|
||||||
certMgr *common.CertManager
|
certMgr *common.CertManager
|
||||||
cleanupTicker *time.Ticker
|
cleanupTicker *time.Ticker
|
||||||
cleanupDone chan bool
|
cleanupDone chan bool
|
||||||
invalidatedJWTTokens sync.Map
|
invalidatedJWTTokens tokenManager
|
||||||
csrfTokenAuth *jwtauth.JWTAuth
|
csrfTokenAuth *jwtauth.JWTAuth
|
||||||
webRootPath string
|
webRootPath string
|
||||||
webBasePath string
|
webBasePath string
|
||||||
|
@ -921,6 +920,7 @@ func (c *Conf) Initialize(configDir string, isShared int) error {
|
||||||
}
|
}
|
||||||
logger.Info(logSender, "", "initializing HTTP server with config %+v", c.getRedacted())
|
logger.Info(logSender, "", "initializing HTTP server with config %+v", c.getRedacted())
|
||||||
configurationDir = configDir
|
configurationDir = configDir
|
||||||
|
invalidatedJWTTokens = newTokenManager(isShared)
|
||||||
resetCodesMgr = newResetCodeManager(isShared)
|
resetCodesMgr = newResetCodeManager(isShared)
|
||||||
oidcMgr = newOIDCManager(isShared)
|
oidcMgr = newOIDCManager(isShared)
|
||||||
oauth2Mgr = newOAuth2Manager(isShared)
|
oauth2Mgr = newOAuth2Manager(isShared)
|
||||||
|
@ -1047,7 +1047,7 @@ func getServicesStatus() *ServicesStatus {
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileServer(r chi.Router, path string, root http.FileSystem) {
|
func fileServer(r chi.Router, path string, root http.FileSystem, disableDirectoryIndex bool) {
|
||||||
if path != "/" && path[len(path)-1] != '/' {
|
if path != "/" && path[len(path)-1] != '/' {
|
||||||
r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
|
r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
|
||||||
path += "/"
|
path += "/"
|
||||||
|
@ -1057,7 +1057,11 @@ func fileServer(r chi.Router, path string, root http.FileSystem) {
|
||||||
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
|
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
|
||||||
rctx := chi.RouteContext(r.Context())
|
rctx := chi.RouteContext(r.Context())
|
||||||
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
|
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
|
||||||
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
|
handler := http.FileServer(root)
|
||||||
|
if disableDirectoryIndex {
|
||||||
|
handler = neuter(handler)
|
||||||
|
}
|
||||||
|
fs := http.StripPrefix(pathPrefix, handler)
|
||||||
fs.ServeHTTP(w, r)
|
fs.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1179,7 +1183,7 @@ func startCleanupTicker(duration time.Duration) {
|
||||||
return
|
return
|
||||||
case <-cleanupTicker.C:
|
case <-cleanupTicker.C:
|
||||||
counter++
|
counter++
|
||||||
cleanupExpiredJWTTokens()
|
invalidatedJWTTokens.Cleanup()
|
||||||
resetCodesMgr.Cleanup()
|
resetCodesMgr.Cleanup()
|
||||||
if counter%2 == 0 {
|
if counter%2 == 0 {
|
||||||
oidcMgr.cleanup()
|
oidcMgr.cleanup()
|
||||||
|
@ -1198,16 +1202,6 @@ func stopCleanupTicker() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupExpiredJWTTokens() {
|
|
||||||
invalidatedJWTTokens.Range(func(key, value any) bool {
|
|
||||||
exp, ok := value.(time.Time)
|
|
||||||
if !ok || exp.Before(time.Now().UTC()) {
|
|
||||||
invalidatedJWTTokens.Delete(key)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSigningKey(signingPassphrase string) []byte {
|
func getSigningKey(signingPassphrase string) []byte {
|
||||||
if signingPassphrase != "" {
|
if signingPassphrase != "" {
|
||||||
sk := sha256.Sum256([]byte(signingPassphrase))
|
sk := sha256.Sum256([]byte(signingPassphrase))
|
||||||
|
|
|
@ -2627,103 +2627,6 @@ func TestEventRuleValidation(t *testing.T) {
|
||||||
assert.Contains(t, string(resp), "invalid Identity Provider login event")
|
assert.Contains(t, string(resp), "invalid Identity Provider login event")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUserTransferLimits(t *testing.T) {
|
|
||||||
u := getTestUser()
|
|
||||||
u.TotalDataTransfer = 100
|
|
||||||
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
|
|
||||||
{
|
|
||||||
Sources: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)
|
|
||||||
assert.NoError(t, err, string(resp))
|
|
||||||
assert.Contains(t, string(resp), "Validation error: no data transfer limit source specified")
|
|
||||||
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
|
|
||||||
{
|
|
||||||
Sources: []string{"a"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
|
|
||||||
assert.NoError(t, err, string(resp))
|
|
||||||
assert.Contains(t, string(resp), "Validation error: could not parse data transfer limit source")
|
|
||||||
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
|
|
||||||
{
|
|
||||||
Sources: []string{"127.0.0.1/32"},
|
|
||||||
UploadDataTransfer: 120,
|
|
||||||
DownloadDataTransfer: 140,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Sources: []string{"192.168.0.0/24", "192.168.1.0/24"},
|
|
||||||
TotalDataTransfer: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Sources: []string{"10.0.0.0/8"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
|
|
||||||
assert.NoError(t, err, string(resp))
|
|
||||||
assert.Len(t, user.Filters.DataTransferLimits, 3)
|
|
||||||
assert.Equal(t, u.Filters.DataTransferLimits, user.Filters.DataTransferLimits)
|
|
||||||
up, down, total := user.GetDataTransferLimits("1.1.1.1")
|
|
||||||
assert.Equal(t, user.TotalDataTransfer*1024*1024, total)
|
|
||||||
assert.Equal(t, user.UploadDataTransfer*1024*1024, up)
|
|
||||||
assert.Equal(t, user.DownloadDataTransfer*1024*1024, down)
|
|
||||||
up, down, total = user.GetDataTransferLimits("127.0.0.1")
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[0].TotalDataTransfer*1024*1024, total)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[0].UploadDataTransfer*1024*1024, up)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[0].DownloadDataTransfer*1024*1024, down)
|
|
||||||
up, down, total = user.GetDataTransferLimits("192.168.1.6")
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[1].TotalDataTransfer*1024*1024, total)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[1].UploadDataTransfer*1024*1024, up)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[1].DownloadDataTransfer*1024*1024, down)
|
|
||||||
up, down, total = user.GetDataTransferLimits("10.1.2.3")
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[2].TotalDataTransfer*1024*1024, total)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[2].UploadDataTransfer*1024*1024, up)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[2].DownloadDataTransfer*1024*1024, down)
|
|
||||||
|
|
||||||
connID := xid.New().String()
|
|
||||||
localAddr := "::1"
|
|
||||||
conn := common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "1.1.1.2", user)
|
|
||||||
transferQuota := conn.GetTransferQuota()
|
|
||||||
assert.Equal(t, user.TotalDataTransfer*1024*1024, transferQuota.AllowedTotalSize)
|
|
||||||
assert.Equal(t, user.UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
|
|
||||||
assert.Equal(t, user.DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
|
|
||||||
|
|
||||||
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "127.0.0.1", user)
|
|
||||||
transferQuota = conn.GetTransferQuota()
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[0].TotalDataTransfer*1024*1024, transferQuota.AllowedTotalSize)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[0].UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[0].DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
|
|
||||||
|
|
||||||
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "192.168.1.5", user)
|
|
||||||
transferQuota = conn.GetTransferQuota()
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[1].TotalDataTransfer*1024*1024, transferQuota.AllowedTotalSize)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[1].UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[1].DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
|
|
||||||
|
|
||||||
u.UsedDownloadDataTransfer = 10 * 1024 * 1024
|
|
||||||
u.UsedUploadDataTransfer = 5 * 1024 * 1024
|
|
||||||
_, err = httpdtest.UpdateTransferQuotaUsage(u, "", http.StatusOK)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "192.168.1.6", user)
|
|
||||||
transferQuota = conn.GetTransferQuota()
|
|
||||||
assert.Equal(t, (user.Filters.DataTransferLimits[1].TotalDataTransfer-15)*1024*1024, transferQuota.AllowedTotalSize)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[1].UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
|
|
||||||
assert.Equal(t, user.Filters.DataTransferLimits[1].DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
|
|
||||||
|
|
||||||
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "10.8.3.4", user)
|
|
||||||
transferQuota = conn.GetTransferQuota()
|
|
||||||
assert.Equal(t, int64(0), transferQuota.AllowedTotalSize)
|
|
||||||
assert.Equal(t, int64(0), transferQuota.AllowedULSize)
|
|
||||||
assert.Equal(t, int64(0), transferQuota.AllowedDLSize)
|
|
||||||
|
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
|
||||||
assert.NoError(t, err)
|
|
||||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserBandwidthLimits(t *testing.T) {
|
func TestUserBandwidthLimits(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.UploadBandwidth = 128
|
u.UploadBandwidth = 128
|
||||||
|
@ -8038,7 +7941,7 @@ func TestLoaddata(t *testing.T) {
|
||||||
if runtime.GOOS != osWindows {
|
if runtime.GOOS != osWindows {
|
||||||
err = os.Chmod(backupFilePath, 0111)
|
err = os.Chmod(backupFilePath, 0111)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, _, err = httpdtest.Loaddata(backupFilePath, "1", "", http.StatusForbidden)
|
_, _, err = httpdtest.Loaddata(backupFilePath, "1", "", http.StatusBadRequest)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.Chmod(backupFilePath, 0644)
|
err = os.Chmod(backupFilePath, 0644)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -8052,7 +7955,7 @@ func TestLoaddata(t *testing.T) {
|
||||||
configsGet, err := dataprovider.GetConfigs()
|
configsGet, err := dataprovider.GetConfigs()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, configs.SMTP, configsGet.SMTP)
|
assert.Equal(t, configs.SMTP, configsGet.SMTP)
|
||||||
assert.Equal(t, configs.SFTPD.HostKeyAlgos, configsGet.SFTPD.HostKeyAlgos)
|
assert.Equal(t, []string{ssh.KeyAlgoRSA}, configsGet.SFTPD.HostKeyAlgos)
|
||||||
assert.Len(t, configsGet.SFTPD.Moduli, 0)
|
assert.Len(t, configsGet.SFTPD.Moduli, 0)
|
||||||
assert.Len(t, configsGet.SFTPD.KexAlgorithms, 0)
|
assert.Len(t, configsGet.SFTPD.KexAlgorithms, 0)
|
||||||
assert.Len(t, configsGet.SFTPD.Ciphers, 0)
|
assert.Len(t, configsGet.SFTPD.Ciphers, 0)
|
||||||
|
@ -12631,7 +12534,7 @@ func TestDefender(t *testing.T) {
|
||||||
req.RemoteAddr = remoteAddr
|
req.RemoteAddr = remoteAddr
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusForbidden, rr)
|
checkResponseCode(t, http.StatusForbidden, rr)
|
||||||
assert.Contains(t, rr.Body.String(), "your IP address is banned")
|
assert.Contains(t, rr.Body.String(), "your IP address is blocked")
|
||||||
|
|
||||||
req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil)
|
req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil)
|
||||||
req.RequestURI = webUsersPath
|
req.RequestURI = webUsersPath
|
||||||
|
@ -12639,7 +12542,7 @@ func TestDefender(t *testing.T) {
|
||||||
req.RemoteAddr = remoteAddr
|
req.RemoteAddr = remoteAddr
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusForbidden, rr)
|
checkResponseCode(t, http.StatusForbidden, rr)
|
||||||
assert.Contains(t, rr.Body.String(), "your IP address is banned")
|
assert.Contains(t, rr.Body.String(), "your IP address is blocked")
|
||||||
|
|
||||||
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)
|
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)
|
||||||
req.Header.Set("X-Real-IP", "127.0.0.1:2345")
|
req.Header.Set("X-Real-IP", "127.0.0.1:2345")
|
||||||
|
@ -12647,7 +12550,7 @@ func TestDefender(t *testing.T) {
|
||||||
req.RemoteAddr = remoteAddr
|
req.RemoteAddr = remoteAddr
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusForbidden, rr)
|
checkResponseCode(t, http.StatusForbidden, rr)
|
||||||
assert.Contains(t, rr.Body.String(), "your IP address is banned")
|
assert.Contains(t, rr.Body.String(), "your IP address is blocked")
|
||||||
|
|
||||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -12870,6 +12773,8 @@ func TestWebConfigsMock(t *testing.T) {
|
||||||
assert.Contains(t, rr.Body.String(), ssh.CertAlgoDSAv01) // invalid algo
|
assert.Contains(t, rr.Body.String(), ssh.CertAlgoDSAv01) // invalid algo
|
||||||
form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
|
form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
|
||||||
form.Add("sftp_host_key_algos", ssh.CertAlgoRSAv01)
|
form.Add("sftp_host_key_algos", ssh.CertAlgoRSAv01)
|
||||||
|
form.Set("sftp_kex_algos", "diffie-hellman-group18-sha512")
|
||||||
|
form.Add("sftp_kex_algos", "diffie-hellman-group16-sha512")
|
||||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
setJWTCookieForReq(req, webToken)
|
setJWTCookieForReq(req, webToken)
|
||||||
|
@ -12880,12 +12785,13 @@ func TestWebConfigsMock(t *testing.T) {
|
||||||
// check SFTP configs
|
// check SFTP configs
|
||||||
configs, err := dataprovider.GetConfigs()
|
configs, err := dataprovider.GetConfigs()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2)
|
assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
|
||||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
|
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
|
||||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
|
|
||||||
assert.Len(t, configs.SFTPD.Moduli, 2)
|
assert.Len(t, configs.SFTPD.Moduli, 2)
|
||||||
assert.Contains(t, configs.SFTPD.Moduli, "path 1")
|
assert.Contains(t, configs.SFTPD.Moduli, "path 1")
|
||||||
assert.Contains(t, configs.SFTPD.Moduli, "path 2")
|
assert.Contains(t, configs.SFTPD.Moduli, "path 2")
|
||||||
|
assert.Len(t, configs.SFTPD.KexAlgorithms, 1)
|
||||||
|
assert.Contains(t, configs.SFTPD.KexAlgorithms, "diffie-hellman-group16-sha512")
|
||||||
// invalid form action
|
// invalid form action
|
||||||
form.Set("form_action", "")
|
form.Set("form_action", "")
|
||||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||||
|
@ -12927,9 +12833,8 @@ func TestWebConfigsMock(t *testing.T) {
|
||||||
// check
|
// check
|
||||||
configs, err = dataprovider.GetConfigs()
|
configs, err = dataprovider.GetConfigs()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2)
|
assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
|
||||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
|
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
|
||||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
|
|
||||||
assert.Len(t, configs.SFTPD.Moduli, 2)
|
assert.Len(t, configs.SFTPD.Moduli, 2)
|
||||||
assert.Equal(t, "mail.example.net", configs.SMTP.Host)
|
assert.Equal(t, "mail.example.net", configs.SMTP.Host)
|
||||||
assert.Equal(t, 587, configs.SMTP.Port)
|
assert.Equal(t, 587, configs.SMTP.Port)
|
||||||
|
@ -12998,9 +12903,8 @@ func TestWebConfigsMock(t *testing.T) {
|
||||||
// check
|
// check
|
||||||
configs, err = dataprovider.GetConfigs()
|
configs, err = dataprovider.GetConfigs()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2)
|
assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
|
||||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
|
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
|
||||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
|
|
||||||
assert.Len(t, configs.SFTPD.Moduli, 2)
|
assert.Len(t, configs.SFTPD.Moduli, 2)
|
||||||
assert.Equal(t, 80, configs.ACME.HTTP01Challenge.Port)
|
assert.Equal(t, 80, configs.ACME.HTTP01Challenge.Port)
|
||||||
assert.Equal(t, 7, configs.ACME.Protocols)
|
assert.Equal(t, 7, configs.ACME.Protocols)
|
||||||
|
@ -13031,7 +12935,7 @@ func TestWebConfigsMock(t *testing.T) {
|
||||||
assert.Contains(t, rr.Body.String(), "Configurations updated")
|
assert.Contains(t, rr.Body.String(), "Configurations updated")
|
||||||
configs, err = dataprovider.GetConfigs()
|
configs, err = dataprovider.GetConfigs()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2)
|
assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
|
||||||
assert.Equal(t, 402, configs.ACME.HTTP01Challenge.Port)
|
assert.Equal(t, 402, configs.ACME.HTTP01Challenge.Port)
|
||||||
assert.Equal(t, 1, configs.ACME.Protocols)
|
assert.Equal(t, 1, configs.ACME.Protocols)
|
||||||
assert.Equal(t, domain, configs.ACME.Domain)
|
assert.Equal(t, domain, configs.ACME.Domain)
|
||||||
|
@ -13993,7 +13897,8 @@ func TestShareUploadSingle(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusNotFound, rr)
|
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||||
|
assert.Contains(t, rr.Body.String(), "operation unsupported")
|
||||||
|
|
||||||
err = os.MkdirAll(filepath.Join(user.GetHomeDir(), "dir"), os.ModePerm)
|
err = os.MkdirAll(filepath.Join(user.GetHomeDir(), "dir"), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -14004,6 +13909,13 @@ func TestShareUploadSingle(t *testing.T) {
|
||||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||||
assert.Contains(t, rr.Body.String(), "operation unsupported")
|
assert.Contains(t, rr.Body.String(), "operation unsupported")
|
||||||
|
|
||||||
|
// only uploads to the share root dir are allowed
|
||||||
|
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "dir", "file.dat"), bytes.NewBuffer(content))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusNotFound, rr)
|
||||||
|
|
||||||
share, err = dataprovider.ShareExists(objectID, user.Username)
|
share, err = dataprovider.ShareExists(objectID, user.Username)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 2, share.UsedTokens)
|
assert.Equal(t, 2, share.UsedTokens)
|
||||||
|
@ -14986,9 +14898,11 @@ func TestUserAPIShares(t *testing.T) {
|
||||||
func TestUsersAPISharesNoPasswordDisabled(t *testing.T) {
|
func TestUsersAPISharesNoPasswordDisabled(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled}
|
u.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled}
|
||||||
|
u.Filters.PasswordStrength = 70
|
||||||
|
u.Password = "ahpoo8baa6EeZieshies"
|
||||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
|
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, u.Password)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
share := dataprovider.Share{
|
share := dataprovider.Share{
|
||||||
|
@ -15012,6 +14926,15 @@ func TestUsersAPISharesNoPasswordDisabled(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
setBearerForReq(req, token)
|
setBearerForReq(req, token)
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||||
|
|
||||||
|
share.Password = "vi5eiJoovee5ya9yahpi"
|
||||||
|
asJSON, err = json.Marshal(share)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setBearerForReq(req, token)
|
||||||
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusCreated, rr)
|
checkResponseCode(t, http.StatusCreated, rr)
|
||||||
location := rr.Header().Get("Location")
|
location := rr.Header().Get("Location")
|
||||||
assert.NotEmpty(t, location)
|
assert.NotEmpty(t, location)
|
||||||
|
@ -15051,6 +14974,13 @@ func TestUserAPIKey(t *testing.T) {
|
||||||
apiKey, _, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)
|
apiKey, _, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
adminAPIKey := dataprovider.APIKey{
|
||||||
|
Name: "testadminkey",
|
||||||
|
Scope: dataprovider.APIKeyScopeAdmin,
|
||||||
|
}
|
||||||
|
adminAPIKey, _, err = httpdtest.AddAPIKey(adminAPIKey, http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
body := new(bytes.Buffer)
|
body := new(bytes.Buffer)
|
||||||
writer := multipart.NewWriter(body)
|
writer := multipart.NewWriter(body)
|
||||||
part, err := writer.CreateFormFile("filenames", "filenametest")
|
part, err := writer.CreateFormFile("filenames", "filenametest")
|
||||||
|
@ -15079,6 +15009,12 @@ func TestUserAPIKey(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, dirEntries, 1)
|
assert.Len(t, dirEntries, 1)
|
||||||
|
|
||||||
|
req, err = http.NewRequest(http.MethodGet, userDirsPath, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setAPIKeyForReq(req, adminAPIKey.Key, user.Username)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusForbidden, rr)
|
||||||
|
|
||||||
user.Status = 0
|
user.Status = 0
|
||||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -15155,6 +15091,9 @@ func TestUserAPIKey(t *testing.T) {
|
||||||
|
|
||||||
_, err = httpdtest.RemoveAPIKey(apiKeyNew, http.StatusOK)
|
_, err = httpdtest.RemoveAPIKey(apiKeyNew, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveAPIKey(adminAPIKey, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebClientViewPDF(t *testing.T) {
|
func TestWebClientViewPDF(t *testing.T) {
|
||||||
|
@ -19686,49 +19625,6 @@ func TestWebUserAddMock(t *testing.T) {
|
||||||
assert.Contains(t, rr.Body.String(), "Validation error: could not parse bandwidth limit source")
|
assert.Contains(t, rr.Body.String(), "Validation error: could not parse bandwidth limit source")
|
||||||
form.Set("bandwidth_limit_sources1", "127.0.0.1/32")
|
form.Set("bandwidth_limit_sources1", "127.0.0.1/32")
|
||||||
form.Set("upload_bandwidth_source1", "-1")
|
form.Set("upload_bandwidth_source1", "-1")
|
||||||
form.Set("data_transfer_limit_sources0", "127.0.1.1")
|
|
||||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
|
||||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
|
||||||
setJWTCookieForReq(req, webToken)
|
|
||||||
req.Header.Set("Content-Type", contentType)
|
|
||||||
rr = executeRequest(req)
|
|
||||||
checkResponseCode(t, http.StatusOK, rr)
|
|
||||||
assert.Contains(t, rr.Body.String(), "could not parse data transfer limit source")
|
|
||||||
form.Set("data_transfer_limit_sources0", "127.0.1.1/32")
|
|
||||||
form.Set("upload_data_transfer_source0", "a")
|
|
||||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
|
||||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
|
||||||
setJWTCookieForReq(req, webToken)
|
|
||||||
req.Header.Set("Content-Type", contentType)
|
|
||||||
rr = executeRequest(req)
|
|
||||||
checkResponseCode(t, http.StatusOK, rr)
|
|
||||||
assert.Contains(t, rr.Body.String(), "invalid upload_data_transfer_source")
|
|
||||||
form.Set("upload_data_transfer_source0", "0")
|
|
||||||
form.Set("download_data_transfer_source0", "a")
|
|
||||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
|
||||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
|
||||||
setJWTCookieForReq(req, webToken)
|
|
||||||
req.Header.Set("Content-Type", contentType)
|
|
||||||
rr = executeRequest(req)
|
|
||||||
checkResponseCode(t, http.StatusOK, rr)
|
|
||||||
assert.Contains(t, rr.Body.String(), "invalid download_data_transfer_source")
|
|
||||||
form.Set("download_data_transfer_source0", "0")
|
|
||||||
form.Set("total_data_transfer_source0", "a")
|
|
||||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
|
||||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
|
||||||
setJWTCookieForReq(req, webToken)
|
|
||||||
req.Header.Set("Content-Type", contentType)
|
|
||||||
rr = executeRequest(req)
|
|
||||||
checkResponseCode(t, http.StatusOK, rr)
|
|
||||||
assert.Contains(t, rr.Body.String(), "invalid total_data_transfer_source")
|
|
||||||
form.Set("total_data_transfer_source0", "0")
|
|
||||||
form.Set("data_transfer_limit_sources10", "192.168.5.0/24, 10.8.0.0/16")
|
|
||||||
form.Set("download_data_transfer_source10", "100")
|
|
||||||
form.Set("upload_data_transfer_source10", "120")
|
|
||||||
form.Set("data_transfer_limit_sources12", "192.168.3.0/24, 10.8.2.0/24,::1/64")
|
|
||||||
form.Set("download_data_transfer_source12", "100")
|
|
||||||
form.Set("upload_data_transfer_source12", "120")
|
|
||||||
form.Set("total_data_transfer_source12", "200")
|
|
||||||
// invalid external auth cache size
|
// invalid external auth cache size
|
||||||
form.Set("external_auth_cache_time", "a")
|
form.Set("external_auth_cache_time", "a")
|
||||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||||
|
@ -19850,30 +19746,6 @@ func TestWebUserAddMock(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if assert.Len(t, newUser.Filters.DataTransferLimits, 3) {
|
|
||||||
for _, dtLimit := range newUser.Filters.DataTransferLimits {
|
|
||||||
switch len(dtLimit.Sources) {
|
|
||||||
case 3:
|
|
||||||
assert.Equal(t, "192.168.3.0/24", dtLimit.Sources[0])
|
|
||||||
assert.Equal(t, "10.8.2.0/24", dtLimit.Sources[1])
|
|
||||||
assert.Equal(t, "::1/64", dtLimit.Sources[2])
|
|
||||||
assert.Equal(t, int64(0), dtLimit.UploadDataTransfer)
|
|
||||||
assert.Equal(t, int64(0), dtLimit.DownloadDataTransfer)
|
|
||||||
assert.Equal(t, int64(200), dtLimit.TotalDataTransfer)
|
|
||||||
case 2:
|
|
||||||
assert.Equal(t, "192.168.5.0/24", dtLimit.Sources[0])
|
|
||||||
assert.Equal(t, "10.8.0.0/16", dtLimit.Sources[1])
|
|
||||||
assert.Equal(t, int64(120), dtLimit.UploadDataTransfer)
|
|
||||||
assert.Equal(t, int64(100), dtLimit.DownloadDataTransfer)
|
|
||||||
assert.Equal(t, int64(0), dtLimit.TotalDataTransfer)
|
|
||||||
case 1:
|
|
||||||
assert.Equal(t, "127.0.1.1/32", dtLimit.Sources[0])
|
|
||||||
assert.Equal(t, int64(0), dtLimit.UploadDataTransfer)
|
|
||||||
assert.Equal(t, int64(0), dtLimit.DownloadDataTransfer)
|
|
||||||
assert.Equal(t, int64(0), dtLimit.TotalDataTransfer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.Len(t, newUser.Groups, 3)
|
assert.Len(t, newUser.Groups, 3)
|
||||||
assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
|
assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
|
||||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)
|
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)
|
||||||
|
@ -22886,6 +22758,72 @@ func TestWebRole(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNameParamSingleSlash(t *testing.T) {
|
||||||
|
err := dataprovider.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = config.LoadConfig(configDir, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
providerConf := config.GetProviderConf()
|
||||||
|
providerConf.NamingRules = 5
|
||||||
|
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
group := getTestGroup()
|
||||||
|
group.Name = "/"
|
||||||
|
form := make(url.Values)
|
||||||
|
form.Set("name", group.Name)
|
||||||
|
form.Set("description", group.Description)
|
||||||
|
form.Set("max_sessions", "0")
|
||||||
|
form.Set("quota_files", "0")
|
||||||
|
form.Set("quota_size", "0")
|
||||||
|
form.Set("upload_bandwidth", "0")
|
||||||
|
form.Set("download_bandwidth", "0")
|
||||||
|
form.Set("upload_data_transfer", "0")
|
||||||
|
form.Set("download_data_transfer", "0")
|
||||||
|
form.Set("total_data_transfer", "0")
|
||||||
|
form.Set("max_upload_file_size", "0")
|
||||||
|
form.Set("default_shares_expiration", "0")
|
||||||
|
form.Set("max_shares_expiration", "0")
|
||||||
|
form.Set("password_expiration", "0")
|
||||||
|
form.Set("password_strength", "0")
|
||||||
|
form.Set("expires_in", "0")
|
||||||
|
form.Set("external_auth_cache_time", "0")
|
||||||
|
form.Set(csrfFormToken, csrfToken)
|
||||||
|
b, contentType, err := getMultipartFormData(form, "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req, err := http.NewRequest(http.MethodPost, webGroupPath, &b)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
setJWTCookieForReq(req, webToken)
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||||
|
|
||||||
|
groupGet, _, err := httpdtest.GetGroupByName(group.Name, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "/", groupGet.Name)
|
||||||
|
// cleanup
|
||||||
|
req, err = http.NewRequest(http.MethodDelete, groupPath+"/"+url.PathEscape(group.Name), nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setBearerForReq(req, apiToken)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
|
|
||||||
|
err = dataprovider.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = config.LoadConfig(configDir, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
providerConf = config.GetProviderConf()
|
||||||
|
providerConf.BackupsPath = backupsPath
|
||||||
|
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAddWebGroup(t *testing.T) {
|
func TestAddWebGroup(t *testing.T) {
|
||||||
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -24406,7 +24344,7 @@ func TestStaticFilesMock(t *testing.T) {
|
||||||
req, err = http.NewRequest(http.MethodGet, location, nil)
|
req, err = http.NewRequest(http.MethodGet, location, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusOK, rr)
|
checkResponseCode(t, http.StatusForbidden, rr)
|
||||||
|
|
||||||
req, err = http.NewRequest(http.MethodGet, "/openapi", nil)
|
req, err = http.NewRequest(http.MethodGet, "/openapi", nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -1991,13 +1991,38 @@ func TestJWTTokenCleanup(t *testing.T) {
|
||||||
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
|
||||||
|
|
||||||
invalidatedJWTTokens.Store(token, time.Now().Add(-tokenDuration).UTC())
|
invalidatedJWTTokens.Add(token, time.Now().Add(-tokenDuration).UTC())
|
||||||
require.True(t, isTokenInvalidated(req))
|
require.True(t, isTokenInvalidated(req))
|
||||||
startCleanupTicker(100 * time.Millisecond)
|
startCleanupTicker(100 * time.Millisecond)
|
||||||
assert.Eventually(t, func() bool { return !isTokenInvalidated(req) }, 1*time.Second, 200*time.Millisecond)
|
assert.Eventually(t, func() bool { return !isTokenInvalidated(req) }, 1*time.Second, 200*time.Millisecond)
|
||||||
stopCleanupTicker()
|
stopCleanupTicker()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDbTokenManager(t *testing.T) {
|
||||||
|
if !isSharedProviderSupported() {
|
||||||
|
t.Skip("this test it is not available with this provider")
|
||||||
|
}
|
||||||
|
mgr := newTokenManager(1)
|
||||||
|
dbTokenManager := mgr.(*dbTokenManager)
|
||||||
|
testToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiV2ViQWRtaW4iLCI6OjEiXSwiZXhwIjoxNjk4NjYwMDM4LCJqdGkiOiJja3ZuazVrYjF1aHUzZXRmZmhyZyIsIm5iZiI6MTY5ODY1ODgwOCwicGVybWlzc2lvbnMiOlsiKiJdLCJzdWIiOiIxNjk3ODIwNDM3NTMyIiwidXNlcm5hbWUiOiJhZG1pbiJ9.LXuFFksvnSuzHqHat6r70yR0jEulNRju7m7SaWrOfy8; csrftoken=mP0C7DqjwpAXsptO2gGCaYBkYw3oNMWB"
|
||||||
|
key := dbTokenManager.getKey(testToken)
|
||||||
|
require.Len(t, key, 64)
|
||||||
|
dbTokenManager.Add(testToken, time.Now().Add(-tokenDuration).UTC())
|
||||||
|
isInvalidated := dbTokenManager.Get(testToken)
|
||||||
|
assert.True(t, isInvalidated)
|
||||||
|
dbTokenManager.Cleanup()
|
||||||
|
isInvalidated = dbTokenManager.Get(testToken)
|
||||||
|
assert.False(t, isInvalidated)
|
||||||
|
dbTokenManager.Add(testToken, time.Now().Add(tokenDuration).UTC())
|
||||||
|
isInvalidated = dbTokenManager.Get(testToken)
|
||||||
|
assert.True(t, isInvalidated)
|
||||||
|
dbTokenManager.Cleanup()
|
||||||
|
isInvalidated = dbTokenManager.Get(testToken)
|
||||||
|
assert.True(t, isInvalidated)
|
||||||
|
err := dataprovider.DeleteSharedSession(key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAllowedProxyUnixDomainSocket(t *testing.T) {
|
func TestAllowedProxyUnixDomainSocket(t *testing.T) {
|
||||||
b := Binding{
|
b := Binding{
|
||||||
Address: filepath.Join(os.TempDir(), "sock"),
|
Address: filepath.Join(os.TempDir(), "sock"),
|
||||||
|
@ -3365,6 +3390,92 @@ func TestGetLogEventString(t *testing.T) {
|
||||||
assert.Empty(t, getLogEventString(0))
|
assert.Empty(t, getLogEventString(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserQuotaUsage(t *testing.T) {
|
||||||
|
usage := userQuotaUsage{
|
||||||
|
QuotaSize: 100,
|
||||||
|
}
|
||||||
|
require.True(t, usage.HasQuotaInfo())
|
||||||
|
require.NotEmpty(t, usage.GetQuotaSize())
|
||||||
|
providerConf := dataprovider.GetProviderConfig()
|
||||||
|
quotaTracking := dataprovider.GetQuotaTracking()
|
||||||
|
providerConf.TrackQuota = 0
|
||||||
|
err := dataprovider.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = dataprovider.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, usage.HasQuotaInfo())
|
||||||
|
providerConf.TrackQuota = quotaTracking
|
||||||
|
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
usage.QuotaSize = 0
|
||||||
|
assert.False(t, usage.HasQuotaInfo())
|
||||||
|
assert.Empty(t, usage.GetQuotaSize())
|
||||||
|
assert.Equal(t, 0, usage.GetQuotaSizePercentage())
|
||||||
|
assert.False(t, usage.IsQuotaSizeLow())
|
||||||
|
assert.False(t, usage.IsDiskQuotaLow())
|
||||||
|
assert.False(t, usage.IsQuotaLow())
|
||||||
|
usage.UsedQuotaSize = 9
|
||||||
|
assert.NotEmpty(t, usage.GetQuotaSize())
|
||||||
|
usage.QuotaSize = 10
|
||||||
|
assert.True(t, usage.IsQuotaSizeLow())
|
||||||
|
assert.True(t, usage.IsDiskQuotaLow())
|
||||||
|
assert.True(t, usage.IsQuotaLow())
|
||||||
|
usage.DownloadDataTransfer = 1
|
||||||
|
assert.True(t, usage.HasQuotaInfo())
|
||||||
|
assert.True(t, usage.HasTranferQuota())
|
||||||
|
assert.Empty(t, usage.GetQuotaFiles())
|
||||||
|
assert.Equal(t, 0, usage.GetQuotaFilesPercentage())
|
||||||
|
usage.QuotaFiles = 1
|
||||||
|
assert.NotEmpty(t, usage.GetQuotaFiles())
|
||||||
|
usage.QuotaFiles = 0
|
||||||
|
usage.UsedQuotaFiles = 9
|
||||||
|
assert.NotEmpty(t, usage.GetQuotaFiles())
|
||||||
|
usage.QuotaFiles = 10
|
||||||
|
usage.DownloadDataTransfer = 0
|
||||||
|
assert.True(t, usage.IsQuotaFilesLow())
|
||||||
|
assert.True(t, usage.IsDiskQuotaLow())
|
||||||
|
assert.False(t, usage.IsTotalTransferQuotaLow())
|
||||||
|
assert.False(t, usage.IsUploadTransferQuotaLow())
|
||||||
|
assert.False(t, usage.IsDownloadTransferQuotaLow())
|
||||||
|
assert.Equal(t, 0, usage.GetTotalTransferQuotaPercentage())
|
||||||
|
assert.Equal(t, 0, usage.GetUploadTransferQuotaPercentage())
|
||||||
|
assert.Equal(t, 0, usage.GetDownloadTransferQuotaPercentage())
|
||||||
|
assert.Empty(t, usage.GetTotalTransferQuota())
|
||||||
|
assert.Empty(t, usage.GetUploadTransferQuota())
|
||||||
|
assert.Empty(t, usage.GetDownloadTransferQuota())
|
||||||
|
usage.TotalDataTransfer = 3
|
||||||
|
usage.UsedUploadDataTransfer = 1 * 1048576
|
||||||
|
assert.NotEmpty(t, usage.GetTotalTransferQuota())
|
||||||
|
usage.TotalDataTransfer = 0
|
||||||
|
assert.NotEmpty(t, usage.GetTotalTransferQuota())
|
||||||
|
assert.NotEmpty(t, usage.GetUploadTransferQuota())
|
||||||
|
usage.UploadDataTransfer = 2
|
||||||
|
assert.NotEmpty(t, usage.GetUploadTransferQuota())
|
||||||
|
usage.UsedDownloadDataTransfer = 1 * 1048576
|
||||||
|
assert.NotEmpty(t, usage.GetDownloadTransferQuota())
|
||||||
|
usage.DownloadDataTransfer = 2
|
||||||
|
assert.NotEmpty(t, usage.GetDownloadTransferQuota())
|
||||||
|
assert.False(t, usage.IsTransferQuotaLow())
|
||||||
|
usage.UsedDownloadDataTransfer = 8 * 1048576
|
||||||
|
usage.TotalDataTransfer = 10
|
||||||
|
assert.True(t, usage.IsTotalTransferQuotaLow())
|
||||||
|
assert.True(t, usage.IsTransferQuotaLow())
|
||||||
|
usage.TotalDataTransfer = 0
|
||||||
|
usage.UploadDataTransfer = 0
|
||||||
|
usage.DownloadDataTransfer = 0
|
||||||
|
assert.False(t, usage.IsTransferQuotaLow())
|
||||||
|
usage.UploadDataTransfer = 10
|
||||||
|
usage.UsedUploadDataTransfer = 9 * 1048576
|
||||||
|
assert.True(t, usage.IsUploadTransferQuotaLow())
|
||||||
|
assert.True(t, usage.IsTransferQuotaLow())
|
||||||
|
usage.DownloadDataTransfer = 10
|
||||||
|
usage.UsedDownloadDataTransfer = 9 * 1048576
|
||||||
|
assert.True(t, usage.IsDownloadTransferQuotaLow())
|
||||||
|
assert.True(t, usage.IsTransferQuotaLow())
|
||||||
|
}
|
||||||
|
|
||||||
func isSharedProviderSupported() bool {
|
func isSharedProviderSupported() bool {
|
||||||
// SQLite shares the implementation with other SQL-based provider but it makes no sense
|
// SQLite shares the implementation with other SQL-based provider but it makes no sense
|
||||||
// to use it outside test cases
|
// to use it outside test cases
|
||||||
|
|
|
@ -384,6 +384,13 @@ func checkAPIKeyAuth(tokenAuth *jwtauth.JWTAuth, scope dataprovider.APIKeyScope)
|
||||||
sendAPIResponse(w, r, errors.New("the provided api key is not valid"), "", http.StatusBadRequest)
|
sendAPIResponse(w, r, errors.New("the provided api key is not valid"), "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if k.Scope != scope {
|
||||||
|
handleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), dataprovider.ErrInvalidCredentials) //nolint:errcheck
|
||||||
|
logger.Debug(logSender, "", "unable to authenticate api key %q: invalid scope: got %d, wanted: %d",
|
||||||
|
apiKey, k.Scope, scope)
|
||||||
|
sendAPIResponse(w, r, fmt.Errorf("the provided api key is invalid for this request"), "", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := k.Authenticate(key); err != nil {
|
if err := k.Authenticate(key); err != nil {
|
||||||
handleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), dataprovider.ErrInvalidCredentials) //nolint:errcheck
|
handleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), dataprovider.ErrInvalidCredentials) //nolint:errcheck
|
||||||
logger.Debug(logSender, "", "unable to authenticate api key %q: %v", apiKey, err)
|
logger.Debug(logSender, "", "unable to authenticate api key %q: %v", apiKey, err)
|
||||||
|
@ -546,3 +553,14 @@ func checkPartialAuth(w http.ResponseWriter, r *http.Request, audience string, t
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func neuter(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/") {
|
||||||
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -105,7 +105,6 @@ func (o *memoryOAuth2Manager) getPendingAuth(state string) (oauth2PendingAuth, e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *memoryOAuth2Manager) cleanup() {
|
func (o *memoryOAuth2Manager) cleanup() {
|
||||||
logger.Debug(logSender, "", "oauth2 manager cleanup")
|
|
||||||
o.mu.Lock()
|
o.mu.Lock()
|
||||||
defer o.mu.Unlock()
|
defer o.mu.Unlock()
|
||||||
|
|
||||||
|
@ -165,6 +164,5 @@ func (o *dbOAuth2Manager) decodePendingAuthData(data any) (oauth2PendingAuth, er
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *dbOAuth2Manager) cleanup() {
|
func (o *dbOAuth2Manager) cleanup() {
|
||||||
logger.Debug(logSender, "", "oauth2 manager cleanup")
|
|
||||||
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOAuth2Auth, time.Now()) //nolint:errcheck
|
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOAuth2Auth, time.Now()) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,6 @@ func (o *memoryOIDCManager) updateTokenUsage(token oidcToken) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *memoryOIDCManager) cleanup() {
|
func (o *memoryOIDCManager) cleanup() {
|
||||||
logger.Debug(logSender, "", "oidc manager cleanup")
|
|
||||||
o.cleanupAuthRequests()
|
o.cleanupAuthRequests()
|
||||||
o.cleanupTokens()
|
o.cleanupTokens()
|
||||||
}
|
}
|
||||||
|
@ -238,7 +237,6 @@ func (o *dbOIDCManager) decodeTokenData(data any) (oidcToken, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *dbOIDCManager) cleanup() {
|
func (o *dbOIDCManager) cleanup() {
|
||||||
logger.Debug(logSender, "", "oidc manager cleanup")
|
|
||||||
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCAuth, time.Now()) //nolint:errcheck
|
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCAuth, time.Now()) //nolint:errcheck
|
||||||
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCToken, time.Now()) //nolint:errcheck
|
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCToken, time.Now()) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,6 @@ import (
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveStaticDir(router chi.Router, path, fsDirPath string) {
|
func serveStaticDir(router chi.Router, path, fsDirPath string, disableDirectoryIndex bool) {
|
||||||
fileServer(router, path, http.Dir(fsDirPath))
|
fileServer(router, path, http.Dir(fsDirPath), disableDirectoryIndex)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,16 +18,20 @@
|
||||||
package httpd
|
package httpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/internal/bundle"
|
"github.com/drakkan/sftpgo/v2/internal/bundle"
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveStaticDir(router chi.Router, path, _ string) {
|
func serveStaticDir(router chi.Router, path, fsDirPath string, disableDirectoryIndex bool) {
|
||||||
switch path {
|
switch path {
|
||||||
case webStaticFilesPath:
|
case webStaticFilesPath:
|
||||||
fileServer(router, path, bundle.GetStaticFs())
|
fileServer(router, path, bundle.GetStaticFs(), disableDirectoryIndex)
|
||||||
case webOpenAPIPath:
|
case webOpenAPIPath:
|
||||||
fileServer(router, path, bundle.GetOpenAPIFs())
|
fileServer(router, path, bundle.GetOpenAPIFs(), disableDirectoryIndex)
|
||||||
|
default:
|
||||||
|
fileServer(router, path, http.Dir(fsDirPath), disableDirectoryIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1046,7 +1046,7 @@ func (s *httpdServer) updateContextFromCookie(r *http.Request) *http.Request {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
|
func (s *httpdServer) parseHeaders(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||||
var ip net.IP
|
var ip net.IP
|
||||||
|
@ -1078,6 +1078,13 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||||
common.Connections.AddClientConnection(ipAddr)
|
common.Connections.AddClientConnection(ipAddr)
|
||||||
defer common.Connections.RemoveClientConnection(ipAddr)
|
defer common.Connections.RemoveClientConnection(ipAddr)
|
||||||
|
|
||||||
|
@ -1087,7 +1094,7 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if common.IsBanned(ipAddr, common.ProtocolHTTP) {
|
if common.IsBanned(ipAddr, common.ProtocolHTTP) {
|
||||||
s.sendForbiddenResponse(w, r, "your IP address is banned")
|
s.sendForbiddenResponse(w, r, "your IP address is blocked")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if delay, err := common.LimitRate(common.ProtocolHTTP, ipAddr); err != nil {
|
if delay, err := common.LimitRate(common.ProtocolHTTP, ipAddr); err != nil {
|
||||||
|
@ -1165,7 +1172,9 @@ func (s *httpdServer) redirectToWebPath(w http.ResponseWriter, r *http.Request,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) isStaticFileURL(r *http.Request) bool {
|
// The StripSlashes causes infinite redirects at the root path if used with http.FileServer.
|
||||||
|
// We also don't strip paths with more than one trailing slash, see #1434
|
||||||
|
func (s *httpdServer) mustStripSlash(r *http.Request) bool {
|
||||||
var urlPath string
|
var urlPath string
|
||||||
rctx := chi.RouteContext(r.Context())
|
rctx := chi.RouteContext(r.Context())
|
||||||
if rctx != nil && rctx.RoutePath != "" {
|
if rctx != nil && rctx.RoutePath != "" {
|
||||||
|
@ -1173,7 +1182,8 @@ func (s *httpdServer) isStaticFileURL(r *http.Request) bool {
|
||||||
} else {
|
} else {
|
||||||
urlPath = r.URL.Path
|
urlPath = r.URL.Path
|
||||||
}
|
}
|
||||||
return !strings.HasPrefix(urlPath, webOpenAPIPath) && !strings.HasPrefix(urlPath, webStaticFilesPath)
|
return !strings.HasSuffix(urlPath, "//") && !strings.HasPrefix(urlPath, webOpenAPIPath) &&
|
||||||
|
!strings.HasPrefix(urlPath, webStaticFilesPath) && !strings.HasPrefix(urlPath, acmeChallengeURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) initializeRouter() {
|
func (s *httpdServer) initializeRouter() {
|
||||||
|
@ -1182,9 +1192,10 @@ func (s *httpdServer) initializeRouter() {
|
||||||
s.router = chi.NewRouter()
|
s.router = chi.NewRouter()
|
||||||
|
|
||||||
s.router.Use(middleware.RequestID)
|
s.router.Use(middleware.RequestID)
|
||||||
s.router.Use(s.checkConnection)
|
s.router.Use(s.parseHeaders)
|
||||||
s.router.Use(logger.NewStructuredLogger(logger.GetLogger()))
|
s.router.Use(logger.NewStructuredLogger(logger.GetLogger()))
|
||||||
s.router.Use(middleware.Recoverer)
|
s.router.Use(middleware.Recoverer)
|
||||||
|
s.router.Use(s.checkConnection)
|
||||||
if s.binding.Security.Enabled {
|
if s.binding.Security.Enabled {
|
||||||
secureMiddleware := secure.New(secure.Options{
|
secureMiddleware := secure.New(secure.Options{
|
||||||
AllowedHosts: s.binding.Security.AllowedHosts,
|
AllowedHosts: s.binding.Security.AllowedHosts,
|
||||||
|
@ -1223,7 +1234,7 @@ func (s *httpdServer) initializeRouter() {
|
||||||
}
|
}
|
||||||
s.router.Use(middleware.GetHead)
|
s.router.Use(middleware.GetHead)
|
||||||
// StripSlashes causes infinite redirects at the root path if used with http.FileServer
|
// StripSlashes causes infinite redirects at the root path if used with http.FileServer
|
||||||
s.router.Use(middleware.Maybe(middleware.StripSlashes, s.isStaticFileURL))
|
s.router.Use(middleware.Maybe(middleware.StripSlashes, s.mustStripSlash))
|
||||||
|
|
||||||
s.router.NotFound(s.notFoundHandler)
|
s.router.NotFound(s.notFoundHandler)
|
||||||
|
|
||||||
|
@ -1237,7 +1248,7 @@ func (s *httpdServer) initializeRouter() {
|
||||||
|
|
||||||
if hasHTTPSRedirect {
|
if hasHTTPSRedirect {
|
||||||
if p := acme.GetHTTP01WebRoot(); p != "" {
|
if p := acme.GetHTTP01WebRoot(); p != "" {
|
||||||
serveStaticDir(s.router, acmeChallengeURI, p)
|
serveStaticDir(s.router, acmeChallengeURI, p, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1434,7 +1445,7 @@ func (s *httpdServer) initializeRouter() {
|
||||||
if s.renderOpenAPI {
|
if s.renderOpenAPI {
|
||||||
s.router.Group(func(router chi.Router) {
|
s.router.Group(func(router chi.Router) {
|
||||||
router.Use(compressor.Handler)
|
router.Use(compressor.Handler)
|
||||||
serveStaticDir(router, webOpenAPIPath, s.openAPIPath)
|
serveStaticDir(router, webOpenAPIPath, s.openAPIPath, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1442,7 +1453,7 @@ func (s *httpdServer) initializeRouter() {
|
||||||
if s.enableWebAdmin || s.enableWebClient {
|
if s.enableWebAdmin || s.enableWebClient {
|
||||||
s.router.Group(func(router chi.Router) {
|
s.router.Group(func(router chi.Router) {
|
||||||
router.Use(compressor.Handler)
|
router.Use(compressor.Handler)
|
||||||
serveStaticDir(router, webStaticFilesPath, s.staticFilesPath)
|
serveStaticDir(router, webStaticFilesPath, s.staticFilesPath, true)
|
||||||
})
|
})
|
||||||
if s.binding.OIDC.isEnabled() {
|
if s.binding.OIDC.isEnabled() {
|
||||||
s.router.Get(webOIDCRedirectPath, s.handleOIDCRedirect)
|
s.router.Get(webOIDCRedirectPath, s.handleOIDCRedirect)
|
||||||
|
|
95
internal/httpd/token.go
Normal file
95
internal/httpd/token.go
Normal 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
|
||||||
|
}
|
|
@ -122,7 +122,7 @@ const (
|
||||||
pageForgotPwdTitle = "SFTPGo Admin - Forgot password"
|
pageForgotPwdTitle = "SFTPGo Admin - Forgot password"
|
||||||
pageResetPwdTitle = "SFTPGo Admin - Reset password"
|
pageResetPwdTitle = "SFTPGo Admin - Reset password"
|
||||||
pageSetupTitle = "Create first admin user"
|
pageSetupTitle = "Create first admin user"
|
||||||
defaultQueryLimit = 500
|
defaultQueryLimit = 1000
|
||||||
inversePatternType = "inverse"
|
inversePatternType = "inverse"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1353,50 +1353,6 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
|
||||||
return permissions
|
return permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDataTransferLimitsFromPostFields(r *http.Request) ([]sdk.DataTransferLimit, error) {
|
|
||||||
var result []sdk.DataTransferLimit
|
|
||||||
|
|
||||||
for k := range r.Form {
|
|
||||||
if strings.HasPrefix(k, "data_transfer_limit_sources") {
|
|
||||||
sources := getSliceFromDelimitedValues(r.Form.Get(k), ",")
|
|
||||||
if len(sources) > 0 {
|
|
||||||
dtLimit := sdk.DataTransferLimit{
|
|
||||||
Sources: sources,
|
|
||||||
}
|
|
||||||
idx := strings.TrimPrefix(k, "data_transfer_limit_sources")
|
|
||||||
ul := r.Form.Get(fmt.Sprintf("upload_data_transfer_source%v", idx))
|
|
||||||
dl := r.Form.Get(fmt.Sprintf("download_data_transfer_source%v", idx))
|
|
||||||
total := r.Form.Get(fmt.Sprintf("total_data_transfer_source%v", idx))
|
|
||||||
if ul != "" {
|
|
||||||
dataUL, err := strconv.ParseInt(ul, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return result, fmt.Errorf("invalid upload_data_transfer_source%v %q: %w", idx, ul, err)
|
|
||||||
}
|
|
||||||
dtLimit.UploadDataTransfer = dataUL
|
|
||||||
}
|
|
||||||
if dl != "" {
|
|
||||||
dataDL, err := strconv.ParseInt(dl, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return result, fmt.Errorf("invalid download_data_transfer_source%v %q: %w", idx, dl, err)
|
|
||||||
}
|
|
||||||
dtLimit.DownloadDataTransfer = dataDL
|
|
||||||
}
|
|
||||||
if total != "" {
|
|
||||||
dataTotal, err := strconv.ParseInt(total, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return result, fmt.Errorf("invalid total_data_transfer_source%v %q: %w", idx, total, err)
|
|
||||||
}
|
|
||||||
dtLimit.TotalDataTransfer = dataTotal
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, dtLimit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) {
|
func getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) {
|
||||||
var result []sdk.BandwidthLimit
|
var result []sdk.BandwidthLimit
|
||||||
|
|
||||||
|
@ -1534,10 +1490,6 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filters, err
|
return filters, err
|
||||||
}
|
}
|
||||||
dtLimits, err := getDataTransferLimitsFromPostFields(r)
|
|
||||||
if err != nil {
|
|
||||||
return filters, err
|
|
||||||
}
|
|
||||||
maxFileSize, err := util.ParseBytes(r.Form.Get("max_upload_file_size"))
|
maxFileSize, err := util.ParseBytes(r.Form.Get("max_upload_file_size"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filters, fmt.Errorf("invalid max upload file size: %w", err)
|
return filters, fmt.Errorf("invalid max upload file size: %w", err)
|
||||||
|
@ -1558,7 +1510,6 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
||||||
filters.FTPSecurity = 1
|
filters.FTPSecurity = 1
|
||||||
}
|
}
|
||||||
filters.BandwidthLimits = bwLimits
|
filters.BandwidthLimits = bwLimits
|
||||||
filters.DataTransferLimits = dtLimits
|
|
||||||
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
||||||
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
|
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
|
||||||
filters.DeniedLoginMethods = r.Form["denied_login_methods"]
|
filters.DeniedLoginMethods = r.Form["denied_login_methods"]
|
||||||
|
@ -1614,7 +1565,7 @@ func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
|
||||||
config.Endpoint = strings.TrimSpace(r.Form.Get("s3_endpoint"))
|
config.Endpoint = strings.TrimSpace(r.Form.Get("s3_endpoint"))
|
||||||
config.StorageClass = strings.TrimSpace(r.Form.Get("s3_storage_class"))
|
config.StorageClass = strings.TrimSpace(r.Form.Get("s3_storage_class"))
|
||||||
config.ACL = strings.TrimSpace(r.Form.Get("s3_acl"))
|
config.ACL = strings.TrimSpace(r.Form.Get("s3_acl"))
|
||||||
config.KeyPrefix = strings.TrimSpace(r.Form.Get("s3_key_prefix"))
|
config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("s3_key_prefix"), "/"))
|
||||||
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
|
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config, fmt.Errorf("invalid s3 upload part size: %w", err)
|
return config, fmt.Errorf("invalid s3 upload part size: %w", err)
|
||||||
|
@ -1650,7 +1601,7 @@ func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
|
||||||
config.Bucket = strings.TrimSpace(r.Form.Get("gcs_bucket"))
|
config.Bucket = strings.TrimSpace(r.Form.Get("gcs_bucket"))
|
||||||
config.StorageClass = strings.TrimSpace(r.Form.Get("gcs_storage_class"))
|
config.StorageClass = strings.TrimSpace(r.Form.Get("gcs_storage_class"))
|
||||||
config.ACL = strings.TrimSpace(r.Form.Get("gcs_acl"))
|
config.ACL = strings.TrimSpace(r.Form.Get("gcs_acl"))
|
||||||
config.KeyPrefix = strings.TrimSpace(r.Form.Get("gcs_key_prefix"))
|
config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("gcs_key_prefix"), "/"))
|
||||||
uploadPartSize, err := strconv.ParseInt(r.Form.Get("gcs_upload_part_size"), 10, 64)
|
uploadPartSize, err := strconv.ParseInt(r.Form.Get("gcs_upload_part_size"), 10, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
config.UploadPartSize = uploadPartSize
|
config.UploadPartSize = uploadPartSize
|
||||||
|
@ -1732,7 +1683,7 @@ func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
|
||||||
config.AccountKey = getSecretFromFormField(r, "az_account_key")
|
config.AccountKey = getSecretFromFormField(r, "az_account_key")
|
||||||
config.SASURL = getSecretFromFormField(r, "az_sas_url")
|
config.SASURL = getSecretFromFormField(r, "az_sas_url")
|
||||||
config.Endpoint = strings.TrimSpace(r.Form.Get("az_endpoint"))
|
config.Endpoint = strings.TrimSpace(r.Form.Get("az_endpoint"))
|
||||||
config.KeyPrefix = strings.TrimSpace(r.Form.Get("az_key_prefix"))
|
config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("az_key_prefix"), "/"))
|
||||||
config.AccessTier = strings.TrimSpace(r.Form.Get("az_access_tier"))
|
config.AccessTier = strings.TrimSpace(r.Form.Get("az_access_tier"))
|
||||||
config.UseEmulator = r.Form.Get("az_use_emulator") != ""
|
config.UseEmulator = r.Form.Get("az_use_emulator") != ""
|
||||||
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
|
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
|
||||||
|
@ -4218,7 +4169,7 @@ func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.R
|
||||||
errTxt := "the OAuth2 provider returned an empty token. " +
|
errTxt := "the OAuth2 provider returned an empty token. " +
|
||||||
"Some providers only return the token when the user first authorizes. " +
|
"Some providers only return the token when the user first authorizes. " +
|
||||||
"If you have already registered SFTPGo with this user in the past, revoke access and try again. " +
|
"If you have already registered SFTPGo with this user in the past, revoke access and try again. " +
|
||||||
"This way you will invalidate the previous token."
|
"This way you will invalidate the previous token"
|
||||||
s.renderMessagePage(w, r, errorTitle, "Unable to get token:", http.StatusBadRequest, errors.New(errTxt), "")
|
s.renderMessagePage(w, r, errorTitle, "Unable to get token:", http.StatusBadRequest, errors.New(errTxt), "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -155,6 +156,7 @@ type filesPage struct {
|
||||||
Error string
|
Error string
|
||||||
Paths []dirMapping
|
Paths []dirMapping
|
||||||
HasIntegrations bool
|
HasIntegrations bool
|
||||||
|
QuotaUsage *userQuotaUsage
|
||||||
}
|
}
|
||||||
|
|
||||||
type shareLoginPage struct {
|
type shareLoginPage struct {
|
||||||
|
@ -229,6 +231,185 @@ type clientSharePage struct {
|
||||||
IsAdd bool
|
IsAdd bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type userQuotaUsage struct {
|
||||||
|
QuotaSize int64
|
||||||
|
QuotaFiles int
|
||||||
|
UsedQuotaSize int64
|
||||||
|
UsedQuotaFiles int
|
||||||
|
UploadDataTransfer int64
|
||||||
|
DownloadDataTransfer int64
|
||||||
|
TotalDataTransfer int64
|
||||||
|
UsedUploadDataTransfer int64
|
||||||
|
UsedDownloadDataTransfer int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) HasQuotaInfo() bool {
|
||||||
|
if dataprovider.GetQuotaTracking() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if u.HasDiskQuota() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return u.HasTranferQuota()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) HasDiskQuota() bool {
|
||||||
|
if u.QuotaSize > 0 || u.UsedQuotaSize > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return u.QuotaFiles > 0 || u.UsedQuotaFiles > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) HasTranferQuota() bool {
|
||||||
|
if u.TotalDataTransfer > 0 || u.UploadDataTransfer > 0 || u.DownloadDataTransfer > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return u.UsedDownloadDataTransfer > 0 || u.UsedUploadDataTransfer > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) GetQuotaSize() string {
|
||||||
|
if u.QuotaSize > 0 {
|
||||||
|
return fmt.Sprintf("%s/%s", util.ByteCountIEC(u.UsedQuotaSize), util.ByteCountIEC(u.QuotaSize))
|
||||||
|
}
|
||||||
|
if u.UsedQuotaSize > 0 {
|
||||||
|
return util.ByteCountIEC(u.UsedQuotaSize)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) GetQuotaFiles() string {
|
||||||
|
if u.QuotaFiles > 0 {
|
||||||
|
return fmt.Sprintf("%d/%d", u.UsedQuotaFiles, u.QuotaFiles)
|
||||||
|
}
|
||||||
|
if u.UsedQuotaFiles > 0 {
|
||||||
|
return strconv.FormatInt(int64(u.UsedQuotaFiles), 10)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) GetQuotaSizePercentage() int {
|
||||||
|
if u.QuotaSize > 0 {
|
||||||
|
return int(math.Round(100 * float64(u.UsedQuotaSize) / float64(u.QuotaSize)))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) GetQuotaFilesPercentage() int {
|
||||||
|
if u.QuotaFiles > 0 {
|
||||||
|
return int(math.Round(100 * float64(u.UsedQuotaFiles) / float64(u.QuotaFiles)))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) IsQuotaSizeLow() bool {
|
||||||
|
return u.GetQuotaSizePercentage() > 85
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) IsQuotaFilesLow() bool {
|
||||||
|
return u.GetQuotaFilesPercentage() > 85
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) IsDiskQuotaLow() bool {
|
||||||
|
return u.IsQuotaSizeLow() || u.IsQuotaFilesLow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) GetTotalTransferQuota() string {
|
||||||
|
total := u.UsedUploadDataTransfer + u.UsedDownloadDataTransfer
|
||||||
|
if u.TotalDataTransfer > 0 {
|
||||||
|
return fmt.Sprintf("%s/%s", util.ByteCountIEC(total), util.ByteCountIEC(u.TotalDataTransfer*1048576))
|
||||||
|
}
|
||||||
|
if total > 0 {
|
||||||
|
return util.ByteCountIEC(total)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) GetUploadTransferQuota() string {
|
||||||
|
if u.UploadDataTransfer > 0 {
|
||||||
|
return fmt.Sprintf("%s/%s", util.ByteCountIEC(u.UsedUploadDataTransfer),
|
||||||
|
util.ByteCountIEC(u.UploadDataTransfer*1048576))
|
||||||
|
}
|
||||||
|
if u.UsedUploadDataTransfer > 0 {
|
||||||
|
return util.ByteCountIEC(u.UsedUploadDataTransfer)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) GetDownloadTransferQuota() string {
|
||||||
|
if u.DownloadDataTransfer > 0 {
|
||||||
|
return fmt.Sprintf("%s/%s", util.ByteCountIEC(u.UsedDownloadDataTransfer),
|
||||||
|
util.ByteCountIEC(u.DownloadDataTransfer*1048576))
|
||||||
|
}
|
||||||
|
if u.UsedDownloadDataTransfer > 0 {
|
||||||
|
return util.ByteCountIEC(u.UsedDownloadDataTransfer)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) GetTotalTransferQuotaPercentage() int {
|
||||||
|
if u.TotalDataTransfer > 0 {
|
||||||
|
return int(math.Round(100 * float64(u.UsedDownloadDataTransfer+u.UsedUploadDataTransfer) / float64(u.TotalDataTransfer*1048576)))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) GetUploadTransferQuotaPercentage() int {
|
||||||
|
if u.UploadDataTransfer > 0 {
|
||||||
|
return int(math.Round(100 * float64(u.UsedUploadDataTransfer) / float64(u.UploadDataTransfer*1048576)))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) GetDownloadTransferQuotaPercentage() int {
|
||||||
|
if u.DownloadDataTransfer > 0 {
|
||||||
|
return int(math.Round(100 * float64(u.UsedDownloadDataTransfer) / float64(u.DownloadDataTransfer*1048576)))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) IsTotalTransferQuotaLow() bool {
|
||||||
|
if u.TotalDataTransfer > 0 {
|
||||||
|
return u.GetTotalTransferQuotaPercentage() > 85
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) IsUploadTransferQuotaLow() bool {
|
||||||
|
if u.UploadDataTransfer > 0 {
|
||||||
|
return u.GetUploadTransferQuotaPercentage() > 85
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) IsDownloadTransferQuotaLow() bool {
|
||||||
|
if u.DownloadDataTransfer > 0 {
|
||||||
|
return u.GetDownloadTransferQuotaPercentage() > 85
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) IsTransferQuotaLow() bool {
|
||||||
|
return u.IsTotalTransferQuotaLow() || u.IsUploadTransferQuotaLow() || u.IsDownloadTransferQuotaLow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userQuotaUsage) IsQuotaLow() bool {
|
||||||
|
return u.IsDiskQuotaLow() || u.IsTransferQuotaLow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUserQuotaUsage(u *dataprovider.User) *userQuotaUsage {
|
||||||
|
return &userQuotaUsage{
|
||||||
|
QuotaSize: u.QuotaSize,
|
||||||
|
QuotaFiles: u.QuotaFiles,
|
||||||
|
UsedQuotaSize: u.UsedQuotaSize,
|
||||||
|
UsedQuotaFiles: u.UsedQuotaFiles,
|
||||||
|
TotalDataTransfer: u.TotalDataTransfer,
|
||||||
|
UploadDataTransfer: u.UploadDataTransfer,
|
||||||
|
DownloadDataTransfer: u.DownloadDataTransfer,
|
||||||
|
UsedUploadDataTransfer: u.UsedUploadDataTransfer,
|
||||||
|
UsedDownloadDataTransfer: u.UsedDownloadDataTransfer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getFileObjectURL(baseDir, name, baseWebPath string) string {
|
func getFileObjectURL(baseDir, name, baseWebPath string) string {
|
||||||
return fmt.Sprintf("%v?path=%v&_=%v", baseWebPath, url.QueryEscape(path.Join(baseDir, name)), time.Now().UTC().Unix())
|
return fmt.Sprintf("%v?path=%v&_=%v", baseWebPath, url.QueryEscape(path.Join(baseDir, name)), time.Now().UTC().Unix())
|
||||||
}
|
}
|
||||||
|
@ -595,7 +776,7 @@ func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Req
|
||||||
renderClientTemplate(w, templateUploadToShare, data)
|
renderClientTemplate(w, templateUploadToShare, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user dataprovider.User,
|
func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user *dataprovider.User,
|
||||||
hasIntegrations bool,
|
hasIntegrations bool,
|
||||||
) {
|
) {
|
||||||
data := filesPage{
|
data := filesPage{
|
||||||
|
@ -615,6 +796,7 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
|
||||||
CanShare: user.CanManageShares(),
|
CanShare: user.CanManageShares(),
|
||||||
HasIntegrations: hasIntegrations,
|
HasIntegrations: hasIntegrations,
|
||||||
Paths: getDirMapping(dirName, webClientFilesPath),
|
Paths: getDirMapping(dirName, webClientFilesPath),
|
||||||
|
QuotaUsage: newUserQuotaUsage(user),
|
||||||
}
|
}
|
||||||
renderClientTemplate(w, templateClientFiles, data)
|
renderClientTemplate(w, templateClientFiles, data)
|
||||||
}
|
}
|
||||||
|
@ -964,11 +1146,11 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %q: %v", name, err),
|
s.renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %q: %v", name, err),
|
||||||
user, len(s.binding.WebClientIntegrations) > 0)
|
&user, len(s.binding.WebClientIntegrations) > 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
s.renderFilesPage(w, r, name, "", user, len(s.binding.WebClientIntegrations) > 0)
|
s.renderFilesPage(w, r, name, "", &user, len(s.binding.WebClientIntegrations) > 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if status, err := downloadFile(w, r, connection, name, info, false, nil); err != nil && status != 0 {
|
if status, err := downloadFile(w, r, connection, name, info, false, nil); err != nil && status != 0 {
|
||||||
|
@ -977,7 +1159,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
|
||||||
s.renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
|
s.renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.renderFilesPage(w, r, path.Dir(name), err.Error(), user, len(s.binding.WebClientIntegrations) > 0)
|
s.renderFilesPage(w, r, path.Dir(name), err.Error(), &user, len(s.binding.WebClientIntegrations) > 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1049,7 +1231,7 @@ func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.renderEditFilePage(w, r, name, b.String(), util.Contains(user.Filters.WebClient, sdk.WebClientWriteDisabled))
|
s.renderEditFilePage(w, r, name, b.String(), !user.CanAddFilesFromWeb(path.Dir(name)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -1303,8 +1485,7 @@ func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
|
||||||
share.Name = strings.TrimSpace(r.Form.Get("name"))
|
share.Name = strings.TrimSpace(r.Form.Get("name"))
|
||||||
share.Description = r.Form.Get("description")
|
share.Description = r.Form.Get("description")
|
||||||
for _, p := range r.Form["paths"] {
|
for _, p := range r.Form["paths"] {
|
||||||
p = strings.TrimSpace(p)
|
if strings.TrimSpace(p) != "" {
|
||||||
if p != "" {
|
|
||||||
share.Paths = append(share.Paths, p)
|
share.Paths = append(share.Paths, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2487,9 +2487,6 @@ func compareUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters
|
||||||
if err := compareUserBandwidthLimitFilters(expected, actual); err != nil {
|
if err := compareUserBandwidthLimitFilters(expected, actual); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := compareUserDataTransferLimitFilters(expected, actual); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return compareUserFilePatternsFilters(expected, actual)
|
return compareUserFilePatternsFilters(expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2505,30 +2502,6 @@ func checkFilterMatch(expected []string, actual []string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareUserDataTransferLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
|
||||||
if len(expected.DataTransferLimits) != len(actual.DataTransferLimits) {
|
|
||||||
return errors.New("data transfer limits filters mismatch")
|
|
||||||
}
|
|
||||||
for idx, l := range expected.DataTransferLimits {
|
|
||||||
if actual.DataTransferLimits[idx].UploadDataTransfer != l.UploadDataTransfer {
|
|
||||||
return errors.New("data transfer limit upload_data_transfer mismatch")
|
|
||||||
}
|
|
||||||
if actual.DataTransferLimits[idx].DownloadDataTransfer != l.DownloadDataTransfer {
|
|
||||||
return errors.New("data transfer limit download_data_transfer mismatch")
|
|
||||||
}
|
|
||||||
if actual.DataTransferLimits[idx].TotalDataTransfer != l.TotalDataTransfer {
|
|
||||||
return errors.New("data transfer limit total_data_transfer mismatch")
|
|
||||||
}
|
|
||||||
for _, source := range actual.DataTransferLimits[idx].Sources {
|
|
||||||
if !util.Contains(l.Sources, source) {
|
|
||||||
return errors.New("data transfer limit source mismatch")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
||||||
if len(expected.BandwidthLimits) != len(actual.BandwidthLimits) {
|
if len(expected.BandwidthLimits) != len(actual.BandwidthLimits) {
|
||||||
return errors.New("bandwidth limits filters mismatch")
|
return errors.New("bandwidth limits filters mismatch")
|
||||||
|
|
|
@ -14,6 +14,12 @@
|
||||||
|
|
||||||
package logger
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/wneessen/go-mail/log"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mailLogSender = "smtpclient"
|
mailLogSender = "smtpclient"
|
||||||
)
|
)
|
||||||
|
@ -24,25 +30,37 @@ type MailAdapter struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errorf emits a log at Error level
|
// Errorf emits a log at Error level
|
||||||
func (l *MailAdapter) Errorf(format string, v ...any) {
|
func (l *MailAdapter) Errorf(logMsg log.Log) {
|
||||||
ErrorToConsole(format, v...)
|
format := l.getFormatString(&logMsg)
|
||||||
Log(LevelError, mailLogSender, l.ConnectionID, format, v...)
|
ErrorToConsole(format, logMsg.Messages...)
|
||||||
|
Log(LevelError, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warnf emits a log at Warn level
|
// Warnf emits a log at Warn level
|
||||||
func (l *MailAdapter) Warnf(format string, v ...any) {
|
func (l *MailAdapter) Warnf(logMsg log.Log) {
|
||||||
WarnToConsole(format, v...)
|
format := l.getFormatString(&logMsg)
|
||||||
Log(LevelWarn, mailLogSender, l.ConnectionID, format, v...)
|
WarnToConsole(format, logMsg.Messages...)
|
||||||
|
Log(LevelWarn, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infof emits a log at Info level
|
// Infof emits a log at Info level
|
||||||
func (l *MailAdapter) Infof(format string, v ...any) {
|
func (l *MailAdapter) Infof(logMsg log.Log) {
|
||||||
InfoToConsole(format, v...)
|
format := l.getFormatString(&logMsg)
|
||||||
Log(LevelInfo, mailLogSender, l.ConnectionID, format, v...)
|
InfoToConsole(format, logMsg.Messages...)
|
||||||
|
Log(LevelInfo, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debugf emits a log at Debug level
|
// Debugf emits a log at Debug level
|
||||||
func (l *MailAdapter) Debugf(format string, v ...any) {
|
func (l *MailAdapter) Debugf(logMsg log.Log) {
|
||||||
DebugToConsole(format, v...)
|
format := l.getFormatString(&logMsg)
|
||||||
Log(LevelDebug, mailLogSender, l.ConnectionID, format, v...)
|
DebugToConsole(format, logMsg.Messages...)
|
||||||
|
Log(LevelDebug, mailLogSender, l.ConnectionID, format, logMsg.Messages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MailAdapter) getFormatString(logMsg *log.Log) string {
|
||||||
|
p := "C <-- S:"
|
||||||
|
if logMsg.Direction == log.DirClientToServer {
|
||||||
|
p = "C --> S:"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", p, logMsg.Format)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,15 @@ func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||||
// Write logs a new entry at the end of the HTTP request
|
// Write logs a new entry at the end of the HTTP request
|
||||||
func (l *StructuredLoggerEntry) Write(status, bytes int, _ http.Header, elapsed time.Duration, _ any) {
|
func (l *StructuredLoggerEntry) Write(status, bytes int, _ http.Header, elapsed time.Duration, _ any) {
|
||||||
metric.HTTPRequestServed(status)
|
metric.HTTPRequestServed(status)
|
||||||
l.Logger.Info().
|
var ev *zerolog.Event
|
||||||
|
if status >= http.StatusInternalServerError {
|
||||||
|
ev = l.Logger.Error()
|
||||||
|
} else if status >= http.StatusBadRequest {
|
||||||
|
ev = l.Logger.Warn()
|
||||||
|
} else {
|
||||||
|
ev = l.Logger.Debug()
|
||||||
|
}
|
||||||
|
ev.
|
||||||
Timestamp().
|
Timestamp().
|
||||||
Str("sender", "httpd").
|
Str("sender", "httpd").
|
||||||
Fields(l.fields).
|
Fields(l.fields).
|
||||||
|
|
|
@ -136,7 +136,7 @@ func (p *authPlugin) initialize() error {
|
||||||
})
|
})
|
||||||
rpcClient, err := client.Client()
|
rpcClient, err := client.Client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(logSender, "", "unable to get rpc client for kms plugin %q: %v", p.config.Cmd, err)
|
logger.Debug(logSender, "", "unable to get rpc client for auth plugin %q: %v", p.config.Cmd, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
raw, err := rpcClient.Dispense(auth.PluginName)
|
raw, err := rpcClient.Dispense(auth.PluginName)
|
||||||
|
|
|
@ -1722,7 +1722,7 @@ func TestSCPUploadFiledata(t *testing.T) {
|
||||||
|
|
||||||
transfer.Connection.AddTransfer(transfer)
|
transfer.Connection.AddTransfer(transfer)
|
||||||
err = scpCommand.getUploadFileData(2, transfer)
|
err = scpCommand.getUploadFileData(2, transfer)
|
||||||
assert.True(t, errors.Is(err, os.ErrClosed))
|
assert.ErrorContains(t, err, os.ErrClosed.Error())
|
||||||
|
|
||||||
err = os.Remove(testfile)
|
err = os.Remove(testfile)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1794,7 +1794,7 @@ func TestTransferFailingReader(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
buf := make([]byte, 32)
|
buf := make([]byte, 32)
|
||||||
_, err = transfer.ReadAt(buf, 0)
|
_, err = transfer.ReadAt(buf, 0)
|
||||||
assert.EqualError(t, err, sftp.ErrSSHFxOpUnsupported.Error())
|
assert.ErrorIs(t, err, sftp.ErrSSHFxOpUnsupported)
|
||||||
if c, ok := transfer.(io.Closer); ok {
|
if c, ok := transfer.(io.Closer); ok {
|
||||||
err = c.Close()
|
err = c.Close()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1809,14 +1809,14 @@ func TestTransferFailingReader(t *testing.T) {
|
||||||
errRead := errors.New("read is not allowed")
|
errRead := errors.New("read is not allowed")
|
||||||
tr := newTransfer(baseTransfer, nil, r, errRead)
|
tr := newTransfer(baseTransfer, nil, r, errRead)
|
||||||
_, err = tr.ReadAt(buf, 0)
|
_, err = tr.ReadAt(buf, 0)
|
||||||
assert.EqualError(t, err, errRead.Error())
|
assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
|
||||||
|
|
||||||
err = tr.Close()
|
err = tr.Close()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
tr = newTransfer(baseTransfer, nil, nil, errRead)
|
tr = newTransfer(baseTransfer, nil, nil, errRead)
|
||||||
_, err = tr.ReadAt(buf, 0)
|
_, err = tr.ReadAt(buf, 0)
|
||||||
assert.EqualError(t, err, errRead.Error())
|
assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
|
||||||
|
|
||||||
err = tr.Close()
|
err = tr.Close()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1984,6 +1984,15 @@ func TestLoadHostKeys(t *testing.T) {
|
||||||
c.HostKeys = []string{nonDefaultKeyName, rsaKeyName, ecdsaKeyName, ed25519KeyName}
|
c.HostKeys = []string{nonDefaultKeyName, rsaKeyName, ecdsaKeyName, ed25519KeyName}
|
||||||
err = c.checkAndLoadHostKeys(configDir, serverConfig)
|
err = c.checkAndLoadHostKeys(configDir, serverConfig)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
c.HostKeyAlgorithms = []string{ssh.KeyAlgoRSASHA256}
|
||||||
|
c.HostKeys = []string{ecdsaKeyName}
|
||||||
|
err = c.checkAndLoadHostKeys(configDir, serverConfig)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "server has no host keys")
|
||||||
|
}
|
||||||
|
c.HostKeyAlgorithms = preferredHostKeyAlgos
|
||||||
|
err = c.checkAndLoadHostKeys(configDir, serverConfig)
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.FileExists(t, rsaKeyName)
|
assert.FileExists(t, rsaKeyName)
|
||||||
assert.FileExists(t, ecdsaKeyName)
|
assert.FileExists(t, ecdsaKeyName)
|
||||||
assert.FileExists(t, ed25519KeyName)
|
assert.FileExists(t, ed25519KeyName)
|
||||||
|
|
|
@ -65,11 +65,8 @@ var (
|
||||||
ssh.KeyAlgoED25519,
|
ssh.KeyAlgoED25519,
|
||||||
}
|
}
|
||||||
preferredHostKeyAlgos = []string{
|
preferredHostKeyAlgos = []string{
|
||||||
ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSASHA256v01,
|
ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512,
|
||||||
ssh.CertAlgoECDSA256v01,
|
|
||||||
ssh.CertAlgoECDSA384v01, ssh.CertAlgoECDSA521v01, ssh.CertAlgoED25519v01,
|
|
||||||
ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
|
ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
|
||||||
ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSASHA256,
|
|
||||||
ssh.KeyAlgoED25519,
|
ssh.KeyAlgoED25519,
|
||||||
}
|
}
|
||||||
supportedKexAlgos = []string{
|
supportedKexAlgos = []string{
|
||||||
|
@ -369,15 +366,6 @@ func (c *Configuration) Initialize(configDir string) error {
|
||||||
return common.ErrNoBinding
|
return common.ErrNoBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.checkAndLoadHostKeys(configDir, serverConfig); err != nil {
|
|
||||||
serviceStatus.HostKeys = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.initializeCertChecker(configDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.loadModuli(configDir)
|
c.loadModuli(configDir)
|
||||||
|
|
||||||
sftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck // we configure valid SFTP Extensions so we cannot get an error
|
sftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck // we configure valid SFTP Extensions so we cannot get an error
|
||||||
|
@ -385,6 +373,13 @@ func (c *Configuration) Initialize(configDir string) error {
|
||||||
if err := c.configureSecurityOptions(serverConfig); err != nil {
|
if err := c.configureSecurityOptions(serverConfig); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := c.checkAndLoadHostKeys(configDir, serverConfig); err != nil {
|
||||||
|
serviceStatus.HostKeys = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.initializeCertChecker(configDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
c.configureKeyboardInteractiveAuth(serverConfig)
|
c.configureKeyboardInteractiveAuth(serverConfig)
|
||||||
c.configureLoginBanner(serverConfig, configDir)
|
c.configureLoginBanner(serverConfig, configDir)
|
||||||
c.checkSSHCommands()
|
c.checkSSHCommands()
|
||||||
|
@ -471,8 +466,6 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
|
||||||
return fmt.Errorf("unsupported host key algorithm %q", hostKeyAlgo)
|
return fmt.Errorf("unsupported host key algorithm %q", hostKeyAlgo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serverConfig.HostKeyAlgorithms = c.HostKeyAlgorithms
|
|
||||||
serviceStatus.HostKeyAlgos = c.HostKeyAlgorithms
|
|
||||||
|
|
||||||
if len(c.KexAlgorithms) > 0 {
|
if len(c.KexAlgorithms) > 0 {
|
||||||
hasDHGroupKEX := util.Contains(supportedKexAlgos, kexDHGroupExchangeSHA256)
|
hasDHGroupKEX := util.Contains(supportedKexAlgos, kexDHGroupExchangeSHA256)
|
||||||
|
@ -989,8 +982,18 @@ func (c *Configuration) loadModuli(configDir string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Configuration) getHostKeyAlgorithms(keyFormat string) []string {
|
||||||
|
var algos []string
|
||||||
|
for _, algo := range algorithmsForKeyFormat(keyFormat) {
|
||||||
|
if util.Contains(c.HostKeyAlgorithms, algo) {
|
||||||
|
algos = append(algos, algo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return algos
|
||||||
|
}
|
||||||
|
|
||||||
// If no host keys are defined we try to use or generate the default ones.
|
// If no host keys are defined we try to use or generate the default ones.
|
||||||
func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh.ServerConfig) error {
|
func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh.ServerConfig) error { //nolint:gocyclo
|
||||||
if err := c.checkHostKeyAutoGeneration(configDir); err != nil {
|
if err := c.checkHostKeyAutoGeneration(configDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1023,22 +1026,45 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
|
||||||
k := HostKey{
|
k := HostKey{
|
||||||
Path: hostKey,
|
Path: hostKey,
|
||||||
Fingerprint: ssh.FingerprintSHA256(private.PublicKey()),
|
Fingerprint: ssh.FingerprintSHA256(private.PublicKey()),
|
||||||
|
Algorithms: c.getHostKeyAlgorithms(private.PublicKey().Type()),
|
||||||
|
}
|
||||||
|
mas, err := ssh.NewSignerWithAlgorithms(private.(ssh.AlgorithmSigner), k.Algorithms)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn(logSender, "", "could not create signer for key %q with algorithms %+v: %v", k.Path, k.Algorithms, err)
|
||||||
|
logger.WarnToConsole("could not create signer for key %q with algorithms %+v: %v", k.Path, k.Algorithms, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
serviceStatus.HostKeys = append(serviceStatus.HostKeys, k)
|
serviceStatus.HostKeys = append(serviceStatus.HostKeys, k)
|
||||||
logger.Info(logSender, "", "Host key %q loaded, type %q, fingerprint %q", hostKey,
|
logger.Info(logSender, "", "Host key %q loaded, type %q, fingerprint %q, algorithms %+v", hostKey,
|
||||||
private.PublicKey().Type(), k.Fingerprint)
|
private.PublicKey().Type(), k.Fingerprint, k.Algorithms)
|
||||||
|
|
||||||
// Add private key to the server configuration.
|
// Add private key to the server configuration.
|
||||||
serverConfig.AddHostKey(private)
|
serverConfig.AddHostKey(mas)
|
||||||
for _, cert := range hostCertificates {
|
for _, cert := range hostCertificates {
|
||||||
signer, err := ssh.NewCertSigner(cert, private)
|
signer, err := ssh.NewCertSigner(cert.Certificate, mas)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
var algos []string
|
||||||
|
for _, algo := range algorithmsForKeyFormat(signer.PublicKey().Type()) {
|
||||||
|
if underlyingAlgo, ok := certKeyAlgoNames[algo]; ok {
|
||||||
|
if util.Contains(mas.Algorithms(), underlyingAlgo) {
|
||||||
|
algos = append(algos, algo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serviceStatus.HostKeys = append(serviceStatus.HostKeys, HostKey{
|
||||||
|
Path: cert.Path,
|
||||||
|
Fingerprint: ssh.FingerprintSHA256(signer.PublicKey()),
|
||||||
|
Algorithms: algos,
|
||||||
|
})
|
||||||
serverConfig.AddHostKey(signer)
|
serverConfig.AddHostKey(signer)
|
||||||
logger.Info(logSender, "", "Host certificate loaded for host key %q, fingerprint %q",
|
logger.Info(logSender, "", "Host certificate loaded for host key %q, fingerprint %q, algorithms %+v",
|
||||||
hostKey, ssh.FingerprintSHA256(signer.PublicKey()))
|
hostKey, ssh.FingerprintSHA256(signer.PublicKey()), algos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(serviceStatus.HostKeys) == 0 {
|
||||||
|
return errors.New("ssh: server has no host keys")
|
||||||
|
}
|
||||||
var fp []string
|
var fp []string
|
||||||
for idx := range serviceStatus.HostKeys {
|
for idx := range serviceStatus.HostKeys {
|
||||||
h := &serviceStatus.HostKeys[idx]
|
h := &serviceStatus.HostKeys[idx]
|
||||||
|
@ -1048,8 +1074,8 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configuration) loadHostCertificates(configDir string) ([]*ssh.Certificate, error) {
|
func (c *Configuration) loadHostCertificates(configDir string) ([]hostCertificate, error) {
|
||||||
var certs []*ssh.Certificate
|
var certs []hostCertificate
|
||||||
for _, certPath := range c.HostCertificates {
|
for _, certPath := range c.HostCertificates {
|
||||||
certPath = strings.TrimSpace(certPath)
|
certPath = strings.TrimSpace(certPath)
|
||||||
if !util.IsFileInputValid(certPath) {
|
if !util.IsFileInputValid(certPath) {
|
||||||
|
@ -1075,7 +1101,10 @@ func (c *Configuration) loadHostCertificates(configDir string) ([]*ssh.Certifica
|
||||||
if cert.CertType != ssh.HostCert {
|
if cert.CertType != ssh.HostCert {
|
||||||
return nil, fmt.Errorf("the file %q is not an host certificate", certPath)
|
return nil, fmt.Errorf("the file %q is not an host certificate", certPath)
|
||||||
}
|
}
|
||||||
certs = append(certs, cert)
|
certs = append(certs, hostCertificate{
|
||||||
|
Path: certPath,
|
||||||
|
Certificate: cert,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return certs, nil
|
return certs, nil
|
||||||
}
|
}
|
||||||
|
@ -1312,3 +1341,14 @@ func (r *revokedCertificates) isRevoked(fp string) bool {
|
||||||
func Reload() error {
|
func Reload() error {
|
||||||
return revokedCertManager.load()
|
return revokedCertManager.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func algorithmsForKeyFormat(keyFormat string) []string {
|
||||||
|
switch keyFormat {
|
||||||
|
case ssh.KeyAlgoRSA:
|
||||||
|
return []string{ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSA}
|
||||||
|
case ssh.CertAlgoRSAv01:
|
||||||
|
return []string{ssh.CertAlgoRSASHA256v01, ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSAv01}
|
||||||
|
default:
|
||||||
|
return []string{keyFormat}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@ package sftpd
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -34,6 +36,18 @@ var (
|
||||||
sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
|
sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
|
||||||
systemCommands = []string{"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"}
|
systemCommands = []string{"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"}
|
||||||
serviceStatus ServiceStatus
|
serviceStatus ServiceStatus
|
||||||
|
certKeyAlgoNames = map[string]string{
|
||||||
|
ssh.CertAlgoRSAv01: ssh.KeyAlgoRSA,
|
||||||
|
ssh.CertAlgoRSASHA256v01: ssh.KeyAlgoRSASHA256,
|
||||||
|
ssh.CertAlgoRSASHA512v01: ssh.KeyAlgoRSASHA512,
|
||||||
|
ssh.CertAlgoDSAv01: ssh.KeyAlgoDSA,
|
||||||
|
ssh.CertAlgoECDSA256v01: ssh.KeyAlgoECDSA256,
|
||||||
|
ssh.CertAlgoECDSA384v01: ssh.KeyAlgoECDSA384,
|
||||||
|
ssh.CertAlgoECDSA521v01: ssh.KeyAlgoECDSA521,
|
||||||
|
ssh.CertAlgoSKECDSA256v01: ssh.KeyAlgoSKECDSA256,
|
||||||
|
ssh.CertAlgoED25519v01: ssh.KeyAlgoED25519,
|
||||||
|
ssh.CertAlgoSKED25519v01: ssh.KeyAlgoSKED25519,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type sshSubsystemExitStatus struct {
|
type sshSubsystemExitStatus struct {
|
||||||
|
@ -44,10 +58,21 @@ type sshSubsystemExecMsg struct {
|
||||||
Command string
|
Command string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hostCertificate struct {
|
||||||
|
Certificate *ssh.Certificate
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
// HostKey defines the details for a used host key
|
// HostKey defines the details for a used host key
|
||||||
type HostKey struct {
|
type HostKey struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Fingerprint string `json:"fingerprint"`
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
Algorithms []string `json:"algorithms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlgosAsString returns the host key algorithms as comma separated string
|
||||||
|
func (h *HostKey) GetAlgosAsString() string {
|
||||||
|
return strings.Join(h.Algorithms, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceStatus defines the service status
|
// ServiceStatus defines the service status
|
||||||
|
@ -57,7 +82,6 @@ type ServiceStatus struct {
|
||||||
SSHCommands []string `json:"ssh_commands"`
|
SSHCommands []string `json:"ssh_commands"`
|
||||||
HostKeys []HostKey `json:"host_keys"`
|
HostKeys []HostKey `json:"host_keys"`
|
||||||
Authentications []string `json:"authentications"`
|
Authentications []string `json:"authentications"`
|
||||||
HostKeyAlgos []string `json:"host_key_algos"`
|
|
||||||
MACs []string `json:"macs"`
|
MACs []string `json:"macs"`
|
||||||
KexAlgorithms []string `json:"kex_algorithms"`
|
KexAlgorithms []string `json:"kex_algorithms"`
|
||||||
Ciphers []string `json:"ciphers"`
|
Ciphers []string `json:"ciphers"`
|
||||||
|
@ -73,11 +97,6 @@ func (s *ServiceStatus) GetSupportedAuthsAsString() string {
|
||||||
return strings.Join(s.Authentications, ", ")
|
return strings.Join(s.Authentications, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHostKeyAlgosAsString returns the enabled host keys algorithms as comma separated string
|
|
||||||
func (s *ServiceStatus) GetHostKeyAlgosAsString() string {
|
|
||||||
return strings.Join(s.HostKeyAlgos, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMACsAsString returns the enabled MAC algorithms as comma separated string
|
// GetMACsAsString returns the enabled MAC algorithms as comma separated string
|
||||||
func (s *ServiceStatus) GetMACsAsString() string {
|
func (s *ServiceStatus) GetMACsAsString() string {
|
||||||
return strings.Join(s.MACs, ", ")
|
return strings.Join(s.MACs, ", ")
|
||||||
|
|
|
@ -573,7 +573,7 @@ func TestBasicSFTPHandling(t *testing.T) {
|
||||||
assert.NotEmpty(t, sshCommands)
|
assert.NotEmpty(t, sshCommands)
|
||||||
sshAuths := status.GetSupportedAuthsAsString()
|
sshAuths := status.GetSupportedAuthsAsString()
|
||||||
assert.NotEmpty(t, sshAuths)
|
assert.NotEmpty(t, sshAuths)
|
||||||
assert.NotEmpty(t, status.GetHostKeyAlgosAsString())
|
assert.NotEmpty(t, status.HostKeys[0].GetAlgosAsString())
|
||||||
assert.NotEmpty(t, status.GetMACsAsString())
|
assert.NotEmpty(t, status.GetMACsAsString())
|
||||||
assert.NotEmpty(t, status.GetKEXsAsString())
|
assert.NotEmpty(t, status.GetKEXsAsString())
|
||||||
assert.NotEmpty(t, status.GetCiphersAsString())
|
assert.NotEmpty(t, status.GetCiphersAsString())
|
||||||
|
@ -3786,6 +3786,17 @@ func TestExternalAuthEmptyResponse(t *testing.T) {
|
||||||
assert.Equal(t, 10, user.MaxSessions)
|
assert.Equal(t, 10, user.MaxSessions)
|
||||||
assert.Equal(t, 100, user.QuotaFiles)
|
assert.Equal(t, 100, user.QuotaFiles)
|
||||||
|
|
||||||
|
// the auth script accepts any password and returns an empty response, the
|
||||||
|
// user password must be updated
|
||||||
|
u.Password = defaultUsername
|
||||||
|
conn, client, err = getSftpClient(u, usePubKey)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer client.Close()
|
||||||
|
err = checkBasicSFTP(client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
@ -4745,7 +4756,9 @@ func TestQuotaLimits(t *testing.T) {
|
||||||
err = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)
|
err = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Contains(t, err.Error(), "SSH_FX_FAILURE")
|
assert.Contains(t, err.Error(), "SSH_FX_FAILURE")
|
||||||
assert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())
|
if user.Username == localUser.Username {
|
||||||
|
assert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, err = client.Stat(testFileName1)
|
_, err = client.Stat(testFileName1)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
@ -8386,6 +8399,35 @@ func TestWildcardPermissions(t *testing.T) {
|
||||||
assert.True(t, user.HasPerm(dataprovider.PermListItems, "/abc/a/a/a/b"))
|
assert.True(t, user.HasPerm(dataprovider.PermListItems, "/abc/a/a/a/b"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRootWildcardPerms(t *testing.T) {
|
||||||
|
user := getTestUser(true)
|
||||||
|
user.Permissions = make(map[string][]string)
|
||||||
|
user.Permissions["/"] = []string{dataprovider.PermListItems}
|
||||||
|
user.Permissions["/*"] = []string{dataprovider.PermDelete}
|
||||||
|
user.Permissions["/p/*"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}
|
||||||
|
user.Permissions["/p/2"] = []string{dataprovider.PermCreateDirs}
|
||||||
|
user.Permissions["/pa"] = []string{dataprovider.PermChmod}
|
||||||
|
user.Permissions["/p/3/4"] = []string{dataprovider.PermChtimes}
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermListItems, "/"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermDelete, "/p1"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermDelete, "/ppppp"))
|
||||||
|
assert.False(t, user.HasPerm(dataprovider.PermDelete, "/pa"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermChmod, "/pa"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermUpload, "/p/1"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermUpload, "/p/p"))
|
||||||
|
assert.False(t, user.HasPerm(dataprovider.PermUpload, "/p/2"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermCreateDirs, "/p/2"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermCreateDirs, "/p/2/a"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermDownload, "/p/3"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermDownload, "/p/a/a/a"))
|
||||||
|
assert.False(t, user.HasPerm(dataprovider.PermDownload, "/p/3/4"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermChtimes, "/p/3/4"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermDelete, "/pb/a/a/a"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermDelete, "/abc/a/a/a"))
|
||||||
|
assert.False(t, user.HasPerm(dataprovider.PermListItems, "/abc/a/a/a/b"))
|
||||||
|
assert.True(t, user.HasPerm(dataprovider.PermDelete, "/abc/a/a/a/b"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestFilterFilePatterns(t *testing.T) {
|
func TestFilterFilePatterns(t *testing.T) {
|
||||||
user := getTestUser(true)
|
user := getTestUser(true)
|
||||||
pattern := sdk.PatternsFilter{
|
pattern := sdk.PatternsFilter{
|
||||||
|
@ -11504,7 +11546,7 @@ func getExtAuthScriptContent(user dataprovider.User, nonJSONResponse, emptyRespo
|
||||||
return extAuthContent
|
return extAuthContent
|
||||||
}
|
}
|
||||||
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%v\"; then\n", user.Username))...)
|
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%v\"; then\n", user.Username))...)
|
||||||
if len(username) > 0 {
|
if username != "" {
|
||||||
user.Username = username
|
user.Username = username
|
||||||
}
|
}
|
||||||
u, _ := json.Marshal(user)
|
u, _ := json.Marshal(user)
|
||||||
|
|
|
@ -336,7 +336,7 @@ func GetIntFromPointer(val *int64) int64 {
|
||||||
// GetTimeFromPointer returns the time value or now
|
// GetTimeFromPointer returns the time value or now
|
||||||
func GetTimeFromPointer(val *time.Time) time.Time {
|
func GetTimeFromPointer(val *time.Time) time.Time {
|
||||||
if val == nil {
|
if val == nil {
|
||||||
return time.Now()
|
return time.Unix(0, 0)
|
||||||
}
|
}
|
||||||
return *val
|
return *val
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ package version
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
const version = "2.5.1-dev"
|
const version = "2.5.7-dev"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
commit = ""
|
commit = ""
|
||||||
|
|
|
@ -115,19 +115,6 @@ func NewS3Fs(connectionID, localTempDir, mountPath string, s3Config S3FsConfig)
|
||||||
awsConfig.Credentials = aws.NewCredentialsCache(
|
awsConfig.Credentials = aws.NewCredentialsCache(
|
||||||
credentials.NewStaticCredentialsProvider(fs.config.AccessKey, fs.config.AccessSecret.GetPayload(), ""))
|
credentials.NewStaticCredentialsProvider(fs.config.AccessKey, fs.config.AccessSecret.GetPayload(), ""))
|
||||||
}
|
}
|
||||||
if fs.config.Endpoint != "" {
|
|
||||||
endpointResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...any) (aws.Endpoint, error) {
|
|
||||||
return aws.Endpoint{
|
|
||||||
URL: fs.config.Endpoint,
|
|
||||||
HostnameImmutable: fs.config.ForcePathStyle,
|
|
||||||
PartitionID: "aws",
|
|
||||||
SigningRegion: fs.config.Region,
|
|
||||||
Source: aws.EndpointSourceCustom,
|
|
||||||
}, nil
|
|
||||||
})
|
|
||||||
awsConfig.EndpointResolverWithOptions = endpointResolver
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.setConfigDefaults()
|
fs.setConfigDefaults()
|
||||||
|
|
||||||
if fs.config.RoleARN != "" {
|
if fs.config.RoleARN != "" {
|
||||||
|
@ -137,6 +124,9 @@ func NewS3Fs(connectionID, localTempDir, mountPath string, s3Config S3FsConfig)
|
||||||
}
|
}
|
||||||
fs.svc = s3.NewFromConfig(awsConfig, func(o *s3.Options) {
|
fs.svc = s3.NewFromConfig(awsConfig, func(o *s3.Options) {
|
||||||
o.UsePathStyle = fs.config.ForcePathStyle
|
o.UsePathStyle = fs.config.ForcePathStyle
|
||||||
|
if fs.config.Endpoint != "" {
|
||||||
|
o.BaseEndpoint = aws.String(fs.config.Endpoint)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return fs, nil
|
return fs, nil
|
||||||
}
|
}
|
||||||
|
@ -165,11 +155,11 @@ func (fs *S3Fs) Stat(name string) (os.FileInfo, error) {
|
||||||
// Some S3 providers (like SeaweedFS) remove the trailing '/' from object keys.
|
// Some S3 providers (like SeaweedFS) remove the trailing '/' from object keys.
|
||||||
// So we check some common content types to detect if this is a "directory".
|
// So we check some common content types to detect if this is a "directory".
|
||||||
isDir := util.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
|
isDir := util.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
|
||||||
if obj.ContentLength == 0 && !isDir {
|
if util.GetIntFromPointer(obj.ContentLength) == 0 && !isDir {
|
||||||
_, err = fs.headObject(name + "/")
|
_, err = fs.headObject(name + "/")
|
||||||
isDir = err == nil
|
isDir = err == nil
|
||||||
}
|
}
|
||||||
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, isDir, obj.ContentLength,
|
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, isDir, util.GetIntFromPointer(obj.ContentLength),
|
||||||
util.GetTimeFromPointer(obj.LastModified), false))
|
util.GetTimeFromPointer(obj.LastModified), false))
|
||||||
}
|
}
|
||||||
if !fs.IsNotExist(err) {
|
if !fs.IsNotExist(err) {
|
||||||
|
@ -195,7 +185,7 @@ func (fs *S3Fs) getStatForDir(name string) (os.FileInfo, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, true, obj.ContentLength,
|
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, true, util.GetIntFromPointer(obj.ContentLength),
|
||||||
util.GetTimeFromPointer(obj.LastModified), false))
|
util.GetTimeFromPointer(obj.LastModified), false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,6 +423,7 @@ func (fs *S3Fs) ReadDir(dirname string) ([]os.FileInfo, error) {
|
||||||
}
|
}
|
||||||
for _, fileObject := range page.Contents {
|
for _, fileObject := range page.Contents {
|
||||||
objectModTime := util.GetTimeFromPointer(fileObject.LastModified)
|
objectModTime := util.GetTimeFromPointer(fileObject.LastModified)
|
||||||
|
objectSize := util.GetIntFromPointer(fileObject.Size)
|
||||||
name, isDir := fs.resolve(fileObject.Key, prefix)
|
name, isDir := fs.resolve(fileObject.Key, prefix)
|
||||||
if name == "" || name == "/" {
|
if name == "" || name == "/" {
|
||||||
continue
|
continue
|
||||||
|
@ -446,7 +437,7 @@ func (fs *S3Fs) ReadDir(dirname string) ([]os.FileInfo, error) {
|
||||||
if t, ok := modTimes[name]; ok {
|
if t, ok := modTimes[name]; ok {
|
||||||
objectModTime = util.GetTimeFromMsecSinceEpoch(t)
|
objectModTime = util.GetTimeFromMsecSinceEpoch(t)
|
||||||
}
|
}
|
||||||
result = append(result, NewFileInfo(name, (isDir && fileObject.Size == 0), fileObject.Size,
|
result = append(result, NewFileInfo(name, (isDir && objectSize == 0), objectSize,
|
||||||
objectModTime, false))
|
objectModTime, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -588,11 +579,12 @@ func (fs *S3Fs) GetDirSize(dirname string) (int, int64, error) {
|
||||||
}
|
}
|
||||||
for _, fileObject := range page.Contents {
|
for _, fileObject := range page.Contents {
|
||||||
isDir := strings.HasSuffix(util.GetStringFromPointer(fileObject.Key), "/")
|
isDir := strings.HasSuffix(util.GetStringFromPointer(fileObject.Key), "/")
|
||||||
if isDir && fileObject.Size == 0 {
|
objectSize := util.GetIntFromPointer(fileObject.Size)
|
||||||
|
if isDir && objectSize == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
numFiles++
|
numFiles++
|
||||||
size += fileObject.Size
|
size += objectSize
|
||||||
if numFiles%1000 == 0 {
|
if numFiles%1000 == 0 {
|
||||||
fsLog(fs, logger.LevelDebug, "dirname %q scan in progress, files: %d, size: %d", dirname, numFiles, size)
|
fsLog(fs, logger.LevelDebug, "dirname %q scan in progress, files: %d, size: %d", dirname, numFiles, size)
|
||||||
}
|
}
|
||||||
|
@ -657,7 +649,8 @@ func (fs *S3Fs) Walk(root string, walkFn filepath.WalkFunc) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err := walkFn(util.GetStringFromPointer(fileObject.Key),
|
err := walkFn(util.GetStringFromPointer(fileObject.Key),
|
||||||
NewFileInfo(name, isDir, fileObject.Size, util.GetTimeFromPointer(fileObject.LastModified), false), nil)
|
NewFileInfo(name, isDir, util.GetIntFromPointer(fileObject.Size),
|
||||||
|
util.GetTimeFromPointer(fileObject.LastModified), false), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -826,10 +819,11 @@ func (fs *S3Fs) mkdirInternal(name string) error {
|
||||||
|
|
||||||
func (fs *S3Fs) hasContents(name string) (bool, error) {
|
func (fs *S3Fs) hasContents(name string) (bool, error) {
|
||||||
prefix := fs.getPrefix(name)
|
prefix := fs.getPrefix(name)
|
||||||
|
maxKeys := int32(2)
|
||||||
paginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{
|
paginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{
|
||||||
Bucket: aws.String(fs.config.Bucket),
|
Bucket: aws.String(fs.config.Bucket),
|
||||||
Prefix: aws.String(prefix),
|
Prefix: aws.String(prefix),
|
||||||
MaxKeys: 2,
|
MaxKeys: &maxKeys,
|
||||||
})
|
})
|
||||||
|
|
||||||
if paginator.HasMorePages() {
|
if paginator.HasMorePages() {
|
||||||
|
@ -923,7 +917,7 @@ func (fs *S3Fs) doMultipartCopy(source, target, contentType string, fileSize int
|
||||||
Bucket: aws.String(fs.config.Bucket),
|
Bucket: aws.String(fs.config.Bucket),
|
||||||
CopySource: aws.String(source),
|
CopySource: aws.String(source),
|
||||||
Key: aws.String(target),
|
Key: aws.String(target),
|
||||||
PartNumber: partNum,
|
PartNumber: &partNum,
|
||||||
UploadId: aws.String(uploadID),
|
UploadId: aws.String(uploadID),
|
||||||
CopySourceRange: aws.String(fmt.Sprintf("bytes=%d-%d", partStart, partEnd-1)),
|
CopySourceRange: aws.String(fmt.Sprintf("bytes=%d-%d", partStart, partEnd-1)),
|
||||||
})
|
})
|
||||||
|
@ -952,7 +946,7 @@ func (fs *S3Fs) doMultipartCopy(source, target, contentType string, fileSize int
|
||||||
partMutex.Lock()
|
partMutex.Lock()
|
||||||
completedParts = append(completedParts, types.CompletedPart{
|
completedParts = append(completedParts, types.CompletedPart{
|
||||||
ETag: partResp.CopyPartResult.ETag,
|
ETag: partResp.CopyPartResult.ETag,
|
||||||
PartNumber: partNum,
|
PartNumber: &partNum,
|
||||||
})
|
})
|
||||||
partMutex.Unlock()
|
partMutex.Unlock()
|
||||||
}(partNumber, start, end)
|
}(partNumber, start, end)
|
||||||
|
@ -965,7 +959,14 @@ func (fs *S3Fs) doMultipartCopy(source, target, contentType string, fileSize int
|
||||||
return copyError
|
return copyError
|
||||||
}
|
}
|
||||||
sort.Slice(completedParts, func(i, j int) bool {
|
sort.Slice(completedParts, func(i, j int) bool {
|
||||||
return completedParts[i].PartNumber < completedParts[j].PartNumber
|
getPartNumber := func(number *int32) int32 {
|
||||||
|
if number == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return *number
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPartNumber(completedParts[i].PartNumber) < getPartNumber(completedParts[j].PartNumber)
|
||||||
})
|
})
|
||||||
|
|
||||||
completeCtx, completeCancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
completeCtx, completeCancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||||
|
|
|
@ -92,154 +92,154 @@ CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
|
||||||
-----END EC PRIVATE KEY-----`
|
-----END EC PRIVATE KEY-----`
|
||||||
caCRT = `-----BEGIN CERTIFICATE-----
|
caCRT = `-----BEGIN CERTIFICATE-----
|
||||||
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
|
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
|
||||||
QXV0aDAeFw0yMjA3MDQxNTQzMTFaFw0yNDAxMDQxNTUzMDhaMBMxETAPBgNVBAMT
|
QXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT
|
||||||
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4eyDJkmW
|
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m
|
||||||
D4OVYo7ddgiZkd6QQdPyLcsa31Wc9jdR2/peEabyNT8jSWteS6ouY84GRlnhfFeZ
|
fi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1
|
||||||
mpXgbaUJu/Z8Y/8riPxwL8XF4vCScQDMywpQnVUd6E9x2/+/uaD4p/BBswgKqKPe
|
v03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh
|
||||||
uDcHZn7MkD4QlquUhMElDrBUi1Dv/AVHnQ6iP4vd5Jlv0F+40jdq/8Wa7yhW7Pu5
|
yzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL
|
||||||
iNvPwCk8HjENBKVur/re+Acif8A2TlbCsuOnVduSQNmnWH+iZmB9upyBZtUszGS0
|
CdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U
|
||||||
JhUwtSnwUX/JapF70Pwte/PV3RK8cJ5FjuAPNeTyJvSuMTELFSAyCeiNynFGgyhW
|
4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb
|
||||||
cqbEiPu6BURLculyVkmh4dOrhTrYZv/n3UJAhyxkdYrbh3INHmTa4izvclcuwoEo
|
KF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo
|
||||||
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
|
||||||
WwSa13hh6yjoDQ==
|
kK8u1YiiVenmoQ==
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
caCRL = `-----BEGIN X509 CRL-----
|
caCRL = `-----BEGIN X509 CRL-----
|
||||||
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
|
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
|
||||||
MjIwNzA0MTU1MzU4WhcNMjQwNzAzMTU1MzU4WjAkMCICEQDZo5Q3lhxFuDUsxGNm
|
MjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg
|
||||||
794YFw0yMjA3MDQxNTUzNThaoCMwITAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8Qvn
|
ZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r
|
||||||
bGzjmOULwDANBgkqhkiG9w0BAQsFAAOCAgEA1lK6g8qmhyY6myx8342dDuaauY03
|
rl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV
|
||||||
0iojkxpasuYcytK6XRm96YqjZK9EETxsHHViVU0vCXES60D6wJ9gw4fTWn3WxEdx
|
+jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199
|
||||||
nIwbGyjUGHh2y+R3uQsfvwxsdYvDsTLAnOLwOo68dAHWmMDZRmgTuGNoYFxVQRGR
|
XSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE
|
||||||
Cn90ZR7LPLpCScclWM8FE/W1B90x3ZE8EhJiCI/WyyTh3EgshmB7A5GoDrFZfmvR
|
jF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF
|
||||||
dzoTKO+F9p2XjtmgfiBE3czWQysfATmbutZUbG/ZRb89u+ZEUyPoC94mg8fhNWoX
|
DouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD
|
||||||
1d5G9QAkZFHp957/5QHLq9OHNfnWXoohhebjF4VWqZH7w+RtLc8t0PIog2lX4t1o
|
MSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4
|
||||||
5N/xFk9akvuoyNGg/fYuJBmN162Q0MdeYfYKDGWdXxf6fpHxVr5v2JrIx6gOwubb
|
iYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE
|
||||||
cIKP22ZBv/PYOeFsAZ755lTl4OTFUjU5ZJEPD6pUc1daaIqfxsxu8gDZP92FZjsB
|
ypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4
|
||||||
zaalMbh30n2OhagSMBzSLg5rE6WmBzlQX0ZN8YrW4l2Vq6twnnFHY+UyblRZS+d4
|
hVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94
|
||||||
oHBaoOaxPEkLxNZ8ulzJS4B6c4D1CXOaBEf++snVzRRUOEdX3x7TvkkrLvIsm06R
|
cp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67
|
||||||
ux0L1zJb9LbZ/1rhuv70z/kIlD55sqYuRqu3RpgTgZuTERU//rYIqWd03Y5Qon8i
|
sn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn
|
||||||
VoC6Yp9DPldQJrk=
|
w7bXJGfJcIMrrKs=
|
||||||
-----END X509 CRL-----`
|
-----END X509 CRL-----`
|
||||||
client1Crt = `-----BEGIN CERTIFICATE-----
|
client1Crt = `-----BEGIN CERTIFICATE-----
|
||||||
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
|
||||||
7LEmxYg=
|
MU3Bo0A=
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
client1Key = `-----BEGIN RSA PRIVATE KEY-----
|
client1Key = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
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
|
||||||
-----END RSA PRIVATE KEY-----`
|
-----END RSA PRIVATE KEY-----`
|
||||||
// client 2 crt is revoked
|
// client 2 crt is revoked
|
||||||
client2Crt = `-----BEGIN CERTIFICATE-----
|
client2Crt = `-----BEGIN CERTIFICATE-----
|
||||||
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
|
||||||
enqNrFI=
|
E0+r2+4=
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
client2Key = `-----BEGIN RSA PRIVATE KEY-----
|
client2Key = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
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
|
||||||
-----END RSA PRIVATE KEY-----`
|
-----END RSA PRIVATE KEY-----`
|
||||||
testFileName = "test_file_dav.dat"
|
testFileName = "test_file_dav.dat"
|
||||||
testDLFileName = "test_download_dav.dat"
|
testDLFileName = "test_download_dav.dat"
|
||||||
|
|
|
@ -33,7 +33,7 @@ paths:
|
||||||
200:
|
200:
|
||||||
description: successful operation
|
description: successful operation
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/FileInfo'
|
$ref: '#/components/schemas/FileInfo'
|
||||||
401:
|
401:
|
||||||
|
@ -350,7 +350,7 @@ paths:
|
||||||
200:
|
200:
|
||||||
description: successful operation
|
description: successful operation
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
@ -384,7 +384,7 @@ paths:
|
||||||
200:
|
200:
|
||||||
description: successful operation
|
description: successful operation
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -424,7 +424,7 @@ paths:
|
||||||
200:
|
200:
|
||||||
description: successful operation
|
description: successful operation
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -459,7 +459,7 @@ paths:
|
||||||
200:
|
200:
|
||||||
description: successful operation
|
description: successful operation
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/StatVFS'
|
$ref: '#/components/schemas/StatVFS'
|
||||||
401:
|
401:
|
||||||
|
@ -479,61 +479,61 @@ components:
|
||||||
OKResponse:
|
OKResponse:
|
||||||
description: successful operation
|
description: successful operation
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
BadRequest:
|
BadRequest:
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
Unauthorized:
|
Unauthorized:
|
||||||
description: Unauthorized
|
description: Unauthorized
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
Forbidden:
|
Forbidden:
|
||||||
description: Forbidden
|
description: Forbidden
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
NotFound:
|
NotFound:
|
||||||
description: Not Found
|
description: Not Found
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
NotImplemented:
|
NotImplemented:
|
||||||
description: Not Implemented
|
description: Not Implemented
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
Conflict:
|
Conflict:
|
||||||
description: Conflict
|
description: Conflict
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
RequestEntityTooLarge:
|
RequestEntityTooLarge:
|
||||||
description: Request Entity Too Large, max allowed size exceeded
|
description: Request Entity Too Large, max allowed size exceeded
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
InternalServerError:
|
InternalServerError:
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
DefaultResponse:
|
DefaultResponse:
|
||||||
description: Unexpected Error
|
description: Unexpected Error
|
||||||
content:
|
content:
|
||||||
application/json; charset=utf-8:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
schemas:
|
schemas:
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
NFPM_VERSION=2.30.1
|
NFPM_VERSION=2.32.0
|
||||||
NFPM_ARCH=${NFPM_ARCH:-amd64}
|
NFPM_ARCH=${NFPM_ARCH:-amd64}
|
||||||
if [ -z ${SFTPGO_VERSION} ]
|
if [ -z ${SFTPGO_VERSION} ]
|
||||||
then
|
then
|
||||||
|
|
|
@ -45,10 +45,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-login-image {
|
.bg-login-image {
|
||||||
background-image: url('{{.StaticURL}}{{.Branding.LoginImagePath}}');
|
background: url('{{.StaticURL}}{{.Branding.LoginImagePath}}');
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
padding: 1em;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row.login-image {
|
.row.login-image {
|
||||||
|
|
|
@ -28,12 +28,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
|
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header py-3">
|
<div class="card-header py-3">
|
||||||
|
|
|
@ -459,7 +459,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
$('#spinnerModal').modal('hide');
|
$('#spinnerModal').modal('hide');
|
||||||
|
@ -526,7 +526,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
$('#spinnerModal').modal('hide');
|
$('#spinnerModal').modal('hide');
|
||||||
|
|
|
@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header py-3">
|
<div class="card-header py-3">
|
||||||
|
|
|
@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header py-3">
|
<div class="card-header py-3">
|
||||||
<h6 class="m-0 font-weight-bold text-primary">View and manage auto blocklist</h6>
|
<h6 class="m-0 font-weight-bold text-primary">View and manage auto blocklist</h6>
|
||||||
|
@ -66,7 +71,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">Do you want to remoce the selected entry?</div>
|
<div class="modal-body">Do you want to remove the selected entry?</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
||||||
Cancel
|
Cancel
|
||||||
|
|
|
@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header py-3">
|
<div class="card-header py-3">
|
||||||
<h6 class="m-0 font-weight-bold text-primary">View and manage event actions</h6>
|
<h6 class="m-0 font-weight-bold text-primary">View and manage event actions</h6>
|
||||||
|
|
|
@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
|
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||||
<div id="successTxt" class="card-body"></div>
|
<div id="successTxt" class="card-body"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,12 +28,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header py-3">
|
<div class="card-header py-3">
|
||||||
<h6 class="m-0 font-weight-bold text-primary">Search logs</h6>
|
<h6 class="m-0 font-weight-bold text-primary">Search logs</h6>
|
||||||
|
|
|
@ -27,12 +27,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
|
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||||
<div id="successTxt" class="card-body"></div>
|
<div id="successTxt" class="card-body"></div>
|
||||||
|
|
|
@ -216,7 +216,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<input type="text" class="form-control" id="idS3KeyPrefix" name="s3_key_prefix" placeholder=""
|
<input type="text" class="form-control" id="idS3KeyPrefix" name="s3_key_prefix" placeholder=""
|
||||||
value="{{.S3Config.KeyPrefix}}" aria-describedby="S3KeyPrefixHelpBlock">
|
value="{{.S3Config.KeyPrefix}}" aria-describedby="S3KeyPrefixHelpBlock">
|
||||||
<small id="S3KeyPrefixHelpBlock" class="form-text text-muted">
|
<small id="S3KeyPrefixHelpBlock" class="form-text text-muted">
|
||||||
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
|
Similar to a chroot for local filesystem. Example: "somedir/subdir/".
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -298,7 +298,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<input type="text" class="form-control" id="idGCSKeyPrefix" name="gcs_key_prefix" placeholder=""
|
<input type="text" class="form-control" id="idGCSKeyPrefix" name="gcs_key_prefix" placeholder=""
|
||||||
value="{{.GCSConfig.KeyPrefix}}" aria-describedby="GCSKeyPrefixHelpBlock">
|
value="{{.GCSConfig.KeyPrefix}}" aria-describedby="GCSKeyPrefixHelpBlock">
|
||||||
<small id="GCSKeyPrefixHelpBlock" class="form-text text-muted">
|
<small id="GCSKeyPrefixHelpBlock" class="form-text text-muted">
|
||||||
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
|
Similar to a chroot for local filesystem. Example: "somedir/subdir/".
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -420,7 +420,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<input type="text" class="form-control" id="idAzKeyPrefix" name="az_key_prefix" placeholder=""
|
<input type="text" class="form-control" id="idAzKeyPrefix" name="az_key_prefix" placeholder=""
|
||||||
value="{{.AzBlobConfig.KeyPrefix}}" aria-describedby="AzKeyPrefixHelpBlock">
|
value="{{.AzBlobConfig.KeyPrefix}}" aria-describedby="AzKeyPrefixHelpBlock">
|
||||||
<small id="AzKeyPrefixHelpBlock" class="form-text text-muted">
|
<small id="AzKeyPrefixHelpBlock" class="form-text text-muted">
|
||||||
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
|
Similar to a chroot for local filesystem. Example: "somedir/subdir/".
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -222,7 +222,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
|
<h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
|
||||||
<p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories</p>
|
<p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories. Setting a denied pattern as "*" and allowed pattern/s for the same directory you can create denied except rules, but note that if you allow a directory, everything in it will be allowed unless more specific patterns/permissions are defined.</p>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-md-12 form_field_patterns_outer">
|
<div class="col-md-12 form_field_patterns_outer">
|
||||||
{{range $idx, $pattern := .Group.UserSettings.Filters.GetFlatFilePatterns -}}
|
{{range $idx, $pattern := .Group.UserSettings.Filters.GetFlatFilePatterns -}}
|
||||||
|
@ -554,105 +554,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-light mb-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<b>Per-source data transfer limits</b>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group row">
|
|
||||||
<div class="col-md-12 form_field_dtlimits_outer">
|
|
||||||
{{range $idx, $dtLimit := .Group.UserSettings.Filters.DataTransferLimits -}}
|
|
||||||
<div class="row form_field_dtlimits_outer_row">
|
|
||||||
<div class="form-group col-md-5">
|
|
||||||
<textarea class="form-control" id="idDataTransferLimitSources{{$idx}}" name="data_transfer_limit_sources{{$idx}}" rows="4" placeholder=""
|
|
||||||
aria-describedby="dtLimitSourcesHelpBlock{{$idx}}">{{$dtLimit.GetSourcesAsString}}</textarea>
|
|
||||||
<small id="dtLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
|
|
||||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idUploadTransferSource{{$idx}}" name="upload_data_transfer_source{{$idx}}"
|
|
||||||
placeholder="" value="{{$dtLimit.UploadDataTransfer}}" min="0" aria-describedby="ulDtHelpBlock{{$idx}}">
|
|
||||||
<small id="ulDtHelpBlock{{$idx}}" class="form-text text-muted">
|
|
||||||
UL (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idDownloadTransferSource{{$idx}}" name="download_data_transfer_source{{$idx}}"
|
|
||||||
placeholder="" value="{{$dtLimit.DownloadDataTransfer}}" min="0" aria-describedby="dlDtHelpBlock{{$idx}}">
|
|
||||||
<small id="dlDtHelpBlock{{$idx}}" class="form-text text-muted">
|
|
||||||
DL (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idTotalTransferSource{{$idx}}" name="total_data_transfer_source{{$idx}}"
|
|
||||||
placeholder="" value="{{$dtLimit.TotalDataTransfer}}" min="0" aria-describedby="totalDtHelpBlock{{$idx}}">
|
|
||||||
<small id="totalDtHelpBlock{{$idx}}" class="form-text text-muted">
|
|
||||||
Total (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-md-1">
|
|
||||||
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="row form_field_dtlimits_outer_row">
|
|
||||||
<div class="form-group col-md-5">
|
|
||||||
<textarea class="form-control" id="idDataTransferLimitSources0" name="data_transfer_limit_sources0" rows="4" placeholder=""
|
|
||||||
aria-describedby="dtLimitSourcesHelpBlock0"></textarea>
|
|
||||||
<small id="dtLimitSourcesHelpBlock0" class="form-text text-muted">
|
|
||||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idUploadTransferSource0" name="upload_data_transfer_source0"
|
|
||||||
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock0">
|
|
||||||
<small id="ulDtHelpBlock0" class="form-text text-muted">
|
|
||||||
UL (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idDownloadTransferSource0" name="download_data_transfer_source0"
|
|
||||||
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock0">
|
|
||||||
<small id="dlDtHelpBlock0" class="form-text text-muted">
|
|
||||||
DL (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idTotalTransferSource0" name="total_data_transfer_source0"
|
|
||||||
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock0">
|
|
||||||
<small id="totalDtHelpBlock0" class="form-text text-muted">
|
|
||||||
Total (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-md-1">
|
|
||||||
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mx-1">
|
|
||||||
<button type="button" class="btn btn-secondary add_new_dtlimit_field_btn">
|
|
||||||
<i class="fas fa-plus"></i> Add new data transfer limit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header py-3">
|
<div class="card-header py-3">
|
||||||
|
|
|
@ -27,12 +27,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header py-3">
|
<div class="card-header py-3">
|
||||||
<h6 class="m-0 font-weight-bold text-primary">View and manage IP Lists</h6>
|
<h6 class="m-0 font-weight-bold text-primary">View and manage IP Lists</h6>
|
||||||
|
@ -114,7 +119,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">Do you want to remoce the selected entry?</div>
|
<div class="modal-body">Do you want to remove the selected entry?</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
||||||
Cancel
|
Cancel
|
||||||
|
|
|
@ -31,12 +31,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<div id="successTOTPMsg" class="card mb-4 border-left-success" style="display: none;">
|
<div id="successTOTPMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||||
<div id="successTOTPTxt" class="card-body"></div>
|
<div id="successTOTPTxt" class="card-body"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="errorTOTPMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorTOTPMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTOTPTxt"></span>
|
<span id="errorTOTPTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorTOTPMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorTOTPMsg(){
|
||||||
|
$('#errorTOTPMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div>
|
<div>
|
||||||
<p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p>
|
<p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,12 +98,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<div id="successRecCodesMsg" class="card mb-4 border-left-success" style="display: none;">
|
<div id="successRecCodesMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||||
<div id="successRecCodesTxt" class="card-body"></div>
|
<div id="successRecCodesTxt" class="card-body"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="errorRecCodesMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorRecCodesMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorRecCodesTxt"></span>
|
<span id="errorRecCodesTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorRecCodesMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorRecCodesMsg(){
|
||||||
|
$('#errorRecCodesMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div>
|
<div>
|
||||||
<p>Recovery codes are a set of one time use codes that can be used in place of the TOTP to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate TOTP configuration.</p>
|
<p>Recovery codes are a set of one time use codes that can be used in place of the TOTP to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate TOTP configuration.</p>
|
||||||
<p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>
|
<p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>
|
||||||
|
@ -166,7 +176,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
|
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
$('.totpDisable').hide();
|
$('.totpDisable').hide();
|
||||||
|
@ -209,7 +219,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
|
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
totpSave();
|
totpSave();
|
||||||
|
@ -242,7 +252,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
|
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
$('#successTOTPTxt').text("Configuration saved");
|
$('#successTOTPTxt').text("Configuration saved");
|
||||||
|
@ -284,7 +294,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify({"enabled": false}),
|
data: JSON.stringify({"enabled": false}),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
location.reload();
|
location.reload();
|
||||||
|
@ -357,7 +367,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
$('.viewRecoveryCodes').hide();
|
$('.viewRecoveryCodes').hide();
|
||||||
|
|
|
@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header py-3">
|
<div class="card-header py-3">
|
||||||
|
|
|
@ -137,58 +137,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
$(this).closest(".form_field_bwlimits_outer_row").remove();
|
$(this).closest(".form_field_bwlimits_outer_row").remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("body").on("click", ".add_new_dtlimit_field_btn", function () {
|
|
||||||
let index = $(".form_field_dtlimits_outer").find(".form_field_dtlimits_outer_row").length;
|
|
||||||
while (document.getElementById("idDataTransferLimitSources"+index) != null){
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
$(".form_field_dtlimits_outer").append(`
|
|
||||||
<div class="row form_field_dtlimits_outer_row">
|
|
||||||
<div class="form-group col-md-5">
|
|
||||||
<textarea class="form-control" id="idDataTransferLimitSources${index}" name="data_transfer_limit_sources${index}" rows="4" placeholder=""
|
|
||||||
aria-describedby="dtLimitSourcesHelpBlock${index}"></textarea>
|
|
||||||
<small id="dtLimitSourcesHelpBlock${index}" class="form-text text-muted">
|
|
||||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idUploadTransferSource${index}" name="upload_data_transfer_source${index}"
|
|
||||||
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock${index}">
|
|
||||||
<small id="ulDtHelpBlock${index}" class="form-text text-muted">
|
|
||||||
UL (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idDownloadTransferSource${index}" name="download_data_transfer_source${index}"
|
|
||||||
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock${index}">
|
|
||||||
<small id="dlDtHelpBlock${index}" class="form-text text-muted">
|
|
||||||
DL (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idTotalTransferSource${index}" name="total_data_transfer_source${index}"
|
|
||||||
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock${index}">
|
|
||||||
<small id="totalDtHelpBlock${index}" class="form-text text-muted">
|
|
||||||
Total (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-md-1">
|
|
||||||
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on("click", ".remove_dtlimit_btn_frm_field", function () {
|
|
||||||
$(this).closest(".form_field_dtlimits_outer_row").remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on("click", ".add_new_pattern_field_btn", function () {
|
$("body").on("click", ".add_new_pattern_field_btn", function () {
|
||||||
let index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
|
let index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
|
||||||
while (document.getElementById("idPatternPath"+index) != null){
|
while (document.getElementById("idPatternPath"+index) != null){
|
||||||
|
|
|
@ -46,10 +46,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<br>
|
<br>
|
||||||
Fingerprint: "{{.Fingerprint}}"
|
Fingerprint: "{{.Fingerprint}}"
|
||||||
<br>
|
<br>
|
||||||
|
Algorithms: "{{.GetAlgosAsString}}"
|
||||||
|
<br>
|
||||||
{{end}}
|
{{end}}
|
||||||
<br>
|
<br>
|
||||||
Host Key algorithms: "{{.Status.SSH.GetHostKeyAlgosAsString}}"
|
|
||||||
<br><br>
|
|
||||||
MAC algorithms: "{{.Status.SSH.GetMACsAsString}}"
|
MAC algorithms: "{{.Status.SSH.GetMACsAsString}}"
|
||||||
<br><br>
|
<br><br>
|
||||||
KEX algorithms: "{{.Status.SSH.GetKEXsAsString}}"
|
KEX algorithms: "{{.Status.SSH.GetKEXsAsString}}"
|
||||||
|
|
|
@ -502,7 +502,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
|
<h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
|
||||||
<p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories</p>
|
<p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories. Setting a denied pattern as "*" and allowed pattern/s for the same directory you can create denied except rules, but note that if you allow a directory, everything in it will be allowed unless more specific patterns/permissions are defined.</p>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-md-12 form_field_patterns_outer">
|
<div class="col-md-12 form_field_patterns_outer">
|
||||||
{{range $idx, $pattern := .User.Filters.GetFlatFilePatterns -}}
|
{{range $idx, $pattern := .User.Filters.GetFlatFilePatterns -}}
|
||||||
|
@ -834,105 +834,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-light mb-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<b>Per-source data transfer limits</b>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group row">
|
|
||||||
<div class="col-md-12 form_field_dtlimits_outer">
|
|
||||||
{{range $idx, $dtLimit := .User.Filters.DataTransferLimits -}}
|
|
||||||
<div class="row form_field_dtlimits_outer_row">
|
|
||||||
<div class="form-group col-md-5">
|
|
||||||
<textarea class="form-control" id="idDataTransferLimitSources{{$idx}}" name="data_transfer_limit_sources{{$idx}}" rows="4" placeholder=""
|
|
||||||
aria-describedby="dtLimitSourcesHelpBlock{{$idx}}">{{$dtLimit.GetSourcesAsString}}</textarea>
|
|
||||||
<small id="dtLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
|
|
||||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idUploadTransferSource{{$idx}}" name="upload_data_transfer_source{{$idx}}"
|
|
||||||
placeholder="" value="{{$dtLimit.UploadDataTransfer}}" min="0" aria-describedby="ulDtHelpBlock{{$idx}}">
|
|
||||||
<small id="ulDtHelpBlock{{$idx}}" class="form-text text-muted">
|
|
||||||
UL (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idDownloadTransferSource{{$idx}}" name="download_data_transfer_source{{$idx}}"
|
|
||||||
placeholder="" value="{{$dtLimit.DownloadDataTransfer}}" min="0" aria-describedby="dlDtHelpBlock{{$idx}}">
|
|
||||||
<small id="dlDtHelpBlock{{$idx}}" class="form-text text-muted">
|
|
||||||
DL (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idTotalTransferSource{{$idx}}" name="total_data_transfer_source{{$idx}}"
|
|
||||||
placeholder="" value="{{$dtLimit.TotalDataTransfer}}" min="0" aria-describedby="totalDtHelpBlock{{$idx}}">
|
|
||||||
<small id="totalDtHelpBlock{{$idx}}" class="form-text text-muted">
|
|
||||||
Total (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-md-1">
|
|
||||||
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="row form_field_dtlimits_outer_row">
|
|
||||||
<div class="form-group col-md-5">
|
|
||||||
<textarea class="form-control" id="idDataTransferLimitSources0" name="data_transfer_limit_sources0" rows="4" placeholder=""
|
|
||||||
aria-describedby="dtLimitSourcesHelpBlock0"></textarea>
|
|
||||||
<small id="dtLimitSourcesHelpBlock0" class="form-text text-muted">
|
|
||||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idUploadTransferSource0" name="upload_data_transfer_source0"
|
|
||||||
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock0">
|
|
||||||
<small id="ulDtHelpBlock0" class="form-text text-muted">
|
|
||||||
UL (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idDownloadTransferSource0" name="download_data_transfer_source0"
|
|
||||||
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock0">
|
|
||||||
<small id="dlDtHelpBlock0" class="form-text text-muted">
|
|
||||||
DL (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" class="form-control" id="idTotalTransferSource0" name="total_data_transfer_source0"
|
|
||||||
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock0">
|
|
||||||
<small id="totalDtHelpBlock0" class="form-text text-muted">
|
|
||||||
Total (MB). 0 means no limit
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-md-1">
|
|
||||||
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mx-1">
|
|
||||||
<button type="button" class="btn btn-secondary add_new_dtlimit_field_btn">
|
|
||||||
<i class="fas fa-plus"></i> Add new data transfer limit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,12 +28,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
|
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
|
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||||
<div id="successTxt" class="card-body"></div>
|
<div id="successTxt" class="card-body"></div>
|
||||||
|
|
|
@ -42,16 +42,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h6 class="d-flex justify-content-between align-items-center">
|
<h6 class="d-flex justify-content-between align-items-center">
|
||||||
<span class="font-weight-bold text-primary">Edit file "{{.Path}}"</span>
|
<span class="font-weight-bold text-primary">{{- if .ReadOnly}}View{{- else}}Edit{{- end}} file "{{.Path}}"</span>
|
||||||
<span class="btn-toolbar">
|
<span class="btn-toolbar">
|
||||||
<a id="idBack" class="btn btn-secondary mx-1 my-1" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a>
|
<a id="idBack" class="btn btn-secondary mx-1 my-1" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a>
|
||||||
{{if not .ReadOnly}}
|
{{if not .ReadOnly}}
|
||||||
|
|
|
@ -34,6 +34,82 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</style>
|
</style>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "additionalnavitems"}}
|
||||||
|
{{if .QuotaUsage.HasQuotaInfo}}
|
||||||
|
<li class="nav-item dropdown no-arrow mx-1">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="quotaDropdown" role="button"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="mr-2 d-none d-lg-inline text-gray-600 small">Quota</span>
|
||||||
|
{{if .QuotaUsage.IsQuotaLow}}
|
||||||
|
<i class="fas fa-exclamation-triangle fa-fw"></i>
|
||||||
|
{{else}}
|
||||||
|
<i class="fas fa-info fa-fw"></i>
|
||||||
|
{{end}}
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
|
||||||
|
ria-labelledby="alertsDropdown">
|
||||||
|
<h6 class="dropdown-header">
|
||||||
|
Quota usage
|
||||||
|
</h6>
|
||||||
|
{{ if .QuotaUsage.HasDiskQuota}}
|
||||||
|
<a class="dropdown-item d-flex align-items-center" href="#">
|
||||||
|
<div class="mr-3">
|
||||||
|
<div class="icon-circle {{if .QuotaUsage.IsDiskQuotaLow}}bg-warning{{else}}bg-success{{end}}">
|
||||||
|
<i class="fas fa-hdd text-white"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="small text-gray-500">Disk quota</div>
|
||||||
|
{{$size := .QuotaUsage.GetQuotaSize}}
|
||||||
|
{{$files := .QuotaUsage.GetQuotaFiles}}
|
||||||
|
{{if $size}}
|
||||||
|
{{$percentage := .QuotaUsage.GetQuotaSizePercentage}}
|
||||||
|
<span class="font-weight-bold {{if .QuotaUsage.IsQuotaSizeLow}}text-warning{{end}}">Size: {{$size}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
|
||||||
|
{{if $files}}<br>{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{if $files}}
|
||||||
|
{{$percentage := .QuotaUsage.GetQuotaFilesPercentage}}
|
||||||
|
<span class="font-weight-bold {{if .QuotaUsage.IsQuotaFilesLow}}text-warning{{end}}">Files: {{$files}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{ if .QuotaUsage.HasTranferQuota}}
|
||||||
|
<a class="dropdown-item d-flex align-items-center" href="#">
|
||||||
|
<div class="mr-3">
|
||||||
|
<div class="icon-circle {{if .QuotaUsage.IsTransferQuotaLow}}bg-warning{{else}}bg-success{{end}}">
|
||||||
|
<i class="fas fa-exchange-alt text-white"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="small text-gray-500">Transfer quota</div>
|
||||||
|
{{$total := .QuotaUsage.GetTotalTransferQuota}}
|
||||||
|
{{$upload := .QuotaUsage.GetUploadTransferQuota}}
|
||||||
|
{{$download := .QuotaUsage.GetDownloadTransferQuota}}
|
||||||
|
{{if $total}}
|
||||||
|
{{$percentage := .QuotaUsage.GetTotalTransferQuotaPercentage}}
|
||||||
|
<span class="font-weight-bold {{if .QuotaUsage.IsTotalTransferQuotaLow}}text-warning{{end}}">Total: {{$total}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
|
||||||
|
{{if or $upload $download}}<br>{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{if $download}}
|
||||||
|
{{$percentage := .QuotaUsage.GetDownloadTransferQuotaPercentage}}
|
||||||
|
<span class="font-weight-bold {{if .QuotaUsage.IsDownloadTransferQuotaLow}}text-warning{{end}}">Download: {{$download}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
|
||||||
|
{{if $upload}}<br>{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{if $upload}}
|
||||||
|
{{$percentage := .QuotaUsage.GetUploadTransferQuotaPercentage}}
|
||||||
|
<span class="font-weight-bold {{if .QuotaUsage.IsUploadTransferQuotaLow}}text-warning{{end}}">Upload: {{$upload}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<div class="topbar-divider d-none d-sm-block"></div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
|
@ -1182,7 +1258,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
if (row["type"] == "1") {
|
if (row["type"] == "1") {
|
||||||
return `<i class="fas fa-folder"></i> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
return `<i class="fas fa-folder"></i> <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> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
return `<i class="fas fa-external-link-alt"></i> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
||||||
}
|
}
|
||||||
let icon = getIconForFile(data);
|
let icon = getIconForFile(data);
|
||||||
|
@ -1195,7 +1271,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"data": "size",
|
"data": "size",
|
||||||
"render": function (data, type, row) {
|
"render": function (data, type, row) {
|
||||||
if (type === 'display') {
|
if (type === 'display') {
|
||||||
if (data){
|
if (data || data === 0){
|
||||||
return fileSizeIEC(data);
|
return fileSizeIEC(data);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
|
|
|
@ -31,12 +31,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<div id="successTOTPMsg" class="card mb-4 border-left-success" style="display: none;">
|
<div id="successTOTPMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||||
<div id="successTOTPTxt" class="card-body"></div>
|
<div id="successTOTPTxt" class="card-body"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="errorTOTPMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorTOTPMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTOTPTxt"></span>
|
<span id="errorTOTPTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorTOTPMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorTOTPMsg(){
|
||||||
|
$('#errorTOTPMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div>
|
<div>
|
||||||
<p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p>
|
<p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,12 +123,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<div id="successRecCodesMsg" class="card mb-4 border-left-success" style="display: none;">
|
<div id="successRecCodesMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||||
<div id="successRecCodesTxt" class="card-body"></div>
|
<div id="successRecCodesTxt" class="card-body"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="errorRecCodesMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorRecCodesMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorRecCodesTxt"></span>
|
<span id="errorRecCodesTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorRecCodesMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorRecCodesMsg(){
|
||||||
|
$('#errorRecCodesMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div>
|
<div>
|
||||||
<p>Recovery codes are a set of one time use codes that can be used in place of the TOTP to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate TOTP configuration.</p>
|
<p>Recovery codes are a set of one time use codes that can be used in place of the TOTP to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate TOTP configuration.</p>
|
||||||
<p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>
|
<p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>
|
||||||
|
@ -192,7 +202,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
|
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
$('.totpDisable').hide();
|
$('.totpDisable').hide();
|
||||||
|
@ -236,7 +246,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
|
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
totpSave();
|
totpSave();
|
||||||
|
@ -273,7 +283,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "protocols": protocolsArray, "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
|
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "protocols": protocolsArray, "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
$('#successTOTPTxt').text("Configuration saved");
|
$('#successTOTPTxt').text("Configuration saved");
|
||||||
|
@ -318,7 +328,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify({"protocols": protocolsArray}),
|
data: JSON.stringify({"protocols": protocolsArray}),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
$('#successTOTPTxt').text("Protocols updated");
|
$('#successTOTPTxt').text("Protocols updated");
|
||||||
|
@ -356,7 +366,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
data: JSON.stringify({"enabled": false}),
|
data: JSON.stringify({"enabled": false}),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
location.reload();
|
location.reload();
|
||||||
|
@ -429,7 +439,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
success: function (result) {
|
||||||
$('.viewRecoveryCodes').hide();
|
$('.viewRecoveryCodes').hide();
|
||||||
|
|
|
@ -46,12 +46,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div id="tableContainer" class="table-responsive">
|
<div id="tableContainer" class="table-responsive">
|
||||||
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
|
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -485,7 +490,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
if (row["type"] == "1") {
|
if (row["type"] == "1") {
|
||||||
return `<i class="fas fa-folder"></i> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
return `<i class="fas fa-folder"></i> <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> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
return `<i class="fas fa-external-link-alt"></i> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
||||||
}
|
}
|
||||||
var icon = getIconForFile(data);
|
var icon = getIconForFile(data);
|
||||||
|
@ -498,7 +503,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"data": "size",
|
"data": "size",
|
||||||
"render": function (data, type, row) {
|
"render": function (data, type, row) {
|
||||||
if (type === 'display') {
|
if (type === 'display') {
|
||||||
if (data){
|
if (data || data === 0){
|
||||||
return fileSizeIEC(data);
|
return fileSizeIEC(data);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
|
|
|
@ -26,12 +26,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "page_body"}}
|
{{define "page_body"}}
|
||||||
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
|
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||||
<span id="errorTxt"></span>
|
<span id="errorTxt"></span>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function dismissErrorMsg(){
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header py-3">
|
<div class="card-header py-3">
|
||||||
|
|
|
@ -99,3 +99,15 @@ Filename: "netsh"; Parameters: "advfirewall firewall delete rule name=""SFTPGo S
|
||||||
|
|
||||||
[Messages]
|
[Messages]
|
||||||
FinishedLabel=Setup has finished installing SFTPGo on your computer. SFTPGo should already be running as a Windows service, it uses TCP port 8080 for HTTP service and TCP port 2022 for SFTP service by default, make sure the configured ports are not used by other services or edit the configuration according to your needs.
|
FinishedLabel=Setup has finished installing SFTPGo on your computer. SFTPGo should already be running as a Windows service, it uses TCP port 8080 for HTTP service and TCP port 2022 for SFTP service by default, make sure the configured ports are not used by other services or edit the configuration according to your needs.
|
||||||
|
|
||||||
|
[Code]
|
||||||
|
|
||||||
|
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||||
|
var
|
||||||
|
Code: Integer;
|
||||||
|
begin
|
||||||
|
if (FileExists(ExpandConstant('{app}\{#MyAppExeName}'))) then
|
||||||
|
begin
|
||||||
|
Exec(ExpandConstant('{app}\{#MyAppExeName}'), 'service stop', '', SW_HIDE, ewWaitUntilTerminated, Code);
|
||||||
|
end
|
||||||
|
end;
|
Loading…
Reference in a new issue