Added sync feature

This commit is contained in:
2023-12-17 17:06:30 +02:00
parent 96fc572bd0
commit 66a6c3e6d6
6 changed files with 94 additions and 58 deletions

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
config.yaml config.yaml
__pycache__/ __pycache__/
sync.log
main.py
.vscode/launch.json

21
lib.py
View File

@ -8,10 +8,9 @@ logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%d-%m-%Y %H:%M:%S') datefmt='%d-%m-%Y %H:%M:%S')
CFG_PATH = '/usr/local/etc/outfleet/config.yaml'
class ServerDict(TypedDict): class ServerDict(TypedDict):
server_id: str
name: str name: str
url: str url: str
cert: str cert: str
@ -30,9 +29,12 @@ class Server:
url: str, url: str,
cert: str, cert: str,
comment: str, comment: str,
# read from config. not the same as real server id you can get from api
local_server_id: str,
): ):
self.client = OutlineVPN(api_url=url, cert_sha256=cert) self.client = OutlineVPN(api_url=url, cert_sha256=cert)
self.data: ServerDict = { self.data: ServerDict = {
'local_server_id': local_server_id,
'name': self.client.get_server_information()["name"], 'name': self.client.get_server_information()["name"],
'url': url, 'url': url,
'cert': cert, 'cert': cert,
@ -49,8 +51,21 @@ class Server:
def info(self) -> ServerDict: def info(self) -> ServerDict:
return self.data return self.data
def check_client(self, name):
# Looking for any users with provided name. len(result) != 1 is a problem.
result = []
for key in self.client.get_keys():
if key.name == name:
result.append(name)
self.log.info(f"check_client found client `{name}` config is correct.")
if len(result) != 1:
self.log.warning(f"check_client found client `{name}` inconsistent. Found {len(result)} keys.")
return False
else:
return True
def apply_config(self, config): def apply_config(self, config, CFG_PATH):
if config.get("name"): if config.get("name"):
self.client.set_server_name(config.get("name")) self.client.set_server_name(config.get("name"))
self.log.info("Changed %s name to '%s'", self.data["server_id"], config.get("name")) self.log.info("Changed %s name to '%s'", self.data["server_id"], config.get("name"))

100
main.py
View File

@ -3,14 +3,18 @@ import logging
from datetime import datetime from datetime import datetime
import random import random
import string import string
import argparse
import uuid
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
logging.getLogger('werkzeug').setLevel(logging.ERROR) logging.getLogger('werkzeug').setLevel(logging.ERROR)
parser = argparse.ArgumentParser()
CFG_PATH = '/usr/local/etc/outfleet/config.yaml' parser.add_argument("-c", "--config", default="/usr/local/etc/outfleet/config.yaml", help="Config file location")
args = parser.parse_args()
CFG_PATH = args.config
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
@ -55,11 +59,11 @@ def update_state():
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')
servers = config.get('servers', dict()) servers = config.get('servers', dict())
for server_id, server_config in servers.items(): for local_server_id, server_config in servers.items():
try: try:
server = Server(url=server_config["url"], cert=server_config["cert"], comment=server_config["comment"]) server = Server(url=server_config["url"], cert=server_config["cert"], comment=server_config["comment"], local_server_id=local_server_id)
SERVERS.append(server) SERVERS.append(server)
log.info("Server state updated: %s", server.info()["name"]) log.info("Server state updated: %s, [%s]", server.info()["name"], local_server_id)
except Exception as e: except Exception as e:
log.warning("Can't access server: %s - %s", server_config["url"], e) log.warning("Can't access server: %s - %s", server_config["url"], e)
@ -81,7 +85,7 @@ def index():
elif request.method == 'POST': elif request.method == 'POST':
server = request.form['server_id'] server = request.form['server_id']
server = next((item for item in SERVERS if item.info()["server_id"] == server), None) server = next((item for item in SERVERS if item.info()["server_id"] == server), None)
server.apply_config(request.form) server.apply_config(request.form, CFG_PATH)
update_state() update_state()
return redirect( return redirect(
url_for('index', nt="Updated Outline VPN Server", selected_server=request.args.get('selected_server'))) url_for('index', nt="Updated Outline VPN Server", selected_server=request.args.get('selected_server')))
@ -128,10 +132,11 @@ def add_server():
config = yaml.safe_load(file) or {} config = yaml.safe_load(file) or {}
servers = config.get('servers', dict()) servers = config.get('servers', dict())
local_server_id = uuid.uuid4()
new_server = Server(url=request.form['url'], cert=request.form['cert'], comment=request.form['comment']) new_server = Server(url=request.form['url'], cert=request.form['cert'], comment=request.form['comment'], local_server_id=local_server_id)
servers[new_server.data["server_id"]] = { servers[new_server.data["local_server_id"]] = {
'name': new_server.data["name"], 'name': new_server.data["name"],
'url': new_server.data["url"], 'url': new_server.data["url"],
'comment': new_server.data["comment"], 'comment': new_server.data["comment"],
@ -167,7 +172,7 @@ def add_client():
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:
if server.data["server_id"] in request.form.getlist('servers'): if server.data["local_server_id"] in request.form.getlist('servers'):
client = next((item for item in server.data["keys"] if item.name == request.form.get('old_name')), None) client = next((item for item in server.data["keys"] if item.name == request.form.get('old_name')), None)
if client: if client:
if client.name == request.form.get('name'): if client.name == request.form.get('name'):
@ -214,14 +219,14 @@ def del_client():
return redirect(url_for('clients', nt="User has been deleted")) return redirect(url_for('clients', nt="User has been deleted"))
@app.route('/dynamic/<server_name>/<client_id>', methods=['GET']) @app.route('/dynamic/<server_name>/<client_id>', methods=['GET'], strict_slashes=False)
def dynamic(server_name, client_id): def dynamic(server_name, client_id):
try: try:
client = next((keys for client, keys in CLIENTS.items() if client == client_id), None) client = next((keys for client, keys in CLIENTS.items() if client == client_id), None)
server = next((item for item in SERVERS if item.info()["name"] == server_name), None) server = next((item for item in SERVERS if item.info()["name"] == server_name), None)
key = next((item for item in server.data["keys"] if item.name == client["name"]), None) key = next((item for item in server.data["keys"] if item.name == client["name"]), None)
if server and client and key: if server and client and key:
if server.data["server_id"] in client["servers"]: if server.data["local_server_id"] in client["servers"]:
log.info("Client %s wants ssconf for %s", client["name"], server.data["name"]) log.info("Client %s wants ssconf for %s", client["name"], server.data["name"])
return { return {
"server": server.data["hostname_for_access_keys"], "server": server.data["hostname_for_access_keys"],
@ -236,46 +241,49 @@ def dynamic(server_name, client_id):
except: except:
log.warning("Hack attempt! Client or server doesn't exist. SCAM") log.warning("Hack attempt! Client or server doesn't exist. SCAM")
return "Hey buddy, i think you got the wrong door the leather-club is two blocks down" return "Hey buddy, i think you got the wrong door the leather-club is two blocks down"
@app.route('/dynamic/', methods=['GET'], strict_slashes=False)
def _dynamic():
log.warning("Hack attempt! Client or server doesn't exist. SCAM")
return "Hey buddy, i think you got the wrong door the leather-club is two blocks down"
@app.route('/sync', methods=['GET', 'POST']) @app.route('/sync', methods=['GET', 'POST'])
def sync(): def sync():
# {% for server in SERVERS %} if request.method == 'GET':
# {% for key in server.data["keys"] %} with open('sync.log', 'r') as file:
# {% if key.name == client['name'] %} lines = file.readlines()
# ssconf://{{ dynamic_hostname }}/dynamic/{{server.info()['name']}}/{{selected_client}}#{{server.info()['comment']}} return render_template(
# {% endif %} 'sync.html',
# {% endfor %} SERVERS=SERVERS,
# {% endfor %} CLIENTS=CLIENTS,
log.info(f"{SERVERS[0]}") lines=lines,
# clients_status = [] )
# for c_id, c_data in CLIENTS: if request.method == 'POST':
# servers_status = [] log = logging.getLogger('sync')
# for server in c_data["servers"]: file_handler = logging.FileHandler('sync.log')
# for server in SERVERS: file_handler.setLevel(logging.DEBUG)
# if server.data["name"] == formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# for key in server.data["keys"]: file_handler.setFormatter(formatter)
# if key.name == c_data["name"]: log.addHandler(file_handler)
# servers_status.append{
server_hash = {}
for server in SERVERS:
server_hash[server.data['local_server_id']] = server
for key, client in CLIENTS.items():
log.info(f"Sync client `{client['name']}`")
for u_server_id in client['servers']:
if u_server_id in server_hash:
if not server_hash[u_server_id].check_client(client['name']):
log.warning(f"Client `{client['name']}` absent on `{server_hash[u_server_id].data['name']}`")
server_hash[u_server_id].create_key(client['name'])
else:
log.info(f"Client `{client['name']}` presented on `{server_hash[u_server_id].data['name']}`")
else:
log.info(f"Client `{client['name']}` incorrect server_id `{u_server_id}`")
update_state()
return redirect(url_for('sync'))
# }
# client = {
# "id": c_id,
# "name": data["name"],
# "servers_status":
# }
# if request.method == 'GET':
# return render_template(
# 'sync.html',
# SERVERS=SERVERS,
# CLIENTS=CLIENTS,
# nt=request.args.get('nt'),
# nl=request.args.get('nl'),
# selected_client=request.args.get('selected_client'),
# add_client=request.args.get('add_client', None),
# format_timestamp=format_timestamp,
# dynamic_hostname=HOSTNAME,
# )
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -42,7 +42,7 @@
<div class="pure-checkbox"> <div class="pure-checkbox">
{% for server in SERVERS %} {% for server in SERVERS %}
<label class="pure-checkbox" for="option{{loop.index0}}">{{server.info()["name"]}} <label class="pure-checkbox" for="option{{loop.index0}}">{{server.info()["name"]}}
<input type="checkbox" id="option{{loop.index0}}" name="servers" value="{{server.info()['server_id']}}"></label> <input type="checkbox" id="option{{loop.index0}}" name="servers" value="{{server.info()['local_server_id']}}"></label>
{% endfor %} {% endfor %}
@ -85,10 +85,10 @@
<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}}">{{server.info()["name"]}}{% if server.info()['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 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 <input
{% if server.info()['server_id'] in client['servers'] %}checked{%endif%} {% if server.info()['local_server_id'] in client['servers'] %}checked{%endif%}
type="checkbox" id="option{{loop.index0}}" name="servers" value="{{server.info()['server_id']}}"></label> type="checkbox" id="option{{loop.index0}}" name="servers" value="{{server.info()['local_server_id']}}"></label>
{% endfor %} {% endfor %}
@ -106,7 +106,7 @@ 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()['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.name == client['name'] %}ssconf://{{ dynamic_hostname }}/dynamic/{{server.info()['name']}}/{{selected_client}}#{{server.info()['comment']}}{% endif %}{% endfor %}``` ```{% 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 %}```
{% endif %} {% endif %}
@ -125,7 +125,7 @@ Same keys will work simultaneously on many devices.
</thead> </thead>
<tbody> <tbody>
{% for server in SERVERS %} {% for server in SERVERS %}
{% if server.info()['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> <td>
@ -148,7 +148,7 @@ Same keys will work simultaneously on many devices.
</thead> </thead>
<tbody> <tbody>
{% for server in SERVERS %} {% for server in SERVERS %}
{% if server.info()['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> <td>

View File

@ -77,7 +77,7 @@
<div class="pure-u-1-2"> <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()["server_id"]}}</span> <span>v.{{server.info()["version"]}} {{server.info()["local_server_id"]}}</span>
</p> </p>
</div> </div>
@ -128,7 +128,7 @@
<label for="created_timestamp_ms">Created</label> <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']) }}"/> <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 type="hidden" readonly id="server_id" class="pure-u-23-24" name="server_id" value="{{server.info()['server_id']}}"/> <input type="hidden" readonly id="server_id" class="pure-u-23-24" name="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">

View File

@ -0,0 +1,10 @@
<h1>Last sync log</h1>
<form action="/sync" class="pure-form pure-form-stacked" method="POST">
<button type="submit" class="pure-button button-error pure-input-1 ">Sync now</button>
</form>
<pre>
<code>
{% for line in lines %}{{ line }}{% endfor %}
</code>
</pre>