Initial webapp version

This commit is contained in:
Ivan Gromov 2019-10-15 01:35:40 +05:00
parent 29a53c5ef6
commit 7dd4044670
2 changed files with 274 additions and 49 deletions

View file

@ -1,38 +1,182 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html class="h-100">
<head> <head>
<title>Algo webapp</title> <title>Algo webapp</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.css"
integrity="sha256-uTIrmf95e6IHlacC0wpDaPS58eWF314UC7OgdrD6AdU=" crossorigin="anonymous"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"
integrity="sha256-chlNFSVx3TdcQ2Xlw7SvnbLAavAQLO0Y/LBiWX04viY=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.js"
integrity="sha256-tDeULIXIGkXbz7dkZ0qcQajBIS22qS8jQ6URaeMoVJs=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.bundle.js"
integrity="sha256-pVreZ67fRaATygHF6T+gQtF1NI700W9kzeAivu6au9U=" crossorigin="anonymous"></script>
</head> </head>
<div class="container" id="app"> <body class="h-100">
<h1>1. Set up user list</h1> <div class="d-flex flex-column h-100" id="app">
<ul class="list-group"> <div class="flex-shrink-0" v-if="formMode">
<li class="list-group-item" <div class="container">
v-for="(user, index) in config.users" <h1 class="mb-5 text-center">Algo VPN Setup</h1>
:key="user" <div class="row">
> <div class="col-md-4 order-md-2 mb-4">
{{ user }} <h2>Users</h2>
<button type="button" class="btn btn-secondary btn-sm float-right" v-on:click="removeUser(index)">Remove</button> <section class="my-3">
</li> <h4>Set up user list</h4>
</ul> <ul class="list-group">
<div class="my-3 form-group"> <li class="list-group-item"
<label for="id_new_user">Add new user</label> v-for="(user, index) in config.users"
<div class="input-group"> :key="user"
>
{{ user }}
<button type="button" class="btn btn-secondary btn-sm float-right"
@click="removeUser(index)">
Remove
</button>
</li>
</ul>
<div class="my-3 form-group">
<label for="id_new_user">Add new user</label>
<div class="input-group">
<input type="text" id="id_new_user" class="form-control" placeholder="username" v-model="newUser"> <input type="text" id="id_new_user" class="form-control" placeholder="username"
<div class="input-group-append"> v-model="newUser">
<button v-on:click="addUser" class="btn btn-outline-primary" type="button" id="button-addon2">Add</button> <div class="input-group-append">
</div> <button @click="addUser" class="btn btn-outline-primary" type="button"
</div> id="button-addon2">
</div> Add
<button v-on:click="save" v-bind:disabled="loading" class="btn btn-primary" type="button">Save</button> </button>
<span v-if="saveConfigMessage" v-bind:class="{ 'text-success': ok, 'text-danged': !ok }">{{saveConfigMessage}}</span> </div>
</div>
</div>
</section>
<div>
<button @click="save" v-bind:disabled="loading" class="btn btn-secondary" type="button">Save
</button>
<span v-if="saveConfigMessage"
v-bind:class="{ 'text-success': ok, 'text-danged': !ok }">{{saveConfigMessage}}</span>
</div>
</div>
<div class="col-md-8 order-md-1">
<h2>VPN Options</h2>
<section class="my-3">
<div class="form-group">
<label>Name the vpn server</label>
<input type="text" class="form-control" placeholder="server name"
v-model="extra_args.server_name"/>
</div>
<label>MacOS/iOS IPsec clients to enable Connect On Demand:</label>
<div class="form-check">
<label title="MacOS/iOS IPsec clients to enable Connect On Demand when connected to cellular
networks?">
<input class="form-check-input" type="checkbox" name="ondemand_cellular"
v-model="extra_args.ondemand_cellular">
when connected to cellular networks
</label>
</div>
<div class="form-check">
<label title="MacOS/iOS IPsec clients to enable Connect On Demand when connected to Wi-Fi?">
<input class="form-check-input" type="checkbox" name="ondemand_wifi"
v-model="extra_args.ondemand_wifi">
when connected to WiFi
</label>
</div>
<div class="form-group">
<label for="id_ondemand_wifi_exclude">Trusted Wi-Fi networks</label>
<input type="text" class="form-control" id="id_ondemand_wifi_exclude"
name="ondemand_wifi_exclude"
placeholder="HomeNet,OfficeWifi,AlgoWiFi"
v-model="extra_args.ondemand_wifi_exclude"/>
<small class="form-text text-muted">
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)
</small>
</div>
<label>Retain the PKI</label>
<div class="form-check">
<label>
<input class="form-check-input" type="checkbox" name="store_pki"
v-model="extra_args.store_pki">
Do you want to retain the keys (PKI)?
<small class="form-text text-muted">required to add users in the future, but less
secure</small>
</label>
</div>
<label>DNS adblocking</label>
<div class="form-check">
<label>
<input class="form-check-input" type="checkbox" name="dns_adblocking"
v-model="extra_args.dns_adblocking">
Enable DNS ad blocking on this VPN server
</label>
</div>
<label>SSH tunneling</label>
<div class="form-check">
<label>
<input class="form-check-input" type="checkbox" name="ssh_tunneling"
v-model="extra_args.ssh_tunneling">
Each user will have their own account for SSH tunneling
</label>
</div>
</section>
</div>
</div>
<hr class="my-3">
<section class="my-3">
<h2>Select cloud provider</h2>
<div class="form-check">
<label>
<input class="form-check-input" type="radio" name="provider" value="digitalocean"
v-model="extra_args.provider">
DigitalOcean
</label>
</div>
<div class="form-check">
<label>
<input class="form-check-input" type="radio" name="provider" value="lightsail"
v-model="extra_args.provider">
Amazon Lightsail
</label>
</div>
</section>
<section class="my-3" v-if="extra_args.provider === 'digitalocean'">
<h4>Digital Ocean Options</h4>
<div class="form-group">
<label for="id_do_token">Enter your API token. The token must have read and write permissions
(https://cloud.digitalocean.com/settings/api/tokens):</label>
<input type="text" class="form-control" id="id_do_token" name="do_token"
v-model="extra_args.do_token"
@blur="load_do_regions"/>
</div>
<div class="form-group">
<label for="id_region">What region should the server be located in?</label>
<select name="region" id="id_region" class="form-control" v-model="extra_args.region">
<option value="" disabled>Select region</option>
<option
v-for="(region, index) in do_regions"
v-bind:value="region.slug">
{{region.name}}
</option>
</select>
</div>
</section>
</div>
</div>
<div v-if="consoleMode" id="terminal">
</div>
<footer class="footer mt-auto py-3" v-if="formMode">
<div class="container text-center">
<button @click="start" v-bind:disabled="loading" class="btn btn-primary btn-lg" type="button">Install
</button>
</div>
</footer>
</div> </div>
<body>
<script> <script>
var ws = new WebSocket("ws://127.0.0.1:8080/ws"); var ws = new WebSocket("ws://127.0.0.1:8080/ws");
window.ws = ws; window.ws = ws;
@ -45,9 +189,21 @@ function init() {
data: { data: {
ok: false, ok: false,
config: {}, config: {},
do_regions: {},
extra_args: {
server_name: 'algo',
ondemand_cellular: false,
ondemand_wifi: false,
dns_adblocking: false,
ssh_tunneling: false,
store_pki: false,
ondemand_wifi_exclude: ''
},
loading: false, loading: false,
newUser: '', newUser: '',
saveConfigMessage: '' saveConfigMessage: '',
formMode: true,
consoleMode: false
}, },
methods: { methods: {
addUser: function () { addUser: function () {
@ -90,11 +246,50 @@ function init() {
}).finally(() => { }).finally(() => {
this.loading = false; this.loading = false;
}); });
},
start: function() {
var args = '';
for (arg in this.extra_args) {
args += `${arg}=${this.extra_args[arg]} `;
}
ws.send(`ansible-playbook main.yml --extra-vars "${args}"`);
this.formMode = false;
var term = new Terminal();
term.open(document.getElementById('terminal'));
ws.onmessage = function(event) {
term.write(event.data + '\r\n');
if (event.data.length > term.cols) {
term.resize(event.data.length, term.rows);
}
};
window.term = term;
this.consoleMode = true;
},
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;
});
}
} }
} }
}); });
app.load(); app.load();
window.app = app; window.app = app;
window.onclose = function() {
if (ws.readyState === ws.OPEN) {
ws.send('close');
}
}
} }

View file

@ -1,12 +1,14 @@
import asyncio import asyncio
from os.path import join, dirname import signal
import sys
import aiohttp import aiohttp
import yaml import yaml
from os.path import join, dirname
from aiohttp import web from aiohttp import web
routes = web.RouteTableDef() routes = web.RouteTableDef()
PROJECT_ROOT = dirname(dirname(__file__)) PROJECT_ROOT = dirname(dirname(__file__))
jobs = []
async def handle_index(_): async def handle_index(_):
@ -28,6 +30,7 @@ async def websocket_handler(request):
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE stderr=asyncio.subprocess.PIPE
) )
jobs.append(p)
while True: while True:
line = await p.stdout.readline() line = await p.stdout.readline()
if not line: if not line:
@ -42,30 +45,57 @@ async def websocket_handler(request):
return ws return ws
@routes.view("/config") @routes.get('/config')
class UsersView(web.View): async def get_config(_):
async def get(self): with open(join(PROJECT_ROOT, 'config.cfg'), 'r') as f:
with open(join(PROJECT_ROOT, 'config.cfg'), 'r') as f: config = yaml.safe_load(f.read())
config = yaml.safe_load(f.read()) return web.json_response(config)
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})
@routes.post('/config')
async def post_config(request):
data = await 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})
@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 = web.Application()
app.router.add_get('/ws', websocket_handler) app.router.add_get('/ws', websocket_handler)
app.router.add_get('/', handle_index) app.router.add_get('/', handle_index)
app.router.add_routes(routes) app.router.add_routes(routes)
web.run_app(app) 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()