diff --git a/app/index.html b/app/index.html deleted file mode 100644 index 8e57c24..0000000 --- a/app/index.html +++ /dev/null @@ -1,298 +0,0 @@ - - - - Algo webapp - - - - - - - - -
-
-
-

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 0000000..f179af2 --- /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 291adfc..ec7be53 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}) - 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()) + 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}) - elif msg.type == aiohttp.WSMsgType.ERROR: - print('ws connection closed with exception %s' % ws.exception()) - print('websocket connection closed') - return ws +@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}) + + +@routes.delete('/playbook') +async def playbook_delete_handler(request): + global task_future + if not task_future: + return web.json_response({'ok': False}) + + 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 0000000..c488c05 --- /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 0000000..3477b2b --- /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

+
+ + +
+
+ + +
+
+
+
+
+
+
+ + + + diff --git a/requirements.txt b/requirements.txt index 6e8c9a2..3113c3f 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