Compare commits

..

9 Commits

Author SHA1 Message Date
Andrea Spacca
2a29083960 handle range with no limit 2023-02-11 11:47:51 +09:00
Andrea Spacca
158e5487ee refactor CloseCheck to avoid panic on nil 2023-02-11 11:47:01 +09:00
Andrea Spacca
806286ab35 refactor CloseCheck to avoid panic on nil, handle range in gdrive storage 2023-02-11 11:46:27 +09:00
Andrea Spacca
d49aee59ba refactor CloseCheck to avoid panic on nil, handle range with no limit 2023-02-11 11:45:57 +09:00
Andrea Spacca
e08225e5f8 refactor CloseCheck to avoid panic on nil, remove range/audio/video special handling on get handler 2023-02-11 11:45:17 +09:00
Andrea Spacca
8597f1d9eb bump gdrive dependecies 2023-02-11 11:44:12 +09:00
Andrea Spacca
9e8ce19cd1 proper param name in error 2023-02-11 11:43:41 +09:00
Vladislav Grubov
2bda0a1e55 Adds 'Accept-Ranges: bytes' header to handlers 2023-02-03 22:45:55 +03:00
Vladislav Grubov
d9369e8b39 Support Range header for GET 2023-01-28 22:23:57 +03:00
16 changed files with 1666 additions and 941 deletions

View File

@@ -6,7 +6,6 @@ bin
*.pyc *.pyc
*.egg-info *.egg-info
.vagrant .vagrant
.git
.tmp .tmp
bower_components bower_components
node_modules node_modules

View File

@@ -13,27 +13,16 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
go_version: go_version:
- '1.18' - 1.15.x
- '1.19' - 1.16.x
- '1.20' - 1.17.x
- tip - 1.18.X
name: Test with ${{ matrix.go_version }} name: Test with ${{ matrix.go_version }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install Go ${{ matrix.go_version }} - uses: actions/setup-go@v1
if: ${{ matrix.go_version != 'tip' }}
uses: actions/setup-go@master
with: with:
go-version: ${{ matrix.go_version }} go-version: ${{ matrix.go_version }}
check-latest: true
- name: Install Go ${{ matrix.go_version }}
if: ${{ matrix.go_version == 'tip' }}
run: |
curl -sL https://storage.googleapis.com/go-build-snap/go/linux-amd64/$(git ls-remote https://github.com/golang/go.git HEAD | awk '{print $1;}').tar.gz -o gotip.tar.gz
ls -lah gotip.tar.gz
mkdir -p ~/sdk/gotip
tar -C ~/sdk/gotip -xzf gotip.tar.gz
echo "PATH=$HOME/go/bin:$HOME/sdk/gotip/bin/:$PATH" >> $GITHUB_ENV
- name: Vet and test - name: Vet and test
run: | run: |
go version go version
@@ -44,10 +33,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-go@master - uses: actions/setup-go@v1
with: with:
go-version: '1.20' go-version: 1.18
check-latest: true
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
with: with:

View File

@@ -1,17 +1,15 @@
# Default to Go 1.20 # Default to Go 1.17
ARG GO_VERSION=1.20 ARG GO_VERSION=1.17
FROM golang:${GO_VERSION}-alpine as build FROM golang:${GO_VERSION}-alpine as build
# Necessary to run 'go get' and to compile the linked binary # Necessary to run 'go get' and to compile the linked binary
RUN apk add git musl-dev mailcap RUN apk add git musl-dev
ADD . /go/src/github.com/dutchcoders/transfer.sh
WORKDIR /go/src/github.com/dutchcoders/transfer.sh WORKDIR /go/src/github.com/dutchcoders/transfer.sh
COPY go.mod go.sum ./ ENV GO111MODULE=on
RUN go mod download
COPY . .
# build & install server # build & install server
RUN CGO_ENABLED=0 go build -tags netgo -ldflags "-X github.com/dutchcoders/transfer.sh/cmd.Version=$(git describe --tags) -a -s -w -extldflags '-static'" -o /go/bin/transfersh RUN CGO_ENABLED=0 go build -tags netgo -ldflags "-X github.com/dutchcoders/transfer.sh/cmd.Version=$(git describe --tags) -a -s -w -extldflags '-static'" -o /go/bin/transfersh
@@ -31,7 +29,6 @@ FROM scratch AS final
LABEL maintainer="Andrea Spacca <andrea.spacca@gmail.com>" LABEL maintainer="Andrea Spacca <andrea.spacca@gmail.com>"
ARG RUNAS ARG RUNAS
COPY --from=build /etc/mime.types /etc/mime.types
COPY --from=build /tmp/empty /tmp COPY --from=build /tmp/empty /tmp
COPY --from=build /tmp/useradd/* /etc/ COPY --from=build /tmp/useradd/* /etc/
COPY --from=build --chown=${RUNAS} /go/bin/transfersh /go/bin/transfersh COPY --from=build --chown=${RUNAS} /go/bin/transfersh /go/bin/transfersh

112
README.md
View File

@@ -47,18 +47,6 @@ $ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Downloads
$ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Days: 1" # Set the number of days before deletion $ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Days: 1" # Set the number of days before deletion
``` ```
### X-Encrypt-Password
#### Beware, use this feature only on your self-hosted server: trusting a third-party service for server side encryption is at your own risk
```bash
$ curl --upload-file ./hello.txt https://your-transfersh-instance.tld/hello.txt -H "X-Encrypt-Password: test" # Encrypt the content sever side with AES265 using "test" as password
```
### X-Decrypt-Password
#### Beware, use this feature only on your self-hosted server: trusting a third-party service for server side encryption is at your own risk
```bash
$ curl https://your-transfersh-instance.tld/BAYh0/hello.txt -H "X-Decrypt-Password: test" # Decrypt the content sever side with AES265 using "test" as password
```
## Response Headers ## Response Headers
### X-Url-Delete ### X-Url-Delete
@@ -86,52 +74,50 @@ https://transfer.sh/1lDau/test.txt --> https://transfer.sh/inline/1lDau/test.txt
## Usage ## Usage
Parameter | Description | Value | Env Parameter | Description | Value | Env
--- |---------------------------------------------------------------------------------------------|------------------------------|----------------------------- --- | --- | --- | ---
listener | port to use for http (:80) | | LISTENER | listener | port to use for http (:80) | | LISTENER |
profile-listener | port to use for profiler (:6060) | | PROFILE_LISTENER | profile-listener | port to use for profiler (:6060) | | PROFILE_LISTENER |
force-https | redirect to https | false | FORCE_HTTPS force-https | redirect to https | false | FORCE_HTTPS
tls-listener | port to use for https (:443) | | TLS_LISTENER | tls-listener | port to use for https (:443) | | TLS_LISTENER |
tls-listener-only | flag to enable tls listener only | | TLS_LISTENER_ONLY | tls-listener-only | flag to enable tls listener only | | TLS_LISTENER_ONLY |
tls-cert-file | path to tls certificate | | TLS_CERT_FILE | tls-cert-file | path to tls certificate | | TLS_CERT_FILE |
tls-private-key | path to tls private key | | TLS_PRIVATE_KEY | tls-private-key | path to tls private key | | TLS_PRIVATE_KEY |
http-auth-user | user for basic http auth on upload | | HTTP_AUTH_USER | http-auth-user | user for basic http auth on upload | | HTTP_AUTH_USER |
http-auth-pass | pass for basic http auth on upload | | HTTP_AUTH_PASS | http-auth-pass | pass for basic http auth on upload | | HTTP_AUTH_PASS |
http-auth-htpasswd | htpasswd file path for basic http auth on upload | | HTTP_AUTH_HTPASSWD | ip-whitelist | comma separated list of ips allowed to connect to the service | | IP_WHITELIST |
http-auth-ip-whitelist | comma separated list of ips allowed to upload without being challenged an http auth | | HTTP_AUTH_IP_WHITELIST | ip-blacklist | comma separated list of ips not allowed to connect to the service | | IP_BLACKLIST |
ip-whitelist | comma separated list of ips allowed to connect to the service | | IP_WHITELIST | temp-path | path to temp folder | system temp | TEMP_PATH |
ip-blacklist | comma separated list of ips not allowed to connect to the service | | IP_BLACKLIST | web-path | path to static web files (for development or custom front end) | | WEB_PATH |
temp-path | path to temp folder | system temp | TEMP_PATH | proxy-path | path prefix when service is run behind a proxy | | PROXY_PATH |
web-path | path to static web files (for development or custom front end) | | WEB_PATH | proxy-port | port of the proxy when the service is run behind a proxy | | PROXY_PORT |
proxy-path | path prefix when service is run behind a proxy | | PROXY_PATH | email-contact | email contact for the front end | | EMAIL_CONTACT |
proxy-port | port of the proxy when the service is run behind a proxy | | PROXY_PORT | ga-key | google analytics key for the front end | | GA_KEY |
email-contact | email contact for the front end | | EMAIL_CONTACT | provider | which storage provider to use | (s3, storj, gdrive or local) |
ga-key | google analytics key for the front end | | GA_KEY | uservoice-key | user voice key for the front end | | USERVOICE_KEY |
provider | which storage provider to use | (s3, storj, gdrive or local) | aws-access-key | aws access key | | AWS_ACCESS_KEY |
uservoice-key | user voice key for the front end | | USERVOICE_KEY | aws-secret-key | aws access key | | AWS_SECRET_KEY |
aws-access-key | aws access key | | AWS_ACCESS_KEY | bucket | aws bucket | | BUCKET |
aws-secret-key | aws access key | | AWS_SECRET_KEY | s3-endpoint | Custom S3 endpoint. | | S3_ENDPOINT |
bucket | aws bucket | | BUCKET | s3-region | region of the s3 bucket | eu-west-1 | S3_REGION |
s3-endpoint | Custom S3 endpoint. | | S3_ENDPOINT | s3-no-multipart | disables s3 multipart upload | false | S3_NO_MULTIPART |
s3-region | region of the s3 bucket | eu-west-1 | S3_REGION | s3-path-style | Forces path style URLs, required for Minio. | false | S3_PATH_STYLE |
s3-no-multipart | disables s3 multipart upload | false | S3_NO_MULTIPART | storj-access | Access for the project | | STORJ_ACCESS |
s3-path-style | Forces path style URLs, required for Minio. | false | S3_PATH_STYLE | storj-bucket | Bucket to use within the project | | STORJ_BUCKET |
storj-access | Access for the project | | STORJ_ACCESS | basedir | path storage for local/gdrive provider | | BASEDIR |
storj-bucket | Bucket to use within the project | | STORJ_BUCKET | gdrive-client-json-filepath | path to oauth client json config for gdrive provider | | GDRIVE_CLIENT_JSON_FILEPATH |
basedir | path storage for local/gdrive provider | | BASEDIR | gdrive-local-config-path | path to store local transfer.sh config cache for gdrive provider| | GDRIVE_LOCAL_CONFIG_PATH |
gdrive-client-json-filepath | path to oauth client json config for gdrive provider | | GDRIVE_CLIENT_JSON_FILEPATH | gdrive-chunk-size | chunk size for gdrive upload in megabytes, must be lower than available memory (8 MB) | | GDRIVE_CHUNK_SIZE |
gdrive-local-config-path | path to store local transfer.sh config cache for gdrive provider | | GDRIVE_LOCAL_CONFIG_PATH | lets-encrypt-hosts | hosts to use for lets encrypt certificates (comma seperated) | | HOSTS |
gdrive-chunk-size | chunk size for gdrive upload in megabytes, must be lower than available memory (8 MB) | | GDRIVE_CHUNK_SIZE | log | path to log file| | LOG |
lets-encrypt-hosts | hosts to use for lets encrypt certificates (comma seperated) | | HOSTS | cors-domains | comma separated list of domains for CORS, setting it enable CORS | | CORS_DOMAINS |
log | path to log file | | LOG | clamav-host | host for clamav feature | | CLAMAV_HOST |
cors-domains | comma separated list of domains for CORS, setting it enable CORS | | CORS_DOMAINS | perform-clamav-prescan | prescan every upload through clamav feature (clamav-host must be a local clamd unix socket) | | PERFORM_CLAMAV_PRESCAN |
clamav-host | host for clamav feature | | CLAMAV_HOST | rate-limit | request per minute | | RATE_LIMIT |
perform-clamav-prescan | prescan every upload through clamav feature (clamav-host must be a local clamd unix socket) | | PERFORM_CLAMAV_PRESCAN | max-upload-size | max upload size in kilobytes | | MAX_UPLOAD_SIZE |
rate-limit | request per minute | | RATE_LIMIT | purge-days | number of days after the uploads are purged automatically | | PURGE_DAYS |
max-upload-size | max upload size in kilobytes | | MAX_UPLOAD_SIZE | purge-interval | interval in hours to run the automatic purge for (not applicable to S3 and Storj) | | PURGE_INTERVAL |
purge-days | number of days after the uploads are purged automatically | | PURGE_DAYS | random-token-length | length of the random token for the upload path (double the size for delete path) | 6 | RANDOM_TOKEN_LENGTH |
purge-interval | interval in hours to run the automatic purge for (not applicable to S3 and Storj) | | PURGE_INTERVAL |
random-token-length | length of the random token for the upload path (double the size for delete path) | 6 | RANDOM_TOKEN_LENGTH |
If you want to use TLS using lets encrypt certificates, set lets-encrypt-hosts to your domain, set tls-listener to :443 and enable force-https. If you want to use TLS using lets encrypt certificates, set lets-encrypt-hosts to your domain, set tls-listener to :443 and enable force-https.
@@ -183,11 +169,11 @@ docker build -t transfer.sh-noroot --build-arg RUNAS=doesntmatter --build-arg PU
## S3 Usage ## S3 Usage
For the usage with a AWS S3 Bucket, you just need to specify the following options: For the usage with a AWS S3 Bucket, you just need to specify the following options:
- provider `--provider s3` - provider
- aws-access-key _(either via flag or environment variable `AWS_ACCESS_KEY`)_ - aws-access-key
- aws-secret-key _(either via flag or environment variable `AWS_SECRET_KEY`)_ - aws-secret-key
- bucket _(either via flag or environment variable `BUCKET`)_ - bucket
- s3-region _(either via flag or environment variable `S3_REGION`)_ - s3-region
If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically. If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically.

View File

@@ -1,17 +1,15 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
"github.com/dutchcoders/transfer.sh/server/storage"
"log" "log"
"os" "os"
"strings" "strings"
"github.com/dutchcoders/transfer.sh/server/storage"
"github.com/dutchcoders/transfer.sh/server" "github.com/dutchcoders/transfer.sh/server"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
"google.golang.org/api/googleapi" "google.golang.org/api/googleapi"
) )
@@ -37,275 +35,263 @@ VERSION:
`{{ "\n"}}` `{{ "\n"}}`
var globalFlags = []cli.Flag{ var globalFlags = []cli.Flag{
&cli.StringFlag{ cli.StringFlag{
Name: "listener", Name: "listener",
Usage: "127.0.0.1:8080", Usage: "127.0.0.1:8080",
Value: "127.0.0.1:8080", Value: "127.0.0.1:8080",
EnvVars: []string{"LISTENER"}, EnvVar: "LISTENER",
}, },
// redirect to https? // redirect to https?
// hostnames // hostnames
&cli.StringFlag{ cli.StringFlag{
Name: "profile-listener", Name: "profile-listener",
Usage: "127.0.0.1:6060", Usage: "127.0.0.1:6060",
Value: "", Value: "",
EnvVars: []string{"PROFILE_LISTENER"}, EnvVar: "PROFILE_LISTENER",
}, },
&cli.BoolFlag{ cli.BoolFlag{
Name: "force-https", Name: "force-https",
Usage: "", Usage: "",
EnvVars: []string{"FORCE_HTTPS"}, EnvVar: "FORCE_HTTPS",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "tls-listener", Name: "tls-listener",
Usage: "127.0.0.1:8443", Usage: "127.0.0.1:8443",
Value: "", Value: "",
EnvVars: []string{"TLS_LISTENER"}, EnvVar: "TLS_LISTENER",
}, },
&cli.BoolFlag{ cli.BoolFlag{
Name: "tls-listener-only", Name: "tls-listener-only",
Usage: "", Usage: "",
EnvVars: []string{"TLS_LISTENER_ONLY"}, EnvVar: "TLS_LISTENER_ONLY",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "tls-cert-file", Name: "tls-cert-file",
Value: "", Value: "",
EnvVars: []string{"TLS_CERT_FILE"}, EnvVar: "TLS_CERT_FILE",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "tls-private-key", Name: "tls-private-key",
Value: "", Value: "",
EnvVars: []string{"TLS_PRIVATE_KEY"}, EnvVar: "TLS_PRIVATE_KEY",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "temp-path", Name: "temp-path",
Usage: "path to temp files", Usage: "path to temp files",
Value: os.TempDir(), Value: os.TempDir(),
EnvVars: []string{"TEMP_PATH"}, EnvVar: "TEMP_PATH",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "web-path", Name: "web-path",
Usage: "path to static web files", Usage: "path to static web files",
Value: "", Value: "",
EnvVars: []string{"WEB_PATH"}, EnvVar: "WEB_PATH",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "proxy-path", Name: "proxy-path",
Usage: "path prefix when service is run behind a proxy", Usage: "path prefix when service is run behind a proxy",
Value: "", Value: "",
EnvVars: []string{"PROXY_PATH"}, EnvVar: "PROXY_PATH",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "proxy-port", Name: "proxy-port",
Usage: "port of the proxy when the service is run behind a proxy", Usage: "port of the proxy when the service is run behind a proxy",
Value: "", Value: "",
EnvVars: []string{"PROXY_PORT"}, EnvVar: "PROXY_PORT",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "email-contact", Name: "email-contact",
Usage: "email address to link in Contact Us (front end)", Usage: "email address to link in Contact Us (front end)",
Value: "", Value: "",
EnvVars: []string{"EMAIL_CONTACT"}, EnvVar: "EMAIL_CONTACT",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "ga-key", Name: "ga-key",
Usage: "key for google analytics (front end)", Usage: "key for google analytics (front end)",
Value: "", Value: "",
EnvVars: []string{"GA_KEY"}, EnvVar: "GA_KEY",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "uservoice-key", Name: "uservoice-key",
Usage: "key for user voice (front end)", Usage: "key for user voice (front end)",
Value: "", Value: "",
EnvVars: []string{"USERVOICE_KEY"}, EnvVar: "USERVOICE_KEY",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "provider", Name: "provider",
Usage: "s3|gdrive|local", Usage: "s3|gdrive|local",
Value: "", Value: "",
EnvVars: []string{"PROVIDER"}, EnvVar: "PROVIDER",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "s3-endpoint", Name: "s3-endpoint",
Usage: "", Usage: "",
Value: "", Value: "",
EnvVars: []string{"S3_ENDPOINT"}, EnvVar: "S3_ENDPOINT",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "s3-region", Name: "s3-region",
Usage: "", Usage: "",
Value: "eu-west-1", Value: "eu-west-1",
EnvVars: []string{"S3_REGION"}, EnvVar: "S3_REGION",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "aws-access-key", Name: "aws-access-key",
Usage: "", Usage: "",
Value: "", Value: "",
EnvVars: []string{"AWS_ACCESS_KEY"}, EnvVar: "AWS_ACCESS_KEY",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "aws-secret-key", Name: "aws-secret-key",
Usage: "", Usage: "",
Value: "", Value: "",
EnvVars: []string{"AWS_SECRET_KEY"}, EnvVar: "AWS_SECRET_KEY",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "bucket", Name: "bucket",
Usage: "", Usage: "",
Value: "", Value: "",
EnvVars: []string{"BUCKET"}, EnvVar: "BUCKET",
}, },
&cli.BoolFlag{ cli.BoolFlag{
Name: "s3-no-multipart", Name: "s3-no-multipart",
Usage: "Disables S3 Multipart Puts", Usage: "Disables S3 Multipart Puts",
EnvVars: []string{"S3_NO_MULTIPART"}, EnvVar: "S3_NO_MULTIPART",
}, },
&cli.BoolFlag{ cli.BoolFlag{
Name: "s3-path-style", Name: "s3-path-style",
Usage: "Forces path style URLs, required for Minio.", Usage: "Forces path style URLs, required for Minio.",
EnvVars: []string{"S3_PATH_STYLE"}, EnvVar: "S3_PATH_STYLE",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "gdrive-client-json-filepath", Name: "gdrive-client-json-filepath",
Usage: "", Usage: "",
Value: "", Value: "",
EnvVars: []string{"GDRIVE_CLIENT_JSON_FILEPATH"}, EnvVar: "GDRIVE_CLIENT_JSON_FILEPATH",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "gdrive-local-config-path", Name: "gdrive-local-config-path",
Usage: "", Usage: "",
Value: "", Value: "",
EnvVars: []string{"GDRIVE_LOCAL_CONFIG_PATH"}, EnvVar: "GDRIVE_LOCAL_CONFIG_PATH",
}, },
&cli.IntFlag{ cli.IntFlag{
Name: "gdrive-chunk-size", Name: "gdrive-chunk-size",
Usage: "", Usage: "",
Value: googleapi.DefaultUploadChunkSize / 1024 / 1024, Value: googleapi.DefaultUploadChunkSize / 1024 / 1024,
EnvVars: []string{"GDRIVE_CHUNK_SIZE"}, EnvVar: "GDRIVE_CHUNK_SIZE",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "storj-access", Name: "storj-access",
Usage: "Access for the project", Usage: "Access for the project",
Value: "", Value: "",
EnvVars: []string{"STORJ_ACCESS"}, EnvVar: "STORJ_ACCESS",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "storj-bucket", Name: "storj-bucket",
Usage: "Bucket to use within the project", Usage: "Bucket to use within the project",
Value: "", Value: "",
EnvVars: []string{"STORJ_BUCKET"}, EnvVar: "STORJ_BUCKET",
}, },
&cli.IntFlag{ cli.IntFlag{
Name: "rate-limit", Name: "rate-limit",
Usage: "requests per minute", Usage: "requests per minute",
Value: 0, Value: 0,
EnvVars: []string{"RATE_LIMIT"}, EnvVar: "RATE_LIMIT",
}, },
&cli.IntFlag{ cli.IntFlag{
Name: "purge-days", Name: "purge-days",
Usage: "number of days after uploads are purged automatically", Usage: "number of days after uploads are purged automatically",
Value: 0, Value: 0,
EnvVars: []string{"PURGE_DAYS"}, EnvVar: "PURGE_DAYS",
}, },
&cli.IntFlag{ cli.IntFlag{
Name: "purge-interval", Name: "purge-interval",
Usage: "interval in hours to run the automatic purge for", Usage: "interval in hours to run the automatic purge for",
Value: 0, Value: 0,
EnvVars: []string{"PURGE_INTERVAL"}, EnvVar: "PURGE_INTERVAL",
}, },
&cli.Int64Flag{ cli.Int64Flag{
Name: "max-upload-size", Name: "max-upload-size",
Usage: "max limit for upload, in kilobytes", Usage: "max limit for upload, in kilobytes",
Value: 0, Value: 0,
EnvVars: []string{"MAX_UPLOAD_SIZE"}, EnvVar: "MAX_UPLOAD_SIZE",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "lets-encrypt-hosts", Name: "lets-encrypt-hosts",
Usage: "host1, host2", Usage: "host1, host2",
Value: "", Value: "",
EnvVars: []string{"HOSTS"}, EnvVar: "HOSTS",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "log", Name: "log",
Usage: "/var/log/transfersh.log", Usage: "/var/log/transfersh.log",
Value: "", Value: "",
EnvVars: []string{"LOG"}, EnvVar: "LOG",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "basedir", Name: "basedir",
Usage: "path to storage", Usage: "path to storage",
Value: "", Value: "",
EnvVars: []string{"BASEDIR"}, EnvVar: "BASEDIR",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "clamav-host", Name: "clamav-host",
Usage: "clamav-host", Usage: "clamav-host",
Value: "", Value: "",
EnvVars: []string{"CLAMAV_HOST"}, EnvVar: "CLAMAV_HOST",
}, },
&cli.BoolFlag{ cli.BoolFlag{
Name: "perform-clamav-prescan", Name: "perform-clamav-prescan",
Usage: "perform-clamav-prescan", Usage: "perform-clamav-prescan",
EnvVars: []string{"PERFORM_CLAMAV_PRESCAN"}, EnvVar: "PERFORM_CLAMAV_PRESCAN",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "virustotal-key", Name: "virustotal-key",
Usage: "virustotal-key", Usage: "virustotal-key",
Value: "", Value: "",
EnvVars: []string{"VIRUSTOTAL_KEY"}, EnvVar: "VIRUSTOTAL_KEY",
}, },
&cli.BoolFlag{ cli.BoolFlag{
Name: "profiler", Name: "profiler",
Usage: "enable profiling", Usage: "enable profiling",
EnvVars: []string{"PROFILER"}, EnvVar: "PROFILER",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "http-auth-user", Name: "http-auth-user",
Usage: "user for http basic auth", Usage: "user for http basic auth",
Value: "", Value: "",
EnvVars: []string{"HTTP_AUTH_USER"}, EnvVar: "HTTP_AUTH_USER",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "http-auth-pass", Name: "http-auth-pass",
Usage: "pass for http basic auth", Usage: "pass for http basic auth",
Value: "", Value: "",
EnvVars: []string{"HTTP_AUTH_PASS"}, EnvVar: "HTTP_AUTH_PASS",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "http-auth-htpasswd", Name: "ip-whitelist",
Usage: "htpasswd file http basic auth", Usage: "comma separated list of ips allowed to connect to the service",
Value: "", Value: "",
EnvVars: []string{"HTTP_AUTH_HTPASSWD"}, EnvVar: "IP_WHITELIST",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "http-auth-ip-whitelist", Name: "ip-blacklist",
Usage: "comma separated list of ips allowed to upload without being challenged an http auth", Usage: "comma separated list of ips not allowed to connect to the service",
Value: "", Value: "",
EnvVars: []string{"HTTP_AUTH_IP_WHITELIST"}, EnvVar: "IP_BLACKLIST",
}, },
&cli.StringFlag{ cli.StringFlag{
Name: "ip-whitelist", Name: "cors-domains",
Usage: "comma separated list of ips allowed to connect to the service", Usage: "comma separated list of domains allowed for CORS requests",
Value: "", Value: "",
EnvVars: []string{"IP_WHITELIST"}, EnvVar: "CORS_DOMAINS",
}, },
&cli.StringFlag{ cli.IntFlag{
Name: "ip-blacklist", Name: "random-token-length",
Usage: "comma separated list of ips not allowed to connect to the service", Usage: "",
Value: "", Value: 6,
EnvVars: []string{"IP_BLACKLIST"}, EnvVar: "RANDOM_TOKEN_LENGTH",
},
&cli.StringFlag{
Name: "cors-domains",
Usage: "comma separated list of domains allowed for CORS requests",
Value: "",
EnvVars: []string{"CORS_DOMAINS"},
},
&cli.IntFlag{
Name: "random-token-length",
Usage: "",
Value: 10,
EnvVars: []string{"RANDOM_TOKEN_LENGTH"},
}, },
} }
@@ -314,9 +300,8 @@ type Cmd struct {
*cli.App *cli.App
} }
func versionCommand(_ *cli.Context) error { func versionCommand(_ *cli.Context) {
fmt.Println(color.YellowString("transfer.sh %s: Easy file sharing from the command line", Version)) fmt.Println(color.YellowString("transfer.sh %s: Easy file sharing from the command line", Version))
return nil
} }
// New is the factory for transfer.sh // New is the factory for transfer.sh
@@ -325,13 +310,13 @@ func New() *Cmd {
app := cli.NewApp() app := cli.NewApp()
app.Name = "transfer.sh" app.Name = "transfer.sh"
app.Authors = []*cli.Author{} app.Author = ""
app.Usage = "transfer.sh" app.Usage = "transfer.sh"
app.Description = `Easy file sharing from the command line` app.Description = `Easy file sharing from the command line`
app.Version = Version app.Version = Version
app.Flags = globalFlags app.Flags = globalFlags
app.CustomAppHelpTemplate = helpTemplate app.CustomAppHelpTemplate = helpTemplate
app.Commands = []*cli.Command{ app.Commands = []cli.Command{
{ {
Name: "version", Name: "version",
Action: versionCommand, Action: versionCommand,
@@ -342,7 +327,7 @@ func New() *Cmd {
return nil return nil
} }
app.Action = func(c *cli.Context) error { app.Action = func(c *cli.Context) {
var options []server.OptionFn var options []server.OptionFn
if v := c.String("listener"); v != "" { if v := c.String("listener"); v != "" {
options = append(options, server.Listener(v)) options = append(options, server.Listener(v))
@@ -411,7 +396,7 @@ func New() *Cmd {
if v := c.Bool("perform-clamav-prescan"); v { if v := c.Bool("perform-clamav-prescan"); v {
if c.String("clamav-host") == "" { if c.String("clamav-host") == "" {
return errors.New("clamav-host not set") panic("clamav-host not set")
} }
options = append(options, server.PerformClamavPrescan(v)) options = append(options, server.PerformClamavPrescan(v))
@@ -454,17 +439,6 @@ func New() *Cmd {
options = append(options, server.HTTPAuthCredentials(httpAuthUser, httpAuthPass)) options = append(options, server.HTTPAuthCredentials(httpAuthUser, httpAuthPass))
} }
if httpAuthHtpasswd := c.String("http-auth-htpasswd"); httpAuthHtpasswd != "" {
options = append(options, server.HTTPAuthHtpasswd(httpAuthHtpasswd))
}
if httpAuthIPWhitelist := c.String("http-auth-ip-whitelist"); httpAuthIPWhitelist != "" {
ipFilterOptions := server.IPFilterOptions{}
ipFilterOptions.AllowedIPs = strings.Split(httpAuthIPWhitelist, ",")
ipFilterOptions.BlockByDefault = false
options = append(options, server.HTTPAUTHFilterOptions(ipFilterOptions))
}
applyIPFilter := false applyIPFilter := false
ipFilterOptions := server.IPFilterOptions{} ipFilterOptions := server.IPFilterOptions{}
if ipWhitelist := c.String("ip-whitelist"); ipWhitelist != "" { if ipWhitelist := c.String("ip-whitelist"); ipWhitelist != "" {
@@ -485,13 +459,13 @@ func New() *Cmd {
switch provider := c.String("provider"); provider { switch provider := c.String("provider"); provider {
case "s3": case "s3":
if accessKey := c.String("aws-access-key"); accessKey == "" { if accessKey := c.String("aws-access-key"); accessKey == "" {
return errors.New("access-key not set.") panic("access-key not set.")
} else if secretKey := c.String("aws-secret-key"); secretKey == "" { } else if secretKey := c.String("aws-secret-key"); secretKey == "" {
return errors.New("secret-key not set.") panic("secret-key not set.")
} else if bucket := c.String("bucket"); bucket == "" { } else if bucket := c.String("bucket"); bucket == "" {
return errors.New("bucket not set.") panic("bucket not set.")
} else if store, err := storage.NewS3Storage(c.Context, accessKey, secretKey, bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil { } else if store, err := storage.NewS3Storage(accessKey, secretKey, bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil {
return err panic(err)
} else { } else {
options = append(options, server.UseStorage(store)) options = append(options, server.UseStorage(store))
} }
@@ -499,36 +473,36 @@ func New() *Cmd {
chunkSize := c.Int("gdrive-chunk-size") * 1024 * 1024 chunkSize := c.Int("gdrive-chunk-size") * 1024 * 1024
if clientJSONFilepath := c.String("gdrive-client-json-filepath"); clientJSONFilepath == "" { if clientJSONFilepath := c.String("gdrive-client-json-filepath"); clientJSONFilepath == "" {
return errors.New("gdrive-client-json-filepath not set.") panic("gdrive-client-json-filepath not set.")
} else if localConfigPath := c.String("gdrive-local-config-path"); localConfigPath == "" { } else if localConfigPath := c.String("gdrive-local-config-path"); localConfigPath == "" {
return errors.New("gdrive-local-config-path not set.") panic("gdrive-local-config-path not set.")
} else if basedir := c.String("basedir"); basedir == "" { } else if basedir := c.String("basedir"); basedir == "" {
return errors.New("basedir not set.") panic("basedir not set.")
} else if store, err := storage.NewGDriveStorage(c.Context, clientJSONFilepath, localConfigPath, basedir, chunkSize, logger); err != nil { } else if store, err := storage.NewGDriveStorage(clientJSONFilepath, localConfigPath, basedir, chunkSize, logger); err != nil {
return err panic(err)
} else { } else {
options = append(options, server.UseStorage(store)) options = append(options, server.UseStorage(store))
} }
case "storj": case "storj":
if access := c.String("storj-access"); access == "" { if access := c.String("storj-access"); access == "" {
return errors.New("storj-access not set.") panic("storj-access not set.")
} else if bucket := c.String("storj-bucket"); bucket == "" { } else if bucket := c.String("storj-bucket"); bucket == "" {
return errors.New("storj-bucket not set.") panic("storj-bucket not set.")
} else if store, err := storage.NewStorjStorage(c.Context, access, bucket, purgeDays, logger); err != nil { } else if store, err := storage.NewStorjStorage(access, bucket, purgeDays, logger); err != nil {
return err panic(err)
} else { } else {
options = append(options, server.UseStorage(store)) options = append(options, server.UseStorage(store))
} }
case "local": case "local":
if v := c.String("basedir"); v == "" { if v := c.String("basedir"); v == "" {
return errors.New("basedir not set.") panic("basedir not set.")
} else if store, err := storage.NewLocalStorage(v, logger); err != nil { } else if store, err := storage.NewLocalStorage(v, logger); err != nil {
return err panic(err)
} else { } else {
options = append(options, server.UseStorage(store)) options = append(options, server.UseStorage(store))
} }
default: default:
return errors.New("Provider not set or invalid.") panic("Provider not set or invalid.")
} }
srvr, err := server.New( srvr, err := server.New(
@@ -537,11 +511,10 @@ func New() *Cmd {
if err != nil { if err != nil {
logger.Println(color.RedString("Error starting server: %s", err.Error())) logger.Println(color.RedString("Error starting server: %s", err.Error()))
return err return
} }
srvr.Run() srvr.Run()
return nil
} }
return &Cmd{ return &Cmd{

View File

@@ -44,8 +44,6 @@
tls-private-key = mkOption { type = types.nullOr types.str; description = "path to tls private key "; }; tls-private-key = mkOption { type = types.nullOr types.str; description = "path to tls private key "; };
http-auth-user = mkOption { type = types.nullOr types.str; description = "user for basic http auth on upload"; }; http-auth-user = mkOption { type = types.nullOr types.str; description = "user for basic http auth on upload"; };
http-auth-pass = mkOption { type = types.nullOr types.str; description = "pass for basic http auth on upload"; }; http-auth-pass = mkOption { type = types.nullOr types.str; description = "pass for basic http auth on upload"; };
http-auth-htpasswd = mkOption { type = types.nullOr types.str; description = "htpasswd file path for basic http auth on upload"; };
http-auth-ip-whitelist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips allowed to upload without being challenged an http auth"; };
ip-whitelist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips allowed to connect to the service"; }; ip-whitelist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips allowed to connect to the service"; };
ip-blacklist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips not allowed to connect to the service"; }; ip-blacklist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips not allowed to connect to the service"; };
temp-path = mkOption { type = types.nullOr types.str; description = "path to temp folder"; }; temp-path = mkOption { type = types.nullOr types.str; description = "path to temp folder"; };

113
go.mod
View File

@@ -1,97 +1,40 @@
module github.com/dutchcoders/transfer.sh module github.com/dutchcoders/transfer.sh
go 1.18 go 1.15
require (
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8
github.com/ProtonMail/gopenpgp/v2 v2.5.2
github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14
github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2
github.com/aws/aws-sdk-go-v2 v1.18.0
github.com/aws/aws-sdk-go-v2/config v1.18.25
github.com/aws/aws-sdk-go-v2/credentials v1.13.24
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
github.com/dutchcoders/go-virustotal v0.0.0-20140923143438-24cc8e6fa329
github.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6
github.com/elazarl/go-bindata-assetfs v1.0.1
github.com/fatih/color v1.14.1
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/microcosm-cc/bluemonday v1.0.23
github.com/russross/blackfriday/v2 v2.1.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tg123/go-htpasswd v1.2.1
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
github.com/urfave/cli/v2 v2.25.3
golang.org/x/crypto v0.6.0
golang.org/x/net v0.8.0
golang.org/x/oauth2 v0.5.0
google.golang.org/api v0.111.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
storj.io/common v0.0.0-20230301105927-7f966760c100
storj.io/uplink v1.10.0
)
require ( require (
cloud.google.com/go/compute v1.18.0 // indirect cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect github.com/aws/aws-sdk-go v1.37.14
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect github.com/dutchcoders/go-virustotal v0.0.0-20140923143438-24cc8e6fa329
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect github.com/dutchcoders/transfer.sh-web v0.0.0-20220824020025-7240e75c3bb8
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect github.com/elazarl/go-bindata-assetfs v1.0.1
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect github.com/fatih/color v1.10.0
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect github.com/garyburd/redigo v1.6.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/calebcase/tmpfile v1.0.3 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/flynn/noise v1.0.0 // indirect
github.com/garyburd/redigo v1.6.4 // indirect
github.com/gogo/protobuf v1.3.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.2 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.2 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/gorilla/handlers v1.5.1
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/gorilla/mux v1.8.0
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/microcosm-cc/bluemonday v1.0.16
github.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f // indirect
github.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0
github.com/spacemonkeygo/monkit/v3 v3.0.19 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 // indirect github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/urfave/cli v1.22.5
github.com/zeebo/blake3 v0.2.3 // indirect golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
github.com/zeebo/errs v1.3.0 // indirect golang.org/x/net v0.6.0 // indirect
go.opencensus.io v0.24.0 // indirect golang.org/x/oauth2 v0.5.0
golang.org/x/sync v0.1.0 // indirect google.golang.org/api v0.109.0
golang.org/x/sys v0.6.0 // indirect google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect
golang.org/x/text v0.8.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230227214838-9b19f0bdc514 // indirect
google.golang.org/grpc v1.53.0 // indirect google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15
storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a // indirect storj.io/common v0.0.0-20220405183405-ffdc3ab808c6
storj.io/picobuf v0.0.1 // indirect storj.io/uplink v1.8.2
) )

1332
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -39,6 +39,7 @@ import (
"html" "html"
htmlTemplate "html/template" htmlTemplate "html/template"
"io" "io"
"io/ioutil"
"mime" "mime"
"net" "net"
"net/http" "net/http"
@@ -52,19 +53,13 @@ import (
textTemplate "text/template" textTemplate "text/template"
"time" "time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/ProtonMail/gopenpgp/v2/constants"
"github.com/dutchcoders/transfer.sh/server/storage" "github.com/dutchcoders/transfer.sh/server/storage"
"github.com/tg123/go-htpasswd"
"github.com/tomasen/realip"
web "github.com/dutchcoders/transfer.sh-web" web "github.com/dutchcoders/transfer.sh-web"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
blackfriday "github.com/russross/blackfriday/v2" "github.com/russross/blackfriday/v2"
qrcode "github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
"golang.org/x/net/idna" "golang.org/x/net/idna"
) )
@@ -96,128 +91,6 @@ func initHTMLTemplates() *htmlTemplate.Template {
return templates return templates
} }
func attachEncryptionReader(reader io.ReadCloser, password string) (io.ReadCloser, error) {
if len(password) == 0 {
return reader, nil
}
return encrypt(reader, []byte(password))
}
func attachDecryptionReader(reader io.ReadCloser, password string) (io.ReadCloser, error) {
if len(password) == 0 {
return reader, nil
}
return decrypt(reader, []byte(password))
}
func decrypt(ciphertext io.ReadCloser, password []byte) (plaintext io.ReadCloser, err error) {
unarmored, err := armor.Decode(ciphertext)
if err != nil {
return
}
firstTimeCalled := true
var prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
if firstTimeCalled {
firstTimeCalled = false
return password, nil
}
// Re-prompt still occurs if SKESK pasrsing fails (i.e. when decrypted cipher algo is invalid).
// For most (but not all) cases, inputting a wrong passwords is expected to trigger this error.
return nil, errors.New("gopenpgp: wrong password in symmetric decryption")
}
config := &packet.Config{
DefaultCipher: packet.CipherAES256,
}
var emptyKeyRing openpgp.EntityList
md, err := openpgp.ReadMessage(unarmored.Body, emptyKeyRing, prompt, config)
if err != nil {
// Parsing errors when reading the message are most likely caused by incorrect password, but we cannot know for sure
return
}
plaintext = io.NopCloser(md.UnverifiedBody)
return
}
type encryptWrapperReader struct {
plaintext io.Reader
encrypt io.WriteCloser
armored io.WriteCloser
buffer io.ReadWriter
plaintextReadZero bool
}
func (e *encryptWrapperReader) Read(p []byte) (n int, err error) {
p2 := make([]byte, len(p))
n, _ = e.plaintext.Read(p2)
if n == 0 {
if !e.plaintextReadZero {
err = e.encrypt.Close()
if err != nil {
return
}
err = e.armored.Close()
if err != nil {
return
}
e.plaintextReadZero = true
}
return e.buffer.Read(p)
}
return e.buffer.Read(p)
}
func (e *encryptWrapperReader) Close() error {
return nil
}
func NewEncryptWrapperReader(plaintext io.Reader, armored, encrypt io.WriteCloser, buffer io.ReadWriter) io.ReadCloser {
return &encryptWrapperReader{
plaintext: io.TeeReader(plaintext, encrypt),
encrypt: encrypt,
armored: armored,
buffer: buffer,
}
}
func encrypt(plaintext io.ReadCloser, password []byte) (ciphertext io.ReadCloser, err error) {
bufferReadWriter := new(bytes.Buffer)
armored, err := armor.Encode(bufferReadWriter, constants.PGPMessageHeader, nil)
if err != nil {
return
}
config := &packet.Config{
DefaultCipher: packet.CipherAES256,
Time: time.Now,
}
hints := &openpgp.FileHints{
IsBinary: true,
FileName: "",
ModTime: time.Unix(time.Now().Unix(), 0),
}
encryptWriter, err := openpgp.SymmetricallyEncrypt(armored, password, hints, config)
if err != nil {
return
}
ciphertext = NewEncryptWrapperReader(plaintext, armored, encryptWriter, bufferReadWriter)
return
}
func healthHandler(w http.ResponseWriter, _ *http.Request) { func healthHandler(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("Approaching Neutral Zone, all systems normal and functioning.")) _, _ = w.Write([]byte("Approaching Neutral Zone, all systems normal and functioning."))
} }
@@ -245,8 +118,6 @@ func canContainsXSS(contentType string) bool {
/* The preview handler will show a preview of the content for browsers (accept type text/html), and referer is not transfer.sh */ /* The preview handler will show a preview of the content for browsers (accept type text/html), and referer is not transfer.sh */
func (s *Server) previewHandler(w http.ResponseWriter, r *http.Request) { func (s *Server) previewHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password")
vars := mux.Vars(r) vars := mux.Vars(r)
token := vars["token"] token := vars["token"]
@@ -374,7 +245,7 @@ func (s *Server) viewHandler(w http.ResponseWriter, r *http.Request) {
purgeTime := "" purgeTime := ""
if s.purgeDays > 0 { if s.purgeDays > 0 {
purgeTime = formatDurationDays(s.purgeDays) purgeTime = s.purgeDays.String()
} }
data := struct { data := struct {
@@ -399,7 +270,6 @@ func (s *Server) viewHandler(w http.ResponseWriter, r *http.Request) {
token(s.randomTokenLength), token(s.randomTokenLength),
} }
w.Header().Set("Vary", "Accept")
if acceptsHTML(r.Header) { if acceptsHTML(r.Header) {
if err := htmlTemplates.ExecuteTemplate(w, "index.html", data); err != nil { if err := htmlTemplates.ExecuteTemplate(w, "index.html", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -448,7 +318,7 @@ func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
file, err := os.CreateTemp(s.tempPath, "transfer-") file, err := ioutil.TempFile(s.tempPath, "transfer-")
defer s.cleanTmpFile(file) defer s.cleanTmpFile(file)
if err != nil { if err != nil {
@@ -493,7 +363,7 @@ func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
metadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r) metadata := metadataForRequest(contentType, s.randomTokenLength, r)
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
if err := json.NewEncoder(buffer).Encode(metadata); err != nil { if err := json.NewEncoder(buffer).Encode(metadata); err != nil {
@@ -510,13 +380,7 @@ func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType) s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType)
reader, err := attachEncryptionReader(file, r.Header.Get("X-Encrypt-Password")) if err = s.storage.Put(r.Context(), token, filename, file, contentType, uint64(contentLength)); err != nil {
if err != nil {
http.Error(w, "Could not crypt file", http.StatusInternalServerError)
return
}
if err = s.storage.Put(r.Context(), token, filename, reader, contentType, uint64(contentLength)); err != nil {
s.logger.Printf("Backend storage error: %s", err.Error()) s.logger.Printf("Backend storage error: %s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@@ -554,8 +418,8 @@ func (s *Server) cleanTmpFile(f *os.File) {
type metadata struct { type metadata struct {
// ContentType is the original uploading content type // ContentType is the original uploading content type
ContentType string ContentType string
// ContentLength is is the original uploading content length // Secret as knowledge to delete file
ContentLength int64 // Secret string
// Downloads is the actual number of downloads // Downloads is the actual number of downloads
Downloads int Downloads int
// MaxDownloads contains the maximum numbers of downloads // MaxDownloads contains the maximum numbers of downloads
@@ -564,16 +428,11 @@ type metadata struct {
MaxDate time.Time MaxDate time.Time
// DeletionToken contains the token to match against for deletion // DeletionToken contains the token to match against for deletion
DeletionToken string DeletionToken string
// Encrypted contains if the file was encrypted
Encrypted bool
// DecryptedContentType is the original uploading content type
DecryptedContentType string
} }
func metadataForRequest(contentType string, contentLength int64, randomTokenLength int, r *http.Request) metadata { func metadataForRequest(contentType string, randomTokenLength int, r *http.Request) metadata {
metadata := metadata{ metadata := metadata{
ContentType: strings.ToLower(contentType), ContentType: strings.ToLower(contentType),
ContentLength: contentLength,
MaxDate: time.Time{}, MaxDate: time.Time{},
Downloads: 0, Downloads: 0,
MaxDownloads: -1, MaxDownloads: -1,
@@ -592,14 +451,6 @@ func metadataForRequest(contentType string, contentLength int64, randomTokenLeng
metadata.MaxDate = time.Now().Add(time.Hour * 24 * time.Duration(v)) metadata.MaxDate = time.Now().Add(time.Hour * 24 * time.Duration(v))
} }
if password := r.Header.Get("X-Encrypt-Password"); password != "" {
metadata.Encrypted = true
metadata.ContentType = "text/plain; charset=utf-8"
metadata.DecryptedContentType = contentType
} else {
metadata.Encrypted = false
}
return metadata return metadata
} }
@@ -612,53 +463,34 @@ func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
defer storage.CloseCheck(r.Body) defer storage.CloseCheck(r.Body)
reader := r.Body file, err := ioutil.TempFile(s.tempPath, "transfer-")
defer s.cleanTmpFile(file)
if err != nil {
s.logger.Printf("%s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if contentLength < 1 || s.performClamavPrescan { // queue file to disk, because s3 needs content length
file, err := os.CreateTemp(s.tempPath, "transfer-") // and clamav prescan scans a file
defer s.cleanTmpFile(file) n, err := io.Copy(file, r.Body)
if err != nil { if err != nil {
s.logger.Printf("%s", err.Error()) s.logger.Printf("%s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// queue file to disk, because s3 needs content length return
// and clamav prescan scans a file }
n, err := io.Copy(file, r.Body)
if err != nil {
s.logger.Printf("%s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return _, err = file.Seek(0, io.SeekStart)
} if err != nil {
s.logger.Printf("%s", err.Error())
http.Error(w, "Cannot reset cache file", http.StatusInternalServerError)
_, err = file.Seek(0, io.SeekStart) return
if err != nil { }
s.logger.Printf("%s", err.Error())
http.Error(w, "Cannot reset cache file", http.StatusInternalServerError)
return
}
if contentLength < 1 {
contentLength = n contentLength = n
if s.performClamavPrescan {
status, err := s.performScan(file.Name())
if err != nil {
s.logger.Printf("%s", err.Error())
http.Error(w, "Could not perform prescan", http.StatusInternalServerError)
return
}
if status != clamavScanStatusOK {
s.logger.Printf("prescan positive: %s", status)
http.Error(w, "Clamav prescan found a virus", http.StatusPreconditionFailed)
return
}
}
reader = file
} }
if s.maxUploadSize > 0 && contentLength > s.maxUploadSize { if s.maxUploadSize > 0 && contentLength > s.maxUploadSize {
@@ -673,11 +505,26 @@ func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if s.performClamavPrescan {
status, err := s.performScan(file.Name())
if err != nil {
s.logger.Printf("%s", err.Error())
http.Error(w, "Could not perform prescan", http.StatusInternalServerError)
return
}
if status != clamavScanStatusOK {
s.logger.Printf("prescan positive: %s", status)
http.Error(w, "Clamav prescan found a virus", http.StatusPreconditionFailed)
return
}
}
contentType := mime.TypeByExtension(filepath.Ext(vars["filename"])) contentType := mime.TypeByExtension(filepath.Ext(vars["filename"]))
token := token(s.randomTokenLength) token := token(s.randomTokenLength)
metadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r) metadata := metadataForRequest(contentType, s.randomTokenLength, r)
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
if err := json.NewEncoder(buffer).Encode(metadata); err != nil { if err := json.NewEncoder(buffer).Encode(metadata); err != nil {
@@ -696,13 +543,7 @@ func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType) s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType)
reader, err := attachEncryptionReader(reader, r.Header.Get("X-Encrypt-Password")) if err = s.storage.Put(r.Context(), token, filename, file, contentType, uint64(contentLength)); err != nil {
if err != nil {
http.Error(w, "Could not crypt file", http.StatusInternalServerError)
return
}
if err = s.storage.Put(r.Context(), token, filename, reader, contentType, uint64(contentLength)); err != nil {
s.logger.Printf("Error putting new file: %s", err.Error()) s.logger.Printf("Error putting new file: %s", err.Error())
http.Error(w, "Could not save file", http.StatusInternalServerError) http.Error(w, "Could not save file", http.StatusInternalServerError)
return return
@@ -1160,7 +1001,6 @@ func (s *Server) headHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
w.Header().Set("X-Remaining-Downloads", remainingDownloads) w.Header().Set("X-Remaining-Downloads", remainingDownloads)
w.Header().Set("X-Remaining-Days", remainingDays) w.Header().Set("X-Remaining-Days", remainingDays)
w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password")
if s.storage.IsRangeSupported() { if s.storage.IsRangeSupported() {
w.Header().Set("Accept-Ranges", "bytes") w.Header().Set("Accept-Ranges", "bytes")
@@ -1191,6 +1031,8 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
reader, contentLength, err := s.storage.Get(r.Context(), token, filename, rng) reader, contentLength, err := s.storage.Get(r.Context(), token, filename, rng)
defer storage.CloseCheck(reader) defer storage.CloseCheck(reader)
rdr := io.Reader(reader)
if s.storage.IsNotExist(err) { if s.storage.IsNotExist(err) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return return
@@ -1205,20 +1047,23 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Accept-Ranges", "bytes") w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Range", cr) w.Header().Set("Content-Range", cr)
if rng.Limit > 0 { if rng.Limit > 0 {
reader = io.NopCloser(io.LimitReader(reader, int64(rng.Limit))) rdr = io.LimitReader(reader, int64(rng.Limit))
} }
} }
} }
var disposition string var disposition string
if action == "inline" { if action == "inline" {
disposition = "inline" disposition = "inline"
/* /*
metadata.ContentType is unable to determine the type of the content,
metadata.ContentType is unable to determine the type of the content,
metadata.ContentType is unable to determine the type of the content, metadata.ContentType is unable to determine the type of the content,
So add text/plain in this case to fix XSS related issues/ So add text/plain in this case to fix XSS related issues/
*/ */
if strings.TrimSpace(contentType) == "" { if strings.TrimSpace(contentType) == "" {
contentType = "text/plain; charset=utf-8" contentType = "text/plain"
} }
} else { } else {
disposition = "attachment" disposition = "attachment"
@@ -1226,37 +1071,23 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
remainingDownloads, remainingDays := metadata.remainingLimitHeaderValues() remainingDownloads, remainingDays := metadata.remainingLimitHeaderValues()
w.Header().Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"`, disposition, filename)) w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", strconv.FormatUint(contentLength, 10))
w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"", disposition, filename))
w.Header().Set("Connection", "keep-alive") w.Header().Set("Connection", "keep-alive")
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Remaining-Downloads", remainingDownloads) w.Header().Set("X-Remaining-Downloads", remainingDownloads)
w.Header().Set("X-Remaining-Days", remainingDays) w.Header().Set("X-Remaining-Days", remainingDays)
password := r.Header.Get("X-Decrypt-Password")
reader, err = attachDecryptionReader(reader, password)
if err != nil {
http.Error(w, "Could not decrypt file", http.StatusInternalServerError)
return
}
if metadata.Encrypted && len(password) > 0 {
contentType = metadata.DecryptedContentType
contentLength = uint64(metadata.ContentLength)
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", strconv.FormatUint(contentLength, 10))
w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password")
if rng != nil && rng.ContentRange() != "" { if rng != nil && rng.ContentRange() != "" {
w.WriteHeader(http.StatusPartialContent) w.WriteHeader(http.StatusPartialContent)
} }
if disposition == "inline" && canContainsXSS(contentType) { if disposition == "inline" && canContainsXSS(contentType) {
reader = io.NopCloser(bluemonday.UGCPolicy().SanitizeReader(reader)) reader = ioutil.NopCloser(bluemonday.UGCPolicy().SanitizeReader(reader))
} }
if _, err = io.Copy(w, reader); err != nil { if _, err = io.Copy(w, rdr); err != nil {
s.logger.Printf("%s", err.Error()) s.logger.Printf("%s", err.Error())
http.Error(w, "Error occurred copying to output stream", http.StatusInternalServerError) http.Error(w, "Error occurred copying to output stream", http.StatusInternalServerError)
return return
@@ -1319,55 +1150,27 @@ func ipFilterHandler(h http.Handler, ipFilterOptions *IPFilterOptions) http.Hand
if ipFilterOptions == nil { if ipFilterOptions == nil {
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
} else { } else {
WrapIPFilter(h, ipFilterOptions).ServeHTTP(w, r) WrapIPFilter(h, *ipFilterOptions).ServeHTTP(w, r)
} }
} }
} }
func (s *Server) basicAuthHandler(h http.Handler) http.HandlerFunc { func (s *Server) basicAuthHandler(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if s.authUser == "" && s.authPass == "" && s.authHtpasswd == "" { if s.AuthUser == "" || s.AuthPass == "" {
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
return return
} }
if s.htpasswdFile == nil && s.authHtpasswd != "" {
htpasswdFile, err := htpasswd.New(s.authHtpasswd, htpasswd.DefaultSystems, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
s.htpasswdFile = htpasswdFile
}
if s.authIPFilter == nil && s.authIPFilterOptions != nil {
s.authIPFilter = newIPFilter(s.authIPFilterOptions)
}
w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"") w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"")
var authorized bool
if s.authIPFilter != nil {
remoteIP := realip.FromRequest(r)
authorized = s.authIPFilter.Allowed(remoteIP)
}
username, password, authOK := r.BasicAuth() username, password, authOK := r.BasicAuth()
if !authOK && !authorized { if !authOK {
http.Error(w, "Not authorized", http.StatusUnauthorized) http.Error(w, "Not authorized", http.StatusUnauthorized)
return return
} }
if !authorized && username == s.authUser && password == s.authPass { if username != s.AuthUser || password != s.AuthPass {
authorized = true
}
if !authorized && s.htpasswdFile != nil {
authorized = s.htpasswdFile.Match(username, password)
}
if !authorized {
http.Error(w, "Not authorized", http.StatusUnauthorized) http.Error(w, "Not authorized", http.StatusUnauthorized)
return return
} }

View File

@@ -21,13 +21,13 @@ import (
"github.com/tomasen/realip" "github.com/tomasen/realip"
) )
// IPFilterOptions for ipFilter. Allowed takes precedence over Blocked. //IPFilterOptions for ipFilter. Allowed takes precedence over Blocked.
// IPs can be IPv4 or IPv6 and can optionally contain subnet //IPs can be IPv4 or IPv6 and can optionally contain subnet
// masks (/24). Note however, determining if a given IP is //masks (/24). Note however, determining if a given IP is
// included in a subnet requires a linear scan so is less performant //included in a subnet requires a linear scan so is less performant
// than looking up single IPs. //than looking up single IPs.
// //
// This could be improved with some algorithmic magic. //This could be improved with some algorithmic magic.
type IPFilterOptions struct { type IPFilterOptions struct {
//explicity allowed IPs //explicity allowed IPs
AllowedIPs []string AllowedIPs []string
@@ -45,6 +45,7 @@ type IPFilterOptions struct {
// ipFilter // ipFilter
type ipFilter struct { type ipFilter struct {
opts IPFilterOptions
//mut protects the below //mut protects the below
//rw since writes are rare //rw since writes are rare
mut sync.RWMutex mut sync.RWMutex
@@ -59,12 +60,13 @@ type subnet struct {
allowed bool allowed bool
} }
func newIPFilter(opts *IPFilterOptions) *ipFilter { func newIPFilter(opts IPFilterOptions) *ipFilter {
if opts.Logger == nil { if opts.Logger == nil {
flags := log.LstdFlags flags := log.LstdFlags
opts.Logger = log.New(os.Stdout, "", flags) opts.Logger = log.New(os.Stdout, "", flags)
} }
f := &ipFilter{ f := &ipFilter{
opts: opts,
ips: map[string]bool{}, ips: map[string]bool{},
defaultAllowed: !opts.BlockByDefault, defaultAllowed: !opts.BlockByDefault,
} }
@@ -125,19 +127,19 @@ func (f *ipFilter) ToggleIP(str string, allowed bool) bool {
return false return false
} }
// ToggleDefault alters the default setting //ToggleDefault alters the default setting
func (f *ipFilter) ToggleDefault(allowed bool) { func (f *ipFilter) ToggleDefault(allowed bool) {
f.mut.Lock() f.mut.Lock()
f.defaultAllowed = allowed f.defaultAllowed = allowed
f.mut.Unlock() f.mut.Unlock()
} }
// Allowed returns if a given IP can pass through the filter //Allowed returns if a given IP can pass through the filter
func (f *ipFilter) Allowed(ipstr string) bool { func (f *ipFilter) Allowed(ipstr string) bool {
return f.NetAllowed(net.ParseIP(ipstr)) return f.NetAllowed(net.ParseIP(ipstr))
} }
// NetAllowed returns if a given net.IP can pass through the filter //NetAllowed returns if a given net.IP can pass through the filter
func (f *ipFilter) NetAllowed(ip net.IP) bool { func (f *ipFilter) NetAllowed(ip net.IP) bool {
//invalid ip //invalid ip
if ip == nil { if ip == nil {
@@ -170,24 +172,24 @@ func (f *ipFilter) NetAllowed(ip net.IP) bool {
return f.defaultAllowed return f.defaultAllowed
} }
// Blocked returns if a given IP can NOT pass through the filter //Blocked returns if a given IP can NOT pass through the filter
func (f *ipFilter) Blocked(ip string) bool { func (f *ipFilter) Blocked(ip string) bool {
return !f.Allowed(ip) return !f.Allowed(ip)
} }
// NetBlocked returns if a given net.IP can NOT pass through the filter //NetBlocked returns if a given net.IP can NOT pass through the filter
func (f *ipFilter) NetBlocked(ip net.IP) bool { func (f *ipFilter) NetBlocked(ip net.IP) bool {
return !f.NetAllowed(ip) return !f.NetAllowed(ip)
} }
// Wrap the provided handler with simple IP blocking middleware //Wrap the provided handler with simple IP blocking middleware
// using this IP filter and its configuration //using this IP filter and its configuration
func (f *ipFilter) Wrap(next http.Handler) http.Handler { func (f *ipFilter) Wrap(next http.Handler) http.Handler {
return &ipFilterMiddleware{ipFilter: f, next: next} return &ipFilterMiddleware{ipFilter: f, next: next}
} }
// WrapIPFilter is equivalent to newIPFilter(opts) then Wrap(next) //WrapIPFilter is equivalent to newIPFilter(opts) then Wrap(next)
func WrapIPFilter(next http.Handler, opts *IPFilterOptions) http.Handler { func WrapIPFilter(next http.Handler, opts IPFilterOptions) http.Handler {
return newIPFilter(opts).Wrap(next) return newIPFilter(opts).Wrap(next)
} }

View File

@@ -49,7 +49,6 @@ import (
"github.com/VojtechVitek/ratelimit/memory" "github.com/VojtechVitek/ratelimit/memory"
gorillaHandlers "github.com/gorilla/handlers" gorillaHandlers "github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/tg123/go-htpasswd"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
web "github.com/dutchcoders/transfer.sh-web" web "github.com/dutchcoders/transfer.sh-web"
@@ -295,26 +294,8 @@ func TLSConfig(cert, pk string) OptionFn {
// HTTPAuthCredentials sets basic http auth credentials // HTTPAuthCredentials sets basic http auth credentials
func HTTPAuthCredentials(user string, pass string) OptionFn { func HTTPAuthCredentials(user string, pass string) OptionFn {
return func(srvr *Server) { return func(srvr *Server) {
srvr.authUser = user srvr.AuthUser = user
srvr.authPass = pass srvr.AuthPass = pass
}
}
// HTTPAuthHtpasswd sets basic http auth htpasswd file
func HTTPAuthHtpasswd(htpasswdPath string) OptionFn {
return func(srvr *Server) {
srvr.authHtpasswd = htpasswdPath
}
}
// HTTPAUTHFilterOptions sets basic http auth ips whitelist
func HTTPAUTHFilterOptions(options IPFilterOptions) OptionFn {
for i, allowedIP := range options.AllowedIPs {
options.AllowedIPs[i] = strings.TrimSpace(allowedIP)
}
return func(srvr *Server) {
srvr.authIPFilterOptions = &options
} }
} }
@@ -335,13 +316,8 @@ func FilterOptions(options IPFilterOptions) OptionFn {
// Server is the main application // Server is the main application
type Server struct { type Server struct {
authUser string AuthUser string
authPass string AuthPass string
authHtpasswd string
authIPFilterOptions *IPFilterOptions
htpasswdFile *htpasswd.File
authIPFilter *ipFilter
logger *log.Logger logger *log.Logger
@@ -490,6 +466,8 @@ func (s *Server) Run() {
r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", s.headHandler).Methods("HEAD") r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", s.headHandler).Methods("HEAD")
r.HandleFunc("/{token}/{filename}", s.previewHandler).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) (match bool) { r.HandleFunc("/{token}/{filename}", s.previewHandler).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) (match bool) {
match = false
// The file will show a preview page when opening the link in browser directly or // The file will show a preview page when opening the link in browser directly or
// from external link. If the referer url path and current path are the same it will be // from external link. If the referer url path and current path are the same it will be
// downloaded. // downloaded.

View File

@@ -4,9 +4,10 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"regexp"
"strconv" "strconv"
"time" "time"
"regexp"
) )
type Range struct { type Range struct {

View File

@@ -35,7 +35,9 @@ const gDriveTokenJSONFile = "token.json"
const gDriveDirectoryMimeType = "application/vnd.google-apps.folder" const gDriveDirectoryMimeType = "application/vnd.google-apps.folder"
// NewGDriveStorage is the factory for GDrive // NewGDriveStorage is the factory for GDrive
func NewGDriveStorage(ctx context.Context, clientJSONFilepath string, localConfigPath string, basedir string, chunkSize int, logger *log.Logger) (*GDrive, error) { func NewGDriveStorage(clientJSONFilepath string, localConfigPath string, basedir string, chunkSize int, logger *log.Logger) (*GDrive, error) {
ctx := context.TODO()
b, err := ioutil.ReadFile(clientJSONFilepath) b, err := ioutil.ReadFile(clientJSONFilepath)
if err != nil { if err != nil {

View File

@@ -2,48 +2,38 @@ package storage
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
"time" "time"
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/aws-sdk-go/service/s3/s3manager"
) )
// S3Storage is a storage backed by AWS S3 // S3Storage is a storage backed by AWS S3
type S3Storage struct { type S3Storage struct {
Storage Storage
bucket string bucket string
s3 *s3.Client session *session.Session
s3 *s3.S3
logger *log.Logger logger *log.Logger
purgeDays time.Duration purgeDays time.Duration
noMultipart bool noMultipart bool
} }
// NewS3Storage is the factory for S3Storage // NewS3Storage is the factory for S3Storage
func NewS3Storage(ctx context.Context, accessKey, secretKey, bucketName string, purgeDays int, region, endpoint string, disableMultipart bool, forcePathStyle bool, logger *log.Logger) (*S3Storage, error) { func NewS3Storage(accessKey, secretKey, bucketName string, purgeDays int, region, endpoint string, disableMultipart bool, forcePathStyle bool, logger *log.Logger) (*S3Storage, error) {
cfg, err := getAwsConfig(ctx, accessKey, secretKey) sess := getAwsSession(accessKey, secretKey, region, endpoint, forcePathStyle)
if err != nil {
return nil, err
}
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.Region = region
o.UsePathStyle = forcePathStyle
if len(endpoint) > 0 {
o.EndpointResolver = s3.EndpointResolverFromURL(endpoint)
}
})
return &S3Storage{ return &S3Storage{
bucket: bucketName, bucket: bucketName,
s3: client, s3: s3.New(sess),
session: sess,
logger: logger, logger: logger,
noMultipart: disableMultipart, noMultipart: disableMultipart,
purgeDays: time.Duration(purgeDays*24) * time.Hour, purgeDays: time.Duration(purgeDays*24) * time.Hour,
@@ -65,12 +55,14 @@ func (s *S3Storage) Head(ctx context.Context, token string, filename string) (co
} }
// content type , content length // content type , content length
response, err := s.s3.HeadObject(ctx, headRequest) response, err := s.s3.HeadObjectWithContext(ctx, headRequest)
if err != nil { if err != nil {
return return
} }
contentLength = uint64(response.ContentLength) if response.ContentLength != nil {
contentLength = uint64(*response.ContentLength)
}
return return
} }
@@ -87,8 +79,14 @@ func (s *S3Storage) IsNotExist(err error) bool {
return false return false
} }
var nkerr *types.NoSuchKey if aerr, ok := err.(awserr.Error); ok {
return errors.As(err, &nkerr) switch aerr.Code() {
case s3.ErrCodeNoSuchKey:
return true
}
}
return false
} }
// Get retrieves a file from storage // Get retrieves a file from storage
@@ -104,12 +102,14 @@ func (s *S3Storage) Get(ctx context.Context, token string, filename string, rng
getRequest.Range = aws.String(rng.Range()) getRequest.Range = aws.String(rng.Range())
} }
response, err := s.s3.GetObject(ctx, getRequest) response, err := s.s3.GetObjectWithContext(ctx, getRequest)
if err != nil { if err != nil {
return return
} }
contentLength = uint64(response.ContentLength) if response.ContentLength != nil {
contentLength = uint64(*response.ContentLength)
}
if rng != nil && response.ContentRange != nil { if rng != nil && response.ContentRange != nil {
rng.SetContentRange(*response.ContentRange) rng.SetContentRange(*response.ContentRange)
} }
@@ -126,7 +126,7 @@ func (s *S3Storage) Delete(ctx context.Context, token string, filename string) (
Key: aws.String(metadata), Key: aws.String(metadata),
} }
_, err = s.s3.DeleteObject(ctx, deleteRequest) _, err = s.s3.DeleteObjectWithContext(ctx, deleteRequest)
if err != nil { if err != nil {
return return
} }
@@ -137,7 +137,7 @@ func (s *S3Storage) Delete(ctx context.Context, token string, filename string) (
Key: aws.String(key), Key: aws.String(key),
} }
_, err = s.s3.DeleteObject(ctx, deleteRequest) _, err = s.s3.DeleteObjectWithContext(ctx, deleteRequest)
return return
} }
@@ -155,7 +155,7 @@ func (s *S3Storage) Put(ctx context.Context, token string, filename string, read
} }
// Create an uploader with the session and custom options // Create an uploader with the session and custom options
uploader := manager.NewUploader(s.s3, func(u *manager.Uploader) { uploader := s3manager.NewUploader(s.session, func(u *s3manager.Uploader) {
u.Concurrency = concurrency // default is 5 u.Concurrency = concurrency // default is 5
u.LeavePartsOnError = false u.LeavePartsOnError = false
}) })
@@ -165,7 +165,7 @@ func (s *S3Storage) Put(ctx context.Context, token string, filename string, read
expire = aws.Time(time.Now().Add(s.purgeDays)) expire = aws.Time(time.Now().Add(s.purgeDays))
} }
_, err = uploader.Upload(ctx, &s3.PutObjectInput{ _, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{
Bucket: aws.String(s.bucket), Bucket: aws.String(s.bucket),
Key: aws.String(key), Key: aws.String(key),
Body: reader, Body: reader,
@@ -178,14 +178,11 @@ func (s *S3Storage) Put(ctx context.Context, token string, filename string, read
func (s *S3Storage) IsRangeSupported() bool { return true } func (s *S3Storage) IsRangeSupported() bool { return true }
func getAwsConfig(ctx context.Context, accessKey, secretKey string) (aws.Config, error) { func getAwsSession(accessKey, secretKey, region, endpoint string, forcePathStyle bool) *session.Session {
return config.LoadDefaultConfig(ctx, return session.Must(session.NewSession(&aws.Config{
config.WithCredentialsProvider(credentials.StaticCredentialsProvider{ Region: aws.String(region),
Value: aws.Credentials{ Endpoint: aws.String(endpoint),
AccessKeyID: accessKey, Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
SecretAccessKey: secretKey, S3ForcePathStyle: aws.Bool(forcePathStyle),
SessionToken: "", }))
},
}),
)
} }

View File

@@ -22,11 +22,13 @@ type StorjStorage struct {
} }
// NewStorjStorage is the factory for StorjStorage // NewStorjStorage is the factory for StorjStorage
func NewStorjStorage(ctx context.Context, access, bucket string, purgeDays int, logger *log.Logger) (*StorjStorage, error) { func NewStorjStorage(access, bucket string, purgeDays int, logger *log.Logger) (*StorjStorage, error) {
var instance StorjStorage var instance StorjStorage
var err error var err error
ctx = fpath.WithTempData(ctx, "", true) pCtx := context.TODO()
ctx := fpath.WithTempData(pCtx, "", true)
uplConf := &uplink.Config{ uplConf := &uplink.Config{
UserAgent: "transfer-sh", UserAgent: "transfer-sh",
@@ -81,18 +83,15 @@ func (s *StorjStorage) Get(ctx context.Context, token string, filename string, r
s.logger.Printf("Getting file %s from Storj Bucket", filename) s.logger.Printf("Getting file %s from Storj Bucket", filename)
var options *uplink.DownloadOptions options := uplink.DownloadOptions{}
if rng != nil { if rng != nil {
options = new(uplink.DownloadOptions)
options.Offset = int64(rng.Start) options.Offset = int64(rng.Start)
if rng.Limit > 0 { if rng.Limit > 0 {
options.Length = int64(rng.Limit) options.Length = int64(rng.Limit)
} else {
options.Length = -1
} }
} }
download, err := s.project.DownloadObject(fpath.WithTempData(ctx, "", true), s.bucket.Name, key, options) download, err := s.project.DownloadObject(fpath.WithTempData(ctx, "", true), s.bucket.Name, key, &options)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }

View File

@@ -32,7 +32,6 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/golang/gddo/httputil/header" "github.com/golang/gddo/httputil/header"
) )
@@ -234,11 +233,3 @@ func formatSize(size int64) string {
getSuffix := suffixes[int(math.Floor(base))] getSuffix := suffixes[int(math.Floor(base))]
return fmt.Sprintf("%s %s", strconv.FormatFloat(newVal, 'f', -1, 64), getSuffix) return fmt.Sprintf("%s %s", strconv.FormatFloat(newVal, 'f', -1, 64), getSuffix)
} }
func formatDurationDays(durationDays time.Duration) string {
days := int(durationDays.Hours() / 24)
if days == 1 {
return fmt.Sprintf("%d day", days)
}
return fmt.Sprintf("%d days", days)
}