App cleanup

This commit is contained in:
Ivan Gromov 2020-04-05 01:49:17 +05:00
parent 2a22234a67
commit 89c2330b80
10 changed files with 168 additions and 533 deletions

View file

@ -1,7 +1,4 @@
import asyncio
import mimetypes
import aiohttp
import yaml
from os.path import join, dirname
from aiohttp import web
@ -20,19 +17,8 @@ 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]
vars = PlaybookCLI(task_program).run()
# TODO: filter only necessary vars
return vars
return PlaybookCLI(task_program).run()
@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(_):
@ -102,4 +88,6 @@ async def post_config(request):
app = web.Application()
app.router.add_routes(routes)
app.add_routes([web.static('/static', join(PROJECT_ROOT, 'app', 'static'))])
app.add_routes([web.static('/results', join(PROJECT_ROOT, 'configs'))])
web.run_app(app, port=9000)

View file

@ -1,433 +0,0 @@
<template>
<div style="overflow: auto">
<div class="container">
<h1 class="mb-5 text-center">Algo VPN Setup</h1>
<div class="row">
<div class="col-md-4 order-md-2 mb-4" id="users_app">
<h2>Users</h2>
<section class="my-3">
<h4>Set up user list</h4>
<ul class="list-group">
<li class="list-group-item" v-for="(user, index) in config.users" :key="user">
{{ user }}
<button
type="button"
class="btn btn-secondary btn-sm float-right"
@click="remove_user(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="new_user"
/>
<div class="input-group-append">
<button
@click="add_user"
class="btn btn-outline-primary"
type="button"
id="button-addon2"
>Add</button>
</div>
</div>
</div>
</section>
<div>
<button
@click="save_config"
v-bind:disabled="loading"
class="btn btn-secondary"
type="button"
>Save</button>
<span
v-if="save_config_message"
v-bind:class="{ 'text-success': ok, 'text-danged': !ok }"
>{{save_config_message}}</span>
</div>
</div>
<div class="col-md-8 order-md-1" id="options_app">
<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" id="provider_app">
<h2>Select cloud provider</h2>
<div class="row">
<div class="col-4">
<ul class="nav flex-column nav-pills">
<li class="nav-item" v-for="provider in providers_map">
<a
class="nav-link"
href="#"
v-bind:class="{ active: provider.alias === extra_args.provider }"
@click="set_provider(provider.alias)"
>{{provider.name}}</a>
</li>
</ul>
</div>
<div class="col-8">
<div 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
v-if="do_regions.length > 0"
for="id_region"
>What region should the server be located in?</label>
<label
v-if="do_regions.length === 0"
for="id_region"
>Please enter API key above to select region</label>
<label v-if="do_region_loading" for="id_region">Loading regions...</label>
<select
name="region"
id="id_region"
class="form-control"
v-model="extra_args.region"
v-bind:disabled="do_region_loading"
>
<option value disabled>Select region</option>
<option
v-for="(region, index) in do_regions"
v-bind:value="region.slug"
>{{region.name}}</option>
</select>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<footer class="footer mt-auto py-3" id="status_app">
<div
class="backdrop d-flex flex-column align-items-center justify-content-center"
v-if="show_backdrop"
>
<span class="spinner-border" role="status" aria-hidden="true"></span>
</div>
<div class="container">
<div v-if="!status || status === 'cancelled'">
<pre class="console">{{cli_preview}}</pre>
<button @click="run" class="btn btn-primary" type="button">Install</button>
</div>
<div v-if="status === 'running'">
<pre class="console">{{program.join(' ')}}</pre>
<button class="btn btn-danger" type="button" @click="stop">Stop</button>
<button class="btn btn-primary" type="button" disabled>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Running...
</button>
</div>
<div v-if="status === 'done'">
<pre class="console">{{program.join(' ')}}</pre>
<div v-if="is_success" class="text-success">Done!</div>
<div v-else class="text-danger">Failed!</div>
</div>
</div>
</footer>
</template>
<script>
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_region_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;
this.do_region_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;
this.do_region_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";
},
is_success() {
return this.result === 0;
}
},
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);
});
}
}
});
</script>
<style scoped>
.console {
background: black;
color: white;
padding: 4px;
border-radius: 2px;
white-space: pre-line;
padding-left: 2em;
position: relative;
}
.console::before {
content: "$";
position: absolute;
left: 1em;
}
.backdrop {
position: fixed;
background: white;
opacity: 0.6;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
pointer-events: none;
}
.footer .container {
position: relative;
z-index: 101;
}
</style>

View file

@ -1,9 +1,5 @@
<template>
<div class="row">
<h2 class="col-12">
<button type="button" class="btn btn-secondary back-button" v-on:click="$emit('back')"><</button>
🧐 Review and Start!
</h2>
<section class="my-3">
<pre class="code"><code>
{{cli_preview}}
@ -28,11 +24,6 @@ module.exports = {
}
</script>
<style scoped>
.back-button {
position: absolute;
border-radius: 50%;
left: -2em;
}
.code {
white-space: normal;
background: black;

View file

@ -4,6 +4,7 @@
<head>
<title>Algo VPN</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="https://cdn.jsdelivr.net/npm/http-vue-loader@1.4.2/src/httpVueLoader.js"
integrity="sha256-aOeVxnlZDaiJOHsqNWVOMNsKdiGxgT8kbLp1p1Rv2sc=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"
@ -17,12 +18,52 @@
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
.back-button {
position: absolute;
border-radius: 50%;
left: 1em;
top: 0.5em;
}
.spin {
animation-name: spin;
animation-duration: 5000ms;
animation-iteration-count: infinite;
animation-delay: 5s;
animation-timing-function: linear;
display: inline-block;
}
@keyframes spin {
from {
transform:rotate(0deg);
}
to {
transform:rotate(360deg);
}
}
</style>
</head>
<body class="d-flex flex-column h-100">
<div class="container" id="algo">
<h1 class="mb-5 text-center">{{ title }}</h1>
<h1 class="mb-5 text-center" v-if="step === 'setup'">Algo VPN Setup</h1>
<h1 class="mb-5 text-center" v-if="step === 'provider'">
<button type="button" class="btn btn-secondary back-button" v-on:click="step = 'setup'"></button>
<span>☁ Cloud Provider Setup</span>
</h1>
<h1 class="mb-5 text-center" v-if="step === 'command'">
<button type="button" class="btn btn-secondary back-button" v-on:click="step = 'provider'"></button>
🧐 Review and Start!
</h1>
<h1 class="mb-5 text-center" v-if="step === 'status-running'">
<span class="spin">🙂</span> Please be patient
</h1>
<h1 class="mb-5 text-center" v-if="step === 'status-error'">
😢 Set up failed
</h1>
<h1 class="mb-5 text-center" v-if="step === 'status-done'">
🥳 Congratulations! Your Algo server is running.
</h1>
</h1>
<transition name="fade">
<div class="row" v-if="step == 'setup'">
<user-config class="col-md-4 order-md-2 mb-4"></user-config>
@ -34,22 +75,33 @@
<transition name="fade">
<provider-setup v-if="step == 'provider'"
v-bind:extra_args="extra_args"
v-on:submit="step = 'command'"
v-on:back="step = 'setup'">
v-on:submit="step = 'command'">
</provider-setup>
</transition>
<transition name="fade">
<command-preview v-if="step == 'command'"
v-bind:extra_args="extra_args"
v-on:submit="start(); step = 'running';"
v-on:back="step = 'provider'">
v-on:submit="start(); step = 'status-running';">
</command-preview>
</transition>
<transition name="fade">
<status-running v-if="step == 'running'"
v-on:submit="stop(); step = 'setup';">
<status-running v-if="step == 'status-running'"
v-on:submit="stop(); step = 'setup';"
v-on:done="step = 'status-done'"
v-on:error="step = 'status-error'"
v-on:cancelled="step = 'setup'">
</status-running>
</transition>
<transition name="fade">
<section v-if="step == 'status-error'" class="text-center">
<p>Now its time to inspect console output</p>
<p>Restart console process to try again</p>
</section>
</transition>
<transition name="fade">
<status-done v-if="step == 'status-done'">
</status-done>
</transition>
</div>
<script>
@ -68,24 +120,23 @@
ondemand_wifi_exclude: []
}
},
computed: {
title() {
return 'Algo VPN Setup';
}
},
components: {
'user-config': window.httpVueLoader('/static/user-config.vue'),
'vpn-setup': window.httpVueLoader('/static/vpn-setup.vue'),
'provider-setup': window.httpVueLoader('/static/provider-setup.vue'),
'command-preview': window.httpVueLoader('/static/command-preview.vue'),
'status-running': window.httpVueLoader('/static/status-running.vue'),
'status-done': window.httpVueLoader('/static/status-done.vue'),
},
created() {
fetch("/playbook")
.then(r => r.json())
.catch(() => {
this.step = 'status-error';
})
.then(data => {
if (data.status === 'running') {
this.step = 'running';
if (data.status && data.status !== 'cancelled'){
this.step = `status-${data.status}`;
}
});
},

View file

@ -39,7 +39,7 @@
>{{region.name}}</option>
</select>
</div>
<button @click="submit" v-bind:disabled="!do_region" class="btn btn-primary" type="button">Next</button>
<button v-on:click="submit" v-bind:disabled="!do_region" class="btn btn-primary" type="button">Next</button>
</div>
</template>

View file

@ -1,10 +1,5 @@
<template>
<div class="row">
<h2 class="col-12">
<button type="button" class="btn btn-secondary back-button" v-on:click="$emit('back')"><</button>
<span v-if="provider">{{ provider.name }} Setup</span>
<span v-else>Select cloud provider</span>
</h2>
<div class="col-4">
<ul class="nav flex-column nav-pills">
<li class="nav-item"
@ -66,10 +61,3 @@ module.exports = {
}
};
</script>
<style scoped>
.back-button {
position: absolute;
border-radius: 50%;
left: -2em;
}
</style>

View file

@ -0,0 +1,63 @@
<template>
<div>
<section>
<p>Config files and certificates are in the ./configs/ directory.</p>
<p>Go to <a href="https://whoer.net/" target="_blank" rel="noopener noopener">https://whoer.net/</a>
after connecting and ensure that all your traffic passes through the VPN.</p>
<p>Local DNS resolver {{result.local_service_ip}}</p>
<p v-if="result.p12_export_password">The p12 and SSH keys password for new users is <code>{{result.p12_export_password}}</code></p>
<p v-if="result.CA_password">The CA key password is <code>{{result.CA_password}}</code></p>
<p v-if="result.ssh_access">Shell access: <code>ssh -F configs/{{result.ansible_ssh_host}}/ssh_config {{config.server_name}}</code></p>
<p>Read more on how to set up clients at the <a href="https://github.com/trailofbits/algo" target="_blank" rel="noopener noopener">Algo home page</a></p>
</section>
<section>
<h2 class="text-center">Client configuration files</h2>
<div v-for="user in config.users" :key="user">
<h3>&#128100; {{user}}</h3>
<div class="d-flex justify-content-between">
<div v-if="config.wireguard_enabled">
<p><strong>WireGuard</strong></p>
<p>
<img v-bind:src="`/results/${result.ansible_ssh_host}/wireguard/${user}.png`" alt="QR Code">
</p>
<p><a v-bind:href="`/results/${result.ansible_ssh_host}/wireguard/${user}.conf`">{{user}}.conf</a></p>
<p>iOS <a v-bind:href="`/results/${result.ansible_ssh_host}/wireguard/apple/ios/${user}.mobileconfig`"> .mobileconfig</a>,
Mac OS <a v-bind:href="`/results/${result.ansible_ssh_host}/wireguard/apple/macos/${user}.mobileconfig`"> .mobileconfig</a></p>
</div>
<div v-if="config.ipsec_enabled">
<p><strong>IPSec</strong></p>
<p>Apple's <a v-bind:href="`/results/${result.ansible_ssh_host}/ipsec/apple/${user}.mobileconfig`"> .mobileconfig</a></p>
<p>Manual configuration:
<a v-bind:href="`/results/${result.ansible_ssh_host}/ipsec/manual/${user}.conf`">{{user}}.conf</a>,
<a v-bind:href="`/results/${result.ansible_ssh_host}/ipsec/manual/${user}.p12`">{{user}}.p12</a>,
<a v-bind:href="`/results/${result.ansible_ssh_host}/ipsec/manual/${user}.secrets`">{{user}}.secrets</a>
</p>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
result: {},
config: { users: [] }
};
},
created() {
fetch("/playbook")
.then(r => r.json())
.then(data => {
this.result = data.result;
});
fetch("/config")
.then(r => r.json())
.then(data => {
this.config = data;
});
}
}
</script>

View file

@ -1,59 +1,47 @@
<template>
<div class="row status-container">
<h2 class="col-12"><span class="spin">🙃</span>Please be patient</h2>
<section class="status-text">
<p>VPN set up usually takes 5-15 minutes</p>
<p>You can close tab and open it again</p>
<p>You can try to <button type="button" class="btn btn-link back-button" v-on:click="$emit('submit')">STOP</button> setup and run it again</p>
<p>Dont close terminal!</p>
</section>
</div>
<section class="text-center">
<p>Set up usually takes 5-15 minutes</p>
<p>You can close tab and open it again</p>
<p>You can try to <button type="button" class="btn btn-link stop-button" v-on:click="$emit('submit')">STOP</button> setup and run it again</p>
<p>Dont close terminal!</p>
</section>
</template>
<script>
module.exports = {
props: function() {
// Warning: Mutable Object to edit partent props
extra_args: Object
created() {
const loop = () => {
this.check()
.then(() => {
setTimeout(loop, 5000);
});
};
setTimeout(loop, 5000);
},
computed: {
cli_preview() {
return 'ansible-playbook main.yml --extra-vars "server_name=algo ondemand_cellular=true ondemand_wifi=true dns_adblocking=false ssh_tunneling=false store_pki=false ondemand_wifi_exclude=[] provider=digitalocean do_token=ad6b4405df208053e7b41e1c9e74b97364af1d6b902f6b6bb1d5575c52eca0c3 region=blr1"';
methods: {
check: function() {
return fetch("/playbook")
.then(r => r.json())
.catch(() => {
this.$emit('error');
})
.then(data => {
if (data.status && data.status === 'done') {
this.$emit('done');
throw new Error();
}
if (!data.status || data.status === 'cancelled') {
this.$emit('cancelled');
throw new Error();
}
});
}
}
}
</script>
<style scoped>
.back-button {
.stop-button {
color: red;
}
.status-container {
display: flex;
flex-direction: column;
}
.status-text {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.spin {
animation-name: spin;
animation-duration: 5000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
display: block;
animation-delay: 5s;
width: 1em;
height: 100%;
}
@keyframes spin {
from {
transform:rotate(0deg);
}
to {
transform:rotate(360deg);
}
text-decoration: underline;
}
</style>

View file

@ -9,7 +9,7 @@
<button
type="button"
class="btn btn-secondary btn-sm float-right"
@click="remove_user(index)"
v-on:click="remove_user(index)"
>Remove</button>
</li>
</ul>
@ -25,7 +25,7 @@
/>
<div class="input-group-append">
<button
@click="add_user"
v-on:click="add_user"
class="btn btn-outline-primary"
type="button"
id="button-addon2"
@ -109,4 +109,4 @@ module.exports = {
}
}
};
</script>
</script>

View file

@ -100,7 +100,6 @@
</section>
</div>
</template>
<script>
module.exports = {
// Warning: Mutable Object to edit partent props