├── .env ├── .github └── workflows │ ├── dependabot.yml │ └── dockerimage.yml ├── .gitignore ├── .gitlab-ci.yml ├── Dockerfile ├── LICENSE-WTFPL ├── README.md ├── api ├── api.go └── v1 │ ├── auth │ └── auth.go │ ├── client │ └── client.go │ ├── server │ └── server.go │ ├── status │ └── status.go │ └── v1.go ├── auth ├── auth.go ├── fake │ └── fake.go ├── github │ └── github.go ├── google │ └── goolge.go └── oauth2oidc │ └── oauth2oidc.go ├── cmd └── wg-gen-web │ └── main.go ├── core ├── client.go ├── server.go └── status.go ├── dev.dockerfile ├── go.mod ├── go.sum ├── model ├── auth.go ├── client.go ├── server.go ├── status.go └── user.go ├── storage └── file.go ├── template └── template.go ├── ui ├── .browserslistrc ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.png │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── Clients.vue │ │ ├── Footer.vue │ │ ├── Header.vue │ │ ├── Notification.vue │ │ ├── Server.vue │ │ └── Status.vue │ ├── main.js │ ├── plugins │ │ ├── axios.js │ │ ├── cidr.js │ │ ├── moment.js │ │ └── vuetify.js │ ├── router │ │ └── index.js │ ├── services │ │ ├── api.service.js │ │ └── token.service.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── auth.js │ │ │ ├── client.js │ │ │ ├── server.js │ │ │ └── status.js │ └── views │ │ ├── Clients.vue │ │ ├── Server.vue │ │ └── Status.vue └── vue.config.js ├── util └── util.go ├── version └── version.go ├── wg-gen-web_cover.png └── wg-gen-web_screenshot.png /.env: -------------------------------------------------------------------------------- 1 | # IP address to listen to 2 | SERVER=0.0.0.0 3 | # port to bind 4 | PORT=8080 5 | # Gin framework release mode 6 | GIN_MODE=release 7 | # where to write all generated config files 8 | WG_CONF_DIR=./wireguard 9 | # WireGuard main config file name, generally .conf 10 | WG_INTERFACE_NAME=wg0.conf 11 | 12 | # SMTP settings to send email to clients 13 | SMTP_HOST=smtp.gmail.com 14 | SMTP_PORT=587 15 | SMTP_USERNAME=account@gmail.com 16 | SMTP_PASSWORD=************* 17 | SMTP_FROM=Wg Gen Web 18 | 19 | # example with gitlab, which is RFC implementation and no need any custom stuff 20 | OAUTH2_PROVIDER_NAME=oauth2oidc 21 | OAUTH2_PROVIDER=https://gitlab.com 22 | OAUTH2_CLIENT_ID= 23 | OAUTH2_CLIENT_SECRET= 24 | OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr 25 | 26 | # example with google 27 | OAUTH2_PROVIDER_NAME=google 28 | OAUTH2_PROVIDER= 29 | OAUTH2_CLIENT_ID= 30 | OAUTH2_CLIENT_SECRET= 31 | OAUTH2_REDIRECT_URL= 32 | 33 | # example with github 34 | OAUTH2_PROVIDER_NAME=github 35 | OAUTH2_PROVIDER=https://github.com 36 | OAUTH2_CLIENT_ID= 37 | OAUTH2_CLIENT_SECRET= 38 | OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr 39 | 40 | # set provider name to fake to disable auth, also the default 41 | OAUTH2_PROVIDER_NAME=fake 42 | 43 | # https://github.com/jamescun/wg-api integration, user and password (basic auth) are optional 44 | WG_STATS_API= 45 | WG_STATS_API_USER= 46 | WG_STATS_API_PASS= -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/dockerimage.yml: -------------------------------------------------------------------------------- 1 | name: Build multi-platform docker images via buildx 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | tags: 11 | - 'v*.*.*' 12 | 13 | jobs: 14 | docker: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - 18 | name: Checkout 19 | uses: actions/checkout@v2 20 | - 21 | name: Set Prepare 22 | id: prep 23 | run: | 24 | DOCKER_IMAGE=vx3r/wg-gen-web 25 | VERSION=edge 26 | if [[ $GITHUB_REF == refs/tags/* ]]; then 27 | VERSION=${GITHUB_REF#refs/tags/} 28 | elif [[ $GITHUB_REF == refs/heads/* ]]; then 29 | VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g') 30 | elif [[ $GITHUB_REF == refs/pull/* ]]; then 31 | VERSION=pr-${{ github.event.number }} 32 | fi 33 | TAGS="${DOCKER_IMAGE}:${VERSION}" 34 | if [ "${{ github.event_name }}" = "push" ]; then 35 | TAGS="$TAGS,${DOCKER_IMAGE}:sha-${GITHUB_SHA::8}" 36 | fi 37 | TAGS="$TAGS,${DOCKER_IMAGE}:latest" 38 | echo ::set-output name=version::${VERSION} 39 | echo ::set-output name=tags::${TAGS} 40 | echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') 41 | echo ::set-output name=sha_short::$(git rev-parse --short HEAD) 42 | - 43 | name: Set up QEMU 44 | uses: docker/setup-qemu-action@v1 45 | - 46 | name: Set up Docker Buildx 47 | uses: docker/setup-buildx-action@v1 48 | - 49 | name: Login to DockerHub 50 | uses: docker/login-action@v1 51 | with: 52 | username: ${{ secrets.DOCKER_LOGIN_USERNAME }} 53 | password: ${{ secrets.DOCKER_LOGIN_PASSWORD }} 54 | - 55 | name: Build and push 56 | uses: docker/build-push-action@v2 57 | with: 58 | context: . 59 | platforms: linux/amd64,linux/arm64,linux/arm/v7 60 | push: ${{ github.event_name != 'pull_request' }} 61 | tags: ${{ steps.prep.outputs.tags }} 62 | labels: | 63 | org.opencontainers.image.source=${{ github.event.repository.html_url }} 64 | org.opencontainers.image.created=${{ steps.prep.outputs.created }} 65 | org.opencontainers.image.revision=${{ github.sha }} 66 | build-args: | 67 | COMMIT=${{ steps.prep.outputs.sha_short }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Vendor 2 | vendor/* 3 | 4 | *.exe 5 | 6 | .idea/* 7 | 8 | wireguard 9 | 10 | ui/.idea/* 11 | ui/dist/* 12 | 13 | .DS_Store 14 | ui/node_modules 15 | /dist 16 | 17 | # local env files 18 | ui/.env.local 19 | ui/.env.*.local 20 | 21 | .idea 22 | 23 | # Log files 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Editor directories and files 29 | .vscode 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | *.sw? 35 | .history -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build artifacts 3 | - build docker image 4 | - push docker hub 5 | 6 | build-back: 7 | stage: build artifacts 8 | image: golang:latest 9 | script: 10 | - GOOS=linux GOARCH=amd64 go build -o ${CI_PROJECT_NAME}-linux-amd64 -ldflags="-X 'github.com/vx3r/wg-gen-web/version.Version=${CI_COMMIT_SHA}'" github.com/vx3r/wg-gen-web/cmd/wg-gen-web 11 | artifacts: 12 | paths: 13 | - ${CI_PROJECT_NAME}-linux-amd64 14 | 15 | build-front: 16 | stage: build artifacts 17 | image: node:10-alpine 18 | script: 19 | - cd ./ui 20 | - npm install 21 | - npm run build 22 | - cd .. 23 | artifacts: 24 | paths: 25 | - ui/dist 26 | 27 | build: 28 | stage: build docker image 29 | image: docker:latest 30 | script: 31 | - docker info 32 | - docker build --build-arg COMMIT=${CI_COMMIT_SHA} --network br_docker --tag ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} . 33 | 34 | push: 35 | stage: push docker hub 36 | image: docker:latest 37 | only: 38 | - master 39 | script: 40 | - echo ${REGISTRY_PASSWORD} | docker login -u ${CI_REGISTRY_USER} --password-stdin ${CI_REGISTRY} 41 | - docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG COMMIT="N/A" 2 | 3 | FROM golang:alpine AS build-back 4 | WORKDIR /app 5 | ARG COMMIT 6 | COPY . . 7 | RUN go build -o wg-gen-web-linux -ldflags="-X 'github.com/vx3r/wg-gen-web/version.Version=${COMMIT}'" github.com/vx3r/wg-gen-web/cmd/wg-gen-web 8 | 9 | FROM node:18.13.0-alpine AS build-front 10 | WORKDIR /app 11 | COPY ui/package*.json ./ 12 | RUN npm install 13 | COPY ui/ ./ 14 | RUN npm run build 15 | 16 | FROM alpine 17 | WORKDIR /app 18 | COPY --from=build-back /app/wg-gen-web-linux . 19 | COPY --from=build-front /app/dist ./ui/dist 20 | COPY .env . 21 | RUN chmod +x ./wg-gen-web-linux 22 | RUN apk add --no-cache ca-certificates 23 | EXPOSE 8080 24 | 25 | CMD ["/app/wg-gen-web-linux"] 26 | -------------------------------------------------------------------------------- /LICENSE-WTFPL: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2013 Stephen Mathieson 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wg Gen Web 2 | 3 |

Simple Web based configuration generator for WireGuard

4 | 5 | Simple Web based configuration generator for [WireGuard](https://wireguard.com). 6 | 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/vx3r/wg-gen-web)](https://goreportcard.com/report/github.com/vx3r/wg-gen-web) 8 | [![License: WTFPL](https://img.shields.io/badge/License-WTFPL-brightgreen.svg)](http://www.wtfpl.net/about/) 9 | ![Discord](https://img.shields.io/discord/681699554189377567) 10 | ![Build docker images via buildx](https://github.com/vx3r/wg-gen-web/actions/workflows/dockerimage.yml/badge.svg) 11 | ![GitHub last commit](https://img.shields.io/github/last-commit/vx3r/wg-gen-web) 12 | ![Docker Pulls](https://img.shields.io/docker/pulls/vx3r/wg-gen-web) 13 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/vx3r/wg-gen-web) 14 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/vx3r/wg-gen-web) 15 | 16 | ## Why another one ? 17 | 18 | All WireGuard UI implementations are trying to manage the service by applying configurations and creating network rules. 19 | This implementation only generates configuration and its up to you to create network rules and apply configuration to WireGuard. 20 | For example by monitoring generated directory with [inotifywait](https://github.com/inotify-tools/inotify-tools/wiki). 21 | 22 | The goal is to run Wg Gen Web in a container and WireGuard on host system. 23 | 24 | ## Features 25 | 26 | * Self-hosted and web based 27 | * Automatically select IP from the network pool assigned to client 28 | * QR-Code for convenient mobile client configuration 29 | * Sent email to client with QR-code and client config 30 | * Enable / Disable client 31 | * Generation of `wg0.conf` after any modification 32 | * IPv6 ready 33 | * User authentication (Oauth2 OIDC) 34 | * Dockerized 35 | * Pretty cool look 36 | 37 | ![Screenshot](wg-gen-web_screenshot.png) 38 | 39 | ## Running 40 | 41 | ### Docker 42 | 43 | The easiest way to run Wg Gen Web is using the container image 44 | ``` 45 | docker run --rm -it -v /tmp/wireguard:/data -p 8080:8080 -e "WG_CONF_DIR=/data" vx3r/wg-gen-web:latest 46 | ``` 47 | Docker compose snippet, used for demo server, wg-json-api service is optional 48 | ``` 49 | version: '3.6' 50 | services: 51 | wg-gen-web-demo: 52 | image: vx3r/wg-gen-web:latest 53 | container_name: wg-gen-web-demo 54 | restart: unless-stopped 55 | expose: 56 | - "8080/tcp" 57 | environment: 58 | - WG_CONF_DIR=/data 59 | - WG_INTERFACE_NAME=wg0.conf 60 | - SMTP_HOST=smtp.gmail.com 61 | - SMTP_PORT=587 62 | - SMTP_USERNAME=no-reply@gmail.com 63 | - SMTP_PASSWORD=****************** 64 | - SMTP_FROM=Wg Gen Web 65 | - OAUTH2_PROVIDER_NAME=github 66 | - OAUTH2_PROVIDER=https://github.com 67 | - OAUTH2_CLIENT_ID=****************** 68 | - OAUTH2_CLIENT_SECRET=****************** 69 | - OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr 70 | volumes: 71 | - /etc/wireguard:/data 72 | wg-json-api: 73 | image: james/wg-api:latest 74 | container_name: wg-json-api 75 | restart: unless-stopped 76 | cap_add: 77 | - NET_ADMIN 78 | network_mode: "host" 79 | command: wg-api --device wg0 --listen :8182 80 | ``` 81 | Please note that mapping ```/etc/wireguard``` to ```/data``` inside the docker, will erase your host's current configuration. 82 | If needed, please make sure to backup your files from ```/etc/wireguard```. 83 | 84 | A workaround would be to change the ```WG_INTERFACE_NAME``` to something different, as it will create a new interface (```wg-auto.conf``` for example), note that if you do so, you will have to adapt your daemon accordingly. 85 | 86 | To get the value for **** take a look at the [WireGuard Status Display](#wireguard-status-display) section. If the status display should be disabled, remove the whole service from the docker-compose file or 87 | use 127.0.0.1 as . 88 | 89 | ### Directly without docker 90 | 91 | Fill free to download latest artifacts from my GitLab server: 92 | * [Backend](https://github.com/vx3r/wg-gen-web/-/jobs/artifacts/master/download?job=build-back) 93 | * [Frontend](https://github.com/vx3r/wg-gen-web/-/jobs/artifacts/master/download?job=build-front) 94 | 95 | Put everything in one directory, create `.env` file with all configurations and run the backend. 96 | 97 | ## Automatically apply changes to WireGuard 98 | 99 | ### Using ```systemd``` 100 | Using `systemd.path` monitor for directory changes see [systemd doc](https://www.freedesktop.org/software/systemd/man/systemd.path.html) 101 | ``` 102 | # /etc/systemd/system/wg-gen-web.path 103 | [Unit] 104 | Description=Watch /etc/wireguard for changes 105 | 106 | [Path] 107 | PathModified=/etc/wireguard 108 | 109 | [Install] 110 | WantedBy=multi-user.target 111 | ``` 112 | This `.path` will activate unit file with the same name 113 | ``` 114 | # /etc/systemd/system/wg-gen-web.service 115 | [Unit] 116 | Description=Reload WireGuard 117 | After=network.target 118 | 119 | [Service] 120 | Type=oneshot 121 | ExecStart=/usr/bin/systemctl reload wg-quick@wg0.service 122 | 123 | [Install] 124 | WantedBy=multi-user.target 125 | ``` 126 | Which will reload WireGuard service 127 | 128 | ### Using ```inotifywait``` 129 | For any other init system, create a daemon running this script 130 | ``` 131 | #!/bin/sh 132 | while inotifywait -e modify -e create /etc/wireguard; do 133 | wg-quick down wg0 134 | wg-quick up wg0 135 | done 136 | ``` 137 | 138 | ## How to use with existing WireGuard configuration 139 | 140 | After first run Wg Gen Web will create `server.json` in data directory with all server informations. 141 | 142 | Feel free to modify this file in order to use your existing keys 143 | 144 | ## What is out of scope 145 | 146 | * Generation or application of any `iptables` or `nftables` rules 147 | * Application of configuration to WireGuard by Wg Gen Web itself 148 | 149 | ## Authentication 150 | 151 | Wg Gen Web can use Oauth2 OpenID Connect provider to authenticate users. 152 | Currently there are 4 implementations: 153 | - `fake` not a real implementation, use this if you don't want to authenticate your clients. 154 | 155 | Add the environment variable: 156 | 157 | ``` 158 | OAUTH2_PROVIDER_NAME=fake 159 | ``` 160 | 161 | - `github` in order to use GitHub as Oauth2 provider. 162 | 163 | Add the environment variable: 164 | 165 | ``` 166 | OAUTH2_PROVIDER_NAME=github 167 | OAUTH2_PROVIDER=https://github.com 168 | OAUTH2_CLIENT_ID=******************** 169 | OAUTH2_CLIENT_SECRET=******************** 170 | OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr 171 | ``` 172 | 173 | - `google` in order to use Google as Oauth2 provider. Not yet implemented 174 | ``` 175 | help wanted 176 | ``` 177 | 178 | - `oauth2oidc` in order to use RFC compliant Oauth2 OpenId Connect provider. 179 | 180 | Add the environment variable: 181 | 182 | ``` 183 | OAUTH2_PROVIDER_NAME=oauth2oidc 184 | OAUTH2_PROVIDER=https://gitlab.com 185 | OAUTH2_CLIENT_ID=******************** 186 | OAUTH2_CLIENT_SECRET=******************** 187 | OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr 188 | ``` 189 | 190 | Wg Gen Web will only access your profile to get email address and your name, no other unnecessary scopes will be requested. 191 | 192 | ## WireGuard Status Display 193 | Wg Gen Web integrates a [WireGuard API implementation](https://github.com/jamescun/wg-api) to display client stats. 194 | In order to enable the Status API integration, the following settings need to be configured: 195 | ``` 196 | # https://github.com/jamescun/wg-api integration 197 | WG_STATS_API=http://:8182 198 | 199 | # Optional: Token Auth 200 | WG_STATS_API_TOKEN= 201 | 202 | # Optional: Basic Auth 203 | WG_STATS_API_USER= 204 | WG_STATS_API_PASS= 205 | ``` 206 | 207 | To setup the WireGuard API take a look at [https://github.com/jamescun/wg-api/blob/master/README.md](https://github.com/jamescun/wg-api/blob/master/README.md), or simply use the provided docker-compose file from above. 208 | 209 | ### API_LISTEN_IP 210 | Due to the fact that the wg-api container operates on the host network, the wg-gen-web container cannot directly talk to the API. Thus the docker-host gateway IP of the wg-gen-web container has to be used. If the default bridge network (docker0) is used, this IP should be `172.17.0.1`. If a custom network is used, you can find the gateway IP by inspecting the output of: 211 | ``` 212 | docker network inspect 213 | ``` 214 | Use the IP address found for **Gateway** as the **API_LISTEN_IP**. 215 | 216 | Please feel free to test and report any bugs. 217 | 218 | ## Need Help 219 | 220 | * Join us on [Discord](https://discord.gg/fjx7gGJ) 221 | * Create an issue 222 | 223 | ## Development 224 | 225 | ### Backend 226 | 227 | From the top level directory run 228 | 229 | ``` 230 | $ go run main.go 231 | ``` 232 | 233 | ### Frontend 234 | 235 | Inside another terminal session navigate into the `ui` folder 236 | 237 | ``` 238 | $ cd ui 239 | ``` 240 | Install required dependencies 241 | ``` 242 | $ npm install 243 | ``` 244 | Set the base url for the api 245 | ``` 246 | $ export VUE_APP_API_BASE_URL=http://localhost:8080/api/v1.0 247 | ``` 248 | Start the development server. It will rebuild and reload the site once you make a change to the source code. 249 | ``` 250 | $ npm run serve 251 | ``` 252 | 253 | Now you can access the site from a webbrowser with the url `http://localhost:8081`. 254 | 255 | ## Application stack 256 | 257 | * [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin) 258 | * [go-template, data-driven templates for generating textual output](https://golang.org/pkg/text/template/) 259 | * [Vue.js, progressive javaScript framework](https://github.com/vuejs/vue) 260 | * [Vuetify, material design component framework](https://github.com/vuetifyjs/vuetify) 261 | 262 | ## License 263 | 264 | * Do What the Fuck You Want to Public License. [LICENSE-WTFPL](LICENSE-WTFPL) or http://www.wtfpl.net 265 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/vx3r/wg-gen-web/api/v1" 6 | ) 7 | 8 | // ApplyRoutes apply routes to gin engine 9 | func ApplyRoutes(r *gin.Engine, private bool) { 10 | api := r.Group("/api") 11 | { 12 | apiv1.ApplyRoutes(api, private) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/v1/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/patrickmn/go-cache" 6 | log "github.com/sirupsen/logrus" 7 | "github.com/vx3r/wg-gen-web/auth" 8 | "github.com/vx3r/wg-gen-web/model" 9 | "github.com/vx3r/wg-gen-web/util" 10 | "golang.org/x/oauth2" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | // ApplyRoutes applies router to gin Router 16 | func ApplyRoutes(r *gin.RouterGroup) { 17 | g := r.Group("/auth") 18 | { 19 | g.GET("/oauth2_url", oauth2URL) 20 | g.POST("/oauth2_exchange", oauth2Exchange) 21 | g.GET("/user", user) 22 | g.GET("/logout", logout) 23 | } 24 | } 25 | 26 | /* 27 | * generate redirect url to get OAuth2 code or let client know that OAuth2 is disabled 28 | */ 29 | func oauth2URL(c *gin.Context) { 30 | cacheDb := c.MustGet("cache").(*cache.Cache) 31 | 32 | state, err := util.GenerateRandomString(32) 33 | if err != nil { 34 | log.WithFields(log.Fields{ 35 | "err": err, 36 | }).Error("failed to generate state random string") 37 | c.AbortWithStatus(http.StatusInternalServerError) 38 | } 39 | 40 | clientId, err := util.GenerateRandomString(32) 41 | if err != nil { 42 | log.WithFields(log.Fields{ 43 | "err": err, 44 | }).Error("failed to generate state random string") 45 | c.AbortWithStatus(http.StatusInternalServerError) 46 | } 47 | // save clientId and state so we can retrieve for verification 48 | cacheDb.Set(clientId, state, 5*time.Minute) 49 | 50 | oauth2Client := c.MustGet("oauth2Client").(auth.Auth) 51 | 52 | data := &model.Auth{ 53 | Oauth2: true, 54 | ClientId: clientId, 55 | State: state, 56 | CodeUrl: oauth2Client.CodeUrl(state), 57 | } 58 | 59 | c.JSON(http.StatusOK, data) 60 | } 61 | 62 | /* 63 | * exchange code and get user infos, if OAuth2 is disable just send fake data 64 | */ 65 | func oauth2Exchange(c *gin.Context) { 66 | var loginVals model.Auth 67 | if err := c.ShouldBind(&loginVals); err != nil { 68 | log.WithFields(log.Fields{ 69 | "err": err, 70 | }).Error("code and state fields are missing") 71 | c.AbortWithStatus(http.StatusUnprocessableEntity) 72 | return 73 | } 74 | 75 | cacheDb := c.MustGet("cache").(*cache.Cache) 76 | savedState, exists := cacheDb.Get(loginVals.ClientId) 77 | 78 | if !exists || savedState != loginVals.State { 79 | log.WithFields(log.Fields{ 80 | "state": loginVals.State, 81 | "savedState": savedState, 82 | }).Error("saved state and client provided state mismatch") 83 | c.AbortWithStatus(http.StatusBadRequest) 84 | return 85 | } 86 | oauth2Client := c.MustGet("oauth2Client").(auth.Auth) 87 | 88 | oauth2Token, err := oauth2Client.Exchange(loginVals.Code) 89 | if err != nil { 90 | log.WithFields(log.Fields{ 91 | "err": err, 92 | }).Error("failed to exchange code for token") 93 | c.AbortWithStatus(http.StatusBadRequest) 94 | return 95 | } 96 | 97 | cacheDb.Delete(loginVals.ClientId) 98 | cacheDb.Set(oauth2Token.AccessToken, oauth2Token, cache.DefaultExpiration) 99 | 100 | c.JSON(http.StatusOK, oauth2Token.AccessToken) 101 | } 102 | 103 | func logout(c *gin.Context) { 104 | cacheDb := c.MustGet("cache").(*cache.Cache) 105 | cacheDb.Delete(c.Request.Header.Get(util.AuthTokenHeaderName)) 106 | c.JSON(http.StatusOK, gin.H{}) 107 | } 108 | 109 | func user(c *gin.Context) { 110 | cacheDb := c.MustGet("cache").(*cache.Cache) 111 | oauth2Token, exists := cacheDb.Get(c.Request.Header.Get(util.AuthTokenHeaderName)) 112 | 113 | if exists && oauth2Token.(*oauth2.Token).AccessToken == c.Request.Header.Get(util.AuthTokenHeaderName) { 114 | oauth2Client := c.MustGet("oauth2Client").(auth.Auth) 115 | user, err := oauth2Client.UserInfo(oauth2Token.(*oauth2.Token)) 116 | if err != nil { 117 | log.WithFields(log.Fields{ 118 | "err": err, 119 | }).Error("failed to get user from oauth2 AccessToken") 120 | c.AbortWithStatus(http.StatusBadRequest) 121 | return 122 | } 123 | 124 | c.JSON(http.StatusOK, user) 125 | return 126 | } 127 | 128 | log.WithFields(log.Fields{ 129 | "exists": exists, 130 | util.AuthTokenHeaderName: c.Request.Header.Get(util.AuthTokenHeaderName), 131 | }).Error("oauth2 AccessToken is not recognized") 132 | 133 | c.AbortWithStatus(http.StatusUnauthorized) 134 | } 135 | -------------------------------------------------------------------------------- /api/v1/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/skip2/go-qrcode" 9 | "github.com/vx3r/wg-gen-web/auth" 10 | "github.com/vx3r/wg-gen-web/core" 11 | "github.com/vx3r/wg-gen-web/model" 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | // ApplyRoutes applies router to gin Router 16 | func ApplyRoutes(r *gin.RouterGroup) { 17 | g := r.Group("/client") 18 | { 19 | g.POST("", createClient) 20 | g.GET("/:id", readClient) 21 | g.PATCH("/:id", updateClient) 22 | g.DELETE("/:id", deleteClient) 23 | g.GET("", readClients) 24 | g.GET("/:id/config", configClient) 25 | g.GET("/:id/email", emailClient) 26 | } 27 | } 28 | 29 | func createClient(c *gin.Context) { 30 | var data model.Client 31 | 32 | if err := c.ShouldBindJSON(&data); err != nil { 33 | log.WithFields(log.Fields{ 34 | "err": err, 35 | }).Error("failed to bind") 36 | c.AbortWithStatus(http.StatusUnprocessableEntity) 37 | return 38 | } 39 | 40 | // get creation user from token and add to client infos 41 | oauth2Token := c.MustGet("oauth2Token").(*oauth2.Token) 42 | oauth2Client := c.MustGet("oauth2Client").(auth.Auth) 43 | user, err := oauth2Client.UserInfo(oauth2Token) 44 | if err != nil { 45 | log.WithFields(log.Fields{ 46 | "oauth2Token": oauth2Token, 47 | "err": err, 48 | }).Error("failed to get user with oauth token") 49 | c.AbortWithStatus(http.StatusInternalServerError) 50 | return 51 | } 52 | data.CreatedBy = user.Name 53 | 54 | client, err := core.CreateClient(&data) 55 | if err != nil { 56 | log.WithFields(log.Fields{ 57 | "err": err, 58 | }).Error("failed to create client") 59 | c.AbortWithStatus(http.StatusInternalServerError) 60 | return 61 | } 62 | 63 | c.JSON(http.StatusOK, client) 64 | } 65 | 66 | func readClient(c *gin.Context) { 67 | id := c.Param("id") 68 | 69 | client, err := core.ReadClient(id) 70 | if err != nil { 71 | log.WithFields(log.Fields{ 72 | "err": err, 73 | }).Error("failed to read client") 74 | c.AbortWithStatus(http.StatusInternalServerError) 75 | return 76 | } 77 | 78 | c.JSON(http.StatusOK, client) 79 | } 80 | 81 | func updateClient(c *gin.Context) { 82 | var data model.Client 83 | id := c.Param("id") 84 | 85 | if err := c.ShouldBindJSON(&data); err != nil { 86 | log.WithFields(log.Fields{ 87 | "err": err, 88 | }).Error("failed to bind") 89 | c.AbortWithStatus(http.StatusUnprocessableEntity) 90 | return 91 | } 92 | 93 | // get update user from token and add to client infos 94 | oauth2Token := c.MustGet("oauth2Token").(*oauth2.Token) 95 | oauth2Client := c.MustGet("oauth2Client").(auth.Auth) 96 | user, err := oauth2Client.UserInfo(oauth2Token) 97 | if err != nil { 98 | log.WithFields(log.Fields{ 99 | "oauth2Token": oauth2Token, 100 | "err": err, 101 | }).Error("failed to get user with oauth token") 102 | c.AbortWithStatus(http.StatusInternalServerError) 103 | return 104 | } 105 | data.UpdatedBy = user.Name 106 | 107 | client, err := core.UpdateClient(id, &data) 108 | if err != nil { 109 | log.WithFields(log.Fields{ 110 | "err": err, 111 | }).Error("failed to update client") 112 | c.AbortWithStatus(http.StatusInternalServerError) 113 | return 114 | } 115 | 116 | c.JSON(http.StatusOK, client) 117 | } 118 | 119 | func deleteClient(c *gin.Context) { 120 | id := c.Param("id") 121 | 122 | err := core.DeleteClient(id) 123 | if err != nil { 124 | log.WithFields(log.Fields{ 125 | "err": err, 126 | }).Error("failed to remove client") 127 | c.AbortWithStatus(http.StatusInternalServerError) 128 | return 129 | } 130 | 131 | c.JSON(http.StatusOK, gin.H{}) 132 | } 133 | 134 | func readClients(c *gin.Context) { 135 | clients, err := core.ReadClients() 136 | if err != nil { 137 | log.WithFields(log.Fields{ 138 | "err": err, 139 | }).Error("failed to list clients") 140 | c.AbortWithStatus(http.StatusInternalServerError) 141 | return 142 | } 143 | 144 | c.JSON(http.StatusOK, clients) 145 | } 146 | 147 | func configClient(c *gin.Context) { 148 | configData, err := core.ReadClientConfig(c.Param("id")) 149 | if err != nil { 150 | log.WithFields(log.Fields{ 151 | "err": err, 152 | }).Error("failed to read client config") 153 | c.AbortWithStatus(http.StatusInternalServerError) 154 | return 155 | } 156 | 157 | formatQr := c.DefaultQuery("qrcode", "false") 158 | if formatQr == "false" { 159 | // return config as txt file 160 | c.Header("Content-Disposition", "attachment; filename=wg0.conf") 161 | c.Data(http.StatusOK, "application/config", configData) 162 | return 163 | } 164 | // return config as png qrcode 165 | png, err := qrcode.Encode(string(configData), qrcode.Medium, 250) 166 | if err != nil { 167 | log.WithFields(log.Fields{ 168 | "err": err, 169 | }).Error("failed to create qrcode") 170 | c.AbortWithStatus(http.StatusInternalServerError) 171 | return 172 | } 173 | c.Data(http.StatusOK, "image/png", png) 174 | return 175 | } 176 | 177 | func emailClient(c *gin.Context) { 178 | id := c.Param("id") 179 | 180 | err := core.EmailClient(id) 181 | if err != nil { 182 | log.WithFields(log.Fields{ 183 | "err": err, 184 | }).Error("failed to send email to client") 185 | c.AbortWithStatus(http.StatusInternalServerError) 186 | return 187 | } 188 | 189 | c.JSON(http.StatusOK, gin.H{}) 190 | } 191 | -------------------------------------------------------------------------------- /api/v1/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/vx3r/wg-gen-web/auth" 7 | "github.com/vx3r/wg-gen-web/core" 8 | "github.com/vx3r/wg-gen-web/model" 9 | "github.com/vx3r/wg-gen-web/version" 10 | "golang.org/x/oauth2" 11 | "net/http" 12 | ) 13 | 14 | // ApplyRoutes applies router to gin Router 15 | func ApplyRoutes(r *gin.RouterGroup) { 16 | g := r.Group("/server") 17 | { 18 | g.GET("", readServer) 19 | g.PATCH("", updateServer) 20 | g.GET("/config", configServer) 21 | g.GET("/version", versionStr) 22 | } 23 | } 24 | 25 | func readServer(c *gin.Context) { 26 | client, err := core.ReadServer() 27 | if err != nil { 28 | log.WithFields(log.Fields{ 29 | "err": err, 30 | }).Error("failed to read client") 31 | c.AbortWithStatus(http.StatusInternalServerError) 32 | return 33 | } 34 | 35 | c.JSON(http.StatusOK, client) 36 | } 37 | 38 | func updateServer(c *gin.Context) { 39 | var data model.Server 40 | 41 | if err := c.ShouldBindJSON(&data); err != nil { 42 | log.WithFields(log.Fields{ 43 | "err": err, 44 | }).Error("failed to bind") 45 | c.AbortWithStatus(http.StatusUnprocessableEntity) 46 | return 47 | } 48 | 49 | // get update user from token and add to server infos 50 | oauth2Token := c.MustGet("oauth2Token").(*oauth2.Token) 51 | oauth2Client := c.MustGet("oauth2Client").(auth.Auth) 52 | user, err := oauth2Client.UserInfo(oauth2Token) 53 | if err != nil { 54 | log.WithFields(log.Fields{ 55 | "oauth2Token": oauth2Token, 56 | "err": err, 57 | }).Error("failed to get user with oauth token") 58 | c.AbortWithStatus(http.StatusInternalServerError) 59 | return 60 | } 61 | data.UpdatedBy = user.Name 62 | 63 | server, err := core.UpdateServer(&data) 64 | if err != nil { 65 | log.WithFields(log.Fields{ 66 | "err": err, 67 | }).Error("failed to update client") 68 | c.AbortWithStatus(http.StatusInternalServerError) 69 | return 70 | } 71 | 72 | c.JSON(http.StatusOK, server) 73 | } 74 | 75 | func configServer(c *gin.Context) { 76 | configData, err := core.ReadWgConfigFile() 77 | if err != nil { 78 | log.WithFields(log.Fields{ 79 | "err": err, 80 | }).Error("failed to read wg config file") 81 | c.AbortWithStatus(http.StatusInternalServerError) 82 | return 83 | } 84 | 85 | // return config as txt file 86 | c.Header("Content-Disposition", "attachment; filename=wg0.conf") 87 | c.Data(http.StatusOK, "application/config", configData) 88 | } 89 | 90 | func versionStr(c *gin.Context) { 91 | c.JSON(http.StatusOK, gin.H{ 92 | "version": version.Version, 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /api/v1/status/status.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | 7 | "github.com/gin-gonic/gin" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/vx3r/wg-gen-web/core" 10 | ) 11 | 12 | // ApplyRoutes applies router to gin Router 13 | func ApplyRoutes(r *gin.RouterGroup) { 14 | g := r.Group("/status") 15 | { 16 | g.GET("/enabled", readEnabled) 17 | g.GET("/interface", readInterfaceStatus) 18 | g.GET("/clients", readClientStatus) 19 | } 20 | } 21 | 22 | func readEnabled(c *gin.Context) { 23 | c.JSON(http.StatusOK, os.Getenv("WG_STATS_API") != "") 24 | } 25 | 26 | func readInterfaceStatus(c *gin.Context) { 27 | status, err := core.ReadInterfaceStatus() 28 | if err != nil { 29 | log.WithFields(log.Fields{ 30 | "err": err, 31 | }).Error("failed to read interface status") 32 | c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error()) 33 | return 34 | } 35 | 36 | c.JSON(http.StatusOK, status) 37 | } 38 | 39 | func readClientStatus(c *gin.Context) { 40 | status, err := core.ReadClientStatus() 41 | if err != nil { 42 | log.WithFields(log.Fields{ 43 | "err": err, 44 | }).Error("failed to read client status") 45 | c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error()) 46 | return 47 | } 48 | 49 | c.JSON(http.StatusOK, status) 50 | } 51 | -------------------------------------------------------------------------------- /api/v1/v1.go: -------------------------------------------------------------------------------- 1 | package apiv1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/vx3r/wg-gen-web/api/v1/auth" 6 | "github.com/vx3r/wg-gen-web/api/v1/client" 7 | "github.com/vx3r/wg-gen-web/api/v1/server" 8 | "github.com/vx3r/wg-gen-web/api/v1/status" 9 | ) 10 | 11 | // ApplyRoutes apply routes to gin router 12 | func ApplyRoutes(r *gin.RouterGroup, private bool) { 13 | v1 := r.Group("/v1.0") 14 | { 15 | if private { 16 | client.ApplyRoutes(v1) 17 | server.ApplyRoutes(v1) 18 | status.ApplyRoutes(v1) 19 | } else { 20 | auth.ApplyRoutes(v1) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/vx3r/wg-gen-web/auth/fake" 7 | "github.com/vx3r/wg-gen-web/auth/github" 8 | "github.com/vx3r/wg-gen-web/auth/oauth2oidc" 9 | "github.com/vx3r/wg-gen-web/model" 10 | "golang.org/x/oauth2" 11 | "os" 12 | ) 13 | 14 | // Auth interface to implement as auth provider 15 | type Auth interface { 16 | Setup() error 17 | CodeUrl(state string) string 18 | Exchange(code string) (*oauth2.Token, error) 19 | UserInfo(oauth2Token *oauth2.Token) (*model.User, error) 20 | } 21 | 22 | // GetAuthProvider get an instance of auth provider based on config 23 | func GetAuthProvider() (Auth, error) { 24 | var oauth2Client Auth 25 | var err error 26 | 27 | switch os.Getenv("OAUTH2_PROVIDER_NAME") { 28 | case "fake": 29 | log.Warn("Oauth is set to fake, no actual authentication will be performed") 30 | oauth2Client = &fake.Fake{} 31 | 32 | case "oauth2oidc": 33 | log.Warn("Oauth is set to oauth2oidc, must be RFC implementation on server side") 34 | oauth2Client = &oauth2oidc.Oauth2idc{} 35 | 36 | case "github": 37 | log.Warn("Oauth is set to github, no openid will be used") 38 | oauth2Client = &github.Github{} 39 | 40 | case "google": 41 | return nil, fmt.Errorf("auth provider name %s not yet implemented", os.Getenv("OAUTH2_PROVIDER_NAME")) 42 | default: 43 | return nil, fmt.Errorf("auth provider name %s unknown", os.Getenv("OAUTH2_PROVIDER_NAME")) 44 | } 45 | 46 | err = oauth2Client.Setup() 47 | 48 | return oauth2Client, err 49 | } 50 | -------------------------------------------------------------------------------- /auth/fake/fake.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "github.com/vx3r/wg-gen-web/model" 5 | "github.com/vx3r/wg-gen-web/util" 6 | "golang.org/x/oauth2" 7 | "time" 8 | ) 9 | 10 | // Fake in order to implement interface, struct is required 11 | type Fake struct{} 12 | 13 | // Setup validate provider 14 | func (o *Fake) Setup() error { 15 | return nil 16 | } 17 | 18 | // CodeUrl get url to redirect client for auth 19 | func (o *Fake) CodeUrl(state string) string { 20 | return "_magic_string_fake_auth_no_redirect_" 21 | } 22 | 23 | // Exchange exchange code for Oauth2 token 24 | func (o *Fake) Exchange(code string) (*oauth2.Token, error) { 25 | rand, err := util.GenerateRandomString(32) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return &oauth2.Token{ 31 | AccessToken: rand, 32 | TokenType: "", 33 | RefreshToken: "", 34 | Expiry: time.Time{}, 35 | }, nil 36 | } 37 | 38 | // UserInfo get token user 39 | func (o *Fake) UserInfo(oauth2Token *oauth2.Token) (*model.User, error) { 40 | return &model.User{ 41 | Sub: "unknown", 42 | Name: "Unknown", 43 | Email: "unknown", 44 | Profile: "unknown", 45 | Issuer: "unknown", 46 | IssuedAt: time.Time{}, 47 | }, nil 48 | } 49 | -------------------------------------------------------------------------------- /auth/github/github.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/vx3r/wg-gen-web/model" 8 | "golang.org/x/oauth2" 9 | oauth2Github "golang.org/x/oauth2/github" 10 | "io/ioutil" 11 | "net/http" 12 | "os" 13 | "time" 14 | ) 15 | 16 | // Github in order to implement interface, struct is required 17 | type Github struct{} 18 | 19 | var ( 20 | oauth2Config *oauth2.Config 21 | ) 22 | 23 | // Setup validate provider 24 | func (o *Github) Setup() error { 25 | oauth2Config = &oauth2.Config{ 26 | ClientID: os.Getenv("OAUTH2_CLIENT_ID"), 27 | ClientSecret: os.Getenv("OAUTH2_CLIENT_SECRET"), 28 | RedirectURL: os.Getenv("OAUTH2_REDIRECT_URL"), 29 | Scopes: []string{"user"}, 30 | Endpoint: oauth2Github.Endpoint, 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // CodeUrl get url to redirect client for auth 37 | func (o *Github) CodeUrl(state string) string { 38 | return oauth2Config.AuthCodeURL(state) 39 | } 40 | 41 | // Exchange exchange code for Oauth2 token 42 | func (o *Github) Exchange(code string) (*oauth2.Token, error) { 43 | oauth2Token, err := oauth2Config.Exchange(context.TODO(), code) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return oauth2Token, nil 49 | } 50 | 51 | // UserInfo get token user 52 | func (o *Github) UserInfo(oauth2Token *oauth2.Token) (*model.User, error) { 53 | // https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/ 54 | 55 | // we have the token, lets get user information 56 | req, err := http.NewRequest("GET", "https://api.github.com/user", nil) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | req.Header.Set("Authorization", fmt.Sprintf("token %s", oauth2Token.AccessToken)) 62 | client := &http.Client{} 63 | resp, err := client.Do(req) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | if resp.StatusCode != 200 { 69 | return nil, fmt.Errorf("http status %s expect 200 OK", resp.Status) 70 | } 71 | 72 | bodyBytes, err := ioutil.ReadAll(resp.Body) 73 | if err != nil { 74 | return nil, err 75 | } 76 | defer resp.Body.Close() 77 | 78 | var data map[string]interface{} 79 | err = json.Unmarshal(bodyBytes, &data) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | // get some infos about user 85 | user := &model.User{} 86 | 87 | if val, ok := data["name"]; ok && val != nil { 88 | user.Name = val.(string) 89 | } 90 | if val, ok := data["email"]; ok && val != nil { 91 | user.Email = val.(string) 92 | } 93 | if val, ok := data["html_url"]; ok && val != nil { 94 | user.Profile = val.(string) 95 | } 96 | 97 | // openid specific 98 | user.Sub = "github is not an openid provider" 99 | user.Issuer = "https://github.com" 100 | user.IssuedAt = time.Now().UTC() 101 | 102 | return user, nil 103 | } 104 | -------------------------------------------------------------------------------- /auth/google/goolge.go: -------------------------------------------------------------------------------- 1 | package google 2 | -------------------------------------------------------------------------------- /auth/oauth2oidc/oauth2oidc.go: -------------------------------------------------------------------------------- 1 | package oauth2oidc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/coreos/go-oidc" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/vx3r/wg-gen-web/model" 9 | "golang.org/x/oauth2" 10 | "os" 11 | ) 12 | 13 | // Oauth2idc in order to implement interface, struct is required 14 | type Oauth2idc struct{} 15 | 16 | var ( 17 | oauth2Config *oauth2.Config 18 | oidcProvider *oidc.Provider 19 | oidcIDTokenVerifier *oidc.IDTokenVerifier 20 | ) 21 | 22 | // Setup validate provider 23 | func (o *Oauth2idc) Setup() error { 24 | var err error 25 | 26 | oidcProvider, err = oidc.NewProvider(context.TODO(), os.Getenv("OAUTH2_PROVIDER")) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | oidcIDTokenVerifier = oidcProvider.Verifier(&oidc.Config{ 32 | ClientID: os.Getenv("OAUTH2_CLIENT_ID"), 33 | }) 34 | 35 | oauth2Config = &oauth2.Config{ 36 | ClientID: os.Getenv("OAUTH2_CLIENT_ID"), 37 | ClientSecret: os.Getenv("OAUTH2_CLIENT_SECRET"), 38 | RedirectURL: os.Getenv("OAUTH2_REDIRECT_URL"), 39 | Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, 40 | Endpoint: oidcProvider.Endpoint(), 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // CodeUrl get url to redirect client for auth 47 | func (o *Oauth2idc) CodeUrl(state string) string { 48 | return oauth2Config.AuthCodeURL(state) 49 | } 50 | 51 | // Exchange exchange code for Oauth2 token 52 | func (o *Oauth2idc) Exchange(code string) (*oauth2.Token, error) { 53 | oauth2Token, err := oauth2Config.Exchange(context.TODO(), code) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return oauth2Token, nil 59 | } 60 | 61 | // UserInfo get token user 62 | func (o *Oauth2idc) UserInfo(oauth2Token *oauth2.Token) (*model.User, error) { 63 | rawIDToken, ok := oauth2Token.Extra("id_token").(string) 64 | if !ok { 65 | return nil, fmt.Errorf("no id_token field in oauth2 token") 66 | } 67 | 68 | iDToken, err := oidcIDTokenVerifier.Verify(context.TODO(), rawIDToken) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | userInfo, err := oidcProvider.UserInfo(context.TODO(), oauth2.StaticTokenSource(oauth2Token)) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | // ID Token payload is just JSON 79 | var claims map[string]interface{} 80 | if err := userInfo.Claims(&claims); err != nil { 81 | return nil, fmt.Errorf("failed to get id token claims: %s", err) 82 | } 83 | 84 | // get some infos about user 85 | user := &model.User{} 86 | user.Sub = userInfo.Subject 87 | user.Email = userInfo.Email 88 | user.Profile = userInfo.Profile 89 | 90 | if v, found := claims["name"]; found && v != nil { 91 | user.Name = v.(string) 92 | } else { 93 | log.Error("name not found in user info claims") 94 | } 95 | 96 | user.Issuer = iDToken.Issuer 97 | user.IssuedAt = iDToken.IssuedAt 98 | 99 | return user, nil 100 | } 101 | -------------------------------------------------------------------------------- /cmd/wg-gen-web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/danielkov/gin-helmet" 6 | "github.com/gin-contrib/cors" 7 | "github.com/gin-contrib/static" 8 | "github.com/gin-gonic/gin" 9 | "github.com/joho/godotenv" 10 | "github.com/patrickmn/go-cache" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/vx3r/wg-gen-web/api" 13 | "github.com/vx3r/wg-gen-web/auth" 14 | "github.com/vx3r/wg-gen-web/core" 15 | "github.com/vx3r/wg-gen-web/util" 16 | "github.com/vx3r/wg-gen-web/version" 17 | "golang.org/x/oauth2" 18 | "net/http" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | "time" 23 | ) 24 | 25 | var ( 26 | cacheDb = cache.New(60*time.Minute, 10*time.Minute) 27 | ) 28 | 29 | func init() { 30 | log.SetFormatter(&log.TextFormatter{}) 31 | log.SetOutput(os.Stderr) 32 | log.SetLevel(log.DebugLevel) 33 | } 34 | 35 | func main() { 36 | log.Infof("Starting Wg Gen Web version: %s", version.Version) 37 | 38 | // load .env environment variables 39 | err := godotenv.Load() 40 | if err != nil { 41 | log.WithFields(log.Fields{ 42 | "err": err, 43 | }).Fatal("failed to load .env file") 44 | } 45 | 46 | // check directories or create it 47 | if !util.DirectoryExists(filepath.Join(os.Getenv("WG_CONF_DIR"))) { 48 | err = os.Mkdir(filepath.Join(os.Getenv("WG_CONF_DIR")), 0755) 49 | if err != nil { 50 | log.WithFields(log.Fields{ 51 | "err": err, 52 | "dir": filepath.Join(os.Getenv("WG_CONF_DIR")), 53 | }).Fatal("failed to create directory") 54 | } 55 | } 56 | 57 | // check if server.json exists otherwise create it with default values 58 | if !util.FileExists(filepath.Join(os.Getenv("WG_CONF_DIR"), "server.json")) { 59 | _, err = core.ReadServer() 60 | if err != nil { 61 | log.WithFields(log.Fields{ 62 | "err": err, 63 | }).Fatal("server.json doesnt not exists and can not read it") 64 | } 65 | } 66 | 67 | if os.Getenv("GIN_MODE") == "debug" { 68 | // set gin release debug 69 | gin.SetMode(gin.DebugMode) 70 | } else { 71 | // set gin release mode 72 | gin.SetMode(gin.ReleaseMode) 73 | // disable console color 74 | gin.DisableConsoleColor() 75 | // log level info 76 | log.SetLevel(log.InfoLevel) 77 | } 78 | 79 | // dump wg config file 80 | err = core.UpdateServerConfigWg() 81 | if err != nil { 82 | log.WithFields(log.Fields{ 83 | "err": err, 84 | }).Fatal("failed to dump wg config file") 85 | } 86 | 87 | // creates a gin router with default middleware: logger and recovery (crash-free) middleware 88 | app := gin.Default() 89 | 90 | // cors middleware 91 | config := cors.DefaultConfig() 92 | config.AllowAllOrigins = true 93 | config.AddAllowHeaders("Authorization", util.AuthTokenHeaderName) 94 | app.Use(cors.New(config)) 95 | 96 | // protection middleware 97 | app.Use(helmet.Default()) 98 | 99 | // add cache storage to gin app 100 | app.Use(func(ctx *gin.Context) { 101 | ctx.Set("cache", cacheDb) 102 | ctx.Next() 103 | }) 104 | 105 | // serve static files 106 | app.Use(static.Serve("/", static.LocalFile("./ui/dist", false))) 107 | 108 | // setup Oauth2 client 109 | oauth2Client, err := auth.GetAuthProvider() 110 | if err != nil { 111 | log.WithFields(log.Fields{ 112 | "err": err, 113 | }).Fatal("failed to setup Oauth2") 114 | } 115 | 116 | app.Use(func(ctx *gin.Context) { 117 | ctx.Set("oauth2Client", oauth2Client) 118 | ctx.Next() 119 | }) 120 | 121 | // apply api routes public 122 | api.ApplyRoutes(app, false) 123 | 124 | // simple middleware to check auth 125 | app.Use(func(c *gin.Context) { 126 | cacheDb := c.MustGet("cache").(*cache.Cache) 127 | 128 | token := c.Request.Header.Get(util.AuthTokenHeaderName) 129 | 130 | oauth2Token, exists := cacheDb.Get(token) 131 | if exists && oauth2Token.(*oauth2.Token).AccessToken == token { 132 | // will be accessible in auth endpoints 133 | c.Set("oauth2Token", oauth2Token) 134 | c.Next() 135 | return 136 | } 137 | 138 | // avoid 401 page for refresh after logout 139 | if !strings.Contains(c.Request.URL.Path, "/api/") { 140 | c.Redirect(301, "/index.html") 141 | return 142 | } 143 | 144 | c.AbortWithStatus(http.StatusUnauthorized) 145 | return 146 | }) 147 | 148 | // apply api router private 149 | api.ApplyRoutes(app, true) 150 | 151 | err = app.Run(fmt.Sprintf("%s:%s", os.Getenv("SERVER"), os.Getenv("PORT"))) 152 | if err != nil { 153 | log.WithFields(log.Fields{ 154 | "err": err, 155 | }).Fatal("failed to start server") 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /core/client.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | uuid "github.com/satori/go.uuid" 6 | log "github.com/sirupsen/logrus" 7 | "github.com/skip2/go-qrcode" 8 | "github.com/vx3r/wg-gen-web/model" 9 | "github.com/vx3r/wg-gen-web/storage" 10 | "github.com/vx3r/wg-gen-web/template" 11 | "github.com/vx3r/wg-gen-web/util" 12 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 13 | "gopkg.in/gomail.v2" 14 | "io/ioutil" 15 | "os" 16 | "path/filepath" 17 | "sort" 18 | "strconv" 19 | "time" 20 | ) 21 | 22 | // CreateClient client with all necessary data 23 | func CreateClient(client *model.Client) (*model.Client, error) { 24 | // check if client is valid 25 | errs := client.IsValid() 26 | if len(errs) != 0 { 27 | for _, err := range errs { 28 | log.WithFields(log.Fields{ 29 | "err": err, 30 | }).Error("client validation error") 31 | } 32 | return nil, errors.New("failed to validate client") 33 | } 34 | 35 | u := uuid.NewV4() 36 | client.Id = u.String() 37 | 38 | key, err := wgtypes.GeneratePrivateKey() 39 | if err != nil { 40 | return nil, err 41 | } 42 | client.PrivateKey = key.String() 43 | client.PublicKey = key.PublicKey().String() 44 | 45 | presharedKey, err := wgtypes.GenerateKey() 46 | if err != nil { 47 | return nil, err 48 | } 49 | client.PresharedKey = presharedKey.String() 50 | 51 | reserverIps, err := GetAllReservedIps() 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | ips := make([]string, 0) 57 | for _, network := range client.Address { 58 | ip, err := util.GetAvailableIp(network, reserverIps) 59 | if err != nil { 60 | return nil, err 61 | } 62 | if util.IsIPv6(ip) { 63 | ip = ip + "/128" 64 | } else { 65 | ip = ip + "/32" 66 | } 67 | ips = append(ips, ip) 68 | } 69 | client.Address = ips 70 | client.Created = time.Now().UTC() 71 | client.Updated = client.Created 72 | 73 | err = storage.Serialize(client.Id, client) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | v, err := storage.Deserialize(client.Id) 79 | if err != nil { 80 | return nil, err 81 | } 82 | client = v.(*model.Client) 83 | 84 | // data modified, dump new config 85 | return client, UpdateServerConfigWg() 86 | } 87 | 88 | // ReadClient client by id 89 | func ReadClient(id string) (*model.Client, error) { 90 | v, err := storage.Deserialize(id) 91 | if err != nil { 92 | return nil, err 93 | } 94 | client := v.(*model.Client) 95 | 96 | return client, nil 97 | } 98 | 99 | // UpdateClient preserve keys 100 | func UpdateClient(Id string, client *model.Client) (*model.Client, error) { 101 | v, err := storage.Deserialize(Id) 102 | if err != nil { 103 | return nil, err 104 | } 105 | current := v.(*model.Client) 106 | 107 | if current.Id != client.Id { 108 | return nil, errors.New("records Id mismatch") 109 | } 110 | 111 | // check if client is valid 112 | errs := client.IsValid() 113 | if len(errs) != 0 { 114 | for _, err := range errs { 115 | log.WithFields(log.Fields{ 116 | "err": err, 117 | }).Error("client validation error") 118 | } 119 | return nil, errors.New("failed to validate client") 120 | } 121 | 122 | // keep keys 123 | client.PrivateKey = current.PrivateKey 124 | client.PublicKey = current.PublicKey 125 | client.Updated = time.Now().UTC() 126 | 127 | err = storage.Serialize(client.Id, client) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | v, err = storage.Deserialize(Id) 133 | if err != nil { 134 | return nil, err 135 | } 136 | client = v.(*model.Client) 137 | 138 | // data modified, dump new config 139 | return client, UpdateServerConfigWg() 140 | } 141 | 142 | // DeleteClient from disk 143 | func DeleteClient(id string) error { 144 | path := filepath.Join(os.Getenv("WG_CONF_DIR"), id) 145 | err := os.Remove(path) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | // data modified, dump new config 151 | return UpdateServerConfigWg() 152 | } 153 | 154 | // ReadClients all clients 155 | func ReadClients() ([]*model.Client, error) { 156 | clients := make([]*model.Client, 0) 157 | 158 | files, err := ioutil.ReadDir(filepath.Join(os.Getenv("WG_CONF_DIR"))) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | for _, f := range files { 164 | // clients file name is an uuid 165 | _, err := uuid.FromString(f.Name()) 166 | if err == nil { 167 | c, err := storage.Deserialize(f.Name()) 168 | if err != nil { 169 | log.WithFields(log.Fields{ 170 | "err": err, 171 | "path": f.Name(), 172 | }).Error("failed to deserialize client") 173 | } else { 174 | clients = append(clients, c.(*model.Client)) 175 | } 176 | } 177 | } 178 | 179 | sort.Slice(clients, func(i, j int) bool { 180 | return clients[i].Created.After(clients[j].Created) 181 | }) 182 | 183 | return clients, nil 184 | } 185 | 186 | // ReadClientConfig in wg format 187 | func ReadClientConfig(id string) ([]byte, error) { 188 | client, err := ReadClient(id) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | server, err := ReadServer() 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | configDataWg, err := template.DumpClientWg(client, server) 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | return configDataWg, nil 204 | } 205 | 206 | // EmailClient send email to client 207 | func EmailClient(id string) error { 208 | client, err := ReadClient(id) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | configData, err := ReadClientConfig(id) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | // conf as .conf file 219 | tmpfileCfg, err := ioutil.TempFile("", "wireguard-vpn-*.conf") 220 | if err != nil { 221 | return err 222 | } 223 | if _, err := tmpfileCfg.Write(configData); err != nil { 224 | return err 225 | } 226 | if err := tmpfileCfg.Close(); err != nil { 227 | return err 228 | } 229 | defer os.Remove(tmpfileCfg.Name()) // clean up 230 | 231 | // conf as png image 232 | png, err := qrcode.Encode(string(configData), qrcode.Medium, 280) 233 | if err != nil { 234 | return err 235 | } 236 | tmpfilePng, err := ioutil.TempFile("", "qrcode-*.png") 237 | if err != nil { 238 | return err 239 | } 240 | if _, err := tmpfilePng.Write(png); err != nil { 241 | return err 242 | } 243 | if err := tmpfilePng.Close(); err != nil { 244 | return err 245 | } 246 | defer os.Remove(tmpfilePng.Name()) // clean up 247 | 248 | // get email body 249 | emailBody, err := template.DumpEmail(client, filepath.Base(tmpfilePng.Name())) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | // port to int 255 | port, err := strconv.Atoi(os.Getenv("SMTP_PORT")) 256 | if err != nil { 257 | return err 258 | } 259 | 260 | d := gomail.NewDialer(os.Getenv("SMTP_HOST"), port, os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD")) 261 | s, err := d.Dial() 262 | if err != nil { 263 | return err 264 | } 265 | m := gomail.NewMessage() 266 | 267 | m.SetHeader("From", os.Getenv("SMTP_FROM")) 268 | m.SetAddressHeader("To", client.Email, client.Name) 269 | m.SetHeader("Subject", "WireGuard VPN Configuration") 270 | m.SetBody("text/html", string(emailBody)) 271 | m.Attach(tmpfileCfg.Name()) 272 | m.Embed(tmpfilePng.Name()) 273 | 274 | err = gomail.Send(s, m) 275 | if err != nil { 276 | return err 277 | } 278 | 279 | return nil 280 | } 281 | -------------------------------------------------------------------------------- /core/server.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/vx3r/wg-gen-web/model" 7 | "github.com/vx3r/wg-gen-web/storage" 8 | "github.com/vx3r/wg-gen-web/template" 9 | "github.com/vx3r/wg-gen-web/util" 10 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 11 | "os" 12 | "path/filepath" 13 | "time" 14 | ) 15 | 16 | // ReadServer object, create default one 17 | func ReadServer() (*model.Server, error) { 18 | if !util.FileExists(filepath.Join(os.Getenv("WG_CONF_DIR"), "server.json")) { 19 | server := &model.Server{} 20 | 21 | key, err := wgtypes.GeneratePrivateKey() 22 | if err != nil { 23 | return nil, err 24 | } 25 | server.PrivateKey = key.String() 26 | server.PublicKey = key.PublicKey().String() 27 | 28 | server.Endpoint = "wireguard.example.com:123" 29 | server.ListenPort = 51820 30 | 31 | server.Address = make([]string, 0) 32 | server.Address = append(server.Address, "fd9f:6666::10:6:6:1/64") 33 | server.Address = append(server.Address, "10.6.6.1/24") 34 | 35 | server.Dns = make([]string, 0) 36 | server.Dns = append(server.Dns, "fd9f::10:0:0:2") 37 | server.Dns = append(server.Dns, "10.0.0.2") 38 | 39 | server.AllowedIPs = make([]string, 0) 40 | server.AllowedIPs = append(server.AllowedIPs, "0.0.0.0/0") 41 | server.AllowedIPs = append(server.AllowedIPs, "::/0") 42 | 43 | server.PersistentKeepalive = 16 44 | server.Mtu = 0 45 | server.PreUp = "echo WireGuard PreUp" 46 | server.PostUp = "echo WireGuard PostUp" 47 | server.PreDown = "echo WireGuard PreDown" 48 | server.PostDown = "echo WireGuard PostDown" 49 | server.Created = time.Now().UTC() 50 | server.Updated = server.Created 51 | 52 | err = storage.Serialize("server.json", server) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // server.json was missing, dump wg config after creation 58 | err = UpdateServerConfigWg() 59 | if err != nil { 60 | return nil, err 61 | } 62 | } 63 | 64 | c, err := storage.Deserialize("server.json") 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return c.(*model.Server), nil 70 | } 71 | 72 | // UpdateServer keep private values from existing one 73 | func UpdateServer(server *model.Server) (*model.Server, error) { 74 | current, err := storage.Deserialize("server.json") 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | // check if server is valid 80 | errs := server.IsValid() 81 | if len(errs) != 0 { 82 | for _, err := range errs { 83 | log.WithFields(log.Fields{ 84 | "err": err, 85 | }).Error("server validation error") 86 | } 87 | return nil, errors.New("failed to validate server") 88 | } 89 | 90 | server.PrivateKey = current.(*model.Server).PrivateKey 91 | server.PublicKey = current.(*model.Server).PublicKey 92 | //server.PresharedKey = current.(*model.Server).PresharedKey 93 | server.Updated = time.Now().UTC() 94 | 95 | err = storage.Serialize("server.json", server) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | v, err := storage.Deserialize("server.json") 101 | if err != nil { 102 | return nil, err 103 | } 104 | server = v.(*model.Server) 105 | 106 | return server, UpdateServerConfigWg() 107 | } 108 | 109 | // UpdateServerConfigWg in wg format 110 | func UpdateServerConfigWg() error { 111 | clients, err := ReadClients() 112 | if err != nil { 113 | return err 114 | } 115 | 116 | server, err := ReadServer() 117 | if err != nil { 118 | return err 119 | } 120 | 121 | _, err = template.DumpServerWg(clients, server) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | return nil 127 | } 128 | 129 | // GetAllReservedIps the list of all reserved IPs, client and server 130 | func GetAllReservedIps() ([]string, error) { 131 | clients, err := ReadClients() 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | server, err := ReadServer() 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | reserverIps := make([]string, 0) 142 | 143 | for _, client := range clients { 144 | for _, cidr := range client.Address { 145 | ip, err := util.GetIpFromCidr(cidr) 146 | if err != nil { 147 | log.WithFields(log.Fields{ 148 | "err": err, 149 | "cidr": cidr, 150 | }).Error("failed to ip from cidr") 151 | } else { 152 | reserverIps = append(reserverIps, ip) 153 | } 154 | } 155 | } 156 | 157 | for _, cidr := range server.Address { 158 | ip, err := util.GetIpFromCidr(cidr) 159 | if err != nil { 160 | log.WithFields(log.Fields{ 161 | "err": err, 162 | "cidr": err, 163 | }).Error("failed to ip from cidr") 164 | } else { 165 | reserverIps = append(reserverIps, ip) 166 | } 167 | } 168 | 169 | return reserverIps, nil 170 | } 171 | 172 | // ReadWgConfigFile return content of wireguard config file 173 | func ReadWgConfigFile() ([]byte, error) { 174 | return util.ReadFile(filepath.Join(os.Getenv("WG_CONF_DIR"), os.Getenv("WG_INTERFACE_NAME"))) 175 | } 176 | -------------------------------------------------------------------------------- /core/status.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "sort" 12 | "time" 13 | 14 | "github.com/vx3r/wg-gen-web/model" 15 | ) 16 | 17 | // apiError implements a top-level JSON-RPC error. 18 | type apiError struct { 19 | Code int `json:"code"` 20 | Message string `json:"message"` 21 | 22 | Data interface{} `json:"data,omitempty"` 23 | } 24 | 25 | type apiRequest struct { 26 | Version string `json:"jsonrpc"` 27 | Method string `json:"method"` 28 | Params json.RawMessage `json:"params,omitempty"` 29 | } 30 | 31 | type apiResponse struct { 32 | Version string `json:"jsonrpc"` 33 | Result interface{} `json:"result,omitempty"` 34 | Error *apiError `json:"error,omitempty"` 35 | ID json.RawMessage `json:"id"` 36 | } 37 | 38 | func fetchWireGuardAPI(reqData apiRequest) (*apiResponse, error) { 39 | apiUrl := os.Getenv("WG_STATS_API") 40 | if apiUrl == "" { 41 | return nil, errors.New("Status API integration not configured") 42 | } 43 | 44 | apiClient := http.Client{ 45 | Timeout: time.Second * 2, // Timeout after 2 seconds 46 | } 47 | jsonData, _ := json.Marshal(reqData) 48 | req, err := http.NewRequest(http.MethodPost, apiUrl, bytes.NewBuffer(jsonData)) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | req.Header.Set("User-Agent", "wg-gen-web") 54 | req.Header.Set("Accept", "application/json") 55 | req.Header.Set("Content-Type", "application/json") 56 | req.Header.Set("Cache-Control", "no-cache") 57 | 58 | if os.Getenv("WG_STATS_API_TOKEN") != "" { 59 | req.Header.Set("Authorization", fmt.Sprintf("Token %s", os.Getenv("WG_STATS_API_TOKEN"))) 60 | } else if os.Getenv("WG_STATS_API_USER") != "" { 61 | req.SetBasicAuth(os.Getenv("WG_STATS_API_USER"), os.Getenv("WG_STATS_API_PASS")) 62 | } 63 | 64 | res, getErr := apiClient.Do(req) 65 | if getErr != nil { 66 | return nil, getErr 67 | } 68 | 69 | if res.Body != nil { 70 | defer res.Body.Close() 71 | } 72 | 73 | body, readErr := io.ReadAll(res.Body) 74 | if readErr != nil { 75 | return nil, readErr 76 | } 77 | 78 | response := apiResponse{} 79 | jsonErr := json.Unmarshal(body, &response) 80 | if jsonErr != nil { 81 | return nil, jsonErr 82 | } 83 | 84 | return &response, nil 85 | } 86 | 87 | // ReadInterfaceStatus object, create default one 88 | func ReadInterfaceStatus() (*model.InterfaceStatus, error) { 89 | interfaceStatus := &model.InterfaceStatus{ 90 | Name: "unknown", 91 | DeviceType: "unknown", 92 | ListenPort: 0, 93 | NumberOfPeers: 0, 94 | PublicKey: "", 95 | } 96 | 97 | data, err := fetchWireGuardAPI(apiRequest{ 98 | Version: "2.0", 99 | Method: "GetDeviceInfo", 100 | Params: nil, 101 | }) 102 | if err != nil { 103 | return interfaceStatus, err 104 | } 105 | 106 | resultData := data.Result.(map[string]interface{}) 107 | device := resultData["device"].(map[string]interface{}) 108 | interfaceStatus.Name = device["name"].(string) 109 | interfaceStatus.DeviceType = device["type"].(string) 110 | interfaceStatus.PublicKey = device["public_key"].(string) 111 | interfaceStatus.ListenPort = int(device["listen_port"].(float64)) 112 | interfaceStatus.NumberOfPeers = int(device["num_peers"].(float64)) 113 | 114 | return interfaceStatus, nil 115 | } 116 | 117 | // ReadClientStatus object, create default one, last recent active client is listed first 118 | func ReadClientStatus() ([]*model.ClientStatus, error) { 119 | var clientStatus []*model.ClientStatus 120 | 121 | data, err := fetchWireGuardAPI(apiRequest{ 122 | Version: "2.0", 123 | Method: "ListPeers", 124 | Params: []byte("{}"), 125 | }) 126 | if err != nil { 127 | return clientStatus, err 128 | } 129 | 130 | resultData := data.Result.(map[string]interface{}) 131 | peers := resultData["peers"].([]interface{}) 132 | 133 | clients, err := ReadClients() 134 | withClientDetails := true 135 | if err != nil { 136 | withClientDetails = false 137 | } 138 | 139 | for _, tmpPeer := range peers { 140 | peer := tmpPeer.(map[string]interface{}) 141 | peerHandshake, _ := time.Parse(time.RFC3339Nano, peer["last_handshake"].(string)) 142 | peerIPs := peer["allowed_ips"].([]interface{}) 143 | peerAddresses := make([]string, len(peerIPs)) 144 | for i, peerIP := range peerIPs { 145 | peerAddresses[i] = peerIP.(string) 146 | } 147 | peerHandshakeRelative := time.Since(peerHandshake) 148 | peerActive := peerHandshakeRelative.Minutes() < 3 // TODO: we need a better detection... ping for example? 149 | 150 | newClientStatus := &model.ClientStatus{ 151 | PublicKey: peer["public_key"].(string), 152 | HasPresharedKey: peer["has_preshared_key"].(bool), 153 | ProtocolVersion: int(peer["protocol_version"].(float64)), 154 | Name: "UNKNOWN", 155 | Email: "UNKNOWN", 156 | Connected: peerActive, 157 | AllowedIPs: peerAddresses, 158 | Endpoint: peer["endpoint"].(string), 159 | LastHandshake: peerHandshake, 160 | LastHandshakeRelative: peerHandshakeRelative, 161 | ReceivedBytes: int(peer["receive_bytes"].(float64)), 162 | TransmittedBytes: int(peer["transmit_bytes"].(float64)), 163 | } 164 | 165 | if withClientDetails { 166 | for _, client := range clients { 167 | if client.PublicKey != newClientStatus.PublicKey { 168 | continue 169 | } 170 | 171 | newClientStatus.Name = client.Name 172 | newClientStatus.Email = client.Email 173 | break 174 | } 175 | } 176 | 177 | clientStatus = append(clientStatus, newClientStatus) 178 | } 179 | 180 | sort.Slice(clientStatus, func(i, j int) bool { 181 | return clientStatus[i].LastHandshakeRelative < clientStatus[j].LastHandshakeRelative 182 | }) 183 | 184 | return clientStatus, nil 185 | } 186 | -------------------------------------------------------------------------------- /dev.dockerfile: -------------------------------------------------------------------------------- 1 | ARG COMMIT="N/A" 2 | 3 | FROM golang AS build-back 4 | WORKDIR /app 5 | ARG COMMIT 6 | COPY . . 7 | RUN go build -o wg-gen-web-linux -gcflags="all=-N -l" -ldflags="-X 'github.com/vx3r/wg-gen-web/version.Version=${COMMIT}'" github.com/vx3r/wg-gen-web/cmd/wg-gen-web 8 | RUN go get github.com/go-delve/delve/cmd/dlv 9 | 10 | FROM node:lts AS build-front 11 | WORKDIR /app 12 | COPY ui/package*.json ./ 13 | RUN npm install 14 | COPY ui/ ./ 15 | RUN npm run build 16 | 17 | FROM debian 18 | WORKDIR /app 19 | COPY --from=build-back /app/wg-gen-web-linux . 20 | COPY --from=build-back /go/bin/dlv . 21 | COPY --from=build-front /app/dist ./ui/dist 22 | COPY .env . 23 | RUN chmod +x ./wg-gen-web-linux 24 | RUN apt-get update && apt-get install -y ca-certificates 25 | EXPOSE 8080 26 | 27 | CMD ["/app/dlv", "--listen=:40000", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/app/wg-gen-web-linux"] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vx3r/wg-gen-web 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/coreos/go-oidc v2.2.1+incompatible 7 | github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e 8 | github.com/gin-contrib/cors v1.3.1 9 | github.com/gin-contrib/static v0.0.1 10 | github.com/gin-gonic/gin v1.7.7 11 | github.com/go-playground/validator/v10 v10.10.0 // indirect 12 | github.com/golang/protobuf v1.5.2 // indirect 13 | github.com/joho/godotenv v1.4.0 14 | github.com/json-iterator/go v1.1.12 // indirect 15 | github.com/mattn/go-isatty v0.0.14 // indirect 16 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 17 | github.com/patrickmn/go-cache v2.1.0+incompatible 18 | github.com/pquerna/cachecontrol v0.1.0 // indirect 19 | github.com/satori/go.uuid v1.2.0 20 | github.com/sirupsen/logrus v1.8.1 21 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 22 | github.com/ugorji/go v1.2.6 // indirect 23 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 24 | golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b 25 | google.golang.org/appengine v1.6.7 // indirect 26 | google.golang.org/protobuf v1.27.1 // indirect 27 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 28 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 29 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect 30 | gopkg.in/yaml.v2 v2.4.0 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /model/auth.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Auth structure 4 | type Auth struct { 5 | Oauth2 bool `json:"oauth2"` 6 | ClientId string `json:"clientId"` 7 | Code string `json:"code"` 8 | State string `json:"state"` 9 | CodeUrl string `json:"codeUrl"` 10 | } 11 | -------------------------------------------------------------------------------- /model/client.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vx3r/wg-gen-web/util" 6 | "time" 7 | ) 8 | 9 | // Client structure 10 | type Client struct { 11 | Id string `json:"id"` 12 | Name string `json:"name"` 13 | Email string `json:"email"` 14 | Enable bool `json:"enable"` 15 | IgnorePersistentKeepalive bool `json:"ignorePersistentKeepalive"` 16 | PresharedKey string `json:"presharedKey"` 17 | AllowedIPs []string `json:"allowedIPs"` 18 | Address []string `json:"address"` 19 | Tags []string `json:"tags"` 20 | PrivateKey string `json:"privateKey"` 21 | PublicKey string `json:"publicKey"` 22 | CreatedBy string `json:"createdBy"` 23 | UpdatedBy string `json:"updatedBy"` 24 | Created time.Time `json:"created"` 25 | Updated time.Time `json:"updated"` 26 | } 27 | 28 | // IsValid check if model is valid 29 | func (a Client) IsValid() []error { 30 | errs := make([]error, 0) 31 | 32 | // check if the name empty 33 | if a.Name == "" { 34 | errs = append(errs, fmt.Errorf("name is required")) 35 | } 36 | // check the name field is between 3 to 40 chars 37 | if len(a.Name) < 2 || len(a.Name) > 40 { 38 | errs = append(errs, fmt.Errorf("name field must be between 2-40 chars")) 39 | } 40 | // email is not required, but if provided must match regex 41 | if a.Email != "" { 42 | if !util.RegexpEmail.MatchString(a.Email) { 43 | errs = append(errs, fmt.Errorf("email %s is invalid", a.Email)) 44 | } 45 | } 46 | // check if the allowedIPs empty 47 | if len(a.AllowedIPs) == 0 { 48 | errs = append(errs, fmt.Errorf("allowedIPs field is required")) 49 | } 50 | // check if the allowedIPs are valid 51 | for _, allowedIP := range a.AllowedIPs { 52 | if !util.IsValidCidr(allowedIP) { 53 | errs = append(errs, fmt.Errorf("allowedIP %s is invalid", allowedIP)) 54 | } 55 | } 56 | // check if the address empty 57 | if len(a.Address) == 0 { 58 | errs = append(errs, fmt.Errorf("address field is required")) 59 | } 60 | // check if the address are valid 61 | for _, address := range a.Address { 62 | if !util.IsValidCidr(address) { 63 | errs = append(errs, fmt.Errorf("address %s is invalid", address)) 64 | } 65 | } 66 | 67 | return errs 68 | } 69 | -------------------------------------------------------------------------------- /model/server.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vx3r/wg-gen-web/util" 6 | "time" 7 | ) 8 | 9 | // Server structure 10 | type Server struct { 11 | Address []string `json:"address"` 12 | ListenPort int `json:"listenPort"` 13 | Mtu int `json:"mtu"` 14 | PrivateKey string `json:"privateKey"` 15 | PublicKey string `json:"publicKey"` 16 | Endpoint string `json:"endpoint"` 17 | PersistentKeepalive int `json:"persistentKeepalive"` 18 | Dns []string `json:"dns"` 19 | AllowedIPs []string `json:"allowedips"` 20 | PreUp string `json:"preUp"` 21 | PostUp string `json:"postUp"` 22 | PreDown string `json:"preDown"` 23 | PostDown string `json:"postDown"` 24 | UpdatedBy string `json:"updatedBy"` 25 | Created time.Time `json:"created"` 26 | Updated time.Time `json:"updated"` 27 | } 28 | 29 | // IsValid check if model is valid 30 | func (a Server) IsValid() []error { 31 | errs := make([]error, 0) 32 | 33 | // check if the address empty 34 | if len(a.Address) == 0 { 35 | errs = append(errs, fmt.Errorf("address is required")) 36 | } 37 | // check if the address are valid 38 | for _, address := range a.Address { 39 | if !util.IsValidCidr(address) { 40 | errs = append(errs, fmt.Errorf("address %s is invalid", address)) 41 | } 42 | } 43 | // check if the listenPort is valid 44 | if a.ListenPort < 0 || a.ListenPort > 65535 { 45 | errs = append(errs, fmt.Errorf("listenPort %s is invalid", a.ListenPort)) 46 | } 47 | // check if the endpoint empty 48 | if a.Endpoint == "" { 49 | errs = append(errs, fmt.Errorf("endpoint is required")) 50 | } 51 | // check if the persistentKeepalive is valid 52 | if a.PersistentKeepalive < 0 { 53 | errs = append(errs, fmt.Errorf("persistentKeepalive %d is invalid", a.PersistentKeepalive)) 54 | } 55 | // check if the mtu is valid 56 | if a.Mtu < 0 { 57 | errs = append(errs, fmt.Errorf("MTU %d is invalid", a.PersistentKeepalive)) 58 | } 59 | // check if the address are valid 60 | for _, dns := range a.Dns { 61 | if !util.IsValidIp(dns) { 62 | errs = append(errs, fmt.Errorf("dns %s is invalid", dns)) 63 | } 64 | } 65 | // check if the allowedIPs are valid 66 | for _, allowedIP := range a.AllowedIPs { 67 | if !util.IsValidCidr(allowedIP) { 68 | errs = append(errs, fmt.Errorf("allowedIP %s is invalid", allowedIP)) 69 | } 70 | } 71 | 72 | return errs 73 | } 74 | -------------------------------------------------------------------------------- /model/status.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // ClientStatus structure 10 | type ClientStatus struct { 11 | PublicKey string `json:"publicKey"` 12 | HasPresharedKey bool `json:"hasPresharedKey"` 13 | ProtocolVersion int `json:"protocolVersion"` 14 | Name string `json:"name"` 15 | Email string `json:"email"` 16 | Connected bool `json:"connected"` 17 | AllowedIPs []string `json:"allowedIPs"` 18 | Endpoint string `json:"endpoint"` 19 | LastHandshake time.Time `json:"lastHandshake"` 20 | LastHandshakeRelative time.Duration `json:"lastHandshakeRelative"` 21 | ReceivedBytes int `json:"receivedBytes"` 22 | TransmittedBytes int `json:"transmittedBytes"` 23 | } 24 | 25 | // MarshalJSON structure to json 26 | func (c *ClientStatus) MarshalJSON() ([]byte, error) { 27 | 28 | duration := fmt.Sprintf("%v ago", c.LastHandshakeRelative) 29 | if c.LastHandshakeRelative.Hours() > 5208 { // 24*7*31 = approx one month 30 | duration = "more than a month ago" 31 | } 32 | return json.Marshal(&struct { 33 | PublicKey string `json:"publicKey"` 34 | HasPresharedKey bool `json:"hasPresharedKey"` 35 | ProtocolVersion int `json:"protocolVersion"` 36 | Name string `json:"name"` 37 | Email string `json:"email"` 38 | Connected bool `json:"connected"` 39 | AllowedIPs []string `json:"allowedIPs"` 40 | Endpoint string `json:"endpoint"` 41 | LastHandshake time.Time `json:"lastHandshake"` 42 | LastHandshakeRelative string `json:"lastHandshakeRelative"` 43 | ReceivedBytes int `json:"receivedBytes"` 44 | TransmittedBytes int `json:"transmittedBytes"` 45 | }{ 46 | PublicKey: c.PublicKey, 47 | HasPresharedKey: c.HasPresharedKey, 48 | ProtocolVersion: c.ProtocolVersion, 49 | Name: c.Name, 50 | Email: c.Email, 51 | Connected: c.Connected, 52 | AllowedIPs: c.AllowedIPs, 53 | Endpoint: c.Endpoint, 54 | LastHandshake: c.LastHandshake, 55 | LastHandshakeRelative: duration, 56 | ReceivedBytes: c.ReceivedBytes, 57 | TransmittedBytes: c.TransmittedBytes, 58 | }) 59 | } 60 | 61 | // InterfaceStatus structure 62 | type InterfaceStatus struct { 63 | Name string `json:"name"` 64 | DeviceType string `json:"type"` 65 | ListenPort int `json:"listenPort"` 66 | NumberOfPeers int `json:"numPeers"` 67 | PublicKey string `json:"publicKey"` 68 | } 69 | -------------------------------------------------------------------------------- /model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // User structure 6 | type User struct { 7 | Sub string `json:"sub"` 8 | Name string `json:"name"` 9 | Email string `json:"email"` 10 | Profile string `json:"profile"` 11 | Issuer string `json:"issuer"` 12 | IssuedAt time.Time `json:"issuedAt"` 13 | } 14 | -------------------------------------------------------------------------------- /storage/file.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/vx3r/wg-gen-web/model" 6 | "github.com/vx3r/wg-gen-web/util" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | // Serialize write interface to disk 12 | func Serialize(id string, c interface{}) error { 13 | b, err := json.MarshalIndent(c, "", " ") 14 | if err != nil { 15 | return err 16 | } 17 | 18 | return util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), id), b) 19 | } 20 | 21 | // Deserialize read interface from disk 22 | func Deserialize(id string) (interface{}, error) { 23 | path := filepath.Join(os.Getenv("WG_CONF_DIR"), id) 24 | 25 | data, err := util.ReadFile(path) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | if id == "server.json" { 31 | var s *model.Server 32 | err = json.Unmarshal(data, &s) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return s, nil 37 | } 38 | 39 | // if not the server, must be client 40 | var c *model.Client 41 | err = json.Unmarshal(data, &c) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return c, nil 47 | } 48 | -------------------------------------------------------------------------------- /template/template.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "bytes" 5 | "github.com/vx3r/wg-gen-web/model" 6 | "github.com/vx3r/wg-gen-web/util" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "text/template" 11 | ) 12 | 13 | var ( 14 | emailTpl = ` 15 | 16 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Email Template 37 | 42 | 43 | 44 | 88 | 89 | 90 | 91 | 92 | 194 | 195 |
93 | 94 | 95 | 191 | 192 |
96 | 97 | 98 | 99 | 100 | 130 | 131 |
101 | 102 | 103 | 127 | 128 |
104 | 105 | 106 | 113 | 114 | 124 | 125 |
107 | 108 | 109 | 110 | 111 |
112 |
115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
Hello
You probably requested VPN configuration. Here is {{.Client.Name}} configuration created {{.Client.Created.Format "Monday, 02 January 06 15:04:05 MST"}}. Scan the Qrcode or open attached configuration file in VPN client.
123 |
126 |
129 |
132 | 133 | 134 | 135 | 136 | 137 | 170 | 171 |
138 | 139 | 140 | 167 | 168 |
141 | 142 | 143 | 164 | 165 |
144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 160 | 161 | 162 |
About WireGuard
WireGuard is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN.
154 | 155 | 156 | 157 | 158 |
Download WireGuard VPN Client
159 |
163 |
166 |
169 |
172 | 173 | 174 | 175 | 176 | 177 | 187 | 188 |
178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 |
Wg Gen Web - Simple Web based configuration generator for WireGuard
More info on Github
186 |
189 | 190 |
193 |
196 | 197 | 198 | ` 199 | 200 | clientTpl = `[Interface] 201 | Address = {{ StringsJoin .Client.Address ", " }} 202 | PrivateKey = {{ .Client.PrivateKey }} 203 | {{ if ne (len .Server.Dns) 0 -}} 204 | DNS = {{ StringsJoin .Server.Dns ", " }} 205 | {{- end }} 206 | {{ if ne .Server.Mtu 0 -}} 207 | MTU = {{.Server.Mtu}} 208 | {{- end}} 209 | [Peer] 210 | PublicKey = {{ .Server.PublicKey }} 211 | PresharedKey = {{ .Client.PresharedKey }} 212 | AllowedIPs = {{ StringsJoin .Client.AllowedIPs ", " }} 213 | Endpoint = {{ .Server.Endpoint }} 214 | {{ if and (ne .Server.PersistentKeepalive 0) (not .Client.IgnorePersistentKeepalive) -}} 215 | PersistentKeepalive = {{.Server.PersistentKeepalive}} 216 | {{- end}} 217 | ` 218 | 219 | wgTpl = `# Updated: {{ .Server.Updated }} / Created: {{ .Server.Created }} 220 | [Interface] 221 | {{- range .Server.Address }} 222 | Address = {{ . }} 223 | {{- end }} 224 | ListenPort = {{ .Server.ListenPort }} 225 | PrivateKey = {{ .Server.PrivateKey }} 226 | {{ if ne .Server.Mtu 0 -}} 227 | MTU = {{.Server.Mtu}} 228 | {{- end}} 229 | PreUp = {{ .Server.PreUp }} 230 | PostUp = {{ .Server.PostUp }} 231 | PreDown = {{ .Server.PreDown }} 232 | PostDown = {{ .Server.PostDown }} 233 | {{- range .Clients }} 234 | {{ if .Enable -}} 235 | # {{.Name}} / {{.Email}} / Updated: {{.Updated}} / Created: {{.Created}} 236 | [Peer] 237 | PublicKey = {{ .PublicKey }} 238 | PresharedKey = {{ .PresharedKey }} 239 | AllowedIPs = {{ StringsJoin .Address ", " }} 240 | {{- end }} 241 | {{ end }}` 242 | ) 243 | 244 | // DumpClientWg dump client wg config with go template 245 | func DumpClientWg(client *model.Client, server *model.Server) ([]byte, error) { 246 | t, err := template.New("client").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(clientTpl) 247 | if err != nil { 248 | return nil, err 249 | } 250 | 251 | return dump(t, struct { 252 | Client *model.Client 253 | Server *model.Server 254 | }{ 255 | Client: client, 256 | Server: server, 257 | }) 258 | } 259 | 260 | // DumpServerWg dump server wg config with go template, write it to file and return bytes 261 | func DumpServerWg(clients []*model.Client, server *model.Server) ([]byte, error) { 262 | t, err := template.New("server").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(wgTpl) 263 | if err != nil { 264 | return nil, err 265 | } 266 | 267 | configDataWg, err := dump(t, struct { 268 | Clients []*model.Client 269 | Server *model.Server 270 | }{ 271 | Clients: clients, 272 | Server: server, 273 | }) 274 | if err != nil { 275 | return nil, err 276 | } 277 | 278 | err = util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), os.Getenv("WG_INTERFACE_NAME")), configDataWg) 279 | if err != nil { 280 | return nil, err 281 | } 282 | 283 | return configDataWg, nil 284 | } 285 | 286 | // DumpEmail dump server wg config with go template 287 | func DumpEmail(client *model.Client, qrcodePngName string) ([]byte, error) { 288 | t, err := template.New("email").Parse(emailTpl) 289 | if err != nil { 290 | return nil, err 291 | } 292 | 293 | return dump(t, struct { 294 | Client *model.Client 295 | QrcodePngName string 296 | }{ 297 | Client: client, 298 | QrcodePngName: qrcodePngName, 299 | }) 300 | } 301 | 302 | func dump(tpl *template.Template, data interface{}) ([]byte, error) { 303 | var tplBuff bytes.Buffer 304 | 305 | err := tpl.Execute(&tplBuff, data) 306 | if err != nil { 307 | return nil, err 308 | } 309 | 310 | return tplBuff.Bytes(), nil 311 | } 312 | -------------------------------------------------------------------------------- /ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # ui 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Customize configuration 19 | See [Configuration Reference](https://cli.vuejs.org/config/). 20 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.21.4", 11 | "is-cidr": "^4.0.2", 12 | "moment": "^2.29.4", 13 | "vue": "^2.6.14", 14 | "vue-axios": "^3.4.0", 15 | "vue-moment": "^4.1.0", 16 | "vue-router": "^3.5.3", 17 | "vuetify": "^2.6.2", 18 | "vuex": "^3.6.2" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-router": "~5.0.8", 22 | "@vue/cli-service": "^5.0.8", 23 | "sass": "^1.48.0", 24 | "sass-loader": "^10.2.1", 25 | "vue-cli-plugin-vuetify": "^2.4.5", 26 | "vue-template-compiler": "^2.6.14", 27 | "vuetify-loader": "^1.7.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vx3r/wg-gen-web/4fd1e34f5f70f4b4fa5b5187957de3d5633abbfd/ui/public/favicon.png -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Wg Gen Web 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 112 | -------------------------------------------------------------------------------- /ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vx3r/wg-gen-web/4fd1e34f5f70f4b4fa5b5187957de3d5633abbfd/ui/src/assets/logo.png -------------------------------------------------------------------------------- /ui/src/components/Clients.vue: -------------------------------------------------------------------------------- 1 | 465 | 618 | -------------------------------------------------------------------------------- /ui/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 52 | -------------------------------------------------------------------------------- /ui/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 82 | -------------------------------------------------------------------------------- /ui/src/components/Notification.vue: -------------------------------------------------------------------------------- 1 | 18 | 26 | -------------------------------------------------------------------------------- /ui/src/components/Server.vue: -------------------------------------------------------------------------------- 1 | 181 | 275 | -------------------------------------------------------------------------------- /ui/src/components/Status.vue: -------------------------------------------------------------------------------- 1 | 90 | 171 | -------------------------------------------------------------------------------- /ui/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import vuetify from './plugins/vuetify'; 6 | import './plugins/moment'; 7 | import './plugins/cidr' 8 | import './plugins/axios' 9 | 10 | // Don't warn about using the dev version of Vue in development. 11 | Vue.config.productionTip = process.env.NODE_ENV === 'production' 12 | 13 | new Vue({ 14 | router, 15 | store, 16 | vuetify, 17 | render: function (h) { return h(App) } 18 | }).$mount('#app') 19 | -------------------------------------------------------------------------------- /ui/src/plugins/axios.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from "axios"; 3 | import VueAxios from "vue-axios"; 4 | import TokenService from "../services/token.service"; 5 | 6 | Vue.use(VueAxios, axios); 7 | 8 | let baseUrl = "/api/v1.0"; 9 | if (process.env.NODE_ENV === "development"){ 10 | baseUrl = process.env.VUE_APP_API_BASE_URL; 11 | } 12 | 13 | Vue.axios.defaults.baseURL = baseUrl; 14 | 15 | Vue.axios.interceptors.response.use(function (response) { 16 | return response; 17 | }, function (error) { 18 | if (401 === error.response.status) { 19 | TokenService.destroyToken(); 20 | TokenService.destroyClientId(); 21 | window.location = '/'; 22 | } else { 23 | return Promise.reject(error); 24 | } 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /ui/src/plugins/cidr.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | const isCidr = require('is-cidr'); 3 | 4 | const plugin = { 5 | install () { 6 | Vue.isCidr = isCidr; 7 | Vue.prototype.$isCidr = isCidr 8 | } 9 | }; 10 | 11 | Vue.use(plugin); 12 | -------------------------------------------------------------------------------- /ui/src/plugins/moment.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import moment from 'moment'; 3 | import VueMoment from 'vue-moment' 4 | 5 | moment.locale('en'); 6 | 7 | Vue.use(VueMoment, { 8 | moment 9 | }); 10 | // $moment() accessible in project 11 | 12 | Vue.filter('formatDate', function (value) { 13 | if (!value) return ''; 14 | return moment(String(value)).format('YYYY-MM-DD HH:mm') 15 | }); 16 | -------------------------------------------------------------------------------- /ui/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | }); 8 | -------------------------------------------------------------------------------- /ui/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import store from "../store"; 4 | 5 | Vue.use(VueRouter); 6 | 7 | const routes = [ 8 | { 9 | path: '/clients', 10 | name: 'clients', 11 | component: function () { 12 | return import(/* webpackChunkName: "Clients" */ '../views/Clients.vue') 13 | }, 14 | meta: { 15 | requiresAuth: true 16 | } 17 | }, 18 | { 19 | path: '/server', 20 | name: 'server', 21 | component: function () { 22 | return import(/* webpackChunkName: "Server" */ '../views/Server.vue') 23 | }, 24 | meta: { 25 | requiresAuth: true 26 | } 27 | }, 28 | { 29 | path: '/status', 30 | name: 'status', 31 | component: function () { 32 | return import(/* webpackChunkName: "Status" */ '../views/Status.vue') 33 | }, 34 | meta: { 35 | requiresAuth: true 36 | } 37 | }, 38 | ]; 39 | 40 | const router = new VueRouter({ 41 | mode: 'history', 42 | base: process.env.BASE_URL, 43 | routes 44 | }); 45 | 46 | router.beforeEach((to, from, next) => { 47 | if(to.matched.some(record => record.meta.requiresAuth)) { 48 | if (store.getters["auth/isAuthenticated"]) { 49 | next() 50 | return 51 | } 52 | next('/') 53 | } else { 54 | next() 55 | } 56 | }) 57 | 58 | export default router 59 | -------------------------------------------------------------------------------- /ui/src/services/api.service.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import TokenService from "./token.service"; 3 | 4 | const ApiService = { 5 | 6 | setHeader() { 7 | Vue.axios.defaults.headers['x-wg-gen-web-auth'] = `${TokenService.getToken()}`; 8 | }, 9 | 10 | get(resource) { 11 | return Vue.axios.get(resource) 12 | .then(response => response.data) 13 | .catch(error => { 14 | if(typeof error.response !== 'undefined') { 15 | throw new Error(`${error.response.status} - ${error.response.statusText}: ${error.response.data}`) 16 | } else { 17 | throw new Error(`ApiService: ${error}`) 18 | } 19 | }); 20 | }, 21 | 22 | post(resource, params) { 23 | return Vue.axios.post(resource, params) 24 | .then(response => response.data) 25 | .catch(error => { 26 | throw new Error(`ApiService: ${error}`) 27 | }); 28 | }, 29 | 30 | put(resource, params) { 31 | return Vue.axios.put(resource, params) 32 | .then(response => response.data) 33 | .catch(error => { 34 | throw new Error(`ApiService: ${error}`) 35 | }); 36 | }, 37 | 38 | patch(resource, params) { 39 | return Vue.axios.patch(resource, params) 40 | .then(response => response.data) 41 | .catch(error => { 42 | throw new Error(`ApiService: ${error}`) 43 | }); 44 | }, 45 | 46 | delete(resource) { 47 | return Vue.axios.delete(resource) 48 | .then(response => response.data) 49 | .catch(error => { 50 | throw new Error(`ApiService: ${error}`) 51 | }); 52 | }, 53 | 54 | getWithConfig(resource, config) { 55 | return Vue.axios.get(resource, config) 56 | .then(response => response.data) 57 | .catch(error => { 58 | throw new Error(`ApiService: ${error}`) 59 | }); 60 | }, 61 | }; 62 | 63 | export default ApiService; 64 | -------------------------------------------------------------------------------- /ui/src/services/token.service.js: -------------------------------------------------------------------------------- 1 | const TOKEN_KEY = "token"; 2 | const CLIENT_ID_KEY = "client_id"; 3 | 4 | export const getToken = () => { 5 | return window.localStorage.getItem(TOKEN_KEY); 6 | }; 7 | 8 | export const saveToken = token => { 9 | window.localStorage.setItem(TOKEN_KEY, token); 10 | }; 11 | 12 | export const destroyToken = () => { 13 | window.localStorage.removeItem(TOKEN_KEY); 14 | }; 15 | 16 | export const getClientId = () => { 17 | return window.localStorage.getItem(CLIENT_ID_KEY); 18 | }; 19 | 20 | export const saveClientId = token => { 21 | window.localStorage.setItem(CLIENT_ID_KEY, token); 22 | }; 23 | 24 | export const destroyClientId = () => { 25 | window.localStorage.removeItem(CLIENT_ID_KEY); 26 | }; 27 | 28 | export default { 29 | getToken, 30 | saveToken, 31 | destroyToken, 32 | getClientId, 33 | saveClientId, 34 | destroyClientId 35 | }; 36 | -------------------------------------------------------------------------------- /ui/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import auth from "./modules/auth"; 4 | import client from "./modules/client"; 5 | import server from "./modules/server"; 6 | import status from "./modules/status"; 7 | 8 | Vue.use(Vuex) 9 | 10 | export default new Vuex.Store({ 11 | state: {}, 12 | getters : {}, 13 | mutations: {}, 14 | actions:{}, 15 | modules: { 16 | auth, 17 | client, 18 | server, 19 | status, 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /ui/src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import ApiService from "../../services/api.service"; 2 | import TokenService from "../../services/token.service"; 3 | 4 | const state = { 5 | error: null, 6 | user: null, 7 | authStatus: '', 8 | authRedirectUrl: '', 9 | }; 10 | 11 | const getters = { 12 | error(state) { 13 | return state.error; 14 | }, 15 | user(state) { 16 | return state.user; 17 | }, 18 | isAuthenticated(state) { 19 | return state.user !== null; 20 | }, 21 | authRedirectUrl(state) { 22 | return state.authRedirectUrl 23 | }, 24 | authStatus(state) { 25 | return state.authStatus 26 | }, 27 | }; 28 | 29 | const actions = { 30 | user({ commit }){ 31 | ApiService.get("/auth/user") 32 | .then( resp => { 33 | commit('user', resp) 34 | }) 35 | .catch(err => { 36 | commit('error', err); 37 | commit('logout') 38 | }); 39 | }, 40 | 41 | oauth2_url({ commit, dispatch }){ 42 | if (TokenService.getToken()) { 43 | ApiService.setHeader(); 44 | dispatch('user'); 45 | return 46 | } 47 | ApiService.get("/auth/oauth2_url") 48 | .then(resp => { 49 | if (resp.codeUrl === '_magic_string_fake_auth_no_redirect_'){ 50 | console.log("server report oauth2 is disabled, fake exchange") 51 | commit('authStatus', 'disabled') 52 | TokenService.saveClientId(resp.clientId) 53 | dispatch('oauth2_exchange', {code: "", state: resp.state}) 54 | } else { 55 | commit('authStatus', 'redirect') 56 | commit('authRedirectUrl', resp) 57 | } 58 | }) 59 | .catch(err => { 60 | commit('authStatus', 'error') 61 | commit('error', err); 62 | commit('logout') 63 | }) 64 | }, 65 | 66 | oauth2_exchange({ commit, dispatch }, data){ 67 | data.clientId = TokenService.getClientId() 68 | ApiService.post("/auth/oauth2_exchange", data) 69 | .then(resp => { 70 | commit('authStatus', 'success') 71 | commit('token', resp) 72 | dispatch('user'); 73 | }) 74 | .catch(err => { 75 | commit('authStatus', 'error') 76 | commit('error', err); 77 | commit('logout') 78 | }) 79 | }, 80 | 81 | logout({ commit }){ 82 | ApiService.get("/auth/logout") 83 | .then(resp => { 84 | commit('logout') 85 | }) 86 | .catch(err => { 87 | commit('authStatus', '') 88 | commit('error', err); 89 | commit('logout') 90 | }) 91 | }, 92 | } 93 | 94 | const mutations = { 95 | error(state, error) { 96 | state.error = error; 97 | }, 98 | authStatus(state, authStatus) { 99 | state.authStatus = authStatus; 100 | }, 101 | authRedirectUrl(state, resp) { 102 | state.authRedirectUrl = resp.codeUrl; 103 | TokenService.saveClientId(resp.clientId); 104 | }, 105 | token(state, token) { 106 | TokenService.saveToken(token); 107 | ApiService.setHeader(); 108 | TokenService.destroyClientId(); 109 | }, 110 | user(state, user) { 111 | state.user = user; 112 | }, 113 | logout(state) { 114 | state.user = null; 115 | TokenService.destroyToken(); 116 | TokenService.destroyClientId(); 117 | } 118 | }; 119 | 120 | export default { 121 | namespaced: true, 122 | state, 123 | getters, 124 | actions, 125 | mutations 126 | } 127 | -------------------------------------------------------------------------------- /ui/src/store/modules/client.js: -------------------------------------------------------------------------------- 1 | import ApiService from "../../services/api.service"; 2 | 3 | const state = { 4 | error: null, 5 | clients: [], 6 | clientQrcodes: [], 7 | clientConfigs: [] 8 | } 9 | 10 | const getters = { 11 | error(state) { 12 | return state.error; 13 | }, 14 | clients(state) { 15 | return state.clients; 16 | }, 17 | getClientQrcode: (state) => (id) => { 18 | let item = state.clientQrcodes.find(item => item.id === id) 19 | // initial load fails, must wait promise and stuff... 20 | return item ? item.qrcode : "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" 21 | }, 22 | getClientConfig: (state) => (id) => { 23 | let item = state.clientConfigs.find(item => item.id === id) 24 | return item ? item.config : null 25 | } 26 | } 27 | 28 | const actions = { 29 | error({ commit }, error){ 30 | commit('error', error) 31 | }, 32 | 33 | readAll({ commit, dispatch }){ 34 | ApiService.get("/client") 35 | .then(resp => { 36 | commit('clients', resp) 37 | dispatch('readQrcodes') 38 | dispatch('readConfigs') 39 | }) 40 | .catch(err => { 41 | commit('error', err) 42 | }) 43 | }, 44 | 45 | create({ commit, dispatch }, client){ 46 | ApiService.post("/client", client) 47 | .then(resp => { 48 | dispatch('readQrcode', resp) 49 | dispatch('readConfig', resp) 50 | commit('create', resp) 51 | }) 52 | .catch(err => { 53 | commit('error', err) 54 | }) 55 | }, 56 | 57 | update({ commit, dispatch }, client){ 58 | ApiService.patch(`/client/${client.id}`, client) 59 | .then(resp => { 60 | dispatch('readQrcode', resp) 61 | dispatch('readConfig', resp) 62 | commit('update', resp) 63 | }) 64 | .catch(err => { 65 | commit('error', err) 66 | }) 67 | }, 68 | 69 | delete({ commit }, client){ 70 | ApiService.delete(`/client/${client.id}`) 71 | .then(() => { 72 | commit('delete', client) 73 | }) 74 | .catch(err => { 75 | commit('error', err) 76 | }) 77 | }, 78 | 79 | email({ commit }, client){ 80 | ApiService.get(`/client/${client.id}/email`) 81 | .then(() => { 82 | }) 83 | .catch(err => { 84 | commit('error', err) 85 | }) 86 | }, 87 | 88 | readQrcode({ state, commit }, client){ 89 | ApiService.getWithConfig(`/client/${client.id}/config?qrcode=true`, {responseType: 'arraybuffer'}) 90 | .then(resp => { 91 | let image = Buffer.from(resp, 'binary').toString('base64') 92 | commit('clientQrcodes', { client, image }) 93 | }) 94 | .catch(err => { 95 | commit('error', err) 96 | }) 97 | }, 98 | 99 | readConfig({ state, commit }, client){ 100 | ApiService.getWithConfig(`/client/${client.id}/config?qrcode=false`, {responseType: 'arraybuffer'}) 101 | .then(resp => { 102 | commit('clientConfigs', { client: client, config: resp }) 103 | }) 104 | .catch(err => { 105 | commit('error', err) 106 | }) 107 | }, 108 | 109 | readQrcodes({ state, dispatch }){ 110 | state.clients.forEach(client => { 111 | dispatch('readQrcode', client) 112 | }) 113 | }, 114 | 115 | readConfigs({ state, dispatch }){ 116 | state.clients.forEach(client => { 117 | dispatch('readConfig', client) 118 | }) 119 | }, 120 | } 121 | 122 | const mutations = { 123 | error(state, error) { 124 | state.error = error; 125 | }, 126 | clients(state, clients){ 127 | state.clients = clients 128 | }, 129 | create(state, client){ 130 | state.clients.push(client) 131 | }, 132 | update(state, client){ 133 | let index = state.clients.findIndex(x => x.id === client.id); 134 | if (index !== -1) { 135 | state.clients.splice(index, 1); 136 | state.clients.push(client); 137 | } else { 138 | state.error = "update client failed, not in list" 139 | } 140 | }, 141 | delete(state, client){ 142 | let index = state.clients.findIndex(x => x.id === client.id); 143 | if (index !== -1) { 144 | state.clients.splice(index, 1); 145 | } else { 146 | state.error = "delete client failed, not in list" 147 | } 148 | }, 149 | clientQrcodes(state, { client, image }){ 150 | let index = state.clientQrcodes.findIndex(x => x.id === client.id); 151 | if (index !== -1) { 152 | state.clientQrcodes.splice(index, 1); 153 | } 154 | state.clientQrcodes.push({ 155 | id: client.id, 156 | qrcode: image 157 | }) 158 | }, 159 | clientConfigs(state, { client, config }){ 160 | let index = state.clientConfigs.findIndex(x => x.id === client.id); 161 | if (index !== -1) { 162 | state.clientConfigs.splice(index, 1); 163 | } 164 | state.clientConfigs.push({ 165 | id: client.id, 166 | config: config 167 | }) 168 | }, 169 | } 170 | 171 | export default { 172 | namespaced: true, 173 | state, 174 | getters, 175 | actions, 176 | mutations 177 | } 178 | -------------------------------------------------------------------------------- /ui/src/store/modules/server.js: -------------------------------------------------------------------------------- 1 | import ApiService from "../../services/api.service"; 2 | 3 | const state = { 4 | error: null, 5 | server: null, 6 | config: '', 7 | version: '_ci_build_not_run_properly_', 8 | } 9 | 10 | const getters = { 11 | error(state) { 12 | return state.error; 13 | }, 14 | 15 | server(state) { 16 | return state.server; 17 | }, 18 | 19 | version(state) { 20 | return state.version; 21 | }, 22 | 23 | config(state) { 24 | return state.config; 25 | }, 26 | } 27 | 28 | const actions = { 29 | error({ commit }, error){ 30 | commit('error', error) 31 | }, 32 | 33 | read({ commit, dispatch }){ 34 | ApiService.get("/server") 35 | .then(resp => { 36 | commit('server', resp) 37 | dispatch('config') 38 | }) 39 | .catch(err => { 40 | commit('error', err) 41 | }) 42 | }, 43 | 44 | update({ commit }, server){ 45 | ApiService.patch(`/server`, server) 46 | .then(resp => { 47 | commit('server', resp) 48 | }) 49 | .catch(err => { 50 | commit('error', err) 51 | }) 52 | }, 53 | 54 | config({ commit }){ 55 | ApiService.getWithConfig("/server/config", {responseType: 'arraybuffer'}) 56 | .then(resp => { 57 | commit('config', resp) 58 | }) 59 | .catch(err => { 60 | commit('error', err) 61 | }) 62 | }, 63 | 64 | version({ commit }){ 65 | ApiService.get("/server/version") 66 | .then(resp => { 67 | commit('version', resp.version) 68 | }) 69 | .catch(err => { 70 | commit('error', err) 71 | }) 72 | }, 73 | 74 | } 75 | 76 | const mutations = { 77 | error(state, error) { 78 | state.error = error; 79 | }, 80 | 81 | server(state, server){ 82 | state.server = server 83 | }, 84 | 85 | config(state, config){ 86 | state.config = config 87 | }, 88 | 89 | version(state, version){ 90 | state.version = version 91 | }, 92 | } 93 | 94 | export default { 95 | namespaced: true, 96 | state, 97 | getters, 98 | actions, 99 | mutations 100 | } 101 | -------------------------------------------------------------------------------- /ui/src/store/modules/status.js: -------------------------------------------------------------------------------- 1 | import ApiService from "../../services/api.service"; 2 | 3 | const state = { 4 | error: null, 5 | enabled: false, 6 | interfaceStatus: null, 7 | clientStatus: [], 8 | version: '_ci_build_not_run_properly_', 9 | } 10 | 11 | const getters = { 12 | error(state) { 13 | return state.error; 14 | }, 15 | 16 | enabled(state) { 17 | return state.enabled; 18 | }, 19 | 20 | interfaceStatus(state) { 21 | return state.interfaceStatus; 22 | }, 23 | 24 | clientStatus(state) { 25 | return state.clientStatus; 26 | }, 27 | 28 | version(state) { 29 | return state.version; 30 | }, 31 | } 32 | 33 | const actions = { 34 | error({ commit }, error){ 35 | commit('error', error) 36 | }, 37 | 38 | read({ commit }){ 39 | ApiService.get("/status/interface") 40 | .then(resp => { 41 | commit('interfaceStatus', resp) 42 | }) 43 | .catch(err => { 44 | commit('interfaceStatus', null); 45 | commit('error', err) 46 | }); 47 | ApiService.get("/status/clients") 48 | .then(resp => { 49 | commit('clientStatus', resp) 50 | }) 51 | .catch(err => { 52 | commit('clientStatus', []); 53 | commit('error', err) 54 | }); 55 | }, 56 | 57 | isEnabled({ commit }){ 58 | ApiService.get("/status/enabled") 59 | .then(resp => { 60 | commit('enabled', resp) 61 | }) 62 | .catch(err => { 63 | commit('enabled', false); 64 | commit('error', err.response.data) 65 | }); 66 | }, 67 | } 68 | 69 | const mutations = { 70 | error(state, error) { 71 | state.error = error; 72 | }, 73 | 74 | enabled(state, enabled) { 75 | state.enabled = enabled; 76 | }, 77 | 78 | interfaceStatus(state, interfaceStatus){ 79 | state.interfaceStatus = interfaceStatus 80 | }, 81 | 82 | clientStatus(state, clientStatus){ 83 | state.clientStatus = clientStatus 84 | }, 85 | 86 | version(state, version){ 87 | state.version = version 88 | }, 89 | } 90 | 91 | export default { 92 | namespaced: true, 93 | state, 94 | getters, 95 | actions, 96 | mutations 97 | } 98 | -------------------------------------------------------------------------------- /ui/src/views/Clients.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /ui/src/views/Server.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /ui/src/views/Status.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /ui/vue.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | module.exports = { 4 | devServer: { 5 | port: 8081, 6 | disableHostCheck: true, 7 | }, 8 | transpileDependencies: [ 9 | "vuetify" 10 | ], 11 | pwa: { 12 | name: 'Wg Gen Web', 13 | }, 14 | configureWebpack: { 15 | plugins: [ 16 | new webpack.ProvidePlugin({ 17 | Buffer: ["buffer", "Buffer"], 18 | }), 19 | ], 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "errors" 7 | "io/ioutil" 8 | "net" 9 | "os" 10 | "regexp" 11 | ) 12 | 13 | var ( 14 | // AuthTokenHeaderName http header for token transport 15 | AuthTokenHeaderName = "x-wg-gen-web-auth" 16 | // RegexpEmail check valid email 17 | RegexpEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") 18 | ) 19 | 20 | // ReadFile file content 21 | func ReadFile(path string) (bytes []byte, err error) { 22 | bytes, err = ioutil.ReadFile(path) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return bytes, nil 28 | } 29 | 30 | // WriteFile content to file 31 | func WriteFile(path string, bytes []byte) (err error) { 32 | err = ioutil.WriteFile(path, bytes, 0644) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return nil 38 | } 39 | 40 | // FileExists check if file exists 41 | func FileExists(name string) bool { 42 | info, err := os.Stat(name) 43 | if os.IsNotExist(err) { 44 | return false 45 | } 46 | return !info.IsDir() 47 | } 48 | 49 | // DirectoryExists check if directory exists 50 | func DirectoryExists(name string) bool { 51 | info, err := os.Stat(name) 52 | if os.IsNotExist(err) { 53 | return false 54 | } 55 | return info.IsDir() 56 | } 57 | 58 | // GetAvailableIp search for an available ip in cidr against a list of reserved ips 59 | func GetAvailableIp(cidr string, reserved []string) (string, error) { 60 | ip, ipnet, err := net.ParseCIDR(cidr) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | // this two addresses are not usable 66 | broadcastAddr := BroadcastAddr(ipnet).String() 67 | networkAddr := ipnet.IP.String() 68 | 69 | for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { 70 | ok := true 71 | address := ip.String() 72 | for _, r := range reserved { 73 | if address == r { 74 | ok = false 75 | break 76 | } 77 | } 78 | if ok && address != networkAddr && address != broadcastAddr { 79 | return address, nil 80 | } 81 | } 82 | 83 | return "", errors.New("no more available address from cidr") 84 | } 85 | 86 | // IsIPv6 check if given ip is IPv6 87 | func IsIPv6(address string) bool { 88 | ip := net.ParseIP(address) 89 | if ip == nil { 90 | return false 91 | } 92 | return ip.To4() == nil 93 | } 94 | 95 | // IsValidIp check if ip is valid 96 | func IsValidIp(ip string) bool { 97 | return net.ParseIP(ip) != nil 98 | } 99 | 100 | // IsValidCidr check if CIDR is valid 101 | func IsValidCidr(cidr string) bool { 102 | _, _, err := net.ParseCIDR(cidr) 103 | return err == nil 104 | } 105 | 106 | // GetIpFromCidr get ip from cidr 107 | func GetIpFromCidr(cidr string) (string, error) { 108 | ip, _, err := net.ParseCIDR(cidr) 109 | if err != nil { 110 | return "", err 111 | } 112 | return ip.String(), nil 113 | } 114 | 115 | // http://play.golang.org/p/m8TNTtygK0 116 | func inc(ip net.IP) { 117 | for j := len(ip) - 1; j >= 0; j-- { 118 | ip[j]++ 119 | if ip[j] > 0 { 120 | break 121 | } 122 | } 123 | } 124 | 125 | // BroadcastAddr returns the last address in the given network, or the broadcast address. 126 | func BroadcastAddr(n *net.IPNet) net.IP { 127 | // The golang net package doesn't make it easy to calculate the broadcast address. :( 128 | var broadcast net.IP 129 | if len(n.IP) == 4 { 130 | broadcast = net.ParseIP("0.0.0.0").To4() 131 | } else { 132 | broadcast = net.ParseIP("::") 133 | } 134 | for i := 0; i < len(n.IP); i++ { 135 | broadcast[i] = n.IP[i] | ^n.Mask[i] 136 | } 137 | return broadcast 138 | } 139 | 140 | // GenerateRandomBytes returns securely generated random bytes. 141 | // It will return an error if the system's secure random 142 | // number generator fails to function correctly, in which 143 | // case the caller should not continue. 144 | func GenerateRandomBytes(n int) ([]byte, error) { 145 | b := make([]byte, n) 146 | _, err := rand.Read(b) 147 | // Note that err == nil only if we read len(b) bytes. 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | return b, nil 153 | } 154 | 155 | // GenerateRandomString returns a URL-safe, base64 encoded 156 | // securely generated random string. 157 | // It will return an error if the system's secure random 158 | // number generator fails to function correctly, in which 159 | // case the caller should not continue. 160 | func GenerateRandomString(s int) (string, error) { 161 | b, err := GenerateRandomBytes(s) 162 | return base64.URLEncoding.EncodeToString(b), err 163 | } 164 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Version build time set version 4 | var Version = "_ci_build_not_run_properly_" 5 | -------------------------------------------------------------------------------- /wg-gen-web_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vx3r/wg-gen-web/4fd1e34f5f70f4b4fa5b5187957de3d5633abbfd/wg-gen-web_cover.png -------------------------------------------------------------------------------- /wg-gen-web_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vx3r/wg-gen-web/4fd1e34f5f70f4b4fa5b5187957de3d5633abbfd/wg-gen-web_screenshot.png --------------------------------------------------------------------------------