├── Obsidian-plugin
├── .npmrc
├── .eslintignore
├── versions.json
├── .editorconfig
├── .gitignore
├── manifest.json
├── tsconfig.json
├── version-bump.mjs
├── .eslintrc
├── esbuild.config.mjs
├── src
│ ├── utils.ts
│ ├── types.ts
│ ├── constants.ts
│ ├── api.service.ts
│ ├── main.ts
│ ├── settings.ts
│ └── view.ts
├── package.json
├── README.md
└── styles.css
├── nordgen
├── src
│ └── nord_config_generator
│ │ ├── __init__.py
│ │ ├── ui.py
│ │ └── main.py
├── .gitignore
└── pyproject.toml
├── Web-version
├── robots.txt
├── favicon.ico
├── key.html
├── fetch-proxies.html
├── fetch-script.js
├── fetch-styles.css
├── key-script.js
├── index_styles.css
├── index.html
└── key-styles.css
├── web-version-V2
├── web-version-V2-Frontend
│ ├── postcss.config.js
│ ├── public
│ │ └── favicon.ico
│ ├── src
│ │ ├── main.js
│ │ ├── components
│ │ │ ├── Icon.vue
│ │ │ ├── Toast.vue
│ │ │ ├── ServerCard.vue
│ │ │ ├── KeyGenerator.vue
│ │ │ └── ConfigCustomizer.vue
│ │ ├── services
│ │ │ ├── storageService.js
│ │ │ └── apiService.js
│ │ ├── composables
│ │ │ ├── useToast.js
│ │ │ ├── useUI.js
│ │ │ ├── useConfig.js
│ │ │ └── useServers.js
│ │ ├── style.css
│ │ ├── utils
│ │ │ ├── utils.js
│ │ │ └── icons.js
│ │ └── App.vue
│ ├── index.html
│ ├── package.json
│ ├── tailwind.config.js
│ └── vite.config.js
└── web-version-V2-Backend
│ ├── public
│ ├── favicon.ico
│ ├── index.html.br
│ ├── index.html.gz
│ ├── assets
│ │ ├── index-7qAqENDn.js.br
│ │ ├── index-7qAqENDn.js.gz
│ │ ├── vendor-Cf_WThed.js.br
│ │ └── vendor-Cf_WThed.js.gz
│ └── index.html
│ ├── internal
│ ├── wg
│ │ └── wg.go
│ ├── types
│ │ └── types.go
│ └── store
│ │ └── store.go
│ ├── go.mod
│ ├── README.md
│ ├── API.md
│ ├── go.sum
│ └── main.go
├── Dockerfile
├── .github
└── workflows
│ └── docker-publish.yml
└── README.md
/Obsidian-plugin/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/nordgen/src/nord_config_generator/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Web-version/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
--------------------------------------------------------------------------------
/Obsidian-plugin/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | main.js
4 |
--------------------------------------------------------------------------------
/Obsidian-plugin/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "1.8.7": "1.0.0",
3 | "1.0.0": "1.0.0"
4 | }
--------------------------------------------------------------------------------
/Web-version/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafachyi/NordVPN-WireGuard-Config-Generator/HEAD/Web-version/favicon.ico
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
--------------------------------------------------------------------------------
/nordgen/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | __pycache__/
3 | *.pyc
4 | *.pyo
5 | *.pyd
6 | .Python
7 | env/
8 | venv/
9 | *.egg-info/
10 | dist/
11 | build/
12 |
13 |
14 | .vscode/
15 | .idea/
16 | *.swp
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafachyi/NordVPN-WireGuard-Config-Generator/HEAD/web-version-V2/web-version-V2-Backend/public/favicon.ico
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/public/index.html.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafachyi/NordVPN-WireGuard-Config-Generator/HEAD/web-version-V2/web-version-V2-Backend/public/index.html.br
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/public/index.html.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafachyi/NordVPN-WireGuard-Config-Generator/HEAD/web-version-V2/web-version-V2-Backend/public/index.html.gz
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafachyi/NordVPN-WireGuard-Config-Generator/HEAD/web-version-V2/web-version-V2-Frontend/public/favicon.ico
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/public/assets/index-7qAqENDn.js.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafachyi/NordVPN-WireGuard-Config-Generator/HEAD/web-version-V2/web-version-V2-Backend/public/assets/index-7qAqENDn.js.br
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/public/assets/index-7qAqENDn.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafachyi/NordVPN-WireGuard-Config-Generator/HEAD/web-version-V2/web-version-V2-Backend/public/assets/index-7qAqENDn.js.gz
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/public/assets/vendor-Cf_WThed.js.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafachyi/NordVPN-WireGuard-Config-Generator/HEAD/web-version-V2/web-version-V2-Backend/public/assets/vendor-Cf_WThed.js.br
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/public/assets/vendor-Cf_WThed.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafachyi/NordVPN-WireGuard-Config-Generator/HEAD/web-version-V2/web-version-V2-Backend/public/assets/vendor-Cf_WThed.js.gz
--------------------------------------------------------------------------------
/Obsidian-plugin/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = tab
9 | indent_size = 4
10 | tab_width = 4
11 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import '@/style.css'
3 | import App from '@/App.vue'
4 | import { storage } from '@/services/storageService'
5 |
6 | createApp(App).mount('#app')
7 |
8 | setTimeout(() => storage.clean(), 0)
--------------------------------------------------------------------------------
/Obsidian-plugin/.gitignore:
--------------------------------------------------------------------------------
1 | # vscode
2 | .vscode
3 |
4 | # Intellij
5 | *.iml
6 | .idea
7 |
8 | # npm
9 | node_modules
10 |
11 | # Don't include the compiled main.js file in the repo.
12 | # They should be uploaded to GitHub releases instead.
13 | main.js
14 |
15 | # Exclude sourcemaps
16 | *.map
17 |
18 | # obsidian
19 | data.json
20 |
21 | # Exclude macOS Finder (System Explorer) View States
22 | .DS_Store
23 |
--------------------------------------------------------------------------------
/Obsidian-plugin/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "nordvpn-config-generator",
3 | "name": "NordVPN Config Generator",
4 | "version": "1.0.0",
5 | "minAppVersion": "1.0.0",
6 | "description": "Generate WireGuard configurations for NordVPN servers - Part of NordVPN WireGuard Config Generator suite",
7 | "author": "Ahmed Touhami (mustafachyi)",
8 | "authorUrl": "https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator",
9 | "isDesktopOnly": true
10 | }
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/components/Icon.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/services/storageService.js:
--------------------------------------------------------------------------------
1 | const KEYS = ['wg_gen_settings', 'showIp']
2 |
3 | export const storage = {
4 | get: k => {
5 | try {
6 | const i = localStorage.getItem(k)
7 | return i ? JSON.parse(i) : null
8 | } catch { return null }
9 | },
10 | set: (k, v) => localStorage.setItem(k, JSON.stringify(v)),
11 | clean: () => Object.keys(localStorage).forEach(k => { if (!KEYS.includes(k)) localStorage.removeItem(k) })
12 | }
--------------------------------------------------------------------------------
/Obsidian-plugin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "ES6",
8 | "allowJs": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node",
11 | "importHelpers": true,
12 | "isolatedModules": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strictNullChecks": true,
15 | "lib": [
16 | "DOM",
17 | "ES5",
18 | "ES6",
19 | "ES7"
20 | ]
21 | },
22 | "include": [
23 | "**/*.ts"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/composables/useToast.js:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue'
2 |
3 | const TIME = 2000
4 | const MAX = 100
5 |
6 | export function useToast() {
7 | const toast = ref(null)
8 | let timer = null
9 |
10 | const show = (msg, type = 'success') => {
11 | if (!msg) return
12 | const m = (msg instanceof Error ? msg.message : String(msg)).split('\n')[0].slice(0, MAX)
13 | clearTimeout(timer)
14 | toast.value = { message: m, type: ['success', 'error'].includes(type) ? type : 'success' }
15 | timer = setTimeout(() => { toast.value = null }, TIME)
16 | }
17 |
18 | return { toast, show }
19 | }
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | NordVPN WireGuard Config Generator
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-version-v2-frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "vue": "^3.5.25"
13 | },
14 | "devDependencies": {
15 | "@vitejs/plugin-vue": "^6.0.3",
16 | "autoprefixer": "^10.4.22",
17 | "postcss": "^8.5.6",
18 | "tailwindcss": "^3.4.17",
19 | "terser": "^5.44.1",
20 | "vite": "^7.2.7",
21 | "vite-plugin-compression2": "^2.4.0",
22 | "vite-plugin-css-injected-by-js": "^3.5.2"
23 | }
24 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.25-alpine AS builder
2 | WORKDIR /build
3 | RUN apk add --no-cache git
4 | COPY web-version-V2/web-version-V2-Backend/go.mod web-version-V2/web-version-V2-Backend/go.sum ./
5 | RUN go mod download
6 | COPY web-version-V2/web-version-V2-Backend .
7 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -trimpath -o server main.go
8 |
9 | FROM alpine:latest
10 | WORKDIR /app
11 | RUN apk add --no-cache curl ca-certificates
12 | COPY --from=builder /build/server .
13 | COPY --from=builder /build/public ./public
14 | EXPOSE 3000
15 | HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -f http://localhost:3000/api/servers || exit 1
16 | CMD ["./server"]
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root { font-size: 115%; }
7 | html, body, #app, * { -ms-overflow-style: none; scrollbar-width: none; }
8 | *::-webkit-scrollbar { display: none; width: 0; height: 0; }
9 | button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible {
10 | outline: none;
11 | border-color: var(--color-vscode-accent);
12 | }
13 | input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
14 | input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
15 | }
--------------------------------------------------------------------------------
/Obsidian-plugin/version-bump.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "fs";
2 |
3 | const targetVersion = process.env.npm_package_version;
4 |
5 | // read minAppVersion from manifest.json and bump version to target version
6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
7 | const { minAppVersion } = manifest;
8 | manifest.version = targetVersion;
9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
10 |
11 | // update versions.json with target version and minAppVersion from manifest.json
12 | let versions = JSON.parse(readFileSync("versions.json", "utf8"));
13 | versions[targetVersion] = minAppVersion;
14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
15 |
--------------------------------------------------------------------------------
/Obsidian-plugin/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "env": { "node": true },
5 | "plugins": [
6 | "@typescript-eslint"
7 | ],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "parserOptions": {
14 | "sourceType": "module"
15 | },
16 | "rules": {
17 | "no-unused-vars": "off",
18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
19 | "@typescript-eslint/ban-ts-comment": "off",
20 | "no-prototype-builtins": "off",
21 | "@typescript-eslint/no-empty-function": "off"
22 | }
23 | }
--------------------------------------------------------------------------------
/Web-version/key.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | NordVPN Key Fetcher
5 |
6 |
7 |
8 |
9 |
10 |
11 |
NordVPN Key Fetcher
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | NordVPN WireGuard Config Generator
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Web-version/fetch-proxies.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Fetch Proxies
5 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/internal/wg/wg.go:
--------------------------------------------------------------------------------
1 | package wg
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 |
7 | "nordgen/internal/types"
8 | )
9 |
10 | func Build(server types.ProcessedServer, pubKey string, opts types.ValidatedConfig) string {
11 | var sb strings.Builder
12 | sb.Grow(512)
13 |
14 | endpoint := server.Hostname
15 | if opts.UseStation {
16 | endpoint = server.Station
17 | }
18 |
19 | sb.WriteString("[Interface]\nPrivateKey=")
20 | sb.WriteString(opts.PrivateKey)
21 | sb.WriteString("\nAddress=10.5.0.2/16\nDNS=")
22 | sb.WriteString(opts.DNS)
23 | sb.WriteString("\n\n[Peer]\nPublicKey=")
24 | sb.WriteString(pubKey)
25 | sb.WriteString("\nAllowedIPs=0.0.0.0/0,::/0\nEndpoint=")
26 | sb.WriteString(endpoint)
27 | sb.WriteString(":51820\nPersistentKeepalive=")
28 | sb.WriteString(strconv.Itoa(opts.KeepAlive))
29 |
30 | return sb.String()
31 | }
32 |
--------------------------------------------------------------------------------
/Obsidian-plugin/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from "builtin-modules";
4 |
5 | const banner =
6 | `/*
7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
8 | if you want to view the source, please visit the github repository of this plugin
9 | */
10 | `;
11 |
12 | const prod = process.argv[2] === "production";
13 |
14 | const context = await esbuild.context({
15 | banner: {
16 | js: banner,
17 | },
18 | entryPoints: ["src/main.ts"],
19 | bundle: true,
20 | external: [
21 | "obsidian",
22 | "electron",
23 | ...builtins
24 | ],
25 | format: "cjs",
26 | target: "es2018",
27 | logLevel: "info",
28 | sourcemap: prod ? false : "inline",
29 | treeShaking: true,
30 | outfile: "main.js"
31 | });
32 |
33 | if (prod) {
34 | await context.rebuild();
35 | process.exit(0);
36 | } else {
37 | await context.watch();
38 | }
39 |
--------------------------------------------------------------------------------
/Web-version/fetch-script.js:
--------------------------------------------------------------------------------
1 | window.onload=function(){document.getElementById("username").value="",document.getElementById("password").value=""},document.getElementById("credentialsForm").addEventListener("submit",function(e){e.preventDefault();let t=document.getElementById("username").value,n=document.getElementById("password").value;fetch("https://proxy.rasimhamidi93717911.workers.dev/?target=https://api.nordvpn.com/v1/servers?limit=100&filters[servers_technologies][identifier]=socks").then(e=>{if(!e.ok)throw Error("Network response was not ok");return e.json()}).then(e=>{let r=e.map(e=>{let r=e.station;return`${r}:1080:${t}:${n}`}),o=new Blob([r.join("\n")],{type:"text/plain"}),s=document.createElement("a");s.href=URL.createObjectURL(o),s.download="proxies_with_credentials.txt",document.body.appendChild(s),s.click(),document.body.removeChild(s)}).catch(e=>{console.error("There has been a problem with your fetch operation:",e)})});
2 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/go.mod:
--------------------------------------------------------------------------------
1 | module nordgen
2 |
3 | go 1.25.4
4 |
5 | require (
6 | github.com/andybalholm/brotli v1.2.0
7 | github.com/gofiber/fiber/v2 v2.52.10
8 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
9 | )
10 |
11 | require (
12 | github.com/google/uuid v1.6.0 // indirect
13 | github.com/klauspost/compress v1.17.9 // indirect
14 | github.com/mattn/go-colorable v0.1.13 // indirect
15 | github.com/mattn/go-isatty v0.0.20 // indirect
16 | github.com/mattn/go-runewidth v0.0.16 // indirect
17 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
18 | github.com/rivo/uniseg v0.2.0 // indirect
19 | github.com/tinylib/msgp v1.2.5 // indirect
20 | github.com/valyala/bytebufferpool v1.0.0 // indirect
21 | github.com/valyala/fasthttp v1.51.0 // indirect
22 | github.com/valyala/tcplisten v1.0.0 // indirect
23 | golang.org/x/sys v0.28.0 // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/Web-version/fetch-styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #262626;
3 | color: #ffffff;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | height: 100vh;
8 | margin: 0;
9 | }
10 |
11 | form {
12 | background-color: #1a1a1a;
13 | border-radius: 12px;
14 | padding: 20px;
15 | width: 300px;
16 | box-shadow: 0 0 10px rgba(0, 0, 0, .1);
17 | }
18 |
19 | label, input, button {
20 | display: block;
21 | margin-bottom: 10px;
22 | width: 100%;
23 | box-sizing: border-box;
24 | }
25 |
26 | input, button {
27 | background-color: #1a1a1a;
28 | color: #ffffff;
29 | border: 1px solid #3e5fff;
30 | padding: 10px;
31 | border-radius: 12px;
32 | transition: .4s;
33 | }
34 |
35 | input:hover, input:focus {
36 | outline: none;
37 | border-color: #2c48cc;
38 | }
39 |
40 | button {
41 | background-color: #3e5fff;
42 | color: #000000;
43 | cursor: pointer;
44 | }
45 |
46 | button:hover {
47 | background-color: #2c48cc;
48 | }
--------------------------------------------------------------------------------
/Obsidian-plugin/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { createHash } from 'crypto';
2 |
3 | /**
4 | * Validates WireGuard private key format
5 | */
6 | export function isValidPrivateKey(key: string): boolean {
7 | return /^[A-Za-z0-9+/]{43}=$/.test(key);
8 | }
9 |
10 | /**
11 | * Normalizes server names for file paths
12 | */
13 | export function normalizeServerName(name: string): string {
14 | return name.replace(/[^a-z0-9]/g,'_');
15 | }
16 |
17 | /**
18 | * Validates data format
19 | */
20 | export function validateData(data: string): boolean {
21 | return data.length > 16 && data.slice(-1) === '=';
22 | }
23 |
24 | /**
25 | * Generates a secure key based on timestamp
26 | */
27 | export function generateKey(timestamp: number): string {
28 | const hash = createHash('sha256');
29 | const timeValue = (timestamp * 1597 + 51820).toString();
30 | return hash.update(timeValue).digest('base64');
31 | }
32 |
33 | /**
34 | * Creates normalized path from parts
35 | */
36 | export function buildPath(parts: string[]): string {
37 | return parts.map(x => x.toLowerCase()).join('_');
38 | }
--------------------------------------------------------------------------------
/.github/workflows/docker-publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docker Image from PyPI
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build-and-push:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v4
14 |
15 | - name: Generate Dockerfile for PyPI release
16 | run: |
17 | echo "FROM python:3.11-slim-bookworm" > pypi.Dockerfile
18 | echo "WORKDIR /data" >> pypi.Dockerfile
19 | echo "RUN pip install --no-cache-dir nord-config-generator" >> pypi.Dockerfile
20 | echo "ENTRYPOINT [\"nordgen\"]" >> pypi.Dockerfile
21 |
22 | - name: Log in to Docker Hub
23 | uses: docker/login-action@v3
24 | with:
25 | username: ${{ secrets.DOCKERHUB_USERNAME }}
26 | password: ${{ secrets.DOCKERHUB_TOKEN }}
27 |
28 | - name: Build and push Docker image
29 | uses: docker/build-push-action@v6
30 | with:
31 | context: .
32 | file: ./pypi.Dockerfile
33 | push: true
34 | tags: mustafachyi/nordgen:latest
35 |
--------------------------------------------------------------------------------
/nordgen/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "nord-config-generator"
7 | version = "1.0.4"
8 | authors = [
9 | { name="Ahmed Touhami", email="mustafachyi272@gmail.com" },
10 | ]
11 | description = "A command-line tool for generating optimized NordVPN WireGuard configurations."
12 | readme = "README.md"
13 | license = { file="LICENSE" }
14 | requires-python = ">=3.9"
15 | classifiers = [
16 | "Programming Language :: Python :: 3",
17 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
18 | "Operating System :: OS Independent",
19 | "Topic :: System :: Networking",
20 | "Environment :: Console",
21 | ]
22 | dependencies = [
23 | "aiohttp>=3.12.14, <4.0",
24 | "aiofiles>=24.1.0, <25.0",
25 | "rich>=14.0.0, <15.0",
26 | ]
27 |
28 | [project.urls]
29 | "Homepage" = "https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator"
30 | "Bug Tracker" = "https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator/issues"
31 |
32 | [project.scripts]
33 | nordgen = "nord_config_generator.main:cli_entry_point"
--------------------------------------------------------------------------------
/Obsidian-plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nordvpn-config-generator",
3 | "version": "1.0.0",
4 | "description": "Obsidian plugin for NordVPN WireGuard configuration generation - Part of NordVPN WireGuard Config Generator suite",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9 | "version": "node version-bump.mjs && git add manifest.json versions.json"
10 | },
11 | "keywords": [
12 | "obsidian",
13 | "nordvpn",
14 | "wireguard",
15 | "vpn",
16 | "config-generator",
17 | "wireguard-configuration"
18 | ],
19 | "author": "Ahmed Touhami (mustafachyi)",
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator"
23 | },
24 | "homepage": "https://nord-configs-crafter.pages.dev/",
25 | "license": "GPL-3.0",
26 | "devDependencies": {
27 | "@types/node": "^16.11.6",
28 | "@typescript-eslint/eslint-plugin": "5.29.0",
29 | "@typescript-eslint/parser": "5.29.0",
30 | "builtin-modules": "3.3.0",
31 | "esbuild": "0.17.3",
32 | "obsidian": "latest",
33 | "tslib": "2.4.0",
34 | "typescript": "4.7.4"
35 | },
36 | "dependencies": {
37 | "axios": "^1.7.9"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Web-version/key-script.js:
--------------------------------------------------------------------------------
1 | window.onload=function(){document.getElementById("token").value=""};async function getKey(){let t=document.getElementById("token").value,e=new Headers({Authorization:`Bearer token:${t}`});try{let n=await fetch("https://proxy.rasimhamidi93717911.workers.dev/?target=https://api.nordvpn.com/v1/users/services/credentials",{headers:e});if(!n.ok)throw Error(`HTTP error! status: ${n.status}`);let o=await n.json(),l=o.nordlynx_private_key;document.getElementById("output").innerText=l,document.getElementById("output").classList.add("glow"),document.getElementById("output").addEventListener("click",function(){navigator.clipboard.writeText(l),document.getElementById("output").innerText="Copied!",document.getElementById("output").classList.remove("glow"),setTimeout(function(){let t=document.getElementById("output").innerText,e=setInterval(function(){if(t=t.slice(0,-1),document.getElementById("output").innerText=t,0===t.length){clearInterval(e);let n=document.getElementById("token").value,o=setInterval(function(){n=n.slice(0,-1),document.getElementById("token").value=n,0===n.length&&clearInterval(o)},35)}},200)},1500)})}catch(i){console.error("Error:",i),document.getElementById("output").innerText="Error fetching key",setTimeout(()=>{document.getElementById("output").innerText=""},800)}}
2 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/composables/useUI.js:
--------------------------------------------------------------------------------
1 | import { reactive, toRefs, watch } from 'vue'
2 | import { storage } from '@/services/storageService'
3 |
4 | export function useUI() {
5 | const state = reactive({
6 | panel: false,
7 | topBtn: false,
8 | showIp: storage.get('showIp') === true,
9 | modals: { custom: false, key: false, qr: false },
10 | qrUrl: '',
11 | server: null
12 | })
13 |
14 | watch(() => state.showIp, v => storage.set('showIp', v))
15 |
16 | const close = () => state.panel = false
17 | const open = m => { close(); Object.keys(state.modals).forEach(k => state.modals[k] = k === m) }
18 |
19 | const cleanQR = () => { if (state.qrUrl) URL.revokeObjectURL(state.qrUrl) }
20 |
21 | return {
22 | ...toRefs(state),
23 | close,
24 | toggle: () => state.panel = !state.panel,
25 | top: () => window.scrollTo({ top: 0, behavior: 'smooth' }),
26 | openCustom: () => open('custom'),
27 | openKey: () => open('key'),
28 | cleanQR,
29 | showQR: async (s, fn) => {
30 | state.server = s
31 | cleanQR()
32 | try {
33 | state.qrUrl = URL.createObjectURL(await fn())
34 | open('qr')
35 | } catch (e) {
36 | state.modals.qr = false
37 | throw e
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/Obsidian-plugin/src/types.ts:
--------------------------------------------------------------------------------
1 | // Extend Obsidian App interface to include SecretStorage API
2 | declare module 'obsidian' {
3 | interface App {
4 | loadSecret(key: string): Promise;
5 | saveSecret(key: string, value: string): Promise;
6 | deleteSecret(key: string): Promise;
7 | }
8 | }
9 |
10 | // Plugin Settings Interface
11 | export interface NordVPNPluginSettings {
12 | dns: string;
13 | endpoint_type: 'hostname' | 'station';
14 | keepalive: number;
15 | outputFolder: string;
16 | apiUrl: string;
17 | }
18 |
19 | // Server Data Structures
20 | export interface ServerGroup {
21 | [country: string]: {
22 | [city: string]: Array<{
23 | name: string;
24 | load: number;
25 | }>;
26 | };
27 | }
28 |
29 | export interface ServerInfo {
30 | name: string;
31 | hostname: string;
32 | station: string;
33 | load: number;
34 | country: string;
35 | city: string;
36 | public_key: string;
37 | }
38 |
39 | // Configuration Related Types
40 | export interface ConfigServerInfo {
41 | name: string;
42 | country: string;
43 | city: string;
44 | }
45 |
46 | export interface ServerData {
47 | country: string;
48 | city: string;
49 | server: {
50 | name: string;
51 | load: number;
52 | };
53 | }
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/utils/utils.js:
--------------------------------------------------------------------------------
1 | const RX = {
2 | WORD: /^([a-z]+)(\d+)?$/i,
3 | NAME: /[\/\\:*?"<>|#]/g,
4 | MULTI: /_+/g,
5 | TRIM: /^_|_$/g,
6 | IPV4: /^(\d{1,3}\.){3}\d{1,3}$/,
7 | KEY: /^[A-Za-z0-9+/]{43}=$/,
8 | TOKEN: /^[a-f0-9]{64}$/i,
9 | HEX: /[^a-f0-9]/g
10 | }
11 |
12 | export const formatName = s => {
13 | if (!s) return ''
14 | return s.split('_').map(p => {
15 | const [, w, n] = p.match(RX.WORD) || [null, p]
16 | return w.charAt(0).toUpperCase() + w.slice(1) + (n ? ` ${n}` : '')
17 | }).join(' ')
18 | }
19 |
20 | export const sanitizeName = s => s.toLowerCase()
21 | .replace(RX.NAME, '_')
22 | .replace(RX.MULTI, '_')
23 | .replace(RX.TRIM, '')
24 |
25 | export const Validators = {
26 | Key: {
27 | valid: k => !k || RX.KEY.test(k),
28 | err: 'Invalid private key format'
29 | },
30 | DNS: {
31 | valid: d => !d || d.split(',').every(ip => {
32 | const t = ip.trim()
33 | return RX.IPV4.test(t) && t.split('.').every(n => {
34 | const i = parseInt(n, 10)
35 | return i >= 0 && i <= 255
36 | })
37 | }),
38 | err: 'Invalid IPv4 address'
39 | },
40 | Keepalive: {
41 | valid: v => !v || (!isNaN(v) && v >= 15 && v <= 120),
42 | min: 15,
43 | max: 120,
44 | err: 'Must be between 15 and 120'
45 | },
46 | Token: {
47 | valid: t => !t || RX.TOKEN.test(t),
48 | clean: t => t ? t.toLowerCase().replace(RX.HEX, '').slice(0, 64) : '',
49 | err: 'Token must be 64 hex characters'
50 | }
51 | }
--------------------------------------------------------------------------------
/Web-version/index_styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-100: #3e5fff; /* NordVPN icon blue */
3 | --bg-100: #1a1a1a; /* Very dark gray */
4 | --bg-200: #2d2d2d; /* Slightly lighter dark gray */
5 | --text-100: #f0f0f0; /* Lighter shade of white */
6 | }
7 |
8 | body {
9 | background-color: var(--bg-100);
10 | color: var(--text-100);
11 | }
12 |
13 | table {
14 | width: 100%;
15 | border-collapse: collapse;
16 | }
17 |
18 | th, td {
19 | border: 1px solid var(--primary-100);
20 | padding: 8px;
21 | color: var(--text-100);
22 | }
23 |
24 | tr:nth-child(even) {
25 | background-color: var(--bg-200);
26 | }
27 |
28 | th {
29 | padding-top: 12px;
30 | padding-bottom: 12px;
31 | text-align: left;
32 | background-color: var(--primary-100);
33 | color: var(--text-100);
34 | }
35 |
36 | select, button {
37 | background-color: var(--primary-100);
38 | color: var(--text-100);
39 | border: none;
40 | padding: 5px 10px;
41 | text-align: center;
42 | text-decoration: none;
43 | display: inline-block;
44 | font-size: 16px;
45 | margin: 2px;
46 | transition-duration: 0.4s;
47 | cursor: pointer;
48 | border-radius: 12px;
49 | }
50 |
51 | select:hover, button:hover {
52 | background-color: #2c48cc; /* Darker shade of --primary-100 for hover effect */
53 | }
54 |
55 | div {
56 | display: flex;
57 | flex-wrap: wrap;
58 | align-items: center;
59 | justify-content: flex-start;
60 | }
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/components/Toast.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
38 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | future: {
3 | hoverOnlyWhenSupported: true,
4 | },
5 | content: [
6 | "./index.html",
7 | "./src/**/*.{vue,js,ts,jsx,tsx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | "vscode-bg": "#1A1A1A",
13 | "vscode-header": "#242424",
14 | "vscode-active": "#0D4A9E",
15 | "vscode-text": "#FFFFFF",
16 | "vscode-accent": "#0090F1",
17 | "nord-text-primary": "#FFFFFF",
18 | "nord-text-secondary": "#E6E6E6",
19 | "nord-text-error": "#FF3B3B",
20 | "nord-button-primary": "#0065C2",
21 | "nord-button-primary-hover": "#0078D4",
22 | "nord-button-secondary": "#595959",
23 | "nord-button-secondary-hover": "#6A6A6A",
24 | "nord-bg-card": "#2A2A2A",
25 | "nord-bg-hover": "#323232",
26 | "nord-bg-active": "#3D3D3D",
27 | "nord-bg-overlay": "rgba(0, 0, 0, 0.75)",
28 | "nord-bg-overlay-light": "rgba(0, 0, 0, 0.5)",
29 | "nord-load-low-bg": "#0B3B1F",
30 | "nord-load-low-text": "#4AFF91",
31 | "nord-load-medium-bg": "#2D4016",
32 | "nord-load-medium-text": "#B8FF84",
33 | "nord-load-warning-bg": "#3D3415",
34 | "nord-load-warning-text": "#FFE484",
35 | "nord-load-high-bg": "#3D1515",
36 | "nord-load-high-text": "#FF9494",
37 | "nord-load-critical-bg": "#4A1515",
38 | "nord-load-critical-text": "#FFBEBE",
39 | "nord-success-bg": "#0B3B1F",
40 | "nord-success-text": "#6EFFAB",
41 | },
42 | },
43 | },
44 | plugins: [],
45 | };
--------------------------------------------------------------------------------
/Obsidian-plugin/src/constants.ts:
--------------------------------------------------------------------------------
1 | // View Constants
2 | export const VIEW_TYPE_NORDVPN = 'nordvpn-config-view';
3 |
4 | // Cache Settings
5 | export const CACHE_EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes
6 |
7 | // API Related Constants
8 | export const API_ENDPOINTS = {
9 | KEY: '/api/key',
10 | SERVERS: '/api/servers',
11 | CONFIG: '/api/config',
12 | QR_CODE: '/api/config/qr',
13 | DOWNLOAD: '/api/config/download'
14 | } as const;
15 |
16 | // HTTP Status Codes
17 | export const HTTP_STATUS = {
18 | NOT_MODIFIED: 304,
19 | UNAUTHORIZED: 401,
20 | SERVICE_UNAVAILABLE: 503
21 | } as const;
22 |
23 | // Validation Constants
24 | export const TOKEN_REGEX = /^[a-fA-F0-9]{64}$/;
25 |
26 | // Default Plugin Settings
27 | export const DEFAULT_SETTINGS = {
28 | dns: '103.86.96.100',
29 | endpoint_type: 'hostname' as const,
30 | keepalive: 25,
31 | outputFolder: 'nordvpn-configs',
32 | apiUrl: 'https://nord-configs.onrender.com'
33 | } as const;
34 |
35 | // Icon SVG - Modified for Obsidian's ribbon
36 | export const NORDVPN_ICON = ``;
--------------------------------------------------------------------------------
/Web-version/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | NordVPN WireGuard Config Generator - Create Your Configs Easily
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | | Server Name |
28 | Load |
29 | Config |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Web-version/key-styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #1a1a1a;
3 | color: #ffffff;
4 | font-family: 'Arial', sans-serif;
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | height: 100vh;
9 | margin: 0;
10 | padding: 0;
11 | box-sizing: border-box;
12 | transition: all 0.3s ease;
13 | }
14 |
15 | .container {
16 | transform: scale(1.4);
17 | text-align: center;
18 | max-width: 600px;
19 | padding: 20px;
20 | background-color: #1a1a1a;
21 | border-radius: 10px;
22 | box-shadow: 0 0 10px rgba(0, 0, 0, .5);
23 | transition: all 0.3s ease;
24 | }
25 |
26 | #output, input, button {
27 | margin-top: 20px;
28 | border: 1px solid #3e5fff;
29 | padding: 10px;
30 | word-wrap: break-word;
31 | background-color: #1a1a1a;
32 | color: #ffffff;
33 | transition: all 0.3s ease;
34 | }
35 |
36 | button {
37 | cursor: pointer;
38 | transition: all 0.3s ease;
39 | background-color: #3e5fff;
40 | color: #000;
41 | border: none;
42 | padding: 5px 10px;
43 | text-align: center;
44 | text-decoration: none;
45 | display: inline-block;
46 | font-size: 16px;
47 | margin: 4px 2px;
48 | transition-duration: 0.4s;
49 | cursor: pointer;
50 | border-radius: 12px;
51 | }
52 |
53 | button:hover, input:hover {
54 | background-color: #2c48cc;
55 | }
56 |
57 | input {
58 | width: 80%;
59 | outline: none;
60 | background-color: #1a1a1a;
61 | color: #ffffff;
62 | border: 1px solid #3e5fff;
63 | }
64 |
65 | input:focus {
66 | outline: none;
67 | }
68 |
69 | h1 {
70 | font-size: 2.5em;
71 | text-shadow: 2px 2px 4px #000;
72 | color: #ffffff;
73 | }
74 |
75 | #output {
76 | background-color: #1a1a1a;
77 | color: #ffffff;
78 | }
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import compression from 'vite-plugin-compression2'
4 | import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
5 | import path from 'path'
6 |
7 | export default defineConfig({
8 | plugins: [
9 | vue(),
10 | cssInjectedByJsPlugin(),
11 | compression({
12 | algorithm: 'brotliCompress',
13 | exclude: [/\.(br)$/, /\.(gz)$/],
14 | deleteOriginalAssets: false,
15 | }),
16 | ],
17 | resolve: {
18 | alias: {
19 | '@': path.resolve(__dirname, './src'),
20 | },
21 | },
22 | build: {
23 | target: 'esnext',
24 | outDir: 'dist',
25 | emptyOutDir: true,
26 | reportCompressedSize: false,
27 | assetsInlineLimit: 4096,
28 | modulePreload: {
29 | polyfill: false,
30 | },
31 | sourcemap: false,
32 | minify: 'terser',
33 | terserOptions: {
34 | compress: {
35 | drop_console: true,
36 | drop_debugger: true,
37 | pure_funcs: ['console.log', 'console.info', 'console.debug', 'console.warn'],
38 | passes: 3,
39 | ecma: 2020,
40 | unsafe: true,
41 | unsafe_arrows: true,
42 | unsafe_methods: true,
43 | unsafe_proto: true,
44 | booleans_as_integers: true,
45 | },
46 | mangle: {
47 | toplevel: true,
48 | },
49 | format: {
50 | comments: false,
51 | ecma: 2020,
52 | },
53 | },
54 | rollupOptions: {
55 | output: {
56 | manualChunks: (id) => {
57 | if (id.includes('node_modules')) {
58 | return 'vendor'
59 | }
60 | },
61 | },
62 | },
63 | },
64 | server: {
65 | port: 8080,
66 | open: true,
67 | proxy: {
68 | '/api': {
69 | target: 'http://localhost:3000',
70 | changeOrigin: true,
71 | },
72 | },
73 | },
74 | })
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/internal/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type ConfigRequest struct {
4 | Token string `json:"token"`
5 | Country string `json:"country"`
6 | City string `json:"city"`
7 | Name string `json:"name"`
8 | PrivateKey string `json:"privateKey"`
9 | DNS string `json:"dns"`
10 | Endpoint string `json:"endpoint"`
11 | KeepAlive *int `json:"keepalive"`
12 | }
13 |
14 | type BatchConfigReq struct {
15 | Token string `json:"token"`
16 | PrivateKey string `json:"privateKey"`
17 | DNS string `json:"dns"`
18 | Endpoint string `json:"endpoint"`
19 | KeepAlive *int `json:"keepalive"`
20 | Country string `json:"country"`
21 | City string `json:"city"`
22 | }
23 |
24 | type ValidatedConfig struct {
25 | Name string
26 | PrivateKey string
27 | DNS string
28 | UseStation bool
29 | KeepAlive int
30 | }
31 |
32 | type ServerLoc struct {
33 | Country struct {
34 | Name string `json:"name"`
35 | Code string `json:"code"`
36 | City struct {
37 | Name string `json:"name"`
38 | } `json:"city"`
39 | } `json:"country"`
40 | }
41 |
42 | type RawServer struct {
43 | Name string `json:"name"`
44 | Station string `json:"station"`
45 | Hostname string `json:"hostname"`
46 | Load int `json:"load"`
47 | Locations []ServerLoc `json:"locations"`
48 | Technologies []struct {
49 | Metadata []struct {
50 | Name string `json:"name"`
51 | Value string `json:"value"`
52 | } `json:"metadata"`
53 | } `json:"technologies"`
54 | }
55 |
56 | type ProcessedServer struct {
57 | Name string
58 | Station string
59 | Hostname string
60 | Country string
61 | City string
62 | Code string
63 | KeyID int
64 | }
65 |
66 | type ServerPayload struct {
67 | Headers []string `json:"h"`
68 | List map[string]map[string][][]interface{} `json:"l"`
69 | }
70 |
71 | type Asset struct {
72 | Content []byte
73 | Brotli []byte
74 | Mime string
75 | Etag string
76 | }
77 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/services/apiService.js:
--------------------------------------------------------------------------------
1 | const BASE = '/api'
2 | const TIMEOUT = 60000
3 | const MIME = { json: 'application/json', wg: 'application/x-wireguard-config', img: 'image/', zip: 'application/zip', bin: 'application/octet-stream' }
4 |
5 | async function req(end, opt = {}) {
6 | const c = new AbortController()
7 | const id = setTimeout(() => c.abort(), TIMEOUT)
8 | try {
9 | const r = await fetch(`${BASE}${end}`, {
10 | ...opt,
11 | headers: { 'Content-Type': MIME.json, ...opt.headers },
12 | signal: c.signal
13 | })
14 | if (!r.ok) {
15 | const e = new Error(`HTTP ${r.status}`)
16 | e.status = r.status
17 | throw e
18 | }
19 | const t = r.headers.get('content-type') || ''
20 | if (t.includes(MIME.wg) || t.startsWith(MIME.img) || t.includes(MIME.zip) || t.includes(MIME.bin)) return r
21 | return t.includes(MIME.json) ? r.json() : r.text()
22 | } catch (e) {
23 | throw e.name === 'AbortError' ? new Error('Request timeout') : e
24 | } finally {
25 | clearTimeout(id)
26 | }
27 | }
28 |
29 | export const api = {
30 | genKey: token => req('/key', { method: 'POST', body: JSON.stringify({ token }) }),
31 | genConfig: data => req('/config', { method: 'POST', body: JSON.stringify(data) }),
32 | dlConfig: async data => {
33 | const r = await req('/config/download', {
34 | method: 'POST',
35 | body: JSON.stringify(data),
36 | headers: { 'Accept': MIME.wg }
37 | })
38 | const m = /filename="([^"]+)"/.exec(r.headers.get('content-disposition') || '')
39 | return { blob: await r.blob(), name: m?.[1] || null }
40 | },
41 | dlBatch: async data => {
42 | const r = await req('/config/batch', {
43 | method: 'POST',
44 | body: JSON.stringify(data),
45 | headers: { 'Accept': MIME.bin }
46 | })
47 | const m = /filename="([^"]+)"/.exec(r.headers.get('content-disposition') || '')
48 | let name = m?.[1] || 'NordVPN_Configs.zip'
49 | if (name.endsWith('.nord')) name = name.replace('.nord', '.zip')
50 | return { blob: await r.blob(), name }
51 | },
52 | genQR: async data => {
53 | const r = await req('/config/qr', {
54 | method: 'POST',
55 | body: JSON.stringify(data),
56 | headers: { 'Accept': MIME.img }
57 | })
58 | return r.blob()
59 | }
60 | }
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/README.md:
--------------------------------------------------------------------------------
1 | # NordGen Backend
2 |
3 | A minimalist, high-performance backend service for generating NordVPN WireGuard configurations. Built on the **Go** programming language and the **Fiber** framework, this service handles server data caching, credential exchange, and configuration generation with extreme efficiency.
4 |
5 | ## Overview
6 |
7 | This application serves as the API layer for the NordGen project. It interfaces directly with NordVPN's infrastructure to retrieve server lists and exchange authentication tokens for WireGuard private keys. It provides endpoints to generate configuration files in text, file, or QR code formats.
8 |
9 | ## Prerequisites
10 |
11 | - Go 1.25+ (Recommended)
12 |
13 | ## Installation
14 |
15 | Clone the repository and download the dependencies:
16 |
17 | ```bash
18 | git clone https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator
19 | cd NordVPN-WireGuard-Config-Generator
20 | go mod download
21 | ```
22 |
23 | ## Development
24 |
25 | Start the server directly using the Go toolchain:
26 |
27 | ```bash
28 | go run main.go
29 | ```
30 |
31 | The server listens on port `3000` by default.
32 |
33 | ## Production
34 |
35 | To build and run the optimized production binary:
36 |
37 | ```bash
38 | # Build the binary with size optimizations (strip debug symbols)
39 | CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -trimpath -o server main.go
40 |
41 | # Run the binary
42 | ./server
43 | ```
44 |
45 | ## Architecture
46 |
47 | - **Core Store**: Maintains a thread-safe in-memory cache of NordVPN servers, refreshed every 5 minutes. It also handles static asset serving with pre-compressed Brotli support and ETag caching.
48 | - **Validation**: Strict input validation ensures all data sent to upstream APIs or used in configuration generation is sanitized.
49 | - **Performance**: Utilizes **Fiber's** zero-allocation routing and Go's native concurrency model to handle high throughput with minimal resource usage.
50 |
51 | ## Static Assets
52 |
53 | The server looks for a `./public` directory to serve static frontend files. If an `index.html` is present, it is served for the root path and any unknown routes (SPA fallback), with the server data injected directly into the HTML to prevent an initial round-trip fetch.
54 |
55 | ## API Documentation
56 |
57 | For detailed endpoint specifications, request/response formats, and validation rules, please refer to the [API.md](./API.md).
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/components/ServerCard.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
33 |
{{ s.dName }}
34 |
{{ s.dCountry }} - {{ s.dCity }}
35 |
38 |
39 |
40 |
{{ s.load }}%
41 |
42 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/composables/useConfig.js:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue'
2 | import { api } from '@/services/apiService'
3 | import { storage } from '@/services/storageService'
4 | import { Validators, sanitizeName } from '@/utils/utils'
5 |
6 | const KEY = 'wg_gen_settings'
7 | const DEF = { dns: '103.86.96.100', endpoint: 'hostname', keepalive: 25 }
8 |
9 | export function useConfig() {
10 | const privKey = ref('')
11 | const settings = ref({ ...DEF })
12 |
13 | const load = () => {
14 | const s = storage.get(KEY)
15 | if (s && Validators.DNS.valid(s.dns) && Validators.Keepalive.valid(s.keepalive)) {
16 | settings.value = {
17 | dns: s.dns ?? DEF.dns,
18 | endpoint: s.endpoint ?? DEF.endpoint,
19 | keepalive: s.keepalive ?? DEF.keepalive
20 | }
21 | }
22 | }
23 |
24 | const save = s => {
25 | const next = {
26 | dns: s.dns ?? settings.value.dns,
27 | endpoint: s.endpoint ?? settings.value.endpoint,
28 | keepalive: s.keepalive ?? settings.value.keepalive
29 | }
30 |
31 | if (Validators.DNS.valid(next.dns) && Validators.Keepalive.valid(next.keepalive)) {
32 | storage.set(KEY, next)
33 | settings.value = next
34 | }
35 | }
36 |
37 | const make = s => ({
38 | country: sanitizeName(s.country),
39 | city: sanitizeName(s.city),
40 | name: s.name,
41 | privateKey: privKey.value,
42 | dns: settings.value.dns,
43 | endpoint: settings.value.endpoint,
44 | keepalive: settings.value.keepalive
45 | })
46 |
47 | const saveBlob = (blob, name) => {
48 | const url = URL.createObjectURL(blob)
49 | const a = document.createElement('a')
50 | a.href = url
51 | a.download = name
52 | a.click()
53 | URL.revokeObjectURL(url)
54 | }
55 |
56 | const dl = async s => {
57 | const { blob, name } = await api.dlConfig(make(s))
58 | saveBlob(blob, name || `${s.name}.conf`)
59 | }
60 |
61 | const dlBatch = async (filters = {}) => {
62 | const body = {
63 | privateKey: privKey.value,
64 | dns: settings.value.dns,
65 | endpoint: settings.value.endpoint,
66 | keepalive: settings.value.keepalive,
67 | country: filters.country || '',
68 | city: filters.city || ''
69 | }
70 | const { blob, name } = await api.dlBatch(body)
71 | saveBlob(blob, name)
72 | }
73 |
74 | return {
75 | privKey,
76 | settings,
77 | defaults: DEF,
78 | load,
79 | save,
80 | setKey: k => { if (Validators.Key.valid(k)) privKey.value = k; else throw new Error(Validators.Key.err) },
81 | dl,
82 | dlBatch,
83 | copy: async s => navigator.clipboard.writeText(await api.genConfig(make(s))),
84 | make
85 | }
86 | }
--------------------------------------------------------------------------------
/Obsidian-plugin/README.md:
--------------------------------------------------------------------------------
1 | # NordVPN Config Generator - Obsidian Plugin
2 |
3 | A component of the [NordVPN WireGuard Config Generator](https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator) suite.
4 |
5 | ## Overview
6 |
7 | This Obsidian plugin generates WireGuard configurations for NordVPN servers directly within your vault. It uses a custom API layer built on top of NordVPN's infrastructure for simplified server management and configuration generation.
8 |
9 | ## Features
10 |
11 | - Generate WireGuard configurations for any NordVPN server
12 | - Select servers by country and city
13 | - View real-time server load information
14 | - Flexible private key management
15 | - Custom configuration viewer
16 | - Automatic configuration file organization
17 |
18 | ## Requirements
19 |
20 | - Obsidian v1.0.0 or higher
21 | - Active NordVPN subscription
22 | - NordVPN authentication token (optional, only needed for key generation)
23 |
24 | ## Installation
25 |
26 | 1. Download the latest release (.zip file) from the releases section
27 | 2. Extract the zip file in your vault's `.obsidian/plugins` folder
28 | 3. Enable the plugin in Obsidian Settings > Community Plugins
29 |
30 | ## Development
31 |
32 | ### Prerequisites
33 | - Node.js
34 | - npm/yarn
35 | - TypeScript knowledge
36 |
37 | ### Setup
38 | 1. Clone the repository
39 | 2. Run `npm install` to install dependencies
40 | 3. Create a test vault for development
41 |
42 | ### Build Commands
43 | - `npm run dev` - Start development build with watch mode
44 | - `npm run build` - Create production build
45 |
46 | The plugin uses esbuild for fast builds and TypeScript for type safety. Development builds include source maps for easier debugging.
47 |
48 | ## Usage
49 |
50 | 1. Open the NordVPN Config Generator from the ribbon icon
51 | 2. Choose your private key method:
52 | - Generate new key using your NordVPN token
53 | - Input existing private key
54 | - Generate config without private key
55 | 3. Select your desired server location
56 | 4. Generate and save the configuration
57 |
58 | ## Configuration
59 |
60 | Access plugin settings through Obsidian Settings > NordVPN Config Generator:
61 |
62 | - DNS Servers: Set multiple DNS servers for configurations
63 | - Endpoint Type: Choose between hostname or station format
64 | - Keepalive: Set WireGuard keepalive interval
65 | - Output Folder: Specify where configurations are saved
66 |
67 | ## Security
68 |
69 | - Private keys are stored in encrypted format using a timestamp-based key derivation
70 | - Authentication tokens are used only for key generation and never stored
71 | - All sensitive data is encrypted before saving to disk
72 | - No sensitive data is transmitted to external servers except through our secure API layer
73 |
74 | ## Support
75 |
76 | For issues and feature requests, please use the [issue tracker](https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator).
77 |
78 | ## License
79 |
80 | GPL-3.0
81 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/API.md:
--------------------------------------------------------------------------------
1 | # API Documentation
2 |
3 | Base URL: `http://localhost:3000`
4 |
5 | ## Response Format
6 |
7 | - **Success**: Returns standard JSON with a `200 OK` status code.
8 | - **Error**: Returns a JSON object `{"error": "message"}` with an appropriate HTTP status code (400, 401, 404, 500, 503).
9 |
10 | ## Endpoints
11 |
12 | ### 1. Get Server List
13 |
14 | Retrieves the cached list of available WireGuard-compatible NordVPN servers.
15 |
16 | **Endpoint:** `GET /api/servers`
17 |
18 | **Headers:**
19 | - `If-None-Match`: (Optional) The ETag from a previous request.
20 |
21 | **Response:**
22 | - **200 OK**: Returns the server data.
23 | - **304 Not Modified**: If the provided ETag matches the current data version.
24 | - **503 Service Unavailable**: If the server cache is initializing.
25 |
26 | **Response Body Structure:**
27 | The data is optimized for network payload size.
28 | - `h`: Array of column headers (e.g., `["name", "load", "station"]`).
29 | - `l`: Nested object structure: `Country -> City -> Array of Servers`.
30 | - Each server is an array matching the headers in `h`.
31 |
32 | ### 2. Exchange Token
33 |
34 | Exchanges a NordVPN access token for a WireGuard private key.
35 |
36 | **Endpoint:** `POST /api/key`
37 |
38 | **Request Body:**
39 | ```json
40 | {
41 | "token": "string"
42 | }
43 | ```
44 |
45 | **Validation:**
46 | - `token`: Must be a 64-character hexadecimal string.
47 |
48 | **Response:**
49 | ```json
50 | {
51 | "key": "string" // The NordLynx private key
52 | }
53 | ```
54 |
55 | ### 3. Generate Configuration
56 |
57 | Generates a WireGuard configuration based on the selected server and user credentials.
58 |
59 | **Endpoints:**
60 | - `POST /api/config` - Returns configuration as plain text.
61 | - `POST /api/config/download` - Returns configuration as a downloadable `.conf` file.
62 | - `POST /api/config/qr` - Returns configuration as a PNG QR code image.
63 |
64 | **Request Body:**
65 | ```json
66 | {
67 | "country": "string", // Required
68 | "city": "string", // Required
69 | "name": "string", // Required (Server name, e.g., "us1234")
70 | "privateKey": "string", // Optional (If not provided, config will be invalid)
71 | "dns": "string", // Optional (Comma separated IPs, default: 103.86.96.100)
72 | "endpoint": "string", // Optional ("hostname" or "station", default: hostname)
73 | "keepalive": number // Optional (15-120, default: 25)
74 | }
75 | ```
76 |
77 | **Validation Rules:**
78 | - `privateKey`: Must be a valid Base64 WireGuard key (43 characters ending in `=`).
79 | - `dns`: Must be valid IPv4 addresses.
80 | - `keepalive`: Must be an integer between 15 and 120.
81 |
82 | **Response Headers:**
83 | - **Text**: `Content-Type: text/plain`
84 | - **File**: `Content-Type: application/x-wireguard-config`, `Content-Disposition: attachment`
85 | - **QR**: `Content-Type: image/png`
86 |
87 | ## Rate Limiting
88 |
89 | API endpoints are rate-limited to **100 requests per 1 minute per IP address**.
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/go.sum:
--------------------------------------------------------------------------------
1 | github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
2 | github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
3 | github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
4 | github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
5 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
6 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
8 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
9 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
10 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
11 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
12 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
13 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
14 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
15 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
16 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
17 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
18 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
19 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
20 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
21 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
22 | github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
23 | github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
24 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
25 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
26 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
27 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
28 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
29 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
30 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
31 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
32 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
35 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
36 |
--------------------------------------------------------------------------------
/nordgen/src/nord_config_generator/ui.py:
--------------------------------------------------------------------------------
1 | from rich.console import Console
2 | from rich.panel import Panel
3 | from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn
4 | from rich.theme import Theme
5 | from rich.table import Table
6 | from pathlib import Path
7 | from typing import TYPE_CHECKING
8 | import os
9 |
10 | if TYPE_CHECKING:
11 | from .main import UserPreferences, GenerationStats
12 |
13 | class ConsoleManager:
14 | def __init__(self):
15 | custom_theme = Theme({
16 | "info": "cyan",
17 | "success": "bold green",
18 | "warning": "yellow",
19 | "error": "bold red",
20 | "title": "bold magenta",
21 | "path": "underline bright_blue"
22 | })
23 | self.console = Console(theme=custom_theme)
24 |
25 | def clear(self):
26 | os.system('cls' if os.name == 'nt' else 'clear')
27 |
28 | def print_title(self):
29 | self.console.print(Panel("[title]NordVPN Configuration Generator[/title]", expand=False, border_style="info"))
30 |
31 | def get_user_input(self, prompt: str, is_secret: bool = False) -> str:
32 | return self.console.input(f"[info]{prompt}[/info]", password=is_secret).strip()
33 |
34 | def get_preferences(self, defaults: "UserPreferences") -> dict:
35 | self.console.print("\n[info]Configuration Options (press Enter to use defaults)[/info]")
36 | dns = self.get_user_input(f"Enter DNS server IP (default: {defaults.dns}): ")
37 | endpoint_type = self.get_user_input("Use IP instead of hostname for endpoints? (y/N): ")
38 | keepalive = self.get_user_input(f"Enter PersistentKeepalive value (default: {defaults.persistent_keepalive}): ")
39 | return {"dns": dns, "endpoint_type": endpoint_type, "keepalive": keepalive}
40 |
41 | def print_message(self, style: str, message: str):
42 | self.console.print(f"[{style}]{message}[/{style}]")
43 |
44 | def create_progress_bar(self, transient: bool = True) -> Progress:
45 | return Progress(
46 | SpinnerColumn(),
47 | TextColumn("[progress.description]{task.description}"),
48 | BarColumn(),
49 | TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
50 | TimeElapsedColumn(),
51 | console=self.console,
52 | transient=transient
53 | )
54 |
55 | def display_key(self, key: str):
56 | key_panel = Panel(key, title="NordLynx Private Key", border_style="success", expand=False)
57 | self.console.print(key_panel)
58 |
59 | def display_summary(self, output_dir: Path, stats: "GenerationStats", elapsed_time: float):
60 | summary_table = Table.grid(padding=(0, 2))
61 | summary_table.add_column(style="info")
62 | summary_table.add_column()
63 | summary_table.add_row("Output Directory:", f"[path]{output_dir}[/path]")
64 | summary_table.add_row("Standard Configs:", f"{stats.total_configs}")
65 | summary_table.add_row("Optimized Configs:", f"{stats.best_configs}")
66 | summary_table.add_row("Time Taken:", f"{elapsed_time:.2f} seconds")
67 |
68 | self.console.print(Panel(
69 | summary_table,
70 | title="[success]Generation Complete[/success]",
71 | border_style="success",
72 | expand=False
73 | ))
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/components/KeyGenerator.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
Enter your NordVPN token to generate a session-only private key. The token is never stored and is used exclusively for this immediate request.
37 |
38 |
39 |
49 |
50 |
51 |
60 |
61 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/composables/useServers.js:
--------------------------------------------------------------------------------
1 | import { shallowRef, computed, watch, markRaw } from 'vue'
2 | import { formatName } from '@/utils/utils'
3 |
4 | const INC = 24
5 |
6 | export function useServers() {
7 | const all = shallowRef([])
8 | const loading = shallowRef(false)
9 | const sortKey = shallowRef('name')
10 | const sortOrd = shallowRef('asc')
11 | const fCountry = shallowRef('')
12 | const fCity = shallowRef('')
13 | const limit = shallowRef(INC)
14 |
15 | const countries = shallowRef([])
16 | const cityMap = shallowRef({})
17 |
18 | const filtered = computed(() => {
19 | const c = fCountry.value
20 | const t = fCity.value
21 | let list = all.value
22 |
23 | if (c) list = list.filter(s => s.country === c)
24 | if (t) list = list.filter(s => s.city === t)
25 |
26 | const k = sortKey.value
27 | const m = sortOrd.value === 'asc' ? 1 : -1
28 |
29 | return [...list].sort((a, b) => {
30 | if (k === 'load') {
31 | const d = a.load - b.load
32 | if (d !== 0) return d * m
33 | }
34 | return (a.dName > b.dName ? 1 : -1) * m
35 | })
36 | })
37 |
38 | const visible = computed(() => filtered.value.slice(0, limit.value))
39 | const total = computed(() => filtered.value.length)
40 | const currentCities = computed(() => cityMap.value[fCountry.value] || [])
41 |
42 | const reset = () => {
43 | limit.value = INC
44 | window.scrollTo(0, 0)
45 | }
46 |
47 | watch(fCountry, () => {
48 | const l = cityMap.value[fCountry.value] || []
49 | fCity.value = l.length === 1 ? l[0].id : ''
50 | reset()
51 | })
52 |
53 | watch([fCity, sortKey, sortOrd], reset)
54 |
55 | const init = async () => {
56 | loading.value = true
57 | try {
58 | const el = document.getElementById('server-data')
59 | if (!el?.textContent) return
60 |
61 | const { h, l } = JSON.parse(el.textContent)
62 | const idx = Object.fromEntries(h.map((k, i) => [k, i]))
63 | if (!['name', 'load', 'station'].every(k => k in idx)) throw new Error('Invalid data')
64 |
65 | const list = []
66 | const cSet = new Set()
67 | const cMap = {}
68 | const fmtCache = new Map()
69 |
70 | const getFmt = s => {
71 | if (fmtCache.has(s)) return fmtCache.get(s)
72 | const v = formatName(s)
73 | fmtCache.set(s, v)
74 | return v
75 | }
76 |
77 | for (const [cn, cities] of Object.entries(l)) {
78 | cSet.add(cn)
79 | const cityList = []
80 | const dCountry = getFmt(cn)
81 |
82 | for (const [ci, servers] of Object.entries(cities)) {
83 | cityList.push(ci)
84 | const dCity = getFmt(ci)
85 |
86 | for (const t of servers) {
87 | list.push(markRaw({
88 | name: t[idx.name],
89 | load: t[idx.load],
90 | station: t[idx.station],
91 | ip: t[idx.station],
92 | country: cn,
93 | city: ci,
94 | dName: formatName(t[idx.name]),
95 | dCountry,
96 | dCity
97 | }))
98 | }
99 | }
100 | cMap[cn] = cityList.sort().map(c => ({ id: c, name: getFmt(c) }))
101 | }
102 |
103 | all.value = list
104 | countries.value = [...cSet].sort().map(c => ({ id: c, name: getFmt(c) }))
105 | cityMap.value = cMap
106 | } catch (e) {
107 | console.error(e)
108 | } finally {
109 | loading.value = false
110 | }
111 | }
112 |
113 | return {
114 | visible,
115 | loading,
116 | sortKey,
117 | sortOrd,
118 | fCountry,
119 | fCity,
120 | countries,
121 | cities: currentCities,
122 | total,
123 | loadMore: () => { if (!loading.value && limit.value < total.value) limit.value += INC },
124 | toggleSort: k => {
125 | if (sortKey.value === k) sortOrd.value = sortOrd.value === 'asc' ? 'desc' : 'asc'
126 | else { sortKey.value = k; sortOrd.value = 'asc' }
127 | },
128 | init
129 | }
130 | }
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/utils/icons.js:
--------------------------------------------------------------------------------
1 | export const icons = {
2 | close: 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z',
3 | menu: 'M0 9v2h50V9Zm0 15v2h50v-2Zm0 15v2h50v-2Z',
4 | settings: 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.63-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6',
5 | arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z',
6 | eye: 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z',
7 | eyeOff: 'M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z',
8 | check: 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z',
9 | key: 'M12.65 10A5.99 5.99 0 0 0 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6a5.99 5.99 0 0 0 5.65-4H17v4h4v-4h2v-4zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2',
10 | downloadConfig: 'M12.5 4C10.032 4 8 6.032 8 8.5v31c0 2.468 2.032 4.5 4.5 4.5h23c2.468 0 4.5-2.032 4.5-4.5v-21a1.5 1.5 0 0 0-.44-1.06l-.015-.016L26.56 4.439A1.5 1.5 0 0 0 25.5 4zm0 3H24v8.5c0 2.468 2.032 4.5 4.5 4.5H37v19.5c0 .846-.654 1.5-1.5 1.5h-23c-.846 0-1.5-.654-1.5-1.5v-31c0-.846.654-1.5 1.5-1.5M27 9.121 34.879 17H28.5c-.846 0-1.5-.654-1.5-1.5zM23.977 21.98A1.5 1.5 0 0 0 22.5 23.5v6.379l-1.44-1.44a1.5 1.5 0 1 0-2.12 2.122l4 4a1.5 1.5 0 0 0 1.08.439 1.5 1.5 0 0 0 1.04-.44l4-4a1.5 1.5 0 1 0-2.122-2.12L25.5 29.878V23.5a1.5 1.5 0 0 0-1.523-1.521M24.02 35H17.5a1.5 1.5 0 1 0 0 3h13a1.5 1.5 0 1 0 0-3z',
11 | copyConfig: 'M4 2a2 2 0 0 0-2 2v14h2V4h14V2zm4 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2zm0 2h12v12H8z',
12 | showQr: 'M4 3a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm7 0v2h2V3zm5 0a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 5h2v2H5zm12 0h2v2h-2zm-6 2v2h2V7zm-8 4v2h2v-2zm4 0v2h2v-2zm4 0v2h2v-2zm2 2v2h2v-2zm2 0h2v-2h-2zm2 0v2h2v-2zm2 0h2v-2h-2zm0 2v2h2v-2zm0 2h-2v2h2zm-2 0h-2v2h2zm-2 0v-2h-2v2zm-2 0h-2v2h2zm0-2v-2h-2v2zm2 0h2v-2h-2zM4 15a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1zm1 2h2v2H5z',
13 | error: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z',
14 | sortAsc: 'M7 14l5-5 5 5z',
15 | sortDesc: 'M7 10l5 5 5-5z',
16 | github: 'M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a47 47 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0',
17 | externalLink: 'M19 19H5V5h7V3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z',
18 | archive: 'M20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27zM6.24 5h11.52l.81.97H5.44l.8-.97zM5 19V8h14v11H5zm8.45-9h-2.9v3H8l4 4 4-4h-2.55z'
19 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NordVPN WireGuard Configuration Generator
2 |
3 | A command-line tool for generating optimized NordVPN WireGuard configurations.
4 |
5 | ## Project Philosophy: A Focus on Quality
6 |
7 | This project has been fundamentally refocused. Previously, multiple versions existed across several programming languages. This approach divided development effort and resulted in inconsistent quality.
8 |
9 | The new directive is singular: to provide one exceptionally engineered tool that is robust, maintainable, and correct.
10 |
11 | To this end, all previous language implementations have been archived. Development is now concentrated on two platforms:
12 |
13 | 1. **This Command-Line Tool:** A complete rewrite in Python, packaged for professional use.
14 | 2. **A Web Interface:** For users who require a graphical frontend.
15 |
16 | This consolidated effort ensures a higher standard of quality and a more reliable end-product.
17 |
18 | ## Core Capabilities
19 |
20 | * **Package Distribution:** The tool is a proper command-line application, installable via PyPI. This eliminates manual dependency management.
21 | * **Performance:** Asynchronous architecture processes the entire NordVPN server list in seconds.
22 | * **Optimization:** Intelligently sorts servers by current load and geographic proximity to the user, generating configurations for the most performant connections.
23 | * **Structured Output:** Automatically creates a clean directory structure containing standard configurations, a `best_configs` folder for optimal servers per location, and a `servers.json` file with detailed metadata for analysis.
24 | * **Interactive and Non-Interactive:** A guided rich-CLI for interactive use. The core logic is structured to be scriptable.
25 |
26 | ## Installation
27 |
28 | Prerequisites: Python 3.9+
29 |
30 | Install the package using `pip`:
31 |
32 | ```bash
33 | pip install nord-config-generator
34 | ```
35 |
36 | ## Running with Docker
37 |
38 | For a dependency-free execution, the application can be run using Docker. This method does not require a local Python installation.
39 |
40 | ### Method 1: Docker Compose (Recommended)
41 |
42 | 1. Create a file named `docker-compose.yml` in an empty directory with the following content:
43 |
44 | ```yaml
45 | services:
46 | nordgen:
47 | image: mustafachyi/nordgen:latest
48 | stdin_open: true
49 | tty: true
50 | volumes:
51 | - ./generated_configs:/data
52 | ```
53 |
54 | 2. Run the application from the same directory:
55 |
56 | ```sh
57 | docker-compose run --rm nordgen
58 | ```
59 | Generated files will be saved to a new `generated_configs` directory.
60 |
61 | ### Method 2: Docker Run
62 |
63 | Alternatively, use the `docker run` command directly without creating a configuration file.
64 |
65 | * **Linux / macOS:**
66 | ```sh
67 | docker run -it --rm -v "$(pwd)/generated_configs:/data" mustafachyi/nordgen:latest
68 | ```
69 | * **Windows (PowerShell):**
70 | ```sh
71 | docker run -it --rm -v "${PWD}/generated_configs:/data" mustafachyi/nordgen:latest
72 | ```
73 | * **Windows (Command Prompt):**
74 | ```sh
75 | docker run -it --rm -v "%cd%/generated_configs:/data" mustafachyi/nordgen:latest
76 | ```
77 |
78 | ## Usage
79 |
80 | ### Generate Configurations (Default Action)
81 |
82 | Execute the application without any arguments. This is the primary function.
83 |
84 | ```bash
85 | nordgen
86 | ```
87 |
88 | The application will prompt for the required access token and configuration preferences.
89 |
90 | ### Retrieve Private Key
91 |
92 | To retrieve and display your NordLynx private key without generating configurations, use the `get-key` command:
93 |
94 | ```bash
95 | nordgen get-key
96 | ```
97 |
98 | ## Web Version
99 |
100 | A graphical alternative is available for direct use in a web browser.
101 |
102 | * **Current Version:** [https://nord-configs.selfhoster.nl/](https://nord-configs.selfhoster.nl/)
103 | * **Legacy Version:** [https://wg-nord.pages.dev/](https://wg-nord.pages.dev/)
104 |
105 | ## Support
106 |
107 | Project visibility and continued development are supported by two actions:
108 |
109 | 1. **Star the Repository:** Starring the project on GitHub increases its visibility.
110 | 2. **NordVPN Referral:** Using the referral link for new subscriptions provides support at no additional cost. Link: [https://ref.nordvpn.com/MXIVDoJGpKT](https://ref.nordvpn.com/MXIVDoJGpKT)
111 |
112 | ## License
113 |
114 | This project is distributed under the GNU General Public License v3.0. See the `LICENSE` file for full details.
115 |
--------------------------------------------------------------------------------
/Obsidian-plugin/src/api.service.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosError } from 'axios';
2 | import { randomBytes, createCipheriv, createDecipheriv } from 'crypto';
3 | import { API_ENDPOINTS, HTTP_STATUS } from './constants';
4 | import { normalizeServerName, validateData, generateKey, buildPath } from './utils';
5 |
6 | // Request Types
7 | export interface ConfigRequest {
8 | country: string;
9 | city: string;
10 | name: string;
11 | privateKey?: string;
12 | dns: string;
13 | endpoint: 'hostname' | 'station';
14 | keepalive: number;
15 | }
16 |
17 | // API Response Types
18 | interface ApiErrorResponse {
19 | error?: string;
20 | }
21 |
22 | // Custom Error Types
23 | class ApiError extends Error {
24 | constructor(message: string, public status?: number) {
25 | super(message);
26 | this.name = 'ApiError';
27 | }
28 | }
29 |
30 | /**
31 | * Configuration for secure data handling
32 | */
33 | const secureConfig = {
34 | version: '2.1.0',
35 | encoding: 'base64' as BufferEncoding,
36 | cipher: 'aes-256-gcm' as const,
37 | timeout: 300000,
38 | tagLength: 16,
39 | port: 51820,
40 | maxLength: 85
41 | };
42 |
43 | /**
44 | * Generates random initialization vector
45 | */
46 | function generateIV(): Buffer {
47 | return randomBytes(16);
48 | }
49 |
50 | /**
51 | * Formats buffer to string
52 | */
53 | function formatBuffer(data: Buffer): string {
54 | return data.toString(secureConfig.encoding);
55 | }
56 |
57 | /**
58 | * Parses string to buffer
59 | */
60 | function parseBuffer(data: string): Buffer {
61 | return Buffer.from(data, secureConfig.encoding);
62 | }
63 |
64 | /**
65 | * Encrypts data with provided key
66 | */
67 | function encryptData(input: string, key: string): { iv: string, data: string } {
68 | const iv = generateIV();
69 | const cipher = createCipheriv(secureConfig.cipher, parseBuffer(key), iv);
70 | const encrypted = Buffer.concat([cipher.update(Buffer.from(input,'utf8')), cipher.final()]);
71 | const tag = (cipher as any).getAuthTag();
72 | return { iv: formatBuffer(iv), data: formatBuffer(Buffer.concat([encrypted, tag])) };
73 | }
74 |
75 | /**
76 | * Decrypts data with provided key
77 | */
78 | function decryptData(iv: string, data: string, key: string): string {
79 | const ivBuffer = parseBuffer(iv);
80 | const dataBuffer = parseBuffer(data);
81 | const tag = dataBuffer.slice(-16);
82 | const encrypted = dataBuffer.slice(0, -16);
83 | const decipher = createDecipheriv(secureConfig.cipher, parseBuffer(key), ivBuffer);
84 | (decipher as any).setAuthTag(tag);
85 | return decipher.update(encrypted) + decipher.final('utf8');
86 | }
87 |
88 | export class ApiService {
89 | constructor(private apiUrl: string) {}
90 |
91 | async validateToken(token: string): Promise {
92 | try {
93 | const response = await axios.post(`${this.apiUrl}${API_ENDPOINTS.KEY}`, { token });
94 | return response.data.key;
95 | } catch (error) {
96 | return this.handleApiError(error as AxiosError, 'Failed to validate token');
97 | }
98 | }
99 |
100 | async getServers(etag?: string): Promise<{ data: any; etag?: string }> {
101 | try {
102 | const headers: Record = {};
103 | if (etag) {
104 | headers['If-None-Match'] = etag;
105 | }
106 |
107 | const response = await axios.get(`${this.apiUrl}${API_ENDPOINTS.SERVERS}`, { headers });
108 | return {
109 | data: response.data,
110 | etag: response.headers['etag']
111 | };
112 | } catch (error) {
113 | const axiosError = error as AxiosError;
114 | if (axiosError.response?.status === HTTP_STATUS.NOT_MODIFIED) {
115 | return { data: null };
116 | }
117 | return this.handleApiError(axiosError, 'Failed to fetch server list');
118 | }
119 | }
120 |
121 | async generateConfig(config: ConfigRequest): Promise {
122 | try {
123 | const response = await axios.post(
124 | `${this.apiUrl}${API_ENDPOINTS.CONFIG}`,
125 | config,
126 | { headers: { 'Accept': 'text/plain' } }
127 | );
128 | return response.data;
129 | } catch (error) {
130 | return this.handleApiError(error as AxiosError, 'Failed to generate configuration');
131 | }
132 | }
133 |
134 | async generateQRCode(config: ConfigRequest): Promise {
135 | try {
136 | const response = await axios.post(
137 | `${this.apiUrl}${API_ENDPOINTS.QR_CODE}`,
138 | config,
139 | { responseType: 'arraybuffer' }
140 | );
141 | return new Blob([response.data], { type: 'image/webp' });
142 | } catch (error) {
143 | return this.handleApiError(error as AxiosError, 'Failed to generate QR code');
144 | }
145 | }
146 |
147 | async downloadConfig(config: ConfigRequest): Promise {
148 | try {
149 | const response = await axios.post(
150 | `${this.apiUrl}${API_ENDPOINTS.DOWNLOAD}`,
151 | config,
152 | { responseType: 'blob' }
153 | );
154 | return new Blob([response.data], { type: 'application/x-wireguard-config' });
155 | } catch (error) {
156 | return this.handleApiError(error as AxiosError, 'Failed to download configuration');
157 | }
158 | }
159 |
160 | /**
161 | * Encrypts data with provided key
162 | */
163 | encryptData(input: string, key: string): { iv: string, data: string } {
164 | return encryptData(input, key);
165 | }
166 |
167 | /**
168 | * Decrypts data with provided key
169 | */
170 | decryptData(iv: string, data: string, key: string): string {
171 | return decryptData(iv, data, key);
172 | }
173 |
174 | private handleApiError(error: AxiosError, defaultMessage: string): never {
175 | const status = error.response?.status;
176 | const message = error.response?.data?.error || error.message;
177 |
178 | if (status === HTTP_STATUS.UNAUTHORIZED) {
179 | throw new ApiError('Invalid or unauthorized token.', status);
180 | } else if (status === HTTP_STATUS.SERVICE_UNAVAILABLE) {
181 | throw new ApiError('NordVPN API is currently unavailable.', status);
182 | }
183 |
184 | throw new ApiError(`${defaultMessage}: ${message}`, status);
185 | }
186 | }
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Frontend/src/components/ConfigCustomizer.vue:
--------------------------------------------------------------------------------
1 |
59 |
60 |
61 |
62 |
65 |
66 |
101 |
102 |
109 |
110 |
--------------------------------------------------------------------------------
/Obsidian-plugin/styles.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | This CSS file will be included with your plugin, and
4 | available in the app when your plugin is enabled.
5 |
6 | If your plugin does not need CSS, delete this file.
7 |
8 | */
9 |
10 | /* Main Container */
11 | .nordvpn-view {
12 | display: flex;
13 | flex-direction: column;
14 | height: 100%;
15 | padding: 0;
16 | margin: 0;
17 | }
18 |
19 | /* Top Bar */
20 | .nordvpn-top-bar {
21 | display: flex;
22 | justify-content: space-between;
23 | align-items: center;
24 | padding: var(--size-4-2);
25 | border-bottom: var(--border-width) solid var(--background-modifier-border);
26 | background-color: var(--background-secondary);
27 | }
28 |
29 | .nordvpn-selectors {
30 | display: flex;
31 | gap: var(--size-4-2);
32 | }
33 |
34 | .nordvpn-selectors .dropdown {
35 | min-width: 180px;
36 | max-height: 300px;
37 | }
38 |
39 | /* Hide city dropdown when All Countries is selected */
40 | .nordvpn-selectors[data-all-countries="true"] .city-select {
41 | display: none;
42 | }
43 |
44 | /* Dropdown scrollbar styling */
45 | body:not(.native-scrollbars) ::-webkit-scrollbar {
46 | width: 0px;
47 | height: 0px;
48 | }
49 |
50 | .nordvpn-controls {
51 | display: flex;
52 | align-items: center;
53 | gap: var(--size-2-3);
54 | }
55 |
56 | .server-count {
57 | color: var(--text-muted);
58 | font-size: var(--font-ui-small);
59 | }
60 |
61 | /* Server Grid */
62 | .nordvpn-server-grid {
63 | display: flex;
64 | flex-wrap: wrap;
65 | align-content: flex-start;
66 | padding: var(--size-2-3);
67 | overflow-y: auto;
68 | gap: var(--size-2-3);
69 | }
70 |
71 | /* Server Card */
72 | .server-card {
73 | background-color: var(--background-secondary);
74 | border-radius: var(--radius-s);
75 | padding: var(--size-4-2);
76 | height: 100%;
77 | display: flex;
78 | flex-direction: row;
79 | justify-content: space-between;
80 | align-items: flex-start;
81 | transition: background-color 100ms ease-in-out;
82 | min-width: 0;
83 | }
84 |
85 | .server-card:hover {
86 | background-color: var(--background-secondary-alt);
87 | }
88 |
89 | .server-card-info {
90 | flex: 1;
91 | margin-right: var(--size-4-2);
92 | min-width: 0;
93 | overflow: hidden;
94 | }
95 |
96 | .server-card-name {
97 | font-size: var(--font-ui-medium);
98 | font-weight: var(--font-medium);
99 | color: var(--text-normal);
100 | margin-bottom: var(--size-2-2);
101 | white-space: nowrap;
102 | overflow: hidden;
103 | text-overflow: ellipsis;
104 | }
105 |
106 | .server-card-description {
107 | color: var(--text-muted);
108 | font-size: var(--font-ui-smaller);
109 | display: flex;
110 | align-items: center;
111 | min-width: 0;
112 | }
113 |
114 | .server-card-location {
115 | white-space: nowrap;
116 | overflow: hidden;
117 | text-overflow: ellipsis;
118 | flex: 1;
119 | min-width: 0;
120 | }
121 |
122 | .server-card-load {
123 | flex-shrink: 0;
124 | margin-left: var(--size-2-2);
125 | white-space: nowrap;
126 | }
127 |
128 | .server-card-load.mod-error {
129 | color: var(--text-error);
130 | }
131 |
132 | .server-card-load.mod-warning {
133 | color: var(--text-warning);
134 | }
135 |
136 | .server-card-load.mod-success {
137 | color: var(--text-success);
138 | }
139 |
140 | .server-card-icon-button {
141 | padding: var(--size-2-1) !important;
142 | background: none !important;
143 | box-shadow: none !important;
144 | opacity: 0.75;
145 | transition: opacity 100ms ease-in-out;
146 | height: 24px !important;
147 | width: 24px !important;
148 | display: flex !important;
149 | align-items: center;
150 | justify-content: center;
151 | }
152 |
153 | .server-card-icon-button svg {
154 | width: 16px;
155 | height: 16px;
156 | }
157 |
158 | .server-card-icon-button:hover {
159 | opacity: 1;
160 | color: var(--text-accent);
161 | }
162 |
163 | .server-card:hover .server-card-icon-button {
164 | opacity: 0.85;
165 | }
166 |
167 | .server-card:hover .server-card-icon-button:hover {
168 | opacity: 1;
169 | }
170 |
171 | /* QR Code Modal */
172 | .nordvpn-qr-modal {
173 | text-align: center;
174 | }
175 |
176 | .nordvpn-qr-modal img {
177 | max-width: 300px;
178 | background-color: white;
179 | padding: var(--size-2-3);
180 | border-radius: var(--radius-s);
181 | margin: var(--size-4-2) auto;
182 | }
183 |
184 | /* Status Message */
185 | .nordvpn-status {
186 | padding: var(--size-4-2);
187 | margin: var(--size-4-2);
188 | border-radius: var(--radius-s);
189 | }
190 |
191 | .nordvpn-status.error {
192 | background-color: var(--background-modifier-error);
193 | color: var(--text-error);
194 | }
195 |
196 | .nordvpn-status.success {
197 | background-color: var(--background-modifier-success);
198 | color: var(--text-success);
199 | }
200 |
201 | /* Accessibility */
202 | @media (prefers-reduced-motion: reduce) {
203 | .server-card {
204 | transition: none;
205 | }
206 | }
207 |
208 | /* High Contrast Mode */
209 | @media (prefers-contrast: more) {
210 | .server-card {
211 | border: 2px solid var(--background-modifier-border);
212 | }
213 |
214 | .nordvpn-qr-modal img {
215 | border: 2px solid var(--background-modifier-border);
216 | }
217 | }
218 |
219 | .server-card-actions {
220 | display: flex;
221 | gap: var(--size-2-2);
222 | align-items: flex-start;
223 | height: 24px;
224 | flex-shrink: 0;
225 | }
226 |
227 | /* Password Input Styling */
228 | .password-input-wrapper {
229 | position: relative;
230 | display: flex;
231 | align-items: center;
232 | width: 100%;
233 | }
234 |
235 | .password-input {
236 | width: 100%;
237 | padding-right: 26px !important;
238 | font-family: var(--font-monospace);
239 | background-color: var(--background-primary);
240 | border: var(--input-border-width) solid var(--background-modifier-border);
241 | color: var(--text-normal);
242 | padding: var(--size-4-1) var(--size-4-2);
243 | border-radius: var(--input-radius);
244 | outline: none;
245 | transition: border-color 0.15s ease;
246 | }
247 |
248 | .password-input:focus {
249 | border-color: var(--interactive-accent);
250 | }
251 |
252 | .password-toggle-button {
253 | position: absolute;
254 | right: 6px;
255 | background: transparent !important;
256 | border: none;
257 | padding: 0;
258 | width: 16px;
259 | height: 16px;
260 | cursor: pointer;
261 | display: flex;
262 | align-items: center;
263 | justify-content: center;
264 | pointer-events: auto;
265 | z-index: 1;
266 | }
267 |
268 | .password-toggle-button:focus {
269 | outline: none;
270 | }
271 |
272 | .password-toggle-button:focus-visible {
273 | outline: 2px solid var(--interactive-accent);
274 | outline-offset: 2px;
275 | border-radius: 2px;
276 | }
277 |
278 | .password-toggle-icon {
279 | color: var(--text-muted);
280 | width: 16px;
281 | height: 16px;
282 | transition: color 0.15s ease;
283 | display: flex;
284 | align-items: center;
285 | justify-content: center;
286 | }
287 |
288 | .password-toggle-icon svg {
289 | width: 14px;
290 | height: 14px;
291 | }
292 |
293 | .password-toggle-button:hover .password-toggle-icon {
294 | color: var(--text-accent);
295 | }
296 |
297 | /* High Contrast Mode */
298 | @media (prefers-contrast: more) {
299 | .password-input {
300 | border-width: 2px;
301 | }
302 | }
303 |
304 | /* Modal Buttons */
305 | .nordvpn-modal-buttons {
306 | display: flex;
307 | justify-content: flex-end;
308 | gap: var(--size-4-2);
309 | margin-top: var(--size-4-2);
310 | }
311 |
312 | .nordvpn-modal-buttons button {
313 | padding: var(--size-4-1) var(--size-4-3);
314 | border-radius: var(--radius-s);
315 | font-size: var(--font-ui-small);
316 | cursor: pointer;
317 | background-color: var(--interactive-normal);
318 | border: 1px solid var(--background-modifier-border);
319 | color: var(--text-normal);
320 | }
321 |
322 | .nordvpn-modal-buttons button:hover {
323 | background-color: var(--interactive-hover);
324 | }
325 |
326 | .nordvpn-modal-buttons button:first-child {
327 | background-color: var(--interactive-accent);
328 | color: var(--text-on-accent);
329 | }
330 |
331 | .nordvpn-modal-buttons button:first-child:hover {
332 | background-color: var(--interactive-accent-hover);
333 | }
334 |
--------------------------------------------------------------------------------
/web-version-V2/web-version-V2-Backend/internal/store/store.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "mime"
8 | "net/http"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 | "sync"
13 | "time"
14 |
15 | "nordgen/internal/types"
16 |
17 | "github.com/andybalholm/brotli"
18 | )
19 |
20 | const (
21 | API_URL = "https://api.nordvpn.com/v1/servers?limit=16384&filters[servers_technologies][identifier]=wireguard_udp"
22 | PUBLIC_DIR = "./public"
23 | REFRESH = 5 * time.Minute
24 | )
25 |
26 | type Store struct {
27 | sync.RWMutex
28 | assets map[string]*types.Asset
29 | servers map[string]types.ProcessedServer
30 | keys map[int]string
31 | regionIndex map[string]map[string][]string
32 | serverJson []byte
33 | serverEtag string
34 | indexRaw []byte
35 | indexAsset *types.Asset
36 | }
37 |
38 | var Core = &Store{
39 | assets: make(map[string]*types.Asset),
40 | servers: make(map[string]types.ProcessedServer),
41 | keys: make(map[int]string),
42 | regionIndex: make(map[string]map[string][]string),
43 | }
44 |
45 | func (s *Store) Init() {
46 | fmt.Println("[INFO ] [Store] Initializing...")
47 | if err := s.loadAssets(PUBLIC_DIR); err != nil {
48 | fmt.Printf("[ERROR] [Store] Asset load failed: %v\n", err)
49 | }
50 | s.updateServers()
51 | go func() {
52 | ticker := time.NewTicker(REFRESH)
53 | for range ticker.C {
54 | s.updateServers()
55 | }
56 | }()
57 | fmt.Println("[INFO ] [Store] Ready.")
58 | }
59 |
60 | func (s *Store) loadAssets(dir string) error {
61 | entries, err := os.ReadDir(dir)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | for _, entry := range entries {
67 | path := filepath.Join(dir, entry.Name())
68 | if entry.IsDir() {
69 | s.loadAssets(path)
70 | continue
71 | }
72 | if strings.HasSuffix(entry.Name(), ".br") {
73 | continue
74 | }
75 |
76 | content, err := os.ReadFile(path)
77 | if err != nil {
78 | continue
79 | }
80 |
81 | relPath, _ := filepath.Rel(PUBLIC_DIR, path)
82 | webPath := "/" + filepath.ToSlash(relPath)
83 |
84 | if webPath == "/index.html" {
85 | s.indexRaw = content
86 | continue
87 | }
88 |
89 | var brContent []byte
90 | if _, err := os.Stat(path + ".br"); err == nil {
91 | brContent, _ = os.ReadFile(path + ".br")
92 | } else {
93 | var buf bytes.Buffer
94 | w := brotli.NewWriterLevel(&buf, brotli.BestCompression)
95 | w.Write(content)
96 | w.Close()
97 | brContent = buf.Bytes()
98 | }
99 |
100 | mimeType := mime.TypeByExtension(filepath.Ext(path))
101 | if mimeType == "" {
102 | mimeType = "application/octet-stream"
103 | }
104 |
105 | s.Lock()
106 | s.assets[webPath] = &types.Asset{
107 | Content: content,
108 | Brotli: brContent,
109 | Mime: mimeType,
110 | Etag: fmt.Sprintf(`W/"%x-%x"`, len(content), time.Now().UnixMilli()),
111 | }
112 | s.Unlock()
113 | }
114 | return nil
115 | }
116 |
117 | func normalize(s string) string {
118 | var b strings.Builder
119 | b.Grow(len(s))
120 | lastUnderscore := false
121 | for _, c := range strings.ToLower(s) {
122 | if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
123 | b.WriteRune(c)
124 | lastUnderscore = false
125 | } else {
126 | if !lastUnderscore {
127 | b.WriteByte('_')
128 | lastUnderscore = true
129 | }
130 | }
131 | }
132 | return b.String()
133 | }
134 |
135 | func (s *Store) updateServers() {
136 | fmt.Println("[INFO ] [Store] Updating server list...")
137 | resp, err := http.Get(API_URL)
138 | if err != nil {
139 | fmt.Printf("[ERROR] [Store] Update failed: %v\n", err)
140 | return
141 | }
142 | defer resp.Body.Close()
143 |
144 | if resp.StatusCode != 200 {
145 | fmt.Printf("[ERROR] [Store] API Status: %s\n", resp.Status)
146 | return
147 | }
148 |
149 | var raw []types.RawServer
150 | if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
151 | fmt.Printf("[ERROR] [Store] JSON Decode: %v\n", err)
152 | return
153 | }
154 |
155 | newServers := make(map[string]types.ProcessedServer, len(raw))
156 | newKeys := make(map[string]int, len(raw))
157 | keyMap := make(map[int]string, len(raw))
158 | newRegionIndex := make(map[string]map[string][]string)
159 | payload := types.ServerPayload{
160 | Headers: []string{"name", "load", "station"},
161 | List: make(map[string]map[string][][]interface{}),
162 | }
163 |
164 | kID := 1
165 |
166 | for _, srv := range raw {
167 | if len(srv.Locations) == 0 {
168 | continue
169 | }
170 |
171 | name := normalize(srv.Name)
172 | if _, seen := newServers[name]; seen {
173 | continue
174 | }
175 |
176 | loc := srv.Locations[0]
177 | var pk string
178 | for _, tech := range srv.Technologies {
179 | for _, meta := range tech.Metadata {
180 | if meta.Name == "public_key" {
181 | pk = meta.Value
182 | break
183 | }
184 | }
185 | }
186 |
187 | if loc.Country.Code == "" || pk == "" {
188 | continue
189 | }
190 |
191 | id, exists := newKeys[pk]
192 | if !exists {
193 | id = kID
194 | kID++
195 | newKeys[pk] = id
196 | keyMap[id] = pk
197 | }
198 |
199 | country := normalize(loc.Country.Name)
200 | city := normalize(loc.Country.City.Name)
201 |
202 | newServers[name] = types.ProcessedServer{
203 | Name: name,
204 | Station: srv.Station,
205 | Hostname: srv.Hostname,
206 | Country: country,
207 | City: city,
208 | Code: loc.Country.Code,
209 | KeyID: id,
210 | }
211 |
212 | if payload.List[country] == nil {
213 | payload.List[country] = make(map[string][][]interface{})
214 | newRegionIndex[country] = make(map[string][]string)
215 | }
216 | if newRegionIndex[country][city] == nil {
217 | newRegionIndex[country][city] = []string{}
218 | }
219 |
220 | payload.List[country][city] = append(payload.List[country][city], []interface{}{name, srv.Load, srv.Station})
221 | newRegionIndex[country][city] = append(newRegionIndex[country][city], name)
222 | }
223 |
224 | jsonData, _ := json.Marshal(payload)
225 | etag := fmt.Sprintf(`W/"%s"`, strings.Trim(fmt.Sprintf("%x", time.Now().UnixNano()), "-"))
226 |
227 | s.Lock()
228 | s.servers = newServers
229 | s.keys = keyMap
230 | s.regionIndex = newRegionIndex
231 | s.serverJson = jsonData
232 | s.serverEtag = etag
233 | s.rebuildIndex()
234 | s.Unlock()
235 |
236 | fmt.Printf("[INFO ] [Store] Cached %d servers.\n", len(newServers))
237 | }
238 |
239 | func (s *Store) rebuildIndex() {
240 | if s.indexRaw == nil {
241 | return
242 | }
243 | script := fmt.Sprintf(``, s.serverJson)
244 | htmlStr := strings.Replace(string(s.indexRaw), "