pull/1616/merge
Ivan Gromov 2 months ago committed by GitHub
commit ccd243bbfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,6 @@
*
!requirements.txt
!playbook.py
!server.py
!run.sh
!static

@ -0,0 +1,10 @@
FROM algo
ARG BUILD_PACKAGES="gcc libffi-dev linux-headers make musl-dev openssl-dev"
COPY requirements.txt /app/requirements.txt
RUN apk --no-cache add ${BUILD_PACKAGES} && \
source .env/bin/activate && \
python3 -m pip --no-cache-dir install -r app/requirements.txt && \
apk del ${BUILD_PACKAGES}
COPY . app
RUN chmod 0755 /algo/app/run.sh
CMD [ "/algo/app/run.sh" ]

@ -0,0 +1,39 @@
# Algo web app UI
## Abstract
[A short description of what project does]
## Background
VUE docs,
asyncio docs
## Rationale
Why A not B
(vue is modern and doesn't require build system and depoendency)
asyncio - same
PBEX is patched because shell access considered insecure
## Implementation
app/server.py management, threading
config yaml writer
generic approach to provider UI (set required fields, validation, inherit ENV, try to detect)
how progress displayed
## testing
testing js: vue-test-library + loader
testing python: pytests
testing pbex compatibility: demo yaml
## Compatibility (if applicable)
[A discussion of the change with regard to the compatibility.]
Due to ansible doesn't have API, have to manually check if custom PBEX would still work
## Open issues (if applicable)
Still requires pip install, consider py2exe, pyinstaller
No task progress displayed, require callback module

@ -0,0 +1,3 @@
aiohttp==3.8.1
ansible-runner==2.2.1
boto3

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -e
ALGO_DIR="/algo"
DATA_DIR="/data"
if [ -z ${VIRTUAL_ENV+x} ]
then
ACTIVATE_SCRIPT="/algo/.env/bin/activate"
if [ -f "$ACTIVATE_SCRIPT" ]
then
# shellcheck source=/dev/null
source "$ACTIVATE_SCRIPT"
else
echo "$ACTIVATE_SCRIPT not found. Did you follow documentation to install dependencies?"
exit 1
fi
fi
tr -d '\r' < "${DATA_DIR}"/config.cfg > "${ALGO_DIR}"/config.cfg
test -d "${DATA_DIR}"/configs && rsync -qLktr --delete "${DATA_DIR}"/configs "${ALGO_DIR}"/
python app/server.py
rsync -qLktr --delete "${ALGO_DIR}"/configs "${DATA_DIR}"/
exit ${retcode}

@ -0,0 +1,462 @@
import configparser
import json
import os
import sys
from os.path import join, dirname, expanduser
from functools import reduce
import ansible_runner
import yaml
from aiohttp import web, ClientSession
try:
import boto3
HAS_BOTO3 = True
except ImportError:
HAS_BOTO3 = False
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
try:
from google.auth.transport.requests import AuthorizedSession
from google.oauth2 import service_account
HAS_GOOGLE_LIBRARIES = True
except ImportError:
HAS_GOOGLE_LIBRARIES = False
try:
from azure.mgmt.automation import AutomationClient
import azure.mgmt.automation.models as AutomationModel
HAS_AZURE_LIBRARIES = True
except ImportError:
HAS_AZURE_LIBRARIES = False
try:
from cs import AIOCloudStack, CloudStackApiException
HAS_CS_LIBRARIES = True
except ImportError:
HAS_CS_LIBRARIES = False
routes = web.RouteTableDef()
PROJECT_ROOT = dirname(dirname(__file__))
pool = None
task_future = None
task_program = ''
class Status:
RUNNING = 'running'
ERROR = 'failed'
TIMEOUT = 'timeout'
CANCELLED = 'canceled'
DONE = 'successful'
NEW = None
def by_path(data: dict, path: str):
def get(obj, attr):
if type(obj) is dict:
return obj.get(attr, None)
elif type(obj) is list:
try:
return obj[int(attr)]
except ValueError:
return None
else:
return None
return reduce(get, path.split('.'), data)
class Playbook:
def __init__(self):
self.status = Status.NEW
self.want_cancel = False
self.events = []
self.config_vars = {}
self._runner = None
def parse(self, event: dict):
data = {}
if by_path(event, 'event_data.task') == 'Set subjectAltName as a fact':
ansible_ssh_host = by_path(event, 'event_data.res.ansible_facts.IP_subject_alt_name')
if ansible_ssh_host:
data['ansible_ssh_host'] = ansible_ssh_host
if by_path(event, 'event_data.play') == 'Configure the server and install required software':
local_service_ip = by_path(event, 'event_data.res.ansible_facts.ansible_lo.ipv4_secondaries.0.address')
ipv6 = by_path(event, 'event_data.res.ansible_facts.ansible_lo.ipv6.0.address')
p12_export_password = by_path(event, 'event_data.res.ansible_facts.p12_export_password')
if local_service_ip:
data['local_service_ip'] = local_service_ip
if ipv6:
data['ipv6'] = ipv6
if p12_export_password:
data['p12_export_password'] = p12_export_password
if by_path(event, 'event_data.play') == 'Provision the server':
host_name = by_path(event, 'event_data.res.add_host.host_name')
if host_name:
data['host_name'] = host_name
return data if data else None
def event_handler(self, data: dict) -> None:
if self.parse(data):
self.config_vars.update(self.parse(data))
self.events.append(data)
def status_handler(self, status_data: dict, *args, **kwargs) -> None:
self.status = status_data.get('status')
def cancel_handler(self) -> bool:
if self.want_cancel:
self.status = Status.CANCELLED
return self.want_cancel
def cancel(self) -> None:
self.want_cancel = True
def run(self, extra_vars: dict) -> None:
self.want_cancel = False
self.status = Status.RUNNING
_, runner = ansible_runner.run_async(
private_data_dir='.',
playbook='main.yml',
extravars=extra_vars,
status_handler=self.status_handler,
cancel_callback=self.cancel_handler,
event_handler=self.event_handler
)
self._runner = runner
playbook = Playbook()
def run_playbook(data: dict):
return playbook.run(data)
@routes.get('/')
async def handle_index(_):
with open(join(PROJECT_ROOT, 'app', 'static', 'index.html'), 'r') as f:
return web.Response(body=f.read(), content_type='text/html')
@routes.get('/playbook')
async def playbook_get_handler(_):
return web.json_response({
'status': playbook.status,
'result': playbook.config_vars if playbook.status == Status.DONE else {},
'events': playbook.events,
})
@routes.post('/playbook')
async def playbook_post_handler(request):
data = await request.json()
run_playbook(data)
return web.json_response({'ok': True})
@routes.delete('/playbook')
async def playbook_delete_handler(_):
playbook.cancel()
return web.json_response({'ok': True})
@routes.get('/config')
async def get_config(_):
with open(join(PROJECT_ROOT, 'config.cfg'), 'r') as f:
config = yaml.safe_load(f.read())
return web.json_response(config)
@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('/exit')
async def post_exit(_):
if task_future and task_future.done():
sys.exit(0)
else:
sys.exit(1)
@routes.get('/do_config')
async def check_do_config(_):
return web.json_response({'has_secret': 'DO_API_TOKEN' in os.environ})
@routes.post('/do_regions')
async def do_regions(request):
data = await request.json()
token = data.get('token', os.environ.get('DO_API_TOKEN'))
if not token:
return web.json_response({'error': 'no token provided'}, status=400)
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer {0}'.format(token),
}
async with ClientSession(headers=headers) as session:
async with session.get('https://api.digitalocean.com/v2/regions') as r:
json_body = await r.json()
return web.json_response(json_body, status=r.status)
@routes.get('/aws_config')
async def aws_config(_):
if not HAS_BOTO3:
return web.json_response({'error': 'missing_boto'}, status=400)
return web.json_response(
{'has_secret': 'AWS_ACCESS_KEY_ID' in os.environ and 'AWS_SECRET_ACCESS_KEY' in os.environ})
@routes.post('/lightsail_regions')
async def lightsail_regions(request):
data = await request.json()
client = boto3.client(
'lightsail',
aws_access_key_id=data.get('aws_access_key'),
aws_secret_access_key=data.get('aws_secret_key')
)
response = client.get_regions(
includeAvailabilityZones=False
)
return web.json_response(response)
@routes.post('/ec2_regions')
async def ec2_regions(request):
data = await request.json()
client = boto3.client(
'ec2',
aws_access_key_id=data.get('aws_access_key'),
aws_secret_access_key=data.get('aws_secret_key')
)
response = client.describe_regions()['Regions']
return web.json_response(response)
@routes.get('/gce_config')
async def check_gce_config(_):
if not HAS_REQUESTS:
return web.json_response({'error': 'missing_requests'}, status=400)
if not HAS_GOOGLE_LIBRARIES:
return web.json_response({'error': 'missing_google'}, status=400)
gce_file = join(PROJECT_ROOT, 'configs', 'gce.json')
response = {}
try:
json.loads(open(gce_file, 'r').read())['project_id']
response['status'] = 'ok'
except IOError:
response['status'] = 'not_available'
except ValueError:
response['status'] = 'wrong_format'
return web.json_response(response)
@routes.post('/gce_regions')
async def gce_regions(request):
data = await request.json()
gce_file_name = join(PROJECT_ROOT, 'configs', 'gce.json')
if data.get('project_id'):
# File is missing, save it. We can't get file path from browser :(
with open(gce_file_name, 'w') as f:
f.write(json.dumps(data))
else:
with open(gce_file_name, 'r') as f:
data = json.loads(f.read())
response = AuthorizedSession(
service_account.Credentials.from_service_account_info(
data).with_scopes(
['https://www.googleapis.com/auth/compute'])).get(
'https://www.googleapis.com/compute/v1/projects/{project_id}/regions'.format(
project_id=data['project_id'])
)
return web.json_response(json.loads(response.content))
@routes.get('/vultr_config')
async def check_vultr_config(request):
response = {'has_secret': False}
if 'VULTR_API_CONFIG' in os.environ:
try:
open(os.environ['VULTR_API_CONFIG'], 'r').read()
response['has_secret'] = True
response['saved_to'] = os.environ.get('VULTR_API_CONFIG')
except IOError:
pass
try:
default_path = expanduser(join('~', '.vultr.ini'))
open(default_path, 'r').read()
response['has_secret'] = True
response['saved_to'] = default_path
except IOError:
pass
return web.json_response(response)
@routes.post('/vultr_config')
async def save_vultr_config(request):
data = await request.json()
token = data.get('token')
path = os.environ.get('VULTR_API_CONFIG') or expanduser(join('~', '.vultr.ini'))
with open(path, 'w') as f:
try:
f.write('[default]\nkey = {0}'.format(token))
except IOError:
return web.json_response({'error': 'can not save config file'}, status=400)
return web.json_response({'saved_to': path})
@routes.get('/vultr_regions')
async def vultr_regions(_):
async with ClientSession() as session:
async with session.get('https://api.vultr.com/v1/regions/list') as r:
json_body = await r.json()
return web.json_response(json_body)
@routes.get('/scaleway_config')
async def check_scaleway_config(_):
return web.json_response({"has_secret": 'SCW_TOKEN' in os.environ})
@routes.get('/hetzner_config')
async def check_hetzner_config(_):
return web.json_response({"has_secret": 'HCLOUD_TOKEN' in os.environ})
@routes.post('/hetzner_regions')
async def hetzner_regions(request):
data = await request.json()
token = data.get('token', os.environ.get('HCLOUD_TOKEN'))
if not token:
return web.json_response({'error': 'no token provided'}, status=400)
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer {0}'.format(token),
}
async with ClientSession(headers=headers) as session:
async with session.get('https://api.hetzner.cloud/v1/datacenters') as r:
json_body = await r.json()
return web.json_response(json_body)
@routes.get('/azure_config')
async def azure_config(_):
if not HAS_REQUESTS:
return web.json_response({'error': 'missing_requests'}, status=400)
if not HAS_AZURE_LIBRARIES:
return web.json_response({'error': 'missing_azure'}, status=400)
response = {'status': 'ok'}
return web.json_response(response)
@routes.get('/azure_regions')
async def azure_regions(_):
with open(join(PROJECT_ROOT, 'roles', 'cloud-azure', 'defaults', 'main.yml'), 'r') as f:
regions_json = yaml.safe_load(f.read())
regions = json.loads(regions_json['_azure_regions'])
return web.json_response(regions)
@routes.get('/linode_config')
async def linode_config(_):
return web.json_response({"has_secret": 'LINODE_API_TOKEN' in os.environ})
@routes.get('/linode_regions')
async def linode_regions(_):
async with ClientSession() as session:
async with session.get('https://api.linode.com/v4/regions') as r:
json_body = await r.json()
return web.json_response(json_body)
@routes.get('/cloudstack_config')
async def check_cloudstack_config(_):
if not HAS_REQUESTS:
return web.json_response({'error': 'missing_requests'}, status=400)
if not HAS_CS_LIBRARIES:
return web.json_response({'error': 'missing_cloudstack'}, status=400)
response = {'has_secret': _read_cloudstack_config() is not None}
return web.json_response(response)
def _read_cloudstack_config():
if 'CLOUDSTACK_CONFIG' in os.environ:
try:
return open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
except IOError:
pass
# check default path
default_path = expanduser(join('~', '.cloudstack.ini'))
try:
return open(default_path, 'r').read()
except IOError:
pass
return None
@routes.post('/cloudstack_regions')
async def cloudstack_regions(request):
data = await request.json()
client_config = data.get('token')
config = configparser.ConfigParser()
config.read_string(_read_cloudstack_config() or client_config)
section = config[config.sections()[0]]
client = AIOCloudStack(**section)
try:
zones = await client.listZones(fetch_list=True)
except CloudStackApiException as resp:
return web.json_response({
'cloud_stack_error': resp.error
}, status=400)
# if config was passed from client, save it after successful zone retrieval
if _read_cloudstack_config() is None:
path = os.environ.get('CLOUDSTACK_CONFIG') or expanduser(join('~', '.cloudstack.ini'))
with open(path, 'w') as f:
try:
f.write(client_config)
except IOError as e:
return web.json_response({'error': 'can not save config file'}, status=400)
return web.json_response(zones)
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)

@ -0,0 +1,33 @@
<template>
<div class="row">
<section class="my-3">
<pre class="code"><code>
{{cli_preview}}
</code></pre>
<button v-on:click="$emit('submit')" class="btn btn-primary" type="button">Run!</button>
</section>
</div>
</template>
<script>
module.exports = {
props: ['extra_args'],
computed: {
cli_preview: function() {
let args = "";
for (arg in this.extra_args) {
args += `${arg}=${this.extra_args[arg]} `;
}
return `ansible-playbook main.yml --extra-vars ${args}`;
}
}
}
</script>
<style scoped>
.code {
white-space: normal;
background: black;
color: lightgrey;
padding: 1em;
}
</style>

@ -0,0 +1,186 @@
<!DOCTYPE html>
<html class="h-100" lang="en">
<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"
integrity="sha256-NSuqgY2hCZJUN6hDMFfdxvkexI7+iLxXQbL540RQ/c4=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css"
integrity="sha256-L/W5Wfqfa0sdBNIKN9cG6QA5F2qx4qICmU2VgLruv9Y=" crossorigin="anonymous">
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 300ms ease-in-out;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
.console-item {
display: block;
max-height: 10em;
}
.console-enter-active, .console-leave-active {
transition: all 500ms;
}
.console-leave-to {
opacity: 0;
max-height: 0;
}
.console-enter {
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" 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-failed'">
😢 Set up failed
</h1>
<h1 class="mb-5 text-center" v-if="step === 'status-successful'">
🥳 Congratulations, your Algo server is running!
</h1>
<h1 class="mb-5 text-center" v-if="step === 'status-exit'">
Config files are saved, bye!
</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>
<vpn-setup class="col-md-8 order-md-1"
v-bind:extra_args="extra_args"
v-on:submit="step = 'provider'"></vpn-setup>
</div>
</transition>
<transition name="fade">
<provider-setup v-if="step === 'provider'"
v-bind:extra_args="extra_args"
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 = 'status-running';">
</command-preview>
</transition>
<transition name="fade">
<status-running v-if="step === 'status-running'"
v-on:submit="stop(); step = 'setup';"
v-on:successful="step = 'status-successful'"
v-on:error="step = 'status-failed'"
v-on:cancelled="step = 'setup'">
</status-running>
</transition>
<transition name="fade">
<section v-if="step === 'status-failed'" 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-successful v-if="step === 'status-successful'"
v-on:submit="exit(); step = 'status-exit'" >
</status-successful>
</transition>
</div>
<script>
new window.Vue({
el: '#algo',
data: {
playbook: {},
step: 'setup',
extra_args: {
server_name: 'algo',
ondemand_cellular: false,
ondemand_wifi: false,
dns_adblocking: false,
ssh_tunneling: false,
store_pki: false,
ondemand_wifi_exclude: []
}
},
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-successful': window.httpVueLoader('/static/status-done.vue')
},
created() {
fetch("/playbook")
.then(r => r.json())
.catch(() => {
this.step = 'status-failed';
})
.then(data => {
if (data.status && data.status !== 'cancelled'){
this.step = `status-${data.status}`;
}
});
},
methods: {
start: function() {
fetch("/playbook", {
method: "POST",
body: JSON.stringify(this.extra_args),
headers: {
"Content-Type": "application/json"
}
});
},
stop() {
fetch("/playbook", {
method: "DELETE"
});
},
exit() {
fetch("/exit", {
method: "POST"
});
}
}
})
</script>
</body>
</html>

@ -0,0 +1,96 @@
<template>
<div>
<div v-if="ui_config_error && ui_config_error === 'missing_requests'" class="form-text alert alert-danger" role="alert">
Python module "requests" is missing, please install it to proceed
</div>
<div v-if="ui_config_error && ui_config_error === 'missing_azure'" class="form-text alert alert-danger" role="alert">
Python Azure SDK libraries are missing, please install it to proceed
</div>
<div class="form-text alert alert-info" role="alert">
<strong>Prerequisites:</strong> <a href="https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md"
target="_blank" rel="noopener noreferrer">Install azure-cli</a>
</div>
<region-select v-model="region"
v-bind:options="ui_region_options"
v-bind:loading="ui_loading_check || ui_loading_regions"
v-bind:error="ui_region_error">
</region-select>
<button v-on:click="submit"
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next
</button>
</div>
</template>
<script>
module.exports = {
data: function () {
return {
region: null,
// helper variables
ui_loading_check: false,
ui_config_error: false,
ui_loading_regions: false,
ui_region_error: null,
ui_region_options: []
}
},
computed: {
is_valid() {
return !!this.region;
}
},
created: function () {
this.check_config();
},
methods: {
check_config() {
this.ui_loading_check = true;
fetch("/azure_config")
.then(r => {
if (r.status === 200 || r.status === 400) {
return r.json();
}
throw new Error(r.status);
})
.then(response => {
if (response.status === 'ok') {
this.load_regions();
} else if (response.error) {
this.ui_config_error = response.error;
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
this.ui_loading_regions = true;
this.ui_region_error = null;
fetch("/azure_regions")
.then((r) => {
if (r.status === 200) {
return r.json();
}
throw new Error(r.status);
})
.then((data) => {
this.ui_region_options = data.map(i => ({key: i.name, value: i.displayName}));
})
.catch((err) => {
this.ui_region_error = err;
})
.finally(() => {
this.ui_loading_regions = false;
});
},
submit() {
this.$emit("submit", {
region: this.region
});
}
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>

@ -0,0 +1,118 @@
<template>
<div>
<div v-if="ui_token_from_env">
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
The token was read from the environment variable
</div>
</div>
<div class="form-group" v-else>
<label for="id_do_token">
Enter your API token. The token must have read and write permissions
<a href="https://cloud.digitalocean.com/settings/api/tokens" title="https://cloud.digitalocean.com/settings/api/tokens" class="badge bagde-pill badge-primary" target="_blank" rel="noopener noreferrer">?</a>
</label>
<input
type="text"
class="form-control"
id="id_do_token"
name="do_token"
v-bind:disabled="ui_loading_check"
v-model="do_token"
@blur="load_regions"
/>
</div>
<region-select v-model="region"
v-bind:options="ui_region_options"
v-bind:loading="ui_loading_check || ui_loading_regions"
v-bind:error="ui_region_error">
</region-select>
<button v-on:click="submit"
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next</button>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
do_token: null,
region: null,
// helper variables
ui_loading_check: false,
ui_loading_regions: false,
ui_region_error: null,
ui_token_from_env: false,
ui_region_options: []
}
},
computed: {
is_valid() {
return (this.do_token || this.ui_token_from_env) && this.region;
}
},
created: function() {
this.check_config();
},
methods: {
check_config() {
this.ui_loading_check = true;
return fetch("/do_config")
.then(r => r.json())
.then(response => {
if (response.has_secret) {
this.ui_token_from_env = true;
this.load_regions();
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
if (this.ui_token_from_env || this.do_token) {
this.ui_loading_regions = true;
this.ui_region_error = null;
const payload = this.ui_token_from_env ? {} : {
token: this.do_token
};
fetch("/do_regions", {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then((r) => {
if (r.status === 200) {
return r.json();
}
throw new Error(r.status);
})
.then((data) => {
this.ui_region_options = data.regions.map(i => ({key: i.slug, value: i.name}));
})
.catch((err) => {
this.ui_region_error = err;
})
.finally(() => {
this.ui_loading_regions = false;
});
}
},
submit() {
if (this.ui_token_from_env) {
this.$emit("submit", {
region: this.region
});
} else {
this.$emit("submit", {
do_token: this.do_token,
region: this.region
});
}
}
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>

@ -0,0 +1,128 @@
<template>
<div>
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
The config file was found on your system
</div>
<div v-else class="form-group">
<label>
Enter your cloudstack.ini file contents below, it will be saved to your system.
</label>
<p>Example config file format (clickable):</p>
<pre class="example" v-on:click="cs_config = ui_example_cfg">{{ ui_example_cfg }}</pre>
<textarea v-model="cs_config"
v-bind:disabled="ui_loading_check"
v-on:blur="load_regions"
class="form-control"
rows="5"></textarea>
<div v-if="ui_region_options.length > 0 && !ui_token_from_env" class="form-text alert alert-success" role="alert">
The config file was saved on your system
</div>
</div>
<region-select v-model="region"
v-bind:options="ui_region_options"
v-bind:loading="ui_loading_check || ui_loading_regions"
v-bind:error="ui_region_error">
</region-select>
<button v-on:click="submit"
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next</button>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
cs_config: null,
region: null,
// helper variables
ui_example_cfg: '[exoscale]\n' +
'endpoint = https://api.exoscale.com/compute\n' +
'key = API Key here\n' +
'secret = Secret key here\n' +
'timeout = 30',
ui_loading_check: false,
ui_loading_regions: false,
ui_region_error: null,
ui_token_from_env: false,
ui_region_options: []
}
},
computed: {
is_valid() {
return (this.cs_config || this.ui_token_from_env) && this.region;
}
},
created: function() {
this.check_config();
},
methods: {
check_config() {
this.ui_loading_check = true;
return fetch("/cloudstack_config")
.then(r => r.json())
.then(response => {
if (response.has_secret) {
this.ui_token_from_env = true;
this.load_regions();
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
if (this.ui_token_from_env || this.cs_config) {
this.ui_loading_regions = true;
this.ui_region_error = null;
const payload = this.ui_token_from_env ? {} : {
token: this.cs_config
};
fetch("/cloudstack_regions", {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then((r) => {
if (r.status === 200) {
return r.json();
}
throw new Error(r.status);
})
.then((data) => {
this.ui_region_options = data.map(i => ({key: i.name, value: i.name}));
})
.catch((err) => {
this.ui_region_error = err;
})
.finally(() => {
this.ui_loading_regions = false;
});
}
},
submit() {
if (this.ui_token_from_env) {
this.$emit("submit", {
region: this.region
});
} else {
this.$emit("submit", {
cs_config: this.cs_config,
region: this.region
});
}
}
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>
<style scoped>
.example {
cursor: pointer;
}
</style>

@ -0,0 +1,118 @@
<template>
<div>
<div v-if="ui_token_from_env">
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
The token was read from the environment variable
</div>
</div>
<div class="form-group" v-else>
<label for="id_do_token">
Enter your API token. The token must have read and write permissions
<a href="https://cloud.digitalocean.com/settings/api/tokens" title="https://cloud.digitalocean.com/settings/api/tokens" class="badge bagde-pill badge-primary" target="_blank" rel="noopener noreferrer">?</a>
</label>
<input
type="text"
class="form-control"
id="id_do_token"
name="do_token"
v-bind:disabled="ui_loading_check"
v-model="do_token"
v-on:blur="load_regions"
/>
</div>
<region-select v-model="region"
v-bind:options="ui_region_options"
v-bind:loading="ui_loading_check || ui_loading_regions"
v-bind:error="ui_region_error">
</region-select>
<button v-on:click="submit"
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next</button>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
do_token: null,
region: null,
// helper variables
ui_loading_check: false,
ui_loading_regions: false,
ui_region_error: null,
ui_token_from_env: false,
ui_region_options: []
}
},
computed: {
is_valid() {
return (this.do_token || this.ui_token_from_env) && this.region;
}
},
created: function() {
this.check_config();
},
methods: {
check_config() {
this.ui_loading_check = true;
return fetch("/do_config")
.then(r => r.json())
.then(response => {
if (response.has_secret) {
this.ui_token_from_env = true;
this.load_regions();
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
if (this.ui_token_from_env || this.do_token) {
this.ui_loading_regions = true;
this.ui_region_error = null;
const payload = this.ui_token_from_env ? {} : {
token: this.do_token
};
fetch("/do_regions", {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then((r) => {
if (r.status === 200) {
return r.json();
}
throw new Error(r.status);
})
.then((data) => {
this.ui_region_options = data.regions.map(i => ({key: i.slug, value: i.name}));
})
.catch((err) => {
this.ui_region_error = err;
})
.finally(() => {
this.ui_loading_regions = false;
});
}
},
submit() {
if (this.ui_token_from_env) {
this.$emit("submit", {
region: this.region
});
} else {
this.$emit("submit", {
do_token: this.do_token,
region: this.region
});
}
}
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>

@ -0,0 +1,172 @@
<template>
<div>
<div v-if="ui_config_error && ui_config_error === 'missing_boto'" class="form-text alert alert-danger" role="alert">
Python module "boto3" is missing, please install it to proceed
</div>
<div v-if="ui_env_secrets" class="form-text alert alert-success" role="alert">
AWS credentials were read from the environment variables
</div>
<div v-else>
<div class="form-group">
<label>
Enter your AWS Access Key
<a
href="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html"
title="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html"
target="_blank"
rel="noreferrer noopener"
class="badge badge-pill badge-primary"
>?</a
>
<br />
Note: Make sure to use an IAM user with an acceptable policy attached
(see
<a
href="https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md"
target="_blank"
rel="noreferrer noopener"
>docs</a
>)
</label>
<input
type="text"
class="form-control"
name="aws_access_key"
v-on:blur="load_regions"
v-model="aws_access_key"
/>
</div>
<div class="form-group">
<label
>Enter your AWS Secret Key
<a
href="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html"
title="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html"
target="_blank"
rel="noreferrer noopener"
class="badge bagde-pill badge-primary"
>?</a
></label
>
<input
type="password"
class="form-control"
name="aws_secret_key"
v-on:blur="load_regions"
v-model="aws_secret_key"
/>
</div>
</div>
<region-select v-model="region"
v-bind:options="ui_region_options"
v-bind:loading="ui_loading_check || ui_loading_regions"
v-bind:error="ui_region_error">
</region-select>
<button
class="btn btn-primary"
type="button"
v-on:click="submit"
v-bind:disabled="!is_valid"
>
Next
</button>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
aws_access_key: null,
aws_secret_key: null,
region: null,
// ui helper variables
ui_region_options: [],
ui_env_secrets: null,
ui_loading_check: false,
ui_loading_regions: false,
ui_config_error: null,
ui_region_error: null
};
},
computed: {
is_valid() {
return this.has_secrets && this.region;
},
has_secrets() {
return this.ui_env_secrets || (this.aws_access_key && this.aws_secret_key);
},
},
created: function() {
this.check_config();
},
methods: {
check_config() {
this.ui_loading_check = true;
fetch("/aws_config")
.then(r => {
if (r.status === 200 || r.status === 400) {
return r.json();
}
throw new Error(r.status);
})
.then(response => {
if (response.has_secret) {
this.ui_env_secrets = true;
this.load_regions();
} else if (response.error) {
this.ui_config_error = response.error;
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
if (this.has_secrets && this.ui_region_options.length === 0) {
this.ui_loading_regions = true;
this.ui_region_error = false;
const payload = this.ui_env_secrets ? {} : {
aws_access_key: this.aws_access_key,
aws_secret_key: this.aws_secret_key
}
fetch('/ec2_regions', {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then((r) => {
if (r.status === 200) {
return r.json();
}
throw new Error(r.status);
})
.then(data => {
this.ui_region_options = data.map(i => ({key: i.RegionName, value: i.RegionName}));
})
.catch((err) => {
this.ui_region_error = err;
})
.finally(() => {
this.ui_loading_regions = false;
});
}
},
submit() {
let submit_value = {
region: this.region
}
if (!this.ui_env_secrets) {
submit_value['aws_access_key'] = this.aws_access_key;
submit_value['aws_secret_key'] = this.aws_secret_key;
}
this.$emit('submit', submit_value);
}
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>

@ -0,0 +1,223 @@
<template>
<div>
<div v-if="ui_config_error && ui_config_error === 'missing_google'" class="form-text alert alert-danger" role="alert">
Python module "google-auth" is missing, please install it to proceed
</div>
<div v-if="ui_config_error && ui_config_error === 'missing_requests'" class="form-text alert alert-danger" role="alert">
Python module "requests" is missing, please install it to proceed
</div>
<div
class="form-group dropzone"
v-if="ui_needs_upload"
v-on:dragover.prevent="dragover_handler"
v-on:dragleave.prevent="dragleave_handler"
v-on:drop.prevent="drop_handler"
v-on:click="show_file_select"
v-bind:class="{
'dropzone--can-drop': ui_can_drop,
'dropzone--error': ui_drop_error,
'dropzone--success': ui_drop_success,
}"
>
<strong>Drag your GCE config file or click here</strong>
<p>
File <code>configs/gce.json</code> was not found. Please create it (<a
href="https://github.com/trailofbits/algo/blob/master/docs/cloud-gce.md"
target="_blank"
rel="noopener noreferrer"
>how?</a
>) or upload.
</p>
<p>After upload it <strong>will be saved</strong> in the configs folder.</p>
<div v-if="ui_drop_error" class="alert alert-warning" role="alert">
<strong>Error:</strong> {{ ui_drop_error }}.
</div>
<div v-if="ui_drop_success" class="alert alert-success" role="alert">
<strong>{{ ui_drop_filename }} loaded successfully</strong>
</div>
</div>
<div v-else class="form-text alert alert-success" role="alert">
Google Compute Engine credentials were found in the project
</div>
<input type="file" accept=".json,applciation/json" v-on:change="filechange_handler" />
<region-select v-model="region" v-bind:options="ui_region_options" v-bind:loading="ui_loading_check || ui_loading_regions">
<label v-if="ui_needs_upload">Please select region</label>
<label v-else>Please specify <code>gce.json</code> credentials file to select region</label>
</region-select>
<button
class="btn btn-primary"
type="button"
v-on:click="submit"
v-bind:disabled="!is_valid"
>
Next
</button>
</div>
</template>
<script>
module.exports = {
data: function () {
return {
drop_error: null,
gce_credentials_file: null,
region: null,
// helper variables
ui_can_drop: false,
ui_drop_error: null,
ui_drop_success: null,
ui_drop_filename: null,
ui_config_error: null,
ui_needs_upload: null,
ui_loading_regions: false,
ui_loading_check: false,
ui_region_options: []
};
},
created: function() {
this.check_config();
},
computed: {
is_valid() {
return this.gce_credentials_file && this.region;
}
},
methods: {
show_file_select(e) {
if (e.target.tagName === 'A') {
return;
}
const input = this.$el.querySelector(['input[type=file]']);
const event = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': true
});
input.dispatchEvent(event);
},
dragover_handler(e) {
this.ui_can_drop = true;
this.ui_drop_success = false;
this.ui_drop_error = false;
this.ui_drop_filename = null;
},
dragleave_handler() {
this.ui_can_drop = false;
},
drop_handler(e) {
try {
const droppedFiles = e.dataTransfer.files;
if (droppedFiles.length !== 1) {
this.ui_drop_error = 'Please upload GCE config as single file';
}
this.read_file(droppedFiles[0]);
} catch (e) {
this.ui_drop_error = 'Unhandled error while trying to read GCE config';
}
},
filechange_handler(e) {
if (e.target.files.length) {
this.read_file(e.target.files[0]);
}
},
read_file(file) {
if (file.type !== 'application/json') {
this.ui_drop_error = 'Incorrect file type';
}
const reader = new FileReader();
reader.onload = e => {
let gce_config_content = null;
try {
gce_config_content = JSON.parse(e.target.result);
this.ui_drop_success = true;
this.ui_drop_filename = file.name;
this.gce_credentials_file = 'configs/gce.json';
} catch (e) {
this.ui_drop_error = 'JSON format error';
}
gce_config_content && this.load_regions(gce_config_content);
}
reader.onerror = e => {
this.ui_drop_error = 'Error while reading file';
}
reader.readAsText(file);
},
check_config() {
this.ui_loading_check = true;
fetch("/gce_config")
.then(r => {
if (r.status === 200 || r.status === 400) {
return r.json();
}
throw new Error(r.status);
})
.then(response => {
if (response.status === 'ok') {
this.gce_credentials_file = 'configs/gce.json';
this.load_regions();
this.ui_needs_upload = false;
} else if (response.error) {
this.ui_config_error = response.error;
} else {
this.ui_needs_upload = true;
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions(gce_config_content) {
if (this.gce_credentials_file && this.ui_region_options.length === 0) {
this.ui_loading_regions = true;
fetch("/gce_regions", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: gce_config_content ? JSON.stringify(gce_config_content) : '{}',
})
.then((r) => r.json())
.then((data) => {
this.ui_region_options = data.items.map(i => ({
value: i.name,
key: i.name
}));
})
.finally(() => {
this.ui_loading_regions = false;
});
}
},
submit() {
this.$emit("submit", {
gce_credentials_file: this.gce_credentials_file,
region: this.region,
});
},
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
},
};
</script>
<style scoped>
.dropzone {
padding: 2em;
border: 5px dotted #ccc;
cursor: pointer;
}
input[type=file] {
visibility: hidden;
}
.dropzone--can-drop {
border-color: var(--blue);
}
.dropzone--error {
border-color: var(--red);
}
.dropzone--success {
border-color: var(--green);
}
</style>

@ -0,0 +1,118 @@
<template>
<div>
<div v-if="ui_token_from_env">
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
The token was read from the environment variable
</div>
</div>
<div class="form-group" v-else>
<label for="id_hcloud_token">
Enter your API token. The token must have read and write permissions
<a href="https://github.com/trailofbits/algo/blob/master/docs/cloud-hetzner.md" title="https://github.com/trailofbits/algo/blob/master/docs/cloud-hetzner.md" class="badge bagde-pill badge-primary" target="_blank" rel="noopener noreferrer">?</a>
</label>
<input
type="text"
class="form-control"
id="id_hcloud_token"
name="hcloud_token"
v-bind:disabled="ui_loading_check"
v-model="hcloud_token"
@blur="load_regions"
/>
</div>
<region-select v-model="region"
v-bind:options="ui_region_options"
v-bind:loading="ui_loading_check || ui_loading_regions"
v-bind:error="ui_region_error">
</region-select>
<button v-on:click="submit"
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next</button>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
hcloud_token: null,
region: null,
// helper variables
ui_loading_check: false,
ui_loading_regions: false,
ui_region_error: null,
ui_token_from_env: false,
ui_region_options: []
}
},
computed: {
is_valid() {
return (this.hcloud_config || this.ui_token_from_env) && this.region;
}
},
created: function() {
this.check_config();
},
methods: {
check_config() {
this.ui_loading_check = true;
return fetch("/hetzner_config")
.then(r => r.json())
.then(response => {
if (response.has_secret) {
this.ui_token_from_env = true;
this.load_regions();
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
if (this.ui_token_from_env || this.hcloud_token) {
this.ui_loading_regions = true;
this.ui_region_error = null;
const payload = this.ui_token_from_env ? {} : {
token: this.hcloud_token
};
fetch("/hetzner_regions", {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then((r) => {
if (r.status === 200) {
return r.json();
}
throw new Error(r.status);
})
.then((data) => {
this.ui_region_options = data.datacenters.map(i => ({key: i.location.name, value: i.location.city}));
})
.catch((err) => {
this.ui_region_error = err;
})
.finally(() => {
this.ui_loading_regions = false;
});
}
},
submit() {
if (this.ui_token_from_env) {
this.$emit("submit", {
region: this.region
});
} else {
this.$emit("submit", {
hcloud_token: this.hcloud_token,
region: this.region
});
}
}
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>

@ -0,0 +1,146 @@
<template>
<div>
<div v-if="ui_config_error && ui_config_error === 'missing_boto'" class="form-text alert alert-danger" role="alert">
Python module "boto3" is missing, please install it to proceed
</div>
<div v-if="ui_env_secrets" class="form-text alert alert-success" role="alert">
AWS credentials were read from the environment variables
</div>
<div v-else>
<div class="form-group">
<label>
Enter your AWS Access Key <a href="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html" title="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html" target="_blank" rel="noreferrer noopener" class="badge bagde-pill badge-primary">?</a>
<br>
Note: Make sure to use an IAM user with an acceptable policy attached (see <a
href="https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md" target="_blank" rel="noreferrer noopener" >docs</a>)
</label>
<input
type="text"
class="form-control"
name="aws_access_key"
v-on:blur="load_regions"
v-model="aws_access_key"
/>
</div>
<div class="form-group">
<label>Enter your AWS Secret Key <a
href="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html" title="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html" target="_blank" rel="noreferrer noopener" class="badge bagde-pill badge-primary">?</a></label>
<input
type="password"
class="form-control"
name="aws_secret_key"
v-on:blur="load_regions"
v-model="aws_secret_key">
</div>
</div>
<region-select v-model="region"
v-bind:options="ui_region_options"
v-bind:loading="ui_loading_check || ui_loading_regions"
v-bind:error="ui_region_error">
</region-select>
<button class="btn btn-primary"
type="button"
v-on:click="submit"
v-bind:disabled="!is_valid">
Next
</button>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
aws_access_key: null,
aws_secret_key: null,
region: null,
// ui helpoer variables
ui_region_options: [],
ui_env_secrets: null,
ui_loading_check: false,
ui_loading_regions: false,
ui_config_error: null,
ui_region_error: null
};
},
computed: {
has_secrets() {
return this.ui_env_secrets || (this.aws_access_key && this.aws_secret_key);
},
is_valid() {
return this.has_secrets && this.region;
}
},
created: function() {
this.check_config();
},
methods: {
check_config() {
this.ui_loading_check = true;
fetch("/aws_config")
.then(r => {
if (r.status === 200 || r.status === 400) {
return r.json();
}
throw new Error(r.status);
})
.then(response => {
if (response.has_secret) {
this.ui_env_secrets = true;
this.load_regions();
} else if (response.error) {
this.ui_config_error = response.error;
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
if (this.has_secrets && this.ui_region_options.length === 0) {
this.ui_loading_regions = true;
this.ui_region_error = false;
const payload = this.ui_env_secrets ? {} : {
aws_access_key: this.aws_access_key,
aws_secret_key: this.aws_secret_key
}
fetch('/lightsail_regions', {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then((r) => {
if (r.status === 200) {
return r.json();
}
throw new Error(r.status);
})
.then(data => {
this.ui_region_options = data.regions.map(i => ({key: i.name, value: i.displayName}));
})
.catch((err) => {
this.ui_region_error = err;
})
.finally(() => {
this.ui_loading_regions = false;
});
}
},
submit() {
let submit_value = {
region: this.region
}
if (!this.ui_env_secrets) {
submit_value['aws_access_key'] = this.aws_access_key;
submit_value['aws_secret_key'] = this.aws_secret_key;
}
this.$emit('submit', submit_value);
}
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>

@ -0,0 +1,109 @@
<template>
<div>
<div v-if="ui_token_from_env">
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
The token was read from the environment variable
</div>
</div>
<div class="form-group" v-else>
<label for="id_token">
Enter your API token. The token must have read and write permissions
<a href="https://github.com/trailofbits/algo/blob/master/docs/cloud-linode.md" title="https://github.com/trailofbits/algo/blob/master/docs/cloud-linode.md" class="badge bagde-pill badge-primary" target="_blank" rel="noopener noreferrer">?</a>
</label>
<input
type="text"
class="form-control"
id="id_token"
name="linode_token"
v-bind:disabled="ui_loading_check"
v-model="linode_token"
/>
</div>
<region-select v-model="region"
v-bind:options="ui_region_options"
v-bind:loading="ui_loading_check || ui_loading_regions"
v-bind:error="ui_region_error">
</region-select>
<button v-on:click="submit"
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next</button>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
linode_token: null,
region: null,
// helper variables
ui_loading_check: false,
ui_loading_regions: false,
ui_region_error: null,
ui_token_from_env: false,
ui_region_options: []
}
},
computed: {
is_valid() {
return (this.linode_token || this.ui_token_from_env) && this.region;
}
},
created: function() {
this.check_config();
this.load_regions();
},
methods: {
check_config() {
this.ui_loading_check = true;
return fetch("/linode_config")
.then(r => r.json())
.then(response => {
if (response.has_secret) {
this.ui_token_from_env = true;
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
this.ui_loading_regions = true;
this.ui_region_error = null;
const payload = this.ui_token_from_env ? {} : {
token: this.hcloud_token
};
fetch("/linode_regions")
.then((r) => {
if (r.status === 200) {
return r.json();
}
throw new Error(r.status);
})
.then((data) => {
this.ui_region_options = data.data.map(i => ({key: i.id, value: i.id}));
})
.catch((err) => {
this.ui_region_error = err;
})
.finally(() => {
this.ui_loading_regions = false;
});
},
submit() {
if (this.ui_token_from_env) {
this.$emit("submit", {
region: this.region
});
} else {
this.$emit("submit", {
linode_token: this.linode_token,
region: this.region
});
}
}
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>

@ -0,0 +1,68 @@
<template>
<div>
<div class="form-group">
<label for="id_server">
Enter the IP address of your server: (or use localhost for local installation):
</label>
<input
type="text"
class="form-control"
id="id_server"
name="server"
v-model="server"
/>
</div>
<div class="form-group">
<label for="id_ssh_user">
What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost)
</label>
<input
type="text"
class="form-control"
id="id_ssh_user"
name="ssh_user"
v-model="ssh_user"
/>
</div>
<div class="form-group">
<label for="id_endpoint">
Enter the public IP address or domain name of your server: (IMPORTANT! This is used to verify the certificate)
</label>
<input
type="text"
class="form-control"
id="id_endpoint"
name="endpoint"
v-model="endpoint"
/>
</div>
<button v-on:click="submit"
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next</button>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
server: null,
ssh_user: null,
endpoint: null
}
},
computed: {
is_valid() {
return this.server && this.ssh_user && this.endpoint;
}
},
methods: {
submit() {
this.$emit("submit", {
server: this.server,
ssh_user: this.ssh_user,
endpoint: this.endpoint
});
}
}
};
</script>

@ -0,0 +1,19 @@
<template>
<div>
<p>
Download OpenStack credentials from the OpenStack dashboard->Compute->API Access and source it in the shell (eg:
source /tmp/dhc-openrc.sh)
</p>
<button v-on:click="submit" class="btn btn-primary" type="button">Next</button>
</div>
</template>
<script>
module.exports = {
methods: {
submit() {
this.$emit("submit");
}
}
};
</script>

@ -0,0 +1,104 @@
<template>
<div>
<div v-if="ui_env_secrets" class="form-text alert alert-success" role="alert">
Token was read from the environment variable
</div>
<div v-else class="form-group">
<label
>Enter your auth token
<a
href="https://trailofbits.github.io/algo/cloud-scaleway.html"
title="https://trailofbits.github.io/algo/cloud-scaleway.html"
target="_blank"
rel="noreferrer noopener"
class="badge bagde-pill badge-primary"
>?</a
></label
>
<input
type="text"
class="form-control"
name="scaleway_token"
v-bind:disabled="ui_loading_check"
v-model="scaleway_token"
/>
</div>
<div class="form-group">
<region-select v-model="region" v-bind:options="ui_region_options">
</region-select>
</div>
<button
class="btn btn-primary"
type="button"
v-on:click="submit"
v-bind:disabled="!is_valid"
>
Next
</button>
</div>
</template>
<script>
module.exports = {
data: function () {
return {
scaleway_token: null,
region: null,
// helper variables
ui_env_secrets: false,
ui_loading_check: false,
ui_region_options: [
{value: 'Paris 1', key: 'par1'},
{value: 'Amsterdam 1', key: 'ams1'}
]
};
},
created: function() {
this.check_config();
},
computed: {
is_valid() {
return this.region && (this.scaleway_token || this.ui_env_secrets);
}
},
methods: {
check_config() {
this.ui_loading_check = true;
fetch("/scaleway_config")
.then(r => {
if (r.status === 200 || r.status === 400) {
return r.json();
}
throw new Error(r.status);
})
.then(response => {
if (response.has_secret) {
this.ui_env_secrets = true;
} else if (response.error) {
this.ui_config_error = response.error;
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
submit() {
if (this.ui_env_secrets) {
this.$emit("submit", {
region: this.region
});
} else {
this.$emit("submit", {
scaleway_token: this.scaleway_token,
region: this.region
});
}
},
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue")
},
};
</script>

@ -0,0 +1,75 @@
<template>
<div class="row">
<div class="col-4">
<ul class="nav flex-column nav-pills">
<li class="nav-item"
v-for="item in providers_map"
v-bind:key="item.alias">
<a
class="nav-link"
href="#"
v-bind:class="{ active: item.alias === (provider && provider.alias) }"
v-on:click="set_provider(item)"
>{{item.name}}</a>
</li>
</ul>
</div>
<div class="col-8">
<component v-if="provider" v-bind:is="provider.alias" v-on:submit="on_provider_submit"></component>
</div>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
loading: false,
provider: null,
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: "Linode", alias: "linode" },
{ 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"
}
]
}
},
// Warning: Mutable Object to edit parent props
props: ['extra_args'],
methods: {
set_provider(provider) {
this.provider = provider;
this.extra_args.provider = provider.alias;
},
on_provider_submit(extra_args) {
Object.assign(this.extra_args, extra_args);
this.$emit('submit');
}
},
components: {
'digitalocean': window.httpVueLoader('/static/provider-do.vue'),
'lightsail': window.httpVueLoader('/static/provider-lightsail.vue'),
'ec2': window.httpVueLoader('/static/provider-ec2.vue'),
'gce': window.httpVueLoader('/static/provider-gce.vue'),
'vultr': window.httpVueLoader('/static/provider-vultr.vue'),
'scaleway': window.httpVueLoader('/static/provider-scaleway.vue'),
'hetzner': window.httpVueLoader('/static/provider-hetzner.vue'),
'azure': window.httpVueLoader('/static/provider-azure.vue'),
'linode': window.httpVueLoader('/static/provider-linode.vue'),
'cloudstack': window.httpVueLoader('/static/provider-cloudstack.vue'),
'openstack': window.httpVueLoader('/static/provider-openstack.vue'),
'local': window.httpVueLoader('/static/provider-local.vue')
}
};
</script>

@ -0,0 +1,152 @@
<template>
<div>
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
Vultr config file was found in your system
</div>
<div v-else class="form-group">
<label
>Enter Vultr API Token, it will be saved in your system
<a
href="https://trailofbits.github.io/algo/cloud-vultr.html"
title="https://trailofbits.github.io/algo/cloud-vultr.html"
target="_blank"
rel="noreferrer noopener"
class="badge bagde-pill badge-primary"
>?</a
></label
>
<input
type="text"
class="form-control"
name="vultr_token"
v-bind:disabled="ui_loading_check"
v-model="ui_token"
v-on:blur="save_config"
/>
<div v-if="vultr_config" class="form-text alert alert-success" role="alert">
The config file was saved on your system
</div>
</div>
<div class="form-group">
<region-select v-model="region"
v-bind:options="ui_region_options"
v-bind:loading="ui_loading_check || ui_loading_regions">
</region-select>
</div>
<button
class="btn btn-primary"
type="button"
v-on:click="submit"
v-bind:disabled="!is_valid"
>
Next
</button>
</div>
</template>
<script>
module.exports = {
data: function () {
return {
vultr_config: null,
region: null,
// helper variables
ui_token: null,
ui_token_from_env: false,
ui_loading_check: false,
ui_loading_regions: false,
ui_region_options: []
};
},
created: function() {
this.check_config();
this.load_regions();
},
computed: {
has_secrets() {
return this.ui_token_from_env || this.vultr_config;
},
is_valid() {
return this.has_secrets && this.region;
}
},
methods: {
check_config() {
this.ui_loading_check = true;
fetch("/vultr_config")
.then(r => {
if (r.status === 200 || r.status === 400) {
return r.json();
}
throw new Error(r.status);
})
.then(response => {
if (response.has_secret) {
this.ui_token_from_env = true;
this.vultr_config = response.saved_to;
this.load_regions();
} else if (response.error) {
this.ui_config_error = response.error;
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
save_config() {
this.ui_loading_check = true;
fetch("/vultr_config", {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: this.ui_token
})
})
.then(r => {
if (r.status === 200 || r.status === 400) {
return r.json();
}
throw new Error(r.status);
})
.then(response => {
if ('saved_to' in response) {
this.vultr_config = response.saved_to;
} else if (response.error) {
this.ui_config_error = response.error;
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
this.ui_loading_regions = true;
fetch("/vultr_regions")
.then((r) => r.json())
.then((data) => {
this.ui_region_options = Object.keys(data).map(k => ({
value: data[k].name,
key: data[k].DCID
}));
})
.finally(() => {
this.ui_loading_regions = false;
});
},
submit() {
let submit_value = {
region: this.region,
vultr_config: this.vultr_config
}
this.$emit("submit", submit_value);
},
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>

@ -0,0 +1,54 @@
<template>
<div class="form-group">
<label v-if="ui_show_slot"><slot></slot></label>
<label v-if="ui_show_loading">Loading regions...</label>
<label v-if="ui_show_select_prompt"
>What region should the server be located in?</label
>
<select
name="region"
class="form-control"
v-bind:class="{ 'is-invalid': has_error }"
v-bind:value="value"
v-bind:disabled="ui_disabled"
v-on:input="$emit('input', $event.target.value)"
>
<option value disabled>Select region</option>
<option
v-for="(region, i) in options"
v-bind:key="i"
v-bind:value="region.key"
>{{ region.value }}</option
>
</select>
<div v-if="has_error" class="invalid-feedback">
There was an error during fetching regions
</div>
</div>
</template>
<script>
module.exports = {
props: ["value", "options", "loading", "error"],
computed: {
has_options: function() {
return this.options && this.options.length;
},
ui_disabled: function() {
return !this.has_options || this.loading;
},
ui_show_slot: function() {
return !this.loading && !this.has_options
},
ui_show_loading: function() {
return this.loading;
},
ui_show_select_prompt: function() {
return this.has_options;
},
has_error: function() {
return !!this.error;
}
}
};
</script>

@ -0,0 +1,69 @@
<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 v-if="result.local_service_ip">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.ansible_ssh_host">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>
<section>
<h2 class="text-center">Finish setup and save configs</h2>
<p class="text-center">
<button v-on:click="$emit('submit')" class="btn btn-primary btn-lg" type="button">Save &amp; Exit</button>
</p>
</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>

@ -0,0 +1,72 @@
<template>
<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>
<transition-group name="console" tag="div">
<code class="console-item" v-for="(event, i) in last_n_events" v-bind:key="event.counter">{{ event.stdout }}</code>
</transition-group>
</section>
</template>
<script>
module.exports = {
data: function () {
return {
events: []
}
},
computed: {
last_n_events() {
return this.events.filter(ev => (ev.stdout)).slice(-6);
}
},
created() {
const loop = () => {
this.check()
.then(() => {
setTimeout(loop, 5000);
});
};
setTimeout(loop, 5000);
},
methods: {
check: function () {
return fetch("/playbook")
.then(r => r.json())
.catch(() => {
this.$emit('error');
})
.then(data => {
this.events = data.events;
if (data.status && data.status === 'successful') {
this.$emit('successful');
throw new Error();
}
if (data.status && data.status === 'failed') {
this.$emit('error');
throw new Error();
}
if (!data.status || data.status === 'cancelled') {
this.$emit('cancelled');
throw new Error();
}
});
}
}
}
</script>
<style scoped>
code {
display: block;
text-align: left;
}
.stop-button {
color: red;
text-decoration: underline;
}
</style>

@ -0,0 +1,112 @@
<template>
<div>
<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"
v-on: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
v-on:click="add_user"
class="btn btn-outline-primary"
type="button"
id="button-addon2"
>Add</button>
</div>
</div>
</div>
</section>
<div>
<span
v-if="save_config_message"
v-bind:class="{ 'text-success': ok, 'text-danged': !ok }"
>{{save_config_message}}</span>
</div>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
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 = "";
this.save_config();
},
remove_user: function(index) {
this.config.users.splice(index, 1);
this.save_config();
},
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;
});
}
}
};
</script>

@ -0,0 +1,108 @@
<template>
<div>
<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>
<button v-on:click="$emit('submit')" class="btn btn-primary" type="button">Select Cloud Provider</button>
</section>
</div>
</template>
<script>
module.exports = {
// Warning: Mutable Object to edit partent props
props: ['extra_args']
}
</script>
Loading…
Cancel
Save