├── .github ├── FUNDING.yml └── workflows │ ├── docker-publish-multiarch-dev.yml │ ├── docker-publish-multiarch-release-custom.yml │ ├── docker-publish-multiarch-release.yml │ └── test-code.yml ├── .gitignore ├── .readthedocs.yaml ├── Dockerfile ├── LICENSE.md ├── README.md ├── SECURITY.md ├── _config.yml ├── build ├── Dockerfile ├── entrypoint.sh ├── generateCoverage.sh ├── go-generate │ ├── buildWasm.go │ ├── copyStaticFiles.go │ ├── minifyStaticContent.go │ ├── updateApiRouting.go │ ├── updateProtectedUrls.go │ └── updateVersionNumbers.go ├── go.mod ├── go.sum ├── makefile └── updateCoverage.sh ├── cmd ├── gokapi │ ├── Main.go │ └── Main_test.go ├── wasmdownloader │ └── Main.go └── wasme2e │ └── Main.go ├── dockerentry.sh ├── docs ├── Makefile ├── README.md ├── advanced.rst ├── changelog.rst ├── conf.py ├── contributions.rst ├── examples.rst ├── index.rst ├── make.bat ├── requirements.txt ├── setup.rst ├── static │ └── custom.css ├── update.rst └── usage.rst ├── go.mod ├── go.sum ├── internal ├── configuration │ ├── Configuration.go │ ├── Configuration_test.go │ ├── cloudconfig │ │ ├── CloudConfig.go │ │ └── CloudConfig_test.go │ ├── configupgrade │ │ ├── Upgrade.go │ │ └── Upgrade_test.go │ ├── database │ │ ├── Database.go │ │ ├── Database_test.go │ │ ├── dbabstraction │ │ │ ├── DbAbstraction.go │ │ │ └── DbAbstraction_test.go │ │ ├── dbcache │ │ │ └── DbCache.go │ │ ├── migration │ │ │ ├── Migration.go │ │ │ └── Migration_test.go │ │ └── provider │ │ │ ├── redis │ │ │ ├── Redis.go │ │ │ ├── Redis_test.go │ │ │ ├── apikeys.go │ │ │ ├── e2econfig.go │ │ │ ├── hotlinks.go │ │ │ ├── metadata.go │ │ │ ├── sessions.go │ │ │ └── users.go │ │ │ └── sqlite │ │ │ ├── Sqlite.go │ │ │ ├── Sqlite_test.go │ │ │ ├── apikeys.go │ │ │ ├── e2econfig.go │ │ │ ├── hotlinks.go │ │ │ ├── metadata.go │ │ │ ├── sessions.go │ │ │ └── users.go │ └── setup │ │ ├── ProtectedUrls.go │ │ ├── Setup.go │ │ ├── Setup_test.go │ │ ├── static │ │ └── setup │ │ │ ├── bootstrap │ │ │ └── bootstrap.css │ │ │ ├── chosen │ │ │ ├── chosen-sprite.png │ │ │ ├── chosen-sprite@2x.png │ │ │ ├── chosen.css │ │ │ ├── chosen.jquery.js │ │ │ ├── chosen.jquery.min.js │ │ │ ├── chosen.proto.js │ │ │ └── chosen.proto.min.js │ │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ └── glyphicons-halflings-regular.woff │ │ │ ├── index.html │ │ │ ├── js │ │ │ ├── bootstrap.js │ │ │ ├── html5shiv-3.7.0.js │ │ │ ├── jquery-2.0.3.min.js │ │ │ ├── jquery-2.0.3.min.map │ │ │ ├── prettify.js │ │ │ └── respond-1.3.0.min.js │ │ │ └── src │ │ │ ├── bootstrap-wizard.css │ │ │ ├── bootstrap-wizard.js │ │ │ ├── index.html │ │ │ └── jquery-2.0.3.min.js │ │ └── templates │ │ └── setup.tmpl ├── encryption │ ├── Encryption.go │ ├── Encryption_test.go │ └── end2end │ │ ├── End2End.go │ │ └── End2End_test.go ├── environment │ ├── BuildVars.go │ ├── Environment.go │ ├── Environment_test.go │ └── flagparser │ │ ├── FlagParser.go │ │ ├── FlagParser_Disable.go │ │ ├── FlagParser_Enable.go │ │ └── FlagParser_test.go ├── helper │ ├── OS.go │ ├── OS_test.go │ ├── StringGeneration.go │ ├── StringGeneration_test.go │ └── systemd │ │ ├── Systemd.go │ │ └── Systemd_linux.go ├── logging │ ├── Logging.go │ └── Logging_test.go ├── models │ ├── ApiKey.go │ ├── ApiKey_test.go │ ├── Authentication.go │ ├── AwsConfig.go │ ├── AwsConfig_test.go │ ├── Configuration.go │ ├── Configuration_test.go │ ├── DbConnection.go │ ├── End2EndEncryption.go │ ├── End2EndEncryption_test.go │ ├── FileList.go │ ├── FileList_test.go │ ├── FileUpload.go │ ├── Session.go │ ├── UploadStatus.go │ ├── User.go │ └── User_test.go ├── storage │ ├── FileServing.go │ ├── FileServing_test.go │ ├── chunking │ │ ├── Chunking.go │ │ └── Chunking_test.go │ ├── filesystem │ │ ├── FileSystem.go │ │ ├── fileSystem_test.go │ │ ├── interfaces │ │ │ ├── Interfaces.go │ │ │ └── Interfaces_test.go │ │ ├── localstorage │ │ │ ├── Localstorage.go │ │ │ └── Localstorage_test.go │ │ └── s3filesystem │ │ │ ├── S3filesystem.go │ │ │ ├── S3filesystem_test.go │ │ │ └── aws │ │ │ ├── Aws.go │ │ │ ├── Aws_mock.go │ │ │ ├── Aws_slim.go │ │ │ └── Aws_test.go │ └── processingstatus │ │ ├── ProcessingStatus.go │ │ ├── ProcessingStatus_test.go │ │ └── pstatusdb │ │ ├── PStatusDb.go │ │ └── PStatusDb_test.go ├── test │ ├── TestHelper.go │ ├── TestHelper_test.go │ └── testconfiguration │ │ ├── TestConfiguration.go │ │ └── TestConfiguration_test.go └── webserver │ ├── CustomStaticContent.go │ ├── Webserver.go │ ├── Webserver_test.go │ ├── api │ ├── Api.go │ ├── Api_test.go │ ├── routing.go │ └── routingParsing.go │ ├── authentication │ ├── Authentication.go │ ├── Authentication_test.go │ ├── oauth │ │ ├── Oauth.go │ │ └── Oauth_test.go │ └── sessionmanager │ │ ├── SessionManager.go │ │ └── SessionManager_test.go │ ├── downloadstatus │ ├── DownloadStatus.go │ └── DownloadStatus_test.go │ ├── fileupload │ ├── FileUpload.go │ └── FileUpload_test.go │ ├── headers │ ├── Headers.go │ └── Headers_test.go │ ├── sse │ ├── Sse.go │ └── Sse_test.go │ ├── ssl │ ├── Ssl.go │ └── Ssl_test.go │ └── web │ ├── static │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apidocumentation │ │ ├── index.html │ │ ├── openapi.json │ │ └── swagger │ │ │ ├── LICENSE │ │ │ ├── oauth2-redirect.html │ │ │ ├── openapiui.js │ │ │ ├── swagger-ui-bundle.js │ │ │ ├── swagger-ui-bundle.js.map │ │ │ ├── swagger-ui-standalone-preset.js │ │ │ ├── swagger-ui-standalone-preset.js.map │ │ │ ├── swagger-ui.css │ │ │ ├── swagger-ui.css.map │ │ │ ├── swagger-ui.js │ │ │ └── swagger-ui.js.map │ ├── apple-touch-icon.png │ ├── assets │ │ ├── background.jpg │ │ ├── dist │ │ │ ├── css │ │ │ │ ├── bootstrap.min.css │ │ │ │ ├── bootstrap.min.css.map │ │ │ │ ├── bootstrap.rtl.min.css │ │ │ │ ├── bootstrap.rtl.min.css.map │ │ │ │ ├── datatables.min.css │ │ │ │ ├── dropzone.min.css │ │ │ │ ├── flatpickr.dark.min.css │ │ │ │ ├── flatpickr.min.css │ │ │ │ └── index.html │ │ │ ├── icons │ │ │ │ ├── LICENSE │ │ │ │ ├── bootstrap-icons.min.css │ │ │ │ ├── fonts │ │ │ │ │ ├── bootstrap-icons.woff │ │ │ │ │ ├── bootstrap-icons.woff2 │ │ │ │ │ └── index.html │ │ │ │ └── index.html │ │ │ ├── index.html │ │ │ └── js │ │ │ │ ├── bootstrap.bundle.min.js │ │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ │ ├── clipboard.min.js │ │ │ │ ├── datatables.min.js │ │ │ │ ├── dropzone.min.js │ │ │ │ ├── flatpickr.min.js │ │ │ │ ├── index.html │ │ │ │ ├── jquery.min.js │ │ │ │ └── qrcode.min.js │ │ └── index.html │ ├── css │ │ ├── cover.css │ │ ├── index.html │ │ ├── min │ │ │ ├── gokapi.min.5.css │ │ │ └── index.html │ │ └── uploadProgress.css │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── index.html │ ├── js │ │ ├── admin_api.js │ │ ├── admin_ui_allPages.js │ │ ├── admin_ui_api.js │ │ ├── admin_ui_logs.js │ │ ├── admin_ui_upload.js │ │ ├── admin_ui_users.js │ │ ├── end2end_admin.js │ │ ├── end2end_download.js │ │ ├── index.html │ │ ├── min │ │ │ ├── admin.min.10.js │ │ │ ├── end2end_admin.min.3.js │ │ │ ├── end2end_admin.min.6.js │ │ │ ├── end2end_download.min.3.js │ │ │ ├── end2end_download.min.6.js │ │ │ ├── index.html │ │ │ ├── polyfill.min.js │ │ │ └── streamsaver.min.js │ │ └── streamsaver.js │ ├── robots.txt │ ├── serviceworker │ │ ├── index.html │ │ └── sw.js │ └── site.webmanifest │ └── templates │ ├── expired_file_svg.tmpl │ ├── html_admin.tmpl │ ├── html_api.tmpl │ ├── html_changepw.tmpl │ ├── html_download.tmpl │ ├── html_download_password.tmpl │ ├── html_end2end.tmpl │ ├── html_error.tmpl │ ├── html_error_auth.tmpl │ ├── html_error_header.tmpl │ ├── html_error_int_oauth.tmpl │ ├── html_footer.tmpl │ ├── html_forgotpw.tmpl │ ├── html_header.tmpl │ ├── html_index.tmpl │ ├── html_login.tmpl │ ├── html_logs.tmpl │ ├── html_redirect_filename.tmpl │ ├── html_users.tmpl │ ├── js_custom.tmpl │ ├── js_pagename.tmpl │ └── string_constants.tmpl ├── makefile └── openapi.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: MBulling 2 | custom: ["https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=donate@bulling.mobi&lc=US&item_name=Gokapi&no_note=0&cn=¤cy_code=EUR&bn=PP-DonationsBF:btn_donateCC_LG.gif:NonHosted"] 3 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish-multiarch-dev.yml: -------------------------------------------------------------------------------- 1 | name: Docker Publish Dev Multiarch 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: checkout code 18 | uses: actions/checkout@v2 19 | - name: install buildx 20 | id: buildx 21 | uses: crazy-max/ghaction-docker-buildx@v1 22 | with: 23 | version: latest 24 | - name: login to docker hub 25 | run: echo "${{ secrets.DOCKER_PW }}" | docker login -u "${{ secrets.DOCKER_USER }}" --password-stdin 26 | - name: build the image 27 | run: | 28 | docker buildx build --tag f0rc3/gokapi:latest --platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 . 29 | - name: push the image 30 | run: | 31 | docker buildx build --push --tag f0rc3/gokapi:latest-dev --platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 . 32 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish-multiarch-release-custom.yml: -------------------------------------------------------------------------------- 1 | name: Docker Publish Custom Release Multiarch 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tagname: 7 | description: 'Tag name to be built' 8 | required: true 9 | 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: checkout code 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - run: git checkout tags/${{ github.event.inputs.tagname }} 23 | - name: install buildx 24 | id: buildx 25 | uses: crazy-max/ghaction-docker-buildx@v1 26 | with: 27 | version: latest 28 | - name: login to docker hub 29 | run: echo "${{ secrets.DOCKER_PW }}" | docker login -u "${{ secrets.DOCKER_USER }}" --password-stdin 30 | - name: build and push the image 31 | run: | 32 | docker buildx build --push --tag f0rc3/gokapi:${{ github.event.inputs.tagname }} --platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 . 33 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish-multiarch-release.yml: -------------------------------------------------------------------------------- 1 | name: Docker Publish Release Multiarch 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [released] 7 | 8 | 9 | permissions: 10 | contents: read 11 | 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: oprypin/find-latest-tag@v1 19 | with: 20 | repository: Forceu/Gokapi 21 | releases-only: true 22 | prefix: 'v' 23 | id: latestversion 24 | 25 | - name: checkout code 26 | uses: actions/checkout@v2 27 | - name: install buildx 28 | id: buildx 29 | uses: crazy-max/ghaction-docker-buildx@v1 30 | with: 31 | version: latest 32 | - name: login to docker hub 33 | run: echo "${{ secrets.DOCKER_PW }}" | docker login -u "${{ secrets.DOCKER_USER }}" --password-stdin 34 | - name: build the image 35 | run: | 36 | docker buildx build --tag f0rc3/gokapi:latest --platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 . 37 | - name: push the image 38 | run: | 39 | docker buildx build --push --tag f0rc3/gokapi:latest --platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 . 40 | docker buildx build --push --tag f0rc3/gokapi:${{ steps.latestversion.outputs.tag }} --platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 . 41 | -------------------------------------------------------------------------------- /.github/workflows/test-code.yml: -------------------------------------------------------------------------------- 1 | name: Compile and run unit tests 2 | on: [workflow_dispatch, push, pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: checkout code 13 | uses: actions/checkout@v2 14 | - uses: actions/setup-go@v2 15 | with: 16 | go-version: '^1.20' 17 | - run: go generate ./... 18 | - run: go test ./... -parallel 8 -count=1 --tags=test,awsmock 19 | - run: go test ./... -parallel 8 -count=1 --tags=test,noaws 20 | - run: go test ./... --tags=test,noaws,integration -count=1 21 | - run: GOKAPI_AWS_BUCKET="gokapi" GOKAPI_AWS_REGION="eu-central-1" GOKAPI_AWS_KEY="keyid" GOKAPI_AWS_KEY_SECRET="secret" go test ./... --tags=test,awstest -count=1 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config/ 2 | data/ 3 | .idea/ 4 | Gokapi 5 | build/*.zip 6 | gokapi 7 | docs/_build 8 | wasmServer 9 | internal/webserver/web/main.wasm 10 | internal/webserver/web/e2e.wasm 11 | internal/webserver/web/static/js/wasm_exec.js 12 | internal/webserver/web/static/js/min/wasm_exec.min.js 13 | .vendor/ 14 | custom/ 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.12" 13 | # You can also specify other tool versions: 14 | # nodejs: "19" 15 | # rust: "1.64" 16 | # golang: "1.19" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | python: 23 | install: 24 | - requirements: docs/requirements.txt 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.0-alpine AS build_base 2 | 3 | ## Usage: 4 | ## docker build . -t gokapi 5 | ## docker run -it -v gokapi-data:/app/data -v gokapi-config:/app/config -p 127.0.0.1:53842:53842 gokapi 6 | 7 | RUN mkdir /compile 8 | COPY go.mod /compile 9 | RUN cd /compile && go mod download 10 | 11 | COPY . /compile 12 | 13 | RUN cd /compile && go generate ./... && CGO_ENABLED=0 go build -ldflags="-s -w -X 'github.com/forceu/gokapi/internal/environment.IsDocker=true' -X 'github.com/forceu/gokapi/internal/environment.Builder=Project Docker File' -X 'github.com/forceu/gokapi/internal/environment.BuildTime=$(date)'" -o /compile/gokapi github.com/forceu/gokapi/cmd/gokapi 14 | 15 | FROM alpine:3.19 16 | 17 | 18 | RUN addgroup -S gokapi && adduser -S gokapi -G gokapi 19 | RUN apk update && apk add --no-cache su-exec tini ca-certificates curl tzdata && \ 20 | mkdir /app && touch /app/.isdocker 21 | 22 | COPY dockerentry.sh /app/run.sh 23 | 24 | 25 | COPY --from=build_base /compile/gokapi /app/gokapi 26 | WORKDIR /app 27 | 28 | ENTRYPOINT ["/sbin/tini", "--"] 29 | CMD ["/app/run.sh"] 30 | HEALTHCHECK --interval=10s --timeout=5s --retries=3 CMD curl --fail http://127.0.0.1:53842 || exit 1 31 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We currently support the latest stable version of Gokapi. Security updates are provided on a best-effort basis for the most recent release. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | Latest | ✅ | 10 | | Older | ❌ | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | If you discover a security vulnerability in Gokapi, please **do not open a public issue**. 15 | 16 | Instead, use GitHub’s [**"Report a vulnerability"**](https://github.com/Forceu/Gokapi/security/advisories/new) feature on this repository. This ensures your report stays private and will be reviewed promptly by the maintainers. 17 | 18 | To report a vulnerability: 19 | 20 | 1. Go to the **Security** tab of the Gokapi repository. 21 | 2. Click on **"Report a vulnerability"**. 22 | 3. Fill out the form with as much detail as possible. 23 | 24 | We aim to acknowledge valid reports within **3 business days** and address them as quickly as possible. 25 | 26 | ## Disclosure Policy 27 | 28 | Once a vulnerability is reported, we will: 29 | 30 | 1. Acknowledge receipt within 72 hours. 31 | 2. Investigate and validate the issue. 32 | 3. Develop a fix or mitigation strategy. 33 | 4. Coordinate a release with credit to the reporter (unless anonymity is requested). 34 | 5. Publish a security advisory via GitHub once the fix is released. 35 | 36 | ## Scope 37 | 38 | This policy applies to the Gokapi codebase and documentation in this repository. Vulnerabilities in third-party dependencies should be reported to the appropriate maintainers. 39 | 40 | --- 41 | 42 | Thank you for helping keep Gokapi secure! 43 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.0-bookworm 2 | 3 | ## To compile: 4 | ## cd Gokapi/build/ 5 | ## docker build . --tag gokapi-builder 6 | ## docker run --rm -it -v ../:/usr/src/myapp -w /usr/src/myapp gokapi-builder 7 | 8 | RUN \ 9 | apt-get update && \ 10 | apt-get install -y ca-certificates openssl zip && \ 11 | update-ca-certificates && \ 12 | rm -rf /var/lib/apt 13 | 14 | COPY go.mod /tmp/tmp/go.mod 15 | 16 | RUN cd /tmp/tmp/ && go mod download && rm -r /tmp/tmp 17 | 18 | COPY entrypoint.sh /entrypoint.sh 19 | 20 | ENTRYPOINT ["/entrypoint.sh"] 21 | -------------------------------------------------------------------------------- /build/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | targets=${@-"darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/riscv64 windows/amd64 windows/arm64"} 6 | 7 | cd /usr/src/myapp 8 | go generate ./... 9 | 10 | for target in $targets; do 11 | os="$(echo $target | cut -d '/' -f1)" 12 | arch="$(echo $target | cut -d '/' -f2)" 13 | output="build/gokapi-${os}_${arch}" 14 | if [ $os = "windows" ]; then 15 | output+='.exe' 16 | fi 17 | 18 | echo "----> Building Gokapi for $target" 19 | GOOS=$os GOARCH=$arch CGO_ENABLED=0 go build -ldflags="-s -w -X 'github.com/forceu/gokapi/internal/environment.Builder=Github Release Builder' -X 'github.com/forceu/gokapi/internal/environment.BuildTime=$(date)'" -o $output github.com/forceu/gokapi/cmd/gokapi 20 | zip -j $output.zip $output >/dev/null 21 | rm $output 22 | done 23 | 24 | echo "----> Build is complete. List of files at build/:" 25 | cd build/ 26 | ls -l gokapi-* 27 | -------------------------------------------------------------------------------- /build/generateCoverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd .. 3 | go test ./... -parallel 8 --tags=test,awsmock -coverprofile=/tmp/coverage1.out 4 | go test ./... -parallel 8 --tags=test,noaws -coverprofile=/tmp/coverage2.out 5 | go test ./... -parallel 8 --tags=test,integration,noaws -coverprofile=/tmp/coverage3.out 6 | GOKAPI_AWS_BUCKET="gokapi" GOKAPI_AWS_REGION="eu-central-1" GOKAPI_AWS_KEY="keyid" GOKAPI_AWS_KEY_SECRET="secret" go test ./... -parallel 8 -coverprofile=/tmp/coverage4.out --tags=test,awstest 7 | 8 | which gocovmerge > /dev/null 9 | if [ $? -eq 0 ]; then 10 | gocovmerge /tmp/coverage1.out /tmp/coverage2.out /tmp/coverage3.out /tmp/coverage4.out > /tmp/coverage.out 11 | go tool cover -html=/tmp/coverage.out 12 | else 13 | go tool cover -html=/tmp/coverage4.out 14 | fi 15 | -------------------------------------------------------------------------------- /build/go-generate/buildWasm.go: -------------------------------------------------------------------------------- 1 | //go:build gogenerate 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | func main() { 12 | err := buildWasmModule("github.com/forceu/gokapi/cmd/wasmdownloader", "../../internal/webserver/web/main.wasm") 13 | if err != nil { 14 | fmt.Println("ERROR: Could not compile wasmdownloader") 15 | fmt.Println(err) 16 | os.Exit(2) 17 | } 18 | fmt.Println("Compiled Downloader WASM module") 19 | err = buildWasmModule("github.com/forceu/gokapi/cmd/wasme2e", "../../internal/webserver/web/e2e.wasm") 20 | if err != nil { 21 | fmt.Println("ERROR: Could not compile wasme2e") 22 | fmt.Println(err) 23 | os.Exit(3) 24 | } 25 | fmt.Println("Compiled E2E WASM module") 26 | } 27 | 28 | func buildWasmModule(src string, dst string) error { 29 | cmd := exec.Command("go", "build", "-o", dst, src) 30 | cmd.Env = append(os.Environ(), 31 | "GOOS=js", "GOARCH=wasm") 32 | return cmd.Run() 33 | } 34 | -------------------------------------------------------------------------------- /build/go-generate/copyStaticFiles.go: -------------------------------------------------------------------------------- 1 | //go:build gogenerate 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "go/build" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | func main() { 13 | copyFile(build.Default.GOROOT+"/lib/wasm/wasm_exec.js", "../../internal/webserver/web/static/js/wasm_exec.js") 14 | copyFile("../../go.mod", "../../build/go.mod") 15 | copyFile("../../openapi.json", "../../internal/webserver/web/static/apidocumentation/openapi.json") 16 | } 17 | 18 | // copyFile should only be used for small files 19 | func copyFile(src string, dst string) { 20 | data, err := os.ReadFile(src) 21 | if err != nil { 22 | fmt.Println("ERROR: Cannot read " + src) 23 | fmt.Println(err) 24 | os.Exit(1) 25 | } 26 | err = os.WriteFile(dst, data, 0644) 27 | if err != nil { 28 | fmt.Println("ERROR: Cannot write " + dst) 29 | fmt.Println(err) 30 | os.Exit(2) 31 | } 32 | filename := filepath.Base(src) 33 | fmt.Println("Copied " + filename) 34 | } 35 | -------------------------------------------------------------------------------- /build/go-generate/updateProtectedUrls.go: -------------------------------------------------------------------------------- 1 | //go:build gogenerate 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "regexp" 9 | "strings" 10 | 11 | slices "golang.org/x/exp/slices" 12 | ) 13 | 14 | const fileSetup = "../../internal/webserver/Webserver.go" 15 | const fileSetupConstants = "../../internal/configuration/setup/ProtectedUrls.go" 16 | const fileDocumentation = "../../docs/setup.rst" 17 | 18 | func main() { 19 | checkFileExistsUrl(fileSetup) 20 | checkFileExistsUrl(fileSetupConstants) 21 | checkFileExistsUrl(fileDocumentation) 22 | urls := parseProtectedUrls() 23 | writeConstantFile(urls) 24 | writeDocumentationFile(urls) 25 | } 26 | 27 | func checkFileExistsUrl(filename string) { 28 | info, err := os.Stat(filename) 29 | if os.IsNotExist(err) { 30 | fmt.Println("ERROR: File does not exist: " + filename) 31 | os.Exit(2) 32 | } 33 | if info.IsDir() { 34 | fmt.Println("ERROR: File is actually directory: " + filename) 35 | os.Exit(3) 36 | } 37 | } 38 | 39 | func parseProtectedUrls() []string { 40 | source, err := os.ReadFile(fileSetup) 41 | if err != nil { 42 | fmt.Println("ERROR: Cannot read file: ") 43 | fmt.Println(err) 44 | os.Exit(4) 45 | } 46 | urls := make([]string, 0) 47 | regex := regexp.MustCompile(`mux\.HandleFunc\("([^"]+)",\s*requireLogin\(`) 48 | matches := regex.FindAllStringSubmatch(string(source), -1) 49 | for _, match := range matches { 50 | fn := strings.TrimSpace(match[1]) 51 | urls = append(urls, fn) 52 | } 53 | if len(urls) < 4 { 54 | fmt.Println("ERROR: Could not find protected URLs") 55 | os.Exit(5) 56 | } 57 | return urls 58 | } 59 | 60 | func writeConstantFile(urls []string) { 61 | var output = `// Code generated by updateProtectedUrls.go - DO NOT EDIT. 62 | package setup 63 | 64 | // Do not modify: This is an automatically generated file created by updateProtectedUrls.go 65 | // It contains all URLs that need to be protected when using an external authentication. 66 | 67 | // protectedUrls contains a list of URLs that need to be protected if authentication is disabled. 68 | // This list will be displayed during the setup 69 | var protectedUrls = []string{` 70 | 71 | slices.Sort(urls) 72 | for i, url := range urls { 73 | output = output + "\"" + url + "\"" 74 | if i < len(urls)-1 { 75 | output = output + ", " 76 | } else { 77 | output = output + "}\n" 78 | } 79 | } 80 | err := os.WriteFile(fileSetupConstants, []byte(output), 0664) 81 | if err != nil { 82 | fmt.Println("ERROR: Cannot write file:") 83 | fmt.Println(err) 84 | os.Exit(1) 85 | } 86 | fmt.Println("Updated protected URLs variable") 87 | } 88 | 89 | func writeDocumentationFile(urls []string) { 90 | documentationContent, err := os.ReadFile(fileDocumentation) 91 | if err != nil { 92 | fmt.Println("ERROR: Cannot read file:") 93 | fmt.Println(err) 94 | os.Exit(6) 95 | } 96 | output := "proxy:\n\n" 97 | for _, url := range urls { 98 | output = output + "- ``" + url + "``\n" 99 | } 100 | regex := regexp.MustCompile(`proxy:(?:\r?\n)+((?:- ` + "``" + `\/\w+` + "``" + `\r?\n)+)`) 101 | matches := regex.FindAllIndex(documentationContent, -1) 102 | if len(matches) != 1 { 103 | fmt.Println("ERROR: Not one match found exactly for documentation") 104 | os.Exit(7) 105 | } 106 | documentationContent = regex.ReplaceAll(documentationContent, []byte(output)) 107 | err = os.WriteFile(fileDocumentation, documentationContent, 0664) 108 | if err != nil { 109 | fmt.Println("ERROR: Cannot write file:") 110 | fmt.Println(err) 111 | os.Exit(8) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /build/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/forceu/gokapi 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/NYTimes/gziphandler v1.1.1 7 | github.com/alicebob/miniredis/v2 v2.34.0 8 | github.com/aws/aws-sdk-go v1.55.6 9 | github.com/caarlos0/env/v6 v6.10.1 10 | github.com/gomodule/redigo v1.9.2 11 | github.com/jinzhu/copier v0.4.0 12 | github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999 13 | github.com/juju/ratelimit v1.0.2 14 | github.com/secure-io/sio-go v0.3.1 15 | golang.org/x/crypto v0.35.0 16 | golang.org/x/oauth2 v0.27.0 17 | golang.org/x/sync v0.11.0 18 | golang.org/x/term v0.29.0 19 | gopkg.in/yaml.v3 v3.0.1 20 | modernc.org/sqlite v1.35.0 21 | ) 22 | 23 | require ( 24 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect 25 | github.com/dustin/go-humanize v1.0.1 // indirect 26 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 27 | github.com/google/uuid v1.6.0 // indirect 28 | github.com/jmespath/go-jmespath v0.4.0 // indirect 29 | github.com/mattn/go-isatty v0.0.20 // indirect 30 | github.com/ncruces/go-strftime v0.1.9 // indirect 31 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 32 | github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect 33 | github.com/tdewolff/minify/v2 v2.20.34 // indirect 34 | github.com/tdewolff/parse/v2 v2.7.15 // indirect 35 | github.com/yuin/gopher-lua v1.1.1 // indirect 36 | go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect 37 | golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect 38 | golang.org/x/tools v0.30.0 // indirect 39 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 40 | modernc.org/libc v1.61.13 // indirect 41 | modernc.org/mathutil v1.7.1 // indirect 42 | modernc.org/memory v1.8.2 // indirect 43 | modernc.org/strutil v1.2.1 // indirect 44 | modernc.org/token v1.1.0 // indirect 45 | ) 46 | 47 | require ( 48 | github.com/coreos/go-oidc/v3 v3.12.0 49 | golang.org/x/sys v0.30.0 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /build/makefile: -------------------------------------------------------------------------------- 1 | # Define variables 2 | IMAGE_NAME=gokapi-builder 3 | CONTAINER_WORK_DIR=/usr/src/myapp 4 | #To use podman, use make CONTAINER_TOOL=podman 5 | CONTAINER_TOOL?=podman 6 | 7 | # Default target 8 | all: compile 9 | 10 | # Compile target 11 | compile: 12 | @echo "Creating build container image for $(CONTAINER_TOOL)..." 13 | $(CONTAINER_TOOL) build . --tag $(IMAGE_NAME) 14 | @echo "Running build container to generate binaries" 15 | $(CONTAINER_TOOL) run --rm -it -v ../:$(CONTAINER_WORK_DIR) -w $(CONTAINER_WORK_DIR) $(IMAGE_NAME) 16 | 17 | # Deletes binaries 18 | clean: 19 | @echo "Deleting binaries..." 20 | rm -f ./*.zip 21 | 22 | # Deletes binaries and docker image 23 | clean-all: 24 | @echo "Deleting binaries and docker image..." 25 | rm -f ./*.zip 26 | $(CONTAINER_TOOL) image rm $(IMAGE_NAME) 27 | 28 | 29 | # PHONY targets to avoid conflicts with files of the same name 30 | .PHONY: all compile clean clean-all 31 | -------------------------------------------------------------------------------- /build/updateCoverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #Called by go generate 3 | #Creates the coverage for the README.md file if gopherbadger is installed 4 | 5 | cd ../ 6 | which gopherbadger > /dev/null 7 | if [ $? -eq 0 ]; then 8 | gopherbadger -png=false -md=README.md -tags "test,awsmock" > /dev/null 9 | rm coverage.out 10 | echo "Updated coverage in readme file" 11 | else 12 | echo "Gopherbadger not installed, not updating coverage" 13 | fi 14 | -------------------------------------------------------------------------------- /cmd/gokapi/Main_test.go: -------------------------------------------------------------------------------- 1 | //go:build !integration && test 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/forceu/gokapi/internal/environment/flagparser" 7 | "github.com/forceu/gokapi/internal/test" 8 | "github.com/forceu/gokapi/internal/test/testconfiguration" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | testconfiguration.Create(false) 15 | exitVal := m.Run() 16 | testconfiguration.Delete() 17 | os.Exit(exitVal) 18 | } 19 | 20 | func TestShowVersion(t *testing.T) { 21 | showVersion(flagparser.MainFlags{}) 22 | osExit = test.ExitCode(t, 0) 23 | showVersion(flagparser.MainFlags{ShowVersion: true}) 24 | } 25 | 26 | func TestNoResetPw(t *testing.T) { 27 | reconfigureServer(flagparser.MainFlags{}) 28 | } 29 | 30 | func TestCreateSsl(t *testing.T) { 31 | test.FileDoesNotExist(t, "test/ssl.key") 32 | createSsl(flagparser.MainFlags{}) 33 | test.FileDoesNotExist(t, "test/ssl.key") 34 | createSsl(flagparser.MainFlags{CreateSsl: true}) 35 | test.FileExists(t, "test/ssl.key") 36 | } 37 | -------------------------------------------------------------------------------- /dockerentry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ "$DOCKER_NONROOT" = "true" ]; then 3 | echo "Setting permissions" && \ 4 | chown -R gokapi:gokapi /app && \ 5 | chmod -R 700 /app && \ 6 | echo "Starting application" && \ 7 | exec su-exec gokapi:gokapi /app/gokapi "$@" 8 | else 9 | exec /app/gokapi "$@" 10 | fi 11 | 12 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Gokapi Documentation 2 | 3 | 4 | This folder contains the documentation for Gokapi. You can find it online at https://gokapi.readthedocs.io/en/stable/ 5 | 6 | 7 | The unstable branch contains the documentation for the current master branch and can be read online at https://gokapi.readthedocs.io/en/latest/ 8 | 9 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Gokapi Documentation' 21 | copyright = '2025, Marc Ole Bulling' 22 | author = 'Marc Ole Bulling' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '1.8.0' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx_rtd_theme" 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = "sphinx_rtd_theme" 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = ['static'] 57 | 58 | html_css_files = ['custom.css'] 59 | 60 | master_doc = 'index' 61 | #autosectionlabel_prefix_document = True 62 | 63 | html_theme_options = { 64 | 'navigation_depth': 5, 65 | } 66 | -------------------------------------------------------------------------------- /docs/contributions.rst: -------------------------------------------------------------------------------- 1 | .. _contributions: 2 | 3 | 4 | ============= 5 | Contributions 6 | ============= 7 | 8 | All contributions are very welcome! If you have an issue or would like to add a pull request, please visit our Github page: 9 | 10 | https://github.com/Forceu/gokapi 11 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | 3 | =========================== 4 | Gokapi 5 | =========================== 6 | 7 | Gokapi is a lightweight server to share files, which expire after a set amount of downloads or days. It is similar to the discontinued Firefox Send, with the difference that only the admin is allowed to upload files. 8 | 9 | This enables companies or individuals to share their files very easily and having them removed afterwards, therefore saving disk space and having control over who downloads the file from the server. 10 | 11 | Identical files will be deduplicated. An API is available to interact with Gokapi. AWS S3 compatible storage can be used instead of local storage. Customization is very easy with HTML/CSS knowledge. 12 | 13 | 14 | Contents 15 | ======== 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | setup 21 | usage 22 | update 23 | advanced 24 | examples 25 | contributions 26 | changelog 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.2.6 2 | sphinx_rtd_theme==2.0.0 3 | readthedocs-sphinx-search==0.3.2 4 | -------------------------------------------------------------------------------- /docs/static/custom.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 80% !important; 3 | } 4 | -------------------------------------------------------------------------------- /docs/update.rst: -------------------------------------------------------------------------------- 1 | .. _update: 2 | 3 | ====================== 4 | Updating Gokapi 5 | ====================== 6 | 7 | *************** 8 | Docker 9 | *************** 10 | 11 | To update, run the following command: 12 | :: 13 | 14 | docker pull f0rc3/gokapi:YOURTAG 15 | 16 | Then stop the running container and follow the same steps as in SETUP. All userdata will be preserved, as it is saved to the ``gokapi-data`` and ``gokapi-data`` volume (``-v`` argument during creation) 17 | 18 | ******************* 19 | Native deployment 20 | ******************* 21 | 22 | Stable version 23 | ============== 24 | 25 | To update, download the latest release and unzip it to the directory that contains the old version. Overwrite any existing files. 26 | 27 | 28 | Unstable version 29 | ================= 30 | 31 | To update, execute the command ``git pull`` and then rebuild the binary with ``go build Gokapi/cmd/gokapi``. 32 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | ===== 4 | Usage 5 | ===== 6 | 7 | Admin Menu 8 | ================ 9 | 10 | 11 | General 12 | ---------------- 13 | 14 | After you have started the Gokapi server, you can login using the your admin credentials by going to `http(s)://your.gokapi.url/admin`` 15 | 16 | There you can list and manage files and upload new files. You will also see three fields: 17 | 18 | - *Allowed downloads* lets you set how many times a file can be downloaded before it gets deleted 19 | - *Expiry in days* lets you set after how many days a file gets deleted latest 20 | - *Password* lets you set a password that a user needs to enter before downloading the file. Please note that the file on the storage server is not encrypted. 21 | 22 | Uploading new files 23 | --------------------- 24 | 25 | To upload, drag and drop a file, folder or multiple files to the Upload Zone. You can also directly paste an image from the clipboard. If you want to change the default expiry conditions, this has to be done before uploading. For each file an entry in the table will appear with a download link. 26 | 27 | Identical files are deduplicated, which means if you upload a file twice, it will only be stored once. 28 | 29 | Sharing files 30 | --------------- 31 | 32 | Once you uploaded an file, you will see the options *Copy URL* and *Copy Hotlink*. By clicking on *Copy URL*, you copy the URL for the Download page to your clipboard. A user can then download the file from that page. 33 | 34 | If a file does not require client-side decryption, you can also use the *Copy Hotlink* button. The hotlink URL is a direct link to the file and can for example be posted as an image on a forum or on a website. Each view counts as a download. Although Gokapi sets a Header to explicitly disallow caching, some browsers or external caches may still cache the image if they are not compliant. 35 | 36 | 37 | File deletion 38 | --------------- 39 | 40 | Every hour Gokapi runs a cleanup routine which deletes all files from the storage that have been expired. If you click on the *Delete* button in the list, that file will be deleted from the disk immediately. AWS files are deleted after 24 hours, as of right now there is no proper way to find out if a download has been completed. 41 | 42 | 43 | API Menu 44 | =============== 45 | 46 | In the API menu you can create API keys, which can be used for API access. Please refer to :ref:`api`. 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/forceu/gokapi 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/NYTimes/gziphandler v1.1.1 7 | github.com/alicebob/miniredis/v2 v2.34.0 8 | github.com/aws/aws-sdk-go v1.55.6 9 | github.com/caarlos0/env/v6 v6.10.1 10 | github.com/gomodule/redigo v1.9.2 11 | github.com/jinzhu/copier v0.4.0 12 | github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999 13 | github.com/juju/ratelimit v1.0.2 14 | github.com/secure-io/sio-go v0.3.1 15 | golang.org/x/crypto v0.35.0 16 | golang.org/x/oauth2 v0.27.0 17 | golang.org/x/sync v0.11.0 18 | golang.org/x/term v0.29.0 19 | gopkg.in/yaml.v3 v3.0.1 20 | modernc.org/sqlite v1.35.0 21 | ) 22 | 23 | require ( 24 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect 25 | github.com/dustin/go-humanize v1.0.1 // indirect 26 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 27 | github.com/google/uuid v1.6.0 // indirect 28 | github.com/jmespath/go-jmespath v0.4.0 // indirect 29 | github.com/mattn/go-isatty v0.0.20 // indirect 30 | github.com/ncruces/go-strftime v0.1.9 // indirect 31 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 32 | github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect 33 | github.com/tdewolff/minify/v2 v2.20.34 // indirect 34 | github.com/tdewolff/parse/v2 v2.7.15 // indirect 35 | github.com/yuin/gopher-lua v1.1.1 // indirect 36 | go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect 37 | golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect 38 | golang.org/x/tools v0.30.0 // indirect 39 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 40 | modernc.org/libc v1.61.13 // indirect 41 | modernc.org/mathutil v1.7.1 // indirect 42 | modernc.org/memory v1.8.2 // indirect 43 | modernc.org/strutil v1.2.1 // indirect 44 | modernc.org/token v1.1.0 // indirect 45 | ) 46 | 47 | require ( 48 | github.com/coreos/go-oidc/v3 v3.12.0 49 | golang.org/x/sys v0.30.0 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /internal/configuration/Configuration_test.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/configuration/cloudconfig" 5 | "github.com/forceu/gokapi/internal/configuration/configupgrade" 6 | "github.com/forceu/gokapi/internal/models" 7 | "github.com/forceu/gokapi/internal/test" 8 | "github.com/forceu/gokapi/internal/test/testconfiguration" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | testconfiguration.Create(false) 15 | exitVal := m.Run() 16 | testconfiguration.Delete() 17 | os.Exit(exitVal) 18 | } 19 | 20 | func TestLoad(t *testing.T) { 21 | test.IsEqualBool(t, Exists(), true) 22 | Load() 23 | test.IsEqualString(t, Environment.ConfigDir, "test") 24 | test.IsEqualString(t, serverSettings.Port, "127.0.0.1:53843") 25 | test.IsEqualString(t, serverSettings.Authentication.Username, "test") 26 | test.IsEqualString(t, serverSettings.ServerUrl, "http://127.0.0.1:53843/") 27 | test.IsEqualString(t, HashPassword("testtest", false), "10340aece68aa4fb14507ae45b05506026f276cf") 28 | test.IsEqualBool(t, serverSettings.UseSsl, false) 29 | 30 | _ = os.Setenv("GOKAPI_LENGTH_ID", "20") 31 | _ = os.Setenv("GOKAPI_LENGTH_HOTLINK_ID", "25") 32 | Load() 33 | test.IsEqualInt(t, serverSettings.LengthId, 20) 34 | test.IsEqualInt(t, serverSettings.LengthHotlinkId, 25) 35 | _ = os.Unsetenv("GOKAPI_LENGTH_ID") 36 | _ = os.Unsetenv("GOKAPI_LENGTH_HOTLINK_ID") 37 | test.IsEqualInt(t, serverSettings.ConfigVersion, configupgrade.CurrentConfigVersion) 38 | testconfiguration.Create(false) 39 | Load() 40 | } 41 | 42 | func TestHashPassword(t *testing.T) { 43 | test.IsEqualString(t, HashPassword("123", false), "423b63a68c68bd7e07b14590927c1e9a473fe035") 44 | test.IsEqualString(t, HashPassword("", false), "") 45 | test.IsEqualString(t, HashPassword("123", true), "7b30508aa9b233ab4b8a11b2af5816bdb58ca3e7") 46 | } 47 | 48 | func TestHashPasswordCustomSalt(t *testing.T) { 49 | test.IsEmpty(t, HashPasswordCustomSalt("", "123")) 50 | test.IsEqualString(t, HashPasswordCustomSalt("test", "salt"), "f438229716cab43569496f3a3630b3727524b81b") 51 | defer test.ExpectPanic(t) 52 | HashPasswordCustomSalt("1234", "") 53 | } 54 | 55 | func TestLoadFromSetup(t *testing.T) { 56 | newConfig := models.Configuration{ 57 | Authentication: models.AuthenticationConfig{}, 58 | Port: "localhost:123", 59 | ServerUrl: "serverurl", 60 | RedirectUrl: "redirect", 61 | ConfigVersion: configupgrade.CurrentConfigVersion, 62 | LengthId: 10, 63 | DataDir: "test", 64 | MaxMemory: 10, 65 | UseSsl: true, 66 | MaxFileSizeMB: 199, 67 | DatabaseUrl: "sqlite://./test/gokapi.sqlite", 68 | } 69 | newCloudConfig := cloudconfig.CloudConfig{Aws: models.AwsConfig{ 70 | Bucket: "bucket", 71 | Region: "region", 72 | KeyId: "keyid", 73 | KeySecret: "secret", 74 | Endpoint: "", 75 | }} 76 | 77 | testconfiguration.WriteCloudConfigFile(true) 78 | LoadFromSetup(newConfig, nil, End2EndReconfigParameters{}, "") 79 | test.FileDoesNotExist(t, "test/cloudconfig.yml") 80 | test.IsEqualString(t, serverSettings.RedirectUrl, "redirect") 81 | 82 | LoadFromSetup(newConfig, &newCloudConfig, End2EndReconfigParameters{}, "") 83 | test.FileExists(t, "test/cloudconfig.yml") 84 | config, ok := cloudconfig.Load() 85 | test.IsEqualBool(t, ok, true) 86 | test.IsEqualString(t, config.Aws.KeyId, "keyid") 87 | test.IsEqualString(t, serverSettings.ServerUrl, "serverurl") 88 | } 89 | 90 | func TestUsesHttps(t *testing.T) { 91 | usesHttps = false 92 | test.IsEqualBool(t, UsesHttps(), false) 93 | usesHttps = true 94 | test.IsEqualBool(t, UsesHttps(), true) 95 | } 96 | -------------------------------------------------------------------------------- /internal/configuration/cloudconfig/CloudConfig.go: -------------------------------------------------------------------------------- 1 | package cloudconfig 2 | 3 | import ( 4 | "fmt" 5 | "github.com/forceu/gokapi/internal/environment" 6 | "github.com/forceu/gokapi/internal/helper" 7 | "github.com/forceu/gokapi/internal/models" 8 | "gopkg.in/yaml.v3" 9 | "io/ioutil" 10 | "os" 11 | ) 12 | 13 | // CloudConfig contains all configuration values / credentials for cloud storage 14 | type CloudConfig struct { 15 | Aws models.AwsConfig `yaml:"aws"` 16 | } 17 | 18 | // Load loads cloud storage configuration / credentials from env variables or data/cloudconfig.yml 19 | func Load() (CloudConfig, bool) { 20 | env := environment.New() 21 | if env.IsAwsProvided() { 22 | return loadFromEnv(&env), true 23 | } 24 | path := env.ConfigDir + "/cloudconfig.yml" 25 | if helper.FileExists(path) { 26 | return loadFromFile(path) 27 | } 28 | return CloudConfig{}, false 29 | } 30 | 31 | // Write saves the cloudconfig file to the set config path 32 | func Write(config CloudConfig) error { 33 | _, configDir, _, awsConfigPath := environment.GetConfigPaths() 34 | helper.CreateDir(configDir) 35 | file, err := os.OpenFile(awsConfigPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 36 | if err != nil { 37 | return err 38 | } 39 | defer file.Close() 40 | 41 | encoder := yaml.NewEncoder(file) 42 | err = encoder.Encode(config) 43 | if err != nil { 44 | return err 45 | } 46 | return nil 47 | } 48 | 49 | // Delete removes the cloud config file from the set config path 50 | func Delete() error { 51 | _, _, _, awsConfigPath := environment.GetConfigPaths() 52 | if helper.FileExists(awsConfigPath) { 53 | err := os.Remove(awsConfigPath) 54 | if err != nil { 55 | return err 56 | } 57 | } 58 | return nil 59 | } 60 | 61 | func loadFromEnv(env *environment.Environment) CloudConfig { 62 | return CloudConfig{Aws: models.AwsConfig{ 63 | Bucket: env.AwsBucket, 64 | Region: env.AwsRegion, 65 | Endpoint: env.AwsEndpoint, 66 | KeyId: env.AwsKeyId, 67 | KeySecret: env.AwsKeySecret, 68 | }} 69 | } 70 | 71 | func loadFromFile(path string) (CloudConfig, bool) { 72 | var result CloudConfig 73 | file, err := ioutil.ReadFile(path) 74 | if err != nil { 75 | fmt.Println("Warning: Unable to read cloudconfig.yml!") 76 | return CloudConfig{}, false 77 | } 78 | err = yaml.Unmarshal(file, &result) 79 | if err != nil { 80 | fmt.Println("Warning: cloudconfig.yml contains invalid yaml!") 81 | return CloudConfig{}, false 82 | } 83 | return result, true 84 | } 85 | -------------------------------------------------------------------------------- /internal/configuration/cloudconfig/CloudConfig_test.go: -------------------------------------------------------------------------------- 1 | package cloudconfig 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "github.com/forceu/gokapi/internal/test" 6 | "github.com/forceu/gokapi/internal/test/testconfiguration" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestMain(m *testing.M) { 12 | testconfiguration.Create(false) 13 | exitVal := m.Run() 14 | testconfiguration.Delete() 15 | os.Exit(exitVal) 16 | } 17 | 18 | func TestLoad(t *testing.T) { 19 | os.Unsetenv("GOKAPI_AWS_REGION") 20 | os.Unsetenv("GOKAPI_AWS_KEY") 21 | os.Unsetenv("GOKAPI_AWS_KEY_SECRET") 22 | _, ok := Load() 23 | test.IsEqualBool(t, ok, false) 24 | testconfiguration.WriteCloudConfigFile(true) 25 | os.Setenv("GOKAPI_AWS_BUCKET", "test") 26 | os.Setenv("GOKAPI_AWS_REGION", "test") 27 | os.Setenv("GOKAPI_AWS_KEY", "test") 28 | os.Setenv("GOKAPI_AWS_KEY_SECRET", "test") 29 | config, ok := Load() 30 | test.IsEqualBool(t, ok, true) 31 | test.IsEqualBool(t, config.Aws == models.AwsConfig{ 32 | Bucket: "test", 33 | Region: "test", 34 | Endpoint: "", 35 | KeyId: "test", 36 | KeySecret: "test", 37 | }, true) 38 | os.Unsetenv("GOKAPI_AWS_BUCKET") 39 | config, ok = Load() 40 | savedConfig := models.AwsConfig{ 41 | Bucket: "gokapi", 42 | Region: "test-region", 43 | Endpoint: "test-endpoint", 44 | KeyId: "test-keyid", 45 | KeySecret: "test-secret", 46 | } 47 | test.IsEqualBool(t, ok, true) 48 | test.IsEqualBool(t, config.Aws == savedConfig, true) 49 | os.Unsetenv("GOKAPI_AWS_REGION") 50 | os.Unsetenv("GOKAPI_AWS_KEY") 51 | os.Unsetenv("GOKAPI_AWS_KEY_SECRET") 52 | config, ok = Load() 53 | test.IsEqualBool(t, ok, true) 54 | test.IsEqualBool(t, config.Aws == savedConfig, true) 55 | os.Remove("test/cloudconfig.yml") 56 | config, ok = Load() 57 | test.IsEqualBool(t, ok, false) 58 | test.IsEqualBool(t, config.Aws == models.AwsConfig{}, true) 59 | testconfiguration.WriteCloudConfigFile(false) 60 | config, ok = Load() 61 | test.IsEqualBool(t, ok, false) 62 | test.IsEqualBool(t, config.Aws == models.AwsConfig{}, true) 63 | } 64 | 65 | func TestWrite(t *testing.T) { 66 | err := os.Remove("test/cloudconfig.yml") 67 | test.IsNil(t, err) 68 | test.FileDoesNotExist(t, "test/cloudconfig.yml") 69 | config := CloudConfig{Aws: models.AwsConfig{ 70 | Bucket: "test1", 71 | Region: "test2", 72 | Endpoint: "test3", 73 | KeyId: "test4", 74 | KeySecret: "test5", 75 | }} 76 | err = Write(config) 77 | test.IsNil(t, err) 78 | test.FileExists(t, "test/cloudconfig.yml") 79 | newConfig, ok := Load() 80 | test.IsEqualBool(t, ok, true) 81 | test.IsEqualBool(t, newConfig.Aws == config.Aws, true) 82 | err = os.Chmod("test/cloudconfig.yml", 0000) 83 | test.IsNil(t, err) 84 | err = Write(config) 85 | test.IsNotNil(t, err) 86 | } 87 | 88 | func TestDelete(t *testing.T) { 89 | test.FileExists(t, "test/cloudconfig.yml") 90 | err := os.Chmod("test/cloudconfig.yml", 0000) 91 | test.IsNil(t, err) 92 | err = Delete() 93 | test.IsNil(t, err) 94 | test.FileDoesNotExist(t, "test/cloudconfig.yml") 95 | _, result := loadFromFile("test/cloudconfig.yml") 96 | test.IsEqualBool(t, result, false) 97 | } 98 | -------------------------------------------------------------------------------- /internal/configuration/configupgrade/Upgrade_test.go: -------------------------------------------------------------------------------- 1 | package configupgrade 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/environment" 5 | "github.com/forceu/gokapi/internal/models" 6 | "github.com/forceu/gokapi/internal/test" 7 | "github.com/forceu/gokapi/internal/test/testconfiguration" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestMain(m *testing.M) { 13 | testconfiguration.Create(false) 14 | exitVal := m.Run() 15 | testconfiguration.Delete() 16 | os.Exit(exitVal) 17 | } 18 | 19 | var oldConfigFile = models.Configuration{ 20 | Authentication: models.AuthenticationConfig{}, 21 | Port: "127.0.0.1:53844", 22 | ServerUrl: "https://gokapi.url/", 23 | RedirectUrl: "https://github.com/Forceu/Gokapi/", 24 | } 25 | 26 | func TestUpgradeDb(t *testing.T) { 27 | exitCode := 0 28 | osExit = func(code int) { 29 | exitCode = code 30 | } 31 | env := environment.New() 32 | // Too old to update 33 | oldConfigFile.ConfigVersion = minConfigVersion - 1 34 | upgradeDone := DoUpgrade(&oldConfigFile, &env) 35 | test.IsEqualBool(t, upgradeDone, true) 36 | test.IsEqualInt(t, exitCode, 1) 37 | 38 | // Updatable version 39 | exitCode = 0 40 | oldConfigFile.ConfigVersion = 21 41 | upgradeDone = DoUpgrade(&oldConfigFile, &env) 42 | test.IsEqualBool(t, upgradeDone, true) 43 | // TODO 44 | test.IsEqualInt(t, exitCode, 0) 45 | 46 | // Current Version 47 | exitCode = 0 48 | oldConfigFile.ConfigVersion = CurrentConfigVersion 49 | upgradeDone = DoUpgrade(&oldConfigFile, &env) 50 | test.IsEqualBool(t, upgradeDone, false) 51 | test.IsEqualInt(t, exitCode, 0) 52 | 53 | } 54 | -------------------------------------------------------------------------------- /internal/configuration/database/dbabstraction/DbAbstraction_test.go: -------------------------------------------------------------------------------- 1 | package dbabstraction 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "github.com/forceu/gokapi/internal/test" 6 | "testing" 7 | ) 8 | 9 | var configSqlite = models.DbConnection{ 10 | Type: 0, // dbabstraction.TypeSqlite 11 | } 12 | 13 | var configRedis = models.DbConnection{ 14 | Type: 1, // dbabstraction.TypeRedis 15 | } 16 | 17 | func TestGetNew(t *testing.T) { 18 | result, err := GetNew(configSqlite) 19 | test.IsNotNil(t, err) 20 | test.IsEqualInt(t, result.GetType(), 0) 21 | result, err = GetNew(configRedis) 22 | test.IsNotNil(t, err) 23 | test.IsEqualInt(t, result.GetType(), 1) 24 | 25 | _, err = GetNew(models.DbConnection{Type: 2}) 26 | test.IsNotNil(t, err) 27 | } 28 | -------------------------------------------------------------------------------- /internal/configuration/database/dbcache/DbCache.go: -------------------------------------------------------------------------------- 1 | package dbcache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | var lastOnlineTimeUpdate map[int]int64 9 | var lastOnlineTimeMutex sync.Mutex 10 | 11 | // Init starts the DB Cache 12 | func Init() { 13 | lastOnlineTimeUpdate = make(map[int]int64) 14 | } 15 | 16 | // LastOnlineRequiresSave returns true if the last update time of the user is older than 60 seconds. 17 | func LastOnlineRequiresSave(userId int) bool { 18 | lastOnlineTimeMutex.Lock() 19 | timestamp := time.Now().Unix() 20 | defer lastOnlineTimeMutex.Unlock() 21 | if lastOnlineTimeUpdate[userId] < (timestamp - 60) { 22 | lastOnlineTimeUpdate[userId] = timestamp 23 | return true 24 | } 25 | return false 26 | } 27 | -------------------------------------------------------------------------------- /internal/configuration/database/migration/Migration.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "fmt" 5 | "github.com/forceu/gokapi/internal/configuration/database" 6 | "github.com/forceu/gokapi/internal/configuration/database/dbabstraction" 7 | "github.com/forceu/gokapi/internal/environment/flagparser" 8 | "os" 9 | ) 10 | 11 | // Do checks the passed flags for a migration and then executes it 12 | func Do(flags flagparser.MigrateFlags) { 13 | oldDb, err := database.ParseUrl(flags.Source, true) 14 | if err != nil { 15 | fmt.Println("Error: " + err.Error()) 16 | osExit(1) 17 | return 18 | } 19 | newDb, err := database.ParseUrl(flags.Destination, false) 20 | if err != nil { 21 | fmt.Println(err.Error()) 22 | osExit(2) 23 | return 24 | } 25 | fmt.Printf("Migrating %s database %s to %s database %s\n", getType(oldDb.Type), oldDb.HostUrl, getType(newDb.Type), newDb.HostUrl) 26 | database.Migrate(oldDb, newDb) 27 | } 28 | 29 | func getType(input int) string { 30 | switch input { 31 | case dbabstraction.TypeSqlite: 32 | return "SQLite" 33 | case dbabstraction.TypeRedis: 34 | return "Redis" 35 | } 36 | return "Invalid" 37 | } 38 | 39 | // Declared for testing 40 | var osExit = os.Exit 41 | -------------------------------------------------------------------------------- /internal/configuration/database/migration/Migration_test.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/configuration" 5 | "github.com/forceu/gokapi/internal/configuration/database" 6 | "github.com/forceu/gokapi/internal/configuration/database/dbabstraction" 7 | "github.com/forceu/gokapi/internal/environment/flagparser" 8 | "github.com/forceu/gokapi/internal/test" 9 | "github.com/forceu/gokapi/internal/test/testconfiguration" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | func TestMain(m *testing.M) { 15 | testconfiguration.Create(false) 16 | exitVal := m.Run() 17 | testconfiguration.Delete() 18 | os.Exit(exitVal) 19 | } 20 | 21 | func TestGetType(t *testing.T) { 22 | test.IsEqualString(t, getType(dbabstraction.TypeSqlite), "SQLite") 23 | test.IsEqualString(t, getType(dbabstraction.TypeRedis), "Redis") 24 | test.IsEqualString(t, getType(2), "Invalid") 25 | } 26 | 27 | var exitCode int 28 | 29 | func TestMigration(t *testing.T) { 30 | osExit = func(code int) { exitCode = code } 31 | Do(flagparser.MigrateFlags{ 32 | Source: "", 33 | Destination: "sqlite://ignore", 34 | }) 35 | test.IsEqualInt(t, exitCode, 1) 36 | exitCode = 0 37 | 38 | Do(flagparser.MigrateFlags{ 39 | Source: "sqlite://./tempfile", 40 | Destination: "", 41 | }) 42 | test.IsEqualInt(t, exitCode, 1) 43 | exitCode = 0 44 | 45 | err := os.WriteFile("tempfile", []byte("ignore"), 777) 46 | test.IsNil(t, err) 47 | Do(flagparser.MigrateFlags{ 48 | Source: "sqlite://./tempfile", 49 | Destination: "", 50 | }) 51 | test.IsEqualInt(t, exitCode, 2) 52 | exitCode = 0 53 | 54 | err = os.Remove("tempfile") 55 | test.IsNil(t, err) 56 | 57 | dbUrl := testconfiguration.SqliteUrl 58 | dbUrlNew := dbUrl + "2" 59 | Do(flagparser.MigrateFlags{ 60 | Source: dbUrl, 61 | Destination: dbUrlNew, 62 | }) 63 | err = os.Setenv("GOKAPI_DATABASE_URL", dbUrlNew) 64 | test.IsNil(t, err) 65 | configuration.Load() 66 | configuration.ConnectDatabase() 67 | _, ok := database.GetHotlink("PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg") 68 | test.IsEqualBool(t, ok, true) 69 | _, ok = database.GetApiKey("validkey") 70 | test.IsEqualBool(t, ok, true) 71 | _, ok = database.GetMetaDataById("Wzol7LyY2QVczXynJtVo") 72 | test.IsEqualBool(t, ok, true) 73 | } 74 | -------------------------------------------------------------------------------- /internal/configuration/database/provider/redis/apikeys.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/helper" 5 | "github.com/forceu/gokapi/internal/models" 6 | redigo "github.com/gomodule/redigo/redis" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | prefixApiKeys = "apikey:" 12 | ) 13 | 14 | func dbToApiKey(id string, input []any) (models.ApiKey, error) { 15 | var result models.ApiKey 16 | err := redigo.ScanStruct(input, &result) 17 | result.Id = strings.Replace(id, prefixApiKeys, "", 1) 18 | return result, err 19 | } 20 | 21 | // GetAllApiKeys returns a map with all API keys 22 | func (p DatabaseProvider) GetAllApiKeys() map[string]models.ApiKey { 23 | result := make(map[string]models.ApiKey) 24 | maps := p.getAllHashesWithPrefix(prefixApiKeys) 25 | for k, v := range maps { 26 | apiKey, err := dbToApiKey(k, v) 27 | helper.Check(err) 28 | result[apiKey.Id] = apiKey 29 | } 30 | return result 31 | } 32 | 33 | // GetApiKey returns a models.ApiKey if valid or false if the ID is not valid 34 | func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) { 35 | result, ok := p.getHashMap(prefixApiKeys + id) 36 | if !ok { 37 | return models.ApiKey{}, false 38 | } 39 | apikey, err := dbToApiKey(id, result) 40 | helper.Check(err) 41 | return apikey, true 42 | } 43 | 44 | // GetSystemKey returns the latest UI API key 45 | func (p DatabaseProvider) GetSystemKey(userId int) (models.ApiKey, bool) { 46 | keys := p.GetAllApiKeys() 47 | foundKey := "" 48 | var latestExpiry int64 49 | for _, key := range keys { 50 | if !key.IsSystemKey { 51 | continue 52 | } 53 | if key.UserId != userId { 54 | continue 55 | } 56 | if key.Expiry > latestExpiry { 57 | foundKey = key.Id 58 | latestExpiry = key.Expiry 59 | } 60 | } 61 | if foundKey == "" { 62 | return models.ApiKey{}, false 63 | } 64 | return keys[foundKey], true 65 | } 66 | 67 | // GetApiKeyByPublicKey returns an API key by using the public key 68 | func (p DatabaseProvider) GetApiKeyByPublicKey(publicKey string) (string, bool) { 69 | keys := p.GetAllApiKeys() 70 | for _, key := range keys { 71 | if key.PublicId == publicKey { 72 | return key.Id, true 73 | } 74 | } 75 | return "", false 76 | } 77 | 78 | // SaveApiKey saves the API key to the database 79 | func (p DatabaseProvider) SaveApiKey(apikey models.ApiKey) { 80 | p.setHashMap(p.buildArgs(prefixApiKeys + apikey.Id).AddFlat(apikey)) 81 | if apikey.Expiry != 0 { 82 | p.setExpiryAt(prefixApiKeys+apikey.Id, apikey.Expiry) 83 | } 84 | } 85 | 86 | // UpdateTimeApiKey writes the content of LastUsage to the database 87 | func (p DatabaseProvider) UpdateTimeApiKey(apikey models.ApiKey) { 88 | p.SaveApiKey(apikey) 89 | } 90 | 91 | // DeleteApiKey deletes an API key with the given ID 92 | func (p DatabaseProvider) DeleteApiKey(id string) { 93 | p.deleteKey(prefixApiKeys + id) 94 | } 95 | -------------------------------------------------------------------------------- /internal/configuration/database/provider/redis/e2econfig.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/helper" 5 | "github.com/forceu/gokapi/internal/models" 6 | redigo "github.com/gomodule/redigo/redis" 7 | "strconv" 8 | ) 9 | 10 | const idE2EInfo = "e2einfo:" 11 | 12 | // SaveEnd2EndInfo stores the encrypted e2e info 13 | func (p DatabaseProvider) SaveEnd2EndInfo(info models.E2EInfoEncrypted, userId int) { 14 | p.setHashMap(p.buildArgs(idE2EInfo + strconv.Itoa(userId)).AddFlat(info)) 15 | } 16 | 17 | // GetEnd2EndInfo retrieves the encrypted e2e info 18 | func (p DatabaseProvider) GetEnd2EndInfo(userId int) models.E2EInfoEncrypted { 19 | result := models.E2EInfoEncrypted{} 20 | value, ok := p.getHashMap(idE2EInfo + strconv.Itoa(userId)) 21 | if !ok { 22 | return models.E2EInfoEncrypted{} 23 | } 24 | err := redigo.ScanStruct(value, &result) 25 | helper.Check(err) 26 | return result 27 | } 28 | 29 | // DeleteEnd2EndInfo resets the encrypted e2e info 30 | func (p DatabaseProvider) DeleteEnd2EndInfo(userId int) { 31 | p.deleteKey(idE2EInfo + strconv.Itoa(userId)) 32 | } 33 | -------------------------------------------------------------------------------- /internal/configuration/database/provider/redis/hotlinks.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | prefixHotlinks = "hl:" 10 | ) 11 | 12 | // GetHotlink returns the id of the file associated or false if not found 13 | func (p DatabaseProvider) GetHotlink(id string) (string, bool) { 14 | return p.getKeyString(prefixHotlinks + id) 15 | } 16 | 17 | // GetAllHotlinks returns an array with all hotlink ids 18 | func (p DatabaseProvider) GetAllHotlinks() []string { 19 | result := make([]string, 0) 20 | for _, key := range p.getAllKeysWithPrefix(prefixHotlinks) { 21 | result = append(result, strings.Replace(key, prefixHotlinks, "", 1)) 22 | } 23 | return result 24 | } 25 | 26 | // SaveHotlink stores the hotlink associated with the file in the database 27 | func (p DatabaseProvider) SaveHotlink(file models.File) { 28 | p.setKey(prefixHotlinks+file.HotlinkId, file.Id) 29 | } 30 | 31 | // DeleteHotlink deletes a hotlink with the given hotlink ID 32 | func (p DatabaseProvider) DeleteHotlink(id string) { 33 | p.deleteKey(prefixHotlinks + id) 34 | } 35 | -------------------------------------------------------------------------------- /internal/configuration/database/provider/redis/metadata.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "github.com/forceu/gokapi/internal/helper" 7 | "github.com/forceu/gokapi/internal/models" 8 | redigo "github.com/gomodule/redigo/redis" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | prefixMetaData = "fmeta:" 14 | ) 15 | 16 | // GetAllMetadata returns a map of all available files 17 | func (p DatabaseProvider) GetAllMetadata() map[string]models.File { 18 | result := make(map[string]models.File) 19 | maps := p.getAllHashesWithPrefix(prefixMetaData) 20 | for k, v := range maps { 21 | file, err := dbToMetadata(k, v) 22 | helper.Check(err) 23 | result[file.Id] = file 24 | } 25 | return result 26 | } 27 | 28 | func dbToMetadata(id string, input []any) (models.File, error) { 29 | var result models.File 30 | err := redigo.ScanStruct(input, &result) 31 | if err != nil { 32 | return models.File{}, err 33 | } 34 | result.Id = strings.Replace(id, prefixMetaData, "", 1) 35 | return unmarshalEncryptionInfo(result) 36 | } 37 | 38 | func marshalEncryptionInfo(f models.File) (models.File, error) { 39 | var encInfo bytes.Buffer 40 | enc := gob.NewEncoder(&encInfo) 41 | err := enc.Encode(f.Encryption) 42 | if err != nil { 43 | return f, err 44 | } 45 | f.InternalRedisEncryption = encInfo.Bytes() 46 | return f, nil 47 | } 48 | 49 | func unmarshalEncryptionInfo(f models.File) (models.File, error) { 50 | if f.InternalRedisEncryption == nil { 51 | f.Encryption = models.EncryptionInfo{} 52 | return f, nil 53 | } 54 | var result models.EncryptionInfo 55 | buf := bytes.NewBuffer(f.InternalRedisEncryption) 56 | dec := gob.NewDecoder(buf) 57 | err := dec.Decode(&result) 58 | if err != nil { 59 | return f, err 60 | } 61 | f.Encryption = result 62 | f.InternalRedisEncryption = nil 63 | return f, nil 64 | } 65 | 66 | // GetAllMetaDataIds returns all Ids that contain metadata 67 | func (p DatabaseProvider) GetAllMetaDataIds() []string { 68 | result := make([]string, 0) 69 | for _, key := range p.getAllKeysWithPrefix(prefixMetaData) { 70 | result = append(result, strings.Replace(key, prefixMetaData, "", 1)) 71 | } 72 | return result 73 | } 74 | 75 | // GetMetaDataById returns a models.File from the ID passed or false if the id is not valid 76 | func (p DatabaseProvider) GetMetaDataById(id string) (models.File, bool) { 77 | result, ok := p.getHashMap(prefixMetaData + id) 78 | if !ok { 79 | return models.File{}, false 80 | } 81 | file, err := dbToMetadata(id, result) 82 | helper.Check(err) 83 | return file, true 84 | } 85 | 86 | // SaveMetaData stores the metadata of a file to the disk 87 | func (p DatabaseProvider) SaveMetaData(file models.File) { 88 | marshalledFile, err := marshalEncryptionInfo(file) 89 | helper.Check(err) 90 | p.setHashMap(p.buildArgs(prefixMetaData + file.Id).AddFlat(marshalledFile)) 91 | } 92 | 93 | // DeleteMetaData deletes information about a file 94 | func (p DatabaseProvider) DeleteMetaData(id string) { 95 | p.deleteKey(prefixMetaData + id) 96 | } 97 | 98 | // IncreaseDownloadCount increases the download count of a file, preventing race conditions 99 | func (p DatabaseProvider) IncreaseDownloadCount(id string, decreaseRemainingDownloads bool) { 100 | if decreaseRemainingDownloads { 101 | p.decreaseHashmapIntField(prefixMetaData+id, "DownloadsRemaining") 102 | } 103 | p.increaseHashmapIntField(prefixMetaData+id, "DownloadCount") 104 | } 105 | -------------------------------------------------------------------------------- /internal/configuration/database/provider/redis/sessions.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/helper" 5 | "github.com/forceu/gokapi/internal/models" 6 | redigo "github.com/gomodule/redigo/redis" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | prefixSessions = "se:" 12 | ) 13 | 14 | // GetSession returns the session with the given ID or false if not a valid ID 15 | func (p DatabaseProvider) GetSession(id string) (models.Session, bool) { 16 | hashmapEntry, ok := p.getHashMap(prefixSessions + id) 17 | if !ok { 18 | return models.Session{}, false 19 | } 20 | var result models.Session 21 | err := redigo.ScanStruct(hashmapEntry, &result) 22 | helper.Check(err) 23 | return result, true 24 | } 25 | 26 | // SaveSession stores the given session. After the expiry passed, it will be deleted automatically 27 | func (p DatabaseProvider) SaveSession(id string, session models.Session) { 28 | p.setHashMap(p.buildArgs(prefixSessions + id).AddFlat(session)) 29 | p.setExpiryAt(prefixSessions+id, session.ValidUntil) 30 | } 31 | 32 | // DeleteSession deletes a session with the given ID 33 | func (p DatabaseProvider) DeleteSession(id string) { 34 | p.deleteKey(prefixSessions + id) 35 | } 36 | 37 | // DeleteAllSessions logs all users out 38 | func (p DatabaseProvider) DeleteAllSessions() { 39 | p.deleteAllWithPrefix(prefixSessions) 40 | } 41 | 42 | // DeleteAllSessionsByUser logs the specific users out 43 | func (p DatabaseProvider) DeleteAllSessionsByUser(userId int) { 44 | maps := p.getAllHashesWithPrefix(prefixSessions) 45 | for k, v := range maps { 46 | var result models.Session 47 | err := redigo.ScanStruct(v, &result) 48 | helper.Check(err) 49 | if result.UserId == userId { 50 | p.DeleteSession(strings.Replace(k, prefixSessions, "", 1)) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/configuration/database/provider/redis/users.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "cmp" 5 | "github.com/forceu/gokapi/internal/helper" 6 | "github.com/forceu/gokapi/internal/models" 7 | redigo "github.com/gomodule/redigo/redis" 8 | "slices" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | const ( 14 | prefixUsers = "users:" 15 | prefixUserIdCounter = "userid_max" 16 | ) 17 | 18 | func dbToUser(input []any) (models.User, error) { 19 | var result models.User 20 | err := redigo.ScanStruct(input, &result) 21 | if err != nil { 22 | return models.User{}, err 23 | } 24 | return result, nil 25 | } 26 | 27 | // GetAllUsers returns a map with all users 28 | func (p DatabaseProvider) GetAllUsers() []models.User { 29 | var result []models.User 30 | maps := p.getAllHashesWithPrefix(prefixUsers) 31 | for _, v := range maps { 32 | user, err := dbToUser(v) 33 | helper.Check(err) 34 | result = append(result, user) 35 | } 36 | return orderUsers(result) 37 | } 38 | 39 | func orderUsers(users []models.User) []models.User { 40 | slices.SortFunc(users, func(a, b models.User) int { 41 | return cmp.Or( 42 | cmp.Compare(a.UserLevel, b.UserLevel), 43 | cmp.Compare(b.LastOnline, a.LastOnline), 44 | cmp.Compare(a.Name, b.Name), 45 | ) 46 | }) 47 | return users 48 | } 49 | 50 | // GetUserByName returns a models.User if valid or false if the email is not valid 51 | func (p DatabaseProvider) GetUserByName(username string) (models.User, bool) { 52 | users := p.GetAllUsers() 53 | for _, user := range users { 54 | if user.Name == username { 55 | return user, true 56 | } 57 | } 58 | return models.User{}, false 59 | } 60 | 61 | // GetUser returns a models.User if valid or false if the ID is not valid 62 | func (p DatabaseProvider) GetUser(id int) (models.User, bool) { 63 | result, ok := p.getHashMap(prefixUsers + strconv.Itoa(id)) 64 | if !ok { 65 | return models.User{}, false 66 | } 67 | user, err := dbToUser(result) 68 | helper.Check(err) 69 | return user, true 70 | } 71 | 72 | // SaveUser saves a user to the database. If isNewUser is true, a new Id will be generated 73 | func (p DatabaseProvider) SaveUser(user models.User, isNewUser bool) { 74 | if isNewUser { 75 | id := p.getIncreasedInt(prefixUserIdCounter) 76 | user.Id = id 77 | } else { 78 | counter, _ := p.getKeyInt(prefixUserIdCounter) 79 | if counter < user.Id { 80 | p.setKey(prefixUserIdCounter, user.Id) 81 | } 82 | } 83 | p.setHashMap(p.buildArgs(prefixUsers + strconv.Itoa(user.Id)).AddFlat(user)) 84 | } 85 | 86 | // UpdateUserLastOnline writes the last online time to the database 87 | func (p DatabaseProvider) UpdateUserLastOnline(id int) { 88 | p.setHashmapField(prefixUsers+strconv.Itoa(id), "LastOnline", time.Now().Unix()) 89 | } 90 | 91 | // DeleteUser deletes a user with the given ID 92 | func (p DatabaseProvider) DeleteUser(id int) { 93 | p.deleteKey(prefixUsers + strconv.Itoa(id)) 94 | } 95 | -------------------------------------------------------------------------------- /internal/configuration/database/provider/sqlite/e2econfig.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "encoding/gob" 7 | "errors" 8 | "github.com/forceu/gokapi/internal/helper" 9 | "github.com/forceu/gokapi/internal/models" 10 | ) 11 | 12 | type schemaE2EConfig struct { 13 | Id int64 14 | Config []byte 15 | UserId int 16 | } 17 | 18 | // SaveEnd2EndInfo stores the encrypted e2e info 19 | func (p DatabaseProvider) SaveEnd2EndInfo(info models.E2EInfoEncrypted, userId int) { 20 | var buf bytes.Buffer 21 | enc := gob.NewEncoder(&buf) 22 | err := enc.Encode(info) 23 | helper.Check(err) 24 | 25 | _, err = p.sqliteDb.Exec("INSERT OR REPLACE INTO E2EConfig ( Config, UserId) VALUES ( ?, ?)", 26 | buf.Bytes(), userId) 27 | helper.Check(err) 28 | } 29 | 30 | // GetEnd2EndInfo retrieves the encrypted e2e info 31 | func (p DatabaseProvider) GetEnd2EndInfo(userId int) models.E2EInfoEncrypted { 32 | result := models.E2EInfoEncrypted{} 33 | rowResult := schemaE2EConfig{} 34 | 35 | row := p.sqliteDb.QueryRow("SELECT Config FROM E2EConfig WHERE UserId = ?", userId) 36 | err := row.Scan(&rowResult.Config) 37 | if err != nil { 38 | if errors.Is(err, sql.ErrNoRows) { 39 | return result 40 | } 41 | helper.Check(err) 42 | return result 43 | } 44 | 45 | buf := bytes.NewBuffer(rowResult.Config) 46 | dec := gob.NewDecoder(buf) 47 | err = dec.Decode(&result) 48 | helper.Check(err) 49 | return result 50 | } 51 | 52 | // DeleteEnd2EndInfo resets the encrypted e2e info 53 | func (p DatabaseProvider) DeleteEnd2EndInfo(userId int) { 54 | _, err := p.sqliteDb.Exec("DELETE FROM E2EConfig WHERE UserId = ?", userId) 55 | helper.Check(err) 56 | } 57 | -------------------------------------------------------------------------------- /internal/configuration/database/provider/sqlite/hotlinks.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "github.com/forceu/gokapi/internal/helper" 7 | "github.com/forceu/gokapi/internal/models" 8 | ) 9 | 10 | type schemaHotlinks struct { 11 | Id string 12 | FileId string 13 | } 14 | 15 | // GetHotlink returns the id of the file associated or false if not found 16 | func (p DatabaseProvider) GetHotlink(id string) (string, bool) { 17 | var rowResult schemaHotlinks 18 | row := p.sqliteDb.QueryRow("SELECT FileId FROM Hotlinks WHERE Id = ?", id) 19 | err := row.Scan(&rowResult.FileId) 20 | if err != nil { 21 | if errors.Is(err, sql.ErrNoRows) { 22 | return "", false 23 | } 24 | helper.Check(err) 25 | return "", false 26 | } 27 | return rowResult.FileId, true 28 | } 29 | 30 | // GetAllHotlinks returns an array with all hotlink ids 31 | func (p DatabaseProvider) GetAllHotlinks() []string { 32 | ids := make([]string, 0) 33 | rows, err := p.sqliteDb.Query("SELECT Id FROM Hotlinks") 34 | helper.Check(err) 35 | defer rows.Close() 36 | for rows.Next() { 37 | rowData := schemaHotlinks{} 38 | err = rows.Scan(&rowData.Id) 39 | helper.Check(err) 40 | ids = append(ids, rowData.Id) 41 | } 42 | return ids 43 | } 44 | 45 | // SaveHotlink stores the hotlink associated with the file in the database 46 | func (p DatabaseProvider) SaveHotlink(file models.File) { 47 | newData := schemaHotlinks{ 48 | Id: file.HotlinkId, 49 | FileId: file.Id, 50 | } 51 | 52 | _, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO Hotlinks (Id, FileId) VALUES (?, ?)", 53 | newData.Id, newData.FileId) 54 | helper.Check(err) 55 | } 56 | 57 | // DeleteHotlink deletes a hotlink with the given hotlink ID 58 | func (p DatabaseProvider) DeleteHotlink(id string) { 59 | if id == "" { 60 | return 61 | } 62 | _, err := p.sqliteDb.Exec("DELETE FROM Hotlinks WHERE Id = ?", id) 63 | helper.Check(err) 64 | } 65 | -------------------------------------------------------------------------------- /internal/configuration/database/provider/sqlite/sessions.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "github.com/forceu/gokapi/internal/helper" 7 | "github.com/forceu/gokapi/internal/models" 8 | "time" 9 | ) 10 | 11 | type schemaSessions struct { 12 | Id string 13 | RenewAt int64 14 | ValidUntil int64 15 | UserId int 16 | } 17 | 18 | // GetSession returns the session with the given ID or false if not a valid ID 19 | func (p DatabaseProvider) GetSession(id string) (models.Session, bool) { 20 | var rowResult schemaSessions 21 | row := p.sqliteDb.QueryRow("SELECT * FROM Sessions WHERE Id = ?", id) 22 | err := row.Scan(&rowResult.Id, &rowResult.RenewAt, &rowResult.ValidUntil, &rowResult.UserId) 23 | if err != nil { 24 | if errors.Is(err, sql.ErrNoRows) { 25 | return models.Session{}, false 26 | } 27 | helper.Check(err) 28 | return models.Session{}, false 29 | } 30 | result := models.Session{ 31 | RenewAt: rowResult.RenewAt, 32 | ValidUntil: rowResult.ValidUntil, 33 | UserId: rowResult.UserId, 34 | } 35 | return result, true 36 | } 37 | 38 | // SaveSession stores the given session. After the expiry passed, it will be deleted automatically 39 | func (p DatabaseProvider) SaveSession(id string, session models.Session) { 40 | newData := schemaSessions{ 41 | Id: id, 42 | RenewAt: session.RenewAt, 43 | ValidUntil: session.ValidUntil, 44 | UserId: session.UserId, 45 | } 46 | 47 | _, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO Sessions (Id, RenewAt, ValidUntil, UserId) VALUES (?, ?, ?, ?)", 48 | newData.Id, newData.RenewAt, newData.ValidUntil, newData.UserId) 49 | helper.Check(err) 50 | } 51 | 52 | // DeleteSession deletes a session with the given ID 53 | func (p DatabaseProvider) DeleteSession(id string) { 54 | _, err := p.sqliteDb.Exec("DELETE FROM Sessions WHERE Id = ?", id) 55 | helper.Check(err) 56 | } 57 | 58 | // DeleteAllSessions logs all users out 59 | func (p DatabaseProvider) DeleteAllSessions() { 60 | //goland:noinspection SqlWithoutWhere 61 | _, err := p.sqliteDb.Exec("DELETE FROM Sessions") 62 | helper.Check(err) 63 | } 64 | 65 | // DeleteAllSessionsByUser logs the specific users out 66 | func (p DatabaseProvider) DeleteAllSessionsByUser(userId int) { 67 | _, err := p.sqliteDb.Exec("DELETE FROM Sessions WHERE UserId = ?", userId) 68 | helper.Check(err) 69 | } 70 | 71 | func (p DatabaseProvider) cleanExpiredSessions() { 72 | _, err := p.sqliteDb.Exec("DELETE FROM Sessions WHERE Sessions.ValidUntil < ?", time.Now().Unix()) 73 | helper.Check(err) 74 | } 75 | -------------------------------------------------------------------------------- /internal/configuration/database/provider/sqlite/users.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "github.com/forceu/gokapi/internal/helper" 7 | "github.com/forceu/gokapi/internal/models" 8 | "time" 9 | ) 10 | 11 | type schemaUser struct { 12 | Id int 13 | Name string 14 | Password sql.NullString 15 | Permissions models.UserPermission 16 | UserLevel models.UserRank 17 | LastOnline int64 18 | ResetPassword int 19 | } 20 | 21 | func (s schemaUser) ToUser() models.User { 22 | pw := "" 23 | if s.Password.Valid { 24 | pw = s.Password.String 25 | } 26 | return models.User{ 27 | Id: s.Id, 28 | Name: s.Name, 29 | Permissions: s.Permissions, 30 | UserLevel: s.UserLevel, 31 | LastOnline: s.LastOnline, 32 | Password: pw, 33 | ResetPassword: s.ResetPassword == 1, 34 | } 35 | } 36 | 37 | // GetAllUsers returns a map with all users 38 | func (p DatabaseProvider) GetAllUsers() []models.User { 39 | var result []models.User 40 | rows, err := p.sqliteDb.Query("SELECT * FROM Users ORDER BY Userlevel ASC, LastOnline DESC, Name ASC") 41 | helper.Check(err) 42 | defer rows.Close() 43 | for rows.Next() { 44 | row := schemaUser{} 45 | err = rows.Scan(&row.Id, &row.Name, &row.Password, &row.Permissions, &row.UserLevel, &row.LastOnline, &row.ResetPassword) 46 | helper.Check(err) 47 | result = append(result, row.ToUser()) 48 | } 49 | return result 50 | } 51 | 52 | func (p DatabaseProvider) getUserWithConstraint(isName bool, searchValue any) (models.User, bool) { 53 | rowResult := schemaUser{} 54 | query := "SELECT * FROM Users WHERE Id = ?" 55 | if isName { 56 | query = "SELECT * FROM Users WHERE Name = ?" 57 | } 58 | row := p.sqliteDb.QueryRow(query, searchValue) 59 | err := row.Scan(&rowResult.Id, &rowResult.Name, &rowResult.Password, &rowResult.Permissions, &rowResult.UserLevel, &rowResult.LastOnline, &rowResult.ResetPassword) 60 | if err != nil { 61 | if errors.Is(err, sql.ErrNoRows) { 62 | return models.User{}, false 63 | } 64 | helper.Check(err) 65 | return models.User{}, false 66 | } 67 | user := rowResult.ToUser() 68 | return user, true 69 | } 70 | 71 | // GetUser returns a models.User if valid or false if the ID is not valid 72 | func (p DatabaseProvider) GetUser(id int) (models.User, bool) { 73 | return p.getUserWithConstraint(false, id) 74 | } 75 | 76 | // GetUserByName returns a models.User if valid or false if the name is not valid 77 | func (p DatabaseProvider) GetUserByName(username string) (models.User, bool) { 78 | return p.getUserWithConstraint(true, username) 79 | } 80 | 81 | // SaveUser saves a user to the database. If isNewUser is true, a new Id will be generated 82 | func (p DatabaseProvider) SaveUser(user models.User, isNewUser bool) { 83 | resetpw := 0 84 | if user.ResetPassword { 85 | resetpw = 1 86 | } 87 | if isNewUser { 88 | _, err := p.sqliteDb.Exec("INSERT INTO Users (Name, Password, Permissions, Userlevel, LastOnline, ResetPassword) VALUES (?, ?, ?, ?, ?, ?)", 89 | user.Name, user.Password, user.Permissions, user.UserLevel, user.LastOnline, resetpw) 90 | helper.Check(err) 91 | } else { 92 | _, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO Users (Id, Name, Password, Permissions, Userlevel, LastOnline, ResetPassword) VALUES (?, ?, ?, ?, ?, ?, ?)", 93 | user.Id, user.Name, user.Password, user.Permissions, user.UserLevel, user.LastOnline, resetpw) 94 | helper.Check(err) 95 | } 96 | } 97 | 98 | // UpdateUserLastOnline writes the last online time to the database 99 | func (p DatabaseProvider) UpdateUserLastOnline(id int) { 100 | timeNow := time.Now().Unix() 101 | _, err := p.sqliteDb.Exec("UPDATE Users SET LastOnline= ? WHERE Id = ?", timeNow, id, timeNow) 102 | helper.Check(err) 103 | } 104 | 105 | // DeleteUser deletes a user with the given ID 106 | func (p DatabaseProvider) DeleteUser(id int) { 107 | _, err := p.sqliteDb.Exec("DELETE FROM Users WHERE Id = ?", id) 108 | helper.Check(err) 109 | } 110 | -------------------------------------------------------------------------------- /internal/configuration/setup/ProtectedUrls.go: -------------------------------------------------------------------------------- 1 | // Code generated by updateProtectedUrls.go - DO NOT EDIT. 2 | package setup 3 | 4 | // Do not modify: This is an automatically generated file created by updateProtectedUrls.go 5 | // It contains all URLs that need to be protected when using an external authentication. 6 | 7 | // protectedUrls contains a list of URLs that need to be protected if authentication is disabled. 8 | // This list will be displayed during the setup 9 | var protectedUrls = []string{"/admin", "/apiKeys", "/changePassword", "/e2eInfo", "/e2eSetup", "/logs", "/uploadChunk", "/uploadStatus", "/users"} 10 | -------------------------------------------------------------------------------- /internal/configuration/setup/static/setup/chosen/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/configuration/setup/static/setup/chosen/chosen-sprite.png -------------------------------------------------------------------------------- /internal/configuration/setup/static/setup/chosen/chosen-sprite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/configuration/setup/static/setup/chosen/chosen-sprite@2x.png -------------------------------------------------------------------------------- /internal/configuration/setup/static/setup/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/configuration/setup/static/setup/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /internal/configuration/setup/static/setup/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/configuration/setup/static/setup/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /internal/configuration/setup/static/setup/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/configuration/setup/static/setup/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /internal/configuration/setup/static/setup/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /internal/configuration/setup/static/setup/js/html5shiv-3.7.0.js: -------------------------------------------------------------------------------- 1 | /* 2 | HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | (function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag(); 5 | a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x"; 6 | c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode|| 7 | "undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f); 8 | if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d #mq-test-1 { width: 42px; }',d.insertBefore(f,e),c=42===g.offsetWidth,d.removeChild(f),{matches:c,media:a}}}(document); 4 | 5 | /*! Respond.js v1.3.0: min/max-width media query polyfill. (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs */ 6 | (function(a){"use strict";function x(){u(!0)}var b={};if(a.respond=b,b.update=function(){},b.mediaQueriesSupported=a.matchMedia&&a.matchMedia("only all").matches,!b.mediaQueriesSupported){var q,r,t,c=a.document,d=c.documentElement,e=[],f=[],g=[],h={},i=30,j=c.getElementsByTagName("head")[0]||d,k=c.getElementsByTagName("base")[0],l=j.getElementsByTagName("link"),m=[],n=function(){for(var b=0;l.length>b;b++){var c=l[b],d=c.href,e=c.media,f=c.rel&&"stylesheet"===c.rel.toLowerCase();d&&f&&!h[d]&&(c.styleSheet&&c.styleSheet.rawCssText?(p(c.styleSheet.rawCssText,d,e),h[d]=!0):(!/^([a-zA-Z:]*\/\/)/.test(d)&&!k||d.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&m.push({href:d,media:e}))}o()},o=function(){if(m.length){var b=m.shift();v(b.href,function(c){p(c,b.href,b.media),h[b.href]=!0,a.setTimeout(function(){o()},0)})}},p=function(a,b,c){var d=a.match(/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi),g=d&&d.length||0;b=b.substring(0,b.lastIndexOf("/"));var h=function(a){return a.replace(/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,"$1"+b+"$2$3")},i=!g&&c;b.length&&(b+="/"),i&&(g=1);for(var j=0;g>j;j++){var k,l,m,n;i?(k=c,f.push(h(a))):(k=d[j].match(/@media *([^\{]+)\{([\S\s]+?)$/)&&RegExp.$1,f.push(RegExp.$2&&h(RegExp.$2))),m=k.split(","),n=m.length;for(var o=0;n>o;o++)l=m[o],e.push({media:l.split("(")[0].match(/(only\s+)?([a-zA-Z]+)\s?/)&&RegExp.$2||"all",rules:f.length-1,hasquery:l.indexOf("(")>-1,minw:l.match(/\(\s*min\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:l.match(/\(\s*max\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},s=function(){var a,b=c.createElement("div"),e=c.body,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",e||(e=f=c.createElement("body"),e.style.background="none"),e.appendChild(b),d.insertBefore(e,d.firstChild),a=b.offsetWidth,f?d.removeChild(e):e.removeChild(b),a=t=parseFloat(a)},u=function(b){var h="clientWidth",k=d[h],m="CSS1Compat"===c.compatMode&&k||c.body[h]||k,n={},o=l[l.length-1],p=(new Date).getTime();if(b&&q&&i>p-q)return a.clearTimeout(r),r=a.setTimeout(u,i),void 0;q=p;for(var v in e)if(e.hasOwnProperty(v)){var w=e[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?t||s():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?t||s():1)),w.hasquery&&(z&&A||!(z||m>=x)||!(A||y>=m))||(n[w.media]||(n[w.media]=[]),n[w.media].push(f[w.rules]))}for(var C in g)g.hasOwnProperty(C)&&g[C]&&g[C].parentNode===j&&j.removeChild(g[C]);for(var D in n)if(n.hasOwnProperty(D)){var E=c.createElement("style"),F=n[D].join("\n");E.type="text/css",E.media=D,j.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(c.createTextNode(F)),g.push(E)}},v=function(a,b){var c=w();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))},w=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}();n(),b.update=n,a.addEventListener?a.addEventListener("resize",x,!1):a.attachEvent&&a.attachEvent("onresize",x)}})(this); -------------------------------------------------------------------------------- /internal/configuration/setup/static/setup/src/bootstrap-wizard.css: -------------------------------------------------------------------------------- 1 | /* WIZARD GENERAL */ 2 | .wizard { 3 | display:none; 4 | } 5 | 6 | .wizard-dialog {} 7 | .wizard-content {} 8 | 9 | .wizard-body { 10 | padding: 0; 11 | margin: 0; 12 | } 13 | 14 | /* WIZARD HEADER */ 15 | .wizard-header { 16 | padding: 9px 15px; 17 | border-bottom: 0; 18 | } 19 | 20 | .wizard-header h3 { 21 | margin: 0; 22 | line-height: 35px; 23 | display: inline; 24 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | font-family: inherit; 26 | font-weight: bold; 27 | text-rendering: optimizelegibility; 28 | color: rgb(51, 51, 51); 29 | } 30 | 31 | .wizard-subtitle { 32 | font-weight:bold; 33 | color:#AFAFAF; 34 | padding-left:20px; 35 | } 36 | 37 | 38 | /* WIZARD NAVIGATION */ 39 | .wizard-steps { 40 | width: 28%; 41 | background-color: #f5f5f5; 42 | border-bottom-left-radius: 6px; 43 | position: relative; 44 | } 45 | 46 | .wizard-nav-container { 47 | padding-bottom: 30px; 48 | } 49 | 50 | .wizard-nav-list { 51 | margin-bottom: 0; 52 | } 53 | 54 | .wizard-nav-link .glyphicon-chevron-right { 55 | float:right; 56 | margin-top:12px; 57 | margin-right:-6px; 58 | opacity:.25; 59 | } 60 | 61 | li.wizard-nav-item.active .glyphicon-chevron-right { 62 | opacity:1; 63 | } 64 | 65 | li.wizard-nav-item { 66 | line-height:40px; 67 | } 68 | 69 | .wizard-nav-list > li > a { 70 | background-color:#f5f5f5; 71 | padding:3px 15px 3px 20px; 72 | cursor:default; 73 | color:#B4B4B4; 74 | } 75 | 76 | .wizard-nav-list > li > a:hover { 77 | background-color: transparent; 78 | } 79 | 80 | .wizard-nav-list > li.already-visited > a.wizard-nav-link { 81 | color:#08C; 82 | cursor:pointer; 83 | } 84 | 85 | .wizard-nav-list > li.active > a.wizard-nav-link { 86 | color:white; 87 | } 88 | 89 | .wizard-nav-item .already-visited .active { 90 | background-color:#08C; 91 | } 92 | 93 | .wizard-nav-list li.active > a { 94 | background-color:#08C; 95 | } 96 | 97 | 98 | /* WIZARD CONTENT */ 99 | .wizard-body form { 100 | padding: 0; 101 | margin: 0; 102 | } 103 | 104 | /* WIZARD PROGRESS BAR */ 105 | .wizard-progress-container { 106 | margin-top: 20px; 107 | padding: 15px; 108 | width: 100%; 109 | position: absolute; 110 | bottom: 0; 111 | } 112 | 113 | .wizard-card-container { 114 | margin-left: 28%; 115 | } 116 | 117 | /* WIZARD CARDS */ 118 | .wizard-error, 119 | .wizard-failure, 120 | .wizard-success, 121 | .wizard-loading, 122 | .wizard-card { 123 | border-top: 1px solid #EEE; 124 | display:none; 125 | padding:35px; 126 | padding-top:20px; 127 | overflow-y:auto; 128 | 129 | /* 130 | position:relative; 131 | height:300px; 132 | margin-right: 5px; 133 | */ 134 | } 135 | 136 | .wizard-card-overlay { 137 | overflow-y: initial; 138 | } 139 | 140 | .wizard-card > h3 { 141 | margin-top:0; 142 | margin-bottom:20px; 143 | font-size:21px; 144 | line-height:40px; 145 | font-weight:normal; 146 | } 147 | 148 | /* WIZARD FOOTER */ 149 | .wizard-footer { 150 | padding:0; 151 | } 152 | 153 | .wizard-buttons-container { 154 | padding:20px; 155 | } 156 | 157 | .wizard-cancel { 158 | margin-left: 12px; 159 | } 160 | 161 | /* Inner Card */ 162 | .wizard-input-section { 163 | margin-bottom:20px; 164 | } 165 | 166 | .wizard-dialog .popover.error-popover { 167 | background-color:#F2DEDE; 168 | color:#B94A48; 169 | border-color:#953B39; 170 | } 171 | 172 | .wizard-dialog .popover.error-popover .arrow::after { 173 | border-right-color:#F2DEDE; 174 | } 175 | 176 | .wizard-dialog .popover.error-popover .popover-title { 177 | display:none; 178 | } 179 | 180 | .wizard-dialog .popover.error-popover .arrow { 181 | border-right-color:#953B39; 182 | } 183 | 184 | 185 | input[type=checkbox] { 186 | /* Double-sized Checkboxes */ 187 | -ms-transform: scale(1.5); /* IE */ 188 | -moz-transform: scale(1.5); /* FF */ 189 | -webkit-transform: scale(1.5); /* Safari and Chrome */ 190 | -o-transform: scale(1.5); /* Opera */ 191 | transform: scale(1.5); 192 | } 193 | -------------------------------------------------------------------------------- /internal/configuration/setup/static/setup/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

Card 1

6 | Some content 7 |
8 | 9 |
10 |

Card 2

11 | Some content 12 |
13 |
14 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /internal/encryption/end2end/End2End.go: -------------------------------------------------------------------------------- 1 | package end2end 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "github.com/forceu/gokapi/internal/encryption" 7 | "github.com/forceu/gokapi/internal/helper" 8 | "github.com/forceu/gokapi/internal/models" 9 | ) 10 | 11 | const e2eVersion = 1 12 | 13 | // EncryptData encrypts the locally stored e2e data to save on the server 14 | func EncryptData(files []models.E2EFile, key []byte) (models.E2EInfoEncrypted, error) { 15 | nonce, err := encryption.GetRandomNonce() 16 | if err != nil { 17 | return models.E2EInfoEncrypted{}, err 18 | } 19 | result := models.E2EInfoEncrypted{ 20 | Nonce: nonce, 21 | } 22 | var buf bytes.Buffer 23 | enc := gob.NewEncoder(&buf) 24 | err = enc.Encode(files) 25 | helper.Check(err) 26 | 27 | encryptedResult, err := encryption.EncryptDecryptBytes(buf.Bytes(), key, nonce, true) 28 | if err != nil { 29 | return models.E2EInfoEncrypted{}, err 30 | } 31 | result.Content = encryptedResult 32 | result.Version = e2eVersion 33 | return result, nil 34 | } 35 | 36 | // DecryptData decrypts the e2e data stored on the server 37 | func DecryptData(encryptedContent models.E2EInfoEncrypted, key []byte) (models.E2EInfoPlainText, error) { 38 | result, err := encryption.EncryptDecryptBytes(encryptedContent.Content, key, encryptedContent.Nonce, false) 39 | if err != nil { 40 | return models.E2EInfoPlainText{}, err 41 | } 42 | 43 | var fileData []models.E2EFile 44 | buf := bytes.NewBuffer(result) 45 | dec := gob.NewDecoder(buf) 46 | err = dec.Decode(&fileData) 47 | helper.Check(err) 48 | return models.E2EInfoPlainText{ 49 | Files: fileData, 50 | }, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/encryption/end2end/End2End_test.go: -------------------------------------------------------------------------------- 1 | package end2end 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/encryption" 5 | "github.com/forceu/gokapi/internal/models" 6 | "github.com/forceu/gokapi/internal/test" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestEncrypting(t *testing.T) { 12 | cipherEncryption, err := encryption.GetRandomCipher() 13 | test.IsNil(t, err) 14 | cipherF1, err := encryption.GetRandomCipher() 15 | test.IsNil(t, err) 16 | cipherF2, err := encryption.GetRandomCipher() 17 | test.IsNil(t, err) 18 | 19 | files := make([]models.E2EFile, 2) 20 | files = append(files, models.E2EFile{ 21 | Uuid: "1234", 22 | Id: "id123", 23 | Filename: "testfile", 24 | Cipher: cipherF1, 25 | }) 26 | files = append(files, models.E2EFile{ 27 | Uuid: "5678", 28 | Id: "id5567", 29 | Filename: "testfile2", 30 | Cipher: cipherF2, 31 | }) 32 | 33 | encryptedFiles, err := EncryptData(files, cipherEncryption) 34 | test.IsNil(t, err) 35 | test.IsEqualBool(t, len(encryptedFiles.Content) > 0, true) 36 | test.IsEqualInt(t, encryptedFiles.Version, 1) 37 | test.IsEqualBool(t, len(encryptedFiles.Nonce) > 0, true) 38 | 39 | decryptedFiles, err := DecryptData(encryptedFiles, cipherEncryption) 40 | test.IsNil(t, err) 41 | test.IsEqualBool(t, reflect.DeepEqual(files, decryptedFiles.Files), true) 42 | 43 | } 44 | -------------------------------------------------------------------------------- /internal/environment/BuildVars.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | /** 4 | Variables that are set during build 5 | */ 6 | 7 | // IsDocker has to be true if compiled for the Docker image (auto-generated value) 8 | var IsDocker = "false" 9 | 10 | // BuildTime is the time of the build (auto-generated value) 11 | var BuildTime = "Dev Build" 12 | 13 | // Builder is the name of builder (auto-generated value) 14 | var Builder = "Manual Build" 15 | 16 | // IsDockerInstance returns true if the binary was compiled with the official docker makefile, which 17 | // sets IsDocker to true 18 | func IsDockerInstance() bool { 19 | return IsDocker != "false" 20 | } 21 | -------------------------------------------------------------------------------- /internal/environment/Environment_test.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/test" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | var returnCode = 0 10 | 11 | func TestMain(m *testing.M) { 12 | 13 | osExit = func(code int) { 14 | returnCode = code 15 | } 16 | exitVal := m.Run() 17 | os.Exit(exitVal) 18 | } 19 | 20 | func TestTempDir(t *testing.T) { 21 | test.IsEqualString(t, os.Getenv("TMPDIR"), "") 22 | New() 23 | test.IsEqualString(t, os.Getenv("TMPDIR"), "") 24 | IsDocker = "true" 25 | New() 26 | test.IsEqualString(t, os.Getenv("TMPDIR"), "data") 27 | os.Setenv("TMPDIR", "test") 28 | New() 29 | test.IsEqualString(t, os.Getenv("TMPDIR"), "test") 30 | os.Unsetenv("TMPDIR") 31 | IsDocker = "false" 32 | } 33 | 34 | func TestEnvLoad(t *testing.T) { 35 | os.Setenv("GOKAPI_CONFIG_DIR", "test") 36 | os.Setenv("GOKAPI_CONFIG_FILE", "test2") 37 | os.Setenv("GOKAPI_LENGTH_ID", "7") 38 | env := New() 39 | test.IsEqualString(t, env.ConfigPath, "test/test2") 40 | test.IsEqualInt(t, env.LengthId, 7) 41 | os.Setenv("GOKAPI_LENGTH_ID", "3") 42 | env = New() 43 | test.IsEqualInt(t, env.LengthId, 5) 44 | os.Setenv("GOKAPI_LENGTH_ID", "86") 45 | env = New() 46 | test.IsEqualInt(t, env.LengthId, 86) 47 | os.Unsetenv("GOKAPI_LENGTH_ID") 48 | env = New() 49 | os.Setenv("GOKAPI_LENGTH_ID", "15") 50 | os.Setenv("GOKAPI_MAX_MEMORY_UPLOAD", "0") 51 | os.Setenv("GOKAPI_MAX_FILESIZE", "0") 52 | env = New() 53 | test.IsEqualInt(t, env.LengthId, 15) 54 | test.IsEqualInt(t, env.MaxFileSize, 5) 55 | test.IsEqualInt(t, env.MaxMemory, 5) 56 | os.Setenv("GOKAPI_MAX_FILESIZE", "invalid") 57 | returnCode = 0 58 | New() 59 | test.IsEqualInt(t, returnCode, 1) 60 | os.Unsetenv("GOKAPI_MAX_FILESIZE") 61 | } 62 | 63 | func TestIsAwsProvided(t *testing.T) { 64 | os.Unsetenv("GOKAPI_AWS_BUCKET") 65 | os.Unsetenv("GOKAPI_AWS_REGION") 66 | os.Unsetenv("GOKAPI_AWS_KEY") 67 | os.Unsetenv("GOKAPI_AWS_KEY_SECRET") 68 | env := New() 69 | test.IsEqualBool(t, env.IsAwsProvided(), false) 70 | os.Setenv("GOKAPI_AWS_BUCKET", "test") 71 | os.Setenv("GOKAPI_AWS_REGION", "test") 72 | os.Setenv("GOKAPI_AWS_KEY", "test") 73 | os.Setenv("GOKAPI_AWS_KEY_SECRET", "test") 74 | env = New() 75 | test.IsEqualBool(t, env.IsAwsProvided(), true) 76 | } 77 | 78 | func TestGetConfigPaths(t *testing.T) { 79 | configPath, configDir, configFile, awsConfig := GetConfigPaths() 80 | test.IsEqualString(t, configPath, "test/test2") 81 | test.IsEqualString(t, configDir, "test") 82 | test.IsEqualString(t, configFile, "test2") 83 | test.IsEqualString(t, awsConfig, "test/cloudconfig.yml") 84 | } 85 | -------------------------------------------------------------------------------- /internal/environment/flagparser/FlagParser_Disable.go: -------------------------------------------------------------------------------- 1 | //go:build test 2 | 3 | package flagparser 4 | 5 | // DisableParsing disables parsing when running unit tests, as parsing is called in the test's init() function, which results in an error 6 | var DisableParsing = true 7 | -------------------------------------------------------------------------------- /internal/environment/flagparser/FlagParser_Enable.go: -------------------------------------------------------------------------------- 1 | //go:build !test 2 | 3 | package flagparser 4 | 5 | // DisableParsing disables parsing when running unit tests, as parsing is called in the test's init() function, which results in an error 6 | var DisableParsing = false 7 | -------------------------------------------------------------------------------- /internal/helper/OS.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | /** 4 | Simplified OS functions 5 | */ 6 | 7 | import ( 8 | "bufio" 9 | "errors" 10 | "golang.org/x/term" 11 | "log" 12 | "os" 13 | "syscall" 14 | ) 15 | 16 | // FolderExists returns true if a folder exists 17 | func FolderExists(folder string) bool { 18 | _, err := os.Stat(folder) 19 | if err == nil { 20 | return true 21 | } 22 | return !os.IsNotExist(err) 23 | } 24 | 25 | // FileExists returns true if a file exists 26 | func FileExists(filename string) bool { 27 | info, err := os.Stat(filename) 28 | if os.IsNotExist(err) { 29 | return false 30 | } 31 | return !info.IsDir() 32 | } 33 | 34 | // CreateDir creates the data folder if it does not exist 35 | func CreateDir(name string) { 36 | if !FolderExists(name) { 37 | err := os.Mkdir(name, 0770) 38 | Check(err) 39 | } 40 | } 41 | 42 | // ReadLine reads a line from the terminal and returns it as a string 43 | func ReadLine() string { 44 | scanner := bufio.NewScanner(os.Stdin) 45 | scanner.Scan() 46 | text := scanner.Text() 47 | return text 48 | } 49 | 50 | // ReadPassword reads a line without displaying input from the terminal and returns it as a string 51 | func ReadPassword() string { 52 | // int conversion is required for Windows systems 53 | pw, err := term.ReadPassword(int(syscall.Stdin)) 54 | if err == nil { 55 | return string(pw) 56 | } 57 | return ReadLine() 58 | } 59 | 60 | // Check panics if err is not nil 61 | func Check(err error) { 62 | if err != nil { 63 | panic(err) 64 | } 65 | } 66 | 67 | // CheckIgnoreTimeout panics if err is not nil and not a timeout 68 | func CheckIgnoreTimeout(err error) { 69 | if err == nil { 70 | return 71 | } 72 | if os.IsTimeout(err) { 73 | log.Println(err) 74 | return 75 | } 76 | Check(err) 77 | } 78 | 79 | // IsInArray returns true if value is in array 80 | func IsInArray(haystack []string, needle string) bool { 81 | for _, item := range haystack { 82 | if needle == item { 83 | return true 84 | } 85 | } 86 | return false 87 | } 88 | 89 | // GetFileSize returns the file size in bytes 90 | func GetFileSize(file *os.File) (int64, error) { 91 | fileInfo, err := file.Stat() 92 | if err != nil { 93 | return 0, err 94 | } 95 | return fileInfo.Size(), nil 96 | } 97 | 98 | // ErrPathDoesNotExist is raised if the requested path does not exist 99 | var ErrPathDoesNotExist = errors.New("path does not exist") 100 | 101 | // ErrPathIsNotDir is raised if the requested path is not a directory 102 | var ErrPathIsNotDir = errors.New("path is not a directory") 103 | -------------------------------------------------------------------------------- /internal/helper/OS_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "errors" 5 | "github.com/forceu/gokapi/internal/test" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestIsInArray(t *testing.T) { 11 | test.IsEqualBool(t, IsInArray([]string{"test", "test2", "test3"}, "test2"), true) 12 | test.IsEqualBool(t, IsInArray([]string{"test", "test2", "test3"}, "invalid"), false) 13 | } 14 | 15 | func TestFolderCreation(t *testing.T) { 16 | test.IsEqualBool(t, FolderExists("invalid"), false) 17 | test.FileDoesNotExist(t, "invalid/file") 18 | test.IsEqualBool(t, FileExists("invalid/file"), false) 19 | CreateDir("invalid") 20 | test.IsEqualBool(t, FolderExists("invalid"), true) 21 | err := os.WriteFile("invalid/file", []byte("test"), 0644) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | test.FileExists(t, "invalid/file") 26 | test.IsEqualBool(t, FileExists("invalid/file"), true) 27 | os.RemoveAll("invalid") 28 | } 29 | 30 | func TestReadLine(t *testing.T) { 31 | original := test.StartMockInputStdin("test") 32 | output := ReadLine() 33 | test.StopMockInputStdin(original) 34 | test.IsEqualString(t, output, "test") 35 | } 36 | 37 | func TestReadPassword(t *testing.T) { 38 | original := test.StartMockInputStdin("testpw") 39 | output := ReadPassword() 40 | test.StopMockInputStdin(original) 41 | test.IsEqualString(t, output, "testpw") 42 | } 43 | 44 | func TestGetFileSize(t *testing.T) { 45 | os.WriteFile("testfile", []byte(""), 0777) 46 | file, err := os.OpenFile("testfile", os.O_RDONLY, 0644) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | size, _ := GetFileSize(file) 51 | test.IsEqualInt(t, int(size), 0) 52 | os.WriteFile("testfile", []byte("123"), 0777) 53 | size, _ = GetFileSize(file) 54 | test.IsEqualInt(t, int(size), 3) 55 | file, _ = os.OpenFile("invalid", os.O_RDONLY, 0644) 56 | size, _ = GetFileSize(file) 57 | test.IsEqualInt(t, int(size), 0) 58 | os.Remove("testfile") 59 | } 60 | 61 | func TestCheck(t *testing.T) { 62 | var err error 63 | Check(err) 64 | defer test.ExpectPanic(t) 65 | err = errors.New("test") 66 | Check(err) 67 | } 68 | 69 | func TestCheckIgnoreTimeout(t *testing.T) { 70 | CheckIgnoreTimeout(nil) 71 | CheckIgnoreTimeout(os.ErrDeadlineExceeded) 72 | defer test.ExpectPanic(t) 73 | CheckIgnoreTimeout(errors.New("other")) 74 | } 75 | -------------------------------------------------------------------------------- /internal/helper/StringGeneration.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | /** 4 | Generates / annotates strings 5 | */ 6 | 7 | import ( 8 | cryptorand "crypto/rand" 9 | "encoding/base64" 10 | "fmt" 11 | "log" 12 | "math/rand" 13 | "regexp" 14 | ) 15 | 16 | // A rune array to be used for pseudo-random string generation 17 | var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 18 | 19 | // Used if unable to generate secure random string. A warning will be output 20 | // to the CLI window 21 | func generateUnsafeId(length int) string { 22 | log.Println("Warning! Cannot generate securely random ID!") 23 | b := make([]rune, length) 24 | for i := range b { 25 | b[i] = characters[rand.Intn(len(characters))] 26 | } 27 | return string(b) 28 | } 29 | 30 | // Returns securely generated random bytes. 31 | // It will return an error if the system's secure random 32 | // number generator fails to function correctly 33 | func generateRandomBytes(n int) ([]byte, error) { 34 | b := make([]byte, n) 35 | _, err := cryptorand.Read(b) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return b, nil 40 | } 41 | 42 | // GenerateRandomString returns a URL-safe, base64 encoded securely generated random string. 43 | func GenerateRandomString(length int) string { 44 | b, err := generateRandomBytes(length + 10) 45 | if err != nil { 46 | return generateUnsafeId(length) 47 | } 48 | result := cleanRandomString(base64.URLEncoding.EncodeToString(b)) 49 | if len(result) < length { 50 | return GenerateRandomString(length) 51 | } 52 | return result[:length] 53 | } 54 | 55 | // ByteCountSI converts bytes to a human-readable format 56 | func ByteCountSI(b int64) string { 57 | const unit = 1024 58 | if b < unit { 59 | return fmt.Sprintf("%d B", b) 60 | } 61 | div, exp := int64(unit), 0 62 | for n := b / unit; n >= unit; n /= unit { 63 | div *= unit 64 | exp++ 65 | } 66 | return fmt.Sprintf("%.1f %cB", 67 | float64(b)/float64(div), "kMGTPE"[exp]) 68 | } 69 | 70 | // Removes special characters from string 71 | func cleanRandomString(input string) string { 72 | reg, err := regexp.Compile("[^a-zA-Z0-9]+") 73 | Check(err) 74 | return reg.ReplaceAllString(input, "") 75 | } 76 | -------------------------------------------------------------------------------- /internal/helper/StringGeneration_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/test" 5 | "testing" 6 | ) 7 | 8 | func TestByteCountSI(t *testing.T) { 9 | test.IsEqualString(t, ByteCountSI(5), "5 B") 10 | test.IsEqualString(t, ByteCountSI(5000), "4.9 kB") 11 | test.IsEqualString(t, ByteCountSI(5000000), "4.8 MB") 12 | test.IsEqualString(t, ByteCountSI(5000000000), "4.7 GB") 13 | test.IsEqualString(t, ByteCountSI(5000000000000), "4.5 TB") 14 | } 15 | 16 | func TestCleanString(t *testing.T) { 17 | test.IsEqualString(t, cleanRandomString("abc-123%%___!"), "abc123") 18 | } 19 | 20 | func TestGenerateRandomString(t *testing.T) { 21 | test.IsEqualBool(t, len(GenerateRandomString(100)) == 100, true) 22 | } 23 | 24 | func TestGenerateUnsafeId(t *testing.T) { 25 | test.IsEqualBool(t, len(generateUnsafeId(100)) == 100, true) 26 | } 27 | -------------------------------------------------------------------------------- /internal/helper/systemd/Systemd.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package systemd 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | // InstallService installs Gokapi as a systemd service 11 | func InstallService() { 12 | invalidOS() 13 | } 14 | 15 | // UninstallService uninstalls Gokapi as a systemd service 16 | func UninstallService() { 17 | invalidOS() 18 | } 19 | 20 | // invalidOS displays an error message and exits the program, as systemd is not supported on Windows 21 | func invalidOS() { 22 | fmt.Println("This feature is only supported on systems using systemd.") 23 | os.Exit(2) 24 | } 25 | -------------------------------------------------------------------------------- /internal/logging/Logging_test.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "github.com/forceu/gokapi/internal/test" 6 | "github.com/forceu/gokapi/internal/test/testconfiguration" 7 | "net/http/httptest" 8 | "os" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestMain(m *testing.M) { 15 | testconfiguration.Create(false) 16 | exitVal := m.Run() 17 | testconfiguration.Delete() 18 | os.Exit(exitVal) 19 | } 20 | 21 | func TestGetIpAddress(t *testing.T) { 22 | r := httptest.NewRequest("GET", "/test", nil) 23 | test.IsEqualString(t, getIpAddress(r), "192.0.2.1") 24 | r = httptest.NewRequest("GET", "/test", nil) 25 | r.RemoteAddr = "127.0.0.1:1234" 26 | test.IsEqualString(t, getIpAddress(r), "127.0.0.1") 27 | r.RemoteAddr = "invalid" 28 | test.IsEqualString(t, getIpAddress(r), "Unknown IP") 29 | r.Header.Add("X-REAL-IP", "1.1.1.1") 30 | test.IsEqualString(t, getIpAddress(r), "1.1.1.1") 31 | r.Header.Add("X-FORWARDED-FOR", "1.1.1.2") 32 | test.IsEqualString(t, getIpAddress(r), "1.1.1.2") 33 | } 34 | 35 | func TestInit(t *testing.T) { 36 | Init("test") 37 | test.IsEqualString(t, logPath, "test/log.txt") 38 | } 39 | 40 | func TestAddString(t *testing.T) { 41 | test.FileDoesNotExist(t, "test/log.txt") 42 | createLogEntry(categoryInfo, "Hello", true) 43 | test.FileExists(t, "test/log.txt") 44 | content, _ := os.ReadFile("test/log.txt") 45 | test.IsEqualBool(t, strings.Contains(string(content), "UTC [info] Hello"), true) 46 | } 47 | 48 | func TestAddDownload(t *testing.T) { 49 | file := models.File{ 50 | Id: "testId", 51 | Name: "testName", 52 | } 53 | r := httptest.NewRequest("GET", "/test", nil) 54 | r.Header.Set("User-Agent", "testAgent") 55 | r.Header.Add("X-REAL-IP", "1.1.1.1") 56 | LogDownload(file, r, true) 57 | // Need sleep, as LogDownload() is non-blocking 58 | time.Sleep(500 * time.Millisecond) 59 | content, _ := os.ReadFile("test/log.txt") 60 | test.IsEqualBool(t, strings.Contains(string(content), "UTC [download] testName, IP 1.1.1.1, ID testId, Useragent testAgent"), true) 61 | r.Header.Add("X-REAL-IP", "2.2.2.2") 62 | LogDownload(file, r, false) 63 | // Need sleep, as LogDownload() is non-blocking 64 | time.Sleep(500 * time.Millisecond) 65 | content, _ = os.ReadFile("test/log.txt") 66 | test.IsEqualBool(t, strings.Contains(string(content), "2.2.2.2"), false) 67 | } 68 | -------------------------------------------------------------------------------- /internal/models/Authentication.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // AuthenticationConfig holds configuration on how to authenticate to Gokapi admin menu 4 | type AuthenticationConfig struct { 5 | Method int `json:"Method"` 6 | SaltAdmin string `json:"SaltAdmin"` 7 | SaltFiles string `json:"SaltFiles"` 8 | Username string `json:"Username"` 9 | HeaderKey string `json:"HeaderKey"` 10 | OAuthProvider string `json:"OauthProvider"` 11 | OAuthClientId string `json:"OAuthClientId"` 12 | OAuthClientSecret string `json:"OAuthClientSecret"` 13 | OAuthGroupScope string `json:"OauthGroupScope"` 14 | OAuthRecheckInterval int `json:"OAuthRecheckInterval"` 15 | OAuthGroups []string `json:"OAuthGroups"` 16 | OnlyRegisteredUsers bool `json:"OnlyRegisteredUsers"` 17 | } 18 | 19 | const ( 20 | // AuthenticationInternal authentication method uses a user / password combination handled by Gokapi 21 | AuthenticationInternal = iota 22 | 23 | // AuthenticationOAuth2 authentication retrieves the users email with Open Connect ID 24 | AuthenticationOAuth2 25 | 26 | // AuthenticationHeader authentication relies on a header from a reverse proxy to parse the username 27 | AuthenticationHeader 28 | 29 | // AuthenticationDisabled authentication ignores all internal authentication procedures. A reverse proxy needs to restrict access 30 | AuthenticationDisabled 31 | ) 32 | -------------------------------------------------------------------------------- /internal/models/AwsConfig.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // AwsConfig contains all configuration values / credentials for AWS cloud storage 4 | type AwsConfig struct { 5 | Bucket string `yaml:"Bucket"` 6 | Region string `yaml:"Region"` 7 | KeyId string `yaml:"KeyId"` 8 | KeySecret string `yaml:"KeySecret"` 9 | Endpoint string `yaml:"Endpoint"` 10 | ProxyDownload bool `yaml:"ProxyDownload"` 11 | } 12 | 13 | // IsAllProvided returns true if all required variables have been set for using AWS S3 / Backblaze 14 | func (c *AwsConfig) IsAllProvided() bool { 15 | return c.Bucket != "" && 16 | c.Region != "" && 17 | c.KeyId != "" && 18 | c.KeySecret != "" 19 | } 20 | -------------------------------------------------------------------------------- /internal/models/AwsConfig_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/test" 5 | "testing" 6 | ) 7 | 8 | func TestIsAwsProvided(t *testing.T) { 9 | config := AwsConfig{} 10 | test.IsEqualBool(t, config.IsAllProvided(), false) 11 | config = AwsConfig{ 12 | Bucket: "test", 13 | Region: "test", 14 | Endpoint: "", 15 | KeyId: "test", 16 | KeySecret: "test", 17 | } 18 | test.IsEqualBool(t, config.IsAllProvided(), true) 19 | } 20 | -------------------------------------------------------------------------------- /internal/models/Configuration.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | ) 7 | 8 | // Configuration is a struct that contains the global configuration 9 | type Configuration struct { 10 | Authentication AuthenticationConfig `json:"Authentication"` 11 | Port string `json:"Port"` 12 | ServerUrl string `json:"ServerUrl"` 13 | RedirectUrl string `json:"RedirectUrl"` 14 | PublicName string `json:"PublicName"` 15 | DataDir string `json:"DataDir"` 16 | DatabaseUrl string `json:"DatabaseUrl"` 17 | ConfigVersion int `json:"ConfigVersion"` 18 | MaxFileSizeMB int `json:"MaxFileSizeMB"` 19 | MaxMemory int `json:"MaxMemory"` 20 | ChunkSize int `json:"ChunkSize"` 21 | MaxParallelUploads int `json:"MaxParallelUploads"` 22 | LengthId int `json:"-"` 23 | LengthHotlinkId int `json:"-"` 24 | Encryption Encryption `json:"Encryption"` 25 | UseSsl bool `json:"UseSsl"` 26 | PicturesAlwaysLocal bool `json:"PicturesAlwaysLocal"` 27 | SaveIp bool `json:"SaveIp"` 28 | IncludeFilename bool `json:"IncludeFilename"` 29 | } 30 | 31 | // Encryption hold information about the encryption used on this file 32 | type Encryption struct { 33 | Level int 34 | Cipher []byte 35 | Salt string 36 | Checksum string 37 | ChecksumSalt string 38 | } 39 | 40 | // ToJson returns an idented JSon representation 41 | func (c Configuration) ToJson() []byte { 42 | result, err := json.MarshalIndent(c, "", " ") 43 | if err != nil { 44 | log.Fatal("Error encoding configuration:", err) 45 | } 46 | return result 47 | } 48 | 49 | // ToString returns the object as an unidented Json string used for test units 50 | func (c Configuration) ToString() string { 51 | result, err := json.Marshal(c) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | return string(result) 56 | } 57 | -------------------------------------------------------------------------------- /internal/models/Configuration_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/test" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | var testConfig = Configuration{ 10 | Authentication: AuthenticationConfig{ 11 | Method: 0, 12 | SaltAdmin: "saltadmin", 13 | SaltFiles: "saltfiles", 14 | Username: "admin", 15 | HeaderKey: "", 16 | OAuthProvider: "", 17 | OAuthClientId: "", 18 | OAuthClientSecret: "", 19 | }, 20 | Port: ":12345", 21 | ServerUrl: "https://testserver.com/", 22 | RedirectUrl: "https://test.com", 23 | DatabaseUrl: "sqlite://./test/gokapitest.sqlite", 24 | ConfigVersion: 14, 25 | LengthId: 5, 26 | LengthHotlinkId: 10, 27 | DataDir: "test", 28 | MaxMemory: 50, 29 | UseSsl: true, 30 | MaxFileSizeMB: 20, 31 | PublicName: "public-name", 32 | Encryption: Encryption{ 33 | Level: 1, 34 | Cipher: []byte{0x00}, 35 | Salt: "encsalt", 36 | Checksum: "encsum", 37 | ChecksumSalt: "encsumsalt", 38 | }, 39 | PicturesAlwaysLocal: true, 40 | } 41 | 42 | func TestConfiguration_ToJson(t *testing.T) { 43 | test.IsEqualBool(t, strings.Contains(string(testConfig.ToJson()), "\"SaltAdmin\": \"saltadmin\""), true) 44 | } 45 | 46 | func TestConfiguration_ToString(t *testing.T) { 47 | test.IsEqualString(t, testConfig.ToString(), exptectedUnidentedOutput) 48 | } 49 | 50 | const exptectedUnidentedOutput = `{"Authentication":{"Method":0,"SaltAdmin":"saltadmin","SaltFiles":"saltfiles","Username":"admin","HeaderKey":"","OauthProvider":"","OAuthClientId":"","OAuthClientSecret":"","OauthGroupScope":"","OAuthRecheckInterval":0,"OAuthGroups":null,"OnlyRegisteredUsers":false},"Port":":12345","ServerUrl":"https://testserver.com/","RedirectUrl":"https://test.com","PublicName":"public-name","DataDir":"test","DatabaseUrl":"sqlite://./test/gokapitest.sqlite","ConfigVersion":14,"MaxFileSizeMB":20,"MaxMemory":50,"ChunkSize":0,"MaxParallelUploads":0,"Encryption":{"Level":1,"Cipher":"AA==","Salt":"encsalt","Checksum":"encsum","ChecksumSalt":"encsumsalt"},"UseSsl":true,"PicturesAlwaysLocal":true,"SaveIp":false,"IncludeFilename":false}` 51 | -------------------------------------------------------------------------------- /internal/models/DbConnection.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // DbConnection is a struct that contains the database configuration for connecting 4 | type DbConnection struct { 5 | HostUrl string 6 | RedisPrefix string 7 | Username string 8 | Password string 9 | RedisUseSsl bool 10 | Type int 11 | } 12 | -------------------------------------------------------------------------------- /internal/models/End2EndEncryption.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // E2EInfoPlainText is stored locally and will be encrypted before storing on server 4 | type E2EInfoPlainText struct { 5 | Files []E2EFile `json:"files"` 6 | } 7 | 8 | // E2EInfoEncrypted is the struct that is stored on the server and decrypted locally 9 | type E2EInfoEncrypted struct { 10 | // Version of the E2E used, must be at least 1 11 | Version int `json:"version" redis:"version"` 12 | // Nonce used for encryption 13 | Nonce []byte `json:"nonce" redis:"nonce"` 14 | // Content that is encrypted 15 | Content []byte `json:"content" redis:"content"` 16 | // AvailableFiles contains a list of all files on the webserver and will be populated 17 | // when reading from the database, but will not be saved to the database 18 | AvailableFiles []string `json:"availablefiles" redis:"-"` 19 | } 20 | 21 | // HasBeenSetUp returns true if E2E setup has been run 22 | func (e *E2EInfoEncrypted) HasBeenSetUp() bool { 23 | return e.Version != 0 && len(e.Content) != 0 24 | } 25 | 26 | // E2EFile contains information about a stored e2e file 27 | type E2EFile struct { 28 | Uuid string `json:"uuid"` 29 | Id string `json:"id"` 30 | Filename string `json:"filename"` 31 | Cipher []byte `json:"cipher"` 32 | } 33 | 34 | // E2EHashContent contains the info that is added after the hash for an e2e link 35 | type E2EHashContent struct { 36 | Filename string `json:"f"` 37 | Cipher string `json:"c"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/models/End2EndEncryption_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "testing" 4 | 5 | func TestE2EInfoEncrypted_HasBeenSetUp(t *testing.T) { 6 | type fields struct { 7 | Version int 8 | Nonce []byte 9 | Content []byte 10 | AvailableFiles []string 11 | } 12 | tests := []struct { 13 | name string 14 | fields fields 15 | want bool 16 | }{ 17 | {"empty", fields{}, false}, 18 | {"version 0", fields{Version: 0}, false}, 19 | {"version 1, empty", fields{Version: 1}, false}, 20 | {"version 0, not empty", fields{Version: 0, Content: []byte("content")}, false}, 21 | {"version 1, not empty", fields{Version: 1, Content: []byte("content")}, true}, 22 | } 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | e := &E2EInfoEncrypted{ 26 | Version: tt.fields.Version, 27 | Nonce: tt.fields.Nonce, 28 | Content: tt.fields.Content, 29 | AvailableFiles: tt.fields.AvailableFiles, 30 | } 31 | if got := e.HasBeenSetUp(); got != tt.want { 32 | t.Errorf("HasBeenSetUp() = %v, want %v", got, tt.want) 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/models/FileList_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "github.com/forceu/gokapi/internal/test" 6 | "testing" 7 | ) 8 | 9 | func TestToJsonResult(t *testing.T) { 10 | file := File{ 11 | Id: "testId", 12 | Name: "testName", 13 | Size: "10 B", 14 | SizeBytes: 10, 15 | SHA1: "sha256", 16 | ExpireAt: 1750852108, 17 | ExpireAtString: "Wed Jun 25 2025 11:48:28", 18 | DownloadsRemaining: 1, 19 | PasswordHash: "pwhash", 20 | HotlinkId: "hotlinkid", 21 | ContentType: "text/html", 22 | AwsBucket: "test", 23 | UploadDate: 1748180908, 24 | UserId: 2, 25 | DownloadCount: 3, 26 | Encryption: EncryptionInfo{ 27 | IsEncrypted: true, 28 | DecryptionKey: []byte{0x01}, 29 | Nonce: []byte{0x02}, 30 | }, 31 | UnlimitedDownloads: true, 32 | UnlimitedTime: true, 33 | PendingDeletion: 100, 34 | } 35 | test.IsEqualString(t, file.ToJsonResult("serverurl/", false), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"Wed Jun 25 2025 11:48:28","UrlDownload":"serverurl/d?id=testId","UrlHotlink":"","UploadDate":1748180908,"ExpireAt":1750852108,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"IsPendingDeletion":true,"UploaderId":2},"IncludeFilename":false}`) 36 | test.IsEqualString(t, file.ToJsonResult("serverurl/", true), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"Wed Jun 25 2025 11:48:28","UrlDownload":"serverurl/d/testId/testName","UrlHotlink":"","UploadDate":1748180908,"ExpireAt":1750852108,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"IsPendingDeletion":true,"UploaderId":2},"IncludeFilename":true}`) 37 | } 38 | 39 | func TestIsLocalStorage(t *testing.T) { 40 | file := File{AwsBucket: "123"} 41 | test.IsEqualBool(t, file.IsLocalStorage(), false) 42 | file.AwsBucket = "" 43 | test.IsEqualBool(t, file.IsLocalStorage(), true) 44 | } 45 | 46 | func TestErrorAsJson(t *testing.T) { 47 | result := errorAsJson(errors.New("testerror")) 48 | test.IsEqualString(t, result, "{\"Result\":\"error\",\"ErrorMessage\":\"testerror\"}") 49 | } 50 | 51 | func TestRequiresClientDecryption(t *testing.T) { 52 | file := File{ 53 | Id: "test", 54 | AwsBucket: "bucket", 55 | Encryption: EncryptionInfo{ 56 | IsEncrypted: true, 57 | }, 58 | } 59 | test.IsEqualBool(t, file.RequiresClientDecryption(), true) 60 | file.Encryption.IsEncrypted = false 61 | test.IsEqualBool(t, file.RequiresClientDecryption(), false) 62 | file.AwsBucket = "" 63 | test.IsEqualBool(t, file.RequiresClientDecryption(), false) 64 | file.Encryption.IsEncrypted = true 65 | test.IsEqualBool(t, file.RequiresClientDecryption(), false) 66 | } 67 | 68 | func TestGetHolinkUrl(t *testing.T) { 69 | file := FileApiOutput{ 70 | Id: "testfile", 71 | Name: "name", 72 | Size: "1 B", 73 | HotlinkId: "test", 74 | RequiresClientSideDecryption: true, 75 | } 76 | url := getHotlinkUrl(file, "testserver/", false) 77 | test.IsEqualString(t, url, "") 78 | file.RequiresClientSideDecryption = false 79 | url = getHotlinkUrl(file, "testserver/", false) 80 | test.IsEqualString(t, url, "testserver/h/test") 81 | file.HotlinkId = "" 82 | url = getHotlinkUrl(file, "testserver/", false) 83 | test.IsEqualString(t, url, "testserver/downloadFile?id=testfile") 84 | url = getHotlinkUrl(file, "testserver/", true) 85 | test.IsEqualString(t, url, "testserver/dh/testfile/name") 86 | } 87 | -------------------------------------------------------------------------------- /internal/models/FileUpload.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // UploadRequest is used to set an upload request 4 | type UploadRequest struct { 5 | UserId int 6 | AllowedDownloads int 7 | Expiry int 8 | MaxMemory int 9 | ExpiryTimestamp int64 10 | RealSize int64 11 | UnlimitedDownload bool 12 | UnlimitedTime bool 13 | IsEndToEndEncrypted bool 14 | Password string 15 | ExternalUrl string 16 | } 17 | -------------------------------------------------------------------------------- /internal/models/Session.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Session contains cookie parameter 4 | type Session struct { 5 | RenewAt int64 `redis:"renew_at"` 6 | ValidUntil int64 `redis:"valid_until"` 7 | UserId int `redis:"user_id"` 8 | } 9 | -------------------------------------------------------------------------------- /internal/models/UploadStatus.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // UploadStatus contains information about the current status of a file upload 4 | type UploadStatus struct { 5 | // ChunkId is the identifier for the chunk 6 | ChunkId string 7 | // CurrentStatus indicates if the chunk is currently being processed (e.g. encrypting or 8 | // hashing) or being moved/uploaded to the file storage 9 | // See ProcessingStatus for definition 10 | CurrentStatus int 11 | // FileId is populated, once a file has been created from a chunk 12 | FileId string 13 | // ErrorMessage is empty, unless an error occurred 14 | ErrorMessage string `json:"errormessage"` 15 | // Creation is the unix time when the status was created and is populated automatically 16 | Creation int64 17 | } 18 | -------------------------------------------------------------------------------- /internal/storage/filesystem/FileSystem.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/storage/filesystem/interfaces" 5 | "github.com/forceu/gokapi/internal/storage/filesystem/localstorage" 6 | "github.com/forceu/gokapi/internal/storage/filesystem/s3filesystem" 7 | "github.com/forceu/gokapi/internal/storage/filesystem/s3filesystem/aws" 8 | "log" 9 | ) 10 | 11 | var dataFilesystem interfaces.System 12 | var s3FileSystem interfaces.System 13 | 14 | // ActiveStorageSystem is a driver for the storage system that is in use currently. Can be either 15 | // the local filesystem or S3, depending on the configuration 16 | var ActiveStorageSystem interfaces.System 17 | 18 | // Init initializes the filesystems and must be called on start 19 | func Init(pathData string) { 20 | dataFilesystem = localstorage.GetDriver() 21 | dataFilesystem.Init(localstorage.Config{ 22 | DataPath: pathData, 23 | }) 24 | ActiveStorageSystem = dataFilesystem 25 | } 26 | 27 | // SetAws sets the AWS filesystem as the default storage 28 | func SetAws() { 29 | if aws.IsIncludedInBuild { 30 | s3FileSystem = s3filesystem.GetDriver() 31 | ok := s3FileSystem.Init(s3filesystem.Config{Bucket: aws.GetDefaultBucketName()}) 32 | if !ok && !isUnitTesting { 33 | log.Println("Unable to set AWS S3 as filesystem") 34 | return 35 | } 36 | ActiveStorageSystem = s3FileSystem 37 | } 38 | } 39 | 40 | // SetLocal sets the local filesystem as the default storage 41 | func SetLocal() { 42 | ActiveStorageSystem = dataFilesystem 43 | } 44 | 45 | // GetLocal gets the local filesystem, regardless of what filesystem is used by default. This is required when encrypted 46 | // pictures are stored locally instead of the cloud to support hotlinking 47 | func GetLocal() interfaces.System { 48 | return dataFilesystem 49 | } 50 | 51 | // isUnitTesting is only set to true when testing, to avoid login with aws 52 | var isUnitTesting = false 53 | -------------------------------------------------------------------------------- /internal/storage/filesystem/fileSystem_test.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | fileInterfaces "github.com/forceu/gokapi/internal/storage/filesystem/interfaces" 6 | "github.com/forceu/gokapi/internal/storage/filesystem/s3filesystem/aws" 7 | "github.com/forceu/gokapi/internal/test" 8 | "testing" 9 | ) 10 | 11 | func TestInit(t *testing.T) { 12 | Init("./test") 13 | test.IsEqualBool(t, ActiveStorageSystem == dataFilesystem, true) 14 | test.IsEqualBool(t, ActiveStorageSystem == s3FileSystem, false) 15 | test.IsEqualString(t, ActiveStorageSystem.GetSystemName(), fileInterfaces.DriverLocal) 16 | } 17 | 18 | func TestSetLocal(t *testing.T) { 19 | ActiveStorageSystem = nil 20 | SetLocal() 21 | test.IsEqualBool(t, ActiveStorageSystem == dataFilesystem, true) 22 | } 23 | 24 | func TestSetAws(t *testing.T) { 25 | ActiveStorageSystem = nil 26 | if !aws.IsIncludedInBuild { 27 | SetAws() 28 | test.IsNil(t, ActiveStorageSystem) 29 | return 30 | } 31 | aws.Init(models.AwsConfig{ 32 | Bucket: "test1", 33 | Region: "test2", 34 | KeyId: "test3", 35 | KeySecret: "test4", 36 | Endpoint: "test5", 37 | }) 38 | SetAws() 39 | test.IsNil(t, ActiveStorageSystem) 40 | isUnitTesting = true 41 | SetAws() 42 | test.IsEqualBool(t, ActiveStorageSystem == s3FileSystem, true) 43 | test.IsEqualBool(t, ActiveStorageSystem == dataFilesystem, false) 44 | test.IsEqualString(t, ActiveStorageSystem.GetSystemName(), fileInterfaces.DriverAws) 45 | 46 | } 47 | -------------------------------------------------------------------------------- /internal/storage/filesystem/interfaces/Interfaces.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "os" 6 | ) 7 | 8 | // DriverLocal is returned as a name for the Local Storage driver 9 | const DriverLocal = "localstorage" 10 | 11 | // DriverAws is returned as a name for the AWS Storage driver 12 | const DriverAws = "awss3" 13 | 14 | // File contains information about the stored file 15 | type File interface { 16 | // Exists returns true if the file exists 17 | Exists() bool 18 | // GetName returns the name of the file 19 | GetName() string 20 | } 21 | 22 | // System is a driver for storing and retrieving files 23 | type System interface { 24 | // Init sets the driver configurations and returns true if successful 25 | Init(input any) bool 26 | // IsAvailable returns true if the driver can be used 27 | IsAvailable() bool 28 | // GetSystemName returns the name of the driver 29 | GetSystemName() string 30 | // MoveToFilesystem moves a file from the local filesystem to the driver's filesystem 31 | MoveToFilesystem(sourceFile *os.File, metaData models.File) error 32 | // GetFile returns a File struct for the corresponding filename 33 | GetFile(filename string) File 34 | // FileExists returns true if the system contains a file with the given relative filepath 35 | FileExists(filepath string) (bool, error) 36 | } 37 | -------------------------------------------------------------------------------- /internal/storage/filesystem/interfaces/Interfaces_test.go: -------------------------------------------------------------------------------- 1 | //go:build test 2 | 3 | package interfaces 4 | -------------------------------------------------------------------------------- /internal/storage/filesystem/localstorage/Localstorage.go: -------------------------------------------------------------------------------- 1 | package localstorage 2 | 3 | import ( 4 | "errors" 5 | "github.com/forceu/gokapi/internal/helper" 6 | "github.com/forceu/gokapi/internal/models" 7 | fileInterfaces "github.com/forceu/gokapi/internal/storage/filesystem/interfaces" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | // GetDriver returns a driver for the local file system 13 | func GetDriver() fileInterfaces.System { 14 | return &localStorageDriver{} 15 | } 16 | 17 | type localStorageDriver struct { 18 | dataPath string 19 | filePrefix string 20 | } 21 | 22 | // Config is the required configuration for the driver 23 | type Config struct { 24 | // DataPath is the top directory where files are stored 25 | DataPath string 26 | // FilePrefix is an optional setting, if files are to be stored with the prefix 27 | FilePrefix string 28 | } 29 | 30 | // MoveToFilesystem moves a file from the local filesystem to data path 31 | func (d *localStorageDriver) MoveToFilesystem(sourceFile *os.File, metaData models.File) error { 32 | err := sourceFile.Close() 33 | if err != nil { 34 | return err 35 | } 36 | if metaData.SHA1 == "" { 37 | return errors.New("empty metadata passed") 38 | } 39 | return os.Rename(sourceFile.Name(), d.getPath()+d.filePrefix+metaData.SHA1) 40 | } 41 | 42 | // Init sets the driver configurations and returns true if successful 43 | // Requires a Config struct as input 44 | func (d *localStorageDriver) Init(input any) bool { 45 | config, ok := input.(Config) 46 | if !ok { 47 | panic("runtime exception: input for local filesystem is not a config object") 48 | } 49 | if config.DataPath == "" { 50 | panic("empty path has been passed") 51 | } 52 | if !strings.HasSuffix(config.DataPath, string(os.PathSeparator)) { 53 | config.DataPath = config.DataPath + string(os.PathSeparator) 54 | } 55 | d.dataPath = config.DataPath 56 | d.filePrefix = config.FilePrefix 57 | return true 58 | } 59 | 60 | // IsAvailable returns true if the data path is writable 61 | func (d *localStorageDriver) IsAvailable() bool { 62 | return true 63 | } 64 | 65 | // GetFile returns a File struct for the corresponding filename 66 | func (d *localStorageDriver) GetFile(filename string) fileInterfaces.File { 67 | return &localFile{Directory: d.getPath(), Filename: d.filePrefix + filename} 68 | } 69 | 70 | // FileExists returns true if the system contains a file with the given relative filepath 71 | func (d *localStorageDriver) FileExists(filename string) (bool, error) { 72 | file := localFile{ 73 | Directory: d.getPath(), 74 | Filename: d.filePrefix + filename, 75 | } 76 | return file.Exists(), nil 77 | } 78 | 79 | // GetSystemName returns the name of the driver 80 | func (d *localStorageDriver) GetSystemName() string { 81 | return fileInterfaces.DriverLocal 82 | } 83 | 84 | func (d *localStorageDriver) getPath() string { 85 | if d.dataPath == "" { 86 | panic("no path has been set!") 87 | } 88 | return d.dataPath 89 | } 90 | 91 | type localFile struct { 92 | Directory string 93 | Filename string 94 | } 95 | 96 | // Exists returns true if the file exists 97 | func (f *localFile) Exists() bool { 98 | return helper.FileExists(f.Directory + f.Filename) 99 | } 100 | 101 | // GetName returns the name of the file 102 | func (f *localFile) GetName() string { 103 | return f.Filename 104 | } 105 | -------------------------------------------------------------------------------- /internal/storage/filesystem/localstorage/Localstorage_test.go: -------------------------------------------------------------------------------- 1 | package localstorage 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "github.com/forceu/gokapi/internal/test" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | os.Mkdir("test/", 0777) 12 | os.Mkdir("test/data", 0777) 13 | exitVal := m.Run() 14 | os.RemoveAll("test") 15 | os.Exit(exitVal) 16 | } 17 | 18 | func getTestDriver(t *testing.T) *localStorageDriver { 19 | t.Helper() 20 | driver := GetDriver() 21 | result, ok := driver.(*localStorageDriver) 22 | test.IsEqualBool(t, ok, true) 23 | return result 24 | } 25 | 26 | func initDriver(t *testing.T, d *localStorageDriver) { 27 | ok := d.Init(Config{ 28 | DataPath: "test/data", 29 | FilePrefix: "123", 30 | }) 31 | test.IsEqualBool(t, ok, true) 32 | } 33 | 34 | func TestGetDriver(t *testing.T) { 35 | getTestDriver(t) 36 | } 37 | 38 | func TestLocalStorageDriver_Init(t *testing.T) { 39 | driver := getTestDriver(t) 40 | ok := driver.Init(Config{ 41 | DataPath: "test", 42 | FilePrefix: "tpref", 43 | }) 44 | test.IsEqualBool(t, ok, true) 45 | test.IsEqualString(t, driver.getPath(), "test/") 46 | ok = driver.Init(Config{ 47 | DataPath: "test2/", 48 | FilePrefix: "", 49 | }) 50 | test.IsEqualBool(t, ok, true) 51 | test.IsEqualString(t, driver.getPath(), "test2/") 52 | defer test.ExpectPanic(t) 53 | driver.Init(struct { 54 | invalid string 55 | }{invalid: "true"}) 56 | } 57 | 58 | func TestLocalStorageDriver_Init2(t *testing.T) { 59 | driver := getTestDriver(t) 60 | defer test.ExpectPanic(t) 61 | driver.Init(Config{ 62 | DataPath: "", 63 | FilePrefix: "tpref", 64 | }) 65 | } 66 | 67 | func TestLocalStorageDriver_IsAvailable(t *testing.T) { 68 | driver := getTestDriver(t) 69 | test.IsEqualBool(t, driver.IsAvailable(), true) 70 | } 71 | 72 | func TestGetDataPath(t *testing.T) { 73 | driver := getTestDriver(t) 74 | initDriver(t, driver) 75 | test.IsEqualString(t, driver.getPath(), "test/data/") 76 | driver.dataPath = "" 77 | defer test.ExpectPanic(t) 78 | driver.getPath() 79 | } 80 | func TestLocalStorageDriver_MoveToFilesystem(t *testing.T) { 81 | driver := getTestDriver(t) 82 | initDriver(t, driver) 83 | metaData := models.File{ 84 | SHA1: "testsha", 85 | } 86 | err := driver.MoveToFilesystem(nil, metaData) 87 | test.IsNotNil(t, err) 88 | err = os.WriteFile("test/testfile", []byte("This is a test"), 0777) 89 | test.IsNil(t, err) 90 | file, err := os.Open("test/testfile") 91 | test.IsNil(t, err) 92 | err = driver.MoveToFilesystem(file, models.File{}) 93 | test.IsNotNil(t, err) 94 | test.FileExists(t, "test/testfile") 95 | file, err = os.Open("test/testfile") 96 | test.IsNil(t, err) 97 | err = driver.MoveToFilesystem(file, metaData) 98 | test.IsNil(t, err) 99 | test.FileDoesNotExist(t, "test/testfile") 100 | test.FileExists(t, "test/data/123testsha") 101 | 102 | } 103 | 104 | func TestLocalFile_Exists(t *testing.T) { 105 | driver := getTestDriver(t) 106 | initDriver(t, driver) 107 | test.FileExists(t, "test/data/123testsha") 108 | test.FileDoesNotExist(t, "test/data/testsha") 109 | file := driver.GetFile("testsha") 110 | test.IsEqualBool(t, file.Exists(), true) 111 | test.IsEqualString(t, file.GetName(), "123testsha") 112 | } 113 | 114 | func TestLocalStorageDriver_FileExists(t *testing.T) { 115 | driver := getTestDriver(t) 116 | initDriver(t, driver) 117 | test.FileExists(t, "test/data/123testsha") 118 | test.FileDoesNotExist(t, "test/data/testsha") 119 | exist, err := driver.FileExists("testsha") 120 | test.IsNil(t, err) 121 | test.IsEqualBool(t, exist, true) 122 | exist, err = driver.FileExists("123testsha") 123 | test.IsNil(t, err) 124 | test.IsEqualBool(t, exist, false) 125 | } 126 | 127 | func TestLocalStorageDriver_GetSystemName(t *testing.T) { 128 | driver := getTestDriver(t) 129 | test.IsEqualString(t, driver.GetSystemName(), "localstorage") 130 | } 131 | -------------------------------------------------------------------------------- /internal/storage/filesystem/s3filesystem/S3filesystem.go: -------------------------------------------------------------------------------- 1 | package s3filesystem 2 | 3 | import ( 4 | "fmt" 5 | "github.com/forceu/gokapi/internal/models" 6 | fileInterfaces "github.com/forceu/gokapi/internal/storage/filesystem/interfaces" 7 | "github.com/forceu/gokapi/internal/storage/filesystem/s3filesystem/aws" 8 | "os" 9 | ) 10 | 11 | // GetDriver returns a driver for the AWS file system 12 | func GetDriver() fileInterfaces.System { 13 | return &s3StorageDriver{} 14 | } 15 | 16 | type s3StorageDriver struct { 17 | Bucket string 18 | } 19 | 20 | // Config is the required configuration for the driver 21 | type Config struct { 22 | // Bucket is the name of the bucket to store new files 23 | Bucket string 24 | } 25 | 26 | // MoveToFilesystem uploads a file from the local filesystem to the bucket specified in the metadata 27 | func (d *s3StorageDriver) MoveToFilesystem(sourceFile *os.File, metaData models.File) error { 28 | _, err := aws.Upload(sourceFile, metaData) 29 | if err != nil { 30 | return err 31 | } 32 | err = sourceFile.Close() 33 | if err != nil { 34 | return err 35 | } 36 | return os.Remove(sourceFile.Name()) 37 | } 38 | 39 | // Init sets the driver configurations and returns true if successful 40 | // Requires a Config struct as input 41 | func (d *s3StorageDriver) Init(input any) bool { 42 | config, ok := input.(Config) 43 | if !ok { 44 | panic("runtime exception: input for aws filesystem is not a config object") 45 | } 46 | if config.Bucket == "" { 47 | panic("empty bucket has been passed") 48 | } 49 | d.Bucket = config.Bucket 50 | return aws.IsAvailable() 51 | } 52 | 53 | // IsAvailable returns true if AWS is available and login was successful once 54 | func (d *s3StorageDriver) IsAvailable() bool { 55 | return aws.IsAvailable() 56 | } 57 | 58 | // GetFile returns a File struct for the corresponding filename 59 | func (d *s3StorageDriver) GetFile(filename string) fileInterfaces.File { 60 | return &awsFile{Bucket: d.Bucket, Filename: filename} 61 | } 62 | 63 | // FileExists returns true if the system contains a file with the given relative filepath in the bucket 64 | func (d *s3StorageDriver) FileExists(filename string) (bool, error) { 65 | exists, _, err := aws.FileExists(models.File{AwsBucket: d.Bucket, SHA1: filename}) 66 | return exists, err 67 | } 68 | 69 | // GetSystemName returns the name of the driver 70 | func (d *s3StorageDriver) GetSystemName() string { 71 | return fileInterfaces.DriverAws 72 | } 73 | 74 | type awsFile struct { 75 | Bucket string 76 | Filename string 77 | } 78 | 79 | func (f *awsFile) Exists() bool { 80 | exists, _, err := aws.FileExists(models.File{AwsBucket: f.Bucket, SHA1: f.Filename}) 81 | if err != nil { 82 | fmt.Println(err) 83 | return false 84 | } 85 | return exists 86 | } 87 | 88 | func (f *awsFile) GetName() string { 89 | return f.Filename 90 | } 91 | -------------------------------------------------------------------------------- /internal/storage/filesystem/s3filesystem/S3filesystem_test.go: -------------------------------------------------------------------------------- 1 | package s3filesystem 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/test" 5 | "testing" 6 | ) 7 | 8 | func getTestDriver(t *testing.T) *s3StorageDriver { 9 | t.Helper() 10 | driver := GetDriver() 11 | result, ok := driver.(*s3StorageDriver) 12 | test.IsEqualBool(t, ok, true) 13 | return result 14 | } 15 | 16 | func TestGetDriver(t *testing.T) { 17 | getTestDriver(t) 18 | } 19 | 20 | func TestS3StorageDriver_Init(t *testing.T) { 21 | driver := getTestDriver(t) 22 | defer test.ExpectPanic(t) 23 | driver.Init("test") 24 | defer test.ExpectPanic(t) 25 | driver.Init(Config{Bucket: ""}) 26 | } 27 | func TestS3StorageDriver_Init2(t *testing.T) { 28 | driver := getTestDriver(t) 29 | defer test.ExpectPanic(t) 30 | driver.Init(Config{Bucket: ""}) 31 | } 32 | 33 | func TestS3StorageDriver_Init3(t *testing.T) { 34 | driver := getTestDriver(t) 35 | ok := driver.Init(Config{Bucket: "test"}) 36 | test.IsEqualBool(t, ok, false) 37 | test.IsEqualString(t, driver.Bucket, "test") 38 | test.IsEqualBool(t, driver.IsAvailable(), false) // TODO 39 | } 40 | 41 | func TestS3StorageDriver_GetSystemName(t *testing.T) { 42 | driver := getTestDriver(t) 43 | test.IsEqualString(t, driver.GetSystemName(), "awss3") 44 | } 45 | 46 | func TestAwsFile_GetName(t *testing.T) { 47 | driver := getTestDriver(t) 48 | driver.Init(Config{Bucket: "test"}) 49 | file := driver.GetFile("testfile") 50 | test.IsEqualString(t, file.GetName(), "testfile") 51 | } 52 | -------------------------------------------------------------------------------- /internal/storage/filesystem/s3filesystem/aws/Aws_slim.go: -------------------------------------------------------------------------------- 1 | //go:build noaws 2 | 3 | package aws 4 | 5 | import ( 6 | "errors" 7 | "github.com/forceu/gokapi/internal/models" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | const errorString = "AWS not supported in this build" 13 | 14 | // IsIncludedInBuild is true if Gokapi has been compiled with AWS support or the API is being mocked 15 | const IsIncludedInBuild = false 16 | 17 | // IsMockApi is true if the API is being mocked and therefore can only be used for testing purposes 18 | const IsMockApi = false 19 | 20 | // Init reads the credentials for AWS 21 | func Init(config models.AwsConfig) bool { 22 | return false 23 | } 24 | 25 | // IsAvailable returns true if valid credentials have been passed 26 | func IsAvailable() bool { 27 | return false 28 | } 29 | 30 | // IsValidLogin checks if a valid login was provided 31 | func IsValidLogin(config models.AwsConfig) (bool, error) { 32 | return false, errors.New(errorString) 33 | } 34 | 35 | // AddBucketName adds the bucket name to the file to be stored 36 | func AddBucketName(file *models.File) { 37 | return 38 | } 39 | 40 | // Upload uploads a file to AWS 41 | func Upload(input io.Reader, file models.File) (string, error) { 42 | return "", errors.New(errorString) 43 | } 44 | 45 | // Download downloads a file from AWS 46 | func Download(writer io.WriterAt, file models.File) (int64, error) { 47 | return 0, errors.New(errorString) 48 | } 49 | 50 | // LogOut resets the credentials 51 | func LogOut() { 52 | } 53 | 54 | // RedirectToDownload creates a presigned link that is valid for 15 seconds and redirects the 55 | // client to this url 56 | func RedirectToDownload(w http.ResponseWriter, r *http.Request, file models.File, forceDownload bool) error { 57 | return errors.New(errorString) 58 | } 59 | 60 | // ServeFile either redirects the user to a pre-signed download url (default) or downloads the file and serves it as a proxy (depending 61 | // on configuration). Returns true if blocking operation (in order to set download status) or false if non-blocking. 62 | func ServeFile(w http.ResponseWriter, r *http.Request, file models.File, forceDownload bool) (bool, error) { 63 | return false, errors.New(errorString) 64 | } 65 | 66 | // FileExists returns true if the object is stored in S3 67 | func FileExists(file models.File) (bool, int64, error) { 68 | return true, 0, errors.New(errorString) 69 | } 70 | 71 | // DeleteObject deletes a file from S3 72 | func DeleteObject(file models.File) (bool, error) { 73 | return false, errors.New(errorString) 74 | } 75 | 76 | // IsCorsCorrectlySet returns true if CORS rules allow download from Gokapi 77 | func IsCorsCorrectlySet(bucket, gokapiUrl string) (bool, error) { 78 | return false, errors.New(errorString) 79 | } 80 | 81 | // GetDefaultBucketName returns the default bucketname where new files are stored 82 | func GetDefaultBucketName() string { 83 | return "" 84 | } 85 | -------------------------------------------------------------------------------- /internal/storage/processingstatus/ProcessingStatus.go: -------------------------------------------------------------------------------- 1 | package processingstatus 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "github.com/forceu/gokapi/internal/storage/processingstatus/pstatusdb" 6 | "github.com/forceu/gokapi/internal/webserver/sse" 7 | ) 8 | 9 | // StatusHashingOrEncrypting indicates that the file has been completely uploaded, but is now processed by Gokapi 10 | const StatusHashingOrEncrypting = 0 11 | 12 | // StatusUploading indicates that the file has been processed, but is now moved to the data filesystem 13 | const StatusUploading = 1 14 | 15 | // StatusFinished indicates that the file has been fully processed and uploaded 16 | const StatusFinished = 2 17 | 18 | // StatusError indicates that there was an error during the upload 19 | const StatusError = 3 20 | 21 | // Set sets the status for an id 22 | func Set(id string, status int, file models.File, err error) { 23 | newStatus := models.UploadStatus{ 24 | ChunkId: id, 25 | CurrentStatus: status, 26 | FileId: file.Id, 27 | } 28 | if err != nil { 29 | newStatus.ErrorMessage = err.Error() 30 | } 31 | pstatusdb.Set(newStatus) 32 | go sse.PublishNewStatus(newStatus) 33 | } 34 | -------------------------------------------------------------------------------- /internal/storage/processingstatus/ProcessingStatus_test.go: -------------------------------------------------------------------------------- 1 | package processingstatus 2 | 3 | import ( 4 | "errors" 5 | "github.com/forceu/gokapi/internal/models" 6 | "github.com/forceu/gokapi/internal/storage/processingstatus/pstatusdb" 7 | "github.com/forceu/gokapi/internal/test" 8 | "testing" 9 | ) 10 | 11 | func TestSetStatus(t *testing.T) { 12 | const id = "testchunk" 13 | status, ok := getStatus(id) 14 | test.IsEqualBool(t, ok, false) 15 | test.IsEmpty(t, status.ChunkId) 16 | Set(id, 2, models.File{Id: "testfile"}, nil) 17 | status, ok = getStatus(id) 18 | test.IsEqualBool(t, ok, true) 19 | test.IsEqualString(t, status.ChunkId, id) 20 | test.IsEqualString(t, status.FileId, "testfile") 21 | test.IsEqualInt(t, status.CurrentStatus, 2) 22 | Set(id, 1, models.File{}, nil) 23 | status, ok = getStatus(id) 24 | test.IsEqualBool(t, ok, true) 25 | test.IsEqualString(t, status.ChunkId, id) 26 | test.IsEqualInt(t, status.CurrentStatus, 2) 27 | Set(id, 3, models.File{Id: "testfile"}, errors.New("test")) 28 | status, ok = getStatus(id) 29 | test.IsEqualBool(t, ok, true) 30 | test.IsEqualString(t, status.ChunkId, id) 31 | test.IsEqualInt(t, status.CurrentStatus, 3) 32 | test.IsEqualString(t, status.FileId, "testfile") 33 | test.IsEqualString(t, status.ErrorMessage, "test") 34 | } 35 | 36 | func getStatus(id string) (models.UploadStatus, bool) { 37 | for _, status := range pstatusdb.GetAll() { 38 | if status.ChunkId == id { 39 | return status, true 40 | } 41 | } 42 | return models.UploadStatus{}, false 43 | } 44 | -------------------------------------------------------------------------------- /internal/storage/processingstatus/pstatusdb/PStatusDb.go: -------------------------------------------------------------------------------- 1 | package pstatusdb 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | var statusMap = make(map[string]models.UploadStatus) 10 | var statusMutex sync.RWMutex 11 | var isGbStarted = false 12 | 13 | // GetAll returns all UploadStatus that were created in the last 24 hours 14 | func GetAll() []models.UploadStatus { 15 | statusMutex.RLock() 16 | result := make([]models.UploadStatus, len(statusMap)) 17 | i := 0 18 | for _, status := range statusMap { 19 | result[i] = status 20 | i++ 21 | } 22 | statusMutex.RUnlock() 23 | return result 24 | } 25 | 26 | // Set saves the upload status for 24 hours 27 | func Set(status models.UploadStatus) { 28 | statusMutex.Lock() 29 | oldStatus, ok := statusMap[status.ChunkId] 30 | if ok && oldStatus.CurrentStatus > status.CurrentStatus { 31 | statusMutex.Unlock() 32 | return 33 | } 34 | status.Creation = time.Now().Unix() 35 | statusMap[status.ChunkId] = status 36 | statusMutex.Unlock() 37 | if !isGbStarted { 38 | isGbStarted = true 39 | go doGarbageCollection(true) 40 | } 41 | } 42 | 43 | func deleteAllExpiredStatus() { 44 | allStatus := GetAll() 45 | cutOff := time.Now().Add(-24 * time.Hour).Unix() 46 | statusMutex.Lock() 47 | newStatusMap := make(map[string]models.UploadStatus) 48 | for _, status := range allStatus { 49 | if status.Creation > cutOff { 50 | newStatusMap[status.ChunkId] = status 51 | } 52 | } 53 | statusMap = newStatusMap 54 | statusMutex.Unlock() 55 | } 56 | 57 | func doGarbageCollection(runPeriodically bool) { 58 | deleteAllExpiredStatus() 59 | if !runPeriodically { 60 | return 61 | } 62 | select { 63 | case <-time.After(1 * time.Hour): 64 | doGarbageCollection(true) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/storage/processingstatus/pstatusdb/PStatusDb_test.go: -------------------------------------------------------------------------------- 1 | package pstatusdb 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "github.com/forceu/gokapi/internal/test" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestSetStatus(t *testing.T) { 11 | isGbStarted = true 12 | const id = "testchunk" 13 | status, ok := getStatus(id) 14 | test.IsEqualBool(t, ok, false) 15 | test.IsEmpty(t, status.ChunkId) 16 | Set(models.UploadStatus{ 17 | ChunkId: id, 18 | CurrentStatus: 2, 19 | FileId: "testfile", 20 | }) 21 | status, ok = getStatus(id) 22 | test.IsEqualBool(t, ok, true) 23 | test.IsEqualString(t, status.ChunkId, id) 24 | test.IsEqualString(t, status.FileId, "testfile") 25 | test.IsEqualInt(t, status.CurrentStatus, 2) 26 | Set(models.UploadStatus{ 27 | ChunkId: id, 28 | CurrentStatus: 1, 29 | }) 30 | status, ok = getStatus(id) 31 | test.IsEqualBool(t, ok, true) 32 | test.IsEqualString(t, status.ChunkId, id) 33 | test.IsEqualInt(t, status.CurrentStatus, 2) 34 | Set(models.UploadStatus{ 35 | ChunkId: id, 36 | CurrentStatus: 3, 37 | FileId: "testfile", 38 | ErrorMessage: "test", 39 | }) 40 | status, ok = getStatus(id) 41 | test.IsEqualBool(t, ok, true) 42 | test.IsEqualString(t, status.ChunkId, id) 43 | test.IsEqualInt(t, status.CurrentStatus, 3) 44 | test.IsEqualString(t, status.FileId, "testfile") 45 | test.IsEqualString(t, status.ErrorMessage, "test") 46 | } 47 | 48 | func TestGarbageCollection(t *testing.T) { 49 | Set(models.UploadStatus{ 50 | ChunkId: "toBeGarbaged", 51 | CurrentStatus: 2, 52 | }) 53 | test.IsEqualInt(t, len(GetAll()), 2) 54 | doGarbageCollection(false) 55 | test.IsEqualInt(t, len(GetAll()), 2) 56 | status, ok := statusMap["toBeGarbaged"] 57 | test.IsEqualBool(t, ok, true) 58 | status.Creation = time.Now().Add(-30 * time.Hour).Unix() 59 | statusMap["toBeGarbaged"] = status 60 | test.IsEqualInt(t, len(GetAll()), 2) 61 | doGarbageCollection(false) 62 | test.IsEqualInt(t, len(GetAll()), 1) 63 | } 64 | 65 | func getStatus(id string) (models.UploadStatus, bool) { 66 | for _, status := range GetAll() { 67 | if status.ChunkId == id { 68 | return status, true 69 | } 70 | } 71 | return models.UploadStatus{}, false 72 | } 73 | -------------------------------------------------------------------------------- /internal/test/testconfiguration/TestConfiguration_test.go: -------------------------------------------------------------------------------- 1 | //go:build test 2 | 3 | package testconfiguration 4 | 5 | import ( 6 | "github.com/forceu/gokapi/internal/configuration/database" 7 | "github.com/forceu/gokapi/internal/configuration/database/dbabstraction" 8 | "github.com/forceu/gokapi/internal/helper" 9 | "github.com/forceu/gokapi/internal/models" 10 | "github.com/forceu/gokapi/internal/storage/filesystem/s3filesystem/aws" 11 | "github.com/forceu/gokapi/internal/test" 12 | "os" 13 | "testing" 14 | ) 15 | 16 | func TestCreate(t *testing.T) { 17 | Create(true) 18 | test.IsEqualBool(t, helper.FolderExists(dataDir), true) 19 | test.FileExists(t, configFile) 20 | test.FileExists(t, "test/data/a8fdc205a9f19cc1c7507a60c4f01b13d11d7fd0") 21 | } 22 | 23 | func TestDelete(t *testing.T) { 24 | Delete() 25 | test.IsEqualBool(t, helper.FolderExists(dataDir), false) 26 | } 27 | 28 | func TestWriteEncryptedFile(t *testing.T) { 29 | database.Connect(models.DbConnection{ 30 | HostUrl: "./test/gokapi.sqlite", 31 | Type: dbabstraction.TypeSqlite, 32 | }) 33 | fileId := WriteEncryptedFile() 34 | file, ok := database.GetMetaDataById(fileId) 35 | test.IsEqualBool(t, ok, true) 36 | test.IsEqualString(t, file.Id, fileId) 37 | database.Close() 38 | } 39 | 40 | func TestEnableS3(t *testing.T) { 41 | EnableS3() 42 | if aws.IsMockApi { 43 | test.IsEqualString(t, os.Getenv("GOKAPI_AWS_REGION"), "mock-region-1") 44 | } 45 | } 46 | 47 | func TestDisableS3S3(t *testing.T) { 48 | DisableS3() 49 | if aws.IsMockApi { 50 | test.IsEqualString(t, os.Getenv("AWS_REGION"), "") 51 | } 52 | } 53 | 54 | func TestUseMockS3Server(t *testing.T) { 55 | previousValue := os.Getenv("REAL_AWS_CREDENTIALS") 56 | os.Setenv("REAL_AWS_CREDENTIALS", "false") 57 | test.IsEqualBool(t, UseMockS3Server(), true) 58 | os.Setenv("REAL_AWS_CREDENTIALS", "true") 59 | test.IsEqualBool(t, UseMockS3Server(), false) 60 | os.Setenv("REAL_AWS_CREDENTIALS", previousValue) 61 | } 62 | 63 | func TestWriteSslCertificates(t *testing.T) { 64 | test.FileDoesNotExist(t, "test/ssl.key") 65 | WriteSslCertificates(true) 66 | test.FileExists(t, "test/ssl.key") 67 | os.Remove("test/ssl.key") 68 | test.FileDoesNotExist(t, "test/ssl.key") 69 | WriteSslCertificates(false) 70 | test.FileExists(t, "test/ssl.key") 71 | Delete() 72 | } 73 | 74 | func TestWriteCloudConfigFile(t *testing.T) { 75 | test.FileDoesNotExist(t, "test/cloudconfig.yml") 76 | WriteCloudConfigFile(true) 77 | test.FileExists(t, "test/cloudconfig.yml") 78 | os.Remove("test/cloudconfig.yml") 79 | test.FileDoesNotExist(t, "test/cloudconfig.yml") 80 | WriteCloudConfigFile(false) 81 | test.FileExists(t, "test/cloudconfig.yml") 82 | Delete() 83 | } 84 | 85 | func TestStartS3TestServer(t *testing.T) { 86 | server := StartS3TestServer() 87 | test.IsNotNil(t, server) 88 | } 89 | -------------------------------------------------------------------------------- /internal/webserver/CustomStaticContent.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/NYTimes/gziphandler" 7 | "github.com/forceu/gokapi/internal/helper" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | const pathCustomFolder = "custom/" 15 | const pathCustomCss = pathCustomFolder + "custom.css" 16 | const pathCustomPublicJs = pathCustomFolder + "public.js" 17 | const pathCustomAdminJs = pathCustomFolder + "admin.js" 18 | const pathCustomVersioning = pathCustomFolder + "version.txt" 19 | 20 | type customStatic struct { 21 | Version string 22 | CustomFolderExists bool 23 | UseCustomCss bool 24 | UseCustomPublicJs bool 25 | UseCustomAdminJs bool 26 | } 27 | 28 | func loadCustomCssJsInfo() { 29 | customStaticInfo = customStatic{} 30 | folderExists := helper.FolderExists(pathCustomFolder) 31 | customStaticInfo.CustomFolderExists = folderExists 32 | if !folderExists { 33 | return 34 | } 35 | customStaticInfo.Version = strconv.Itoa(readCustomStaticVersion()) 36 | customStaticInfo.UseCustomCss = helper.FileExists(pathCustomCss) 37 | customStaticInfo.UseCustomPublicJs = helper.FileExists(pathCustomPublicJs) 38 | customStaticInfo.UseCustomAdminJs = helper.FileExists(pathCustomAdminJs) 39 | } 40 | 41 | func addMuxForCustomContent(mux *http.ServeMux) { 42 | if !customStaticInfo.CustomFolderExists { 43 | return 44 | } 45 | fmt.Println("Serving custom static content") 46 | // Serve the user-created "custom" folder to /custom 47 | mux.Handle("/custom/", http.StripPrefix("/custom/", http.FileServer(http.Dir(pathCustomFolder)))) 48 | // Allow versioning to prevent caching old version 49 | if customStaticInfo.UseCustomCss { 50 | mux.Handle("/custom/custom.v"+customStaticInfo.Version+".css", gziphandler.GzipHandler(http.HandlerFunc(serveCustomCss))) 51 | } 52 | if customStaticInfo.UseCustomPublicJs { 53 | mux.Handle("/custom/public.v"+customStaticInfo.Version+".js", gziphandler.GzipHandler(http.HandlerFunc(serveCustomPublicJs))) 54 | } 55 | if customStaticInfo.UseCustomAdminJs { 56 | mux.Handle("/custom/admin.v"+customStaticInfo.Version+".js", gziphandler.GzipHandler(http.HandlerFunc(serveCustomAdminJs))) 57 | } 58 | } 59 | 60 | func serveCustomCss(w http.ResponseWriter, r *http.Request) { 61 | serveCustomFile(pathCustomCss, w, r) 62 | } 63 | func serveCustomPublicJs(w http.ResponseWriter, r *http.Request) { 64 | serveCustomFile(pathCustomPublicJs, w, r) 65 | } 66 | func serveCustomAdminJs(w http.ResponseWriter, r *http.Request) { 67 | serveCustomFile(pathCustomAdminJs, w, r) 68 | } 69 | 70 | func serveCustomFile(filePath string, w http.ResponseWriter, r *http.Request) { 71 | w.Header().Add("Cache-Control", "public, max-age=100800") // 2 days 72 | http.ServeFile(w, r, filePath) 73 | } 74 | 75 | func readCustomStaticVersion() int { 76 | if !helper.FileExists(pathCustomVersioning) { 77 | return 0 78 | } 79 | file, err := os.Open(pathCustomVersioning) 80 | if err != nil { 81 | fmt.Println(err) 82 | return 0 83 | } 84 | defer file.Close() 85 | sc := bufio.NewScanner(file) 86 | if !sc.Scan() { 87 | return 0 88 | } 89 | line := strings.TrimSpace(sc.Text()) 90 | version, err := strconv.Atoi(line) 91 | if err != nil { 92 | fmt.Println("Content of " + pathCustomVersioning + " must be numerical") 93 | } 94 | return version 95 | } 96 | -------------------------------------------------------------------------------- /internal/webserver/authentication/oauth/Oauth_test.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/test" 5 | "github.com/forceu/gokapi/internal/webserver/authentication" 6 | "testing" 7 | ) 8 | 9 | func TestSetCallbackCookie(t *testing.T) { 10 | w, _ := test.GetRecorder("GET", "/", nil, nil, nil) 11 | setCallbackCookie(w, "test") 12 | cookies := w.Result().Cookies() 13 | test.IsEqualInt(t, len(cookies), 1) 14 | test.IsEqualString(t, cookies[0].Name, authentication.CookieOauth) 15 | value := cookies[0].Value 16 | test.IsEqualString(t, value, "test") 17 | } 18 | -------------------------------------------------------------------------------- /internal/webserver/authentication/sessionmanager/SessionManager.go: -------------------------------------------------------------------------------- 1 | package sessionmanager 2 | 3 | /** 4 | Manages the sessions for the admin user or to access password-protected files 5 | */ 6 | 7 | import ( 8 | "github.com/forceu/gokapi/internal/configuration/database" 9 | "github.com/forceu/gokapi/internal/helper" 10 | "github.com/forceu/gokapi/internal/models" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | // If no login occurred during this time, the admin session will be deleted. Default 30 days 16 | const cookieLifeAdmin = 30 * 24 * time.Hour 17 | const lengthSessionId = 60 18 | 19 | // IsValidSession checks if the user is submitting a valid session token 20 | // If valid session is found, useSession will be called 21 | // Returns true if authenticated, otherwise false 22 | func IsValidSession(w http.ResponseWriter, r *http.Request, isOauth bool, OAuthRecheckInterval int) (models.User, bool) { 23 | cookie, err := r.Cookie("session_token") 24 | if err == nil { 25 | sessionString := cookie.Value 26 | if sessionString != "" { 27 | session, ok := database.GetSession(sessionString) 28 | if ok { 29 | user, userExists := database.GetUser(session.UserId) 30 | if !userExists { 31 | return user, false 32 | } 33 | return user, useSession(w, sessionString, session, isOauth, OAuthRecheckInterval) 34 | } 35 | } 36 | } 37 | return models.User{}, false 38 | } 39 | 40 | // useSession checks if a session is still valid. It Changes the session string 41 | // if it has // been used for more than an hour to limit session hijacking 42 | // Returns true if session is still valid 43 | // Returns false if session is invalid (and deletes it) 44 | func useSession(w http.ResponseWriter, id string, session models.Session, isOauth bool, OAuthRecheckInterval int) bool { 45 | if session.ValidUntil < time.Now().Unix() { 46 | database.DeleteSession(id) 47 | return false 48 | } 49 | if session.RenewAt < time.Now().Unix() { 50 | CreateSession(w, isOauth, OAuthRecheckInterval, session.UserId) 51 | database.DeleteSession(id) 52 | } 53 | go database.UpdateUserLastOnline(session.UserId) 54 | return true 55 | } 56 | 57 | // CreateSession creates a new session - called after login with correct username / password 58 | // If sessions parameter is nil, it will be loaded from config 59 | func CreateSession(w http.ResponseWriter, isOauth bool, OAuthRecheckInterval int, userId int) { 60 | timeExpiry := time.Now().Add(cookieLifeAdmin) 61 | if isOauth { 62 | timeExpiry = time.Now().Add(time.Duration(OAuthRecheckInterval) * time.Hour) 63 | } 64 | 65 | sessionString := helper.GenerateRandomString(lengthSessionId) 66 | database.SaveSession(sessionString, models.Session{ 67 | RenewAt: time.Now().Add(12 * time.Hour).Unix(), 68 | ValidUntil: timeExpiry.Unix(), 69 | UserId: userId, 70 | }) 71 | writeSessionCookie(w, sessionString, timeExpiry) 72 | } 73 | 74 | // LogoutSession logs out user and deletes session 75 | func LogoutSession(w http.ResponseWriter, r *http.Request) { 76 | cookie, err := r.Cookie("session_token") 77 | if err == nil { 78 | database.DeleteSession(cookie.Value) 79 | } 80 | writeSessionCookie(w, "", time.Now()) 81 | } 82 | 83 | // Writes session cookie to browser 84 | func writeSessionCookie(w http.ResponseWriter, sessionString string, expiry time.Time) { 85 | c := &http.Cookie{ 86 | Name: "session_token", 87 | Value: sessionString, 88 | Expires: expiry, 89 | } 90 | http.SetCookie(w, c) 91 | } 92 | -------------------------------------------------------------------------------- /internal/webserver/authentication/sessionmanager/SessionManager_test.go: -------------------------------------------------------------------------------- 1 | package sessionmanager 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/configuration" 5 | "github.com/forceu/gokapi/internal/configuration/database" 6 | "github.com/forceu/gokapi/internal/models" 7 | "github.com/forceu/gokapi/internal/test" 8 | "github.com/forceu/gokapi/internal/test/testconfiguration" 9 | "net/http" 10 | "net/http/httptest" 11 | "os" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | var newSession string 17 | 18 | func TestMain(m *testing.M) { 19 | testconfiguration.Create(false) 20 | configuration.Load() 21 | configuration.ConnectDatabase() 22 | exitVal := m.Run() 23 | testconfiguration.Delete() 24 | os.Exit(exitVal) 25 | } 26 | 27 | func getRecorder(cookies []test.Cookie) (*httptest.ResponseRecorder, *http.Request, bool, int) { 28 | w, r := test.GetRecorder("GET", "/", cookies, nil, nil) 29 | return w, r, false, 1 30 | } 31 | 32 | func TestIsValidSession(t *testing.T) { 33 | user, ok := IsValidSession(getRecorder(nil)) 34 | test.IsEqualBool(t, ok, false) 35 | user, ok = IsValidSession(getRecorder([]test.Cookie{{ 36 | Name: "session_token", 37 | Value: "invalid"}, 38 | })) 39 | test.IsEqualBool(t, ok, false) 40 | user, ok = IsValidSession(getRecorder([]test.Cookie{{ 41 | Name: "session_token", 42 | Value: ""}, 43 | })) 44 | test.IsEqualBool(t, ok, false) 45 | user, ok = IsValidSession(getRecorder([]test.Cookie{{ 46 | Name: "session_token", 47 | Value: "expiredsession"}, 48 | })) 49 | test.IsEqualBool(t, ok, false) 50 | user, ok = IsValidSession(getRecorder([]test.Cookie{{ 51 | Name: "session_token", 52 | Value: "validsession"}, 53 | })) 54 | test.IsEqualBool(t, ok, true) 55 | _, ok = IsValidSession(getRecorder([]test.Cookie{{ 56 | Name: "session_token", 57 | Value: "validSessionInvalidUser"}, 58 | })) 59 | test.IsEqualBool(t, ok, false) 60 | test.IsEqualInt(t, user.Id, 7) 61 | w, r, _, _ := getRecorder([]test.Cookie{{ 62 | Name: "session_token", 63 | Value: "needsRenewal"}, 64 | }) 65 | user, ok = IsValidSession(w, r, false, 1) 66 | cookies := w.Result().Cookies() 67 | test.IsEqualInt(t, len(cookies), 1) 68 | test.IsEqualString(t, cookies[0].Name, "session_token") 69 | session := cookies[0].Value 70 | test.IsEqualInt(t, len(session), 60) 71 | test.IsNotEqualString(t, session, "needsRenewal") 72 | } 73 | 74 | func TestCreateSession(t *testing.T) { 75 | w, _, _, _ := getRecorder(nil) 76 | CreateSession(w, false, 1, 5) 77 | cookies := w.Result().Cookies() 78 | test.IsEqualInt(t, len(cookies), 1) 79 | test.IsEqualString(t, cookies[0].Name, "session_token") 80 | newSession = cookies[0].Value 81 | test.IsEqualInt(t, len(newSession), 60) 82 | 83 | user, ok := IsValidSession(getRecorder([]test.Cookie{{ 84 | Name: "session_token", 85 | Value: newSession}, 86 | })) 87 | test.IsEqualBool(t, ok, true) 88 | test.IsEqualInt(t, user.Id, 5) 89 | 90 | w, _, _, _ = getRecorder(nil) 91 | CreateSession(w, true, 20, 50) 92 | cookies = w.Result().Cookies() 93 | newOauthSession := cookies[0].Value 94 | 95 | var session models.Session 96 | session, ok = database.GetSession(newOauthSession) 97 | test.IsEqualBool(t, ok, true) 98 | isEqual := time.Now().Add(20*time.Hour).Unix()-session.ValidUntil < 10 && 99 | time.Now().Add(20*time.Hour).Unix()-session.ValidUntil > -1 100 | test.IsEqualBool(t, isEqual, true) 101 | } 102 | 103 | func TestLogoutSession(t *testing.T) { 104 | user, ok := IsValidSession(getRecorder([]test.Cookie{{ 105 | Name: "session_token", 106 | Value: newSession}, 107 | })) 108 | test.IsEqualBool(t, ok, true) 109 | test.IsEqualInt(t, user.Id, 5) 110 | w, r, _, _ := getRecorder([]test.Cookie{{ 111 | Name: "session_token", 112 | Value: newSession}, 113 | }) 114 | LogoutSession(w, r) 115 | _, ok = IsValidSession(getRecorder([]test.Cookie{{ 116 | Name: "session_token", 117 | Value: newSession}, 118 | })) 119 | test.IsEqualBool(t, ok, false) 120 | } 121 | -------------------------------------------------------------------------------- /internal/webserver/downloadstatus/DownloadStatus.go: -------------------------------------------------------------------------------- 1 | package downloadstatus 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/helper" 5 | "github.com/forceu/gokapi/internal/models" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | var statusMap = make(map[string]models.DownloadStatus) 11 | var statusMutex sync.RWMutex 12 | 13 | // SetDownload creates a new DownloadStatus struct and returns its Id 14 | func SetDownload(file models.File) string { 15 | newStatus := newDownloadStatus(file) 16 | statusMutex.Lock() 17 | statusMap[newStatus.Id] = newStatus 18 | statusMutex.Unlock() 19 | return newStatus.Id 20 | } 21 | 22 | // SetComplete removes the download object 23 | func SetComplete(downloadStatusId string) { 24 | statusMutex.Lock() 25 | delete(statusMap, downloadStatusId) 26 | statusMutex.Unlock() 27 | } 28 | 29 | // Clean removes all expires status objects 30 | func Clean() { 31 | now := time.Now().Unix() 32 | for _, item := range statusMap { 33 | if item.ExpireAt < now { 34 | SetComplete(item.Id) 35 | } 36 | } 37 | } 38 | 39 | // newDownloadStatus initialises a new DownloadStatus item 40 | func newDownloadStatus(file models.File) models.DownloadStatus { 41 | s := models.DownloadStatus{ 42 | Id: helper.GenerateRandomString(30), 43 | FileId: file.Id, 44 | ExpireAt: time.Now().Add(24 * time.Hour).Unix(), 45 | } 46 | return s 47 | } 48 | 49 | // IsCurrentlyDownloading returns true if file is currently being downloaded 50 | func IsCurrentlyDownloading(file models.File) bool { 51 | isDownloading := false 52 | statusMutex.RLock() 53 | for _, status := range statusMap { 54 | if status.FileId == file.Id { 55 | if status.ExpireAt > time.Now().Unix() { 56 | isDownloading = true 57 | break 58 | } 59 | } 60 | } 61 | statusMutex.RUnlock() 62 | return isDownloading 63 | } 64 | 65 | // SetAllComplete removes all download status associated with this file 66 | func SetAllComplete(fileId string) { 67 | statusMutex.Lock() 68 | for _, status := range statusMap { 69 | if status.FileId == fileId { 70 | delete(statusMap, status.Id) 71 | } 72 | } 73 | statusMutex.Unlock() 74 | } 75 | 76 | // DeleteAll removes all download status 77 | func DeleteAll() { 78 | statusMutex.Lock() 79 | statusMap = make(map[string]models.DownloadStatus) 80 | statusMutex.Unlock() 81 | } 82 | -------------------------------------------------------------------------------- /internal/webserver/downloadstatus/DownloadStatus_test.go: -------------------------------------------------------------------------------- 1 | package downloadstatus 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "github.com/forceu/gokapi/internal/test" 6 | "os" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | var testFile models.File 12 | var statusId string 13 | 14 | func TestMain(m *testing.M) { 15 | testFile = models.File{ 16 | Id: "test", 17 | Name: "testName", 18 | Size: "3 B", 19 | SHA1: "123456", 20 | ExpireAt: 500, 21 | ExpireAtString: "expire", 22 | DownloadsRemaining: 1, 23 | } 24 | exitVal := m.Run() 25 | os.Exit(exitVal) 26 | } 27 | 28 | func TestNewDownloadStatus(t *testing.T) { 29 | newStatus := newDownloadStatus(models.File{Id: "testId"}) 30 | test.IsNotEmpty(t, newStatus.Id) 31 | test.IsEqualString(t, newStatus.FileId, "testId") 32 | test.IsEqualBool(t, newStatus.ExpireAt > time.Now().Unix(), true) 33 | } 34 | 35 | func TestSetDownload(t *testing.T) { 36 | statusId = SetDownload(testFile) 37 | newStatus := statusMap[statusId] 38 | test.IsNotEmpty(t, newStatus.Id) 39 | test.IsEqualString(t, newStatus.Id, statusId) 40 | test.IsEqualString(t, newStatus.FileId, testFile.Id) 41 | test.IsEqualBool(t, newStatus.ExpireAt > time.Now().Unix(), true) 42 | } 43 | 44 | func TestSetComplete(t *testing.T) { 45 | newStatus := statusMap[statusId] 46 | test.IsNotEmpty(t, newStatus.Id) 47 | SetComplete(statusId) 48 | newStatus = statusMap[statusId] 49 | test.IsEmpty(t, newStatus.Id) 50 | } 51 | 52 | func TestIsCurrentlyDownloading(t *testing.T) { 53 | test.IsEqualBool(t, IsCurrentlyDownloading(testFile), false) 54 | statusIdFirst := SetDownload(testFile) 55 | firstStatus := statusMap[statusIdFirst] 56 | test.IsEqualBool(t, IsCurrentlyDownloading(testFile), true) 57 | statusIdSecond := SetDownload(testFile) 58 | secondStatus := statusMap[statusIdSecond] 59 | test.IsEqualBool(t, IsCurrentlyDownloading(testFile), true) 60 | 61 | firstStatus.ExpireAt = 0 62 | statusMap[firstStatus.Id] = firstStatus 63 | test.IsEqualBool(t, IsCurrentlyDownloading(testFile), true) 64 | secondStatus.ExpireAt = 0 65 | statusMap[secondStatus.Id] = secondStatus 66 | test.IsEqualBool(t, IsCurrentlyDownloading(testFile), false) 67 | 68 | statusId = SetDownload(testFile) 69 | test.IsEqualBool(t, IsCurrentlyDownloading(models.File{Id: "notDownloading"}), false) 70 | } 71 | func TestClean(t *testing.T) { 72 | test.IsEqualInt(t, len(statusMap), 3) 73 | Clean() 74 | test.IsEqualInt(t, len(statusMap), 1) 75 | newStatus := statusMap[statusId] 76 | newStatus.ExpireAt = 1 77 | statusMap[statusId] = newStatus 78 | test.IsEqualInt(t, len(statusMap), 1) 79 | Clean() 80 | test.IsEqualInt(t, len(statusMap), 0) 81 | } 82 | 83 | func TestDeleteAll(t *testing.T) { 84 | statusId = SetDownload(testFile) 85 | test.IsEqualBool(t, len(statusMap) != 0, true) 86 | DeleteAll() 87 | test.IsEqualInt(t, len(statusMap), 0) 88 | } 89 | 90 | func TestSetAllComplete(t *testing.T) { 91 | test.IsEqualInt(t, len(statusMap), 0) 92 | SetDownload(models.File{Id: "stillDownloading"}) 93 | SetDownload(models.File{Id: "stillDownloading"}) 94 | status1 := SetDownload(models.File{Id: "stillDownloading"}) 95 | SetDownload(models.File{Id: "fileToBeDeleted"}) 96 | SetDownload(models.File{Id: "fileToBeDeleted"}) 97 | SetDownload(models.File{Id: "fileToBeDeleted"}) 98 | status2 := SetDownload(models.File{Id: "fileToBeDeleted"}) 99 | test.IsEqualInt(t, len(statusMap), 7) 100 | SetAllComplete("fileToBeDeleted") 101 | test.IsEqualInt(t, len(statusMap), 3) 102 | _, ok := statusMap[status1] 103 | test.IsEqualBool(t, ok, true) 104 | _, ok = statusMap[status2] 105 | test.IsEqualBool(t, ok, false) 106 | } 107 | -------------------------------------------------------------------------------- /internal/webserver/headers/Headers.go: -------------------------------------------------------------------------------- 1 | package headers 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | // Write sets headers to either display the file inline or to force download, the content type 10 | // and if the file is encrypted, the creation timestamp to now 11 | func Write(file models.File, w http.ResponseWriter, forceDownload bool) { 12 | if forceDownload { 13 | w.Header().Set("Content-Disposition", "attachment; filename=\""+file.Name+"\"") 14 | } else { 15 | w.Header().Set("Content-Disposition", "inline; filename=\""+file.Name+"\"") 16 | } 17 | w.Header().Set("Content-Type", file.ContentType) 18 | 19 | if file.Encryption.IsEncrypted { 20 | w.Header().Set("Accept-Ranges", "bytes") 21 | w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/webserver/headers/Headers_test.go: -------------------------------------------------------------------------------- 1 | package headers 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/models" 5 | "github.com/forceu/gokapi/internal/test" 6 | "testing" 7 | ) 8 | 9 | func TestWriteDownloadHeaders(t *testing.T) { 10 | file := models.File{Name: "testname", ContentType: "testtype"} 11 | w, _ := test.GetRecorder("GET", "/test", nil, nil, nil) 12 | Write(file, w, true) 13 | test.IsEqualString(t, w.Result().Header.Get("Content-Disposition"), "attachment; filename=\"testname\"") 14 | w, _ = test.GetRecorder("GET", "/test", nil, nil, nil) 15 | Write(file, w, false) 16 | test.IsEqualString(t, w.Result().Header.Get("Content-Disposition"), "inline; filename=\"testname\"") 17 | test.IsEqualString(t, w.Result().Header.Get("Content-Type"), "testtype") 18 | file.Encryption.IsEncrypted = true 19 | w, _ = test.GetRecorder("GET", "/test", nil, nil, nil) 20 | Write(file, w, false) 21 | test.IsEqualString(t, w.Result().Header.Get("Accept-Ranges"), "bytes") 22 | } 23 | -------------------------------------------------------------------------------- /internal/webserver/ssl/Ssl_test.go: -------------------------------------------------------------------------------- 1 | package ssl 2 | 3 | import ( 4 | "github.com/forceu/gokapi/internal/test" 5 | "github.com/forceu/gokapi/internal/test/testconfiguration" 6 | "os" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestMain(m *testing.M) { 12 | testconfiguration.Create(false) 13 | testconfiguration.WriteSslCertificates(true) 14 | exitVal := m.Run() 15 | testconfiguration.Delete() 16 | os.Exit(exitVal) 17 | } 18 | 19 | func TestIsCertificatePresent(t *testing.T) { 20 | test.IsEqualBool(t, isCertificatePresent(), true) 21 | os.Remove("test/ssl.crt") 22 | test.IsEqualBool(t, isCertificatePresent(), false) 23 | os.Remove("test/ssl.key") 24 | test.IsEqualBool(t, isCertificatePresent(), false) 25 | testconfiguration.WriteSslCertificates(true) 26 | os.Remove("test/ssl.key") 27 | test.IsEqualBool(t, isCertificatePresent(), false) 28 | testconfiguration.WriteSslCertificates(true) 29 | test.IsEqualBool(t, isCertificatePresent(), true) 30 | } 31 | 32 | func TestGetCertificateLocations(t *testing.T) { 33 | cert, key := GetCertificateLocations() 34 | test.IsEqualString(t, cert, "test/ssl.crt") 35 | test.IsEqualString(t, key, "test/ssl.key") 36 | } 37 | 38 | func TestGetDomain(t *testing.T) { 39 | test.IsEqualString(t, getDomain("http://127.0.0.1"), "127.0.0.1") 40 | test.IsEqualString(t, getDomain("http://127.0.0.1:123"), "127.0.0.1") 41 | test.IsEqualString(t, getDomain("http://localhost/test"), "localhost") 42 | test.IsEqualString(t, getDomain("http://localhost:8080/test"), "localhost") 43 | test.IsEqualString(t, getDomain("https://github.com/forceu/gokapi"), "github.com") 44 | } 45 | 46 | func TestGetDaysRemaining(t *testing.T) { 47 | expiry := time.Unix(2147483645, 0) 48 | remainingDays := getDaysRemaining() 49 | result := time.Now().Add(time.Duration(remainingDays) * 24 * time.Hour).Sub(expiry) 50 | test.IsEqualBool(t, result.Hours() <= 12 && result.Hours() >= -12, true) 51 | os.Remove("test/ssl.key") 52 | test.IsEqualInt(t, getDaysRemaining(), -1) 53 | testconfiguration.WriteSslCertificates(false) 54 | test.IsEqualBool(t, getDaysRemaining() <= 0, true) 55 | } 56 | 57 | func TestGenerateIfInvalidCert(t *testing.T) { 58 | testconfiguration.WriteSslCertificates(true) 59 | GenerateIfInvalidCert("http://mydomain.com", false) 60 | test.IsEqualBool(t, getDaysRemaining() > 500, true) 61 | GenerateIfInvalidCert("http://mydomain.com", true) 62 | test.IsEqualInt(t, getDaysRemaining(), 365) 63 | testconfiguration.WriteSslCertificates(false) 64 | test.IsEqualBool(t, getDaysRemaining() <= 0, true) 65 | GenerateIfInvalidCert("https://127.0.0.1:8080/", false) 66 | test.IsEqualInt(t, getDaysRemaining(), 365) 67 | os.Remove("test/ssl.crt") 68 | test.IsEqualInt(t, getDaysRemaining(), -1) 69 | GenerateIfInvalidCert("http://127.0.0.1/", false) 70 | test.IsEqualInt(t, getDaysRemaining(), 365) 71 | } 72 | -------------------------------------------------------------------------------- /internal/webserver/web/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /internal/webserver/web/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /internal/webserver/web/static/apidocumentation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | REST API & data model documentation | Gokapi 10 | 11 | 16 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /internal/webserver/web/static/apidocumentation/swagger/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Swagger UI: OAuth2 Redirect 4 | 5 | 6 | 7 | 69 | -------------------------------------------------------------------------------- /internal/webserver/web/static/apidocumentation/swagger/openapiui.js: -------------------------------------------------------------------------------- 1 | function HideTopbarPlugin() 2 | { 3 | return { 4 | components: { 5 | Topbar: function () { return null } 6 | } 7 | } 8 | } 9 | 10 | const swaggerUi = SwaggerUIBundle({ 11 | url: Gokapi.OpenApi.SpecUrl, 12 | dom_id: '#swagger-ui', 13 | deepLinking: true, 14 | presets: [ 15 | SwaggerUIBundle.presets.apis, 16 | SwaggerUIStandalonePreset 17 | ], 18 | plugins: [ 19 | SwaggerUIBundle.plugins.DownloadUrl, 20 | HideTopbarPlugin 21 | ], 22 | layout: 'StandaloneLayout', 23 | docExpansion: "list" 24 | }); 25 | 26 | window.ui = swaggerUi; 27 | -------------------------------------------------------------------------------- /internal/webserver/web/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/apple-touch-icon.png -------------------------------------------------------------------------------- /internal/webserver/web/static/assets/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/assets/background.jpg -------------------------------------------------------------------------------- /internal/webserver/web/static/assets/dist/css/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/assets/dist/css/index.html -------------------------------------------------------------------------------- /internal/webserver/web/static/assets/dist/icons/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2023 The Bootstrap Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/webserver/web/static/assets/dist/icons/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/assets/dist/icons/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /internal/webserver/web/static/assets/dist/icons/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/assets/dist/icons/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /internal/webserver/web/static/assets/dist/icons/fonts/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/assets/dist/icons/fonts/index.html -------------------------------------------------------------------------------- /internal/webserver/web/static/assets/dist/icons/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/assets/dist/icons/index.html -------------------------------------------------------------------------------- /internal/webserver/web/static/assets/dist/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/assets/dist/index.html -------------------------------------------------------------------------------- /internal/webserver/web/static/assets/dist/js/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/assets/dist/js/index.html -------------------------------------------------------------------------------- /internal/webserver/web/static/assets/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/assets/index.html -------------------------------------------------------------------------------- /internal/webserver/web/static/css/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/css/index.html -------------------------------------------------------------------------------- /internal/webserver/web/static/css/min/gokapi.min.5.css: -------------------------------------------------------------------------------- 1 | .btn-secondary,.btn-secondary:hover,.btn-secondary:focus{color:#333;text-shadow:none}body{background:url(../../assets/background.jpg)no-repeat 50% fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center}td{vertical-align:middle;position:relative}a{color:inherit}a:hover{color:inherit;filter:brightness(80%)}.dropzone{background:#2f343a!important;color:#fff;border-radius:5px}.dropzone:hover{background:#33393f!important;color:#fff;border-radius:5px}.card{margin:0 auto;float:none;margin-bottom:10px;border:2px solid #33393f}.card-body{background-color:#212529;color:#ddd}.card-title{font-weight:900}.admin-input{text-align:center}.form-control:disabled{background:#bababa}.break{flex-basis:100%;height:0}.bd-placeholder-img{font-size:1.125rem;text-anchor:middle;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(min-width:768px){.bd-placeholder-img-lg{font-size:3.5rem}.break{flex-basis:0}}.masthead{margin-bottom:2rem}.masthead-brand{margin-bottom:0}.nav-masthead .nav-link{padding:.25rem 0;font-weight:700;color:rgba(255,255,255,.5);background-color:initial;border-bottom:.25rem solid transparent}.nav-masthead .nav-link:hover,.nav-masthead .nav-link:focus{border-bottom-color:rgba(255,255,255,.25)}.nav-masthead .nav-link+.nav-link{margin-left:1rem}.nav-masthead .active{color:#fff;border-bottom-color:#fff}#qroverlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.3)}#qrcode{position:absolute;top:50%;left:50%;margin-top:-105px;margin-left:-105px;width:210px;height:210px;border:5px solid #fff}.toastnotification{pointer-events:none;position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background-color:#333;color:#fff;padding:15px;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-in-out;z-index:9999}.toastnotification.show{opacity:1;pointer-events:auto}.toast-undo{margin-left:20px;color:#4fc3f7;cursor:pointer;text-decoration:underline;font-weight:700;pointer-events:auto}.toast-undo:hover{color:#81d4fa}.toastnotification:not(.show){pointer-events:none!important}.toastnotification:not(.show) .toast-undo{pointer-events:none}.perm-granted{cursor:pointer;color:#0edf00}.perm-notgranted{cursor:pointer;color:#9f9999}.perm-unavailable{color:#525252}.perm-processing{pointer-events:none;color:#e5eb00}.perm-nochange{cursor:default}.prevent-select{-webkit-user-select:none;-ms-user-select:none;user-select:none}.gokapi-dialog{background-color:#212529;color:#ddd}@keyframes subtleHighlight{0%{background-color:#444950}100%{background-color:initial}}@keyframes subtleHighlightNewJson{0%{background-color:green}100%{background-color:initial}}.updatedDownloadCount{animation:subtleHighlight .5s ease-out}.newApiKey{animation:subtleHighlightNewJson .7s ease-out}.newUser{animation:subtleHighlightNewJson .7s ease-out}.newItem{animation:subtleHighlightNewJson 1.5s ease-out}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.rowDeleting{animation:fadeOut .3s ease-out forwards}.highlighted-password{background-color:#444;color:#ddd;padding:2px 6px;border-radius:4px;font-weight:700;font-family:monospace;display:inline-block;margin-left:8px;border:1px solid #555}.filename{font-weight:700;font-size:14px;margin-bottom:5px}.upload-progress-container{display:flex;align-items:center}.upload-progress-bar{position:relative;height:10px;background-color:#eee;flex:1;margin-right:10px;border-radius:4px}.upload-progress-bar-progress{position:absolute;top:0;left:0;height:100%;background-color:#0a0;border-radius:4px;transition:width .2s ease-in-out}.upload-progress-info{font-size:12px}.us-container{margin-top:10px;margin-bottom:20px}.uploaderror{font-weight:700;color:red;margin-bottom:5px}.uploads-container{background-color:#2f343a;border:2px solid rgba(0,0,0,.3);border-radius:5px;margin-left:0;margin-right:0;max-width:none;visibility:hidden} -------------------------------------------------------------------------------- /internal/webserver/web/static/css/min/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/css/min/index.html -------------------------------------------------------------------------------- /internal/webserver/web/static/css/uploadProgress.css: -------------------------------------------------------------------------------- 1 | .filename { 2 | font-weight: bold; 3 | font-size: 14px; 4 | margin-bottom: 5px; 5 | } 6 | .upload-progress-container { 7 | display: flex; 8 | align-items: center; 9 | } 10 | .upload-progress-bar { 11 | position: relative; 12 | height: 10px; 13 | background-color: #eee; 14 | flex: 1; 15 | margin-right: 10px; 16 | border-radius: 4px; 17 | } 18 | .upload-progress-bar-progress { 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | height: 100%; 23 | background-color: #0a0; 24 | border-radius: 4px; 25 | transition: width 0.2s ease-in-out; 26 | } 27 | .upload-progress-info { 28 | font-size: 12px; 29 | } 30 | .us-container { 31 | margin-top: 10px; 32 | margin-bottom: 20px; 33 | } 34 | .uploaderror { 35 | font-weight: bold; 36 | color: red; 37 | margin-bottom: 5px; 38 | } 39 | .uploads-container { 40 | background-color: rgb(47, 52, 58); 41 | border: 2px solid rgba(0,0,0,.3); 42 | border-radius: 5px; 43 | margin-left: 0px; 44 | margin-right: 0px; 45 | max-width: none; 46 | visibility: hidden; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /internal/webserver/web/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/favicon-16x16.png -------------------------------------------------------------------------------- /internal/webserver/web/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/favicon-32x32.png -------------------------------------------------------------------------------- /internal/webserver/web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/favicon.ico -------------------------------------------------------------------------------- /internal/webserver/web/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /internal/webserver/web/static/js/admin_ui_allPages.js: -------------------------------------------------------------------------------- 1 | // This file contains shared JS code for all admin views 2 | // All files named admin_*.js will be merged together and minimised by calling 3 | // go generate ./... 4 | 5 | 6 | var clipboard = new ClipboardJS('.copyurl'); 7 | 8 | var toastId; 9 | 10 | function showToast(timeout, text) { 11 | let notification = document.getElementById("toastnotification"); 12 | if (typeof text !== 'undefined') 13 | notification.innerText = text; 14 | else 15 | notification.innerText = notification.dataset.default; 16 | notification.classList.add("show"); 17 | 18 | clearTimeout(toastId); 19 | toastId = setTimeout(() => { 20 | hideToast(); 21 | }, timeout); 22 | } 23 | 24 | function hideToast() { 25 | document.getElementById("toastnotification").classList.remove("show"); 26 | } 27 | -------------------------------------------------------------------------------- /internal/webserver/web/static/js/admin_ui_logs.js: -------------------------------------------------------------------------------- 1 | // This file contains JS code for the Logs view 2 | // All files named admin_*.js will be merged together and minimised by calling 3 | // go generate ./... 4 | 5 | function filterLogs(tag) { 6 | if (tag == "all") { 7 | textarea.value = logContent; 8 | } else { 9 | textarea.value = logContent.split("\n").filter(line => line.includes("[" + tag + "]")).join("\n"); 10 | } 11 | textarea.scrollTop = textarea.scrollHeight; 12 | } 13 | 14 | function deleteLogs(cutoff) { 15 | if (cutoff == "none") { 16 | return; 17 | } 18 | if (!confirm("Do you want to delete the selected logs?")) { 19 | document.getElementById('deleteLogs').selectedIndex = 0; 20 | return; 21 | } 22 | let timestamp = Math.floor(Date.now() / 1000) 23 | switch (cutoff) { 24 | case "all": 25 | timestamp = 0; 26 | break; 27 | case "2": 28 | timestamp = timestamp - 2 * 24 * 60 * 60; 29 | break; 30 | case "7": 31 | timestamp = timestamp - 7 * 24 * 60 * 60; 32 | break; 33 | case "14": 34 | timestamp = timestamp - 14 * 24 * 60 * 60; 35 | break; 36 | case "30": 37 | timestamp = timestamp - 30 * 24 * 60 * 60; 38 | break; 39 | } 40 | apiLogsDelete(timestamp) 41 | .then(data => { 42 | location.reload(); 43 | }) 44 | .catch(error => { 45 | alert("Unable to delete logs: " + error); 46 | console.error('Error:', error); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /internal/webserver/web/static/js/end2end_download.js: -------------------------------------------------------------------------------- 1 | function parseHashValue(id) { 2 | let key = sessionStorage.getItem("key-" + id); 3 | let filename = sessionStorage.getItem("fn-" + id); 4 | 5 | if (key === null || filename === null) { 6 | hash = window.location.hash.substr(1); 7 | if (hash.length < 50) { 8 | redirectToE2EError(); 9 | return; 10 | } 11 | let info; 12 | try { 13 | let infoJson = atob(hash); 14 | info = JSON.parse(infoJson) 15 | } catch (err) { 16 | redirectToE2EError(); 17 | return; 18 | } 19 | if (!isCorrectJson(info)) { 20 | redirectToE2EError(); 21 | return; 22 | } 23 | sessionStorage.setItem("key-" + id, info.c); 24 | sessionStorage.setItem("fn-" + id, info.f); 25 | } 26 | } 27 | 28 | function isCorrectJson(input) { 29 | return (input.f !== undefined && 30 | input.c !== undefined && 31 | typeof input.f === 'string' && 32 | typeof input.c === 'string' && 33 | input.f != "" && 34 | input.c != ""); 35 | } 36 | 37 | function redirectToE2EError() { 38 | window.location = "./error?e2e"; 39 | } 40 | -------------------------------------------------------------------------------- /internal/webserver/web/static/js/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/js/index.html -------------------------------------------------------------------------------- /internal/webserver/web/static/js/min/end2end_download.min.3.js: -------------------------------------------------------------------------------- 1 | function parseHashValue(e){let t=sessionStorage.getItem("key-"+e),n=sessionStorage.getItem("fn-"+e);if(t===null||n===null){if(hash=window.location.hash.substr(1),hash.length<50){redirectToE2EError();return}let t;try{let e=atob(hash);t=JSON.parse(e)}catch{redirectToE2EError();return}if(!isCorrectJson(t)){redirectToE2EError();return}sessionStorage.setItem("key-"+e,t.c),sessionStorage.setItem("fn-"+e,t.f)}}function isCorrectJson(e){return e.f!==void 0&&e.c!==void 0&&typeof e.f=="string"&&typeof e.c=="string"&&e.f!=""&&e.c!=""}function redirectToE2EError(){window.location="./error?e2e"} -------------------------------------------------------------------------------- /internal/webserver/web/static/js/min/end2end_download.min.6.js: -------------------------------------------------------------------------------- 1 | function parseHashValue(e){let t=sessionStorage.getItem("key-"+e),n=sessionStorage.getItem("fn-"+e);if(t===null||n===null){if(hash=window.location.hash.substr(1),hash.length<50){redirectToE2EError();return}let t;try{let e=atob(hash);t=JSON.parse(e)}catch{redirectToE2EError();return}if(!isCorrectJson(t)){redirectToE2EError();return}sessionStorage.setItem("key-"+e,t.c),sessionStorage.setItem("fn-"+e,t.f)}}function isCorrectJson(e){return e.f!==void 0&&e.c!==void 0&&typeof e.f=="string"&&typeof e.c=="string"&&e.f!=""&&e.c!=""}function redirectToE2EError(){window.location="./error?e2e"} -------------------------------------------------------------------------------- /internal/webserver/web/static/js/min/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Forceu/Gokapi/9f36e90153e9f4f76f0b437f8a0930c8d5eb5021/internal/webserver/web/static/js/min/index.html -------------------------------------------------------------------------------- /internal/webserver/web/static/js/min/streamsaver.min.js: -------------------------------------------------------------------------------- 1 | /*! streamsaver. MIT License. Jimmy Wärting */((e,t)=>{typeof module!="undefined"?module.exports=t():typeof define=="function"&&typeof define.amd=="object"?define(t):this[e]=t()})("streamSaver",()=>{"use strict";const t=typeof window=="object"?window:this;t.HTMLElement||console.warn("streamsaver is meant to run on browsers main thread");let e=null,r=!1;const l=e=>{try{e()}catch{}},c=t.WebStreamsPolyfill||{},i=t.isSecureContext;let s=/constructor/i.test(t.HTMLElement)||!!t.safari||!!t.WebKitPoint;const a=i||"MozAppearance"in document.documentElement.style?"iframe":"navigate",n={createWriteStream:h,WritableStream:t.WritableStream||c.WritableStream,supported:!0,version:{full:"2.0.5",major:2,minor:0,dot:5},mitm:"https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0"};function o(e){if(!e)throw new Error("meh");const t=document.createElement("iframe");return t.hidden=!0,t.src=e,t.loaded=!1,t.name="iframe",t.isIframe=!0,t.postMessage=(...e)=>t.contentWindow.postMessage(...e),t.addEventListener("load",()=>{t.loaded=!0},{once:!0}),document.body.appendChild(t),t}function d(e){const i="width=200,height=100",s=document.createDocumentFragment(),n={frame:t.open(e,"popup",i),loaded:!1,isIframe:!1,isPopup:!0,remove(){n.frame.close()},addEventListener(...e){s.addEventListener(...e)},dispatchEvent(...e){s.dispatchEvent(...e)},removeEventListener(...e){s.removeEventListener(...e)},postMessage(...e){n.frame.postMessage(...e)}},o=e=>{e.source===n.frame&&(n.loaded=!0,t.removeEventListener("message",o),n.dispatchEvent(new Event("load")))};return t.addEventListener("message",o),n}try{new Response(new ReadableStream),i&&!("serviceWorker"in navigator)&&(s=!0)}catch{s=!0}l(()=>{const{readable:t}=new TransformStream,e=new MessageChannel;e.port1.postMessage(t,[t]),e.port1.close(),e.port2.close(),r=!0,Object.defineProperty(n,"TransformStream",{configurable:!1,writable:!1,value:TransformStream})});function u(){e||(e=i?o(n.mitm):d(n.mitm))}function h(t,i,c){let d={size:null,pathname:null,writableStrategy:void 0,readableStrategy:void 0},p=0,h=null,l=null,m=null;if(Number.isFinite(i)?([c,i]=[i,c],console.warn("[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream"),d.size=c,d.writableStrategy=i):i&&i.highWaterMark?(console.warn("[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream"),d.size=c,d.writableStrategy=i):d=i||{},!s){u(),l=new MessageChannel,t=encodeURIComponent(t.replace(/\//g,":")).replace(/['()]/g,escape).replace(/\*/g,"%2A");const s={transferringReadable:r,pathname:d.pathname||Math.random().toString().slice(-6)+"/"+t,headers:{"Content-Type":"application/octet-stream; charset=utf-8","Content-Disposition":"attachment; filename*=UTF-8''"+t}};d.size&&(s.headers["Content-Length"]=d.size);const i=[s,"*",[l.port2]];if(r){const t=a==="iframe"?void 0:{transform(e,t){if(!(e instanceof Uint8Array))throw new TypeError("Can only write Uint8Arrays");p+=e.length,t.enqueue(e),h&&(location.href=h,h=null)},flush(){h&&(location.href=h)}};m=new n.TransformStream(t,d.writableStrategy,d.readableStrategy);const e=m.readable;l.port1.postMessage({readableStream:e},[e])}l.port1.onmessage=t=>{t.data.download?a==="navigate"?(e.remove(),e=null,p?location.href=t.data.download:h=t.data.download):(e.isPopup&&(e.remove(),e=null,a==="iframe"&&o(n.mitm)),o(t.data.download)):t.data.abort&&(f=[],l.port1.postMessage("abort"),l.port1.onmessage=null,l.port1.close(),l.port2.close(),l=null)},e.loaded?e.postMessage(...i):e.addEventListener("load",()=>{e.postMessage(...i)},{once:!0})}let f=[];return!s&&m&&m.writable||new n.WritableStream({write(e){if(!(e instanceof Uint8Array))throw new TypeError("Can only write Uint8Arrays");if(s){f.push(e);return}l.port1.postMessage(e),p+=e.length,h&&(location.href=h,h=null)},close(){if(s){const n=new Blob(f,{type:"application/octet-stream; charset=utf-8"}),e=document.createElement("a");e.href=URL.createObjectURL(n),e.download=t,e.click()}else l.port1.postMessage("end")},abort(){f=[],l.port1.postMessage("abort"),l.port1.onmessage=null,l.port1.close(),l.port2.close(),l=null}},d.writableStrategy)}return n}) -------------------------------------------------------------------------------- /internal/webserver/web/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /internal/webserver/web/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /internal/webserver/web/templates/expired_file_svg.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{.PublicName}} 4 | The requested file has expired 5 | 6 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_changepw.tmpl: -------------------------------------------------------------------------------- 1 | {{define "changepw"}}{{ template "header" . }} 2 |
3 |
4 |
5 |
6 |

Change Password

7 |
8 |

A password change has been requested.
Please enter a new password.

9 |

10 | 11 |

12 | 13 | 14 | {{ if ne .ErrorMessage "" }} 15 |

{{.ErrorMessage}}

16 | {{ end }} 17 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 | 57 | {{ template "pagename" "ChangePw"}} 58 | {{ template "customjs" .}} 59 | {{ template "footer" }} 60 | {{end}} 61 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_download_password.tmpl: -------------------------------------------------------------------------------- 1 | {{define "download_password"}}{{template "header" .}} 2 | 3 | {{ if .EndToEndEncryption }} 4 | 7 | {{ end }} 8 | 9 |
10 |
11 |
12 |
13 |

Password required

14 |
15 |
16 |
17 | 18 |
19 | {{ if .IsFailedLogin }} 20 | Incorrect password!
21 | {{ end }} 22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 37 | {{ template "pagename" "PublicDownloadPw"}} 38 | {{ template "customjs" .}} 39 | {{template "footer"}} 40 | {{end}} 41 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_error.tmpl: -------------------------------------------------------------------------------- 1 | {{define "error"}}{{template "header" .}} 2 | 3 |
4 |
5 |
6 |
7 |

Error

8 |

9 |
10 | {{ if eq .ErrorId 0 }} 11 | Sorry, this file cannot be found.

Either the link has expired or it has been downloaded too many times. 12 | {{ end }} 13 | {{ if eq .ErrorId 1 }} 14 | This file is encrypted and no key has been passed.

Please contact the uploader to give you the correct link, including the value after the hash. 15 | {{ end }} 16 | {{ if eq .ErrorId 2 }} 17 | This file is encrypted and an incorrect key has been passed.

If this file is end-to-end encrypted, please contact the uploader to give you the correct link, including the value after the hash. 18 | {{ end }} 19 |
  20 |

21 |
22 |
23 |
24 |
25 | {{ template "pagename" "PublicError"}} 26 | {{ template "customjs" .}} 27 | {{template "footer"}} 28 | {{end}} 29 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_error_auth.tmpl: -------------------------------------------------------------------------------- 1 | {{define "error_auth"}}{{template "header" .}} 2 | 3 |
4 |
5 |
6 |
7 |

Unauthorised user

8 |
9 |

Login with OAuth provider was sucessful, however this user is not authorised to use Gokapi.



10 | Log in as different user 11 |
12 |
13 |
14 |
15 | {{ template "pagename" "LoginError"}} 16 | {{ template "customjs" .}} 17 | {{template "footer"}} 18 | {{end}} 19 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_error_header.tmpl: -------------------------------------------------------------------------------- 1 | {{define "error_auth_header"}}{{template "header" .}} 2 | 3 |
4 |
5 |
6 |
7 |

Unauthorised

8 |
9 |

Error: No login information was sent from the authentication provider.


10 |
11 |
12 |
13 |
14 | {{ template "pagename" "LoginErrorHeader"}} 15 | {{ template "customjs" .}} 16 | {{template "footer"}} 17 | {{end}} 18 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_error_int_oauth.tmpl: -------------------------------------------------------------------------------- 1 | {{define "error_int_oauth"}}{{template "header" .}} 2 | 3 |
4 |
5 | {{ if eq .ErrorProvidedName "access_denied"}} 6 |
7 |
8 |

Access denied

9 |
10 |

The request was denied by the user or authentication provider.


11 | {{ else }} 12 |
13 |
14 |

OIDC Provider Error {{.ErrorProvidedName}}

15 |
16 |

Login with OAuth provider was not sucessful, the following error was raised:

17 | {{ if .ErrorProvidedMessage }} 18 |

{{ .ErrorProvidedMessage }}

19 | {{ end}} 20 |

{{ .ErrorGenericMessage }}


21 | {{ end }} 22 | Try again 23 |
24 |
25 |
26 |
27 | {{ template "pagename" "LoginErrorOauth"}} 28 | {{ template "customjs" .}} 29 | {{template "footer"}} 30 | {{end}} 31 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_footer.tmpl: -------------------------------------------------------------------------------- 1 | {{define "footer"}} 2 | 3 | 4 | 7 |
8 | 9 | 10 | {{end}} 11 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_forgotpw.tmpl: -------------------------------------------------------------------------------- 1 | {{define "forgotpw"}}{{ template "header" . }} 2 |
3 |
4 |
5 |
6 |

Forgot password

7 |
8 |

9 | If you forgot your user password, please ask your administrator to reset it.

To reset an administrator password, restart the server with the argument --reconfigure and change it in the authentication section. 10 |


11 |
12 |
13 |
14 |
15 | {{ template "pagename" "ForgotPw"}} 16 | {{ template "customjs" .}} 17 | {{ template "footer" }} 18 | {{end}} 19 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_index.tmpl: -------------------------------------------------------------------------------- 1 | {{define "index"}} 2 | 3 | {{end}} 4 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_login.tmpl: -------------------------------------------------------------------------------- 1 | {{define "login"}}{{ template "header" . }} 2 |
3 |
4 |
5 |
6 |

Login

7 |
8 |

9 |

10 | 11 | 12 |

13 | 14 | 15 |

16 | {{ if .IsFailedLogin }} 17 | Incorrect username or password!

18 | {{ end }} 19 | 20 |
21 |

22 | Forgot password

23 |
24 |
25 |
26 |
27 | 37 | {{ template "pagename" "Login"}} 38 | {{ template "customjs" .}} 39 | {{ template "footer" }} 40 | {{end}} 41 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_logs.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "logs" }}{{ template "header" . }} 2 |
3 |
4 |
5 |
6 |

Log File

7 |
8 | 9 | 11 | 12 |
13 |
14 | 23 | 24 | 32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 |
40 | 41 | 47 | {{ template "pagename" "LogOverview"}} 48 | {{ template "customjs" .}} 49 | {{ template "footer" true}} 50 | {{ end }} 51 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/html_redirect_filename.tmpl: -------------------------------------------------------------------------------- 1 | {{define "redirect_filename"}} 2 | 3 | 4 | {{ if .PasswordRequired }} 5 | {{.PublicName}}: Password required 6 | 7 | 8 | 9 | 10 | 11 | {{ else }} 12 | {{.PublicName}}: {{.Name}} 13 | 14 | 15 | 16 | 17 | 18 | {{end }} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | {{end}} 33 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/js_custom.tmpl: -------------------------------------------------------------------------------- 1 | {{define "customjs"}} 2 | {{ if .IsAdminView }} 3 | {{ if .CustomContent.UseCustomAdminJs }} 4 | 5 | {{ end }} 6 | {{else}} 7 | {{ if .CustomContent.UseCustomPublicJs }} 8 | 9 | {{ end }} 10 | {{end}} 11 | {{end}} 12 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/js_pagename.tmpl: -------------------------------------------------------------------------------- 1 | {{define "pagename"}} 2 | 5 | {{end}} 6 | -------------------------------------------------------------------------------- /internal/webserver/web/templates/string_constants.tmpl: -------------------------------------------------------------------------------- 1 | // File contains auto-generated values. Do not change manually 2 | {{define "version"}}2.0.0{{end}} 3 | 4 | // Specifies the version of JS files, so that the browser doesn't 5 | // use a cached version, if the file has been updated 6 | {{define "js_admin_version"}}10{{end}} 7 | {{define "js_dropzone_version"}}5{{end}} 8 | {{define "js_e2eversion"}}6{{end}} 9 | {{define "css_main"}}5{{end}} -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | GOPACKAGE=github.com/forceu/gokapi 2 | BUILD_FLAGS=-ldflags="-s -w -X '$(GOPACKAGE)/internal/environment.Builder=Make Script' -X '$(GOPACKAGE)/internal/environment.BuildTime=$(shell date)'" 3 | BUILD_FLAGS_DEBUG=-ldflags="-X '$(GOPACKAGE)/internal/environment.Builder=Make Script' -X '$(GOPACKAGE)/internal/environment.BuildTime=$(shell date)'" 4 | DOCKER_IMAGE_NAME=gokapi 5 | CONTAINER_TOOL ?= docker 6 | 7 | # Default target 8 | .PHONY: all 9 | all: build 10 | 11 | 12 | .PHONY: build 13 | # Build Gokapi binary 14 | build : 15 | @echo "Building binary..." 16 | @echo 17 | go generate ./... 18 | CGO_ENABLED=0 go build $(BUILD_FLAGS) -o ./gokapi $(GOPACKAGE)/cmd/gokapi 19 | 20 | .PHONY: build-debug 21 | # Build Gokapi binary 22 | build-debug : 23 | @echo "Building binary with debug info..." 24 | @echo 25 | go generate ./... 26 | CGO_ENABLED=0 go build $(BUILD_FLAGS_DEBUG) -o ./gokapi $(GOPACKAGE)/cmd/gokapi 27 | 28 | .PHONY: coverage 29 | coverage: 30 | @echo Generating coverage 31 | @echo 32 | GOKAPI_AWS_BUCKET="gokapi" GOKAPI_AWS_REGION="eu-central-1" GOKAPI_AWS_KEY="keyid" GOKAPI_AWS_KEY_SECRET="secret" go test ./... -parallel 8 --tags=test,awstest -coverprofile=/tmp/coverage1.out && go tool cover -html=/tmp/coverage1.out 33 | 34 | .PHONY: coverage-specific 35 | coverage-specific: 36 | @echo Generating coverage for "$(TEST_PACKAGE)" 37 | @echo 38 | go test $(GOPACKAGE)/$(TEST_PACKAGE)/... -parallel 8 --tags=test,awsmock -coverprofile=/tmp/coverage2.out && go tool cover -html=/tmp/coverage2.out 39 | 40 | 41 | .PHONY: coverage-all 42 | coverage-all: 43 | @echo Generating coverage 44 | @echo 45 | GOKAPI_AWS_BUCKET="gokapi" GOKAPI_AWS_REGION="eu-central-1" GOKAPI_AWS_KEY="keyid" GOKAPI_AWS_KEY_SECRET="secret" go test ./... -parallel 8 --tags=test,awstest -coverprofile=/tmp/coverage1.out && go tool cover -html=/tmp/coverage1.out 46 | 47 | 48 | .PHONY: test 49 | test: 50 | @echo Testing with AWS mock 51 | @echo 52 | go test ./... -parallel 8 --tags=test,awsmock 53 | 54 | 55 | .PHONY: test-specific 56 | test-specific: 57 | @echo Testing package "$(TEST_PACKAGE)" 58 | @echo 59 | go test $(GOPACKAGE)/$(TEST_PACKAGE)/... -parallel 8 -count=1 --tags=test,awsmock 60 | 61 | 62 | .PHONY: test-all 63 | test-all: 64 | @echo Testing all tags 65 | @echo 66 | go test ./... -parallel 8 --tags=test,noaws 67 | go test ./... -parallel 8 --tags=test,awsmock 68 | GOKAPI_AWS_BUCKET="gokapi" GOKAPI_AWS_REGION="eu-central-1" GOKAPI_AWS_KEY="keyid" GOKAPI_AWS_KEY_SECRET="secret" go test ./... -parallel 8 --tags=test,awstest 69 | 70 | .PHONY: clean 71 | # Deletes binary 72 | clean: 73 | @echo "Cleaning up..." 74 | rm -f $(OUTPUT_BIN) 75 | 76 | .PHONY: docker-build 77 | # Create a Docker image 78 | # Use make docker-build CONTAINER_TOOL=podman for podman instead of Docker 79 | docker-build: build 80 | @echo "Building container image..." 81 | $(CONTAINER_TOOL) build . -t $(DOCKER_IMAGE_NAME) 82 | --------------------------------------------------------------------------------