diff --git a/Dockerfile b/Dockerfile index ca427af2..17f7a730 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,12 +34,31 @@ RUN apk add --no-cache \ iptables-legacy \ wireguard-tools +# Download and instll Go +RUN wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz &&\ + tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz &&\ + rm go1.22.5.linux-amd64.tar.gz + + +# Configure Go +RUN echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile &&\ + echo 'export GOPATH=$HOME/goproject' >> /etc/profile &&\ + echo 'export PATH=$PATH:$GOPATH/bin' >> /etc/profile &&\ + source /etc/profile &&\ + mkdir $HOME/goproject + +# Set Go Environment +ENV PATH=$PATH:/usr/local/go/bin +ENV GOPATH=/root/goproject +ENV PATH=$PATH:$GOPATH/bin + # Use iptables-legacy RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save -# Set Environment +# Set WG Environment ENV DEBUG=Server,WireGuard -# Run Web UI +# Start cron in the background and then start the main application WORKDIR /app -CMD ["/usr/bin/dumb-init", "node", "server.js"] +CMD ["/usr/bin/dumb-init",\ + "/bin/sh", "-c", "exec node server.js & watch -n 86400 go run ./services/ClientSwitcher.go ${WG_HOST} ${PORT} ${PASSWORD}"] diff --git a/docker-compose.yml b/docker-compose.yml index 7a6a4f49..611a9510 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,7 +29,7 @@ services: # - UI_TRAFFIC_STATS=true # - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart) - image: ghcr.io/wg-easy/wg-easy + image: ghcr.io/amarseillaise/wg-easy-amars container_name: wg-easy volumes: - etc_wireguard:/etc/wireguard diff --git a/src/lib/Server.js b/src/lib/Server.js index 40341ee5..3d0bc0e2 100644 --- a/src/lib/Server.js +++ b/src/lib/Server.js @@ -223,6 +223,15 @@ module.exports = class Server { await WireGuard.disableClient({ clientId }); return { success: true }; })) + .put('/api/wireguard/client/:clientId/enableduntil', defineEventHandler(async (event) => { + const clientId = getRouterParam(event, 'clientId'); + if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { + throw createError({ status: 403 }); + } + const { date } = await readBody(event); + await WireGuard.changeEnabledUntilDate({ clientId, date }); + return { success: true }; + })) .put('/api/wireguard/client/:clientId/name', defineEventHandler(async (event) => { const clientId = getRouterParam(event, 'clientId'); if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js index d4f32c5f..d501b519 100644 --- a/src/lib/WireGuard.js +++ b/src/lib/WireGuard.js @@ -136,6 +136,9 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' id: clientId, name: client.name, enabled: client.enabled, + enabledUntil: client.enabledUntil !== null + ? new Date(client.enabledUntil) + : null, address: client.address, publicKey: client.publicKey, createdAt: new Date(client.createdAt), @@ -261,6 +264,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`; updatedAt: new Date(), enabled: true, + enabledUntil: null }; config.clients[id] = client; @@ -297,6 +301,14 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`; await this.saveConfig(); } + async changeEnabledUntilDate({ clientId, date }) { + const client = await this.getClient({ clientId }); + const dateObj = new Date(date); + client.enabledUntil = dateObj; + + await this.saveConfig(); + } + async updateClientName({ clientId, name }) { const client = await this.getClient({ clientId }); diff --git a/src/services/ClientSwitcher.go b/src/services/ClientSwitcher.go new file mode 100644 index 00000000..7cdf7221 --- /dev/null +++ b/src/services/ClientSwitcher.go @@ -0,0 +1,114 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/cookiejar" + "os" + "strings" + "time" +) + +type Client struct { + ID string `json:"id"` + Enabled bool `json:"enabled"` + EnabledUntil time.Time `json:"enabledUntil"` +} + +const ( + Api = "api" + Wireguard = "wireguard" + ClientApi = "client" + Session = "session" +) + +func makeRequest(client *http.Client, url string, method string) *http.Response { + request, _ := http.NewRequest(method, url, nil) + response, err := client.Do(request) + if err != nil || response.StatusCode != 200 { + fmt.Println(response) + panic(response) + } + return response +} + +func initClients(resp *http.Response, clients *[]Client) error { + defer resp.Body.Close() + return json.NewDecoder(resp.Body).Decode(clients) +} + +func switchClientsStatus(httpClient *http.Client, clients []Client) { + today := time.Now() + for _, client := range clients { + if client.EnabledUntil.Before(today) { + switchStatus(httpClient, client.ID, "disable") + } else if client.EnabledUntil.After(today) { + switchStatus(httpClient, client.ID, "enable") + } + } +} + +func switchStatus(client *http.Client, clientID, status string) { + url := strings.Join([]string{getHost(), Api, Wireguard, ClientApi, clientID, status}, "/") + makeRequest(client, url, "POST") +} + +func checkAuth(client *http.Client) bool { + type AuthChecker struct { + RequiresPassword bool `json:"requiresPassword"` + Authenticated bool `json:"authenticated"` + } + var auth AuthChecker + + url := strings.Join([]string{getHost(), Api, Session}, "/") + response := makeRequest(client, url, "GET") + json.NewDecoder(response.Body).Decode(&auth) + result := (auth.RequiresPassword && auth.Authenticated) || (!auth.RequiresPassword && auth.Authenticated) + return result +} + +func authenticate(client *http.Client) { + args := os.Args[1:] + type Password struct { + Password string `json:"password"` + } + + password := Password{ + Password: args[2], + } + url := strings.Join([]string{getHost(), Api, Session}, "/") + passwdJson, _ := json.Marshal(password) + + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(passwdJson)) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil || resp.StatusCode != 200 { + fmt.Println(resp) + panic(resp) + } + defer resp.Body.Close() +} + +func getHost() string { + args := os.Args[1:] + host := args[0] + ":" + args[1] + return host +} + +func main() { + getClientsUrl := strings.Join([]string{getHost(), Api, Wireguard, ClientApi}, "/") + var clients []Client + jar, _ := cookiejar.New(nil) + client := &http.Client{Jar: jar} + if !checkAuth(client) { + authenticate(client) + } + + resp := makeRequest(client, getClientsUrl, "GET") + + initClients(resp, &clients) + switchClientsStatus(client, clients) +} diff --git a/src/www/css/app.css b/src/www/css/app.css index ead64396..2c80cfd4 100644 --- a/src/www/css/app.css +++ b/src/www/css/app.css @@ -1686,6 +1686,10 @@ video { font-size: 1rem; line-height: 1.5rem; } + .md\:text-base-small { + font-size: 0.9rem; + line-height: 1.1rem; + } } .dark\:border-neutral-500:where(.dark, .dark *) { diff --git a/src/www/index.html b/src/www/index.html index 72044728..6444bad8 100644 --- a/src/www/index.html +++ b/src/www/index.html @@ -271,7 +271,13 @@
- + + +
+ +
+
diff --git a/src/www/js/api.js b/src/www/js/api.js index 356164c5..7e20c02d 100644 --- a/src/www/js/api.js +++ b/src/www/js/api.js @@ -122,6 +122,14 @@ class API { }); } + async changeEnabledUntilDate({ clientId, date }) { + return this.call({ + method: 'put', + path: `/wireguard/client/${clientId}/enableduntil/`, + body: { date }, + }); + } + async updateClientName({ clientId, name }) { return this.call({ method: 'put', diff --git a/src/www/js/app.js b/src/www/js/app.js index 6745f698..c7f1ffa8 100644 --- a/src/www/js/app.js +++ b/src/www/js/app.js @@ -195,6 +195,9 @@ new Vue({ this.clientsPersist[client.id].transferRxPrevious = client.transferRx; this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious; this.clientsPersist[client.id].transferTxPrevious = client.transferTx; + this.clientsPersist[client.id].enabledUntil = client.enabledUntil !== null + ? new Date(client.enabledUntil).toISOString().slice(0, 10) + : null; if (updateCharts) { this.clientsPersist[client.id].transferRxHistory.push(this.clientsPersist[client.id].transferRxCurrent); @@ -226,6 +229,7 @@ new Vue({ client.hoverTx = this.clientsPersist[client.id].hoverTx; client.hoverRx = this.clientsPersist[client.id].hoverRx; + client.enabledUntil = this.clientsPersist[client.id].enabledUntil; return client; }); @@ -289,6 +293,19 @@ new Vue({ .catch((err) => alert(err.message || err.toString())) .finally(() => this.refresh().catch(console.error)); }, + changeEnabledUntilDate(client, date){ + const dateObj = new Date(date); + const currentDate = new Date(); + + if (dateObj < currentDate) { + alert('The date cannot be less than the current date'); + return; + } + + this.api.changeEnabledUntilDate({ clientId: client.id, date }) + .catch((err) => alert(err.message || err.toString())) + .finally(() => this.refresh().catch(console.error)); + }, updateClientName(client, name) { this.api.updateClientName({ clientId: client.id, name }) .catch((err) => alert(err.message || err.toString())) diff --git a/src/www/js/i18n.js b/src/www/js/i18n.js index 467bb460..5e143913 100644 --- a/src/www/js/i18n.js +++ b/src/www/js/i18n.js @@ -22,6 +22,7 @@ const messages = { // eslint-disable-line no-unused-vars newClient: 'New Client', disableClient: 'Disable Client', enableClient: 'Enable Client', + enabledUntil: 'Enabled until', noClients: 'There are no clients yet.', noPrivKey: 'This client has no known private key. Cannot create Configuration.', showQR: 'Show QR Code', @@ -79,6 +80,7 @@ const messages = { // eslint-disable-line no-unused-vars newClient: 'Создать клиента', disableClient: 'Выключить клиента', enableClient: 'Включить клиента', + enabledUntil: 'Включено до', noClients: 'Пока нет клиентов.', showQR: 'Показать QR-код', downloadConfig: 'Скачать конфигурацию',