REST API: expose OpenAPI schema and render it using Swagger UI

Fixes #609
This commit is contained in:
Nicola Murino 2021-11-21 09:32:51 +01:00
parent fb8f013ea7
commit 3d6b09e949
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
33 changed files with 244 additions and 51 deletions

View file

@ -98,30 +98,13 @@ jobs:
cp sftpgo.json output/ cp sftpgo.json output/
cp -r templates output/ cp -r templates output/
cp -r static output/ cp -r static output/
cp -r openapi output/
cp init/com.github.drakkan.sftpgo.plist output/init/ cp init/com.github.drakkan.sftpgo.plist output/init/
./sftpgo gen completion bash > output/bash_completion/sftpgo ./sftpgo gen completion bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1 ./sftpgo gen man -d output/man/man1
gzip output/man/man1/* gzip output/man/man1/*
- name: Prepare build artifact for Windows
if: startsWith(matrix.os, 'windows-')
run: |
mkdir output
copy .\sftpgo.exe .\output
copy .\sftpgo.json .\output
mkdir output\templates
xcopy .\templates .\output\templates\ /E
mkdir output\static
xcopy .\static .\output\static\ /E
- name: Upload build artifact
if: startsWith(matrix.os, 'ubuntu-') != true
uses: actions/upload-artifact@v2
with:
name: sftpgo-${{ matrix.os }}-go-${{ matrix.go }}
path: output
- name: Prepare Windows installer - name: Prepare Windows installer
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }} if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
run: | run: |
@ -135,6 +118,8 @@ jobs:
xcopy .\templates .\output\templates\ /E xcopy .\templates .\output\templates\ /E
mkdir output\static mkdir output\static
xcopy .\static .\output\static\ /E xcopy .\static .\output\static\ /E
mkdir output\openapi
xcopy .\openapi .\output\openapi\ /E
$LATEST_TAG = ((git describe --tags $(git rev-list --tags --max-count=1)) | Out-String).Trim() $LATEST_TAG = ((git describe --tags $(git rev-list --tags --max-count=1)) | Out-String).Trim()
$REV_LIST=$LATEST_TAG+"..HEAD" $REV_LIST=$LATEST_TAG+"..HEAD"
$COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim() $COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim()
@ -143,8 +128,9 @@ jobs:
[IO.File]::WriteAllBytes($CERT_PATH,[System.Convert]::FromBase64String($Env:CERT_DATA)) [IO.File]::WriteAllBytes($CERT_PATH,[System.Convert]::FromBase64String($Env:CERT_DATA))
certutil -f -p "$Env:CERT_PASS" -importpfx MY "$CERT_PATH" certutil -f -p "$Env:CERT_PASS" -importpfx MY "$CERT_PATH"
rm "$CERT_PATH" rm "$CERT_PATH"
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\sftpgo.exe
$INNO_S='/Ssigntool=$qC:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe$q sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n $qNicola Murino$q /d $qSFTPGo$q $f' $INNO_S='/Ssigntool=$qC:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe$q sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n $qNicola Murino$q /d $qSFTPGo$q $f'
iscc "$INNO_S" windows-installer\sftpgo.iss iscc "$INNO_S" .\windows-installer\sftpgo.iss
certutil -delstore MY "Nicola Murino" certutil -delstore MY "Nicola Murino"
env: env:
CERT_DATA: ${{ secrets.CERT_DATA }} CERT_DATA: ${{ secrets.CERT_DATA }}
@ -157,6 +143,27 @@ jobs:
name: sftpgo_windows_installer_x86_64 name: sftpgo_windows_installer_x86_64
path: ./sftpgo_windows_x86_64.exe path: ./sftpgo_windows_x86_64.exe
- name: Prepare build artifact for Windows
if: startsWith(matrix.os, 'windows-')
run: |
Remove-Item -LiteralPath "output" -Force -Recurse -ErrorAction Ignore
mkdir output
copy .\sftpgo.exe .\output
copy .\sftpgo.json .\output
mkdir output\templates
xcopy .\templates .\output\templates\ /E
mkdir output\static
xcopy .\static .\output\static\ /E
mkdir output\openapi
xcopy .\openapi .\output\openapi\ /E
- name: Upload build artifact
if: startsWith(matrix.os, 'ubuntu-') != true
uses: actions/upload-artifact@v2
with:
name: sftpgo-${{ matrix.os }}-go-${{ matrix.go }}
path: output
test-goarch-386: test-goarch-386:
name: Run test cases on 32-bit arch name: Run test cases on 32-bit arch
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -308,6 +315,7 @@ jobs:
cp sftpgo.json output/ cp sftpgo.json output/
cp -r templates output/ cp -r templates output/
cp -r static output/ cp -r static output/
cp -r openapi output/
cp init/sftpgo.service output/init/ cp init/sftpgo.service output/init/
./sftpgo gen completion bash > output/bash_completion/sftpgo ./sftpgo gen completion bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
@ -354,6 +362,7 @@ jobs:
cp sftpgo.json output/ cp sftpgo.json output/
cp -r templates output/ cp -r templates output/
cp -r static output/ cp -r static output/
cp -r openapi output/
cp init/sftpgo.service output/init/ cp init/sftpgo.service output/init/
./sftpgo gen completion bash > output/bash_completion/sftpgo ./sftpgo gen completion bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo

View file

@ -105,6 +105,7 @@ jobs:
cp sftpgo.json output/ cp sftpgo.json output/
cp sftpgo.db output/sqlite/ cp sftpgo.db output/sqlite/
cp -r static output/ cp -r static output/
cp -r openapi output/
cp -r templates output/ cp -r templates output/
cp init/com.github.drakkan.sftpgo.plist output/init/ cp init/com.github.drakkan.sftpgo.plist output/init/
./sftpgo gen completion bash > output/bash_completion/sftpgo ./sftpgo gen completion bash > output/bash_completion/sftpgo
@ -134,13 +135,25 @@ jobs:
xcopy .\templates .\output\templates\ /E xcopy .\templates .\output\templates\ /E
mkdir output\static mkdir output\static
xcopy .\static .\output\static\ /E xcopy .\static .\output\static\ /E
mkdir output\openapi
xcopy .\openapi .\output\openapi\ /E
$CERT_PATH=(Get-Location -PSProvider FileSystem).ProviderPath + "\cert.pfx" $CERT_PATH=(Get-Location -PSProvider FileSystem).ProviderPath + "\cert.pfx"
[IO.File]::WriteAllBytes($CERT_PATH,[System.Convert]::FromBase64String($Env:CERT_DATA)) [IO.File]::WriteAllBytes($CERT_PATH,[System.Convert]::FromBase64String($Env:CERT_DATA))
certutil -f -p "$Env:CERT_PASS" -importpfx MY "$CERT_PATH" certutil -f -p "$Env:CERT_PASS" -importpfx MY "$CERT_PATH"
rm "$CERT_PATH" rm "$CERT_PATH"
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\sftpgo.exe
$INNO_S='/Ssigntool=$qC:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe$q sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n $qNicola Murino$q /d $qSFTPGo$q $f' $INNO_S='/Ssigntool=$qC:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe$q sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n $qNicola Murino$q /d $qSFTPGo$q $f'
iscc "$INNO_S" windows-installer\sftpgo.iss iscc "$INNO_S" .\windows-installer\sftpgo.iss
certutil -delstore MY "Nicola Murino"
env:
SFTPGO_ISS_VERSION: ${{ steps.get_version.outputs.VERSION }}
SFTPGO_ISS_DOC_URL: https://github.com/drakkan/sftpgo/blob/${{ steps.get_version.outputs.VERSION }}/README.md
CERT_DATA: ${{ secrets.CERT_DATA }}
CERT_PASS: ${{ secrets.CERT_PASS }}
- name: Prepare Portable Release for Windows
if: startsWith(matrix.os, 'windows-')
run: |
mkdir win-portable mkdir win-portable
copy .\sftpgo.exe .\win-portable copy .\sftpgo.exe .\win-portable
copy .\sftpgo.json .\win-portable copy .\sftpgo.json .\win-portable
@ -150,14 +163,9 @@ jobs:
xcopy .\templates .\win-portable\templates\ /E xcopy .\templates .\win-portable\templates\ /E
mkdir win-portable\static mkdir win-portable\static
xcopy .\static .\win-portable\static\ /E xcopy .\static .\win-portable\static\ /E
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\win-portable\sftpgo.exe mkdir win-portable\openapi
xcopy .\openapi .\win-portable\openapi\ /E
Compress-Archive .\win-portable\* sftpgo_portable_x86_64.zip Compress-Archive .\win-portable\* sftpgo_portable_x86_64.zip
certutil -delstore MY "Nicola Murino"
env:
SFTPGO_ISS_VERSION: ${{ steps.get_version.outputs.VERSION }}
SFTPGO_ISS_DOC_URL: https://github.com/drakkan/sftpgo/blob/${{ steps.get_version.outputs.VERSION }}/README.md
CERT_DATA: ${{ secrets.CERT_DATA }}
CERT_PASS: ${{ secrets.CERT_PASS }}
- name: Upload macOS x86_64 artifact - name: Upload macOS x86_64 artifact
if: startsWith(matrix.os, 'macos-') if: startsWith(matrix.os, 'macos-')
@ -250,6 +258,7 @@ jobs:
cp sftpgo.json output/ cp sftpgo.json output/
cp -r templates output/ cp -r templates output/
cp -r static output/ cp -r static output/
cp -r openapi output/
cp init/sftpgo.service output/init/ cp init/sftpgo.service output/init/
./sftpgo initprovider ./sftpgo initprovider
./sftpgo gen completion bash > output/bash_completion/sftpgo ./sftpgo gen completion bash > output/bash_completion/sftpgo
@ -297,6 +306,7 @@ jobs:
cp sftpgo.json output/ cp sftpgo.json output/
cp -r templates output/ cp -r templates output/
cp -r static output/ cp -r static output/
cp -r openapi output/
cp init/sftpgo.service output/init/ cp init/sftpgo.service output/init/
./sftpgo initprovider ./sftpgo initprovider
./sftpgo gen completion bash > output/bash_completion/sftpgo ./sftpgo gen completion bash > output/bash_completion/sftpgo

View file

@ -42,6 +42,7 @@ RUN groupadd --system -g 1000 sftpgo && \
COPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json COPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
COPY --from=builder /workspace/static /usr/share/sftpgo/static COPY --from=builder /workspace/static /usr/share/sftpgo/static
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
COPY --from=builder /workspace/sftpgo /usr/local/bin/ COPY --from=builder /workspace/sftpgo /usr/local/bin/
# Log to the stdout so the logs will be available using docker logs # Log to the stdout so the logs will be available using docker logs
@ -50,6 +51,7 @@ ENV SFTPGO_LOG_FILE_PATH=""
ENV SFTPGO_HTTPD__TEMPLATES_PATH=/usr/share/sftpgo/templates ENV SFTPGO_HTTPD__TEMPLATES_PATH=/usr/share/sftpgo/templates
ENV SFTPGO_SMTP__TEMPLATES_PATH=/usr/share/sftpgo/templates ENV SFTPGO_SMTP__TEMPLATES_PATH=/usr/share/sftpgo/templates
ENV SFTPGO_HTTPD__STATIC_FILES_PATH=/usr/share/sftpgo/static ENV SFTPGO_HTTPD__STATIC_FILES_PATH=/usr/share/sftpgo/static
ENV SFTPGO_HTTPD__OPENAPI_PATH=/usr/share/sftpgo/openapi
# Modify the default configuration file # Modify the default configuration file
RUN sed -i "s|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|" /etc/sftpgo/sftpgo.json && \ RUN sed -i "s|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|" /etc/sftpgo/sftpgo.json && \

View file

@ -47,6 +47,7 @@ RUN addgroup -g 1000 -S sftpgo && \
COPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json COPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
COPY --from=builder /workspace/static /usr/share/sftpgo/static COPY --from=builder /workspace/static /usr/share/sftpgo/static
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
COPY --from=builder /workspace/sftpgo /usr/local/bin/ COPY --from=builder /workspace/sftpgo /usr/local/bin/
# Log to the stdout so the logs will be available using docker logs # Log to the stdout so the logs will be available using docker logs
@ -55,6 +56,7 @@ ENV SFTPGO_LOG_FILE_PATH=""
ENV SFTPGO_HTTPD__TEMPLATES_PATH=/usr/share/sftpgo/templates ENV SFTPGO_HTTPD__TEMPLATES_PATH=/usr/share/sftpgo/templates
ENV SFTPGO_SMTP__TEMPLATES_PATH=/usr/share/sftpgo/templates ENV SFTPGO_SMTP__TEMPLATES_PATH=/usr/share/sftpgo/templates
ENV SFTPGO_HTTPD__STATIC_FILES_PATH=/usr/share/sftpgo/static ENV SFTPGO_HTTPD__STATIC_FILES_PATH=/usr/share/sftpgo/static
ENV SFTPGO_HTTPD__OPENAPI_PATH=/usr/share/sftpgo/openapi
# Modify the default configuration file # Modify the default configuration file
RUN sed -i "s|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|" /etc/sftpgo/sftpgo.json && \ RUN sed -i "s|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|" /etc/sftpgo/sftpgo.json && \

View file

@ -40,6 +40,7 @@ COPY --from=builder --chown=1000:1000 /var/lib/sftpgo /var/lib/sftpgo
COPY --from=builder --chown=1000:1000 /workspace/sftpgo.json /etc/sftpgo/sftpgo.json COPY --from=builder --chown=1000:1000 /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
COPY --from=builder /workspace/static /usr/share/sftpgo/static COPY --from=builder /workspace/static /usr/share/sftpgo/static
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
COPY --from=builder /workspace/sftpgo /usr/local/bin/ COPY --from=builder /workspace/sftpgo /usr/local/bin/
COPY --from=builder /etc/mime.types /etc/mime.types COPY --from=builder /etc/mime.types /etc/mime.types
@ -49,6 +50,7 @@ ENV SFTPGO_LOG_FILE_PATH=""
ENV SFTPGO_HTTPD__TEMPLATES_PATH=/usr/share/sftpgo/templates ENV SFTPGO_HTTPD__TEMPLATES_PATH=/usr/share/sftpgo/templates
ENV SFTPGO_SMTP__TEMPLATES_PATH=/usr/share/sftpgo/templates ENV SFTPGO_SMTP__TEMPLATES_PATH=/usr/share/sftpgo/templates
ENV SFTPGO_HTTPD__STATIC_FILES_PATH=/usr/share/sftpgo/static ENV SFTPGO_HTTPD__STATIC_FILES_PATH=/usr/share/sftpgo/static
ENV SFTPGO_HTTPD__OPENAPI_PATH=/usr/share/sftpgo/openapi
# These env vars are required to avoid the following error when calling user.Current(): # These env vars are required to avoid the following error when calling user.Current():
# unable to get the current user: user: Current requires cgo or $USER set in environment # unable to get the current user: user: Current requires cgo or $USER set in environment
ENV USER=sftpgo ENV USER=sftpgo

View file

@ -194,7 +194,7 @@ After starting SFTPGo you can manage users and folders using:
To support embedded data providers like `bolt` and `SQLite` we can't have a CLI that directly write users and folders to the data provider, we always have to use the REST API. To support embedded data providers like `bolt` and `SQLite` we can't have a CLI that directly write users and folders to the data provider, we always have to use the REST API.
Full details for users, folders, admins and other resources are documented in the [OpenAPI](/httpd/schema/openapi.yaml) schema. If you want to render the schema without importing it manually, you can explore it on [Stoplight](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml). Full details for users, folders, admins and other resources are documented in the [OpenAPI](/openapi/openapi.yaml) schema. If you want to render the schema without importing it manually, you can explore it on [Stoplight](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml).
## Tutorials ## Tutorials

View file

@ -78,6 +78,7 @@ var (
TLSCipherSuites: nil, TLSCipherSuites: nil,
ProxyAllowed: nil, ProxyAllowed: nil,
HideLoginURL: 0, HideLoginURL: 0,
RenderOpenAPI: true,
} }
defaultRateLimiter = common.RateLimiterConfig{ defaultRateLimiter = common.RateLimiterConfig{
Average: 0, Average: 0,
@ -273,6 +274,7 @@ func Init() {
TemplatesPath: "templates", TemplatesPath: "templates",
StaticFilesPath: "static", StaticFilesPath: "static",
BackupsPath: "backups", BackupsPath: "backups",
OpenAPIPath: "openapi",
WebRoot: "", WebRoot: "",
CertificateFile: "", CertificateFile: "",
CertificateKeyFile: "", CertificateKeyFile: "",
@ -992,6 +994,7 @@ func getHTTPDBindingFromEnv(idx int) {
binding := httpd.Binding{ binding := httpd.Binding{
EnableWebAdmin: true, EnableWebAdmin: true,
EnableWebClient: true, EnableWebClient: true,
RenderOpenAPI: true,
} }
if len(globalConf.HTTPDConfig.Bindings) > idx { if len(globalConf.HTTPDConfig.Bindings) > idx {
binding = globalConf.HTTPDConfig.Bindings[idx] binding = globalConf.HTTPDConfig.Bindings[idx]
@ -1023,6 +1026,12 @@ func getHTTPDBindingFromEnv(idx int) {
isSet = true isSet = true
} }
renderOpenAPI, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__RENDER_OPENAPI", idx))
if ok {
binding.RenderOpenAPI = renderOpenAPI
isSet = true
}
enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_HTTPS", idx)) enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_HTTPS", idx))
if ok { if ok {
binding.EnableHTTPS = enableHTTPS binding.EnableHTTPS = enableHTTPS
@ -1219,6 +1228,7 @@ func setViperDefaults() {
viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath) viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath)
viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath) viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath)
viper.SetDefault("httpd.backups_path", globalConf.HTTPDConfig.BackupsPath) viper.SetDefault("httpd.backups_path", globalConf.HTTPDConfig.BackupsPath)
viper.SetDefault("httpd.openapi_path", globalConf.HTTPDConfig.OpenAPIPath)
viper.SetDefault("httpd.web_root", globalConf.HTTPDConfig.WebRoot) viper.SetDefault("httpd.web_root", globalConf.HTTPDConfig.WebRoot)
viper.SetDefault("httpd.certificate_file", globalConf.HTTPDConfig.CertificateFile) viper.SetDefault("httpd.certificate_file", globalConf.HTTPDConfig.CertificateFile)
viper.SetDefault("httpd.certificate_key_file", globalConf.HTTPDConfig.CertificateKeyFile) viper.SetDefault("httpd.certificate_key_file", globalConf.HTTPDConfig.CertificateKeyFile)

View file

@ -752,6 +752,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PORT", "9000") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PORT", "9000")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN", "0") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN", "0")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT", "0") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT", "0")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI", "0")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS", "1 ") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS", "1 ")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE", "1") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES", " TLS_AES_256_GCM_SHA384 , TLS_CHACHA20_POLY1305_SHA256") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES", " TLS_AES_256_GCM_SHA384 , TLS_CHACHA20_POLY1305_SHA256")
@ -770,6 +771,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED")
@ -786,6 +788,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.False(t, bindings[0].EnableHTTPS) require.False(t, bindings[0].EnableHTTPS)
require.True(t, bindings[0].EnableWebAdmin) require.True(t, bindings[0].EnableWebAdmin)
require.True(t, bindings[0].EnableWebClient) require.True(t, bindings[0].EnableWebClient)
require.True(t, bindings[0].RenderOpenAPI)
require.Len(t, bindings[0].TLSCipherSuites, 1) require.Len(t, bindings[0].TLSCipherSuites, 1)
require.Equal(t, "TLS_AES_128_GCM_SHA256", bindings[0].TLSCipherSuites[0]) require.Equal(t, "TLS_AES_128_GCM_SHA256", bindings[0].TLSCipherSuites[0])
require.Equal(t, 0, bindings[0].HideLoginURL) require.Equal(t, 0, bindings[0].HideLoginURL)
@ -794,6 +797,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.False(t, bindings[1].EnableHTTPS) require.False(t, bindings[1].EnableHTTPS)
require.True(t, bindings[1].EnableWebAdmin) require.True(t, bindings[1].EnableWebAdmin)
require.True(t, bindings[1].EnableWebClient) require.True(t, bindings[1].EnableWebClient)
require.True(t, bindings[1].RenderOpenAPI)
require.Nil(t, bindings[1].TLSCipherSuites) require.Nil(t, bindings[1].TLSCipherSuites)
require.Equal(t, 1, bindings[1].HideLoginURL) require.Equal(t, 1, bindings[1].HideLoginURL)
@ -802,6 +806,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.True(t, bindings[2].EnableHTTPS) require.True(t, bindings[2].EnableHTTPS)
require.False(t, bindings[2].EnableWebAdmin) require.False(t, bindings[2].EnableWebAdmin)
require.False(t, bindings[2].EnableWebClient) require.False(t, bindings[2].EnableWebClient)
require.False(t, bindings[2].RenderOpenAPI)
require.Equal(t, 1, bindings[2].ClientAuthType) require.Equal(t, 1, bindings[2].ClientAuthType)
require.Len(t, bindings[2].TLSCipherSuites, 2) require.Len(t, bindings[2].TLSCipherSuites, 2)
require.Equal(t, "TLS_AES_256_GCM_SHA384", bindings[2].TLSCipherSuites[0]) require.Equal(t, "TLS_AES_256_GCM_SHA384", bindings[2].TLSCipherSuites[0])

View file

@ -1,6 +1,6 @@
# Account's configuration properties # Account's configuration properties
Please take a look at the [OpenAPI schema](../httpd/schema/openapi.yaml) for the exact definitions of user, folder and admin fields. Please take a look at the [OpenAPI schema](../openapi/openapi.yaml) for the exact definitions of user, folder and admin fields.
If you need an example you can export a dump using the Web Admin or by invoking the `dumpdata` endpoint directly, you need to obtain an access token first, for example: If you need an example you can export a dump using the Web Admin or by invoking the `dumpdata` endpoint directly, you need to obtain an access token first, for example:
```shell ```shell

View file

@ -103,7 +103,7 @@ If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. Th
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations. The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.
The structure for SFTPGo objects can be found within the [OpenAPI schema](../httpd/schema/openapi.yaml). The structure for SFTPGo objects can be found within the [OpenAPI schema](../openapi/openapi.yaml).
## Pub/Sub services ## Pub/Sub services

View file

@ -56,4 +56,4 @@ fi
Please note that this is a demo program and it might not work in all cases. For example, the username should be obtained by parsing the JSON serialized user and not by searching the username inside the JSON as shown here. Please note that this is a demo program and it might not work in all cases. For example, the username should be obtained by parsing the JSON serialized user and not by searching the username inside the JSON as shown here.
The structure for SFTPGo users can be found within the [OpenAPI schema](../httpd/schema/openapi.yaml). The structure for SFTPGo users can be found within the [OpenAPI schema](../openapi/openapi.yaml).

View file

@ -66,7 +66,7 @@ else
fi fi
``` ```
The structure for SFTPGo users can be found within the [OpenAPI schema](../httpd/schema/openapi.yaml). The structure for SFTPGo users can be found within the [OpenAPI schema](../openapi/openapi.yaml).
You can disable the hook on a per-user basis so that you can mix external and internal users. You can disable the hook on a per-user basis so that you can mix external and internal users.

View file

@ -222,9 +222,11 @@ The configuration file contains the following sections:
- `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty. - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
- `proxy_allowed`, list of IP addresses and IP ranges allowed to set `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto`, `CF-Connecting-IP`, `True-Client-IP` headers. Any of the indicated headers, if set on requests from a connection address not in this list, will be silently ignored. Default: empty. - `proxy_allowed`, list of IP addresses and IP ranges allowed to set `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto`, `CF-Connecting-IP`, `True-Client-IP` headers. Any of the indicated headers, if set on requests from a connection address not in this list, will be silently ignored. Default: empty.
- `hide_login_url`, integer. If both web admin and web client are enabled each login page will show a link to the other one. This setting allows to hide this link. 0 means that the login links are displayed on both admin and client login page. This is the default. 1 means that the login link to the web client login page is hidden on admin login page. 2 means that the login link to the web admin login page is hidden on client login page. The flags can be combined, for example 3 will disable both login links. - `hide_login_url`, integer. If both web admin and web client are enabled each login page will show a link to the other one. This setting allows to hide this link. 0 means that the login links are displayed on both admin and client login page. This is the default. 1 means that the login link to the web client login page is hidden on admin login page. 2 means that the login link to the web admin login page is hidden on client login page. The flags can be combined, for example 3 will disable both login links.
- `render_openapi`, boolean. Set to `false` to disable serving of the OpenAPI schema and renderer. Default `true`.
- `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir - `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
- `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled - `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled
- `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons - `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons
- `openapi_path`, string. Path to the directory that contains the OpenAPI schema and the default renderer. This can be an absolute path or a path relative to the config dir. If empty the OpenAPI schema and the renderer will not be served regardless of the `render_openapi` directive
- `web_root`, string. Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored - `web_root`, string. Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored
- `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir. - `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, the server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. - `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, the server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.

View file

@ -69,17 +69,21 @@ Open the SFTPGo configuration file, search for the `httpd` section and change it
"enable_https": true, "enable_https": true,
"client_auth_type": 0, "client_auth_type": 0,
"tls_cipher_suites": [], "tls_cipher_suites": [],
"proxy_allowed": [] "proxy_allowed": [],
"hide_login_url": 0,
"render_openapi": true
} }
], ],
"templates_path": "/usr/share/sftpgo/templates", "templates_path": "/usr/share/sftpgo/templates",
"static_files_path": "/usr/share/sftpgo/static", "static_files_path": "/usr/share/sftpgo/static",
"backups_path": "/srv/sftpgo/backups", "backups_path": "/srv/sftpgo/backups",
"openapi_path": "/srv/sftpgo/openapi",
"web_root": "", "web_root": "",
"certificate_file": "/etc/sftpgo/certs/sftpgo.com.crt", "certificate_file": "/etc/sftpgo/certs/sftpgo.com.crt",
"certificate_key_file": "/etc/sftpgo/certs/sftpgo.com.key", "certificate_key_file": "/etc/sftpgo/certs/sftpgo.com.key",
"ca_certificates": [], "ca_certificates": [],
"ca_revocation_lists": [] "ca_revocation_lists": [],
....
} }
``` ```

View file

@ -20,7 +20,7 @@ The program must finish within 20 seconds.
If the hook is an HTTP URL then it will be invoked as HTTP POST. The login method, the used protocol, the ip address and the status of the user are added to the query string, for example `<http_url>?login_method=password&ip=1.2.3.4&protocol=SSH&status=1`. If the hook is an HTTP URL then it will be invoked as HTTP POST. The login method, the used protocol, the ip address and the status of the user are added to the query string, for example `<http_url>?login_method=password&ip=1.2.3.4&protocol=SSH&status=1`.
The request body will contain the user serialized as JSON. The request body will contain the user serialized as JSON.
The structure for SFTPGo users can be found within the [OpenAPI schema](../httpd/schema/openapi.yaml). The structure for SFTPGo users can be found within the [OpenAPI schema](../openapi/openapi.yaml).
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations. The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.

View file

@ -94,8 +94,8 @@ You can find an example script that shows how to manage data retention [here](..
:warning: Deleting files is an irreversible action, please make sure you fully understand what you are doing before using this feature, you may have users with overlapping home directories or virtual folders shared between multiple users, it is relatively easy to inadvertently delete files you need. :warning: Deleting files is an irreversible action, please make sure you fully understand what you are doing before using this feature, you may have users with overlapping home directories or virtual folders shared between multiple users, it is relatively easy to inadvertently delete files you need.
The OpenAPI 3 schema for the exposed API can be found inside the source tree: [openapi.yaml](../httpd/schema/openapi.yaml "OpenAPI 3 specs"). If you want to render the schema without importing it manually, you can explore it on [Stoplight](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml). The OpenAPI 3 schema for the exposed API can be found inside the source tree: [openapi.yaml](../openapi/openapi.yaml "OpenAPI 3 specs"). You can render the schema and try the API using the `/openapi` endpoint. SFTPGo uses by default [Swagger UI](https://github.com/swagger-api/swagger-ui), you can use another renderer just by copying it to the defined OpenAPI path.
You can generate your own REST client in your preferred programming language, or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/). You can also explore the schema on [Stoplight](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml).
You can also use [Swagger UI](https://github.com/swagger-api/swagger-ui). You can generate your own REST API client in your preferred programming language, or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/).

View file

@ -38,6 +38,7 @@ sudo install -Dm644 sftpgo.json /etc/sftpgo/
# override some configuration keys using environment variables # override some configuration keys using environment variables
sudo sh -c 'echo "SFTPGO_HTTPD__TEMPLATES_PATH=/usr/share/sftpgo/templates" > /etc/sftpgo/sftpgo.env' sudo sh -c 'echo "SFTPGO_HTTPD__TEMPLATES_PATH=/usr/share/sftpgo/templates" > /etc/sftpgo/sftpgo.env'
sudo sh -c 'echo "SFTPGO_HTTPD__STATIC_FILES_PATH=/usr/share/sftpgo/static" >> /etc/sftpgo/sftpgo.env' sudo sh -c 'echo "SFTPGO_HTTPD__STATIC_FILES_PATH=/usr/share/sftpgo/static" >> /etc/sftpgo/sftpgo.env'
sudo sh -c 'echo "SFTPGO_HTTPD__OPENAPI_PATH=/usr/share/sftpgo/openapi" >> /etc/sftpgo/sftpgo.env'
sudo sh -c 'echo "SFTPGO_HTTPD__BACKUPS_PATH=/var/lib/sftpgo/backups" >> /etc/sftpgo/sftpgo.env' sudo sh -c 'echo "SFTPGO_HTTPD__BACKUPS_PATH=/var/lib/sftpgo/backups" >> /etc/sftpgo/sftpgo.env'
sudo sh -c 'echo "SFTPGO_DATA_PROVIDER__CREDENTIALS_PATH=/var/lib/sftpgo/credentials" >> /etc/sftpgo/sftpgo.env' sudo sh -c 'echo "SFTPGO_DATA_PROVIDER__CREDENTIALS_PATH=/var/lib/sftpgo/credentials" >> /etc/sftpgo/sftpgo.env'
# if you use a file based data provider such as sqlite or bolt consider to set the database path too, for example: # if you use a file based data provider such as sqlite or bolt consider to set the database path too, for example:

View file

@ -26,7 +26,8 @@ Know issues:
- removing a directory tree on Cloud Storage backends could generate a `not found` error when removing the last (virtual) directory. This happens if the client cycles the directories tree itself and removes files and directories one by one instead of issuing a single remove command - removing a directory tree on Cloud Storage backends could generate a `not found` error when removing the last (virtual) directory. This happens if the client cycles the directories tree itself and removes files and directories one by one instead of issuing a single remove command
- the used [WebDAV library](https://pkg.go.dev/golang.org/x/net/webdav?tab=doc) asks to open a file to execute a `stat` and sometimes reads some bytes to find the content type. Stat calls are executed before and after a download too, so to be able to properly list a directory you need to grant both `list` and `download` permissions and to be able to upload files you need to gran both `list` and `upload` permissions - the used [WebDAV library](https://pkg.go.dev/golang.org/x/net/webdav?tab=doc) asks to open a file to execute a `stat` and sometimes reads some bytes to find the content type. Stat calls are executed before and after a download too, so to be able to properly list a directory you need to grant both `list` and `download` permissions and to be able to upload files you need to gran both `list` and `upload` permissions
- the used [WebDAV library](https://pkg.go.dev/golang.org/x/net/webdav?tab=doc) not always returns a proper error code/message, most of the times it simply returns `Method not Allowed`. I'll try to improve the library error codes in the future - the used [WebDAV library](https://pkg.go.dev/golang.org/x/net/webdav?tab=doc) not always returns a proper error code/message, most of the times it simply returns `Method not Allowed`. I'll try to improve the library error codes in the future
- if a file or a directory cannot be accessed, for example due to OS permissions issues or because a mapped path for a virtual folder is a missing, it will be omitted from the directory listing. If there is a different error then the whole directory listing will fail. This behavior is different from SFTP/FTP where you will be able to see the problematic file/directory in the directory listing, you will only get an error if you try to access it. - if a file or a directory cannot be accessed, for example due to OS permissions issues or because a mapped path for a virtual folder is a missing, it will be omitted from the directory listing. If there is a different error then the whole directory listing will fail. This behavior is different from SFTP/FTP where you will be able to see the problematic file/directory in the directory listing, you will only get an error if you try to access it
- if you use the native Windows client please check its usage and pay particular attention to the [registry settings](https://docs.microsoft.com/en-us/iis/publish/using-webdav/using-the-webdav-redirector#webdav-redirector-registry-settings). The default file size limit is 50MB and if you don't configure SFTPGo to use HTTPS you have to set `BasicAuthLevel` to `2`
We plan to add [Dead Properties](https://tools.ietf.org/html/rfc4918#section-3) support in future releases. We need a design decision here, probably the best solution is to store dead properties inside the data provider but this could increase a lot its size. Alternately we could store them on disk for local filesystem and add as metadata for Cloud Storage, this means that we need to do a separate `HEAD` request to retrieve dead properties for an S3 file. For big folders will do a lot of requests to the Cloud Provider, I don't like this solution. Another option is to expose a hook and allow you to implement `dead properties` outside SFTPGo. We plan to add [Dead Properties](https://tools.ietf.org/html/rfc4918#section-3) support in future releases. We need a design decision here, probably the best solution is to store dead properties inside the data provider but this could increase a lot its size. Alternately we could store them on disk for local filesystem and add as metadata for Cloud Storage, this means that we need to do a separate `HEAD` request to retrieve dead properties for an S3 file. For big folders will do a lot of requests to the Cloud Provider, I don't like this solution. Another option is to expose a hook and allow you to implement `dead properties` outside SFTPGo.

View file

@ -1,6 +1,6 @@
# REST API CLI client # REST API CLI client
:warning: This sample client is deprecated and it will work only with API V1 (SFTPGo <= 1.2.2). You can easily build your own client from the [OpenAPI](../../httpd/schema/openapi.yaml) schema or use [Swagger UI](https://github.com/swagger-api/swagger-ui). :warning: This sample client is deprecated and it will work only with API V1 (SFTPGo <= 1.2.2). You can easily build your own client from the [OpenAPI](../../openapi/openapi.yaml) schema or use [Swagger UI](https://github.com/swagger-api/swagger-ui).
`sftpgo_api_cli` is a very simple command line client for `SFTPGo` REST API written in python. `sftpgo_api_cli` is a very simple command line client for `SFTPGo` REST API written in python.

View file

@ -1,6 +1,6 @@
// Package httpd implements REST API and Web interface for SFTPGo. // Package httpd implements REST API and Web interface for SFTPGo.
// The OpenAPI 3 schema for the exposed API can be found inside the source tree: // The OpenAPI 3 schema for the exposed API can be found inside the source tree:
// https://github.com/drakkan/sftpgo/blob/main/httpd/schema/openapi.yaml // https://github.com/drakkan/sftpgo/blob/main/openapi/openapi.yaml
package httpd package httpd
import ( import (
@ -137,6 +137,7 @@ const (
webClientForgotPwdPathDefault = "/web/client/forgot-password" webClientForgotPwdPathDefault = "/web/client/forgot-password"
webClientResetPwdPathDefault = "/web/client/reset-password" webClientResetPwdPathDefault = "/web/client/reset-password"
webStaticFilesPathDefault = "/static" webStaticFilesPathDefault = "/static"
webOpenAPIPathDefault = "/openapi"
// MaxRestoreSize defines the max size for the loaddata input file // MaxRestoreSize defines the max size for the loaddata input file
MaxRestoreSize = 10485760 // 10 MB MaxRestoreSize = 10485760 // 10 MB
maxRequestSize = 1048576 // 1MB maxRequestSize = 1048576 // 1MB
@ -210,6 +211,7 @@ var (
webClientForgotPwdPath string webClientForgotPwdPath string
webClientResetPwdPath string webClientResetPwdPath string
webStaticFilesPath string webStaticFilesPath string
webOpenAPIPath string
// max upload size for http clients, 1GB by default // max upload size for http clients, 1GB by default
maxUploadFileSize = int64(1048576000) maxUploadFileSize = int64(1048576000)
) )
@ -256,7 +258,9 @@ type Binding struct {
// - 1 the login link to the web client login page is hidden on admin login page // - 1 the login link to the web client login page is hidden on admin login page
// - 2 the login link to the web admin login page is hidden on client login page // - 2 the login link to the web admin login page is hidden on client login page
// The flags can be combined, for example 3 will disable both login links. // The flags can be combined, for example 3 will disable both login links.
HideLoginURL int `json:"hide_login_url" mapstructure:"hide_login_url"` HideLoginURL int `json:"hide_login_url" mapstructure:"hide_login_url"`
// Enable the built-in OpenAPI renderer
RenderOpenAPI bool `json:"render_openapi" mapstructure:"render_openapi"`
allowHeadersFrom []func(net.IP) bool allowHeadersFrom []func(net.IP) bool
} }
@ -341,6 +345,9 @@ type Conf struct {
StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"` StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"`
// Path to the backup directory. This can be an absolute path or a path relative to the config dir // Path to the backup directory. This can be an absolute path or a path relative to the config dir
BackupsPath string `json:"backups_path" mapstructure:"backups_path"` BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
// Path to the directory that contains the OpenAPI schema and the default renderer.
// This can be an absolute path or a path relative to the config dir
OpenAPIPath string `json:"openapi_path" mapstructure:"openapi_path"`
// Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will // Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will
// be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored. // be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored.
WebRoot string `json:"web_root" mapstructure:"web_root"` WebRoot string `json:"web_root" mapstructure:"web_root"`
@ -420,6 +427,7 @@ func (c *Conf) Initialize(configDir string) error {
backupsPath = getConfigPath(c.BackupsPath, configDir) backupsPath = getConfigPath(c.BackupsPath, configDir)
staticFilesPath := getConfigPath(c.StaticFilesPath, configDir) staticFilesPath := getConfigPath(c.StaticFilesPath, configDir)
templatesPath := getConfigPath(c.TemplatesPath, configDir) templatesPath := getConfigPath(c.TemplatesPath, configDir)
openAPIPath := getConfigPath(c.OpenAPIPath, configDir)
if backupsPath == "" { if backupsPath == "" {
return fmt.Errorf("required directory is invalid, backup path %#v", backupsPath) return fmt.Errorf("required directory is invalid, backup path %#v", backupsPath)
} }
@ -469,7 +477,7 @@ func (c *Conf) Initialize(configDir string) error {
} }
go func(b Binding) { go func(b Binding) {
server := newHttpdServer(b, staticFilesPath, c.SigningPassphrase, c.Cors) server := newHttpdServer(b, staticFilesPath, c.SigningPassphrase, c.Cors, openAPIPath)
exitChannel <- server.listenAndServe() exitChannel <- server.listenAndServe()
}(binding) }(binding)
@ -603,6 +611,7 @@ func updateWebAdminURLs(baseURL string) {
webDefenderHostsPath = path.Join(baseURL, webDefenderHostsPathDefault) webDefenderHostsPath = path.Join(baseURL, webDefenderHostsPathDefault)
webDefenderPath = path.Join(baseURL, webDefenderPathDefault) webDefenderPath = path.Join(baseURL, webDefenderPathDefault)
webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault) webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)
webOpenAPIPath = path.Join(baseURL, webOpenAPIPathDefault)
} }
// GetHTTPRouter returns an HTTP handler suitable to use for test cases // GetHTTPRouter returns an HTTP handler suitable to use for test cases
@ -612,8 +621,9 @@ func GetHTTPRouter() http.Handler {
Port: 8080, Port: 8080,
EnableWebAdmin: true, EnableWebAdmin: true,
EnableWebClient: true, EnableWebClient: true,
RenderOpenAPI: true,
} }
server := newHttpdServer(b, "../static", "", CorsConfig{}) server := newHttpdServer(b, "../static", "", CorsConfig{}, "../openapi")
server.initializeRouter() server.initializeRouter()
return server.router return server.router
} }

View file

@ -14837,9 +14837,37 @@ func TestGetWebStatusMock(t *testing.T) {
} }
func TestStaticFilesMock(t *testing.T) { func TestStaticFilesMock(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "/static/favicon.ico", nil) req, err := http.NewRequest(http.MethodGet, "/static/favicon.ico", nil)
assert.NoError(t, err)
rr := executeRequest(req) rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodGet, "/openapi/openapi.yaml", nil)
assert.NoError(t, err)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodGet, "/static", nil)
assert.NoError(t, err)
rr = executeRequest(req)
checkResponseCode(t, http.StatusMovedPermanently, rr)
location := rr.Header().Get("Location")
assert.Equal(t, "/static/", location)
req, err = http.NewRequest(http.MethodGet, location, nil)
assert.NoError(t, err)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodGet, "/openapi", nil)
assert.NoError(t, err)
rr = executeRequest(req)
checkResponseCode(t, http.StatusMovedPermanently, rr)
location = rr.Header().Get("Location")
assert.Equal(t, "/openapi/", location)
req, err = http.NewRequest(http.MethodGet, location, nil)
assert.NoError(t, err)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
} }
func waitForUsersQuotaScan(t *testing.T, token string) { func waitForUsersQuotaScan(t *testing.T, token string) {

View file

@ -1406,7 +1406,7 @@ func TestProxyHeaders(t *testing.T) {
} }
err = b.parseAllowedProxy() err = b.parseAllowedProxy()
assert.NoError(t, err) assert.NoError(t, err)
server := newHttpdServer(b, "", "", CorsConfig{Enabled: true}) server := newHttpdServer(b, "", "", CorsConfig{Enabled: true}, "")
server.initializeRouter() server.initializeRouter()
testServer := httptest.NewServer(server.router) testServer := httptest.NewServer(server.router)
defer testServer.Close() defer testServer.Close()
@ -1492,7 +1492,7 @@ func TestRecoverer(t *testing.T) {
EnableWebAdmin: true, EnableWebAdmin: true,
EnableWebClient: false, EnableWebClient: false,
} }
server := newHttpdServer(b, "../static", "", CorsConfig{}) server := newHttpdServer(b, "../static", "", CorsConfig{}, "../openapi")
server.initializeRouter() server.initializeRouter()
server.router.Get(recoveryPath, func(w http.ResponseWriter, r *http.Request) { server.router.Get(recoveryPath, func(w http.ResponseWriter, r *http.Request) {
panic("panic") panic("panic")
@ -1607,7 +1607,7 @@ func TestWebAdminRedirect(t *testing.T) {
EnableWebAdmin: true, EnableWebAdmin: true,
EnableWebClient: false, EnableWebClient: false,
} }
server := newHttpdServer(b, "../static", "", CorsConfig{}) server := newHttpdServer(b, "../static", "", CorsConfig{}, "../openapi")
server.initializeRouter() server.initializeRouter()
testServer := httptest.NewServer(server.router) testServer := httptest.NewServer(server.router)
defer testServer.Close() defer testServer.Close()

View file

@ -9,6 +9,7 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -37,20 +38,29 @@ var (
type httpdServer struct { type httpdServer struct {
binding Binding binding Binding
staticFilesPath string staticFilesPath string
openAPIPath string
enableWebAdmin bool enableWebAdmin bool
enableWebClient bool enableWebClient bool
renderOpenAPI bool
router *chi.Mux router *chi.Mux
tokenAuth *jwtauth.JWTAuth tokenAuth *jwtauth.JWTAuth
signingPassphrase string signingPassphrase string
cors CorsConfig cors CorsConfig
} }
func newHttpdServer(b Binding, staticFilesPath, signingPassphrase string, cors CorsConfig) *httpdServer { func newHttpdServer(b Binding, staticFilesPath, signingPassphrase string, cors CorsConfig,
openAPIPath string,
) *httpdServer {
if openAPIPath == "" {
b.RenderOpenAPI = false
}
return &httpdServer{ return &httpdServer{
binding: b, binding: b,
staticFilesPath: staticFilesPath, staticFilesPath: staticFilesPath,
openAPIPath: openAPIPath,
enableWebAdmin: b.EnableWebAdmin, enableWebAdmin: b.EnableWebAdmin,
enableWebClient: b.EnableWebClient, enableWebClient: b.EnableWebClient,
renderOpenAPI: b.RenderOpenAPI,
signingPassphrase: signingPassphrase, signingPassphrase: signingPassphrase,
cors: cors, cors: cors,
} }
@ -940,6 +950,17 @@ func (s *httpdServer) redirectToWebPath(w http.ResponseWriter, r *http.Request,
} }
} }
func (s *httpdServer) isStaticFileURL(r *http.Request) bool {
var urlPath string
rctx := chi.RouteContext(r.Context())
if rctx != nil && rctx.RoutePath != "" {
urlPath = rctx.RoutePath
} else {
urlPath = r.URL.Path
}
return !strings.HasPrefix(urlPath, webOpenAPIPath) && !strings.HasPrefix(urlPath, webStaticFilesPath)
}
func (s *httpdServer) initializeRouter() { func (s *httpdServer) initializeRouter() {
s.tokenAuth = jwtauth.New(jwa.HS256.String(), getSigningKey(s.signingPassphrase), nil) s.tokenAuth = jwtauth.New(jwa.HS256.String(), getSigningKey(s.signingPassphrase), nil)
s.router = chi.NewRouter() s.router = chi.NewRouter()
@ -960,7 +981,8 @@ func (s *httpdServer) initializeRouter() {
s.router.Use(c.Handler) s.router.Use(c.Handler)
} }
s.router.Use(middleware.GetHead) s.router.Use(middleware.GetHead)
s.router.Use(middleware.StripSlashes) // StripSlashes causes infinite redirects at the root path if used with http.FileServer
s.router.Use(middleware.Maybe(middleware.StripSlashes, s.isStaticFileURL))
s.router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
@ -1135,6 +1157,13 @@ func (s *httpdServer) initializeRouter() {
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Delete(userSharesPath+"/{id}", deleteShare) router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Delete(userSharesPath+"/{id}", deleteShare)
}) })
if s.renderOpenAPI {
s.router.Group(func(router chi.Router) {
router.Use(compressor.Handler)
fileServer(router, webOpenAPIPath, http.Dir(s.openAPIPath))
})
}
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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

View file

@ -0,0 +1,60 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "../openapi.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -42,6 +42,7 @@ sed -i "s|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|"
sed -i "s|\"templates\"|\"/usr/share/sftpgo/templates\"|" sftpgo.json sed -i "s|\"templates\"|\"/usr/share/sftpgo/templates\"|" sftpgo.json
sed -i "s|\"static\"|\"/usr/share/sftpgo/static\"|" sftpgo.json sed -i "s|\"static\"|\"/usr/share/sftpgo/static\"|" sftpgo.json
sed -i "s|\"backups\"|\"/srv/sftpgo/backups\"|" sftpgo.json sed -i "s|\"backups\"|\"/srv/sftpgo/backups\"|" sftpgo.json
sed -i "s|\"openapi\"|\"/usr/share/sftpgo/openapi\"|" sftpgo.json
sed -i "s|\"credentials\"|\"/var/lib/sftpgo/credentials\"|" sftpgo.json sed -i "s|\"credentials\"|\"/var/lib/sftpgo/credentials\"|" sftpgo.json
cat >nfpm.yaml <<EOF cat >nfpm.yaml <<EOF
@ -82,6 +83,9 @@ contents:
- src: "${BASE_DIR}/static/*" - src: "${BASE_DIR}/static/*"
dst: "/usr/share/sftpgo/static/" dst: "/usr/share/sftpgo/static/"
- src: "${BASE_DIR}/openapi/*"
dst: "/usr/share/sftpgo/openapi/"
- src: "./sftpgo.json" - src: "./sftpgo.json"
dst: "/etc/sftpgo/sftpgo.json" dst: "/etc/sftpgo/sftpgo.json"
type: "config|noreplace" type: "config|noreplace"

View file

@ -209,12 +209,14 @@
"client_auth_type": 0, "client_auth_type": 0,
"tls_cipher_suites": [], "tls_cipher_suites": [],
"proxy_allowed": [], "proxy_allowed": [],
"hide_login_url": 0 "hide_login_url": 0,
"render_openapi": true
} }
], ],
"templates_path": "templates", "templates_path": "templates",
"static_files_path": "static", "static_files_path": "static",
"backups_path": "backups", "backups_path": "backups",
"openapi_path": "openapi",
"web_root": "", "web_root": "",
"certificate_file": "", "certificate_file": "",
"certificate_key_file": "", "certificate_key_file": "",

View file

@ -52,6 +52,7 @@ Source: "{#MyAppDir}\LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppDir}\sftpgo.json"; DestDir: "{commonappdata}\{#MyAppName}"; Flags: onlyifdoesntexist uninsneveruninstall Source: "{#MyAppDir}\sftpgo.json"; DestDir: "{commonappdata}\{#MyAppName}"; Flags: onlyifdoesntexist uninsneveruninstall
Source: "{#MyAppDir}\templates\*"; DestDir: "{commonappdata}\{#MyAppName}\templates"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#MyAppDir}\templates\*"; DestDir: "{commonappdata}\{#MyAppName}\templates"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppDir}\static\*"; DestDir: "{commonappdata}\{#MyAppName}\static"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#MyAppDir}\static\*"; DestDir: "{commonappdata}\{#MyAppName}\static"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppDir}\openapi\*"; DestDir: "{commonappdata}\{#MyAppName}\openapi"; Flags: ignoreversion recursesubdirs createallsubdirs
[Dirs] [Dirs]
Name: "{commonappdata}\{#MyAppName}\logs"; Permissions: everyone-full Name: "{commonappdata}\{#MyAppName}\logs"; Permissions: everyone-full
@ -61,6 +62,7 @@ Name: "{commonappdata}\{#MyAppName}\credentials"; Permissions: everyone-full
[Icons] [Icons]
Name: "{group}\Web Admin"; Filename: "http://127.0.0.1:8080/web/admin"; Name: "{group}\Web Admin"; Filename: "http://127.0.0.1:8080/web/admin";
Name: "{group}\Web Client"; Filename: "http://127.0.0.1:8080/web/client"; Name: "{group}\Web Client"; Filename: "http://127.0.0.1:8080/web/client";
Name: "{group}\OpenAPI"; Filename: "http://127.0.0.1:8080/openapi";
Name: "{group}\Service Control"; WorkingDir: "{app}"; Filename: "powershell.exe"; Parameters: "-Command ""Start-Process cmd \""/k cd {app} & {#MyAppExeName} service --help\"" -Verb RunAs"; Comment: "Manage SFTPGo Service" Name: "{group}\Service Control"; WorkingDir: "{app}"; Filename: "powershell.exe"; Parameters: "-Command ""Start-Process cmd \""/k cd {app} & {#MyAppExeName} service --help\"" -Verb RunAs"; Comment: "Manage SFTPGo Service"
Name: "{group}\Documentation"; Filename: "{#DocURL}"; Name: "{group}\Documentation"; Filename: "{#DocURL}";
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"