The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .bowerrc
├── .dockerignore
├── .github
    ├── build
    │   └── friendly-filenames.json
    └── workflows
    │   ├── build-docker-images.yml
    │   ├── release.yml
    │   └── test.yml
├── .gitignore
├── .golangci.yml
├── .jshintrc
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── Vagrantfile
├── cmd
    └── cmd.go
├── examples.md
├── extras
    ├── clamd
    └── transfersh
├── flake.lock
├── flake.nix
├── go.mod
├── go.sum
├── main.go
├── manifest.json
└── server
    ├── clamav.go
    ├── handlers.go
    ├── handlers_test.go
    ├── ip_filter.go
    ├── server.go
    ├── storage
        ├── common.go
        ├── gdrive.go
        ├── local.go
        ├── s3.go
        └── storj.go
    ├── token.go
    ├── token_test.go
    ├── utils.go
    └── virustotal.go


/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 |     "directory": "transfersh-web/bower_components"
3 | }
4 | 


--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
 1 | build
 2 | pkg
 3 | dist
 4 | src
 5 | bin
 6 | *.pyc
 7 | *.egg-info
 8 | .vagrant
 9 | .git
10 | .tmp
11 | bower_components
12 | node_modules
13 | extras
14 | build
15 | transfersh-server/run.sh
16 | .elasticbeanstalk
17 | Dockerfile
18 | 


--------------------------------------------------------------------------------
/.github/build/friendly-filenames.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "android-arm64": { "friendlyName": "android-arm64-v8a" },
 3 |     "darwin-amd64": { "friendlyName": "darwin-amd64" },
 4 |     "darwin-arm64": { "friendlyName": "darwin-arm64" },
 5 |     "dragonfly-amd64": { "friendlyName": "dragonfly-amd64" },
 6 |     "freebsd-386": { "friendlyName": "freebsd-386" },
 7 |     "freebsd-amd64": { "friendlyName": "freebsd-amd64" },
 8 |     "freebsd-arm64": { "friendlyName": "freebsd-arm64-v8a" },
 9 |     "freebsd-arm7": { "friendlyName": "freebsd-arm32-v7a" },
10 |     "linux-386": { "friendlyName": "linux-386" },
11 |     "linux-amd64": { "friendlyName": "linux-amd64" },
12 |     "linux-arm5": { "friendlyName": "linux-arm32-v5" },
13 |     "linux-arm64": { "friendlyName": "linux-arm64-v8a" },
14 |     "linux-arm6": { "friendlyName": "linux-arm32-v6" },
15 |     "linux-arm7": { "friendlyName": "linux-armv7" },
16 |     "linux-mips64le": { "friendlyName": "linux-mips64le" },
17 |     "linux-mips64": { "friendlyName": "linux-mips64" },
18 |     "linux-mipslesoftfloat": { "friendlyName": "linux-mips32le-softfloat" },
19 |     "linux-mipsle": { "friendlyName": "linux-mips32le" },
20 |     "linux-mipssoftfloat": { "friendlyName": "linux-mips32-softfloat" },
21 |     "linux-mips": { "friendlyName": "linux-mips32" },
22 |     "linux-ppc64le": { "friendlyName": "linux-ppc64le" },
23 |     "linux-ppc64": { "friendlyName": "linux-ppc64" },
24 |     "linux-riscv64": { "friendlyName": "linux-riscv64" },
25 |     "linux-s390x": { "friendlyName": "linux-s390x" },
26 |     "openbsd-386": { "friendlyName": "openbsd-386" },
27 |     "openbsd-amd64": { "friendlyName": "openbsd-amd64" },
28 |     "openbsd-arm64": { "friendlyName": "openbsd-arm64-v8a" },
29 |     "openbsd-arm7": { "friendlyName": "openbsd-arm32-v7a" },
30 |     "windows-386": { "friendlyName": "windows-386" },
31 |     "windows-amd64": { "friendlyName": "windows-amd64" },
32 |     "windows-arm7": { "friendlyName": "windows-arm32-v7a" }
33 |   }
34 | 


--------------------------------------------------------------------------------
/.github/workflows/build-docker-images.yml:
--------------------------------------------------------------------------------
  1 | name: deploy multi-architecture Docker images for transfer.sh with buildx
  2 | 
  3 | on:
  4 |   schedule:
  5 |     - cron: '0 0 * * *' # everyday at midnight UTC
  6 |   pull_request:
  7 |     branches: main
  8 |   push:
  9 |     branches: main
 10 |     tags:
 11 |       - v*
 12 | 
 13 | jobs:
 14 |   buildx:
 15 |     runs-on: ubuntu-latest
 16 |     steps:
 17 |       -
 18 |         name: Checkout
 19 |         uses: actions/checkout@v2
 20 |       -
 21 |         name: Prepare
 22 |         id: prepare
 23 |         run: |
 24 |           DOCKER_IMAGE=dutchcoders/transfer.sh
 25 |           DOCKER_PLATFORMS=linux/amd64,linux/arm/v7,linux/arm64,linux/386
 26 |           VERSION=edge
 27 | 
 28 |           if [[ $GITHUB_REF == refs/tags/* ]]; then
 29 |             VERSION=v${GITHUB_REF#refs/tags/v}
 30 |           fi
 31 | 
 32 |           if [ "${{ github.event_name }}" = "schedule" ]; then
 33 |             VERSION=nightly
 34 |           fi
 35 | 
 36 |           TAGS="--tag ${DOCKER_IMAGE}:${VERSION}"
 37 |           TAGS_NOROOT="--tag ${DOCKER_IMAGE}:${VERSION}-noroot"
 38 | 
 39 |           if [ $VERSION = edge -o $VERSION = nightly ]; then
 40 |             TAGS="$TAGS --tag ${DOCKER_IMAGE}:latest"
 41 |             TAGS_NOROOT="$TAGS_NOROOT --tag ${DOCKER_IMAGE}:latest-noroot"
 42 |           fi
 43 | 
 44 |           echo ::set-output name=docker_image::${DOCKER_IMAGE}
 45 |           echo ::set-output name=version::${VERSION}
 46 |           echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
 47 |             --build-arg VERSION=${VERSION} \
 48 |             --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
 49 |             --build-arg VCS_REF=${GITHUB_SHA::8} \
 50 |             ${TAGS} .
 51 |           echo ::set-output name=buildx_args_noroot::--platform ${DOCKER_PLATFORMS} \
 52 |             --build-arg VERSION=${VERSION} \
 53 |             --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
 54 |             --build-arg VCS_REF=${GITHUB_SHA::8} \
 55 |             --build-arg RUNAS=noroot \
 56 |             ${TAGS_NOROOT} .
 57 |       -
 58 |         name: Set up QEMU
 59 |         uses: docker/setup-qemu-action@v1
 60 |         with:
 61 |           platforms: all
 62 |       -
 63 |         name: Set up Docker Buildx
 64 |         id: buildx
 65 |         uses: docker/setup-buildx-action@v1
 66 |         with:
 67 |           version: latest
 68 |       -
 69 |         name: Available platforms
 70 |         run: echo ${{ steps.buildx.outputs.platforms }}
 71 |       -
 72 |         name: Docker Buildx (build)
 73 |         run: |
 74 |           docker buildx build --no-cache --pull --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args }}
 75 |           docker buildx build --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args_noroot }}
 76 |       -
 77 |         name: Docker Login
 78 |         if: success() && github.event_name != 'pull_request'
 79 |         env:
 80 |           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
 81 |           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
 82 |         run: |
 83 |           echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
 84 |       -
 85 |         name: Docker Buildx (push)
 86 |         if: success() && github.event_name != 'pull_request'
 87 |         run: |
 88 |           docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }}
 89 |           docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args_noroot }}
 90 |       -
 91 |         name: Docker Check Manifest
 92 |         if: always() && github.event_name != 'pull_request'
 93 |         run: |
 94 |           docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}
 95 |           docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}-noroot
 96 |       -
 97 |         name: Clear
 98 |         if: always() && github.event_name != 'pull_request'
 99 |         run: |
100 |           rm -f ${HOME}/.docker/config.json
101 | 


--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
  1 | name: Build and Release
  2 | 
  3 | on:
  4 |   workflow_dispatch:
  5 |   release:
  6 |     types: [published]
  7 | jobs:
  8 |   build:
  9 |     strategy:
 10 |       matrix:
 11 |         # Include amd64 on all platforms.
 12 |         goos: [windows, freebsd, openbsd, linux, dragonfly, darwin]
 13 |         goarch: [amd64, 386]
 14 |         exclude:
 15 |           # Exclude i386 on darwin and dragonfly.
 16 |           - goarch: 386
 17 |             goos: dragonfly
 18 |           - goarch: 386
 19 |             goos: darwin
 20 |         include:
 21 |           # BEIGIN MacOS ARM64
 22 |           - goos: darwin
 23 |             goarch: arm64
 24 |           # END MacOS ARM64
 25 |           # BEGIN Linux ARM 5 6 7
 26 |           - goos: linux
 27 |             goarch: arm
 28 |             goarm: 7
 29 |           - goos: linux
 30 |             goarch: arm
 31 |             goarm: 6
 32 |           - goos: linux
 33 |             goarch: arm
 34 |             goarm: 5
 35 |           # END Linux ARM 5 6 7
 36 |           # BEGIN Android ARM 8
 37 |           - goos: android
 38 |             goarch: arm64
 39 |           # END Android ARM 8
 40 |           # Windows ARM 7
 41 |           - goos: windows
 42 |             goarch: arm
 43 |             goarm: 7
 44 |           # BEGIN Other architectures
 45 |           # BEGIN riscv64 & ARM64
 46 |           - goos: linux
 47 |             goarch: arm64
 48 |           - goos: linux
 49 |             goarch: riscv64
 50 |           # END riscv64 & ARM64
 51 |           # BEGIN MIPS
 52 |           - goos: linux
 53 |             goarch: mips64
 54 |           - goos: linux
 55 |             goarch: mips64le
 56 |           - goos: linux
 57 |             goarch: mipsle
 58 |           - goos: linux
 59 |             goarch: mips
 60 |           # END MIPS
 61 |           # BEGIN PPC
 62 |           - goos: linux
 63 |             goarch: ppc64
 64 |           - goos: linux
 65 |             goarch: ppc64le
 66 |           # END PPC
 67 |           # BEGIN FreeBSD ARM
 68 |           - goos: freebsd
 69 |             goarch: arm64
 70 |           - goos: freebsd
 71 |             goarch: arm
 72 |             goarm: 7
 73 |           # END FreeBSD ARM
 74 |           # BEGIN S390X
 75 |           - goos: linux
 76 |             goarch: s390x
 77 |           # END S390X
 78 |           # END Other architectures
 79 |           # BEGIN OPENBSD ARM
 80 |           - goos: openbsd
 81 |             goarch: arm64
 82 |           - goos: openbsd
 83 |             goarch: arm
 84 |             goarm: 7
 85 |           # END OPENBSD ARM
 86 |       fail-fast: false
 87 | 
 88 |     runs-on: ubuntu-latest
 89 |     env:
 90 |       GOOS: ${{ matrix.goos }}
 91 |       GOARCH: ${{ matrix.goarch }}
 92 |       GOARM: ${{ matrix.goarm }}
 93 |       CGO_ENABLED: 0
 94 |     steps:
 95 |       - name: Checkout codebase
 96 |         uses: actions/checkout@v2
 97 | 
 98 |       - name: Show workflow information 
 99 |         id: get_filename
100 |         run: |
101 |           export _NAME=$(jq ".[\"$GOOS-$GOARCH$GOARM$GOMIPS\"].friendlyName" -r < .github/build/friendly-filenames.json)
102 |           echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, GOMIPS: $GOMIPS, RELEASE_NAME: $_NAME"
103 |           echo "::set-output name=ASSET_NAME::$_NAME"
104 |           echo "::set-output name=GIT_TAG::${GITHUB_REF##*/}"
105 |           echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
106 | 
107 |       - name: Set up Go
108 |         uses: actions/setup-go@v2
109 |         with:
110 |           go-version: ^1.18
111 | 
112 |       - name: Get project dependencies
113 |         run: go mod download
114 |  
115 |       - name: Build Transfersh
116 |         run: |
117 |           mkdir -p build_assets
118 |           go build -tags netgo -ldflags "-X github.com/dutchcoders/transfer.sh/cmd.Version=${GITHUB_REF##*/} -a -s -w -extldflags '-static'" -o build_assets/transfersh-${GITHUB_REF##*/}-${ASSET_NAME}
119 |     
120 |       - name: Build Mips softfloat Transfersh
121 |         if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle'
122 |         run: |
123 |           GOMIPS=softfloat go build -tags netgo -ldflags "-X github.com/dutchcoders/transfer.sh/cmd.Version=${GITHUB_REF##*/} -a -s -w -extldflags '-static'" -o build_assets/transfersh-softfloat-${GITHUB_REF##*/}-${ASSET_NAME}
124 | 
125 |       - name: Rename Windows Transfersh
126 |         if: matrix.goos == 'windows'
127 |         run: |
128 |           cd ./build_assets || exit 1
129 |           mv transfersh-${GITHUB_REF##*/}-${ASSET_NAME} transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.exe
130 | 
131 |       - name: Prepare to release
132 |         run: |
133 |           cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md
134 |           cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE
135 | 
136 |       - name: Create Gzip archive
137 |         shell: bash
138 |         run: |
139 |           pushd build_assets || exit 1
140 |           touch -mt $(date +%Y01010000) *
141 |           tar zcvf transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz *
142 |           mv transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz ../
143 |           FILE=`find . -name "transfersh-${GITHUB_REF##*/}-${ASSET_NAME}*"`
144 |           DGST=$FILE.sha256sum
145 |           echo `sha256sum $FILE` > $DGST
146 |           popd || exit 1
147 |           FILE=./transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz
148 |           DGST=$FILE.sha256sum
149 |           echo `sha256sum $FILE` > $DGST
150 | 
151 |       - name: Change the name
152 |         run: |
153 |           mv build_assets transfersh-${GITHUB_REF##*/}-${ASSET_NAME}
154 | 
155 |       - name: Upload files to Artifacts
156 |         uses: actions/upload-artifact@v2
157 |         with:
158 |           name: transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}
159 |           path: |
160 |             ./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}/*
161 | 
162 |       - name: Upload binaries to release
163 |         uses: softprops/action-gh-release@v1
164 |         if: github.event_name == 'release'
165 |         with:
166 |           files: |
167 |             ./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}.tar.gz*
168 |             ./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}/transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}*
169 |         env:
170 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
171 | 
172 | 


--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
 1 | name: test
 2 | on:
 3 |   pull_request:
 4 |     branches:
 5 |       - "*"
 6 |   push:
 7 |     branches:
 8 |       - "*"
 9 | jobs:
10 |   test:
11 |     runs-on: ubuntu-latest
12 |     strategy:
13 |       fail-fast: false
14 |       matrix:
15 |         go_version:
16 |           - '1.21'
17 |           - '1.22'
18 |           - '1.23'
19 |           - tip
20 |     name: Test with ${{ matrix.go_version }}
21 |     steps:
22 |       - uses: actions/checkout@v2
23 |       - name: Install Go ${{ matrix.go_version }}
24 |         if: ${{ matrix.go_version != 'tip' }}
25 |         uses: actions/setup-go@master
26 |         with:
27 |           go-version: ${{ matrix.go_version }}
28 |           check-latest: true
29 |       - name: Install Go ${{ matrix.go_version }}
30 |         if: ${{ matrix.go_version == 'tip' }}
31 |         run: |
32 |           go install golang.org/dl/gotip@latest
33 |           `go env GOPATH`/bin/gotip download
34 |       - name: Vet and test no tip
35 |         if: ${{ matrix.go_version != 'tip' }}
36 |         run: |
37 |           go version
38 |           go vet ./...
39 |           go test ./...
40 |       - name: Vet and test gotip
41 |         if: ${{ matrix.go_version == 'tip' }}
42 |         run: |
43 |           `go env GOPATH`/bin/gotip version
44 |           `go env GOPATH`/bin/gotip vet ./...
45 |           `go env GOPATH`/bin/gotip test ./...
46 |   golangci:
47 |     name: Linting
48 |     runs-on: ubuntu-latest
49 |     steps:
50 |       - uses: actions/checkout@v2
51 |       - uses: actions/setup-go@master
52 |         with:
53 |           go-version: '1.23'
54 |           check-latest: true
55 |       - name: golangci-lint
56 |         uses: golangci/golangci-lint-action@v2
57 |         with:
58 |           version: latest
59 |           skip-go-installation: true
60 |           args: "--config .golangci.yml"
61 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | build/
 2 | pkg/
 3 | dist/
 4 | src/
 5 | bin/
 6 | *.pyc
 7 | *.egg-info/
 8 | .idea/
 9 | 
10 | .tmp
11 | .vagrant
12 | 
13 | bower_components/
14 | node_modules/
15 | 
16 | transfersh-server/run.sh
17 | .elasticbeanstalk/
18 | 
19 | # Elastic Beanstalk Files
20 | .elasticbeanstalk/*
21 | !.elasticbeanstalk/*.cfg.yml
22 | !.elasticbeanstalk/*.global.yml
23 | 
24 | !.github/build/
25 | 


--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
 1 | run:
 2 |   deadline: 10m
 3 |   issues-exit-code: 1
 4 |   tests: true
 5 | 
 6 | output:
 7 |   format: colored-line-number
 8 |   print-issued-lines: true
 9 |   print-linter-name: true
10 | 
11 | linters:
12 |   disable:
13 |     - deadcode
14 |     - unused
15 | 
16 | issues:
17 |   max-issues-per-linter: 0
18 |   max-same-issues: 0
19 |   new: false
20 |   exclude-use-default: false
21 | 


--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
 1 | {
 2 |     "node": true,
 3 |     "browser": true,
 4 |     "esnext": true,
 5 |     "bitwise": true,
 6 |     "camelcase": true,
 7 |     "curly": true,
 8 |     "eqeqeq": true,
 9 |     "immed": true,
10 |     "indent": 2,
11 |     "latedef": true,
12 |     "newcap": true,
13 |     "noarg": true,
14 |     "quotmark": "single",
15 |     "regexp": true,
16 |     "undef": true,
17 |     "unused": true,
18 |     "strict": true,
19 |     "trailing": true,
20 |     "smarttabs": true,
21 |     "jquery": true,
22 |     "white": true
23 | }
24 | 


--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
 1 | 
 2 | # Contributor Code of Conduct
 3 | 
 4 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
 5 | 
 6 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
 7 | 
 8 | Examples of unacceptable behavior by participants include:
 9 | 
10 | * The use of sexualized language or imagery
11 | * Personal attacks
12 | * Trolling or insulting/derogatory comments
13 | * Public or private harassment
14 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission
15 | * Other unethical or unprofessional conduct
16 | * Use of harsh language
17 | 
18 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
19 | 
20 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
21 | 
22 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
23 | 
24 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.2.0, available at https://www.contributor-covenant.org/version/1/2/0/code-of-conduct.html
25 | 
26 | 


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | # Default to Go 1.20
 2 | ARG GO_VERSION=1.20
 3 | FROM golang:${GO_VERSION}-alpine as build
 4 | 
 5 | # Necessary to run 'go get' and to compile the linked binary
 6 | RUN apk add git musl-dev mailcap
 7 | 
 8 | WORKDIR /go/src/github.com/dutchcoders/transfer.sh
 9 | 
10 | COPY go.mod go.sum ./
11 | 
12 | RUN go mod download
13 | 
14 | COPY . .
15 | 
16 | # build & install server
17 | 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
18 | 
19 | ARG PUID=5000 \
20 |     PGID=5000 \
21 |     RUNAS
22 | 
23 | RUN mkdir -p /tmp/useradd /tmp/empty && \
24 |     if [ ! -z "$RUNAS" ]; then \
25 |     echo "${RUNAS}:x:${PUID}:${PGID}::/nonexistent:/sbin/nologin" >> /tmp/useradd/passwd && \
26 |     echo "${RUNAS}:!:::::::" >> /tmp/useradd/shadow && \
27 |     echo "${RUNAS}:x:${PGID}:" >> /tmp/useradd/group && \
28 |     echo "${RUNAS}:!::" >> /tmp/useradd/groupshadow; else touch /tmp/useradd/unused; fi
29 | 
30 | FROM scratch AS final
31 | LABEL maintainer="Andrea Spacca <andrea.spacca@gmail.com>"
32 | ARG RUNAS
33 | 
34 | COPY --from=build /etc/mime.types /etc/mime.types
35 | COPY --from=build /tmp/empty /tmp
36 | COPY --from=build /tmp/useradd/* /etc/
37 | COPY --from=build --chown=${RUNAS}  /go/bin/transfersh /go/bin/transfersh
38 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
39 | 
40 | USER ${RUNAS}
41 | 
42 | ENTRYPOINT ["/go/bin/transfersh", "--listener", ":8080"]
43 | 
44 | EXPOSE 8080
45 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | The MIT License (MIT)
 2 | 
 3 | Copyright (c) 2014-2018 DutchCoders [https://github.com/dutchcoders/]
 4 | Copyright (c) 2018-2020 Andrea Spacca.
 5 | Copyright (c) 2020- Andrea Spacca and Stefan Benten.
 6 | 
 7 | Permission is hereby granted, free of charge, to any person obtaining a copy
 8 | of this software and associated documentation files (the "Software"), to deal
 9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 | 
14 | The above copyright notice and this permission notice shall be included in
15 | all copies or substantial portions of the Software.
16 | 
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | THE SOFTWARE.
24 | 


--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: lint
2 | 
3 | lint:
4 | 	golangci-lint run --out-format=github-actions --config .golangci.yml 
5 | 
6 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # transfer.sh [![Go Report Card](https://goreportcard.com/badge/github.com/dutchcoders/transfer.sh)](https://goreportcard.com/report/github.com/dutchcoders/transfer.sh) [![Docker pulls](https://img.shields.io/docker/pulls/dutchcoders/transfer.sh.svg)](https://hub.docker.com/r/dutchcoders/transfer.sh/) [![Build Status](https://github.com/dutchcoders/transfer.sh/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/dutchcoders/transfer.sh/actions/workflows/test.yml?query=branch%3Amain)
  2 | 
  3 | Easy and fast file sharing from the command-line. This code contains the server with everything you need to create your own instance.
  4 | 
  5 | Transfer.sh currently supports the s3 (Amazon S3), gdrive (Google Drive), storj (Storj) providers, and local file system (local).
  6 | 
  7 | ## Disclaimer
  8 | 
  9 | @stefanbenten happens to be a maintainer of this repository _and_ the person who host a well known public installation of the software in the repo.
 10 | 
 11 | The two are anyway unrelated, and the repo is not the place to direct requests and issues for any of the pubblic installation.
 12 | 
 13 | No third-party public installation of the software in the repo will be advertised or mentioned in the repo itself, for security reasons.
 14 | 
 15 | The official position of me, @aspacca, as maintainer of the repo, is that if you want to use the software you should host your own installation.
 16 | 
 17 | ## Usage
 18 | 
 19 | ### Upload:
 20 | ```bash
 21 | $ curl -v --upload-file ./hello.txt https://transfer.sh/hello.txt
 22 | ```
 23 | 
 24 | ### Encrypt & Upload:
 25 | ```bash
 26 | $ gpg --armor --symmetric --output - /tmp/hello.txt | curl --upload-file - https://transfer.sh/test.txt
 27 | ```
 28 | 
 29 | ### Download & Decrypt:
 30 | ```bash
 31 | $ curl https://transfer.sh/1lDau/test.txt | gpg --decrypt --output /tmp/hello.txt
 32 | ```
 33 | 
 34 | ### Upload to Virustotal:
 35 | ```bash
 36 | $ curl -X PUT --upload-file nhgbhhj https://transfer.sh/test.txt/virustotal
 37 | ```
 38 | 
 39 | ### Deleting
 40 | ```bash
 41 | $ curl -X DELETE <X-Url-Delete Response Header URL>
 42 | ```
 43 | 
 44 | ## Request Headers
 45 | 
 46 | ### Max-Downloads
 47 | ```bash
 48 | $ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Downloads: 1" # Limit the number of downloads
 49 | ```
 50 | 
 51 | ### Max-Days
 52 | ```bash
 53 | $ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Days: 1" # Set the number of days before deletion
 54 | ```
 55 | 
 56 | ### X-Encrypt-Password
 57 | #### Beware, use this feature only on your self-hosted server: trusting a third-party service for server side encryption is at your own risk
 58 | ```bash
 59 | $ curl --upload-file ./hello.txt https://your-transfersh-instance.tld/hello.txt -H "X-Encrypt-Password: test" # Encrypt the content server side with AES256 using "test" as password
 60 | ```
 61 | 
 62 | ### X-Decrypt-Password
 63 | #### Beware, use this feature only on your self-hosted server: trusting a third-party service for server side encryption is at your own risk
 64 | ```bash
 65 | $ curl https://your-transfersh-instance.tld/BAYh0/hello.txt -H "X-Decrypt-Password: test" # Decrypt the content server side with AES256 using "test" as password
 66 | ```
 67 | 
 68 | ## Response Headers
 69 | 
 70 | ### X-Url-Delete
 71 | 
 72 | The URL used to request the deletion of a file and returned as a response header.
 73 | ```bash
 74 | curl -sD - --upload-file ./hello.txt https://transfer.sh/hello.txt | grep -i -E 'transfer\.sh|x-url-delete'
 75 | x-url-delete: https://transfer.sh/hello.txt/BAYh0/hello.txt/PDw0NHPcqU
 76 | https://transfer.sh/hello.txt/BAYh0/hello.txt
 77 | ```
 78 | 
 79 | ## Examples
 80 | 
 81 | See good usage examples on [examples.md](examples.md)
 82 | 
 83 | ## Link aliases
 84 | 
 85 | Create direct download link:
 86 | 
 87 | https://transfer.sh/1lDau/test.txt --> https://transfer.sh/get/1lDau/test.txt
 88 | 
 89 | Inline file:
 90 | 
 91 | https://transfer.sh/1lDau/test.txt --> https://transfer.sh/inline/1lDau/test.txt
 92 | 
 93 | ## Usage
 94 | 
 95 | Parameter | Description                                                                                 | Value                        | Env                         
 96 | --- |---------------------------------------------------------------------------------------------|------------------------------|-----------------------------
 97 | listener | port to use for http (:80)                                                                  |                              | LISTENER                    |
 98 | profile-listener | port to use for profiler (:6060)                                                            |                              | PROFILE_LISTENER            |
 99 | force-https | redirect to https                                                                           | false                        | FORCE_HTTPS                 
100 | tls-listener | port to use for https (:443)                                                                |                              | TLS_LISTENER                |
101 | tls-listener-only | flag to enable tls listener only                                                            |                              | TLS_LISTENER_ONLY           |
102 | tls-cert-file | path to tls certificate                                                                     |                              | TLS_CERT_FILE               |
103 | tls-private-key | path to tls private key                                                                     |                              | TLS_PRIVATE_KEY             |
104 | http-auth-user | user for basic http auth on upload                                                          |                              | HTTP_AUTH_USER              |
105 | http-auth-pass | pass for basic http auth on upload                                                          |                              | HTTP_AUTH_PASS              |
106 | http-auth-htpasswd | htpasswd file path for basic http auth on upload                                            |                              | HTTP_AUTH_HTPASSWD          |
107 | http-auth-ip-whitelist | comma separated list of ips allowed to upload without being challenged an http auth        |                              | HTTP_AUTH_IP_WHITELIST      |
108 | ip-whitelist | comma separated list of ips allowed to connect to the service                               |                              | IP_WHITELIST                |
109 | ip-blacklist | comma separated list of ips not allowed to connect to the service                           |                              | IP_BLACKLIST                |
110 | temp-path | path to temp folder                                                                         | system temp                  | TEMP_PATH                   |
111 | web-path | path to static web files (for development or custom front end)                              |                              | WEB_PATH                    |
112 | proxy-path | path prefix when service is run behind a proxy                                              |                              | PROXY_PATH                  |
113 | proxy-port | port of the proxy when the service is run behind a proxy                                    |                              | PROXY_PORT                  |
114 | email-contact | email contact for the front end                                                             |                              | EMAIL_CONTACT               |
115 | ga-key | google analytics key for the front end                                                      |                              | GA_KEY                      |
116 | provider | which storage provider to use                                                               | (s3, storj, gdrive or local) |
117 | uservoice-key | user voice key for the front end                                                            |                              | USERVOICE_KEY               |
118 | aws-access-key | aws access key                                                                              |                              | AWS_ACCESS_KEY              |
119 | aws-secret-key | aws access key                                                                              |                              | AWS_SECRET_KEY              |
120 | bucket | aws bucket                                                                                  |                              | BUCKET                      |
121 | s3-endpoint | Custom S3 endpoint.                                                                         |                              | S3_ENDPOINT                 |
122 | s3-region | region of the s3 bucket                                                                     | eu-west-1                    | S3_REGION                   |
123 | s3-no-multipart | disables s3 multipart upload                                                                | false                        | S3_NO_MULTIPART             |
124 | s3-path-style | Forces path style URLs, required for Minio.                                                 | false                        | S3_PATH_STYLE               |
125 | storj-access | Access for the project                                                                      |                              | STORJ_ACCESS                |
126 | storj-bucket | Bucket to use within the project                                                            |                              | STORJ_BUCKET                |
127 | basedir | path storage for local/gdrive provider                                                      |                              | BASEDIR                     |
128 | gdrive-client-json-filepath | path to oauth client json config for gdrive provider                                        |                              | GDRIVE_CLIENT_JSON_FILEPATH |
129 | gdrive-local-config-path | path to store local transfer.sh config cache for gdrive provider                            |                              | GDRIVE_LOCAL_CONFIG_PATH    |
130 | gdrive-chunk-size | chunk size for gdrive upload in megabytes, must be lower than available memory (8 MB)       |                              | GDRIVE_CHUNK_SIZE           |
131 | lets-encrypt-hosts | hosts to use for lets encrypt certificates (comma separated)                                |                              | HOSTS                       |
132 | log | path to log file                                                                            |                              | LOG                         |
133 | cors-domains | comma separated list of domains for CORS, setting it enable CORS                            |                              | CORS_DOMAINS                |
134 | clamav-host | host for clamav feature                                                                     |                              | CLAMAV_HOST                 |
135 | perform-clamav-prescan | prescan every upload through clamav feature (clamav-host must be a local clamd unix socket) |                              | PERFORM_CLAMAV_PRESCAN      |
136 | rate-limit | request per minute                                                                          |                              | RATE_LIMIT                  |
137 | max-upload-size | max upload size in kilobytes                                                                |                              | MAX_UPLOAD_SIZE             |
138 | purge-days | number of days after the uploads are purged automatically                                   |                              | PURGE_DAYS                  |   
139 | purge-interval | interval in hours to run the automatic purge for (not applicable to S3 and Storj)           |                              | PURGE_INTERVAL              |   
140 | random-token-length | length of the random token for the upload path (double the size for delete path)            | 6                            | RANDOM_TOKEN_LENGTH         |   
141 | 
142 | 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.
143 | 
144 | If you want to use TLS using your own certificates, set tls-listener to :443, force-https, tls-cert-file and tls-private-key.
145 | 
146 | ## Development
147 | 
148 | Switched to GO111MODULE
149 | 
150 | ```bash
151 | go run main.go --provider=local --listener :8080 --temp-path=/tmp/ --basedir=/tmp/
152 | ```
153 | 
154 | ## Build
155 | 
156 | ```bash
157 | $ git clone git@github.com:dutchcoders/transfer.sh.git
158 | $ cd transfer.sh
159 | $ go build -o transfersh main.go
160 | ```
161 | 
162 | ## Docker
163 | 
164 | For easy deployment, we've created an official Docker container. There are two variants, differing only by which user runs the process.
165 | 
166 | The default one will run as `root`:
167 | 
168 | > [!WARNING]
169 | > It is discouraged to use `latest` tag for WatchTower or similar tools. The `latest` tag can reference unreleased developer, test builds, and patch releases for older versions. Use an actual version tag until transfer.sh supports major or minor version tags.
170 | 
171 | ```bash
172 | docker run --publish 8080:8080 dutchcoders/transfer.sh:latest --provider local --basedir /tmp/
173 | ```
174 | 
175 | ### No root
176 | 
177 | The `-noroot` tags indicate image builds that run with least priviledge to reduce the attack surface might an application get compromised.
178 | > [!NOTE]
179 | > Using `-noroot` is **recommended**
180 | 
181 | The one tagged with the suffix `-noroot` will use `5000` as both UID and GID:
182 | ```bash
183 | docker run --publish 8080:8080 dutchcoders/transfer.sh:latest-noroot --provider local --basedir /tmp/
184 | ```
185 | 
186 | > [!NOTE]
187 | > Development history details at:
188 | > - https://github.com/dutchcoders/transfer.sh/pull/418
189 | 
190 | ### Tags
191 | 
192 | Name | Usage
193 | --|--
194 | latest| Latest CI build, can be nightly, at commit, at tag, etc.
195 | latest-noroot| Latest CI build, can be nightly, at commit, at tag, etc. using [no root]
196 | nightly| Scheduled CI build every midnight UTC
197 | nightly-noroot| Scheduled CI build every midnight UTC using [no root]
198 | edge| Latest CI build after every commit on `main`
199 | edge-noroot| Latest CI build after every commit on `main` using [no root]
200 | x.y.z| CI build after tagging a release
201 | x.y.z-noroot| CI build after tagging a release using [no root]
202 | 
203 | 
204 | ### Building the Container
205 | You can also build the container yourself. This allows you to choose which UID/GID will be used, e.g. when using NFS mounts:
206 | ```bash
207 | # Build arguments:
208 | # * RUNAS: If empty, the container will run as root.
209 | #          Set this to anything to enable UID/GID selection.
210 | # * PUID:  UID of the process. Needs RUNAS != "". Defaults to 5000.
211 | # * PGID:  GID of the process. Needs RUNAS != "". Defaults to 5000.
212 | 
213 | docker build -t transfer.sh-noroot --build-arg RUNAS=doesntmatter --build-arg PUID=1337 --build-arg PGID=1338 .
214 | ```
215 | 
216 | ## S3 Usage
217 | 
218 | For the usage with a AWS S3 Bucket, you just need to specify the following options:
219 | - provider `--provider s3`
220 | - aws-access-key _(either via flag or environment variable `AWS_ACCESS_KEY`)_
221 | - aws-secret-key _(either via flag or environment variable `AWS_SECRET_KEY`)_
222 | - bucket _(either via flag or environment variable `BUCKET`)_
223 | - s3-region _(either via flag or environment variable `S3_REGION`)_
224 | 
225 | If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically.
226 | 
227 | ### Custom S3 providers
228 | 
229 | To use a custom non-AWS S3 provider, you need to specify the endpoint as defined from your cloud provider.
230 | 
231 | ## Storj Network Provider
232 | 
233 | To use the Storj Network as a storage provider you need to specify the following flags:
234 | - provider `--provider storj`
235 | - storj-access _(either via flag or environment variable STORJ_ACCESS)_
236 | - storj-bucket _(either via flag or environment variable STORJ_BUCKET)_
237 | 
238 | ### Creating Bucket and Scope
239 | 
240 | You need to create an access grant (or copy it from the uplink configuration) and a bucket in preparation.
241 | 
242 | To get started, log in to your account and go to the Access Grant Menu and start the Wizard on the upper right.
243 | 
244 | Enter your access grant name of choice, hit *Next* and restrict it as necessary/preferred.
245 | Afterwards continue either in CLI or within the Browser. Next, you'll be asked for a Passphrase used as Encryption Key.
246 | **Make sure to save it in a safe place. Without it, you will lose the ability to decrypt your files!**
247 | 
248 | Afterwards, you can copy the access grant and then start the startup of the transfer.sh endpoint. 
249 | It is recommended to provide both the access grant and the bucket name as ENV Variables for enhanced security.
250 | 
251 | Example:
252 | ```
253 | export STORJ_BUCKET=<BUCKET NAME>
254 | export STORJ_ACCESS=<ACCESS GRANT>
255 | transfer.sh --provider storj
256 | ```
257 | 
258 | ## Google Drive Usage
259 | 
260 | For the usage with Google drive, you need to specify the following options:
261 | - provider
262 | - gdrive-client-json-filepath
263 | - gdrive-local-config-path
264 | - basedir
265 | 
266 | ### Creating Gdrive Client Json
267 | 
268 | You need to create an OAuth Client id from console.cloud.google.com, download the file, and place it into a safe directory.
269 | 
270 | ### Usage example
271 | 
272 | ```go run main.go --provider gdrive --basedir /tmp/ --gdrive-client-json-filepath /[credential_dir] --gdrive-local-config-path [directory_to_save_config] ```
273 | 
274 | ## Shell functions
275 | 
276 | ### Bash, ash and zsh (multiple files uploaded as zip archive)
277 | ##### Add this to .bashrc or .zshrc or its equivalent
278 | ```bash
279 | transfer() (if [ $# -eq 0 ]; then printf "No arguments specified.\nUsage:\n transfer <file|directory>\n ... | transfer <file_name>\n">&2; return 1; fi; file_name=$(basename "$1"); if [ -t 0 ]; then file="$1"; if [ ! -e "$file" ]; then echo "$file: No such file or directory">&2; return 1; fi; if [ -d "$file" ]; then cd "$file" || return 1; file_name="$file_name.zip"; set -- zip -r -q - .; else set -- cat "$file"; fi; else set -- cat; fi; url=$("$@" | curl --silent --show-error --progress-bar --upload-file "-" "https://transfer.sh/$file_name"); echo "$url"; )
280 | ```
281 | 
282 | #### Now you can use transfer function
283 | ```
284 | $ transfer hello.txt
285 | ```
286 | 
287 | 
288 | ### Bash and zsh (with delete url, delete token output and prompt before uploading)
289 | ##### Add this to .bashrc or .zshrc or its equivalent
290 | 
291 | <details><summary>Expand</summary><p>
292 | 
293 | ```bash
294 | transfer()
295 | {
296 |     local file
297 |     declare -a file_array
298 |     file_array=("${@}")
299 | 
300 |     if [[ "${file_array[@]}" == "" || "${1}" == "--help" || "${1}" == "-h" ]]
301 |     then
302 |         echo "${0} - Upload arbitrary files to \"transfer.sh\"."
303 |         echo ""
304 |         echo "Usage: ${0} [options] [<file>]..."
305 |         echo ""
306 |         echo "OPTIONS:"
307 |         echo "  -h, --help"
308 |         echo "      show this message"
309 |         echo ""
310 |         echo "EXAMPLES:"
311 |         echo "  Upload a single file from the current working directory:"
312 |         echo "      ${0} \"image.img\""
313 |         echo ""
314 |         echo "  Upload multiple files from the current working directory:"
315 |         echo "      ${0} \"image.img\" \"image2.img\""
316 |         echo ""
317 |         echo "  Upload a file from a different directory:"
318 |         echo "      ${0} \"/tmp/some_file\""
319 |         echo ""
320 |         echo "  Upload all files from the current working directory. Be aware of the webserver's rate limiting!:"
321 |         echo "      ${0} *"
322 |         echo ""
323 |         echo "  Upload a single file from the current working directory and filter out the delete token and download link:"
324 |         echo "      ${0} \"image.img\" | awk --field-separator=\": \" '/Delete token:/ { print \$2 } /Download link:/ { print \$2 }'"
325 |         echo ""
326 |         echo "  Show help text from \"transfer.sh\":"
327 |         echo "      curl --request GET \"https://transfer.sh\""
328 |         return 0
329 |     else
330 |         for file in "${file_array[@]}"
331 |         do
332 |             if [[ ! -f "${file}" ]]
333 |             then
334 |                 echo -e "\e[01;31m'${file}' could not be found or is not a file.\e[0m" >&2
335 |                 return 1
336 |             fi
337 |         done
338 |         unset file
339 |     fi
340 | 
341 |     local upload_files
342 |     local curl_output
343 |     local awk_output
344 | 
345 |     du -c -k -L "${file_array[@]}" >&2
346 |     # be compatible with "bash"
347 |     if [[ "${ZSH_NAME}" == "zsh" ]]
348 |     then
349 |         read 
#39;upload_files?\e[01;31mDo you really want to upload the above files ('"${#file_array[@]}"
#39;) to "transfer.sh"? (Y/n): \e[0m'
350 |     elif [[ "${BASH}" == *"bash"* ]]
351 |     then
352 |         read -p 
#39;\e[01;31mDo you really want to upload the above files ('"${#file_array[@]}"
#39;) to "transfer.sh"? (Y/n): \e[0m' upload_files
353 |     fi
354 | 
355 |     case "${upload_files:-y}" in
356 |         "y"|"Y")
357 |             # for the sake of the progress bar, execute "curl" for each file.
358 |             # the parameters "--include" and "--form" will suppress the progress bar.
359 |             for file in "${file_array[@]}"
360 |             do
361 |                 # show delete link and filter out the delete token from the response header after upload.
362 |                 # it is important to save "curl's" "stdout" via a subshell to a variable or redirect it to another command,
363 |                 # which just redirects to "stdout" in order to have a sane output afterwards.
364 |                 # the progress bar is redirected to "stderr" and is only displayed,
365 |                 # if "stdout" is redirected to something; e.g. ">/dev/null", "tee /dev/null" or "| <some_command>".
366 |                 # the response header is redirected to "stdout", so redirecting "stdout" to "/dev/null" does not make any sense.
367 |                 # redirecting "curl's" "stderr" to "stdout" ("2>&1") will suppress the progress bar.
368 |                 curl_output=$(curl --request PUT --progress-bar --dump-header - --upload-file "${file}" "https://transfer.sh/")
369 |                 awk_output=$(awk \
370 |                     'gsub("\r", "", $0) && tolower($1) ~ /x-url-delete/ \
371 |                     {
372 |                         delete_link=$2;
373 |                         print "Delete command: curl --request DELETE " "\""delete_link"\"";
374 | 
375 |                         gsub(".*/", "", delete_link);
376 |                         delete_token=delete_link;
377 |                         print "Delete token: " delete_token;
378 |                     }
379 | 
380 |                     END{
381 |                         print "Download link: " $0;
382 |                     }' <<< "${curl_output}")
383 | 
384 |                 # return the results via "stdout", "awk" does not do this for some reason.
385 |                 echo -e "${awk_output}\n"
386 | 
387 |                 # avoid rate limiting as much as possible; nginx: too many requests.
388 |                 if (( ${#file_array[@]} > 4 ))
389 |                 then
390 |                     sleep 5
391 |                 fi
392 |             done
393 |             ;;
394 | 
395 |         "n"|"N")
396 |             return 1
397 |             ;;
398 | 
399 |         *)
400 |             echo -e "\e[01;31mWrong input: '${upload_files}'.\e[0m" >&2
401 |             return 1
402 |     esac
403 | }
404 | ```
405 | 
406 | </p></details>
407 | 
408 | #### Sample output
409 | ```bash
410 | $ ls -lh
411 | total 20M
412 | -rw-r--r-- 1 <some_username> <some_username> 10M Apr  4 21:08 image.img
413 | -rw-r--r-- 1 <some_username> <some_username> 10M Apr  4 21:08 image2.img
414 | $ transfer image*
415 | 10240K  image2.img
416 | 10240K  image.img
417 | 20480K  total
418 | Do you really want to upload the above files (2) to "transfer.sh"? (Y/n):
419 | ######################################################################################################################################################################################################################################## 100.0%
420 | Delete command: curl --request DELETE "https://transfer.sh/wJw9pz/image2.img/mSctGx7pYCId"
421 | Delete token: mSctGx7pYCId
422 | Download link: https://transfer.sh/wJw9pz/image2.img
423 | 
424 | ######################################################################################################################################################################################################################################## 100.0%
425 | Delete command: curl --request DELETE "https://transfer.sh/ljJc5I/image.img/nw7qaoiKUwCU"
426 | Delete token: nw7qaoiKUwCU
427 | Download link: https://transfer.sh/ljJc5I/image.img
428 | 
429 | $ transfer "image.img" | awk --field-separator=": " '/Delete token:/ { print $2 } /Download link:/ { print $2 }'
430 | 10240K  image.img
431 | 10240K  total
432 | Do you really want to upload the above files (1) to "transfer.sh"? (Y/n):
433 | ######################################################################################################################################################################################################################################## 100.0%
434 | tauN5dE3fWJe
435 | https://transfer.sh/MYkuqn/image.img
436 | ```
437 | 
438 | ## Contributions
439 | 
440 | Contributions are welcome.
441 | 
442 | ## Creators
443 | 
444 | **Remco Verhoef**
445 | - <https://twitter.com/remco_verhoef>
446 | - <https://twitter.com/dutchcoders>
447 | 
448 | **Uvis Grinfelds**
449 | 
450 | ## Maintainer
451 | 
452 | **Andrea Spacca**
453 | 
454 | **Stefan Benten**
455 | 
456 | ## Copyright and License
457 | 
458 | Code and documentation copyright 2011-2018 Remco Verhoef.
459 | Code and documentation copyright 2018-2020 Andrea Spacca.
460 | Code and documentation copyright 2020- Andrea Spacca and Stefan Benten.
461 | 
462 | Code released under [the MIT license](LICENSE).
463 | 


--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
 1 | # -*- mode: ruby -*-
 2 | # vi: set ft=ruby :
 3 | 
 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
 5 | VAGRANTFILE_API_VERSION = "2"
 6 | 
 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
 8 |     # Every Vagrant virtual environment requires a box to build off of.
 9 |     config.vm.box = "puphpet/ubuntu1404-x64"
10 |     config.vm.provider "vmware_fusion" do |v|
11 |         v.gui = true
12 |     end
13 | end
14 | 


--------------------------------------------------------------------------------
/cmd/cmd.go:
--------------------------------------------------------------------------------
  1 | package cmd
  2 | 
  3 | import (
  4 | 	"errors"
  5 | 	"fmt"
  6 | 	"log"
  7 | 	"os"
  8 | 	"strings"
  9 | 
 10 | 	"github.com/dutchcoders/transfer.sh/server/storage"
 11 | 
 12 | 	"github.com/dutchcoders/transfer.sh/server"
 13 | 	"github.com/fatih/color"
 14 | 	"github.com/urfave/cli/v2"
 15 | 	"google.golang.org/api/googleapi"
 16 | )
 17 | 
 18 | // Version is inject at build time
 19 | var Version = "0.0.0"
 20 | var helpTemplate = `NAME:
 21 | {{.Name}} - {{.Usage}}
 22 | 
 23 | DESCRIPTION:
 24 | {{.Description}}
 25 | 
 26 | USAGE:
 27 | {{.Name}} {{if .Flags}}[flags] {{end}}command{{if .Flags}}{{end}} [arguments...]
 28 | 
 29 | COMMANDS:
 30 | {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
 31 | {{end}}{{if .Flags}}
 32 | FLAGS:
 33 | {{range .Flags}}{{.}}
 34 | {{end}}{{end}}
 35 | VERSION:
 36 | ` + Version +
 37 | 	`{{ "\n"}}`
 38 | 
 39 | var globalFlags = []cli.Flag{
 40 | 	&cli.StringFlag{
 41 | 		Name:    "listener",
 42 | 		Usage:   "127.0.0.1:8080",
 43 | 		Value:   "127.0.0.1:8080",
 44 | 		EnvVars: []string{"LISTENER"},
 45 | 	},
 46 | 	// redirect to https?
 47 | 	// hostnames
 48 | 	&cli.StringFlag{
 49 | 		Name:    "profile-listener",
 50 | 		Usage:   "127.0.0.1:6060",
 51 | 		Value:   "",
 52 | 		EnvVars: []string{"PROFILE_LISTENER"},
 53 | 	},
 54 | 	&cli.BoolFlag{
 55 | 		Name:    "force-https",
 56 | 		Usage:   "",
 57 | 		EnvVars: []string{"FORCE_HTTPS"},
 58 | 	},
 59 | 	&cli.StringFlag{
 60 | 		Name:    "tls-listener",
 61 | 		Usage:   "127.0.0.1:8443",
 62 | 		Value:   "",
 63 | 		EnvVars: []string{"TLS_LISTENER"},
 64 | 	},
 65 | 	&cli.BoolFlag{
 66 | 		Name:    "tls-listener-only",
 67 | 		Usage:   "",
 68 | 		EnvVars: []string{"TLS_LISTENER_ONLY"},
 69 | 	},
 70 | 	&cli.StringFlag{
 71 | 		Name:    "tls-cert-file",
 72 | 		Value:   "",
 73 | 		EnvVars: []string{"TLS_CERT_FILE"},
 74 | 	},
 75 | 	&cli.StringFlag{
 76 | 		Name:    "tls-private-key",
 77 | 		Value:   "",
 78 | 		EnvVars: []string{"TLS_PRIVATE_KEY"},
 79 | 	},
 80 | 	&cli.StringFlag{
 81 | 		Name:    "temp-path",
 82 | 		Usage:   "path to temp files",
 83 | 		Value:   os.TempDir(),
 84 | 		EnvVars: []string{"TEMP_PATH"},
 85 | 	},
 86 | 	&cli.StringFlag{
 87 | 		Name:    "web-path",
 88 | 		Usage:   "path to static web files",
 89 | 		Value:   "",
 90 | 		EnvVars: []string{"WEB_PATH"},
 91 | 	},
 92 | 	&cli.StringFlag{
 93 | 		Name:    "proxy-path",
 94 | 		Usage:   "path prefix when service is run behind a proxy",
 95 | 		Value:   "",
 96 | 		EnvVars: []string{"PROXY_PATH"},
 97 | 	},
 98 | 	&cli.StringFlag{
 99 | 		Name:    "proxy-port",
100 | 		Usage:   "port of the proxy when the service is run behind a proxy",
101 | 		Value:   "",
102 | 		EnvVars: []string{"PROXY_PORT"},
103 | 	},
104 | 	&cli.StringFlag{
105 | 		Name:    "email-contact",
106 | 		Usage:   "email address to link in Contact Us (front end)",
107 | 		Value:   "",
108 | 		EnvVars: []string{"EMAIL_CONTACT"},
109 | 	},
110 | 	&cli.StringFlag{
111 | 		Name:    "ga-key",
112 | 		Usage:   "key for google analytics (front end)",
113 | 		Value:   "",
114 | 		EnvVars: []string{"GA_KEY"},
115 | 	},
116 | 	&cli.StringFlag{
117 | 		Name:    "uservoice-key",
118 | 		Usage:   "key for user voice (front end)",
119 | 		Value:   "",
120 | 		EnvVars: []string{"USERVOICE_KEY"},
121 | 	},
122 | 	&cli.StringFlag{
123 | 		Name:    "provider",
124 | 		Usage:   "s3|gdrive|local",
125 | 		Value:   "",
126 | 		EnvVars: []string{"PROVIDER"},
127 | 	},
128 | 	&cli.StringFlag{
129 | 		Name:    "s3-endpoint",
130 | 		Usage:   "",
131 | 		Value:   "",
132 | 		EnvVars: []string{"S3_ENDPOINT"},
133 | 	},
134 | 	&cli.StringFlag{
135 | 		Name:    "s3-region",
136 | 		Usage:   "",
137 | 		Value:   "eu-west-1",
138 | 		EnvVars: []string{"S3_REGION"},
139 | 	},
140 | 	&cli.StringFlag{
141 | 		Name:    "aws-access-key",
142 | 		Usage:   "",
143 | 		Value:   "",
144 | 		EnvVars: []string{"AWS_ACCESS_KEY"},
145 | 	},
146 | 	&cli.StringFlag{
147 | 		Name:    "aws-secret-key",
148 | 		Usage:   "",
149 | 		Value:   "",
150 | 		EnvVars: []string{"AWS_SECRET_KEY"},
151 | 	},
152 | 	&cli.StringFlag{
153 | 		Name:    "bucket",
154 | 		Usage:   "",
155 | 		Value:   "",
156 | 		EnvVars: []string{"BUCKET"},
157 | 	},
158 | 	&cli.BoolFlag{
159 | 		Name:    "s3-no-multipart",
160 | 		Usage:   "Disables S3 Multipart Puts",
161 | 		EnvVars: []string{"S3_NO_MULTIPART"},
162 | 	},
163 | 	&cli.BoolFlag{
164 | 		Name:    "s3-path-style",
165 | 		Usage:   "Forces path style URLs, required for Minio.",
166 | 		EnvVars: []string{"S3_PATH_STYLE"},
167 | 	},
168 | 	&cli.StringFlag{
169 | 		Name:    "gdrive-client-json-filepath",
170 | 		Usage:   "",
171 | 		Value:   "",
172 | 		EnvVars: []string{"GDRIVE_CLIENT_JSON_FILEPATH"},
173 | 	},
174 | 	&cli.StringFlag{
175 | 		Name:    "gdrive-local-config-path",
176 | 		Usage:   "",
177 | 		Value:   "",
178 | 		EnvVars: []string{"GDRIVE_LOCAL_CONFIG_PATH"},
179 | 	},
180 | 	&cli.IntFlag{
181 | 		Name:    "gdrive-chunk-size",
182 | 		Usage:   "",
183 | 		Value:   googleapi.DefaultUploadChunkSize / 1024 / 1024,
184 | 		EnvVars: []string{"GDRIVE_CHUNK_SIZE"},
185 | 	},
186 | 	&cli.StringFlag{
187 | 		Name:    "storj-access",
188 | 		Usage:   "Access for the project",
189 | 		Value:   "",
190 | 		EnvVars: []string{"STORJ_ACCESS"},
191 | 	},
192 | 	&cli.StringFlag{
193 | 		Name:    "storj-bucket",
194 | 		Usage:   "Bucket to use within the project",
195 | 		Value:   "",
196 | 		EnvVars: []string{"STORJ_BUCKET"},
197 | 	},
198 | 	&cli.IntFlag{
199 | 		Name:    "rate-limit",
200 | 		Usage:   "requests per minute",
201 | 		Value:   0,
202 | 		EnvVars: []string{"RATE_LIMIT"},
203 | 	},
204 | 	&cli.IntFlag{
205 | 		Name:    "purge-days",
206 | 		Usage:   "number of days after uploads are purged automatically",
207 | 		Value:   0,
208 | 		EnvVars: []string{"PURGE_DAYS"},
209 | 	},
210 | 	&cli.IntFlag{
211 | 		Name:    "purge-interval",
212 | 		Usage:   "interval in hours to run the automatic purge for",
213 | 		Value:   0,
214 | 		EnvVars: []string{"PURGE_INTERVAL"},
215 | 	},
216 | 	&cli.Int64Flag{
217 | 		Name:    "max-upload-size",
218 | 		Usage:   "max limit for upload, in kilobytes",
219 | 		Value:   0,
220 | 		EnvVars: []string{"MAX_UPLOAD_SIZE"},
221 | 	},
222 | 	&cli.StringFlag{
223 | 		Name:    "lets-encrypt-hosts",
224 | 		Usage:   "host1, host2",
225 | 		Value:   "",
226 | 		EnvVars: []string{"HOSTS"},
227 | 	},
228 | 	&cli.StringFlag{
229 | 		Name:    "log",
230 | 		Usage:   "/var/log/transfersh.log",
231 | 		Value:   "",
232 | 		EnvVars: []string{"LOG"},
233 | 	},
234 | 	&cli.StringFlag{
235 | 		Name:    "basedir",
236 | 		Usage:   "path to storage",
237 | 		Value:   "",
238 | 		EnvVars: []string{"BASEDIR"},
239 | 	},
240 | 	&cli.StringFlag{
241 | 		Name:    "clamav-host",
242 | 		Usage:   "clamav-host",
243 | 		Value:   "",
244 | 		EnvVars: []string{"CLAMAV_HOST"},
245 | 	},
246 | 	&cli.BoolFlag{
247 | 		Name:    "perform-clamav-prescan",
248 | 		Usage:   "perform-clamav-prescan",
249 | 		EnvVars: []string{"PERFORM_CLAMAV_PRESCAN"},
250 | 	},
251 | 	&cli.StringFlag{
252 | 		Name:    "virustotal-key",
253 | 		Usage:   "virustotal-key",
254 | 		Value:   "",
255 | 		EnvVars: []string{"VIRUSTOTAL_KEY"},
256 | 	},
257 | 	&cli.BoolFlag{
258 | 		Name:    "profiler",
259 | 		Usage:   "enable profiling",
260 | 		EnvVars: []string{"PROFILER"},
261 | 	},
262 | 	&cli.StringFlag{
263 | 		Name:    "http-auth-user",
264 | 		Usage:   "user for http basic auth",
265 | 		Value:   "",
266 | 		EnvVars: []string{"HTTP_AUTH_USER"},
267 | 	},
268 | 	&cli.StringFlag{
269 | 		Name:    "http-auth-pass",
270 | 		Usage:   "pass for http basic auth",
271 | 		Value:   "",
272 | 		EnvVars: []string{"HTTP_AUTH_PASS"},
273 | 	},
274 | 	&cli.StringFlag{
275 | 		Name:    "http-auth-htpasswd",
276 | 		Usage:   "htpasswd file http basic auth",
277 | 		Value:   "",
278 | 		EnvVars: []string{"HTTP_AUTH_HTPASSWD"},
279 | 	},
280 | 	&cli.StringFlag{
281 | 		Name:    "http-auth-ip-whitelist",
282 | 		Usage:   "comma separated list of ips allowed to upload without being challenged an http auth",
283 | 		Value:   "",
284 | 		EnvVars: []string{"HTTP_AUTH_IP_WHITELIST"},
285 | 	},
286 | 	&cli.StringFlag{
287 | 		Name:    "ip-whitelist",
288 | 		Usage:   "comma separated list of ips allowed to connect to the service",
289 | 		Value:   "",
290 | 		EnvVars: []string{"IP_WHITELIST"},
291 | 	},
292 | 	&cli.StringFlag{
293 | 		Name:    "ip-blacklist",
294 | 		Usage:   "comma separated list of ips not allowed to connect to the service",
295 | 		Value:   "",
296 | 		EnvVars: []string{"IP_BLACKLIST"},
297 | 	},
298 | 	&cli.StringFlag{
299 | 		Name:    "cors-domains",
300 | 		Usage:   "comma separated list of domains allowed for CORS requests",
301 | 		Value:   "",
302 | 		EnvVars: []string{"CORS_DOMAINS"},
303 | 	},
304 | 	&cli.IntFlag{
305 | 		Name:    "random-token-length",
306 | 		Usage:   "",
307 | 		Value:   10,
308 | 		EnvVars: []string{"RANDOM_TOKEN_LENGTH"},
309 | 	},
310 | }
311 | 
312 | // Cmd wraps cli.app
313 | type Cmd struct {
314 | 	*cli.App
315 | }
316 | 
317 | func versionCommand(_ *cli.Context) error {
318 | 	fmt.Println(color.YellowString("transfer.sh %s: Easy file sharing from the command line", Version))
319 | 	return nil
320 | }
321 | 
322 | // New is the factory for transfer.sh
323 | func New() *Cmd {
324 | 	logger := log.New(os.Stdout, "[transfer.sh]", log.LstdFlags)
325 | 
326 | 	app := cli.NewApp()
327 | 	app.Name = "transfer.sh"
328 | 	app.Authors = []*cli.Author{}
329 | 	app.Usage = "transfer.sh"
330 | 	app.Description = `Easy file sharing from the command line`
331 | 	app.Version = Version
332 | 	app.Flags = globalFlags
333 | 	app.CustomAppHelpTemplate = helpTemplate
334 | 	app.Commands = []*cli.Command{
335 | 		{
336 | 			Name:   "version",
337 | 			Action: versionCommand,
338 | 		},
339 | 	}
340 | 
341 | 	app.Before = func(c *cli.Context) error {
342 | 		return nil
343 | 	}
344 | 
345 | 	app.Action = func(c *cli.Context) error {
346 | 		var options []server.OptionFn
347 | 		if v := c.String("listener"); v != "" {
348 | 			options = append(options, server.Listener(v))
349 | 		}
350 | 
351 | 		if v := c.String("cors-domains"); v != "" {
352 | 			options = append(options, server.CorsDomains(v))
353 | 		}
354 | 
355 | 		if v := c.String("tls-listener"); v == "" {
356 | 		} else if c.Bool("tls-listener-only") {
357 | 			options = append(options, server.TLSListener(v, true))
358 | 		} else {
359 | 			options = append(options, server.TLSListener(v, false))
360 | 		}
361 | 
362 | 		if v := c.String("profile-listener"); v != "" {
363 | 			options = append(options, server.ProfileListener(v))
364 | 		}
365 | 
366 | 		if v := c.String("web-path"); v != "" {
367 | 			options = append(options, server.WebPath(v))
368 | 		}
369 | 
370 | 		if v := c.String("proxy-path"); v != "" {
371 | 			options = append(options, server.ProxyPath(v))
372 | 		}
373 | 
374 | 		if v := c.String("proxy-port"); v != "" {
375 | 			options = append(options, server.ProxyPort(v))
376 | 		}
377 | 
378 | 		if v := c.String("email-contact"); v != "" {
379 | 			options = append(options, server.EmailContact(v))
380 | 		}
381 | 
382 | 		if v := c.String("ga-key"); v != "" {
383 | 			options = append(options, server.GoogleAnalytics(v))
384 | 		}
385 | 
386 | 		if v := c.String("uservoice-key"); v != "" {
387 | 			options = append(options, server.UserVoice(v))
388 | 		}
389 | 
390 | 		if v := c.String("temp-path"); v != "" {
391 | 			options = append(options, server.TempPath(v))
392 | 		}
393 | 
394 | 		if v := c.String("log"); v != "" {
395 | 			options = append(options, server.LogFile(logger, v))
396 | 		} else {
397 | 			options = append(options, server.Logger(logger))
398 | 		}
399 | 
400 | 		if v := c.String("lets-encrypt-hosts"); v != "" {
401 | 			options = append(options, server.UseLetsEncrypt(strings.Split(v, ",")))
402 | 		}
403 | 
404 | 		if v := c.String("virustotal-key"); v != "" {
405 | 			options = append(options, server.VirustotalKey(v))
406 | 		}
407 | 
408 | 		if v := c.String("clamav-host"); v != "" {
409 | 			options = append(options, server.ClamavHost(v))
410 | 		}
411 | 
412 | 		if v := c.Bool("perform-clamav-prescan"); v {
413 | 			if c.String("clamav-host") == "" {
414 | 				return errors.New("clamav-host not set")
415 | 			}
416 | 
417 | 			options = append(options, server.PerformClamavPrescan(v))
418 | 		}
419 | 
420 | 		if v := c.Int64("max-upload-size"); v > 0 {
421 | 			options = append(options, server.MaxUploadSize(v))
422 | 		}
423 | 
424 | 		if v := c.Int("rate-limit"); v > 0 {
425 | 			options = append(options, server.RateLimit(v))
426 | 		}
427 | 
428 | 		v := c.Int("random-token-length")
429 | 		options = append(options, server.RandomTokenLength(v))
430 | 
431 | 		purgeDays := c.Int("purge-days")
432 | 		purgeInterval := c.Int("purge-interval")
433 | 		if purgeDays > 0 && purgeInterval > 0 {
434 | 			options = append(options, server.Purge(purgeDays, purgeInterval))
435 | 		}
436 | 
437 | 		if cert := c.String("tls-cert-file"); cert == "" {
438 | 		} else if pk := c.String("tls-private-key"); pk == "" {
439 | 		} else {
440 | 			options = append(options, server.TLSConfig(cert, pk))
441 | 		}
442 | 
443 | 		if c.Bool("profiler") {
444 | 			options = append(options, server.EnableProfiler())
445 | 		}
446 | 
447 | 		if c.Bool("force-https") {
448 | 			options = append(options, server.ForceHTTPS())
449 | 		}
450 | 
451 | 		if httpAuthUser := c.String("http-auth-user"); httpAuthUser == "" {
452 | 		} else if httpAuthPass := c.String("http-auth-pass"); httpAuthPass == "" {
453 | 		} else {
454 | 			options = append(options, server.HTTPAuthCredentials(httpAuthUser, httpAuthPass))
455 | 		}
456 | 
457 | 		if httpAuthHtpasswd := c.String("http-auth-htpasswd"); httpAuthHtpasswd != "" {
458 | 			options = append(options, server.HTTPAuthHtpasswd(httpAuthHtpasswd))
459 | 		}
460 | 
461 | 		if httpAuthIPWhitelist := c.String("http-auth-ip-whitelist"); httpAuthIPWhitelist != "" {
462 | 			ipFilterOptions := server.IPFilterOptions{}
463 | 			ipFilterOptions.AllowedIPs = strings.Split(httpAuthIPWhitelist, ",")
464 | 			ipFilterOptions.BlockByDefault = true
465 | 			options = append(options, server.HTTPAUTHFilterOptions(ipFilterOptions))
466 | 		}
467 | 
468 | 		applyIPFilter := false
469 | 		ipFilterOptions := server.IPFilterOptions{}
470 | 		if ipWhitelist := c.String("ip-whitelist"); ipWhitelist != "" {
471 | 			applyIPFilter = true
472 | 			ipFilterOptions.AllowedIPs = strings.Split(ipWhitelist, ",")
473 | 			ipFilterOptions.BlockByDefault = true
474 | 		}
475 | 
476 | 		if ipBlacklist := c.String("ip-blacklist"); ipBlacklist != "" {
477 | 			applyIPFilter = true
478 | 			ipFilterOptions.BlockedIPs = strings.Split(ipBlacklist, ",")
479 | 		}
480 | 
481 | 		if applyIPFilter {
482 | 			options = append(options, server.FilterOptions(ipFilterOptions))
483 | 		}
484 | 
485 | 		switch provider := c.String("provider"); provider {
486 | 		case "s3":
487 | 			if accessKey := c.String("aws-access-key"); accessKey == "" {
488 | 				return errors.New("access-key not set.")
489 | 			} else if secretKey := c.String("aws-secret-key"); secretKey == "" {
490 | 				return errors.New("secret-key not set.")
491 | 			} else if bucket := c.String("bucket"); bucket == "" {
492 | 				return errors.New("bucket not set.")
493 | 			} 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 {
494 | 				return err
495 | 			} else {
496 | 				options = append(options, server.UseStorage(store))
497 | 			}
498 | 		case "gdrive":
499 | 			chunkSize := c.Int("gdrive-chunk-size") * 1024 * 1024
500 | 
501 | 			if clientJSONFilepath := c.String("gdrive-client-json-filepath"); clientJSONFilepath == "" {
502 | 				return errors.New("gdrive-client-json-filepath not set.")
503 | 			} else if localConfigPath := c.String("gdrive-local-config-path"); localConfigPath == "" {
504 | 				return errors.New("gdrive-local-config-path not set.")
505 | 			} else if basedir := c.String("basedir"); basedir == "" {
506 | 				return errors.New("basedir not set.")
507 | 			} else if store, err := storage.NewGDriveStorage(c.Context, clientJSONFilepath, localConfigPath, basedir, chunkSize, logger); err != nil {
508 | 				return err
509 | 			} else {
510 | 				options = append(options, server.UseStorage(store))
511 | 			}
512 | 		case "storj":
513 | 			if access := c.String("storj-access"); access == "" {
514 | 				return errors.New("storj-access not set.")
515 | 			} else if bucket := c.String("storj-bucket"); bucket == "" {
516 | 				return errors.New("storj-bucket not set.")
517 | 			} else if store, err := storage.NewStorjStorage(c.Context, access, bucket, purgeDays, logger); err != nil {
518 | 				return err
519 | 			} else {
520 | 				options = append(options, server.UseStorage(store))
521 | 			}
522 | 		case "local":
523 | 			if v := c.String("basedir"); v == "" {
524 | 				return errors.New("basedir not set.")
525 | 			} else if store, err := storage.NewLocalStorage(v, logger); err != nil {
526 | 				return err
527 | 			} else {
528 | 				options = append(options, server.UseStorage(store))
529 | 			}
530 | 		default:
531 | 			return errors.New("Provider not set or invalid.")
532 | 		}
533 | 
534 | 		srvr, err := server.New(
535 | 			options...,
536 | 		)
537 | 
538 | 		if err != nil {
539 | 			logger.Println(color.RedString("Error starting server: %s", err.Error()))
540 | 			return err
541 | 		}
542 | 
543 | 		srvr.Run()
544 | 		return nil
545 | 	}
546 | 
547 | 	return &Cmd{
548 | 		App: app,
549 | 	}
550 | }
551 | 


--------------------------------------------------------------------------------
/examples.md:
--------------------------------------------------------------------------------
  1 | # Table of Contents
  2 | 
  3 | * [Aliases](#aliases)
  4 | * [Uploading and downloading](#uploading-and-downloading)
  5 | * [Archiving and backups](#archiving-and-backups)
  6 | * [Encrypting and decrypting](#encrypting-and-decrypting)
  7 | * [Scanning for viruses](#scanning-for-viruses)
  8 | * [Uploading and copy download command](#uploading-and-copy-download-command)
  9 | * [Uploading and displaying URL and deletion token](#uploading-and-displaying-url-and-deletion-token)
 10 | 
 11 | ## Aliases
 12 | <a name="aliases"/>
 13 | 
 14 | ## Add alias to .bashrc or .zshrc
 15 | 
 16 | ### Using curl
 17 | ```bash
 18 | transfer() {
 19 |     curl --progress-bar --upload-file "$1" https://transfer.sh/$(basename "$1") | tee /dev/null;
 20 |     echo
 21 | }
 22 | 
 23 | alias transfer=transfer
 24 | ```
 25 | 
 26 | ### Using wget
 27 | ```bash
 28 | transfer() {
 29 |     wget -t 1 -qO - --method=PUT --body-file="$1" --header="Content-Type: $(file -b --mime-type "$1")" https://transfer.sh/$(basename "$1");
 30 |     echo
 31 | }
 32 | 
 33 | alias transfer=transfer
 34 | ```
 35 | 
 36 | ## Add alias for fish-shell
 37 | 
 38 | ### Using curl
 39 | ```fish
 40 | function transfer --description 'Upload a file to transfer.sh'
 41 |     if [ $argv[1] ]
 42 |         # write to output to tmpfile because of progress bar
 43 |         set -l tmpfile ( mktemp -t transferXXXXXX )
 44 |         curl --progress-bar --upload-file "$argv[1]" https://transfer.sh/(basename $argv[1]) >> $tmpfile
 45 |         cat $tmpfile
 46 |         command rm -f $tmpfile
 47 |     else
 48 |         echo 'usage: transfer FILE_TO_TRANSFER'
 49 |     end
 50 | end
 51 | 
 52 | funcsave transfer
 53 | ```
 54 | 
 55 | ### Using wget
 56 | ```fish
 57 | function transfer --description 'Upload a file to transfer.sh'
 58 |     if [ $argv[1] ]
 59 |         wget -t 1 -qO - --method=PUT --body-file="$argv[1]" --header="Content-Type: (file -b --mime-type $argv[1])" https://transfer.sh/(basename $argv[1])
 60 |     else
 61 |         echo 'usage: transfer FILE_TO_TRANSFER'
 62 |     end
 63 | end
 64 | 
 65 | funcsave transfer
 66 | ```
 67 | 
 68 | Now run it like this:
 69 | ```bash
 70 | $ transfer test.txt
 71 | ```
 72 | 
 73 | ## Add alias on Windows
 74 | 
 75 | Put a file called `transfer.cmd` somewhere in your PATH with this inside it:
 76 | ```cmd
 77 | @echo off
 78 | setlocal
 79 | :: use env vars to pass names to PS, to avoid escaping issues
 80 | set FN=%~nx1
 81 | set FULL=%1
 82 | powershell -noprofile -command "$(Invoke-Webrequest -Method put -Infile $Env:FULL https://transfer.sh/$Env:FN).Content"
 83 | ```
 84 | 
 85 | ## Uploading and Downloading
 86 | <a name="uploading-and-downloading"/>
 87 | 
 88 | ### Uploading with wget
 89 | ```bash
 90 | $ wget --method PUT --body-file=/tmp/file.tar https://transfer.sh/file.tar -O - -nv 
 91 | ```
 92 | 
 93 | ### Uploading with PowerShell
 94 | ```posh
 95 | PS H:\> invoke-webrequest -method put -infile .\file.txt https://transfer.sh/file.txt 
 96 | ```
 97 | 
 98 | ### Upload using HTTPie
 99 | ```bash
100 | $ http https://transfer.sh/ -vv < /tmp/test.log 
101 | ```
102 | 
103 | ### Uploading a filtered text file
104 | ```bash
105 | $ grep 'pound' /var/log/syslog | curl --upload-file - https://transfer.sh/pound.log 
106 | ```
107 | 
108 | ### Downloading with curl
109 | ```bash
110 | $ curl https://transfer.sh/1lDau/test.txt -o test.txt
111 | ```
112 | 
113 | ### Downloading with wget
114 | ```bash
115 | $ wget https://transfer.sh/1lDau/test.txt
116 | ```
117 | 
118 | ## Archiving and backups
119 | <a name="archiving-and-backups"/>
120 | 
121 | ### Backup, encrypt and transfer a MySQL dump
122 | ```bash
123 | $ mysqldump --all-databases | gzip | gpg -ac -o- | curl -X PUT --upload-file "-" https://transfer.sh/test.txt
124 | ```
125 | 
126 | ### Archive and upload directory
127 | ```bash
128 | $ tar -czf - /var/log/journal | curl --upload-file - https://transfer.sh/journal.tar.gz
129 | ```
130 | 
131 | ### Uploading multiple files at once
132 | ```bash
133 | $ curl -i -F filedata=@/tmp/hello.txt -F filedata=@/tmp/hello2.txt https://transfer.sh/
134 | ```
135 | 
136 | ### Combining downloads as zip or tar.gz archive
137 | ```bash
138 | $ curl https://transfer.sh/(15HKz/hello.txt,15HKz/hello.txt).tar.gz
139 | $ curl https://transfer.sh/(15HKz/hello.txt,15HKz/hello.txt).zip 
140 | ```
141 | 
142 | ### Transfer and send email with link (using an alias)
143 | ```bash
144 | $ transfer /tmp/hello.txt | mail -s "Hello World" user@yourmaildomain.com 
145 | ```
146 | ## Encrypting and decrypting
147 | <a name="encrypting-and-decrypting"/>
148 | 
149 | ### Encrypting files with password using gpg
150 | ```bash
151 | $ gpg --armor --symmetric --output - /tmp/hello.txt | curl --upload-file - https://transfer.sh/test.txt
152 | ```
153 | 
154 | ### Downloading and decrypting
155 | ```bash
156 | $ curl https://transfer.sh/1lDau/test.txt | gpg --decrypt --output /tmp/hello.txt
157 | ```
158 | 
159 | ### Import keys from [keybase](https://keybase.io/)
160 | ```bash
161 | $ keybase track [them] # Encrypt for recipient(s)
162 | $ cat somebackupfile.tar.gz | keybase encrypt [them] | curl --upload-file '-' https://transfer.sh/test.txt # Decrypt
163 | $ curl https://transfer.sh/sqUFi/test.md | keybase decrypt
164 | ```
165 | 
166 | ## Scanning for viruses
167 | <a name="scanning-for-viruses"/>
168 | 
169 | ### Scan for malware or viruses using Clamav
170 | ```bash
171 | $ wget http://www.eicar.org/download/eicar.com
172 | $ curl -X PUT --upload-file ./eicar.com https://transfer.sh/eicar.com/scan
173 | ```
174 | 
175 | ### Upload malware to VirusTotal, get a permalink in return
176 | ```bash
177 | $ curl -X PUT --upload-file nhgbhhj https://transfer.sh/test.txt/virustotal 
178 | ```
179 | 
180 | ### Upload encrypted password protected files
181 | 
182 | By default files upload for only 1 download, you can specify download limit using -D flag like `transfer-encrypted -D 50 %file/folder%`
183 | 
184 | #### One line for bashrc
185 | ```bash
186 | transfer-encrypted() { if [ $# -eq 0 ]; then echo "No arguments specified.\nUsage:\n transfer <file|directory>\n ... | transfer <file_name>" >&2; return 1; fi; while getopts ":D:" opt; do case $opt in D) max_downloads=$OPTARG;; \?) echo "Invalid option: -$OPTARG" >&2;; esac; done; shift "$((OPTIND - 1))"; file="$1"; file_name=$(basename "$file"); if [ ! -e "$file" ]; then echo "$file: No such file or directory" >&2; return 1; fi; if [ -d "$file" ]; then file_name="$file_name.zip"; (cd "$file" && zip -r -q - .) | openssl aes-256-cbc -pbkdf2 -e > "tmp-$file_name" && cat "tmp-$file_name" | curl -H "Max-Downloads: $max_downloads" -w '\n' --upload-file "tmp-$file_name" "https://transfer.sh/$file_name" | tee /dev/null; rm "tmp-$file_name"; else cat "$file" | openssl aes-256-cbc -pbkdf2 -e > "tmp-$file" && cat "tmp-$file" | curl -H "Max-Downloads: $max_downloads" -w '\n' --upload-file - "https://transfer.sh/$file_name" | tee /dev/null; rm "tmp-$file"; fi; }
187 | ```
188 | #### Human readable code 
189 | ```bash
190 | transfer-encrypted() {
191 |     if [ $# -eq 0 ]; then
192 |         echo "No arguments specified.\nUsage:\n transfer <file|directory>\n ... | transfer <file_name>" >&2
193 |         return 1
194 |     fi
195 | 
196 |     while getopts ":D:" opt; do
197 |         case $opt in
198 |             D)
199 |                 max_downloads=$OPTARG
200 |                 ;;
201 |             \?)
202 |                 echo "Invalid option: -$OPTARG" >&2
203 |                 ;;
204 |         esac
205 |     done
206 | 
207 |     shift "$((OPTIND - 1))"
208 |     file="$1"
209 |     file_name=$(basename "$file")
210 | 
211 |     if [ ! -e "$file" ]; then
212 |         echo "$file: No such file or directory" >&2
213 |         return 1
214 |     fi
215 | 
216 |     if [ -d "$file" ]; then
217 |         file_name="$file_name.zip"
218 |         (cd "$file" && zip -r -q - .) | openssl aes-256-cbc -pbkdf2 -e > "tmp-$file_name" && cat "tmp-$file_name" | curl -H "Max-Downloads: $max_downloads" -w '\n' --upload-file "tmp-$file_name" "https://transfer.sh/$file_name" | tee /dev/null
219 |         rm "tmp-$file_name"
220 |     else
221 |         cat "$file" | openssl aes-256-cbc -pbkdf2 -e > "tmp-$file" && cat "tmp-$file" | curl -H "Max-Downloads: $max_downloads" -w '\n' --upload-file - "https://transfer.sh/$file_name" | tee /dev/null
222 |         rm "tmp-$file"
223 |     fi
224 | }
225 | ```
226 | #### Decrypt using
227 | ```bash
228 | curl -s https://transfer.sh/some/file | openssl aes-256-cbc -pbkdf2 -d > output_filename
229 | ```
230 | 
231 | ## Uploading and copy download command
232 | 
233 | Download commands can be automatically copied to the clipboard after files are uploaded using transfer.sh.
234 | 
235 | It was designed for Linux or macOS.
236 | 
237 | ### 1. Install xclip or xsel for Linux, macOS skips this step
238 | 
239 | - install xclip see https://command-not-found.com/xclip
240 | 
241 | - install xsel  see https://command-not-found.com/xsel
242 | 
243 | Install later, add pbcopy and pbpaste to .bashrc or .zshrc or its equivalent.
244 | 
245 | - If use xclip, paste the following lines:
246 | 
247 | ```sh
248 | alias pbcopy='xclip -selection clipboard'
249 | alias pbpaste='xclip -selection clipboard -o'
250 | ```
251 | 
252 | - If use xsel, paste the following lines:
253 | 
254 | ```sh
255 | alias pbcopy='xsel --clipboard --input'
256 | alias pbpaste='xsel --clipboard --output'
257 | ```
258 | 
259 | ### 2. Add Uploading and copy download command shell function
260 | 
261 | 1. Open .bashrc or .zshrc  or its equivalent.
262 | 
263 | 2. Add the following shell script:
264 | 
265 |    ```sh
266 |    transfer() {
267 |      curl --progress-bar --upload-file "$1" https://transfer.sh/$(basename "$1") | pbcopy;
268 |      echo "1) Download link:"
269 |      echo "$(pbpaste)"
270 |    
271 |      echo "\n2) Linux or macOS download command:"
272 |      linux_macos_download_command="wget $(pbpaste)"
273 |      echo $linux_macos_download_command
274 |    
275 |      echo "\n3) Windows download command:"
276 |      windows_download_command="Invoke-WebRequest -Uri "$(pbpaste)" -OutFile $(basename $1)"
277 |      echo $windows_download_command
278 |    
279 |      case $2 in
280 |        l|m)  echo $linux_macos_download_command | pbcopy
281 |        ;;
282 |        w)  echo $windows_download_command | pbcopy
283 |        ;;
284 |      esac
285 |    }
286 |    ```
287 | 
288 | 
289 | ### 3. Test
290 | 
291 | The transfer command has two parameters:
292 | 
293 | 1. The first parameter is the path to upload the file.
294 | 
295 | 2. The second parameter indicates which system's download command is copied. optional:
296 | 
297 |    - This parameter is empty to copy the download link.
298 | 
299 |    - `l` or `m` copy the Linux or macOS command that downloaded the file.
300 | 
301 |    -  `w` copy the Windows command that downloaded the file.
302 | 
303 | For example, The command to download the file on Windows will be copied:
304 | 
305 | ```sh
306 | $ transfer ~/temp/a.log w
307 | ######################################################################## 100.0%
308 | 1) Download link:
309 | https://transfer.sh/y0qr2c/a.log
310 | 
311 | 2) Linux or macOS download command:
312 | wget https://transfer.sh/y0qr2c/a.log
313 | 
314 | 3) Windows download command:
315 | Invoke-WebRequest -Uri https://transfer.sh/y0qr2c/a.log -OutFile a.log
316 | ```
317 | ## Uploading and displaying URL and deletion token
318 | ```bash
319 | # tempfile
320 | URLFILE=$HOME/temp/transfersh.url
321 | # insert number of downloads and days saved
322 | if [ -f $1 ]; then
323 | read -p "Allowed number of downloads: " num_down
324 | read -p "Number of days on server: " num_save
325 | # transfer
326 | curl -sD - -H "Max-Downloads: $num_down" -H "Max-Days: $num_save"--progress-bar --upload-file $1 https://transfer.sh/$(basename $1) | grep -i -E 'transfer\.sh|x-url-delete' &> $URLFILE
327 | # display URL and deletion token
328 | if [ -f $URLFILE ]; then
329 | URL=$(tail -n1 $URLFILE)
330 | TOKEN=$(grep delete $URLFILE | awk -F "/" '{print $NF}')
331 | echo "*********************************"
332 | echo "Data is saved in $URLFILE"
333 | echo "**********************************"
334 | echo "URL is: $URL"
335 | echo "Deletion Token is: $TOKEN"
336 | echo "**********************************"
337 | else
338 | echo "NO URL-File found !!"
339 | fi
340 | else
341 | echo "!!!!!!"
342 | echo "\"$1\" not found !!"
343 | echo "!!!!!!"
344 | fi
345 | ```
346 | 


--------------------------------------------------------------------------------
/extras/clamd:
--------------------------------------------------------------------------------
  1 | #! /bin/sh
  2 | ### BEGIN INIT INFO
  3 | # Provides:          skeleton
  4 | # Required-Start:    $remote_fs $syslog
  5 | # Required-Stop:     $remote_fs $syslog
  6 | # Default-Start:     2 3 4 5
  7 | # Default-Stop:      0 1 6
  8 | # Short-Description: Example initscript
  9 | # Description:       This file should be used to construct scripts to be
 10 | #                    placed in /etc/init.d.
 11 | ### END INIT INFO
 12 | 
 13 | # Author: Foo Bar <foobar@baz.org>
 14 | #
 15 | # Please remove the "Author" lines above and replace them
 16 | # with your own name if you copy and modify this script.
 17 | 
 18 | # Do NOT "set -e"
 19 | 
 20 | # PATH should only include /usr/* if it runs after the mountnfs.sh script
 21 | PATH=/sbin:/usr/sbin:/bin:/usr/bin
 22 | DESC="Clam Daemon"
 23 | NAME=clamd
 24 | DAEMON="/usr/local/sbin/clamd"
 25 | DAEMON_ARGS="-c /usr/local/etc/clamd.conf"
 26 | PIDFILE=/var/run/$NAME.pid
 27 | SCRIPTNAME=/etc/init.d/$NAME
 28 | 
 29 | # Exit if the package is not installed
 30 | [ -x "$DAEMON" ] || exit 0
 31 | 
 32 | # Read configuration variable file if it is present
 33 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME
 34 | 
 35 | # Load the VERBOSE setting and other rcS variables
 36 | . /lib/init/vars.sh
 37 | 
 38 | # Define LSB log_* functions.
 39 | # Depend on lsb-base (>= 3.2-14) to ensure that this file is present
 40 | # and status_of_proc is working.
 41 | . /lib/lsb/init-functions
 42 | 
 43 | #
 44 | # Function that starts the daemon/service
 45 | #
 46 | do_start()
 47 | {
 48 | 	# Return
 49 | 	#   0 if daemon has been started
 50 | 	#   1 if daemon was already running
 51 | 	#   2 if daemon could not be started
 52 | 	start-stop-daemon --background --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
 53 | 		|| return 1
 54 | 	start-stop-daemon --background --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
 55 | 		$DAEMON_ARGS \
 56 | 		|| return 2
 57 | 	# Add code here, if necessary, that waits for the process to be ready
 58 | 	# to handle requests from services started subsequently which depend
 59 | 	# on this one.  As a last resort, sleep for some time.
 60 | }
 61 | 
 62 | #
 63 | # Function that stops the daemon/service
 64 | #
 65 | do_stop()
 66 | {
 67 | 	# Return
 68 | 	#   0 if daemon has been stopped
 69 | 	#   1 if daemon was already stopped
 70 | 	#   2 if daemon could not be stopped
 71 | 	#   other if a failure occurred
 72 | 	start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
 73 | 	RETVAL="$?"
 74 | 	[ "$RETVAL" = 2 ] && return 2
 75 | 	# Wait for children to finish too if this is a daemon that forks
 76 | 	# and if the daemon is only ever run from this initscript.
 77 | 	# If the above conditions are not satisfied then add some other code
 78 | 	# that waits for the process to drop all resources that could be
 79 | 	# needed by services started subsequently.  A last resort is to
 80 | 	# sleep for some time.
 81 | 	start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
 82 | 	[ "$?" = 2 ] && return 2
 83 | 	# Many daemons don't delete their pidfiles when they exit.
 84 | 	rm -f $PIDFILE
 85 | 	return "$RETVAL"
 86 | }
 87 | 
 88 | #
 89 | # Function that sends a SIGHUP to the daemon/service
 90 | #
 91 | do_reload() {
 92 | 	#
 93 | 	# If the daemon can reload its configuration without
 94 | 	# restarting (for example, when it is sent a SIGHUP),
 95 | 	# then implement that here.
 96 | 	#
 97 | 	start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
 98 | 	return 0
 99 | }
100 | 
101 | case "$1" in
102 |   start)
103 | 	[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
104 | 	do_start
105 | 	case "$?" in
106 | 		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
107 | 		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
108 | 	esac
109 | 	;;
110 |   stop)
111 | 	[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
112 | 	do_stop
113 | 	case "$?" in
114 | 		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
115 | 		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
116 | 	esac
117 | 	;;
118 |   status)
119 | 	status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
120 | 	;;
121 |   #reload|force-reload)
122 | 	#
123 | 	# If do_reload() is not implemented then leave this commented out
124 | 	# and leave 'force-reload' as an alias for 'restart'.
125 | 	#
126 | 	#log_daemon_msg "Reloading $DESC" "$NAME"
127 | 	#do_reload
128 | 	#log_end_msg $?
129 | 	#;;
130 |   restart|force-reload)
131 | 	#
132 | 	# If the "reload" option is implemented then remove the
133 | 	# 'force-reload' alias
134 | 	#
135 | 	log_daemon_msg "Restarting $DESC" "$NAME"
136 | 	do_stop
137 | 	case "$?" in
138 | 	  0|1)
139 | 		do_start
140 | 		case "$?" in
141 | 			0) log_end_msg 0 ;;
142 | 			1) log_end_msg 1 ;; # Old process is still running
143 | 			*) log_end_msg 1 ;; # Failed to start
144 | 		esac
145 | 		;;
146 | 	  *)
147 | 		# Failed to stop
148 | 		log_end_msg 1
149 | 		;;
150 | 	esac
151 | 	;;
152 |   *)
153 | 	#echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
154 | 	echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
155 | 	exit 3
156 | 	;;
157 | esac
158 | 
159 | :
160 | 


--------------------------------------------------------------------------------
/extras/transfersh:
--------------------------------------------------------------------------------
  1 | #! /bin/sh
  2 | ### BEGIN INIT INFO
  3 | # Provides:          skeleton
  4 | # Required-Start:    $remote_fs $syslog
  5 | # Required-Stop:     $remote_fs $syslog
  6 | # Default-Start:     2 3 4 5
  7 | # Default-Stop:      0 1 6
  8 | # Short-Description: Example initscript
  9 | # Description:       This file should be used to construct scripts to be
 10 | #                    placed in /etc/init.d.
 11 | ### END INIT INFO
 12 | 
 13 | # Author: Foo Bar <foobar@baz.org>
 14 | #
 15 | # Please remove the "Author" lines above and replace them
 16 | # with your own name if you copy and modify this script.
 17 | 
 18 | # Do NOT "set -e"
 19 | 
 20 | # PATH should only include /usr/* if it runs after the mountnfs.sh script
 21 | PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/go/bin
 22 | DESC="Transfersh Web server"
 23 | NAME=transfersh
 24 | DAEMON="/opt/transfer.sh/main"
 25 | DAEMON_ARGS="--port 80 --temp /tmp/"
 26 | PIDFILE=/var/run/$NAME.pid
 27 | SCRIPTNAME=/etc/init.d/$NAME
 28 | 
 29 | export BUCKET={bucket}
 30 | export AWS_ACCESS_KEY={aws_access_key}
 31 | export AWS_SECRET_KEY={aws_secret_key}
 32 | export VIRUSTOTAL_KEY={virustotal_key}
 33 | export GOPATH=/opt/go/
 34 | 
 35 | # Exit if the package is not installed
 36 | [ -x "$DAEMON" ] || exit 0
 37 | 
 38 | # Read configuration variable file if it is present
 39 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME
 40 | 
 41 | # Load the VERBOSE setting and other rcS variables
 42 | . /lib/init/vars.sh
 43 | 
 44 | # Define LSB log_* functions.
 45 | # Depend on lsb-base (>= 3.2-14) to ensure that this file is present
 46 | # and status_of_proc is working.
 47 | . /lib/lsb/init-functions
 48 | 
 49 | #
 50 | # Function that starts the daemon/service
 51 | #
 52 | do_start()
 53 | {
 54 | 	# Return
 55 | 	#   0 if daemon has been started
 56 | 	#   1 if daemon was already running
 57 | 	#   2 if daemon could not be started
 58 | 	start-stop-daemon --background --start --chdir /opt/transfer.sh --quiet --pidfile $PIDFILE --make-pidfile --exec $DAEMON --test > /dev/null \
 59 | 		|| return 1
 60 | 	start-stop-daemon --background --start --chdir /opt/transfer.sh --quiet --pidfile $PIDFILE --make-pidfile --exec $DAEMON -- \
 61 | 		$DAEMON_ARGS \
 62 | 		|| return 2
 63 | 	# Add code here, if necessary, that waits for the process to be ready
 64 | 	# to handle requests from services started subsequently which depend
 65 | 	# on this one.  As a last resort, sleep for some time.
 66 | }
 67 | 
 68 | #
 69 | # Function that stops the daemon/service
 70 | #
 71 | do_stop()
 72 | {
 73 | 	# Return
 74 | 	#   0 if daemon has been stopped
 75 | 	#   1 if daemon was already stopped
 76 | 	#   2 if daemon could not be stopped
 77 | 	#   other if a failure occurred
 78 | 	start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
 79 | 	RETVAL="$?"
 80 | 	[ "$RETVAL" = 2 ] && return 2
 81 | 	# Wait for children to finish too if this is a daemon that forks
 82 | 	# and if the daemon is only ever run from this initscript.
 83 | 	# If the above conditions are not satisfied then add some other code
 84 | 	# that waits for the process to drop all resources that could be
 85 | 	# needed by services started subsequently.  A last resort is to
 86 | 	# sleep for some time.
 87 | 	start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
 88 | 	[ "$?" = 2 ] && return 2
 89 | 	# Many daemons don't delete their pidfiles when they exit.
 90 | 	rm -f $PIDFILE
 91 | 	return "$RETVAL"
 92 | }
 93 | 
 94 | #
 95 | # Function that sends a SIGHUP to the daemon/service
 96 | #
 97 | do_reload() {
 98 | 	#
 99 | 	# If the daemon can reload its configuration without
100 | 	# restarting (for example, when it is sent a SIGHUP),
101 | 	# then implement that here.
102 | 	#
103 | 	start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
104 | 	return 0
105 | }
106 | 
107 | case "$1" in
108 |   start)
109 | 	[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
110 | 	do_start
111 | 	case "$?" in
112 | 		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
113 | 		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
114 | 	esac
115 | 	;;
116 |   stop)
117 | 	[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
118 | 	do_stop
119 | 	case "$?" in
120 | 		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
121 | 		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
122 | 	esac
123 | 	;;
124 |   status)
125 | 	status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
126 | 	;;
127 |   #reload|force-reload)
128 | 	#
129 | 	# If do_reload() is not implemented then leave this commented out
130 | 	# and leave 'force-reload' as an alias for 'restart'.
131 | 	#
132 | 	#log_daemon_msg "Reloading $DESC" "$NAME"
133 | 	#do_reload
134 | 	#log_end_msg $?
135 | 	#;;
136 |   restart|force-reload)
137 | 	#
138 | 	# If the "reload" option is implemented then remove the
139 | 	# 'force-reload' alias
140 | 	#
141 | 	log_daemon_msg "Restarting $DESC" "$NAME"
142 | 	do_stop
143 | 	case "$?" in
144 | 	  0|1)
145 | 		do_start
146 | 		case "$?" in
147 | 			0) log_end_msg 0 ;;
148 | 			1) log_end_msg 1 ;; # Old process is still running
149 | 			*) log_end_msg 1 ;; # Failed to start
150 | 		esac
151 | 		;;
152 | 	  *)
153 | 		# Failed to stop
154 | 		log_end_msg 1
155 | 		;;
156 | 	esac
157 | 	;;
158 |   *)
159 | 	#echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
160 | 	echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
161 | 	exit 3
162 | 	;;
163 | esac
164 | 
165 | :
166 | 


--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
 1 | {
 2 |   "nodes": {
 3 |     "flake-utils": {
 4 |       "locked": {
 5 |         "lastModified": 1631561581,
 6 |         "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=",
 7 |         "owner": "numtide",
 8 |         "repo": "flake-utils",
 9 |         "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19",
10 |         "type": "github"
11 |       },
12 |       "original": {
13 |         "owner": "numtide",
14 |         "repo": "flake-utils",
15 |         "type": "github"
16 |       }
17 |     },
18 |     "nixpkgs": {
19 |       "locked": {
20 |         "lastModified": 1632470817,
21 |         "narHash": "sha256-tGyOesdpqQEVqlmVeElsC98OJ2GDy+LNaCThSby/GQM=",
22 |         "owner": "NixOS",
23 |         "repo": "nixpkgs",
24 |         "rev": "39e8ec2db68b863543bd377e44fbe02f8d05864e",
25 |         "type": "github"
26 |       },
27 |       "original": {
28 |         "id": "nixpkgs",
29 |         "type": "indirect"
30 |       }
31 |     },
32 |     "root": {
33 |       "inputs": {
34 |         "flake-utils": "flake-utils",
35 |         "nixpkgs": "nixpkgs"
36 |       }
37 |     }
38 |   },
39 |   "root": "root",
40 |   "version": 7
41 | }
42 | 


--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
  1 | {
  2 |   description = "Transfer.sh";
  3 | 
  4 |   inputs.flake-utils.url = "github:numtide/flake-utils";
  5 | 
  6 |   outputs = { self, nixpkgs, flake-utils }:
  7 |     let
  8 |       transfer-sh = pkgs: pkgs.buildGoModule {
  9 |         src = self;
 10 |         name = "transfer.sh";
 11 |         vendorSha256 = "sha256-bgQUMiC33yVorcKOWhegT1/YU+fvxsz2pkeRvjf3R7g=";
 12 |       };
 13 |     in
 14 | 
 15 |       flake-utils.lib.eachDefaultSystem (
 16 |         system:
 17 |           let
 18 |             pkgs = nixpkgs.legacyPackages.${system};
 19 |           in
 20 |             rec {
 21 |               packages = flake-utils.lib.flattenTree {
 22 |                 transfer-sh = transfer-sh pkgs;
 23 |               };
 24 |               defaultPackage = packages.transfer-sh;
 25 |               apps.transfer-sh = flake-utils.lib.mkApp { drv = packages.transfer-sh; };
 26 |               defaultApp = apps.transfer-sh;
 27 |             }
 28 |       ) // rec {
 29 | 
 30 |         nixosModules = {
 31 |           transfer-sh = { config, lib, pkgs, ... }: with lib; let
 32 |             RUNTIME_DIR = "/var/lib/transfer.sh";
 33 |             cfg = config.services.transfer-sh;
 34 | 
 35 |             general_options = {
 36 | 
 37 |               enable = mkEnableOption "Transfer.sh service";
 38 |               listener = mkOption { default = 80; type = types.int; description = "port to use for http (:80)"; };
 39 |               profile-listener = mkOption { default = 6060; type = types.int; description = "port to use for profiler (:6060)"; };
 40 |               force-https = mkOption { type = types.nullOr types.bool; description = "redirect to https"; };
 41 |               tls-listener = mkOption { default = 443; type = types.int; description = "port to use for https (:443)"; };
 42 |               tls-listener-only = mkOption { type = types.nullOr types.bool; description = "flag to enable tls listener only"; };
 43 |               tls-cert-file = mkOption { type = types.nullOr types.str; description = "path to tls certificate"; };
 44 |               tls-private-key = mkOption { type = types.nullOr types.str; description = "path to tls private key "; };
 45 |               http-auth-user = mkOption { type = types.nullOr types.str; description = "user for basic http auth on upload"; };
 46 |               http-auth-pass = mkOption { type = types.nullOr types.str; description = "pass for basic http auth on upload"; };
 47 |               http-auth-htpasswd = mkOption { type = types.nullOr types.str; description = "htpasswd file path for basic http auth on upload"; };
 48 |               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"; };
 49 |               ip-whitelist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips allowed to connect to the service"; };
 50 |               ip-blacklist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips not allowed to connect to the service"; };
 51 |               temp-path = mkOption { type = types.nullOr types.str; description = "path to temp folder"; };
 52 |               web-path = mkOption { type = types.nullOr types.str; description = "path to static web files (for development or custom front end)"; };
 53 |               proxy-path = mkOption { type = types.nullOr types.str; description = "path prefix when service is run behind a proxy"; };
 54 |               proxy-port = mkOption { type = types.nullOr types.str; description = "port of the proxy when the service is run behind a proxy"; };
 55 |               ga-key = mkOption { type = types.nullOr types.str; description = "google analytics key for the front end"; };
 56 |               email-contact = mkOption { type = types.nullOr types.str; description = "email contact for the front end"; };
 57 |               uservoice-key = mkOption { type = types.nullOr types.str; description = "user voice key for the front end"; };
 58 |               lets-encrypt-hosts = mkOption { type = types.nullOr (types.listOf types.str); description = "hosts to use for lets encrypt certificates"; };
 59 |               log = mkOption { type = types.nullOr types.str; description = "path to log file"; };
 60 |               cors-domains = mkOption { type = types.nullOr (types.listOf types.str); description = "comma separated list of domains for CORS, setting it enable CORS "; };
 61 |               clamav-host = mkOption { type = types.nullOr types.str; description = "host for clamav feature"; };
 62 |               rate-limit = mkOption { type = types.nullOr types.int; description = "request per minute"; };
 63 |               max-upload-size = mkOption { type = types.nullOr types.int; description = "max upload size in kilobytes  "; };
 64 |               purge-days = mkOption { type = types.nullOr types.int; description = "number of days after the uploads are purged automatically "; };
 65 |               random-token-length = mkOption { type = types.nullOr types.int; description = "length of the random token for the upload path (double the size for delete path)"; };
 66 | 
 67 |             };
 68 | 
 69 |             provider_options = {
 70 | 
 71 |                 aws = {
 72 |                   enable = mkEnableOption "Enable AWS backend";
 73 |                   aws-access-key = mkOption { type = types.str; description = "aws access key"; };
 74 |                   aws-secret-key = mkOption { type = types.str; description = "aws secret key"; };
 75 |                   bucket = mkOption { type = types.str; description = "aws bucket "; };
 76 |                   s3-endpoint = mkOption {
 77 |                     type = types.nullOr types.str;
 78 |                     description = ''
 79 |                       Custom S3 endpoint. 
 80 |                       If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically.
 81 |                     '';
 82 |                   };
 83 |                   s3-region = mkOption { type = types.str; description = "region of the s3 bucket eu-west-"; };
 84 |                   s3-no-multipart = mkOption { type = types.nullOr types.bool; description = "disables s3 multipart upload "; };
 85 |                   s3-path-style = mkOption { type = types.nullOr types.str; description = "Forces path style URLs, required for Minio. "; };
 86 |                 };
 87 | 
 88 |                 storj = {
 89 |                   enable = mkEnableOption "Enable storj backend";
 90 |                   storj-access = mkOption { type = types.str; description = "Access for the project"; };
 91 |                   storj-bucket = mkOption { type = types.str; description = "Bucket to use within the project"; };
 92 |                 };
 93 | 
 94 |                 gdrive = {
 95 |                   enable = mkEnableOption "Enable gdrive backend";
 96 |                   gdrive-client-json = mkOption { type = types.str; description = "oauth client json config for gdrive provider"; };
 97 |                   gdrive-chunk-size = mkOption { default = 8; type = types.nullOr types.int; description = "chunk size for gdrive upload in megabytes, must be lower than available memory (8 MB)"; };
 98 |                   basedir = mkOption { type = types.str; description = "path storage for gdrive provider"; default = "${cfg.stateDir}/store"; };
 99 |                   purge-interval = mkOption { type = types.nullOr types.int; description = "interval in hours to run the automatic purge for (not applicable to S3 and Storj)"; };
100 | 
101 |                 };
102 | 
103 |                 local = {
104 |                   enable = mkEnableOption "Enable local backend";
105 |                   basedir = mkOption { type = types.str; description = "path storage for local provider"; default = "${cfg.stateDir}/store"; };
106 |                   purge-interval = mkOption { type = types.nullOr types.int; description = "interval in hours to run the automatic purge for (not applicable to S3 and Storj)"; };
107 |                 };
108 | 
109 |               };
110 |           in
111 |             {
112 |               options.services.transfer-sh = fold recursiveUpdate {} [
113 |                 general_options
114 |                 {
115 |                   provider = provider_options;
116 |                   user = mkOption {
117 |                     type = types.str;
118 |                     description = "User to run the service under";
119 |                     default = "transfer.sh";
120 |                   };
121 |                   group = mkOption {
122 |                     type = types.str;
123 |                     description = "Group to run the service under";
124 |                     default = "transfer.sh";
125 |                   };
126 |                   stateDir = mkOption {
127 |                     type = types.path;
128 |                     description = "Variable state directory";
129 |                     default = RUNTIME_DIR;
130 |                   };
131 |                 }
132 |               ];
133 | 
134 |               config = let
135 | 
136 |                 mkFlags = cfg: options:
137 |                   let
138 |                     mkBoolFlag = option: if cfg.${option} then [ "--${option}" ] else [];
139 |                     mkFlag = option:
140 |                       if isBool cfg.${option}
141 |                       then mkBoolFlag option
142 |                       else [ "--${option}" "${cfg.${option}}" ];
143 | 
144 |                   in
145 |                     lists.flatten (map (mkFlag) (filter (option: cfg.${option} != null && option != "enable") options));
146 | 
147 |                 aws-config = (mkFlags cfg.provider.aws (attrNames provider_options)) ++ [ "--provider" "aws" ];
148 |                 gdrive-config = mkFlags cfg.provider.gdrive (attrNames provider_options.gdrive) ++ [ "--provider" "gdrive" ];
149 |                 storj-config = mkFlags cfg.provider.storj (attrNames provider_options.storj) ++ [ "--provider" "storj" ];
150 |                 local-config = mkFlags cfg.provider.local (attrNames provider_options.local) ++ [ "--provider" "local" ];
151 | 
152 |                 general-config = concatStringsSep " " (mkFlags cfg (attrNames general_options));
153 |                 provider-config = concatStringsSep " " (
154 |                   if cfg.provider.aws.enable && !cfg.provider.storj.enable && !cfg.provider.gdrive.enable && !cfg.provider.local.enable then aws-config
155 |                   else if !cfg.provider.aws.enable && cfg.provider.storj.enable && !cfg.provider.gdrive.enable && !cfg.provider.local.enable then storj-config
156 |                   else if !cfg.provider.aws.enable && !cfg.provider.storj.enable && cfg.provider.gdrive.enable && !cfg.provider.local.enable then gdrive-config
157 |                   else if !cfg.provider.aws.enable && !cfg.provider.storj.enable && !cfg.provider.gdrive.enable && cfg.provider.local.enable then local-config
158 |                   else throw "transfer.sh requires exactly one provider (aws, storj, gdrive, local)"
159 |                 );
160 | 
161 |               in
162 |                 lib.mkIf cfg.enable
163 |                   {
164 |                     systemd.tmpfiles.rules = [
165 |                       "d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -"
166 |                     ] ++ optional cfg.provider.gdrive.enable cfg.provider.gdrive.basedir
167 |                     ++ optional cfg.provider.local.enable cfg.provider.local.basedir;
168 | 
169 |                     systemd.services.transfer-sh = {
170 |                       wantedBy = [ "multi-user.target" ];
171 |                       after = [ "network.target" ];
172 |                       serviceConfig = {
173 |                         User = cfg.user;
174 |                         Group = cfg.group;
175 |                         ExecStart = "${transfer-sh pkgs}/bin/transfer.sh ${general-config} ${provider-config} ";
176 |                       };
177 |                     };
178 | 
179 |                     networking.firewall.allowedTCPPorts = [ cfg.listener cfg.profile-listener cfg.tls-listener ];
180 |                   };
181 |             };
182 | 
183 |           default = { self, pkgs, ... }: {
184 |             imports = [ nixosModules.transfer-sh ];
185 |             # Network configuration.
186 | 
187 |             # useDHCP is generally considered to better be turned off in favor
188 |             # of <adapter>.useDHCP
189 |             networking.useDHCP = false;
190 |             networking.firewall.allowedTCPPorts = [];
191 | 
192 |             # Enable the inventaire server.
193 |             services.transfer-sh = {
194 |               enable = true;
195 |               provider.local = {
196 |                 enable = true;
197 |               };
198 |             };
199 | 
200 |             nixpkgs.config.allowUnfree = true;
201 |           };
202 |         };
203 | 
204 | 
205 |         nixosConfigurations."container" = nixpkgs.lib.nixosSystem {
206 |           system = "x86_64-linux";
207 |           modules = [
208 |             nixosModules.default
209 |             ({ ... }: { boot.isContainer = true; })
210 |           ];
211 |         };
212 | 
213 |       };
214 | }
215 | 


--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
 1 | module github.com/dutchcoders/transfer.sh
 2 | 
 3 | go 1.18
 4 | 
 5 | require (
 6 | 	github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8
 7 | 	github.com/ProtonMail/gopenpgp/v2 v2.5.2
 8 | 	github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14
 9 | 	github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2
10 | 	github.com/aws/aws-sdk-go-v2 v1.18.0
11 | 	github.com/aws/aws-sdk-go-v2/config v1.18.25
12 | 	github.com/aws/aws-sdk-go-v2/credentials v1.13.24
13 | 	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67
14 | 	github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
15 | 	github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
16 | 	github.com/dutchcoders/go-virustotal v0.0.0-20140923143438-24cc8e6fa329
17 | 	github.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6
18 | 	github.com/elazarl/go-bindata-assetfs v1.0.1
19 | 	github.com/fatih/color v1.14.1
20 | 	github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
21 | 	github.com/gorilla/handlers v1.5.1
22 | 	github.com/gorilla/mux v1.8.0
23 | 	github.com/microcosm-cc/bluemonday v1.0.23
24 | 	github.com/russross/blackfriday/v2 v2.1.0
25 | 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
26 | 	github.com/tg123/go-htpasswd v1.2.1
27 | 	github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
28 | 	github.com/urfave/cli/v2 v2.25.3
29 | 	golang.org/x/crypto v0.21.0
30 | 	golang.org/x/net v0.23.0
31 | 	golang.org/x/oauth2 v0.7.0
32 | 	golang.org/x/text v0.14.0
33 | 	google.golang.org/api v0.114.0
34 | 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
35 | 	storj.io/common v0.0.0-20230301105927-7f966760c100
36 | 	storj.io/uplink v1.10.0
37 | )
38 | 
39 | require (
40 | 	cloud.google.com/go/compute v1.19.1 // indirect
41 | 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
42 | 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
43 | 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
44 | 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect
45 | 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
46 | 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
47 | 	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
48 | 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
49 | 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
50 | 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
51 | 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
52 | 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
53 | 	github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
54 | 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
55 | 	github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect
56 | 	github.com/aws/smithy-go v1.13.5 // indirect
57 | 	github.com/aymerick/douceur v0.2.0 // indirect
58 | 	github.com/calebcase/tmpfile v1.0.3 // indirect
59 | 	github.com/cloudflare/circl v1.3.7 // indirect
60 | 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
61 | 	github.com/felixge/httpsnoop v1.0.3 // indirect
62 | 	github.com/flynn/noise v1.0.0 // indirect
63 | 	github.com/garyburd/redigo v1.6.4 // indirect
64 | 	github.com/gogo/protobuf v1.3.2 // indirect
65 | 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
66 | 	github.com/golang/protobuf v1.5.3 // indirect
67 | 	github.com/google/uuid v1.3.0 // indirect
68 | 	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
69 | 	github.com/googleapis/gax-go/v2 v2.7.1 // indirect
70 | 	github.com/gorilla/css v1.0.0 // indirect
71 | 	github.com/gorilla/securecookie v1.1.1 // indirect
72 | 	github.com/jmespath/go-jmespath v0.4.0 // indirect
73 | 	github.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f // indirect
74 | 	github.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059 // indirect
75 | 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
76 | 	github.com/kr/pretty v0.3.1 // indirect
77 | 	github.com/kr/text v0.2.0 // indirect
78 | 	github.com/mattn/go-colorable v0.1.13 // indirect
79 | 	github.com/mattn/go-isatty v0.0.17 // indirect
80 | 	github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
81 | 	github.com/rogpeppe/go-internal v1.9.0 // indirect
82 | 	github.com/spacemonkeygo/monkit/v3 v3.0.19 // indirect
83 | 	github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 // indirect
84 | 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
85 | 	github.com/zeebo/blake3 v0.2.3 // indirect
86 | 	github.com/zeebo/errs v1.3.0 // indirect
87 | 	go.opencensus.io v0.24.0 // indirect
88 | 	golang.org/x/sync v0.1.0 // indirect
89 | 	golang.org/x/sys v0.18.0 // indirect
90 | 	google.golang.org/appengine v1.6.7 // indirect
91 | 	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
92 | 	google.golang.org/grpc v1.56.3 // indirect
93 | 	google.golang.org/protobuf v1.33.0 // indirect
94 | 	gopkg.in/yaml.v2 v2.4.0 // indirect
95 | 	storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a // indirect
96 | 	storj.io/picobuf v0.0.1 // indirect
97 | )
98 | 


--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
  1 | cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
  2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
  3 | cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
  4 | cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
  5 | cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
  6 | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
  7 | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
  8 | cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
  9 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 10 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 11 | github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
 12 | github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
 13 | github.com/ProtonMail/go-crypto v0.0.0-20230124153114-0acdc8ae009b/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
 14 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
 15 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
 16 | github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
 17 | github.com/ProtonMail/gopenpgp/v2 v2.5.2 h1:97SjlWNAxXl9P22lgwgrZRshQdiEfAht0g3ZoiA1GCw=
 18 | github.com/ProtonMail/gopenpgp/v2 v2.5.2/go.mod h1:52qDaCnto6r+CoWbuU50T77XQt99lIs46HtHtvgFO3o=
 19 | github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14 h1:3zOOc7WdrATDXof+h/rBgMsg0sAmZIEVHft1UbWHh94=
 20 | github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14/go.mod h1:+VFiaivV54Sa94ijzA/ZHQLoHuoUIS9hIqCK6f/76Zw=
 21 | github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2 h1:sIvihcW4qpN5qGSjmrsDDAbLpEq5tuHjJJfWY0Hud5Y=
 22 | github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2/go.mod h1:3YwJE8rEisS9eraee0hygGG4G3gqX8H8Nyu+nPTUnGU=
 23 | github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=
 24 | github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
 25 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
 26 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
 27 | github.com/aws/aws-sdk-go-v2/config v1.18.25 h1:JuYyZcnMPBiFqn87L2cRppo+rNwgah6YwD3VuyvaW6Q=
 28 | github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4=
 29 | github.com/aws/aws-sdk-go-v2/credentials v1.13.24 h1:PjiYyls3QdCrzqUN35jMWtUK1vqVZ+zLfdOa/UPFDp0=
 30 | github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o=
 31 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w=
 32 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
 33 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 h1:fI9/5BDEaAv/pv1VO1X1n3jfP9it+IGqWsCuuBQI8wM=
 34 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67/go.mod h1:zQClPRIwQZfJlZq6WZve+s4Tb4JW+3V6eS+4+KrYeP8=
 35 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4=
 36 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
 37 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk=
 38 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
 39 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8=
 40 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=
 41 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs=
 42 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g=
 43 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
 44 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
 45 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w=
 46 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs=
 47 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo=
 48 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
 49 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ=
 50 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM=
 51 | github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 h1:O+9nAy9Bb6bJFTpeNFtd9UfHbgxO1o4ZDAM9rQp5NsY=
 52 | github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8=
 53 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 h1:UBQjaMTCKwyUYwiVnUt6toEJwGXsLBI6al083tpjJzY=
 54 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=
 55 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA=
 56 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
 57 | github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E=
 58 | github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
 59 | github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
 60 | github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
 61 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 62 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 63 | github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
 64 | github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 65 | github.com/calebcase/tmpfile v1.0.3 h1:BZrOWZ79gJqQ3XbAQlihYZf/YCV0H4KPIdM5K5oMpJo=
 66 | github.com/calebcase/tmpfile v1.0.3/go.mod h1:UAUc01aHeC+pudPagY/lWvt2qS9ZO5Zzof6/tIUzqeI=
 67 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 68 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 69 | github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
 70 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
 71 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
 72 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 73 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 74 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 75 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 76 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 77 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 78 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 79 | github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
 80 | github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e h1:rcHHSQqzCgvlwP0I/fQ8rQMn/MpHE5gWSLdtpxtP6KQ=
 81 | github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e/go.mod h1:Byz7q8MSzSPkouskHJhX0er2mZY/m0Vj5bMeMCkkyY4=
 82 | github.com/dutchcoders/go-virustotal v0.0.0-20140923143438-24cc8e6fa329 h1:ERqCkG/uSyT74P1m/j9yR+so+7ynY4fbTvLY/Mr1ZMg=
 83 | github.com/dutchcoders/go-virustotal v0.0.0-20140923143438-24cc8e6fa329/go.mod h1:G5qOfE5bQZ5scycLpB7fYWgN4y3xdfXo+pYWM8z2epY=
 84 | github.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6 h1:7uTRy44YpQi6/mtDq0N9zeQRCGEh93o7gKq/usGgpF8=
 85 | github.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6/go.mod h1:F6Q37CxDh2MHr5KXkcZmNB3tdkK7v+bgE+OpBY+9ilI=
 86 | github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
 87 | github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
 88 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 89 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 90 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 91 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 92 | github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
 93 | github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
 94 | github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 95 | github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
 96 | github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 97 | github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ=
 98 | github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
 99 | github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
100 | github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
101 | github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg=
102 | github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=
103 | github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
104 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
105 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
106 | github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=
107 | github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4=
108 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
109 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
110 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
111 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
112 | github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
113 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
114 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
115 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
116 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
117 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
118 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
119 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
120 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
121 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
122 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
123 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
124 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
125 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
126 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
127 | github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
128 | github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
129 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
130 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
131 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
132 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
133 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
134 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
135 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
136 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
137 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
138 | github.com/google/pprof v0.0.0-20211108044417-e9b028704de0 h1:rsq1yB2xiFLDYYaYdlGBsSkwVzsCo500wMhxvW5A/bk=
139 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
140 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
141 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
142 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
143 | github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
144 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
145 | github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A=
146 | github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
147 | github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
148 | github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
149 | github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
150 | github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
151 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
152 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
153 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
154 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
155 | github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
156 | github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
157 | github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
158 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
159 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
160 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
161 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
162 | github.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f h1:HM2D/tnqbzNoN5DGIeB8ibM1BMYCkRWOqyWWcNAWw8o=
163 | github.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f/go.mod h1:PXFUrknJu7TkBNyL8t7XWDPtDFFLFrNQQAdsXv9YfJE=
164 | github.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059 h1:4xdaxDg3xe+DKZC8NjbH/gvTs4iNYUnzOAiD5QL5NrM=
165 | github.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059/go.mod h1:f0ijQHcvHYAuxX6JA/JUr/Z0FVn12D9REaT/HAWVgP4=
166 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
167 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
168 | github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
169 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
170 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
171 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
172 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
173 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
174 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
175 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
176 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
177 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
178 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
179 | github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
180 | github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
181 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
182 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
183 | github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
184 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
185 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
186 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
187 | github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
188 | github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
189 | github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
190 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
191 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
192 | github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
193 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
194 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
195 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
196 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
197 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
198 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
199 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
200 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
201 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
202 | github.com/shuLhan/go-bindata v4.0.0+incompatible/go.mod h1:pkcPAATLBDD2+SpAPnX5vEM90F7fcwHCvvLCMXcmw3g=
203 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
204 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
205 | github.com/spacemonkeygo/monkit/v3 v3.0.19 h1:wqBb9bpD7jXkVi4XwIp8jn1fektaVBQ+cp9SHRXgAdo=
206 | github.com/spacemonkeygo/monkit/v3 v3.0.19/go.mod h1:kj1ViJhlyADa7DiA4xVnTuPA46lFKbM7mxQTrXCuJP4=
207 | github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
208 | github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
209 | github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
210 | github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
211 | github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
212 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
213 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
214 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
215 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
216 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
217 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
218 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
219 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
220 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
221 | github.com/tg123/go-htpasswd v1.2.1 h1:i4wfsX1KvvkyoMiHZzjS0VzbAPWfxzI8INcZAKtutoU=
222 | github.com/tg123/go-htpasswd v1.2.1/go.mod h1:erHp1B86KXdwQf1X5ZrLb7erXZnWueEQezb2dql4q58=
223 | github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
224 | github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
225 | github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
226 | github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
227 | github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 h1:zMsHhfK9+Wdl1F7sIKLyx3wrOFofpb3rWFbA4HgcK5k=
228 | github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3/go.mod h1:R0Gbuw7ElaGSLOZUSwBm/GgVwMd30jWxBDdAyMOeTuc=
229 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
230 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
231 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
232 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
233 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
234 | github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
235 | github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A=
236 | github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
237 | github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
238 | github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
239 | github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
240 | github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
241 | github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
242 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
243 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
244 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
245 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
246 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
247 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
248 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
249 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
250 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
251 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
252 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
253 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
254 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
255 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
256 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
257 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
258 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
259 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
260 | golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
261 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
262 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
263 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
264 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
265 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
266 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
267 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
268 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
269 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
270 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
271 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
272 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
273 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
274 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
275 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
276 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
277 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
278 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
279 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
280 | golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
281 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
282 | golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
283 | golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
284 | golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
285 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
286 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
287 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
288 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
289 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
290 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
291 | golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
292 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
293 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
294 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
295 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
296 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
297 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
298 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
299 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
300 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
301 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
302 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
303 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
304 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
305 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
306 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
307 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
308 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
309 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
310 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
311 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
312 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
313 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
314 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
315 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
316 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
317 | golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
318 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
319 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
320 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
321 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
322 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
323 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
324 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
325 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
326 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
327 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
328 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
329 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
330 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
331 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
332 | google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
333 | google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
334 | google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
335 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
336 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
337 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
338 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
339 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
340 | google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
341 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
342 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
343 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
344 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
345 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
346 | google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
347 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
348 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
349 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
350 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
351 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
352 | google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
353 | google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
354 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
355 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
356 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
357 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
358 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
359 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
360 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
361 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
362 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
363 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
364 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
365 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
366 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
367 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
368 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
369 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
370 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
371 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
372 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
373 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
374 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
375 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
376 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
377 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
378 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
379 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
380 | storj.io/common v0.0.0-20230301105927-7f966760c100 h1:0Rc6boo10ZgiHdadHi1o2OUv25YvTn8fSc/VyRz2Tyk=
381 | storj.io/common v0.0.0-20230301105927-7f966760c100/go.mod h1:tDgoLthBVcrTPEokBgPdjrn39p/gyNx06j6ehhTSiUg=
382 | storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a h1:FBaOc8c5efmW3tmPsiGy07USMkOSu/tyYCZpu2ro0y8=
383 | storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a/go.mod h1:6rcOyR/QQkSTX/9L5ZGtlZaE2PtXTTZl8d+ulSeeYEg=
384 | storj.io/picobuf v0.0.1 h1:ekEvxSQCbEjTVIi/qxj2za13SJyfRE37yE30IBkZeT0=
385 | storj.io/picobuf v0.0.1/go.mod h1:7ZTAMs6VesgTHbbhFU79oQ9hDaJ+MD4uoFQZ1P4SEz0=
386 | storj.io/uplink v1.10.0 h1:3hS0hszupHSxEoC4DsMpljaRy0uNoijEPVF6siIE28Q=
387 | storj.io/uplink v1.10.0/go.mod h1:gJIQumB8T3tBHPRive51AVpbc+v2xe+P/goFNMSRLG4=
388 | 


--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"log"
 5 | 	"os"
 6 | 
 7 | 	"github.com/dutchcoders/transfer.sh/cmd"
 8 | )
 9 | 
10 | func main() {
11 | 	app := cmd.New()
12 | 	err := app.Run(os.Args)
13 | 	if err != nil {
14 | 		log.Fatal(err)
15 | 	}
16 | }
17 | 


--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 |     "dependencies": {
3 |         "github.com/dutchcoders/transfer.sh-web": {
4 |             "branch": "master"
5 |         }
6 |     }
7 | }
8 | 


--------------------------------------------------------------------------------
/server/clamav.go:
--------------------------------------------------------------------------------
  1 | /*
  2 | The MIT License (MIT)
  3 | 
  4 | Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]
  5 | Copyright (c) 2018-2020 Andrea Spacca.
  6 | Copyright (c) 2020- Andrea Spacca and Stefan Benten.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a copy
  9 | of this software and associated documentation files (the "Software"), to deal
 10 | in the Software without restriction, including without limitation the rights
 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12 | copies of the Software, and to permit persons to whom the Software is
 13 | furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included in
 16 | all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 24 | THE SOFTWARE.
 25 | */
 26 | 
 27 | package server
 28 | 
 29 | import (
 30 | 	"errors"
 31 | 	"fmt"
 32 | 	"io"
 33 | 	"net/http"
 34 | 	"os"
 35 | 	"time"
 36 | 
 37 | 	"github.com/dutchcoders/go-clamd"
 38 | 	"github.com/gorilla/mux"
 39 | )
 40 | 
 41 | const clamavScanStatusOK = "OK"
 42 | 
 43 | func (s *Server) scanHandler(w http.ResponseWriter, r *http.Request) {
 44 | 	vars := mux.Vars(r)
 45 | 
 46 | 	filename := sanitize(vars["filename"])
 47 | 
 48 | 	contentLength := r.ContentLength
 49 | 	contentType := r.Header.Get("Content-Type")
 50 | 
 51 | 	s.logger.Printf("Scanning %s %d %s", filename, contentLength, contentType)
 52 | 
 53 | 	file, err := os.CreateTemp(s.tempPath, "clamav-")
 54 | 	defer s.cleanTmpFile(file)
 55 | 	if err != nil {
 56 | 		s.logger.Printf("%s", err.Error())
 57 | 		http.Error(w, err.Error(), http.StatusInternalServerError)
 58 | 		return
 59 | 	}
 60 | 
 61 | 	_, err = io.Copy(file, r.Body)
 62 | 	if err != nil {
 63 | 		s.logger.Printf("%s", err.Error())
 64 | 		http.Error(w, err.Error(), http.StatusInternalServerError)
 65 | 		return
 66 | 	}
 67 | 
 68 | 	status, err := s.performScan(file.Name())
 69 | 	if err != nil {
 70 | 		s.logger.Printf("%s", err.Error())
 71 | 		http.Error(w, err.Error(), http.StatusInternalServerError)
 72 | 		return
 73 | 	}
 74 | 
 75 | 	_, _ = w.Write([]byte(fmt.Sprintf("%v\n", status)))
 76 | }
 77 | 
 78 | func (s *Server) performScan(path string) (string, error) {
 79 | 	c := clamd.NewClamd(s.ClamAVDaemonHost)
 80 | 
 81 | 	responseCh := make(chan chan *clamd.ScanResult)
 82 | 	errCh := make(chan error)
 83 | 	go func(responseCh chan chan *clamd.ScanResult, errCh chan error) {
 84 | 		response, err := c.ScanFile(path)
 85 | 		if err != nil {
 86 | 			errCh <- err
 87 | 			return
 88 | 		}
 89 | 
 90 | 		responseCh <- response
 91 | 	}(responseCh, errCh)
 92 | 
 93 | 	select {
 94 | 	case err := <-errCh:
 95 | 		return "", err
 96 | 	case response := <-responseCh:
 97 | 		st := <-response
 98 | 		return st.Status, nil
 99 | 	case <-time.After(time.Second * 60):
100 | 		return "", errors.New("clamav scan timeout")
101 | 	}
102 | }
103 | 


--------------------------------------------------------------------------------
/server/handlers_test.go:
--------------------------------------------------------------------------------
  1 | package server
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"net/http"
  6 | 	"net/http/httptest"
  7 | 	"testing"
  8 | 
  9 | 	. "gopkg.in/check.v1"
 10 | )
 11 | 
 12 | // Hook up gocheck into the "go test" runner.
 13 | func Test(t *testing.T) { TestingT(t) }
 14 | 
 15 | var (
 16 | 	_ = Suite(&suiteRedirectWithForceHTTPS{})
 17 | 	_ = Suite(&suiteRedirectWithoutForceHTTPS{})
 18 | )
 19 | 
 20 | type suiteRedirectWithForceHTTPS struct {
 21 | 	handler http.HandlerFunc
 22 | }
 23 | 
 24 | func (s *suiteRedirectWithForceHTTPS) SetUpTest(c *C) {
 25 | 	srvr, err := New(ForceHTTPS())
 26 | 	c.Assert(err, IsNil)
 27 | 
 28 | 	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 29 | 		_, _ = fmt.Fprintln(w, "Hello, client")
 30 | 	})
 31 | 
 32 | 	s.handler = srvr.RedirectHandler(handler)
 33 | }
 34 | 
 35 | func (s *suiteRedirectWithForceHTTPS) TestHTTPs(c *C) {
 36 | 	req := httptest.NewRequest("GET", "https://test/test", nil)
 37 | 
 38 | 	w := httptest.NewRecorder()
 39 | 	s.handler(w, req)
 40 | 
 41 | 	resp := w.Result()
 42 | 	c.Assert(resp.StatusCode, Equals, http.StatusOK)
 43 | }
 44 | 
 45 | func (s *suiteRedirectWithForceHTTPS) TestOnion(c *C) {
 46 | 	req := httptest.NewRequest("GET", "http://test.onion/test", nil)
 47 | 
 48 | 	w := httptest.NewRecorder()
 49 | 	s.handler(w, req)
 50 | 
 51 | 	resp := w.Result()
 52 | 	c.Assert(resp.StatusCode, Equals, http.StatusOK)
 53 | }
 54 | 
 55 | func (s *suiteRedirectWithForceHTTPS) TestXForwardedFor(c *C) {
 56 | 	req := httptest.NewRequest("GET", "http://127.0.0.1/test", nil)
 57 | 	req.Header.Set("X-Forwarded-Proto", "https")
 58 | 
 59 | 	w := httptest.NewRecorder()
 60 | 	s.handler(w, req)
 61 | 
 62 | 	resp := w.Result()
 63 | 	c.Assert(resp.StatusCode, Equals, http.StatusOK)
 64 | }
 65 | 
 66 | func (s *suiteRedirectWithForceHTTPS) TestHTTP(c *C) {
 67 | 	req := httptest.NewRequest("GET", "http://127.0.0.1/test", nil)
 68 | 
 69 | 	w := httptest.NewRecorder()
 70 | 	s.handler(w, req)
 71 | 
 72 | 	resp := w.Result()
 73 | 	c.Assert(resp.StatusCode, Equals, http.StatusPermanentRedirect)
 74 | 	c.Assert(resp.Header.Get("Location"), Equals, "https://127.0.0.1/test")
 75 | }
 76 | 
 77 | type suiteRedirectWithoutForceHTTPS struct {
 78 | 	handler http.HandlerFunc
 79 | }
 80 | 
 81 | func (s *suiteRedirectWithoutForceHTTPS) SetUpTest(c *C) {
 82 | 	srvr, err := New()
 83 | 	c.Assert(err, IsNil)
 84 | 
 85 | 	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 86 | 		_, _ = fmt.Fprintln(w, "Hello, client")
 87 | 	})
 88 | 
 89 | 	s.handler = srvr.RedirectHandler(handler)
 90 | }
 91 | 
 92 | func (s *suiteRedirectWithoutForceHTTPS) TestHTTP(c *C) {
 93 | 	req := httptest.NewRequest("GET", "http://127.0.0.1/test", nil)
 94 | 
 95 | 	w := httptest.NewRecorder()
 96 | 	s.handler(w, req)
 97 | 
 98 | 	resp := w.Result()
 99 | 	c.Assert(resp.StatusCode, Equals, http.StatusOK)
100 | }
101 | 
102 | func (s *suiteRedirectWithoutForceHTTPS) TestHTTPs(c *C) {
103 | 	req := httptest.NewRequest("GET", "https://127.0.0.1/test", nil)
104 | 
105 | 	w := httptest.NewRecorder()
106 | 	s.handler(w, req)
107 | 
108 | 	resp := w.Result()
109 | 	c.Assert(resp.StatusCode, Equals, http.StatusOK)
110 | }
111 | 


--------------------------------------------------------------------------------
/server/ip_filter.go:
--------------------------------------------------------------------------------
  1 | /*
  2 | MIT License
  3 | Copyright © 2016 <dev@jpillora.com>
  4 | 
  5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  6 | 
  7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  8 | 
  9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 10 | */
 11 | 
 12 | package server
 13 | 
 14 | import (
 15 | 	"log"
 16 | 	"net"
 17 | 	"net/http"
 18 | 	"os"
 19 | 	"sync"
 20 | 
 21 | 	"github.com/tomasen/realip"
 22 | )
 23 | 
 24 | // IPFilterOptions for ipFilter. Allowed takes precedence over Blocked.
 25 | // IPs can be IPv4 or IPv6 and can optionally contain subnet
 26 | // masks (/24). Note however, determining if a given IP is
 27 | // included in a subnet requires a linear scan so is less performant
 28 | // than looking up single IPs.
 29 | //
 30 | // This could be improved with some algorithmic magic.
 31 | type IPFilterOptions struct {
 32 | 	//explicity allowed IPs
 33 | 	AllowedIPs []string
 34 | 	//explicity blocked IPs
 35 | 	BlockedIPs []string
 36 | 	//block by default (defaults to allow)
 37 | 	BlockByDefault bool
 38 | 	// TrustProxy enable check request IP from proxy
 39 | 	TrustProxy bool
 40 | 
 41 | 	Logger interface {
 42 | 		Printf(format string, v ...interface{})
 43 | 	}
 44 | }
 45 | 
 46 | // ipFilter
 47 | type ipFilter struct {
 48 | 	//mut protects the below
 49 | 	//rw since writes are rare
 50 | 	mut            sync.RWMutex
 51 | 	defaultAllowed bool
 52 | 	ips            map[string]bool
 53 | 	subnets        []*subnet
 54 | }
 55 | 
 56 | type subnet struct {
 57 | 	str     string
 58 | 	ipnet   *net.IPNet
 59 | 	allowed bool
 60 | }
 61 | 
 62 | func newIPFilter(opts *IPFilterOptions) *ipFilter {
 63 | 	if opts.Logger == nil {
 64 | 		flags := log.LstdFlags
 65 | 		opts.Logger = log.New(os.Stdout, "", flags)
 66 | 	}
 67 | 	f := &ipFilter{
 68 | 		ips:            map[string]bool{},
 69 | 		defaultAllowed: !opts.BlockByDefault,
 70 | 	}
 71 | 	for _, ip := range opts.BlockedIPs {
 72 | 		f.BlockIP(ip)
 73 | 	}
 74 | 	for _, ip := range opts.AllowedIPs {
 75 | 		f.AllowIP(ip)
 76 | 	}
 77 | 	return f
 78 | }
 79 | 
 80 | func (f *ipFilter) AllowIP(ip string) bool {
 81 | 	return f.ToggleIP(ip, true)
 82 | }
 83 | 
 84 | func (f *ipFilter) BlockIP(ip string) bool {
 85 | 	return f.ToggleIP(ip, false)
 86 | }
 87 | 
 88 | func (f *ipFilter) ToggleIP(str string, allowed bool) bool {
 89 | 	//check if provided string describes a subnet
 90 | 	if ip, network, err := net.ParseCIDR(str); err == nil {
 91 | 		// containing only one ip?
 92 | 		if n, total := network.Mask.Size(); n == total {
 93 | 			f.mut.Lock()
 94 | 			f.ips[ip.String()] = allowed
 95 | 			f.mut.Unlock()
 96 | 			return true
 97 | 		}
 98 | 		//check for existing
 99 | 		f.mut.Lock()
100 | 		found := false
101 | 		for _, subnet := range f.subnets {
102 | 			if subnet.str == str {
103 | 				found = true
104 | 				subnet.allowed = allowed
105 | 				break
106 | 			}
107 | 		}
108 | 		if !found {
109 | 			f.subnets = append(f.subnets, &subnet{
110 | 				str:     str,
111 | 				ipnet:   network,
112 | 				allowed: allowed,
113 | 			})
114 | 		}
115 | 		f.mut.Unlock()
116 | 		return true
117 | 	}
118 | 	//check if plain ip
119 | 	if ip := net.ParseIP(str); ip != nil {
120 | 		f.mut.Lock()
121 | 		f.ips[ip.String()] = allowed
122 | 		f.mut.Unlock()
123 | 		return true
124 | 	}
125 | 	return false
126 | }
127 | 
128 | // ToggleDefault alters the default setting
129 | func (f *ipFilter) ToggleDefault(allowed bool) {
130 | 	f.mut.Lock()
131 | 	f.defaultAllowed = allowed
132 | 	f.mut.Unlock()
133 | }
134 | 
135 | // Allowed returns if a given IP can pass through the filter
136 | func (f *ipFilter) Allowed(ipstr string) bool {
137 | 	return f.NetAllowed(net.ParseIP(ipstr))
138 | }
139 | 
140 | // NetAllowed returns if a given net.IP can pass through the filter
141 | func (f *ipFilter) NetAllowed(ip net.IP) bool {
142 | 	//invalid ip
143 | 	if ip == nil {
144 | 		return false
145 | 	}
146 | 	//read lock entire function
147 | 	//except for db access
148 | 	f.mut.RLock()
149 | 	defer f.mut.RUnlock()
150 | 	//check single ips
151 | 	allowed, ok := f.ips[ip.String()]
152 | 	if ok {
153 | 		return allowed
154 | 	}
155 | 	//scan subnets for any allow/block
156 | 	blocked := false
157 | 	for _, subnet := range f.subnets {
158 | 		if subnet.ipnet.Contains(ip) {
159 | 			if subnet.allowed {
160 | 				return true
161 | 			}
162 | 			blocked = true
163 | 		}
164 | 	}
165 | 	if blocked {
166 | 		return false
167 | 	}
168 | 
169 | 	//use default setting
170 | 	return f.defaultAllowed
171 | }
172 | 
173 | // Blocked returns if a given IP can NOT pass through the filter
174 | func (f *ipFilter) Blocked(ip string) bool {
175 | 	return !f.Allowed(ip)
176 | }
177 | 
178 | // NetBlocked returns if a given net.IP can NOT pass through the filter
179 | func (f *ipFilter) NetBlocked(ip net.IP) bool {
180 | 	return !f.NetAllowed(ip)
181 | }
182 | 
183 | // Wrap the provided handler with simple IP blocking middleware
184 | // using this IP filter and its configuration
185 | func (f *ipFilter) Wrap(next http.Handler) http.Handler {
186 | 	return &ipFilterMiddleware{ipFilter: f, next: next}
187 | }
188 | 
189 | // WrapIPFilter is equivalent to newIPFilter(opts) then Wrap(next)
190 | func WrapIPFilter(next http.Handler, opts *IPFilterOptions) http.Handler {
191 | 	return newIPFilter(opts).Wrap(next)
192 | }
193 | 
194 | type ipFilterMiddleware struct {
195 | 	*ipFilter
196 | 	next http.Handler
197 | }
198 | 
199 | func (m *ipFilterMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
200 | 	remoteIP := realip.FromRequest(r)
201 | 
202 | 	if !m.ipFilter.Allowed(remoteIP) {
203 | 		//show simple forbidden text
204 | 		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
205 | 		return
206 | 	}
207 | 
208 | 	//success!
209 | 	m.next.ServeHTTP(w, r)
210 | }
211 | 


--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
  1 | /*
  2 | The MIT License (MIT)
  3 | 
  4 | Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]
  5 | 
  6 | Permission is hereby granted, free of charge, to any person obtaining a copy
  7 | of this software and associated documentation files (the "Software"), to deal
  8 | in the Software without restriction, including without limitation the rights
  9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10 | copies of the Software, and to permit persons to whom the Software is
 11 | furnished to do so, subject to the following conditions:
 12 | 
 13 | The above copyright notice and this permission notice shall be included in
 14 | all copies or substantial portions of the Software.
 15 | 
 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22 | THE SOFTWARE.
 23 | */
 24 | 
 25 | package server
 26 | 
 27 | import (
 28 | 	"context"
 29 | 	cryptoRand "crypto/rand"
 30 | 	"crypto/tls"
 31 | 	"encoding/binary"
 32 | 	"errors"
 33 | 	"log"
 34 | 	"math/rand"
 35 | 	"mime"
 36 | 	"net/http"
 37 | 	_ "net/http/pprof"
 38 | 	"net/url"
 39 | 	"os"
 40 | 	"os/signal"
 41 | 	"path/filepath"
 42 | 	"strings"
 43 | 	"sync"
 44 | 	"syscall"
 45 | 	"time"
 46 | 
 47 | 	"github.com/PuerkitoBio/ghost/handlers"
 48 | 	"github.com/VojtechVitek/ratelimit"
 49 | 	"github.com/VojtechVitek/ratelimit/memory"
 50 | 	gorillaHandlers "github.com/gorilla/handlers"
 51 | 	"github.com/gorilla/mux"
 52 | 	"github.com/tg123/go-htpasswd"
 53 | 	"golang.org/x/crypto/acme/autocert"
 54 | 
 55 | 	web "github.com/dutchcoders/transfer.sh-web"
 56 | 	"github.com/dutchcoders/transfer.sh/server/storage"
 57 | 	assetfs "github.com/elazarl/go-bindata-assetfs"
 58 | )
 59 | 
 60 | // parse request with maximum memory of _24Kilobits
 61 | const _24K = (1 << 3) * 24
 62 | 
 63 | // parse request with maximum memory of _5Megabytes
 64 | const _5M = (1 << 20) * 5
 65 | 
 66 | // OptionFn is the option function type
 67 | type OptionFn func(*Server)
 68 | 
 69 | // ClamavHost sets clamav host
 70 | func ClamavHost(s string) OptionFn {
 71 | 	return func(srvr *Server) {
 72 | 		srvr.ClamAVDaemonHost = s
 73 | 	}
 74 | }
 75 | 
 76 | // PerformClamavPrescan enables clamav prescan on upload
 77 | func PerformClamavPrescan(b bool) OptionFn {
 78 | 	return func(srvr *Server) {
 79 | 		srvr.performClamavPrescan = b
 80 | 	}
 81 | }
 82 | 
 83 | // VirustotalKey sets virus total key
 84 | func VirustotalKey(s string) OptionFn {
 85 | 	return func(srvr *Server) {
 86 | 		srvr.VirusTotalKey = s
 87 | 	}
 88 | }
 89 | 
 90 | // Listener set listener
 91 | func Listener(s string) OptionFn {
 92 | 	return func(srvr *Server) {
 93 | 		srvr.ListenerString = s
 94 | 	}
 95 | 
 96 | }
 97 | 
 98 | // CorsDomains sets CORS domains
 99 | func CorsDomains(s string) OptionFn {
100 | 	return func(srvr *Server) {
101 | 		srvr.CorsDomains = s
102 | 	}
103 | 
104 | }
105 | 
106 | // EmailContact sets email contact
107 | func EmailContact(emailContact string) OptionFn {
108 | 	return func(srvr *Server) {
109 | 		srvr.emailContact = emailContact
110 | 	}
111 | }
112 | 
113 | // GoogleAnalytics sets GA key
114 | func GoogleAnalytics(gaKey string) OptionFn {
115 | 	return func(srvr *Server) {
116 | 		srvr.gaKey = gaKey
117 | 	}
118 | }
119 | 
120 | // UserVoice sets UV key
121 | func UserVoice(userVoiceKey string) OptionFn {
122 | 	return func(srvr *Server) {
123 | 		srvr.userVoiceKey = userVoiceKey
124 | 	}
125 | }
126 | 
127 | // TLSListener sets TLS listener and option
128 | func TLSListener(s string, t bool) OptionFn {
129 | 	return func(srvr *Server) {
130 | 		srvr.TLSListenerString = s
131 | 		srvr.TLSListenerOnly = t
132 | 	}
133 | 
134 | }
135 | 
136 | // ProfileListener sets profile listener
137 | func ProfileListener(s string) OptionFn {
138 | 	return func(srvr *Server) {
139 | 		srvr.ProfileListenerString = s
140 | 	}
141 | }
142 | 
143 | // WebPath sets web path
144 | func WebPath(s string) OptionFn {
145 | 	return func(srvr *Server) {
146 | 		if s[len(s)-1:] != "/" {
147 | 			s = filepath.Join(s, "")
148 | 		}
149 | 
150 | 		srvr.webPath = s
151 | 	}
152 | }
153 | 
154 | // ProxyPath sets proxy path
155 | func ProxyPath(s string) OptionFn {
156 | 	return func(srvr *Server) {
157 | 		if s[len(s)-1:] != "/" {
158 | 			s = filepath.Join(s, "")
159 | 		}
160 | 
161 | 		srvr.proxyPath = s
162 | 	}
163 | }
164 | 
165 | // ProxyPort sets proxy port
166 | func ProxyPort(s string) OptionFn {
167 | 	return func(srvr *Server) {
168 | 		srvr.proxyPort = s
169 | 	}
170 | }
171 | 
172 | // TempPath sets temp path
173 | func TempPath(s string) OptionFn {
174 | 	return func(srvr *Server) {
175 | 		if s[len(s)-1:] != "/" {
176 | 			s = filepath.Join(s, "")
177 | 		}
178 | 
179 | 		srvr.tempPath = s
180 | 	}
181 | }
182 | 
183 | // LogFile sets log file
184 | func LogFile(logger *log.Logger, s string) OptionFn {
185 | 	return func(srvr *Server) {
186 | 		f, err := os.OpenFile(s, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
187 | 		if err != nil {
188 | 			logger.Fatalf("error opening file: %v", err)
189 | 		}
190 | 
191 | 		logger.SetOutput(f)
192 | 		srvr.logger = logger
193 | 	}
194 | }
195 | 
196 | // Logger sets logger
197 | func Logger(logger *log.Logger) OptionFn {
198 | 	return func(srvr *Server) {
199 | 		srvr.logger = logger
200 | 	}
201 | }
202 | 
203 | // MaxUploadSize sets max upload size
204 | func MaxUploadSize(kbytes int64) OptionFn {
205 | 	return func(srvr *Server) {
206 | 		srvr.maxUploadSize = kbytes * 1024
207 | 	}
208 | 
209 | }
210 | 
211 | // RateLimit set rate limit
212 | func RateLimit(requests int) OptionFn {
213 | 	return func(srvr *Server) {
214 | 		srvr.rateLimitRequests = requests
215 | 	}
216 | }
217 | 
218 | // RandomTokenLength sets random token length
219 | func RandomTokenLength(length int) OptionFn {
220 | 	return func(srvr *Server) {
221 | 		srvr.randomTokenLength = length
222 | 	}
223 | }
224 | 
225 | // Purge sets purge days and option
226 | func Purge(days, interval int) OptionFn {
227 | 	return func(srvr *Server) {
228 | 		srvr.purgeDays = time.Duration(days) * time.Hour * 24
229 | 		srvr.purgeInterval = time.Duration(interval) * time.Hour
230 | 	}
231 | }
232 | 
233 | // ForceHTTPS sets forcing https
234 | func ForceHTTPS() OptionFn {
235 | 	return func(srvr *Server) {
236 | 		srvr.forceHTTPS = true
237 | 	}
238 | }
239 | 
240 | // EnableProfiler sets enable profiler
241 | func EnableProfiler() OptionFn {
242 | 	return func(srvr *Server) {
243 | 		srvr.profilerEnabled = true
244 | 	}
245 | }
246 | 
247 | // UseStorage set storage to use
248 | func UseStorage(s storage.Storage) OptionFn {
249 | 	return func(srvr *Server) {
250 | 		srvr.storage = s
251 | 	}
252 | }
253 | 
254 | // UseLetsEncrypt set letsencrypt usage
255 | func UseLetsEncrypt(hosts []string) OptionFn {
256 | 	return func(srvr *Server) {
257 | 		cacheDir := "./cache/"
258 | 
259 | 		m := autocert.Manager{
260 | 			Prompt: autocert.AcceptTOS,
261 | 			Cache:  autocert.DirCache(cacheDir),
262 | 			HostPolicy: func(_ context.Context, host string) error {
263 | 				found := false
264 | 
265 | 				for _, h := range hosts {
266 | 					found = found || strings.HasSuffix(host, h)
267 | 				}
268 | 
269 | 				if !found {
270 | 					return errors.New("acme/autocert: host not configured")
271 | 				}
272 | 
273 | 				return nil
274 | 			},
275 | 		}
276 | 
277 | 		srvr.tlsConfig = m.TLSConfig()
278 | 		srvr.tlsConfig.GetCertificate = m.GetCertificate
279 | 	}
280 | }
281 | 
282 | // TLSConfig sets TLS config
283 | func TLSConfig(cert, pk string) OptionFn {
284 | 	certificate, err := tls.LoadX509KeyPair(cert, pk)
285 | 	return func(srvr *Server) {
286 | 		srvr.tlsConfig = &tls.Config{
287 | 			GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
288 | 				return &certificate, err
289 | 			},
290 | 		}
291 | 	}
292 | }
293 | 
294 | // HTTPAuthCredentials sets basic http auth credentials
295 | func HTTPAuthCredentials(user string, pass string) OptionFn {
296 | 	return func(srvr *Server) {
297 | 		srvr.authUser = user
298 | 		srvr.authPass = pass
299 | 	}
300 | }
301 | 
302 | // HTTPAuthHtpasswd sets basic http auth htpasswd file
303 | func HTTPAuthHtpasswd(htpasswdPath string) OptionFn {
304 | 	return func(srvr *Server) {
305 | 		srvr.authHtpasswd = htpasswdPath
306 | 	}
307 | }
308 | 
309 | // HTTPAUTHFilterOptions sets basic http auth ips whitelist
310 | func HTTPAUTHFilterOptions(options IPFilterOptions) OptionFn {
311 | 	for i, allowedIP := range options.AllowedIPs {
312 | 		options.AllowedIPs[i] = strings.TrimSpace(allowedIP)
313 | 	}
314 | 
315 | 	return func(srvr *Server) {
316 | 		srvr.authIPFilterOptions = &options
317 | 	}
318 | }
319 | 
320 | // FilterOptions sets ip filtering
321 | func FilterOptions(options IPFilterOptions) OptionFn {
322 | 	for i, allowedIP := range options.AllowedIPs {
323 | 		options.AllowedIPs[i] = strings.TrimSpace(allowedIP)
324 | 	}
325 | 
326 | 	for i, blockedIP := range options.BlockedIPs {
327 | 		options.BlockedIPs[i] = strings.TrimSpace(blockedIP)
328 | 	}
329 | 
330 | 	return func(srvr *Server) {
331 | 		srvr.ipFilterOptions = &options
332 | 	}
333 | }
334 | 
335 | // Server is the main application
336 | type Server struct {
337 | 	authUser            string
338 | 	authPass            string
339 | 	authHtpasswd        string
340 | 	authIPFilterOptions *IPFilterOptions
341 | 
342 | 	htpasswdFile *htpasswd.File
343 | 	authIPFilter *ipFilter
344 | 
345 | 	logger *log.Logger
346 | 
347 | 	tlsConfig *tls.Config
348 | 
349 | 	profilerEnabled bool
350 | 
351 | 	locks sync.Map
352 | 
353 | 	maxUploadSize     int64
354 | 	rateLimitRequests int
355 | 
356 | 	purgeDays     time.Duration
357 | 	purgeInterval time.Duration
358 | 
359 | 	storage storage.Storage
360 | 
361 | 	forceHTTPS bool
362 | 
363 | 	randomTokenLength int
364 | 
365 | 	ipFilterOptions *IPFilterOptions
366 | 
367 | 	VirusTotalKey        string
368 | 	ClamAVDaemonHost     string
369 | 	performClamavPrescan bool
370 | 
371 | 	tempPath string
372 | 
373 | 	webPath      string
374 | 	proxyPath    string
375 | 	proxyPort    string
376 | 	emailContact string
377 | 	gaKey        string
378 | 	userVoiceKey string
379 | 
380 | 	TLSListenerOnly bool
381 | 
382 | 	CorsDomains           string
383 | 	ListenerString        string
384 | 	TLSListenerString     string
385 | 	ProfileListenerString string
386 | 
387 | 	Certificate string
388 | 
389 | 	LetsEncryptCache string
390 | }
391 | 
392 | // New is the factory fot Server
393 | func New(options ...OptionFn) (*Server, error) {
394 | 	s := &Server{
395 | 		locks: sync.Map{},
396 | 	}
397 | 
398 | 	for _, optionFn := range options {
399 | 		optionFn(s)
400 | 	}
401 | 
402 | 	return s, nil
403 | }
404 | 
405 | var theRand *rand.Rand
406 | 
407 | func init() {
408 | 	var seedBytes [8]byte
409 | 	if _, err := cryptoRand.Read(seedBytes[:]); err != nil {
410 | 		panic("cannot obtain cryptographically secure seed")
411 | 	}
412 | 
413 | 	theRand = rand.New(rand.NewSource(int64(binary.LittleEndian.Uint64(seedBytes[:]))))
414 | }
415 | 
416 | // Run starts Server
417 | func (s *Server) Run() {
418 | 	listening := false
419 | 
420 | 	if s.profilerEnabled {
421 | 		listening = true
422 | 
423 | 		go func() {
424 | 			s.logger.Println("Profiled listening at: :6060")
425 | 
426 | 			_ = http.ListenAndServe(":6060", nil)
427 | 		}()
428 | 	}
429 | 
430 | 	r := mux.NewRouter()
431 | 
432 | 	var fs http.FileSystem
433 | 
434 | 	if s.webPath != "" {
435 | 		s.logger.Println("Using static file path: ", s.webPath)
436 | 
437 | 		fs = http.Dir(s.webPath)
438 | 
439 | 		htmlTemplates, _ = htmlTemplates.ParseGlob(filepath.Join(s.webPath, "*.html"))
440 | 		textTemplates, _ = textTemplates.ParseGlob(filepath.Join(s.webPath, "*.txt"))
441 | 	} else {
442 | 		fs = &assetfs.AssetFS{
443 | 			Asset:    web.Asset,
444 | 			AssetDir: web.AssetDir,
445 | 			AssetInfo: func(path string) (os.FileInfo, error) {
446 | 				return os.Stat(path)
447 | 			},
448 | 			Prefix: web.Prefix,
449 | 		}
450 | 
451 | 		for _, path := range web.AssetNames() {
452 | 			bytes, err := web.Asset(path)
453 | 			if err != nil {
454 | 				s.logger.Panicf("Unable to parse: path=%s, err=%s", path, err)
455 | 			}
456 | 
457 | 			if strings.HasSuffix(path, ".html") {
458 | 				_, err = htmlTemplates.New(stripPrefix(path)).Parse(string(bytes))
459 | 				if err != nil {
460 | 					s.logger.Println("Unable to parse html template", err)
461 | 				}
462 | 			}
463 | 			if strings.HasSuffix(path, ".txt") {
464 | 				_, err = textTemplates.New(stripPrefix(path)).Parse(string(bytes))
465 | 				if err != nil {
466 | 					s.logger.Println("Unable to parse text template", err)
467 | 				}
468 | 			}
469 | 		}
470 | 	}
471 | 
472 | 	staticHandler := http.FileServer(fs)
473 | 
474 | 	r.PathPrefix("/images/").Handler(staticHandler).Methods("GET")
475 | 	r.PathPrefix("/styles/").Handler(staticHandler).Methods("GET")
476 | 	r.PathPrefix("/scripts/").Handler(staticHandler).Methods("GET")
477 | 	r.PathPrefix("/fonts/").Handler(staticHandler).Methods("GET")
478 | 	r.PathPrefix("/ico/").Handler(staticHandler).Methods("GET")
479 | 	r.HandleFunc("/favicon.ico", staticHandler.ServeHTTP).Methods("GET")
480 | 	r.HandleFunc("/robots.txt", staticHandler.ServeHTTP).Methods("GET")
481 | 
482 | 	r.HandleFunc("/{filename:(?:favicon\\.ico|robots\\.txt|health\\.html)}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
483 | 
484 | 	r.HandleFunc("/health.html", healthHandler).Methods("GET")
485 | 	r.HandleFunc("/", s.viewHandler).Methods("GET")
486 | 
487 | 	r.HandleFunc("/({files:.*}).zip", s.zipHandler).Methods("GET")
488 | 	r.HandleFunc("/({files:.*}).tar", s.tarHandler).Methods("GET")
489 | 	r.HandleFunc("/({files:.*}).tar.gz", s.tarGzHandler).Methods("GET")
490 | 
491 | 	r.HandleFunc("/{token}/{filename}", s.headHandler).Methods("HEAD")
492 | 	r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", s.headHandler).Methods("HEAD")
493 | 
494 | 	r.HandleFunc("/{token}/{filename}", s.previewHandler).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) (match bool) {
495 | 		// The file will show a preview page when opening the link in browser directly or
496 | 		// from external link. If the referer url path and current path are the same it will be
497 | 		// downloaded.
498 | 		if !acceptsHTML(r.Header) {
499 | 			return false
500 | 		}
501 | 
502 | 		match = r.Referer() == ""
503 | 
504 | 		u, err := url.Parse(r.Referer())
505 | 		if err != nil {
506 | 			s.logger.Fatal(err)
507 | 			return
508 | 		}
509 | 
510 | 		match = match || (u.Path != r.URL.Path)
511 | 		return
512 | 	}).Methods("GET")
513 | 
514 | 	getHandlerFn := s.getHandler
515 | 	if s.rateLimitRequests > 0 {
516 | 		getHandlerFn = ratelimit.Request(ratelimit.IP).Rate(s.rateLimitRequests, 60*time.Second).LimitBy(memory.New())(http.HandlerFunc(getHandlerFn)).ServeHTTP
517 | 	}
518 | 
519 | 	r.HandleFunc("/{token}/{filename}", getHandlerFn).Methods("GET")
520 | 	r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", getHandlerFn).Methods("GET")
521 | 
522 | 	r.HandleFunc("/{filename}/virustotal", s.virusTotalHandler).Methods("PUT")
523 | 	r.HandleFunc("/{filename}/scan", s.scanHandler).Methods("PUT")
524 | 	r.HandleFunc("/put/{filename}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
525 | 	r.HandleFunc("/upload/{filename}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
526 | 	r.HandleFunc("/{filename}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
527 | 	r.HandleFunc("/", s.basicAuthHandler(http.HandlerFunc(s.postHandler))).Methods("POST")
528 | 	// r.HandleFunc("/{page}", viewHandler).Methods("GET")
529 | 
530 | 	r.HandleFunc("/{token}/{filename}/{deletionToken}", s.deleteHandler).Methods("DELETE")
531 | 
532 | 	r.NotFoundHandler = http.HandlerFunc(s.notFoundHandler)
533 | 
534 | 	_ = mime.AddExtensionType(".md", "text/x-markdown")
535 | 
536 | 	s.logger.Printf("Transfer.sh server started.\nusing temp folder: %s\nusing storage provider: %s", s.tempPath, s.storage.Type())
537 | 
538 | 	var cors func(http.Handler) http.Handler
539 | 	if len(s.CorsDomains) > 0 {
540 | 		cors = gorillaHandlers.CORS(
541 | 			gorillaHandlers.AllowedHeaders([]string{"*"}),
542 | 			gorillaHandlers.AllowedOrigins(strings.Split(s.CorsDomains, ",")),
543 | 			gorillaHandlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}),
544 | 		)
545 | 	} else {
546 | 		cors = func(h http.Handler) http.Handler {
547 | 			return h
548 | 		}
549 | 	}
550 | 
551 | 	h := handlers.PanicHandler(
552 | 		ipFilterHandler(
553 | 			handlers.LogHandler(
554 | 				LoveHandler(
555 | 					s.RedirectHandler(cors(r))),
556 | 				handlers.NewLogOptions(s.logger.Printf, "_default_"),
557 | 			),
558 | 			s.ipFilterOptions,
559 | 		),
560 | 		nil,
561 | 	)
562 | 
563 | 	if !s.TLSListenerOnly {
564 | 		listening = true
565 | 		s.logger.Printf("starting to listen on: %v\n", s.ListenerString)
566 | 
567 | 		go func() {
568 | 			srvr := &http.Server{
569 | 				Addr:    s.ListenerString,
570 | 				Handler: h,
571 | 			}
572 | 
573 | 			if err := srvr.ListenAndServe(); err != nil {
574 | 				s.logger.Fatal(err)
575 | 			}
576 | 		}()
577 | 	}
578 | 
579 | 	if s.TLSListenerString != "" {
580 | 		listening = true
581 | 		s.logger.Printf("starting to listen for TLS on: %v\n", s.TLSListenerString)
582 | 
583 | 		go func() {
584 | 			srvr := &http.Server{
585 | 				Addr:      s.TLSListenerString,
586 | 				Handler:   h,
587 | 				TLSConfig: s.tlsConfig,
588 | 			}
589 | 
590 | 			if err := srvr.ListenAndServeTLS("", ""); err != nil {
591 | 				s.logger.Fatal(err)
592 | 			}
593 | 		}()
594 | 	}
595 | 
596 | 	s.logger.Printf("---------------------------")
597 | 
598 | 	if s.purgeDays > 0 {
599 | 		go s.purgeHandler()
600 | 	}
601 | 
602 | 	term := make(chan os.Signal, 1)
603 | 	signal.Notify(term, os.Interrupt)
604 | 	signal.Notify(term, syscall.SIGTERM)
605 | 
606 | 	if listening {
607 | 		<-term
608 | 	} else {
609 | 		s.logger.Printf("No listener active.")
610 | 	}
611 | 
612 | 	s.logger.Printf("Server stopped.")
613 | }
614 | 


--------------------------------------------------------------------------------
/server/storage/common.go:
--------------------------------------------------------------------------------
  1 | package storage
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"io"
  7 | 	"regexp"
  8 | 	"strconv"
  9 | 	"time"
 10 | )
 11 | 
 12 | type Range struct {
 13 | 	Start        uint64
 14 | 	Limit        uint64
 15 | 	contentRange string
 16 | }
 17 | 
 18 | // Range Reconstructs Range header and returns it
 19 | func (r *Range) Range() string {
 20 | 	if r.Limit > 0 {
 21 | 		return fmt.Sprintf("bytes=%d-%d", r.Start, r.Start+r.Limit-1)
 22 | 	} else {
 23 | 		return fmt.Sprintf("bytes=%d-", r.Start)
 24 | 	}
 25 | }
 26 | 
 27 | // AcceptLength Tries to accept given range
 28 | // returns newContentLength if range was satisfied, otherwise returns given contentLength
 29 | func (r *Range) AcceptLength(contentLength uint64) (newContentLength uint64) {
 30 | 	newContentLength = contentLength
 31 | 	if r.Limit == 0 {
 32 | 		r.Limit = newContentLength - r.Start
 33 | 	}
 34 | 	if contentLength < r.Start {
 35 | 		return
 36 | 	}
 37 | 	if r.Limit > contentLength-r.Start {
 38 | 		return
 39 | 	}
 40 | 	r.contentRange = fmt.Sprintf("bytes %d-%d/%d", r.Start, r.Start+r.Limit-1, contentLength)
 41 | 	newContentLength = r.Limit
 42 | 	return
 43 | }
 44 | 
 45 | func (r *Range) SetContentRange(cr string) {
 46 | 	r.contentRange = cr
 47 | }
 48 | 
 49 | // Returns accepted Content-Range header. If range wasn't accepted empty string is returned
 50 | func (r *Range) ContentRange() string {
 51 | 	return r.contentRange
 52 | }
 53 | 
 54 | var rexp *regexp.Regexp = regexp.MustCompile(`^bytes=([0-9]+)-([0-9]*)



    
    

    
    

    
    
    
    

    
    
    
    




    

    
The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
) 55 | 56 | // Parses HTTP Range header and returns struct on success 57 | // only bytes=start-finish supported 58 | func ParseRange(rng string) *Range { 59 | if rng == "" { 60 | return nil 61 | } 62 | 63 | matches := rexp.FindAllStringSubmatch(rng, -1) 64 | if len(matches) != 1 || len(matches[0]) != 3 { 65 | return nil 66 | } 67 | if len(matches[0][0]) != len(rng) || len(matches[0][1]) == 0 { 68 | return nil 69 | } 70 | 71 | start, err := strconv.ParseUint(matches[0][1], 10, 64) 72 | if err != nil { 73 | return nil 74 | } 75 | 76 | if len(matches[0][2]) == 0 { 77 | return &Range{Start: start, Limit: 0} 78 | } 79 | 80 | finish, err := strconv.ParseUint(matches[0][2], 10, 64) 81 | if err != nil { 82 | return nil 83 | } 84 | if finish < start || finish+1 < finish { 85 | return nil 86 | } 87 | 88 | return &Range{Start: start, Limit: finish - start + 1} 89 | } 90 | 91 | // Storage is the interface for storage operation 92 | type Storage interface { 93 | // Get retrieves a file from storage 94 | Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) 95 | // Head retrieves content length of a file from storage 96 | Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) 97 | // Put saves a file on storage 98 | Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) error 99 | // Delete removes a file from storage 100 | Delete(ctx context.Context, token string, filename string) error 101 | // IsNotExist indicates if a file doesn't exist on storage 102 | IsNotExist(err error) bool 103 | // Purge cleans up the storage 104 | Purge(ctx context.Context, days time.Duration) error 105 | // Whether storage supports Get with Range header 106 | IsRangeSupported() bool 107 | // Type returns the storage type 108 | Type() string 109 | } 110 | 111 | func CloseCheck(c io.Closer) { 112 | if c == nil { 113 | return 114 | } 115 | 116 | if err := c.Close(); err != nil { 117 | fmt.Println("Received close error:", err) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /server/storage/gdrive.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "golang.org/x/oauth2" 16 | "golang.org/x/oauth2/google" 17 | "google.golang.org/api/drive/v3" 18 | "google.golang.org/api/googleapi" 19 | "google.golang.org/api/option" 20 | ) 21 | 22 | // GDrive is a storage backed by GDrive 23 | type GDrive struct { 24 | service *drive.Service 25 | rootID string 26 | basedir string 27 | localConfigPath string 28 | chunkSize int 29 | logger *log.Logger 30 | } 31 | 32 | const gDriveRootConfigFile = "root_id.conf" 33 | const gDriveTokenJSONFile = "token.json" 34 | const gDriveDirectoryMimeType = "application/vnd.google-apps.folder" 35 | 36 | // NewGDriveStorage is the factory for GDrive 37 | func NewGDriveStorage(ctx context.Context, clientJSONFilepath string, localConfigPath string, basedir string, chunkSize int, logger *log.Logger) (*GDrive, error) { 38 | 39 | b, err := os.ReadFile(clientJSONFilepath) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | // If modifying these scopes, delete your previously saved client_secret.json. 45 | config, err := google.ConfigFromJSON(b, drive.DriveScope, drive.DriveMetadataScope) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | httpClient := getGDriveClient(ctx, config, localConfigPath, logger) 51 | 52 | srv, err := drive.NewService(ctx, option.WithHTTPClient(httpClient)) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | storage := &GDrive{service: srv, basedir: basedir, rootID: "", localConfigPath: localConfigPath, chunkSize: chunkSize, logger: logger} 58 | err = storage.setupRoot() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return storage, nil 64 | } 65 | 66 | func (s *GDrive) setupRoot() error { 67 | rootFileConfig := filepath.Join(s.localConfigPath, gDriveRootConfigFile) 68 | 69 | rootID, err := os.ReadFile(rootFileConfig) 70 | if err != nil && !os.IsNotExist(err) { 71 | return err 72 | } 73 | 74 | if string(rootID) != "" { 75 | s.rootID = string(rootID) 76 | return nil 77 | } 78 | 79 | dir := &drive.File{ 80 | Name: s.basedir, 81 | MimeType: gDriveDirectoryMimeType, 82 | } 83 | 84 | di, err := s.service.Files.Create(dir).Fields("id").Do() 85 | if err != nil { 86 | return err 87 | } 88 | 89 | s.rootID = di.Id 90 | err = os.WriteFile(rootFileConfig, []byte(s.rootID), os.FileMode(0600)) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | 98 | func (s *GDrive) hasChecksum(f *drive.File) bool { 99 | return f.Md5Checksum != "" 100 | } 101 | 102 | func (s *GDrive) list(nextPageToken string, q string) (*drive.FileList, error) { 103 | return s.service.Files.List().Fields("nextPageToken, files(id, name, mimeType)").Q(q).PageToken(nextPageToken).Do() 104 | } 105 | 106 | func (s *GDrive) findID(filename string, token string) (string, error) { 107 | filename = strings.Replace(filename, `'`, `\'`, -1) 108 | filename = strings.Replace(filename, `"`, `\"`, -1) 109 | 110 | fileID, tokenID, nextPageToken := "", "", "" 111 | 112 | q := fmt.Sprintf("'%s' in parents and name='%s' and mimeType='%s' and trashed=false", s.rootID, token, gDriveDirectoryMimeType) 113 | l, err := s.list(nextPageToken, q) 114 | if err != nil { 115 | return "", err 116 | } 117 | 118 | for 0 < len(l.Files) { 119 | for _, fi := range l.Files { 120 | tokenID = fi.Id 121 | break 122 | } 123 | 124 | if l.NextPageToken == "" { 125 | break 126 | } 127 | 128 | l, err = s.list(l.NextPageToken, q) 129 | if err != nil { 130 | return "", err 131 | } 132 | } 133 | 134 | if filename == "" { 135 | return tokenID, nil 136 | } else if tokenID == "" { 137 | return "", fmt.Errorf("cannot find file %s/%s", token, filename) 138 | } 139 | 140 | q = fmt.Sprintf("'%s' in parents and name='%s' and mimeType!='%s' and trashed=false", tokenID, filename, gDriveDirectoryMimeType) 141 | l, err = s.list(nextPageToken, q) 142 | if err != nil { 143 | return "", err 144 | } 145 | 146 | for 0 < len(l.Files) { 147 | for _, fi := range l.Files { 148 | 149 | fileID = fi.Id 150 | break 151 | } 152 | 153 | if l.NextPageToken == "" { 154 | break 155 | } 156 | 157 | l, err = s.list(l.NextPageToken, q) 158 | if err != nil { 159 | return "", err 160 | } 161 | } 162 | 163 | if fileID == "" { 164 | return "", fmt.Errorf("cannot find file %s/%s", token, filename) 165 | } 166 | 167 | return fileID, nil 168 | } 169 | 170 | // Type returns the storage type 171 | func (s *GDrive) Type() string { 172 | return "gdrive" 173 | } 174 | 175 | // Head retrieves content length of a file from storage 176 | func (s *GDrive) Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) { 177 | var fileID string 178 | fileID, err = s.findID(filename, token) 179 | if err != nil { 180 | return 181 | } 182 | 183 | var fi *drive.File 184 | if fi, err = s.service.Files.Get(fileID).Context(ctx).Fields("size").Do(); err != nil { 185 | return 186 | } 187 | 188 | contentLength = uint64(fi.Size) 189 | 190 | return 191 | } 192 | 193 | // Get retrieves a file from storage 194 | func (s *GDrive) Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) { 195 | var fileID string 196 | fileID, err = s.findID(filename, token) 197 | if err != nil { 198 | return 199 | } 200 | 201 | var fi *drive.File 202 | fi, err = s.service.Files.Get(fileID).Fields("size", "md5Checksum").Do() 203 | if err != nil { 204 | return 205 | } 206 | if !s.hasChecksum(fi) { 207 | err = fmt.Errorf("cannot find file %s/%s", token, filename) 208 | return 209 | } 210 | 211 | contentLength = uint64(fi.Size) 212 | 213 | fileGetCall := s.service.Files.Get(fileID) 214 | if rng != nil { 215 | header := fileGetCall.Header() 216 | header.Set("Range", rng.Range()) 217 | } 218 | 219 | var res *http.Response 220 | res, err = fileGetCall.Context(ctx).Download() 221 | if err != nil { 222 | return 223 | } 224 | 225 | if rng != nil { 226 | reader = res.Body 227 | rng.AcceptLength(contentLength) 228 | return 229 | } 230 | 231 | reader = res.Body 232 | 233 | return 234 | } 235 | 236 | // Delete removes a file from storage 237 | func (s *GDrive) Delete(ctx context.Context, token string, filename string) (err error) { 238 | metadata, _ := s.findID(fmt.Sprintf("%s.metadata", filename), token) 239 | _ = s.service.Files.Delete(metadata).Do() 240 | 241 | var fileID string 242 | fileID, err = s.findID(filename, token) 243 | if err != nil { 244 | return 245 | } 246 | 247 | err = s.service.Files.Delete(fileID).Context(ctx).Do() 248 | return 249 | } 250 | 251 | // Purge cleans up the storage 252 | func (s *GDrive) Purge(ctx context.Context, days time.Duration) (err error) { 253 | nextPageToken := "" 254 | 255 | expirationDate := time.Now().Add(-1 * days).Format(time.RFC3339) 256 | q := fmt.Sprintf("'%s' in parents and modifiedTime < '%s' and mimeType!='%s' and trashed=false", s.rootID, expirationDate, gDriveDirectoryMimeType) 257 | l, err := s.list(nextPageToken, q) 258 | if err != nil { 259 | return err 260 | } 261 | 262 | for 0 < len(l.Files) { 263 | for _, fi := range l.Files { 264 | err = s.service.Files.Delete(fi.Id).Context(ctx).Do() 265 | if err != nil { 266 | return 267 | } 268 | } 269 | 270 | if l.NextPageToken == "" { 271 | break 272 | } 273 | 274 | l, err = s.list(l.NextPageToken, q) 275 | if err != nil { 276 | return 277 | } 278 | } 279 | 280 | return 281 | } 282 | 283 | // IsNotExist indicates if a file doesn't exist on storage 284 | func (s *GDrive) IsNotExist(err error) bool { 285 | if err == nil { 286 | return false 287 | } 288 | 289 | if e, ok := err.(*googleapi.Error); ok { 290 | return e.Code == http.StatusNotFound 291 | } 292 | 293 | return false 294 | } 295 | 296 | // Put saves a file on storage 297 | func (s *GDrive) Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) error { 298 | dirID, err := s.findID("", token) 299 | if err != nil { 300 | return err 301 | } 302 | 303 | if dirID == "" { 304 | dir := &drive.File{ 305 | Name: token, 306 | Parents: []string{s.rootID}, 307 | MimeType: gDriveDirectoryMimeType, 308 | } 309 | 310 | di, err := s.service.Files.Create(dir).Fields("id").Do() 311 | if err != nil { 312 | return err 313 | } 314 | 315 | dirID = di.Id 316 | } 317 | 318 | // Instantiate empty drive file 319 | dst := &drive.File{ 320 | Name: filename, 321 | Parents: []string{dirID}, 322 | MimeType: contentType, 323 | } 324 | 325 | _, err = s.service.Files.Create(dst).Context(ctx).Media(reader, googleapi.ChunkSize(s.chunkSize)).Do() 326 | 327 | if err != nil { 328 | return err 329 | } 330 | 331 | return nil 332 | } 333 | 334 | func (s *GDrive) IsRangeSupported() bool { return true } 335 | 336 | // Retrieve a token, saves the token, then returns the generated client. 337 | func getGDriveClient(ctx context.Context, config *oauth2.Config, localConfigPath string, logger *log.Logger) *http.Client { 338 | tokenFile := filepath.Join(localConfigPath, gDriveTokenJSONFile) 339 | tok, err := gDriveTokenFromFile(tokenFile) 340 | if err != nil { 341 | tok = getGDriveTokenFromWeb(ctx, config, logger) 342 | saveGDriveToken(tokenFile, tok, logger) 343 | } 344 | 345 | return config.Client(ctx, tok) 346 | } 347 | 348 | // Request a token from the web, then returns the retrieved token. 349 | func getGDriveTokenFromWeb(ctx context.Context, config *oauth2.Config, logger *log.Logger) *oauth2.Token { 350 | authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) 351 | fmt.Printf("Go to the following link in your browser then type the "+ 352 | "authorization code: \n%v\n", authURL) 353 | 354 | var authCode string 355 | if _, err := fmt.Scan(&authCode); err != nil { 356 | logger.Fatalf("Unable to read authorization code %v", err) 357 | } 358 | 359 | tok, err := config.Exchange(ctx, authCode) 360 | if err != nil { 361 | logger.Fatalf("Unable to retrieve token from web %v", err) 362 | } 363 | return tok 364 | } 365 | 366 | // Retrieves a token from a local file. 367 | func gDriveTokenFromFile(file string) (*oauth2.Token, error) { 368 | f, err := os.Open(file) 369 | defer CloseCheck(f) 370 | if err != nil { 371 | return nil, err 372 | } 373 | tok := &oauth2.Token{} 374 | err = json.NewDecoder(f).Decode(tok) 375 | return tok, err 376 | } 377 | 378 | // Saves a token to a file path. 379 | func saveGDriveToken(path string, token *oauth2.Token, logger *log.Logger) { 380 | logger.Printf("Saving credential file to: %s\n", path) 381 | f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 382 | defer CloseCheck(f) 383 | if err != nil { 384 | logger.Fatalf("Unable to cache oauth token: %v", err) 385 | } 386 | 387 | err = json.NewEncoder(f).Encode(token) 388 | if err != nil { 389 | logger.Fatalf("Unable to encode oauth token: %v", err) 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /server/storage/local.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | ) 12 | 13 | // LocalStorage is a local storage 14 | type LocalStorage struct { 15 | Storage 16 | basedir string 17 | logger *log.Logger 18 | } 19 | 20 | // NewLocalStorage is the factory for LocalStorage 21 | func NewLocalStorage(basedir string, logger *log.Logger) (*LocalStorage, error) { 22 | return &LocalStorage{basedir: basedir, logger: logger}, nil 23 | } 24 | 25 | // Type returns the storage type 26 | func (s *LocalStorage) Type() string { 27 | return "local" 28 | } 29 | 30 | // Head retrieves content length of a file from storage 31 | func (s *LocalStorage) Head(_ context.Context, token string, filename string) (contentLength uint64, err error) { 32 | path := filepath.Join(s.basedir, token, filename) 33 | 34 | var fi os.FileInfo 35 | if fi, err = os.Lstat(path); err != nil { 36 | return 37 | } 38 | 39 | contentLength = uint64(fi.Size()) 40 | 41 | return 42 | } 43 | 44 | // Get retrieves a file from storage 45 | func (s *LocalStorage) Get(_ context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) { 46 | path := filepath.Join(s.basedir, token, filename) 47 | 48 | var file *os.File 49 | 50 | // content type , content length 51 | if file, err = os.Open(path); err != nil { 52 | return 53 | } 54 | reader = file 55 | 56 | var fi os.FileInfo 57 | if fi, err = os.Lstat(path); err != nil { 58 | return 59 | } 60 | 61 | contentLength = uint64(fi.Size()) 62 | if rng != nil { 63 | contentLength = rng.AcceptLength(contentLength) 64 | if _, err = file.Seek(int64(rng.Start), 0); err != nil { 65 | return 66 | } 67 | } 68 | 69 | return 70 | } 71 | 72 | // Delete removes a file from storage 73 | func (s *LocalStorage) Delete(_ context.Context, token string, filename string) (err error) { 74 | metadata := filepath.Join(s.basedir, token, fmt.Sprintf("%s.metadata", filename)) 75 | _ = os.Remove(metadata) 76 | 77 | path := filepath.Join(s.basedir, token, filename) 78 | err = os.Remove(path) 79 | return 80 | } 81 | 82 | // Purge cleans up the storage 83 | func (s *LocalStorage) Purge(_ context.Context, days time.Duration) (err error) { 84 | err = filepath.Walk(s.basedir, 85 | func(path string, info os.FileInfo, err error) error { 86 | if err != nil { 87 | return err 88 | } 89 | if info.IsDir() { 90 | return nil 91 | } 92 | 93 | if info.ModTime().Before(time.Now().Add(-1 * days)) { 94 | err = os.Remove(path) 95 | return err 96 | } 97 | 98 | return nil 99 | }) 100 | 101 | return 102 | } 103 | 104 | // IsNotExist indicates if a file doesn't exist on storage 105 | func (s *LocalStorage) IsNotExist(err error) bool { 106 | if err == nil { 107 | return false 108 | } 109 | 110 | return os.IsNotExist(err) 111 | } 112 | 113 | // Put saves a file on storage 114 | func (s *LocalStorage) Put(_ context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) error { 115 | var f io.WriteCloser 116 | var err error 117 | 118 | path := filepath.Join(s.basedir, token) 119 | 120 | if err = os.MkdirAll(path, 0700); err != nil && !os.IsExist(err) { 121 | return err 122 | } 123 | 124 | f, err = os.OpenFile(filepath.Join(path, filename), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 125 | defer CloseCheck(f) 126 | 127 | if err != nil { 128 | return err 129 | } 130 | 131 | if _, err = io.Copy(f, reader); err != nil { 132 | return err 133 | } 134 | 135 | return nil 136 | } 137 | 138 | func (s *LocalStorage) IsRangeSupported() bool { return true } 139 | -------------------------------------------------------------------------------- /server/storage/s3.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "time" 10 | 11 | "github.com/aws/aws-sdk-go-v2/aws" 12 | "github.com/aws/aws-sdk-go-v2/config" 13 | "github.com/aws/aws-sdk-go-v2/credentials" 14 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 15 | "github.com/aws/aws-sdk-go-v2/service/s3" 16 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 17 | ) 18 | 19 | // S3Storage is a storage backed by AWS S3 20 | type S3Storage struct { 21 | Storage 22 | bucket string 23 | s3 *s3.Client 24 | logger *log.Logger 25 | purgeDays time.Duration 26 | noMultipart bool 27 | } 28 | 29 | // NewS3Storage is the factory for S3Storage 30 | func NewS3Storage(ctx context.Context, accessKey, secretKey, bucketName string, purgeDays int, region, endpoint string, disableMultipart bool, forcePathStyle bool, logger *log.Logger) (*S3Storage, error) { 31 | cfg, err := getAwsConfig(ctx, accessKey, secretKey) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | client := s3.NewFromConfig(cfg, func(o *s3.Options) { 37 | o.Region = region 38 | o.UsePathStyle = forcePathStyle 39 | if len(endpoint) > 0 { 40 | o.EndpointResolver = s3.EndpointResolverFromURL(endpoint) 41 | } 42 | }) 43 | 44 | return &S3Storage{ 45 | bucket: bucketName, 46 | s3: client, 47 | logger: logger, 48 | noMultipart: disableMultipart, 49 | purgeDays: time.Duration(purgeDays*24) * time.Hour, 50 | }, nil 51 | } 52 | 53 | // Type returns the storage type 54 | func (s *S3Storage) Type() string { 55 | return "s3" 56 | } 57 | 58 | // Head retrieves content length of a file from storage 59 | func (s *S3Storage) Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) { 60 | key := fmt.Sprintf("%s/%s", token, filename) 61 | 62 | headRequest := &s3.HeadObjectInput{ 63 | Bucket: aws.String(s.bucket), 64 | Key: aws.String(key), 65 | } 66 | 67 | // content type , content length 68 | response, err := s.s3.HeadObject(ctx, headRequest) 69 | if err != nil { 70 | return 71 | } 72 | 73 | contentLength = uint64(response.ContentLength) 74 | 75 | return 76 | } 77 | 78 | // Purge cleans up the storage 79 | func (s *S3Storage) Purge(context.Context, time.Duration) (err error) { 80 | // NOOP expiration is set at upload time 81 | return nil 82 | } 83 | 84 | // IsNotExist indicates if a file doesn't exist on storage 85 | func (s *S3Storage) IsNotExist(err error) bool { 86 | if err == nil { 87 | return false 88 | } 89 | 90 | var nkerr *types.NoSuchKey 91 | return errors.As(err, &nkerr) 92 | } 93 | 94 | // Get retrieves a file from storage 95 | func (s *S3Storage) Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) { 96 | key := fmt.Sprintf("%s/%s", token, filename) 97 | 98 | getRequest := &s3.GetObjectInput{ 99 | Bucket: aws.String(s.bucket), 100 | Key: aws.String(key), 101 | } 102 | 103 | if rng != nil { 104 | getRequest.Range = aws.String(rng.Range()) 105 | } 106 | 107 | response, err := s.s3.GetObject(ctx, getRequest) 108 | if err != nil { 109 | return 110 | } 111 | 112 | contentLength = uint64(response.ContentLength) 113 | if rng != nil && response.ContentRange != nil { 114 | rng.SetContentRange(*response.ContentRange) 115 | } 116 | 117 | reader = response.Body 118 | return 119 | } 120 | 121 | // Delete removes a file from storage 122 | func (s *S3Storage) Delete(ctx context.Context, token string, filename string) (err error) { 123 | metadata := fmt.Sprintf("%s/%s.metadata", token, filename) 124 | deleteRequest := &s3.DeleteObjectInput{ 125 | Bucket: aws.String(s.bucket), 126 | Key: aws.String(metadata), 127 | } 128 | 129 | _, err = s.s3.DeleteObject(ctx, deleteRequest) 130 | if err != nil { 131 | return 132 | } 133 | 134 | key := fmt.Sprintf("%s/%s", token, filename) 135 | deleteRequest = &s3.DeleteObjectInput{ 136 | Bucket: aws.String(s.bucket), 137 | Key: aws.String(key), 138 | } 139 | 140 | _, err = s.s3.DeleteObject(ctx, deleteRequest) 141 | 142 | return 143 | } 144 | 145 | // Put saves a file on storage 146 | func (s *S3Storage) Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, _ uint64) (err error) { 147 | key := fmt.Sprintf("%s/%s", token, filename) 148 | 149 | s.logger.Printf("Uploading file %s to S3 Bucket", filename) 150 | var concurrency int 151 | if !s.noMultipart { 152 | concurrency = 20 153 | } else { 154 | concurrency = 1 155 | } 156 | 157 | // Create an uploader with the session and custom options 158 | uploader := manager.NewUploader(s.s3, func(u *manager.Uploader) { 159 | u.Concurrency = concurrency // default is 5 160 | u.LeavePartsOnError = false 161 | }) 162 | 163 | var expire *time.Time 164 | if s.purgeDays.Hours() > 0 { 165 | expire = aws.Time(time.Now().Add(s.purgeDays)) 166 | } 167 | 168 | _, err = uploader.Upload(ctx, &s3.PutObjectInput{ 169 | Bucket: aws.String(s.bucket), 170 | Key: aws.String(key), 171 | Body: reader, 172 | Expires: expire, 173 | ContentType: aws.String(contentType), 174 | }) 175 | 176 | return 177 | } 178 | 179 | func (s *S3Storage) IsRangeSupported() bool { return true } 180 | 181 | func getAwsConfig(ctx context.Context, accessKey, secretKey string) (aws.Config, error) { 182 | return config.LoadDefaultConfig(ctx, 183 | config.WithCredentialsProvider(credentials.StaticCredentialsProvider{ 184 | Value: aws.Credentials{ 185 | AccessKeyID: accessKey, 186 | SecretAccessKey: secretKey, 187 | SessionToken: "", 188 | }, 189 | }), 190 | ) 191 | } 192 | -------------------------------------------------------------------------------- /server/storage/storj.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "log" 8 | "time" 9 | 10 | "storj.io/common/fpath" 11 | "storj.io/common/storj" 12 | "storj.io/uplink" 13 | ) 14 | 15 | // StorjStorage is a storage backed by Storj 16 | type StorjStorage struct { 17 | Storage 18 | project *uplink.Project 19 | bucket *uplink.Bucket 20 | purgeDays time.Duration 21 | logger *log.Logger 22 | } 23 | 24 | // NewStorjStorage is the factory for StorjStorage 25 | func NewStorjStorage(ctx context.Context, access, bucket string, purgeDays int, logger *log.Logger) (*StorjStorage, error) { 26 | var instance StorjStorage 27 | var err error 28 | 29 | ctx = fpath.WithTempData(ctx, "", true) 30 | 31 | uplConf := &uplink.Config{ 32 | UserAgent: "transfer-sh", 33 | } 34 | 35 | parsedAccess, err := uplink.ParseAccess(access) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | instance.project, err = uplConf.OpenProject(ctx, parsedAccess) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | instance.bucket, err = instance.project.EnsureBucket(ctx, bucket) 46 | if err != nil { 47 | //Ignoring the error to return the one that occurred first, but try to clean up. 48 | _ = instance.project.Close() 49 | return nil, err 50 | } 51 | 52 | instance.purgeDays = time.Duration(purgeDays*24) * time.Hour 53 | 54 | instance.logger = logger 55 | 56 | return &instance, nil 57 | } 58 | 59 | // Type returns the storage type 60 | func (s *StorjStorage) Type() string { 61 | return "storj" 62 | } 63 | 64 | // Head retrieves content length of a file from storage 65 | func (s *StorjStorage) Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) { 66 | key := storj.JoinPaths(token, filename) 67 | 68 | obj, err := s.project.StatObject(fpath.WithTempData(ctx, "", true), s.bucket.Name, key) 69 | if err != nil { 70 | return 0, err 71 | } 72 | 73 | contentLength = uint64(obj.System.ContentLength) 74 | 75 | return 76 | } 77 | 78 | // Get retrieves a file from storage 79 | func (s *StorjStorage) Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) { 80 | key := storj.JoinPaths(token, filename) 81 | 82 | s.logger.Printf("Getting file %s from Storj Bucket", filename) 83 | 84 | var options *uplink.DownloadOptions 85 | if rng != nil { 86 | options = new(uplink.DownloadOptions) 87 | options.Offset = int64(rng.Start) 88 | if rng.Limit > 0 { 89 | options.Length = int64(rng.Limit) 90 | } else { 91 | options.Length = -1 92 | } 93 | } 94 | 95 | download, err := s.project.DownloadObject(fpath.WithTempData(ctx, "", true), s.bucket.Name, key, options) 96 | if err != nil { 97 | return nil, 0, err 98 | } 99 | 100 | contentLength = uint64(download.Info().System.ContentLength) 101 | if rng != nil { 102 | contentLength = rng.AcceptLength(contentLength) 103 | } 104 | 105 | reader = download 106 | return 107 | } 108 | 109 | // Delete removes a file from storage 110 | func (s *StorjStorage) Delete(ctx context.Context, token string, filename string) (err error) { 111 | key := storj.JoinPaths(token, filename) 112 | 113 | s.logger.Printf("Deleting file %s from Storj Bucket", filename) 114 | 115 | _, err = s.project.DeleteObject(fpath.WithTempData(ctx, "", true), s.bucket.Name, key) 116 | 117 | return 118 | } 119 | 120 | // Purge cleans up the storage 121 | func (s *StorjStorage) Purge(context.Context, time.Duration) (err error) { 122 | // NOOP expiration is set at upload time 123 | return nil 124 | } 125 | 126 | // Put saves a file on storage 127 | func (s *StorjStorage) Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) (err error) { 128 | key := storj.JoinPaths(token, filename) 129 | 130 | s.logger.Printf("Uploading file %s to Storj Bucket", filename) 131 | 132 | var uploadOptions *uplink.UploadOptions 133 | if s.purgeDays.Hours() > 0 { 134 | uploadOptions = &uplink.UploadOptions{Expires: time.Now().Add(s.purgeDays)} 135 | } 136 | 137 | writer, err := s.project.UploadObject(fpath.WithTempData(ctx, "", true), s.bucket.Name, key, uploadOptions) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | n, err := io.Copy(writer, reader) 143 | if err != nil || uint64(n) != contentLength { 144 | //Ignoring the error to return the one that occurred first, but try to clean up. 145 | _ = writer.Abort() 146 | return err 147 | } 148 | err = writer.SetCustomMetadata(ctx, uplink.CustomMetadata{"content-type": contentType}) 149 | if err != nil { 150 | //Ignoring the error to return the one that occurred first, but try to clean up. 151 | _ = writer.Abort() 152 | return err 153 | } 154 | 155 | err = writer.Commit() 156 | return err 157 | } 158 | 159 | func (s *StorjStorage) IsRangeSupported() bool { return true } 160 | 161 | // IsNotExist indicates if a file doesn't exist on storage 162 | func (s *StorjStorage) IsNotExist(err error) bool { 163 | return errors.Is(err, uplink.ErrObjectNotFound) 164 | } 165 | -------------------------------------------------------------------------------- /server/token.go: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2020- Andrea Spacca and Stefan Benten. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | package server 26 | 27 | import ( 28 | "strings" 29 | ) 30 | 31 | const ( 32 | // SYMBOLS characters used for short-urls 33 | SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 34 | ) 35 | 36 | // generate a token 37 | func token(length int) string { 38 | var builder strings.Builder 39 | builder.Grow(length) 40 | 41 | for i := 0; i < length; i++ { 42 | x := theRand.Intn(len(SYMBOLS) - 1) 43 | builder.WriteByte(SYMBOLS[x]) 44 | } 45 | 46 | return builder.String() 47 | } 48 | -------------------------------------------------------------------------------- /server/token_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "testing" 4 | 5 | func BenchmarkTokenConcat(b *testing.B) { 6 | for i := 0; i < b.N; i++ { 7 | _ = token(5) + token(5) 8 | } 9 | } 10 | 11 | func BenchmarkTokenLonger(b *testing.B) { 12 | for i := 0; i < b.N; i++ { 13 | _ = token(10) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/] 5 | Copyright (c) 2018-2020 Andrea Spacca. 6 | Copyright (c) 2020- Andrea Spacca and Stefan Benten. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | */ 26 | 27 | package server 28 | 29 | import ( 30 | "fmt" 31 | "math" 32 | "net/http" 33 | "strconv" 34 | "strings" 35 | "time" 36 | 37 | "github.com/golang/gddo/httputil/header" 38 | ) 39 | 40 | func formatNumber(format string, s uint64) string { 41 | return renderFloat(format, float64(s)) 42 | } 43 | 44 | var renderFloatPrecisionMultipliers = [10]float64{ 45 | 1, 46 | 10, 47 | 100, 48 | 1000, 49 | 10000, 50 | 100000, 51 | 1000000, 52 | 10000000, 53 | 100000000, 54 | 1000000000, 55 | } 56 | 57 | var renderFloatPrecisionRounders = [10]float64{ 58 | 0.5, 59 | 0.05, 60 | 0.005, 61 | 0.0005, 62 | 0.00005, 63 | 0.000005, 64 | 0.0000005, 65 | 0.00000005, 66 | 0.000000005, 67 | 0.0000000005, 68 | } 69 | 70 | func renderFloat(format string, n float64) string { 71 | // Special cases: 72 | // NaN = "NaN" 73 | // +Inf = "+Infinity" 74 | // -Inf = "-Infinity" 75 | if math.IsNaN(n) { 76 | return "NaN" 77 | } 78 | if n > math.MaxFloat64 { 79 | return "Infinity" 80 | } 81 | if n < -math.MaxFloat64 { 82 | return "-Infinity" 83 | } 84 | 85 | // default format 86 | precision := 2 87 | decimalStr := "." 88 | thousandStr := "," 89 | positiveStr := "" 90 | negativeStr := "-" 91 | 92 | if len(format) > 0 { 93 | // If there is an explicit format directive, 94 | // then default values are these: 95 | precision = 9 96 | thousandStr = "" 97 | 98 | // collect indices of meaningful formatting directives 99 | formatDirectiveChars := []rune(format) 100 | formatDirectiveIndices := make([]int, 0) 101 | for i, char := range formatDirectiveChars { 102 | if char != '#' && char != '0' { 103 | formatDirectiveIndices = append(formatDirectiveIndices, i) 104 | } 105 | } 106 | 107 | if len(formatDirectiveIndices) > 0 { 108 | // Directive at index 0: 109 | // Must be a '+' 110 | // Raise an error if not the case 111 | // index: 0123456789 112 | // +0.000,000 113 | // +000,000.0 114 | // +0000.00 115 | // +0000 116 | if formatDirectiveIndices[0] == 0 { 117 | if formatDirectiveChars[formatDirectiveIndices[0]] != '+' { 118 | panic("renderFloat(): invalid positive sign directive") 119 | } 120 | positiveStr = "+" 121 | formatDirectiveIndices = formatDirectiveIndices[1:] 122 | } 123 | 124 | // Two directives: 125 | // First is thousands separator 126 | // Raise an error if not followed by 3-digit 127 | // 0123456789 128 | // 0.000,000 129 | // 000,000.00 130 | if len(formatDirectiveIndices) == 2 { 131 | if (formatDirectiveIndices[1] - formatDirectiveIndices[0]) != 4 { 132 | panic("renderFloat(): thousands separator directive must be followed by 3 digit-specifiers") 133 | } 134 | thousandStr = string(formatDirectiveChars[formatDirectiveIndices[0]]) 135 | formatDirectiveIndices = formatDirectiveIndices[1:] 136 | } 137 | 138 | // One directive: 139 | // Directive is decimal separator 140 | // The number of digit-specifier following the separator indicates wanted precision 141 | // 0123456789 142 | // 0.00 143 | // 000,0000 144 | if len(formatDirectiveIndices) == 1 { 145 | decimalStr = string(formatDirectiveChars[formatDirectiveIndices[0]]) 146 | precision = len(formatDirectiveChars) - formatDirectiveIndices[0] - 1 147 | } 148 | } 149 | } 150 | 151 | // generate sign part 152 | var signStr string 153 | if n >= 0.000000001 { 154 | signStr = positiveStr 155 | } else if n <= -0.000000001 { 156 | signStr = negativeStr 157 | n = -n 158 | } else { 159 | signStr = "" 160 | n = 0.0 161 | } 162 | 163 | // split number into integer and fractional parts 164 | intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision]) 165 | 166 | // generate integer part string 167 | intStr := strconv.Itoa(int(intf)) 168 | 169 | // add thousand separator if required 170 | if len(thousandStr) > 0 { 171 | for i := len(intStr); i > 3; { 172 | i -= 3 173 | intStr = intStr[:i] + thousandStr + intStr[i:] 174 | } 175 | } 176 | 177 | // no fractional part, we can leave now 178 | if precision == 0 { 179 | return signStr + intStr 180 | } 181 | 182 | // generate fractional part 183 | fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision])) 184 | // may need padding 185 | if len(fracStr) < precision { 186 | fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr 187 | } 188 | 189 | return signStr + intStr + decimalStr + fracStr 190 | } 191 | 192 | // Request.RemoteAddress contains port, which we want to remove i.e.: 193 | // "[::1]:58292" => "[::1]" 194 | func ipAddrFromRemoteAddr(s string) string { 195 | idx := strings.LastIndex(s, ":") 196 | if idx == -1 { 197 | return s 198 | } 199 | return s[:idx] 200 | } 201 | 202 | func acceptsHTML(hdr http.Header) bool { 203 | actual := header.ParseAccept(hdr, "Accept") 204 | 205 | for _, s := range actual { 206 | if s.Value == "text/html" { 207 | return true 208 | } 209 | } 210 | 211 | return false 212 | } 213 | 214 | func formatSize(size int64) string { 215 | sizeFloat := float64(size) 216 | base := math.Log(sizeFloat) / math.Log(1024) 217 | 218 | sizeOn := math.Pow(1024, base-math.Floor(base)) 219 | 220 | var round float64 221 | pow := math.Pow(10, float64(2)) 222 | digit := pow * sizeOn 223 | round = math.Floor(digit) 224 | 225 | newVal := round / pow 226 | 227 | var suffixes [5]string 228 | suffixes[0] = "B" 229 | suffixes[1] = "KB" 230 | suffixes[2] = "MB" 231 | suffixes[3] = "GB" 232 | suffixes[4] = "TB" 233 | 234 | getSuffix := suffixes[int(math.Floor(base))] 235 | return fmt.Sprintf("%s %s", strconv.FormatFloat(newVal, 'f', -1, 64), getSuffix) 236 | } 237 | 238 | func formatDurationDays(durationDays time.Duration) string { 239 | days := int(durationDays.Hours() / 24) 240 | if days == 1 { 241 | return fmt.Sprintf("%d day", days) 242 | } 243 | return fmt.Sprintf("%d days", days) 244 | } 245 | -------------------------------------------------------------------------------- /server/virustotal.go: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/] 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | package server 26 | 27 | import ( 28 | "fmt" 29 | "net/http" 30 | 31 | "github.com/gorilla/mux" 32 | 33 | "github.com/dutchcoders/go-virustotal" 34 | ) 35 | 36 | func (s *Server) virusTotalHandler(w http.ResponseWriter, r *http.Request) { 37 | vars := mux.Vars(r) 38 | 39 | filename := sanitize(vars["filename"]) 40 | 41 | contentLength := r.ContentLength 42 | contentType := r.Header.Get("Content-Type") 43 | 44 | s.logger.Printf("Submitting to VirusTotal: %s %d %s", filename, contentLength, contentType) 45 | 46 | vt, err := virustotal.NewVirusTotal(s.VirusTotalKey) 47 | if err != nil { 48 | http.Error(w, err.Error(), http.StatusInternalServerError) 49 | } 50 | 51 | reader := r.Body 52 | 53 | result, err := vt.Scan(filename, reader) 54 | if err != nil { 55 | http.Error(w, err.Error(), http.StatusInternalServerError) 56 | } 57 | 58 | s.logger.Println(result) 59 | _, _ = w.Write([]byte(fmt.Sprintf("%v\n", result.Permalink))) 60 | } 61 | --------------------------------------------------------------------------------