Compare commits

..

19 Commits

Author SHA1 Message Date
Andrea Spacca
c5664b433d better logic 2023-06-06 19:35:45 +09:00
Andrea Spacca
fc6f897eaa fix basic auth 2023-06-06 19:30:32 +09:00
Michael Manganiello
fc844ac341 Upgrade aws-sdk-go to v2 (#559)
* Upgrade aws-sdk-go to v2

`aws-sdk-go-v2` is the newer SDK version, replacing the one being used
at the moment by the project.

This change maintains full compatibility with existing flags and
configurations, and only replaces the underlying library.

* Simplify and isolate AWS config logic
2023-05-24 18:37:06 +09:00
Michael Manganiello
a653181ea8 Improve Docker layer caching for Go dependencies (#560)
Running `go mod download` before copying the entire project to the
Docker image avoids dependencies from being re-downloaded every time any
file is modified, and `docker build` runs again.

This follows the steps detailed in the official Docker guide for Go
images:
https://docs.docker.com/language/golang/build-images/#create-a-dockerfile-for-the-application

Also, `GO111MODULE` doesn't make any difference for the supported
Go versions, so it can be removed from the Dockerfile.
2023-05-23 11:54:02 +09:00
Natalí Paura
bafbf0c1a0 Improve purgeTime display in web page (#558)
- changing the line `purgeTime = s.purgeDays.String()` to use a function that formats the days like this: "N days" or "1 day"
- adding the function `formatDurationDays` in utils.go file

Fixes #557

Co-authored-by: Andrea Spacca <andrea.spacca@gmail.com>
2023-05-19 19:01:54 +09:00
Michael Manganiello
df0d04f0da Upgrade github.com/urfave/cli to v2 (#551)
`urfave/cli` v2 was released in 2019, and it doesn't introduce relevant
user-facing breaking changes.

Migration guide from v1 can be found at: https://cli.urfave.org/migrate-v1-to-v2/

The only user-facing change is within the "Flags before args" section,
but I haven't found that to affect `transfer.sh` as arguments are not
used.

Co-authored-by: Stefan Benten <mail@stefan-benten.de>
Co-authored-by: Andrea Spacca <andrea.spacca@gmail.com>
2023-05-19 14:51:40 +09:00
Junoh Moon
2a11ca13a3 Add mime.types to docker container to select charset properly (#547)
* Add charset to content type in getHandler

Add charset to content type in the getHandler function to fix CJK-letter related issues.
If the content type is empty after trimming, set it to "text/plain; charset=utf-8".

* Add mailcap and mime.types to transfer.sh container

This commit includes /etc/mime.types file to the container, which is necessary to properly select the charset using MIME typing during file upload.

For more information, read https://github.com/dutchcoders/transfer.sh/pull/545#issuecomment-1528712181

---------

Co-authored-by: Andrea Spacca <andrea.spacca@gmail.com>
2023-05-19 13:09:37 +09:00
dependabot[bot]
1e6346c3d1 Bump github.com/cloudflare/circl from 1.1.0 to 1.3.3 (#552)
Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.1.0 to 1.3.3.
- [Release notes](https://github.com/cloudflare/circl/releases)
- [Commits](https://github.com/cloudflare/circl/compare/v1.1.0...v1.3.3)

---
updated-dependencies:
- dependency-name: github.com/cloudflare/circl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-19 11:57:12 +09:00
Michael Manganiello
eecf529138 Dockerfile: Use Go 1.20 by default (#550)
Use Go 1.20 by default in Dockerfile

Updating the default Go version used in the Dockerfile, to the latest
one supported by the project at the moment.

Also, Go `1.17` support was removed in
9f1fe62e05
2023-05-11 18:23:58 +02:00
Junoh Moon
e837849d44 Add charset to content type in getHandler (#545)
Add charset to content type in the getHandler function to fix CJK-letter related issues.
If the content type is empty after trimming, set it to "text/plain; charset=utf-8".
2023-04-29 20:07:52 +09:00
Andrea Spacca
a5dacb36a9 call WriteHeader after last change to header map (#542)
* call WriteHeader after last change to header map

* fix reader/decryptionReader
2023-04-05 23:30:58 +09:00
Kot
3dcbfe2e4d Add Vary headers in responses (#536)
* Add `Vary` headers in responses

* Add `Referer` to file handler `Vary`

* Fix Vary value for routes
2023-03-16 10:25:46 +09:00
Andrea Spacca
1fb67f49ff add IP_FILTERLIST_BYPASS_HTTP_AUTH (#538)
* add IP_FILTERLIST_BYPASS_HTTP_AUTH

* refactor to separated ip whitelist
2023-03-12 13:34:41 +09:00
Andrea Spacca
54cacb5487 add http-auth-htpasswd (#537)
* add http-auth-htpasswd

* go mod tidy
2023-03-12 11:52:45 +09:00
Andrea Spacca
3b777e1c19 gpg encryption support (#162)
* gpg encryption support

* gpg encryption support

* refinement

* refactor using protonmail opengpg

* warning

* remove exception in linting

* reduce I/O on decrypt

* fix typo

* streaming in encrypt

* Update README.md

do not reference server public hosting version in encrypt/decrypt headers example

* linting

* linting fix

* increase default random-token-length to 10
2023-03-11 10:08:55 +09:00
Andrea Spacca
28fc9c73f5 Lint accept range (#535)
* Support Range header for GET

* Adds 'Accept-Ranges: bytes' header to handlers

* proper param name in error

* bump gdrive dependecies

* refactor CloseCheck to avoid panic on nil, remove range/audio/video special handling on get handler

* refactor CloseCheck to avoid panic on nil, handle range with no limit

* refactor CloseCheck to avoid panic on nil, handle range in gdrive storage

* refactor CloseCheck to avoid panic on nil

* handle range with no limit

* lint

* bump deps

* go mod tidy

* gofmt

* cr fixes, replace deprecated ioutil.NopCloser

* linting

* do not write to file by default

* fix storj

---------

Co-authored-by: Vladislav Grubov <vladgrubov@gmail.com>
2023-03-11 01:41:43 +09:00
dependabot[bot]
c452bd4719 Bump golang.org/x/net from 0.0.0-20220513224357-95641704303c to 0.7.0 (#534)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20220513224357-95641704303c to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-01 16:39:47 +01:00
dependabot[bot]
a4475513bc Bump golang.org/x/crypto from 0.0.0-20220131195533-30dcbda58838 to 0.1.0 (#533)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.0.0-20220131195533-30dcbda58838 to 0.1.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-01 16:07:10 +01:00
Andrea Spacca
9f1fe62e05 min go version 1.18, include tip for test (#532)
* min go version 1.18, include tip for test

* lint

* lint

* lint

* lint

* lint

* lint

* lint

* lint

* lint

* lint

* lint

* lint

* lint
2023-03-01 21:51:24 +09:00
16 changed files with 941 additions and 1666 deletions

View File

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

View File

@@ -13,16 +13,27 @@ jobs:
fail-fast: false
matrix:
go_version:
- 1.15.x
- 1.16.x
- 1.17.x
- 1.18.X
- '1.18'
- '1.19'
- '1.20'
- tip
name: Test with ${{ matrix.go_version }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v1
- name: Install Go ${{ matrix.go_version }}
if: ${{ matrix.go_version != 'tip' }}
uses: actions/setup-go@master
with:
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
run: |
go version
@@ -33,9 +44,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v1
- uses: actions/setup-go@master
with:
go-version: 1.18
go-version: '1.20'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:

View File

@@ -1,15 +1,17 @@
# Default to Go 1.17
ARG GO_VERSION=1.17
# Default to Go 1.20
ARG GO_VERSION=1.20
FROM golang:${GO_VERSION}-alpine as build
# Necessary to run 'go get' and to compile the linked binary
RUN apk add git musl-dev
ADD . /go/src/github.com/dutchcoders/transfer.sh
RUN apk add git musl-dev mailcap
WORKDIR /go/src/github.com/dutchcoders/transfer.sh
ENV GO111MODULE=on
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 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
@@ -29,6 +31,7 @@ FROM scratch AS final
LABEL maintainer="Andrea Spacca <andrea.spacca@gmail.com>"
ARG RUNAS
COPY --from=build /etc/mime.types /etc/mime.types
COPY --from=build /tmp/empty /tmp
COPY --from=build /tmp/useradd/* /etc/
COPY --from=build --chown=${RUNAS} /go/bin/transfersh /go/bin/transfersh

112
README.md
View File

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

View File

@@ -44,6 +44,8 @@
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-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-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"; };

113
go.mod
View File

@@ -1,40 +1,97 @@
module github.com/dutchcoders/transfer.sh
go 1.15
go 1.18
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 (
cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14
github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2
github.com/aws/aws-sdk-go v1.37.14
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
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-20220824020025-7240e75c3bb8
github.com/elazarl/go-bindata-assetfs v1.0.1
github.com/fatih/color v1.10.0
github.com/garyburd/redigo v1.6.2 // indirect
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
github.com/aws/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/googleapis/enterprise-certificate-proxy v0.2.2 // indirect
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
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/microcosm-cc/bluemonday v1.0.16
github.com/jmespath/go-jmespath v0.4.0 // indirect
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/russross/blackfriday/v2 v2.1.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
github.com/urfave/cli v1.22.5
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
golang.org/x/net v0.6.0 // indirect
golang.org/x/oauth2 v0.5.0
google.golang.org/api v0.109.0
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/spacemonkeygo/monkit/v3 v3.0.19 // indirect
github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
github.com/zeebo/errs v1.3.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // 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/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15
storj.io/common v0.0.0-20220405183405-ffdc3ab808c6
storj.io/uplink v1.8.2
gopkg.in/yaml.v2 v2.4.0 // indirect
storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a // indirect
storj.io/picobuf v0.0.1 // indirect
)

1332
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,6 @@ import (
"html"
htmlTemplate "html/template"
"io"
"io/ioutil"
"mime"
"net"
"net/http"
@@ -53,13 +52,19 @@ import (
textTemplate "text/template"
"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/tg123/go-htpasswd"
"github.com/tomasen/realip"
web "github.com/dutchcoders/transfer.sh-web"
"github.com/gorilla/mux"
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2"
"github.com/skip2/go-qrcode"
blackfriday "github.com/russross/blackfriday/v2"
qrcode "github.com/skip2/go-qrcode"
"golang.org/x/net/idna"
)
@@ -91,6 +96,128 @@ func initHTMLTemplates() *htmlTemplate.Template {
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) {
_, _ = w.Write([]byte("Approaching Neutral Zone, all systems normal and functioning."))
}
@@ -118,6 +245,8 @@ 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 */
func (s *Server) previewHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password")
vars := mux.Vars(r)
token := vars["token"]
@@ -245,7 +374,7 @@ func (s *Server) viewHandler(w http.ResponseWriter, r *http.Request) {
purgeTime := ""
if s.purgeDays > 0 {
purgeTime = s.purgeDays.String()
purgeTime = formatDurationDays(s.purgeDays)
}
data := struct {
@@ -270,6 +399,7 @@ func (s *Server) viewHandler(w http.ResponseWriter, r *http.Request) {
token(s.randomTokenLength),
}
w.Header().Set("Vary", "Accept")
if acceptsHTML(r.Header) {
if err := htmlTemplates.ExecuteTemplate(w, "index.html", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -318,7 +448,7 @@ func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {
return
}
file, err := ioutil.TempFile(s.tempPath, "transfer-")
file, err := os.CreateTemp(s.tempPath, "transfer-")
defer s.cleanTmpFile(file)
if err != nil {
@@ -363,7 +493,7 @@ func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {
}
}
metadata := metadataForRequest(contentType, s.randomTokenLength, r)
metadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r)
buffer := &bytes.Buffer{}
if err := json.NewEncoder(buffer).Encode(metadata); err != nil {
@@ -380,7 +510,13 @@ func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType)
if err = s.storage.Put(r.Context(), token, filename, file, contentType, uint64(contentLength)); err != nil {
reader, err := attachEncryptionReader(file, r.Header.Get("X-Encrypt-Password"))
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())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -418,8 +554,8 @@ func (s *Server) cleanTmpFile(f *os.File) {
type metadata struct {
// ContentType is the original uploading content type
ContentType string
// Secret as knowledge to delete file
// Secret string
// ContentLength is is the original uploading content length
ContentLength int64
// Downloads is the actual number of downloads
Downloads int
// MaxDownloads contains the maximum numbers of downloads
@@ -428,11 +564,16 @@ type metadata struct {
MaxDate time.Time
// DeletionToken contains the token to match against for deletion
DeletionToken string
// Encrypted contains if the file was encrypted
Encrypted bool
// DecryptedContentType is the original uploading content type
DecryptedContentType string
}
func metadataForRequest(contentType string, randomTokenLength int, r *http.Request) metadata {
func metadataForRequest(contentType string, contentLength int64, randomTokenLength int, r *http.Request) metadata {
metadata := metadata{
ContentType: strings.ToLower(contentType),
ContentLength: contentLength,
MaxDate: time.Time{},
Downloads: 0,
MaxDownloads: -1,
@@ -451,6 +592,14 @@ func metadataForRequest(contentType string, randomTokenLength int, r *http.Reque
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
}
@@ -463,34 +612,53 @@ func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
defer storage.CloseCheck(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
}
reader := r.Body
// queue file to disk, because s3 needs content length
// 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)
if contentLength < 1 || s.performClamavPrescan {
file, err := os.CreateTemp(s.tempPath, "transfer-")
defer s.cleanTmpFile(file)
if err != nil {
s.logger.Printf("%s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
return
}
// queue file to disk, because s3 needs content length
// 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)
_, err = file.Seek(0, io.SeekStart)
if err != nil {
s.logger.Printf("%s", err.Error())
http.Error(w, "Cannot reset cache file", http.StatusInternalServerError)
return
}
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)
return
}
if contentLength < 1 {
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 {
@@ -505,26 +673,11 @@ func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
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"]))
token := token(s.randomTokenLength)
metadata := metadataForRequest(contentType, s.randomTokenLength, r)
metadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r)
buffer := &bytes.Buffer{}
if err := json.NewEncoder(buffer).Encode(metadata); err != nil {
@@ -543,7 +696,13 @@ func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType)
if err = s.storage.Put(r.Context(), token, filename, file, contentType, uint64(contentLength)); err != nil {
reader, err := attachEncryptionReader(reader, r.Header.Get("X-Encrypt-Password"))
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())
http.Error(w, "Could not save file", http.StatusInternalServerError)
return
@@ -1001,6 +1160,7 @@ func (s *Server) headHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Connection", "close")
w.Header().Set("X-Remaining-Downloads", remainingDownloads)
w.Header().Set("X-Remaining-Days", remainingDays)
w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password")
if s.storage.IsRangeSupported() {
w.Header().Set("Accept-Ranges", "bytes")
@@ -1031,8 +1191,6 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
reader, contentLength, err := s.storage.Get(r.Context(), token, filename, rng)
defer storage.CloseCheck(reader)
rdr := io.Reader(reader)
if s.storage.IsNotExist(err) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
@@ -1047,23 +1205,20 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Range", cr)
if rng.Limit > 0 {
rdr = io.LimitReader(reader, int64(rng.Limit))
reader = io.NopCloser(io.LimitReader(reader, int64(rng.Limit)))
}
}
}
var disposition string
if action == "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,
So add text/plain in this case to fix XSS related issues/
*/
if strings.TrimSpace(contentType) == "" {
contentType = "text/plain"
contentType = "text/plain; charset=utf-8"
}
} else {
disposition = "attachment"
@@ -1071,23 +1226,37 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
remainingDownloads, remainingDays := metadata.remainingLimitHeaderValues()
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("Content-Disposition", fmt.Sprintf(`%s; filename="%s"`, disposition, filename))
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Remaining-Downloads", remainingDownloads)
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() != "" {
w.WriteHeader(http.StatusPartialContent)
}
if disposition == "inline" && canContainsXSS(contentType) {
reader = ioutil.NopCloser(bluemonday.UGCPolicy().SanitizeReader(reader))
reader = io.NopCloser(bluemonday.UGCPolicy().SanitizeReader(reader))
}
if _, err = io.Copy(w, rdr); err != nil {
if _, err = io.Copy(w, reader); err != nil {
s.logger.Printf("%s", err.Error())
http.Error(w, "Error occurred copying to output stream", http.StatusInternalServerError)
return
@@ -1150,27 +1319,55 @@ func ipFilterHandler(h http.Handler, ipFilterOptions *IPFilterOptions) http.Hand
if ipFilterOptions == nil {
h.ServeHTTP(w, r)
} else {
WrapIPFilter(h, *ipFilterOptions).ServeHTTP(w, r)
WrapIPFilter(h, ipFilterOptions).ServeHTTP(w, r)
}
}
}
func (s *Server) basicAuthHandler(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if s.AuthUser == "" || s.AuthPass == "" {
if s.authUser == "" && s.authPass == "" && s.authHtpasswd == "" {
h.ServeHTTP(w, r)
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\"")
var authorized bool
if s.authIPFilter != nil {
remoteIP := realip.FromRequest(r)
authorized = s.authIPFilter.Allowed(remoteIP)
}
username, password, authOK := r.BasicAuth()
if !authOK {
if !authOK && !authorized {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
if username != s.AuthUser || password != s.AuthPass {
if !authorized && 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)
return
}

View File

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

View File

@@ -49,6 +49,7 @@ import (
"github.com/VojtechVitek/ratelimit/memory"
gorillaHandlers "github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/tg123/go-htpasswd"
"golang.org/x/crypto/acme/autocert"
web "github.com/dutchcoders/transfer.sh-web"
@@ -294,8 +295,26 @@ func TLSConfig(cert, pk string) OptionFn {
// HTTPAuthCredentials sets basic http auth credentials
func HTTPAuthCredentials(user string, pass string) OptionFn {
return func(srvr *Server) {
srvr.AuthUser = user
srvr.AuthPass = pass
srvr.authUser = user
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
}
}
@@ -316,8 +335,13 @@ func FilterOptions(options IPFilterOptions) OptionFn {
// Server is the main application
type Server struct {
AuthUser string
AuthPass string
authUser string
authPass string
authHtpasswd string
authIPFilterOptions *IPFilterOptions
htpasswdFile *htpasswd.File
authIPFilter *ipFilter
logger *log.Logger
@@ -466,8 +490,6 @@ func (s *Server) Run() {
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) {
match = false
// 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
// downloaded.

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ import (
"net/http"
"strconv"
"strings"
"time"
"github.com/golang/gddo/httputil/header"
)
@@ -233,3 +234,11 @@ func formatSize(size int64) string {
getSuffix := suffixes[int(math.Floor(base))]
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)
}