[
  {
    "path": ".bowerrc",
    "content": "{\n    \"directory\": \"transfersh-web/bower_components\"\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": "build\npkg\ndist\nsrc\nbin\n*.pyc\n*.egg-info\n.vagrant\n.git\n.tmp\nbower_components\nnode_modules\nextras\nbuild\ntransfersh-server/run.sh\n.elasticbeanstalk\nDockerfile\n"
  },
  {
    "path": ".github/build/friendly-filenames.json",
    "content": "{\n    \"android-arm64\": { \"friendlyName\": \"android-arm64-v8a\" },\n    \"darwin-amd64\": { \"friendlyName\": \"darwin-amd64\" },\n    \"darwin-arm64\": { \"friendlyName\": \"darwin-arm64\" },\n    \"dragonfly-amd64\": { \"friendlyName\": \"dragonfly-amd64\" },\n    \"freebsd-386\": { \"friendlyName\": \"freebsd-386\" },\n    \"freebsd-amd64\": { \"friendlyName\": \"freebsd-amd64\" },\n    \"freebsd-arm64\": { \"friendlyName\": \"freebsd-arm64-v8a\" },\n    \"freebsd-arm7\": { \"friendlyName\": \"freebsd-arm32-v7a\" },\n    \"linux-386\": { \"friendlyName\": \"linux-386\" },\n    \"linux-amd64\": { \"friendlyName\": \"linux-amd64\" },\n    \"linux-arm5\": { \"friendlyName\": \"linux-arm32-v5\" },\n    \"linux-arm64\": { \"friendlyName\": \"linux-arm64-v8a\" },\n    \"linux-arm6\": { \"friendlyName\": \"linux-arm32-v6\" },\n    \"linux-arm7\": { \"friendlyName\": \"linux-armv7\" },\n    \"linux-mips64le\": { \"friendlyName\": \"linux-mips64le\" },\n    \"linux-mips64\": { \"friendlyName\": \"linux-mips64\" },\n    \"linux-mipslesoftfloat\": { \"friendlyName\": \"linux-mips32le-softfloat\" },\n    \"linux-mipsle\": { \"friendlyName\": \"linux-mips32le\" },\n    \"linux-mipssoftfloat\": { \"friendlyName\": \"linux-mips32-softfloat\" },\n    \"linux-mips\": { \"friendlyName\": \"linux-mips32\" },\n    \"linux-ppc64le\": { \"friendlyName\": \"linux-ppc64le\" },\n    \"linux-ppc64\": { \"friendlyName\": \"linux-ppc64\" },\n    \"linux-riscv64\": { \"friendlyName\": \"linux-riscv64\" },\n    \"linux-s390x\": { \"friendlyName\": \"linux-s390x\" },\n    \"openbsd-386\": { \"friendlyName\": \"openbsd-386\" },\n    \"openbsd-amd64\": { \"friendlyName\": \"openbsd-amd64\" },\n    \"openbsd-arm64\": { \"friendlyName\": \"openbsd-arm64-v8a\" },\n    \"openbsd-arm7\": { \"friendlyName\": \"openbsd-arm32-v7a\" },\n    \"windows-386\": { \"friendlyName\": \"windows-386\" },\n    \"windows-amd64\": { \"friendlyName\": \"windows-amd64\" },\n    \"windows-arm7\": { \"friendlyName\": \"windows-arm32-v7a\" }\n  }\n"
  },
  {
    "path": ".github/workflows/build-docker-images.yml",
    "content": "name: deploy multi-architecture Docker images for transfer.sh with buildx\n\non:\n  schedule:\n    - cron: '0 0 * * *' # everyday at midnight UTC\n  pull_request:\n    branches: main\n  push:\n    branches: main\n    tags:\n      - v*\n\njobs:\n  buildx:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v2\n      -\n        name: Prepare\n        id: prepare\n        run: |\n          DOCKER_IMAGE=dutchcoders/transfer.sh\n          DOCKER_PLATFORMS=linux/amd64,linux/arm/v7,linux/arm64,linux/386\n          VERSION=edge\n\n          if [[ $GITHUB_REF == refs/tags/* ]]; then\n            VERSION=v${GITHUB_REF#refs/tags/v}\n          fi\n\n          if [ \"${{ github.event_name }}\" = \"schedule\" ]; then\n            VERSION=nightly\n          fi\n\n          TAGS=\"--tag ${DOCKER_IMAGE}:${VERSION}\"\n          TAGS_NOROOT=\"--tag ${DOCKER_IMAGE}:${VERSION}-noroot\"\n\n          if [ $VERSION = edge -o $VERSION = nightly ]; then\n            TAGS=\"$TAGS --tag ${DOCKER_IMAGE}:latest\"\n            TAGS_NOROOT=\"$TAGS_NOROOT --tag ${DOCKER_IMAGE}:latest-noroot\"\n          fi\n\n          echo ::set-output name=docker_image::${DOCKER_IMAGE}\n          echo ::set-output name=version::${VERSION}\n          echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \\\n            --build-arg VERSION=${VERSION} \\\n            --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \\\n            --build-arg VCS_REF=${GITHUB_SHA::8} \\\n            ${TAGS} .\n          echo ::set-output name=buildx_args_noroot::--platform ${DOCKER_PLATFORMS} \\\n            --build-arg VERSION=${VERSION} \\\n            --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \\\n            --build-arg VCS_REF=${GITHUB_SHA::8} \\\n            --build-arg RUNAS=noroot \\\n            ${TAGS_NOROOT} .\n      -\n        name: Set up QEMU\n        uses: docker/setup-qemu-action@v1\n        with:\n          platforms: all\n      -\n        name: Set up Docker Buildx\n        id: buildx\n        uses: docker/setup-buildx-action@v1\n        with:\n          version: latest\n      -\n        name: Available platforms\n        run: echo ${{ steps.buildx.outputs.platforms }}\n      -\n        name: Docker Buildx (build)\n        run: |\n          docker buildx build --no-cache --pull --output \"type=image,push=false\" ${{ steps.prepare.outputs.buildx_args }}\n          docker buildx build --output \"type=image,push=false\" ${{ steps.prepare.outputs.buildx_args_noroot }}\n      -\n        name: Docker Login\n        if: success() && github.event_name != 'pull_request'\n        env:\n          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}\n          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}\n        run: |\n          echo \"${DOCKER_PASSWORD}\" | docker login --username \"${DOCKER_USERNAME}\" --password-stdin\n      -\n        name: Docker Buildx (push)\n        if: success() && github.event_name != 'pull_request'\n        run: |\n          docker buildx build --output \"type=image,push=true\" ${{ steps.prepare.outputs.buildx_args }}\n          docker buildx build --output \"type=image,push=true\" ${{ steps.prepare.outputs.buildx_args_noroot }}\n      -\n        name: Docker Check Manifest\n        if: always() && github.event_name != 'pull_request'\n        run: |\n          docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}\n          docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}-noroot\n      -\n        name: Clear\n        if: always() && github.event_name != 'pull_request'\n        run: |\n          rm -f ${HOME}/.docker/config.json\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Build and Release\n\non:\n  workflow_dispatch:\n  release:\n    types: [published]\njobs:\n  build:\n    strategy:\n      matrix:\n        # Include amd64 on all platforms.\n        goos: [windows, freebsd, openbsd, linux, dragonfly, darwin]\n        goarch: [amd64, 386]\n        exclude:\n          # Exclude i386 on darwin and dragonfly.\n          - goarch: 386\n            goos: dragonfly\n          - goarch: 386\n            goos: darwin\n        include:\n          # BEIGIN MacOS ARM64\n          - goos: darwin\n            goarch: arm64\n          # END MacOS ARM64\n          # BEGIN Linux ARM 5 6 7\n          - goos: linux\n            goarch: arm\n            goarm: 7\n          - goos: linux\n            goarch: arm\n            goarm: 6\n          - goos: linux\n            goarch: arm\n            goarm: 5\n          # END Linux ARM 5 6 7\n          # BEGIN Android ARM 8\n          - goos: android\n            goarch: arm64\n          # END Android ARM 8\n          # Windows ARM 7\n          - goos: windows\n            goarch: arm\n            goarm: 7\n          # BEGIN Other architectures\n          # BEGIN riscv64 & ARM64\n          - goos: linux\n            goarch: arm64\n          - goos: linux\n            goarch: riscv64\n          # END riscv64 & ARM64\n          # BEGIN MIPS\n          - goos: linux\n            goarch: mips64\n          - goos: linux\n            goarch: mips64le\n          - goos: linux\n            goarch: mipsle\n          - goos: linux\n            goarch: mips\n          # END MIPS\n          # BEGIN PPC\n          - goos: linux\n            goarch: ppc64\n          - goos: linux\n            goarch: ppc64le\n          # END PPC\n          # BEGIN FreeBSD ARM\n          - goos: freebsd\n            goarch: arm64\n          - goos: freebsd\n            goarch: arm\n            goarm: 7\n          # END FreeBSD ARM\n          # BEGIN S390X\n          - goos: linux\n            goarch: s390x\n          # END S390X\n          # END Other architectures\n          # BEGIN OPENBSD ARM\n          - goos: openbsd\n            goarch: arm64\n          - goos: openbsd\n            goarch: arm\n            goarm: 7\n          # END OPENBSD ARM\n      fail-fast: false\n\n    runs-on: ubuntu-latest\n    env:\n      GOOS: ${{ matrix.goos }}\n      GOARCH: ${{ matrix.goarch }}\n      GOARM: ${{ matrix.goarm }}\n      CGO_ENABLED: 0\n    steps:\n      - name: Checkout codebase\n        uses: actions/checkout@v2\n\n      - name: Show workflow information \n        id: get_filename\n        run: |\n          export _NAME=$(jq \".[\\\"$GOOS-$GOARCH$GOARM$GOMIPS\\\"].friendlyName\" -r < .github/build/friendly-filenames.json)\n          echo \"GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, GOMIPS: $GOMIPS, RELEASE_NAME: $_NAME\"\n          echo \"::set-output name=ASSET_NAME::$_NAME\"\n          echo \"::set-output name=GIT_TAG::${GITHUB_REF##*/}\"\n          echo \"ASSET_NAME=$_NAME\" >> $GITHUB_ENV\n\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ^1.22\n\n      - name: Get project dependencies\n        run: go mod download\n \n      - name: Build Transfersh\n        run: |\n          mkdir -p build_assets\n          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}\n    \n      - name: Build Mips softfloat Transfersh\n        if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle'\n        run: |\n          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}\n\n      - name: Rename Windows Transfersh\n        if: matrix.goos == 'windows'\n        run: |\n          cd ./build_assets || exit 1\n          mv transfersh-${GITHUB_REF##*/}-${ASSET_NAME} transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.exe\n\n      - name: Prepare to release\n        run: |\n          cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md\n          cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE\n\n      - name: Create Gzip archive\n        shell: bash\n        run: |\n          pushd build_assets || exit 1\n          touch -mt $(date +%Y01010000) *\n          tar zcvf transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz *\n          mv transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz ../\n          FILE=`find . -name \"transfersh-${GITHUB_REF##*/}-${ASSET_NAME}*\"`\n          DGST=$FILE.sha256sum\n          echo `sha256sum $FILE` > $DGST\n          popd || exit 1\n          FILE=./transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz\n          DGST=$FILE.sha256sum\n          echo `sha256sum $FILE` > $DGST\n\n      - name: Change the name\n        run: |\n          mv build_assets transfersh-${GITHUB_REF##*/}-${ASSET_NAME}\n\n      - name: Upload files to Artifacts\n        uses: actions/upload-artifact@v2\n        with:\n          name: transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}\n          path: |\n            ./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}/*\n\n      - name: Upload binaries to release\n        uses: softprops/action-gh-release@v1\n        if: github.event_name == 'release'\n        with:\n          files: |\n            ./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}.tar.gz*\n            ./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 }}*\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\non:\n  pull_request:\n    branches:\n      - \"*\"\n  push:\n    branches:\n      - \"*\"\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        go_version:\n          - '1.22'\n          - '1.23'\n          - '1.24'\n          - tip\n    name: Test with ${{ matrix.go_version }}\n    steps:\n      - uses: actions/checkout@v2\n      - name: Install Go ${{ matrix.go_version }}\n        if: ${{ matrix.go_version != 'tip' }}\n        uses: actions/setup-go@master\n        with:\n          go-version: ${{ matrix.go_version }}\n          check-latest: true\n      - name: Install Go ${{ matrix.go_version }}\n        if: ${{ matrix.go_version == 'tip' }}\n        run: |\n          go install golang.org/dl/gotip@latest\n          `go env GOPATH`/bin/gotip download\n      - name: Vet and test no tip\n        if: ${{ matrix.go_version != 'tip' }}\n        run: |\n          go version\n          go vet ./...\n          go test ./...\n      - name: Vet and test gotip\n        if: ${{ matrix.go_version == 'tip' }}\n        run: |\n          `go env GOPATH`/bin/gotip version\n          `go env GOPATH`/bin/gotip vet ./...\n          `go env GOPATH`/bin/gotip test ./...\n  golangci:\n    name: Linting\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-go@master\n        with:\n          go-version: '1.24'\n          check-latest: true\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v2\n        with:\n          version: latest\n          skip-go-installation: true\n          args: \"--config .golangci.yml\"\n"
  },
  {
    "path": ".gitignore",
    "content": "build/\npkg/\ndist/\nsrc/\nbin/\n*.pyc\n*.egg-info/\n.idea/\n\n.tmp\n.vagrant\n\nbower_components/\nnode_modules/\n\ntransfersh-server/run.sh\n.elasticbeanstalk/\n\n# Elastic Beanstalk Files\n.elasticbeanstalk/*\n!.elasticbeanstalk/*.cfg.yml\n!.elasticbeanstalk/*.global.yml\n\n!.github/build/\n"
  },
  {
    "path": ".golangci.yml",
    "content": "run:\n  deadline: 10m\n  issues-exit-code: 1\n  tests: true\n\noutput:\n  format: colored-line-number\n  print-issued-lines: true\n  print-linter-name: true\n\nlinters:\n  disable:\n    - deadcode\n    - unused\n\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\n  new: false\n  exclude-use-default: false\n"
  },
  {
    "path": ".jshintrc",
    "content": "{\n    \"node\": true,\n    \"browser\": true,\n    \"esnext\": true,\n    \"bitwise\": true,\n    \"camelcase\": true,\n    \"curly\": true,\n    \"eqeqeq\": true,\n    \"immed\": true,\n    \"indent\": 2,\n    \"latedef\": true,\n    \"newcap\": true,\n    \"noarg\": true,\n    \"quotmark\": \"single\",\n    \"regexp\": true,\n    \"undef\": true,\n    \"unused\": true,\n    \"strict\": true,\n    \"trailing\": true,\n    \"smarttabs\": true,\n    \"jquery\": true,\n    \"white\": true\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "\n# Contributor Code of Conduct\n\nAs 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.\n\nWe 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.\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery\n* Personal attacks\n* Trolling or insulting/derogatory comments\n* Public or private harassment\n* Publishing other's private information, such as physical or electronic addresses, without explicit permission\n* Other unethical or unprofessional conduct\n* Use of harsh language\n\nProject 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.\n\nThis code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.\n\nThis 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\n\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Default to Go 1.24\nARG GO_VERSION=1.24\nFROM golang:${GO_VERSION}-alpine as build\n\n# Necessary to run 'go get' and to compile the linked binary\nRUN apk add git musl-dev mailcap\n\nWORKDIR /go/src/github.com/dutchcoders/transfer.sh\n\nCOPY go.mod go.sum ./\n\nRUN go mod download\n\nCOPY . .\n\n# build & install server\nRUN 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\n\nARG PUID=5000 \\\n    PGID=5000 \\\n    RUNAS\n\nRUN mkdir -p /tmp/useradd /tmp/empty && \\\n    if [ ! -z \"$RUNAS\" ]; then \\\n    echo \"${RUNAS}:x:${PUID}:${PGID}::/nonexistent:/sbin/nologin\" >> /tmp/useradd/passwd && \\\n    echo \"${RUNAS}:!:::::::\" >> /tmp/useradd/shadow && \\\n    echo \"${RUNAS}:x:${PGID}:\" >> /tmp/useradd/group && \\\n    echo \"${RUNAS}:!::\" >> /tmp/useradd/groupshadow; else touch /tmp/useradd/unused; fi\n\nFROM scratch AS final\nLABEL maintainer=\"Andrea Spacca <andrea.spacca@gmail.com>\"\nARG RUNAS\n\nCOPY --from=build /etc/mime.types /etc/mime.types\nCOPY --from=build /tmp/empty /tmp\nCOPY --from=build /tmp/useradd/* /etc/\nCOPY --from=build --chown=${RUNAS}  /go/bin/transfersh /go/bin/transfersh\nCOPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt\n\nUSER ${RUNAS}\n\nENTRYPOINT [\"/go/bin/transfersh\", \"--listener\", \":8080\"]\n\nEXPOSE 8080\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014-2018 DutchCoders [https://github.com/dutchcoders/]\nCopyright (c) 2018-2020 Andrea Spacca.\nCopyright (c) 2020- Andrea Spacca and Stefan Benten.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: lint\n\nlint:\n\tgolangci-lint run --out-format=github-actions --config .golangci.yml \n\n"
  },
  {
    "path": "README.md",
    "content": "# 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)\n\nEasy and fast file sharing from the command-line. This code contains the server with everything you need to create your own instance.\n\nTransfer.sh currently supports the s3 (Amazon S3), gdrive (Google Drive), storj (Storj) providers, and local file system (local).\n\n<br />\n\n---\n\n<br />\n\n## Disclaimer\n\n@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.\n\nThe two are anyway unrelated, and the repo is not the place to direct requests and issues for any of the pubblic installation.\n\nNo third-party public installation of the software in the repo will be advertised or mentioned in the repo itself, for security reasons.\n\nThe 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.\n\n<br />\n\n---\n\n<br />\n\n## Usage\n\nThis section outlines how to use transfer.sh\n\n<br />\n\n### Upload\n\n```bash\n$ curl -v --upload-file ./hello.txt https://transfer.sh/hello.txt\n```\n\n<br />\n\n### Encrypt & Upload\n\n```bash\n$ gpg --armor --symmetric --output - /tmp/hello.txt | curl --upload-file - https://transfer.sh/test.txt\n```\n\n<br />\n\n### Download & Decrypt\n\n```bash\n$ curl https://transfer.sh/1lDau/test.txt | gpg --decrypt --output /tmp/hello.txt\n```\n\n<br />\n\n### Upload to Virustotal\n\n```bash\n$ curl -X PUT --upload-file nhgbhhj https://transfer.sh/test.txt/virustotal\n```\n\n<br />\n\n### Deleting\n\n```bash\n$ curl -X DELETE <X-Url-Delete Response Header URL>\n```\n\n<br />\n\n---\n\n<br />\n\n## Request Headers\n\nThis section explains how to handle request headers with curl:\n\n<br />\n\n### Max-Downloads\n\n```bash\n$ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H \"Max-Downloads: 1\" # Limit the number of downloads\n```\n\n<br />\n\n### Max-Days\n\n```bash\n$ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H \"Max-Days: 1\" # Set the number of days before deletion\n```\n\n<br />\n\n### X-Encrypt-Password\n\n#### Beware, use this feature only on your self-hosted server: trusting a third-party service for server side encryption is at your own risk\n```bash\n$ 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\n```\n\n<br />\n\n### X-Decrypt-Password\n#### Beware, use this feature only on your self-hosted server: trusting a third-party service for server side encryption is at your own risk\n\n```bash\n$ 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\n```\n\n<br />\n\n---\n\n<br />\n\n## Response Headers\n\nThis section explains how to handle response headers:\n\n<br />\n\n### X-Url-Delete\n\nThe URL used to request the deletion of a file and returned as a response header:\n\n```bash\ncurl -sD - --upload-file ./hello.txt https://transfer.sh/hello.txt | grep -i -E 'transfer\\.sh|x-url-delete'\nx-url-delete: https://transfer.sh/hello.txt/BAYh0/hello.txt/PDw0NHPcqU\nhttps://transfer.sh/hello.txt/BAYh0/hello.txt\n```\n\n<br />\n\n---\n\n<br />\n\n## Examples\n\nSee good usage examples on [examples.md](examples.md)\n\n<br />\n\n## Link aliases\n\nCreate direct download link:\n\nhttps://transfer.sh/1lDau/test.txt --> https://transfer.sh/get/1lDau/test.txt\n\nInline file:\n\nhttps://transfer.sh/1lDau/test.txt --> https://transfer.sh/inline/1lDau/test.txt\n\n<br />\n\n---\n\n<br />\n\n## Usage\n\nParameter | Description                                                                             | Value                         | Env                         \n--- |-----------------------------------------------------------------------------------------------|-------------------------------|-------------------------------|\nlistener | port to use for http (:80)                                                               |                               | LISTENER                      |\nprofile-listener | port to use for profiler (:6060)                                                 |                               | PROFILE_LISTENER              |\nforce-https | redirect to https                                                                     | false                         | FORCE_HTTPS                   |\ntls-listener | port to use for https (:443)                                                         |                               | TLS_LISTENER                  |\ntls-listener-only | flag to enable tls listener only                                                |                               | TLS_LISTENER_ONLY             |\ntls-cert-file | path to tls certificate                                                             |                               | TLS_CERT_FILE                 |\ntls-private-key | path to tls private key                                                           |                               | TLS_PRIVATE_KEY               |\nhttp-auth-user | user for basic http auth on upload                                                 |                               | HTTP_AUTH_USER                |\nhttp-auth-pass | pass for basic http auth on upload                                                 |                               | HTTP_AUTH_PASS                |\nhttp-auth-htpasswd | htpasswd file path for basic http auth on upload                               |                               | HTTP_AUTH_HTPASSWD            |\nhttp-auth-ip-whitelist | comma separated list of allowed ips to upload without auth challenge       |                               | HTTP_AUTH_IP_WHITELIST        |\nvirustotal-key | VirusTotal API key                                                                 |                               | VIRUSTOTAL_KEY                |\nip-whitelist | comma separated list of ips allowed to connect to the service                        |                               | IP_WHITELIST                  |\nip-blacklist | comma separated list of ips not allowed to connect to the service                    |                               | IP_BLACKLIST                  |\ntemp-path | path to temp folder                                                                     | system temp                   | TEMP_PATH                     |\nweb-path | path to static web files (for development or custom front end)                           |                               | WEB_PATH                      |\nproxy-path | path prefix when service is run behind a proxy (a `/` prefix will be trimmed)          |                               | PROXY_PATH                    |\nproxy-port | port of the proxy when the service is run behind a proxy                               |                               | PROXY_PORT                    |\nemail-contact | email contact for the front end                                                     |                               | EMAIL_CONTACT                 |\nga-key | google analytics key for the front end                                                     |                               | GA_KEY                        |\nprovider | which storage provider to use                                                            | (s3, storj, gdrive or local)  |                               |\nuservoice-key | user voice key for the front end                                                    |                               | USERVOICE_KEY                 |\naws-access-key | aws access key                                                                     |                               | AWS_ACCESS_KEY                |\naws-secret-key | aws access key                                                                     |                               | AWS_SECRET_KEY                |\nbucket | aws bucket                                                                                 |                               | BUCKET                        |\ns3-endpoint | Custom S3 endpoint.                                                                   |                               | S3_ENDPOINT                   |\ns3-region | region of the s3 bucket                                                                 | eu-west-1                     | S3_REGION                     |\ns3-no-multipart | disables s3 multipart upload                                                      | false                         | S3_NO_MULTIPART               |\ns3-path-style | Forces path style URLs, required for Minio.                                         | false                         | S3_PATH_STYLE                 |\nstorj-access | Access for the project                                                               |                               | STORJ_ACCESS                  |\nstorj-bucket | Bucket to use within the project                                                     |                               | STORJ_BUCKET                  |\nbasedir | path storage for local/gdrive provider                                                    |                               | BASEDIR                       |\ngdrive-client-json-filepath | path to oauth client json config for gdrive provider                  |                               | GDRIVE_CLIENT_JSON_FILEPATH   |\ngdrive-local-config-path | path to store local transfer.sh config cache for gdrive provider         |                               | GDRIVE_LOCAL_CONFIG_PATH      |\ngdrive-chunk-size | chunk size for gdrive upload in megabytes, must be lower than available memory (8 MB) |                         | GDRIVE_CHUNK_SIZE             |\nlets-encrypt-hosts | hosts to use for lets encrypt certificates (comma separated)                   |                               | HOSTS                         |\nlog | path to log file                                                                              |                               | LOG                           |\ncors-domains | comma separated list of domains for CORS, setting it enable CORS                     |                               | CORS_DOMAINS                  |\nclamav-host | host for clamav feature                                                               |                               | CLAMAV_HOST                   |\nperform-clamav-prescan | prescan every upload using clamav (clamav-host must be local clamd unix socket)    |                       | PERFORM_CLAMAV_PRESCAN        |\nrate-limit | request per minute                                                                     |                               | RATE_LIMIT                    |\nmax-upload-size | max upload size in kilobytes                                                      |                               | MAX_UPLOAD_SIZE               |\npurge-days | number of days after the uploads are purged automatically                              |                               | PURGE_DAYS                    |   \npurge-interval | interval (hours) to run automatic purge for (excluding S3 and Storj)               |                               | PURGE_INTERVAL                |   \nrandom-token-length | length of random token for upload path (double the size for delete path)      | 6                             | RANDOM_TOKEN_LENGTH           |   \n\nIf 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.\n\nIf you want to use TLS using your own certificates, set tls-listener to :443, force-https, tls-cert-file and tls-private-key.\n\n<br />\n\n---\n\n<br />\n\n## Development\n\nSwitched to GO111MODULE\n\n```bash\ngo run main.go --provider=local --listener :8080 --temp-path=/tmp/ --basedir=/tmp/\n```\n\n<br />\n\n---\n\n<br />\n\n## Build\n\n```bash\n$ git clone git@github.com:dutchcoders/transfer.sh.git\n$ cd transfer.sh\n$ go build -o transfersh main.go\n```\n\n<br />\n\n---\n\n<br />\n\n## Docker\n\nFor easy deployment, we've created an official Docker container. There are two variants, differing only by which user runs the process.\n\nThe default one will run as `root`:\n\n> [!WARNING]\n> 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.\n\n```bash\ndocker run --publish 8080:8080 dutchcoders/transfer.sh:latest --provider local --basedir /tmp/\n```\n\n<br />\n\n### No root\n\nThe `-noroot` tags indicate image builds that run with least priviledge to reduce the attack surface might an application get compromised.\n\n> [!NOTE]\n> Using `-noroot` is **recommended**\n\n<br />\n\nThe one tagged with the suffix `-noroot` will use `5000` as both UID and GID:\n\n```bash\ndocker run --publish 8080:8080 dutchcoders/transfer.sh:latest-noroot --provider local --basedir /tmp/\n```\n\n<br />\n\n> [!NOTE]\n> Development history details at:\n> - https://github.com/dutchcoders/transfer.sh/pull/418\n\n<br />\n\n### Tags\n\nName | Usage\n--|--\nlatest| Latest CI build, can be nightly, at commit, at tag, etc.\nlatest-noroot| Latest CI build, can be nightly, at commit, at tag, etc. using [no root]\nnightly| Scheduled CI build every midnight UTC\nnightly-noroot| Scheduled CI build every midnight UTC using [no root]\nedge| Latest CI build after every commit on `main`\nedge-noroot| Latest CI build after every commit on `main` using [no root]\nv`x.y.z`| CI build after tagging a release\nv`x.y.z`-noroot| CI build after tagging a release using [no root]\n\n<br />\n\n### Building the Container\n\nYou can also build the container yourself. This allows you to choose which UID/GID will be used, e.g. when using NFS mounts:\n\n```bash\n# Build arguments:\n# * RUNAS: If empty, the container will run as root.\n#          Set this to anything to enable UID/GID selection.\n# * PUID:  UID of the process. Needs RUNAS != \"\". Defaults to 5000.\n# * PGID:  GID of the process. Needs RUNAS != \"\". Defaults to 5000.\n\ndocker build -t transfer.sh-noroot --build-arg RUNAS=doesntmatter --build-arg PUID=1337 --build-arg PGID=1338 .\n```\n\n<br />\n\n---\n\n<br />\n\n## S3 Usage\n\nFor the usage with a AWS S3 Bucket, you just need to specify the following options:\n- provider `--provider s3`\n- aws-access-key _(either via flag or environment variable `AWS_ACCESS_KEY`)_\n- aws-secret-key _(either via flag or environment variable `AWS_SECRET_KEY`)_\n- bucket _(either via flag or environment variable `BUCKET`)_\n- s3-region _(either via flag or environment variable `S3_REGION`)_\n\nIf you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically.\n\n<br />\n\n### Custom S3 providers\n\nTo use a custom non-AWS S3 provider, you need to specify the endpoint as defined from your cloud provider.\n\n<br />\n\n---\n\n<br />\n\n## Storj Network Provider\n\nTo use the Storj Network as a storage provider you need to specify the following flags:\n- provider `--provider storj`\n- storj-access _(either via flag or environment variable STORJ_ACCESS)_\n- storj-bucket _(either via flag or environment variable STORJ_BUCKET)_\n\n<br />\n\n### Creating Bucket and Scope\n\nYou need to create an access grant (or copy it from the uplink configuration) and a bucket in preparation.\n\nTo get started, log in to your account and go to the Access Grant Menu and start the Wizard on the upper right.\n\nEnter your access grant name of choice, hit *Next* and restrict it as necessary/preferred.\nAfterwards continue either in CLI or within the Browser. Next, you'll be asked for a Passphrase used as Encryption Key.\n**Make sure to save it in a safe place. Without it, you will lose the ability to decrypt your files!**\n\nAfterwards, you can copy the access grant and then start the startup of the transfer.sh endpoint. \nIt is recommended to provide both the access grant and the bucket name as ENV Variables for enhanced security.\n\nExample:\n\n```\nexport STORJ_BUCKET=<BUCKET NAME>\nexport STORJ_ACCESS=<ACCESS GRANT>\ntransfer.sh --provider storj\n```\n\n<br />\n\n---\n\n<br />\n\n## Google Drive Usage\n\nFor the usage with Google drive, you need to specify the following options:\n- provider\n- gdrive-client-json-filepath\n- gdrive-local-config-path\n- basedir\n\n<br />\n\n### Creating Gdrive Client Json\n\nYou need to create an OAuth Client id from console.cloud.google.com, download the file, and place it into a safe directory.\n\n<br />\n\n### Usage example\n\n```go run main.go --provider gdrive --basedir /tmp/ --gdrive-client-json-filepath /[credential_dir] --gdrive-local-config-path [directory_to_save_config] ```\n\n<br />\n\n---\n\n<br />\n\n## Shell functions\n\n### Bash, ash and zsh (multiple files uploaded as zip archive)\n##### Add this to .bashrc or .zshrc or its equivalent\n```bash\ntransfer() (if [ $# -eq 0 ]; then printf \"No arguments specified.\\nUsage:\\n transfer <file|directory>\\n ... | transfer <file_name>\\n\">&2; return 1; fi; file_name=$(basename \"$1\"); if [ -t 0 ]; then file=\"$1\"; if [ ! -e \"$file\" ]; then echo \"$file: No such file or directory\">&2; return 1; fi; if [ -d \"$file\" ]; then cd \"$file\" || return 1; file_name=\"$file_name.zip\"; set -- zip -r -q - .; else set -- cat \"$file\"; fi; else set -- cat; fi; url=$(\"$@\" | curl --silent --show-error --progress-bar --upload-file \"-\" \"https://transfer.sh/$file_name\"); echo \"$url\"; )\n```\n\n<br />\n\n#### Now you can use transfer function\n```\n$ transfer hello.txt\n```\n\n<br />\n\n### Bash and zsh (with delete url, delete token output and prompt before uploading)\n##### Add this to .bashrc or .zshrc or its equivalent\n\n<details><summary>Expand</summary><p>\n\n```bash\ntransfer()\n{\n    local file\n    declare -a file_array\n    file_array=(\"${@}\")\n\n    if [[ \"${file_array[@]}\" == \"\" || \"${1}\" == \"--help\" || \"${1}\" == \"-h\" ]]\n    then\n        echo \"${0} - Upload arbitrary files to \\\"transfer.sh\\\".\"\n        echo \"\"\n        echo \"Usage: ${0} [options] [<file>]...\"\n        echo \"\"\n        echo \"OPTIONS:\"\n        echo \"  -h, --help\"\n        echo \"      show this message\"\n        echo \"\"\n        echo \"EXAMPLES:\"\n        echo \"  Upload a single file from the current working directory:\"\n        echo \"      ${0} \\\"image.img\\\"\"\n        echo \"\"\n        echo \"  Upload multiple files from the current working directory:\"\n        echo \"      ${0} \\\"image.img\\\" \\\"image2.img\\\"\"\n        echo \"\"\n        echo \"  Upload a file from a different directory:\"\n        echo \"      ${0} \\\"/tmp/some_file\\\"\"\n        echo \"\"\n        echo \"  Upload all files from the current working directory. Be aware of the webserver's rate limiting!:\"\n        echo \"      ${0} *\"\n        echo \"\"\n        echo \"  Upload a single file from the current working directory and filter out the delete token and download link:\"\n        echo \"      ${0} \\\"image.img\\\" | awk --field-separator=\\\": \\\" '/Delete token:/ { print \\$2 } /Download link:/ { print \\$2 }'\"\n        echo \"\"\n        echo \"  Show help text from \\\"transfer.sh\\\":\"\n        echo \"      curl --request GET \\\"https://transfer.sh\\\"\"\n        return 0\n    else\n        for file in \"${file_array[@]}\"\n        do\n            if [[ ! -f \"${file}\" ]]\n            then\n                echo -e \"\\e[01;31m'${file}' could not be found or is not a file.\\e[0m\" >&2\n                return 1\n            fi\n        done\n        unset file\n    fi\n\n    local upload_files\n    local curl_output\n    local awk_output\n\n    du -c -k -L \"${file_array[@]}\" >&2\n    # be compatible with \"bash\"\n    if [[ \"${ZSH_NAME}\" == \"zsh\" ]]\n    then\n        read $'upload_files?\\e[01;31mDo you really want to upload the above files ('\"${#file_array[@]}\"$') to \"transfer.sh\"? (Y/n): \\e[0m'\n    elif [[ \"${BASH}\" == *\"bash\"* ]]\n    then\n        read -p $'\\e[01;31mDo you really want to upload the above files ('\"${#file_array[@]}\"$') to \"transfer.sh\"? (Y/n): \\e[0m' upload_files\n    fi\n\n    case \"${upload_files:-y}\" in\n        \"y\"|\"Y\")\n            # for the sake of the progress bar, execute \"curl\" for each file.\n            # the parameters \"--include\" and \"--form\" will suppress the progress bar.\n            for file in \"${file_array[@]}\"\n            do\n                # show delete link and filter out the delete token from the response header after upload.\n                # it is important to save \"curl's\" \"stdout\" via a subshell to a variable or redirect it to another command,\n                # which just redirects to \"stdout\" in order to have a sane output afterwards.\n                # the progress bar is redirected to \"stderr\" and is only displayed,\n                # if \"stdout\" is redirected to something; e.g. \">/dev/null\", \"tee /dev/null\" or \"| <some_command>\".\n                # the response header is redirected to \"stdout\", so redirecting \"stdout\" to \"/dev/null\" does not make any sense.\n                # redirecting \"curl's\" \"stderr\" to \"stdout\" (\"2>&1\") will suppress the progress bar.\n                curl_output=$(curl --request PUT --progress-bar --dump-header - --upload-file \"${file}\" \"https://transfer.sh/\")\n                awk_output=$(awk \\\n                    'gsub(\"\\r\", \"\", $0) && tolower($1) ~ /x-url-delete/ \\\n                    {\n                        delete_link=$2;\n                        print \"Delete command: curl --request DELETE \" \"\\\"\"delete_link\"\\\"\";\n\n                        gsub(\".*/\", \"\", delete_link);\n                        delete_token=delete_link;\n                        print \"Delete token: \" delete_token;\n                    }\n\n                    END{\n                        print \"Download link: \" $0;\n                    }' <<< \"${curl_output}\")\n\n                # return the results via \"stdout\", \"awk\" does not do this for some reason.\n                echo -e \"${awk_output}\\n\"\n\n                # avoid rate limiting as much as possible; nginx: too many requests.\n                if (( ${#file_array[@]} > 4 ))\n                then\n                    sleep 5\n                fi\n            done\n            ;;\n\n        \"n\"|\"N\")\n            return 1\n            ;;\n\n        *)\n            echo -e \"\\e[01;31mWrong input: '${upload_files}'.\\e[0m\" >&2\n            return 1\n    esac\n}\n```\n\n</p></details>\n\n#### Sample output\n```bash\n$ ls -lh\ntotal 20M\n-rw-r--r-- 1 <some_username> <some_username> 10M Apr  4 21:08 image.img\n-rw-r--r-- 1 <some_username> <some_username> 10M Apr  4 21:08 image2.img\n$ transfer image*\n10240K  image2.img\n10240K  image.img\n20480K  total\nDo you really want to upload the above files (2) to \"transfer.sh\"? (Y/n):\n######################################################################################################################################################################################################################################## 100.0%\nDelete command: curl --request DELETE \"https://transfer.sh/wJw9pz/image2.img/mSctGx7pYCId\"\nDelete token: mSctGx7pYCId\nDownload link: https://transfer.sh/wJw9pz/image2.img\n\n######################################################################################################################################################################################################################################## 100.0%\nDelete command: curl --request DELETE \"https://transfer.sh/ljJc5I/image.img/nw7qaoiKUwCU\"\nDelete token: nw7qaoiKUwCU\nDownload link: https://transfer.sh/ljJc5I/image.img\n\n$ transfer \"image.img\" | awk --field-separator=\": \" '/Delete token:/ { print $2 } /Download link:/ { print $2 }'\n10240K  image.img\n10240K  total\nDo you really want to upload the above files (1) to \"transfer.sh\"? (Y/n):\n######################################################################################################################################################################################################################################## 100.0%\ntauN5dE3fWJe\nhttps://transfer.sh/MYkuqn/image.img\n```\n\n<br />\n\n---\n\n<br />\n\n## Contributions\n\nContributions are welcome.\n\n<br />\n\n---\n\n<br />\n\n## Creators\n\n**Remco Verhoef**\n- <https://twitter.com/remco_verhoef>\n- <https://twitter.com/dutchcoders>\n\n**Uvis Grinfelds**\n\n<br />\n\n---\n\n<br />\n\n## Maintainers\n\n- **Andrea Spacca**\n- **Stefan Benten**\n\n<br />\n\n---\n\n<br />\n\n## Copyright and License\n\nCode and documentation copyright 2011-2018 Remco Verhoef.\nCode and documentation copyright 2018-2020 Andrea Spacca.\nCode and documentation copyright 2020- Andrea Spacca and Stefan Benten.\n\nCode released under [the MIT license](LICENSE).\n"
  },
  {
    "path": "Vagrantfile",
    "content": "# -*- mode: ruby -*-\n# vi: set ft=ruby :\n\n# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!\nVAGRANTFILE_API_VERSION = \"2\"\n\nVagrant.configure(VAGRANTFILE_API_VERSION) do |config|\n    # Every Vagrant virtual environment requires a box to build off of.\n    config.vm.box = \"puphpet/ubuntu1404-x64\"\n    config.vm.provider \"vmware_fusion\" do |v|\n        v.gui = true\n    end\nend\n"
  },
  {
    "path": "cmd/cmd.go",
    "content": "package cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/dutchcoders/transfer.sh/server/storage\"\n\n\t\"github.com/dutchcoders/transfer.sh/server\"\n\t\"github.com/fatih/color\"\n\t\"github.com/urfave/cli/v2\"\n\t\"google.golang.org/api/googleapi\"\n)\n\n// Version is inject at build time\nvar Version = \"0.0.0\"\nvar helpTemplate = `NAME:\n{{.Name}} - {{.Usage}}\n\nDESCRIPTION:\n{{.Description}}\n\nUSAGE:\n{{.Name}} {{if .Flags}}[flags] {{end}}command{{if .Flags}}{{end}} [arguments...]\n\nCOMMANDS:\n{{range .Commands}}{{join .Names \", \"}}{{ \"\\t\" }}{{.Usage}}\n{{end}}{{if .Flags}}\nFLAGS:\n{{range .Flags}}{{.}}\n{{end}}{{end}}\nVERSION:\n` + Version +\n\t`{{ \"\\n\"}}`\n\nvar globalFlags = []cli.Flag{\n\t&cli.StringFlag{\n\t\tName:    \"listener\",\n\t\tUsage:   \"127.0.0.1:8080\",\n\t\tValue:   \"127.0.0.1:8080\",\n\t\tEnvVars: []string{\"LISTENER\"},\n\t},\n\t// redirect to https?\n\t// hostnames\n\t&cli.StringFlag{\n\t\tName:    \"profile-listener\",\n\t\tUsage:   \"127.0.0.1:6060\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"PROFILE_LISTENER\"},\n\t},\n\t&cli.BoolFlag{\n\t\tName:    \"force-https\",\n\t\tUsage:   \"\",\n\t\tEnvVars: []string{\"FORCE_HTTPS\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"tls-listener\",\n\t\tUsage:   \"127.0.0.1:8443\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"TLS_LISTENER\"},\n\t},\n\t&cli.BoolFlag{\n\t\tName:    \"tls-listener-only\",\n\t\tUsage:   \"\",\n\t\tEnvVars: []string{\"TLS_LISTENER_ONLY\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"tls-cert-file\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"TLS_CERT_FILE\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"tls-private-key\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"TLS_PRIVATE_KEY\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"temp-path\",\n\t\tUsage:   \"path to temp files\",\n\t\tValue:   os.TempDir(),\n\t\tEnvVars: []string{\"TEMP_PATH\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"web-path\",\n\t\tUsage:   \"path to static web files\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"WEB_PATH\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"proxy-path\",\n\t\tUsage:   \"path prefix when service is run behind a proxy\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"PROXY_PATH\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"proxy-port\",\n\t\tUsage:   \"port of the proxy when the service is run behind a proxy\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"PROXY_PORT\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"email-contact\",\n\t\tUsage:   \"email address to link in Contact Us (front end)\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"EMAIL_CONTACT\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"ga-key\",\n\t\tUsage:   \"key for google analytics (front end)\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"GA_KEY\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"uservoice-key\",\n\t\tUsage:   \"key for user voice (front end)\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"USERVOICE_KEY\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"provider\",\n\t\tUsage:   \"s3|gdrive|local\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"PROVIDER\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"s3-endpoint\",\n\t\tUsage:   \"\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"S3_ENDPOINT\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"s3-region\",\n\t\tUsage:   \"\",\n\t\tValue:   \"eu-west-1\",\n\t\tEnvVars: []string{\"S3_REGION\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"aws-access-key\",\n\t\tUsage:   \"\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"AWS_ACCESS_KEY\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"aws-secret-key\",\n\t\tUsage:   \"\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"AWS_SECRET_KEY\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"bucket\",\n\t\tUsage:   \"\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"BUCKET\"},\n\t},\n\t&cli.BoolFlag{\n\t\tName:    \"s3-no-multipart\",\n\t\tUsage:   \"Disables S3 Multipart Puts\",\n\t\tEnvVars: []string{\"S3_NO_MULTIPART\"},\n\t},\n\t&cli.BoolFlag{\n\t\tName:    \"s3-path-style\",\n\t\tUsage:   \"Forces path style URLs, required for Minio.\",\n\t\tEnvVars: []string{\"S3_PATH_STYLE\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"gdrive-client-json-filepath\",\n\t\tUsage:   \"\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"GDRIVE_CLIENT_JSON_FILEPATH\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"gdrive-local-config-path\",\n\t\tUsage:   \"\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"GDRIVE_LOCAL_CONFIG_PATH\"},\n\t},\n\t&cli.IntFlag{\n\t\tName:    \"gdrive-chunk-size\",\n\t\tUsage:   \"\",\n\t\tValue:   googleapi.DefaultUploadChunkSize / 1024 / 1024,\n\t\tEnvVars: []string{\"GDRIVE_CHUNK_SIZE\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"storj-access\",\n\t\tUsage:   \"Access for the project\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"STORJ_ACCESS\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"storj-bucket\",\n\t\tUsage:   \"Bucket to use within the project\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"STORJ_BUCKET\"},\n\t},\n\t&cli.IntFlag{\n\t\tName:    \"rate-limit\",\n\t\tUsage:   \"requests per minute\",\n\t\tValue:   0,\n\t\tEnvVars: []string{\"RATE_LIMIT\"},\n\t},\n\t&cli.IntFlag{\n\t\tName:    \"purge-days\",\n\t\tUsage:   \"number of days after uploads are purged automatically\",\n\t\tValue:   0,\n\t\tEnvVars: []string{\"PURGE_DAYS\"},\n\t},\n\t&cli.IntFlag{\n\t\tName:    \"purge-interval\",\n\t\tUsage:   \"interval in hours to run the automatic purge for\",\n\t\tValue:   0,\n\t\tEnvVars: []string{\"PURGE_INTERVAL\"},\n\t},\n\t&cli.Int64Flag{\n\t\tName:    \"max-upload-size\",\n\t\tUsage:   \"max limit for upload, in kilobytes\",\n\t\tValue:   0,\n\t\tEnvVars: []string{\"MAX_UPLOAD_SIZE\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"lets-encrypt-hosts\",\n\t\tUsage:   \"host1, host2\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"HOSTS\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"log\",\n\t\tUsage:   \"/var/log/transfersh.log\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"LOG\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"basedir\",\n\t\tUsage:   \"path to storage\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"BASEDIR\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"clamav-host\",\n\t\tUsage:   \"clamav-host\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"CLAMAV_HOST\"},\n\t},\n\t&cli.BoolFlag{\n\t\tName:    \"perform-clamav-prescan\",\n\t\tUsage:   \"perform-clamav-prescan\",\n\t\tEnvVars: []string{\"PERFORM_CLAMAV_PRESCAN\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"virustotal-key\",\n\t\tUsage:   \"virustotal-key\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"VIRUSTOTAL_KEY\"},\n\t},\n\t&cli.BoolFlag{\n\t\tName:    \"profiler\",\n\t\tUsage:   \"enable profiling\",\n\t\tEnvVars: []string{\"PROFILER\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"http-auth-user\",\n\t\tUsage:   \"user for http basic auth\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"HTTP_AUTH_USER\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"http-auth-pass\",\n\t\tUsage:   \"pass for http basic auth\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"HTTP_AUTH_PASS\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"http-auth-htpasswd\",\n\t\tUsage:   \"htpasswd file http basic auth\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"HTTP_AUTH_HTPASSWD\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"http-auth-ip-whitelist\",\n\t\tUsage:   \"comma separated list of ips allowed to upload without being challenged an http auth\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"HTTP_AUTH_IP_WHITELIST\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"ip-whitelist\",\n\t\tUsage:   \"comma separated list of ips allowed to connect to the service\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"IP_WHITELIST\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"ip-blacklist\",\n\t\tUsage:   \"comma separated list of ips not allowed to connect to the service\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"IP_BLACKLIST\"},\n\t},\n\t&cli.StringFlag{\n\t\tName:    \"cors-domains\",\n\t\tUsage:   \"comma separated list of domains allowed for CORS requests\",\n\t\tValue:   \"\",\n\t\tEnvVars: []string{\"CORS_DOMAINS\"},\n\t},\n\t&cli.IntFlag{\n\t\tName:    \"random-token-length\",\n\t\tUsage:   \"\",\n\t\tValue:   10,\n\t\tEnvVars: []string{\"RANDOM_TOKEN_LENGTH\"},\n\t},\n}\n\n// Cmd wraps cli.app\ntype Cmd struct {\n\t*cli.App\n}\n\nfunc versionCommand(_ *cli.Context) error {\n\tfmt.Println(color.YellowString(\"transfer.sh %s: Easy file sharing from the command line\", Version))\n\treturn nil\n}\n\n// New is the factory for transfer.sh\nfunc New() *Cmd {\n\tlogger := log.New(os.Stdout, \"[transfer.sh]\", log.LstdFlags)\n\n\tapp := cli.NewApp()\n\tapp.Name = \"transfer.sh\"\n\tapp.Authors = []*cli.Author{}\n\tapp.Usage = \"transfer.sh\"\n\tapp.Description = `Easy file sharing from the command line`\n\tapp.Version = Version\n\tapp.Flags = globalFlags\n\tapp.CustomAppHelpTemplate = helpTemplate\n\tapp.Commands = []*cli.Command{\n\t\t{\n\t\t\tName:   \"version\",\n\t\t\tAction: versionCommand,\n\t\t},\n\t}\n\n\tapp.Before = func(c *cli.Context) error {\n\t\treturn nil\n\t}\n\n\tapp.Action = func(c *cli.Context) error {\n\t\tvar options []server.OptionFn\n\t\tif v := c.String(\"listener\"); v != \"\" {\n\t\t\toptions = append(options, server.Listener(v))\n\t\t}\n\n\t\tif v := c.String(\"cors-domains\"); v != \"\" {\n\t\t\toptions = append(options, server.CorsDomains(v))\n\t\t}\n\n\t\tif v := c.String(\"tls-listener\"); v == \"\" {\n\t\t} else if c.Bool(\"tls-listener-only\") {\n\t\t\toptions = append(options, server.TLSListener(v, true))\n\t\t} else {\n\t\t\toptions = append(options, server.TLSListener(v, false))\n\t\t}\n\n\t\tif v := c.String(\"profile-listener\"); v != \"\" {\n\t\t\toptions = append(options, server.ProfileListener(v))\n\t\t}\n\n\t\tif v := c.String(\"web-path\"); v != \"\" {\n\t\t\toptions = append(options, server.WebPath(v))\n\t\t}\n\n\t\tif v := c.String(\"proxy-path\"); v != \"\" {\n\t\t\toptions = append(options, server.ProxyPath(v))\n\t\t}\n\n\t\tif v := c.String(\"proxy-port\"); v != \"\" {\n\t\t\toptions = append(options, server.ProxyPort(v))\n\t\t}\n\n\t\tif v := c.String(\"email-contact\"); v != \"\" {\n\t\t\toptions = append(options, server.EmailContact(v))\n\t\t}\n\n\t\tif v := c.String(\"ga-key\"); v != \"\" {\n\t\t\toptions = append(options, server.GoogleAnalytics(v))\n\t\t}\n\n\t\tif v := c.String(\"uservoice-key\"); v != \"\" {\n\t\t\toptions = append(options, server.UserVoice(v))\n\t\t}\n\n\t\tif v := c.String(\"temp-path\"); v != \"\" {\n\t\t\toptions = append(options, server.TempPath(v))\n\t\t}\n\n\t\tif v := c.String(\"log\"); v != \"\" {\n\t\t\toptions = append(options, server.LogFile(logger, v))\n\t\t} else {\n\t\t\toptions = append(options, server.Logger(logger))\n\t\t}\n\n\t\tif v := c.String(\"lets-encrypt-hosts\"); v != \"\" {\n\t\t\toptions = append(options, server.UseLetsEncrypt(strings.Split(v, \",\")))\n\t\t}\n\n\t\tif v := c.String(\"virustotal-key\"); v != \"\" {\n\t\t\toptions = append(options, server.VirustotalKey(v))\n\t\t}\n\n\t\tif v := c.String(\"clamav-host\"); v != \"\" {\n\t\t\toptions = append(options, server.ClamavHost(v))\n\t\t}\n\n\t\tif v := c.Bool(\"perform-clamav-prescan\"); v {\n\t\t\tif c.String(\"clamav-host\") == \"\" {\n\t\t\t\treturn errors.New(\"clamav-host not set\")\n\t\t\t}\n\n\t\t\toptions = append(options, server.PerformClamavPrescan(v))\n\t\t}\n\n\t\tif v := c.Int64(\"max-upload-size\"); v > 0 {\n\t\t\toptions = append(options, server.MaxUploadSize(v))\n\t\t}\n\n\t\tif v := c.Int(\"rate-limit\"); v > 0 {\n\t\t\toptions = append(options, server.RateLimit(v))\n\t\t}\n\n\t\tv := c.Int(\"random-token-length\")\n\t\toptions = append(options, server.RandomTokenLength(v))\n\n\t\tpurgeDays := c.Int(\"purge-days\")\n\t\tpurgeInterval := c.Int(\"purge-interval\")\n\t\tif purgeDays > 0 && purgeInterval > 0 {\n\t\t\toptions = append(options, server.Purge(purgeDays, purgeInterval))\n\t\t}\n\n\t\tif cert := c.String(\"tls-cert-file\"); cert == \"\" {\n\t\t} else if pk := c.String(\"tls-private-key\"); pk == \"\" {\n\t\t} else {\n\t\t\toptions = append(options, server.TLSConfig(cert, pk))\n\t\t}\n\n\t\tif c.Bool(\"profiler\") {\n\t\t\toptions = append(options, server.EnableProfiler())\n\t\t}\n\n\t\tif c.Bool(\"force-https\") {\n\t\t\toptions = append(options, server.ForceHTTPS())\n\t\t}\n\n\t\tif httpAuthUser := c.String(\"http-auth-user\"); httpAuthUser == \"\" {\n\t\t} else if httpAuthPass := c.String(\"http-auth-pass\"); httpAuthPass == \"\" {\n\t\t} else {\n\t\t\toptions = append(options, server.HTTPAuthCredentials(httpAuthUser, httpAuthPass))\n\t\t}\n\n\t\tif httpAuthHtpasswd := c.String(\"http-auth-htpasswd\"); httpAuthHtpasswd != \"\" {\n\t\t\toptions = append(options, server.HTTPAuthHtpasswd(httpAuthHtpasswd))\n\t\t}\n\n\t\tif httpAuthIPWhitelist := c.String(\"http-auth-ip-whitelist\"); httpAuthIPWhitelist != \"\" {\n\t\t\tipFilterOptions := server.IPFilterOptions{}\n\t\t\tipFilterOptions.AllowedIPs = strings.Split(httpAuthIPWhitelist, \",\")\n\t\t\tipFilterOptions.BlockByDefault = true\n\t\t\toptions = append(options, server.HTTPAUTHFilterOptions(ipFilterOptions))\n\t\t}\n\n\t\tapplyIPFilter := false\n\t\tipFilterOptions := server.IPFilterOptions{}\n\t\tif ipWhitelist := c.String(\"ip-whitelist\"); ipWhitelist != \"\" {\n\t\t\tapplyIPFilter = true\n\t\t\tipFilterOptions.AllowedIPs = strings.Split(ipWhitelist, \",\")\n\t\t\tipFilterOptions.BlockByDefault = true\n\t\t}\n\n\t\tif ipBlacklist := c.String(\"ip-blacklist\"); ipBlacklist != \"\" {\n\t\t\tapplyIPFilter = true\n\t\t\tipFilterOptions.BlockedIPs = strings.Split(ipBlacklist, \",\")\n\t\t}\n\n\t\tif applyIPFilter {\n\t\t\toptions = append(options, server.FilterOptions(ipFilterOptions))\n\t\t}\n\n\t\tswitch provider := c.String(\"provider\"); provider {\n\t\tcase \"s3\":\n\t\t\tif accessKey := c.String(\"aws-access-key\"); accessKey == \"\" {\n\t\t\t\treturn errors.New(\"access-key not set.\")\n\t\t\t} else if secretKey := c.String(\"aws-secret-key\"); secretKey == \"\" {\n\t\t\t\treturn errors.New(\"secret-key not set.\")\n\t\t\t} else if bucket := c.String(\"bucket\"); bucket == \"\" {\n\t\t\t\treturn errors.New(\"bucket not set.\")\n\t\t\t} 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 {\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\toptions = append(options, server.UseStorage(store))\n\t\t\t}\n\t\tcase \"gdrive\":\n\t\t\tchunkSize := c.Int(\"gdrive-chunk-size\") * 1024 * 1024\n\n\t\t\tif clientJSONFilepath := c.String(\"gdrive-client-json-filepath\"); clientJSONFilepath == \"\" {\n\t\t\t\treturn errors.New(\"gdrive-client-json-filepath not set.\")\n\t\t\t} else if localConfigPath := c.String(\"gdrive-local-config-path\"); localConfigPath == \"\" {\n\t\t\t\treturn errors.New(\"gdrive-local-config-path not set.\")\n\t\t\t} else if basedir := c.String(\"basedir\"); basedir == \"\" {\n\t\t\t\treturn errors.New(\"basedir not set.\")\n\t\t\t} else if store, err := storage.NewGDriveStorage(c.Context, clientJSONFilepath, localConfigPath, basedir, chunkSize, logger); err != nil {\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\toptions = append(options, server.UseStorage(store))\n\t\t\t}\n\t\tcase \"storj\":\n\t\t\tif access := c.String(\"storj-access\"); access == \"\" {\n\t\t\t\treturn errors.New(\"storj-access not set.\")\n\t\t\t} else if bucket := c.String(\"storj-bucket\"); bucket == \"\" {\n\t\t\t\treturn errors.New(\"storj-bucket not set.\")\n\t\t\t} else if store, err := storage.NewStorjStorage(c.Context, access, bucket, purgeDays, logger); err != nil {\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\toptions = append(options, server.UseStorage(store))\n\t\t\t}\n\t\tcase \"local\":\n\t\t\tif v := c.String(\"basedir\"); v == \"\" {\n\t\t\t\treturn errors.New(\"basedir not set.\")\n\t\t\t} else if store, err := storage.NewLocalStorage(v, logger); err != nil {\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\toptions = append(options, server.UseStorage(store))\n\t\t\t}\n\t\tdefault:\n\t\t\treturn errors.New(\"Provider not set or invalid.\")\n\t\t}\n\n\t\tsrvr, err := server.New(\n\t\t\toptions...,\n\t\t)\n\n\t\tif err != nil {\n\t\t\tlogger.Println(color.RedString(\"Error starting server: %s\", err.Error()))\n\t\t\treturn err\n\t\t}\n\n\t\tsrvr.Run()\n\t\treturn nil\n\t}\n\n\treturn &Cmd{\n\t\tApp: app,\n\t}\n}\n"
  },
  {
    "path": "examples.md",
    "content": "# Table of Contents\n\n* [Aliases](#aliases)\n* [Uploading and downloading](#uploading-and-downloading)\n* [Archiving and backups](#archiving-and-backups)\n* [Encrypting and decrypting](#encrypting-and-decrypting)\n* [Scanning for viruses](#scanning-for-viruses)\n* [Uploading and copy download command](#uploading-and-copy-download-command)\n* [Uploading and displaying URL and deletion token](#uploading-and-displaying-url-and-deletion-token)\n\n## Aliases\n<a name=\"aliases\"/>\n\n## Add alias to .bashrc or .zshrc\n\n### Using curl\n```bash\ntransfer() {\n    curl --progress-bar --upload-file \"$1\" https://transfer.sh/$(basename \"$1\") | tee /dev/null;\n    echo\n}\n\nalias transfer=transfer\n```\n\n### Using wget\n```bash\ntransfer() {\n    wget -t 1 -qO - --method=PUT --body-file=\"$1\" --header=\"Content-Type: $(file -b --mime-type \"$1\")\" https://transfer.sh/$(basename \"$1\");\n    echo\n}\n\nalias transfer=transfer\n```\n\n## Add alias for fish-shell\n\n### Using curl\n```fish\nfunction transfer --description 'Upload a file to transfer.sh'\n    if [ $argv[1] ]\n        # write to output to tmpfile because of progress bar\n        set -l tmpfile ( mktemp -t transferXXXXXX )\n        curl --progress-bar --upload-file \"$argv[1]\" https://transfer.sh/(basename $argv[1]) >> $tmpfile\n        cat $tmpfile\n        command rm -f $tmpfile\n    else\n        echo 'usage: transfer FILE_TO_TRANSFER'\n    end\nend\n\nfuncsave transfer\n```\n\n### Using wget\n```fish\nfunction transfer --description 'Upload a file to transfer.sh'\n    if [ $argv[1] ]\n        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])\n    else\n        echo 'usage: transfer FILE_TO_TRANSFER'\n    end\nend\n\nfuncsave transfer\n```\n\nNow run it like this:\n```bash\n$ transfer test.txt\n```\n\n## Add alias on Windows\n\nPut a file called `transfer.cmd` somewhere in your PATH with this inside it:\n```cmd\n@echo off\nsetlocal\n:: use env vars to pass names to PS, to avoid escaping issues\nset FN=%~nx1\nset FULL=%1\npowershell -noprofile -command \"$(Invoke-Webrequest -Method put -Infile $Env:FULL https://transfer.sh/$Env:FN).Content\"\n```\n\n## Uploading and Downloading\n<a name=\"uploading-and-downloading\"/>\n\n### Uploading with wget\n```bash\n$ wget --method PUT --body-file=/tmp/file.tar https://transfer.sh/file.tar -O - -nv \n```\n\n### Uploading with PowerShell\n```posh\nPS H:\\> invoke-webrequest -method put -infile .\\file.txt https://transfer.sh/file.txt \n```\n\n### Upload using HTTPie\n```bash\n$ http https://transfer.sh/ -vv < /tmp/test.log \n```\n\n### Uploading a filtered text file\n```bash\n$ grep 'pound' /var/log/syslog | curl --upload-file - https://transfer.sh/pound.log \n```\n\n### Downloading with curl\n```bash\n$ curl https://transfer.sh/1lDau/test.txt -o test.txt\n```\n\n### Downloading with wget\n```bash\n$ wget https://transfer.sh/1lDau/test.txt\n```\n\n## Archiving and backups\n<a name=\"archiving-and-backups\"/>\n\n### Backup, encrypt and transfer a MySQL dump\n```bash\n$ mysqldump --all-databases | gzip | gpg -ac -o- | curl -X PUT --upload-file \"-\" https://transfer.sh/test.txt\n```\n\n### Archive and upload directory\n```bash\n$ tar -czf - /var/log/journal | curl --upload-file - https://transfer.sh/journal.tar.gz\n```\n\n### Uploading multiple files at once\n```bash\n$ curl -i -F filedata=@/tmp/hello.txt -F filedata=@/tmp/hello2.txt https://transfer.sh/\n```\n\n### Combining downloads as zip or tar.gz archive\n```bash\n$ curl https://transfer.sh/(15HKz/hello.txt,15HKz/hello.txt).tar.gz\n$ curl https://transfer.sh/(15HKz/hello.txt,15HKz/hello.txt).zip \n```\n\n### Transfer and send email with link (using an alias)\n```bash\n$ transfer /tmp/hello.txt | mail -s \"Hello World\" user@yourmaildomain.com \n```\n## Encrypting and decrypting\n<a name=\"encrypting-and-decrypting\"/>\n\n### Encrypting files with password using gpg\n```bash\n$ gpg --armor --symmetric --output - /tmp/hello.txt | curl --upload-file - https://transfer.sh/test.txt\n```\n\n### Downloading and decrypting\n```bash\n$ curl https://transfer.sh/1lDau/test.txt | gpg --decrypt --output /tmp/hello.txt\n```\n\n### Import keys from [keybase](https://keybase.io/)\n```bash\n$ keybase track [them] # Encrypt for recipient(s)\n$ cat somebackupfile.tar.gz | keybase encrypt [them] | curl --upload-file '-' https://transfer.sh/test.txt # Decrypt\n$ curl https://transfer.sh/sqUFi/test.md | keybase decrypt\n```\n\n## Scanning for viruses\n<a name=\"scanning-for-viruses\"/>\n\n### Scan for malware or viruses using Clamav\n```bash\n$ wget http://www.eicar.org/download/eicar.com\n$ curl -X PUT --upload-file ./eicar.com https://transfer.sh/eicar.com/scan\n```\n\n### Upload malware to VirusTotal, get a permalink in return\n```bash\n$ curl -X PUT --upload-file nhgbhhj https://transfer.sh/test.txt/virustotal \n```\n\n### Upload encrypted password protected files\n\nBy default files upload for only 1 download, you can specify download limit using -D flag like `transfer-encrypted -D 50 %file/folder%`\n\n#### One line for bashrc\n```bash\ntransfer-encrypted() { if [ $# -eq 0 ]; then echo \"No arguments specified.\\nUsage:\\n transfer <file|directory>\\n ... | transfer <file_name>\" >&2; return 1; fi; while getopts \":D:\" opt; do case $opt in D) max_downloads=$OPTARG;; \\?) echo \"Invalid option: -$OPTARG\" >&2;; esac; done; shift \"$((OPTIND - 1))\"; file=\"$1\"; file_name=$(basename \"$file\"); if [ ! -e \"$file\" ]; then echo \"$file: No such file or directory\" >&2; return 1; fi; if [ -d \"$file\" ]; then file_name=\"$file_name.zip\"; (cd \"$file\" && zip -r -q - .) | openssl aes-256-cbc -pbkdf2 -e > \"tmp-$file_name\" && cat \"tmp-$file_name\" | curl -H \"Max-Downloads: $max_downloads\" -w '\\n' --upload-file \"tmp-$file_name\" \"https://transfer.sh/$file_name\" | tee /dev/null; rm \"tmp-$file_name\"; else cat \"$file\" | openssl aes-256-cbc -pbkdf2 -e > \"tmp-$file\" && cat \"tmp-$file\" | curl -H \"Max-Downloads: $max_downloads\" -w '\\n' --upload-file - \"https://transfer.sh/$file_name\" | tee /dev/null; rm \"tmp-$file\"; fi; }\n```\n#### Human readable code \n```bash\ntransfer-encrypted() {\n    if [ $# -eq 0 ]; then\n        echo \"No arguments specified.\\nUsage:\\n transfer <file|directory>\\n ... | transfer <file_name>\" >&2\n        return 1\n    fi\n\n    while getopts \":D:\" opt; do\n        case $opt in\n            D)\n                max_downloads=$OPTARG\n                ;;\n            \\?)\n                echo \"Invalid option: -$OPTARG\" >&2\n                ;;\n        esac\n    done\n\n    shift \"$((OPTIND - 1))\"\n    file=\"$1\"\n    file_name=$(basename \"$file\")\n\n    if [ ! -e \"$file\" ]; then\n        echo \"$file: No such file or directory\" >&2\n        return 1\n    fi\n\n    if [ -d \"$file\" ]; then\n        file_name=\"$file_name.zip\"\n        (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\n        rm \"tmp-$file_name\"\n    else\n        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\n        rm \"tmp-$file\"\n    fi\n}\n```\n#### Decrypt using\n```bash\ncurl -s https://transfer.sh/some/file | openssl aes-256-cbc -pbkdf2 -d > output_filename\n```\n\n## Uploading and copy download command\n\nDownload commands can be automatically copied to the clipboard after files are uploaded using transfer.sh.\n\nIt was designed for Linux or macOS.\n\n### 1. Install xclip or xsel for Linux, macOS skips this step\n\n- install xclip see https://command-not-found.com/xclip\n\n- install xsel  see https://command-not-found.com/xsel\n\nInstall later, add pbcopy and pbpaste to .bashrc or .zshrc or its equivalent.\n\n- If use xclip, paste the following lines:\n\n```sh\nalias pbcopy='xclip -selection clipboard'\nalias pbpaste='xclip -selection clipboard -o'\n```\n\n- If use xsel, paste the following lines:\n\n```sh\nalias pbcopy='xsel --clipboard --input'\nalias pbpaste='xsel --clipboard --output'\n```\n\n### 2. Add Uploading and copy download command shell function\n\n1. Open .bashrc or .zshrc  or its equivalent.\n\n2. Add the following shell script:\n\n   ```sh\n   transfer() {\n     curl --progress-bar --upload-file \"$1\" https://transfer.sh/$(basename \"$1\") | pbcopy;\n     echo \"1) Download link:\"\n     echo \"$(pbpaste)\"\n   \n     echo \"\\n2) Linux or macOS download command:\"\n     linux_macos_download_command=\"wget $(pbpaste)\"\n     echo $linux_macos_download_command\n   \n     echo \"\\n3) Windows download command:\"\n     windows_download_command=\"Invoke-WebRequest -Uri \"$(pbpaste)\" -OutFile $(basename $1)\"\n     echo $windows_download_command\n   \n     case $2 in\n       l|m)  echo $linux_macos_download_command | pbcopy\n       ;;\n       w)  echo $windows_download_command | pbcopy\n       ;;\n     esac\n   }\n   ```\n\n\n### 3. Test\n\nThe transfer command has two parameters:\n\n1. The first parameter is the path to upload the file.\n\n2. The second parameter indicates which system's download command is copied. optional:\n\n   - This parameter is empty to copy the download link.\n\n   - `l` or `m` copy the Linux or macOS command that downloaded the file.\n\n   -  `w` copy the Windows command that downloaded the file.\n\nFor example, The command to download the file on Windows will be copied:\n\n```sh\n$ transfer ~/temp/a.log w\n######################################################################## 100.0%\n1) Download link:\nhttps://transfer.sh/y0qr2c/a.log\n\n2) Linux or macOS download command:\nwget https://transfer.sh/y0qr2c/a.log\n\n3) Windows download command:\nInvoke-WebRequest -Uri https://transfer.sh/y0qr2c/a.log -OutFile a.log\n```\n## Uploading and displaying URL and deletion token\n```bash\n# tempfile\nURLFILE=$HOME/temp/transfersh.url\n# insert number of downloads and days saved\nif [ -f $1 ]; then\nread -p \"Allowed number of downloads: \" num_down\nread -p \"Number of days on server: \" num_save\n# transfer\ncurl -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\n# display URL and deletion token\nif [ -f $URLFILE ]; then\nURL=$(tail -n1 $URLFILE)\nTOKEN=$(grep delete $URLFILE | awk -F \"/\" '{print $NF}')\necho \"*********************************\"\necho \"Data is saved in $URLFILE\"\necho \"**********************************\"\necho \"URL is: $URL\"\necho \"Deletion Token is: $TOKEN\"\necho \"**********************************\"\nelse\necho \"NO URL-File found !!\"\nfi\nelse\necho \"!!!!!!\"\necho \"\\\"$1\\\" not found !!\"\necho \"!!!!!!\"\nfi\n```\n"
  },
  {
    "path": "extras/clamd",
    "content": "#! /bin/sh\n### BEGIN INIT INFO\n# Provides:          skeleton\n# Required-Start:    $remote_fs $syslog\n# Required-Stop:     $remote_fs $syslog\n# Default-Start:     2 3 4 5\n# Default-Stop:      0 1 6\n# Short-Description: Example initscript\n# Description:       This file should be used to construct scripts to be\n#                    placed in /etc/init.d.\n### END INIT INFO\n\n# Author: Foo Bar <foobar@baz.org>\n#\n# Please remove the \"Author\" lines above and replace them\n# with your own name if you copy and modify this script.\n\n# Do NOT \"set -e\"\n\n# PATH should only include /usr/* if it runs after the mountnfs.sh script\nPATH=/sbin:/usr/sbin:/bin:/usr/bin\nDESC=\"Clam Daemon\"\nNAME=clamd\nDAEMON=\"/usr/local/sbin/clamd\"\nDAEMON_ARGS=\"-c /usr/local/etc/clamd.conf\"\nPIDFILE=/var/run/$NAME.pid\nSCRIPTNAME=/etc/init.d/$NAME\n\n# Exit if the package is not installed\n[ -x \"$DAEMON\" ] || exit 0\n\n# Read configuration variable file if it is present\n[ -r /etc/default/$NAME ] && . /etc/default/$NAME\n\n# Load the VERBOSE setting and other rcS variables\n. /lib/init/vars.sh\n\n# Define LSB log_* functions.\n# Depend on lsb-base (>= 3.2-14) to ensure that this file is present\n# and status_of_proc is working.\n. /lib/lsb/init-functions\n\n#\n# Function that starts the daemon/service\n#\ndo_start()\n{\n\t# Return\n\t#   0 if daemon has been started\n\t#   1 if daemon was already running\n\t#   2 if daemon could not be started\n\tstart-stop-daemon --background --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \\\n\t\t|| return 1\n\tstart-stop-daemon --background --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \\\n\t\t$DAEMON_ARGS \\\n\t\t|| return 2\n\t# Add code here, if necessary, that waits for the process to be ready\n\t# to handle requests from services started subsequently which depend\n\t# on this one.  As a last resort, sleep for some time.\n}\n\n#\n# Function that stops the daemon/service\n#\ndo_stop()\n{\n\t# Return\n\t#   0 if daemon has been stopped\n\t#   1 if daemon was already stopped\n\t#   2 if daemon could not be stopped\n\t#   other if a failure occurred\n\tstart-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME\n\tRETVAL=\"$?\"\n\t[ \"$RETVAL\" = 2 ] && return 2\n\t# Wait for children to finish too if this is a daemon that forks\n\t# and if the daemon is only ever run from this initscript.\n\t# If the above conditions are not satisfied then add some other code\n\t# that waits for the process to drop all resources that could be\n\t# needed by services started subsequently.  A last resort is to\n\t# sleep for some time.\n\tstart-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON\n\t[ \"$?\" = 2 ] && return 2\n\t# Many daemons don't delete their pidfiles when they exit.\n\trm -f $PIDFILE\n\treturn \"$RETVAL\"\n}\n\n#\n# Function that sends a SIGHUP to the daemon/service\n#\ndo_reload() {\n\t#\n\t# If the daemon can reload its configuration without\n\t# restarting (for example, when it is sent a SIGHUP),\n\t# then implement that here.\n\t#\n\tstart-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME\n\treturn 0\n}\n\ncase \"$1\" in\n  start)\n\t[ \"$VERBOSE\" != no ] && log_daemon_msg \"Starting $DESC\" \"$NAME\"\n\tdo_start\n\tcase \"$?\" in\n\t\t0|1) [ \"$VERBOSE\" != no ] && log_end_msg 0 ;;\n\t\t2) [ \"$VERBOSE\" != no ] && log_end_msg 1 ;;\n\tesac\n\t;;\n  stop)\n\t[ \"$VERBOSE\" != no ] && log_daemon_msg \"Stopping $DESC\" \"$NAME\"\n\tdo_stop\n\tcase \"$?\" in\n\t\t0|1) [ \"$VERBOSE\" != no ] && log_end_msg 0 ;;\n\t\t2) [ \"$VERBOSE\" != no ] && log_end_msg 1 ;;\n\tesac\n\t;;\n  status)\n\tstatus_of_proc \"$DAEMON\" \"$NAME\" && exit 0 || exit $?\n\t;;\n  #reload|force-reload)\n\t#\n\t# If do_reload() is not implemented then leave this commented out\n\t# and leave 'force-reload' as an alias for 'restart'.\n\t#\n\t#log_daemon_msg \"Reloading $DESC\" \"$NAME\"\n\t#do_reload\n\t#log_end_msg $?\n\t#;;\n  restart|force-reload)\n\t#\n\t# If the \"reload\" option is implemented then remove the\n\t# 'force-reload' alias\n\t#\n\tlog_daemon_msg \"Restarting $DESC\" \"$NAME\"\n\tdo_stop\n\tcase \"$?\" in\n\t  0|1)\n\t\tdo_start\n\t\tcase \"$?\" in\n\t\t\t0) log_end_msg 0 ;;\n\t\t\t1) log_end_msg 1 ;; # Old process is still running\n\t\t\t*) log_end_msg 1 ;; # Failed to start\n\t\tesac\n\t\t;;\n\t  *)\n\t\t# Failed to stop\n\t\tlog_end_msg 1\n\t\t;;\n\tesac\n\t;;\n  *)\n\t#echo \"Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}\" >&2\n\techo \"Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}\" >&2\n\texit 3\n\t;;\nesac\n\n:\n"
  },
  {
    "path": "extras/transfersh",
    "content": "#! /bin/sh\n### BEGIN INIT INFO\n# Provides:          skeleton\n# Required-Start:    $remote_fs $syslog\n# Required-Stop:     $remote_fs $syslog\n# Default-Start:     2 3 4 5\n# Default-Stop:      0 1 6\n# Short-Description: Example initscript\n# Description:       This file should be used to construct scripts to be\n#                    placed in /etc/init.d.\n### END INIT INFO\n\n# Author: Foo Bar <foobar@baz.org>\n#\n# Please remove the \"Author\" lines above and replace them\n# with your own name if you copy and modify this script.\n\n# Do NOT \"set -e\"\n\n# PATH should only include /usr/* if it runs after the mountnfs.sh script\nPATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/go/bin\nDESC=\"Transfersh Web server\"\nNAME=transfersh\nDAEMON=\"/opt/transfer.sh/main\"\nDAEMON_ARGS=\"--port 80 --temp /tmp/\"\nPIDFILE=/var/run/$NAME.pid\nSCRIPTNAME=/etc/init.d/$NAME\n\nexport BUCKET={bucket}\nexport AWS_ACCESS_KEY={aws_access_key}\nexport AWS_SECRET_KEY={aws_secret_key}\nexport VIRUSTOTAL_KEY={virustotal_key}\nexport GOPATH=/opt/go/\n\n# Exit if the package is not installed\n[ -x \"$DAEMON\" ] || exit 0\n\n# Read configuration variable file if it is present\n[ -r /etc/default/$NAME ] && . /etc/default/$NAME\n\n# Load the VERBOSE setting and other rcS variables\n. /lib/init/vars.sh\n\n# Define LSB log_* functions.\n# Depend on lsb-base (>= 3.2-14) to ensure that this file is present\n# and status_of_proc is working.\n. /lib/lsb/init-functions\n\n#\n# Function that starts the daemon/service\n#\ndo_start()\n{\n\t# Return\n\t#   0 if daemon has been started\n\t#   1 if daemon was already running\n\t#   2 if daemon could not be started\n\tstart-stop-daemon --background --start --chdir /opt/transfer.sh --quiet --pidfile $PIDFILE --make-pidfile --exec $DAEMON --test > /dev/null \\\n\t\t|| return 1\n\tstart-stop-daemon --background --start --chdir /opt/transfer.sh --quiet --pidfile $PIDFILE --make-pidfile --exec $DAEMON -- \\\n\t\t$DAEMON_ARGS \\\n\t\t|| return 2\n\t# Add code here, if necessary, that waits for the process to be ready\n\t# to handle requests from services started subsequently which depend\n\t# on this one.  As a last resort, sleep for some time.\n}\n\n#\n# Function that stops the daemon/service\n#\ndo_stop()\n{\n\t# Return\n\t#   0 if daemon has been stopped\n\t#   1 if daemon was already stopped\n\t#   2 if daemon could not be stopped\n\t#   other if a failure occurred\n\tstart-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME\n\tRETVAL=\"$?\"\n\t[ \"$RETVAL\" = 2 ] && return 2\n\t# Wait for children to finish too if this is a daemon that forks\n\t# and if the daemon is only ever run from this initscript.\n\t# If the above conditions are not satisfied then add some other code\n\t# that waits for the process to drop all resources that could be\n\t# needed by services started subsequently.  A last resort is to\n\t# sleep for some time.\n\tstart-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON\n\t[ \"$?\" = 2 ] && return 2\n\t# Many daemons don't delete their pidfiles when they exit.\n\trm -f $PIDFILE\n\treturn \"$RETVAL\"\n}\n\n#\n# Function that sends a SIGHUP to the daemon/service\n#\ndo_reload() {\n\t#\n\t# If the daemon can reload its configuration without\n\t# restarting (for example, when it is sent a SIGHUP),\n\t# then implement that here.\n\t#\n\tstart-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME\n\treturn 0\n}\n\ncase \"$1\" in\n  start)\n\t[ \"$VERBOSE\" != no ] && log_daemon_msg \"Starting $DESC\" \"$NAME\"\n\tdo_start\n\tcase \"$?\" in\n\t\t0|1) [ \"$VERBOSE\" != no ] && log_end_msg 0 ;;\n\t\t2) [ \"$VERBOSE\" != no ] && log_end_msg 1 ;;\n\tesac\n\t;;\n  stop)\n\t[ \"$VERBOSE\" != no ] && log_daemon_msg \"Stopping $DESC\" \"$NAME\"\n\tdo_stop\n\tcase \"$?\" in\n\t\t0|1) [ \"$VERBOSE\" != no ] && log_end_msg 0 ;;\n\t\t2) [ \"$VERBOSE\" != no ] && log_end_msg 1 ;;\n\tesac\n\t;;\n  status)\n\tstatus_of_proc \"$DAEMON\" \"$NAME\" && exit 0 || exit $?\n\t;;\n  #reload|force-reload)\n\t#\n\t# If do_reload() is not implemented then leave this commented out\n\t# and leave 'force-reload' as an alias for 'restart'.\n\t#\n\t#log_daemon_msg \"Reloading $DESC\" \"$NAME\"\n\t#do_reload\n\t#log_end_msg $?\n\t#;;\n  restart|force-reload)\n\t#\n\t# If the \"reload\" option is implemented then remove the\n\t# 'force-reload' alias\n\t#\n\tlog_daemon_msg \"Restarting $DESC\" \"$NAME\"\n\tdo_stop\n\tcase \"$?\" in\n\t  0|1)\n\t\tdo_start\n\t\tcase \"$?\" in\n\t\t\t0) log_end_msg 0 ;;\n\t\t\t1) log_end_msg 1 ;; # Old process is still running\n\t\t\t*) log_end_msg 1 ;; # Failed to start\n\t\tesac\n\t\t;;\n\t  *)\n\t\t# Failed to stop\n\t\tlog_end_msg 1\n\t\t;;\n\tesac\n\t;;\n  *)\n\t#echo \"Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}\" >&2\n\techo \"Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}\" >&2\n\texit 3\n\t;;\nesac\n\n:\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"Transfer.sh\";\n\n  inputs.flake-utils.url = \"github:numtide/flake-utils\";\n\n  outputs = { self, nixpkgs, flake-utils }:\n    let\n      transfer-sh = pkgs: pkgs.buildGoModule {\n        src = self;\n        name = \"transfer.sh\";\n        vendorSha256 = \"sha256-bgQUMiC33yVorcKOWhegT1/YU+fvxsz2pkeRvjf3R7g=\";\n      };\n    in\n\n      flake-utils.lib.eachDefaultSystem (\n        system:\n          let\n            pkgs = nixpkgs.legacyPackages.${system};\n          in\n            rec {\n              packages = flake-utils.lib.flattenTree {\n                transfer-sh = transfer-sh pkgs;\n              };\n              defaultPackage = packages.transfer-sh;\n              apps.transfer-sh = flake-utils.lib.mkApp { drv = packages.transfer-sh; };\n              defaultApp = apps.transfer-sh;\n            }\n      ) // rec {\n\n        nixosModules = {\n          transfer-sh = { config, lib, pkgs, ... }: with lib; let\n            RUNTIME_DIR = \"/var/lib/transfer.sh\";\n            cfg = config.services.transfer-sh;\n\n            general_options = {\n\n              enable = mkEnableOption \"Transfer.sh service\";\n              listener = mkOption { default = 80; type = types.int; description = \"port to use for http (:80)\"; };\n              profile-listener = mkOption { default = 6060; type = types.int; description = \"port to use for profiler (:6060)\"; };\n              force-https = mkOption { type = types.nullOr types.bool; description = \"redirect to https\"; };\n              tls-listener = mkOption { default = 443; type = types.int; description = \"port to use for https (:443)\"; };\n              tls-listener-only = mkOption { type = types.nullOr types.bool; description = \"flag to enable tls listener only\"; };\n              tls-cert-file = mkOption { type = types.nullOr types.str; description = \"path to tls certificate\"; };\n              tls-private-key = mkOption { type = types.nullOr types.str; description = \"path to tls private key \"; };\n              http-auth-user = mkOption { type = types.nullOr types.str; description = \"user for basic http auth on upload\"; };\n              http-auth-pass = mkOption { type = types.nullOr types.str; description = \"pass for basic http auth on upload\"; };\n              http-auth-htpasswd = mkOption { type = types.nullOr types.str; description = \"htpasswd file path for basic http auth on upload\"; };\n              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\"; };\n              ip-whitelist = mkOption { type = types.nullOr types.str; description = \"comma separated list of ips allowed to connect to the service\"; };\n              ip-blacklist = mkOption { type = types.nullOr types.str; description = \"comma separated list of ips not allowed to connect to the service\"; };\n              temp-path = mkOption { type = types.nullOr types.str; description = \"path to temp folder\"; };\n              web-path = mkOption { type = types.nullOr types.str; description = \"path to static web files (for development or custom front end)\"; };\n              proxy-path = mkOption { type = types.nullOr types.str; description = \"path prefix when service is run behind a proxy\"; };\n              proxy-port = mkOption { type = types.nullOr types.str; description = \"port of the proxy when the service is run behind a proxy\"; };\n              ga-key = mkOption { type = types.nullOr types.str; description = \"google analytics key for the front end\"; };\n              email-contact = mkOption { type = types.nullOr types.str; description = \"email contact for the front end\"; };\n              uservoice-key = mkOption { type = types.nullOr types.str; description = \"user voice key for the front end\"; };\n              lets-encrypt-hosts = mkOption { type = types.nullOr (types.listOf types.str); description = \"hosts to use for lets encrypt certificates\"; };\n              log = mkOption { type = types.nullOr types.str; description = \"path to log file\"; };\n              cors-domains = mkOption { type = types.nullOr (types.listOf types.str); description = \"comma separated list of domains for CORS, setting it enable CORS \"; };\n              clamav-host = mkOption { type = types.nullOr types.str; description = \"host for clamav feature\"; };\n              rate-limit = mkOption { type = types.nullOr types.int; description = \"request per minute\"; };\n              max-upload-size = mkOption { type = types.nullOr types.int; description = \"max upload size in kilobytes  \"; };\n              purge-days = mkOption { type = types.nullOr types.int; description = \"number of days after the uploads are purged automatically \"; };\n              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)\"; };\n\n            };\n\n            provider_options = {\n\n                aws = {\n                  enable = mkEnableOption \"Enable AWS backend\";\n                  aws-access-key = mkOption { type = types.str; description = \"aws access key\"; };\n                  aws-secret-key = mkOption { type = types.str; description = \"aws secret key\"; };\n                  bucket = mkOption { type = types.str; description = \"aws bucket \"; };\n                  s3-endpoint = mkOption {\n                    type = types.nullOr types.str;\n                    description = ''\n                      Custom S3 endpoint. \n                      If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically.\n                    '';\n                  };\n                  s3-region = mkOption { type = types.str; description = \"region of the s3 bucket eu-west-\"; };\n                  s3-no-multipart = mkOption { type = types.nullOr types.bool; description = \"disables s3 multipart upload \"; };\n                  s3-path-style = mkOption { type = types.nullOr types.str; description = \"Forces path style URLs, required for Minio. \"; };\n                };\n\n                storj = {\n                  enable = mkEnableOption \"Enable storj backend\";\n                  storj-access = mkOption { type = types.str; description = \"Access for the project\"; };\n                  storj-bucket = mkOption { type = types.str; description = \"Bucket to use within the project\"; };\n                };\n\n                gdrive = {\n                  enable = mkEnableOption \"Enable gdrive backend\";\n                  gdrive-client-json = mkOption { type = types.str; description = \"oauth client json config for gdrive provider\"; };\n                  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)\"; };\n                  basedir = mkOption { type = types.str; description = \"path storage for gdrive provider\"; default = \"${cfg.stateDir}/store\"; };\n                  purge-interval = mkOption { type = types.nullOr types.int; description = \"interval in hours to run the automatic purge for (not applicable to S3 and Storj)\"; };\n\n                };\n\n                local = {\n                  enable = mkEnableOption \"Enable local backend\";\n                  basedir = mkOption { type = types.str; description = \"path storage for local provider\"; default = \"${cfg.stateDir}/store\"; };\n                  purge-interval = mkOption { type = types.nullOr types.int; description = \"interval in hours to run the automatic purge for (not applicable to S3 and Storj)\"; };\n                };\n\n              };\n          in\n            {\n              options.services.transfer-sh = fold recursiveUpdate {} [\n                general_options\n                {\n                  provider = provider_options;\n                  user = mkOption {\n                    type = types.str;\n                    description = \"User to run the service under\";\n                    default = \"transfer.sh\";\n                  };\n                  group = mkOption {\n                    type = types.str;\n                    description = \"Group to run the service under\";\n                    default = \"transfer.sh\";\n                  };\n                  stateDir = mkOption {\n                    type = types.path;\n                    description = \"Variable state directory\";\n                    default = RUNTIME_DIR;\n                  };\n                }\n              ];\n\n              config = let\n\n                mkFlags = cfg: options:\n                  let\n                    mkBoolFlag = option: if cfg.${option} then [ \"--${option}\" ] else [];\n                    mkFlag = option:\n                      if isBool cfg.${option}\n                      then mkBoolFlag option\n                      else [ \"--${option}\" \"${cfg.${option}}\" ];\n\n                  in\n                    lists.flatten (map (mkFlag) (filter (option: cfg.${option} != null && option != \"enable\") options));\n\n                aws-config = (mkFlags cfg.provider.aws (attrNames provider_options)) ++ [ \"--provider\" \"aws\" ];\n                gdrive-config = mkFlags cfg.provider.gdrive (attrNames provider_options.gdrive) ++ [ \"--provider\" \"gdrive\" ];\n                storj-config = mkFlags cfg.provider.storj (attrNames provider_options.storj) ++ [ \"--provider\" \"storj\" ];\n                local-config = mkFlags cfg.provider.local (attrNames provider_options.local) ++ [ \"--provider\" \"local\" ];\n\n                general-config = concatStringsSep \" \" (mkFlags cfg (attrNames general_options));\n                provider-config = concatStringsSep \" \" (\n                  if cfg.provider.aws.enable && !cfg.provider.storj.enable && !cfg.provider.gdrive.enable && !cfg.provider.local.enable then aws-config\n                  else if !cfg.provider.aws.enable && cfg.provider.storj.enable && !cfg.provider.gdrive.enable && !cfg.provider.local.enable then storj-config\n                  else if !cfg.provider.aws.enable && !cfg.provider.storj.enable && cfg.provider.gdrive.enable && !cfg.provider.local.enable then gdrive-config\n                  else if !cfg.provider.aws.enable && !cfg.provider.storj.enable && !cfg.provider.gdrive.enable && cfg.provider.local.enable then local-config\n                  else throw \"transfer.sh requires exactly one provider (aws, storj, gdrive, local)\"\n                );\n\n              in\n                lib.mkIf cfg.enable\n                  {\n                    systemd.tmpfiles.rules = [\n                      \"d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -\"\n                    ] ++ optional cfg.provider.gdrive.enable cfg.provider.gdrive.basedir\n                    ++ optional cfg.provider.local.enable cfg.provider.local.basedir;\n\n                    systemd.services.transfer-sh = {\n                      wantedBy = [ \"multi-user.target\" ];\n                      after = [ \"network.target\" ];\n                      serviceConfig = {\n                        User = cfg.user;\n                        Group = cfg.group;\n                        ExecStart = \"${transfer-sh pkgs}/bin/transfer.sh ${general-config} ${provider-config} \";\n                      };\n                    };\n\n                    networking.firewall.allowedTCPPorts = [ cfg.listener cfg.profile-listener cfg.tls-listener ];\n                  };\n            };\n\n          default = { self, pkgs, ... }: {\n            imports = [ nixosModules.transfer-sh ];\n            # Network configuration.\n\n            # useDHCP is generally considered to better be turned off in favor\n            # of <adapter>.useDHCP\n            networking.useDHCP = false;\n            networking.firewall.allowedTCPPorts = [];\n\n            # Enable the inventaire server.\n            services.transfer-sh = {\n              enable = true;\n              provider.local = {\n                enable = true;\n              };\n            };\n\n            nixpkgs.config.allowUnfree = true;\n          };\n        };\n\n\n        nixosConfigurations.\"container\" = nixpkgs.lib.nixosSystem {\n          system = \"x86_64-linux\";\n          modules = [\n            nixosModules.default\n            ({ ... }: { boot.isContainer = true; })\n          ];\n        };\n\n      };\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/dutchcoders/transfer.sh\n\ngo 1.22.0\n\nrequire (\n\tgithub.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8\n\tgithub.com/ProtonMail/gopenpgp/v2 v2.5.2\n\tgithub.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14\n\tgithub.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2\n\tgithub.com/aws/aws-sdk-go-v2 v1.18.0\n\tgithub.com/aws/aws-sdk-go-v2/config v1.18.25\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.13.24\n\tgithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.33.1\n\tgithub.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e\n\tgithub.com/Aetherinox/go-virustotal v0.0.0-20250520084801-0eb8c8f901c8\n\tgithub.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6\n\tgithub.com/elazarl/go-bindata-assetfs v1.0.1\n\tgithub.com/fatih/color v1.14.1\n\tgithub.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f\n\tgithub.com/gorilla/handlers v1.5.1\n\tgithub.com/gorilla/mux v1.8.0\n\tgithub.com/microcosm-cc/bluemonday v1.0.23\n\tgithub.com/russross/blackfriday/v2 v2.1.0\n\tgithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e\n\tgithub.com/tg123/go-htpasswd v1.2.1\n\tgithub.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce\n\tgithub.com/urfave/cli/v2 v2.25.3\n\tgolang.org/x/crypto v0.21.0\n\tgolang.org/x/net v0.23.0\n\tgolang.org/x/oauth2 v0.7.0\n\tgolang.org/x/text v0.14.0\n\tgoogle.golang.org/api v0.114.0\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c\n\tstorj.io/common v0.0.0-20230301105927-7f966760c100\n\tstorj.io/uplink v1.10.0\n)\n\nrequire (\n\tcloud.google.com/go/compute v1.19.1 // indirect\n\tcloud.google.com/go/compute/metadata v0.2.3 // indirect\n\tgithub.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect\n\tgithub.com/aws/smithy-go v1.13.5 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/calebcase/tmpfile v1.0.3 // indirect\n\tgithub.com/cloudflare/circl v1.6.1 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.3 // indirect\n\tgithub.com/flynn/noise v1.0.0 // indirect\n\tgithub.com/garyburd/redigo v1.6.4 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/golang/protobuf v1.5.3 // indirect\n\tgithub.com/google/uuid v1.3.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.7.1 // indirect\n\tgithub.com/gorilla/css v1.0.0 // indirect\n\tgithub.com/gorilla/securecookie v1.1.1 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f // indirect\n\tgithub.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.4 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.17 // indirect\n\tgithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect\n\tgithub.com/rogpeppe/go-internal v1.9.0 // indirect\n\tgithub.com/spacemonkeygo/monkit/v3 v3.0.19 // indirect\n\tgithub.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 // indirect\n\tgithub.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect\n\tgithub.com/zeebo/blake3 v0.2.3 // indirect\n\tgithub.com/zeebo/errs v1.3.0 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgolang.org/x/sync v0.1.0 // indirect\n\tgolang.org/x/sys v0.18.0 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect\n\tgoogle.golang.org/grpc v1.56.3 // indirect\n\tgoogle.golang.org/protobuf v1.33.0 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tstorj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a // indirect\n\tstorj.io/picobuf v0.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=\ncloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=\ncloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=\ncloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=\ncloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=\ncloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=\ncloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=\ngithub.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=\ngithub.com/ProtonMail/go-crypto v0.0.0-20230124153114-0acdc8ae009b/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=\ngithub.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=\ngithub.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=\ngithub.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=\ngithub.com/ProtonMail/gopenpgp/v2 v2.5.2 h1:97SjlWNAxXl9P22lgwgrZRshQdiEfAht0g3ZoiA1GCw=\ngithub.com/ProtonMail/gopenpgp/v2 v2.5.2/go.mod h1:52qDaCnto6r+CoWbuU50T77XQt99lIs46HtHtvgFO3o=\ngithub.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14 h1:3zOOc7WdrATDXof+h/rBgMsg0sAmZIEVHft1UbWHh94=\ngithub.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14/go.mod h1:+VFiaivV54Sa94ijzA/ZHQLoHuoUIS9hIqCK6f/76Zw=\ngithub.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2 h1:sIvihcW4qpN5qGSjmrsDDAbLpEq5tuHjJJfWY0Hud5Y=\ngithub.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2/go.mod h1:3YwJE8rEisS9eraee0hygGG4G3gqX8H8Nyu+nPTUnGU=\ngithub.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=\ngithub.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=\ngithub.com/aws/aws-sdk-go-v2/config v1.18.25 h1:JuYyZcnMPBiFqn87L2cRppo+rNwgah6YwD3VuyvaW6Q=\ngithub.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.13.24 h1:PjiYyls3QdCrzqUN35jMWtUK1vqVZ+zLfdOa/UPFDp0=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 h1:fI9/5BDEaAv/pv1VO1X1n3jfP9it+IGqWsCuuBQI8wM=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67/go.mod h1:zQClPRIwQZfJlZq6WZve+s4Tb4JW+3V6eS+4+KrYeP8=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 h1:O+9nAy9Bb6bJFTpeNFtd9UfHbgxO1o4ZDAM9rQp5NsY=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.33.1/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.12.10 h1:UBQjaMTCKwyUYwiVnUt6toEJwGXsLBI6al083tpjJzY=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=\ngithub.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=\ngithub.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=\ngithub.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=\ngithub.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=\ngithub.com/calebcase/tmpfile v1.0.3 h1:BZrOWZ79gJqQ3XbAQlihYZf/YCV0H4KPIdM5K5oMpJo=\ngithub.com/calebcase/tmpfile v1.0.3/go.mod h1:UAUc01aHeC+pudPagY/lWvt2qS9ZO5Zzof6/tIUzqeI=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=\ngithub.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=\ngithub.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=\ngithub.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=\ngithub.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e h1:rcHHSQqzCgvlwP0I/fQ8rQMn/MpHE5gWSLdtpxtP6KQ=\ngithub.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e/go.mod h1:Byz7q8MSzSPkouskHJhX0er2mZY/m0Vj5bMeMCkkyY4=\ngithub.com/Aetherinox/go-virustotal v0.0.0-20250520084801-0eb8c8f901c8 h1:wEwYJxNLG29OesabDdAJWFBIO42HOL4x5kjvGuZLIyk=\ngithub.com/Aetherinox/go-virustotal v0.0.0-20250520084801-0eb8c8f901c8/go.mod h1:myGG2GhfY2AgAPe8lFZw6Y1+IxhU+ED7ilotbpdQsDw=\ngithub.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6 h1:7uTRy44YpQi6/mtDq0N9zeQRCGEh93o7gKq/usGgpF8=\ngithub.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6/go.mod h1:F6Q37CxDh2MHr5KXkcZmNB3tdkK7v+bgE+OpBY+9ilI=\ngithub.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=\ngithub.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=\ngithub.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=\ngithub.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=\ngithub.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ=\ngithub.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=\ngithub.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=\ngithub.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg=\ngithub.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=\ngithub.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=\ngithub.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=\ngithub.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/pprof v0.0.0-20211108044417-e9b028704de0 h1:rsq1yB2xiFLDYYaYdlGBsSkwVzsCo500wMhxvW5A/bk=\ngithub.com/google/pprof v0.0.0-20211108044417-e9b028704de0/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=\ngithub.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=\ngithub.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=\ngithub.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A=\ngithub.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=\ngithub.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=\ngithub.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=\ngithub.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=\ngithub.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=\ngithub.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=\ngithub.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f h1:HM2D/tnqbzNoN5DGIeB8ibM1BMYCkRWOqyWWcNAWw8o=\ngithub.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f/go.mod h1:PXFUrknJu7TkBNyL8t7XWDPtDFFLFrNQQAdsXv9YfJE=\ngithub.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059 h1:4xdaxDg3xe+DKZC8NjbH/gvTs4iNYUnzOAiD5QL5NrM=\ngithub.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059/go.mod h1:f0ijQHcvHYAuxX6JA/JUr/Z0FVn12D9REaT/HAWVgP4=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=\ngithub.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=\ngithub.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=\ngithub.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=\ngithub.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=\ngithub.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=\ngithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=\ngithub.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/shuLhan/go-bindata v4.0.0+incompatible/go.mod h1:pkcPAATLBDD2+SpAPnX5vEM90F7fcwHCvvLCMXcmw3g=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=\ngithub.com/spacemonkeygo/monkit/v3 v3.0.19 h1:wqBb9bpD7jXkVi4XwIp8jn1fektaVBQ+cp9SHRXgAdo=\ngithub.com/spacemonkeygo/monkit/v3 v3.0.19/go.mod h1:kj1ViJhlyADa7DiA4xVnTuPA46lFKbM7mxQTrXCuJP4=\ngithub.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=\ngithub.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/tg123/go-htpasswd v1.2.1 h1:i4wfsX1KvvkyoMiHZzjS0VzbAPWfxzI8INcZAKtutoU=\ngithub.com/tg123/go-htpasswd v1.2.1/go.mod h1:erHp1B86KXdwQf1X5ZrLb7erXZnWueEQezb2dql4q58=\ngithub.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=\ngithub.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=\ngithub.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=\ngithub.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=\ngithub.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 h1:zMsHhfK9+Wdl1F7sIKLyx3wrOFofpb3rWFbA4HgcK5k=\ngithub.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3/go.mod h1:R0Gbuw7ElaGSLOZUSwBm/GgVwMd30jWxBDdAyMOeTuc=\ngithub.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=\ngithub.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=\ngithub.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A=\ngithub.com/zeebo/assert v1.3.1/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=\ngithub.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=\ngithub.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=\ngithub.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=\ngithub.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=\ngithub.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=\ngithub.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=\ngolang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=\ngolang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=\ngolang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=\ngolang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=\ngolang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=\ngolang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=\ngoogle.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=\ngoogle.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=\ngoogle.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=\ngoogle.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=\ngoogle.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=\ngoogle.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nstorj.io/common v0.0.0-20230301105927-7f966760c100 h1:0Rc6boo10ZgiHdadHi1o2OUv25YvTn8fSc/VyRz2Tyk=\nstorj.io/common v0.0.0-20230301105927-7f966760c100/go.mod h1:tDgoLthBVcrTPEokBgPdjrn39p/gyNx06j6ehhTSiUg=\nstorj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a h1:FBaOc8c5efmW3tmPsiGy07USMkOSu/tyYCZpu2ro0y8=\nstorj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a/go.mod h1:6rcOyR/QQkSTX/9L5ZGtlZaE2PtXTTZl8d+ulSeeYEg=\nstorj.io/picobuf v0.0.1 h1:ekEvxSQCbEjTVIi/qxj2za13SJyfRE37yE30IBkZeT0=\nstorj.io/picobuf v0.0.1/go.mod h1:7ZTAMs6VesgTHbbhFU79oQ9hDaJ+MD4uoFQZ1P4SEz0=\nstorj.io/uplink v1.10.0 h1:3hS0hszupHSxEoC4DsMpljaRy0uNoijEPVF6siIE28Q=\nstorj.io/uplink v1.10.0/go.mod h1:gJIQumB8T3tBHPRive51AVpbc+v2xe+P/goFNMSRLG4=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/dutchcoders/transfer.sh/cmd\"\n)\n\nfunc main() {\n\tapp := cmd.New()\n\terr := app.Run(os.Args)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n    \"dependencies\": {\n        \"github.com/dutchcoders/transfer.sh-web\": {\n            \"branch\": \"master\"\n        }\n    }\n}\n"
  },
  {
    "path": "server/clamav.go",
    "content": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]\nCopyright (c) 2018-2020 Andrea Spacca.\nCopyright (c) 2020- Andrea Spacca and Stefan Benten.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n*/\n\npackage server\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/dutchcoders/go-clamd\"\n\t\"github.com/gorilla/mux\"\n)\n\nconst clamavScanStatusOK = \"OK\"\n\nfunc (s *Server) scanHandler(w http.ResponseWriter, r *http.Request) {\n\tvars := mux.Vars(r)\n\n\tfilename := sanitize(vars[\"filename\"])\n\n\tcontentLength := r.ContentLength\n\tcontentType := r.Header.Get(\"Content-Type\")\n\n\ts.logger.Printf(\"Scanning %s %d %s\", filename, contentLength, contentType)\n\n\tfile, err := os.CreateTemp(s.tempPath, \"clamav-\")\n\tdefer s.cleanTmpFile(file)\n\tif err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t_, err = io.Copy(file, r.Body)\n\tif err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tstatus, err := s.performScan(file.Name())\n\tif err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t_, _ = w.Write([]byte(fmt.Sprintf(\"%v\\n\", status)))\n}\n\nfunc (s *Server) performScan(path string) (string, error) {\n\tc := clamd.NewClamd(s.ClamAVDaemonHost)\n\n\tresponseCh := make(chan chan *clamd.ScanResult)\n\terrCh := make(chan error)\n\tgo func(responseCh chan chan *clamd.ScanResult, errCh chan error) {\n\t\tresponse, err := c.ScanFile(path)\n\t\tif err != nil {\n\t\t\terrCh <- err\n\t\t\treturn\n\t\t}\n\n\t\tresponseCh <- response\n\t}(responseCh, errCh)\n\n\tselect {\n\tcase err := <-errCh:\n\t\treturn \"\", err\n\tcase response := <-responseCh:\n\t\tst := <-response\n\t\treturn st.Status, nil\n\tcase <-time.After(time.Second * 60):\n\t\treturn \"\", errors.New(\"clamav scan timeout\")\n\t}\n}\n"
  },
  {
    "path": "server/handlers.go",
    "content": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]\nCopyright (c) 2018-2020 Andrea Spacca.\nCopyright (c) 2020- Andrea Spacca and Stefan Benten.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n*/\n\npackage server\n\nimport (\n\t\"archive/tar\"\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html\"\n\thtmlTemplate \"html/template\"\n\t\"io\"\n\t\"mime\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\ttextTemplate \"text/template\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/ProtonMail/go-crypto/openpgp/armor\"\n\t\"github.com/ProtonMail/go-crypto/openpgp/packet\"\n\t\"github.com/ProtonMail/gopenpgp/v2/constants\"\n\t\"github.com/dutchcoders/transfer.sh/server/storage\"\n\t\"github.com/tg123/go-htpasswd\"\n\t\"github.com/tomasen/realip\"\n\n\tweb \"github.com/dutchcoders/transfer.sh-web\"\n\t\"github.com/gorilla/mux\"\n\t\"github.com/microcosm-cc/bluemonday\"\n\tblackfriday \"github.com/russross/blackfriday/v2\"\n\tqrcode \"github.com/skip2/go-qrcode\"\n\t\"golang.org/x/net/idna\"\n\t\"golang.org/x/text/runes\"\n\t\"golang.org/x/text/transform\"\n\t\"golang.org/x/text/unicode/norm\"\n)\n\nconst getPathPart = \"get\"\n\nvar (\n\thtmlTemplates = initHTMLTemplates()\n\ttextTemplates = initTextTemplates()\n)\n\nfunc stripPrefix(path string) string {\n\treturn strings.Replace(path, web.Prefix+\"/\", \"\", -1)\n}\n\nfunc initTextTemplates() *textTemplate.Template {\n\ttemplateMap := textTemplate.FuncMap{\"format\": formatNumber}\n\n\t// Templates with functions available to them\n\tvar templates = textTemplate.New(\"\").Funcs(templateMap)\n\treturn templates\n}\n\nfunc initHTMLTemplates() *htmlTemplate.Template {\n\ttemplateMap := htmlTemplate.FuncMap{\"format\": formatNumber}\n\n\t// Templates with functions available to them\n\tvar templates = htmlTemplate.New(\"\").Funcs(templateMap)\n\n\treturn templates\n}\n\nfunc attachEncryptionReader(reader io.ReadCloser, password string) (io.ReadCloser, error) {\n\tif len(password) == 0 {\n\t\treturn reader, nil\n\t}\n\n\treturn encrypt(reader, []byte(password))\n}\n\nfunc attachDecryptionReader(reader io.ReadCloser, password string) (io.ReadCloser, error) {\n\tif len(password) == 0 {\n\t\treturn reader, nil\n\t}\n\n\treturn decrypt(reader, []byte(password))\n}\n\nfunc decrypt(ciphertext io.ReadCloser, password []byte) (plaintext io.ReadCloser, err error) {\n\tunarmored, err := armor.Decode(ciphertext)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfirstTimeCalled := true\n\tvar prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) {\n\t\tif firstTimeCalled {\n\t\t\tfirstTimeCalled = false\n\t\t\treturn password, nil\n\t\t}\n\t\t// Re-prompt still occurs if SKESK pasrsing fails (i.e. when decrypted cipher algo is invalid).\n\t\t// For most (but not all) cases, inputting a wrong passwords is expected to trigger this error.\n\t\treturn nil, errors.New(\"gopenpgp: wrong password in symmetric decryption\")\n\t}\n\n\tconfig := &packet.Config{\n\t\tDefaultCipher: packet.CipherAES256,\n\t}\n\n\tvar emptyKeyRing openpgp.EntityList\n\tmd, err := openpgp.ReadMessage(unarmored.Body, emptyKeyRing, prompt, config)\n\tif err != nil {\n\t\t// Parsing errors when reading the message are most likely caused by incorrect password, but we cannot know for sure\n\t\treturn\n\t}\n\n\tplaintext = io.NopCloser(md.UnverifiedBody)\n\n\treturn\n}\n\ntype encryptWrapperReader struct {\n\tplaintext         io.Reader\n\tencrypt           io.WriteCloser\n\tarmored           io.WriteCloser\n\tbuffer            io.ReadWriter\n\tplaintextReadZero bool\n}\n\nfunc (e *encryptWrapperReader) Read(p []byte) (n int, err error) {\n\tp2 := make([]byte, len(p))\n\n\tn, _ = e.plaintext.Read(p2)\n\tif n == 0 {\n\t\tif !e.plaintextReadZero {\n\t\t\terr = e.encrypt.Close()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terr = e.armored.Close()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\te.plaintextReadZero = true\n\t\t}\n\n\t\treturn e.buffer.Read(p)\n\t}\n\n\treturn e.buffer.Read(p)\n}\n\nfunc (e *encryptWrapperReader) Close() error {\n\treturn nil\n}\n\nfunc NewEncryptWrapperReader(plaintext io.Reader, armored, encrypt io.WriteCloser, buffer io.ReadWriter) io.ReadCloser {\n\treturn &encryptWrapperReader{\n\t\tplaintext: io.TeeReader(plaintext, encrypt),\n\t\tencrypt:   encrypt,\n\t\tarmored:   armored,\n\t\tbuffer:    buffer,\n\t}\n}\n\nfunc encrypt(plaintext io.ReadCloser, password []byte) (ciphertext io.ReadCloser, err error) {\n\tbufferReadWriter := new(bytes.Buffer)\n\tarmored, err := armor.Encode(bufferReadWriter, constants.PGPMessageHeader, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\tconfig := &packet.Config{\n\t\tDefaultCipher: packet.CipherAES256,\n\t\tTime:          time.Now,\n\t}\n\n\thints := &openpgp.FileHints{\n\t\tIsBinary: true,\n\t\tFileName: \"\",\n\t\tModTime:  time.Unix(time.Now().Unix(), 0),\n\t}\n\n\tencryptWriter, err := openpgp.SymmetricallyEncrypt(armored, password, hints, config)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tciphertext = NewEncryptWrapperReader(plaintext, armored, encryptWriter, bufferReadWriter)\n\n\treturn\n}\n\nfunc healthHandler(w http.ResponseWriter, _ *http.Request) {\n\t_, _ = w.Write([]byte(\"Approaching Neutral Zone, all systems normal and functioning.\"))\n}\n\nfunc canContainsXSS(contentType string) bool {\n\tswitch {\n\tcase strings.Contains(contentType, \"cache-manifest\"):\n\t\tfallthrough\n\tcase strings.Contains(contentType, \"html\"):\n\t\tfallthrough\n\tcase strings.Contains(contentType, \"rdf\"):\n\t\tfallthrough\n\tcase strings.Contains(contentType, \"vtt\"):\n\t\tfallthrough\n\tcase strings.Contains(contentType, \"xml\"):\n\t\tfallthrough\n\tcase strings.Contains(contentType, \"xsl\"):\n\t\treturn true\n\tcase strings.Contains(contentType, \"x-mixed-replace\"):\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n/* The preview handler will show a preview of the content for browsers (accept type text/html), and referer is not transfer.sh */\nfunc (s *Server) previewHandler(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Vary\", \"Range, Referer, X-Decrypt-Password\")\n\n\tvars := mux.Vars(r)\n\n\ttoken := vars[\"token\"]\n\tfilename := vars[\"filename\"]\n\n\tmetadata, err := s.checkMetadata(r.Context(), token, filename, false)\n\n\tif err != nil {\n\t\ts.logger.Printf(\"Error metadata: %s\", err.Error())\n\t\thttp.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)\n\t\treturn\n\t}\n\n\tcontentType := metadata.ContentType\n\tcontentLength, err := s.storage.Head(r.Context(), token, filename)\n\tif err != nil {\n\t\thttp.Error(w, http.StatusText(404), 404)\n\t\treturn\n\t}\n\n\tvar templatePath string\n\tvar content htmlTemplate.HTML\n\n\tswitch {\n\tcase strings.HasPrefix(contentType, \"image/\"):\n\t\ttemplatePath = \"download.image.html\"\n\tcase strings.HasPrefix(contentType, \"video/\"):\n\t\ttemplatePath = \"download.video.html\"\n\tcase strings.HasPrefix(contentType, \"audio/\"):\n\t\ttemplatePath = \"download.audio.html\"\n\tcase strings.HasPrefix(contentType, \"text/\"):\n\t\ttemplatePath = \"download.markdown.html\"\n\n\t\tvar reader io.ReadCloser\n\t\tif reader, _, err = s.storage.Get(r.Context(), token, filename, nil); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tvar data []byte\n\t\tdata = make([]byte, _5M)\n\t\tif _, err = reader.Read(data); err != io.EOF && err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif strings.HasPrefix(contentType, \"text/x-markdown\") || strings.HasPrefix(contentType, \"text/markdown\") {\n\t\t\tunsafe := blackfriday.Run(data)\n\t\t\toutput := bluemonday.UGCPolicy().SanitizeBytes(unsafe)\n\t\t\tcontent = htmlTemplate.HTML(output)\n\t\t} else if strings.HasPrefix(contentType, \"text/plain\") {\n\t\t\tcontent = htmlTemplate.HTML(fmt.Sprintf(\"<pre>%s</pre>\", html.EscapeString(string(data))))\n\t\t} else {\n\t\t\ttemplatePath = \"download.sandbox.html\"\n\t\t}\n\n\tdefault:\n\t\ttemplatePath = \"download.html\"\n\t}\n\n\trelativeURL, _ := url.Parse(path.Join(s.proxyPath, token, filename))\n\tresolvedURL := resolveURL(r, relativeURL, s.proxyPort)\n\trelativeURLGet, _ := url.Parse(path.Join(s.proxyPath, getPathPart, token, filename))\n\tresolvedURLGet := resolveURL(r, relativeURLGet, s.proxyPort)\n\tvar png []byte\n\tpng, err = qrcode.Encode(resolvedURL, qrcode.High, 150)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tqrCode := base64.StdEncoding.EncodeToString(png)\n\n\thostname := getURL(r, s.proxyPort).Host\n\twebAddress := resolveWebAddress(r, s.proxyPath, s.proxyPort)\n\n\tdata := struct {\n\t\tContentType    string\n\t\tContent        htmlTemplate.HTML\n\t\tFilename       string\n\t\tURL            string\n\t\tURLGet         string\n\t\tURLRandomToken string\n\t\tHostname       string\n\t\tWebAddress     string\n\t\tContentLength  uint64\n\t\tGAKey          string\n\t\tUserVoiceKey   string\n\t\tQRCode         string\n\t}{\n\t\tcontentType,\n\t\tcontent,\n\t\tfilename,\n\t\tresolvedURL,\n\t\tresolvedURLGet,\n\t\ttoken,\n\t\thostname,\n\t\twebAddress,\n\t\tcontentLength,\n\t\ts.gaKey,\n\t\ts.userVoiceKey,\n\t\tqrCode,\n\t}\n\n\tif err := htmlTemplates.ExecuteTemplate(w, templatePath, data); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n}\n\n// this handler will output html or text, depending on the\n// support of the client (Accept header).\n\nfunc (s *Server) viewHandler(w http.ResponseWriter, r *http.Request) {\n\t// vars := mux.Vars(r)\n\n\thostname := getURL(r, s.proxyPort).Host\n\twebAddress := resolveWebAddress(r, s.proxyPath, s.proxyPort)\n\n\tmaxUploadSize := \"\"\n\tif s.maxUploadSize > 0 {\n\t\tmaxUploadSize = formatSize(s.maxUploadSize)\n\t}\n\n\tpurgeTime := \"\"\n\tif s.purgeDays > 0 {\n\t\tpurgeTime = formatDurationDays(s.purgeDays)\n\t}\n\n\tdata := struct {\n\t\tHostname      string\n\t\tWebAddress    string\n\t\tEmailContact  string\n\t\tGAKey         string\n\t\tUserVoiceKey  string\n\t\tPurgeTime     string\n\t\tMaxUploadSize string\n\t\tSampleToken   string\n\t\tSampleToken2  string\n\t}{\n\t\thostname,\n\t\twebAddress,\n\t\ts.emailContact,\n\t\ts.gaKey,\n\t\ts.userVoiceKey,\n\t\tpurgeTime,\n\t\tmaxUploadSize,\n\t\ttoken(s.randomTokenLength),\n\t\ttoken(s.randomTokenLength),\n\t}\n\n\tw.Header().Set(\"Vary\", \"Accept\")\n\tif acceptsHTML(r.Header) {\n\t\tif err := htmlTemplates.ExecuteTemplate(w, \"index.html\", data); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tif err := textTemplates.ExecuteTemplate(w, \"index.txt\", data); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (s *Server) notFoundHandler(w http.ResponseWriter, _ *http.Request) {\n\thttp.Error(w, http.StatusText(404), 404)\n}\n\nfunc sanitize(fileName string) string {\n\tt := transform.Chain(\n\t\tnorm.NFD,\n\t\trunes.Remove(runes.In(unicode.Cc)),\n\t\trunes.Remove(runes.In(unicode.Cf)),\n\t\trunes.Remove(runes.In(unicode.Co)),\n\t\trunes.Remove(runes.In(unicode.Cs)),\n\t\trunes.Remove(runes.In(unicode.Other)),\n\t\trunes.Remove(runes.In(unicode.Zl)),\n\t\trunes.Remove(runes.In(unicode.Zp)),\n\t\tnorm.NFC)\n\tnewName, _, err := transform.String(t, fileName)\n\tif err != nil {\n\t\treturn path.Base(fileName)\n\t}\n\tif len(newName) == 0 {\n\t\tnewName = \"_\"\n\t}\n\treturn path.Base(newName)\n}\n\nfunc (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {\n\tif err := r.ParseMultipartForm(_24K); nil != err {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, \"Error occurred copying to output stream\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\ttoken := token(s.randomTokenLength)\n\n\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\n\tresponseBody := \"\"\n\n\tfor _, fHeaders := range r.MultipartForm.File {\n\t\tfor _, fHeader := range fHeaders {\n\t\t\tfilename := sanitize(fHeader.Filename)\n\t\t\tcontentType := mime.TypeByExtension(filepath.Ext(fHeader.Filename))\n\n\t\t\tvar f io.Reader\n\t\t\tvar err error\n\n\t\t\tif f, err = fHeader.Open(); err != nil {\n\t\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfile, err := os.CreateTemp(s.tempPath, \"transfer-\")\n\t\t\tdefer s.cleanTmpFile(file)\n\n\t\t\tif err != nil {\n\t\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tn, err := io.Copy(file, f)\n\t\t\tif err != nil {\n\t\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcontentLength := n\n\n\t\t\t_, err = file.Seek(0, io.SeekStart)\n\t\t\tif err != nil {\n\t\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif s.maxUploadSize > 0 && contentLength > s.maxUploadSize {\n\t\t\t\ts.logger.Print(\"Entity too large\")\n\t\t\t\thttp.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif s.performClamavPrescan {\n\t\t\t\tstatus, err := s.performScan(file.Name())\n\t\t\t\tif err != nil {\n\t\t\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\t\t\thttp.Error(w, \"Could not perform prescan\", http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif status != clamavScanStatusOK {\n\t\t\t\t\ts.logger.Printf(\"prescan positive: %s\", status)\n\t\t\t\t\thttp.Error(w, \"Clamav prescan found a virus\", http.StatusPreconditionFailed)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmetadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r)\n\n\t\t\tbuffer := &bytes.Buffer{}\n\t\t\tif err := json.NewEncoder(buffer).Encode(metadata); err != nil {\n\t\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\t\thttp.Error(w, \"Could not encode metadata\", http.StatusInternalServerError)\n\n\t\t\t\treturn\n\t\t\t} else if err := s.storage.Put(r.Context(), token, fmt.Sprintf(\"%s.metadata\", filename), buffer, \"text/json\", uint64(buffer.Len())); err != nil {\n\t\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\t\thttp.Error(w, \"Could not save metadata\", http.StatusInternalServerError)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ts.logger.Printf(\"Uploading %s %s %d %s\", token, filename, contentLength, contentType)\n\n\t\t\treader, err := attachEncryptionReader(file, r.Header.Get(\"X-Encrypt-Password\"))\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, \"Could not crypt file\", http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err = s.storage.Put(r.Context(), token, filename, reader, contentType, uint64(contentLength)); err != nil {\n\t\t\t\ts.logger.Printf(\"Backend storage error: %s\", err.Error())\n\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\n\t\t\t}\n\n\t\t\tfilename = url.PathEscape(filename)\n\t\t\trelativeURL, _ := url.Parse(path.Join(s.proxyPath, token, filename))\n\t\t\tdeleteURL, _ := url.Parse(path.Join(s.proxyPath, token, filename, metadata.DeletionToken))\n\t\t\tw.Header().Add(\"X-Url-Delete\", resolveURL(r, deleteURL, s.proxyPort))\n\t\t\tresponseBody += fmt.Sprintln(getURL(r, s.proxyPort).ResolveReference(relativeURL).String())\n\t\t}\n\t}\n\t_, err := w.Write([]byte(responseBody))\n\tif err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n}\n\nfunc (s *Server) cleanTmpFile(f *os.File) {\n\tif f != nil {\n\t\terr := f.Close()\n\t\tif err != nil {\n\t\t\ts.logger.Printf(\"Error closing tmpfile: %s (%s)\", err, f.Name())\n\t\t}\n\n\t\terr = os.Remove(f.Name())\n\t\tif err != nil {\n\t\t\ts.logger.Printf(\"Error removing tmpfile: %s (%s)\", err, f.Name())\n\t\t}\n\t}\n}\n\ntype metadata struct {\n\t// ContentType is the original uploading content type\n\tContentType string\n\t// ContentLength is is the original uploading content length\n\tContentLength int64\n\t// Downloads is the actual number of downloads\n\tDownloads int\n\t// MaxDownloads contains the maximum numbers of downloads\n\tMaxDownloads int\n\t// MaxDate contains the max age of the file\n\tMaxDate time.Time\n\t// DeletionToken contains the token to match against for deletion\n\tDeletionToken string\n\t// Encrypted contains if the file was encrypted\n\tEncrypted bool\n\t// DecryptedContentType is the original uploading content type\n\tDecryptedContentType string\n}\n\nfunc metadataForRequest(contentType string, contentLength int64, randomTokenLength int, r *http.Request) metadata {\n\tmetadata := metadata{\n\t\tContentType:   strings.ToLower(contentType),\n\t\tContentLength: contentLength,\n\t\tMaxDate:       time.Time{},\n\t\tDownloads:     0,\n\t\tMaxDownloads:  -1,\n\t\tDeletionToken: token(randomTokenLength) + token(randomTokenLength),\n\t}\n\n\tif v := r.Header.Get(\"Max-Downloads\"); v == \"\" {\n\t} else if v, err := strconv.Atoi(v); err != nil {\n\t} else {\n\t\tmetadata.MaxDownloads = v\n\t}\n\n\tif v := r.Header.Get(\"Max-Days\"); v == \"\" {\n\t} else if v, err := strconv.Atoi(v); err != nil {\n\t} else {\n\t\tmetadata.MaxDate = time.Now().Add(time.Hour * 24 * time.Duration(v))\n\t}\n\n\tif password := r.Header.Get(\"X-Encrypt-Password\"); password != \"\" {\n\t\tmetadata.Encrypted = true\n\t\tmetadata.ContentType = \"text/plain; charset=utf-8\"\n\t\tmetadata.DecryptedContentType = contentType\n\t} else {\n\t\tmetadata.Encrypted = false\n\t}\n\n\treturn metadata\n}\n\nfunc (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {\n\tvars := mux.Vars(r)\n\n\tfilename := sanitize(vars[\"filename\"])\n\n\tcontentLength := r.ContentLength\n\n\tdefer storage.CloseCheck(r.Body)\n\n\treader := r.Body\n\n\tif contentLength < 1 || s.performClamavPrescan {\n\t\tfile, err := os.CreateTemp(s.tempPath, \"transfer-\")\n\t\tdefer s.cleanTmpFile(file)\n\t\tif err != nil {\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t// queue file to disk, because s3 needs content length\n\t\t// and clamav prescan scans a file\n\t\tn, err := io.Copy(file, r.Body)\n\t\tif err != nil {\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\n\t\t\treturn\n\t\t}\n\n\t\t_, err = file.Seek(0, io.SeekStart)\n\t\tif err != nil {\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, \"Cannot reset cache file\", http.StatusInternalServerError)\n\n\t\t\treturn\n\t\t}\n\n\t\tcontentLength = n\n\n\t\tif s.performClamavPrescan {\n\t\t\tstatus, err := s.performScan(file.Name())\n\t\t\tif err != nil {\n\t\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\t\thttp.Error(w, \"Could not perform prescan\", http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif status != clamavScanStatusOK {\n\t\t\t\ts.logger.Printf(\"prescan positive: %s\", status)\n\t\t\t\thttp.Error(w, \"Clamav prescan found a virus\", http.StatusPreconditionFailed)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\treader = file\n\t}\n\n\tif s.maxUploadSize > 0 && contentLength > s.maxUploadSize {\n\t\ts.logger.Print(\"Entity too large\")\n\t\thttp.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge)\n\t\treturn\n\t}\n\n\tif contentLength == 0 {\n\t\ts.logger.Print(\"Empty content-length\")\n\t\thttp.Error(w, \"Could not upload empty file\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tcontentType := mime.TypeByExtension(filepath.Ext(vars[\"filename\"]))\n\n\ttoken := token(s.randomTokenLength)\n\n\tmetadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r)\n\n\tbuffer := &bytes.Buffer{}\n\tif err := json.NewEncoder(buffer).Encode(metadata); err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, \"Could not encode metadata\", http.StatusInternalServerError)\n\t\treturn\n\t} else if !metadata.MaxDate.IsZero() && time.Now().After(metadata.MaxDate) {\n\t\ts.logger.Print(\"Invalid MaxDate\")\n\t\thttp.Error(w, \"Invalid MaxDate, make sure Max-Days is smaller than 290 years\", http.StatusBadRequest)\n\t\treturn\n\t} else if err := s.storage.Put(r.Context(), token, fmt.Sprintf(\"%s.metadata\", filename), buffer, \"text/json\", uint64(buffer.Len())); err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, \"Could not save metadata\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\ts.logger.Printf(\"Uploading %s %s %d %s\", token, filename, contentLength, contentType)\n\n\treader, err := attachEncryptionReader(reader, r.Header.Get(\"X-Encrypt-Password\"))\n\tif err != nil {\n\t\thttp.Error(w, \"Could not crypt file\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif err = s.storage.Put(r.Context(), token, filename, reader, contentType, uint64(contentLength)); err != nil {\n\t\ts.logger.Printf(\"Error putting new file: %s\", err.Error())\n\t\thttp.Error(w, \"Could not save file\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// w.Statuscode = 200\n\n\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\n\tfilename = url.PathEscape(filename)\n\trelativeURL, _ := url.Parse(path.Join(s.proxyPath, token, filename))\n\tdeleteURL, _ := url.Parse(path.Join(s.proxyPath, token, filename, metadata.DeletionToken))\n\n\tw.Header().Set(\"X-Url-Delete\", resolveURL(r, deleteURL, s.proxyPort))\n\n\t_, _ = w.Write([]byte(resolveURL(r, relativeURL, s.proxyPort)))\n}\n\nfunc resolveURL(r *http.Request, u *url.URL, proxyPort string) string {\n\tr.URL.Path = \"\"\n\n\treturn getURL(r, proxyPort).ResolveReference(u).String()\n}\n\nfunc resolveKey(key, proxyPath string) string {\n\tkey = strings.TrimPrefix(key, \"/\")\n\n\tkey = strings.TrimPrefix(key, proxyPath)\n\n\tkey = strings.Replace(key, \"\\\\\", \"/\", -1)\n\n\treturn key\n}\n\nfunc resolveWebAddress(r *http.Request, proxyPath string, proxyPort string) string {\n\trUrl := getURL(r, proxyPort)\n\n\tvar webAddress string\n\n\tif len(proxyPath) == 0 {\n\t\twebAddress = fmt.Sprintf(\"%s://%s/\",\n\t\t\trUrl.ResolveReference(rUrl).Scheme,\n\t\t\trUrl.ResolveReference(rUrl).Host)\n\t} else {\n\t\twebAddress = fmt.Sprintf(\"%s://%s/%s\",\n\t\t\trUrl.ResolveReference(rUrl).Scheme,\n\t\t\trUrl.ResolveReference(rUrl).Host,\n\t\t\tstrings.TrimPrefix(proxyPath, \"/\"))\n\t}\n\n\treturn webAddress\n}\n\n// Similar to the logic found here:\n// https://github.com/golang/go/blob/release-branch.go1.14/src/net/http/clone.go#L22-L33\nfunc cloneURL(u *url.URL) *url.URL {\n\tc := &url.URL{}\n\t*c = *u\n\n\tif u.User != nil {\n\t\tc.User = &url.Userinfo{}\n\t\t*c.User = *u.User\n\t}\n\n\treturn c\n}\n\nfunc getURL(r *http.Request, proxyPort string) *url.URL {\n\tu := cloneURL(r.URL)\n\n\tif r.TLS != nil {\n\t\tu.Scheme = \"https\"\n\t} else if proto := r.Header.Get(\"X-Forwarded-Proto\"); proto != \"\" {\n\t\tu.Scheme = proto\n\t} else {\n\t\tu.Scheme = \"http\"\n\t}\n\n\thost, port, err := net.SplitHostPort(r.Host)\n\tif err != nil {\n\t\thost = r.Host\n\t\tport = \"\"\n\t}\n\n\tp := idna.New(idna.ValidateForRegistration())\n\tvar hostFromPunycode string\n\thostFromPunycode, err = p.ToUnicode(host)\n\tif err == nil {\n\t\thost = hostFromPunycode\n\t}\n\n\tif len(proxyPort) != 0 {\n\t\tport = proxyPort\n\t}\n\n\tif len(port) == 0 {\n\t\tu.Host = host\n\t} else {\n\t\tif port == \"80\" && u.Scheme == \"http\" {\n\t\t\tu.Host = host\n\t\t} else if port == \"443\" && u.Scheme == \"https\" {\n\t\t\tu.Host = host\n\t\t} else {\n\t\t\tu.Host = net.JoinHostPort(host, port)\n\t\t}\n\t}\n\n\treturn u\n}\n\nfunc (metadata metadata) remainingLimitHeaderValues() (remainingDownloads, remainingDays string) {\n\tif metadata.MaxDate.IsZero() {\n\t\tremainingDays = \"n/a\"\n\t} else {\n\t\ttimeDifference := time.Until(metadata.MaxDate)\n\t\tremainingDays = strconv.Itoa(int(timeDifference.Hours()/24) + 1)\n\t}\n\n\tif metadata.MaxDownloads == -1 {\n\t\tremainingDownloads = \"n/a\"\n\t} else {\n\t\tremainingDownloads = strconv.Itoa(metadata.MaxDownloads - metadata.Downloads)\n\t}\n\n\treturn remainingDownloads, remainingDays\n}\n\nfunc (s *Server) lock(token, filename string) {\n\tkey := path.Join(token, filename)\n\n\tlock, _ := s.locks.LoadOrStore(key, &sync.Mutex{})\n\n\tlock.(*sync.Mutex).Lock()\n}\n\nfunc (s *Server) unlock(token, filename string) {\n\tkey := path.Join(token, filename)\n\n\tlock, _ := s.locks.LoadOrStore(key, &sync.Mutex{})\n\n\tlock.(*sync.Mutex).Unlock()\n}\n\nfunc (s *Server) checkMetadata(ctx context.Context, token, filename string, increaseDownload bool) (metadata, error) {\n\ts.lock(token, filename)\n\tdefer s.unlock(token, filename)\n\n\tvar metadata metadata\n\n\tr, _, err := s.storage.Get(ctx, token, fmt.Sprintf(\"%s.metadata\", filename), nil)\n\tdefer storage.CloseCheck(r)\n\n\tif err != nil {\n\t\treturn metadata, err\n\t}\n\n\tif err := json.NewDecoder(r).Decode(&metadata); err != nil {\n\t\treturn metadata, err\n\t} else if metadata.MaxDownloads != -1 && metadata.Downloads >= metadata.MaxDownloads {\n\t\treturn metadata, errors.New(\"maxDownloads expired\")\n\t} else if !metadata.MaxDate.IsZero() && time.Now().After(metadata.MaxDate) {\n\t\treturn metadata, errors.New(\"maxDate expired\")\n\t} else if metadata.MaxDownloads != -1 && increaseDownload {\n\t\t// todo(nl5887): mutex?\n\n\t\t// update number of downloads\n\t\tmetadata.Downloads++\n\n\t\tbuffer := &bytes.Buffer{}\n\t\tif err := json.NewEncoder(buffer).Encode(metadata); err != nil {\n\t\t\treturn metadata, errors.New(\"could not encode metadata\")\n\t\t} else if err := s.storage.Put(ctx, token, fmt.Sprintf(\"%s.metadata\", filename), buffer, \"text/json\", uint64(buffer.Len())); err != nil {\n\t\t\treturn metadata, errors.New(\"could not save metadata\")\n\t\t}\n\t}\n\n\treturn metadata, nil\n}\n\nfunc (s *Server) checkDeletionToken(ctx context.Context, deletionToken, token, filename string) error {\n\ts.lock(token, filename)\n\tdefer s.unlock(token, filename)\n\n\tvar metadata metadata\n\n\tr, _, err := s.storage.Get(ctx, token, fmt.Sprintf(\"%s.metadata\", filename), nil)\n\tdefer storage.CloseCheck(r)\n\n\tif s.storage.IsNotExist(err) {\n\t\treturn errors.New(\"metadata doesn't exist\")\n\t} else if err != nil {\n\t\treturn err\n\t}\n\n\tif err := json.NewDecoder(r).Decode(&metadata); err != nil {\n\t\treturn err\n\t} else if metadata.DeletionToken != deletionToken {\n\t\treturn errors.New(\"deletion token doesn't match\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *Server) purgeHandler() {\n\tticker := time.NewTicker(s.purgeInterval)\n\tgo func() {\n\t\tfor {\n\t\t\t<-ticker.C\n\t\t\terr := s.storage.Purge(context.TODO(), s.purgeDays)\n\t\t\tif err != nil {\n\t\t\t\ts.logger.Printf(\"error cleaning up expired files: %v\", err)\n\t\t\t}\n\t\t}\n\t}()\n}\n\nfunc (s *Server) deleteHandler(w http.ResponseWriter, r *http.Request) {\n\tvars := mux.Vars(r)\n\n\ttoken := vars[\"token\"]\n\tfilename := vars[\"filename\"]\n\tdeletionToken := vars[\"deletionToken\"]\n\n\tif err := s.checkDeletionToken(r.Context(), deletionToken, token, filename); err != nil {\n\t\ts.logger.Printf(\"Error metadata: %s\", err.Error())\n\t\thttp.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)\n\t\treturn\n\t}\n\n\terr := s.storage.Delete(r.Context(), token, filename)\n\tif s.storage.IsNotExist(err) {\n\t\thttp.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)\n\t\treturn\n\t} else if err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, \"Could not delete file.\", http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc (s *Server) zipHandler(w http.ResponseWriter, r *http.Request) {\n\tvars := mux.Vars(r)\n\n\tfiles := vars[\"files\"]\n\n\tzipfilename := fmt.Sprintf(\"transfersh-%d.zip\", uint16(time.Now().UnixNano()))\n\n\tw.Header().Set(\"Content-Type\", \"application/zip\")\n\tcommonHeader(w, zipfilename)\n\n\tzw := zip.NewWriter(w)\n\n\tfor _, key := range strings.Split(files, \",\") {\n\t\tkey = resolveKey(key, s.proxyPath)\n\n\t\ttoken := strings.Split(key, \"/\")[0]\n\t\tfilename := sanitize(strings.Split(key, \"/\")[1])\n\n\t\tif _, err := s.checkMetadata(r.Context(), token, filename, true); err != nil {\n\t\t\ts.logger.Printf(\"Error metadata: %s\", err.Error())\n\t\t\tcontinue\n\t\t}\n\n\t\treader, _, err := s.storage.Get(r.Context(), token, filename, nil)\n\t\tdefer storage.CloseCheck(reader)\n\n\t\tif err != nil {\n\t\t\tif s.storage.IsNotExist(err) {\n\t\t\t\thttp.Error(w, \"File not found\", 404)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, \"Could not retrieve file.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\theader := &zip.FileHeader{\n\t\t\tName:   strings.Split(key, \"/\")[1],\n\t\t\tMethod: zip.Store,\n\n\t\t\tModified: time.Now().UTC(),\n\t\t}\n\n\t\tfw, err := zw.CreateHeader(header)\n\n\t\tif err != nil {\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, \"Internal server error.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif _, err = io.Copy(fw, reader); err != nil {\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, \"Internal server error.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif err := zw.Close(); err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, \"Internal server error.\", http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc (s *Server) tarGzHandler(w http.ResponseWriter, r *http.Request) {\n\tvars := mux.Vars(r)\n\n\tfiles := vars[\"files\"]\n\n\ttarfilename := fmt.Sprintf(\"transfersh-%d.tar.gz\", uint16(time.Now().UnixNano()))\n\n\tw.Header().Set(\"Content-Type\", \"application/x-gzip\")\n\tcommonHeader(w, tarfilename)\n\n\tgw := gzip.NewWriter(w)\n\tdefer storage.CloseCheck(gw)\n\n\tzw := tar.NewWriter(gw)\n\tdefer storage.CloseCheck(zw)\n\n\tfor _, key := range strings.Split(files, \",\") {\n\t\tkey = resolveKey(key, s.proxyPath)\n\n\t\ttoken := strings.Split(key, \"/\")[0]\n\t\tfilename := sanitize(strings.Split(key, \"/\")[1])\n\n\t\tif _, err := s.checkMetadata(r.Context(), token, filename, true); err != nil {\n\t\t\ts.logger.Printf(\"Error metadata: %s\", err.Error())\n\t\t\tcontinue\n\t\t}\n\n\t\treader, contentLength, err := s.storage.Get(r.Context(), token, filename, nil)\n\t\tdefer storage.CloseCheck(reader)\n\n\t\tif err != nil {\n\t\t\tif s.storage.IsNotExist(err) {\n\t\t\t\thttp.Error(w, \"File not found\", 404)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, \"Could not retrieve file.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\theader := &tar.Header{\n\t\t\tName: strings.Split(key, \"/\")[1],\n\t\t\tSize: int64(contentLength),\n\t\t}\n\n\t\terr = zw.WriteHeader(header)\n\t\tif err != nil {\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, \"Internal server error.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif _, err = io.Copy(zw, reader); err != nil {\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, \"Internal server error.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (s *Server) tarHandler(w http.ResponseWriter, r *http.Request) {\n\tvars := mux.Vars(r)\n\n\tfiles := vars[\"files\"]\n\n\ttarfilename := fmt.Sprintf(\"transfersh-%d.tar\", uint16(time.Now().UnixNano()))\n\n\tw.Header().Set(\"Content-Type\", \"application/x-tar\")\n\tcommonHeader(w, tarfilename)\n\n\tzw := tar.NewWriter(w)\n\tdefer storage.CloseCheck(zw)\n\n\tfor _, key := range strings.Split(files, \",\") {\n\t\tkey = resolveKey(key, s.proxyPath)\n\n\t\ttoken := strings.Split(key, \"/\")[0]\n\t\tfilename := strings.Split(key, \"/\")[1]\n\n\t\tif _, err := s.checkMetadata(r.Context(), token, filename, true); err != nil {\n\t\t\ts.logger.Printf(\"Error metadata: %s\", err.Error())\n\t\t\tcontinue\n\t\t}\n\n\t\treader, contentLength, err := s.storage.Get(r.Context(), token, filename, nil)\n\t\tdefer storage.CloseCheck(reader)\n\n\t\tif err != nil {\n\t\t\tif s.storage.IsNotExist(err) {\n\t\t\t\thttp.Error(w, \"File not found\", 404)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, \"Could not retrieve file.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\theader := &tar.Header{\n\t\t\tName: strings.Split(key, \"/\")[1],\n\t\t\tSize: int64(contentLength),\n\t\t}\n\n\t\terr = zw.WriteHeader(header)\n\t\tif err != nil {\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, \"Internal server error.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif _, err = io.Copy(zw, reader); err != nil {\n\t\t\ts.logger.Printf(\"%s\", err.Error())\n\t\t\thttp.Error(w, \"Internal server error.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (s *Server) headHandler(w http.ResponseWriter, r *http.Request) {\n\tvars := mux.Vars(r)\n\n\ttoken := vars[\"token\"]\n\tfilename := vars[\"filename\"]\n\n\tmetadata, err := s.checkMetadata(r.Context(), token, filename, false)\n\n\tif err != nil {\n\t\ts.logger.Printf(\"Error metadata: %s\", err.Error())\n\t\thttp.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)\n\t\treturn\n\t}\n\n\tcontentType := metadata.ContentType\n\tcontentLength, err := s.storage.Head(r.Context(), token, filename)\n\tif s.storage.IsNotExist(err) {\n\t\thttp.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)\n\t\treturn\n\t} else if err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, \"Could not retrieve file.\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tremainingDownloads, remainingDays := metadata.remainingLimitHeaderValues()\n\n\tw.Header().Set(\"Content-Type\", contentType)\n\tw.Header().Set(\"Content-Length\", strconv.FormatUint(contentLength, 10))\n\tw.Header().Set(\"Connection\", \"close\")\n\tw.Header().Set(\"X-Remaining-Downloads\", remainingDownloads)\n\tw.Header().Set(\"X-Remaining-Days\", remainingDays)\n\tw.Header().Set(\"Vary\", \"Range, Referer, X-Decrypt-Password\")\n\n\tif s.storage.IsRangeSupported() {\n\t\tw.Header().Set(\"Accept-Ranges\", \"bytes\")\n\t}\n}\n\nfunc (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {\n\tvars := mux.Vars(r)\n\n\taction := vars[\"action\"]\n\ttoken := vars[\"token\"]\n\tfilename := vars[\"filename\"]\n\n\tmetadata, err := s.checkMetadata(r.Context(), token, filename, true)\n\n\tif err != nil {\n\t\ts.logger.Printf(\"Error metadata: %s\", err.Error())\n\t\thttp.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)\n\t\treturn\n\t}\n\n\tvar rng *storage.Range\n\tif r.Header.Get(\"Range\") != \"\" {\n\t\trng = storage.ParseRange(r.Header.Get(\"Range\"))\n\t}\n\n\tcontentType := metadata.ContentType\n\treader, contentLength, err := s.storage.Get(r.Context(), token, filename, rng)\n\tdefer storage.CloseCheck(reader)\n\n\tif s.storage.IsNotExist(err) {\n\t\thttp.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)\n\t\treturn\n\t} else if err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, \"Could not retrieve file.\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tif rng != nil {\n\t\tcr := rng.ContentRange()\n\t\tif cr != \"\" {\n\t\t\tw.Header().Set(\"Accept-Ranges\", \"bytes\")\n\t\t\tw.Header().Set(\"Content-Range\", cr)\n\t\t\tif rng.Limit > 0 {\n\t\t\t\treader = io.NopCloser(io.LimitReader(reader, int64(rng.Limit)))\n\t\t\t}\n\t\t}\n\t}\n\n\tvar disposition string\n\tif action == \"inline\" {\n\t\tdisposition = \"inline\"\n\t\t/*\n\t\t\tmetadata.ContentType is unable to determine the type of the content,\n\t\t\tSo add text/plain in this case to fix XSS related issues/\n\t\t*/\n\t\tif strings.TrimSpace(contentType) == \"\" {\n\t\t\tcontentType = \"text/plain; charset=utf-8\"\n\t\t}\n\t} else {\n\t\tdisposition = \"attachment\"\n\t}\n\n\tremainingDownloads, remainingDays := metadata.remainingLimitHeaderValues()\n\n\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(`%s; filename=\"%s\"`, disposition, filename))\n\tw.Header().Set(\"Connection\", \"keep-alive\")\n\tw.Header().Set(\"Cache-Control\", \"no-store\")\n\tw.Header().Set(\"X-Remaining-Downloads\", remainingDownloads)\n\tw.Header().Set(\"X-Remaining-Days\", remainingDays)\n\n\tpassword := r.Header.Get(\"X-Decrypt-Password\")\n\treader, err = attachDecryptionReader(reader, password)\n\tif err != nil {\n\t\thttp.Error(w, \"Could not decrypt file\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif metadata.Encrypted && len(password) > 0 {\n\t\tcontentType = metadata.DecryptedContentType\n\t\tcontentLength = uint64(metadata.ContentLength)\n\t}\n\n\tw.Header().Set(\"Content-Type\", contentType)\n\tw.Header().Set(\"Content-Length\", strconv.FormatUint(contentLength, 10))\n\tw.Header().Set(\"Vary\", \"Range, Referer, X-Decrypt-Password\")\n\n\tif rng != nil && rng.ContentRange() != \"\" {\n\t\tw.WriteHeader(http.StatusPartialContent)\n\t}\n\n\tif disposition == \"inline\" && canContainsXSS(contentType) {\n\t\treader = io.NopCloser(bluemonday.UGCPolicy().SanitizeReader(reader))\n\t}\n\n\tif _, err = io.Copy(w, reader); err != nil {\n\t\ts.logger.Printf(\"%s\", err.Error())\n\t\thttp.Error(w, \"Error occurred copying to output stream\", http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc commonHeader(w http.ResponseWriter, filename string) {\n\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=\\\"%s\\\"\", filename))\n\tw.Header().Set(\"Connection\", \"close\")\n\tw.Header().Set(\"Cache-Control\", \"no-store\")\n}\n\n// RedirectHandler handles redirect\nfunc (s *Server) RedirectHandler(h http.Handler) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tif !s.forceHTTPS {\n\t\t\t// we don't want to enforce https\n\t\t} else if r.URL.Path == \"/health.html\" {\n\t\t\t// health check url won't redirect\n\t\t} else if strings.HasSuffix(ipAddrFromRemoteAddr(r.Host), \".onion\") {\n\t\t\t// .onion addresses cannot get a valid certificate, so don't redirect\n\t\t} else if r.Header.Get(\"X-Forwarded-Proto\") == \"https\" {\n\t\t} else if r.TLS != nil {\n\t\t} else {\n\t\t\tu := getURL(r, s.proxyPort)\n\t\t\tu.Scheme = \"https\"\n\t\t\tif len(s.proxyPort) == 0 && len(s.TLSListenerString) > 0 {\n\t\t\t\t_, port, err := net.SplitHostPort(s.TLSListenerString)\n\t\t\t\tif err != nil || port == \"443\" {\n\t\t\t\t\tport = \"\"\n\t\t\t\t}\n\n\t\t\t\tif len(port) > 0 {\n\t\t\t\t\tu.Host = net.JoinHostPort(u.Hostname(), port)\n\t\t\t\t} else {\n\t\t\t\t\tu.Host = u.Hostname()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\thttp.Redirect(w, r, u.String(), http.StatusPermanentRedirect)\n\t\t\treturn\n\t\t}\n\n\t\th.ServeHTTP(w, r)\n\t}\n}\n\n// LoveHandler Create a log handler for every request it receives.\nfunc LoveHandler(h http.Handler) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"x-made-with\", \"<3 by DutchCoders\")\n\t\tw.Header().Set(\"x-served-by\", \"Proudly served by DutchCoders\")\n\t\tw.Header().Set(\"server\", \"Transfer.sh HTTP Server\")\n\t\th.ServeHTTP(w, r)\n\t}\n}\n\nfunc ipFilterHandler(h http.Handler, ipFilterOptions *IPFilterOptions) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tif ipFilterOptions == nil {\n\t\t\th.ServeHTTP(w, r)\n\t\t} else {\n\t\t\tWrapIPFilter(h, ipFilterOptions).ServeHTTP(w, r)\n\t\t}\n\t}\n}\n\nfunc (s *Server) basicAuthHandler(h http.Handler) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tif s.authUser == \"\" && s.authPass == \"\" && s.authHtpasswd == \"\" {\n\t\t\th.ServeHTTP(w, r)\n\t\t\treturn\n\t\t}\n\n\t\tif s.htpasswdFile == nil && s.authHtpasswd != \"\" {\n\t\t\thtpasswdFile, err := htpasswd.New(s.authHtpasswd, htpasswd.DefaultSystems, nil)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ts.htpasswdFile = htpasswdFile\n\t\t}\n\n\t\tif s.authIPFilter == nil && s.authIPFilterOptions != nil {\n\t\t\ts.authIPFilter = newIPFilter(s.authIPFilterOptions)\n\t\t}\n\n\t\tw.Header().Set(\"WWW-Authenticate\", \"Basic realm=\\\"Restricted\\\"\")\n\n\t\tvar authorized bool\n\t\tif s.authIPFilter != nil {\n\t\t\tremoteIP := realip.FromRequest(r)\n\t\t\tauthorized = s.authIPFilter.Allowed(remoteIP)\n\t\t}\n\n\t\tusername, password, authOK := r.BasicAuth()\n\t\tif !authOK && !authorized {\n\t\t\thttp.Error(w, \"Not authorized\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\n\t\tif !authorized && username == s.authUser && password == s.authPass {\n\t\t\tauthorized = true\n\t\t}\n\n\t\tif !authorized && s.htpasswdFile != nil {\n\t\t\tauthorized = s.htpasswdFile.Match(username, password)\n\t\t}\n\n\t\tif !authorized {\n\t\t\thttp.Error(w, \"Not authorized\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\n\t\th.ServeHTTP(w, r)\n\t}\n}\n"
  },
  {
    "path": "server/handlers_test.go",
    "content": "package server\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t. \"gopkg.in/check.v1\"\n)\n\n// Hook up gocheck into the \"go test\" runner.\nfunc Test(t *testing.T) { TestingT(t) }\n\nvar (\n\t_ = Suite(&suiteRedirectWithForceHTTPS{})\n\t_ = Suite(&suiteRedirectWithoutForceHTTPS{})\n)\n\ntype suiteRedirectWithForceHTTPS struct {\n\thandler http.HandlerFunc\n}\n\nfunc (s *suiteRedirectWithForceHTTPS) SetUpTest(c *C) {\n\tsrvr, err := New(ForceHTTPS())\n\tc.Assert(err, IsNil)\n\n\thandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t_, _ = fmt.Fprintln(w, \"Hello, client\")\n\t})\n\n\ts.handler = srvr.RedirectHandler(handler)\n}\n\nfunc (s *suiteRedirectWithForceHTTPS) TestHTTPs(c *C) {\n\treq := httptest.NewRequest(\"GET\", \"https://test/test\", nil)\n\n\tw := httptest.NewRecorder()\n\ts.handler(w, req)\n\n\tresp := w.Result()\n\tc.Assert(resp.StatusCode, Equals, http.StatusOK)\n}\n\nfunc (s *suiteRedirectWithForceHTTPS) TestOnion(c *C) {\n\treq := httptest.NewRequest(\"GET\", \"http://test.onion/test\", nil)\n\n\tw := httptest.NewRecorder()\n\ts.handler(w, req)\n\n\tresp := w.Result()\n\tc.Assert(resp.StatusCode, Equals, http.StatusOK)\n}\n\nfunc (s *suiteRedirectWithForceHTTPS) TestXForwardedFor(c *C) {\n\treq := httptest.NewRequest(\"GET\", \"http://127.0.0.1/test\", nil)\n\treq.Header.Set(\"X-Forwarded-Proto\", \"https\")\n\n\tw := httptest.NewRecorder()\n\ts.handler(w, req)\n\n\tresp := w.Result()\n\tc.Assert(resp.StatusCode, Equals, http.StatusOK)\n}\n\nfunc (s *suiteRedirectWithForceHTTPS) TestHTTP(c *C) {\n\treq := httptest.NewRequest(\"GET\", \"http://127.0.0.1/test\", nil)\n\n\tw := httptest.NewRecorder()\n\ts.handler(w, req)\n\n\tresp := w.Result()\n\tc.Assert(resp.StatusCode, Equals, http.StatusPermanentRedirect)\n\tc.Assert(resp.Header.Get(\"Location\"), Equals, \"https://127.0.0.1/test\")\n}\n\ntype suiteRedirectWithoutForceHTTPS struct {\n\thandler http.HandlerFunc\n}\n\nfunc (s *suiteRedirectWithoutForceHTTPS) SetUpTest(c *C) {\n\tsrvr, err := New()\n\tc.Assert(err, IsNil)\n\n\thandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t_, _ = fmt.Fprintln(w, \"Hello, client\")\n\t})\n\n\ts.handler = srvr.RedirectHandler(handler)\n}\n\nfunc (s *suiteRedirectWithoutForceHTTPS) TestHTTP(c *C) {\n\treq := httptest.NewRequest(\"GET\", \"http://127.0.0.1/test\", nil)\n\n\tw := httptest.NewRecorder()\n\ts.handler(w, req)\n\n\tresp := w.Result()\n\tc.Assert(resp.StatusCode, Equals, http.StatusOK)\n}\n\nfunc (s *suiteRedirectWithoutForceHTTPS) TestHTTPs(c *C) {\n\treq := httptest.NewRequest(\"GET\", \"https://127.0.0.1/test\", nil)\n\n\tw := httptest.NewRecorder()\n\ts.handler(w, req)\n\n\tresp := w.Result()\n\tc.Assert(resp.StatusCode, Equals, http.StatusOK)\n}\n"
  },
  {
    "path": "server/ip_filter.go",
    "content": "/*\nMIT License\nCopyright © 2016 <dev@jpillora.com>\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n*/\n\npackage server\n\nimport (\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/tomasen/realip\"\n)\n\n// IPFilterOptions for ipFilter. Allowed takes precedence over Blocked.\n// IPs can be IPv4 or IPv6 and can optionally contain subnet\n// masks (/24). Note however, determining if a given IP is\n// included in a subnet requires a linear scan so is less performant\n// than looking up single IPs.\n//\n// This could be improved with some algorithmic magic.\ntype IPFilterOptions struct {\n\t//explicity allowed IPs\n\tAllowedIPs []string\n\t//explicity blocked IPs\n\tBlockedIPs []string\n\t//block by default (defaults to allow)\n\tBlockByDefault bool\n\t// TrustProxy enable check request IP from proxy\n\tTrustProxy bool\n\n\tLogger interface {\n\t\tPrintf(format string, v ...interface{})\n\t}\n}\n\n// ipFilter\ntype ipFilter struct {\n\t//mut protects the below\n\t//rw since writes are rare\n\tmut            sync.RWMutex\n\tdefaultAllowed bool\n\tips            map[string]bool\n\tsubnets        []*subnet\n}\n\ntype subnet struct {\n\tstr     string\n\tipnet   *net.IPNet\n\tallowed bool\n}\n\nfunc newIPFilter(opts *IPFilterOptions) *ipFilter {\n\tif opts.Logger == nil {\n\t\tflags := log.LstdFlags\n\t\topts.Logger = log.New(os.Stdout, \"\", flags)\n\t}\n\tf := &ipFilter{\n\t\tips:            map[string]bool{},\n\t\tdefaultAllowed: !opts.BlockByDefault,\n\t}\n\tfor _, ip := range opts.BlockedIPs {\n\t\tf.BlockIP(ip)\n\t}\n\tfor _, ip := range opts.AllowedIPs {\n\t\tf.AllowIP(ip)\n\t}\n\treturn f\n}\n\nfunc (f *ipFilter) AllowIP(ip string) bool {\n\treturn f.ToggleIP(ip, true)\n}\n\nfunc (f *ipFilter) BlockIP(ip string) bool {\n\treturn f.ToggleIP(ip, false)\n}\n\nfunc (f *ipFilter) ToggleIP(str string, allowed bool) bool {\n\t//check if provided string describes a subnet\n\tif ip, network, err := net.ParseCIDR(str); err == nil {\n\t\t// containing only one ip?\n\t\tif n, total := network.Mask.Size(); n == total {\n\t\t\tf.mut.Lock()\n\t\t\tf.ips[ip.String()] = allowed\n\t\t\tf.mut.Unlock()\n\t\t\treturn true\n\t\t}\n\t\t//check for existing\n\t\tf.mut.Lock()\n\t\tfound := false\n\t\tfor _, subnet := range f.subnets {\n\t\t\tif subnet.str == str {\n\t\t\t\tfound = true\n\t\t\t\tsubnet.allowed = allowed\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tf.subnets = append(f.subnets, &subnet{\n\t\t\t\tstr:     str,\n\t\t\t\tipnet:   network,\n\t\t\t\tallowed: allowed,\n\t\t\t})\n\t\t}\n\t\tf.mut.Unlock()\n\t\treturn true\n\t}\n\t//check if plain ip\n\tif ip := net.ParseIP(str); ip != nil {\n\t\tf.mut.Lock()\n\t\tf.ips[ip.String()] = allowed\n\t\tf.mut.Unlock()\n\t\treturn true\n\t}\n\treturn false\n}\n\n// ToggleDefault alters the default setting\nfunc (f *ipFilter) ToggleDefault(allowed bool) {\n\tf.mut.Lock()\n\tf.defaultAllowed = allowed\n\tf.mut.Unlock()\n}\n\n// Allowed returns if a given IP can pass through the filter\nfunc (f *ipFilter) Allowed(ipstr string) bool {\n\treturn f.NetAllowed(net.ParseIP(ipstr))\n}\n\n// NetAllowed returns if a given net.IP can pass through the filter\nfunc (f *ipFilter) NetAllowed(ip net.IP) bool {\n\t//invalid ip\n\tif ip == nil {\n\t\treturn false\n\t}\n\t//read lock entire function\n\t//except for db access\n\tf.mut.RLock()\n\tdefer f.mut.RUnlock()\n\t//check single ips\n\tallowed, ok := f.ips[ip.String()]\n\tif ok {\n\t\treturn allowed\n\t}\n\t//scan subnets for any allow/block\n\tblocked := false\n\tfor _, subnet := range f.subnets {\n\t\tif subnet.ipnet.Contains(ip) {\n\t\t\tif subnet.allowed {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tblocked = true\n\t\t}\n\t}\n\tif blocked {\n\t\treturn false\n\t}\n\n\t//use default setting\n\treturn f.defaultAllowed\n}\n\n// Blocked returns if a given IP can NOT pass through the filter\nfunc (f *ipFilter) Blocked(ip string) bool {\n\treturn !f.Allowed(ip)\n}\n\n// NetBlocked returns if a given net.IP can NOT pass through the filter\nfunc (f *ipFilter) NetBlocked(ip net.IP) bool {\n\treturn !f.NetAllowed(ip)\n}\n\n// Wrap the provided handler with simple IP blocking middleware\n// using this IP filter and its configuration\nfunc (f *ipFilter) Wrap(next http.Handler) http.Handler {\n\treturn &ipFilterMiddleware{ipFilter: f, next: next}\n}\n\n// WrapIPFilter is equivalent to newIPFilter(opts) then Wrap(next)\nfunc WrapIPFilter(next http.Handler, opts *IPFilterOptions) http.Handler {\n\treturn newIPFilter(opts).Wrap(next)\n}\n\ntype ipFilterMiddleware struct {\n\t*ipFilter\n\tnext http.Handler\n}\n\nfunc (m *ipFilterMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tremoteIP := realip.FromRequest(r)\n\n\tif !m.ipFilter.Allowed(remoteIP) {\n\t\t//show simple forbidden text\n\t\thttp.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)\n\t\treturn\n\t}\n\n\t//success!\n\tm.next.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "server/server.go",
    "content": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n*/\n\npackage server\n\nimport (\n\t\"context\"\n\tcryptoRand \"crypto/rand\"\n\t\"crypto/tls\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"log\"\n\t\"math/rand\"\n\t\"mime\"\n\t\"net/http\"\n\t_ \"net/http/pprof\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/PuerkitoBio/ghost/handlers\"\n\t\"github.com/VojtechVitek/ratelimit\"\n\t\"github.com/VojtechVitek/ratelimit/memory\"\n\tgorillaHandlers \"github.com/gorilla/handlers\"\n\t\"github.com/gorilla/mux\"\n\t\"github.com/tg123/go-htpasswd\"\n\t\"golang.org/x/crypto/acme/autocert\"\n\n\tweb \"github.com/dutchcoders/transfer.sh-web\"\n\t\"github.com/dutchcoders/transfer.sh/server/storage\"\n\tassetfs \"github.com/elazarl/go-bindata-assetfs\"\n)\n\n// parse request with maximum memory of _24Kilobits\nconst _24K = (1 << 3) * 24\n\n// parse request with maximum memory of _5Megabytes\nconst _5M = (1 << 20) * 5\n\n// OptionFn is the option function type\ntype OptionFn func(*Server)\n\n// ClamavHost sets clamav host\nfunc ClamavHost(s string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.ClamAVDaemonHost = s\n\t}\n}\n\n// PerformClamavPrescan enables clamav prescan on upload\nfunc PerformClamavPrescan(b bool) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.performClamavPrescan = b\n\t}\n}\n\n// VirustotalKey sets virus total key\nfunc VirustotalKey(s string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.VirusTotalKey = s\n\t}\n}\n\n// Listener set listener\nfunc Listener(s string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.ListenerString = s\n\t}\n\n}\n\n// CorsDomains sets CORS domains\nfunc CorsDomains(s string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.CorsDomains = s\n\t}\n\n}\n\n// EmailContact sets email contact\nfunc EmailContact(emailContact string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.emailContact = emailContact\n\t}\n}\n\n// GoogleAnalytics sets GA key\nfunc GoogleAnalytics(gaKey string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.gaKey = gaKey\n\t}\n}\n\n// UserVoice sets UV key\nfunc UserVoice(userVoiceKey string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.userVoiceKey = userVoiceKey\n\t}\n}\n\n// TLSListener sets TLS listener and option\nfunc TLSListener(s string, t bool) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.TLSListenerString = s\n\t\tsrvr.TLSListenerOnly = t\n\t}\n\n}\n\n// ProfileListener sets profile listener\nfunc ProfileListener(s string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.ProfileListenerString = s\n\t}\n}\n\n// WebPath sets web path\nfunc WebPath(s string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tif s[len(s)-1:] != \"/\" {\n\t\t\ts = filepath.Join(s, \"\")\n\t\t}\n\n\t\tsrvr.webPath = s\n\t}\n}\n\n// ProxyPath sets proxy path\nfunc ProxyPath(s string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tif s[len(s)-1:] != \"/\" {\n\t\t\ts = filepath.Join(s, \"\")\n\t\t}\n\n\t\tsrvr.proxyPath = s\n\t}\n}\n\n// ProxyPort sets proxy port\nfunc ProxyPort(s string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.proxyPort = s\n\t}\n}\n\n// TempPath sets temp path\nfunc TempPath(s string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tif s[len(s)-1:] != \"/\" {\n\t\t\ts = filepath.Join(s, \"\")\n\t\t}\n\n\t\tsrvr.tempPath = s\n\t}\n}\n\n// LogFile sets log file\nfunc LogFile(logger *log.Logger, s string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tf, err := os.OpenFile(s, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)\n\t\tif err != nil {\n\t\t\tlogger.Fatalf(\"error opening file: %v\", err)\n\t\t}\n\n\t\tlogger.SetOutput(f)\n\t\tsrvr.logger = logger\n\t}\n}\n\n// Logger sets logger\nfunc Logger(logger *log.Logger) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.logger = logger\n\t}\n}\n\n// MaxUploadSize sets max upload size\nfunc MaxUploadSize(kbytes int64) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.maxUploadSize = kbytes * 1024\n\t}\n\n}\n\n// RateLimit set rate limit\nfunc RateLimit(requests int) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.rateLimitRequests = requests\n\t}\n}\n\n// RandomTokenLength sets random token length\nfunc RandomTokenLength(length int) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.randomTokenLength = length\n\t}\n}\n\n// Purge sets purge days and option\nfunc Purge(days, interval int) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.purgeDays = time.Duration(days) * time.Hour * 24\n\t\tsrvr.purgeInterval = time.Duration(interval) * time.Hour\n\t}\n}\n\n// ForceHTTPS sets forcing https\nfunc ForceHTTPS() OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.forceHTTPS = true\n\t}\n}\n\n// EnableProfiler sets enable profiler\nfunc EnableProfiler() OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.profilerEnabled = true\n\t}\n}\n\n// UseStorage set storage to use\nfunc UseStorage(s storage.Storage) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.storage = s\n\t}\n}\n\n// UseLetsEncrypt set letsencrypt usage\nfunc UseLetsEncrypt(hosts []string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tcacheDir := \"./cache/\"\n\n\t\tm := autocert.Manager{\n\t\t\tPrompt: autocert.AcceptTOS,\n\t\t\tCache:  autocert.DirCache(cacheDir),\n\t\t\tHostPolicy: func(_ context.Context, host string) error {\n\t\t\t\tfound := false\n\n\t\t\t\tfor _, h := range hosts {\n\t\t\t\t\tfound = found || strings.HasSuffix(host, h)\n\t\t\t\t}\n\n\t\t\t\tif !found {\n\t\t\t\t\treturn errors.New(\"acme/autocert: host not configured\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}\n\n\t\tsrvr.tlsConfig = m.TLSConfig()\n\t\tsrvr.tlsConfig.GetCertificate = m.GetCertificate\n\t}\n}\n\n// TLSConfig sets TLS config\nfunc TLSConfig(cert, pk string) OptionFn {\n\tcertificate, err := tls.LoadX509KeyPair(cert, pk)\n\treturn func(srvr *Server) {\n\t\tsrvr.tlsConfig = &tls.Config{\n\t\t\tGetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {\n\t\t\t\treturn &certificate, err\n\t\t\t},\n\t\t}\n\t}\n}\n\n// HTTPAuthCredentials sets basic http auth credentials\nfunc HTTPAuthCredentials(user string, pass string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.authUser = user\n\t\tsrvr.authPass = pass\n\t}\n}\n\n// HTTPAuthHtpasswd sets basic http auth htpasswd file\nfunc HTTPAuthHtpasswd(htpasswdPath string) OptionFn {\n\treturn func(srvr *Server) {\n\t\tsrvr.authHtpasswd = htpasswdPath\n\t}\n}\n\n// HTTPAUTHFilterOptions sets basic http auth ips whitelist\nfunc HTTPAUTHFilterOptions(options IPFilterOptions) OptionFn {\n\tfor i, allowedIP := range options.AllowedIPs {\n\t\toptions.AllowedIPs[i] = strings.TrimSpace(allowedIP)\n\t}\n\n\treturn func(srvr *Server) {\n\t\tsrvr.authIPFilterOptions = &options\n\t}\n}\n\n// FilterOptions sets ip filtering\nfunc FilterOptions(options IPFilterOptions) OptionFn {\n\tfor i, allowedIP := range options.AllowedIPs {\n\t\toptions.AllowedIPs[i] = strings.TrimSpace(allowedIP)\n\t}\n\n\tfor i, blockedIP := range options.BlockedIPs {\n\t\toptions.BlockedIPs[i] = strings.TrimSpace(blockedIP)\n\t}\n\n\treturn func(srvr *Server) {\n\t\tsrvr.ipFilterOptions = &options\n\t}\n}\n\n// Server is the main application\ntype Server struct {\n\tauthUser            string\n\tauthPass            string\n\tauthHtpasswd        string\n\tauthIPFilterOptions *IPFilterOptions\n\n\thtpasswdFile *htpasswd.File\n\tauthIPFilter *ipFilter\n\n\tlogger *log.Logger\n\n\ttlsConfig *tls.Config\n\n\tprofilerEnabled bool\n\n\tlocks sync.Map\n\n\tmaxUploadSize     int64\n\trateLimitRequests int\n\n\tpurgeDays     time.Duration\n\tpurgeInterval time.Duration\n\n\tstorage storage.Storage\n\n\tforceHTTPS bool\n\n\trandomTokenLength int\n\n\tipFilterOptions *IPFilterOptions\n\n\tVirusTotalKey        string\n\tClamAVDaemonHost     string\n\tperformClamavPrescan bool\n\n\ttempPath string\n\n\twebPath      string\n\tproxyPath    string\n\tproxyPort    string\n\temailContact string\n\tgaKey        string\n\tuserVoiceKey string\n\n\tTLSListenerOnly bool\n\n\tCorsDomains           string\n\tListenerString        string\n\tTLSListenerString     string\n\tProfileListenerString string\n\n\tCertificate string\n\n\tLetsEncryptCache string\n}\n\n// New is the factory fot Server\nfunc New(options ...OptionFn) (*Server, error) {\n\ts := &Server{\n\t\tlocks: sync.Map{},\n\t}\n\n\tfor _, optionFn := range options {\n\t\toptionFn(s)\n\t}\n\n\treturn s, nil\n}\n\nvar theRand *rand.Rand\n\nfunc init() {\n\tvar seedBytes [8]byte\n\tif _, err := cryptoRand.Read(seedBytes[:]); err != nil {\n\t\tpanic(\"cannot obtain cryptographically secure seed\")\n\t}\n\n\ttheRand = rand.New(rand.NewSource(int64(binary.LittleEndian.Uint64(seedBytes[:]))))\n}\n\n// Run starts Server\nfunc (s *Server) Run() {\n\tlistening := false\n\n\tif s.profilerEnabled {\n\t\tlistening = true\n\n\t\tgo func() {\n\t\t\ts.logger.Println(\"Profiled listening at: :6060\")\n\n\t\t\t_ = http.ListenAndServe(\":6060\", nil)\n\t\t}()\n\t}\n\n\tr := mux.NewRouter()\n\n\tvar fs http.FileSystem\n\n\tif s.webPath != \"\" {\n\t\ts.logger.Println(\"Using static file path: \", s.webPath)\n\n\t\tfs = http.Dir(s.webPath)\n\n\t\thtmlTemplates, _ = htmlTemplates.ParseGlob(filepath.Join(s.webPath, \"*.html\"))\n\t\ttextTemplates, _ = textTemplates.ParseGlob(filepath.Join(s.webPath, \"*.txt\"))\n\t} else {\n\t\tfs = &assetfs.AssetFS{\n\t\t\tAsset:    web.Asset,\n\t\t\tAssetDir: web.AssetDir,\n\t\t\tAssetInfo: func(path string) (os.FileInfo, error) {\n\t\t\t\treturn os.Stat(path)\n\t\t\t},\n\t\t\tPrefix: web.Prefix,\n\t\t}\n\n\t\tfor _, path := range web.AssetNames() {\n\t\t\tbytes, err := web.Asset(path)\n\t\t\tif err != nil {\n\t\t\t\ts.logger.Panicf(\"Unable to parse: path=%s, err=%s\", path, err)\n\t\t\t}\n\n\t\t\tif strings.HasSuffix(path, \".html\") {\n\t\t\t\t_, err = htmlTemplates.New(stripPrefix(path)).Parse(string(bytes))\n\t\t\t\tif err != nil {\n\t\t\t\t\ts.logger.Println(\"Unable to parse html template\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif strings.HasSuffix(path, \".txt\") {\n\t\t\t\t_, err = textTemplates.New(stripPrefix(path)).Parse(string(bytes))\n\t\t\t\tif err != nil {\n\t\t\t\t\ts.logger.Println(\"Unable to parse text template\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tstaticHandler := http.FileServer(fs)\n\n\tr.PathPrefix(\"/images/\").Handler(staticHandler).Methods(\"GET\")\n\tr.PathPrefix(\"/styles/\").Handler(staticHandler).Methods(\"GET\")\n\tr.PathPrefix(\"/scripts/\").Handler(staticHandler).Methods(\"GET\")\n\tr.PathPrefix(\"/fonts/\").Handler(staticHandler).Methods(\"GET\")\n\tr.PathPrefix(\"/ico/\").Handler(staticHandler).Methods(\"GET\")\n\tr.HandleFunc(\"/favicon.ico\", staticHandler.ServeHTTP).Methods(\"GET\")\n\tr.HandleFunc(\"/robots.txt\", staticHandler.ServeHTTP).Methods(\"GET\")\n\n\tr.HandleFunc(\"/{filename:(?:favicon\\\\.ico|robots\\\\.txt|health\\\\.html)}\", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods(\"PUT\")\n\n\tr.HandleFunc(\"/health.html\", healthHandler).Methods(\"GET\")\n\tr.HandleFunc(\"/\", s.viewHandler).Methods(\"GET\")\n\n\tr.HandleFunc(\"/({files:.*}).zip\", s.zipHandler).Methods(\"GET\")\n\tr.HandleFunc(\"/({files:.*}).tar\", s.tarHandler).Methods(\"GET\")\n\tr.HandleFunc(\"/({files:.*}).tar.gz\", s.tarGzHandler).Methods(\"GET\")\n\n\tr.HandleFunc(\"/{token}/{filename}\", s.headHandler).Methods(\"HEAD\")\n\tr.HandleFunc(\"/{action:(?:download|get|inline)}/{token}/{filename}\", s.headHandler).Methods(\"HEAD\")\n\n\tr.HandleFunc(\"/{token}/{filename}\", s.previewHandler).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) (match bool) {\n\t\t// The file will show a preview page when opening the link in browser directly or\n\t\t// from external link. If the referer url path and current path are the same it will be\n\t\t// downloaded.\n\t\tif !acceptsHTML(r.Header) {\n\t\t\treturn false\n\t\t}\n\n\t\tmatch = r.Referer() == \"\"\n\n\t\tu, err := url.Parse(r.Referer())\n\t\tif err != nil {\n\t\t\ts.logger.Fatal(err)\n\t\t\treturn\n\t\t}\n\n\t\tmatch = match || (u.Path != r.URL.Path)\n\t\treturn\n\t}).Methods(\"GET\")\n\n\tgetHandlerFn := s.getHandler\n\tif s.rateLimitRequests > 0 {\n\t\tgetHandlerFn = ratelimit.Request(ratelimit.IP).Rate(s.rateLimitRequests, 60*time.Second).LimitBy(memory.New())(http.HandlerFunc(getHandlerFn)).ServeHTTP\n\t}\n\n\tr.HandleFunc(\"/{token}/{filename}\", getHandlerFn).Methods(\"GET\")\n\tr.HandleFunc(\"/{action:(?:download|get|inline)}/{token}/{filename}\", getHandlerFn).Methods(\"GET\")\n\n\tr.HandleFunc(\"/{filename}/virustotal\", s.virusTotalHandler).Methods(\"PUT\")\n\tr.HandleFunc(\"/{filename}/scan\", s.scanHandler).Methods(\"PUT\")\n\tr.HandleFunc(\"/put/{filename}\", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods(\"PUT\")\n\tr.HandleFunc(\"/upload/{filename}\", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods(\"PUT\")\n\tr.HandleFunc(\"/{filename}\", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods(\"PUT\")\n\tr.HandleFunc(\"/\", s.basicAuthHandler(http.HandlerFunc(s.postHandler))).Methods(\"POST\")\n\t// r.HandleFunc(\"/{page}\", viewHandler).Methods(\"GET\")\n\n\tr.HandleFunc(\"/{token}/{filename}/{deletionToken}\", s.deleteHandler).Methods(\"DELETE\")\n\n\tr.NotFoundHandler = http.HandlerFunc(s.notFoundHandler)\n\n\t_ = mime.AddExtensionType(\".md\", \"text/x-markdown\")\n\n\ts.logger.Printf(\"Transfer.sh server started.\\nusing temp folder: %s\\nusing storage provider: %s\", s.tempPath, s.storage.Type())\n\n\tvar cors func(http.Handler) http.Handler\n\tif len(s.CorsDomains) > 0 {\n\t\tcors = gorillaHandlers.CORS(\n\t\t\tgorillaHandlers.AllowedHeaders([]string{\"*\"}),\n\t\t\tgorillaHandlers.AllowedOrigins(strings.Split(s.CorsDomains, \",\")),\n\t\t\tgorillaHandlers.AllowedMethods([]string{\"GET\", \"HEAD\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\"}),\n\t\t)\n\t} else {\n\t\tcors = func(h http.Handler) http.Handler {\n\t\t\treturn h\n\t\t}\n\t}\n\n\th := handlers.PanicHandler(\n\t\tipFilterHandler(\n\t\t\thandlers.LogHandler(\n\t\t\t\tLoveHandler(\n\t\t\t\t\ts.RedirectHandler(cors(r))),\n\t\t\t\thandlers.NewLogOptions(s.logger.Printf, \"_default_\"),\n\t\t\t),\n\t\t\ts.ipFilterOptions,\n\t\t),\n\t\tnil,\n\t)\n\n\tif !s.TLSListenerOnly {\n\t\tlistening = true\n\t\ts.logger.Printf(\"starting to listen on: %v\\n\", s.ListenerString)\n\n\t\tgo func() {\n\t\t\tsrvr := &http.Server{\n\t\t\t\tAddr:    s.ListenerString,\n\t\t\t\tHandler: h,\n\t\t\t}\n\n\t\t\tif err := srvr.ListenAndServe(); err != nil {\n\t\t\t\ts.logger.Fatal(err)\n\t\t\t}\n\t\t}()\n\t}\n\n\tif s.TLSListenerString != \"\" {\n\t\tlistening = true\n\t\ts.logger.Printf(\"starting to listen for TLS on: %v\\n\", s.TLSListenerString)\n\n\t\tgo func() {\n\t\t\tsrvr := &http.Server{\n\t\t\t\tAddr:      s.TLSListenerString,\n\t\t\t\tHandler:   h,\n\t\t\t\tTLSConfig: s.tlsConfig,\n\t\t\t}\n\n\t\t\tif err := srvr.ListenAndServeTLS(\"\", \"\"); err != nil {\n\t\t\t\ts.logger.Fatal(err)\n\t\t\t}\n\t\t}()\n\t}\n\n\ts.logger.Printf(\"---------------------------\")\n\n\tif s.purgeDays > 0 {\n\t\tgo s.purgeHandler()\n\t}\n\n\tterm := make(chan os.Signal, 1)\n\tsignal.Notify(term, os.Interrupt)\n\tsignal.Notify(term, syscall.SIGTERM)\n\n\tif listening {\n\t\t<-term\n\t} else {\n\t\ts.logger.Printf(\"No listener active.\")\n\t}\n\n\ts.logger.Printf(\"Server stopped.\")\n}\n"
  },
  {
    "path": "server/storage/common.go",
    "content": "package storage\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"time\"\n)\n\ntype Range struct {\n\tStart        uint64\n\tLimit        uint64\n\tcontentRange string\n}\n\n// Range Reconstructs Range header and returns it\nfunc (r *Range) Range() string {\n\tif r.Limit > 0 {\n\t\treturn fmt.Sprintf(\"bytes=%d-%d\", r.Start, r.Start+r.Limit-1)\n\t} else {\n\t\treturn fmt.Sprintf(\"bytes=%d-\", r.Start)\n\t}\n}\n\n// AcceptLength Tries to accept given range\n// returns newContentLength if range was satisfied, otherwise returns given contentLength\nfunc (r *Range) AcceptLength(contentLength uint64) (newContentLength uint64) {\n\tnewContentLength = contentLength\n\tif r.Limit == 0 {\n\t\tr.Limit = newContentLength - r.Start\n\t}\n\tif contentLength < r.Start {\n\t\treturn\n\t}\n\tif r.Limit > contentLength-r.Start {\n\t\treturn\n\t}\n\tr.contentRange = fmt.Sprintf(\"bytes %d-%d/%d\", r.Start, r.Start+r.Limit-1, contentLength)\n\tnewContentLength = r.Limit\n\treturn\n}\n\nfunc (r *Range) SetContentRange(cr string) {\n\tr.contentRange = cr\n}\n\n// Returns accepted Content-Range header. If range wasn't accepted empty string is returned\nfunc (r *Range) ContentRange() string {\n\treturn r.contentRange\n}\n\nvar rexp *regexp.Regexp = regexp.MustCompile(`^bytes=([0-9]+)-([0-9]*)$`)\n\n// Parses HTTP Range header and returns struct on success\n// only bytes=start-finish supported\nfunc ParseRange(rng string) *Range {\n\tif rng == \"\" {\n\t\treturn nil\n\t}\n\n\tmatches := rexp.FindAllStringSubmatch(rng, -1)\n\tif len(matches) != 1 || len(matches[0]) != 3 {\n\t\treturn nil\n\t}\n\tif len(matches[0][0]) != len(rng) || len(matches[0][1]) == 0 {\n\t\treturn nil\n\t}\n\n\tstart, err := strconv.ParseUint(matches[0][1], 10, 64)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tif len(matches[0][2]) == 0 {\n\t\treturn &Range{Start: start, Limit: 0}\n\t}\n\n\tfinish, err := strconv.ParseUint(matches[0][2], 10, 64)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tif finish < start || finish+1 < finish {\n\t\treturn nil\n\t}\n\n\treturn &Range{Start: start, Limit: finish - start + 1}\n}\n\n// Storage is the interface for storage operation\ntype Storage interface {\n\t// Get retrieves a file from storage\n\tGet(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error)\n\t// Head retrieves content length of a file from storage\n\tHead(ctx context.Context, token string, filename string) (contentLength uint64, err error)\n\t// Put saves a file on storage\n\tPut(ctx context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) error\n\t// Delete removes a file from storage\n\tDelete(ctx context.Context, token string, filename string) error\n\t// IsNotExist indicates if a file doesn't exist on storage\n\tIsNotExist(err error) bool\n\t// Purge cleans up the storage\n\tPurge(ctx context.Context, days time.Duration) error\n\t// Whether storage supports Get with Range header\n\tIsRangeSupported() bool\n\t// Type returns the storage type\n\tType() string\n}\n\nfunc CloseCheck(c io.Closer) {\n\tif c == nil {\n\t\treturn\n\t}\n\n\tif err := c.Close(); err != nil {\n\t\tfmt.Println(\"Received close error:\", err)\n\t}\n}\n"
  },
  {
    "path": "server/storage/gdrive.go",
    "content": "package storage\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n\t\"google.golang.org/api/drive/v3\"\n\t\"google.golang.org/api/googleapi\"\n\t\"google.golang.org/api/option\"\n)\n\n// GDrive is a storage backed by GDrive\ntype GDrive struct {\n\tservice         *drive.Service\n\trootID          string\n\tbasedir         string\n\tlocalConfigPath string\n\tchunkSize       int\n\tlogger          *log.Logger\n}\n\nconst gDriveRootConfigFile = \"root_id.conf\"\nconst gDriveTokenJSONFile = \"token.json\"\nconst gDriveDirectoryMimeType = \"application/vnd.google-apps.folder\"\n\n// NewGDriveStorage is the factory for GDrive\nfunc NewGDriveStorage(ctx context.Context, clientJSONFilepath string, localConfigPath string, basedir string, chunkSize int, logger *log.Logger) (*GDrive, error) {\n\n\tb, err := os.ReadFile(clientJSONFilepath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// If modifying these scopes, delete your previously saved client_secret.json.\n\tconfig, err := google.ConfigFromJSON(b, drive.DriveScope, drive.DriveMetadataScope)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpClient := getGDriveClient(ctx, config, localConfigPath, logger)\n\n\tsrv, err := drive.NewService(ctx, option.WithHTTPClient(httpClient))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstorage := &GDrive{service: srv, basedir: basedir, rootID: \"\", localConfigPath: localConfigPath, chunkSize: chunkSize, logger: logger}\n\terr = storage.setupRoot()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn storage, nil\n}\n\nfunc (s *GDrive) setupRoot() error {\n\trootFileConfig := filepath.Join(s.localConfigPath, gDriveRootConfigFile)\n\n\trootID, err := os.ReadFile(rootFileConfig)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn err\n\t}\n\n\tif string(rootID) != \"\" {\n\t\ts.rootID = string(rootID)\n\t\treturn nil\n\t}\n\n\tdir := &drive.File{\n\t\tName:     s.basedir,\n\t\tMimeType: gDriveDirectoryMimeType,\n\t}\n\n\tdi, err := s.service.Files.Create(dir).Fields(\"id\").Do()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.rootID = di.Id\n\terr = os.WriteFile(rootFileConfig, []byte(s.rootID), os.FileMode(0600))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *GDrive) hasChecksum(f *drive.File) bool {\n\treturn f.Md5Checksum != \"\"\n}\n\nfunc (s *GDrive) list(nextPageToken string, q string) (*drive.FileList, error) {\n\treturn s.service.Files.List().Fields(\"nextPageToken, files(id, name, mimeType)\").Q(q).PageToken(nextPageToken).Do()\n}\n\nfunc (s *GDrive) findID(filename string, token string) (string, error) {\n\tfilename = strings.Replace(filename, `'`, `\\'`, -1)\n\tfilename = strings.Replace(filename, `\"`, `\\\"`, -1)\n\n\tfileID, tokenID, nextPageToken := \"\", \"\", \"\"\n\n\tq := fmt.Sprintf(\"'%s' in parents and name='%s' and mimeType='%s' and trashed=false\", s.rootID, token, gDriveDirectoryMimeType)\n\tl, err := s.list(nextPageToken, q)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor 0 < len(l.Files) {\n\t\tfor _, fi := range l.Files {\n\t\t\ttokenID = fi.Id\n\t\t\tbreak\n\t\t}\n\n\t\tif l.NextPageToken == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tl, err = s.list(l.NextPageToken, q)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tif filename == \"\" {\n\t\treturn tokenID, nil\n\t} else if tokenID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"cannot find file %s/%s\", token, filename)\n\t}\n\n\tq = fmt.Sprintf(\"'%s' in parents and name='%s' and mimeType!='%s' and trashed=false\", tokenID, filename, gDriveDirectoryMimeType)\n\tl, err = s.list(nextPageToken, q)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor 0 < len(l.Files) {\n\t\tfor _, fi := range l.Files {\n\n\t\t\tfileID = fi.Id\n\t\t\tbreak\n\t\t}\n\n\t\tif l.NextPageToken == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tl, err = s.list(l.NextPageToken, q)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tif fileID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"cannot find file %s/%s\", token, filename)\n\t}\n\n\treturn fileID, nil\n}\n\n// Type returns the storage type\nfunc (s *GDrive) Type() string {\n\treturn \"gdrive\"\n}\n\n// Head retrieves content length of a file from storage\nfunc (s *GDrive) Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) {\n\tvar fileID string\n\tfileID, err = s.findID(filename, token)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar fi *drive.File\n\tif fi, err = s.service.Files.Get(fileID).Context(ctx).Fields(\"size\").Do(); err != nil {\n\t\treturn\n\t}\n\n\tcontentLength = uint64(fi.Size)\n\n\treturn\n}\n\n// Get retrieves a file from storage\nfunc (s *GDrive) Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) {\n\tvar fileID string\n\tfileID, err = s.findID(filename, token)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar fi *drive.File\n\tfi, err = s.service.Files.Get(fileID).Fields(\"size\", \"md5Checksum\").Do()\n\tif err != nil {\n\t\treturn\n\t}\n\tif !s.hasChecksum(fi) {\n\t\terr = fmt.Errorf(\"cannot find file %s/%s\", token, filename)\n\t\treturn\n\t}\n\n\tcontentLength = uint64(fi.Size)\n\n\tfileGetCall := s.service.Files.Get(fileID)\n\tif rng != nil {\n\t\theader := fileGetCall.Header()\n\t\theader.Set(\"Range\", rng.Range())\n\t}\n\n\tvar res *http.Response\n\tres, err = fileGetCall.Context(ctx).Download()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif rng != nil {\n\t\treader = res.Body\n\t\trng.AcceptLength(contentLength)\n\t\treturn\n\t}\n\n\treader = res.Body\n\n\treturn\n}\n\n// Delete removes a file from storage\nfunc (s *GDrive) Delete(ctx context.Context, token string, filename string) (err error) {\n\tmetadata, _ := s.findID(fmt.Sprintf(\"%s.metadata\", filename), token)\n\t_ = s.service.Files.Delete(metadata).Do()\n\n\tvar fileID string\n\tfileID, err = s.findID(filename, token)\n\tif err != nil {\n\t\treturn\n\t}\n\n\terr = s.service.Files.Delete(fileID).Context(ctx).Do()\n\treturn\n}\n\n// Purge cleans up the storage\nfunc (s *GDrive) Purge(ctx context.Context, days time.Duration) (err error) {\n\tnextPageToken := \"\"\n\n\texpirationDate := time.Now().Add(-1 * days).Format(time.RFC3339)\n\tq := fmt.Sprintf(\"'%s' in parents and modifiedTime < '%s' and mimeType!='%s' and trashed=false\", s.rootID, expirationDate, gDriveDirectoryMimeType)\n\tl, err := s.list(nextPageToken, q)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor 0 < len(l.Files) {\n\t\tfor _, fi := range l.Files {\n\t\t\terr = s.service.Files.Delete(fi.Id).Context(ctx).Do()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif l.NextPageToken == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tl, err = s.list(l.NextPageToken, q)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\treturn\n}\n\n// IsNotExist indicates if a file doesn't exist on storage\nfunc (s *GDrive) IsNotExist(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\tif e, ok := err.(*googleapi.Error); ok {\n\t\treturn e.Code == http.StatusNotFound\n\t}\n\n\treturn false\n}\n\n// Put saves a file on storage\nfunc (s *GDrive) Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) error {\n\tdirID, err := s.findID(\"\", token)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif dirID == \"\" {\n\t\tdir := &drive.File{\n\t\t\tName:     token,\n\t\t\tParents:  []string{s.rootID},\n\t\t\tMimeType: gDriveDirectoryMimeType,\n\t\t}\n\n\t\tdi, err := s.service.Files.Create(dir).Fields(\"id\").Do()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdirID = di.Id\n\t}\n\n\t// Instantiate empty drive file\n\tdst := &drive.File{\n\t\tName:     filename,\n\t\tParents:  []string{dirID},\n\t\tMimeType: contentType,\n\t}\n\n\t_, err = s.service.Files.Create(dst).Context(ctx).Media(reader, googleapi.ChunkSize(s.chunkSize)).Do()\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *GDrive) IsRangeSupported() bool { return true }\n\n// Retrieve a token, saves the token, then returns the generated client.\nfunc getGDriveClient(ctx context.Context, config *oauth2.Config, localConfigPath string, logger *log.Logger) *http.Client {\n\ttokenFile := filepath.Join(localConfigPath, gDriveTokenJSONFile)\n\ttok, err := gDriveTokenFromFile(tokenFile)\n\tif err != nil {\n\t\ttok = getGDriveTokenFromWeb(ctx, config, logger)\n\t\tsaveGDriveToken(tokenFile, tok, logger)\n\t}\n\n\treturn config.Client(ctx, tok)\n}\n\n// Request a token from the web, then returns the retrieved token.\nfunc getGDriveTokenFromWeb(ctx context.Context, config *oauth2.Config, logger *log.Logger) *oauth2.Token {\n\tauthURL := config.AuthCodeURL(\"state-token\", oauth2.AccessTypeOffline)\n\tfmt.Printf(\"Go to the following link in your browser then type the \"+\n\t\t\"authorization code: \\n%v\\n\", authURL)\n\n\tvar authCode string\n\tif _, err := fmt.Scan(&authCode); err != nil {\n\t\tlogger.Fatalf(\"Unable to read authorization code %v\", err)\n\t}\n\n\ttok, err := config.Exchange(ctx, authCode)\n\tif err != nil {\n\t\tlogger.Fatalf(\"Unable to retrieve token from web %v\", err)\n\t}\n\treturn tok\n}\n\n// Retrieves a token from a local file.\nfunc gDriveTokenFromFile(file string) (*oauth2.Token, error) {\n\tf, err := os.Open(file)\n\tdefer CloseCheck(f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttok := &oauth2.Token{}\n\terr = json.NewDecoder(f).Decode(tok)\n\treturn tok, err\n}\n\n// Saves a token to a file path.\nfunc saveGDriveToken(path string, token *oauth2.Token, logger *log.Logger) {\n\tlogger.Printf(\"Saving credential file to: %s\\n\", path)\n\tf, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)\n\tdefer CloseCheck(f)\n\tif err != nil {\n\t\tlogger.Fatalf(\"Unable to cache oauth token: %v\", err)\n\t}\n\n\terr = json.NewEncoder(f).Encode(token)\n\tif err != nil {\n\t\tlogger.Fatalf(\"Unable to encode oauth token: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "server/storage/local.go",
    "content": "package storage\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\n// LocalStorage is a local storage\ntype LocalStorage struct {\n\tStorage\n\tbasedir string\n\tlogger  *log.Logger\n}\n\n// NewLocalStorage is the factory for LocalStorage\nfunc NewLocalStorage(basedir string, logger *log.Logger) (*LocalStorage, error) {\n\treturn &LocalStorage{basedir: basedir, logger: logger}, nil\n}\n\n// Type returns the storage type\nfunc (s *LocalStorage) Type() string {\n\treturn \"local\"\n}\n\n// Head retrieves content length of a file from storage\nfunc (s *LocalStorage) Head(_ context.Context, token string, filename string) (contentLength uint64, err error) {\n\tpath := filepath.Join(s.basedir, token, filename)\n\n\tvar fi os.FileInfo\n\tif fi, err = os.Lstat(path); err != nil {\n\t\treturn\n\t}\n\n\tcontentLength = uint64(fi.Size())\n\n\treturn\n}\n\n// Get retrieves a file from storage\nfunc (s *LocalStorage) Get(_ context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) {\n\tpath := filepath.Join(s.basedir, token, filename)\n\n\tvar file *os.File\n\n\t// content type , content length\n\tif file, err = os.Open(path); err != nil {\n\t\treturn\n\t}\n\treader = file\n\n\tvar fi os.FileInfo\n\tif fi, err = os.Lstat(path); err != nil {\n\t\treturn\n\t}\n\n\tcontentLength = uint64(fi.Size())\n\tif rng != nil {\n\t\tcontentLength = rng.AcceptLength(contentLength)\n\t\tif _, err = file.Seek(int64(rng.Start), 0); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\treturn\n}\n\n// Delete removes a file from storage\nfunc (s *LocalStorage) Delete(_ context.Context, token string, filename string) (err error) {\n\tmetadata := filepath.Join(s.basedir, token, fmt.Sprintf(\"%s.metadata\", filename))\n\t_ = os.Remove(metadata)\n\n\tpath := filepath.Join(s.basedir, token, filename)\n\terr = os.Remove(path)\n\treturn\n}\n\n// Purge cleans up the storage\nfunc (s *LocalStorage) Purge(_ context.Context, days time.Duration) (err error) {\n\terr = filepath.Walk(s.basedir,\n\t\tfunc(path string, info os.FileInfo, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif info.IsDir() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif info.ModTime().Before(time.Now().Add(-1 * days)) {\n\t\t\t\terr = os.Remove(path)\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\treturn\n}\n\n// IsNotExist indicates if a file doesn't exist on storage\nfunc (s *LocalStorage) IsNotExist(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\treturn os.IsNotExist(err)\n}\n\n// Put saves a file on storage\nfunc (s *LocalStorage) Put(_ context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) error {\n\tvar f io.WriteCloser\n\tvar err error\n\n\tpath := filepath.Join(s.basedir, token)\n\n\tif err = os.MkdirAll(path, 0700); err != nil && !os.IsExist(err) {\n\t\treturn err\n\t}\n\n\tf, err = os.OpenFile(filepath.Join(path, filename), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)\n\tdefer CloseCheck(f)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif _, err = io.Copy(f, reader); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *LocalStorage) IsRangeSupported() bool { return true }\n"
  },
  {
    "path": "server/storage/s3.go",
    "content": "package storage\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/feature/s3/manager\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n)\n\n// S3Storage is a storage backed by AWS S3\ntype S3Storage struct {\n\tStorage\n\tbucket      string\n\ts3          *s3.Client\n\tlogger      *log.Logger\n\tpurgeDays   time.Duration\n\tnoMultipart bool\n}\n\n// NewS3Storage is the factory for S3Storage\nfunc NewS3Storage(ctx context.Context, accessKey, secretKey, bucketName string, purgeDays int, region, endpoint string, disableMultipart bool, forcePathStyle bool, logger *log.Logger) (*S3Storage, error) {\n\tcfg, err := getAwsConfig(ctx, accessKey, secretKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := s3.NewFromConfig(cfg, func(o *s3.Options) {\n\t\to.Region = region\n\t\to.UsePathStyle = forcePathStyle\n\t\tif len(endpoint) > 0 {\n\t\t\to.EndpointResolver = s3.EndpointResolverFromURL(endpoint)\n\t\t}\n\t})\n\n\treturn &S3Storage{\n\t\tbucket:      bucketName,\n\t\ts3:          client,\n\t\tlogger:      logger,\n\t\tnoMultipart: disableMultipart,\n\t\tpurgeDays:   time.Duration(purgeDays*24) * time.Hour,\n\t}, nil\n}\n\n// Type returns the storage type\nfunc (s *S3Storage) Type() string {\n\treturn \"s3\"\n}\n\n// Head retrieves content length of a file from storage\nfunc (s *S3Storage) Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) {\n\tkey := fmt.Sprintf(\"%s/%s\", token, filename)\n\n\theadRequest := &s3.HeadObjectInput{\n\t\tBucket: aws.String(s.bucket),\n\t\tKey:    aws.String(key),\n\t}\n\n\t// content type , content length\n\tresponse, err := s.s3.HeadObject(ctx, headRequest)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tcontentLength = uint64(response.ContentLength)\n\n\treturn\n}\n\n// Purge cleans up the storage\nfunc (s *S3Storage) Purge(context.Context, time.Duration) (err error) {\n\t// NOOP expiration is set at upload time\n\treturn nil\n}\n\n// IsNotExist indicates if a file doesn't exist on storage\nfunc (s *S3Storage) IsNotExist(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\tvar nkerr *types.NoSuchKey\n\treturn errors.As(err, &nkerr)\n}\n\n// Get retrieves a file from storage\nfunc (s *S3Storage) Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) {\n\tkey := fmt.Sprintf(\"%s/%s\", token, filename)\n\n\tgetRequest := &s3.GetObjectInput{\n\t\tBucket: aws.String(s.bucket),\n\t\tKey:    aws.String(key),\n\t}\n\n\tif rng != nil {\n\t\tgetRequest.Range = aws.String(rng.Range())\n\t}\n\n\tresponse, err := s.s3.GetObject(ctx, getRequest)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tcontentLength = uint64(response.ContentLength)\n\tif rng != nil && response.ContentRange != nil {\n\t\trng.SetContentRange(*response.ContentRange)\n\t}\n\n\treader = response.Body\n\treturn\n}\n\n// Delete removes a file from storage\nfunc (s *S3Storage) Delete(ctx context.Context, token string, filename string) (err error) {\n\tmetadata := fmt.Sprintf(\"%s/%s.metadata\", token, filename)\n\tdeleteRequest := &s3.DeleteObjectInput{\n\t\tBucket: aws.String(s.bucket),\n\t\tKey:    aws.String(metadata),\n\t}\n\n\t_, err = s.s3.DeleteObject(ctx, deleteRequest)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tkey := fmt.Sprintf(\"%s/%s\", token, filename)\n\tdeleteRequest = &s3.DeleteObjectInput{\n\t\tBucket: aws.String(s.bucket),\n\t\tKey:    aws.String(key),\n\t}\n\n\t_, err = s.s3.DeleteObject(ctx, deleteRequest)\n\n\treturn\n}\n\n// Put saves a file on storage\nfunc (s *S3Storage) Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, _ uint64) (err error) {\n\tkey := fmt.Sprintf(\"%s/%s\", token, filename)\n\n\ts.logger.Printf(\"Uploading file %s to S3 Bucket\", filename)\n\tvar concurrency int\n\tif !s.noMultipart {\n\t\tconcurrency = 20\n\t} else {\n\t\tconcurrency = 1\n\t}\n\n\t// Create an uploader with the session and custom options\n\tuploader := manager.NewUploader(s.s3, func(u *manager.Uploader) {\n\t\tu.Concurrency = concurrency // default is 5\n\t\tu.LeavePartsOnError = false\n\t})\n\n\tvar expire *time.Time\n\tif s.purgeDays.Hours() > 0 {\n\t\texpire = aws.Time(time.Now().Add(s.purgeDays))\n\t}\n\n\t_, err = uploader.Upload(ctx, &s3.PutObjectInput{\n\t\tBucket:      aws.String(s.bucket),\n\t\tKey:         aws.String(key),\n\t\tBody:        reader,\n\t\tExpires:     expire,\n\t\tContentType: aws.String(contentType),\n\t})\n\n\treturn\n}\n\nfunc (s *S3Storage) IsRangeSupported() bool { return true }\n\nfunc getAwsConfig(ctx context.Context, accessKey, secretKey string) (aws.Config, error) {\n\treturn config.LoadDefaultConfig(ctx,\n\t\tconfig.WithCredentialsProvider(credentials.StaticCredentialsProvider{\n\t\t\tValue: aws.Credentials{\n\t\t\t\tAccessKeyID:     accessKey,\n\t\t\t\tSecretAccessKey: secretKey,\n\t\t\t\tSessionToken:    \"\",\n\t\t\t},\n\t\t}),\n\t)\n}\n"
  },
  {
    "path": "server/storage/storj.go",
    "content": "package storage\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"time\"\n\n\t\"storj.io/common/fpath\"\n\t\"storj.io/common/storj\"\n\t\"storj.io/uplink\"\n)\n\n// StorjStorage is a storage backed by Storj\ntype StorjStorage struct {\n\tStorage\n\tproject   *uplink.Project\n\tbucket    *uplink.Bucket\n\tpurgeDays time.Duration\n\tlogger    *log.Logger\n}\n\n// NewStorjStorage is the factory for StorjStorage\nfunc NewStorjStorage(ctx context.Context, access, bucket string, purgeDays int, logger *log.Logger) (*StorjStorage, error) {\n\tvar instance StorjStorage\n\tvar err error\n\n\tctx = fpath.WithTempData(ctx, \"\", true)\n\n\tuplConf := &uplink.Config{\n\t\tUserAgent: \"transfer-sh\",\n\t}\n\n\tparsedAccess, err := uplink.ParseAccess(access)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinstance.project, err = uplConf.OpenProject(ctx, parsedAccess)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinstance.bucket, err = instance.project.EnsureBucket(ctx, bucket)\n\tif err != nil {\n\t\t//Ignoring the error to return the one that occurred first, but try to clean up.\n\t\t_ = instance.project.Close()\n\t\treturn nil, err\n\t}\n\n\tinstance.purgeDays = time.Duration(purgeDays*24) * time.Hour\n\n\tinstance.logger = logger\n\n\treturn &instance, nil\n}\n\n// Type returns the storage type\nfunc (s *StorjStorage) Type() string {\n\treturn \"storj\"\n}\n\n// Head retrieves content length of a file from storage\nfunc (s *StorjStorage) Head(ctx context.Context, token string, filename string) (contentLength uint64, err error) {\n\tkey := storj.JoinPaths(token, filename)\n\n\tobj, err := s.project.StatObject(fpath.WithTempData(ctx, \"\", true), s.bucket.Name, key)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tcontentLength = uint64(obj.System.ContentLength)\n\n\treturn\n}\n\n// Get retrieves a file from storage\nfunc (s *StorjStorage) Get(ctx context.Context, token string, filename string, rng *Range) (reader io.ReadCloser, contentLength uint64, err error) {\n\tkey := storj.JoinPaths(token, filename)\n\n\ts.logger.Printf(\"Getting file %s from Storj Bucket\", filename)\n\n\tvar options *uplink.DownloadOptions\n\tif rng != nil {\n\t\toptions = new(uplink.DownloadOptions)\n\t\toptions.Offset = int64(rng.Start)\n\t\tif rng.Limit > 0 {\n\t\t\toptions.Length = int64(rng.Limit)\n\t\t} else {\n\t\t\toptions.Length = -1\n\t\t}\n\t}\n\n\tdownload, err := s.project.DownloadObject(fpath.WithTempData(ctx, \"\", true), s.bucket.Name, key, options)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tcontentLength = uint64(download.Info().System.ContentLength)\n\tif rng != nil {\n\t\tcontentLength = rng.AcceptLength(contentLength)\n\t}\n\n\treader = download\n\treturn\n}\n\n// Delete removes a file from storage\nfunc (s *StorjStorage) Delete(ctx context.Context, token string, filename string) (err error) {\n\tkey := storj.JoinPaths(token, filename)\n\n\ts.logger.Printf(\"Deleting file %s from Storj Bucket\", filename)\n\n\t_, err = s.project.DeleteObject(fpath.WithTempData(ctx, \"\", true), s.bucket.Name, key)\n\n\treturn\n}\n\n// Purge cleans up the storage\nfunc (s *StorjStorage) Purge(context.Context, time.Duration) (err error) {\n\t// NOOP expiration is set at upload time\n\treturn nil\n}\n\n// Put saves a file on storage\nfunc (s *StorjStorage) Put(ctx context.Context, token string, filename string, reader io.Reader, contentType string, contentLength uint64) (err error) {\n\tkey := storj.JoinPaths(token, filename)\n\n\ts.logger.Printf(\"Uploading file %s to Storj Bucket\", filename)\n\n\tvar uploadOptions *uplink.UploadOptions\n\tif s.purgeDays.Hours() > 0 {\n\t\tuploadOptions = &uplink.UploadOptions{Expires: time.Now().Add(s.purgeDays)}\n\t}\n\n\twriter, err := s.project.UploadObject(fpath.WithTempData(ctx, \"\", true), s.bucket.Name, key, uploadOptions)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tn, err := io.Copy(writer, reader)\n\tif err != nil || uint64(n) != contentLength {\n\t\t//Ignoring the error to return the one that occurred first, but try to clean up.\n\t\t_ = writer.Abort()\n\t\treturn err\n\t}\n\terr = writer.SetCustomMetadata(ctx, uplink.CustomMetadata{\"content-type\": contentType})\n\tif err != nil {\n\t\t//Ignoring the error to return the one that occurred first, but try to clean up.\n\t\t_ = writer.Abort()\n\t\treturn err\n\t}\n\n\terr = writer.Commit()\n\treturn err\n}\n\nfunc (s *StorjStorage) IsRangeSupported() bool { return true }\n\n// IsNotExist indicates if a file doesn't exist on storage\nfunc (s *StorjStorage) IsNotExist(err error) bool {\n\treturn errors.Is(err, uplink.ErrObjectNotFound)\n}\n"
  },
  {
    "path": "server/token.go",
    "content": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2020- Andrea Spacca and Stefan Benten.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n*/\n\npackage server\n\nimport (\n\t\"strings\"\n)\n\nconst (\n\t// SYMBOLS characters used for short-urls\n\tSYMBOLS = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n)\n\n// generate a token\nfunc token(length int) string {\n\tvar builder strings.Builder\n\tbuilder.Grow(length)\n\t\n\tfor i := 0; i < length; i++ {\n\t\tx := theRand.Intn(len(SYMBOLS) - 1)\n\t\tbuilder.WriteByte(SYMBOLS[x])\n\t}\n\n\treturn builder.String()\n}\n"
  },
  {
    "path": "server/token_test.go",
    "content": "package server\n\nimport \"testing\"\n\nfunc BenchmarkTokenConcat(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = token(5) + token(5)\n\t}\n}\n\nfunc BenchmarkTokenLonger(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = token(10)\n\t}\n}\n"
  },
  {
    "path": "server/utils.go",
    "content": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]\nCopyright (c) 2018-2020 Andrea Spacca.\nCopyright (c) 2020- Andrea Spacca and Stefan Benten.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n*/\n\npackage server\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/golang/gddo/httputil/header\"\n)\n\nfunc formatNumber(format string, s uint64) string {\n\treturn renderFloat(format, float64(s))\n}\n\nvar renderFloatPrecisionMultipliers = [10]float64{\n\t1,\n\t10,\n\t100,\n\t1000,\n\t10000,\n\t100000,\n\t1000000,\n\t10000000,\n\t100000000,\n\t1000000000,\n}\n\nvar renderFloatPrecisionRounders = [10]float64{\n\t0.5,\n\t0.05,\n\t0.005,\n\t0.0005,\n\t0.00005,\n\t0.000005,\n\t0.0000005,\n\t0.00000005,\n\t0.000000005,\n\t0.0000000005,\n}\n\nfunc renderFloat(format string, n float64) string {\n\t// Special cases:\n\t// NaN = \"NaN\"\n\t// +Inf = \"+Infinity\"\n\t// -Inf = \"-Infinity\"\n\tif math.IsNaN(n) {\n\t\treturn \"NaN\"\n\t}\n\tif n > math.MaxFloat64 {\n\t\treturn \"Infinity\"\n\t}\n\tif n < -math.MaxFloat64 {\n\t\treturn \"-Infinity\"\n\t}\n\n\t// default format\n\tprecision := 2\n\tdecimalStr := \".\"\n\tthousandStr := \",\"\n\tpositiveStr := \"\"\n\tnegativeStr := \"-\"\n\n\tif len(format) > 0 {\n\t\t// If there is an explicit format directive,\n\t\t// then default values are these:\n\t\tprecision = 9\n\t\tthousandStr = \"\"\n\n\t\t// collect indices of meaningful formatting directives\n\t\tformatDirectiveChars := []rune(format)\n\t\tformatDirectiveIndices := make([]int, 0)\n\t\tfor i, char := range formatDirectiveChars {\n\t\t\tif char != '#' && char != '0' {\n\t\t\t\tformatDirectiveIndices = append(formatDirectiveIndices, i)\n\t\t\t}\n\t\t}\n\n\t\tif len(formatDirectiveIndices) > 0 {\n\t\t\t// Directive at index 0:\n\t\t\t// Must be a '+'\n\t\t\t// Raise an error if not the case\n\t\t\t// index: 0123456789\n\t\t\t// +0.000,000\n\t\t\t// +000,000.0\n\t\t\t// +0000.00\n\t\t\t// +0000\n\t\t\tif formatDirectiveIndices[0] == 0 {\n\t\t\t\tif formatDirectiveChars[formatDirectiveIndices[0]] != '+' {\n\t\t\t\t\tpanic(\"renderFloat(): invalid positive sign directive\")\n\t\t\t\t}\n\t\t\t\tpositiveStr = \"+\"\n\t\t\t\tformatDirectiveIndices = formatDirectiveIndices[1:]\n\t\t\t}\n\n\t\t\t// Two directives:\n\t\t\t// First is thousands separator\n\t\t\t// Raise an error if not followed by 3-digit\n\t\t\t// 0123456789\n\t\t\t// 0.000,000\n\t\t\t// 000,000.00\n\t\t\tif len(formatDirectiveIndices) == 2 {\n\t\t\t\tif (formatDirectiveIndices[1] - formatDirectiveIndices[0]) != 4 {\n\t\t\t\t\tpanic(\"renderFloat(): thousands separator directive must be followed by 3 digit-specifiers\")\n\t\t\t\t}\n\t\t\t\tthousandStr = string(formatDirectiveChars[formatDirectiveIndices[0]])\n\t\t\t\tformatDirectiveIndices = formatDirectiveIndices[1:]\n\t\t\t}\n\n\t\t\t// One directive:\n\t\t\t// Directive is decimal separator\n\t\t\t// The number of digit-specifier following the separator indicates wanted precision\n\t\t\t// 0123456789\n\t\t\t// 0.00\n\t\t\t// 000,0000\n\t\t\tif len(formatDirectiveIndices) == 1 {\n\t\t\t\tdecimalStr = string(formatDirectiveChars[formatDirectiveIndices[0]])\n\t\t\t\tprecision = len(formatDirectiveChars) - formatDirectiveIndices[0] - 1\n\t\t\t}\n\t\t}\n\t}\n\n\t// generate sign part\n\tvar signStr string\n\tif n >= 0.000000001 {\n\t\tsignStr = positiveStr\n\t} else if n <= -0.000000001 {\n\t\tsignStr = negativeStr\n\t\tn = -n\n\t} else {\n\t\tsignStr = \"\"\n\t\tn = 0.0\n\t}\n\n\t// split number into integer and fractional parts\n\tintf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])\n\n\t// generate integer part string\n\tintStr := strconv.Itoa(int(intf))\n\n\t// add thousand separator if required\n\tif len(thousandStr) > 0 {\n\t\tfor i := len(intStr); i > 3; {\n\t\t\ti -= 3\n\t\t\tintStr = intStr[:i] + thousandStr + intStr[i:]\n\t\t}\n\t}\n\n\t// no fractional part, we can leave now\n\tif precision == 0 {\n\t\treturn signStr + intStr\n\t}\n\n\t// generate fractional part\n\tfracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))\n\t// may need padding\n\tif len(fracStr) < precision {\n\t\tfracStr = \"000000000000000\"[:precision-len(fracStr)] + fracStr\n\t}\n\n\treturn signStr + intStr + decimalStr + fracStr\n}\n\n// Request.RemoteAddress contains port, which we want to remove i.e.:\n// \"[::1]:58292\" => \"[::1]\"\nfunc ipAddrFromRemoteAddr(s string) string {\n\tidx := strings.LastIndex(s, \":\")\n\tif idx == -1 {\n\t\treturn s\n\t}\n\treturn s[:idx]\n}\n\nfunc acceptsHTML(hdr http.Header) bool {\n\tactual := header.ParseAccept(hdr, \"Accept\")\n\n\tfor _, s := range actual {\n\t\tif s.Value == \"text/html\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc formatSize(size int64) string {\n\tsizeFloat := float64(size)\n\tbase := math.Log(sizeFloat) / math.Log(1024)\n\n\tsizeOn := math.Pow(1024, base-math.Floor(base))\n\n\tvar round float64\n\tpow := math.Pow(10, float64(2))\n\tdigit := pow * sizeOn\n\tround = math.Floor(digit)\n\n\tnewVal := round / pow\n\n\tvar suffixes [5]string\n\tsuffixes[0] = \"B\"\n\tsuffixes[1] = \"KB\"\n\tsuffixes[2] = \"MB\"\n\tsuffixes[3] = \"GB\"\n\tsuffixes[4] = \"TB\"\n\n\tgetSuffix := suffixes[int(math.Floor(base))]\n\treturn fmt.Sprintf(\"%s %s\", strconv.FormatFloat(newVal, 'f', -1, 64), getSuffix)\n}\n\nfunc formatDurationDays(durationDays time.Duration) string {\n\tdays := int(durationDays.Hours() / 24)\n\tif days == 1 {\n\t\treturn fmt.Sprintf(\"%d day\", days)\n\t}\n\treturn fmt.Sprintf(\"%d days\", days)\n}\n"
  },
  {
    "path": "server/virustotal.go",
    "content": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n*/\n\npackage server\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/gorilla/mux\"\n\n\t\"github.com/Aetherinox/go-virustotal\"\n)\n\nfunc (s *Server) virusTotalHandler(w http.ResponseWriter, r *http.Request) {\n\tvars := mux.Vars(r)\n\n\tfilename := sanitize(vars[\"filename\"])\n\n\tcontentLength := r.ContentLength\n\tcontentType := r.Header.Get(\"Content-Type\")\n\n\ts.logger.Printf(\"Submitting to VirusTotal: %s %d %s\", filename, contentLength, contentType)\n\n\tvt, err := virustotal.NewVirusTotal(s.VirusTotalKey)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n\n\treader := r.Body\n\n\tresult, err := vt.Scan(filename, reader)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n\n\ts.logger.Println(result)\n\t_, _ = w.Write([]byte(fmt.Sprintf(\"%v\\n\", result.Permalink)))\n}\n"
  }
]