From cf7d55d71a8a8b2ad88370a46881e81d826b81b0 Mon Sep 17 00:00:00 2001 From: Per-Arne Date: Sat, 9 May 2020 23:13:05 +0200 Subject: [PATCH 01/29] * Breaking changes: Must remove subnet from client configurations in dashboard * Added subnet support as described in #11 --- wg_dashboard_backend/main.py | 2 +- .../versions/004_create_server_subnet.py | 21 +++++++++++++++++++ wg_dashboard_backend/models.py | 1 + wg_dashboard_backend/routers/v1/peer.py | 4 ++-- wg_dashboard_backend/schemas.py | 1 + wg_dashboard_backend/templates/peer.j2 | 2 +- wg_dashboard_backend/templates/server.j2 | 4 ++-- .../src/app/interfaces/server.ts | 1 + .../add-server/add-server.component.html | 15 +++++++++++-- .../add-server/add-server.component.ts | 18 +++++++++++----- .../app/page/dashboard/dashboard.module.ts | 2 ++ .../dashboard/server/server.component.html | 2 +- .../app/validators/ip-address.validator.ts | 2 +- 13 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 wg_dashboard_backend/migrations/versions/004_create_server_subnet.py diff --git a/wg_dashboard_backend/main.py b/wg_dashboard_backend/main.py index 00a0fce..a876841 100644 --- a/wg_dashboard_backend/main.py +++ b/wg_dashboard_backend/main.py @@ -37,7 +37,7 @@ _db: Session = SessionLocal() if not database_exists(engine.url): ADMIN_USERNAME = os.getenv("ADMIN_USERNAME") if not ADMIN_USERNAME: - raise RuntimeError("Database does not exist and no ADMIN_USER is set") + raise RuntimeError("Database does not exist and no ADMIN_USERNAME is set") ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD") diff --git a/wg_dashboard_backend/migrations/versions/004_create_server_subnet.py b/wg_dashboard_backend/migrations/versions/004_create_server_subnet.py new file mode 100644 index 0000000..539a81d --- /dev/null +++ b/wg_dashboard_backend/migrations/versions/004_create_server_subnet.py @@ -0,0 +1,21 @@ +from sqlalchemy import * +from migrate import * + + +def upgrade(migrate_engine): + try: + meta = MetaData(bind=migrate_engine) + server = Table('server', meta, autoload=True) + subnet = Column('subnet', Text) + subnet.create(server) + except: + pass + + +def downgrade(migrate_engine): + try: + meta = MetaData(bind=migrate_engine) + server = Table('server', meta, autoload=True) + server.c.subnet.drop() + except: + pass diff --git a/wg_dashboard_backend/models.py b/wg_dashboard_backend/models.py index 0e4a07d..0b615d5 100644 --- a/wg_dashboard_backend/models.py +++ b/wg_dashboard_backend/models.py @@ -21,6 +21,7 @@ class WGServer(Base): id = Column(Integer, primary_key=True, index=True) interface = Column(sqlalchemy.String, unique=True, index=True) + subnet = Column(sqlalchemy.Integer, nullable=False) address = Column(sqlalchemy.String, unique=True) listen_port = Column(sqlalchemy.String, unique=True) private_key = Column(sqlalchemy.String) diff --git a/wg_dashboard_backend/routers/v1/peer.py b/wg_dashboard_backend/routers/v1/peer.py index c81aa08..c49c903 100644 --- a/wg_dashboard_backend/routers/v1/peer.py +++ b/wg_dashboard_backend/routers/v1/peer.py @@ -21,7 +21,7 @@ def add_peer( server = schemas.WGServer(interface=peer_add.server_interface).from_db(sess) peer = schemas.WGPeer(server_id=server.id) - address_space = set(ipaddress.ip_network(server.address, strict=False).hosts()) + address_space = set(ipaddress.ip_network(f"{server.address}/{server.subnet}", strict=False).hosts()) occupied_space = set() # Try add server IP to list. @@ -41,7 +41,7 @@ def add_peer( address_space -= occupied_space # Select first available address - peer.address = str(list(sorted(address_space)).pop(0)) + "/32" + peer.address = str(list(sorted(address_space)).pop(0)) # Private public key generation keys = script.wireguard.generate_keys() diff --git a/wg_dashboard_backend/schemas.py b/wg_dashboard_backend/schemas.py index 6a53614..d98c08a 100644 --- a/wg_dashboard_backend/schemas.py +++ b/wg_dashboard_backend/schemas.py @@ -144,6 +144,7 @@ class PSK(GenericModel): class WGServer(GenericModel): id: int = None address: str = None + subnet: int = None interface: str listen_port: int = None endpoint: str = None diff --git a/wg_dashboard_backend/templates/peer.j2 b/wg_dashboard_backend/templates/peer.j2 index c02656a..5639bff 100644 --- a/wg_dashboard_backend/templates/peer.j2 +++ b/wg_dashboard_backend/templates/peer.j2 @@ -1,5 +1,5 @@ [Interface] -Address = {{ data.peer.address.replace("/32", "/24") }} +Address = {{ data.peer.address }}/{{ data.server.subnet }} PrivateKey = {{ data.peer.private_key }} DNS = {{ data.peer.dns }} diff --git a/wg_dashboard_backend/templates/server.j2 b/wg_dashboard_backend/templates/server.j2 index e4f50dd..4f1c83c 100644 --- a/wg_dashboard_backend/templates/server.j2 +++ b/wg_dashboard_backend/templates/server.j2 @@ -1,5 +1,5 @@ [Interface] -Address = {{ data.address }} +Address = {{ data.address }}/{{ data.subnet }} ListenPort = {{ data.listen_port }} PrivateKey = {{ data.private_key }} @@ -13,5 +13,5 @@ PublicKey = {{ peer.public_key }} {%- if peer.shared_key %} PresharedKey = {{ peer.shared_key }} {%- endif %} -AllowedIPs = {{ peer.address }} +AllowedIPs = {{ peer.address }}/32 {% endfor %} diff --git a/wg_dashboard_frontend/src/app/interfaces/server.ts b/wg_dashboard_frontend/src/app/interfaces/server.ts index fb0d536..5c28a10 100644 --- a/wg_dashboard_frontend/src/app/interfaces/server.ts +++ b/wg_dashboard_frontend/src/app/interfaces/server.ts @@ -12,5 +12,6 @@ export interface Server { post_up: string; post_down: string; configuration: string; + subnet: number; peers: Peer[]; } diff --git a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html index a30a80a..e32498a 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html +++ b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html @@ -29,10 +29,21 @@ - Address Scope - + Address + + + + + Subnet + + + + + diff --git a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts index ab81121..2262814 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts +++ b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts @@ -5,10 +5,10 @@ import { NumberValidator } from '../../../validators/number.validator'; import { Server } from '../../../interfaces/server'; import { ServerService } from '../../../services/server.service'; import { DataService } from '../../../services/data.service'; -import Parser, {Property, Section, Sections} from "@jedmao/ini-parser"; +import Parser, {Section} from "@jedmao/ini-parser"; import {Peer} from "../../../interfaces/peer"; -import {forkJoin, from, Observable, of} from "rxjs"; -import {concatAll, concatMap, filter, map, mergeAll, mergeMap, switchMap} from "rxjs/operators"; +import {forkJoin, from} from "rxjs"; +import {map, mergeMap} from "rxjs/operators"; import {NotifierService} from "angular-notifier"; @Component({ selector: 'app-add-server', @@ -34,13 +34,17 @@ export class AddServerComponent implements OnInit { "DNS": "dns" } + subnets = []; + selectedSubnet = 24; + serverForm: FormGroup = null; isEdit = false; editServer: Server = null; initForm(){ this.serverForm = new FormGroup({ - address: new FormControl('', [IPValidator.isIPAddress]), + address: new FormControl('', [Validators.required, IPValidator.isIPAddress]), + subnet: new FormControl('', [Validators.required, Validators.min(1), Validators.max(32)]), interface: new FormControl('', [Validators.required, Validators.minLength(3)]), listen_port: new FormControl('', [Validators.required, NumberValidator.stringIsNumber]), endpoint: new FormControl('', Validators.required), @@ -62,6 +66,7 @@ export class AddServerComponent implements OnInit { } ngOnInit(): void { + this.subnets = Array(32).fill(1).map((x,i)=>i+1); this.initForm(); this.comm.on('server-edit').subscribe((data: Server) => { @@ -140,11 +145,14 @@ export class AddServerComponent implements OnInit { return false; } + iFace.nodes["subnet"] = iFace.nodes["address"].split("/")[1]; + iFace.nodes["address"] = iFace.nodes["address"].split("/")[0]; + iFace.nodes["peers"] = sPeers .map( x => x.nodes) .map( x => { x.server_id = -1; - x.address = x.allowed_ips; // Allowed_ips in server is the address of the peer (Seen from server perspective) + x.address = x.allowed_ips.split("/")[0]; // Allowed_ips in server is the address of the peer (Seen from server perspective) x.allowed_ips = null; // This should be retrieved from peer data config return x; }) diff --git a/wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts b/wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts index 7560191..2257217 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts +++ b/wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts @@ -18,6 +18,7 @@ import { MatTableModule } from '@angular/material/table'; import { PeerComponent } from './peer/peer.component'; import { QRCodeModule } from 'angularx-qrcode'; import {MatTooltipModule} from "@angular/material/tooltip"; +import {MatSelectModule} from "@angular/material/select"; @NgModule({ declarations: [ @@ -43,6 +44,7 @@ import {MatTooltipModule} from "@angular/material/tooltip"; FormsModule, QRCodeModule, MatTooltipModule, + MatSelectModule, ], }) diff --git a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html index 01b933e..86c181c 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html +++ b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html @@ -83,7 +83,7 @@ - Endpoint: {{server.endpoint}}:{{server.listen_port}} - Address Space: {{server.address}} + Endpoint: {{server.endpoint}}:{{server.listen_port}} - Address: {{server.address}}/{{server.subnet}} diff --git a/wg_dashboard_frontend/src/app/validators/ip-address.validator.ts b/wg_dashboard_frontend/src/app/validators/ip-address.validator.ts index 00bd47c..b29c386 100644 --- a/wg_dashboard_frontend/src/app/validators/ip-address.validator.ts +++ b/wg_dashboard_frontend/src/app/validators/ip-address.validator.ts @@ -4,7 +4,7 @@ import * as IPCIDR from 'ip-cidr'; export class IPValidator { static isIPAddress(control: AbstractControl): ValidationErrors | null { - if (!control.value || !(new IPCIDR(control.value).isValid()) || !control.value.includes('/')) { + if (!control.value || !(new IPCIDR(control.value).isValid())) { return { validIP: true }; } return null; From 6d944bcd99aaef1f9b45f3a788618391fd415353 Mon Sep 17 00:00:00 2001 From: Simon Lerpard Date: Wed, 3 Jun 2020 13:41:16 +0200 Subject: [PATCH 02/29] Fix keeping the server interface state when stop The server interface wasn't updated in the database when it got stopped. Which led to 'is_running' was always set to true, if the server has been started once. Also fixed a copy-paste issue, which didn't affect the code --- wg_dashboard_backend/routers/v1/server.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/wg_dashboard_backend/routers/v1/server.py b/wg_dashboard_backend/routers/v1/server.py index 1f9be45..bed6eb1 100644 --- a/wg_dashboard_backend/routers/v1/server.py +++ b/wg_dashboard_backend/routers/v1/server.py @@ -73,12 +73,14 @@ def add_interface( @router.post("/stop", response_model=schemas.WGServer) -def start_server( - form_data: schemas.WGServer +def stop_server( + server: schemas.WGServer, + sess: Session = Depends(middleware.get_db) ): - script.wireguard.stop_interface(form_data) - form_data.is_running = script.wireguard.is_running(form_data) - return form_data + script.wireguard.stop_interface(server) + server.is_running = script.wireguard.is_running(server) + server.sync(sess) + return server @router.post("/start", response_model=schemas.WGServer) From 93193415f1184ba26d14cc44b01a828680389049 Mon Sep 17 00:00:00 2001 From: Simon Lerpard Date: Wed, 3 Jun 2020 13:44:56 +0200 Subject: [PATCH 03/29] Fix detection of 'wg show interface' not running When main.py initializes the subprocess returns an output of 'No such device' but it doesn't throw any exception, for some reason...? So, the output was of course returned to the is_running process and since it wasn't "None" it though the interface was in the 'running' state. --- wg_dashboard_backend/script/wireguard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wg_dashboard_backend/script/wireguard.py b/wg_dashboard_backend/script/wireguard.py index dcce036..2aa3e84 100644 --- a/wg_dashboard_backend/script/wireguard.py +++ b/wg_dashboard_backend/script/wireguard.py @@ -108,7 +108,7 @@ def restart_interface(server: schemas.WGServer): def is_running(server: schemas.WGServer): try: output = _run_wg(server, ["show", server.interface]) - if output is None: + if output is None or b'Unable to access interface: No such device' in output: return False except Exception as e: if b'No such device' in e.output: From a562822459e09a301db9bca0ae64427513683cb1 Mon Sep 17 00:00:00 2001 From: Simon Lerpard Date: Wed, 3 Jun 2020 14:02:16 +0200 Subject: [PATCH 04/29] Fix incorrect port mapping in docker-compose Fixed an incorrect port mapping the the docker-compose example as well as added a restart attribute, since it's most likely a good use case to always keep the server up and running, even if it fails or the host is updated/restarted. Also added a missing end parenthesis in the table of env variables. --- README.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 21b2087..6865297 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,12 @@ The features of wg-manager includes: wireguard: container_name: wg-manager image: perara/wg-manager + restart: always cap_add: - NET_ADMIN #network_mode: host # Alternatively ports: - - 51800:51900/udp + - 51800-51900:51800-51900/udp - 8888:8888 volumes: - ./wg-manager:/config @@ -62,16 +63,16 @@ When docker container/server has started, go to http://localhost:8888 # Environment variables -| Environment | Description | Recommended | -|------------------|--------------------------------------------------------------------------|-------------| -| GUNICORN_CONF | Location of custom gunicorn configuration | default | -| WORKERS_PER_CORE | How many concurrent workers should there be per available core (Gunicorn | default | -| WEB_CONCURRENCY | The number of worker processes for handling requests. (Gunicorn) | 1 | -| HOST | 0.0.0.0 or unix:/tmp/gunicorn.sock if reverse proxy. Remember to mount | 0.0.0.0 | -| PORT | The port to use if running with IP host bind | 80 | -| LOG_LEVEL | Logging level of gunicorn/python | info | -| ADMIN_USERNAME | Default admin username on database creation | admin | -| ADMIN_PASSWORD | Default admin password on database creation | admin | +| Environment | Description | Recommended | +|------------------|---------------------------------------------------------------------------|-------------| +| GUNICORN_CONF | Location of custom gunicorn configuration | default | +| WORKERS_PER_CORE | How many concurrent workers should there be per available core (Gunicorn) | default | +| WEB_CONCURRENCY | The number of worker processes for handling requests. (Gunicorn) | 1 | +| HOST | 0.0.0.0 or unix:/tmp/gunicorn.sock if reverse proxy. Remember to mount | 0.0.0.0 | +| PORT | The port to use if running with IP host bind | 80 | +| LOG_LEVEL | Logging level of gunicorn/python | info | +| ADMIN_USERNAME | Default admin username on database creation | admin | +| ADMIN_PASSWORD | Default admin password on database creation | admin | # Showcase ![Illustration](docs/images/0.png) From eb3bbc69ef297318af339d6f7fd9c755c0a9c7eb Mon Sep 17 00:00:00 2001 From: Simon Lerpard <11946823+simonlerpard@users.noreply.github.com> Date: Mon, 8 Jun 2020 18:43:38 +0200 Subject: [PATCH 05/29] Add env HOST to docker run example args --- docs/guides/docker_configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guides/docker_configuration.md b/docs/guides/docker_configuration.md index 1a26141..f7bb5a4 100644 --- a/docs/guides/docker_configuration.md +++ b/docs/guides/docker_configuration.md @@ -7,6 +7,7 @@ docker run -d \ -p "51800-51900:51800-51900/udp" \ -p "8888:8888" \ -v wg-manager:/config \ +-e HOST="0.0.0.0" \ -e PORT="8888" \ -e ADMIN_USERNAME="admin" \ -e ADMIN_PASSWORD="admin" \ From 994d59a75e80af40dfe1efa9c882582bbbd48f6b Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Sun, 19 Jul 2020 19:49:54 +0200 Subject: [PATCH 06/29] * Fixes #18 "dark theme slider" --- .../src/app/layout/layout.module.ts | 28 ++++++++++--------- .../app/layout/layout/layout.component.html | 7 +++-- .../src/app/layout/layout/layout.component.ts | 14 +++++----- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/wg_dashboard_frontend/src/app/layout/layout.module.ts b/wg_dashboard_frontend/src/app/layout/layout.module.ts index 8cce19f..bf56d15 100644 --- a/wg_dashboard_frontend/src/app/layout/layout.module.ts +++ b/wg_dashboard_frontend/src/app/layout/layout.module.ts @@ -12,22 +12,24 @@ import { RouterModule } from '@angular/router'; import { MatMenuModule } from '@angular/material/menu'; import {MatSlideToggleModule} from "@angular/material/slide-toggle"; import {NotifierModule} from "angular-notifier"; +import {FormsModule} from "@angular/forms"; @NgModule({ declarations: [LayoutComponent], - imports: [ - CommonModule, - MatSidenavModule, - MatToolbarModule, - MatListModule, - MatIconModule, - MatButtonModule, - FlexLayoutModule, - RouterModule, - MatMenuModule, - MatSlideToggleModule, - NotifierModule, - ], + imports: [ + CommonModule, + MatSidenavModule, + MatToolbarModule, + MatListModule, + MatIconModule, + MatButtonModule, + FlexLayoutModule, + RouterModule, + MatMenuModule, + MatSlideToggleModule, + NotifierModule, + FormsModule, + ], exports: [ ], diff --git a/wg_dashboard_frontend/src/app/layout/layout/layout.component.html b/wg_dashboard_frontend/src/app/layout/layout/layout.component.html index d156832..048b281 100644 --- a/wg_dashboard_frontend/src/app/layout/layout/layout.component.html +++ b/wg_dashboard_frontend/src/app/layout/layout/layout.component.html @@ -36,10 +36,11 @@ + [(ngModel)]="darkMode" + (change)="toggleDarkMode()" + (click)="$event.stopPropagation();"> Dark diff --git a/wg_dashboard_frontend/src/app/layout/layout/layout.component.ts b/wg_dashboard_frontend/src/app/layout/layout/layout.component.ts index 07d0761..1b5edec 100644 --- a/wg_dashboard_frontend/src/app/layout/layout/layout.component.ts +++ b/wg_dashboard_frontend/src/app/layout/layout/layout.component.ts @@ -40,22 +40,22 @@ export class LayoutComponent implements OnInit { public auth: AuthService, private comm: DataService, private cookieService: CookieService - ) {} - ngOnInit(): void { - console.log('Layout'); + ) { + this.darkMode = (this.cookieService.get("darkMode") === 'true'); if(this.cookieService.check("currentTheme")){ this.currentTheme = JSON.parse(this.cookieService.get("currentTheme")); - this.darkMode = (this.cookieService.get("darkMode") === 'true'); }else { this.currentTheme = { ... this.themes[0]} } + this.sendData() } + ngOnInit(): void { + console.log('Layout'); + } - toggleDarkMode($event){ - $event.stopPropagation(); - this.darkMode = !this.darkMode; + toggleDarkMode(){ this.cookieService.set("darkMode", String(this.darkMode)); this.sendData(); } From 0809ea3534311dee370d102f8af6f694e6770053 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Sun, 19 Jul 2020 19:51:47 +0200 Subject: [PATCH 07/29] * Fixes auth issue #18 --- wg_dashboard_frontend/src/app/page/page-routing.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wg_dashboard_frontend/src/app/page/page-routing.module.ts b/wg_dashboard_frontend/src/app/page/page-routing.module.ts index 6e4bb68..e006aa2 100644 --- a/wg_dashboard_frontend/src/app/page/page-routing.module.ts +++ b/wg_dashboard_frontend/src/app/page/page-routing.module.ts @@ -16,7 +16,7 @@ const routes: Routes = [ }, { path: 'user', component: LayoutComponent, children: [ - { path: 'edit', component: EditComponent, pathMatch: 'full' }, + { path: 'edit', component: EditComponent, pathMatch: 'full', canActivate: [AuthGuard]}, { path: 'login', component: LoginComponent, pathMatch: 'full' }, ], }, From e4d951ebb4c038141525d687dec251b291863241 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Sun, 19 Jul 2020 19:59:56 +0200 Subject: [PATCH 08/29] * Added docs for IPV6 support #20 * Added ip6tables package to Dockerfile #20 --- Dockerfile | 2 +- README.md | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index c3c6676..28ab4b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ MAINTAINER per@sysx.no ENV IS_DOCKER True WORKDIR /app # Install dependencies -RUN apk add --no-cache --update wireguard-tools py3-gunicorn python3 py3-pip +RUN apk add --no-cache --update wireguard-tools py3-gunicorn python3 py3-pip ip6tables COPY wg_dashboard_backend /app diff --git a/README.md b/README.md index 3725c87..7f69ebc 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,19 @@ The features of wg-manager includes: * Linux >= 5.6 *(Alternatively: wireguard-dkms)* # Common Installation Steps -1. Enable ip forwarding with `sysctl -w net.ipv4.ip_forward=1` - * To make the forwarding persistent add `net.ipv4.ip_forward = 1` to `/etc/sysctl.d/99-sysctl.conf` -2. It is recommended to have a firewall protecting your servers +1. Enable ip forwarding: + ``` + sysctl -w net.ipv4.ip_forward=1 # IPV4 Support + sysctl -w net.ipv6.conf.all.forwarding=1 # IPV6 Support + ``` +2. For persistent configuration: + ``` + cat > /etc/sysctl.d/99-sysctl.conf << EOF + net.ipv4.ip_forward = 1 + net.ipv6.conf.all.forwarding=1 + EOF + ``` +3. It is recommended to have a firewall protecting your servers ## Notes * A few people has experienced issues with running the dockerized method using bridged networking. To fix this, you can use `network_mode: host`. Note that you can no longer reverse-proxy the web interface from reverse proxies such as [jwilder/nginx-proxy](https://hub.docker.com/r/jwilder/nginx-proxy/). From 8470f363c950e3912996c7c61136157921a2402a Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Sun, 19 Jul 2020 20:24:00 +0200 Subject: [PATCH 09/29] * Add gunicorn preload so that const is loaded pre-fork. Fixes #23 --- docker/start.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/start.py b/docker/start.py index a7de3b0..57b0714 100644 --- a/docker/start.py +++ b/docker/start.py @@ -18,6 +18,7 @@ if __name__ == "__main__": APP_MODULE = os.getenv("APP_MODULE", f"{MODULE_NAME}:{VARIABLE_NAME}") GUNICORN_CONF = os.getenv("GUNICORN_CONF", get_location(DEFAULT_GUNICORN_CONF)) OPTIONS = [ + "--preload", "-k", "uvicorn.workers.UvicornWorker", "-c", From 99bdc5451729431d9d3d79c8294c029562621bce Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Sun, 19 Jul 2020 20:38:46 +0200 Subject: [PATCH 10/29] * Fixes #24 - ID was not fetched after insert. --- wg_dashboard_backend/routers/v1/peer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wg_dashboard_backend/routers/v1/peer.py b/wg_dashboard_backend/routers/v1/peer.py index c49c903..c299521 100644 --- a/wg_dashboard_backend/routers/v1/peer.py +++ b/wg_dashboard_backend/routers/v1/peer.py @@ -62,6 +62,7 @@ def add_peer( )) peer.sync(sess) + peer.from_db(sess) # If server is running. Add peer if script.wireguard.is_running(server): From f3016756466f2f3efce27c33b1de4c8025a60220 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Sun, 19 Jul 2020 21:06:55 +0200 Subject: [PATCH 11/29] * Moving away from silly .sync/.from_db scheme and use "raw" sqlalchemy. * Fixed bug where server interface were queried using ID and not the name. Fixes #22 --- wg_dashboard_backend/db/wireguard.py | 3 ++- wg_dashboard_backend/routers/v1/peer.py | 13 +++++++------ wg_dashboard_backend/schemas.py | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/wg_dashboard_backend/db/wireguard.py b/wg_dashboard_backend/db/wireguard.py index 4ee50c2..bb4270c 100644 --- a/wg_dashboard_backend/db/wireguard.py +++ b/wg_dashboard_backend/db/wireguard.py @@ -43,7 +43,8 @@ def peer_dns_set(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer: def peer_remove(sess: Session, peer: schemas.WGPeer) -> bool: - db_peers = peer.filter_query(sess).all() + + db_peers = sess.query(models.WGPeer).filter_by(id=peer.id).all() for db_peer in db_peers: sess.delete(db_peer) diff --git a/wg_dashboard_backend/routers/v1/peer.py b/wg_dashboard_backend/routers/v1/peer.py index c299521..e57e1a0 100644 --- a/wg_dashboard_backend/routers/v1/peer.py +++ b/wg_dashboard_backend/routers/v1/peer.py @@ -61,14 +61,15 @@ def add_peer( server=server )) - peer.sync(sess) - peer.from_db(sess) + db_peer = models.WGPeer(**peer.dict()) + sess.add(db_peer) + sess.commit() # If server is running. Add peer if script.wireguard.is_running(server): script.wireguard.add_peer(server, peer) - return peer + return schemas.WGPeer.from_orm(db_peer) @router.post("/delete", response_model=schemas.WGPeer) @@ -76,13 +77,13 @@ def delete_peer( peer: schemas.WGPeer, sess: Session = Depends(middleware.get_db) ): - peer.from_db(sess) # Sync full object + + server = sess.query(models.WGServer).filter_by(id=peer.server_id).one() if not db.wireguard.peer_remove(sess, peer): raise HTTPException(400, detail="Were not able to delete peer %s (%s)" % (peer.name, peer.public_key)) - server = schemas.WGServer(interface=peer.server_id) - if script.wireguard.is_running(server): + if script.wireguard.is_running(schemas.WGServer(interface=server.interface)): script.wireguard.remove_peer(server, peer) return peer diff --git a/wg_dashboard_backend/schemas.py b/wg_dashboard_backend/schemas.py index d98c08a..f7ebf54 100644 --- a/wg_dashboard_backend/schemas.py +++ b/wg_dashboard_backend/schemas.py @@ -124,8 +124,8 @@ class WGPeer(GenericModel): class Meta: model = models.WGPeer - key = "address" - excludes = {"id"} + key = "id" + excludes = {} class WGPeerConfig(GenericModel): From c273dff215d281b4ac1964d45f842824fa5e8340 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Sun, 19 Jul 2020 21:10:52 +0200 Subject: [PATCH 12/29] * Added warning for #22 --- wg_dashboard_backend/script/wireguard.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wg_dashboard_backend/script/wireguard.py b/wg_dashboard_backend/script/wireguard.py index 2aa3e84..7e0de11 100644 --- a/wg_dashboard_backend/script/wireguard.py +++ b/wg_dashboard_backend/script/wireguard.py @@ -48,7 +48,7 @@ def _run_wg(server: schemas.WGServer, command): return output except Exception as e: if b'Operation not permitted' in e.output: - raise WGPermissionsError("The user has insufficientt permissions for interface %s" % server.interface) + raise WGPermissionsError("The user has insufficient permissions for interface %s" % server.interface) def is_installed(): @@ -109,6 +109,9 @@ def is_running(server: schemas.WGServer): try: output = _run_wg(server, ["show", server.interface]) if output is None or b'Unable to access interface: No such device' in output: + _LOGGER.warning("Unable to access interface: No such device. " + "This may indicate that there is a bug somewhere, " + "or that you have manually deleted parts of the database") return False except Exception as e: if b'No such device' in e.output: From 41abe44f45bdfdec916970af64100d3021502060 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Sun, 19 Jul 2020 22:06:46 +0200 Subject: [PATCH 13/29] * Fixed bug where server configuration did not update after peer update. --- wg_dashboard_backend/db/wireguard.py | 31 +++++++++++++++++++ wg_dashboard_backend/routers/v1/peer.py | 27 +++------------- wg_dashboard_backend/routers/v1/server.py | 8 +++++ wg_dashboard_backend/script/wireguard.py | 4 +-- .../src/app/interfaces/server.ts | 1 + .../modal-confirm/modal-confirm.component.ts | 2 ++ .../app/page/dashboard/peer/peer.component.ts | 15 ++++++--- .../dashboard/server/server.component.html | 9 ++++-- .../page/dashboard/server/server.component.ts | 13 ++++++-- .../src/app/services/server.service.ts | 6 ++-- 10 files changed, 81 insertions(+), 35 deletions(-) diff --git a/wg_dashboard_backend/db/wireguard.py b/wg_dashboard_backend/db/wireguard.py index bb4270c..71187fe 100644 --- a/wg_dashboard_backend/db/wireguard.py +++ b/wg_dashboard_backend/db/wireguard.py @@ -50,9 +50,32 @@ def peer_remove(sess: Session, peer: schemas.WGPeer) -> bool: sess.delete(db_peer) sess.commit() + server_update_configuration(sess, peer.server_id) + return True +def peer_edit(sess: Session, peer: schemas.WGPeer): + # Retrieve server from db + server: models.WGServer = get_server_by_id(sess, peer.server_id) + + # Generate peer configuration + peer.configuration = script.wireguard.generate_config(dict( + peer=peer, + server=server + )) + + # Update database record for Peer + sess.query(models.WGPeer) \ + .filter_by(id=peer.id) \ + .update(peer.dict(exclude={"id"})) + sess.commit() + + server_update_configuration(sess, server.id) + + return peer + + def peer_key_pair_generate(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer: db_peer: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).one() private_key, public_key = script.wireguard.generate_keys() @@ -159,3 +182,11 @@ def server_post_down_set(sess: Session, server: schemas.WGServer) -> bool: def server_endpoint_set(sess: Session, server: schemas.WGServer) -> bool: return server_update_field(sess, server.interface, server, {"endpoint"}) + + +def server_update_configuration(sess: Session, server_id: int) -> bool: + # Generate server configuration + server: models.WGServer = sess.query(models.WGServer).filter_by(id=server_id).one() + server.configuration = script.wireguard.generate_config(server) + sess.add(server) + sess.commit() diff --git a/wg_dashboard_backend/routers/v1/peer.py b/wg_dashboard_backend/routers/v1/peer.py index e57e1a0..7f9f4d4 100644 --- a/wg_dashboard_backend/routers/v1/peer.py +++ b/wg_dashboard_backend/routers/v1/peer.py @@ -69,6 +69,9 @@ def add_peer( if script.wireguard.is_running(server): script.wireguard.add_peer(server, peer) + # Update server configuration + db.wireguard.server_update_configuration(sess, db_peer.server_id) + return schemas.WGPeer.from_orm(db_peer) @@ -94,27 +97,7 @@ def edit_peer( peer: schemas.WGPeer, sess: Session = Depends(middleware.get_db) ): - # Retrieve server from db - server: models.WGServer = db.wireguard.get_server_by_id(sess, peer.server_id) - - # Generate peer configuration - peer.configuration = script.wireguard.generate_config(dict( - peer=peer, - server=server - )) - # Update database record for Peer - sess.query(models.WGPeer)\ - .filter_by(id=peer.id)\ - .update(peer.dict(exclude={"id"})) + peer = db.wireguard.peer_edit(sess, peer) - # Generate server configuration - server.configuration = script.wireguard.generate_config(server) - sess.add(server) - - sess.commit() - - return dict( - peer=peer, - server_configuration=server.configuration - ) + return peer diff --git a/wg_dashboard_backend/routers/v1/server.py b/wg_dashboard_backend/routers/v1/server.py index bed6eb1..185c501 100644 --- a/wg_dashboard_backend/routers/v1/server.py +++ b/wg_dashboard_backend/routers/v1/server.py @@ -158,3 +158,11 @@ def edit_server( return server + +@router.get("/config/{server_id}", response_model=str) +def server_config( + server_id: int, + sess: Session = Depends(middleware.get_db) +): + + return db.wireguard.get_server_by_id(sess, server_id=server_id).configuration diff --git a/wg_dashboard_backend/script/wireguard.py b/wg_dashboard_backend/script/wireguard.py index 7e0de11..668d8f7 100644 --- a/wg_dashboard_backend/script/wireguard.py +++ b/wg_dashboard_backend/script/wireguard.py @@ -109,9 +109,9 @@ def is_running(server: schemas.WGServer): try: output = _run_wg(server, ["show", server.interface]) if output is None or b'Unable to access interface: No such device' in output: - _LOGGER.warning("Unable to access interface: No such device. " + _LOGGER.warning("Unable to access interface: No such device. (%s)" "This may indicate that there is a bug somewhere, " - "or that you have manually deleted parts of the database") + "or that you have manually deleted parts of the database", server.interface) return False except Exception as e: if b'No such device' in e.output: diff --git a/wg_dashboard_frontend/src/app/interfaces/server.ts b/wg_dashboard_frontend/src/app/interfaces/server.ts index 5c28a10..dd8a801 100644 --- a/wg_dashboard_frontend/src/app/interfaces/server.ts +++ b/wg_dashboard_frontend/src/app/interfaces/server.ts @@ -1,6 +1,7 @@ import { Peer } from './peer'; export interface Server { + id: number; address: string; interface: string; listen_port: string; diff --git a/wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.ts b/wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.ts index 767be8b..03f4b99 100644 --- a/wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.ts +++ b/wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.ts @@ -37,6 +37,7 @@ export class ModalConfirmComponent implements OnInit { } open($event){ + $event.stopPropagation(); if (this.noConfirm) { this.onConfirm.emit($event); return true; @@ -54,6 +55,7 @@ export class ModalConfirmComponent implements OnInit { } cancel($event){ + $event.stopPropagation(); this.onCancel.emit($event); this.shown = false } diff --git a/wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.ts b/wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.ts index fc50bf3..09a4290 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.ts +++ b/wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.ts @@ -16,6 +16,7 @@ export class PeerComponent implements OnInit { @Input('server') server: Server; @Input('selectedPeer') selectedPeer: Peer; @Input('onEvent') editPeerEmitter: EventEmitter = new EventEmitter(); + @Input('cbOnPeerUpdate') cbOnPeerUpdate: Function; constructor(public serverAPI: ServerService) { } @@ -28,6 +29,7 @@ export class PeerComponent implements OnInit { if (msg.type === 'edit') { this.edit(); + } else if (msg.type == 'delete') { this.delete(); } @@ -41,11 +43,13 @@ export class PeerComponent implements OnInit { // Submit the edit (True -> False) const idx = this.server.peers.indexOf(this.peer); - this.serverAPI.editPeer(this.peer).subscribe((data) => { - this.server.configuration = data.server_configuration; - Object.keys(data.peer).forEach(k => { - this.server.peers[idx][k] = data.peer[k]; + this.serverAPI.editPeer(this.peer).subscribe((peer) => { + Object.keys(peer).forEach(k => { + this.server.peers[idx][k] = peer[k]; }); + + // Query server for server configuration update + this.cbOnPeerUpdate(); }); } else if (!this.peer._edit) { @@ -63,6 +67,9 @@ export class PeerComponent implements OnInit { const idx = this.server.peers.indexOf(this.peer); this.serverAPI.deletePeer(this.peer).subscribe((apiServer) => { this.server.peers.splice(idx, 1); + + // Query server for server configuration update + this.cbOnPeerUpdate(); }); } diff --git a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html index 86c181c..b6f3843 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html +++ b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html @@ -127,7 +127,7 @@ @@ -148,7 +148,12 @@ diff --git a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts index 6e19383..dffb40c 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts +++ b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit } from '@angular/core'; import { Server } from '../../../interfaces/server'; import { ServerService } from '../../../services/server.service'; import { DataService } from '../../../services/data.service'; @@ -47,6 +47,9 @@ export class ServerComponent implements OnInit { server_interface: this.server.interface }).subscribe((peer) => { this.server.peers.push(peer); + + // Query server for server configuration update + this.onPeerUpdate(); }); } @@ -58,7 +61,7 @@ export class ServerComponent implements OnInit { delete() { const index = this.servers.indexOf(this.server); - this.serverAPI.deleteServer(this.server).subscribe((apiServer) => { + this.serverAPI.deleteServer(this.server).subscribe(() => { this.servers.splice(index, 1); }); } @@ -72,6 +75,12 @@ export class ServerComponent implements OnInit { this.editPeerEmitter.emit({ type: 'open', peer }); } + onPeerUpdate(){ + this.serverAPI.serverConfig(this.server).subscribe((configuration) => { + this.server.configuration = configuration + }) + } + pInt(string: string) { return parseInt(string); } diff --git a/wg_dashboard_frontend/src/app/services/server.service.ts b/wg_dashboard_frontend/src/app/services/server.service.ts index 2e61b2c..a4ed45b 100644 --- a/wg_dashboard_frontend/src/app/services/server.service.ts +++ b/wg_dashboard_frontend/src/app/services/server.service.ts @@ -35,7 +35,7 @@ export class ServerService { return this.http.post(this.peerURL + '/add', server_interface); } - public editPeer(peer: Peer): Subscribable<{ peer: Peer, server_configuration: string }> { + public editPeer(peer: Peer): Subscribable { return this.http.post(this.peerURL + '/edit', peer); } @@ -84,8 +84,8 @@ export class ServerService { return this.http.post(this.peerURL + '/config', peer); } - public serverConfig(server: Server) { - return this.http.post(this.serverURL + '/config', server); + public serverConfig(server: Server): Subscribable { + return this.http.get(this.serverURL + '/config/' + server.id.toString()); } public serverStats(server: Server) { From 66617b769885a696c847c49fc228b0fe76306f2b Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Sun, 19 Jul 2020 22:12:27 +0200 Subject: [PATCH 14/29] * Removed warning on missing devices as this is a is_running check. No device appears when device is NOT running. * Check if interface runs before checking stats. --- wg_dashboard_backend/routers/v1/server.py | 6 +++++- wg_dashboard_backend/script/wireguard.py | 5 +---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/wg_dashboard_backend/routers/v1/server.py b/wg_dashboard_backend/routers/v1/server.py index 185c501..1a44ee3 100644 --- a/wg_dashboard_backend/routers/v1/server.py +++ b/wg_dashboard_backend/routers/v1/server.py @@ -122,7 +122,11 @@ def delete_server( @router.post("/stats", dependencies=[Depends(middleware.auth)]) def stats_server(server: schemas.WGServer): - stats = script.wireguard.get_stats(server) + if script.wireguard.is_running(server): + stats = script.wireguard.get_stats(server) + else: + stats = [] + return JSONResponse(content=stats) diff --git a/wg_dashboard_backend/script/wireguard.py b/wg_dashboard_backend/script/wireguard.py index 668d8f7..7459217 100644 --- a/wg_dashboard_backend/script/wireguard.py +++ b/wg_dashboard_backend/script/wireguard.py @@ -108,10 +108,7 @@ def restart_interface(server: schemas.WGServer): def is_running(server: schemas.WGServer): try: output = _run_wg(server, ["show", server.interface]) - if output is None or b'Unable to access interface: No such device' in output: - _LOGGER.warning("Unable to access interface: No such device. (%s)" - "This may indicate that there is a bug somewhere, " - "or that you have manually deleted parts of the database", server.interface) + if output is None: return False except Exception as e: if b'No such device' in e.output: From 1eae153572607a5e5ae169e57888a32474b1c97b Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 20 Jul 2020 01:27:31 +0200 Subject: [PATCH 15/29] * Possible fix for peers not updating subnet. This should finish up #11 * Initial Support for IPv6. #20 --- README.md | 12 +- docker-compose.yaml | 24 + docs/guides/docker_configuration.md | 1 + wg_dashboard_backend/const.py | 7 +- .../versions/004_create_server_subnet.py | 2 +- .../versions/005_create_v6_address.py | 32 + .../versions/006_create_v6_subnet.py | 21 + wg_dashboard_backend/models.py | 3 + wg_dashboard_backend/requirements.txt | 1 + wg_dashboard_backend/routers/v1/peer.py | 37 +- wg_dashboard_backend/routers/v1/server.py | 30 +- wg_dashboard_backend/schemas.py | 3 + wg_dashboard_backend/script/wireguard.py | 13 +- wg_dashboard_backend/templates/peer.j2 | 2 +- wg_dashboard_backend/templates/server.j2 | 8 +- wg_dashboard_frontend/package-lock.json | 3640 ++++++++++++++++- .../add-server/add-server.component.html | 61 +- .../add-server/add-server.component.ts | 51 +- .../app/page/dashboard/dashboard.module.ts | 2 + 19 files changed, 3671 insertions(+), 279 deletions(-) create mode 100644 docker-compose.yaml create mode 100644 wg_dashboard_backend/migrations/versions/005_create_v6_address.py create mode 100644 wg_dashboard_backend/migrations/versions/006_create_v6_subnet.py diff --git a/README.md b/README.md index 7f69ebc..795103c 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,11 @@ The wg-manager provides an easy-to-use graphical web interface to import, setup, The features of wg-manager includes: **Server** +* IPv4 **and** IPv6 support * Create/Delete/Modify * Start/Stop/Restart server -* Import existing +* Import existing configurations +* Export server config, along with client config as zip. **Peer** * Create/Delete/Modify @@ -40,10 +42,14 @@ The features of wg-manager includes: ## Method #1: Docker-compose ```yaml +version: "2.1" +services: wireguard: container_name: wg-manager image: perara/wg-manager restart: always + sysctls: + net.ipv6.conf.all.disable_ipv6: 0 # Required for IPV6 cap_add: - NET_ADMIN #network_mode: host # Alternatively @@ -91,6 +97,10 @@ When docker container/server has started, go to http://localhost:8888 | LOG_LEVEL | Logging level of gunicorn/python | info | | ADMIN_USERNAME | Default admin username on database creation | admin | | ADMIN_PASSWORD | Default admin password on database creation | admin | +| POST_UP | The POST_UP Command (version 4) | default | +| POST_DOWN | The POST_DOWN Command (version 4) | default | +| POST_UP_V6 | The POST_UP Command (version 6) | default | +| POST_DOWN_V6 | The POST_DOWN Command (version 6) | default | # Showcase ![Illustration](docs/images/0.png) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..19ee233 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,24 @@ +version: "2.1" +services: + + server: + container_name: wg-manager + build: . + restart: always + sysctls: + net.ipv6.conf.all.disable_ipv6: 0 + cap_add: + - NET_ADMIN + #network_mode: host # Alternatively + ports: + - 11820:11820/udp + - 51800-51900:51800-51900/udp + - 8888:8888 + volumes: + - ./wg-manager:/config + environment: + HOST: 0.0.0.0 + PORT: 8888 + ADMIN_PASSWORD: admin + ADMIN_USERNAME: admin + WEB_CONCURRENCY: 2 \ No newline at end of file diff --git a/docs/guides/docker_configuration.md b/docs/guides/docker_configuration.md index f7bb5a4..9c0f063 100644 --- a/docs/guides/docker_configuration.md +++ b/docs/guides/docker_configuration.md @@ -1,6 +1,7 @@ # Docker Configuration ```bash docker run -d \ +--sysctl net.ipv6.conf.all.disable_ipv6=0 \ --cap-add NET_ADMIN \ --name wg-manager \ #--net host \ diff --git a/wg_dashboard_backend/const.py b/wg_dashboard_backend/const.py index 5abf302..67a1b02 100644 --- a/wg_dashboard_backend/const.py +++ b/wg_dashboard_backend/const.py @@ -6,9 +6,10 @@ DATABASE_FILE = "/config/database.db" if IS_DOCKER else "./database.db" DATABASE_URL = f"sqlite:///{DATABASE_FILE}" os.makedirs("build", exist_ok=True) - -DEFAULT_POST_UP = os.getenv("POST_UP", "iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE") -DEFAULT_POST_DOWN = os.getenv("POST_DOWN", "iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE") +DEFAULT_POST_UP = os.getenv("POST_UP", "iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;") +DEFAULT_POST_DOWN = os.getenv("POST_DOWN", "iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;") +DEFAULT_POST_UP_v6 = os.getenv("POST_UP_V6", "ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;") +DEFAULT_POST_DOWN_v6 = os.getenv("POST_DOWN_V6", "ip6tables -D FORWARD -i %i -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;") SECRET_KEY = ''.join(random.choices(string.ascii_uppercase + string.digits, k=64)) ALGORITHM = "HS256" diff --git a/wg_dashboard_backend/migrations/versions/004_create_server_subnet.py b/wg_dashboard_backend/migrations/versions/004_create_server_subnet.py index 539a81d..3ad23ff 100644 --- a/wg_dashboard_backend/migrations/versions/004_create_server_subnet.py +++ b/wg_dashboard_backend/migrations/versions/004_create_server_subnet.py @@ -6,7 +6,7 @@ def upgrade(migrate_engine): try: meta = MetaData(bind=migrate_engine) server = Table('server', meta, autoload=True) - subnet = Column('subnet', Text) + subnet = Column('subnet', Integer, nullable=False) subnet.create(server) except: pass diff --git a/wg_dashboard_backend/migrations/versions/005_create_v6_address.py b/wg_dashboard_backend/migrations/versions/005_create_v6_address.py new file mode 100644 index 0000000..fff1984 --- /dev/null +++ b/wg_dashboard_backend/migrations/versions/005_create_v6_address.py @@ -0,0 +1,32 @@ +from sqlalchemy import * +from migrate import * + + +def upgrade(migrate_engine): + try: + meta = MetaData(bind=migrate_engine) + server = Table('server', meta, autoload=True) + v6_address_server = Column('v6_address', VARCHAR, unique=True, nullable=True) + v6_address_server.create(server) + + meta = MetaData(bind=migrate_engine) + peer = Table('peer', meta, autoload=True) + v6_address_peer = Column('v6_address', VARCHAR, nullable=True) + v6_address_peer.create(peer) + except: + pass + + +def downgrade(migrate_engine): + try: + meta = MetaData(bind=migrate_engine) + server = Table('server', meta, autoload=True) + server.c.v6_address.drop() + + meta = MetaData(bind=migrate_engine) + peer = Table('peer', meta, autoload=True) + peer.c.v6_address.drop() + except: + pass + + diff --git a/wg_dashboard_backend/migrations/versions/006_create_v6_subnet.py b/wg_dashboard_backend/migrations/versions/006_create_v6_subnet.py new file mode 100644 index 0000000..a17ff63 --- /dev/null +++ b/wg_dashboard_backend/migrations/versions/006_create_v6_subnet.py @@ -0,0 +1,21 @@ +from sqlalchemy import * +from migrate import * + + +def upgrade(migrate_engine): + try: + meta = MetaData(bind=migrate_engine) + peer = Table('server', meta, autoload=True) + v6_subnet = Column('v6_subnet', INTEGER) + v6_subnet.create(peer) + except: + pass + + +def downgrade(migrate_engine): + try: + meta = MetaData(bind=migrate_engine) + peer = Table('server', meta, autoload=True) + peer.c.v6_subnet.drop() + except: + pass diff --git a/wg_dashboard_backend/models.py b/wg_dashboard_backend/models.py index 0b615d5..c6eedb0 100644 --- a/wg_dashboard_backend/models.py +++ b/wg_dashboard_backend/models.py @@ -23,6 +23,8 @@ class WGServer(Base): interface = Column(sqlalchemy.String, unique=True, index=True) subnet = Column(sqlalchemy.Integer, nullable=False) address = Column(sqlalchemy.String, unique=True) + v6_address = Column(sqlalchemy.String, unique=True) + v6_subnet = Column(sqlalchemy.Integer, nullable=False) listen_port = Column(sqlalchemy.String, unique=True) private_key = Column(sqlalchemy.String) public_key = Column(sqlalchemy.String) @@ -43,6 +45,7 @@ class WGPeer(Base): id = Column(Integer, primary_key=True, index=True) name = Column(sqlalchemy.String, default="Unnamed") address = Column(sqlalchemy.String) + v6_address = Column(sqlalchemy.String) public_key = Column(sqlalchemy.String) private_key = Column(sqlalchemy.String) shared_key = Column(sqlalchemy.Text) diff --git a/wg_dashboard_backend/requirements.txt b/wg_dashboard_backend/requirements.txt index 1ec17e6..30347ba 100644 --- a/wg_dashboard_backend/requirements.txt +++ b/wg_dashboard_backend/requirements.txt @@ -12,3 +12,4 @@ jinja2 sqlalchemy_utils sqlalchemy-migrate requests +uvicorn \ No newline at end of file diff --git a/wg_dashboard_backend/routers/v1/peer.py b/wg_dashboard_backend/routers/v1/peer.py index 7f9f4d4..9d1b639 100644 --- a/wg_dashboard_backend/routers/v1/peer.py +++ b/wg_dashboard_backend/routers/v1/peer.py @@ -1,5 +1,5 @@ import ipaddress - +import itertools from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session @@ -13,20 +13,18 @@ import script.wireguard router = APIRouter() -@router.post("/add", response_model=schemas.WGPeer) -def add_peer( - peer_add: schemas.WGPeerAdd, - sess: Session = Depends(middleware.get_db) -): - server = schemas.WGServer(interface=peer_add.server_interface).from_db(sess) - peer = schemas.WGPeer(server_id=server.id) - - address_space = set(ipaddress.ip_network(f"{server.address}/{server.subnet}", strict=False).hosts()) +def generate_ip_address(server: schemas.WGServer, v6): + if v6: + address_space = set( + itertools.islice(ipaddress.ip_network("fd42:42:42::1/64", strict=False).hosts(), 1, 1024) + ) + else: + address_space = set(ipaddress.ip_network(f"{server.address}/{server.subnet}", strict=False).hosts()) occupied_space = set() # Try add server IP to list. try: - occupied_space.add(ipaddress.ip_address(server.address.split("/")[0])) + occupied_space.add(ipaddress.ip_address(server.v6_address if v6 else server.address)) except ValueError: pass @@ -34,14 +32,27 @@ def add_peer( # Try add peer ip to list. try: - occupied_space.add(ipaddress.ip_address(p.address.split("/")[0])) + occupied_space.add(ipaddress.ip_address(p.v6_address if v6 else p.address)) except ValueError as e: pass # Ignore invalid addresses. These are out of address_space address_space -= occupied_space # Select first available address - peer.address = str(list(sorted(address_space)).pop(0)) + return str(list(sorted(address_space)).pop(0)) + + +@router.post("/add", response_model=schemas.WGPeer) +def add_peer( + peer_add: schemas.WGPeerAdd, + sess: Session = Depends(middleware.get_db) +): + server = schemas.WGServer(interface=peer_add.server_interface).from_db(sess) + peer = schemas.WGPeer(server_id=server.id) + + if server.v6_address: + peer.v6_address = generate_ip_address(server, v6=True) + peer.address = generate_ip_address(server, v6=False) # Private public key generation keys = script.wireguard.generate_keys() diff --git a/wg_dashboard_backend/routers/v1/server.py b/wg_dashboard_backend/routers/v1/server.py index 1a44ee3..938881d 100644 --- a/wg_dashboard_backend/routers/v1/server.py +++ b/wg_dashboard_backend/routers/v1/server.py @@ -32,15 +32,30 @@ def add_interface( server: schemas.WGServerAdd, sess: Session = Depends(middleware.get_db) ): - server.post_up = server.post_up if server.post_up != "" else const.DEFAULT_POST_UP - server.post_down = server.post_up if server.post_up != "" else const.DEFAULT_POST_DOWN + + # Configure POST UP with defaults if not manually set. + if server.post_up == "": + server.post_up = const.DEFAULT_POST_UP + if server.v6_address is not None: + server.post_up += const.DEFAULT_POST_UP_v6 + + # Configure POST DOWN with defaults if not manually set. + if server.post_down == "": + server.post_down = const.DEFAULT_POST_DOWN + if server.v6_address is not None: + server.post_down += const.DEFAULT_POST_DOWN_v6 + peers = server.peers if server.peers else [] # Public/Private key try: - if server.filter_query(sess).count() != 0: - raise HTTPException(status_code=400, detail="The server interface %s already exists in the database" % server.interface) + if sess.query(models.WGServer)\ + .filter( + (models.WGServer.interface == server.interface) | + (models.WGServer.address == server.address) | + (models.WGServer.v6_address == server.v6_address)).count() != 0: + raise HTTPException(status_code=400, detail="The server interface or ip %s already exists in the database" % server.interface) if not server.private_key: keys = script.wireguard.generate_keys() @@ -153,11 +168,14 @@ def edit_server( peer=peer, server=server )) - peer.sync(sess) + + db_peer = models.WGPeer(**peer.dict()) + sess.merge(db_peer) + sess.commit() script.wireguard.start_interface(server) server.is_running = script.wireguard.is_running(server) - server.sync(sess) + server.sync(sess) # TODO - fix this sync mess. server.from_db(sess) return server diff --git a/wg_dashboard_backend/schemas.py b/wg_dashboard_backend/schemas.py index f7ebf54..f6cd8b7 100644 --- a/wg_dashboard_backend/schemas.py +++ b/wg_dashboard_backend/schemas.py @@ -114,6 +114,7 @@ class WGPeer(GenericModel): id: int = None name: str = None address: str = None + v6_address: str = None private_key: str = None public_key: str = None shared_key: str = None @@ -144,7 +145,9 @@ class PSK(GenericModel): class WGServer(GenericModel): id: int = None address: str = None + v6_address: str = None subnet: int = None + v6_subnet: int = None interface: str listen_port: int = None endpoint: str = None diff --git a/wg_dashboard_backend/script/wireguard.py b/wg_dashboard_backend/script/wireguard.py index 7459217..8286df4 100644 --- a/wg_dashboard_backend/script/wireguard.py +++ b/wg_dashboard_backend/script/wireguard.py @@ -27,6 +27,9 @@ class WGPermissionsError(Exception): pass +class WGPortAlreadyInUse(Exception): + pass + class TempServerFile(): def __init__(self, server: schemas.WGServer): self.server = server @@ -82,8 +85,11 @@ def start_interface(server: schemas.WGServer): output = subprocess.check_output(const.CMD_WG_QUICK + ["up", server_file], stderr=subprocess.STDOUT) return output except Exception as e: + print(e.output) if b'already exists' in e.output: raise WGAlreadyStartedError("The wireguard device %s is already started." % server.interface) + elif b'Address already in use' in e.output: + raise WGPortAlreadyInUse("The port %s is already used by another application." % server.listen_port) def stop_interface(server: schemas.WGServer): @@ -92,7 +98,6 @@ def stop_interface(server: schemas.WGServer): output = subprocess.check_output(const.CMD_WG_QUICK + ["down", server_file], stderr=subprocess.STDOUT) return output except Exception as e: - if b'is not a WireGuard interface' in e.output: raise WGAlreadyStoppedError("The wireguard device %s is already stopped." % server.interface) @@ -111,6 +116,7 @@ def is_running(server: schemas.WGServer): if output is None: return False except Exception as e: + print(e.output) if b'No such device' in e.output: return False return True @@ -197,13 +203,16 @@ def move_server_dir(interface, interface1): def generate_config(obj: typing.Union[typing.Dict[schemas.WGPeer, schemas.WGServer], schemas.WGServer]): if isinstance(obj, dict) and "server" in obj and "peer" in obj: template = "peer.j2" + is_ipv6 = obj["server"].v6_address is not None elif isinstance(obj, schemas.WGServer) or isinstance(obj, models.WGServer): template = "server.j2" + is_ipv6 = obj.v6_address is not None else: raise ValueError("Incorrect input type. Should be WGPeer or WGServer") result = util.jinja_env.get_template(template).render( - data=obj + data=obj, + is_ipv6=is_ipv6 ) return result diff --git a/wg_dashboard_backend/templates/peer.j2 b/wg_dashboard_backend/templates/peer.j2 index 5639bff..b78a305 100644 --- a/wg_dashboard_backend/templates/peer.j2 +++ b/wg_dashboard_backend/templates/peer.j2 @@ -1,5 +1,5 @@ [Interface] -Address = {{ data.peer.address }}/{{ data.server.subnet }} +Address = {{ data.peer.address }}/{{ data.server.subnet }}{%- if is_ipv6 -%},{{ data.peer.v6_address }}/{{ data.server.v6_subnet }}{%- endif %} PrivateKey = {{ data.peer.private_key }} DNS = {{ data.peer.dns }} diff --git a/wg_dashboard_backend/templates/server.j2 b/wg_dashboard_backend/templates/server.j2 index 4f1c83c..e542f3d 100644 --- a/wg_dashboard_backend/templates/server.j2 +++ b/wg_dashboard_backend/templates/server.j2 @@ -1,10 +1,10 @@ [Interface] -Address = {{ data.address }}/{{ data.subnet }} +Address = {{ data.address }}/{{ data.subnet }}{%- if is_ipv6 -%},{{ data.v6_address }}/{{ data.v6_subnet }}{%- endif %} ListenPort = {{ data.listen_port }} PrivateKey = {{ data.private_key }} -PostUp = {{ data.post_up }} -PostDown = {{ data.post_down }} +PostUp = {{ data.post_up }}{%- if is_ipv6 -%} {{ data.v6_post_up }}{%- endif %} +PostDown = {{ data.post_down }}{%- if is_ipv6 -%} {{ data.v6_post_down }}{%- endif %} {% for peer in data.peers %} [Peer] @@ -13,5 +13,5 @@ PublicKey = {{ peer.public_key }} {%- if peer.shared_key %} PresharedKey = {{ peer.shared_key }} {%- endif %} -AllowedIPs = {{ peer.address }}/32 +AllowedIPs = {{ peer.address }}/32{%- if is_ipv6 -%},{{ peer.v6_address }}/128{%- endif %} {% endfor %} diff --git a/wg_dashboard_frontend/package-lock.json b/wg_dashboard_frontend/package-lock.json index 9934e5e..9348e38 100644 --- a/wg_dashboard_frontend/package-lock.json +++ b/wg_dashboard_frontend/package-lock.json @@ -1479,6 +1479,11 @@ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true }, + "@jedmao/ini-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@jedmao/ini-parser/-/ini-parser-0.2.4.tgz", + "integrity": "sha512-h/hrZFTA23DAXBF2ssK2jGI9W6X0YYVmlzRpsuOAEXdRfBTfh9W47w26FDHmTqJEQGPNxHp0mqD3G+eThZGMag==" + }, "@jsdevtools/coverage-istanbul-loader": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.3.tgz", @@ -2127,6 +2132,14 @@ "resolved": "https://registry.npmjs.org/angular-material-dynamic-themes/-/angular-material-dynamic-themes-1.0.4.tgz", "integrity": "sha512-5oW++xtLOAYyLGUN5o/KUKhWhkoQwpr3G9xMXepJV0fbezr7R7GDR+wzJNxOCY8BbiWvNO6S2AHG2s7dpCqykQ==" }, + "angular-notifier": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/angular-notifier/-/angular-notifier-6.0.1.tgz", + "integrity": "sha512-eC312pNkHfjBU1nflOxtRKbkuxZGthZj23wK1unZ9u+sIhdrUm2Fd2j4Tud8+5z9wQX+ec59v4pBfayO3AFx+Q==", + "requires": { + "tslib": "^1.9.0" + } + }, "angularx-qrcode": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/angularx-qrcode/-/angularx-qrcode-2.1.0.tgz", @@ -3070,6 +3083,14 @@ "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==", "dev": true }, + "chainsaw": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.0.9.tgz", + "integrity": "sha1-EaBRAtHEx4W20EFdM21aOhYSkT4=", + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, "chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -3753,8 +3774,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { "version": "5.2.1", @@ -5067,6 +5087,11 @@ "schema-utils": "^2.6.5" } }, + "file-saver": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz", + "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -5731,6 +5756,14 @@ "minimalistic-assert": "^1.0.1" } }, + "hashish": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz", + "integrity": "sha1-bWC8b/r3Ebav1g5CbQd5iAFOZVQ=", + "requires": { + "traverse": ">=0.2.4" + } + }, "hex-color-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", @@ -5984,6 +6017,11 @@ "dev": true, "optional": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "import-cwd": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", @@ -6099,6 +6137,11 @@ "through": "^2.3.6" } }, + "install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==" + }, "internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -6588,6 +6631,11 @@ "integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==", "dev": true }, + "js-file-download": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.11.tgz", + "integrity": "sha512-VHrnxlhsZC7lTs+WlQ8Tj+7LVFflll3C6ac3Y0akOT+oEgBFT5WIK2PF7wYWdHh8BfKdy8H0IpUkMWJW12jovQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6684,6 +6732,17 @@ "verror": "1.10.0" } }, + "jszip": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.3.0.tgz", + "integrity": "sha512-EJ9k766htB1ZWnsV5ZMDkKLgA+201r/ouFF8R2OigVjVdcm2rurcBrrdXaeqBJbqnUVMko512PYmlncBKE1Huw==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, "karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -6817,6 +6876,14 @@ "webpack-sources": "^1.2.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -7970,227 +8037,3333 @@ "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", "dev": true }, - "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "dev": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-install-checks": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", - "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", - "dev": true, - "requires": { - "semver": "^7.1.1" - }, - "dependencies": { - "semver": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.2.1.tgz", - "integrity": "sha512-aHhm1pD02jXXkyIpq25qBZjr3CQgg8KST8uX0OWXch3xE6jw+1bfbWnCjzMwojsTquroUmKFHNzU6x26mEiRxw==", - "dev": true - } - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true - }, - "npm-package-arg": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.0.1.tgz", - "integrity": "sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ==", - "dev": true, - "requires": { - "hosted-git-info": "^3.0.2", - "semver": "^7.0.0", - "validate-npm-package-name": "^3.0.0" - }, - "dependencies": { - "semver": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.2.1.tgz", - "integrity": "sha512-aHhm1pD02jXXkyIpq25qBZjr3CQgg8KST8uX0OWXch3xE6jw+1bfbWnCjzMwojsTquroUmKFHNzU6x26mEiRxw==", - "dev": true - } - } - }, - "npm-packlist": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.1.1.tgz", - "integrity": "sha512-95TSDvGwujIhqfSpIiRRLodEF+y6mJMopuZdahoGzqtRDFZXGav46S0p6ngeWaiAkb5R72w6eVARhzej0HvZeQ==", - "dev": true, - "requires": { + "npm": { + "version": "6.14.4", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.4.tgz", + "integrity": "sha512-B8UDDbWvdkW6RgXFn8/h2cHJP/u/FPa4HWeGzW23aNEBARN3QPrRaHqPIZW2NSN3fW649gtgUDNZpaRs0zTMPw==", + "requires": { + "JSONStream": "^1.3.5", + "abbrev": "~1.1.1", + "ansicolors": "~0.3.2", + "ansistyles": "~0.1.3", + "aproba": "^2.0.0", + "archy": "~1.0.0", + "bin-links": "^1.1.7", + "bluebird": "^3.5.5", + "byte-size": "^5.0.1", + "cacache": "^12.0.3", + "call-limit": "^1.1.1", + "chownr": "^1.1.4", + "ci-info": "^2.0.0", + "cli-columns": "^3.1.2", + "cli-table3": "^0.5.1", + "cmd-shim": "^3.0.3", + "columnify": "~1.5.4", + "config-chain": "^1.1.12", + "debuglog": "*", + "detect-indent": "~5.0.0", + "detect-newline": "^2.1.0", + "dezalgo": "~1.0.3", + "editor": "~1.0.0", + "figgy-pudding": "^3.5.1", + "find-npm-prefix": "^1.0.2", + "fs-vacuum": "~1.2.10", + "fs-write-stream-atomic": "~1.0.10", + "gentle-fs": "^2.3.0", "glob": "^7.1.6", - "ignore-walk": "^3.0.3", - "npm-bundled": "^1.1.1", - "npm-normalize-package-bin": "^1.0.1" + "graceful-fs": "^4.2.3", + "has-unicode": "~2.0.1", + "hosted-git-info": "^2.8.8", + "iferr": "^1.0.2", + "imurmurhash": "*", + "infer-owner": "^1.0.4", + "inflight": "~1.0.6", + "inherits": "^2.0.4", + "ini": "^1.3.5", + "init-package-json": "^1.10.3", + "is-cidr": "^3.0.0", + "json-parse-better-errors": "^1.0.2", + "lazy-property": "~1.0.0", + "libcipm": "^4.0.7", + "libnpm": "^3.0.1", + "libnpmaccess": "^3.0.2", + "libnpmhook": "^5.0.3", + "libnpmorg": "^1.0.1", + "libnpmsearch": "^2.0.2", + "libnpmteam": "^1.0.2", + "libnpx": "^10.2.2", + "lock-verify": "^2.1.0", + "lockfile": "^1.0.4", + "lodash._baseindexof": "*", + "lodash._baseuniq": "~4.6.0", + "lodash._bindcallback": "*", + "lodash._cacheindexof": "*", + "lodash._createcache": "*", + "lodash._getnative": "*", + "lodash.clonedeep": "~4.5.0", + "lodash.restparam": "*", + "lodash.union": "~4.6.0", + "lodash.uniq": "~4.5.0", + "lodash.without": "~4.4.0", + "lru-cache": "^5.1.1", + "meant": "~1.0.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.4", + "move-concurrently": "^1.0.1", + "node-gyp": "^5.1.0", + "nopt": "~4.0.1", + "normalize-package-data": "^2.5.0", + "npm-audit-report": "^1.3.2", + "npm-cache-filename": "~1.0.2", + "npm-install-checks": "^3.0.2", + "npm-lifecycle": "^3.1.4", + "npm-package-arg": "^6.1.1", + "npm-packlist": "^1.4.8", + "npm-pick-manifest": "^3.0.2", + "npm-profile": "^4.0.4", + "npm-registry-fetch": "^4.0.3", + "npm-user-validate": "~1.0.0", + "npmlog": "~4.1.2", + "once": "~1.4.0", + "opener": "^1.5.1", + "osenv": "^0.1.5", + "pacote": "^9.5.12", + "path-is-inside": "~1.0.2", + "promise-inflight": "~1.0.1", + "qrcode-terminal": "^0.12.0", + "query-string": "^6.8.2", + "qw": "~1.0.1", + "read": "~1.0.7", + "read-cmd-shim": "^1.0.5", + "read-installed": "~4.0.3", + "read-package-json": "^2.1.1", + "read-package-tree": "^5.3.1", + "readable-stream": "^3.6.0", + "readdir-scoped-modules": "^1.1.0", + "request": "^2.88.0", + "retry": "^0.12.0", + "rimraf": "^2.7.1", + "safe-buffer": "^5.1.2", + "semver": "^5.7.1", + "sha": "^3.0.0", + "slide": "~1.1.6", + "sorted-object": "~2.0.1", + "sorted-union-stream": "~2.1.3", + "ssri": "^6.0.1", + "stringify-package": "^1.0.1", + "tar": "^4.4.13", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "uid-number": "0.0.6", + "umask": "~1.1.0", + "unique-filename": "^1.1.1", + "unpipe": "~1.0.0", + "update-notifier": "^2.5.0", + "uuid": "^3.3.3", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "~3.0.0", + "which": "^1.3.1", + "worker-farm": "^1.7.0", + "write-file-atomic": "^2.4.3" }, "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, + "JSONStream": { + "version": "1.3.5", + "bundled": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" } - } - } - }, - "npm-pick-manifest": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.0.0.tgz", - "integrity": "sha512-PdJpXMvjqt4nftNEDpCgjBUF8yI3Q3MyuAmVB9nemnnCg32F4BPL/JFBfdj8DubgHCYUFQhtLWmBPvdsFtjWMg==", - "dev": true, - "requires": { - "npm-install-checks": "^4.0.0", - "npm-package-arg": "^8.0.0", - "semver": "^7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.2.1.tgz", - "integrity": "sha512-aHhm1pD02jXXkyIpq25qBZjr3CQgg8KST8uX0OWXch3xE6jw+1bfbWnCjzMwojsTquroUmKFHNzU6x26mEiRxw==", - "dev": true - } - } - }, - "npm-registry-fetch": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-8.0.0.tgz", - "integrity": "sha512-975WwLvZjX97y9UWWQ8nAyr7bw02s9xKPHqvEm5T900LQsB1HXb8Gb9ebYtCBLSX+K8gSOrO5KS/9yV/naLZmQ==", - "dev": true, - "requires": { - "@npmcli/ci-detect": "^1.0.0", - "lru-cache": "^5.1.1", - "make-fetch-happen": "^8.0.2", - "minipass": "^3.0.0", - "minipass-fetch": "^1.1.2", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.0.0", - "npm-package-arg": "^8.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, + }, + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "agent-base": { + "version": "4.3.0", + "bundled": true, "requires": { - "is-descriptor": "^0.1.0" + "es6-promisify": "^5.0.0" } }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, + "agentkeepalive": { + "version": "3.5.2", + "bundled": true, "requires": { - "is-buffer": "^1.1.5" + "humanize-ms": "^1.2.1" } - } - } - }, - "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true - }, - "object-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", - "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==", - "dev": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + }, + "ajv": { + "version": "5.5.2", + "bundled": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ansi-align": { + "version": "2.0.0", + "bundled": true, + "requires": { + "string-width": "^2.0.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "ansi-styles": { + "version": "3.2.1", + "bundled": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "ansicolors": { + "version": "0.3.2", + "bundled": true + }, + "ansistyles": { + "version": "0.1.3", + "bundled": true + }, + "aproba": { + "version": "2.0.0", + "bundled": true + }, + "archy": { + "version": "1.0.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "asap": { + "version": "2.0.6", + "bundled": true + }, + "asn1": { + "version": "0.2.4", + "bundled": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "bundled": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true + }, + "aws-sign2": { + "version": "0.7.0", + "bundled": true + }, + "aws4": { + "version": "1.8.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bin-links": { + "version": "1.1.7", + "bundled": true, + "requires": { + "bluebird": "^3.5.3", + "cmd-shim": "^3.0.0", + "gentle-fs": "^2.3.0", + "graceful-fs": "^4.1.15", + "npm-normalize-package-bin": "^1.0.0", + "write-file-atomic": "^2.3.0" + } + }, + "bluebird": { + "version": "3.5.5", + "bundled": true + }, + "boxen": { + "version": "1.3.0", + "bundled": true, + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-from": { + "version": "1.0.0", + "bundled": true + }, + "builtins": { + "version": "1.0.3", + "bundled": true + }, + "byline": { + "version": "5.0.0", + "bundled": true + }, + "byte-size": { + "version": "5.0.1", + "bundled": true + }, + "cacache": { + "version": "12.0.3", + "bundled": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "call-limit": { + "version": "1.1.1", + "bundled": true + }, + "camelcase": { + "version": "4.1.0", + "bundled": true + }, + "capture-stack-trace": { + "version": "1.0.0", + "bundled": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true + }, + "chalk": { + "version": "2.4.1", + "bundled": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chownr": { + "version": "1.1.4", + "bundled": true + }, + "ci-info": { + "version": "2.0.0", + "bundled": true + }, + "cidr-regex": { + "version": "2.0.10", + "bundled": true, + "requires": { + "ip-regex": "^2.1.0" + } + }, + "cli-boxes": { + "version": "1.0.0", + "bundled": true + }, + "cli-columns": { + "version": "3.1.2", + "bundled": true, + "requires": { + "string-width": "^2.0.0", + "strip-ansi": "^3.0.1" + } + }, + "cli-table3": { + "version": "0.5.1", + "bundled": true, + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + } + }, + "cliui": { + "version": "4.1.0", + "bundled": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "clone": { + "version": "1.0.4", + "bundled": true + }, + "cmd-shim": { + "version": "3.0.3", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "mkdirp": "~0.5.0" + } + }, + "co": { + "version": "4.6.0", + "bundled": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "color-convert": { + "version": "1.9.1", + "bundled": true, + "requires": { + "color-name": "^1.1.1" + } + }, + "color-name": { + "version": "1.1.3", + "bundled": true + }, + "colors": { + "version": "1.3.3", + "bundled": true, + "optional": true + }, + "columnify": { + "version": "1.5.4", + "bundled": true, + "requires": { + "strip-ansi": "^3.0.0", + "wcwidth": "^1.0.0" + } + }, + "combined-stream": { + "version": "1.0.6", + "bundled": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "concat-stream": { + "version": "1.6.2", + "bundled": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "config-chain": { + "version": "1.1.12", + "bundled": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "configstore": { + "version": "3.1.2", + "bundled": true, + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "copy-concurrently": { + "version": "1.0.5", + "bundled": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + }, + "dependencies": { + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "iferr": { + "version": "0.1.5", + "bundled": true + } + } + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "create-error-class": { + "version": "3.0.2", + "bundled": true, + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, + "cross-spawn": { + "version": "5.1.0", + "bundled": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "bundled": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "bundled": true + } + } + }, + "crypto-random-string": { + "version": "1.0.0", + "bundled": true + }, + "cyclist": { + "version": "0.2.2", + "bundled": true + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "3.1.0", + "bundled": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "bundled": true + } + } + }, + "debuglog": { + "version": "1.0.1", + "bundled": true + }, + "decamelize": { + "version": "1.2.0", + "bundled": true + }, + "decode-uri-component": { + "version": "0.2.0", + "bundled": true + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true + }, + "defaults": { + "version": "1.0.3", + "bundled": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-properties": { + "version": "1.1.3", + "bundled": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "detect-indent": { + "version": "5.0.0", + "bundled": true + }, + "detect-newline": { + "version": "2.1.0", + "bundled": true + }, + "dezalgo": { + "version": "1.0.3", + "bundled": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "dot-prop": { + "version": "4.2.0", + "bundled": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "dotenv": { + "version": "5.0.1", + "bundled": true + }, + "duplexer3": { + "version": "0.1.4", + "bundled": true + }, + "duplexify": { + "version": "3.6.0", + "bundled": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "editor": { + "version": "1.0.0", + "bundled": true + }, + "encoding": { + "version": "0.1.12", + "bundled": true, + "requires": { + "iconv-lite": "~0.4.13" + } + }, + "end-of-stream": { + "version": "1.4.1", + "bundled": true, + "requires": { + "once": "^1.4.0" + } + }, + "env-paths": { + "version": "2.2.0", + "bundled": true + }, + "err-code": { + "version": "1.1.2", + "bundled": true + }, + "errno": { + "version": "0.1.7", + "bundled": true, + "requires": { + "prr": "~1.0.1" + } + }, + "es-abstract": { + "version": "1.12.0", + "bundled": true, + "requires": { + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "bundled": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-promise": { + "version": "4.2.8", + "bundled": true + }, + "es6-promisify": { + "version": "5.0.0", + "bundled": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true + }, + "execa": { + "version": "0.7.0", + "bundled": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "bundled": true + } + } + }, + "extend": { + "version": "3.0.2", + "bundled": true + }, + "extsprintf": { + "version": "1.3.0", + "bundled": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "bundled": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "bundled": true + }, + "figgy-pudding": { + "version": "3.5.1", + "bundled": true + }, + "find-npm-prefix": { + "version": "1.0.2", + "bundled": true + }, + "find-up": { + "version": "2.1.0", + "bundled": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "flush-write-stream": { + "version": "1.0.3", + "bundled": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.4" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true + }, + "form-data": { + "version": "2.3.2", + "bundled": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "from2": { + "version": "2.3.0", + "bundled": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs-minipass": { + "version": "1.2.7", + "bundled": true, + "requires": { + "minipass": "^2.6.0" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } + } + }, + "fs-vacuum": { + "version": "1.2.10", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "path-is-inside": "^1.0.1", + "rimraf": "^2.5.2" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + }, + "dependencies": { + "iferr": { + "version": "0.1.5", + "bundled": true + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "function-bind": { + "version": "1.1.1", + "bundled": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "genfun": { + "version": "5.0.0", + "bundled": true + }, + "gentle-fs": { + "version": "2.3.0", + "bundled": true, + "requires": { + "aproba": "^1.1.2", + "chownr": "^1.1.2", + "cmd-shim": "^3.0.3", + "fs-vacuum": "^1.2.10", + "graceful-fs": "^4.1.11", + "iferr": "^0.1.5", + "infer-owner": "^1.0.4", + "mkdirp": "^0.5.1", + "path-is-inside": "^1.0.2", + "read-cmd-shim": "^1.0.1", + "slide": "^1.1.6" + }, + "dependencies": { + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "iferr": { + "version": "0.1.5", + "bundled": true + } + } + }, + "get-caller-file": { + "version": "1.0.3", + "bundled": true + }, + "get-stream": { + "version": "4.1.0", + "bundled": true, + "requires": { + "pump": "^3.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "global-dirs": { + "version": "0.1.1", + "bundled": true, + "requires": { + "ini": "^1.3.4" + } + }, + "got": { + "version": "6.7.1", + "bundled": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "bundled": true + } + } + }, + "graceful-fs": { + "version": "4.2.3", + "bundled": true + }, + "har-schema": { + "version": "2.0.0", + "bundled": true + }, + "har-validator": { + "version": "5.1.0", + "bundled": true, + "requires": { + "ajv": "^5.3.0", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "bundled": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "bundled": true + }, + "has-symbols": { + "version": "1.0.0", + "bundled": true + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "hosted-git-info": { + "version": "2.8.8", + "bundled": true + }, + "http-cache-semantics": { + "version": "3.8.1", + "bundled": true + }, + "http-proxy-agent": { + "version": "2.1.0", + "bundled": true, + "requires": { + "agent-base": "4", + "debug": "3.1.0" + } + }, + "http-signature": { + "version": "1.2.0", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "bundled": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, + "humanize-ms": { + "version": "1.2.1", + "bundled": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.23", + "bundled": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "iferr": { + "version": "1.0.2", + "bundled": true + }, + "ignore-walk": { + "version": "3.0.3", + "bundled": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "import-lazy": { + "version": "2.1.0", + "bundled": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true + }, + "infer-owner": { + "version": "1.0.4", + "bundled": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true + }, + "init-package-json": { + "version": "1.10.3", + "bundled": true, + "requires": { + "glob": "^7.1.1", + "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", + "promzard": "^0.3.0", + "read": "~1.0.1", + "read-package-json": "1 || 2", + "semver": "2.x || 3.x || 4 || 5", + "validate-npm-package-license": "^3.0.1", + "validate-npm-package-name": "^3.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "bundled": true + }, + "ip": { + "version": "1.1.5", + "bundled": true + }, + "ip-regex": { + "version": "2.1.0", + "bundled": true + }, + "is-callable": { + "version": "1.1.4", + "bundled": true + }, + "is-ci": { + "version": "1.2.1", + "bundled": true, + "requires": { + "ci-info": "^1.5.0" + }, + "dependencies": { + "ci-info": { + "version": "1.6.0", + "bundled": true + } + } + }, + "is-cidr": { + "version": "3.0.0", + "bundled": true, + "requires": { + "cidr-regex": "^2.0.10" + } + }, + "is-date-object": { + "version": "1.0.1", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-installed-globally": { + "version": "0.1.0", + "bundled": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "bundled": true + }, + "is-obj": { + "version": "1.0.1", + "bundled": true + }, + "is-path-inside": { + "version": "1.0.1", + "bundled": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-redirect": { + "version": "1.0.0", + "bundled": true + }, + "is-regex": { + "version": "1.0.4", + "bundled": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-retry-allowed": { + "version": "1.2.0", + "bundled": true + }, + "is-stream": { + "version": "1.1.0", + "bundled": true + }, + "is-symbol": { + "version": "1.0.2", + "bundled": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "bundled": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "bundled": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true + }, + "jsonparse": { + "version": "1.3.1", + "bundled": true + }, + "jsprim": { + "version": "1.4.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "latest-version": { + "version": "3.1.0", + "bundled": true, + "requires": { + "package-json": "^4.0.0" + } + }, + "lazy-property": { + "version": "1.0.0", + "bundled": true + }, + "lcid": { + "version": "2.0.0", + "bundled": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "libcipm": { + "version": "4.0.7", + "bundled": true, + "requires": { + "bin-links": "^1.1.2", + "bluebird": "^3.5.1", + "figgy-pudding": "^3.5.1", + "find-npm-prefix": "^1.0.2", + "graceful-fs": "^4.1.11", + "ini": "^1.3.5", + "lock-verify": "^2.0.2", + "mkdirp": "^0.5.1", + "npm-lifecycle": "^3.0.0", + "npm-logical-tree": "^1.2.1", + "npm-package-arg": "^6.1.0", + "pacote": "^9.1.0", + "read-package-json": "^2.0.13", + "rimraf": "^2.6.2", + "worker-farm": "^1.6.0" + } + }, + "libnpm": { + "version": "3.0.1", + "bundled": true, + "requires": { + "bin-links": "^1.1.2", + "bluebird": "^3.5.3", + "find-npm-prefix": "^1.0.2", + "libnpmaccess": "^3.0.2", + "libnpmconfig": "^1.2.1", + "libnpmhook": "^5.0.3", + "libnpmorg": "^1.0.1", + "libnpmpublish": "^1.1.2", + "libnpmsearch": "^2.0.2", + "libnpmteam": "^1.0.2", + "lock-verify": "^2.0.2", + "npm-lifecycle": "^3.0.0", + "npm-logical-tree": "^1.2.1", + "npm-package-arg": "^6.1.0", + "npm-profile": "^4.0.2", + "npm-registry-fetch": "^4.0.0", + "npmlog": "^4.1.2", + "pacote": "^9.5.3", + "read-package-json": "^2.0.13", + "stringify-package": "^1.0.0" + } + }, + "libnpmaccess": { + "version": "3.0.2", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "get-stream": "^4.0.0", + "npm-package-arg": "^6.1.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "libnpmconfig": { + "version": "1.2.1", + "bundled": true, + "requires": { + "figgy-pudding": "^3.5.1", + "find-up": "^3.0.0", + "ini": "^1.3.5" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "bundled": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "bundled": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "bundled": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "bundled": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "bundled": true + } + } + }, + "libnpmhook": { + "version": "5.0.3", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.4.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "libnpmorg": { + "version": "1.0.1", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.4.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "libnpmpublish": { + "version": "1.1.2", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.0.0", + "lodash.clonedeep": "^4.5.0", + "normalize-package-data": "^2.4.0", + "npm-package-arg": "^6.1.0", + "npm-registry-fetch": "^4.0.0", + "semver": "^5.5.1", + "ssri": "^6.0.1" + } + }, + "libnpmsearch": { + "version": "2.0.2", + "bundled": true, + "requires": { + "figgy-pudding": "^3.5.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "libnpmteam": { + "version": "1.0.2", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.4.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "libnpx": { + "version": "10.2.2", + "bundled": true, + "requires": { + "dotenv": "^5.0.1", + "npm-package-arg": "^6.0.0", + "rimraf": "^2.6.2", + "safe-buffer": "^5.1.0", + "update-notifier": "^2.3.0", + "which": "^1.3.0", + "y18n": "^4.0.0", + "yargs": "^11.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "bundled": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lock-verify": { + "version": "2.1.0", + "bundled": true, + "requires": { + "npm-package-arg": "^6.1.0", + "semver": "^5.4.1" + } + }, + "lockfile": { + "version": "1.0.4", + "bundled": true, + "requires": { + "signal-exit": "^3.0.2" + } + }, + "lodash._baseindexof": { + "version": "3.1.0", + "bundled": true + }, + "lodash._baseuniq": { + "version": "4.6.0", + "bundled": true, + "requires": { + "lodash._createset": "~4.0.0", + "lodash._root": "~3.0.0" + } + }, + "lodash._bindcallback": { + "version": "3.0.1", + "bundled": true + }, + "lodash._cacheindexof": { + "version": "3.0.2", + "bundled": true + }, + "lodash._createcache": { + "version": "3.1.2", + "bundled": true, + "requires": { + "lodash._getnative": "^3.0.0" + } + }, + "lodash._createset": { + "version": "4.0.3", + "bundled": true + }, + "lodash._getnative": { + "version": "3.9.1", + "bundled": true + }, + "lodash._root": { + "version": "3.0.1", + "bundled": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "bundled": true + }, + "lodash.restparam": { + "version": "3.6.1", + "bundled": true + }, + "lodash.union": { + "version": "4.6.0", + "bundled": true + }, + "lodash.uniq": { + "version": "4.5.0", + "bundled": true + }, + "lodash.without": { + "version": "4.4.0", + "bundled": true + }, + "lowercase-keys": { + "version": "1.0.1", + "bundled": true + }, + "lru-cache": { + "version": "5.1.1", + "bundled": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "1.3.0", + "bundled": true, + "requires": { + "pify": "^3.0.0" + } + }, + "make-fetch-happen": { + "version": "5.0.2", + "bundled": true, + "requires": { + "agentkeepalive": "^3.4.1", + "cacache": "^12.0.0", + "http-cache-semantics": "^3.8.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "node-fetch-npm": "^2.0.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^6.0.0" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "bundled": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "meant": { + "version": "1.0.1", + "bundled": true + }, + "mem": { + "version": "4.3.0", + "bundled": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "bundled": true + } + } + }, + "mime-db": { + "version": "1.35.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.19", + "bundled": true, + "requires": { + "mime-db": "~1.35.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minizlib": { + "version": "1.3.3", + "bundled": true, + "requires": { + "minipass": "^2.9.0" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } + } + }, + "mississippi": { + "version": "3.0.0", + "bundled": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mkdirp": { + "version": "0.5.4", + "bundled": true, + "requires": { + "minimist": "^1.2.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "bundled": true + } + } + }, + "move-concurrently": { + "version": "1.0.1", + "bundled": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + }, + "dependencies": { + "aproba": { + "version": "1.2.0", + "bundled": true + } + } + }, + "ms": { + "version": "2.1.1", + "bundled": true + }, + "mute-stream": { + "version": "0.0.7", + "bundled": true + }, + "nice-try": { + "version": "1.0.5", + "bundled": true + }, + "node-fetch-npm": { + "version": "2.0.2", + "bundled": true, + "requires": { + "encoding": "^0.1.11", + "json-parse-better-errors": "^1.0.0", + "safe-buffer": "^5.1.1" + } + }, + "node-gyp": { + "version": "5.1.0", + "bundled": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "npmlog": "^4.1.2", + "request": "^2.88.0", + "rimraf": "^2.6.3", + "semver": "^5.7.1", + "tar": "^4.4.12", + "which": "^1.3.1" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "bundled": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "resolve": { + "version": "1.10.0", + "bundled": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "npm-audit-report": { + "version": "1.3.2", + "bundled": true, + "requires": { + "cli-table3": "^0.5.0", + "console-control-strings": "^1.1.0" + } + }, + "npm-bundled": { + "version": "1.1.1", + "bundled": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-cache-filename": { + "version": "1.0.2", + "bundled": true + }, + "npm-install-checks": { + "version": "3.0.2", + "bundled": true, + "requires": { + "semver": "^2.3.0 || 3.x || 4 || 5" + } + }, + "npm-lifecycle": { + "version": "3.1.4", + "bundled": true, + "requires": { + "byline": "^5.0.0", + "graceful-fs": "^4.1.15", + "node-gyp": "^5.0.2", + "resolve-from": "^4.0.0", + "slide": "^1.1.6", + "uid-number": "0.0.6", + "umask": "^1.1.0", + "which": "^1.3.1" + } + }, + "npm-logical-tree": { + "version": "1.2.1", + "bundled": true + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true + }, + "npm-package-arg": { + "version": "6.1.1", + "bundled": true, + "requires": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "npm-packlist": { + "version": "1.4.8", + "bundled": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "3.0.2", + "bundled": true, + "requires": { + "figgy-pudding": "^3.5.1", + "npm-package-arg": "^6.0.0", + "semver": "^5.4.1" + } + }, + "npm-profile": { + "version": "4.0.4", + "bundled": true, + "requires": { + "aproba": "^1.1.2 || 2", + "figgy-pudding": "^3.4.1", + "npm-registry-fetch": "^4.0.0" + } + }, + "npm-registry-fetch": { + "version": "4.0.3", + "bundled": true, + "requires": { + "JSONStream": "^1.3.4", + "bluebird": "^3.5.1", + "figgy-pudding": "^3.4.1", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "npm-package-arg": "^6.1.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "bundled": true + } + } + }, + "npm-run-path": { + "version": "2.0.2", + "bundled": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npm-user-validate": { + "version": "1.0.0", + "bundled": true + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "oauth-sign": { + "version": "0.9.0", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "object-keys": { + "version": "1.0.12", + "bundled": true + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "bundled": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "opener": { + "version": "1.5.1", + "bundled": true + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-locale": { + "version": "3.1.0", + "bundled": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "bundled": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "bundled": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + } + } + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "bundled": true + }, + "p-finally": { + "version": "1.0.0", + "bundled": true + }, + "p-is-promise": { + "version": "2.1.0", + "bundled": true + }, + "p-limit": { + "version": "1.2.0", + "bundled": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "bundled": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "bundled": true + }, + "package-json": { + "version": "4.0.1", + "bundled": true, + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, + "pacote": { + "version": "9.5.12", + "bundled": true, + "requires": { + "bluebird": "^3.5.3", + "cacache": "^12.0.2", + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.1.0", + "glob": "^7.1.3", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "minimatch": "^3.0.4", + "minipass": "^2.3.5", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "normalize-package-data": "^2.4.0", + "npm-normalize-package-bin": "^1.0.0", + "npm-package-arg": "^6.1.0", + "npm-packlist": "^1.1.12", + "npm-pick-manifest": "^3.0.0", + "npm-registry-fetch": "^4.0.0", + "osenv": "^0.1.5", + "promise-inflight": "^1.0.1", + "promise-retry": "^1.1.1", + "protoduck": "^5.0.1", + "rimraf": "^2.6.2", + "safe-buffer": "^5.1.2", + "semver": "^5.6.0", + "ssri": "^6.0.1", + "tar": "^4.4.10", + "unique-filename": "^1.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } + } + }, + "parallel-transform": { + "version": "1.1.0", + "bundled": true, + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "path-exists": { + "version": "3.0.0", + "bundled": true + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "path-is-inside": { + "version": "1.0.2", + "bundled": true + }, + "path-key": { + "version": "2.0.1", + "bundled": true + }, + "path-parse": { + "version": "1.0.6", + "bundled": true + }, + "performance-now": { + "version": "2.1.0", + "bundled": true + }, + "pify": { + "version": "3.0.0", + "bundled": true + }, + "prepend-http": { + "version": "1.0.4", + "bundled": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true + }, + "promise-inflight": { + "version": "1.0.1", + "bundled": true + }, + "promise-retry": { + "version": "1.1.1", + "bundled": true, + "requires": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + }, + "dependencies": { + "retry": { + "version": "0.10.1", + "bundled": true + } + } + }, + "promzard": { + "version": "0.3.0", + "bundled": true, + "requires": { + "read": "1" + } + }, + "proto-list": { + "version": "1.2.4", + "bundled": true + }, + "protoduck": { + "version": "5.0.1", + "bundled": true, + "requires": { + "genfun": "^5.0.0" + } + }, + "prr": { + "version": "1.0.1", + "bundled": true + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true + }, + "psl": { + "version": "1.1.29", + "bundled": true + }, + "pump": { + "version": "3.0.0", + "bundled": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "bundled": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "bundled": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "1.4.1", + "bundled": true + }, + "qrcode-terminal": { + "version": "0.12.0", + "bundled": true + }, + "qs": { + "version": "6.5.2", + "bundled": true + }, + "query-string": { + "version": "6.8.2", + "bundled": true, + "requires": { + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, + "qw": { + "version": "1.0.1", + "bundled": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "bundled": true + } + } + }, + "read": { + "version": "1.0.7", + "bundled": true, + "requires": { + "mute-stream": "~0.0.4" + } + }, + "read-cmd-shim": { + "version": "1.0.5", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2" + } + }, + "read-installed": { + "version": "4.0.3", + "bundled": true, + "requires": { + "debuglog": "^1.0.1", + "graceful-fs": "^4.1.2", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + } + }, + "read-package-json": { + "version": "2.1.1", + "bundled": true, + "requires": { + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "json-parse-better-errors": "^1.0.1", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "read-package-tree": { + "version": "5.3.1", + "bundled": true, + "requires": { + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "util-promisify": "^2.1.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "bundled": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-scoped-modules": { + "version": "1.1.0", + "bundled": true, + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "registry-auth-token": { + "version": "3.4.0", + "bundled": true, + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "bundled": true, + "requires": { + "rc": "^1.0.1" + } + }, + "request": { + "version": "2.88.0", + "bundled": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require-directory": { + "version": "2.1.1", + "bundled": true + }, + "require-main-filename": { + "version": "1.0.1", + "bundled": true + }, + "resolve-from": { + "version": "4.0.0", + "bundled": true + }, + "retry": { + "version": "0.12.0", + "bundled": true + }, + "rimraf": { + "version": "2.7.1", + "bundled": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-queue": { + "version": "1.0.3", + "bundled": true, + "requires": { + "aproba": "^1.1.1" + }, + "dependencies": { + "aproba": { + "version": "1.2.0", + "bundled": true + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true + }, + "semver": { + "version": "5.7.1", + "bundled": true + }, + "semver-diff": { + "version": "2.1.0", + "bundled": true, + "requires": { + "semver": "^5.0.3" + } + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "sha": { + "version": "3.0.0", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "bundled": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "slide": { + "version": "1.1.6", + "bundled": true + }, + "smart-buffer": { + "version": "4.1.0", + "bundled": true + }, + "socks": { + "version": "2.3.3", + "bundled": true, + "requires": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "bundled": true, + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "bundled": true, + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, + "sorted-object": { + "version": "2.0.1", + "bundled": true + }, + "sorted-union-stream": { + "version": "2.1.3", + "bundled": true, + "requires": { + "from2": "^1.3.0", + "stream-iterate": "^1.1.0" + }, + "dependencies": { + "from2": { + "version": "1.3.0", + "bundled": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~1.1.10" + } + }, + "isarray": { + "version": "0.0.1", + "bundled": true + }, + "readable-stream": { + "version": "1.1.14", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "bundled": true + } + } + }, + "spdx-correct": { + "version": "3.0.0", + "bundled": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "bundled": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "bundled": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.3", + "bundled": true + }, + "split-on-first": { + "version": "1.1.0", + "bundled": true + }, + "sshpk": { + "version": "1.14.2", + "bundled": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "bundled": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stream-each": { + "version": "1.2.2", + "bundled": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-iterate": { + "version": "1.2.0", + "bundled": true, + "requires": { + "readable-stream": "^2.1.5", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-shift": { + "version": "1.0.0", + "bundled": true + }, + "strict-uri-encode": { + "version": "2.0.0", + "bundled": true + }, + "string-width": { + "version": "2.1.1", + "bundled": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "bundled": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "bundled": true + } + } + }, + "stringify-package": { + "version": "1.0.1", + "bundled": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "bundled": true + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true + }, + "supports-color": { + "version": "5.4.0", + "bundled": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "4.4.13", + "bundled": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } + } + }, + "term-size": { + "version": "1.2.0", + "bundled": true, + "requires": { + "execa": "^0.7.0" + } + }, + "text-table": { + "version": "0.2.0", + "bundled": true + }, + "through": { + "version": "2.3.8", + "bundled": true + }, + "through2": { + "version": "2.0.3", + "bundled": true, + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "timed-out": { + "version": "4.0.1", + "bundled": true + }, + "tiny-relative-date": { + "version": "1.3.0", + "bundled": true + }, + "tough-cookie": { + "version": "2.4.3", + "bundled": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true + }, + "typedarray": { + "version": "0.0.6", + "bundled": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true + }, + "umask": { + "version": "1.1.0", + "bundled": true + }, + "unique-filename": { + "version": "1.1.1", + "bundled": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.0", + "bundled": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unique-string": { + "version": "1.0.0", + "bundled": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "bundled": true + }, + "unzip-response": { + "version": "2.0.1", + "bundled": true + }, + "update-notifier": { + "version": "2.5.0", + "bundled": true, + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "url-parse-lax": { + "version": "1.0.0", + "bundled": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "util-extend": { + "version": "1.0.3", + "bundled": true + }, + "util-promisify": { + "version": "2.1.0", + "bundled": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "uuid": { + "version": "3.3.3", + "bundled": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "bundled": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "3.0.0", + "bundled": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "verror": { + "version": "1.10.0", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "bundled": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "which": { + "version": "1.3.1", + "bundled": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "bundled": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "requires": { + "string-width": "^1.0.2" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "widest-line": { + "version": "2.0.1", + "bundled": true, + "requires": { + "string-width": "^2.1.1" + } + }, + "worker-farm": { + "version": "1.7.0", + "bundled": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "bundled": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "write-file-atomic": { + "version": "2.4.3", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "bundled": true + }, + "xtend": { + "version": "4.0.1", + "bundled": true + }, + "y18n": { + "version": "4.0.0", + "bundled": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true + }, + "yargs": { + "version": "11.1.1", + "bundled": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" + }, + "dependencies": { + "y18n": { + "version": "3.2.1", + "bundled": true + } + } + }, + "yargs-parser": { + "version": "9.0.2", + "bundled": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "npm-bundled": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-install-checks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", + "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", + "dev": true, + "requires": { + "semver": "^7.1.1" + }, + "dependencies": { + "semver": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.2.1.tgz", + "integrity": "sha512-aHhm1pD02jXXkyIpq25qBZjr3CQgg8KST8uX0OWXch3xE6jw+1bfbWnCjzMwojsTquroUmKFHNzU6x26mEiRxw==", + "dev": true + } + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "npm-package-arg": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.0.1.tgz", + "integrity": "sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ==", + "dev": true, + "requires": { + "hosted-git-info": "^3.0.2", + "semver": "^7.0.0", + "validate-npm-package-name": "^3.0.0" + }, + "dependencies": { + "semver": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.2.1.tgz", + "integrity": "sha512-aHhm1pD02jXXkyIpq25qBZjr3CQgg8KST8uX0OWXch3xE6jw+1bfbWnCjzMwojsTquroUmKFHNzU6x26mEiRxw==", + "dev": true + } + } + }, + "npm-packlist": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.1.1.tgz", + "integrity": "sha512-95TSDvGwujIhqfSpIiRRLodEF+y6mJMopuZdahoGzqtRDFZXGav46S0p6ngeWaiAkb5R72w6eVARhzej0HvZeQ==", + "dev": true, + "requires": { + "glob": "^7.1.6", + "ignore-walk": "^3.0.3", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "npm-pick-manifest": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.0.0.tgz", + "integrity": "sha512-PdJpXMvjqt4nftNEDpCgjBUF8yI3Q3MyuAmVB9nemnnCg32F4BPL/JFBfdj8DubgHCYUFQhtLWmBPvdsFtjWMg==", + "dev": true, + "requires": { + "npm-install-checks": "^4.0.0", + "npm-package-arg": "^8.0.0", + "semver": "^7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.2.1.tgz", + "integrity": "sha512-aHhm1pD02jXXkyIpq25qBZjr3CQgg8KST8uX0OWXch3xE6jw+1bfbWnCjzMwojsTquroUmKFHNzU6x26mEiRxw==", + "dev": true + } + } + }, + "npm-registry-fetch": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-8.0.0.tgz", + "integrity": "sha512-975WwLvZjX97y9UWWQ8nAyr7bw02s9xKPHqvEm5T900LQsB1HXb8Gb9ebYtCBLSX+K8gSOrO5KS/9yV/naLZmQ==", + "dev": true, + "requires": { + "@npmcli/ci-detect": "^1.0.0", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^8.0.2", + "minipass": "^3.0.0", + "minipass-fetch": "^1.1.2", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.0.0", + "npm-package-arg": "^8.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", + "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", "dev": true, "requires": { "isobject": "^3.0.0" @@ -8511,8 +11684,7 @@ "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "parallel-transform": { "version": "1.2.0", @@ -9705,8 +12877,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "promise": { "version": "7.3.1", @@ -10205,7 +13376,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -10219,8 +13389,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" } } }, @@ -10416,6 +13585,14 @@ "xtend": "^4.0.1" } }, + "remove": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/remove/-/remove-0.1.5.tgz", + "integrity": "sha1-CV/9gn1lyfQa2X0z5BanWBEHmVU=", + "requires": { + "seq": ">= 0.3.5" + } + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -10983,6 +14160,15 @@ } } }, + "seq": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/seq/-/seq-0.3.5.tgz", + "integrity": "sha1-rgKvOkJHk9jMvyEtaRdODFTf/jg=", + "requires": { + "chainsaw": ">=0.0.7 <0.1", + "hashish": ">=0.0.2 <0.1" + } + }, "serialize-javascript": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", @@ -11062,6 +14248,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -11836,7 +15027,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -12805,6 +15995,11 @@ "punycode": "^2.1.1" } }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" + }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -13386,8 +16581,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util-promisify": { "version": "2.1.0", diff --git a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html index e32498a..f5c8882 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html +++ b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html @@ -20,52 +20,91 @@

Essentials

+
- + +
+ + + + + + +
Interface - + + + Endpoint + + + + + Port + + +
+ + + + +
- Address - + IPv4 Address + Subnet - +
+ + + +
+ IPv6 Support +
+ + + + + +
- Endpoint - + IPv6 Address + - - Port - + + Subnet + +
+
Default DNS - +
diff --git a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts index 2262814..77b5ac7 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts +++ b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import {Component, Input, OnInit, ViewChild, ViewEncapsulation} from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { IPValidator } from '../../../validators/ip-address.validator'; import { NumberValidator } from '../../../validators/number.validator'; @@ -10,6 +10,7 @@ import {Peer} from "../../../interfaces/peer"; import {forkJoin, from} from "rxjs"; import {map, mergeMap} from "rxjs/operators"; import {NotifierService} from "angular-notifier"; +import {MatCheckboxChange} from "@angular/material/checkbox"; @Component({ selector: 'app-add-server', templateUrl: './add-server.component.html', @@ -34,8 +35,16 @@ export class AddServerComponent implements OnInit { "DNS": "dns" } - subnets = []; - selectedSubnet = 24; + v4Subnets = []; + v6Subnets = []; + defaultListenPort = "51820" + defaultInterface = "wg0" + defaultIPv4Subnet = 24; + defaultIPv6Subnet = 64; + defaultIPv4Address = "10.0.200.1" + defaultDNS = this.defaultIPv4Address + ",8.8.8.8" + defaultIPv6Address = "fd42:42:42::1" + serverForm: FormGroup = null; isEdit = false; @@ -43,12 +52,14 @@ export class AddServerComponent implements OnInit { initForm(){ this.serverForm = new FormGroup({ - address: new FormControl('', [Validators.required, IPValidator.isIPAddress]), - subnet: new FormControl('', [Validators.required, Validators.min(1), Validators.max(32)]), - interface: new FormControl('', [Validators.required, Validators.minLength(3)]), - listen_port: new FormControl('', [Validators.required, NumberValidator.stringIsNumber]), + address: new FormControl(this.defaultIPv4Address, [Validators.required, IPValidator.isIPAddress]), + v6_address: new FormControl(this.defaultIPv6Address, [Validators.required, IPValidator.isIPAddress]), + subnet: new FormControl(this.defaultIPv4Subnet, [Validators.required, Validators.min(1), Validators.max(32)]), + v6_subnet: new FormControl(this.defaultIPv6Subnet, [Validators.required, Validators.min(1), Validators.max(64)]), + interface: new FormControl(this.defaultInterface, [Validators.required, Validators.minLength(3)]), + listen_port: new FormControl(this.defaultListenPort, [Validators.required, NumberValidator.stringIsNumber]), endpoint: new FormControl('', Validators.required), - dns: new FormControl(''), + dns: new FormControl(this.defaultDNS), private_key: new FormControl('' ), public_key: new FormControl('' ), post_up: new FormControl(''), @@ -61,12 +72,25 @@ export class AddServerComponent implements OnInit { }); } + ipv6SupportChanged($event: MatCheckboxChange){ + let v6AddressControl = this.serverForm.get("v6_address"); + let v6SubnetControl = this.serverForm.get("v6_subnet"); + if($event.checked){ + v6AddressControl.enable() + v6SubnetControl.enable() + }else { + v6AddressControl.disable() + v6SubnetControl.disable() + } + } + constructor(private serverAPI: ServerService, private comm: DataService, private notify: NotifierService) { } ngOnInit(): void { - this.subnets = Array(32).fill(1).map((x,i)=>i+1); + this.v4Subnets = Array(32).fill(1).map((x,i)=>i+1); + this.v6Subnets = Array(64).fill(1).map((x,i)=>i+1); this.initForm(); this.comm.on('server-edit').subscribe((data: Server) => { @@ -208,10 +232,9 @@ export class AddServerComponent implements OnInit { }); } - this.isEdit = false; - this.editServer = null; - this.serverForm.reset(); - this.serverForm.clearValidators(); + + this.resetForm(); + } getKeyPair() { @@ -228,7 +251,7 @@ export class AddServerComponent implements OnInit { resetForm() { this.isEdit = false; this.editServer = null; - + this.serverForm.clearValidators(); this.initForm() } } diff --git a/wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts b/wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts index 2257217..e881692 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts +++ b/wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts @@ -19,6 +19,7 @@ import { PeerComponent } from './peer/peer.component'; import { QRCodeModule } from 'angularx-qrcode'; import {MatTooltipModule} from "@angular/material/tooltip"; import {MatSelectModule} from "@angular/material/select"; +import {MatCheckboxModule} from "@angular/material/checkbox"; @NgModule({ declarations: [ @@ -45,6 +46,7 @@ import {MatSelectModule} from "@angular/material/select"; QRCodeModule, MatTooltipModule, MatSelectModule, + MatCheckboxModule, ], }) From 2c80a718f36403b685154182fda435193ba77dbf Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 20 Jul 2020 01:55:14 +0200 Subject: [PATCH 16/29] * API Docs. --- README.md | 2 + wg_dashboard_frontend/package-lock.json | 872 +++++++++++++++++++++--- wg_dashboard_frontend/package.json | 2 + wg_dashboard_frontend/widdershins.json | 3 + 4 files changed, 795 insertions(+), 84 deletions(-) create mode 100644 wg_dashboard_frontend/widdershins.json diff --git a/README.md b/README.md index 795103c..bdc906c 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,8 @@ build: # Usage When docker container/server has started, go to http://localhost:8888 +# API Docs +The API docs is found [here](./docs/api.md). # Environment variables | Environment | Description | Recommended | diff --git a/wg_dashboard_frontend/package-lock.json b/wg_dashboard_frontend/package-lock.json index 9348e38..26f5fa8 100644 --- a/wg_dashboard_frontend/package-lock.json +++ b/wg_dashboard_frontend/package-lock.json @@ -1398,7 +1398,6 @@ "version": "7.9.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz", "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==", - "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -2095,7 +2094,6 @@ "version": "6.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2234,7 +2232,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "~1.0.2" }, @@ -2242,8 +2239,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" } } }, @@ -2421,8 +2417,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -2659,6 +2654,66 @@ "tweetnacl": "^0.14.3" } }, + "better-ajv-errors": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-0.6.7.tgz", + "integrity": "sha512-PYgt/sCzR4aGpyNy5+ViSQ77ognMnWq7745zM+/flYO4/Yisdtp9wDQW2IKCyVYPUxQt3E/b5GBSwfhd1LPdlg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/runtime": "^7.0.0", + "chalk": "^2.4.1", + "core-js": "^3.2.1", + "json-to-ast": "^2.0.3", + "jsonpointer": "^4.0.1", + "leven": "^3.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -3000,6 +3055,11 @@ "unset-value": "^1.0.0" } }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, "caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", @@ -3280,6 +3340,11 @@ "is-regexp": "^2.0.0" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, "coa": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", @@ -3343,11 +3408,15 @@ } } }, + "code-error-fragment": { + "version": "0.0.230", + "resolved": "https://registry.npmjs.org/code-error-fragment/-/code-error-fragment-0.0.230.tgz", + "integrity": "sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==" + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "codelyzer": { "version": "5.2.2", @@ -3444,7 +3513,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -3452,8 +3520,7 @@ "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "commondir": { "version": "1.0.1", @@ -3829,7 +3896,6 @@ "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, "requires": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -4300,8 +4366,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", @@ -4482,6 +4547,11 @@ "domelementtype": "1" } }, + "dot": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dot/-/dot-1.1.3.tgz", + "integrity": "sha512-/nt74Rm+PcfnirXGEdhZleTwGC2LMnuKTeeTIlI82xb5loBBoXNYzr2ezCroPSMtilK8EZIfcNZwOcHN+ib1Lg==" + }, "dot-prop": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", @@ -4491,6 +4561,11 @@ "is-obj": "^2.0.0" } }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -4571,7 +4646,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -4590,8 +4664,7 @@ "entities": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", - "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", - "dev": true + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" }, "err-code": { "version": "1.1.2", @@ -4647,6 +4720,11 @@ "is-symbol": "^1.0.2" } }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -4701,6 +4779,20 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, "eventemitter3": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", @@ -4736,7 +4828,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, "requires": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -4985,8 +5076,7 @@ "fast-deep-equal": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, "fast-glob": { "version": "3.2.2", @@ -5026,8 +5116,12 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" }, "fastparse": { "version": "1.1.2", @@ -5257,6 +5351,11 @@ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -5295,6 +5394,11 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" + }, "from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -5325,6 +5429,14 @@ "minipass": "^3.0.0" } }, + "fs-readfile-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-readfile-promise/-/fs-readfile-promise-2.0.1.tgz", + "integrity": "sha1-gAI4I5gfn//+AWCei+Zo9prknnA=", + "requires": { + "graceful-fs": "^4.1.2" + } + }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -5337,6 +5449,30 @@ "readable-stream": "1 || 2" } }, + "fs-writefile-promise": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-writefile-promise/-/fs-writefile-promise-1.0.3.tgz", + "integrity": "sha1-4C+bWP/CVe2CKtx6ARFPRF1I0GM=", + "requires": { + "mkdirp-promise": "^1.0.0", + "pinkie-promise": "^1.0.0" + }, + "dependencies": { + "pinkie": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-1.0.0.tgz", + "integrity": "sha1-Wkfyi6EBXQIBvae/DzWOR77Ix+Q=" + }, + "pinkie-promise": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-1.0.0.tgz", + "integrity": "sha1-0dpn9UglY7t89X8oauKCLs+/NnA=", + "requires": { + "pinkie": "^1.0.0" + } + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5464,6 +5600,11 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" + }, "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", @@ -5474,7 +5615,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, "requires": { "pump": "^3.0.0" } @@ -5604,8 +5744,12 @@ "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, "hammerjs": { "version": "2.0.8", @@ -5621,14 +5765,12 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -5653,7 +5795,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" }, @@ -5661,8 +5802,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" } } }, @@ -5770,6 +5910,11 @@ "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", "dev": true }, + "highlightjs": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/highlightjs/-/highlightjs-9.16.2.tgz", + "integrity": "sha512-FK1vmMj8BbEipEy8DLIvp71t5UsC7n2D6En/UfM/91PCwmOpj6f2iu0Y0coRC62KSRHHC+dquM2xMULV/X7NFg==" + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -5949,6 +6094,11 @@ "sshpk": "^1.7.0" } }, + "http2-client": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.3.tgz", + "integrity": "sha512-nUxLymWQ9pzkzTmir24p2RtsgruLmhje7lH3hLX1IpwvyTg77fW+1brenPPP3USAR+rQ36p5sTA/x7sjCJVkAA==" + }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -5965,6 +6115,83 @@ "debug": "4" } }, + "httpsnippet": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/httpsnippet/-/httpsnippet-1.20.0.tgz", + "integrity": "sha512-8cMk8qGnlXkWh6kN6RiTIooybNiqG71rZ6RIMT/NSxgoW74JZ2okE6QBuWeZjMGU78y/Nk6HIBEhF/ZjUUeu+w==", + "requires": { + "chalk": "^1.1.1", + "commander": "^2.9.0", + "debug": "^2.2.0", + "event-stream": "3.3.4", + "form-data": "3.0.0", + "fs-readfile-promise": "^2.0.1", + "fs-writefile-promise": "^1.0.3", + "har-validator": "^5.0.0", + "pinkie-promise": "^2.0.0", + "stringify-object": "^3.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -6170,8 +6397,7 @@ "invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" }, "ip": { "version": "1.1.5", @@ -6506,8 +6732,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-svg": { "version": "3.0.0", @@ -6571,8 +6796,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", @@ -6625,6 +6849,11 @@ "supports-color": "^7.0.0" } }, + "jgexml": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/jgexml/-/jgexml-0.4.3.tgz", + "integrity": "sha512-N4doRtHFobW+prTgh32GRIRIflJIQrzf/ll4ePsg1wnonyhiBb29zeyVQ5MkwZDYPUbLuEAyVSx5XikPECXU9g==" + }, "js-base64": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz", @@ -6673,6 +6902,14 @@ "integrity": "sha512-2tLgY7LRNZ9Hd6gmCuBG5/OjRHQpSgJQqJoYyLLOhUgn8LdOYrjaZLcxkWnDads+AD/haWWioPNziXQcgvQJ/g==", "dev": true }, + "json-pointer": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz", + "integrity": "sha1-jlAFUKaqxUZKRzN32leqbMIoKNc=", + "requires": { + "foreach": "^2.0.4" + } + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -6682,8 +6919,7 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stringify-safe": { "version": "5.0.1", @@ -6691,6 +6927,15 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "json-to-ast": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json-to-ast/-/json-to-ast-2.1.0.tgz", + "integrity": "sha512-W9Lq347r8tA1DfMvAGn9QNcgYm4Wm7Yc+k8e6vezpMnRT+NHbtlxgNBXRVjXe9YM6eTn6+p/MKOlV/aABJcSnQ==", + "requires": { + "code-error-fragment": "0.0.230", + "grapheme-splitter": "^1.0.4" + } + }, "json3": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", @@ -6720,6 +6965,11 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "jsonpointer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz", + "integrity": "sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -6774,7 +7024,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, "requires": { "invert-kv": "^2.0.0" } @@ -6854,8 +7103,7 @@ "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" }, "levenary": { "version": "1.1.1", @@ -6890,6 +7138,14 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", "dev": true }, + "linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -7135,7 +7391,6 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, "requires": { "p-defer": "^1.0.0" } @@ -7152,6 +7407,11 @@ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", "dev": true }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=" + }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -7167,6 +7427,23 @@ "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", "dev": true }, + "markdown-it": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", + "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", + "requires": { + "argparse": "^1.0.7", + "entities": "~2.0.0", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=" + }, "markdown-table": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", @@ -7210,6 +7487,11 @@ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "dev": true }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -7220,7 +7502,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, "requires": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^2.0.0", @@ -7420,14 +7701,12 @@ "mime-db": { "version": "1.43.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", - "dev": true + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" }, "mime-types": { "version": "2.1.26", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", - "dev": true, "requires": { "mime-db": "1.43.0" } @@ -7435,8 +7714,7 @@ "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, "min-indent": { "version": "1.0.0", @@ -7662,6 +7940,11 @@ "minimist": "^1.2.5" } }, + "mkdirp-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-1.1.0.tgz", + "integrity": "sha1-LISJPtZ24NmPsY+5piEv0bK5qBk=" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -7773,8 +8056,20 @@ "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, + "node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "requires": { + "http2-client": "^1.2.5" + } }, "node-forge": { "version": "0.9.0", @@ -7885,6 +8180,14 @@ } } }, + "node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha1-271K8SE04uY1wkXvk//Pb2BnOl0=", + "requires": { + "es6-promise": "^3.2.1" + } + }, "node-releases": { "version": "1.1.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz", @@ -11261,7 +11564,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, "requires": { "path-key": "^2.0.0" } @@ -11296,8 +11598,101 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "requires": { + "fast-safe-stringify": "^2.0.7" + } + }, + "oas-linter": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.1.3.tgz", + "integrity": "sha512-jFWBHjSoqODGo7cKA/VWqqWSLbHNtnyCEpa2nMMS64SzCUbZDk63Oe7LqQZ2qJA0K2VRreYLt6cVkYy6MqNRDg==", + "requires": { + "should": "^13.2.1", + "yaml": "^1.8.3" + } + }, + "oas-resolver": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.4.1.tgz", + "integrity": "sha512-rRmUv9mDTKPtsB2OGaoNMK4BC1Q/pL+tWRPKRjXJEBoLmfegJhecOZPBtIR0gKEVQb9iAA0MqulkgY43EiCFDg==", + "requires": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.3", + "yaml": "^1.8.3", + "yargs": "^15.3.1" + }, + "dependencies": { + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + } + } + }, + "oas-schema-walker": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.4.tgz", + "integrity": "sha512-foVDDS0RJYMfhQEDh/WdBuCzydTcsCnGo9EeD8SpWq1uW10JXiz+8SfYVDA7LO87kjmlnTRZle/2gr5qxabaEA==" + }, + "oas-validator": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-4.0.6.tgz", + "integrity": "sha512-WNKjQCw6UnbHRQM00stje0sn//OSzrhGWBD5s4p9dv3t8r3+CZcJgc/BcIPSM2IKaR2iOZWJjyfGGYzi6Hd5lQ==", + "requires": { + "ajv": "^5.5.2", + "better-ajv-errors": "^0.6.7", + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.1.3", + "oas-resolver": "^2.4.1", + "oas-schema-walker": "^1.1.4", + "reftools": "^1.1.3", + "should": "^13.2.1", + "yaml": "^1.8.3" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + } + } }, "oauth-sign": { "version": "0.9.0", @@ -11460,6 +11855,14 @@ "is-wsl": "^2.1.1" } }, + "openapi-sampler": { + "version": "1.0.0-beta.16", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0-beta.16.tgz", + "integrity": "sha512-05+GvwMagTY7GxoDQoWJfmAUFlxfebciiEzqKmu4iq6+MqBEn62AMUkn0CTxyKhnUGIaR2KXjTeslxIeJwVIOw==", + "requires": { + "json-pointer": "^0.6.0" + } + }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -11518,7 +11921,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, "requires": { "execa": "^1.0.0", "lcid": "^2.0.0", @@ -11550,20 +11952,17 @@ "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" }, "p-limit": { "version": "2.3.0", @@ -11801,8 +12200,7 @@ "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, "path-parse": { "version": "1.0.6", @@ -11832,6 +12230,14 @@ } } }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "requires": { + "through": "~2.3" + } + }, "pbkdf2": { "version": "3.0.17", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", @@ -11866,14 +12272,12 @@ "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, "requires": { "pinkie": "^2.0.0" } @@ -12959,7 +13363,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -12991,8 +13394,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "q": { "version": "1.5.1", @@ -13441,6 +13843,11 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", "dev": true }, + "reftools": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.3.tgz", + "integrity": "sha512-JTlhKmSzqE/gt5Z5RX25yZDq67MlRRtTz1gLy/NY+wPDx1e1vEJsv1PoNrpKZBwitcEMXs2k7pzmbmraP1ZMAQ==" + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -13459,8 +13866,7 @@ "regenerator-runtime": { "version": "0.13.5", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", - "dev": true + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" }, "regenerator-transform": { "version": "0.14.4", @@ -14311,7 +14717,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, "requires": { "shebang-regex": "^1.0.0" } @@ -14319,14 +14724,60 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "requires": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "requires": { + "should-type": "^1.4.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=" + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==" }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "simple-swizzle": { "version": "0.2.2", @@ -14838,6 +15289,14 @@ } } }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "requires": { + "through": "2" + } + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -14936,6 +15395,14 @@ "readable-stream": "^2.0.2" } }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "requires": { + "duplexer": "~0.1.1" + } + }, "stream-each": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", @@ -15043,6 +15510,28 @@ "is-hexadecimal": "^1.0.0" } }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "dependencies": { + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + } + } + }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", @@ -15063,8 +15552,7 @@ "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "strip-indent": { "version": "1.0.1", @@ -15692,6 +16180,44 @@ } } }, + "swagger2openapi": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-6.2.1.tgz", + "integrity": "sha512-CY3miXK2YZ0rjvGkVBzJps8ohDR7zGmbFhCVynAGlxIKbLvBuD99aS2ikZcvSo09uVOKcb5FspmvF/PKWdYV1Q==", + "requires": { + "better-ajv-errors": "^0.6.1", + "call-me-maybe": "^1.0.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.4.1", + "oas-schema-walker": "^1.1.4", + "oas-validator": "^4.0.6", + "reftools": "^1.1.3", + "yaml": "^1.8.3", + "yargs": "^15.3.1" + }, + "dependencies": { + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + } + } + }, "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", @@ -15884,8 +16410,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "2.0.5", @@ -16273,6 +16798,11 @@ "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", "dev": true }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "unherit": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", @@ -16516,11 +17046,15 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" } }, + "urijs": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.2.tgz", + "integrity": "sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==" + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -18663,7 +19197,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, "requires": { "isexe": "^2.0.0" } @@ -18673,6 +19206,178 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "widdershins": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widdershins/-/widdershins-4.0.1.tgz", + "integrity": "sha512-y7TGynno+J/EqRPtUrpEuEjJUc1N2ajfP7R4sHU7Qg8I/VFHGavBxL7ZTeOAVmd1fhmY2wJIbpX2LMDWf37vVA==", + "requires": { + "dot": "^1.1.3", + "fast-safe-stringify": "^2.0.7", + "highlightjs": "^9.12.0", + "httpsnippet": "^1.19.0", + "jgexml": "^0.4.3", + "markdown-it": "^10.0.0", + "markdown-it-emoji": "^1.4.0", + "node-fetch": "^2.0.0", + "oas-resolver": "^2.3.1", + "oas-schema-walker": "^1.1.3", + "openapi-sampler": "^1.0.0-beta.15", + "reftools": "^1.1.0", + "swagger2openapi": "^6.0.1", + "urijs": "^1.19.0", + "yaml": "^1.8.3", + "yargs": "^12.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -18827,7 +19532,6 @@ "version": "1.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.8.3.tgz", "integrity": "sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw==", - "dev": true, "requires": { "@babel/runtime": "^7.8.7" } diff --git a/wg_dashboard_frontend/package.json b/wg_dashboard_frontend/package.json index 543befb..01fd985 100644 --- a/wg_dashboard_frontend/package.json +++ b/wg_dashboard_frontend/package.json @@ -8,6 +8,7 @@ "license": "MIT", "scripts": { "ng": "ng", + "md-doc": "npm install widdershins && wget http://127.0.0.1:8000/openapi.json -O /tmp/wg-openapi.json && widdershins /tmp/wg-openapi.json --environment widdershins.json --resolve=true --omitHeader=true --language_tabs 'python:Python' -o ../docs/api.md", "start": "ng serve --host 0.0.0.0 --disable-host-check", "build": "ng build", "buildwatch": "ng build --watch --aot --outputPath=../wg_dashboard_backend/build/ --host 0.0.0.0 --disable-host-check", @@ -59,6 +60,7 @@ "rxjs": "6.5.5", "tslib": "^1.10.0", "web-animations-js": "^2.3.2", + "widdershins": "^4.0.1", "zone.js": "^0.10.3" }, "devDependencies": { diff --git a/wg_dashboard_frontend/widdershins.json b/wg_dashboard_frontend/widdershins.json new file mode 100644 index 0000000..774ce7e --- /dev/null +++ b/wg_dashboard_frontend/widdershins.json @@ -0,0 +1,3 @@ +{ + "language_tabs": [{ "python": "Python" }] +} From 8a2896594e309ff1c2cf253ddc6ba8cac4bea984 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 20 Jul 2020 01:55:50 +0200 Subject: [PATCH 17/29] * Include the api doc file. --- docs/api.md | 1779 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1779 insertions(+) create mode 100644 docs/api.md diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..9d30304 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,1779 @@ + + +

FastAPI v0.1.0

+ +> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. + +# Authentication + +- oAuth2 authentication. + + - Flow: password + + - Token URL = [/api/v1/login](/api/v1/login) + +|Scope|Scope Description| +|---|---| + +

Default

+ +## root__get + + + +> Code samples + +```python +import requests +headers = { + 'Accept': 'application/json' +} + +r = requests.get('/', headers = headers) + +print(r.json()) + +``` + +`GET /` + +*Root* + +> Example responses + +> 200 Response + +```json +null +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|Inline| + +

Response Schema

+ + + +

user

+ +## logout_api_v1_logout_get + + + +> Code samples + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/api/v1/logout', headers = headers) + +print(r.json()) + +``` + +`GET /api/v1/logout` + +*Logout* + +> Example responses + +> 200 Response + +```json +null +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|Inline| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| + +

Response Schema

+ + + +## edit_api_v1_user_edit_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/user/edit', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/user/edit` + +*Edit* + +> Body parameter + +```json +{ + "id": 0, + "username": "string", + "email": "string", + "full_name": "string", + "role": "string", + "password": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[UserInDB](#schemauserindb)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "username": "string", + "email": "string", + "full_name": "string", + "role": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[User](#schemauser)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +## login_api_v1_login_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' +} + +r = requests.post('/api/v1/login', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/login` + +*Login* + +> Body parameter + +```yaml +username: string +password: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[Body_login_api_v1_login_post](#schemabody_login_api_v1_login_post)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "access_token": "string", + "token_type": "string", + "user": { + "id": 0, + "username": "string", + "email": "string", + "full_name": "string", + "role": "string" + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[Token](#schematoken)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +## create_user_api_v1_users_create__post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/users/create/', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/users/create/` + +*Create User* + +> Body parameter + +```json +{ + "id": 0, + "username": "string", + "email": "string", + "full_name": "string", + "role": "string", + "password": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[UserInDB](#schemauserindb)|true|none| + +> Example responses + +> 200 Response + +```json +null +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|Inline| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + +

Response Schema

+ + + +

server

+ +## servers_all_api_v1_server_all_get + + + +> Code samples + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/api/v1/server/all', headers = headers) + +print(r.json()) + +``` + +`GET /api/v1/server/all` + +*Servers All* + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|Inline| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| + +

Response Schema

+ +Status Code **200** + +*Response Servers All Api V1 Server All Get* + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|Response Servers All Api V1 Server All Get|[[WGServer](#schemawgserver)]|false|none|none| +|» WGServer|[WGServer](#schemawgserver)|false|none|none| +|»» id|integer|false|none|none| +|»» address|string|false|none|none| +|»» v6_address|string|false|none|none| +|»» subnet|integer|false|none|none| +|»» v6_subnet|integer|false|none|none| +|»» interface|string|true|none|none| +|»» listen_port|integer|false|none|none| +|»» endpoint|string|false|none|none| +|»» private_key|string|false|none|none| +|»» public_key|string|false|none|none| +|»» is_running|boolean|false|none|none| +|»» configuration|string|false|none|none| +|»» post_up|string|false|none|none| +|»» post_down|string|false|none|none| +|»» dns|string|false|none|none| +|»» peers|[[WGPeer](#schemawgpeer)]|false|none|none| +|»»» WGPeer|[WGPeer](#schemawgpeer)|false|none|none| +|»»»» id|integer|false|none|none| +|»»»» name|string|false|none|none| +|»»»» address|string|false|none|none| +|»»»» v6_address|string|false|none|none| +|»»»» private_key|string|false|none|none| +|»»»» public_key|string|false|none|none| +|»»»» shared_key|string|false|none|none| +|»»»» server_id|string|true|none|none| +|»»»» dns|string|false|none|none| +|»»»» allowed_ips|string|false|none|none| +|»»»» configuration|string|false|none|none| + + + +## add_interface_api_v1_server_add_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/server/add', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/server/add` + +*Add Interface* + +> Body parameter + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[WGServerAdd](#schemawgserveradd)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[WGServer](#schemawgserver)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +## stop_server_api_v1_server_stop_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/server/stop', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/server/stop` + +*Stop Server* + +> Body parameter + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[WGServer](#schemawgserver)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[WGServer](#schemawgserver)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +## start_server_api_v1_server_start_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/server/start', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/server/start` + +*Start Server* + +> Body parameter + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[WGServer](#schemawgserver)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[WGServer](#schemawgserver)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +## restart_server_api_v1_server_restart_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/server/restart', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/server/restart` + +*Restart Server* + +> Body parameter + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[WGServer](#schemawgserver)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[WGServer](#schemawgserver)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +## delete_server_api_v1_server_delete_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/server/delete', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/server/delete` + +*Delete Server* + +> Body parameter + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[WGServer](#schemawgserver)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[WGServer](#schemawgserver)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +## stats_server_api_v1_server_stats_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/server/stats', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/server/stats` + +*Stats Server* + +> Body parameter + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[WGServer](#schemawgserver)|true|none| + +> Example responses + +> 200 Response + +```json +null +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|Inline| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + +

Response Schema

+ + + +## edit_server_api_v1_server_edit_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/server/edit', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/server/edit` + +*Edit Server* + +> Body parameter + +```json +{} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[WGServer](#schemawgserver)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +## server_config_api_v1_server_config__server_id__get + + + +> Code samples + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/api/v1/server/config/{server_id}', headers = headers) + +print(r.json()) + +``` + +`GET /api/v1/server/config/{server_id}` + +*Server Config* + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|server_id|path|integer|true|none| + +> Example responses + +> 200 Response + +```json +"string" +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|string| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +

peer

+ +## add_peer_api_v1_peer_add_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/peer/add', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/peer/add` + +*Add Peer* + +> Body parameter + +```json +{ + "server_interface": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[WGPeerAdd](#schemawgpeeradd)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "address": "string", + "v6_address": "string", + "private_key": "string", + "public_key": "string", + "shared_key": "string", + "server_id": "string", + "dns": "string", + "allowed_ips": "string", + "configuration": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[WGPeer](#schemawgpeer)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +## delete_peer_api_v1_peer_delete_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/peer/delete', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/peer/delete` + +*Delete Peer* + +> Body parameter + +```json +{ + "id": 0, + "name": "string", + "address": "string", + "v6_address": "string", + "private_key": "string", + "public_key": "string", + "shared_key": "string", + "server_id": "string", + "dns": "string", + "allowed_ips": "string", + "configuration": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[WGPeer](#schemawgpeer)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "address": "string", + "v6_address": "string", + "private_key": "string", + "public_key": "string", + "shared_key": "string", + "server_id": "string", + "dns": "string", + "allowed_ips": "string", + "configuration": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[WGPeer](#schemawgpeer)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + + + +## edit_peer_api_v1_peer_edit_post + + + +> Code samples + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/api/v1/peer/edit', headers = headers) + +print(r.json()) + +``` + +`POST /api/v1/peer/edit` + +*Edit Peer* + +> Body parameter + +```json +{ + "id": 0, + "name": "string", + "address": "string", + "v6_address": "string", + "private_key": "string", + "public_key": "string", + "shared_key": "string", + "server_id": "string", + "dns": "string", + "allowed_ips": "string", + "configuration": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[WGPeer](#schemawgpeer)|true|none| + +> Example responses + +> 200 Response + +```json +null +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|Inline| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| +|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Validation Error|[HTTPValidationError](#schemahttpvalidationerror)| + +

Response Schema

+ + + +

wg

+ +## generate_psk_api_v1_wg_generate_psk_get + + + +> Code samples + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/api/v1/wg/generate_psk', headers = headers) + +print(r.json()) + +``` + +`GET /api/v1/wg/generate_psk` + +*Generate Psk* + +> Example responses + +> 200 Response + +```json +{ + "psk": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[PSK](#schemapsk)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| + + + +## generate_key_pair_api_v1_wg_generate_keypair_get + + + +> Code samples + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/api/v1/wg/generate_keypair', headers = headers) + +print(r.json()) + +``` + +`GET /api/v1/wg/generate_keypair` + +*Generate Key Pair* + +> Example responses + +> 200 Response + +```json +{ + "public_key": "string", + "private_key": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful Response|[KeyPair](#schemakeypair)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| + + + +# Schemas + +

Body_login_api_v1_login_post

+ + + + + + +```json +{ + "username": "string", + "password": "string" +} + +``` + +Body_login_api_v1_login_post + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|username|string|true|none|none| +|password|string|true|none|none| + +

HTTPValidationError

+ + + + + + +```json +{ + "detail": [ + { + "loc": [ + "string" + ], + "msg": "string", + "type": "string" + } + ] +} + +``` + +HTTPValidationError + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|detail|[[ValidationError](#schemavalidationerror)]|false|none|none| + +

KeyPair

+ + + + + + +```json +{ + "public_key": "string", + "private_key": "string" +} + +``` + +KeyPair + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|public_key|string|true|none|none| +|private_key|string|true|none|none| + +

PSK

+ + + + + + +```json +{ + "psk": "string" +} + +``` + +PSK + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|psk|string|true|none|none| + +

Token

+ + + + + + +```json +{ + "access_token": "string", + "token_type": "string", + "user": { + "id": 0, + "username": "string", + "email": "string", + "full_name": "string", + "role": "string" + } +} + +``` + +Token + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|access_token|string|true|none|none| +|token_type|string|true|none|none| +|user|[User](#schemauser)|true|none|none| + +

User

+ + + + + + +```json +{ + "id": 0, + "username": "string", + "email": "string", + "full_name": "string", + "role": "string" +} + +``` + +User + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer|false|none|none| +|username|string|true|none|none| +|email|string|false|none|none| +|full_name|string|false|none|none| +|role|string|false|none|none| + +

UserInDB

+ + + + + + +```json +{ + "id": 0, + "username": "string", + "email": "string", + "full_name": "string", + "role": "string", + "password": "string" +} + +``` + +UserInDB + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer|false|none|none| +|username|string|true|none|none| +|email|string|false|none|none| +|full_name|string|false|none|none| +|role|string|false|none|none| +|password|string|true|none|none| + +

ValidationError

+ + + + + + +```json +{ + "loc": [ + "string" + ], + "msg": "string", + "type": "string" +} + +``` + +ValidationError + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|loc|[string]|true|none|none| +|msg|string|true|none|none| +|type|string|true|none|none| + +

WGPeer

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "address": "string", + "v6_address": "string", + "private_key": "string", + "public_key": "string", + "shared_key": "string", + "server_id": "string", + "dns": "string", + "allowed_ips": "string", + "configuration": "string" +} + +``` + +WGPeer + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer|false|none|none| +|name|string|false|none|none| +|address|string|false|none|none| +|v6_address|string|false|none|none| +|private_key|string|false|none|none| +|public_key|string|false|none|none| +|shared_key|string|false|none|none| +|server_id|string|true|none|none| +|dns|string|false|none|none| +|allowed_ips|string|false|none|none| +|configuration|string|false|none|none| + +

WGPeerAdd

+ + + + + + +```json +{ + "server_interface": "string" +} + +``` + +WGPeerAdd + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|server_interface|string|true|none|none| + +

WGServer

+ + + + + + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} + +``` + +WGServer + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer|false|none|none| +|address|string|false|none|none| +|v6_address|string|false|none|none| +|subnet|integer|false|none|none| +|v6_subnet|integer|false|none|none| +|interface|string|true|none|none| +|listen_port|integer|false|none|none| +|endpoint|string|false|none|none| +|private_key|string|false|none|none| +|public_key|string|false|none|none| +|is_running|boolean|false|none|none| +|configuration|string|false|none|none| +|post_up|string|false|none|none| +|post_down|string|false|none|none| +|dns|string|false|none|none| +|peers|[[WGPeer](#schemawgpeer)]|false|none|none| + +

WGServerAdd

+ + + + + + +```json +{ + "id": 0, + "address": "string", + "v6_address": "string", + "subnet": 0, + "v6_subnet": 0, + "interface": "string", + "listen_port": 0, + "endpoint": "string", + "private_key": "string", + "public_key": "string", + "is_running": true, + "configuration": "string", + "post_up": "string", + "post_down": "string", + "dns": "string", + "peers": [] +} + +``` + +WGServerAdd + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer|false|none|none| +|address|string|true|none|none| +|v6_address|string|false|none|none| +|subnet|integer|false|none|none| +|v6_subnet|integer|false|none|none| +|interface|string|true|none|none| +|listen_port|integer|true|none|none| +|endpoint|string|false|none|none| +|private_key|string|false|none|none| +|public_key|string|false|none|none| +|is_running|boolean|false|none|none| +|configuration|string|false|none|none| +|post_up|string|false|none|none| +|post_down|string|false|none|none| +|dns|string|false|none|none| +|peers|[[WGPeer](#schemawgpeer)]|false|none|none| + From b56a9db335bfb229b8800804ffc05c7095209cff Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 20 Jul 2020 02:24:43 +0200 Subject: [PATCH 18/29] * Added configuration dump capabilities which address #21 * Removed unused code. --- wg_dashboard_backend/routers/v1/user.py | 6 ----- wg_dashboard_backend/routers/v1/wg.py | 29 ++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/wg_dashboard_backend/routers/v1/user.py b/wg_dashboard_backend/routers/v1/user.py index 0ad8a46..10840f7 100644 --- a/wg_dashboard_backend/routers/v1/user.py +++ b/wg_dashboard_backend/routers/v1/user.py @@ -78,9 +78,3 @@ def create_user( )): raise HTTPException(status_code=400, detail="Could not create user") - return login_for_access_token(OAuth2PasswordRequestForm( - username=form_data.username, - password=form_data.password, - scope="" - ), sess) - diff --git a/wg_dashboard_backend/routers/v1/wg.py b/wg_dashboard_backend/routers/v1/wg.py index a7005e2..cad3aa1 100644 --- a/wg_dashboard_backend/routers/v1/wg.py +++ b/wg_dashboard_backend/routers/v1/wg.py @@ -1,8 +1,14 @@ -from fastapi import APIRouter +from datetime import datetime +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from zipfile import ZipFile +from io import BytesIO import middleware +from starlette.responses import StreamingResponse import schemas import script.wireguard +import db.wireguard router = APIRouter() @@ -23,3 +29,24 @@ def generate_key_pair(): private_key=private_key, public_key=public_key ) + + +@router.get("/dump") +def dump_database( + sess: Session = Depends(middleware.get_db) +): + in_memory = BytesIO() + zf = ZipFile(in_memory, mode="w") + for server in db.wireguard.server_get_all(sess): + zf.writestr(f"{server.interface}/{server.interface}.conf", server.configuration) + + for peer in server.peers: + zf.writestr(f"{server.interface}/peers/{peer.name}_{peer.address.replace('.','-')}.conf", server.configuration) + + zf.close() + in_memory.seek(0) + + now = datetime.now().strftime("%m-%d-%Y-%H:%M:%S") + return StreamingResponse(in_memory, media_type="application/zip", headers={ + "Content-Disposition": f'attachment; filename="wg-manager-dump-{now}.zip"' + }) From d70f35c65e93000b650610b0c4ba0b8a84e442b9 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 20 Jul 2020 04:06:05 +0200 Subject: [PATCH 19/29] * API-Key support. This ease automation, such as #21 --- README.md | 7 +++ wg_dashboard_backend/const.py | 1 + wg_dashboard_backend/middleware.py | 23 ++++++-- wg_dashboard_backend/models.py | 13 ++++- wg_dashboard_backend/routers/v1/user.py | 53 +++++++++++++++++-- wg_dashboard_backend/routers/v1/wg.py | 2 +- wg_dashboard_backend/schemas.py | 11 ++++ .../src/app/page/page.module.ts | 5 +- .../user/edit/api-key/api-key.component.html | 43 +++++++++++++++ .../user/edit/api-key/api-key.component.scss | 0 .../edit/api-key/api-key.component.spec.ts | 25 +++++++++ .../user/edit/api-key/api-key.component.ts | 45 ++++++++++++++++ .../app/page/user/edit/edit.component.html | 7 ++- .../app/page/user/edit/edit.component.scss | 3 ++ .../src/app/page/user/edit/edit.component.ts | 7 ++- .../src/app/services/server.service.ts | 18 +++++++ 16 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html create mode 100644 wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.scss create mode 100644 wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts create mode 100644 wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts diff --git a/README.md b/README.md index bdc906c..50d4421 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The features of wg-manager includes: * Create/Delete/Modify * Bandwidth usage statistics * Export by QRCode, Text +* Authentication via API-Keys for automation (Created in GUI) **General** * Modify Admin User @@ -88,6 +89,12 @@ When docker container/server has started, go to http://localhost:8888 # API Docs The API docs is found [here](./docs/api.md). +# API-Keys +1. Login to wg-manager +2. Go to edit profile +3. Create API-Key and take note of the key. Use the X-API-Key header to authenticate. +4. Example: `curl -i -H "X-API-Key: " http://:/api/v1/users/api-key/list` + # Environment variables | Environment | Description | Recommended | |------------------|---------------------------------------------------------------------------|-------------| diff --git a/wg_dashboard_backend/const.py b/wg_dashboard_backend/const.py index 67a1b02..1ea55ab 100644 --- a/wg_dashboard_backend/const.py +++ b/wg_dashboard_backend/const.py @@ -14,6 +14,7 @@ DEFAULT_POST_DOWN_v6 = os.getenv("POST_DOWN_V6", "ip6tables -D FORWARD -i %i -j SECRET_KEY = ''.join(random.choices(string.ascii_uppercase + string.digits, k=64)) ALGORITHM = "HS256" +API_KEY_LENGTH = 32 ACCESS_TOKEN_EXPIRE_MINUTES = 30 CMD_WG_COMMAND = ["wg"] CMD_WG_QUICK = ["wg-quick"] diff --git a/wg_dashboard_backend/middleware.py b/wg_dashboard_backend/middleware.py index 708decb..58e7462 100644 --- a/wg_dashboard_backend/middleware.py +++ b/wg_dashboard_backend/middleware.py @@ -11,6 +11,7 @@ from starlette.requests import Request from starlette.responses import Response import const +import models import schemas from database import SessionLocal import db.user @@ -56,7 +57,13 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None): return encoded_jwt -def auth(token: str = Depends(oauth2_scheme), sess: Session = Depends(get_db)): +def retrieve_api_key(request: Request): + return request.headers.get("X-API-Key", None) + + +def auth(token: str = Depends(oauth2_scheme), api_key: str = Depends(retrieve_api_key), sess: Session = Depends(get_db)): + + username = None credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -64,14 +71,22 @@ def auth(token: str = Depends(oauth2_scheme), sess: Session = Depends(get_db)): headers={"WWW-Authenticate": "Bearer"}, ) + # Attempt to authenticate using JWT try: payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM]) username: str = payload.get("sub") - if username is None: - raise credentials_exception - except PyJWTError: + pass + + try: + db_user_api_key = sess.query(models.UserAPIKey).filter_by(key=api_key).one() + username = db_user_api_key.user.username + except Exception: + pass + + if username is None: raise credentials_exception + user = schemas.User.from_orm( schemas.UserInDB(username=username, password="").from_db(sess) ) diff --git a/wg_dashboard_backend/models.py b/wg_dashboard_backend/models.py index c6eedb0..13fc980 100644 --- a/wg_dashboard_backend/models.py +++ b/wg_dashboard_backend/models.py @@ -1,6 +1,8 @@ +import datetime + import sqlalchemy -from sqlalchemy import Integer, Column +from sqlalchemy import Integer, Column, DateTime from sqlalchemy.orm import relationship, backref from database import Base @@ -16,6 +18,15 @@ class User(Base): role = Column(sqlalchemy.String) +class UserAPIKey(Base): + __tablename__ = "api_keys" + id = Column(Integer, primary_key=True, autoincrement=True) + key = Column(sqlalchemy.String, unique=True) + user_id = Column(Integer, sqlalchemy.ForeignKey('users.id', ondelete="CASCADE", onupdate="CASCADE")) + user = relationship("User", foreign_keys=[user_id]) + created_date = Column(DateTime, default=datetime.datetime.utcnow) + + class WGServer(Base): __tablename__ = "server" diff --git a/wg_dashboard_backend/routers/v1/user.py b/wg_dashboard_backend/routers/v1/user.py index 10840f7..1cec360 100644 --- a/wg_dashboard_backend/routers/v1/user.py +++ b/wg_dashboard_backend/routers/v1/user.py @@ -1,9 +1,12 @@ +import os from datetime import timedelta -from fastapi import APIRouter, HTTPException, Depends, Form +from fastapi import APIRouter, HTTPException, Depends, Form, Body +from fastapi.responses import PlainTextResponse, JSONResponse +import typing from sqlalchemy.orm import Session from starlette import status - +from binascii import hexlify import const import db.user import middleware @@ -28,6 +31,51 @@ def edit(form_data: schemas.UserInDB, return form_data +@router.get("/users/api-key/add", response_model=schemas.UserAPIKeyFull) +def add_api_key( + user: schemas.UserInDB = Depends(middleware.auth), + sess: Session = Depends(middleware.get_db) +): + key = hexlify(os.urandom(const.API_KEY_LENGTH)).decode() + + api_key = models.UserAPIKey( + user_id=user.id, + key=key, + ) + sess.add(api_key) + sess.commit() + + return schemas.UserAPIKeyFull.from_orm(api_key) + + +@router.post("/users/api-key/delete") +def delete_api_keys( + key_id: int = Body(None, embed=True), + user: schemas.UserInDB = Depends(middleware.auth), + sess: Session = Depends(middleware.get_db) +): + + count = sess.query(models.UserAPIKey)\ + .filter_by(id=key_id)\ + .delete() + sess.commit() + + return JSONResponse({ + "message": "Key deleted OK" if count == 1 else "There was an error while deleting the api-key" + }) + + +@router.get("/users/api-key/list", response_model=typing.List[schemas.UserAPIKey]) +def get_api_keys( + user: schemas.UserInDB = Depends(middleware.auth), + sess: Session = Depends(middleware.get_db) +): + keys = [schemas.UserAPIKey.from_orm(x) for x in sess.query(models.UserAPIKey) + .filter(models.UserAPIKey.user_id == user.id).all()] + + return keys + + @router.post("/login", response_model=schemas.Token) def login(*, username: str = Form(...), password: str = Form(...), sess: Session = Depends(middleware.get_db)): user: schemas.UserInDB = schemas.UserInDB(username=username, password="").from_db(sess) @@ -77,4 +125,3 @@ def create_user( role=form_data.role, )): raise HTTPException(status_code=400, detail="Could not create user") - diff --git a/wg_dashboard_backend/routers/v1/wg.py b/wg_dashboard_backend/routers/v1/wg.py index cad3aa1..476946d 100644 --- a/wg_dashboard_backend/routers/v1/wg.py +++ b/wg_dashboard_backend/routers/v1/wg.py @@ -46,7 +46,7 @@ def dump_database( zf.close() in_memory.seek(0) - now = datetime.now().strftime("%m-%d-%Y-%H:%M:%S") + now = datetime.now().strftime("%m-%d-%Y-%H:%M:%S") return StreamingResponse(in_memory, media_type="application/zip", headers={ "Content-Disposition": f'attachment; filename="wg-manager-dump-{now}.zip"' }) diff --git a/wg_dashboard_backend/schemas.py b/wg_dashboard_backend/schemas.py index f6cd8b7..7a16513 100644 --- a/wg_dashboard_backend/schemas.py +++ b/wg_dashboard_backend/schemas.py @@ -1,3 +1,5 @@ +from datetime import datetime + import pydantic from pydantic import BaseModel, typing from sqlalchemy.orm import Session, Query @@ -100,6 +102,15 @@ class User(GenericModel): excludes = {"id"} +class UserAPIKey(GenericModel): + id: int + created_date: datetime + + +class UserAPIKeyFull(UserAPIKey): + key: str + + class UserInDB(User): password: str diff --git a/wg_dashboard_frontend/src/app/page/page.module.ts b/wg_dashboard_frontend/src/app/page/page.module.ts index f48a155..37942ab 100644 --- a/wg_dashboard_frontend/src/app/page/page.module.ts +++ b/wg_dashboard_frontend/src/app/page/page.module.ts @@ -9,9 +9,11 @@ import { MatInputModule } from '@angular/material/input'; import { FlexModule } from '@angular/flex-layout'; import { EditComponent } from './user/edit/edit.component'; import { MatButtonModule } from '@angular/material/button'; +import {MatTableModule} from "@angular/material/table"; +import { ApiKeyComponent } from './user/edit/api-key/api-key.component'; @NgModule({ - declarations: [LoginComponent, EditComponent], + declarations: [LoginComponent, EditComponent, ApiKeyComponent], imports: [ CommonModule, PageRoutingModule, @@ -22,6 +24,7 @@ import { MatButtonModule } from '@angular/material/button'; MatInputModule, FlexModule, MatButtonModule, + MatTableModule, ], }) diff --git a/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html new file mode 100644 index 0000000..1186c7a --- /dev/null +++ b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html @@ -0,0 +1,43 @@ + + + API Keys + + + + You can use API-Keys to perform authenticated actions. These are less secure than using OAuth2, but at the gain for increased convenience. +
Note: A newly created API Key will only show once. This means that you have to take note of the key and safe it somewhere safe. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID. {{element.id}} API-Key {{(element.key) ? element.key : "[HIDDEN]"}} Creation Date {{element.created_date | date:'medium'}} Delete
+ + +
+
diff --git a/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.scss b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts new file mode 100644 index 0000000..2b44fe4 --- /dev/null +++ b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ApiKeyComponent } from './api-key.component'; + +describe('ApiKeyComponent', () => { + let component: ApiKeyComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ApiKeyComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ApiKeyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts new file mode 100644 index 0000000..fe9bbed --- /dev/null +++ b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts @@ -0,0 +1,45 @@ +import {Component, OnInit} from '@angular/core'; +import {ServerService} from "../../../../services/server.service"; + +@Component({ + selector: 'app-api-key', + templateUrl: './api-key.component.html', + styleUrls: ['./api-key.component.scss'] +}) +export class ApiKeyComponent implements OnInit { + + displayedColumns: string[] = ['id', 'key', 'created_at', 'delete']; + dataSource = []; + + constructor(private serverService: ServerService + ) { } + + ngOnInit(): void { + + + this.serverService.getAPIKeys().subscribe((apiKeys: Array) => { + this.dataSource = [...apiKeys] + + console.log(this.dataSource) + }) + } + + deleteAPIKey(elem){ + let idx = this.dataSource.indexOf(elem); + this.serverService.deleteAPIKey(elem.id).subscribe(x => { + this.dataSource.splice(idx, 1); + this.dataSource = [...this.dataSource] + }) + } + + createAPIKey(){ + + this.serverService.addAPIKey().subscribe(key => { + this.dataSource.push(key) + this.dataSource = [...this.dataSource] + + }) + + } + +} diff --git a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.html b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.html index dc50960..0dc3a3f 100644 --- a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.html +++ b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.html @@ -1,5 +1,5 @@ -
-
+
+
@@ -47,6 +47,9 @@
+
+ +
diff --git a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss index e69de29..fd81a9f 100644 --- a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss +++ b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss @@ -0,0 +1,3 @@ +.user-edit-component{ + padding: 20px; +} diff --git a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts index aef49ba..a7de58a 100644 --- a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts +++ b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { AuthService } from '@services/*'; import { Router } from '@angular/router'; +import {ServerService} from "../../../services/server.service"; @Component({ selector: 'app-edit', @@ -23,8 +24,10 @@ export class EditComponent implements OnInit { public user: any; public error: string; - constructor(private authService: AuthService, - private router: Router) { + constructor( + private authService: AuthService, + private router: Router + ) { } diff --git a/wg_dashboard_frontend/src/app/services/server.service.ts b/wg_dashboard_frontend/src/app/services/server.service.ts index a4ed45b..fafe45a 100644 --- a/wg_dashboard_frontend/src/app/services/server.service.ts +++ b/wg_dashboard_frontend/src/app/services/server.service.ts @@ -16,6 +16,9 @@ export class ServerService { public serverURL = this.base + "server"; public peerURL = this.base + "peer"; public wgURL = this.base + "wg"; + public userURL = this.base + "users"; + public apiKeyURL = this.userURL + "/api-key" + constructor(private config: ConfigService, private http: HttpClient, private notify: NotifierService) { @@ -91,4 +94,19 @@ export class ServerService { public serverStats(server: Server) { return this.http.post(this.serverURL + '/stats', server); } + + public addAPIKey() { + return this.http.get(this.apiKeyURL + '/add'); + } + + public getAPIKeys() { + return this.http.get(this.apiKeyURL + '/list'); + } + + public deleteAPIKey(api_key_id: { id: number }) { + return this.http.post(this.apiKeyURL + '/delete', { + key_id: api_key_id + }); + + } } From 5d4ca2e96a6b4fdf09a59cb39b8d85c710491dbb Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 20 Jul 2020 04:18:14 +0200 Subject: [PATCH 20/29] * Added route for retrieving configuration directly when adding new peer * Added example in documentation --- README.md | 6 +----- docker/push.sh | 0 wg_dashboard_backend/routers/v1/peer.py | 14 ++++++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) mode change 100755 => 100644 docker/push.sh diff --git a/README.md b/README.md index 50d4421..e544b9b 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ The API docs is found [here](./docs/api.md). 2. Go to edit profile 3. Create API-Key and take note of the key. Use the X-API-Key header to authenticate. 4. Example: `curl -i -H "X-API-Key: " http://:/api/v1/users/api-key/list` - +5. Example 2: `curl -X POST "http://:/api/v1/peer/add/configuration" -H "accept: application/json" -H "Content-Type: application/json" -H "X-API-Key: " -d "{\"server_interface\":\"wg0\"}"` # Environment variables | Environment | Description | Recommended | |------------------|---------------------------------------------------------------------------|-------------| @@ -135,7 +135,3 @@ The API docs is found [here](./docs/api.md). - Implement multi-server support (setting up site-2-site servers from the GUI) - Extending multi-server support to enable custom access lists (A peer can be assigned to multiple servers, as part of the ACL) -### Other -* Eventual bugfixes -* Improve Auth -* Improve everything... diff --git a/docker/push.sh b/docker/push.sh old mode 100755 new mode 100644 diff --git a/wg_dashboard_backend/routers/v1/peer.py b/wg_dashboard_backend/routers/v1/peer.py index 9d1b639..4fe642b 100644 --- a/wg_dashboard_backend/routers/v1/peer.py +++ b/wg_dashboard_backend/routers/v1/peer.py @@ -2,6 +2,7 @@ import ipaddress import itertools from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session +from starlette.responses import PlainTextResponse import const import models @@ -48,6 +49,10 @@ def add_peer( sess: Session = Depends(middleware.get_db) ): server = schemas.WGServer(interface=peer_add.server_interface).from_db(sess) + + if server is None: + raise HTTPException(500, detail="The server-interface '%s' does not exist!" % peer_add.server_interface) + peer = schemas.WGPeer(server_id=server.id) if server.v6_address: @@ -86,6 +91,15 @@ def add_peer( return schemas.WGPeer.from_orm(db_peer) +@router.post("/add/configuration") +def add_peer_get_config(peer_add: schemas.WGPeerAdd, + sess: Session = Depends(middleware.get_db) + ): + wg_peer: schemas.WGPeer = add_peer(peer_add, sess) + + return PlainTextResponse(wg_peer.configuration) + + @router.post("/delete", response_model=schemas.WGPeer) def delete_peer( peer: schemas.WGPeer, From 66e78de9e2e23b57da43d043fe9b9ee3b45ae4fd Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 20 Jul 2020 19:58:29 +0200 Subject: [PATCH 21/29] * Initial work on obfs4 tor bridge for #19 --- wg_dashboard_backend/const.py | 8 ++ wg_dashboard_backend/requirements.txt | 3 +- .../script/obfuscate/__init__.py | 56 +++++++++ .../script/obfuscate/obfs4.py | 30 +++++ wg_dashboard_backend/script/obfuscate/tor.py | 119 ++++++++++++++++++ 5 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 wg_dashboard_backend/script/obfuscate/__init__.py create mode 100644 wg_dashboard_backend/script/obfuscate/obfs4.py create mode 100644 wg_dashboard_backend/script/obfuscate/tor.py diff --git a/wg_dashboard_backend/const.py b/wg_dashboard_backend/const.py index 1ea55ab..a3f9196 100644 --- a/wg_dashboard_backend/const.py +++ b/wg_dashboard_backend/const.py @@ -5,6 +5,14 @@ IS_DOCKER = os.getenv("IS_DOCKER", "False") == "True" DATABASE_FILE = "/config/database.db" if IS_DOCKER else "./database.db" DATABASE_URL = f"sqlite:///{DATABASE_FILE}" +OBFUSCATE_ENABLED = os.getenv("OBFUSCATION", "True") == "True" # TODO should be false by default +OBFUSCATE_MODE = os.getenv("OBFUSCATION_MODE", "obfs4") +OBFUSCATE_SOCKS_TOR_PORT = int(os.getenv("OBFUSCATE_SOCKS_TOR_PORT", "5555")) +OBFUSCATE_TOR_LISTEN_ADDR = int(os.getenv("OBFUSCATE_TOR_LISTEN_ADDR", "5556")) +OBFUSCATE_SUPPORTED = ["obfs4"] +assert OBFUSCATE_MODE in OBFUSCATE_SUPPORTED, "Invalid OBFUSCATE_MODE=%s, Valid options are: %s" % (OBFUSCATE_MODE, + OBFUSCATE_SUPPORTED) + os.makedirs("build", exist_ok=True) DEFAULT_POST_UP = os.getenv("POST_UP", "iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;") DEFAULT_POST_DOWN = os.getenv("POST_DOWN", "iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;") diff --git a/wg_dashboard_backend/requirements.txt b/wg_dashboard_backend/requirements.txt index 30347ba..7200e5f 100644 --- a/wg_dashboard_backend/requirements.txt +++ b/wg_dashboard_backend/requirements.txt @@ -12,4 +12,5 @@ jinja2 sqlalchemy_utils sqlalchemy-migrate requests -uvicorn \ No newline at end of file +uvicorn +qrcode[pil] \ No newline at end of file diff --git a/wg_dashboard_backend/script/obfuscate/__init__.py b/wg_dashboard_backend/script/obfuscate/__init__.py new file mode 100644 index 0000000..9db98d6 --- /dev/null +++ b/wg_dashboard_backend/script/obfuscate/__init__.py @@ -0,0 +1,56 @@ +import abc +from pathlib import Path +import subprocess +import shlex + + +class BaseObfuscation(abc.ABC): + + def __init__(self, binary_name=None, binary_path=None, algorithm=None): + + assert binary_name is not None or binary_path is not None + self.binary_name = binary_name if binary_name is not None else Path(self.binary_path).name + self.binary_path = binary_path if binary_path else "" + self.algorithm = algorithm + + def ensure_installed(self): + + # Attempt to find process by path + binary = Path(self.binary_path) + if not binary.is_file(): + # Did not find by path, attempt to find using which + proc_which = subprocess.Popen(["which", self.binary_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + data = [x.decode().strip() for x in proc_which.communicate() if x != b''][0] + + if proc_which.returncode != 0: + raise RuntimeError("Could not find binary '%s'" % data) + + self.binary_path = data + + def execute(self, *args, kill_first=False, override_command=None): + + if kill_first: + # TODO try to delete by full name as we dont want to kill other processes. + pattern = self.binary_name + self.execute(*[pattern], override_command="pkill") + #pattern = self.binary_path + " " + ' '.join(args) + #print(pattern) + #kill_output, kill_code = self.execute(*[pattern], override_command="pkill") + + command = override_command if override_command is not None else self.binary_path + print(shlex.join([command] + list(args))) + proc_which = subprocess.Popen([command] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + raw_data = proc_which.communicate() + + data = [x.decode().strip() for x in raw_data if x != b''] + if len(data) == 0: + data = "" + else: + data = data[0] + return data, proc_which.returncode + + + + + + diff --git a/wg_dashboard_backend/script/obfuscate/obfs4.py b/wg_dashboard_backend/script/obfuscate/obfs4.py new file mode 100644 index 0000000..7bb592a --- /dev/null +++ b/wg_dashboard_backend/script/obfuscate/obfs4.py @@ -0,0 +1,30 @@ +from script.obfuscate import BaseObfuscation +import re + + +class ObfuscateOBFS4(BaseObfuscation): + + def __init__(self): + super().__init__( + binary_name="obfs4proxy", + binary_path="/usr/bin/obfs4proxy", + algorithm="obfs4" + ) + + self.ensure_installed() + + def ensure_installed(self): + super().ensure_installed() + + output, code = self.execute("-version") + + if re.match(f'{self.binary_name}-[0-9]+.[0-9]+.[0-9]+', output) and code == 0: + return True + else: + raise RuntimeError(f"Could not verify that {self.binary_name} is installed correctly.") + + +if __name__ == "__main__": + + x = ObfuscateOBFS4() + x.ensure_installed() \ No newline at end of file diff --git a/wg_dashboard_backend/script/obfuscate/tor.py b/wg_dashboard_backend/script/obfuscate/tor.py new file mode 100644 index 0000000..3883fcb --- /dev/null +++ b/wg_dashboard_backend/script/obfuscate/tor.py @@ -0,0 +1,119 @@ +from pathlib import Path + +import requests + +import const +from script.obfuscate import BaseObfuscation +import re +import os +import qrcode +import socket + +from script.obfuscate.obfs4 import ObfuscateOBFS4 + + +class ObfuscationViaTOR(BaseObfuscation): + + def __init__(self, algorithm: BaseObfuscation): + super().__init__( + binary_name="tor" + ) + self.algorithm = algorithm + self.tor_data_dir = "/tmp/wg-manager-tor-proxy" + self.tor_config_file = "/tmp/wg-manager-tor-proxy/torrc" + self.tor_fingerprint_file = f"{self.tor_data_dir}/fingerprint" + self.tor_bridge_file = f"{self.tor_data_dir}/pt_state/obfs4_bridgeline.txt" + + Path(self.tor_config_file).touch() + os.makedirs(self.tor_data_dir, exist_ok=True) + + def __del__(self): + pass + + def ensure_installed(self): + super().ensure_installed() + output, code = self.execute("--version") + + if re.match(f'Tor version .*', output) and code == 0: + return True + else: + raise RuntimeError(f"Could not verify that {self.binary_name} is installed correctly.") + + def start(self): + + output, code = self.execute( + "-f", self.tor_config_file, + "--DataDirectory", self.tor_data_dir, + "--RunAsDaemon", "1", + "--ExitPolicy", "reject *:*", + "--ORPort", str(const.OBFUSCATE_SOCKS_TOR_PORT), + "--BridgeRelay", "1", + "--PublishServerDescriptor", "0", + "--ServerTransportPlugin", f"{self.algorithm.algorithm} exec {self.algorithm.binary_path}", + "--ServerTransportListenAddr", f"{self.algorithm.algorithm} 0.0.0.0:{const.OBFUSCATE_TOR_LISTEN_ADDR}", + "--ExtORPort", "auto", + "--ContactInfo", "wg-manager@github.com", + "--Nickname", "wgmanager", + kill_first=True + ) + + print(output) + + def generate_bridge_line(self, local=False): + + if local: + ip_address = socket.gethostbyname(socket.gethostname()) + else: + ip_address = requests.get("https://api.ipify.org").text + + with open(self.tor_fingerprint_file, "r") as f: + fingerprint = f.read().split(" ") + assert len(fingerprint) == 2, "Could not load fingerprint correctly. " \ + "Should be a list of 2 items (name, fingerprint)" + fingerprint = fingerprint[1] + + with open(self.tor_bridge_file, "r") as f: + bridge_line_raw = f.read() + + bridge_line = re.search(r"^Bridge .*", bridge_line_raw, re.MULTILINE).group(0) + bridge_line = bridge_line\ + .replace("", ip_address)\ + .replace("", str(const.OBFUSCATE_TOR_LISTEN_ADDR))\ + .replace("", fingerprint)\ + .replace("Bridge ", "bridge://")\ + .replace("\n", "") + #bridge_line = f"bridge://{self.algorithm.algorithm} {ip_address}:{const.OBFUSCATE_SOCKS_TOR_PORT} {fingerprint}" + print(bridge_line) + return bridge_line + + def output_qr(self, text, image=False): + + qr = qrcode.QRCode( + version=10, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(text) + qr.make(fit=True) + + if image: + img = qr.make_image(fill_color="black", back_color="white") + img.show() + else: + try: + qr.print_tty() + except: + qr.print_ascii() + + +if __name__ == "__main__": + + x = ObfuscationViaTOR( + algorithm=ObfuscateOBFS4() + ) + x.ensure_installed() + x.start() + bridge_line = x.generate_bridge_line(local=False) + x.output_qr(bridge_line, image=True) + #x.generate_bridge_line(local=False) \ No newline at end of file From 89b41bcf697f50a9d72e5bc39289d8e9dc778efa Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 20 Jul 2020 23:52:51 +0200 Subject: [PATCH 22/29] * Clearified error message when no ADMIN_USERNAME, and ADMIN_PASSWORD env variable is set. #27 --- wg_dashboard_backend/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wg_dashboard_backend/main.py b/wg_dashboard_backend/main.py index a876841..2b4857b 100644 --- a/wg_dashboard_backend/main.py +++ b/wg_dashboard_backend/main.py @@ -37,12 +37,12 @@ _db: Session = SessionLocal() if not database_exists(engine.url): ADMIN_USERNAME = os.getenv("ADMIN_USERNAME") if not ADMIN_USERNAME: - raise RuntimeError("Database does not exist and no ADMIN_USERNAME is set") + raise RuntimeError("Database does not exist and the environment variable ADMIN_USERNAME is set") ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD") if not ADMIN_PASSWORD: - raise RuntimeError("Database does not exist and no ADMIN_PASSWORD is set") + raise RuntimeError("Database does not exist and the environment variable ADMIN_PASSWORD is set") # Create database from metadata models.Base.metadata.create_all(engine) From df1c94b8e9b04e12f27779b8a5c158f14bc904a6 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Tue, 21 Jul 2020 02:27:07 +0200 Subject: [PATCH 23/29] * Initial version of client-mode. * A lot of small fixes * Added gui for client mode, where Clients are read-only (may change in the future) * NEED FEEBACK On this :) --- README.md | 88 +++++++- docker-compose.yaml | 43 +++- wg_dashboard_backend/const.py | 8 + wg_dashboard_backend/db/api_key.py | 27 +++ wg_dashboard_backend/db/user.py | 4 +- wg_dashboard_backend/db/wireguard.py | 94 ++++++++- wg_dashboard_backend/main.py | 19 +- .../versions/007_create_read_only_client.py | 34 +++ wg_dashboard_backend/models.py | 2 + wg_dashboard_backend/requirements.txt | 2 +- wg_dashboard_backend/routers/v1/peer.py | 26 ++- wg_dashboard_backend/routers/v1/server.py | 53 +---- wg_dashboard_backend/schemas.py | 8 +- wg_dashboard_backend/script/wireguard.py | 195 +++++++++++++++++- .../src/app/interfaces/peer.ts | 1 + .../src/app/interfaces/server.ts | 1 + .../add-server/add-server.component.ts | 1 + .../dashboard/server/server.component.html | 102 ++++++++- .../dashboard/server/server.component.scss | 3 + .../page/dashboard/server/server.component.ts | 6 + .../src/app/services/config.service.ts | 3 +- .../src/app/services/server.service.ts | 34 +-- 22 files changed, 663 insertions(+), 91 deletions(-) create mode 100644 wg_dashboard_backend/db/api_key.py create mode 100644 wg_dashboard_backend/migrations/versions/007_create_read_only_client.py diff --git a/README.md b/README.md index e544b9b..8869786 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The features of wg-manager includes: * Bandwidth usage statistics * Export by QRCode, Text * Authentication via API-Keys for automation (Created in GUI) +* Automatic setup using docker **General** * Modify Admin User @@ -94,7 +95,81 @@ The API docs is found [here](./docs/api.md). 2. Go to edit profile 3. Create API-Key and take note of the key. Use the X-API-Key header to authenticate. 4. Example: `curl -i -H "X-API-Key: " http://:/api/v1/users/api-key/list` -5. Example 2: `curl -X POST "http://:/api/v1/peer/add/configuration" -H "accept: application/json" -H "Content-Type: application/json" -H "X-API-Key: " -d "{\"server_interface\":\"wg0\"}"` +5. Example 2: `curl -X POST "http://:/api/v1/peer/configuration/add" -H "accept: application/json" -H "Content-Type: application/json" -H "X-API-Key: " -d "{\"server_interface\":\"wg0\"}"` + +# Client Mode +wg-manager can also run in client-mode, with near-automatic setup and connection. To automatically setup the client, +you will need: +1. wg-manager server url +2. name of the interface the client should run on +3. wg-manager server api key + +You can setup multiple clients using the numbered environment variables. The following configuration runs a server and client automatically: +```dockerfile +version: "2.1" +services: + + server: + container_name: wg-manager + build: . + restart: always + sysctls: + net.ipv6.conf.all.disable_ipv6: 0 + cap_add: + - NET_ADMIN + #network_mode: host # Alternatively + ports: + - 11820:11820/udp + - 51800-51900:51800-51900/udp + - 8888:8888 + environment: + HOST: 0.0.0.0 + PORT: 8888 + ADMIN_USERNAME: admin + ADMIN_PASSWORD: admin + WEB_CONCURRENCY: 2 + SERVER_INIT_INTERFACE_START: 1 + + #endpoint dynamic variables: ||external|| , ||internal|| + SERVER_INIT_INTERFACE: '{"address":"10.0.200.1","v6_address":"fd42:42:42::1","subnet":24,"v6_subnet":64,"interface":"wg0","listen_port":"51820","endpoint":"server","dns":"10.0.200.1,8.8.8.8","private_key":"","public_key":"","post_up":"","post_down":"","configuration":"","is_running":false,"peers":[]}' + SERVER_STARTUP_API_KEY: thisisasecretkeythatnobodyknows + networks: + - wg-manager-net + + client: + container_name: wg-manager-server-with-client + build: . + restart: always + sysctls: + net.ipv6.conf.all.disable_ipv6: 0 + cap_add: + - NET_ADMIN + ports: + - 8889:8889 + privileged: true + environment: + HOST: 0.0.0.0 # Optional (For Accessing WEB-Gui) + PORT: 8889 # Optional (Web-GUI Listen Port) + WEB_CONCURRENCY: 1 # Optional + ADMIN_USERNAME: admin + ADMIN_PASSWORD: admin + INIT_SLEEP: 5 # If you run into concurrency issues + SERVER: 0 # If you want to host a server as well + CLIENT: 1 # If you want to connect to servers + CLIENT_START_AUTOMATICALLY: 1 # If you want the client to start automatically + CLIENT_1_NAME: "client-1" # Name of first client + CLIENT_1_SERVER_HOST: "http://server:8888" # Endpoint of first server + CLIENT_1_SERVER_INTERFACE: "wg0" # Interface of first server (to get config) + CLIENT_1_API_KEY: "thisisasecretkeythatnobodyknows" # API-Key of first server (to get config) + networks: + - wg-manager-net + +networks: + wg-manager-net: + driver: bridge +``` + + # Environment variables | Environment | Description | Recommended | |------------------|---------------------------------------------------------------------------|-------------| @@ -110,6 +185,17 @@ The API docs is found [here](./docs/api.md). | POST_DOWN | The POST_DOWN Command (version 4) | default | | POST_UP_V6 | The POST_UP Command (version 6) | default | | POST_DOWN_V6 | The POST_DOWN Command (version 6) | default | +| INIT_SLEEP | Sleep before bootstrap. Useful for delaying client boot | integer | +| SERVER_STARTUP_API_KEY | Create a initial, and known API key on server init | secret | +| SERVER_INIT_INTERFACE | Create a initial wireguard interface on server init. See docs | json | +| SERVER_INIT_INTERFACE_START | If the interface should start immediately | 1 or 0 | +| SERVER | If the container should enable server-mode | 1 or 0 | +| CLIENT | If the container should enable client-mode | 1 or 0 | +| CLIENT_START_AUTOMATICALLY | If client is enabled. should it start immediately? | 1 or 0 | +| CLIENT_X_NAME | Name of the automatically generated client. X = incremental number from 1 | string | +| CLIENT_X_SERVER_HOST | The url to wg-manager server e.g. "http://server:8888" See docs | url | +| CLIENT_X_SERVER_INTERFACE | The wg-interface to create client on e.g"wg0". See docs | string | +| CLIENT_X_API_KEY | A valid API-Key that is active on the server. Works well with SERVER_STARTUP_API_KEY | string | # Showcase ![Illustration](docs/images/0.png) diff --git a/docker-compose.yaml b/docker-compose.yaml index 19ee233..10da013 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,11 +14,48 @@ services: - 11820:11820/udp - 51800-51900:51800-51900/udp - 8888:8888 - volumes: - - ./wg-manager:/config environment: HOST: 0.0.0.0 PORT: 8888 + ADMIN_USERNAME: admin ADMIN_PASSWORD: admin + WEB_CONCURRENCY: 2 + SERVER_INIT_INTERFACE_START: 1 + + #endpoint dynamic variables: ||external|| , ||internal|| + SERVER_INIT_INTERFACE: '{"address":"10.0.200.1","v6_address":"fd42:42:42::1","subnet":24,"v6_subnet":64,"interface":"wg0","listen_port":"51820","endpoint":"server","dns":"10.0.200.1,8.8.8.8","private_key":"","public_key":"","post_up":"","post_down":"","configuration":"","is_running":false,"peers":[]}' + SERVER_STARTUP_API_KEY: thisisasecretkeythatnobodyknows + networks: + - wg-manager-net + + client: + container_name: wg-manager-server-with-client + build: . + restart: always + sysctls: + net.ipv6.conf.all.disable_ipv6: 0 + cap_add: + - NET_ADMIN + ports: + - 8889:8889 + privileged: true + environment: + HOST: 0.0.0.0 # Optional (For Accessing WEB-Gui) + PORT: 8889 # Optional (Web-GUI Listen Port) + WEB_CONCURRENCY: 1 # Optional ADMIN_USERNAME: admin - WEB_CONCURRENCY: 2 \ No newline at end of file + ADMIN_PASSWORD: admin + INIT_SLEEP: 5 # If you run into concurrency issues + SERVER: 0 # If you want to host a server as well + CLIENT: 1 # If you want to connect to servers + CLIENT_START_AUTOMATICALLY: 1 # If you want the client to start automatically + CLIENT_1_NAME: "client-1" # Name of first client + CLIENT_1_SERVER_HOST: "http://server:8888" # Endpoint of first server + CLIENT_1_SERVER_INTERFACE: "wg0" # Interface of first server (to get config) + CLIENT_1_API_KEY: "thisisasecretkeythatnobodyknows" # API-Key of first server (to get config) + networks: + - wg-manager-net + +networks: + wg-manager-net: + driver: bridge \ No newline at end of file diff --git a/wg_dashboard_backend/const.py b/wg_dashboard_backend/const.py index 1ea55ab..bfd8e89 100644 --- a/wg_dashboard_backend/const.py +++ b/wg_dashboard_backend/const.py @@ -19,6 +19,14 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30 CMD_WG_COMMAND = ["wg"] CMD_WG_QUICK = ["wg-quick"] +INIT_SLEEP = int(os.getenv("INIT_SLEEP", "0")) +SERVER_STARTUP_API_KEY = os.getenv("SERVER_STARTUP_API_KEY", None) +SERVER_INIT_INTERFACE = os.getenv("SERVER_INIT_INTERFACE", None) +SERVER_INIT_INTERFACE_START = os.getenv("SERVER_INIT_INTERFACE_START", "1") == "1" +SERVER = os.getenv("SERVER", "1") == "1" +CLIENT = os.getenv("CLIENT", "0") == "1" +CLIENT_START_AUTOMATICALLY = os.getenv("CLIENT_START_AUTOMATICALLY", "1") == "1" + if not IS_DOCKER: CMD_WG_COMMAND = ["sudo"] + CMD_WG_COMMAND CMD_WG_QUICK = ["sudo"] + CMD_WG_QUICK diff --git a/wg_dashboard_backend/db/api_key.py b/wg_dashboard_backend/db/api_key.py new file mode 100644 index 0000000..898a4dc --- /dev/null +++ b/wg_dashboard_backend/db/api_key.py @@ -0,0 +1,27 @@ +from sqlalchemy.orm import Session + +import models + + +def add_initial_api_key_for_admin(sess: Session, api_key): + + db_user = sess.query(models.User)\ + .filter_by(username="admin")\ + .one() + + exists_api_key = sess.query(models.UserAPIKey)\ + .filter_by( + user_id=db_user.id, + key=api_key + )\ + .count() + + if exists_api_key == 0: + db_api_key = models.UserAPIKey() + db_api_key.key = api_key + db_api_key.user_id = db_user.id + + sess.add(db_api_key) + sess.commit() + + return True diff --git a/wg_dashboard_backend/db/user.py b/wg_dashboard_backend/db/user.py index b5f5509..ea5634c 100644 --- a/wg_dashboard_backend/db/user.py +++ b/wg_dashboard_backend/db/user.py @@ -7,13 +7,11 @@ from passlib.context import CryptContext import schemas - - def update_user(sess: Session, form_data: schemas.UserInDB): user = get_user_by_name(sess, form_data.username) user.password = form_data.password user.full_name = form_data.full_name - user.email = form_data.email # TOD this section should be updated + user.email = form_data.email # TODO this section should be updated sess.add(user) sess.commit() diff --git a/wg_dashboard_backend/db/wireguard.py b/wg_dashboard_backend/db/wireguard.py index 71187fe..0fb93d4 100644 --- a/wg_dashboard_backend/db/wireguard.py +++ b/wg_dashboard_backend/db/wireguard.py @@ -1,7 +1,11 @@ import ipaddress +import json import os import shutil import typing + +from starlette.exceptions import HTTPException + import const import script.wireguard from sqlalchemy import exists @@ -43,7 +47,6 @@ def peer_dns_set(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer: def peer_remove(sess: Session, peer: schemas.WGPeer) -> bool: - db_peers = sess.query(models.WGPeer).filter_by(id=peer.id).all() for db_peer in db_peers: @@ -132,10 +135,97 @@ def server_update_field(sess: Session, interface: str, server: schemas.WGServer, def server_get_all(sess: Session) -> typing.List[schemas.WGServer]: - db_interfaces = sess.query(models.WGServer).all() + db_interfaces = sess.query(models.WGServer) \ + .all() return [schemas.WGServer.from_orm(db_interface) for db_interface in db_interfaces] +def server_add_on_init(sess: Session): + """ + Routine for adding server from env variable. + :param server: + :param sess: + :return: + """ + try: + init_data = json.loads(const.SERVER_INIT_INTERFACE) + + if init_data["endpoint"] == "||external||": + import requests + init_data["endpoint"] = requests.get("https://api.ipify.org").text + elif init_data["endpoint"] == "||internal||": + import socket + init_data["endpoint"] = socket.gethostbyname(socket.gethostname()) + + if sess.query(models.WGServer) \ + .filter_by(endpoint=init_data["endpoint"], listen_port=init_data["listen_port"]) \ + .count() == 0: + # Only add if it does not already exists. + server_add(schemas.WGServerAdd(**init_data), sess, start=const.SERVER_INIT_INTERFACE_START) + except Exception as e: + _LOGGER.warning("Failed to setup initial server interface with exception:") + _LOGGER.exception(e) + + +def server_add(server: schemas.WGServerAdd, sess: Session, start=False): + # Configure POST UP with defaults if not manually set. + if server.post_up == "": + server.post_up = const.DEFAULT_POST_UP + if server.v6_address is not None: + server.post_up += const.DEFAULT_POST_UP_v6 + + # Configure POST DOWN with defaults if not manually set. + if server.post_down == "": + server.post_down = const.DEFAULT_POST_DOWN + if server.v6_address is not None: + server.post_down += const.DEFAULT_POST_DOWN_v6 + + peers = server.peers if server.peers else [] + + # Public/Private key + try: + + if sess.query(models.WGServer) \ + .filter( + (models.WGServer.interface == server.interface) | + (models.WGServer.address == server.address) | + (models.WGServer.v6_address == server.v6_address)).count() != 0: + raise HTTPException(status_code=400, + detail="The server interface or ip %s already exists in the database" % server.interface) + + if not server.private_key: + keys = script.wireguard.generate_keys() + server.private_key = keys["private_key"] + server.public_key = keys["public_key"] + + server.configuration = script.wireguard.generate_config(server) + server.peers = [] + server.sync(sess) + + if len(peers) > 0: + server.from_db(sess) + + for schemaPeer in peers: + schemaPeer.server_id = server.id + schemaPeer.configuration = script.wireguard.generate_config(dict( + peer=schemaPeer, + server=server + )) + dbPeer = models.WGPeer(**schemaPeer.dict()) + sess.add(dbPeer) + sess.commit() + + server.from_db(sess) + + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + if start and not script.wireguard.is_running(server): + script.wireguard.start_interface(server) + + return server + + def server_remove(sess: Session, server: schemas.WGServer) -> bool: db_server = server_query_get_by_interface(sess, server.interface).one() if db_server is None: diff --git a/wg_dashboard_backend/main.py b/wg_dashboard_backend/main.py index 2b4857b..f9d9a59 100644 --- a/wg_dashboard_backend/main.py +++ b/wg_dashboard_backend/main.py @@ -1,10 +1,14 @@ import logging import os +import time import typing from sqlalchemy_utils import database_exists from starlette.middleware.base import BaseHTTPMiddleware +import const +import db.wireguard +import db.api_key import middleware from database import engine, SessionLocal from routers.v1 import user, server, peer, wg @@ -27,6 +31,8 @@ from migrate import DatabaseAlreadyControlledError from migrate.versioning.shell import main import models +# Sleep the wait timer. +time.sleep(const.INIT_SLEEP) app = FastAPI() app.add_middleware(BaseHTTPMiddleware, dispatch=middleware.db_session_middleware) @@ -34,6 +40,7 @@ app.add_middleware(BaseHTTPMiddleware, dispatch=middleware.db_session_middleware _db: Session = SessionLocal() # Ensure database existence + if not database_exists(engine.url): ADMIN_USERNAME = os.getenv("ADMIN_USERNAME") if not ADMIN_USERNAME: @@ -48,14 +55,14 @@ if not database_exists(engine.url): models.Base.metadata.create_all(engine) # Create default user - _db.add(models.User( + _db.merge(models.User( username=ADMIN_USERNAME, password=middleware.get_password_hash(ADMIN_PASSWORD), full_name="Admin", role="admin", email="" )) -_db.commit() + _db.commit() # Do migrations @@ -75,6 +82,14 @@ for s in servers: except Exception as e: print(e) +if const.CLIENT: + script.wireguard.load_environment_clients(_db) + +if const.SERVER_INIT_INTERFACE is not None: + db.wireguard.server_add_on_init(_db) + +if const.SERVER_STARTUP_API_KEY is not None: + db.api_key.add_initial_api_key_for_admin(_db, const.SERVER_STARTUP_API_KEY) _db.close() diff --git a/wg_dashboard_backend/migrations/versions/007_create_read_only_client.py b/wg_dashboard_backend/migrations/versions/007_create_read_only_client.py new file mode 100644 index 0000000..4ae29b9 --- /dev/null +++ b/wg_dashboard_backend/migrations/versions/007_create_read_only_client.py @@ -0,0 +1,34 @@ +from sqlalchemy import * +from migrate import * + + +def upgrade(migrate_engine): + try: + meta = MetaData(bind=migrate_engine) + server = Table('server', meta, autoload=True) + read_only = Column('read_only', INTEGER, default=0) + read_only.create(server) + except: + pass + + try: + meta = MetaData(bind=migrate_engine) + peer = Table('peer', meta, autoload=True) + read_only = Column('read_only', INTEGER, default=0) + read_only.create(peer) + except: + pass + +def downgrade(migrate_engine): + try: + meta = MetaData(bind=migrate_engine) + server = Table('server', meta, autoload=True) + server.c.read_only.drop() + except: + pass + try: + meta = MetaData(bind=migrate_engine) + server = Table('peer', meta, autoload=True) + server.c.read_only.drop() + except: + pass \ No newline at end of file diff --git a/wg_dashboard_backend/models.py b/wg_dashboard_backend/models.py index 13fc980..0c7ef68 100644 --- a/wg_dashboard_backend/models.py +++ b/wg_dashboard_backend/models.py @@ -41,6 +41,7 @@ class WGServer(Base): public_key = Column(sqlalchemy.String) endpoint = Column(sqlalchemy.String) dns = Column(sqlalchemy.String) + read_only = Column(sqlalchemy.Integer, default=0) post_up = Column(sqlalchemy.String) post_down = Column(sqlalchemy.String) @@ -62,6 +63,7 @@ class WGPeer(Base): shared_key = Column(sqlalchemy.Text) dns = Column(sqlalchemy.Text) allowed_ips = Column(sqlalchemy.String) + read_only = Column(sqlalchemy.Integer, default=0) server_id = Column(Integer, sqlalchemy.ForeignKey('server.id', ondelete="CASCADE", onupdate="CASCADE")) server = relationship("WGServer", backref=backref("server")) diff --git a/wg_dashboard_backend/requirements.txt b/wg_dashboard_backend/requirements.txt index 30347ba..d81d475 100644 --- a/wg_dashboard_backend/requirements.txt +++ b/wg_dashboard_backend/requirements.txt @@ -12,4 +12,4 @@ jinja2 sqlalchemy_utils sqlalchemy-migrate requests -uvicorn \ No newline at end of file +uvicorn diff --git a/wg_dashboard_backend/routers/v1/peer.py b/wg_dashboard_backend/routers/v1/peer.py index 4fe642b..7bc8949 100644 --- a/wg_dashboard_backend/routers/v1/peer.py +++ b/wg_dashboard_backend/routers/v1/peer.py @@ -45,7 +45,7 @@ def generate_ip_address(server: schemas.WGServer, v6): @router.post("/add", response_model=schemas.WGPeer) def add_peer( - peer_add: schemas.WGPeerAdd, + peer_add: schemas.WGPeerConfigAdd, sess: Session = Depends(middleware.get_db) ): server = schemas.WGServer(interface=peer_add.server_interface).from_db(sess) @@ -68,7 +68,7 @@ def add_peer( peer.allowed_ips = ', '.join(const.PEER_DEFAULT_ALLOWED_IPS) # Set unnamed - peer.name = "Unnamed" + peer.name = "Unnamed" if not peer_add.name else peer_add.name peer.dns = server.dns @@ -91,8 +91,26 @@ def add_peer( return schemas.WGPeer.from_orm(db_peer) -@router.post("/add/configuration") -def add_peer_get_config(peer_add: schemas.WGPeerAdd, +@router.post("/configuration/get_or_add") +def get_or_add_peer_return_config(peer_get: schemas.WGPeerConfigGetByName, + sess: Session = Depends(middleware.get_db) + ): + server = sess.query(models.WGServer).filter_by(interface=peer_get.server_interface).one() + peer = sess.query(models.WGPeer).filter_by(name=peer_get.name, server_id=server.id).all() + + if not peer: + return add_peer_get_config(schemas.WGPeerConfigAdd( + name=peer_get.name, + server_interface=peer_get.server_interface + ), sess=sess) + + peer = peer[0] + + return PlainTextResponse(peer.configuration) + + +@router.post("/configuration/add") +def add_peer_get_config(peer_add: schemas.WGPeerConfigAdd, sess: Session = Depends(middleware.get_db) ): wg_peer: schemas.WGPeer = add_peer(peer_add, sess) diff --git a/wg_dashboard_backend/routers/v1/server.py b/wg_dashboard_backend/routers/v1/server.py index 938881d..4bf4d8d 100644 --- a/wg_dashboard_backend/routers/v1/server.py +++ b/wg_dashboard_backend/routers/v1/server.py @@ -33,58 +33,7 @@ def add_interface( sess: Session = Depends(middleware.get_db) ): - # Configure POST UP with defaults if not manually set. - if server.post_up == "": - server.post_up = const.DEFAULT_POST_UP - if server.v6_address is not None: - server.post_up += const.DEFAULT_POST_UP_v6 - - # Configure POST DOWN with defaults if not manually set. - if server.post_down == "": - server.post_down = const.DEFAULT_POST_DOWN - if server.v6_address is not None: - server.post_down += const.DEFAULT_POST_DOWN_v6 - - peers = server.peers if server.peers else [] - - # Public/Private key - try: - - if sess.query(models.WGServer)\ - .filter( - (models.WGServer.interface == server.interface) | - (models.WGServer.address == server.address) | - (models.WGServer.v6_address == server.v6_address)).count() != 0: - raise HTTPException(status_code=400, detail="The server interface or ip %s already exists in the database" % server.interface) - - if not server.private_key: - keys = script.wireguard.generate_keys() - server.private_key = keys["private_key"] - server.public_key = keys["public_key"] - - server.configuration = script.wireguard.generate_config(server) - server.peers = [] - server.sync(sess) - - if len(peers) > 0: - server.from_db(sess) - - for schemaPeer in peers: - schemaPeer.server_id = server.id - schemaPeer.configuration = script.wireguard.generate_config(dict( - peer=schemaPeer, - server=server - )) - dbPeer = models.WGPeer(**schemaPeer.dict()) - sess.add(dbPeer) - sess.commit() - - server.from_db(sess) - - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - - return server + return db.wireguard.server_add(server, sess) @router.post("/stop", response_model=schemas.WGServer) diff --git a/wg_dashboard_backend/schemas.py b/wg_dashboard_backend/schemas.py index 7a16513..2e71529 100644 --- a/wg_dashboard_backend/schemas.py +++ b/wg_dashboard_backend/schemas.py @@ -169,6 +169,7 @@ class WGServer(GenericModel): post_up: str = None post_down: str = None dns: str = None + read_only: int = None peers: pydantic.typing.List['WGPeer'] = [] @@ -188,6 +189,11 @@ class WGServerAdd(WGServer): listen_port: int -class WGPeerAdd(GenericModel): +class WGPeerConfigAdd(GenericModel): server_interface: str + name: str = None + +class WGPeerConfigGetByName(GenericModel): + server_interface: str + name: str = None \ No newline at end of file diff --git a/wg_dashboard_backend/script/wireguard.py b/wg_dashboard_backend/script/wireguard.py index 8286df4..e252bb6 100644 --- a/wg_dashboard_backend/script/wireguard.py +++ b/wg_dashboard_backend/script/wireguard.py @@ -1,16 +1,21 @@ import logging +import random import subprocess import tempfile - +import requests import typing +import configparser + +from sqlalchemy.orm import Session import const import models import schemas import os import re - +import ipaddress import util +from database import SessionLocal _LOGGER = logging.getLogger(__name__) @@ -30,6 +35,7 @@ class WGPermissionsError(Exception): class WGPortAlreadyInUse(Exception): pass + class TempServerFile(): def __init__(self, server: schemas.WGServer): self.server = server @@ -78,10 +84,10 @@ def generate_psk(): return subprocess.check_output(const.CMD_WG_COMMAND + ["genpsk"]).decode("utf-8").strip() -def start_interface(server: schemas.WGServer): +def start_interface(server: typing.Union[schemas.WGServer, schemas.WGPeer]): with TempServerFile(server) as server_file: try: - #print(*const.CMD_WG_QUICK, "up", server_file) + # print(*const.CMD_WG_QUICK, "up", server_file) output = subprocess.check_output(const.CMD_WG_QUICK + ["up", server_file], stderr=subprocess.STDOUT) return output except Exception as e: @@ -113,7 +119,7 @@ def restart_interface(server: schemas.WGServer): def is_running(server: schemas.WGServer): try: output = _run_wg(server, ["show", server.interface]) - if output is None: + if output is None or b'Unable to access interface: No such device' in output: return False except Exception as e: print(e.output) @@ -217,3 +223,182 @@ def generate_config(obj: typing.Union[typing.Dict[schemas.WGPeer, schemas.WGServ return result + +def retrieve_client_conf_from_server( + client_name, + server_interface, + server_host, + server_api_key +): + const.CLIENT_NAME = "client-1" + const.CLIENT_SERVER_INTERFACE = "wg0" + const.CLIENT_SERVER_HOST = "http://localhost:4200" + const.CLIENT_API_KEY = "8bae20143fb962930614952d80634822361fd5ab9488053866a56de5881f9d7b" + + assert server_interface is not None and \ + server_host is not None and \ + server_api_key is not None, "Client configuration is invalid: %s, %s, api-key-is-null?: %s" % ( + server_interface, + server_host, + server_api_key is None + ) + + api_get_or_add = f"{server_host}/api/v1/peer/configuration/get_or_add" + + response = requests.post(api_get_or_add, json={ + "server_interface": server_interface, + "name": client_name + }, headers={ + "X-API-Key": server_api_key + }) + + if response.status_code != 200: + print(response.text) + raise RuntimeError("Could not retrieve config from server: %s" % (api_get_or_add,)) + + return response.text + + +def create_client_config(sess: Session, configuration, client_name): + + parser = configparser.ConfigParser() + parser.read_string(configuration) + public_key = parser["Peer"]["PublicKey"] + + assert len(set(parser.sections()) - {"Interface", "Peer"}) == 0, "Missing Interface or Peer section" + + # Parse Server + # Check if server already exists. + + is_new_server = False + is_new_peer = False + + try: + db_server = sess.query(models.WGServer).filter_by( + public_key=public_key, + read_only=1 + ).one() + except: + db_server = None + + if db_server is None: + db_server = models.WGServer() + is_new_server = True + + db_server.read_only = 1 + db_server.public_key = parser["Peer"]["PublicKey"] + db_server.address = parser["Peer"]["Endpoint"] + db_server.listen_port = random.randint(69000, 19292009) + + db_server.v6_address = "N/A" + db_server.v6_subnet = 0 + db_server.address = "N/A" + db_server.subnet = 0 + db_server.private_key = "N/A" + db_server.dns = "N/A" + db_server.post_up = "N/A" + db_server.post_down = "N/A" + db_server.is_running = False + db_server.configuration = "N/A" + + # Parse client + try: + db_peer = sess.query(models.WGPeer).filter_by( + private_key=parser["Interface"]["PrivateKey"], + read_only=1 + ).one() + except: + db_peer = None + + if db_peer is None: + db_peer = models.WGPeer() + is_new_peer = True + + db_peer.read_only = 1 + db_peer.name = client_name + + addresses_split = parser["Interface"]["Address"].split(",") + assert len(addresses_split) > 0, "Must be at least one address" + + for address_with_subnet in addresses_split: + addr, subnet = address_with_subnet.split("/") + + if isinstance(ipaddress.ip_address(addr), ipaddress.IPv4Address): + db_peer.address = address_with_subnet + elif isinstance(ipaddress.ip_address(addr), ipaddress.IPv6Address): + db_peer.v6_address = address_with_subnet + else: + raise RuntimeError("Incorrect IP Address: %s, %s" % (addr, subnet)) + + db_peer.private_key = parser["Interface"]["PrivateKey"] + db_peer.public_key = "N/A" + db_peer.allowed_ips = parser["Peer"]["AllowedIPs"] + db_peer.configuration = configuration + db_server.interface = f"client_{db_peer.name}" + db_server.configuration = configuration + try: + db_peer.shared_key = parser["Interface"]["PrivateKey"] + except KeyError: + db_peer.shared_key = "N/A" + + db_peer.dns = parser["Interface"]["DNS"] + db_peer.server = db_server + + if is_new_server: + sess.add(db_server) + else: + sess.merge(db_server) + sess.commit() + + if is_new_peer: + sess.add(db_peer) + else: + sess.merge(db_peer) + sess.commit() + + if const.CLIENT_START_AUTOMATICALLY and not is_running(db_server): + start_interface(db_server) + + +def load_environment_clients(sess: Session): + i = 1 + while True: + + client_name = os.getenv(f"CLIENT_{i}_NAME", None) + client_server_interface = os.getenv(f"CLIENT_{i}_SERVER_INTERFACE", None) + client_server_host = os.getenv(f"CLIENT_{i}_SERVER_HOST", None) + client_api_key = os.getenv(f"CLIENT_{i}_API_KEY", None) + + if client_api_key is None or \ + client_server_interface is None or \ + client_server_host is None or \ + client_api_key is None: + break + + _LOGGER.warning( + f"Found client configuration: name={client_name},siface={client_server_interface},shost={client_server_host}") + + config = retrieve_client_conf_from_server( + client_name=client_name, + server_interface=client_server_interface, + server_host=client_server_host, + server_api_key=client_api_key + ) + + create_client_config(sess, configuration=config, client_name=client_name) + + i += 1 + + +if __name__ == "__main__": + os.environ["CLIENT_1_NAME"] = "client-1" + os.environ["CLIENT_1_SERVER_INTERFACE"] = "wg0" + os.environ["CLIENT_1_SERVER_HOST"] = "http://localhost:4200" + os.environ["CLIENT_1_API_KEY"] = "8bae20143fb962930614952d80634822361fd5ab9488053866a56de5881f9d7b" + os.environ["CLIENT_2_NAME"] = "client-2" + os.environ["CLIENT_2_SERVER_INTERFACE"] = "wg0" + os.environ["CLIENT_2_SERVER_HOST"] = "http://localhost:4200" + os.environ["CLIENT_2_API_KEY"] = "8bae20143fb962930614952d80634822361fd5ab9488053866a56de5881f9d7b" + sess: Session = SessionLocal() + load_environment_clients(sess) + sess.close() diff --git a/wg_dashboard_frontend/src/app/interfaces/peer.ts b/wg_dashboard_frontend/src/app/interfaces/peer.ts index f8ca8aa..c96cb1d 100644 --- a/wg_dashboard_frontend/src/app/interfaces/peer.ts +++ b/wg_dashboard_frontend/src/app/interfaces/peer.ts @@ -1,6 +1,7 @@ export interface Peer { _stats: any; address: string; + v6_address: string; public_key: string; private_key: string; shared_key: string; diff --git a/wg_dashboard_frontend/src/app/interfaces/server.ts b/wg_dashboard_frontend/src/app/interfaces/server.ts index dd8a801..2105223 100644 --- a/wg_dashboard_frontend/src/app/interfaces/server.ts +++ b/wg_dashboard_frontend/src/app/interfaces/server.ts @@ -14,5 +14,6 @@ export interface Server { post_down: string; configuration: string; subnet: number; + read_only: number; peers: Peer[]; } diff --git a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts index 77b5ac7..0227145 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts +++ b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts @@ -64,6 +64,7 @@ export class AddServerComponent implements OnInit { public_key: new FormControl('' ), post_up: new FormControl(''), post_down: new FormControl(''), + read_only: new FormControl(''), // Unused on backend configuration: new FormControl(''), diff --git a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html index b6f3843..db8b7a0 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html +++ b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html @@ -1,5 +1,5 @@ - + @@ -15,6 +15,7 @@ - Endpoint: {{server.endpoint}}:{{server.listen_port}} - Address: {{server.address}}/{{server.subnet}} + Endpoint: {{server.endpoint}}:{{server.listen_port}} - Address: {{server.address}}/{{server.subnet}} + + + + Address: {{server.peers[0].address}}, {{server.peers[0].v6_address}} +
DNS: {{server.peers[0].dns}} +
Allowed-IP's {{server.peers[0].allowed_ips}} +
Endpoint: {{ getEndpointFromConfig(server.peers[0].configuration)}} +
- + - +
@@ -169,3 +180,86 @@ + + + + + + + + + check_circle + {{srv_peer.name}} + + + + + + + + + + + + + + + + + + + + + + + + Address: {{srv_peer.address}}, {{srv_peer.v6_address}} +
DNS: {{srv_peer.dns}} +
Allowed-IP's {{srv_peer.allowed_ips}} +
Endpoint: {{ getEndpointFromConfig(srv_peer.configuration)}} +
+
+ + + + + + + +
+ +
diff --git a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss index 9aa55a2..770ec8c 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss +++ b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss @@ -11,3 +11,6 @@ table { font-size: 20px; } +.dashboard-card{ + margin-bottom: 10px; +} diff --git a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts index dffb40c..3581eca 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts +++ b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts @@ -101,4 +101,10 @@ export class ServerComponent implements OnInit { saveAs(content, `${this.server.interface}_${this.server.address}.zip`); }); } + + getEndpointFromConfig(config){ + console.log(config) + let res = config.match("Endpoint = (.*)") // TODO handle whitespace + return res[1] + } } diff --git a/wg_dashboard_frontend/src/app/services/config.service.ts b/wg_dashboard_frontend/src/app/services/config.service.ts index cc87719..5564fe0 100644 --- a/wg_dashboard_frontend/src/app/services/config.service.ts +++ b/wg_dashboard_frontend/src/app/services/config.service.ts @@ -10,7 +10,8 @@ export class ConfigService { public applicationName = 'WireGuard Manager'; - constructor(private notify: NotifierService) { } + constructor(private notify: NotifierService) { + } public handleError(error: HttpErrorResponse) { if (error.error instanceof ErrorEvent) { diff --git a/wg_dashboard_frontend/src/app/services/server.service.ts b/wg_dashboard_frontend/src/app/services/server.service.ts index fafe45a..ea69dc9 100644 --- a/wg_dashboard_frontend/src/app/services/server.service.ts +++ b/wg_dashboard_frontend/src/app/services/server.service.ts @@ -31,15 +31,17 @@ export class ServerService { public serverPerformAction(action: string, item: any): Subscribable { return this.http.post(this.serverURL + '/' + action, item) - .pipe(catchError(this.config.handleError)); + .pipe(catchError(this.config.handleError.bind(this))); } public addPeer(server_interface: any): Subscribable { - return this.http.post(this.peerURL + '/add', server_interface); + return this.http.post(this.peerURL + '/add', server_interface) + .pipe(catchError(this.config.handleError.bind(this))); } public editPeer(peer: Peer): Subscribable { - return this.http.post(this.peerURL + '/edit', peer); + return this.http.post(this.peerURL + '/edit', peer) + .pipe(catchError(this.config.handleError.bind(this))); } public getServers(): Observable { @@ -53,7 +55,7 @@ export class ServerService { } public startServer(item: Server): Subscribable { - return this.serverPerformAction('start', item); + return this.serverPerformAction('start', item) } public stopServer(item: Server): Subscribable { @@ -76,37 +78,45 @@ export class ServerService { } public getKeyPair() { - return this.http.get(this.wgURL + '/generate_keypair'); + return this.http.get(this.wgURL + '/generate_keypair') + .pipe(catchError(this.config.handleError.bind(this))); } public getPSK() { - return this.http.get(this.wgURL + '/generate_psk'); + return this.http.get(this.wgURL + '/generate_psk') + .pipe(catchError(this.config.handleError.bind(this))); } public peerConfig(peer: Peer) { - return this.http.post(this.peerURL + '/config', peer); + return this.http.post(this.peerURL + '/config', peer) + .pipe(catchError(this.config.handleError.bind(this))); } public serverConfig(server: Server): Subscribable { - return this.http.get(this.serverURL + '/config/' + server.id.toString()); + return this.http.get(this.serverURL + '/config/' + server.id.toString()) + .pipe(catchError(this.config.handleError.bind(this))); } public serverStats(server: Server) { - return this.http.post(this.serverURL + '/stats', server); + return this.http.post(this.serverURL + '/stats', server) + .pipe(catchError(this.config.handleError.bind(this))); } public addAPIKey() { - return this.http.get(this.apiKeyURL + '/add'); + return this.http.get(this.apiKeyURL + '/add') + .pipe(catchError(this.config.handleError.bind(this))); } public getAPIKeys() { - return this.http.get(this.apiKeyURL + '/list'); + return this.http.get(this.apiKeyURL + '/list') + .pipe(catchError(this.config.handleError.bind(this))); } public deleteAPIKey(api_key_id: { id: number }) { return this.http.post(this.apiKeyURL + '/delete', { key_id: api_key_id - }); + }) + .pipe(catchError(this.config.handleError.bind(this))); } } From a2a5aca1bcf3b3518862a71d66c4ba9bb98b5539 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Tue, 21 Jul 2020 02:29:27 +0200 Subject: [PATCH 24/29] * docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8869786..88b0001 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ The features of wg-manager includes: **General** * Modify Admin User +* Create and manage API-Keys # Dependencies * Linux >= 5.6 *(Alternatively: wireguard-dkms)* From be19d96bdc5477b7c174481f996c4e67f04d2a36 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 27 Jul 2020 21:53:39 +0200 Subject: [PATCH 25/29] * Added missing deps. --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 28ab4b9..4a70b02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,12 +14,12 @@ WORKDIR /app RUN apk add --no-cache --update wireguard-tools py3-gunicorn python3 py3-pip ip6tables COPY wg_dashboard_backend /app - +ENV LIBRARY_PATH=/lib:/usr/lib # Install dependencies -RUN apk add --no-cache build-base python3-dev libffi-dev && \ +RUN apk add --no-cache build-base python3-dev libffi-dev jpeg-dev zlib-dev && \ pip3 install uvicorn && \ pip3 install -r requirements.txt && \ -apk del build-base python3-dev libffi-dev +apk del build-base python3-dev libffi-dev jpeg-dev zlib-dev # Copy startup scripts COPY docker/ ./startup From ca376269762483b835605b8685c640b0d108b423 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 27 Jul 2020 22:18:19 +0200 Subject: [PATCH 26/29] * ADMIN_USERNAME is now used when setting initial API Key --- docker/push.sh | 5 +++-- wg_dashboard_backend/db/api_key.py | 4 ++-- wg_dashboard_backend/main.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docker/push.sh b/docker/push.sh index 589d2c0..91bd4e6 100644 --- a/docker/push.sh +++ b/docker/push.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash +cd .. docker login -docker build -t perara/wg-manager . -docker push perara/wg-manager +docker build -t perara/wg-manager:dev . +docker push perara/wg-manager:dev diff --git a/wg_dashboard_backend/db/api_key.py b/wg_dashboard_backend/db/api_key.py index 898a4dc..6d16105 100644 --- a/wg_dashboard_backend/db/api_key.py +++ b/wg_dashboard_backend/db/api_key.py @@ -3,10 +3,10 @@ from sqlalchemy.orm import Session import models -def add_initial_api_key_for_admin(sess: Session, api_key): +def add_initial_api_key_for_admin(sess: Session, api_key, ADMIN_USERNAME): db_user = sess.query(models.User)\ - .filter_by(username="admin")\ + .filter_by(username=ADMIN_USERNAME)\ .one() exists_api_key = sess.query(models.UserAPIKey)\ diff --git a/wg_dashboard_backend/main.py b/wg_dashboard_backend/main.py index f9d9a59..f2fe03a 100644 --- a/wg_dashboard_backend/main.py +++ b/wg_dashboard_backend/main.py @@ -89,7 +89,7 @@ if const.SERVER_INIT_INTERFACE is not None: db.wireguard.server_add_on_init(_db) if const.SERVER_STARTUP_API_KEY is not None: - db.api_key.add_initial_api_key_for_admin(_db, const.SERVER_STARTUP_API_KEY) + db.api_key.add_initial_api_key_for_admin(_db, const.SERVER_STARTUP_API_KEY, ADMIN_USERNAME=ADMIN_USERNAME) _db.close() From fd44c7c6f8c38ba980ed049afb44a47f8b62edbc Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 27 Jul 2020 22:40:47 +0200 Subject: [PATCH 27/29] * Updated readme for when ipv6 fails to start. --- README.md | 4 ++++ wg_dashboard_backend/main.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 88b0001..1cdcb15 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,10 @@ networks: | CLIENT_X_SERVER_INTERFACE | The wg-interface to create client on e.g"wg0". See docs | string | | CLIENT_X_API_KEY | A valid API-Key that is active on the server. Works well with SERVER_STARTUP_API_KEY | string | +# Q&A +I'm trying to start the device but recieve the message: `Perhaps ip6tables or your kernel needs to be upgraded.`. + Try: `modprobe ip6table_nat` on the host. + # Showcase ![Illustration](docs/images/0.png) diff --git a/wg_dashboard_backend/main.py b/wg_dashboard_backend/main.py index f2fe03a..65cf0a1 100644 --- a/wg_dashboard_backend/main.py +++ b/wg_dashboard_backend/main.py @@ -89,7 +89,8 @@ if const.SERVER_INIT_INTERFACE is not None: db.wireguard.server_add_on_init(_db) if const.SERVER_STARTUP_API_KEY is not None: - db.api_key.add_initial_api_key_for_admin(_db, const.SERVER_STARTUP_API_KEY, ADMIN_USERNAME=ADMIN_USERNAME) + ADMIN_USERNAME = os.getenv("ADMIN_USERNAME") + db.api_key.add_initial_api_key_for_admin(_db, const.SERVER_STARTUP_API_KEY, ADMIN_USERNAME) _db.close() From 4d8d30c76a800d7fefa9cf9e277f7d2a2511c685 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 27 Jul 2020 23:36:47 +0200 Subject: [PATCH 28/29] * Allow custom client routes. --- back/test.py | 16 ++++++++++++++++ docker-compose.yaml | 1 + wg_dashboard_backend/script/wireguard.py | 7 ++++--- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 back/test.py diff --git a/back/test.py b/back/test.py new file mode 100644 index 0000000..870ad27 --- /dev/null +++ b/back/test.py @@ -0,0 +1,16 @@ +import requests + +if __name__ == "__main__": + sess = requests.Session() + + resp = sess.post("http://localhost:8888/api/v1/login", data={ + "username": "admin", + "password": "admin" + }) + print(resp.json()) + sess.headers.update({ + "Authorization": f"Bearer {resp.json()['access_token']}" + }) + + for _ in range(20): + print(sess.get("http://localhost:8888/api/v1/wg/generate_psk").json()) diff --git a/docker-compose.yaml b/docker-compose.yaml index 10da013..0810aae 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -50,6 +50,7 @@ services: CLIENT: 1 # If you want to connect to servers CLIENT_START_AUTOMATICALLY: 1 # If you want the client to start automatically CLIENT_1_NAME: "client-1" # Name of first client + CLIENT_1_ROUTES: "10.0.200.0/24" CLIENT_1_SERVER_HOST: "http://server:8888" # Endpoint of first server CLIENT_1_SERVER_INTERFACE: "wg0" # Interface of first server (to get config) CLIENT_1_API_KEY: "thisisasecretkeythatnobodyknows" # API-Key of first server (to get config) diff --git a/wg_dashboard_backend/script/wireguard.py b/wg_dashboard_backend/script/wireguard.py index e252bb6..d65f5d9 100644 --- a/wg_dashboard_backend/script/wireguard.py +++ b/wg_dashboard_backend/script/wireguard.py @@ -259,7 +259,7 @@ def retrieve_client_conf_from_server( return response.text -def create_client_config(sess: Session, configuration, client_name): +def create_client_config(sess: Session, configuration, client_name, client_routes): parser = configparser.ConfigParser() parser.read_string(configuration) @@ -332,7 +332,7 @@ def create_client_config(sess: Session, configuration, client_name): db_peer.private_key = parser["Interface"]["PrivateKey"] db_peer.public_key = "N/A" - db_peer.allowed_ips = parser["Peer"]["AllowedIPs"] + db_peer.allowed_ips = client_routes if client_routes else parser["Peer"]["AllowedIPs"] db_peer.configuration = configuration db_server.interface = f"client_{db_peer.name}" db_server.configuration = configuration @@ -368,6 +368,7 @@ def load_environment_clients(sess: Session): client_server_interface = os.getenv(f"CLIENT_{i}_SERVER_INTERFACE", None) client_server_host = os.getenv(f"CLIENT_{i}_SERVER_HOST", None) client_api_key = os.getenv(f"CLIENT_{i}_API_KEY", None) + client_routes = os.getenv(f"CLIENT_{i}_ROUTES", None) if client_api_key is None or \ client_server_interface is None or \ @@ -385,7 +386,7 @@ def load_environment_clients(sess: Session): server_api_key=client_api_key ) - create_client_config(sess, configuration=config, client_name=client_name) + create_client_config(sess, configuration=config, client_name=client_name, client_routes=client_routes) i += 1 From 122a1f2c9ca8f9749b4b33a21d77ea7ef7a5992b Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Fri, 4 Sep 2020 07:08:43 +0200 Subject: [PATCH 29/29] Create travis file for automated multi-arch builds --- .travis.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..adfbce1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +dist: xenial +sudo: required +env: + global: + - DOCKER_REPO=perara/wg-manager +before_install: + - curl -fsSL https://get.docker.com | sh + - echo '{"experimental":"enabled"}' | sudo tee /etc/docker/daemon.json + - mkdir -p $HOME/.docker + - echo '{"experimental":"enabled"}' | sudo tee $HOME/.docker/config.json + - sudo service docker start +install: + - docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + - docker buildx create --name xbuilder --use +script: bash ci.sh
Name