├── .github ├── FUNDING.yml └── workflows │ ├── binary-release.yml │ ├── dev-docker-io.yml │ ├── main-docker-all.yml │ └── readme-docker.yml ├── .gitignore ├── .goreleaser.yaml ├── .version ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets ├── Screenshot.png ├── Screenshot1.png └── logo.png ├── cmd └── ForAuth │ └── main.go ├── configs ├── ForAuth.service ├── ForAuth@.service ├── install.sh └── postinstall.sh ├── docs └── BCRYPT.md ├── go.mod ├── go.sum └── internal ├── auth ├── auth.go ├── cookie.go ├── hash.go ├── models-vars.go ├── session.go └── timeparse.go ├── check ├── error.go └── path.go ├── conf └── getconfig.go ├── models └── models.go ├── notify └── shout.go ├── web ├── config.go ├── const-var.go ├── login.go ├── public │ ├── favicon.png │ └── version ├── templates │ ├── config.html │ ├── footer.html │ ├── header.html │ └── login.html └── webgui.go └── yaml └── readwrite.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://boosty.to/aceberg/donate', 'https://github.com/aceberg#donate'] 4 | -------------------------------------------------------------------------------- /.github/workflows/binary-release.yml: -------------------------------------------------------------------------------- 1 | name: Binary-release 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | generate: 10 | name: Create release-artifacts 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout the repository 14 | uses: actions/checkout@master 15 | 16 | - uses: actions/setup-go@v4 17 | with: 18 | go-version: 'stable' 19 | - run: go version 20 | 21 | - uses: goreleaser/goreleaser-action@v6 22 | with: 23 | distribution: goreleaser 24 | version: '~> v2' 25 | args: release --clean 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/dev-docker-io.yml: -------------------------------------------------------------------------------- 1 | name: Dev-to-docker 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | IMAGE_NAME: forauth 8 | TAGS: dev 9 | 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Build and Push Docker Image to docker.io 20 | uses: mr-smithers-excellent/docker-build-push@v6 21 | with: 22 | image: ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }} 23 | tags: ${{ env.TAGS }} 24 | registry: docker.io 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_PASSWORD }} 27 | 28 | # - name: Login to GHCR 29 | # uses: docker/login-action@v3 30 | # with: 31 | # registry: ghcr.io 32 | # username: ${{ github.actor }} 33 | # password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | # - name: Build and push 36 | # uses: docker/build-push-action@v6 37 | # with: 38 | # context: . 39 | # platforms: linux/amd64 40 | # push: true 41 | # tags: | 42 | # ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.TAGS }} -------------------------------------------------------------------------------- /.github/workflows/main-docker-all.yml: -------------------------------------------------------------------------------- 1 | name: Main-Docker 2 | 3 | on: 4 | workflow_dispatch: 5 | # push: 6 | # branches: [ "main" ] 7 | # paths: 8 | # - 'Dockerfile' 9 | # - 'src/**' 10 | 11 | env: 12 | IMAGE_NAME: forauth 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Get version tag from env file 23 | uses: c-py/action-dotenv-to-setenv@v5 24 | with: 25 | env-file: .version 26 | 27 | - name: Set up QEMU 28 | uses: docker/setup-qemu-action@v3 29 | 30 | - name: Set up Docker Buildx 31 | id: buildx 32 | uses: docker/setup-buildx-action@v3 33 | 34 | - name: Login to GHCR 35 | uses: docker/login-action@v3 36 | with: 37 | registry: ghcr.io 38 | username: ${{ github.actor }} 39 | password: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Login to Docker Hub 42 | uses: docker/login-action@v3 43 | with: 44 | username: ${{ secrets.DOCKER_USERNAME }} 45 | password: ${{ secrets.DOCKER_PASSWORD }} 46 | 47 | - name: Build and push 48 | uses: docker/build-push-action@v6 49 | with: 50 | context: . 51 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 52 | push: true 53 | tags: | 54 | ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest 55 | ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }} 56 | ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest 57 | ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }} -------------------------------------------------------------------------------- /.github/workflows/readme-docker.yml: -------------------------------------------------------------------------------- 1 | name: README-to-DockerHub 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main" ] 7 | paths: 8 | - 'README.md' 9 | 10 | env: 11 | IMAGE_NAME: forauth 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Sync README.md to DockerHub 22 | uses: ms-jpq/sync-dockerhub-readme@v1 23 | with: 24 | username: ${{ secrets.DOCKER_USERNAME }} 25 | password: ${{ secrets.DOCKER_PASSWORD }} 26 | repository: ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }} 27 | readme: "./README.md" 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore tmp 2 | tmp/ -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: ForAuth 2 | builds: 3 | - main: ./cmd/ForAuth/ 4 | binary: forauth 5 | id: default 6 | env: [CGO_ENABLED=0] 7 | goos: 8 | - linux 9 | - windows 10 | - darwin 11 | goarch: 12 | - 386 13 | - amd64 14 | - arm 15 | - arm64 16 | goarm: 17 | - "5" 18 | - "6" 19 | - "7" 20 | ignore: 21 | - goos: darwin 22 | goarch: 386 23 | - goos: darwin 24 | goarch: arm 25 | - goos: windows 26 | goarch: 386 27 | - goos: windows 28 | goarch: arm 29 | 30 | nfpms: 31 | - maintainer: aceberg 32 | description: Simple auth app (session-cookie) with multiple targets support 33 | homepage: https://github.com/aceberg/ForAuth 34 | license: MIT 35 | section: utils 36 | formats: 37 | - deb 38 | - rpm 39 | - apk 40 | - termux.deb 41 | contents: 42 | - src: ./configs/ForAuth.service 43 | dst: /lib/systemd/system/ForAuth.service 44 | - src: ./configs/ForAuth@.service 45 | dst: /lib/systemd/system/ForAuth@.service 46 | scripts: 47 | postinstall: ./configs/postinstall.sh 48 | 49 | archives: 50 | - files: 51 | - LICENSE 52 | - README.md 53 | - CHANGELOG.md 54 | - src: ./configs/ForAuth.service 55 | dst: ForAuth.service 56 | - src: ./configs/ForAuth@.service 57 | dst: ForAuth@.service 58 | - src: ./configs/install.sh 59 | dst: install.sh 60 | wrap_in_directory: true 61 | format_overrides: 62 | - goos: windows 63 | format: zip 64 | 65 | checksum: 66 | name_template: "checksums.txt" 67 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | internal/web/public/version -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [0.1.4] - 2025-03-11 5 | ### Added 6 | - Show current sessions on Config page 7 | 8 | ## [0.1.3] - 2025-03-10 9 | ### Added 10 | - Log INFO when user session expires 11 | 12 | ### Fixed 13 | - Session bug: concurrent map writes 14 | 15 | ## [0.1.2] - 2025-02-01 16 | ### Added 17 | - Multiple targets 18 | - Logs and notifications text updated 19 | 20 | ### Fixed 21 | - Logout bug 22 | 23 | ## [0.1.1] - 2024-11-02 24 | ### Added 25 | - Login page for Config 26 | - Version file 27 | - Notify on login -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | RUN apk add build-base 4 | COPY . /src 5 | RUN cd /src/cmd/ForAuth/ && CGO_ENABLED=0 go build -o /ForAuth . 6 | 7 | 8 | FROM alpine 9 | 10 | WORKDIR /data/ForAuth 11 | WORKDIR /app 12 | 13 | COPY --from=builder /ForAuth /app/ 14 | 15 | ENTRYPOINT ["./ForAuth"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Andrew Erlikh 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG_NAME=ForAuth 2 | USR_NAME=aceberg 3 | 4 | mod: 5 | rm go.mod || true && \ 6 | rm go.sum || true && \ 7 | go mod init github.com/$(USR_NAME)/$(PKG_NAME) && \ 8 | go mod tidy 9 | 10 | run: 11 | cd cmd/$(PKG_NAME)/ && \ 12 | go run . 13 | 14 | fmt: 15 | go fmt ./... 16 | 17 | lint: 18 | golangci-lint run 19 | golint ./... 20 | 21 | check: fmt lint 22 | 23 | go-build: 24 | cd cmd/$(PKG_NAME)/ && \ 25 | CGO_ENABLED=0 go build -o ../../tmp/$(PKG_NAME) . 26 | 27 | docker-build: 28 | docker build -t $(USR_NAME)/$(PKG_NAME) . 29 | 30 | docker-run: 31 | docker rm $(PKG_NAME) || true 32 | docker run --name $(PKG_NAME) \ 33 | -e "TZ=Asia/Novosibirsk" \ 34 | -v ~/.dockerdata/$(PKG_NAME):/data/$(PKG_NAME) \ 35 | $(USR_NAME)/$(PKG_NAME) 36 | 37 | clean: 38 | rm tmp/$(PKG_NAME) || true 39 | docker rmi -f $(USR_NAME)/$(PKG_NAME) 40 | 41 | dev: docker-build docker-run -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Main-Docker](https://github.com/aceberg/ForAuth/actions/workflows/main-docker-all.yml/badge.svg)](https://github.com/aceberg/ForAuth/actions/workflows/main-docker-all.yml) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/aceberg/forauth)](https://goreportcard.com/report/github.com/aceberg/forauth) 3 | ![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/aceberg/forauth) 4 | 5 |

6 | 7 | ForAuth

8 | 9 | ForAuth (Forward Auth) - simple auth app (session-cookie) with notifications on login and [multiple targets](https://github.com/aceberg/forauth#multiple-targets) option 10 | 11 | - [Security](https://github.com/aceberg/forauth#security) 12 | - [Quick start](https://github.com/aceberg/forauth#quick-start) 13 | - [Config](https://github.com/aceberg/forauth#config) 14 | - [Options](https://github.com/aceberg/forauth#options) 15 | - [Multiple Targets](https://github.com/aceberg/forauth#multiple-targets) 16 | - [Local network only](https://github.com/aceberg/forauth#local-network-only) 17 | - [CURL](https://github.com/aceberg/forauth#curl) 18 | - [Thanks](https://github.com/aceberg/forauth#thanks) 19 | 20 | ![Screenshot](https://raw.githubusercontent.com/aceberg/forauth/main/assets/Screenshot.png) 21 |
22 | Screenshot 2 23 | 24 | ![Screenshot1](https://raw.githubusercontent.com/aceberg/forauth/main/assets/Screenshot1.png) 25 |
26 | 27 | ## Securuty 28 | - This app is only safe when used with `https` 29 | - Use strong password 30 | - Make sure direct access to Target app is closed with firewall or other measures 31 | 32 | ## Quick start 33 | ```sh 34 | docker run --name forauth \ 35 | -v ~/.dockerdata/ForAuth:/data/ForAuth \ 36 | -p 8800:8800 \ # Proxy port 37 | -p 8801:8801 \ # Config port 38 | aceberg/forauth 39 | ``` 40 | Then open Config page in browser and set up Auth and Target app. 41 | 42 | Example [docker-compose-auth.yml](https://github.com/aceberg/WatchYourPorts/blob/main/docker-compose-auth.yml) for [WatchYourPorts](https://github.com/aceberg/WatchYourPorts). This should work with other apps too. 43 | 44 | ## Config 45 | 46 | Configuration can be done through config file, GUI or environment variables. Variable names is `config.yaml` file are the same, but in lowcase. 47 | 48 | | Variable | Description | Default | 49 | | -------- | ----------- | ------- | 50 | | FA_AUTH | Enable Session-Cookie authentication | false | 51 | | FA_AUTH_EXPIRE | Session expiration time. A number and suffix: **m, h, d** or **M**. | 7d | 52 | | FA_AUTH_USER | Username | | 53 | | FA_AUTH_PASSWORD | Encrypted password (bcrypt). [How to encrypt password with bcrypt?](docs/BCRYPT.md) | | 54 | 55 | | Variable | Description | Default | 56 | | -------- | ----------- | ------- | 57 | | FA_HOST | Listen address for both Config and Proxy | 0.0.0.0 | 58 | | FA_PORT | Port for Proxy | 8800 | 59 | | FA_PORTCONF | Port for Config page | 8801 | 60 | | FA_TARGET | Where to proxy after login (host:port). Example: `192.168.1.1:8840` | | 61 | | FA_THEME | Any theme name from https://bootswatch.com in lowcase or [additional](https://github.com/aceberg/aceberg-bootswatch-fork) (emerald, grass, grayscale, ocean, sand, wood)| united | 62 | | FA_COLOR | Background color: light or dark | dark | 63 | | FA_NODEPATH | Path to local JS and Themes ([node-bootstrap](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap)) | | 64 | | FA_NOTIFY | Shoutrrr URL. ForAuth uses [Shoutrrr](https://github.com/containrrr/shoutrrr) to send notifications. It is already integrated, just needs a correct URL. Examples for Discord, Email, Gotify, Matrix, Ntfy, Pushover, Slack, Telegram, Generic Webhook and etc are [here](https://containrrr.dev/shoutrrr/v0.8/services/gotify/) | | 65 | | TZ | Set your timezone for correct time | | 66 | 67 | ## Options 68 | 69 | | Key | Description | Default | 70 | | -------- | ----------- | ------- | 71 | | -d | Path to config dir | /data/ForAuth | 72 | | -n | Path to local JS and Themes ([node-bootstrap](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap)) | | 73 | 74 | ## Multiple Targets 75 | Multiple Targets can be gonfigured from `Config` page or in `targets.yaml` file inside the config dir. 76 | 77 |
78 | Example: 79 | 80 | ```yaml 81 | 0.0.0.0:8854: # where proxy will listen 82 | name: DiaryMD # name 83 | target: 127.0.0.1:8754 # where an app listens 84 | 0.0.0.0:8855: 85 | name: AnyAppStart 86 | target: 127.0.0.1:8755 87 | ``` 88 | 89 |
90 | 91 | ## Local network only 92 | By default, this app pulls themes, icons and fonts from the internet. But, in some cases, it may be useful to have an independent from global network setup. I created a separate [image](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap) with all necessary modules and fonts. 93 | ```sh 94 | docker run --name node-bootstrap \ 95 | -p 8850:8850 \ 96 | aceberg/node-bootstrap 97 | ``` 98 | ```sh 99 | docker run --name forauth \ 100 | -v ~/.dockerdata/ForAuth:/data/ForAuth \ 101 | -p 8800:8800 \ 102 | -p 8801:8801 \ 103 | aceberg/forauth -n "http://$YOUR_IP:8850" 104 | ``` 105 | ## CURL 106 | To access Target app with `curl`: 107 | 108 | ```sh 109 | curl -X POST http://localhost:8800 -H "Content-Type: application/x-www-form-urlencoded" -d "username=user&password=pw" -c fileCookie 110 | ``` 111 | ```sh 112 | curl http://localhost:8800 -b fileCookie 113 | ``` 114 | 115 | ## Thanks 116 | - All go packages listed in [dependencies](https://github.com/aceberg/forauth/network/dependencies) 117 | - [Bootstrap](https://getbootstrap.com/) 118 | - Themes: [Free themes for Bootstrap](https://bootswatch.com) 119 | - Favicon and logo: [Flaticon](https://www.flaticon.com/icons/) -------------------------------------------------------------------------------- /assets/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/ForAuth/b120d7c58e342cd86833ac260922f0a436797ffe/assets/Screenshot.png -------------------------------------------------------------------------------- /assets/Screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/ForAuth/b120d7c58e342cd86833ac260922f0a436797ffe/assets/Screenshot1.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/ForAuth/b120d7c58e342cd86833ac260922f0a436797ffe/assets/logo.png -------------------------------------------------------------------------------- /cmd/ForAuth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | _ "time/tzdata" 7 | 8 | "github.com/aceberg/ForAuth/internal/web" 9 | ) 10 | 11 | const dirPath = "/data/ForAuth" 12 | const nodePath = "" 13 | 14 | func main() { 15 | dirPtr := flag.String("d", dirPath, "Path to config dir") 16 | nodePtr := flag.String("n", nodePath, "Path to node modules") 17 | flag.Parse() 18 | 19 | web.Gui(*dirPtr, *nodePtr) // webgui.go 20 | } 21 | -------------------------------------------------------------------------------- /configs/ForAuth.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ForAuth 3 | Documentation=https://github.com/aceberg/ForAuth 4 | After=network-online.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | ExecStart=/usr/bin/forauth -d /etc/ForAuth/ 9 | Restart=on-failure 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /configs/ForAuth@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ForAuth 3 | Documentation=https://github.com/aceberg/ForAuth 4 | After=network-online.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | User=%i 9 | ExecStart=/usr/bin/forauth -d /home/%i/.config/ForAuth/ 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /configs/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cp forauth /usr/bin/ 4 | cp ForAuth.service /lib/systemd/system/ 5 | cp ForAuth@.service /lib/systemd/system/ -------------------------------------------------------------------------------- /configs/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | systemctl daemon-reload -------------------------------------------------------------------------------- /docs/BCRYPT.md: -------------------------------------------------------------------------------- 1 | # How to encrypt password with bcrypt? 2 | 3 | It is not safe to store password unencrypted, so this app uses `bcrypt` encryption. There are several ways to encrypt your password. 4 | 5 | ## 1. Set password through web GUI 6 | Then the app will encrypt it for you. 7 | 8 | ## 2. Encrypt password yourself 9 | On Linux encryption can be done with `htpasswd` command: 10 | ```sh 11 | htpasswd -nbBC 10 USER YourSecretPassword | sed 's/USER://' 12 | ``` 13 | 14 | ## 3. Encrypt password online 15 | There are online tools for `bcrypt` encryption. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aceberg/ForAuth 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/containrrr/shoutrrr v0.8.0 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/google/uuid v1.6.0 9 | github.com/spf13/viper v1.19.0 10 | golang.org/x/crypto v0.36.0 11 | gopkg.in/yaml.v3 v3.0.1 12 | ) 13 | 14 | require ( 15 | github.com/bytedance/sonic v1.11.6 // indirect 16 | github.com/bytedance/sonic/loader v0.1.1 // indirect 17 | github.com/cloudwego/base64x v0.1.4 // indirect 18 | github.com/cloudwego/iasm v0.2.0 // indirect 19 | github.com/fatih/color v1.15.0 // indirect 20 | github.com/fsnotify/fsnotify v1.7.0 // indirect 21 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 22 | github.com/gin-contrib/sse v0.1.0 // indirect 23 | github.com/go-playground/locales v0.14.1 // indirect 24 | github.com/go-playground/universal-translator v0.18.1 // indirect 25 | github.com/go-playground/validator/v10 v10.20.0 // indirect 26 | github.com/goccy/go-json v0.10.2 // indirect 27 | github.com/hashicorp/hcl v1.0.0 // indirect 28 | github.com/json-iterator/go v1.1.12 // indirect 29 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 30 | github.com/leodido/go-urn v1.4.0 // indirect 31 | github.com/magiconair/properties v1.8.7 // indirect 32 | github.com/mattn/go-colorable v0.1.13 // indirect 33 | github.com/mattn/go-isatty v0.0.20 // indirect 34 | github.com/mitchellh/mapstructure v1.5.0 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 38 | github.com/sagikazarmark/locafero v0.4.0 // indirect 39 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 40 | github.com/sourcegraph/conc v0.3.0 // indirect 41 | github.com/spf13/afero v1.11.0 // indirect 42 | github.com/spf13/cast v1.6.0 // indirect 43 | github.com/spf13/pflag v1.0.5 // indirect 44 | github.com/subosito/gotenv v1.6.0 // indirect 45 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 46 | github.com/ugorji/go/codec v1.2.12 // indirect 47 | go.uber.org/atomic v1.9.0 // indirect 48 | go.uber.org/multierr v1.9.0 // indirect 49 | golang.org/x/arch v0.8.0 // indirect 50 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 51 | golang.org/x/net v0.25.0 // indirect 52 | golang.org/x/sys v0.31.0 // indirect 53 | golang.org/x/text v0.23.0 // indirect 54 | google.golang.org/protobuf v1.34.1 // indirect 55 | gopkg.in/ini.v1 v1.67.0 // indirect 56 | ) 57 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 2 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 3 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 4 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 5 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 6 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 7 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= 10 | github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 16 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 17 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 18 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 19 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 20 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 21 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 22 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 23 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 24 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 25 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 26 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 27 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 28 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 29 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 30 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 31 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 32 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 33 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 34 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 35 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 36 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 37 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 38 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 39 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 40 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 41 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 42 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 43 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 44 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 45 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 46 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 47 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 48 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 49 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 50 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 51 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 52 | github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= 53 | github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= 54 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 55 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 56 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 57 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 58 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 59 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 60 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 61 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 62 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 63 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 64 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 65 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 66 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 67 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 68 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 69 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 70 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 71 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 72 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 73 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 74 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 75 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 76 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 77 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 78 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 79 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 80 | github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= 81 | github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= 82 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 83 | github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= 84 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 85 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 86 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 87 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 88 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 89 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 90 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 91 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 92 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 93 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 94 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 95 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 96 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 97 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 98 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 99 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 100 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 101 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 102 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 103 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 104 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 105 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 106 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 107 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 108 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 109 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 110 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 111 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 112 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 113 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 114 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 115 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 116 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 117 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 118 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 119 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 120 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 121 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 122 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 123 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 124 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 125 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 126 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 127 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 128 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 129 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 130 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 131 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 132 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 133 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 134 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 135 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 136 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 137 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 140 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 141 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 142 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 143 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 144 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 145 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 146 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 147 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 148 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 149 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 150 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 151 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 152 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 153 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 154 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 155 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 156 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 157 | -------------------------------------------------------------------------------- /internal/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // Auth - main auth func 11 | func Auth(c *gin.Context, conf *Conf) bool { 12 | 13 | if !conf.Auth || conf.User == "" || conf.Password == "" { 14 | return true 15 | } 16 | 17 | authConf = *conf 18 | sessionToken := getTokenFromCookie(c) 19 | 20 | userSession, exists := allSessions[sessionToken] 21 | exp := userSession.Expire.Before(time.Now()) 22 | 23 | if exists && !exp { 24 | return true 25 | } 26 | 27 | if exists && exp { 28 | log.Println("INFO: session for user '" + authConf.User + "' logged in from " + c.Request.RemoteAddr + " expired.") 29 | delete(allSessions, sessionToken) 30 | } 31 | 32 | return false 33 | } 34 | -------------------------------------------------------------------------------- /internal/auth/cookie.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func setTokenCookie(c *gin.Context, token string) { 10 | 11 | cookie := http.Cookie{Name: cookieName, Value: token, Path: "/"} 12 | http.SetCookie(c.Writer, &cookie) 13 | } 14 | 15 | func getTokenFromCookie(c *gin.Context) string { 16 | 17 | cookie, err := c.Request.Cookie(cookieName) 18 | if err != nil { 19 | 20 | return "" 21 | } 22 | 23 | return cookie.Value 24 | } 25 | -------------------------------------------------------------------------------- /internal/auth/hash.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "log" 5 | 6 | "golang.org/x/crypto/bcrypt" 7 | ) 8 | 9 | // HashPassword - generate hash from password 10 | func HashPassword(pw string) string { 11 | 12 | hashed, err := bcrypt.GenerateFromPassword([]byte(pw), 10) 13 | if err != nil { 14 | log.Println("PASSWORD ERROR:", err) 15 | return "" 16 | } 17 | 18 | return string(hashed) 19 | } 20 | 21 | // MatchPasswords - check if password and hash matches 22 | func MatchPasswords(hash, pw string) bool { 23 | 24 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) 25 | 26 | return err == nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/auth/models-vars.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Conf - auth config 8 | type Conf struct { 9 | Auth bool 10 | User string 11 | Password string 12 | ExpStr string 13 | Expire time.Duration 14 | } 15 | 16 | var authConf Conf 17 | 18 | // Session - one session 19 | type Session struct { 20 | User string 21 | Host string 22 | Expire time.Time 23 | TimeStr string 24 | } 25 | 26 | var allSessions = make(map[string]Session) 27 | 28 | var cookieName = "forauth_session_token" 29 | -------------------------------------------------------------------------------- /internal/auth/session.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | var mu sync.Mutex 13 | 14 | // StartSession for new login 15 | func StartSession(c *gin.Context) { 16 | var ses Session 17 | 18 | sessionToken := uuid.NewString() 19 | 20 | ses.User = authConf.User 21 | ses.Host = c.Request.Host 22 | ses.Expire = time.Now().Add(authConf.Expire) 23 | ses.TimeStr = ses.Expire.Format("2006-01-02 15:04:05") 24 | 25 | mu.Lock() 26 | allSessions[sessionToken] = ses 27 | mu.Unlock() 28 | 29 | setTokenCookie(c, sessionToken) 30 | } 31 | 32 | // LogOut - log out 33 | func LogOut(c *gin.Context) { 34 | 35 | sessionToken := getTokenFromCookie(c) 36 | 37 | delete(allSessions, sessionToken) 38 | 39 | setTokenCookie(c, "") 40 | } 41 | 42 | // LogOutByToken - log out 43 | func LogOutByToken(token string) { 44 | 45 | delete(allSessions, token) 46 | } 47 | 48 | // GetAllSessions - get current sessions 49 | func GetAllSessions() map[string]Session { 50 | return allSessions 51 | } 52 | -------------------------------------------------------------------------------- /internal/auth/timeparse.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | func timeParse(timeout string) (time.Duration, error) { 9 | var err error 10 | var t time.Duration 11 | 12 | length := len(timeout) 13 | if length > 1 { 14 | suffix := timeout[length-1] 15 | 16 | switch string(suffix) { 17 | case "h": 18 | t, err = time.ParseDuration(timeout) 19 | case "m": 20 | t, err = time.ParseDuration(timeout) 21 | case "d": //day 22 | t, err = time.ParseDuration(timeout[:length-1] + "h") 23 | t = 24 * t 24 | case "M": // month 25 | t, err = time.ParseDuration(timeout[:length-1] + "h") 26 | t = 730 * t 27 | default: 28 | err = errors.New("ERROR: TimeParse: wrong time format") 29 | } 30 | } else { 31 | err = errors.New("ERROR: TimeParse: wrong time format") 32 | } 33 | 34 | return t, err 35 | } 36 | 37 | // ToTime - converts string (example: 3d) to time.Duration 38 | func ToTime(s string) time.Duration { 39 | 40 | t, err := timeParse(s) 41 | if err != nil { 42 | t, _ = timeParse("7d") 43 | } 44 | 45 | return t 46 | } 47 | -------------------------------------------------------------------------------- /internal/check/error.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // IfError prints error, if it is not nil 8 | func IfError(err error) bool { 9 | if err == nil { 10 | return false 11 | } 12 | 13 | log.Println("ERROR:", err) 14 | return true 15 | } 16 | -------------------------------------------------------------------------------- /internal/check/path.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // Path - create path if not exists 9 | func Path(path string) bool { 10 | 11 | _, err := os.Stat(path) 12 | 13 | if path != "" && err != nil { 14 | 15 | dir := filepath.Dir(path) 16 | 17 | err = os.MkdirAll(dir, os.ModePerm) 18 | IfError(err) 19 | 20 | _, err = os.Create(path) 21 | IfError(err) 22 | 23 | return false 24 | } 25 | 26 | return true 27 | } 28 | 29 | // Exists - check is file exists 30 | func Exists(path string) bool { 31 | 32 | _, err := os.Stat(path) 33 | 34 | if path != "" && err != nil { 35 | 36 | return false 37 | } 38 | 39 | return true 40 | } 41 | 42 | // IsYaml - check if file got .yaml or .yml extension 43 | func IsYaml(path string) bool { 44 | 45 | if Exists(path) { 46 | ext := filepath.Ext(path) 47 | if ext == ".yaml" || ext == ".yml" { 48 | return true 49 | } 50 | } 51 | 52 | return false 53 | } 54 | -------------------------------------------------------------------------------- /internal/conf/getconfig.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | 6 | "github.com/aceberg/ForAuth/internal/auth" 7 | "github.com/aceberg/ForAuth/internal/check" 8 | "github.com/aceberg/ForAuth/internal/models" 9 | ) 10 | 11 | // Get - read config from file or env 12 | func Get(path string) (config models.Conf, authConf auth.Conf) { 13 | 14 | viper.SetDefault("FA_HOST", "0.0.0.0") 15 | viper.SetDefault("FA_PORT", "8800") 16 | viper.SetDefault("FA_PORTCONF", "8801") 17 | viper.SetDefault("FA_TARGET", "") 18 | viper.SetDefault("FA_THEME", "united") 19 | viper.SetDefault("FA_COLOR", "dark") 20 | viper.SetDefault("FA_NODEPATH", "") 21 | viper.SetDefault("FA_NOTIFY", "") 22 | 23 | viper.SetDefault("FA_AUTH_USER", "") 24 | viper.SetDefault("FA_AUTH_PASSWORD", "") 25 | viper.SetDefault("FA_AUTH_EXPIRE", "7d") 26 | 27 | viper.SetConfigFile(path) 28 | viper.SetConfigType("yaml") 29 | err := viper.ReadInConfig() 30 | check.IfError(err) 31 | 32 | viper.AutomaticEnv() // Get ENVIRONMENT variables 33 | 34 | config.Host, _ = viper.Get("FA_HOST").(string) 35 | config.Port, _ = viper.Get("FA_PORT").(string) 36 | config.PortConf, _ = viper.Get("FA_PORTCONF").(string) 37 | config.Target, _ = viper.Get("FA_TARGET").(string) 38 | config.Theme, _ = viper.Get("FA_THEME").(string) 39 | config.Color, _ = viper.Get("FA_COLOR").(string) 40 | config.NodePath, _ = viper.Get("FA_NODEPATH").(string) 41 | config.Notify, _ = viper.Get("FA_NOTIFY").(string) 42 | 43 | authConf.Auth = viper.GetBool("FA_AUTH") 44 | authConf.User, _ = viper.Get("FA_AUTH_USER").(string) 45 | authConf.Password, _ = viper.Get("FA_AUTH_PASSWORD").(string) 46 | authConf.ExpStr, _ = viper.Get("FA_AUTH_EXPIRE").(string) 47 | 48 | authConf.Expire = auth.ToTime(authConf.ExpStr) 49 | 50 | return config, authConf 51 | } 52 | 53 | // Write - write config to file 54 | func Write(config models.Conf, authConf auth.Conf) { 55 | 56 | viper.SetConfigFile(config.ConfPath) 57 | viper.SetConfigType("yaml") 58 | 59 | viper.Set("fa_host", config.Host) 60 | viper.Set("fa_port", config.Port) 61 | viper.Set("fa_portconf", config.PortConf) 62 | viper.Set("fa_target", config.Target) 63 | viper.Set("fa_theme", config.Theme) 64 | viper.Set("fa_color", config.Color) 65 | viper.Set("fa_nodepath", config.NodePath) 66 | viper.Set("fa_notify", config.Notify) 67 | 68 | viper.Set("fa_auth", authConf.Auth) 69 | viper.Set("fa_auth_user", authConf.User) 70 | viper.Set("fa_auth_password", authConf.Password) 71 | viper.Set("fa_auth_expire", authConf.ExpStr) 72 | 73 | err := viper.WriteConfig() 74 | check.IfError(err) 75 | } 76 | -------------------------------------------------------------------------------- /internal/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/aceberg/ForAuth/internal/auth" 5 | ) 6 | 7 | // Conf - web gui config 8 | type Conf struct { 9 | Host string 10 | Port string 11 | PortConf string 12 | Theme string 13 | Color string 14 | DirPath string 15 | ConfPath string 16 | YamlPath string 17 | NodePath string 18 | Target string 19 | Notify string 20 | } 21 | 22 | // TargetStruct - for Multi Target 23 | type TargetStruct struct { 24 | Name string `yaml:"name"` 25 | Target string `yaml:"target"` 26 | } 27 | 28 | // GuiData - web gui data 29 | type GuiData struct { 30 | Config Conf 31 | Themes []string 32 | Version string 33 | Auth auth.Conf 34 | TargetMap map[string]TargetStruct 35 | Sessions map[string]auth.Session 36 | } 37 | -------------------------------------------------------------------------------- /internal/notify/shout.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "github.com/containrrr/shoutrrr" 5 | "log" 6 | ) 7 | 8 | // Shout - send message with shoutrrr 9 | func Shout(message string, url string) { 10 | if url != "" { 11 | err := shoutrrr.Send(url, message) 12 | if err != nil { 13 | log.Println("ERROR: Notification failed (shoutrrr):", err) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/web/config.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | "github.com/aceberg/ForAuth/internal/auth" 10 | "github.com/aceberg/ForAuth/internal/check" 11 | "github.com/aceberg/ForAuth/internal/conf" 12 | "github.com/aceberg/ForAuth/internal/models" 13 | "github.com/aceberg/ForAuth/internal/yaml" 14 | ) 15 | 16 | func logoutHandler(c *gin.Context) { 17 | 18 | authOk := auth.Auth(c, &authConf) 19 | if authOk { 20 | auth.LogOut(c) 21 | c.Redirect(http.StatusFound, "/") 22 | } 23 | } 24 | 25 | func sessionDelHandler(c *gin.Context) { 26 | 27 | authOk := auth.Auth(c, &authConf) 28 | if authOk { 29 | key := c.Query("key") 30 | auth.LogOutByToken(key) 31 | c.Redirect(http.StatusFound, "/") 32 | } 33 | } 34 | 35 | func configHandler(c *gin.Context) { 36 | 37 | authOk := auth.Auth(c, &authConf) 38 | if authOk { 39 | var guiData models.GuiData 40 | 41 | guiData.Config = appConfig 42 | guiData.Auth = authConf 43 | guiData.TargetMap = yaml.Read(appConfig.YamlPath) 44 | guiData.Sessions = auth.GetAllSessions() 45 | 46 | guiData.Themes = []string{"cerulean", "cosmo", "cyborg", "darkly", "emerald", "flatly", "grass", "grayscale", "journal", "litera", "lumen", "lux", "materia", "minty", "morph", "ocean", "pulse", "quartz", "sand", "sandstone", "simplex", "sketchy", "slate", "solar", "spacelab", "superhero", "united", "vapor", "wood", "yeti", "zephyr"} 47 | 48 | file, err := pubFS.ReadFile("public/version") 49 | check.IfError(err) 50 | version := string(file) 51 | guiData.Version = version[8:] 52 | 53 | c.HTML(http.StatusOK, "header.html", guiData) 54 | c.HTML(http.StatusOK, "config.html", guiData) 55 | } else { 56 | loginScreen(c, appConfig.Host+":"+appConfig.PortConf, "Config") // login.go 57 | } 58 | } 59 | 60 | func saveConfigHandler(c *gin.Context) { 61 | 62 | authOk := auth.Auth(c, &authConf) 63 | if authOk { 64 | appConfig.Host = c.PostForm("host") 65 | appConfig.Port = c.PostForm("port") 66 | appConfig.PortConf = c.PostForm("portconf") 67 | appConfig.Target = c.PostForm("target") 68 | appConfig.Theme = c.PostForm("theme") 69 | appConfig.Color = c.PostForm("color") 70 | appConfig.NodePath = c.PostForm("nodepath") 71 | appConfig.Notify = c.PostForm("notify") 72 | 73 | conf.Write(appConfig, authConf) 74 | 75 | log.Println("INFO: writing new config to", appConfig.ConfPath) 76 | } 77 | c.Redirect(http.StatusFound, "/") 78 | } 79 | 80 | func saveConfigAuth(c *gin.Context) { 81 | 82 | authOk := auth.Auth(c, &authConf) 83 | if authOk { 84 | authConf.User = c.PostForm("user") 85 | authConf.ExpStr = c.PostForm("expire") 86 | authStr := c.PostForm("auth") 87 | pw := c.PostForm("password") 88 | 89 | if authStr == "on" { 90 | authConf.Auth = true 91 | } else { 92 | authConf.Auth = false 93 | } 94 | 95 | if pw != "" { 96 | authConf.Password = auth.HashPassword(pw) 97 | } 98 | 99 | authConf.Expire = auth.ToTime(authConf.ExpStr) 100 | 101 | if authConf.Auth && (authConf.User == "" || authConf.Password == "") { 102 | log.Println("WARNING: Auth won't work with empty login or password.") 103 | authConf.Auth = false 104 | } 105 | 106 | log.Println("INFO: writing new auth config to", appConfig.ConfPath) 107 | conf.Write(appConfig, authConf) 108 | } 109 | 110 | c.Redirect(http.StatusFound, "/") 111 | } 112 | 113 | func addTargetHandler(c *gin.Context) { 114 | 115 | authOk := auth.Auth(c, &authConf) 116 | if authOk { 117 | name := c.PostForm("name") 118 | proxy := c.PostForm("proxy") 119 | target := c.PostForm("target") 120 | 121 | targetMap[proxy] = models.TargetStruct{Name: name, Target: target} 122 | yaml.Write(appConfig.YamlPath, targetMap) 123 | } 124 | c.Redirect(http.StatusFound, "/") 125 | } 126 | 127 | func delTargetHandler(c *gin.Context) { 128 | 129 | authOk := auth.Auth(c, &authConf) 130 | if authOk { 131 | key := c.Query("key") 132 | 133 | delete(targetMap, key) 134 | yaml.Write(appConfig.YamlPath, targetMap) 135 | } 136 | c.Redirect(http.StatusFound, c.Request.Referer()) 137 | } 138 | -------------------------------------------------------------------------------- /internal/web/const-var.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/aceberg/ForAuth/internal/auth" 7 | "github.com/aceberg/ForAuth/internal/models" 8 | ) 9 | 10 | var ( 11 | // appConfig - config for Web Gui 12 | appConfig models.Conf 13 | 14 | // authConf - config for auth 15 | authConf auth.Conf 16 | 17 | // targetMap - targets 18 | targetMap map[string]models.TargetStruct 19 | ) 20 | 21 | // templFS - html templates 22 | // 23 | //go:embed templates/* 24 | var templFS embed.FS 25 | 26 | // pubFS - public folder 27 | // 28 | //go:embed public/* 29 | var pubFS embed.FS 30 | -------------------------------------------------------------------------------- /internal/web/login.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/http/httputil" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | "github.com/aceberg/ForAuth/internal/auth" 11 | "github.com/aceberg/ForAuth/internal/models" 12 | "github.com/aceberg/ForAuth/internal/notify" 13 | ) 14 | 15 | func loginHandler(c *gin.Context) { 16 | var target, name string 17 | 18 | proxyAddr := c.MustGet("proxyAddr").(string) 19 | targetStruct, ok := targetMap[proxyAddr] 20 | 21 | if ok { 22 | target = targetStruct.Target 23 | name = targetStruct.Name 24 | } else { 25 | target = appConfig.Target 26 | name = "Default" 27 | } 28 | 29 | authOk := auth.Auth(c, &authConf) 30 | if authOk { 31 | reverseProxy(c, target) 32 | } else { 33 | loginScreen(c, target, name) 34 | } 35 | } 36 | 37 | func reverseProxy(c *gin.Context, target string) { 38 | 39 | director := func(req *http.Request) { 40 | req.URL.Scheme = "http" 41 | req.URL.Host = target 42 | } 43 | 44 | proxy := &httputil.ReverseProxy{Director: director} 45 | proxy.ServeHTTP(c.Writer, c.Request) 46 | } 47 | 48 | func loginScreen(c *gin.Context, target, name string) { 49 | var guiData models.GuiData 50 | 51 | username := c.PostForm("username") 52 | password := c.PostForm("password") 53 | 54 | if username == authConf.User && auth.MatchPasswords(authConf.Password, password) { 55 | 56 | msg := "User '" + username + "' logged in from " + c.Request.RemoteAddr + ". Session expires in " + authConf.Expire.String() + ". Target: " + target + " (" + name + ")" 57 | log.Println("INFO:", msg) 58 | notify.Shout("ForAuth: "+msg, appConfig.Notify) 59 | 60 | auth.StartSession(c) 61 | 62 | c.Redirect(http.StatusFound, "/") 63 | } else { 64 | if username != "" { 65 | msg := "Incorrect login attempt by '" + username + "' with password '" + password + "' logged in from " + c.Request.RemoteAddr + ". Target: " + target + " (" + name + ")" 66 | log.Println("WARNING:", msg) 67 | notify.Shout("ForAuth: "+msg, appConfig.Notify) 68 | } 69 | 70 | guiData.Config = appConfig 71 | 72 | c.HTML(http.StatusOK, "header.html", guiData) 73 | c.HTML(http.StatusOK, "login.html", guiData) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/ForAuth/b120d7c58e342cd86833ac260922f0a436797ffe/internal/web/public/favicon.png -------------------------------------------------------------------------------- /internal/web/public/version: -------------------------------------------------------------------------------- 1 | VERSION=0.1.4 -------------------------------------------------------------------------------- /internal/web/templates/config.html: -------------------------------------------------------------------------------- 1 | {{ define "config.html" }} 2 | 3 | 4 |
5 |
6 |
7 |
8 |
Auth
9 |
10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 |
Enable 15 |
16 | {{ if .Auth.Auth }} 17 | 18 | {{ else }} 19 | 20 | {{ end }} 21 |
22 |
Expire after
Login
New password
39 | 40 |
44 |
45 |
46 |
47 |
Config
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
Host
Proxy Port
Config Port
Target (host:port)
Theme
Color mode
Node path
Notify URL 91 | 92 |
100 |
101 |
102 |
103 | 104 |
105 |

106 | 107 | Warning - changing any Host, Port or Target requires app restart

108 |

● Expire after - session expiration time. A number and suffix: m, h, d or M. Example: 7d

109 |

● Proxy Port - the Target app can be reached on this port after login. Default 8800

110 |

● Config Port - config GUI can be reached on this port. Default 8801

111 |

● Target - where to proxy after login (host:port). Example: 192.168.1.1:8840

112 |

● Node path - path to local Fonts and Themes (node-bootstrap)

113 |

● Notify URL - provides notifications to Discord, Email, Gotify, Telegram and other services. Link to documentation

114 |

If you find this app useful, please, donate

115 |
116 |
117 |
118 |
119 |
120 |
Multi Target
121 |
122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {{ range $key, $value := .TargetMap }} 133 | 134 | 135 | 136 | 137 | 141 | 142 | {{ end }} 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 |
NameProxy (host:port)Target (host:port)Del
{{ $value.Name }}{{ $key }}{{ $value.Target }} 138 | 139 | 140 |
153 |
154 |
155 |
156 |
Current Sessions
157 |
158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | {{ range $key, $value := .Sessions }} 169 | 170 | 171 | 172 | 173 | 177 | 178 | {{ end }} 179 | 180 |
UserHostExpiresDel
{{ $value.User }}{{ $value.Host }}{{ $value.TimeStr }} 174 | 175 | 176 |
181 |
182 |
183 |
184 |
185 | 186 | 187 | {{ template "footer.html" }} 188 | {{ end }} -------------------------------------------------------------------------------- /internal/web/templates/footer.html: -------------------------------------------------------------------------------- 1 | {{ define "footer.html"}} 2 | 3 | 4 | {{ end }} -------------------------------------------------------------------------------- /internal/web/templates/header.html: -------------------------------------------------------------------------------- 1 | {{ define "header.html"}} 2 | 3 | 4 | 5 | 6 | ForAuth 7 | 8 | 9 | 10 | 11 | {{ if eq .Config.NodePath "" }} 12 | 13 | 14 | {{ else }} 15 | 16 | {{ end }} 17 | 18 | 19 | 25 | {{ end }} -------------------------------------------------------------------------------- /internal/web/templates/login.html: -------------------------------------------------------------------------------- 1 | {{ define "login.html" }} 2 | 3 | 4 |
5 |
6 |
7 |
8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 18 | {{ template "footer.html" }} 19 | {{ end }} 20 | -------------------------------------------------------------------------------- /internal/web/webgui.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | "github.com/aceberg/ForAuth/internal/check" 11 | "github.com/aceberg/ForAuth/internal/conf" 12 | "github.com/aceberg/ForAuth/internal/yaml" 13 | ) 14 | 15 | var templ *template.Template 16 | 17 | // Gui - start web server 18 | func Gui(dirPath, nodePath string) { 19 | 20 | confPath := dirPath + "/config.yaml" 21 | check.Path(confPath) 22 | 23 | appConfig, authConf = conf.Get(confPath) 24 | appConfig.DirPath = dirPath 25 | appConfig.ConfPath = confPath 26 | if nodePath != "" { 27 | appConfig.NodePath = nodePath 28 | } 29 | 30 | appConfig.YamlPath = dirPath + "/targets.yaml" 31 | check.Path(appConfig.YamlPath) 32 | 33 | log.Println("INFO: starting web gui with config", appConfig.ConfPath) 34 | 35 | addressConf := appConfig.Host + ":" + appConfig.PortConf 36 | 37 | log.Println("=================================== ") 38 | log.Println("Config at http://" + addressConf) 39 | log.Println("=================================== ") 40 | 41 | gin.SetMode(gin.ReleaseMode) 42 | routerConf := gin.New() 43 | 44 | templ = template.Must(template.New("").ParseFS(templFS, "templates/*")) 45 | 46 | routerConf.SetHTMLTemplate(templ) // templates 47 | routerConf.StaticFS("/fs/", http.FS(pubFS)) // public 48 | 49 | routerConf.GET("/", configHandler) // config.go 50 | routerConf.GET("/logout", logoutHandler) // config.go 51 | routerConf.GET("/target/del", delTargetHandler) // config.go 52 | routerConf.GET("/session", sessionDelHandler) // config.go 53 | routerConf.POST("/", configHandler) // config.go 54 | routerConf.POST("/config/", saveConfigHandler) // config.go 55 | routerConf.POST("/config/auth", saveConfigAuth) // config.go 56 | routerConf.POST("/target/add", addTargetHandler) // config.go 57 | 58 | if appConfig.Port != "" { 59 | proxy := appConfig.Host + ":" + appConfig.Port 60 | go newRouter(proxy, appConfig.Target, "Default") 61 | } 62 | 63 | targetMap = yaml.Read(appConfig.YamlPath) 64 | for proxy, target := range targetMap { 65 | go newRouter(proxy, target.Target, target.Name) 66 | } 67 | 68 | err := routerConf.Run(addressConf) 69 | check.IfError(err) 70 | } 71 | 72 | func newRouter(proxy, target, name string) { 73 | 74 | routerProxy := gin.New() 75 | routerProxy.SetHTMLTemplate(templ) // templates 76 | 77 | // Middleware to add variable to context 78 | routerProxy.Use(func(c *gin.Context) { 79 | c.Set("proxyAddr", proxy) 80 | c.Next() 81 | }) 82 | 83 | routerProxy.GET("/*any", loginHandler) // login.go 84 | routerProxy.POST("/*any", loginHandler) // login.go 85 | routerProxy.PUT("/*any", loginHandler) // login.go 86 | routerProxy.DELETE("/*any", loginHandler) // login.go 87 | routerProxy.PATCH("/*any", loginHandler) // login.go 88 | routerProxy.HEAD("/*any", loginHandler) // login.go 89 | routerProxy.OPTIONS("/*any", loginHandler) // login.go 90 | 91 | log.Println("Proxy at http://"+proxy, "=> http://"+target, "("+name+")") 92 | 93 | err := routerProxy.Run(proxy) 94 | check.IfError(err) 95 | } 96 | -------------------------------------------------------------------------------- /internal/yaml/readwrite.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "gopkg.in/yaml.v3" 8 | 9 | "github.com/aceberg/ForAuth/internal/check" 10 | "github.com/aceberg/ForAuth/internal/models" 11 | ) 12 | 13 | // Read - read .yaml file to struct 14 | func Read(path string) map[string]models.TargetStruct { 15 | 16 | file, err := os.ReadFile(path) 17 | check.IfError(err) 18 | 19 | items := make(map[string]models.TargetStruct) 20 | err = yaml.Unmarshal(file, &items) 21 | check.IfError(err) 22 | 23 | return items 24 | } 25 | 26 | // Write - write struct to .yaml file 27 | func Write(path string, items map[string]models.TargetStruct) { 28 | 29 | yamlData, err := yaml.Marshal(&items) 30 | check.IfError(err) 31 | 32 | err = os.WriteFile(path, yamlData, 0644) 33 | check.IfError(err) 34 | 35 | log.Println("INFO: writing new tagrets to", path) 36 | } 37 | --------------------------------------------------------------------------------