From 29a53c5ef662909157093b4c54a62fe67e664bc2 Mon Sep 17 00:00:00 2001 From: Ivan Gromov Date: Sun, 13 Oct 2019 21:28:11 +0500 Subject: [PATCH 01/39] Initial working server + client for modifying config, running commands --- app/index.html | 103 +++++++++++++++++++++++++++++++++++++++++++++++++ app/server.py | 71 ++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 app/index.html create mode 100644 app/server.py diff --git a/app/index.html b/app/index.html new file mode 100644 index 000000000..927a60abe --- /dev/null +++ b/app/index.html @@ -0,0 +1,103 @@ + + + + Algo webapp + + + +
+

1. Set up user list

+ +
+ +
+ + +
+ +
+
+
+ + {{saveConfigMessage}} + +
+ + + + + diff --git a/app/server.py b/app/server.py new file mode 100644 index 000000000..f6660478e --- /dev/null +++ b/app/server.py @@ -0,0 +1,71 @@ +import asyncio +from os.path import join, dirname + +import aiohttp +import yaml +from aiohttp import web + +routes = web.RouteTableDef() +PROJECT_ROOT = dirname(dirname(__file__)) + + +async def handle_index(_): + with open(join(PROJECT_ROOT, 'app', 'index.html'), 'r') as f: + return web.Response(body=f.read(), content_type='text/html') + + +async def websocket_handler(request): + ws = web.WebSocketResponse() + await ws.prepare(request) + + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + if msg.data == 'close': + await ws.close() + else: + p = await asyncio.create_subprocess_shell( + msg.data, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + while True: + line = await p.stdout.readline() + if not line: + break + else: + await ws.send_str(line.decode('ascii').rstrip()) + + elif msg.type == aiohttp.WSMsgType.ERROR: + print('ws connection closed with exception %s' % ws.exception()) + + print('websocket connection closed') + return ws + + +@routes.view("/config") +class UsersView(web.View): + async def get(self): + with open(join(PROJECT_ROOT, 'config.cfg'), 'r') as f: + config = yaml.safe_load(f.read()) + return web.json_response(config) + + async def post(self): + data = await self.request.json() + with open(join(PROJECT_ROOT, 'config.cfg'), 'w') as f: + try: + config = yaml.safe_dump(data) + except Exception as e: + return web.json_response({'error': { + 'code': type(e).__name__, + 'message': e, + }}, status=400) + else: + f.write(config) + return web.json_response({'ok': True}) + + +app = web.Application() +app.router.add_get('/ws', websocket_handler) +app.router.add_get('/', handle_index) +app.router.add_routes(routes) +web.run_app(app) From 7dd4044670b9b4e9f0d831624da223231c640a84 Mon Sep 17 00:00:00 2001 From: Ivan Gromov Date: Tue, 15 Oct 2019 01:35:40 +0500 Subject: [PATCH 02/39] Initial webapp version --- app/index.html | 251 +++++++++++++++++++++++++++++++++++++++++++------ app/server.py | 74 ++++++++++----- 2 files changed, 275 insertions(+), 50 deletions(-) diff --git a/app/index.html b/app/index.html index 927a60abe..8e57c24e5 100644 --- a/app/index.html +++ b/app/index.html @@ -1,38 +1,182 @@ - + Algo webapp + - + + + + -
-

1. Set up user list

-
    -
  • - {{ user }} - -
  • -
-
- -
+ +
+
+
+

Algo VPN Setup

+
+
+

Users

+
+

Set up user list

+
    +
  • + {{ user }} + +
  • +
+
+ +
- -
- -
-
-
- - {{saveConfigMessage}} - -
+ +
+ +
+
+
+ +
+ + {{saveConfigMessage}} +
+
+
+

VPN Options

+
+
+ + +
+ +
+ +
+
+ +
+ +
+ + + + List the names of any trusted Wi-Fi networks where + macOS/iOS + IPsec clients should not use "Connect On Demand" + (e.g., your home network. Comma-separated value, e.g., + HomeNet,OfficeWifi,AlgoWiFi) + +
+ +
+ - +
+ +
+ +
+ +
+ +
+
+
+
+
+
+

Select cloud provider

+
+ +
+
+ +
+
+
+

Digital Ocean Options

+
+ + +
+
+ + +
+
+
+
+
+
+
+
+ +
+
+
- - - - -
-
-
-

Algo VPN Setup

-
-
-

Users

-
-

Set up user list

-
    -
  • - {{ user }} - -
  • -
-
- -
- - -
- -
-
-
-
-
- - {{saveConfigMessage}} -
-
-
-

VPN Options

-
-
- - -
- -
- -
-
- -
- -
- - - - List the names of any trusted Wi-Fi networks where - macOS/iOS - IPsec clients should not use "Connect On Demand" - (e.g., your home network. Comma-separated value, e.g., - HomeNet,OfficeWifi,AlgoWiFi) - -
- -
- - -
- -
- -
- -
- -
-
-
-
-
-
-

Select cloud provider

-
- -
-
- -
-
-
-

Digital Ocean Options

-
- - -
-
- - -
-
-
-
-
-
-
-
- -
-
-
- - - diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 000000000..f179af209 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1 @@ +aiohttp==3.6.2 \ No newline at end of file diff --git a/app/server.py b/app/server.py index 291adfcd9..ec7be53d2 100644 --- a/app/server.py +++ b/app/server.py @@ -1,48 +1,80 @@ import asyncio -import signal -import sys +import mimetypes + import aiohttp import yaml from os.path import join, dirname from aiohttp import web +from ansible.cli.playbook import PlaybookCLI +from time import sleep +import concurrent.futures + routes = web.RouteTableDef() PROJECT_ROOT = dirname(dirname(__file__)) -jobs = [] - - +pool = None +task_future = None +task_program = '' + +def run_playbook(data={}): + global task_program + extra_vars = ' '.join(['{0}={1}'.format(key, data[key]) for key in data.keys()]) + task_program = ['ansible-playbook', 'main.yml', '--extra-vars', extra_vars] + cli = PlaybookCLI(task_program).run() + return cli + + +@routes.get('/static/{path}') +async def handle_static(request): + filepath = request.match_info['path'] + mimetype = mimetypes.guess_type(filepath) + try: + with open(join(dirname(__file__), 'static', *filepath.split('/')), 'r') as f: + return web.Response(body=f.read(), content_type=mimetype[0]) + except FileNotFoundError: + return web.Response(status=404) + +@routes.get('/') async def handle_index(_): - with open(join(PROJECT_ROOT, 'app', 'index.html'), 'r') as f: + with open(join(PROJECT_ROOT, 'app', 'static', 'index.html'), 'r') as f: return web.Response(body=f.read(), content_type='text/html') -async def websocket_handler(request): - ws = web.WebSocketResponse() - await ws.prepare(request) +@routes.get('/playbook') +async def playbook_get_handler(request): + if not task_future: + return web.json_response({'status': None}) + + if task_future.done(): + return web.json_response({'status': 'done', 'program': task_program, 'result': task_future.result()}) + elif task_future.cancelled(): + return web.json_response({'status': 'cancelled', 'program': task_program}) + else: + return web.json_response({'status': 'running', 'program': task_program}) + + +@routes.post('/playbook') +async def playbook_post_handler(request): + global task_future + global pool + data = await request.json() + loop = asyncio.get_running_loop() + + pool = concurrent.futures.ThreadPoolExecutor() + task_future = loop.run_in_executor(pool, run_playbook, data) + return web.json_response({'ok': True}) - async for msg in ws: - if msg.type == aiohttp.WSMsgType.TEXT: - if msg.data == 'close': - await ws.close() - else: - p = await asyncio.create_subprocess_shell( - msg.data, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - jobs.append(p) - while True: - line = await p.stdout.readline() - if not line: - break - else: - await ws.send_str(line.decode('ascii').rstrip()) - elif msg.type == aiohttp.WSMsgType.ERROR: - print('ws connection closed with exception %s' % ws.exception()) +@routes.delete('/playbook') +async def playbook_delete_handler(request): + global task_future + if not task_future: + return web.json_response({'ok': False}) - print('websocket connection closed') - return ws + cancelled = task_future.cancel() + pool.shutdown(wait=False) + task_future = None + return web.json_response({'ok': cancelled}) @routes.get('/config') @@ -81,21 +113,7 @@ async def get_do_regions(request): json_body = await r.json() return web.json_response(json_body) + app = web.Application() -app.router.add_get('/ws', websocket_handler) -app.router.add_get('/', handle_index) app.router.add_routes(routes) web.run_app(app) - -def signal_handler(sig, frame): - print('Closing child processes') - for p in jobs: - try: - p.terminate() - except: - pass - sys.exit(0) - -signal.signal(signal.SIGINT, signal_handler) -print('Press Ctrl+C to stop') -signal.pause() diff --git a/app/static/app.js b/app/static/app.js new file mode 100644 index 000000000..c488c0570 --- /dev/null +++ b/app/static/app.js @@ -0,0 +1,202 @@ +new Vue({ + el: '#users_app', + data: { + config: {}, + loading: false, + new_user: '', + save_config_message: '' + }, + created: function() { + this.load_config(); + }, + methods: { + add_user: function() { + this.config.users.push(this.new_user); + this.new_user = ''; + }, + remove_user: function(index) { + this.config.users.splice(index, 1); + }, + save_config: function() { + if (this.loading) return; + this.loading = true; + fetch('/config', { + method: 'POST', + body: JSON.stringify(this.config), + headers: { + 'Content-Type': 'application/json' + } + }) + .then(r => r.json()) + .then(result => { + if (result.ok) { + this.ok = true; + this.save_config_message = 'Saved!'; + setTimeout(() => { + this.save_config_message = ''; + }, 1000); + } else { + this.ok = false; + this.save_config_message = 'Not Saved!'; + setTimeout(() => { + this.save_config_message = ''; + }, 1000); + } + }) + .finally(() => { + this.loading = false; + }); + }, + load_config: function() { + this.loading = true; + fetch('/config') + .then(r => r.json()) + .then(config => { + this.config = config; + }) + .finally(() => { + this.loading = false; + }); + } + } +}); + +var vpn_options_extra_args = { + server_name: 'algo', + ondemand_cellular: false, + ondemand_wifi: false, + dns_adblocking: false, + ssh_tunneling: false, + store_pki: false, + ondemand_wifi_exclude: '' +}; + +new Vue({ + el: '#options_app', + data: { + extra_args: vpn_options_extra_args + } +}); + +var provider_extra_args = { + provider: null +}; + +new Vue({ + el: '#provider_app', + data: { + loading: false, + do_regions: [], + extra_args: provider_extra_args, + providers_map: [ + { name: 'DigitalOcean', alias: 'digitalocean' }, + { name: 'Amazon Lightsail', alias: 'lightsail' }, + { name: 'Amazon EC2', alias: 'ec2' }, + { name: 'Microsoft Azure', alias: 'azure' }, + { name: 'Google Compute Engine', alias: 'gce' }, + { name: 'Hetzner Cloud', alias: 'hetzner' }, + { name: 'Vultr', alias: 'vultr' }, + { name: 'Scaleway', alias: 'scaleway' }, + { name: 'OpenStack (DreamCompute optimised)', alias: 'openstack' }, + { name: 'CloudStack (Exoscale optimised)', alias: 'cloudstack' }, + { + name: 'Install to existing Ubuntu 18.04 or 19.04 server (Advanced)', + alias: 'local' + } + ] + }, + methods: { + set_provider(provider) { + this.extra_args.provider = provider; + }, + load_do_regions: function() { + if ( + this.extra_args.provider === 'digitalocean' && + this.extra_args.do_token + ) { + this.loading = true; + fetch('/do/regions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ token: this.extra_args.do_token }) + }) + .then(r => r.json()) + .then(r => { + this.do_regions = r.regions; + }) + .finally(() => { + this.loading = false; + }); + } + } + } +}); + +new Vue({ + el: '#status_app', + data: { + status: null, + program: null, + result: null, + error: null, + // shared data, do not write there + vpn_options_extra_args, + provider_extra_args, + }, + created() { + this.loop = setInterval(this.get_status, 1000); + }, + computed: { + extra_args() { + return Object.assign({}, this.vpn_options_extra_args, this.provider_extra_args); + }, + cli_preview() { + var args = ''; + for (arg in this.extra_args) { + args += `${arg}=${this.extra_args[arg]} `; + } + return `ansible-playbook main.yml --extra-vars ${args}`; + }, + show_backdrop() { + return this.status === 'running'; + } + }, + watch: { + status: function () { + if (this.status === 'done') { + clearInterval(this.loop); + } + } + }, + methods: { + run() { + fetch('/playbook', { + method: 'POST', + body: JSON.stringify(this.extra_args), + headers: { + 'Content-Type': 'application/json' + } + }); + }, + stop() { + fetch('/playbook', { + method: 'DELETE' + }); + }, + get_status() { + fetch('/playbook') + .then(r => r.json()) + .then(status => { + this.status = status.status; + this.program = status.program; + this.result = status.result; + }) + .catch(err => { + alert('Server error'); + clearInterval(this.loop); + }); + } + } +}); diff --git a/app/static/index.html b/app/static/index.html new file mode 100644 index 000000000..3477b2b5f --- /dev/null +++ b/app/static/index.html @@ -0,0 +1,222 @@ + + + + Algo VPN + + + + + + + +
+
+

Algo VPN Setup

+
+
+

Users

+
+

Set up user list

+
    +
  • + {{ user }} + +
  • +
+
+ +
+ + +
+ +
+
+
+
+
+ + {{save_config_message}} +
+
+
+

VPN Options

+
+
+ + +
+ +
+ +
+
+ +
+ +
+ + + + List the names of any trusted Wi-Fi networks where + macOS/iOS + IPsec clients should not use "Connect On Demand" + (e.g., your home network. Comma-separated value, e.g., + HomeNet,OfficeWifi,AlgoWiFi) + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
+
+
+
+

Select cloud provider

+
+
+ +
+
+
+

Digital Ocean Options

+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+ +
+
+
+
{{cli_preview}}
+ +
+
+
{{program.join(' ')}}
+ + +
+
+
{{program.join(' ')}}
+
Done!
+
+
+
+ + + diff --git a/requirements.txt b/requirements.txt index 6e8c9a2af..3113c3f3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,3 @@ ansible==2.9.20 jinja2==2.8 netaddr -PyYAML==5.1.2 -aiodns==2.0.0 -aiohttp==3.6.2 From 28ac1313502784650eeea311487f63e3860d3f95 Mon Sep 17 00:00:00 2001 From: Ivan Gromov Date: Wed, 26 Feb 2020 01:07:03 +0500 Subject: [PATCH 05/39] Added loading indication for DO regions --- app/static/app.js | 6 ++++++ app/static/index.html | 15 ++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/static/app.js b/app/static/app.js index c488c0570..3d5f2ac1e 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -86,6 +86,7 @@ new Vue({ el: '#provider_app', data: { loading: false, + do_region_loading: false, do_regions: [], extra_args: provider_extra_args, providers_map: [ @@ -115,6 +116,7 @@ new Vue({ this.extra_args.do_token ) { this.loading = true; + this.do_region_loading = true; fetch('/do/regions', { method: 'POST', headers: { @@ -128,6 +130,7 @@ new Vue({ }) .finally(() => { this.loading = false; + this.do_region_loading = false; }); } } @@ -161,6 +164,9 @@ new Vue({ }, show_backdrop() { return this.status === 'running'; + }, + is_success() { + return this.result === 0; } }, watch: { diff --git a/app/static/index.html b/app/static/index.html index 3477b2b5f..d517902ed 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -5,8 +5,6 @@ - diff --git a/app/static/index.html b/app/static/index.html index d517902ed..d391bca1b 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -1,227 +1,67 @@ + Algo VPN - - - - + + + + - -
-
-

Algo VPN Setup

-
-
-

Users

-
-

Set up user list

-
    -
  • - {{ user }} - -
  • -
-
- -
- - -
- -
-
-
-
-
- - {{save_config_message}} -
-
-
-

VPN Options

-
-
- - -
- -
- -
-
- -
-
- - - - List the names of any trusted Wi-Fi networks where - macOS/iOS - IPsec clients should not use "Connect On Demand" - (e.g., your home network. Comma-separated value, e.g., - HomeNet,OfficeWifi,AlgoWiFi) - -
- -
- - -
- -
- -
- -
- -
-
-
-
-
-
-

Select cloud provider

-
-
- -
-
-
-

Digital Ocean Options

-
- - -
-
- - - - -
-
-
-
-
-
-
-
-
- -
-
-
-
{{cli_preview}}
- -
-
-
{{program.join(' ')}}
- - -
-
-
{{program.join(' ')}}
-
Done!
-
Failed!
-
+ +
+

{{ title }}

+
+ +
-
- + + + + + - + + \ No newline at end of file diff --git a/app/static/provider-setup.vue b/app/static/provider-setup.vue new file mode 100644 index 000000000..faf5356e0 --- /dev/null +++ b/app/static/provider-setup.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/app/static/user-config.vue b/app/static/user-config.vue new file mode 100644 index 000000000..4e61f3232 --- /dev/null +++ b/app/static/user-config.vue @@ -0,0 +1,112 @@ + + + \ No newline at end of file diff --git a/app/static/vpn-setup.vue b/app/static/vpn-setup.vue new file mode 100644 index 000000000..75a4143ad --- /dev/null +++ b/app/static/vpn-setup.vue @@ -0,0 +1,111 @@ + + + \ No newline at end of file From 34211a0362aa209c635b54c07dad8be81542956d Mon Sep 17 00:00:00 2001 From: Ivan Gromov Date: Fri, 13 Mar 2020 01:10:29 +0500 Subject: [PATCH 08/39] added DO provider module --- app/static/provider-do.vue | 82 +++++++++++++++++++++++++++++++++++ app/static/provider-setup.vue | 10 ++++- 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 app/static/provider-do.vue diff --git a/app/static/provider-do.vue b/app/static/provider-do.vue new file mode 100644 index 000000000..16c5e90fc --- /dev/null +++ b/app/static/provider-do.vue @@ -0,0 +1,82 @@ + + + \ No newline at end of file diff --git a/app/static/provider-setup.vue b/app/static/provider-setup.vue index faf5356e0..31ac68d20 100644 --- a/app/static/provider-setup.vue +++ b/app/static/provider-setup.vue @@ -12,14 +12,14 @@ {{item.name}}
- {{ provider && provider.alias }} +
@@ -51,7 +51,13 @@ module.exports = { methods: { set_provider(provider) { this.provider = provider; + }, + on_provider_submit(extra_args) { + this.$emit('submit', Object.assign({provider: this.provider.alias}, extra_args)); } + }, + components: { + 'digitalocean': window.httpVueLoader('/static/provider-do.vue') } }; \ No newline at end of file From 992b2d60bb24ad05e9382d44dafd7ede1cf125a1 Mon Sep 17 00:00:00 2001 From: Ivan Gromov Date: Thu, 2 Apr 2020 00:17:55 +0500 Subject: [PATCH 09/39] Added full-page steps with transitions --- app/static/index.html | 44 +++++++++++++++++++++++------------ app/static/provider-setup.vue | 12 ++++++++-- app/static/vpn-setup.vue | 4 ++-- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/app/static/index.html b/app/static/index.html index d391bca1b..2de60d1db 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -1,5 +1,5 @@ - + Algo VPN @@ -10,19 +10,34 @@ integrity="sha256-NSuqgY2hCZJUN6hDMFfdxvkexI7+iLxXQbL540RQ/c4=" crossorigin="anonymous"> +

{{ title }}

-
- - -
- - + +
+ + +
+
+ + + +
- \ No newline at end of file + diff --git a/app/static/provider-setup.vue b/app/static/provider-setup.vue index 31ac68d20..22f685679 100644 --- a/app/static/provider-setup.vue +++ b/app/static/provider-setup.vue @@ -1,6 +1,7 @@ @@ -108,4 +108,4 @@ module.exports = { extra_args: Object } } - \ No newline at end of file + From 32a8b08c3ab41873273ecbbb909985177da01f64 Mon Sep 17 00:00:00 2001 From: Ivan Gromov Date: Thu, 2 Apr 2020 01:47:18 +0500 Subject: [PATCH 10/39] Removed do regions request, re-added run and status --- app/server.py | 14 -------- app/static/command-preview.vue | 42 ++++++++++++++++++++++++ app/static/index.html | 47 ++++++++++++++++++++++----- app/static/provider-do.vue | 11 +++---- app/static/provider-setup.vue | 10 ++++-- app/static/status-running.vue | 59 ++++++++++++++++++++++++++++++++++ app/static/vpn-setup.vue | 6 ++-- 7 files changed, 154 insertions(+), 35 deletions(-) create mode 100644 app/static/command-preview.vue create mode 100644 app/static/status-running.vue diff --git a/app/server.py b/app/server.py index 25a4fded9..76b0651ac 100644 --- a/app/server.py +++ b/app/server.py @@ -100,20 +100,6 @@ async def post_config(request): return web.json_response({'ok': True}) -@routes.post('/do/regions') -async def get_do_regions(request): - data = await request.json() - async with aiohttp.ClientSession() as session: - url = 'https://api.digitalocean.com/v2/regions' - headers = { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer {0}'.format(data['token']), - } - async with session.get(url, headers=headers) as r: - json_body = await r.json() - return web.json_response(json_body) - - app = web.Application() app.router.add_routes(routes) web.run_app(app, port=9000) diff --git a/app/static/command-preview.vue b/app/static/command-preview.vue new file mode 100644 index 000000000..2177bbdb6 --- /dev/null +++ b/app/static/command-preview.vue @@ -0,0 +1,42 @@ + + + + diff --git a/app/static/index.html b/app/static/index.html index 2de60d1db..fbd2e54d7 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -28,16 +28,28 @@

{{ title }}

+ v-on:submit="step = 'provider'"> + v-on:submit="step = 'command'" + v-on:back="step = 'setup'"> + + + + + + + + \ No newline at end of file + diff --git a/app/static/provider-setup.vue b/app/static/provider-setup.vue index 22f685679..cde04509e 100644 --- a/app/static/provider-setup.vue +++ b/app/static/provider-setup.vue @@ -1,5 +1,5 @@ From a8ccad9ed4a9eb295ad08af2cf9765acf0e8bc77 Mon Sep 17 00:00:00 2001 From: Ivan Gromov Date: Thu, 29 Oct 2020 23:00:46 +0500 Subject: [PATCH 26/39] Secrets from env for EC2 provider --- app/static/provider-do.vue | 39 +++---- app/static/provider-ec2.vue | 211 +++++++++++++++++++++--------------- 2 files changed, 137 insertions(+), 113 deletions(-) diff --git a/app/static/provider-do.vue b/app/static/provider-do.vue index ef71bd5f1..f34bdbd66 100644 --- a/app/static/provider-do.vue +++ b/app/static/provider-do.vue @@ -1,33 +1,24 @@