Merge pull request #6 from house-of-vanity/k8s

K8s
This commit is contained in:
Alexandr Bogomyakov
2024-03-18 19:12:05 +02:00
committed by GitHub
24 changed files with 504 additions and 533 deletions

0
.github/workflows/main.yml vendored Normal file → Executable file
View File

3
.gitignore vendored Normal file → Executable file
View File

@ -2,7 +2,8 @@ config.yaml
__pycache__/ __pycache__/
sync.log sync.log
main.py main.py
.vscode/launch.json .idea/*
.vscode/*
*.swp *.swp
*.swo *.swo
*.swn *.swn

3
.idea/.gitignore generated vendored
View File

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

13
.idea/OutlineFleet.iml generated
View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PackageRequirementsSettings">
<option name="versionSpecifier" value="Strong equality (==x.y.z)" />
</component>
</module>

View File

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

10
.idea/misc.xml generated
View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.10 (outfleet)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (outfleet)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/OutlineFleet.iml" filepath="$PROJECT_DIR$/.idea/OutlineFleet.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,6 +0,0 @@
{
"recommendations": [
"tht13.python",
"ms-python.python"
]
}

0
Dockerfile Normal file → Executable file
View File

0
LICENSE Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

0
buildx.yaml Normal file → Executable file
View File

0
img/servers.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

77
k8s.py Executable file
View File

@ -0,0 +1,77 @@
import base64
import json
import yaml
import logging
from kubernetes import client, config
from kubernetes.client.rest import ApiException
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%d-%m-%Y %H:%M:%S",
)
log = logging.getLogger("OutFleet.k8s")
file_handler = logging.FileHandler("sync.log")
file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
file_handler.setFormatter(formatter)
log.addHandler(file_handler)
def write_config(config):
config_map = client.V1ConfigMap(
api_version="v1",
kind="ConfigMap",
metadata=client.V1ObjectMeta(
name=f"config-outfleet",
labels={
"app": "outfleet",
}
),
data={"config.yaml": yaml.dump(config)}
)
try:
api_response = v1.create_namespaced_config_map(
namespace=NAMESPACE,
body=config_map,
)
except ApiException as e:
api_response = v1.patch_namespaced_config_map(
name="config-outfleet",
namespace=NAMESPACE,
body=config_map,
)
config.load_incluster_config()
v1 = client.CoreV1Api()
NAMESPACE = False
SERVERS = list()
CONFIG = None
log.info("Checking for Kubernetes environment")
try:
with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace") as f:
NAMESPACE = f.read().strip()
log.info(f"Found Kubernetes environment. Namespace {NAMESPACE}")
except IOError:
log.info("Kubernetes environment not detected")
pass
# config = v1.list_namespaced_config_map(NAMESPACE, label_selector="app=outfleet").items["data"]["config.yaml"]
try:
CONFIG = yaml.safe_load(v1.read_namespaced_config_map(name="config-outfleet", namespace=NAMESPACE).data['config.yaml'])
log.info(f"ConfigMap config.yaml loaded from Kubernetes API. Servers: {len(CONFIG['servers'])}, Clients: {len(CONFIG['clients'])}")
except ApiException as e:
log.warning(f"ConfigMap not found. Fisrt run?")
#servers = v1.list_namespaced_secret(NAMESPACE, label_selector="app=shadowbox")
if not CONFIG:
log.info(f"Creating new ConfigMap [config-outfleet]")
write_config({"clients": [], "servers": [], "ui_hostname": "accessible-address.com"})
CONFIG = yaml.safe_load(v1.read_namespaced_config_map(name="config-outfleet", namespace=NAMESPACE).data['config.yaml'])

45
lib.py Normal file → Executable file
View File

@ -1,7 +1,10 @@
import argparse
import logging import logging
from typing import TypedDict, List from typing import TypedDict, List
from outline_vpn.outline_vpn import OutlineKey, OutlineVPN from outline_vpn.outline_vpn import OutlineKey, OutlineVPN
import yaml import yaml
import k8s
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
@ -9,6 +12,42 @@ logging.basicConfig(
datefmt="%d-%m-%Y %H:%M:%S", datefmt="%d-%m-%Y %H:%M:%S",
) )
log = logging.getLogger(f'OutFleet.lib')
parser = argparse.ArgumentParser()
parser.add_argument(
"-c",
"--config",
default="/usr/local/etc/outfleet/config.yaml",
help="Config file location",
)
args = parser.parse_args()
def get_config():
if not k8s.NAMESPACE:
try:
with open(args.config, "r") as file:
config = yaml.safe_load(file)
except:
try:
with open(args.config, "w"):
pass
except Exception as exp:
log.error(f"Couldn't create config. {exp}")
return None
return config
else:
return k8s.CONFIG
def write_config(config):
if not k8s.NAMESPACE:
try:
with open(args.config, "w") as file:
yaml.safe_dump(config, file)
except Exception as e:
log.error(f"Couldn't write Outfleet config: {e}")
else:
k8s.write_config(config)
class ServerDict(TypedDict): class ServerDict(TypedDict):
server_id: str server_id: str
@ -114,13 +153,11 @@ class Server:
config.get("hostname_for_access_keys"), config.get("hostname_for_access_keys"),
) )
if config.get("comment"): if config.get("comment"):
with open(CFG_PATH, "r") as file: config_file = get_config()
config_file = yaml.safe_load(file) or {}
config_file["servers"][self.data["local_server_id"]]["comment"] = config.get( config_file["servers"][self.data["local_server_id"]]["comment"] = config.get(
"comment" "comment"
) )
with open(CFG_PATH, "w") as file: write_config(config_file)
yaml.safe_dump(config_file, file)
self.log.info( self.log.info(
"Changed %s comment to '%s'", "Changed %s comment to '%s'",
self.data["local_server_id"], self.data["local_server_id"],

90
main.py Normal file → Executable file
View File

@ -6,9 +6,12 @@ import string
import argparse import argparse
import uuid import uuid
import k8s
from flask import Flask, render_template, request, url_for, redirect from flask import Flask, render_template, request, url_for, redirect
from flask_cors import CORS from flask_cors import CORS
from lib import Server from lib import Server, write_config, get_config, args
logging.getLogger("werkzeug").setLevel(logging.ERROR) logging.getLogger("werkzeug").setLevel(logging.ERROR)
@ -27,17 +30,11 @@ formatter = logging.Formatter(
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
log.addHandler(file_handler) log.addHandler(file_handler)
parser = argparse.ArgumentParser()
parser.add_argument(
"-c",
"--config",
default="/usr/local/etc/outfleet/config.yaml",
help="Config file location",
)
args = parser.parse_args()
CFG_PATH = args.config
CFG_PATH = args.config
NAMESPACE = k8s.NAMESPACE
SERVERS = list() SERVERS = list()
BROKEN_SERVERS = list()
CLIENTS = dict() CLIENTS = dict()
VERSION = '3' VERSION = '3'
HOSTNAME = "" HOSTNAME = ""
@ -55,22 +52,18 @@ def random_string(length=64):
return "".join(random.choice(letters) for i in range(length)) return "".join(random.choice(letters) for i in range(length))
def update_state(): def update_state():
global SERVERS global SERVERS
global CLIENTS global CLIENTS
global BROKEN_SERVERS
global HOSTNAME global HOSTNAME
SERVERS = list() SERVERS = list()
BROKEN_SERVERS = list()
CLIENTS = dict() CLIENTS = dict()
config = dict() config = get_config()
try:
with open(CFG_PATH, "r") as file:
config = yaml.safe_load(file)
except:
try:
with open(CFG_PATH, "w"):
pass
except Exception as exp:
log.error(f"Couldn't create config. {exp}")
if config: if config:
HOSTNAME = config.get("ui_hostname", "my-own-SSL-ENABLED-domain.com") HOSTNAME = config.get("ui_hostname", "my-own-SSL-ENABLED-domain.com")
@ -90,6 +83,11 @@ def update_state():
local_server_id, local_server_id,
) )
except Exception as e: except Exception as e:
BROKEN_SERVERS.append({
"config": server_config,
"error": e,
"id": local_server_id
})
log.warning("Can't access server: %s - %s", server_config["url"], e) log.warning("Can't access server: %s - %s", server_config["url"], e)
CLIENTS = config.get("clients", dict()) CLIENTS = config.get("clients", dict())
@ -98,13 +96,16 @@ def update_state():
@app.route("/", methods=["GET", "POST"]) @app.route("/", methods=["GET", "POST"])
def index(): def index():
if request.method == "GET": if request.method == "GET":
#if request.args.get("broken") == True:
return render_template( return render_template(
"index.html", "index.html",
SERVERS=SERVERS, SERVERS=SERVERS,
VERSION=VERSION, VERSION=VERSION,
BROKEN_SERVERS=BROKEN_SERVERS,
nt=request.args.get("nt"), nt=request.args.get("nt"),
nl=request.args.get("nl"), nl=request.args.get("nl"),
selected_server=request.args.get("selected_server"), selected_server=request.args.get("selected_server"),
broken=request.args.get("broken", False),
add_server=request.args.get("add_server", None), add_server=request.args.get("add_server", None),
format_timestamp=format_timestamp, format_timestamp=format_timestamp,
) )
@ -128,13 +129,6 @@ def index():
@app.route("/clients", methods=["GET", "POST"]) @app.route("/clients", methods=["GET", "POST"])
def clients(): def clients():
# {% for server in SERVERS %}
# {% for key in server.data["keys"] %}
# {% if key.name == client['name'] %}
# ssconf://{{ dynamic_hostname }}/dynamic/{{server.info()['name']}}/{{selected_client}}#{{server.info()['comment']}}
# {% endif %}
# {% endfor %}
# {% endfor %}
if request.method == "GET": if request.method == "GET":
return render_template( return render_template(
"clients.html", "clients.html",
@ -148,22 +142,13 @@ def clients():
format_timestamp=format_timestamp, format_timestamp=format_timestamp,
dynamic_hostname=HOSTNAME, dynamic_hostname=HOSTNAME,
) )
# else:
# server = request.form['server_id']
# server = next((item for item in SERVERS if item.info()["server_id"] == server), None)
# server.apply_config(request.form)
# update_state()
# return redirect(
# url_for('index', nt="Updated Outline VPN Server", selected_server=request.args.get('selected_server')))
@app.route("/add_server", methods=["POST"]) @app.route("/add_server", methods=["POST"])
def add_server(): def add_server():
if request.method == "POST": if request.method == "POST":
try: try:
with open(CFG_PATH, "r") as file: config = get_config()
config = yaml.safe_load(file) or {}
servers = config.get("servers", dict()) servers = config.get("servers", dict())
local_server_id = str(uuid.uuid4()) local_server_id = str(uuid.uuid4())
@ -181,16 +166,7 @@ def add_server():
"cert": request.form["cert"], "cert": request.form["cert"],
} }
config["servers"] = servers config["servers"] = servers
try: write_config(config)
with open(CFG_PATH, "w") as file:
yaml.safe_dump(config, file)
except Exception as e:
return redirect(
url_for(
"index", nt=f"Couldn't write Outfleet config: {e}", nl="error"
)
)
log.info("Added server: %s", new_server.data["name"]) log.info("Added server: %s", new_server.data["name"])
update_state() update_state()
return redirect(url_for("index", nt="Added Outline VPN Server")) return redirect(url_for("index", nt="Added Outline VPN Server"))
@ -204,8 +180,7 @@ def add_server():
@app.route("/del_server", methods=["POST"]) @app.route("/del_server", methods=["POST"])
def del_server(): def del_server():
if request.method == "POST": if request.method == "POST":
with open(CFG_PATH, "r") as file: config = get_config()
config = yaml.safe_load(file) or {}
local_server_id = request.form.get("local_server_id") local_server_id = request.form.get("local_server_id")
server_name = None server_name = None
@ -218,9 +193,7 @@ def del_server():
client_config["servers"].remove(local_server_id) client_config["servers"].remove(local_server_id)
except ValueError as e: except ValueError as e:
pass pass
write_config(config)
with open(CFG_PATH, "w") as file:
yaml.safe_dump(config, file)
log.info("Deleting server %s [%s]", server_name, request.form.get("local_server_id")) log.info("Deleting server %s [%s]", server_name, request.form.get("local_server_id"))
update_state() update_state()
return redirect(url_for("index", nt=f"Server {server_name} has been deleted")) return redirect(url_for("index", nt=f"Server {server_name} has been deleted"))
@ -229,8 +202,7 @@ def del_server():
@app.route("/add_client", methods=["POST"]) @app.route("/add_client", methods=["POST"])
def add_client(): def add_client():
if request.method == "POST": if request.method == "POST":
with open(CFG_PATH, "r") as file: config = get_config()
config = yaml.safe_load(file) or {}
clients = config.get("clients", dict()) clients = config.get("clients", dict())
user_id = request.form.get("user_id", random_string()) user_id = request.form.get("user_id", random_string())
@ -241,8 +213,7 @@ def add_client():
"servers": request.form.getlist("servers"), "servers": request.form.getlist("servers"),
} }
config["clients"] = clients config["clients"] = clients
with open(CFG_PATH, "w") as file: write_config(config)
yaml.safe_dump(config, file)
log.info("Client %s updated", request.form.get("name")) log.info("Client %s updated", request.form.get("name"))
for server in SERVERS: for server in SERVERS:
@ -304,9 +275,7 @@ def add_client():
@app.route("/del_client", methods=["POST"]) @app.route("/del_client", methods=["POST"])
def del_client(): def del_client():
if request.method == "POST": if request.method == "POST":
with open(CFG_PATH, "r") as file: config = get_config()
config = yaml.safe_load(file) or {}
clients = config.get("clients", dict()) clients = config.get("clients", dict())
user_id = request.form.get("user_id") user_id = request.form.get("user_id")
if user_id in clients: if user_id in clients:
@ -323,8 +292,7 @@ def del_client():
server.delete_key(client.key_id) server.delete_key(client.key_id)
config["clients"].pop(user_id) config["clients"].pop(user_id)
with open(CFG_PATH, "w") as file: write_config(config)
yaml.safe_dump(config, file)
log.info("Deleting client %s", request.form.get("name")) log.info("Deleting client %s", request.form.get("name"))
update_state() update_state()
return redirect(url_for("clients", nt="User has been deleted")) return redirect(url_for("clients", nt="User has been deleted"))

1
requirements.txt Normal file → Executable file
View File

@ -1,4 +1,5 @@
outline-vpn-api outline-vpn-api
kubernetes
PyYAML>=6.0.1 PyYAML>=6.0.1
Flask>=2.3.3 Flask>=2.3.3
flask-cors flask-cors

9
static/layout.css Normal file → Executable file
View File

@ -4,6 +4,7 @@
*/ */
body { body {
color: #333; color: #333;
word-wrap: break-word;
} }
@ -22,8 +23,8 @@ a {
border-radius: 4px; border-radius: 4px;
} }
.delete-button { .delete-button {
background: #a20c0c; background: #9d2c2c;
border: 2px solid #310404; border: 1px solid #480b0b;
color: #ffffff; color: #ffffff;
} }
@ -158,6 +159,10 @@ a {
.server-item-unread { .server-item-unread {
border-left: 6px solid #1b98f8; border-left: 6px solid #1b98f8;
} }
.server-item-broken {
border-left: 6px solid #880d06;
}
.server-item:hover { .server-item:hover {
background: #d1d0d0; background: #d1d0d0;
} }

0
static/pure.css Normal file → Executable file
View File

View File

@ -1,28 +1,37 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>{% block title %}Dashboard{% endblock %}</title> <title>{% block title %}Dashboard{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='pure.css') }}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='pure.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='layout.css') }}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='layout.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<style> </head>
.border {
border: 0px solid black;
}
.content { <body>
padding: 10px;
} <div id="layout" class="content pure-g">
.form-field { <div id="nav" class="pure-u-1-3">
margin: 5px; <a href="#" id="menuLink" class="nav-menu-button">Menu</a>
width: 100%;
} <div class="nav-inner">
</style> <button onclick="location.href='/';" style="cursor:pointer;" class="primary-button pure-button">OutFleet v.{{ VERSION }}</button>
<!-- Script to make the Menu link work -->
<!-- Just stripped down version of the js/ui.js script for the side-menu layout --> <div class="pure-menu custom-restricted-width">
<script> <ul class="pure-menu-list">
<li class="pure-menu-item"><a href="/" class="pure-menu-link">Servers</a></li>
<li class="pure-menu-item"><a href="/clients" class="pure-menu-link">Clients</a></li>
<li class="pure-menu-item"><a href="/sync" class="pure-menu-link">Sync status</a></li>
</ul>
{{ VERSION }}
</div>
</div>
</div>
{% block content %}{% endblock %}
</div>
<!-- Script to make the Menu link work -->
<!-- Just stripped down version of the js/ui.js script for the side-menu layout -->
<script>
function getElements() { function getElements() {
return { return {
menu: document.getElementById('nav'), menu: document.getElementById('nav'),
@ -70,41 +79,15 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('click', handleEvent); document.addEventListener('click', handleEvent);
}); });
</script> </script>
</head> {% if nt %}
<body>
<div class="pure-g">
<div class="pure-u-2-24 border" style="background: rgb(37, 42, 58);">
<div class="content">
<div class="nav-inner">
<h1 onclick="location.href='/';" style="cursor:pointer;" class="pure-button">OutFleet {{VERSION}}</h1>
<ul class="pure-menu-list">
<li class="pure-menu-item"><a href="/" class="pure-menu-link">Servers</a></li>
<li class="pure-menu-item"><a href="/clients" class="pure-menu-link">Clients</a></li>
<li class="pure-menu-item"><a href="/sync" class="pure-menu-link">Sync status</a></li>
</ul>
</div>
</div>
</div>
<div class="pure-u-22-24 border">
<div class="content">
{% block content %}{% endblock %}
</div>
</div>
</div>
{% if nt %}
<label> <label>
<input type="checkbox" class="alertCheckbox" autocomplete="off" /> <input type="checkbox" class="alertCheckbox" autocomplete="off" />
<div class="alert {% if nl == 'error' %}error{% else %}success{% endif %}"> <div class="alert {% if nl == 'error' %}error{% else %}success{% endif %}">
<span class="alertText">{{nt}} <span class="alertText">{{nt}}
<br class="clear" /></span> <br class="clear"/></span>
</div> </div>
</label> </label>
{% endif %} {% endif %}
</body> </body>
</html> </html>

View File

@ -2,142 +2,121 @@
{% block content %} {% block content %}
<div class="pure-u-3-24"> <div id="list" class="pure-u-1-3" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
<div class="server-item"> <div class="server-item pure-g">
<h1 class="server-content-title">Clients</h1> <h1 class="server-content-title">Clients</h1>
</div> </div>
<div onclick="location.href='/clients?add_client=True';" class="server-item server-add ">
<div class="">
+
</div>
</div>
{% for client, values in CLIENTS.items() %} {% for client, values in CLIENTS.items() %}
<div onclick="location.href='/clients?selected_client={{ client }}';" <div class="server-item server-item-{% if client == selected_client %}unread{% else %}selected{% endif %} pure-g">
class="server-item server-item-{% if client == selected_client %}unread{% else %}selected{% endif %} "> <div class="pure-u-3-4" onclick="location.href='/clients?selected_client={{ client }}';">
<div class="">
<h5 class="server-name">{{ values["name"] }}</h5> <h5 class="server-name">{{ values["name"] }}</h5>
<h4 class="server-info">Allowed {{ values["servers"]|length }} server{% if values["servers"]|length >1 <h4 class="server-info">Allowed {{ values["servers"]|length }} server{% if values["servers"]|length >1 %}s{%endif%}</h4>
%}s{%endif%}</h4>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<div onclick="location.href='/clients?add_client=True';" class="server-item server-add pure-g">
<div class="pure-u-1">
+
</div>
</div>
</div> </div>
<div class="pure-u-20-24">
{% if add_client %}
<div class="server-content-header"> {% if add_client %}
<div class=""> <div class="pure-u-1-3">
<div class="server-content-header pure-g">
<div class="pure-u-1-2">
<h1 class="server-content-title">Add new client</h1> <h1 class="server-content-title">Add new client</h1>
</div> </div>
</div> </div>
<div class="server-content-body"> <div class="server-content-body">
<form action="/add_client" class="pure-form pure-form-stacked" method="POST"> <form action="/add_client" class="pure-form pure-form-stacked" method="POST">
<fieldset> <fieldset>
<input type="text" class="form-field pure-u-8-24" name="name" required placeholder="Name" /> <div class="pure-g">
<input type="text" class="form-field pure-u-8-24" name="comment" placeholder="Comment" /> <div class="pure-u-1 pure-u-md-1-3">
<input type="text" class="pure-u-23-24" name="name" required placeholder="Name"/>
</div>
<div class="pure-u-1 pure-u-md-1-3">
<input type="text" class="pure-u-23-24" name="comment" placeholder="Comment"/>
</div>
<div class="pure-checkbox">
{% for server in SERVERS %} {% for server in SERVERS %}
<label class="pure-checkbox" for="option{{loop.index0}}"> <label class="pure-checkbox" for="option{{loop.index0}}">{{server.info()["name"]}}
<input type="checkbox" id="option{{loop.index0}}" name="servers" <input type="checkbox" id="option{{loop.index0}}" name="servers" value="{{server.info()['local_server_id']}}"></label>
value="{{server.info()['local_server_id']}}">{{server.info()["name"]}}</label>
{% endfor %} {% endfor %}
<button type="submit" class="pure-button pure-button-primary button">Add client</button>
</div>
</div>
<button type="submit" class="pure-button pure-input-1 pure-button-primary">Add</button>
</fieldset> </fieldset>
</form> </form>
</div> </div>
{% endif %} </div>
{% endif %}
{% if selected_client and not add_client %}
{% if selected_client and not add_client %}
{% set client = CLIENTS[selected_client] %} {% set client = CLIENTS[selected_client] %}
<div class="pure-g">
<div class="pure-u-1"> <div class="pure-u-1-2">
<div class="server-content-header"> <div class="server-content-header pure-g">
<div class=""> <div class="pure-u-1-2">
<h1 class="server-content-title">{{client['name']}}</h1> <h1 class="server-content-title">{{client['name']}}</h1>
<h4 class="server-info">{{ selected_client }}</h4> <h4 class="server-info">{{ client['comment'] }}</h4>
{% if client['comment'] != "" %}<h4 class="server-info">{{ client['comment'] }}</h4>{% endif %} <h4 class="server-info">id {{ selected_client }}</h4>
</div>
</div>
</div>
</div>
<div class="pure-g">
<div class="pure-u-6-24">
</div>
</div>
<div class="server-content-body"> <div class="server-content-body">
<form action="/add_client" class="pure-form pure-form-aligned" method="POST"> <form action="/add_client" class="pure-form pure-form-stacked" method="POST">
<fieldset> <fieldset>
<div class="pure-g">
<div class=""> <div class="pure-u-1 pure-u-md-1-3">
<input type="text" class="form-field" name="name" required value="{{client['name']}}" /> <input type="text" class="pure-u-1" name="name" required value="{{client['name']}}"/>
<input type="hidden" class="form-field" name="old_name" required <input type="hidden" class="pure-u-1" name="old_name" required value="{{client['name']}}"/>
value="{{client['name']}}" />
</div> </div>
<div class=""> <div class="pure-u-1 pure-u-md-1-3">
<input type="text" class="form-field" name="comment" value="{{client['comment']}}" /> <input type="text" class="pure-u-1" name="comment" value="{{client['comment']}}"/>
</div> </div>
<input type="hidden" class="form-field" name="user_id" value="{{selected_client}}" /> <input type="hidden" class="pure-u-1" name="user_id" value="{{selected_client}}"/>
<div class="pure-checkbox"> <div class="pure-checkbox">
<p>Allow access to:</p> <p>Allow access to:</p>
{% for server in SERVERS %} {% for server in SERVERS %}
<label class="pure-checkbox" for="option{{loop.index0}}"> <label class="pure-checkbox" for="option{{loop.index0}}">{{server.info()["name"]}}{% if server.info()['local_server_id'] in client['servers'] %} ( Used {% for key in server.data["keys"] %}{% if key.name == client['name'] %}{{ (key.used_bytes if key.used_bytes else 0) | filesizeformat }}{% endif %}{% endfor %}){%endif%}
<input {% if server.info()['local_server_id'] in client['servers'] %}checked{%endif%} <input
type="checkbox" id="option{{loop.index0}}" name="servers" {% if server.info()['local_server_id'] in client['servers'] %}checked{%endif%}
value="{{server.info()['local_server_id']}}">{{server.info()["name"]}} type="checkbox" id="option{{loop.index0}}" name="servers" value="{{server.info()['local_server_id']}}"></label>
{% if server.info()['local_server_id'] in client['servers'] %} ( Used {% for key in
server.data["keys"] %}{% if key.name == client['name'] %}{{ (key.used_bytes if
key.used_bytes else 0) | filesizeformat }}{% endif %}{% endfor %}){%endif%}</label>
{% endfor %} {% endfor %}
</div> </div>
<button type="submit" class="pure-button pure-button-primary button">Save and Apply</button>
</div>
<button type="submit" class="pure-button pure-input-1 pure-button-primary">Save and apply</button>
</fieldset> </fieldset>
</form> </form>
<form action="/del_client" class="pure-form pure-form-stacked" method="POST">
<input type="hidden" name="name" required value="{{client['name']}}" />
<input type="hidden" name="user_id" value="{{selected_client}}" />
<label for="really" class="pure-radio">
<button type="submit" id="really"
class="pure-button pure-button-primary delete-button button">Delete Client 🔒<input
type="checkbox" id="agree" name="agree" required></button></label>
</form>
</div>
</div>
<div style="" class="pure-u-18-24">
<div class="pure-u-9-24">
<div> <div>
<h3>Invite text</h3> <h3>Invite text</h3><hr>
<textarea style="width: 100%; rows=10">
<textarea style="width: 96%; height: 360px; resize:none" disabled>
Install OutLine VPN. Copy and paste below keys to OutLine client. Install OutLine VPN. Copy and paste below keys to OutLine client.
Same keys will work simultaneously on many devices. Same keys will work simultaneously on many devices.
{% for server in SERVERS -%} {% for server in SERVERS -%}
{% if server.info()['local_server_id'] in client['servers'] %} {% if server.info()['local_server_id'] in client['servers'] %}
{{server.info()['name']}} {{server.info()['name']}}
```{% for key in server.data["keys"] %}{% if key.key_id == client['name'] %}ssconf://{{ ```{% for key in server.data["keys"] %}{% if key.key_id == client['name'] %}ssconf://{{ dynamic_hostname }}/dynamic/{{server.info()['name']}}/{{selected_client}}#{{server.info()['comment']}}{% endif %}{% endfor %}```
dynamic_hostname
}}/dynamic/{{server.info()['name']}}/{{selected_client}}#{{server.info()['comment']}}{% endif %}{%
endfor %}```
{% endif %} {% endif %}
{%- endfor -%} {%- endfor -%}</textarea>
</textarea>
</div> </div>
</div> <hr>
<div class="pure-u-14-24"> <div style="padding-top: 15px; padding-bottom: 15px">
<div class="pure-u-1">
</div>
</div>
</div>
<div class="pure-g">
<div class="pure-u-1 content">
<h3>Dynamic Access Keys</h3> <h3>Dynamic Access Keys</h3>
<table class="pure-table" style="width: 100%"> <table class="pure-table">
<thead> <thead>
<tr> <tr>
<th>Server</th> <th>Server</th>
@ -149,11 +128,8 @@ endfor %}```
{% if server.info()['local_server_id'] in client['servers'] %} {% if server.info()['local_server_id'] in client['servers'] %}
<tr> <tr>
<td>{{ server.info()['name'] }}</td> <td>{{ server.info()['name'] }}</td>
<td style="font-size: 10pt"> <td>
{% for key in server.data["keys"] %}{% if key.key_id == client['name'] %}ssconf://{{ <p style="font-size: 10pt">{% for key in server.data["keys"] %}{% if key.key_id == client['name'] %}ssconf://{{ dynamic_hostname }}/dynamic/{{server.info()['name']}}/{{selected_client}}#{{server.info()['comment']}}{% endif %}{% endfor %}</p>
dynamic_hostname
}}/dynamic/{{server.info()['name']}}/{{selected_client}}#{{server.info()['comment']}}{%
endif %}{% endfor %}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
@ -161,9 +137,9 @@ endfor %}```
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="pure-u-1 content"> <div class="pure-u-1 pure-u-md-1">
<h3>SS Links</h3> <h3>SS Links</h3>
<table class="pure-table" style="width: 100%"> <table class="pure-table">
<thead> <thead>
<tr> <tr>
<th>Server</th> <th>Server</th>
@ -175,9 +151,8 @@ endfor %}```
{% if server.info()['local_server_id'] in client['servers'] %} {% if server.info()['local_server_id'] in client['servers'] %}
<tr> <tr>
<td>{{ server.info()['name'] }}</td> <td>{{ server.info()['name'] }}</td>
<td style="font-size: 10pt"> <td>
{% for key in server.data["keys"] %}{% if key.key_id == client['name'] %}{{ <pre style="font-size: 10pt">{% for key in server.data["keys"] %}{% if key.key_id == client['name'] %}{{ key.access_url }}{% endif %}{% endfor %}</pre>
key.access_url }}{% endif %}{% endfor %}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
@ -185,7 +160,15 @@ endfor %}```
</tbody> </tbody>
</table> </table>
</div> </div>
<hr>
</div> </div>
{% endif %} <form action="/del_client" class="pure-form pure-form-stacked" method="POST">
</div> <input type="hidden" class="pure-u-1" name="name" required value="{{client['name']}}"/>
<input type="hidden" class="pure-u-1" name="user_id" value="{{selected_client}}"/>
<button type="submit" class="pure-button button-error pure-input-1 ">Delete</button>
</form>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,9 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="pure-g">
<div class="pure-u-4-24"> <div id="list" class="pure-u-1-3" xmlns="http://www.w3.org/1999/html">
<div class="server-item "> <div class="server-item pure-g">
<h1 class="server-content-title">Servers</h1> <h1 class="server-content-title">Servers</h1>
</div> </div>
{% for server in SERVERS %} {% for server in SERVERS %}
@ -13,69 +13,68 @@
{% set list_ns.total_bytes = list_ns.total_bytes + key.used_bytes %} {% set list_ns.total_bytes = list_ns.total_bytes + key.used_bytes %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<div <div class="server-item server-item-{% if loop.index0 == selected_server|int %}unread{% else %}selected{% endif %} pure-g">
class="server-item server-item-{% if loop.index0 == selected_server|int %}unread{% else %}selected{% endif %} "> <div class="pure-u-3-4" onclick="location.href='/?selected_server={{loop.index0}}';">
<div onclick="location.href='/?selected_server={{loop.index0}}';">
<h5 class="server-name">{{ server.info()["name"] }}</h5> <h5 class="server-name">{{ server.info()["name"] }}</h5>
<h4 class="server-info">API {{ '/'.join(server.info()["url"].split('/')[0:-1]).split("://")[1] }}</h4> <h4 class="server-info">{{ '/'.join(server.info()["url"].split('/')[0:-1]) }}</h4>
<h4 class="server-info">Client Port: {{ server.info()["port_for_new_access_keys"] }}</h4> <h4 class="server-info">Port {{ server.info()["port_for_new_access_keys"] }}</h4>
<h4 class="server-info">Hostname: {{ server.info()["hostname_for_access_keys"] }}</h4> <h4 class="server-info">Hostname {{ server.info()["hostname_for_access_keys"] }}</h4>
<h4 class="server-info">Traffic: {{ list_ns.total_bytes | filesizeformat }}</h4> <h4 class="server-info">Traffic: {{ list_ns.total_bytes | filesizeformat }}</h4>
<h4 class="server-info">Version: {{ server.info()["version"] }}</h4> <h4 class="server-info">v.{{ server.info()["version"] }}</h4>
<p class="server-comment"> <p class="server-comment">
{{ server.info()["comment"] }} {{ server.info()["comment"] }}
</p> </p>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<div onclick="location.href='/?add_server=True';" class="server-item server-add "> <div onclick="location.href='/?add_server=True';" class="server-item server-add pure-g">
<div class=""> <div class="pure-u-1">
+ +
</div> </div>
</div> </div>
</div>
<div class="pure-u-19-24"> </div>
{% if add_server %}
<div class="server-content-header"> {% if add_server %}
<div class=""> <div class="pure-u-1-3">
<div class="server-content-header pure-g">
<div class="pure-u-1-2">
<h1 class="server-content-title">Add new server</h1> <h1 class="server-content-title">Add new server</h1>
</div> </div>
</div> </div>
<div class="server-content-body"> <div class="server-content-body">
<form action="/add_server" class="pure-form pure-form-stacked" method="POST"> <form action="/add_server" class="pure-form pure-form-stacked" method="POST">
<fieldset> <fieldset>
<div class=""> <div class="pure-g">
<div class="form-field"> <div class="pure-u-1 pure-u-md-1-3">
<input type="text" class="form-field" name="url" placeholder="Server management URL" /> <input type="text" class="pure-u-23-24" name="url" placeholder="Server management URL"/>
</div> </div>
<div class="form-field"> <div class="pure-u-1 pure-u-md-1-3">
<input type="text" class="form-field" name="cert" placeholder="Certificate" /> <input type="text"class="pure-u-23-24" name="cert" placeholder="Certificate"/>
</div> </div>
<div class="form-field"> <div class="pure-u-1 pure-u-md-1-3">
<input type="text" class="form-field" name="comment" placeholder="Comment" /> <input type="text" class="pure-u-23-24" name="comment" placeholder="Comment"/>
</div> </div>
</div> </div>
<button type="submit" class="pure-button pure-button-primary button">Add server</button> <button type="submit" class="pure-button pure-input-1 pure-button-primary">Add</button>
</fieldset> </fieldset>
</form> </form>
</div> </div>
</div> </div>
{% endif %}
{% endif %}
{% if SERVERS|length != 0 and not add_server %} {% if SERVERS|length != 0 and not add_server %}
{% if selected_server is none %} {% if selected_server is none %}
{% set server = SERVERS[0] %} {% set server = SERVERS[0] %}
{% else %} {% else %}
{% set server = SERVERS[selected_server|int] %} {% set server = SERVERS[selected_server|int] %}
{% endif %} {% endif %}
<div class="pure-u-19-24"> <div id="main" class="pure-u-1">
<div class="">
<div class="server-content"> <div class="server-content">
<div class="server-content-header "> <div class="server-content-header pure-g">
<div class=""> <div class="pure-u-1-2">
<h1 class="server-content-title">{{server.info()["name"]}}</h1> <h1 class="server-content-title">{{server.info()["name"]}}</h1>
<p class="server-content-subtitle"> <p class="server-content-subtitle">
<span>v.{{server.info()["version"]}} {{server.info()["local_server_id"]}}</span> <span>v.{{server.info()["version"]}} {{server.info()["local_server_id"]}}</span>
@ -93,75 +92,59 @@
<div class="server-content-body"> <div class="server-content-body">
<h3>Clients: {{ server.info()['keys']|length }}</h3> <h3>Clients: {{ server.info()['keys']|length }}</h3>
<h3>Total traffic: {{ ns.total_bytes | filesizeformat }}</h3> <h3>Total traffic: {{ ns.total_bytes | filesizeformat }}</h3>
<form class="pure-form pure-form-aligned" method="POST"> <form class="pure-form pure-form-stacked" method="POST">
<fieldset> <fieldset>
<div class=""> <div class="pure-g">
<div class=""> <div class="pure-u-1 pure-u-md-1-3">
<label for="name">Server Name</br>Must be unique. Used for Dynamic Link <label for="name">Server Name</br> Note that this will not be reflected on the devices of the users that you invited to connect to it.</label>
generation.</label> <input type="text" id="name" class="pure-u-23-24" name="name" value="{{server.info()['name']}}"/>
<input class="form-field" type="text" id="name" class="" name="name"
value="{{server.info()['name']}}" />
</div> </div>
<div class=""> <div class="pure-u-1 pure-u-md-1-3">
<label for="comment">Comment</br>Will be used as "Server name" in client <label for="comment">Comment</br>This value will be used as "Server name" in client app.</label>
app.</label> <input type="text" id="comment" class="pure-u-23-24" name="comment" value="{{server.info()['comment']}}"/>
<input class="form-field" type="text" id="comment" class="" name="comment"
value="{{server.info()['comment']}}" />
</div> </div>
<div class=""> <div class="pure-u-1 pure-u-md-1-3">
<label for="port_for_new_access_keys">Port For New Access Keys</label> <label for="port_for_new_access_keys">Port For New Access Keys</label>
<input class="form-field" type="text" id="port_for_new_access_keys" class="" <input type="text" id="port_for_new_access_keys" class="pure-u-23-24" name="port_for_new_access_keys" value="{{server.info()['port_for_new_access_keys']}}"/>
name="port_for_new_access_keys"
value="{{server.info()['port_for_new_access_keys']}}" />
</div> </div>
<div class=""> <div class="pure-u-1 pure-u-md-1-3">
<label for="hostname_for_access_keys">Hostname For Access Keys</label> <label for="hostname_for_access_keys">Hostname For Access Keys</label>
<input class="form-field" type="text" id="hostname_for_access_keys" class="" <input type="text" id="hostname_for_access_keys" class="pure-u-23-24" name="hostname_for_access_keys" value="{{server.info()['hostname_for_access_keys']}}"/>
name="hostname_for_access_keys"
value="{{server.info()['hostname_for_access_keys']}}" />
</div> </div>
<div class=""> <div class="pure-u-1 pure-u-md-1-3">
<label for="url">Server URL</label> <label for="url">Server URL</label>
<input class="form-field" type="text" readonly id="url" class="" name="url" <input type="text" readonly id="url" class="pure-u-23-24" name="url" value="{{server.info()['url']}}"/>
value="{{server.info()['url']}}" />
</div> </div>
<div class=""> <div class="pure-u-1 pure-u-md-1-3">
<label for="cert">Server Access Certificate</label> <label for="cert">Server Access Certificate</label>
<input class="form-field" type="text" readonly id="cert" class="" name="cert" <input type="text" readonly id="cert" class="pure-u-23-24" name="cert" value="{{server.info()['cert']}}"/>
value="{{server.info()['cert']}}" />
</div> </div>
<div class=""> <div class="pure-u-1 pure-u-md-1-3">
Created {{format_timestamp(server.info()['created_timestamp_ms']) }} <label for="created_timestamp_ms">Created</label>
<input type="text" readonly id="created_timestamp_ms" class="pure-u-23-24" name="created_timestamp_ms" value="{{format_timestamp(server.info()['created_timestamp_ms']) }}"/>
</div> </div>
<input class="form-field" type="hidden" readonly id="server_id" class="" name="server_id" <input type="hidden" readonly id="server_id" class="pure-u-23-24" name="server_id" value="{{server.info()['local_server_id']}}"/>
value="{{server.info()['local_server_id']}}" />
</div> </div>
<p>Share anonymous metrics</p> <p>Share anonymous metrics</p>
<label for="metrics_enabled" class="pure-radio"> <label for="metrics_enabled" class="pure-radio">
<input type="radio" id="metrics_enabled" name="metrics" value="True" {% if <input type="radio" id="metrics_enabled" name="metrics" value="True" {% if server.info()['metrics_enabled'] == True %}checked{% endif %} /> Enable
server.info()['metrics_enabled']==True %}checked{% endif %} /> Enable
</label> </label>
<label for="metrics_disabled" class="pure-radio"> <label for="metrics_disabled" class="pure-radio">
<input type="radio" id="metrics_disabled" name="metrics" value="False" {% if <input type="radio" id="metrics_disabled" name="metrics" value="False" {% if server.info()['metrics_enabled'] == False %}checked{% endif %} /> Disable
server.info()['metrics_enabled']==False %}checked{% endif %} /> Disable
</label> </label>
<button type="submit" class="pure-button pure-button-primary button">Save and apply</button> <button type="submit" class="pure-button pure-button-primary button">Save and apply</button>
</fieldset> </fieldset>
</form> </form>
<form action="/del_server" method="post"> <form action="/del_server" method="post">
<input type="hidden" name="local_server_id" value="{{ server.info()["local_server_id"] }}">
<input type="hidden" id="really" value="{{ server.info()[" local_server_id"] }}"> <button type="submit" class="pure-button pure-button-primary delete-button button">Delete Server</button>
<label for="really" class="pure-radio"> <input type="checkbox" id="agree" name="agree" required>
<button type="submit" class="pure-button pure-button-primary delete-button button">Delete
Server 🔒<input type="checkbox" id="agree" name="agree" required></button></label>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endif %} {% endif %}
</div>
{% endblock %} {% endblock %}

View File

@ -1,32 +1,17 @@
{% extends "base.html" %} <h1>Last sync log</h1>
{% block content %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- and it's easy to individually load additional languages -->
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script> -->
<script>hljs.highlightAll();</script>
<h1>Last logs</h1>
<form action="/sync" class="pure-form pure-form-stacked" method="POST"> <form action="/sync" class="pure-form pure-form-stacked" method="POST">
<button type="submit" class="pure-button pure-button-primary button">Sync now</button> <p>Wipe ALL keys on ALL servers?</p>
<p>Also wipe ALL keys on ALL servers? Use in case of inconsistency.</p>
<label for="no_wipe" class="pure-radio"> <label for="no_wipe" class="pure-radio">
<input type="radio" id="no_wipe" name="wipe" value="no_wipe" checked /> No <input type="radio" id="no_wipe" name="wipe" value="no_wipe" checked /> No
</label> </label>
<label for="do_wipe" class="pure-radio"> <label for="do_wipe" class="pure-radio">
<input type="radio" id="do_wipe" name="wipe" value="all" /> Yes <input type="radio" id="do_wipe" name="wipe" value="all" /> Yes
</label> </label>
<button type="submit" class="pure-button button-error pure-input-1 ">Sync now</button>
</form>
</form> <pre>
<code>
<pre style="height: 600px; overflow: scroll;">
<code class="language-c">
{% for line in lines %}{{ line }}{% endfor %} {% for line in lines %}{{ line }}{% endfor %}
</code> </code>
</pre> </pre>
{% endblock %}