Repository: dutchcoders/transfer.sh Branch: main Commit: 6743a4cf4621 Files: 38 Total size: 226.0 KB Directory structure: gitextract_9idmy_oq/ ├── .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.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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bowerrc ================================================ { "directory": "transfersh-web/bower_components" } ================================================ FILE: .dockerignore ================================================ build pkg dist src bin *.pyc *.egg-info .vagrant .git .tmp bower_components node_modules extras build transfersh-server/run.sh .elasticbeanstalk Dockerfile ================================================ FILE: .github/build/friendly-filenames.json ================================================ { "android-arm64": { "friendlyName": "android-arm64-v8a" }, "darwin-amd64": { "friendlyName": "darwin-amd64" }, "darwin-arm64": { "friendlyName": "darwin-arm64" }, "dragonfly-amd64": { "friendlyName": "dragonfly-amd64" }, "freebsd-386": { "friendlyName": "freebsd-386" }, "freebsd-amd64": { "friendlyName": "freebsd-amd64" }, "freebsd-arm64": { "friendlyName": "freebsd-arm64-v8a" }, "freebsd-arm7": { "friendlyName": "freebsd-arm32-v7a" }, "linux-386": { "friendlyName": "linux-386" }, "linux-amd64": { "friendlyName": "linux-amd64" }, "linux-arm5": { "friendlyName": "linux-arm32-v5" }, "linux-arm64": { "friendlyName": "linux-arm64-v8a" }, "linux-arm6": { "friendlyName": "linux-arm32-v6" }, "linux-arm7": { "friendlyName": "linux-armv7" }, "linux-mips64le": { "friendlyName": "linux-mips64le" }, "linux-mips64": { "friendlyName": "linux-mips64" }, "linux-mipslesoftfloat": { "friendlyName": "linux-mips32le-softfloat" }, "linux-mipsle": { "friendlyName": "linux-mips32le" }, "linux-mipssoftfloat": { "friendlyName": "linux-mips32-softfloat" }, "linux-mips": { "friendlyName": "linux-mips32" }, "linux-ppc64le": { "friendlyName": "linux-ppc64le" }, "linux-ppc64": { "friendlyName": "linux-ppc64" }, "linux-riscv64": { "friendlyName": "linux-riscv64" }, "linux-s390x": { "friendlyName": "linux-s390x" }, "openbsd-386": { "friendlyName": "openbsd-386" }, "openbsd-amd64": { "friendlyName": "openbsd-amd64" }, "openbsd-arm64": { "friendlyName": "openbsd-arm64-v8a" }, "openbsd-arm7": { "friendlyName": "openbsd-arm32-v7a" }, "windows-386": { "friendlyName": "windows-386" }, "windows-amd64": { "friendlyName": "windows-amd64" }, "windows-arm7": { "friendlyName": "windows-arm32-v7a" } } ================================================ FILE: .github/workflows/build-docker-images.yml ================================================ name: deploy multi-architecture Docker images for transfer.sh with buildx on: schedule: - cron: '0 0 * * *' # everyday at midnight UTC pull_request: branches: main push: branches: main tags: - v* jobs: buildx: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Prepare id: prepare run: | DOCKER_IMAGE=dutchcoders/transfer.sh DOCKER_PLATFORMS=linux/amd64,linux/arm/v7,linux/arm64,linux/386 VERSION=edge if [[ $GITHUB_REF == refs/tags/* ]]; then VERSION=v${GITHUB_REF#refs/tags/v} fi if [ "${{ github.event_name }}" = "schedule" ]; then VERSION=nightly fi TAGS="--tag ${DOCKER_IMAGE}:${VERSION}" TAGS_NOROOT="--tag ${DOCKER_IMAGE}:${VERSION}-noroot" if [ $VERSION = edge -o $VERSION = nightly ]; then TAGS="$TAGS --tag ${DOCKER_IMAGE}:latest" TAGS_NOROOT="$TAGS_NOROOT --tag ${DOCKER_IMAGE}:latest-noroot" fi echo ::set-output name=docker_image::${DOCKER_IMAGE} echo ::set-output name=version::${VERSION} echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \ --build-arg VERSION=${VERSION} \ --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ --build-arg VCS_REF=${GITHUB_SHA::8} \ ${TAGS} . echo ::set-output name=buildx_args_noroot::--platform ${DOCKER_PLATFORMS} \ --build-arg VERSION=${VERSION} \ --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ --build-arg VCS_REF=${GITHUB_SHA::8} \ --build-arg RUNAS=noroot \ ${TAGS_NOROOT} . - name: Set up QEMU uses: docker/setup-qemu-action@v1 with: platforms: all - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v1 with: version: latest - name: Available platforms run: echo ${{ steps.buildx.outputs.platforms }} - name: Docker Buildx (build) run: | docker buildx build --no-cache --pull --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args }} docker buildx build --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args_noroot }} - name: Docker Login if: success() && github.event_name != 'pull_request' env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} run: | echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin - name: Docker Buildx (push) if: success() && github.event_name != 'pull_request' run: | docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args_noroot }} - name: Docker Check Manifest if: always() && github.event_name != 'pull_request' run: | docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }} docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}-noroot - name: Clear if: always() && github.event_name != 'pull_request' run: | rm -f ${HOME}/.docker/config.json ================================================ FILE: .github/workflows/release.yml ================================================ name: Build and Release on: workflow_dispatch: release: types: [published] jobs: build: strategy: matrix: # Include amd64 on all platforms. goos: [windows, freebsd, openbsd, linux, dragonfly, darwin] goarch: [amd64, 386] exclude: # Exclude i386 on darwin and dragonfly. - goarch: 386 goos: dragonfly - goarch: 386 goos: darwin include: # BEIGIN MacOS ARM64 - goos: darwin goarch: arm64 # END MacOS ARM64 # BEGIN Linux ARM 5 6 7 - goos: linux goarch: arm goarm: 7 - goos: linux goarch: arm goarm: 6 - goos: linux goarch: arm goarm: 5 # END Linux ARM 5 6 7 # BEGIN Android ARM 8 - goos: android goarch: arm64 # END Android ARM 8 # Windows ARM 7 - goos: windows goarch: arm goarm: 7 # BEGIN Other architectures # BEGIN riscv64 & ARM64 - goos: linux goarch: arm64 - goos: linux goarch: riscv64 # END riscv64 & ARM64 # BEGIN MIPS - goos: linux goarch: mips64 - goos: linux goarch: mips64le - goos: linux goarch: mipsle - goos: linux goarch: mips # END MIPS # BEGIN PPC - goos: linux goarch: ppc64 - goos: linux goarch: ppc64le # END PPC # BEGIN FreeBSD ARM - goos: freebsd goarch: arm64 - goos: freebsd goarch: arm goarm: 7 # END FreeBSD ARM # BEGIN S390X - goos: linux goarch: s390x # END S390X # END Other architectures # BEGIN OPENBSD ARM - goos: openbsd goarch: arm64 - goos: openbsd goarch: arm goarm: 7 # END OPENBSD ARM fail-fast: false runs-on: ubuntu-latest env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} GOARM: ${{ matrix.goarm }} CGO_ENABLED: 0 steps: - name: Checkout codebase uses: actions/checkout@v2 - name: Show workflow information id: get_filename run: | export _NAME=$(jq ".[\"$GOOS-$GOARCH$GOARM$GOMIPS\"].friendlyName" -r < .github/build/friendly-filenames.json) echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, GOMIPS: $GOMIPS, RELEASE_NAME: $_NAME" echo "::set-output name=ASSET_NAME::$_NAME" echo "::set-output name=GIT_TAG::${GITHUB_REF##*/}" echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV - name: Set up Go uses: actions/setup-go@v5 with: go-version: ^1.22 - name: Get project dependencies run: go mod download - name: Build Transfersh run: | mkdir -p build_assets 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} - name: Build Mips softfloat Transfersh if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle' run: | 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} - name: Rename Windows Transfersh if: matrix.goos == 'windows' run: | cd ./build_assets || exit 1 mv transfersh-${GITHUB_REF##*/}-${ASSET_NAME} transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.exe - name: Prepare to release run: | cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE - name: Create Gzip archive shell: bash run: | pushd build_assets || exit 1 touch -mt $(date +%Y01010000) * tar zcvf transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz * mv transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz ../ FILE=`find . -name "transfersh-${GITHUB_REF##*/}-${ASSET_NAME}*"` DGST=$FILE.sha256sum echo `sha256sum $FILE` > $DGST popd || exit 1 FILE=./transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz DGST=$FILE.sha256sum echo `sha256sum $FILE` > $DGST - name: Change the name run: | mv build_assets transfersh-${GITHUB_REF##*/}-${ASSET_NAME} - name: Upload files to Artifacts uses: actions/upload-artifact@v2 with: name: transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }} path: | ./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}/* - name: Upload binaries to release uses: softprops/action-gh-release@v1 if: github.event_name == 'release' with: files: | ./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}.tar.gz* ./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 }}* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: pull_request: branches: - "*" push: branches: - "*" jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: go_version: - '1.22' - '1.23' - '1.24' - tip name: Test with ${{ matrix.go_version }} steps: - uses: actions/checkout@v2 - name: Install Go ${{ matrix.go_version }} if: ${{ matrix.go_version != 'tip' }} uses: actions/setup-go@master with: go-version: ${{ matrix.go_version }} check-latest: true - name: Install Go ${{ matrix.go_version }} if: ${{ matrix.go_version == 'tip' }} run: | go install golang.org/dl/gotip@latest `go env GOPATH`/bin/gotip download - name: Vet and test no tip if: ${{ matrix.go_version != 'tip' }} run: | go version go vet ./... go test ./... - name: Vet and test gotip if: ${{ matrix.go_version == 'tip' }} run: | `go env GOPATH`/bin/gotip version `go env GOPATH`/bin/gotip vet ./... `go env GOPATH`/bin/gotip test ./... golangci: name: Linting runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-go@master with: go-version: '1.24' check-latest: true - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: version: latest skip-go-installation: true args: "--config .golangci.yml" ================================================ FILE: .gitignore ================================================ build/ pkg/ dist/ src/ bin/ *.pyc *.egg-info/ .idea/ .tmp .vagrant bower_components/ node_modules/ transfersh-server/run.sh .elasticbeanstalk/ # Elastic Beanstalk Files .elasticbeanstalk/* !.elasticbeanstalk/*.cfg.yml !.elasticbeanstalk/*.global.yml !.github/build/ ================================================ FILE: .golangci.yml ================================================ run: deadline: 10m issues-exit-code: 1 tests: true output: format: colored-line-number print-issued-lines: true print-linter-name: true linters: disable: - deadcode - unused issues: max-issues-per-linter: 0 max-same-issues: 0 new: false exclude-use-default: false ================================================ FILE: .jshintrc ================================================ { "node": true, "browser": true, "esnext": true, "bitwise": true, "camelcase": true, "curly": true, "eqeqeq": true, "immed": true, "indent": 2, "latedef": true, "newcap": true, "noarg": true, "quotmark": "single", "regexp": true, "undef": true, "unused": true, "strict": true, "trailing": true, "smarttabs": true, "jquery": true, "white": true } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Code of Conduct 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. 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. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct * Use of harsh language 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. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 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 ================================================ FILE: Dockerfile ================================================ # Default to Go 1.24 ARG GO_VERSION=1.24 FROM golang:${GO_VERSION}-alpine as build # Necessary to run 'go get' and to compile the linked binary RUN apk add git musl-dev mailcap WORKDIR /go/src/github.com/dutchcoders/transfer.sh COPY go.mod go.sum ./ RUN go mod download COPY . . # build & install server RUN CGO_ENABLED=0 go build -tags netgo -ldflags "-X github.com/dutchcoders/transfer.sh/cmd.Version=$(git describe --tags) -a -s -w -extldflags '-static'" -o /go/bin/transfersh ARG PUID=5000 \ PGID=5000 \ RUNAS RUN mkdir -p /tmp/useradd /tmp/empty && \ if [ ! -z "$RUNAS" ]; then \ echo "${RUNAS}:x:${PUID}:${PGID}::/nonexistent:/sbin/nologin" >> /tmp/useradd/passwd && \ echo "${RUNAS}:!:::::::" >> /tmp/useradd/shadow && \ echo "${RUNAS}:x:${PGID}:" >> /tmp/useradd/group && \ echo "${RUNAS}:!::" >> /tmp/useradd/groupshadow; else touch /tmp/useradd/unused; fi FROM scratch AS final LABEL maintainer="Andrea Spacca " ARG RUNAS COPY --from=build /etc/mime.types /etc/mime.types COPY --from=build /tmp/empty /tmp COPY --from=build /tmp/useradd/* /etc/ COPY --from=build --chown=${RUNAS} /go/bin/transfersh /go/bin/transfersh COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt USER ${RUNAS} ENTRYPOINT ["/go/bin/transfersh", "--listener", ":8080"] EXPOSE 8080 ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014-2018 DutchCoders [https://github.com/dutchcoders/] Copyright (c) 2018-2020 Andrea Spacca. Copyright (c) 2020- Andrea Spacca and Stefan Benten. 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ================================================ FILE: Makefile ================================================ .PHONY: lint lint: golangci-lint run --out-format=github-actions --config .golangci.yml ================================================ FILE: README.md ================================================ # 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) Easy and fast file sharing from the command-line. This code contains the server with everything you need to create your own instance. Transfer.sh currently supports the s3 (Amazon S3), gdrive (Google Drive), storj (Storj) providers, and local file system (local).
---
## Disclaimer @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. The two are anyway unrelated, and the repo is not the place to direct requests and issues for any of the pubblic installation. No third-party public installation of the software in the repo will be advertised or mentioned in the repo itself, for security reasons. 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.
---
## Usage This section outlines how to use transfer.sh
### Upload ```bash $ curl -v --upload-file ./hello.txt https://transfer.sh/hello.txt ```
### Encrypt & Upload ```bash $ gpg --armor --symmetric --output - /tmp/hello.txt | curl --upload-file - https://transfer.sh/test.txt ```
### Download & Decrypt ```bash $ curl https://transfer.sh/1lDau/test.txt | gpg --decrypt --output /tmp/hello.txt ```
### Upload to Virustotal ```bash $ curl -X PUT --upload-file nhgbhhj https://transfer.sh/test.txt/virustotal ```
### Deleting ```bash $ curl -X DELETE ```
---
## Request Headers This section explains how to handle request headers with curl:
### Max-Downloads ```bash $ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Downloads: 1" # Limit the number of downloads ```
### Max-Days ```bash $ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Days: 1" # Set the number of days before deletion ```
### X-Encrypt-Password #### Beware, use this feature only on your self-hosted server: trusting a third-party service for server side encryption is at your own risk ```bash $ curl --upload-file ./hello.txt https://your-transfersh-instance.tld/hello.txt -H "X-Encrypt-Password: test" # Encrypt the content server side with AES256 using "test" as password ```
### X-Decrypt-Password #### Beware, use this feature only on your self-hosted server: trusting a third-party service for server side encryption is at your own risk ```bash $ curl https://your-transfersh-instance.tld/BAYh0/hello.txt -H "X-Decrypt-Password: test" # Decrypt the content server side with AES256 using "test" as password ```
---
## Response Headers This section explains how to handle response headers:
### X-Url-Delete The URL used to request the deletion of a file and returned as a response header: ```bash curl -sD - --upload-file ./hello.txt https://transfer.sh/hello.txt | grep -i -E 'transfer\.sh|x-url-delete' x-url-delete: https://transfer.sh/hello.txt/BAYh0/hello.txt/PDw0NHPcqU https://transfer.sh/hello.txt/BAYh0/hello.txt ```
---
## Examples See good usage examples on [examples.md](examples.md)
## Link aliases Create direct download link: https://transfer.sh/1lDau/test.txt --> https://transfer.sh/get/1lDau/test.txt Inline file: https://transfer.sh/1lDau/test.txt --> https://transfer.sh/inline/1lDau/test.txt
---
## Usage Parameter | Description | Value | Env --- |-----------------------------------------------------------------------------------------------|-------------------------------|-------------------------------| listener | port to use for http (:80) | | LISTENER | profile-listener | port to use for profiler (:6060) | | PROFILE_LISTENER | force-https | redirect to https | false | FORCE_HTTPS | tls-listener | port to use for https (:443) | | TLS_LISTENER | tls-listener-only | flag to enable tls listener only | | TLS_LISTENER_ONLY | tls-cert-file | path to tls certificate | | TLS_CERT_FILE | tls-private-key | path to tls private key | | TLS_PRIVATE_KEY | http-auth-user | user for basic http auth on upload | | HTTP_AUTH_USER | http-auth-pass | pass for basic http auth on upload | | HTTP_AUTH_PASS | http-auth-htpasswd | htpasswd file path for basic http auth on upload | | HTTP_AUTH_HTPASSWD | http-auth-ip-whitelist | comma separated list of allowed ips to upload without auth challenge | | HTTP_AUTH_IP_WHITELIST | virustotal-key | VirusTotal API key | | VIRUSTOTAL_KEY | ip-whitelist | comma separated list of ips allowed to connect to the service | | IP_WHITELIST | ip-blacklist | comma separated list of ips not allowed to connect to the service | | IP_BLACKLIST | temp-path | path to temp folder | system temp | TEMP_PATH | web-path | path to static web files (for development or custom front end) | | WEB_PATH | proxy-path | path prefix when service is run behind a proxy (a `/` prefix will be trimmed) | | PROXY_PATH | proxy-port | port of the proxy when the service is run behind a proxy | | PROXY_PORT | email-contact | email contact for the front end | | EMAIL_CONTACT | ga-key | google analytics key for the front end | | GA_KEY | provider | which storage provider to use | (s3, storj, gdrive or local) | | uservoice-key | user voice key for the front end | | USERVOICE_KEY | aws-access-key | aws access key | | AWS_ACCESS_KEY | aws-secret-key | aws access key | | AWS_SECRET_KEY | bucket | aws bucket | | BUCKET | s3-endpoint | Custom S3 endpoint. | | S3_ENDPOINT | s3-region | region of the s3 bucket | eu-west-1 | S3_REGION | s3-no-multipart | disables s3 multipart upload | false | S3_NO_MULTIPART | s3-path-style | Forces path style URLs, required for Minio. | false | S3_PATH_STYLE | storj-access | Access for the project | | STORJ_ACCESS | storj-bucket | Bucket to use within the project | | STORJ_BUCKET | basedir | path storage for local/gdrive provider | | BASEDIR | gdrive-client-json-filepath | path to oauth client json config for gdrive provider | | GDRIVE_CLIENT_JSON_FILEPATH | gdrive-local-config-path | path to store local transfer.sh config cache for gdrive provider | | GDRIVE_LOCAL_CONFIG_PATH | gdrive-chunk-size | chunk size for gdrive upload in megabytes, must be lower than available memory (8 MB) | | GDRIVE_CHUNK_SIZE | lets-encrypt-hosts | hosts to use for lets encrypt certificates (comma separated) | | HOSTS | log | path to log file | | LOG | cors-domains | comma separated list of domains for CORS, setting it enable CORS | | CORS_DOMAINS | clamav-host | host for clamav feature | | CLAMAV_HOST | perform-clamav-prescan | prescan every upload using clamav (clamav-host must be local clamd unix socket) | | PERFORM_CLAMAV_PRESCAN | rate-limit | request per minute | | RATE_LIMIT | max-upload-size | max upload size in kilobytes | | MAX_UPLOAD_SIZE | purge-days | number of days after the uploads are purged automatically | | PURGE_DAYS | purge-interval | interval (hours) to run automatic purge for (excluding S3 and Storj) | | PURGE_INTERVAL | random-token-length | length of random token for upload path (double the size for delete path) | 6 | RANDOM_TOKEN_LENGTH | If you want to use TLS using lets encrypt certificates, set lets-encrypt-hosts to your domain, set tls-listener to :443 and enable force-https. If you want to use TLS using your own certificates, set tls-listener to :443, force-https, tls-cert-file and tls-private-key.
---
## Development Switched to GO111MODULE ```bash go run main.go --provider=local --listener :8080 --temp-path=/tmp/ --basedir=/tmp/ ```
---
## Build ```bash $ git clone git@github.com:dutchcoders/transfer.sh.git $ cd transfer.sh $ go build -o transfersh main.go ```
---
## Docker For easy deployment, we've created an official Docker container. There are two variants, differing only by which user runs the process. The default one will run as `root`: > [!WARNING] > 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. ```bash docker run --publish 8080:8080 dutchcoders/transfer.sh:latest --provider local --basedir /tmp/ ```
### No root The `-noroot` tags indicate image builds that run with least priviledge to reduce the attack surface might an application get compromised. > [!NOTE] > Using `-noroot` is **recommended**
The one tagged with the suffix `-noroot` will use `5000` as both UID and GID: ```bash docker run --publish 8080:8080 dutchcoders/transfer.sh:latest-noroot --provider local --basedir /tmp/ ```
> [!NOTE] > Development history details at: > - https://github.com/dutchcoders/transfer.sh/pull/418
### Tags Name | Usage --|-- latest| Latest CI build, can be nightly, at commit, at tag, etc. latest-noroot| Latest CI build, can be nightly, at commit, at tag, etc. using [no root] nightly| Scheduled CI build every midnight UTC nightly-noroot| Scheduled CI build every midnight UTC using [no root] edge| Latest CI build after every commit on `main` edge-noroot| Latest CI build after every commit on `main` using [no root] v`x.y.z`| CI build after tagging a release v`x.y.z`-noroot| CI build after tagging a release using [no root]
### Building the Container You can also build the container yourself. This allows you to choose which UID/GID will be used, e.g. when using NFS mounts: ```bash # Build arguments: # * RUNAS: If empty, the container will run as root. # Set this to anything to enable UID/GID selection. # * PUID: UID of the process. Needs RUNAS != "". Defaults to 5000. # * PGID: GID of the process. Needs RUNAS != "". Defaults to 5000. docker build -t transfer.sh-noroot --build-arg RUNAS=doesntmatter --build-arg PUID=1337 --build-arg PGID=1338 . ```
---
## S3 Usage For the usage with a AWS S3 Bucket, you just need to specify the following options: - provider `--provider s3` - aws-access-key _(either via flag or environment variable `AWS_ACCESS_KEY`)_ - aws-secret-key _(either via flag or environment variable `AWS_SECRET_KEY`)_ - bucket _(either via flag or environment variable `BUCKET`)_ - s3-region _(either via flag or environment variable `S3_REGION`)_ If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically.
### Custom S3 providers To use a custom non-AWS S3 provider, you need to specify the endpoint as defined from your cloud provider.
---
## Storj Network Provider To use the Storj Network as a storage provider you need to specify the following flags: - provider `--provider storj` - storj-access _(either via flag or environment variable STORJ_ACCESS)_ - storj-bucket _(either via flag or environment variable STORJ_BUCKET)_
### Creating Bucket and Scope You need to create an access grant (or copy it from the uplink configuration) and a bucket in preparation. To get started, log in to your account and go to the Access Grant Menu and start the Wizard on the upper right. Enter your access grant name of choice, hit *Next* and restrict it as necessary/preferred. Afterwards continue either in CLI or within the Browser. Next, you'll be asked for a Passphrase used as Encryption Key. **Make sure to save it in a safe place. Without it, you will lose the ability to decrypt your files!** Afterwards, you can copy the access grant and then start the startup of the transfer.sh endpoint. It is recommended to provide both the access grant and the bucket name as ENV Variables for enhanced security. Example: ``` export STORJ_BUCKET= export STORJ_ACCESS= transfer.sh --provider storj ```
---
## Google Drive Usage For the usage with Google drive, you need to specify the following options: - provider - gdrive-client-json-filepath - gdrive-local-config-path - basedir
### Creating Gdrive Client Json You need to create an OAuth Client id from console.cloud.google.com, download the file, and place it into a safe directory.
### Usage example ```go run main.go --provider gdrive --basedir /tmp/ --gdrive-client-json-filepath /[credential_dir] --gdrive-local-config-path [directory_to_save_config] ```
---
## Shell functions ### Bash, ash and zsh (multiple files uploaded as zip archive) ##### Add this to .bashrc or .zshrc or its equivalent ```bash transfer() (if [ $# -eq 0 ]; then printf "No arguments specified.\nUsage:\n transfer \n ... | transfer \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"; ) ```
#### Now you can use transfer function ``` $ transfer hello.txt ```
### Bash and zsh (with delete url, delete token output and prompt before uploading) ##### Add this to .bashrc or .zshrc or its equivalent
Expand

```bash transfer() { local file declare -a file_array file_array=("${@}") if [[ "${file_array[@]}" == "" || "${1}" == "--help" || "${1}" == "-h" ]] then echo "${0} - Upload arbitrary files to \"transfer.sh\"." echo "" echo "Usage: ${0} [options] []..." echo "" echo "OPTIONS:" echo " -h, --help" echo " show this message" echo "" echo "EXAMPLES:" echo " Upload a single file from the current working directory:" echo " ${0} \"image.img\"" echo "" echo " Upload multiple files from the current working directory:" echo " ${0} \"image.img\" \"image2.img\"" echo "" echo " Upload a file from a different directory:" echo " ${0} \"/tmp/some_file\"" echo "" echo " Upload all files from the current working directory. Be aware of the webserver's rate limiting!:" echo " ${0} *" echo "" echo " Upload a single file from the current working directory and filter out the delete token and download link:" echo " ${0} \"image.img\" | awk --field-separator=\": \" '/Delete token:/ { print \$2 } /Download link:/ { print \$2 }'" echo "" echo " Show help text from \"transfer.sh\":" echo " curl --request GET \"https://transfer.sh\"" return 0 else for file in "${file_array[@]}" do if [[ ! -f "${file}" ]] then echo -e "\e[01;31m'${file}' could not be found or is not a file.\e[0m" >&2 return 1 fi done unset file fi local upload_files local curl_output local awk_output du -c -k -L "${file_array[@]}" >&2 # be compatible with "bash" if [[ "${ZSH_NAME}" == "zsh" ]] then read $'upload_files?\e[01;31mDo you really want to upload the above files ('"${#file_array[@]}"$') to "transfer.sh"? (Y/n): \e[0m' elif [[ "${BASH}" == *"bash"* ]] then read -p $'\e[01;31mDo you really want to upload the above files ('"${#file_array[@]}"$') to "transfer.sh"? (Y/n): \e[0m' upload_files fi case "${upload_files:-y}" in "y"|"Y") # for the sake of the progress bar, execute "curl" for each file. # the parameters "--include" and "--form" will suppress the progress bar. for file in "${file_array[@]}" do # show delete link and filter out the delete token from the response header after upload. # it is important to save "curl's" "stdout" via a subshell to a variable or redirect it to another command, # which just redirects to "stdout" in order to have a sane output afterwards. # the progress bar is redirected to "stderr" and is only displayed, # if "stdout" is redirected to something; e.g. ">/dev/null", "tee /dev/null" or "| ". # the response header is redirected to "stdout", so redirecting "stdout" to "/dev/null" does not make any sense. # redirecting "curl's" "stderr" to "stdout" ("2>&1") will suppress the progress bar. curl_output=$(curl --request PUT --progress-bar --dump-header - --upload-file "${file}" "https://transfer.sh/") awk_output=$(awk \ 'gsub("\r", "", $0) && tolower($1) ~ /x-url-delete/ \ { delete_link=$2; print "Delete command: curl --request DELETE " "\""delete_link"\""; gsub(".*/", "", delete_link); delete_token=delete_link; print "Delete token: " delete_token; } END{ print "Download link: " $0; }' <<< "${curl_output}") # return the results via "stdout", "awk" does not do this for some reason. echo -e "${awk_output}\n" # avoid rate limiting as much as possible; nginx: too many requests. if (( ${#file_array[@]} > 4 )) then sleep 5 fi done ;; "n"|"N") return 1 ;; *) echo -e "\e[01;31mWrong input: '${upload_files}'.\e[0m" >&2 return 1 esac } ```

#### Sample output ```bash $ ls -lh total 20M -rw-r--r-- 1 10M Apr 4 21:08 image.img -rw-r--r-- 1 10M Apr 4 21:08 image2.img $ transfer image* 10240K image2.img 10240K image.img 20480K total Do you really want to upload the above files (2) to "transfer.sh"? (Y/n): ######################################################################################################################################################################################################################################## 100.0% Delete command: curl --request DELETE "https://transfer.sh/wJw9pz/image2.img/mSctGx7pYCId" Delete token: mSctGx7pYCId Download link: https://transfer.sh/wJw9pz/image2.img ######################################################################################################################################################################################################################################## 100.0% Delete command: curl --request DELETE "https://transfer.sh/ljJc5I/image.img/nw7qaoiKUwCU" Delete token: nw7qaoiKUwCU Download link: https://transfer.sh/ljJc5I/image.img $ transfer "image.img" | awk --field-separator=": " '/Delete token:/ { print $2 } /Download link:/ { print $2 }' 10240K image.img 10240K total Do you really want to upload the above files (1) to "transfer.sh"? (Y/n): ######################################################################################################################################################################################################################################## 100.0% tauN5dE3fWJe https://transfer.sh/MYkuqn/image.img ```
---
## Contributions Contributions are welcome.
---
## Creators **Remco Verhoef** - - **Uvis Grinfelds**
---
## Maintainers - **Andrea Spacca** - **Stefan Benten**
---
## Copyright and License Code and documentation copyright 2011-2018 Remco Verhoef. Code and documentation copyright 2018-2020 Andrea Spacca. Code and documentation copyright 2020- Andrea Spacca and Stefan Benten. Code released under [the MIT license](LICENSE). ================================================ FILE: Vagrantfile ================================================ # -*- mode: ruby -*- # vi: set ft=ruby : # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| # Every Vagrant virtual environment requires a box to build off of. config.vm.box = "puphpet/ubuntu1404-x64" config.vm.provider "vmware_fusion" do |v| v.gui = true end end ================================================ FILE: cmd/cmd.go ================================================ package cmd import ( "errors" "fmt" "log" "os" "strings" "github.com/dutchcoders/transfer.sh/server/storage" "github.com/dutchcoders/transfer.sh/server" "github.com/fatih/color" "github.com/urfave/cli/v2" "google.golang.org/api/googleapi" ) // Version is inject at build time var Version = "0.0.0" var helpTemplate = `NAME: {{.Name}} - {{.Usage}} DESCRIPTION: {{.Description}} USAGE: {{.Name}} {{if .Flags}}[flags] {{end}}command{{if .Flags}}{{end}} [arguments...] COMMANDS: {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} {{end}}{{if .Flags}} FLAGS: {{range .Flags}}{{.}} {{end}}{{end}} VERSION: ` + Version + `{{ "\n"}}` var globalFlags = []cli.Flag{ &cli.StringFlag{ Name: "listener", Usage: "127.0.0.1:8080", Value: "127.0.0.1:8080", EnvVars: []string{"LISTENER"}, }, // redirect to https? // hostnames &cli.StringFlag{ Name: "profile-listener", Usage: "127.0.0.1:6060", Value: "", EnvVars: []string{"PROFILE_LISTENER"}, }, &cli.BoolFlag{ Name: "force-https", Usage: "", EnvVars: []string{"FORCE_HTTPS"}, }, &cli.StringFlag{ Name: "tls-listener", Usage: "127.0.0.1:8443", Value: "", EnvVars: []string{"TLS_LISTENER"}, }, &cli.BoolFlag{ Name: "tls-listener-only", Usage: "", EnvVars: []string{"TLS_LISTENER_ONLY"}, }, &cli.StringFlag{ Name: "tls-cert-file", Value: "", EnvVars: []string{"TLS_CERT_FILE"}, }, &cli.StringFlag{ Name: "tls-private-key", Value: "", EnvVars: []string{"TLS_PRIVATE_KEY"}, }, &cli.StringFlag{ Name: "temp-path", Usage: "path to temp files", Value: os.TempDir(), EnvVars: []string{"TEMP_PATH"}, }, &cli.StringFlag{ Name: "web-path", Usage: "path to static web files", Value: "", EnvVars: []string{"WEB_PATH"}, }, &cli.StringFlag{ Name: "proxy-path", Usage: "path prefix when service is run behind a proxy", Value: "", EnvVars: []string{"PROXY_PATH"}, }, &cli.StringFlag{ Name: "proxy-port", Usage: "port of the proxy when the service is run behind a proxy", Value: "", EnvVars: []string{"PROXY_PORT"}, }, &cli.StringFlag{ Name: "email-contact", Usage: "email address to link in Contact Us (front end)", Value: "", EnvVars: []string{"EMAIL_CONTACT"}, }, &cli.StringFlag{ Name: "ga-key", Usage: "key for google analytics (front end)", Value: "", EnvVars: []string{"GA_KEY"}, }, &cli.StringFlag{ Name: "uservoice-key", Usage: "key for user voice (front end)", Value: "", EnvVars: []string{"USERVOICE_KEY"}, }, &cli.StringFlag{ Name: "provider", Usage: "s3|gdrive|local", Value: "", EnvVars: []string{"PROVIDER"}, }, &cli.StringFlag{ Name: "s3-endpoint", Usage: "", Value: "", EnvVars: []string{"S3_ENDPOINT"}, }, &cli.StringFlag{ Name: "s3-region", Usage: "", Value: "eu-west-1", EnvVars: []string{"S3_REGION"}, }, &cli.StringFlag{ Name: "aws-access-key", Usage: "", Value: "", EnvVars: []string{"AWS_ACCESS_KEY"}, }, &cli.StringFlag{ Name: "aws-secret-key", Usage: "", Value: "", EnvVars: []string{"AWS_SECRET_KEY"}, }, &cli.StringFlag{ Name: "bucket", Usage: "", Value: "", EnvVars: []string{"BUCKET"}, }, &cli.BoolFlag{ Name: "s3-no-multipart", Usage: "Disables S3 Multipart Puts", EnvVars: []string{"S3_NO_MULTIPART"}, }, &cli.BoolFlag{ Name: "s3-path-style", Usage: "Forces path style URLs, required for Minio.", EnvVars: []string{"S3_PATH_STYLE"}, }, &cli.StringFlag{ Name: "gdrive-client-json-filepath", Usage: "", Value: "", EnvVars: []string{"GDRIVE_CLIENT_JSON_FILEPATH"}, }, &cli.StringFlag{ Name: "gdrive-local-config-path", Usage: "", Value: "", EnvVars: []string{"GDRIVE_LOCAL_CONFIG_PATH"}, }, &cli.IntFlag{ Name: "gdrive-chunk-size", Usage: "", Value: googleapi.DefaultUploadChunkSize / 1024 / 1024, EnvVars: []string{"GDRIVE_CHUNK_SIZE"}, }, &cli.StringFlag{ Name: "storj-access", Usage: "Access for the project", Value: "", EnvVars: []string{"STORJ_ACCESS"}, }, &cli.StringFlag{ Name: "storj-bucket", Usage: "Bucket to use within the project", Value: "", EnvVars: []string{"STORJ_BUCKET"}, }, &cli.IntFlag{ Name: "rate-limit", Usage: "requests per minute", Value: 0, EnvVars: []string{"RATE_LIMIT"}, }, &cli.IntFlag{ Name: "purge-days", Usage: "number of days after uploads are purged automatically", Value: 0, EnvVars: []string{"PURGE_DAYS"}, }, &cli.IntFlag{ Name: "purge-interval", Usage: "interval in hours to run the automatic purge for", Value: 0, EnvVars: []string{"PURGE_INTERVAL"}, }, &cli.Int64Flag{ Name: "max-upload-size", Usage: "max limit for upload, in kilobytes", Value: 0, EnvVars: []string{"MAX_UPLOAD_SIZE"}, }, &cli.StringFlag{ Name: "lets-encrypt-hosts", Usage: "host1, host2", Value: "", EnvVars: []string{"HOSTS"}, }, &cli.StringFlag{ Name: "log", Usage: "/var/log/transfersh.log", Value: "", EnvVars: []string{"LOG"}, }, &cli.StringFlag{ Name: "basedir", Usage: "path to storage", Value: "", EnvVars: []string{"BASEDIR"}, }, &cli.StringFlag{ Name: "clamav-host", Usage: "clamav-host", Value: "", EnvVars: []string{"CLAMAV_HOST"}, }, &cli.BoolFlag{ Name: "perform-clamav-prescan", Usage: "perform-clamav-prescan", EnvVars: []string{"PERFORM_CLAMAV_PRESCAN"}, }, &cli.StringFlag{ Name: "virustotal-key", Usage: "virustotal-key", Value: "", EnvVars: []string{"VIRUSTOTAL_KEY"}, }, &cli.BoolFlag{ Name: "profiler", Usage: "enable profiling", EnvVars: []string{"PROFILER"}, }, &cli.StringFlag{ Name: "http-auth-user", Usage: "user for http basic auth", Value: "", EnvVars: []string{"HTTP_AUTH_USER"}, }, &cli.StringFlag{ Name: "http-auth-pass", Usage: "pass for http basic auth", Value: "", EnvVars: []string{"HTTP_AUTH_PASS"}, }, &cli.StringFlag{ Name: "http-auth-htpasswd", Usage: "htpasswd file http basic auth", Value: "", EnvVars: []string{"HTTP_AUTH_HTPASSWD"}, }, &cli.StringFlag{ Name: "http-auth-ip-whitelist", Usage: "comma separated list of ips allowed to upload without being challenged an http auth", Value: "", EnvVars: []string{"HTTP_AUTH_IP_WHITELIST"}, }, &cli.StringFlag{ Name: "ip-whitelist", Usage: "comma separated list of ips allowed to connect to the service", Value: "", EnvVars: []string{"IP_WHITELIST"}, }, &cli.StringFlag{ Name: "ip-blacklist", Usage: "comma separated list of ips not allowed to connect to the service", Value: "", EnvVars: []string{"IP_BLACKLIST"}, }, &cli.StringFlag{ Name: "cors-domains", Usage: "comma separated list of domains allowed for CORS requests", Value: "", EnvVars: []string{"CORS_DOMAINS"}, }, &cli.IntFlag{ Name: "random-token-length", Usage: "", Value: 10, EnvVars: []string{"RANDOM_TOKEN_LENGTH"}, }, } // Cmd wraps cli.app type Cmd struct { *cli.App } func versionCommand(_ *cli.Context) error { fmt.Println(color.YellowString("transfer.sh %s: Easy file sharing from the command line", Version)) return nil } // New is the factory for transfer.sh func New() *Cmd { logger := log.New(os.Stdout, "[transfer.sh]", log.LstdFlags) app := cli.NewApp() app.Name = "transfer.sh" app.Authors = []*cli.Author{} app.Usage = "transfer.sh" app.Description = `Easy file sharing from the command line` app.Version = Version app.Flags = globalFlags app.CustomAppHelpTemplate = helpTemplate app.Commands = []*cli.Command{ { Name: "version", Action: versionCommand, }, } app.Before = func(c *cli.Context) error { return nil } app.Action = func(c *cli.Context) error { var options []server.OptionFn if v := c.String("listener"); v != "" { options = append(options, server.Listener(v)) } if v := c.String("cors-domains"); v != "" { options = append(options, server.CorsDomains(v)) } if v := c.String("tls-listener"); v == "" { } else if c.Bool("tls-listener-only") { options = append(options, server.TLSListener(v, true)) } else { options = append(options, server.TLSListener(v, false)) } if v := c.String("profile-listener"); v != "" { options = append(options, server.ProfileListener(v)) } if v := c.String("web-path"); v != "" { options = append(options, server.WebPath(v)) } if v := c.String("proxy-path"); v != "" { options = append(options, server.ProxyPath(v)) } if v := c.String("proxy-port"); v != "" { options = append(options, server.ProxyPort(v)) } if v := c.String("email-contact"); v != "" { options = append(options, server.EmailContact(v)) } if v := c.String("ga-key"); v != "" { options = append(options, server.GoogleAnalytics(v)) } if v := c.String("uservoice-key"); v != "" { options = append(options, server.UserVoice(v)) } if v := c.String("temp-path"); v != "" { options = append(options, server.TempPath(v)) } if v := c.String("log"); v != "" { options = append(options, server.LogFile(logger, v)) } else { options = append(options, server.Logger(logger)) } if v := c.String("lets-encrypt-hosts"); v != "" { options = append(options, server.UseLetsEncrypt(strings.Split(v, ","))) } if v := c.String("virustotal-key"); v != "" { options = append(options, server.VirustotalKey(v)) } if v := c.String("clamav-host"); v != "" { options = append(options, server.ClamavHost(v)) } if v := c.Bool("perform-clamav-prescan"); v { if c.String("clamav-host") == "" { return errors.New("clamav-host not set") } options = append(options, server.PerformClamavPrescan(v)) } if v := c.Int64("max-upload-size"); v > 0 { options = append(options, server.MaxUploadSize(v)) } if v := c.Int("rate-limit"); v > 0 { options = append(options, server.RateLimit(v)) } v := c.Int("random-token-length") options = append(options, server.RandomTokenLength(v)) purgeDays := c.Int("purge-days") purgeInterval := c.Int("purge-interval") if purgeDays > 0 && purgeInterval > 0 { options = append(options, server.Purge(purgeDays, purgeInterval)) } if cert := c.String("tls-cert-file"); cert == "" { } else if pk := c.String("tls-private-key"); pk == "" { } else { options = append(options, server.TLSConfig(cert, pk)) } if c.Bool("profiler") { options = append(options, server.EnableProfiler()) } if c.Bool("force-https") { options = append(options, server.ForceHTTPS()) } if httpAuthUser := c.String("http-auth-user"); httpAuthUser == "" { } else if httpAuthPass := c.String("http-auth-pass"); httpAuthPass == "" { } else { options = append(options, server.HTTPAuthCredentials(httpAuthUser, httpAuthPass)) } if httpAuthHtpasswd := c.String("http-auth-htpasswd"); httpAuthHtpasswd != "" { options = append(options, server.HTTPAuthHtpasswd(httpAuthHtpasswd)) } if httpAuthIPWhitelist := c.String("http-auth-ip-whitelist"); httpAuthIPWhitelist != "" { ipFilterOptions := server.IPFilterOptions{} ipFilterOptions.AllowedIPs = strings.Split(httpAuthIPWhitelist, ",") ipFilterOptions.BlockByDefault = true options = append(options, server.HTTPAUTHFilterOptions(ipFilterOptions)) } applyIPFilter := false ipFilterOptions := server.IPFilterOptions{} if ipWhitelist := c.String("ip-whitelist"); ipWhitelist != "" { applyIPFilter = true ipFilterOptions.AllowedIPs = strings.Split(ipWhitelist, ",") ipFilterOptions.BlockByDefault = true } if ipBlacklist := c.String("ip-blacklist"); ipBlacklist != "" { applyIPFilter = true ipFilterOptions.BlockedIPs = strings.Split(ipBlacklist, ",") } if applyIPFilter { options = append(options, server.FilterOptions(ipFilterOptions)) } switch provider := c.String("provider"); provider { case "s3": if accessKey := c.String("aws-access-key"); accessKey == "" { return errors.New("access-key not set.") } else if secretKey := c.String("aws-secret-key"); secretKey == "" { return errors.New("secret-key not set.") } else if bucket := c.String("bucket"); bucket == "" { return errors.New("bucket not set.") } else if store, err := storage.NewS3Storage(c.Context, accessKey, secretKey, bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil { return err } else { options = append(options, server.UseStorage(store)) } case "gdrive": chunkSize := c.Int("gdrive-chunk-size") * 1024 * 1024 if clientJSONFilepath := c.String("gdrive-client-json-filepath"); clientJSONFilepath == "" { return errors.New("gdrive-client-json-filepath not set.") } else if localConfigPath := c.String("gdrive-local-config-path"); localConfigPath == "" { return errors.New("gdrive-local-config-path not set.") } else if basedir := c.String("basedir"); basedir == "" { return errors.New("basedir not set.") } else if store, err := storage.NewGDriveStorage(c.Context, clientJSONFilepath, localConfigPath, basedir, chunkSize, logger); err != nil { return err } else { options = append(options, server.UseStorage(store)) } case "storj": if access := c.String("storj-access"); access == "" { return errors.New("storj-access not set.") } else if bucket := c.String("storj-bucket"); bucket == "" { return errors.New("storj-bucket not set.") } else if store, err := storage.NewStorjStorage(c.Context, access, bucket, purgeDays, logger); err != nil { return err } else { options = append(options, server.UseStorage(store)) } case "local": if v := c.String("basedir"); v == "" { return errors.New("basedir not set.") } else if store, err := storage.NewLocalStorage(v, logger); err != nil { return err } else { options = append(options, server.UseStorage(store)) } default: return errors.New("Provider not set or invalid.") } srvr, err := server.New( options..., ) if err != nil { logger.Println(color.RedString("Error starting server: %s", err.Error())) return err } srvr.Run() return nil } return &Cmd{ App: app, } } ================================================ FILE: examples.md ================================================ # Table of Contents * [Aliases](#aliases) * [Uploading and downloading](#uploading-and-downloading) * [Archiving and backups](#archiving-and-backups) * [Encrypting and decrypting](#encrypting-and-decrypting) * [Scanning for viruses](#scanning-for-viruses) * [Uploading and copy download command](#uploading-and-copy-download-command) * [Uploading and displaying URL and deletion token](#uploading-and-displaying-url-and-deletion-token) ## Aliases ## Add alias to .bashrc or .zshrc ### Using curl ```bash transfer() { curl --progress-bar --upload-file "$1" https://transfer.sh/$(basename "$1") | tee /dev/null; echo } alias transfer=transfer ``` ### Using wget ```bash transfer() { wget -t 1 -qO - --method=PUT --body-file="$1" --header="Content-Type: $(file -b --mime-type "$1")" https://transfer.sh/$(basename "$1"); echo } alias transfer=transfer ``` ## Add alias for fish-shell ### Using curl ```fish function transfer --description 'Upload a file to transfer.sh' if [ $argv[1] ] # write to output to tmpfile because of progress bar set -l tmpfile ( mktemp -t transferXXXXXX ) curl --progress-bar --upload-file "$argv[1]" https://transfer.sh/(basename $argv[1]) >> $tmpfile cat $tmpfile command rm -f $tmpfile else echo 'usage: transfer FILE_TO_TRANSFER' end end funcsave transfer ``` ### Using wget ```fish function transfer --description 'Upload a file to transfer.sh' if [ $argv[1] ] 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]) else echo 'usage: transfer FILE_TO_TRANSFER' end end funcsave transfer ``` Now run it like this: ```bash $ transfer test.txt ``` ## Add alias on Windows Put a file called `transfer.cmd` somewhere in your PATH with this inside it: ```cmd @echo off setlocal :: use env vars to pass names to PS, to avoid escaping issues set FN=%~nx1 set FULL=%1 powershell -noprofile -command "$(Invoke-Webrequest -Method put -Infile $Env:FULL https://transfer.sh/$Env:FN).Content" ``` ## Uploading and Downloading ### Uploading with wget ```bash $ wget --method PUT --body-file=/tmp/file.tar https://transfer.sh/file.tar -O - -nv ``` ### Uploading with PowerShell ```posh PS H:\> invoke-webrequest -method put -infile .\file.txt https://transfer.sh/file.txt ``` ### Upload using HTTPie ```bash $ http https://transfer.sh/ -vv < /tmp/test.log ``` ### Uploading a filtered text file ```bash $ grep 'pound' /var/log/syslog | curl --upload-file - https://transfer.sh/pound.log ``` ### Downloading with curl ```bash $ curl https://transfer.sh/1lDau/test.txt -o test.txt ``` ### Downloading with wget ```bash $ wget https://transfer.sh/1lDau/test.txt ``` ## Archiving and backups ### Backup, encrypt and transfer a MySQL dump ```bash $ mysqldump --all-databases | gzip | gpg -ac -o- | curl -X PUT --upload-file "-" https://transfer.sh/test.txt ``` ### Archive and upload directory ```bash $ tar -czf - /var/log/journal | curl --upload-file - https://transfer.sh/journal.tar.gz ``` ### Uploading multiple files at once ```bash $ curl -i -F filedata=@/tmp/hello.txt -F filedata=@/tmp/hello2.txt https://transfer.sh/ ``` ### Combining downloads as zip or tar.gz archive ```bash $ curl https://transfer.sh/(15HKz/hello.txt,15HKz/hello.txt).tar.gz $ curl https://transfer.sh/(15HKz/hello.txt,15HKz/hello.txt).zip ``` ### Transfer and send email with link (using an alias) ```bash $ transfer /tmp/hello.txt | mail -s "Hello World" user@yourmaildomain.com ``` ## Encrypting and decrypting ### Encrypting files with password using gpg ```bash $ gpg --armor --symmetric --output - /tmp/hello.txt | curl --upload-file - https://transfer.sh/test.txt ``` ### Downloading and decrypting ```bash $ curl https://transfer.sh/1lDau/test.txt | gpg --decrypt --output /tmp/hello.txt ``` ### Import keys from [keybase](https://keybase.io/) ```bash $ keybase track [them] # Encrypt for recipient(s) $ cat somebackupfile.tar.gz | keybase encrypt [them] | curl --upload-file '-' https://transfer.sh/test.txt # Decrypt $ curl https://transfer.sh/sqUFi/test.md | keybase decrypt ``` ## Scanning for viruses ### Scan for malware or viruses using Clamav ```bash $ wget http://www.eicar.org/download/eicar.com $ curl -X PUT --upload-file ./eicar.com https://transfer.sh/eicar.com/scan ``` ### Upload malware to VirusTotal, get a permalink in return ```bash $ curl -X PUT --upload-file nhgbhhj https://transfer.sh/test.txt/virustotal ``` ### Upload encrypted password protected files By default files upload for only 1 download, you can specify download limit using -D flag like `transfer-encrypted -D 50 %file/folder%` #### One line for bashrc ```bash transfer-encrypted() { if [ $# -eq 0 ]; then echo "No arguments specified.\nUsage:\n transfer \n ... | transfer " >&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; } ``` #### Human readable code ```bash transfer-encrypted() { if [ $# -eq 0 ]; then echo "No arguments specified.\nUsage:\n transfer \n ... | transfer " >&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 } ``` #### Decrypt using ```bash curl -s https://transfer.sh/some/file | openssl aes-256-cbc -pbkdf2 -d > output_filename ``` ## Uploading and copy download command Download commands can be automatically copied to the clipboard after files are uploaded using transfer.sh. It was designed for Linux or macOS. ### 1. Install xclip or xsel for Linux, macOS skips this step - install xclip see https://command-not-found.com/xclip - install xsel see https://command-not-found.com/xsel Install later, add pbcopy and pbpaste to .bashrc or .zshrc or its equivalent. - If use xclip, paste the following lines: ```sh alias pbcopy='xclip -selection clipboard' alias pbpaste='xclip -selection clipboard -o' ``` - If use xsel, paste the following lines: ```sh alias pbcopy='xsel --clipboard --input' alias pbpaste='xsel --clipboard --output' ``` ### 2. Add Uploading and copy download command shell function 1. Open .bashrc or .zshrc or its equivalent. 2. Add the following shell script: ```sh transfer() { curl --progress-bar --upload-file "$1" https://transfer.sh/$(basename "$1") | pbcopy; echo "1) Download link:" echo "$(pbpaste)" echo "\n2) Linux or macOS download command:" linux_macos_download_command="wget $(pbpaste)" echo $linux_macos_download_command echo "\n3) Windows download command:" windows_download_command="Invoke-WebRequest -Uri "$(pbpaste)" -OutFile $(basename $1)" echo $windows_download_command case $2 in l|m) echo $linux_macos_download_command | pbcopy ;; w) echo $windows_download_command | pbcopy ;; esac } ``` ### 3. Test The transfer command has two parameters: 1. The first parameter is the path to upload the file. 2. The second parameter indicates which system's download command is copied. optional: - This parameter is empty to copy the download link. - `l` or `m` copy the Linux or macOS command that downloaded the file. - `w` copy the Windows command that downloaded the file. For example, The command to download the file on Windows will be copied: ```sh $ transfer ~/temp/a.log w ######################################################################## 100.0% 1) Download link: https://transfer.sh/y0qr2c/a.log 2) Linux or macOS download command: wget https://transfer.sh/y0qr2c/a.log 3) Windows download command: Invoke-WebRequest -Uri https://transfer.sh/y0qr2c/a.log -OutFile a.log ``` ## Uploading and displaying URL and deletion token ```bash # tempfile URLFILE=$HOME/temp/transfersh.url # insert number of downloads and days saved if [ -f $1 ]; then read -p "Allowed number of downloads: " num_down read -p "Number of days on server: " num_save # transfer 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 # display URL and deletion token if [ -f $URLFILE ]; then URL=$(tail -n1 $URLFILE) TOKEN=$(grep delete $URLFILE | awk -F "/" '{print $NF}') echo "*********************************" echo "Data is saved in $URLFILE" echo "**********************************" echo "URL is: $URL" echo "Deletion Token is: $TOKEN" echo "**********************************" else echo "NO URL-File found !!" fi else echo "!!!!!!" echo "\"$1\" not found !!" echo "!!!!!!" fi ``` ================================================ FILE: extras/clamd ================================================ #! /bin/sh ### BEGIN INIT INFO # Provides: skeleton # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Example initscript # Description: This file should be used to construct scripts to be # placed in /etc/init.d. ### END INIT INFO # Author: Foo Bar # # Please remove the "Author" lines above and replace them # with your own name if you copy and modify this script. # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="Clam Daemon" NAME=clamd DAEMON="/usr/local/sbin/clamd" DAEMON_ARGS="-c /usr/local/etc/clamd.conf" PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.2-14) to ensure that this file is present # and status_of_proc is working. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --background --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --background --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Wait for children to finish too if this is a daemon that forks # and if the daemon is only ever run from this initscript. # If the above conditions are not satisfied then add some other code # that waits for the process to drop all resources that could be # needed by services started subsequently. A last resort is to # sleep for some time. start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; status) status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac : ================================================ FILE: extras/transfersh ================================================ #! /bin/sh ### BEGIN INIT INFO # Provides: skeleton # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Example initscript # Description: This file should be used to construct scripts to be # placed in /etc/init.d. ### END INIT INFO # Author: Foo Bar # # Please remove the "Author" lines above and replace them # with your own name if you copy and modify this script. # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/go/bin DESC="Transfersh Web server" NAME=transfersh DAEMON="/opt/transfer.sh/main" DAEMON_ARGS="--port 80 --temp /tmp/" PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME export BUCKET={bucket} export AWS_ACCESS_KEY={aws_access_key} export AWS_SECRET_KEY={aws_secret_key} export VIRUSTOTAL_KEY={virustotal_key} export GOPATH=/opt/go/ # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.2-14) to ensure that this file is present # and status_of_proc is working. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --background --start --chdir /opt/transfer.sh --quiet --pidfile $PIDFILE --make-pidfile --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --background --start --chdir /opt/transfer.sh --quiet --pidfile $PIDFILE --make-pidfile --exec $DAEMON -- \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Wait for children to finish too if this is a daemon that forks # and if the daemon is only ever run from this initscript. # If the above conditions are not satisfied then add some other code # that waits for the process to drop all resources that could be # needed by services started subsequently. A last resort is to # sleep for some time. start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; status) status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac : ================================================ FILE: flake.nix ================================================ { description = "Transfer.sh"; inputs.flake-utils.url = "github:numtide/flake-utils"; outputs = { self, nixpkgs, flake-utils }: let transfer-sh = pkgs: pkgs.buildGoModule { src = self; name = "transfer.sh"; vendorSha256 = "sha256-bgQUMiC33yVorcKOWhegT1/YU+fvxsz2pkeRvjf3R7g="; }; in flake-utils.lib.eachDefaultSystem ( system: let pkgs = nixpkgs.legacyPackages.${system}; in rec { packages = flake-utils.lib.flattenTree { transfer-sh = transfer-sh pkgs; }; defaultPackage = packages.transfer-sh; apps.transfer-sh = flake-utils.lib.mkApp { drv = packages.transfer-sh; }; defaultApp = apps.transfer-sh; } ) // rec { nixosModules = { transfer-sh = { config, lib, pkgs, ... }: with lib; let RUNTIME_DIR = "/var/lib/transfer.sh"; cfg = config.services.transfer-sh; general_options = { enable = mkEnableOption "Transfer.sh service"; listener = mkOption { default = 80; type = types.int; description = "port to use for http (:80)"; }; profile-listener = mkOption { default = 6060; type = types.int; description = "port to use for profiler (:6060)"; }; force-https = mkOption { type = types.nullOr types.bool; description = "redirect to https"; }; tls-listener = mkOption { default = 443; type = types.int; description = "port to use for https (:443)"; }; tls-listener-only = mkOption { type = types.nullOr types.bool; description = "flag to enable tls listener only"; }; tls-cert-file = mkOption { type = types.nullOr types.str; description = "path to tls certificate"; }; tls-private-key = mkOption { type = types.nullOr types.str; description = "path to tls private key "; }; http-auth-user = mkOption { type = types.nullOr types.str; description = "user for basic http auth on upload"; }; http-auth-pass = mkOption { type = types.nullOr types.str; description = "pass for basic http auth on upload"; }; http-auth-htpasswd = mkOption { type = types.nullOr types.str; description = "htpasswd file path for basic http auth on upload"; }; http-auth-ip-whitelist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips allowed to upload without being challenged an http auth"; }; ip-whitelist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips allowed to connect to the service"; }; ip-blacklist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips not allowed to connect to the service"; }; temp-path = mkOption { type = types.nullOr types.str; description = "path to temp folder"; }; web-path = mkOption { type = types.nullOr types.str; description = "path to static web files (for development or custom front end)"; }; proxy-path = mkOption { type = types.nullOr types.str; description = "path prefix when service is run behind a proxy"; }; proxy-port = mkOption { type = types.nullOr types.str; description = "port of the proxy when the service is run behind a proxy"; }; ga-key = mkOption { type = types.nullOr types.str; description = "google analytics key for the front end"; }; email-contact = mkOption { type = types.nullOr types.str; description = "email contact for the front end"; }; uservoice-key = mkOption { type = types.nullOr types.str; description = "user voice key for the front end"; }; lets-encrypt-hosts = mkOption { type = types.nullOr (types.listOf types.str); description = "hosts to use for lets encrypt certificates"; }; log = mkOption { type = types.nullOr types.str; description = "path to log file"; }; cors-domains = mkOption { type = types.nullOr (types.listOf types.str); description = "comma separated list of domains for CORS, setting it enable CORS "; }; clamav-host = mkOption { type = types.nullOr types.str; description = "host for clamav feature"; }; rate-limit = mkOption { type = types.nullOr types.int; description = "request per minute"; }; max-upload-size = mkOption { type = types.nullOr types.int; description = "max upload size in kilobytes "; }; purge-days = mkOption { type = types.nullOr types.int; description = "number of days after the uploads are purged automatically "; }; 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)"; }; }; provider_options = { aws = { enable = mkEnableOption "Enable AWS backend"; aws-access-key = mkOption { type = types.str; description = "aws access key"; }; aws-secret-key = mkOption { type = types.str; description = "aws secret key"; }; bucket = mkOption { type = types.str; description = "aws bucket "; }; s3-endpoint = mkOption { type = types.nullOr types.str; description = '' Custom S3 endpoint. If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically. ''; }; s3-region = mkOption { type = types.str; description = "region of the s3 bucket eu-west-"; }; s3-no-multipart = mkOption { type = types.nullOr types.bool; description = "disables s3 multipart upload "; }; s3-path-style = mkOption { type = types.nullOr types.str; description = "Forces path style URLs, required for Minio. "; }; }; storj = { enable = mkEnableOption "Enable storj backend"; storj-access = mkOption { type = types.str; description = "Access for the project"; }; storj-bucket = mkOption { type = types.str; description = "Bucket to use within the project"; }; }; gdrive = { enable = mkEnableOption "Enable gdrive backend"; gdrive-client-json = mkOption { type = types.str; description = "oauth client json config for gdrive provider"; }; 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)"; }; basedir = mkOption { type = types.str; description = "path storage for gdrive provider"; default = "${cfg.stateDir}/store"; }; purge-interval = mkOption { type = types.nullOr types.int; description = "interval in hours to run the automatic purge for (not applicable to S3 and Storj)"; }; }; local = { enable = mkEnableOption "Enable local backend"; basedir = mkOption { type = types.str; description = "path storage for local provider"; default = "${cfg.stateDir}/store"; }; purge-interval = mkOption { type = types.nullOr types.int; description = "interval in hours to run the automatic purge for (not applicable to S3 and Storj)"; }; }; }; in { options.services.transfer-sh = fold recursiveUpdate {} [ general_options { provider = provider_options; user = mkOption { type = types.str; description = "User to run the service under"; default = "transfer.sh"; }; group = mkOption { type = types.str; description = "Group to run the service under"; default = "transfer.sh"; }; stateDir = mkOption { type = types.path; description = "Variable state directory"; default = RUNTIME_DIR; }; } ]; config = let mkFlags = cfg: options: let mkBoolFlag = option: if cfg.${option} then [ "--${option}" ] else []; mkFlag = option: if isBool cfg.${option} then mkBoolFlag option else [ "--${option}" "${cfg.${option}}" ]; in lists.flatten (map (mkFlag) (filter (option: cfg.${option} != null && option != "enable") options)); aws-config = (mkFlags cfg.provider.aws (attrNames provider_options)) ++ [ "--provider" "aws" ]; gdrive-config = mkFlags cfg.provider.gdrive (attrNames provider_options.gdrive) ++ [ "--provider" "gdrive" ]; storj-config = mkFlags cfg.provider.storj (attrNames provider_options.storj) ++ [ "--provider" "storj" ]; local-config = mkFlags cfg.provider.local (attrNames provider_options.local) ++ [ "--provider" "local" ]; general-config = concatStringsSep " " (mkFlags cfg (attrNames general_options)); provider-config = concatStringsSep " " ( if cfg.provider.aws.enable && !cfg.provider.storj.enable && !cfg.provider.gdrive.enable && !cfg.provider.local.enable then aws-config else if !cfg.provider.aws.enable && cfg.provider.storj.enable && !cfg.provider.gdrive.enable && !cfg.provider.local.enable then storj-config else if !cfg.provider.aws.enable && !cfg.provider.storj.enable && cfg.provider.gdrive.enable && !cfg.provider.local.enable then gdrive-config else if !cfg.provider.aws.enable && !cfg.provider.storj.enable && !cfg.provider.gdrive.enable && cfg.provider.local.enable then local-config else throw "transfer.sh requires exactly one provider (aws, storj, gdrive, local)" ); in lib.mkIf cfg.enable { systemd.tmpfiles.rules = [ "d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -" ] ++ optional cfg.provider.gdrive.enable cfg.provider.gdrive.basedir ++ optional cfg.provider.local.enable cfg.provider.local.basedir; systemd.services.transfer-sh = { wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; serviceConfig = { User = cfg.user; Group = cfg.group; ExecStart = "${transfer-sh pkgs}/bin/transfer.sh ${general-config} ${provider-config} "; }; }; networking.firewall.allowedTCPPorts = [ cfg.listener cfg.profile-listener cfg.tls-listener ]; }; }; default = { self, pkgs, ... }: { imports = [ nixosModules.transfer-sh ]; # Network configuration. # useDHCP is generally considered to better be turned off in favor # of .useDHCP networking.useDHCP = false; networking.firewall.allowedTCPPorts = []; # Enable the inventaire server. services.transfer-sh = { enable = true; provider.local = { enable = true; }; }; nixpkgs.config.allowUnfree = true; }; }; nixosConfigurations."container" = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ nixosModules.default ({ ... }: { boot.isContainer = true; }) ]; }; }; } ================================================ FILE: go.mod ================================================ module github.com/dutchcoders/transfer.sh go 1.22.0 require ( github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 github.com/ProtonMail/gopenpgp/v2 v2.5.2 github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14 github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2 github.com/aws/aws-sdk-go-v2 v1.18.0 github.com/aws/aws-sdk-go-v2/config v1.18.25 github.com/aws/aws-sdk-go-v2/credentials v1.13.24 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e github.com/Aetherinox/go-virustotal v0.0.0-20250520084801-0eb8c8f901c8 github.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6 github.com/elazarl/go-bindata-assetfs v1.0.1 github.com/fatih/color v1.14.1 github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/microcosm-cc/bluemonday v1.0.23 github.com/russross/blackfriday/v2 v2.1.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tg123/go-htpasswd v1.2.1 github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce github.com/urfave/cli/v2 v2.25.3 golang.org/x/crypto v0.21.0 golang.org/x/net v0.23.0 golang.org/x/oauth2 v0.7.0 golang.org/x/text v0.14.0 google.golang.org/api v0.114.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c storj.io/common v0.0.0-20230301105927-7f966760c100 storj.io/uplink v1.10.0 ) require ( cloud.google.com/go/compute v1.19.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect github.com/aws/smithy-go v1.13.5 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/calebcase/tmpfile v1.0.3 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/flynn/noise v1.0.0 // indirect github.com/garyburd/redigo v1.6.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/googleapis/gax-go/v2 v2.7.1 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f // indirect github.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/spacemonkeygo/monkit/v3 v3.0.19 // indirect github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/errs v1.3.0 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.18.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.56.3 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a // indirect storj.io/picobuf v0.0.1 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw= github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/ProtonMail/go-crypto v0.0.0-20230124153114-0acdc8ae009b/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM= github.com/ProtonMail/gopenpgp/v2 v2.5.2 h1:97SjlWNAxXl9P22lgwgrZRshQdiEfAht0g3ZoiA1GCw= github.com/ProtonMail/gopenpgp/v2 v2.5.2/go.mod h1:52qDaCnto6r+CoWbuU50T77XQt99lIs46HtHtvgFO3o= github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14 h1:3zOOc7WdrATDXof+h/rBgMsg0sAmZIEVHft1UbWHh94= github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14/go.mod h1:+VFiaivV54Sa94ijzA/ZHQLoHuoUIS9hIqCK6f/76Zw= github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2 h1:sIvihcW4qpN5qGSjmrsDDAbLpEq5tuHjJJfWY0Hud5Y= github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2/go.mod h1:3YwJE8rEisS9eraee0hygGG4G3gqX8H8Nyu+nPTUnGU= github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY= github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= github.com/aws/aws-sdk-go-v2/config v1.18.25 h1:JuYyZcnMPBiFqn87L2cRppo+rNwgah6YwD3VuyvaW6Q= github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4= github.com/aws/aws-sdk-go-v2/credentials v1.13.24 h1:PjiYyls3QdCrzqUN35jMWtUK1vqVZ+zLfdOa/UPFDp0= github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 h1:fI9/5BDEaAv/pv1VO1X1n3jfP9it+IGqWsCuuBQI8wM= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67/go.mod h1:zQClPRIwQZfJlZq6WZve+s4Tb4JW+3V6eS+4+KrYeP8= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM= github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 h1:O+9nAy9Bb6bJFTpeNFtd9UfHbgxO1o4ZDAM9rQp5NsY= github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8= github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 h1:UBQjaMTCKwyUYwiVnUt6toEJwGXsLBI6al083tpjJzY= github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk= github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E= github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8= github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/calebcase/tmpfile v1.0.3 h1:BZrOWZ79gJqQ3XbAQlihYZf/YCV0H4KPIdM5K5oMpJo= github.com/calebcase/tmpfile v1.0.3/go.mod h1:UAUc01aHeC+pudPagY/lWvt2qS9ZO5Zzof6/tIUzqeI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e h1:rcHHSQqzCgvlwP0I/fQ8rQMn/MpHE5gWSLdtpxtP6KQ= github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e/go.mod h1:Byz7q8MSzSPkouskHJhX0er2mZY/m0Vj5bMeMCkkyY4= github.com/Aetherinox/go-virustotal v0.0.0-20250520084801-0eb8c8f901c8 h1:wEwYJxNLG29OesabDdAJWFBIO42HOL4x5kjvGuZLIyk= github.com/Aetherinox/go-virustotal v0.0.0-20250520084801-0eb8c8f901c8/go.mod h1:myGG2GhfY2AgAPe8lFZw6Y1+IxhU+ED7ilotbpdQsDw= github.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6 h1:7uTRy44YpQi6/mtDq0N9zeQRCGEh93o7gKq/usGgpF8= github.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6/go.mod h1:F6Q37CxDh2MHr5KXkcZmNB3tdkK7v+bgE+OpBY+9ilI= github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg= github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg= github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20211108044417-e9b028704de0 h1:rsq1yB2xiFLDYYaYdlGBsSkwVzsCo500wMhxvW5A/bk= github.com/google/pprof v0.0.0-20211108044417-e9b028704de0/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A= github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f h1:HM2D/tnqbzNoN5DGIeB8ibM1BMYCkRWOqyWWcNAWw8o= github.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f/go.mod h1:PXFUrknJu7TkBNyL8t7XWDPtDFFLFrNQQAdsXv9YfJE= github.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059 h1:4xdaxDg3xe+DKZC8NjbH/gvTs4iNYUnzOAiD5QL5NrM= github.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059/go.mod h1:f0ijQHcvHYAuxX6JA/JUr/Z0FVn12D9REaT/HAWVgP4= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shuLhan/go-bindata v4.0.0+incompatible/go.mod h1:pkcPAATLBDD2+SpAPnX5vEM90F7fcwHCvvLCMXcmw3g= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/spacemonkeygo/monkit/v3 v3.0.19 h1:wqBb9bpD7jXkVi4XwIp8jn1fektaVBQ+cp9SHRXgAdo= github.com/spacemonkeygo/monkit/v3 v3.0.19/go.mod h1:kj1ViJhlyADa7DiA4xVnTuPA46lFKbM7mxQTrXCuJP4= github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tg123/go-htpasswd v1.2.1 h1:i4wfsX1KvvkyoMiHZzjS0VzbAPWfxzI8INcZAKtutoU= github.com/tg123/go-htpasswd v1.2.1/go.mod h1:erHp1B86KXdwQf1X5ZrLb7erXZnWueEQezb2dql4q58= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 h1:zMsHhfK9+Wdl1F7sIKLyx3wrOFofpb3rWFbA4HgcK5k= github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3/go.mod h1:R0Gbuw7ElaGSLOZUSwBm/GgVwMd30jWxBDdAyMOeTuc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A= github.com/zeebo/assert v1.3.1/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= storj.io/common v0.0.0-20230301105927-7f966760c100 h1:0Rc6boo10ZgiHdadHi1o2OUv25YvTn8fSc/VyRz2Tyk= storj.io/common v0.0.0-20230301105927-7f966760c100/go.mod h1:tDgoLthBVcrTPEokBgPdjrn39p/gyNx06j6ehhTSiUg= storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a h1:FBaOc8c5efmW3tmPsiGy07USMkOSu/tyYCZpu2ro0y8= storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a/go.mod h1:6rcOyR/QQkSTX/9L5ZGtlZaE2PtXTTZl8d+ulSeeYEg= storj.io/picobuf v0.0.1 h1:ekEvxSQCbEjTVIi/qxj2za13SJyfRE37yE30IBkZeT0= storj.io/picobuf v0.0.1/go.mod h1:7ZTAMs6VesgTHbbhFU79oQ9hDaJ+MD4uoFQZ1P4SEz0= storj.io/uplink v1.10.0 h1:3hS0hszupHSxEoC4DsMpljaRy0uNoijEPVF6siIE28Q= storj.io/uplink v1.10.0/go.mod h1:gJIQumB8T3tBHPRive51AVpbc+v2xe+P/goFNMSRLG4= ================================================ FILE: main.go ================================================ package main import ( "log" "os" "github.com/dutchcoders/transfer.sh/cmd" ) func main() { app := cmd.New() err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ================================================ FILE: manifest.json ================================================ { "dependencies": { "github.com/dutchcoders/transfer.sh-web": { "branch": "master" } } } ================================================ FILE: server/clamav.go ================================================ /* The MIT License (MIT) Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/] Copyright (c) 2018-2020 Andrea Spacca. Copyright (c) 2020- Andrea Spacca and Stefan Benten. 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. */ package server import ( "errors" "fmt" "io" "net/http" "os" "time" "github.com/dutchcoders/go-clamd" "github.com/gorilla/mux" ) const clamavScanStatusOK = "OK" func (s *Server) scanHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) filename := sanitize(vars["filename"]) contentLength := r.ContentLength contentType := r.Header.Get("Content-Type") s.logger.Printf("Scanning %s %d %s", filename, contentLength, contentType) file, err := os.CreateTemp(s.tempPath, "clamav-") defer s.cleanTmpFile(file) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } _, err = io.Copy(file, r.Body) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } status, err := s.performScan(file.Name()) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } _, _ = w.Write([]byte(fmt.Sprintf("%v\n", status))) } func (s *Server) performScan(path string) (string, error) { c := clamd.NewClamd(s.ClamAVDaemonHost) responseCh := make(chan chan *clamd.ScanResult) errCh := make(chan error) go func(responseCh chan chan *clamd.ScanResult, errCh chan error) { response, err := c.ScanFile(path) if err != nil { errCh <- err return } responseCh <- response }(responseCh, errCh) select { case err := <-errCh: return "", err case response := <-responseCh: st := <-response return st.Status, nil case <-time.After(time.Second * 60): return "", errors.New("clamav scan timeout") } } ================================================ FILE: server/handlers.go ================================================ /* The MIT License (MIT) Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/] Copyright (c) 2018-2020 Andrea Spacca. Copyright (c) 2020- Andrea Spacca and Stefan Benten. 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. */ package server import ( "archive/tar" "archive/zip" "bytes" "compress/gzip" "context" "encoding/base64" "encoding/json" "errors" "fmt" "html" htmlTemplate "html/template" "io" "mime" "net" "net/http" "net/url" "os" "path" "path/filepath" "strconv" "strings" "sync" textTemplate "text/template" "time" "unicode" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/gopenpgp/v2/constants" "github.com/dutchcoders/transfer.sh/server/storage" "github.com/tg123/go-htpasswd" "github.com/tomasen/realip" web "github.com/dutchcoders/transfer.sh-web" "github.com/gorilla/mux" "github.com/microcosm-cc/bluemonday" blackfriday "github.com/russross/blackfriday/v2" qrcode "github.com/skip2/go-qrcode" "golang.org/x/net/idna" "golang.org/x/text/runes" "golang.org/x/text/transform" "golang.org/x/text/unicode/norm" ) const getPathPart = "get" var ( htmlTemplates = initHTMLTemplates() textTemplates = initTextTemplates() ) func stripPrefix(path string) string { return strings.Replace(path, web.Prefix+"/", "", -1) } func initTextTemplates() *textTemplate.Template { templateMap := textTemplate.FuncMap{"format": formatNumber} // Templates with functions available to them var templates = textTemplate.New("").Funcs(templateMap) return templates } func initHTMLTemplates() *htmlTemplate.Template { templateMap := htmlTemplate.FuncMap{"format": formatNumber} // Templates with functions available to them var templates = htmlTemplate.New("").Funcs(templateMap) return templates } func attachEncryptionReader(reader io.ReadCloser, password string) (io.ReadCloser, error) { if len(password) == 0 { return reader, nil } return encrypt(reader, []byte(password)) } func attachDecryptionReader(reader io.ReadCloser, password string) (io.ReadCloser, error) { if len(password) == 0 { return reader, nil } return decrypt(reader, []byte(password)) } func decrypt(ciphertext io.ReadCloser, password []byte) (plaintext io.ReadCloser, err error) { unarmored, err := armor.Decode(ciphertext) if err != nil { return } firstTimeCalled := true var prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) { if firstTimeCalled { firstTimeCalled = false return password, nil } // Re-prompt still occurs if SKESK pasrsing fails (i.e. when decrypted cipher algo is invalid). // For most (but not all) cases, inputting a wrong passwords is expected to trigger this error. return nil, errors.New("gopenpgp: wrong password in symmetric decryption") } config := &packet.Config{ DefaultCipher: packet.CipherAES256, } var emptyKeyRing openpgp.EntityList md, err := openpgp.ReadMessage(unarmored.Body, emptyKeyRing, prompt, config) if err != nil { // Parsing errors when reading the message are most likely caused by incorrect password, but we cannot know for sure return } plaintext = io.NopCloser(md.UnverifiedBody) return } type encryptWrapperReader struct { plaintext io.Reader encrypt io.WriteCloser armored io.WriteCloser buffer io.ReadWriter plaintextReadZero bool } func (e *encryptWrapperReader) Read(p []byte) (n int, err error) { p2 := make([]byte, len(p)) n, _ = e.plaintext.Read(p2) if n == 0 { if !e.plaintextReadZero { err = e.encrypt.Close() if err != nil { return } err = e.armored.Close() if err != nil { return } e.plaintextReadZero = true } return e.buffer.Read(p) } return e.buffer.Read(p) } func (e *encryptWrapperReader) Close() error { return nil } func NewEncryptWrapperReader(plaintext io.Reader, armored, encrypt io.WriteCloser, buffer io.ReadWriter) io.ReadCloser { return &encryptWrapperReader{ plaintext: io.TeeReader(plaintext, encrypt), encrypt: encrypt, armored: armored, buffer: buffer, } } func encrypt(plaintext io.ReadCloser, password []byte) (ciphertext io.ReadCloser, err error) { bufferReadWriter := new(bytes.Buffer) armored, err := armor.Encode(bufferReadWriter, constants.PGPMessageHeader, nil) if err != nil { return } config := &packet.Config{ DefaultCipher: packet.CipherAES256, Time: time.Now, } hints := &openpgp.FileHints{ IsBinary: true, FileName: "", ModTime: time.Unix(time.Now().Unix(), 0), } encryptWriter, err := openpgp.SymmetricallyEncrypt(armored, password, hints, config) if err != nil { return } ciphertext = NewEncryptWrapperReader(plaintext, armored, encryptWriter, bufferReadWriter) return } func healthHandler(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("Approaching Neutral Zone, all systems normal and functioning.")) } func canContainsXSS(contentType string) bool { switch { case strings.Contains(contentType, "cache-manifest"): fallthrough case strings.Contains(contentType, "html"): fallthrough case strings.Contains(contentType, "rdf"): fallthrough case strings.Contains(contentType, "vtt"): fallthrough case strings.Contains(contentType, "xml"): fallthrough case strings.Contains(contentType, "xsl"): return true case strings.Contains(contentType, "x-mixed-replace"): return true } return false } /* The preview handler will show a preview of the content for browsers (accept type text/html), and referer is not transfer.sh */ func (s *Server) previewHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password") vars := mux.Vars(r) token := vars["token"] filename := vars["filename"] metadata, err := s.checkMetadata(r.Context(), token, filename, false) if err != nil { s.logger.Printf("Error metadata: %s", err.Error()) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } contentType := metadata.ContentType contentLength, err := s.storage.Head(r.Context(), token, filename) if err != nil { http.Error(w, http.StatusText(404), 404) return } var templatePath string var content htmlTemplate.HTML switch { case strings.HasPrefix(contentType, "image/"): templatePath = "download.image.html" case strings.HasPrefix(contentType, "video/"): templatePath = "download.video.html" case strings.HasPrefix(contentType, "audio/"): templatePath = "download.audio.html" case strings.HasPrefix(contentType, "text/"): templatePath = "download.markdown.html" var reader io.ReadCloser if reader, _, err = s.storage.Get(r.Context(), token, filename, nil); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } var data []byte data = make([]byte, _5M) if _, err = reader.Read(data); err != io.EOF && err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if strings.HasPrefix(contentType, "text/x-markdown") || strings.HasPrefix(contentType, "text/markdown") { unsafe := blackfriday.Run(data) output := bluemonday.UGCPolicy().SanitizeBytes(unsafe) content = htmlTemplate.HTML(output) } else if strings.HasPrefix(contentType, "text/plain") { content = htmlTemplate.HTML(fmt.Sprintf("
%s
", html.EscapeString(string(data)))) } else { templatePath = "download.sandbox.html" } default: templatePath = "download.html" } relativeURL, _ := url.Parse(path.Join(s.proxyPath, token, filename)) resolvedURL := resolveURL(r, relativeURL, s.proxyPort) relativeURLGet, _ := url.Parse(path.Join(s.proxyPath, getPathPart, token, filename)) resolvedURLGet := resolveURL(r, relativeURLGet, s.proxyPort) var png []byte png, err = qrcode.Encode(resolvedURL, qrcode.High, 150) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } qrCode := base64.StdEncoding.EncodeToString(png) hostname := getURL(r, s.proxyPort).Host webAddress := resolveWebAddress(r, s.proxyPath, s.proxyPort) data := struct { ContentType string Content htmlTemplate.HTML Filename string URL string URLGet string URLRandomToken string Hostname string WebAddress string ContentLength uint64 GAKey string UserVoiceKey string QRCode string }{ contentType, content, filename, resolvedURL, resolvedURLGet, token, hostname, webAddress, contentLength, s.gaKey, s.userVoiceKey, qrCode, } if err := htmlTemplates.ExecuteTemplate(w, templatePath, data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } // this handler will output html or text, depending on the // support of the client (Accept header). func (s *Server) viewHandler(w http.ResponseWriter, r *http.Request) { // vars := mux.Vars(r) hostname := getURL(r, s.proxyPort).Host webAddress := resolveWebAddress(r, s.proxyPath, s.proxyPort) maxUploadSize := "" if s.maxUploadSize > 0 { maxUploadSize = formatSize(s.maxUploadSize) } purgeTime := "" if s.purgeDays > 0 { purgeTime = formatDurationDays(s.purgeDays) } data := struct { Hostname string WebAddress string EmailContact string GAKey string UserVoiceKey string PurgeTime string MaxUploadSize string SampleToken string SampleToken2 string }{ hostname, webAddress, s.emailContact, s.gaKey, s.userVoiceKey, purgeTime, maxUploadSize, token(s.randomTokenLength), token(s.randomTokenLength), } w.Header().Set("Vary", "Accept") if acceptsHTML(r.Header) { if err := htmlTemplates.ExecuteTemplate(w, "index.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } else { if err := textTemplates.ExecuteTemplate(w, "index.txt", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } func (s *Server) notFoundHandler(w http.ResponseWriter, _ *http.Request) { http.Error(w, http.StatusText(404), 404) } func sanitize(fileName string) string { t := transform.Chain( norm.NFD, runes.Remove(runes.In(unicode.Cc)), runes.Remove(runes.In(unicode.Cf)), runes.Remove(runes.In(unicode.Co)), runes.Remove(runes.In(unicode.Cs)), runes.Remove(runes.In(unicode.Other)), runes.Remove(runes.In(unicode.Zl)), runes.Remove(runes.In(unicode.Zp)), norm.NFC) newName, _, err := transform.String(t, fileName) if err != nil { return path.Base(fileName) } if len(newName) == 0 { newName = "_" } return path.Base(newName) } func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(_24K); nil != err { s.logger.Printf("%s", err.Error()) http.Error(w, "Error occurred copying to output stream", http.StatusInternalServerError) return } token := token(s.randomTokenLength) w.Header().Set("Content-Type", "text/plain") responseBody := "" for _, fHeaders := range r.MultipartForm.File { for _, fHeader := range fHeaders { filename := sanitize(fHeader.Filename) contentType := mime.TypeByExtension(filepath.Ext(fHeader.Filename)) var f io.Reader var err error if f, err = fHeader.Open(); err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } file, err := os.CreateTemp(s.tempPath, "transfer-") defer s.cleanTmpFile(file) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } n, err := io.Copy(file, f) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } contentLength := n _, err = file.Seek(0, io.SeekStart) if err != nil { s.logger.Printf("%s", err.Error()) return } if s.maxUploadSize > 0 && contentLength > s.maxUploadSize { s.logger.Print("Entity too large") http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) return } if s.performClamavPrescan { status, err := s.performScan(file.Name()) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Could not perform prescan", http.StatusInternalServerError) return } if status != clamavScanStatusOK { s.logger.Printf("prescan positive: %s", status) http.Error(w, "Clamav prescan found a virus", http.StatusPreconditionFailed) return } } metadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r) buffer := &bytes.Buffer{} if err := json.NewEncoder(buffer).Encode(metadata); err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Could not encode metadata", http.StatusInternalServerError) return } else if err := s.storage.Put(r.Context(), token, fmt.Sprintf("%s.metadata", filename), buffer, "text/json", uint64(buffer.Len())); err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Could not save metadata", http.StatusInternalServerError) return } s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType) reader, err := attachEncryptionReader(file, r.Header.Get("X-Encrypt-Password")) if err != nil { http.Error(w, "Could not crypt file", http.StatusInternalServerError) return } if err = s.storage.Put(r.Context(), token, filename, reader, contentType, uint64(contentLength)); err != nil { s.logger.Printf("Backend storage error: %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } filename = url.PathEscape(filename) relativeURL, _ := url.Parse(path.Join(s.proxyPath, token, filename)) deleteURL, _ := url.Parse(path.Join(s.proxyPath, token, filename, metadata.DeletionToken)) w.Header().Add("X-Url-Delete", resolveURL(r, deleteURL, s.proxyPort)) responseBody += fmt.Sprintln(getURL(r, s.proxyPort).ResolveReference(relativeURL).String()) } } _, err := w.Write([]byte(responseBody)) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) } } func (s *Server) cleanTmpFile(f *os.File) { if f != nil { err := f.Close() if err != nil { s.logger.Printf("Error closing tmpfile: %s (%s)", err, f.Name()) } err = os.Remove(f.Name()) if err != nil { s.logger.Printf("Error removing tmpfile: %s (%s)", err, f.Name()) } } } type metadata struct { // ContentType is the original uploading content type ContentType string // ContentLength is is the original uploading content length ContentLength int64 // Downloads is the actual number of downloads Downloads int // MaxDownloads contains the maximum numbers of downloads MaxDownloads int // MaxDate contains the max age of the file MaxDate time.Time // DeletionToken contains the token to match against for deletion DeletionToken string // Encrypted contains if the file was encrypted Encrypted bool // DecryptedContentType is the original uploading content type DecryptedContentType string } func metadataForRequest(contentType string, contentLength int64, randomTokenLength int, r *http.Request) metadata { metadata := metadata{ ContentType: strings.ToLower(contentType), ContentLength: contentLength, MaxDate: time.Time{}, Downloads: 0, MaxDownloads: -1, DeletionToken: token(randomTokenLength) + token(randomTokenLength), } if v := r.Header.Get("Max-Downloads"); v == "" { } else if v, err := strconv.Atoi(v); err != nil { } else { metadata.MaxDownloads = v } if v := r.Header.Get("Max-Days"); v == "" { } else if v, err := strconv.Atoi(v); err != nil { } else { metadata.MaxDate = time.Now().Add(time.Hour * 24 * time.Duration(v)) } if password := r.Header.Get("X-Encrypt-Password"); password != "" { metadata.Encrypted = true metadata.ContentType = "text/plain; charset=utf-8" metadata.DecryptedContentType = contentType } else { metadata.Encrypted = false } return metadata } func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) filename := sanitize(vars["filename"]) contentLength := r.ContentLength defer storage.CloseCheck(r.Body) reader := r.Body if contentLength < 1 || s.performClamavPrescan { file, err := os.CreateTemp(s.tempPath, "transfer-") defer s.cleanTmpFile(file) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } // queue file to disk, because s3 needs content length // and clamav prescan scans a file n, err := io.Copy(file, r.Body) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } _, err = file.Seek(0, io.SeekStart) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Cannot reset cache file", http.StatusInternalServerError) return } contentLength = n if s.performClamavPrescan { status, err := s.performScan(file.Name()) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Could not perform prescan", http.StatusInternalServerError) return } if status != clamavScanStatusOK { s.logger.Printf("prescan positive: %s", status) http.Error(w, "Clamav prescan found a virus", http.StatusPreconditionFailed) return } } reader = file } if s.maxUploadSize > 0 && contentLength > s.maxUploadSize { s.logger.Print("Entity too large") http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) return } if contentLength == 0 { s.logger.Print("Empty content-length") http.Error(w, "Could not upload empty file", http.StatusBadRequest) return } contentType := mime.TypeByExtension(filepath.Ext(vars["filename"])) token := token(s.randomTokenLength) metadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r) buffer := &bytes.Buffer{} if err := json.NewEncoder(buffer).Encode(metadata); err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Could not encode metadata", http.StatusInternalServerError) return } else if !metadata.MaxDate.IsZero() && time.Now().After(metadata.MaxDate) { s.logger.Print("Invalid MaxDate") http.Error(w, "Invalid MaxDate, make sure Max-Days is smaller than 290 years", http.StatusBadRequest) return } else if err := s.storage.Put(r.Context(), token, fmt.Sprintf("%s.metadata", filename), buffer, "text/json", uint64(buffer.Len())); err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Could not save metadata", http.StatusInternalServerError) return } s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType) reader, err := attachEncryptionReader(reader, r.Header.Get("X-Encrypt-Password")) if err != nil { http.Error(w, "Could not crypt file", http.StatusInternalServerError) return } if err = s.storage.Put(r.Context(), token, filename, reader, contentType, uint64(contentLength)); err != nil { s.logger.Printf("Error putting new file: %s", err.Error()) http.Error(w, "Could not save file", http.StatusInternalServerError) return } // w.Statuscode = 200 w.Header().Set("Content-Type", "text/plain") filename = url.PathEscape(filename) relativeURL, _ := url.Parse(path.Join(s.proxyPath, token, filename)) deleteURL, _ := url.Parse(path.Join(s.proxyPath, token, filename, metadata.DeletionToken)) w.Header().Set("X-Url-Delete", resolveURL(r, deleteURL, s.proxyPort)) _, _ = w.Write([]byte(resolveURL(r, relativeURL, s.proxyPort))) } func resolveURL(r *http.Request, u *url.URL, proxyPort string) string { r.URL.Path = "" return getURL(r, proxyPort).ResolveReference(u).String() } func resolveKey(key, proxyPath string) string { key = strings.TrimPrefix(key, "/") key = strings.TrimPrefix(key, proxyPath) key = strings.Replace(key, "\\", "/", -1) return key } func resolveWebAddress(r *http.Request, proxyPath string, proxyPort string) string { rUrl := getURL(r, proxyPort) var webAddress string if len(proxyPath) == 0 { webAddress = fmt.Sprintf("%s://%s/", rUrl.ResolveReference(rUrl).Scheme, rUrl.ResolveReference(rUrl).Host) } else { webAddress = fmt.Sprintf("%s://%s/%s", rUrl.ResolveReference(rUrl).Scheme, rUrl.ResolveReference(rUrl).Host, strings.TrimPrefix(proxyPath, "/")) } return webAddress } // Similar to the logic found here: // https://github.com/golang/go/blob/release-branch.go1.14/src/net/http/clone.go#L22-L33 func cloneURL(u *url.URL) *url.URL { c := &url.URL{} *c = *u if u.User != nil { c.User = &url.Userinfo{} *c.User = *u.User } return c } func getURL(r *http.Request, proxyPort string) *url.URL { u := cloneURL(r.URL) if r.TLS != nil { u.Scheme = "https" } else if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { u.Scheme = proto } else { u.Scheme = "http" } host, port, err := net.SplitHostPort(r.Host) if err != nil { host = r.Host port = "" } p := idna.New(idna.ValidateForRegistration()) var hostFromPunycode string hostFromPunycode, err = p.ToUnicode(host) if err == nil { host = hostFromPunycode } if len(proxyPort) != 0 { port = proxyPort } if len(port) == 0 { u.Host = host } else { if port == "80" && u.Scheme == "http" { u.Host = host } else if port == "443" && u.Scheme == "https" { u.Host = host } else { u.Host = net.JoinHostPort(host, port) } } return u } func (metadata metadata) remainingLimitHeaderValues() (remainingDownloads, remainingDays string) { if metadata.MaxDate.IsZero() { remainingDays = "n/a" } else { timeDifference := time.Until(metadata.MaxDate) remainingDays = strconv.Itoa(int(timeDifference.Hours()/24) + 1) } if metadata.MaxDownloads == -1 { remainingDownloads = "n/a" } else { remainingDownloads = strconv.Itoa(metadata.MaxDownloads - metadata.Downloads) } return remainingDownloads, remainingDays } func (s *Server) lock(token, filename string) { key := path.Join(token, filename) lock, _ := s.locks.LoadOrStore(key, &sync.Mutex{}) lock.(*sync.Mutex).Lock() } func (s *Server) unlock(token, filename string) { key := path.Join(token, filename) lock, _ := s.locks.LoadOrStore(key, &sync.Mutex{}) lock.(*sync.Mutex).Unlock() } func (s *Server) checkMetadata(ctx context.Context, token, filename string, increaseDownload bool) (metadata, error) { s.lock(token, filename) defer s.unlock(token, filename) var metadata metadata r, _, err := s.storage.Get(ctx, token, fmt.Sprintf("%s.metadata", filename), nil) defer storage.CloseCheck(r) if err != nil { return metadata, err } if err := json.NewDecoder(r).Decode(&metadata); err != nil { return metadata, err } else if metadata.MaxDownloads != -1 && metadata.Downloads >= metadata.MaxDownloads { return metadata, errors.New("maxDownloads expired") } else if !metadata.MaxDate.IsZero() && time.Now().After(metadata.MaxDate) { return metadata, errors.New("maxDate expired") } else if metadata.MaxDownloads != -1 && increaseDownload { // todo(nl5887): mutex? // update number of downloads metadata.Downloads++ buffer := &bytes.Buffer{} if err := json.NewEncoder(buffer).Encode(metadata); err != nil { return metadata, errors.New("could not encode metadata") } else if err := s.storage.Put(ctx, token, fmt.Sprintf("%s.metadata", filename), buffer, "text/json", uint64(buffer.Len())); err != nil { return metadata, errors.New("could not save metadata") } } return metadata, nil } func (s *Server) checkDeletionToken(ctx context.Context, deletionToken, token, filename string) error { s.lock(token, filename) defer s.unlock(token, filename) var metadata metadata r, _, err := s.storage.Get(ctx, token, fmt.Sprintf("%s.metadata", filename), nil) defer storage.CloseCheck(r) if s.storage.IsNotExist(err) { return errors.New("metadata doesn't exist") } else if err != nil { return err } if err := json.NewDecoder(r).Decode(&metadata); err != nil { return err } else if metadata.DeletionToken != deletionToken { return errors.New("deletion token doesn't match") } return nil } func (s *Server) purgeHandler() { ticker := time.NewTicker(s.purgeInterval) go func() { for { <-ticker.C err := s.storage.Purge(context.TODO(), s.purgeDays) if err != nil { s.logger.Printf("error cleaning up expired files: %v", err) } } }() } func (s *Server) deleteHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) token := vars["token"] filename := vars["filename"] deletionToken := vars["deletionToken"] if err := s.checkDeletionToken(r.Context(), deletionToken, token, filename); err != nil { s.logger.Printf("Error metadata: %s", err.Error()) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } err := s.storage.Delete(r.Context(), token, filename) if s.storage.IsNotExist(err) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } else if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Could not delete file.", http.StatusInternalServerError) return } } func (s *Server) zipHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) files := vars["files"] zipfilename := fmt.Sprintf("transfersh-%d.zip", uint16(time.Now().UnixNano())) w.Header().Set("Content-Type", "application/zip") commonHeader(w, zipfilename) zw := zip.NewWriter(w) for _, key := range strings.Split(files, ",") { key = resolveKey(key, s.proxyPath) token := strings.Split(key, "/")[0] filename := sanitize(strings.Split(key, "/")[1]) if _, err := s.checkMetadata(r.Context(), token, filename, true); err != nil { s.logger.Printf("Error metadata: %s", err.Error()) continue } reader, _, err := s.storage.Get(r.Context(), token, filename, nil) defer storage.CloseCheck(reader) if err != nil { if s.storage.IsNotExist(err) { http.Error(w, "File not found", 404) return } s.logger.Printf("%s", err.Error()) http.Error(w, "Could not retrieve file.", http.StatusInternalServerError) return } header := &zip.FileHeader{ Name: strings.Split(key, "/")[1], Method: zip.Store, Modified: time.Now().UTC(), } fw, err := zw.CreateHeader(header) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Internal server error.", http.StatusInternalServerError) return } if _, err = io.Copy(fw, reader); err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Internal server error.", http.StatusInternalServerError) return } } if err := zw.Close(); err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Internal server error.", http.StatusInternalServerError) return } } func (s *Server) tarGzHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) files := vars["files"] tarfilename := fmt.Sprintf("transfersh-%d.tar.gz", uint16(time.Now().UnixNano())) w.Header().Set("Content-Type", "application/x-gzip") commonHeader(w, tarfilename) gw := gzip.NewWriter(w) defer storage.CloseCheck(gw) zw := tar.NewWriter(gw) defer storage.CloseCheck(zw) for _, key := range strings.Split(files, ",") { key = resolveKey(key, s.proxyPath) token := strings.Split(key, "/")[0] filename := sanitize(strings.Split(key, "/")[1]) if _, err := s.checkMetadata(r.Context(), token, filename, true); err != nil { s.logger.Printf("Error metadata: %s", err.Error()) continue } reader, contentLength, err := s.storage.Get(r.Context(), token, filename, nil) defer storage.CloseCheck(reader) if err != nil { if s.storage.IsNotExist(err) { http.Error(w, "File not found", 404) return } s.logger.Printf("%s", err.Error()) http.Error(w, "Could not retrieve file.", http.StatusInternalServerError) return } header := &tar.Header{ Name: strings.Split(key, "/")[1], Size: int64(contentLength), } err = zw.WriteHeader(header) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Internal server error.", http.StatusInternalServerError) return } if _, err = io.Copy(zw, reader); err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Internal server error.", http.StatusInternalServerError) return } } } func (s *Server) tarHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) files := vars["files"] tarfilename := fmt.Sprintf("transfersh-%d.tar", uint16(time.Now().UnixNano())) w.Header().Set("Content-Type", "application/x-tar") commonHeader(w, tarfilename) zw := tar.NewWriter(w) defer storage.CloseCheck(zw) for _, key := range strings.Split(files, ",") { key = resolveKey(key, s.proxyPath) token := strings.Split(key, "/")[0] filename := strings.Split(key, "/")[1] if _, err := s.checkMetadata(r.Context(), token, filename, true); err != nil { s.logger.Printf("Error metadata: %s", err.Error()) continue } reader, contentLength, err := s.storage.Get(r.Context(), token, filename, nil) defer storage.CloseCheck(reader) if err != nil { if s.storage.IsNotExist(err) { http.Error(w, "File not found", 404) return } s.logger.Printf("%s", err.Error()) http.Error(w, "Could not retrieve file.", http.StatusInternalServerError) return } header := &tar.Header{ Name: strings.Split(key, "/")[1], Size: int64(contentLength), } err = zw.WriteHeader(header) if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Internal server error.", http.StatusInternalServerError) return } if _, err = io.Copy(zw, reader); err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Internal server error.", http.StatusInternalServerError) return } } } func (s *Server) headHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) token := vars["token"] filename := vars["filename"] metadata, err := s.checkMetadata(r.Context(), token, filename, false) if err != nil { s.logger.Printf("Error metadata: %s", err.Error()) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } contentType := metadata.ContentType contentLength, err := s.storage.Head(r.Context(), token, filename) if s.storage.IsNotExist(err) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } else if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Could not retrieve file.", http.StatusInternalServerError) return } remainingDownloads, remainingDays := metadata.remainingLimitHeaderValues() w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Length", strconv.FormatUint(contentLength, 10)) w.Header().Set("Connection", "close") w.Header().Set("X-Remaining-Downloads", remainingDownloads) w.Header().Set("X-Remaining-Days", remainingDays) w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password") if s.storage.IsRangeSupported() { w.Header().Set("Accept-Ranges", "bytes") } } func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) action := vars["action"] token := vars["token"] filename := vars["filename"] metadata, err := s.checkMetadata(r.Context(), token, filename, true) if err != nil { s.logger.Printf("Error metadata: %s", err.Error()) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } var rng *storage.Range if r.Header.Get("Range") != "" { rng = storage.ParseRange(r.Header.Get("Range")) } contentType := metadata.ContentType reader, contentLength, err := s.storage.Get(r.Context(), token, filename, rng) defer storage.CloseCheck(reader) if s.storage.IsNotExist(err) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } else if err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Could not retrieve file.", http.StatusInternalServerError) return } if rng != nil { cr := rng.ContentRange() if cr != "" { w.Header().Set("Accept-Ranges", "bytes") w.Header().Set("Content-Range", cr) if rng.Limit > 0 { reader = io.NopCloser(io.LimitReader(reader, int64(rng.Limit))) } } } var disposition string if action == "inline" { disposition = "inline" /* metadata.ContentType is unable to determine the type of the content, So add text/plain in this case to fix XSS related issues/ */ if strings.TrimSpace(contentType) == "" { contentType = "text/plain; charset=utf-8" } } else { disposition = "attachment" } remainingDownloads, remainingDays := metadata.remainingLimitHeaderValues() w.Header().Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"`, disposition, filename)) w.Header().Set("Connection", "keep-alive") w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Remaining-Downloads", remainingDownloads) w.Header().Set("X-Remaining-Days", remainingDays) password := r.Header.Get("X-Decrypt-Password") reader, err = attachDecryptionReader(reader, password) if err != nil { http.Error(w, "Could not decrypt file", http.StatusInternalServerError) return } if metadata.Encrypted && len(password) > 0 { contentType = metadata.DecryptedContentType contentLength = uint64(metadata.ContentLength) } w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Length", strconv.FormatUint(contentLength, 10)) w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password") if rng != nil && rng.ContentRange() != "" { w.WriteHeader(http.StatusPartialContent) } if disposition == "inline" && canContainsXSS(contentType) { reader = io.NopCloser(bluemonday.UGCPolicy().SanitizeReader(reader)) } if _, err = io.Copy(w, reader); err != nil { s.logger.Printf("%s", err.Error()) http.Error(w, "Error occurred copying to output stream", http.StatusInternalServerError) return } } func commonHeader(w http.ResponseWriter, filename string) { w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) w.Header().Set("Connection", "close") w.Header().Set("Cache-Control", "no-store") } // RedirectHandler handles redirect func (s *Server) RedirectHandler(h http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !s.forceHTTPS { // we don't want to enforce https } else if r.URL.Path == "/health.html" { // health check url won't redirect } else if strings.HasSuffix(ipAddrFromRemoteAddr(r.Host), ".onion") { // .onion addresses cannot get a valid certificate, so don't redirect } else if r.Header.Get("X-Forwarded-Proto") == "https" { } else if r.TLS != nil { } else { u := getURL(r, s.proxyPort) u.Scheme = "https" if len(s.proxyPort) == 0 && len(s.TLSListenerString) > 0 { _, port, err := net.SplitHostPort(s.TLSListenerString) if err != nil || port == "443" { port = "" } if len(port) > 0 { u.Host = net.JoinHostPort(u.Hostname(), port) } else { u.Host = u.Hostname() } } http.Redirect(w, r, u.String(), http.StatusPermanentRedirect) return } h.ServeHTTP(w, r) } } // LoveHandler Create a log handler for every request it receives. func LoveHandler(h http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("x-made-with", "<3 by DutchCoders") w.Header().Set("x-served-by", "Proudly served by DutchCoders") w.Header().Set("server", "Transfer.sh HTTP Server") h.ServeHTTP(w, r) } } func ipFilterHandler(h http.Handler, ipFilterOptions *IPFilterOptions) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if ipFilterOptions == nil { h.ServeHTTP(w, r) } else { WrapIPFilter(h, ipFilterOptions).ServeHTTP(w, r) } } } func (s *Server) basicAuthHandler(h http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if s.authUser == "" && s.authPass == "" && s.authHtpasswd == "" { h.ServeHTTP(w, r) return } if s.htpasswdFile == nil && s.authHtpasswd != "" { htpasswdFile, err := htpasswd.New(s.authHtpasswd, htpasswd.DefaultSystems, nil) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } s.htpasswdFile = htpasswdFile } if s.authIPFilter == nil && s.authIPFilterOptions != nil { s.authIPFilter = newIPFilter(s.authIPFilterOptions) } w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"") var authorized bool if s.authIPFilter != nil { remoteIP := realip.FromRequest(r) authorized = s.authIPFilter.Allowed(remoteIP) } username, password, authOK := r.BasicAuth() if !authOK && !authorized { http.Error(w, "Not authorized", http.StatusUnauthorized) return } if !authorized && username == s.authUser && password == s.authPass { authorized = true } if !authorized && s.htpasswdFile != nil { authorized = s.htpasswdFile.Match(username, password) } if !authorized { http.Error(w, "Not authorized", http.StatusUnauthorized) return } h.ServeHTTP(w, r) } } ================================================ FILE: server/handlers_test.go ================================================ package server import ( "fmt" "net/http" "net/http/httptest" "testing" . "gopkg.in/check.v1" ) // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { TestingT(t) } var ( _ = Suite(&suiteRedirectWithForceHTTPS{}) _ = Suite(&suiteRedirectWithoutForceHTTPS{}) ) type suiteRedirectWithForceHTTPS struct { handler http.HandlerFunc } func (s *suiteRedirectWithForceHTTPS) SetUpTest(c *C) { srvr, err := New(ForceHTTPS()) c.Assert(err, IsNil) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, "Hello, client") }) s.handler = srvr.RedirectHandler(handler) } func (s *suiteRedirectWithForceHTTPS) TestHTTPs(c *C) { req := httptest.NewRequest("GET", "https://test/test", nil) w := httptest.NewRecorder() s.handler(w, req) resp := w.Result() c.Assert(resp.StatusCode, Equals, http.StatusOK) } func (s *suiteRedirectWithForceHTTPS) TestOnion(c *C) { req := httptest.NewRequest("GET", "http://test.onion/test", nil) w := httptest.NewRecorder() s.handler(w, req) resp := w.Result() c.Assert(resp.StatusCode, Equals, http.StatusOK) } func (s *suiteRedirectWithForceHTTPS) TestXForwardedFor(c *C) { req := httptest.NewRequest("GET", "http://127.0.0.1/test", nil) req.Header.Set("X-Forwarded-Proto", "https") w := httptest.NewRecorder() s.handler(w, req) resp := w.Result() c.Assert(resp.StatusCode, Equals, http.StatusOK) } func (s *suiteRedirectWithForceHTTPS) TestHTTP(c *C) { req := httptest.NewRequest("GET", "http://127.0.0.1/test", nil) w := httptest.NewRecorder() s.handler(w, req) resp := w.Result() c.Assert(resp.StatusCode, Equals, http.StatusPermanentRedirect) c.Assert(resp.Header.Get("Location"), Equals, "https://127.0.0.1/test") } type suiteRedirectWithoutForceHTTPS struct { handler http.HandlerFunc } func (s *suiteRedirectWithoutForceHTTPS) SetUpTest(c *C) { srvr, err := New() c.Assert(err, IsNil) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, "Hello, client") }) s.handler = srvr.RedirectHandler(handler) } func (s *suiteRedirectWithoutForceHTTPS) TestHTTP(c *C) { req := httptest.NewRequest("GET", "http://127.0.0.1/test", nil) w := httptest.NewRecorder() s.handler(w, req) resp := w.Result() c.Assert(resp.StatusCode, Equals, http.StatusOK) } func (s *suiteRedirectWithoutForceHTTPS) TestHTTPs(c *C) { req := httptest.NewRequest("GET", "https://127.0.0.1/test", nil) w := httptest.NewRecorder() s.handler(w, req) resp := w.Result() c.Assert(resp.StatusCode, Equals, http.StatusOK) } ================================================ FILE: server/ip_filter.go ================================================ /* MIT License Copyright © 2016 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. */ package server import ( "log" "net" "net/http" "os" "sync" "github.com/tomasen/realip" ) // IPFilterOptions for ipFilter. Allowed takes precedence over Blocked. // IPs can be IPv4 or IPv6 and can optionally contain subnet // masks (/24). Note however, determining if a given IP is // included in a subnet requires a linear scan so is less performant // than looking up single IPs. // // This could be improved with some algorithmic magic. type IPFilterOptions struct { //explicity allowed IPs AllowedIPs []string //explicity blocked IPs BlockedIPs []string //block by default (defaults to allow) BlockByDefault bool // TrustProxy enable check request IP from proxy TrustProxy bool Logger interface { Printf(format string, v ...interface{}) } } // ipFilter type ipFilter struct { //mut protects the below //rw since writes are rare mut sync.RWMutex defaultAllowed bool ips map[string]bool subnets []*subnet } type subnet struct { str string ipnet *net.IPNet allowed bool } func newIPFilter(opts *IPFilterOptions) *ipFilter { if opts.Logger == nil { flags := log.LstdFlags opts.Logger = log.New(os.Stdout, "", flags) } f := &ipFilter{ ips: map[string]bool{}, defaultAllowed: !opts.BlockByDefault, } for _, ip := range opts.BlockedIPs { f.BlockIP(ip) } for _, ip := range opts.AllowedIPs { f.AllowIP(ip) } return f } func (f *ipFilter) AllowIP(ip string) bool { return f.ToggleIP(ip, true) } func (f *ipFilter) BlockIP(ip string) bool { return f.ToggleIP(ip, false) } func (f *ipFilter) ToggleIP(str string, allowed bool) bool { //check if provided string describes a subnet if ip, network, err := net.ParseCIDR(str); err == nil { // containing only one ip? if n, total := network.Mask.Size(); n == total { f.mut.Lock() f.ips[ip.String()] = allowed f.mut.Unlock() return true } //check for existing f.mut.Lock() found := false for _, subnet := range f.subnets { if subnet.str == str { found = true subnet.allowed = allowed break } } if !found { f.subnets = append(f.subnets, &subnet{ str: str, ipnet: network, allowed: allowed, }) } f.mut.Unlock() return true } //check if plain ip if ip := net.ParseIP(str); ip != nil { f.mut.Lock() f.ips[ip.String()] = allowed f.mut.Unlock() return true } return false } // ToggleDefault alters the default setting func (f *ipFilter) ToggleDefault(allowed bool) { f.mut.Lock() f.defaultAllowed = allowed f.mut.Unlock() } // Allowed returns if a given IP can pass through the filter func (f *ipFilter) Allowed(ipstr string) bool { return f.NetAllowed(net.ParseIP(ipstr)) } // NetAllowed returns if a given net.IP can pass through the filter func (f *ipFilter) NetAllowed(ip net.IP) bool { //invalid ip if ip == nil { return false } //read lock entire function //except for db access f.mut.RLock() defer f.mut.RUnlock() //check single ips allowed, ok := f.ips[ip.String()] if ok { return allowed } //scan subnets for any allow/block blocked := false for _, subnet := range f.subnets { if subnet.ipnet.Contains(ip) { if subnet.allowed { return true } blocked = true } } if blocked { return false } //use default setting return f.defaultAllowed } // Blocked returns if a given IP can NOT pass through the filter func (f *ipFilter) Blocked(ip string) bool { return !f.Allowed(ip) } // NetBlocked returns if a given net.IP can NOT pass through the filter func (f *ipFilter) NetBlocked(ip net.IP) bool { return !f.NetAllowed(ip) } // Wrap the provided handler with simple IP blocking middleware // using this IP filter and its configuration func (f *ipFilter) Wrap(next http.Handler) http.Handler { return &ipFilterMiddleware{ipFilter: f, next: next} } // WrapIPFilter is equivalent to newIPFilter(opts) then Wrap(next) func WrapIPFilter(next http.Handler, opts *IPFilterOptions) http.Handler { return newIPFilter(opts).Wrap(next) } type ipFilterMiddleware struct { *ipFilter next http.Handler } func (m *ipFilterMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { remoteIP := realip.FromRequest(r) if !m.ipFilter.Allowed(remoteIP) { //show simple forbidden text http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) return } //success! m.next.ServeHTTP(w, r) } ================================================ FILE: server/server.go ================================================ /* The MIT License (MIT) Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/] 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. */ package server import ( "context" cryptoRand "crypto/rand" "crypto/tls" "encoding/binary" "errors" "log" "math/rand" "mime" "net/http" _ "net/http/pprof" "net/url" "os" "os/signal" "path/filepath" "strings" "sync" "syscall" "time" "github.com/PuerkitoBio/ghost/handlers" "github.com/VojtechVitek/ratelimit" "github.com/VojtechVitek/ratelimit/memory" gorillaHandlers "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/tg123/go-htpasswd" "golang.org/x/crypto/acme/autocert" web "github.com/dutchcoders/transfer.sh-web" "github.com/dutchcoders/transfer.sh/server/storage" assetfs "github.com/elazarl/go-bindata-assetfs" ) // parse request with maximum memory of _24Kilobits const _24K = (1 << 3) * 24 // parse request with maximum memory of _5Megabytes const _5M = (1 << 20) * 5 // OptionFn is the option function type type OptionFn func(*Server) // ClamavHost sets clamav host func ClamavHost(s string) OptionFn { return func(srvr *Server) { srvr.ClamAVDaemonHost = s } } // PerformClamavPrescan enables clamav prescan on upload func PerformClamavPrescan(b bool) OptionFn { return func(srvr *Server) { srvr.performClamavPrescan = b } } // VirustotalKey sets virus total key func VirustotalKey(s string) OptionFn { return func(srvr *Server) { srvr.VirusTotalKey = s } } // Listener set listener func Listener(s string) OptionFn { return func(srvr *Server) { srvr.ListenerString = s } } // CorsDomains sets CORS domains func CorsDomains(s string) OptionFn { return func(srvr *Server) { srvr.CorsDomains = s } } // EmailContact sets email contact func EmailContact(emailContact string) OptionFn { return func(srvr *Server) { srvr.emailContact = emailContact } } // GoogleAnalytics sets GA key func GoogleAnalytics(gaKey string) OptionFn { return func(srvr *Server) { srvr.gaKey = gaKey } } // UserVoice sets UV key func UserVoice(userVoiceKey string) OptionFn { return func(srvr *Server) { srvr.userVoiceKey = userVoiceKey } } // TLSListener sets TLS listener and option func TLSListener(s string, t bool) OptionFn { return func(srvr *Server) { srvr.TLSListenerString = s srvr.TLSListenerOnly = t } } // ProfileListener sets profile listener func ProfileListener(s string) OptionFn { return func(srvr *Server) { srvr.ProfileListenerString = s } } // WebPath sets web path func WebPath(s string) OptionFn { return func(srvr *Server) { if s[len(s)-1:] != "/" { s = filepath.Join(s, "") } srvr.webPath = s } } // ProxyPath sets proxy path func ProxyPath(s string) OptionFn { return func(srvr *Server) { if s[len(s)-1:] != "/" { s = filepath.Join(s, "") } srvr.proxyPath = s } } // ProxyPort sets proxy port func ProxyPort(s string) OptionFn { return func(srvr *Server) { srvr.proxyPort = s } } // TempPath sets temp path func TempPath(s string) OptionFn { return func(srvr *Server) { if s[len(s)-1:] != "/" { s = filepath.Join(s, "") } srvr.tempPath = s } } // LogFile sets log file func LogFile(logger *log.Logger, s string) OptionFn { return func(srvr *Server) { f, err := os.OpenFile(s, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { logger.Fatalf("error opening file: %v", err) } logger.SetOutput(f) srvr.logger = logger } } // Logger sets logger func Logger(logger *log.Logger) OptionFn { return func(srvr *Server) { srvr.logger = logger } } // MaxUploadSize sets max upload size func MaxUploadSize(kbytes int64) OptionFn { return func(srvr *Server) { srvr.maxUploadSize = kbytes * 1024 } } // RateLimit set rate limit func RateLimit(requests int) OptionFn { return func(srvr *Server) { srvr.rateLimitRequests = requests } } // RandomTokenLength sets random token length func RandomTokenLength(length int) OptionFn { return func(srvr *Server) { srvr.randomTokenLength = length } } // Purge sets purge days and option func Purge(days, interval int) OptionFn { return func(srvr *Server) { srvr.purgeDays = time.Duration(days) * time.Hour * 24 srvr.purgeInterval = time.Duration(interval) * time.Hour } } // ForceHTTPS sets forcing https func ForceHTTPS() OptionFn { return func(srvr *Server) { srvr.forceHTTPS = true } } // EnableProfiler sets enable profiler func EnableProfiler() OptionFn { return func(srvr *Server) { srvr.profilerEnabled = true } } // UseStorage set storage to use func UseStorage(s storage.Storage) OptionFn { return func(srvr *Server) { srvr.storage = s } } // UseLetsEncrypt set letsencrypt usage func UseLetsEncrypt(hosts []string) OptionFn { return func(srvr *Server) { cacheDir := "./cache/" m := autocert.Manager{ Prompt: autocert.AcceptTOS, Cache: autocert.DirCache(cacheDir), HostPolicy: func(_ context.Context, host string) error { found := false for _, h := range hosts { found = found || strings.HasSuffix(host, h) } if !found { return errors.New("acme/autocert: host not configured") } return nil }, } srvr.tlsConfig = m.TLSConfig() srvr.tlsConfig.GetCertificate = m.GetCertificate } } // TLSConfig sets TLS config func TLSConfig(cert, pk string) OptionFn { certificate, err := tls.LoadX509KeyPair(cert, pk) return func(srvr *Server) { srvr.tlsConfig = &tls.Config{ GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { return &certificate, err }, } } } // HTTPAuthCredentials sets basic http auth credentials func HTTPAuthCredentials(user string, pass string) OptionFn { return func(srvr *Server) { srvr.authUser = user srvr.authPass = pass } } // HTTPAuthHtpasswd sets basic http auth htpasswd file func HTTPAuthHtpasswd(htpasswdPath string) OptionFn { return func(srvr *Server) { srvr.authHtpasswd = htpasswdPath } } // HTTPAUTHFilterOptions sets basic http auth ips whitelist func HTTPAUTHFilterOptions(options IPFilterOptions) OptionFn { for i, allowedIP := range options.AllowedIPs { options.AllowedIPs[i] = strings.TrimSpace(allowedIP) } return func(srvr *Server) { srvr.authIPFilterOptions = &options } } // FilterOptions sets ip filtering func FilterOptions(options IPFilterOptions) OptionFn { for i, allowedIP := range options.AllowedIPs { options.AllowedIPs[i] = strings.TrimSpace(allowedIP) } for i, blockedIP := range options.BlockedIPs { options.BlockedIPs[i] = strings.TrimSpace(blockedIP) } return func(srvr *Server) { srvr.ipFilterOptions = &options } } // Server is the main application type Server struct { authUser string authPass string authHtpasswd string authIPFilterOptions *IPFilterOptions htpasswdFile *htpasswd.File authIPFilter *ipFilter logger *log.Logger tlsConfig *tls.Config profilerEnabled bool locks sync.Map maxUploadSize int64 rateLimitRequests int purgeDays time.Duration purgeInterval time.Duration storage storage.Storage forceHTTPS bool randomTokenLength int ipFilterOptions *IPFilterOptions VirusTotalKey string ClamAVDaemonHost string performClamavPrescan bool tempPath string webPath string proxyPath string proxyPort string emailContact string gaKey string userVoiceKey string TLSListenerOnly bool CorsDomains string ListenerString string TLSListenerString string ProfileListenerString string Certificate string LetsEncryptCache string } // New is the factory fot Server func New(options ...OptionFn) (*Server, error) { s := &Server{ locks: sync.Map{}, } for _, optionFn := range options { optionFn(s) } return s, nil } var theRand *rand.Rand func init() { var seedBytes [8]byte if _, err := cryptoRand.Read(seedBytes[:]); err != nil { panic("cannot obtain cryptographically secure seed") } theRand = rand.New(rand.NewSource(int64(binary.LittleEndian.Uint64(seedBytes[:])))) } // Run starts Server func (s *Server) Run() { listening := false if s.profilerEnabled { listening = true go func() { s.logger.Println("Profiled listening at: :6060") _ = http.ListenAndServe(":6060", nil) }() } r := mux.NewRouter() var fs http.FileSystem if s.webPath != "" { s.logger.Println("Using static file path: ", s.webPath) fs = http.Dir(s.webPath) htmlTemplates, _ = htmlTemplates.ParseGlob(filepath.Join(s.webPath, "*.html")) textTemplates, _ = textTemplates.ParseGlob(filepath.Join(s.webPath, "*.txt")) } else { fs = &assetfs.AssetFS{ Asset: web.Asset, AssetDir: web.AssetDir, AssetInfo: func(path string) (os.FileInfo, error) { return os.Stat(path) }, Prefix: web.Prefix, } for _, path := range web.AssetNames() { bytes, err := web.Asset(path) if err != nil { s.logger.Panicf("Unable to parse: path=%s, err=%s", path, err) } if strings.HasSuffix(path, ".html") { _, err = htmlTemplates.New(stripPrefix(path)).Parse(string(bytes)) if err != nil { s.logger.Println("Unable to parse html template", err) } } if strings.HasSuffix(path, ".txt") { _, err = textTemplates.New(stripPrefix(path)).Parse(string(bytes)) if err != nil { s.logger.Println("Unable to parse text template", err) } } } } staticHandler := http.FileServer(fs) r.PathPrefix("/images/").Handler(staticHandler).Methods("GET") r.PathPrefix("/styles/").Handler(staticHandler).Methods("GET") r.PathPrefix("/scripts/").Handler(staticHandler).Methods("GET") r.PathPrefix("/fonts/").Handler(staticHandler).Methods("GET") r.PathPrefix("/ico/").Handler(staticHandler).Methods("GET") r.HandleFunc("/favicon.ico", staticHandler.ServeHTTP).Methods("GET") r.HandleFunc("/robots.txt", staticHandler.ServeHTTP).Methods("GET") r.HandleFunc("/{filename:(?:favicon\\.ico|robots\\.txt|health\\.html)}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT") r.HandleFunc("/health.html", healthHandler).Methods("GET") r.HandleFunc("/", s.viewHandler).Methods("GET") r.HandleFunc("/({files:.*}).zip", s.zipHandler).Methods("GET") r.HandleFunc("/({files:.*}).tar", s.tarHandler).Methods("GET") r.HandleFunc("/({files:.*}).tar.gz", s.tarGzHandler).Methods("GET") r.HandleFunc("/{token}/{filename}", s.headHandler).Methods("HEAD") r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", s.headHandler).Methods("HEAD") r.HandleFunc("/{token}/{filename}", s.previewHandler).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) (match bool) { // The file will show a preview page when opening the link in browser directly or // from external link. If the referer url path and current path are the same it will be // downloaded. if !acceptsHTML(r.Header) { return false } match = r.Referer() == "" u, err := url.Parse(r.Referer()) if err != nil { s.logger.Fatal(err) return } match = match || (u.Path != r.URL.Path) return }).Methods("GET") getHandlerFn := s.getHandler if s.rateLimitRequests > 0 { getHandlerFn = ratelimit.Request(ratelimit.IP).Rate(s.rateLimitRequests, 60*time.Second).LimitBy(memory.New())(http.HandlerFunc(getHandlerFn)).ServeHTTP } r.HandleFunc("/{token}/{filename}", getHandlerFn).Methods("GET") r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", getHandlerFn).Methods("GET") r.HandleFunc("/{filename}/virustotal", s.virusTotalHandler).Methods("PUT") r.HandleFunc("/{filename}/scan", s.scanHandler).Methods("PUT") r.HandleFunc("/put/{filename}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT") r.HandleFunc("/upload/{filename}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT") r.HandleFunc("/{filename}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT") r.HandleFunc("/", s.basicAuthHandler(http.HandlerFunc(s.postHandler))).Methods("POST") // r.HandleFunc("/{page}", viewHandler).Methods("GET") r.HandleFunc("/{token}/{filename}/{deletionToken}", s.deleteHandler).Methods("DELETE") r.NotFoundHandler = http.HandlerFunc(s.notFoundHandler) _ = mime.AddExtensionType(".md", "text/x-markdown") s.logger.Printf("Transfer.sh server started.\nusing temp folder: %s\nusing storage provider: %s", s.tempPath, s.storage.Type()) var cors func(http.Handler) http.Handler if len(s.CorsDomains) > 0 { cors = gorillaHandlers.CORS( gorillaHandlers.AllowedHeaders([]string{"*"}), gorillaHandlers.AllowedOrigins(strings.Split(s.CorsDomains, ",")), gorillaHandlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}), ) } else { cors = func(h http.Handler) http.Handler { return h } } h := handlers.PanicHandler( ipFilterHandler( handlers.LogHandler( LoveHandler( s.RedirectHandler(cors(r))), handlers.NewLogOptions(s.logger.Printf, "_default_"), ), s.ipFilterOptions, ), nil, ) if !s.TLSListenerOnly { listening = true s.logger.Printf("starting to listen on: %v\n", s.ListenerString) go func() { srvr := &http.Server{ Addr: s.ListenerString, Handler: h, } if err := srvr.ListenAndServe(); err != nil { s.logger.Fatal(err) } }() } if s.TLSListenerString != "" { listening = true s.logger.Printf("starting to listen for TLS on: %v\n", s.TLSListenerString) go func() { srvr := &http.Server{ Addr: s.TLSListenerString, Handler: h, TLSConfig: s.tlsConfig, } if err := srvr.ListenAndServeTLS("", ""); err != nil { s.logger.Fatal(err) } }() } s.logger.Printf("---------------------------") if s.purgeDays > 0 { go s.purgeHandler() } term := make(chan os.Signal, 1) signal.Notify(term, os.Interrupt) signal.Notify(term, syscall.SIGTERM) if listening { <-term } else { s.logger.Printf("No listener active.") } s.logger.Printf("Server stopped.") } ================================================ FILE: server/storage/common.go ================================================ package storage import ( "context" "fmt" "io" "regexp" "strconv" "time" ) type Range struct { Start uint64 Limit uint64 contentRange string } // Range Reconstructs Range header and returns it func (r *Range) Range() string { if r.Limit > 0 { return fmt.Sprintf("bytes=%d-%d", r.Start, r.Start+r.Limit-1) } else { return fmt.Sprintf("bytes=%d-", r.Start) } } // AcceptLength Tries to accept given range // returns newContentLength if range was satisfied, otherwise returns given contentLength func (r *Range) AcceptLength(contentLength uint64) (newContentLength uint64) { newContentLength = contentLength if r.Limit == 0 { r.Limit = newContentLength - r.Start } if contentLength < r.Start { return } if r.Limit > contentLength-r.Start { return } r.contentRange = fmt.Sprintf("bytes %d-%d/%d", r.Start, r.Start+r.Limit-1, contentLength) newContentLength = r.Limit return } func (r *Range) SetContentRange(cr string) { r.contentRange = cr } // Returns accepted Content-Range header. If range wasn't accepted empty string is returned func (r *Range) ContentRange() string { return r.contentRange } var rexp *regexp.Regexp = regexp.MustCompile(`^bytes=([0-9]+)-([0-9]*)$`) // Parses HTTP Range header and returns struct on success // only bytes=start-finish supported func ParseRange(rng string) *Range { if rng == "" { return nil } matches := rexp.FindAllStringSubmatch(rng, -1) if len(matches) != 1 || len(matches[0]) != 3 { return nil } if len(matches[0][0]) != len(rng) || len(matches[0][1]) == 0 { return nil } start, err := strconv.ParseUint(matches[0][1], 10, 64) if err != nil { return nil } if len(matches[0][2]) == 0 { return &Range{Start: start, Limit: 0} } finish, err := strconv.ParseUint(matches[0][2], 10, 64) if err != nil { return nil } if finish < start || finish+1 < finish { return nil } return &Range{Start: start, Limit: finish - start + 1} } // Storage is the interface for storage operation type Storage interface { // Get retrieves a file from storage Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) // Head retrieves content length of a file from storage Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) // Put saves a file on storage Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) error // Delete removes a file from storage Delete(ctx context.Context, token string, filename string) error // IsNotExist indicates if a file doesn't exist on storage IsNotExist(err error) bool // Purge cleans up the storage Purge(ctx context.Context, days time.Duration) error // Whether storage supports Get with Range header IsRangeSupported() bool // Type returns the storage type Type() string } func CloseCheck(c io.Closer) { if c == nil { return } if err := c.Close(); err != nil { fmt.Println("Received close error:", err) } } ================================================ FILE: server/storage/gdrive.go ================================================ package storage import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "time" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/drive/v3" "google.golang.org/api/googleapi" "google.golang.org/api/option" ) // GDrive is a storage backed by GDrive type GDrive struct { service *drive.Service rootID string basedir string localConfigPath string chunkSize int logger *log.Logger } const gDriveRootConfigFile = "root_id.conf" const gDriveTokenJSONFile = "token.json" const gDriveDirectoryMimeType = "application/vnd.google-apps.folder" // NewGDriveStorage is the factory for GDrive func NewGDriveStorage(ctx context.Context, clientJSONFilepath string, localConfigPath string, basedir string, chunkSize int, logger *log.Logger) (*GDrive, error) { b, err := os.ReadFile(clientJSONFilepath) if err != nil { return nil, err } // If modifying these scopes, delete your previously saved client_secret.json. config, err := google.ConfigFromJSON(b, drive.DriveScope, drive.DriveMetadataScope) if err != nil { return nil, err } httpClient := getGDriveClient(ctx, config, localConfigPath, logger) srv, err := drive.NewService(ctx, option.WithHTTPClient(httpClient)) if err != nil { return nil, err } storage := &GDrive{service: srv, basedir: basedir, rootID: "", localConfigPath: localConfigPath, chunkSize: chunkSize, logger: logger} err = storage.setupRoot() if err != nil { return nil, err } return storage, nil } func (s *GDrive) setupRoot() error { rootFileConfig := filepath.Join(s.localConfigPath, gDriveRootConfigFile) rootID, err := os.ReadFile(rootFileConfig) if err != nil && !os.IsNotExist(err) { return err } if string(rootID) != "" { s.rootID = string(rootID) return nil } dir := &drive.File{ Name: s.basedir, MimeType: gDriveDirectoryMimeType, } di, err := s.service.Files.Create(dir).Fields("id").Do() if err != nil { return err } s.rootID = di.Id err = os.WriteFile(rootFileConfig, []byte(s.rootID), os.FileMode(0600)) if err != nil { return err } return nil } func (s *GDrive) hasChecksum(f *drive.File) bool { return f.Md5Checksum != "" } func (s *GDrive) list(nextPageToken string, q string) (*drive.FileList, error) { return s.service.Files.List().Fields("nextPageToken, files(id, name, mimeType)").Q(q).PageToken(nextPageToken).Do() } func (s *GDrive) findID(filename string, token string) (string, error) { filename = strings.Replace(filename, `'`, `\'`, -1) filename = strings.Replace(filename, `"`, `\"`, -1) fileID, tokenID, nextPageToken := "", "", "" q := fmt.Sprintf("'%s' in parents and name='%s' and mimeType='%s' and trashed=false", s.rootID, token, gDriveDirectoryMimeType) l, err := s.list(nextPageToken, q) if err != nil { return "", err } for 0 < len(l.Files) { for _, fi := range l.Files { tokenID = fi.Id break } if l.NextPageToken == "" { break } l, err = s.list(l.NextPageToken, q) if err != nil { return "", err } } if filename == "" { return tokenID, nil } else if tokenID == "" { return "", fmt.Errorf("cannot find file %s/%s", token, filename) } q = fmt.Sprintf("'%s' in parents and name='%s' and mimeType!='%s' and trashed=false", tokenID, filename, gDriveDirectoryMimeType) l, err = s.list(nextPageToken, q) if err != nil { return "", err } for 0 < len(l.Files) { for _, fi := range l.Files { fileID = fi.Id break } if l.NextPageToken == "" { break } l, err = s.list(l.NextPageToken, q) if err != nil { return "", err } } if fileID == "" { return "", fmt.Errorf("cannot find file %s/%s", token, filename) } return fileID, nil } // Type returns the storage type func (s *GDrive) Type() string { return "gdrive" } // Head retrieves content length of a file from storage func (s *GDrive) Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) { var fileID string fileID, err = s.findID(filename, token) if err != nil { return } var fi *drive.File if fi, err = s.service.Files.Get(fileID).Context(ctx).Fields("size").Do(); err != nil { return } contentLength = uint64(fi.Size) return } // Get retrieves a file from storage func (s *GDrive) Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) { var fileID string fileID, err = s.findID(filename, token) if err != nil { return } var fi *drive.File fi, err = s.service.Files.Get(fileID).Fields("size", "md5Checksum").Do() if err != nil { return } if !s.hasChecksum(fi) { err = fmt.Errorf("cannot find file %s/%s", token, filename) return } contentLength = uint64(fi.Size) fileGetCall := s.service.Files.Get(fileID) if rng != nil { header := fileGetCall.Header() header.Set("Range", rng.Range()) } var res *http.Response res, err = fileGetCall.Context(ctx).Download() if err != nil { return } if rng != nil { reader = res.Body rng.AcceptLength(contentLength) return } reader = res.Body return } // Delete removes a file from storage func (s *GDrive) Delete(ctx context.Context, token string, filename string) (err error) { metadata, _ := s.findID(fmt.Sprintf("%s.metadata", filename), token) _ = s.service.Files.Delete(metadata).Do() var fileID string fileID, err = s.findID(filename, token) if err != nil { return } err = s.service.Files.Delete(fileID).Context(ctx).Do() return } // Purge cleans up the storage func (s *GDrive) Purge(ctx context.Context, days time.Duration) (err error) { nextPageToken := "" expirationDate := time.Now().Add(-1 * days).Format(time.RFC3339) q := fmt.Sprintf("'%s' in parents and modifiedTime < '%s' and mimeType!='%s' and trashed=false", s.rootID, expirationDate, gDriveDirectoryMimeType) l, err := s.list(nextPageToken, q) if err != nil { return err } for 0 < len(l.Files) { for _, fi := range l.Files { err = s.service.Files.Delete(fi.Id).Context(ctx).Do() if err != nil { return } } if l.NextPageToken == "" { break } l, err = s.list(l.NextPageToken, q) if err != nil { return } } return } // IsNotExist indicates if a file doesn't exist on storage func (s *GDrive) IsNotExist(err error) bool { if err == nil { return false } if e, ok := err.(*googleapi.Error); ok { return e.Code == http.StatusNotFound } return false } // Put saves a file on storage func (s *GDrive) Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) error { dirID, err := s.findID("", token) if err != nil { return err } if dirID == "" { dir := &drive.File{ Name: token, Parents: []string{s.rootID}, MimeType: gDriveDirectoryMimeType, } di, err := s.service.Files.Create(dir).Fields("id").Do() if err != nil { return err } dirID = di.Id } // Instantiate empty drive file dst := &drive.File{ Name: filename, Parents: []string{dirID}, MimeType: contentType, } _, err = s.service.Files.Create(dst).Context(ctx).Media(reader, googleapi.ChunkSize(s.chunkSize)).Do() if err != nil { return err } return nil } func (s *GDrive) IsRangeSupported() bool { return true } // Retrieve a token, saves the token, then returns the generated client. func getGDriveClient(ctx context.Context, config *oauth2.Config, localConfigPath string, logger *log.Logger) *http.Client { tokenFile := filepath.Join(localConfigPath, gDriveTokenJSONFile) tok, err := gDriveTokenFromFile(tokenFile) if err != nil { tok = getGDriveTokenFromWeb(ctx, config, logger) saveGDriveToken(tokenFile, tok, logger) } return config.Client(ctx, tok) } // Request a token from the web, then returns the retrieved token. func getGDriveTokenFromWeb(ctx context.Context, config *oauth2.Config, logger *log.Logger) *oauth2.Token { authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) fmt.Printf("Go to the following link in your browser then type the "+ "authorization code: \n%v\n", authURL) var authCode string if _, err := fmt.Scan(&authCode); err != nil { logger.Fatalf("Unable to read authorization code %v", err) } tok, err := config.Exchange(ctx, authCode) if err != nil { logger.Fatalf("Unable to retrieve token from web %v", err) } return tok } // Retrieves a token from a local file. func gDriveTokenFromFile(file string) (*oauth2.Token, error) { f, err := os.Open(file) defer CloseCheck(f) if err != nil { return nil, err } tok := &oauth2.Token{} err = json.NewDecoder(f).Decode(tok) return tok, err } // Saves a token to a file path. func saveGDriveToken(path string, token *oauth2.Token, logger *log.Logger) { logger.Printf("Saving credential file to: %s\n", path) f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) defer CloseCheck(f) if err != nil { logger.Fatalf("Unable to cache oauth token: %v", err) } err = json.NewEncoder(f).Encode(token) if err != nil { logger.Fatalf("Unable to encode oauth token: %v", err) } } ================================================ FILE: server/storage/local.go ================================================ package storage import ( "context" "fmt" "io" "log" "os" "path/filepath" "time" ) // LocalStorage is a local storage type LocalStorage struct { Storage basedir string logger *log.Logger } // NewLocalStorage is the factory for LocalStorage func NewLocalStorage(basedir string, logger *log.Logger) (*LocalStorage, error) { return &LocalStorage{basedir: basedir, logger: logger}, nil } // Type returns the storage type func (s *LocalStorage) Type() string { return "local" } // Head retrieves content length of a file from storage func (s *LocalStorage) Head(_ context.Context, token string, filename string) (contentLength uint64, err error) { path := filepath.Join(s.basedir, token, filename) var fi os.FileInfo if fi, err = os.Lstat(path); err != nil { return } contentLength = uint64(fi.Size()) return } // Get retrieves a file from storage func (s *LocalStorage) Get(_ context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) { path := filepath.Join(s.basedir, token, filename) var file *os.File // content type , content length if file, err = os.Open(path); err != nil { return } reader = file var fi os.FileInfo if fi, err = os.Lstat(path); err != nil { return } contentLength = uint64(fi.Size()) if rng != nil { contentLength = rng.AcceptLength(contentLength) if _, err = file.Seek(int64(rng.Start), 0); err != nil { return } } return } // Delete removes a file from storage func (s *LocalStorage) Delete(_ context.Context, token string, filename string) (err error) { metadata := filepath.Join(s.basedir, token, fmt.Sprintf("%s.metadata", filename)) _ = os.Remove(metadata) path := filepath.Join(s.basedir, token, filename) err = os.Remove(path) return } // Purge cleans up the storage func (s *LocalStorage) Purge(_ context.Context, days time.Duration) (err error) { err = filepath.Walk(s.basedir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } if info.ModTime().Before(time.Now().Add(-1 * days)) { err = os.Remove(path) return err } return nil }) return } // IsNotExist indicates if a file doesn't exist on storage func (s *LocalStorage) IsNotExist(err error) bool { if err == nil { return false } return os.IsNotExist(err) } // Put saves a file on storage func (s *LocalStorage) Put(_ context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) error { var f io.WriteCloser var err error path := filepath.Join(s.basedir, token) if err = os.MkdirAll(path, 0700); err != nil && !os.IsExist(err) { return err } f, err = os.OpenFile(filepath.Join(path, filename), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) defer CloseCheck(f) if err != nil { return err } if _, err = io.Copy(f, reader); err != nil { return err } return nil } func (s *LocalStorage) IsRangeSupported() bool { return true } ================================================ FILE: server/storage/s3.go ================================================ package storage import ( "context" "errors" "fmt" "io" "log" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" ) // S3Storage is a storage backed by AWS S3 type S3Storage struct { Storage bucket string s3 *s3.Client logger *log.Logger purgeDays time.Duration noMultipart bool } // NewS3Storage is the factory for S3Storage func NewS3Storage(ctx context.Context, accessKey, secretKey, bucketName string, purgeDays int, region, endpoint string, disableMultipart bool, forcePathStyle bool, logger *log.Logger) (*S3Storage, error) { cfg, err := getAwsConfig(ctx, accessKey, secretKey) if err != nil { return nil, err } client := s3.NewFromConfig(cfg, func(o *s3.Options) { o.Region = region o.UsePathStyle = forcePathStyle if len(endpoint) > 0 { o.EndpointResolver = s3.EndpointResolverFromURL(endpoint) } }) return &S3Storage{ bucket: bucketName, s3: client, logger: logger, noMultipart: disableMultipart, purgeDays: time.Duration(purgeDays*24) * time.Hour, }, nil } // Type returns the storage type func (s *S3Storage) Type() string { return "s3" } // Head retrieves content length of a file from storage func (s *S3Storage) Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) { key := fmt.Sprintf("%s/%s", token, filename) headRequest := &s3.HeadObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), } // content type , content length response, err := s.s3.HeadObject(ctx, headRequest) if err != nil { return } contentLength = uint64(response.ContentLength) return } // Purge cleans up the storage func (s *S3Storage) Purge(context.Context, time.Duration) (err error) { // NOOP expiration is set at upload time return nil } // IsNotExist indicates if a file doesn't exist on storage func (s *S3Storage) IsNotExist(err error) bool { if err == nil { return false } var nkerr *types.NoSuchKey return errors.As(err, &nkerr) } // Get retrieves a file from storage func (s *S3Storage) Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) { key := fmt.Sprintf("%s/%s", token, filename) getRequest := &s3.GetObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), } if rng != nil { getRequest.Range = aws.String(rng.Range()) } response, err := s.s3.GetObject(ctx, getRequest) if err != nil { return } contentLength = uint64(response.ContentLength) if rng != nil && response.ContentRange != nil { rng.SetContentRange(*response.ContentRange) } reader = response.Body return } // Delete removes a file from storage func (s *S3Storage) Delete(ctx context.Context, token string, filename string) (err error) { metadata := fmt.Sprintf("%s/%s.metadata", token, filename) deleteRequest := &s3.DeleteObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(metadata), } _, err = s.s3.DeleteObject(ctx, deleteRequest) if err != nil { return } key := fmt.Sprintf("%s/%s", token, filename) deleteRequest = &s3.DeleteObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), } _, err = s.s3.DeleteObject(ctx, deleteRequest) return } // Put saves a file on storage func (s *S3Storage) Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, _ uint64) (err error) { key := fmt.Sprintf("%s/%s", token, filename) s.logger.Printf("Uploading file %s to S3 Bucket", filename) var concurrency int if !s.noMultipart { concurrency = 20 } else { concurrency = 1 } // Create an uploader with the session and custom options uploader := manager.NewUploader(s.s3, func(u *manager.Uploader) { u.Concurrency = concurrency // default is 5 u.LeavePartsOnError = false }) var expire *time.Time if s.purgeDays.Hours() > 0 { expire = aws.Time(time.Now().Add(s.purgeDays)) } _, err = uploader.Upload(ctx, &s3.PutObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), Body: reader, Expires: expire, ContentType: aws.String(contentType), }) return } func (s *S3Storage) IsRangeSupported() bool { return true } func getAwsConfig(ctx context.Context, accessKey, secretKey string) (aws.Config, error) { return config.LoadDefaultConfig(ctx, config.WithCredentialsProvider(credentials.StaticCredentialsProvider{ Value: aws.Credentials{ AccessKeyID: accessKey, SecretAccessKey: secretKey, SessionToken: "", }, }), ) } ================================================ FILE: server/storage/storj.go ================================================ package storage import ( "context" "errors" "io" "log" "time" "storj.io/common/fpath" "storj.io/common/storj" "storj.io/uplink" ) // StorjStorage is a storage backed by Storj type StorjStorage struct { Storage project *uplink.Project bucket *uplink.Bucket purgeDays time.Duration logger *log.Logger } // NewStorjStorage is the factory for StorjStorage func NewStorjStorage(ctx context.Context, access, bucket string, purgeDays int, logger *log.Logger) (*StorjStorage, error) { var instance StorjStorage var err error ctx = fpath.WithTempData(ctx, "", true) uplConf := &uplink.Config{ UserAgent: "transfer-sh", } parsedAccess, err := uplink.ParseAccess(access) if err != nil { return nil, err } instance.project, err = uplConf.OpenProject(ctx, parsedAccess) if err != nil { return nil, err } instance.bucket, err = instance.project.EnsureBucket(ctx, bucket) if err != nil { //Ignoring the error to return the one that occurred first, but try to clean up. _ = instance.project.Close() return nil, err } instance.purgeDays = time.Duration(purgeDays*24) * time.Hour instance.logger = logger return &instance, nil } // Type returns the storage type func (s *StorjStorage) Type() string { return "storj" } // Head retrieves content length of a file from storage func (s *StorjStorage) Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) { key := storj.JoinPaths(token, filename) obj, err := s.project.StatObject(fpath.WithTempData(ctx, "", true), s.bucket.Name, key) if err != nil { return 0, err } contentLength = uint64(obj.System.ContentLength) return } // Get retrieves a file from storage func (s *StorjStorage) Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) { key := storj.JoinPaths(token, filename) s.logger.Printf("Getting file %s from Storj Bucket", filename) var options *uplink.DownloadOptions if rng != nil { options = new(uplink.DownloadOptions) options.Offset = int64(rng.Start) if rng.Limit > 0 { options.Length = int64(rng.Limit) } else { options.Length = -1 } } download, err := s.project.DownloadObject(fpath.WithTempData(ctx, "", true), s.bucket.Name, key, options) if err != nil { return nil, 0, err } contentLength = uint64(download.Info().System.ContentLength) if rng != nil { contentLength = rng.AcceptLength(contentLength) } reader = download return } // Delete removes a file from storage func (s *StorjStorage) Delete(ctx context.Context, token string, filename string) (err error) { key := storj.JoinPaths(token, filename) s.logger.Printf("Deleting file %s from Storj Bucket", filename) _, err = s.project.DeleteObject(fpath.WithTempData(ctx, "", true), s.bucket.Name, key) return } // Purge cleans up the storage func (s *StorjStorage) Purge(context.Context, time.Duration) (err error) { // NOOP expiration is set at upload time return nil } // Put saves a file on storage func (s *StorjStorage) Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) (err error) { key := storj.JoinPaths(token, filename) s.logger.Printf("Uploading file %s to Storj Bucket", filename) var uploadOptions *uplink.UploadOptions if s.purgeDays.Hours() > 0 { uploadOptions = &uplink.UploadOptions{Expires: time.Now().Add(s.purgeDays)} } writer, err := s.project.UploadObject(fpath.WithTempData(ctx, "", true), s.bucket.Name, key, uploadOptions) if err != nil { return err } n, err := io.Copy(writer, reader) if err != nil || uint64(n) != contentLength { //Ignoring the error to return the one that occurred first, but try to clean up. _ = writer.Abort() return err } err = writer.SetCustomMetadata(ctx, uplink.CustomMetadata{"content-type": contentType}) if err != nil { //Ignoring the error to return the one that occurred first, but try to clean up. _ = writer.Abort() return err } err = writer.Commit() return err } func (s *StorjStorage) IsRangeSupported() bool { return true } // IsNotExist indicates if a file doesn't exist on storage func (s *StorjStorage) IsNotExist(err error) bool { return errors.Is(err, uplink.ErrObjectNotFound) } ================================================ FILE: server/token.go ================================================ /* The MIT License (MIT) Copyright (c) 2020- Andrea Spacca and Stefan Benten. 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. */ package server import ( "strings" ) const ( // SYMBOLS characters used for short-urls SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ) // generate a token func token(length int) string { var builder strings.Builder builder.Grow(length) for i := 0; i < length; i++ { x := theRand.Intn(len(SYMBOLS) - 1) builder.WriteByte(SYMBOLS[x]) } return builder.String() } ================================================ FILE: server/token_test.go ================================================ package server import "testing" func BenchmarkTokenConcat(b *testing.B) { for i := 0; i < b.N; i++ { _ = token(5) + token(5) } } func BenchmarkTokenLonger(b *testing.B) { for i := 0; i < b.N; i++ { _ = token(10) } } ================================================ FILE: server/utils.go ================================================ /* The MIT License (MIT) Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/] Copyright (c) 2018-2020 Andrea Spacca. Copyright (c) 2020- Andrea Spacca and Stefan Benten. 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. */ package server import ( "fmt" "math" "net/http" "strconv" "strings" "time" "github.com/golang/gddo/httputil/header" ) func formatNumber(format string, s uint64) string { return renderFloat(format, float64(s)) } var renderFloatPrecisionMultipliers = [10]float64{ 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, } var renderFloatPrecisionRounders = [10]float64{ 0.5, 0.05, 0.005, 0.0005, 0.00005, 0.000005, 0.0000005, 0.00000005, 0.000000005, 0.0000000005, } func renderFloat(format string, n float64) string { // Special cases: // NaN = "NaN" // +Inf = "+Infinity" // -Inf = "-Infinity" if math.IsNaN(n) { return "NaN" } if n > math.MaxFloat64 { return "Infinity" } if n < -math.MaxFloat64 { return "-Infinity" } // default format precision := 2 decimalStr := "." thousandStr := "," positiveStr := "" negativeStr := "-" if len(format) > 0 { // If there is an explicit format directive, // then default values are these: precision = 9 thousandStr = "" // collect indices of meaningful formatting directives formatDirectiveChars := []rune(format) formatDirectiveIndices := make([]int, 0) for i, char := range formatDirectiveChars { if char != '#' && char != '0' { formatDirectiveIndices = append(formatDirectiveIndices, i) } } if len(formatDirectiveIndices) > 0 { // Directive at index 0: // Must be a '+' // Raise an error if not the case // index: 0123456789 // +0.000,000 // +000,000.0 // +0000.00 // +0000 if formatDirectiveIndices[0] == 0 { if formatDirectiveChars[formatDirectiveIndices[0]] != '+' { panic("renderFloat(): invalid positive sign directive") } positiveStr = "+" formatDirectiveIndices = formatDirectiveIndices[1:] } // Two directives: // First is thousands separator // Raise an error if not followed by 3-digit // 0123456789 // 0.000,000 // 000,000.00 if len(formatDirectiveIndices) == 2 { if (formatDirectiveIndices[1] - formatDirectiveIndices[0]) != 4 { panic("renderFloat(): thousands separator directive must be followed by 3 digit-specifiers") } thousandStr = string(formatDirectiveChars[formatDirectiveIndices[0]]) formatDirectiveIndices = formatDirectiveIndices[1:] } // One directive: // Directive is decimal separator // The number of digit-specifier following the separator indicates wanted precision // 0123456789 // 0.00 // 000,0000 if len(formatDirectiveIndices) == 1 { decimalStr = string(formatDirectiveChars[formatDirectiveIndices[0]]) precision = len(formatDirectiveChars) - formatDirectiveIndices[0] - 1 } } } // generate sign part var signStr string if n >= 0.000000001 { signStr = positiveStr } else if n <= -0.000000001 { signStr = negativeStr n = -n } else { signStr = "" n = 0.0 } // split number into integer and fractional parts intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision]) // generate integer part string intStr := strconv.Itoa(int(intf)) // add thousand separator if required if len(thousandStr) > 0 { for i := len(intStr); i > 3; { i -= 3 intStr = intStr[:i] + thousandStr + intStr[i:] } } // no fractional part, we can leave now if precision == 0 { return signStr + intStr } // generate fractional part fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision])) // may need padding if len(fracStr) < precision { fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr } return signStr + intStr + decimalStr + fracStr } // Request.RemoteAddress contains port, which we want to remove i.e.: // "[::1]:58292" => "[::1]" func ipAddrFromRemoteAddr(s string) string { idx := strings.LastIndex(s, ":") if idx == -1 { return s } return s[:idx] } func acceptsHTML(hdr http.Header) bool { actual := header.ParseAccept(hdr, "Accept") for _, s := range actual { if s.Value == "text/html" { return true } } return false } func formatSize(size int64) string { sizeFloat := float64(size) base := math.Log(sizeFloat) / math.Log(1024) sizeOn := math.Pow(1024, base-math.Floor(base)) var round float64 pow := math.Pow(10, float64(2)) digit := pow * sizeOn round = math.Floor(digit) newVal := round / pow var suffixes [5]string suffixes[0] = "B" suffixes[1] = "KB" suffixes[2] = "MB" suffixes[3] = "GB" suffixes[4] = "TB" getSuffix := suffixes[int(math.Floor(base))] return fmt.Sprintf("%s %s", strconv.FormatFloat(newVal, 'f', -1, 64), getSuffix) } func formatDurationDays(durationDays time.Duration) string { days := int(durationDays.Hours() / 24) if days == 1 { return fmt.Sprintf("%d day", days) } return fmt.Sprintf("%d days", days) } ================================================ FILE: server/virustotal.go ================================================ /* The MIT License (MIT) Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/] 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. */ package server import ( "fmt" "net/http" "github.com/gorilla/mux" "github.com/Aetherinox/go-virustotal" ) func (s *Server) virusTotalHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) filename := sanitize(vars["filename"]) contentLength := r.ContentLength contentType := r.Header.Get("Content-Type") s.logger.Printf("Submitting to VirusTotal: %s %d %s", filename, contentLength, contentType) vt, err := virustotal.NewVirusTotal(s.VirusTotalKey) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } reader := r.Body result, err := vt.Scan(filename, reader) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } s.logger.Println(result) _, _ = w.Write([]byte(fmt.Sprintf("%v\n", result.Permalink))) }