mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-12-16 17:37:51 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7a41e6a2f |
@@ -82,15 +82,6 @@ server {
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Setup sslocal service on Windows
|
||||
Shadowsocks servers can be used directly with **sslocal**. For automatic and regular password updates, you can create a Task Scheduler job to rotate the passwords when they change, as OutFleet manages the passwords automatically.
|
||||
You may run script in Admin PowerShell to create Task for autorun **sslocal** and update connection details automatically using Outfleet API
|
||||
```PowerShell
|
||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force; Invoke-Expression (Invoke-WebRequest -Uri "https://raw.githubusercontent.com/house-of-vanity/OutFleet/refs/heads/master/tools/windows-helper.ps1" -UseBasicParsing).Content
|
||||
```
|
||||
[Firefox PluginProxy Switcher and Manager](https://addons.mozilla.org/en-US/firefox/addon/proxy-switcher-and-manager/) && [Chrome plugin Proxy Switcher and Manager](https://chromewebstore.google.com/detail/proxy-switcher-and-manage/onnfghpihccifgojkpnnncpagjcdbjod)
|
||||
|
||||
Keep in mind that all user keys are stored in a single **config.yaml** file. If this file is lost, user keys will remain on the servers, but OutFleet will lose the ability to manage them. Handle with extreme caution and use backups.
|
||||
|
||||
## Authors
|
||||
|
||||
3
k8s.py
3
k8s.py
@@ -92,8 +92,9 @@ def write_config(config):
|
||||
def reload_config():
|
||||
global CONFIG
|
||||
while True:
|
||||
new_config = yaml.safe_load(V1.read_namespaced_config_map(name="config-outfleet", namespace=NAMESPACE).data['config.yaml'])
|
||||
with lib.lock:
|
||||
CONFIG = yaml.safe_load(V1.read_namespaced_config_map(name="config-outfleet", namespace=NAMESPACE).data['config.yaml'])
|
||||
CONFIG = new_config
|
||||
log.debug(f"Synced system config with ConfigMap [config-outfleet].")
|
||||
time.sleep(30)
|
||||
|
||||
|
||||
11
lib.py
11
lib.py
@@ -33,19 +33,10 @@ def get_config():
|
||||
try:
|
||||
with open(args.config, "r") as file:
|
||||
config = yaml.safe_load(file)
|
||||
if config == None:
|
||||
config = {
|
||||
"servers": {},
|
||||
"clients": {}
|
||||
}
|
||||
except:
|
||||
try:
|
||||
with open(args.config, "w"):
|
||||
config = {
|
||||
"servers": {},
|
||||
"clients": {}
|
||||
}
|
||||
yaml.safe_dump(config, file)
|
||||
pass
|
||||
except Exception as exp:
|
||||
log.error(f"Couldn't create config. {exp}")
|
||||
return None
|
||||
|
||||
138
main.py
138
main.py
@@ -5,26 +5,14 @@ import logging
|
||||
from datetime import datetime
|
||||
import random
|
||||
import string
|
||||
import argparse
|
||||
import uuid
|
||||
import bcrypt
|
||||
|
||||
|
||||
import k8s
|
||||
from flask import Flask, render_template, request, url_for, redirect
|
||||
from flask import Flask, render_template, request, url_for, redirect, jsonify
|
||||
from flask_cors import CORS
|
||||
from werkzeug.routing import BaseConverter
|
||||
from lib import Server, write_config, get_config, args, lock
|
||||
|
||||
class DuplicateFilter(logging.Filter):
|
||||
|
||||
def filter(self, record):
|
||||
# add other fields if you need more granular comparison, depends on your app
|
||||
current_log = (record.module, record.levelno, record.msg)
|
||||
if current_log != getattr(self, "last_log", None):
|
||||
self.last_log = current_log
|
||||
return True
|
||||
return False
|
||||
|
||||
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||
|
||||
@@ -41,20 +29,14 @@ formatter = logging.Formatter(
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
log.addHandler(file_handler)
|
||||
duplicate_filter = DuplicateFilter()
|
||||
log.addFilter(duplicate_filter)
|
||||
|
||||
CFG_PATH = args.config
|
||||
NAMESPACE = k8s.NAMESPACE
|
||||
SERVERS = list()
|
||||
BROKEN_SERVERS = list()
|
||||
CLIENTS = dict()
|
||||
VERSION = '8.1'
|
||||
SECRET_LINK_LENGTH = 8
|
||||
SECRET_LINK_PREFIX = '$2b$12$'
|
||||
SS_PREFIX = "\u0005\u00DC\u005F\u00E0\u0001\u0020"
|
||||
VERSION = '5'
|
||||
HOSTNAME = ""
|
||||
WRONG_DOOR = "Hey buddy, i think you got the wrong door the leather-club is two blocks down"
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
@@ -69,7 +51,9 @@ def random_string(length=64):
|
||||
return "".join(random.choice(letters) for i in range(length))
|
||||
|
||||
|
||||
|
||||
def update_state(timer=40):
|
||||
|
||||
while True:
|
||||
with lock:
|
||||
global SERVERS
|
||||
@@ -109,7 +93,6 @@ def update_state(timer=40):
|
||||
break
|
||||
time.sleep(40)
|
||||
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def index():
|
||||
if request.method == "GET":
|
||||
@@ -144,6 +127,33 @@ def index():
|
||||
else:
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@app.route("/servers", methods=["GET", "POST"])
|
||||
@app.route("/servers/<string:local_server_id>", methods=["GET", "POST", "DELETE"])
|
||||
def servers(local_server_id=None):
|
||||
if local_server_id:
|
||||
log.info(f"Got {local_server_id} Config {get_config()}")
|
||||
server = get_config()['servers'].get(local_server_id, None)
|
||||
return jsonify(server)
|
||||
if request.method == "GET":
|
||||
return jsonify({ "servers": get_config()['servers']})
|
||||
|
||||
elif request.method == "POST":
|
||||
server = request.form["server_id"]
|
||||
server = next(
|
||||
(item for item in SERVERS if item.info()["local_server_id"] == server), None
|
||||
)
|
||||
server.apply_config(request.form)
|
||||
update_state(timer=0)
|
||||
return redirect(
|
||||
url_for(
|
||||
"index",
|
||||
nt="Updated Outline VPN Server",
|
||||
selected_server=request.args.get("selected_server"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.route("/clients", methods=["GET", "POST"])
|
||||
def clients():
|
||||
@@ -151,11 +161,8 @@ def clients():
|
||||
return render_template(
|
||||
"clients.html",
|
||||
SERVERS=SERVERS,
|
||||
bcrypt=bcrypt,
|
||||
CLIENTS=CLIENTS,
|
||||
VERSION=VERSION,
|
||||
SECRET_LINK_LENGTH=SECRET_LINK_LENGTH,
|
||||
SECRET_LINK_PREFIX=SECRET_LINK_PREFIX,
|
||||
K8S_NAMESPACE=k8s.NAMESPACE,
|
||||
nt=request.args.get("nt"),
|
||||
nl=request.args.get("nl"),
|
||||
@@ -199,7 +206,6 @@ def add_server():
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.route("/del_server", methods=["POST"])
|
||||
def del_server():
|
||||
if request.method == "POST":
|
||||
@@ -321,70 +327,8 @@ def del_client():
|
||||
return redirect(url_for("clients", nt="User has been deleted"))
|
||||
|
||||
|
||||
def append_to_log(log_entry):
|
||||
with open("access_log.log", "a") as log_file:
|
||||
log_file.write(log_entry + "\n")
|
||||
|
||||
@app.route("/dynamic/<path:hash_secret>", methods=["GET"], strict_slashes=False)
|
||||
def dynamic(hash_secret):
|
||||
# Depricated scheme.
|
||||
for server in SERVERS:
|
||||
if hash_secret.startswith(server.data["name"]):
|
||||
log.warning("Deprecated key request")
|
||||
server_name = hash_secret.split('/')[0]
|
||||
client_id = hash_secret.split('/')[1]
|
||||
return dynamic_depticated(server_name, client_id, hash_secret)
|
||||
try:
|
||||
short_hash_server = hash_secret[0:SECRET_LINK_LENGTH]
|
||||
short_hash_client = hash_secret[SECRET_LINK_LENGTH:SECRET_LINK_LENGTH * 2 ]
|
||||
client_provided_secret = hash_secret[SECRET_LINK_LENGTH * 2:]
|
||||
hash_server = None
|
||||
hash_client = None
|
||||
server = None
|
||||
client = None
|
||||
for _server in SERVERS:
|
||||
if _server.data["local_server_id"][:SECRET_LINK_LENGTH] == short_hash_server:
|
||||
hash_server = _server.data["local_server_id"]
|
||||
server = _server
|
||||
|
||||
for client_id, values in CLIENTS.items():
|
||||
if client_id[:SECRET_LINK_LENGTH] == short_hash_client:
|
||||
hash_client = client_id
|
||||
client = CLIENTS[client_id]
|
||||
|
||||
if server and client:
|
||||
append_to_log(f"User: {client["name"]}. Server: {server.data['name']} client secret string: {hash_secret}")
|
||||
client_shadowsocks_key = next(
|
||||
(item for item in server.data["keys"] if item.key_id == client["name"]), None
|
||||
)
|
||||
|
||||
secret_string = hash_server + hash_client
|
||||
check_secret_hash = bcrypt.checkpw(
|
||||
password=secret_string.encode('utf-8'),
|
||||
hashed_password=f"{SECRET_LINK_PREFIX}{client_provided_secret}".encode('utf-8')
|
||||
)
|
||||
if check_secret_hash:
|
||||
log.info(f"Client {client['name']} has been requested ssconf for {server.data['name']}. Bcrypt client hash {client_provided_secret[0:16]}...[FULL HASH SECURED]")
|
||||
return {
|
||||
"server": server.data["hostname_for_access_keys"],
|
||||
"server_port": client_shadowsocks_key.port,
|
||||
"password": client_shadowsocks_key.password,
|
||||
"method": client_shadowsocks_key.method,
|
||||
"prefix": SS_PREFIX,
|
||||
"info": "Managed by OutFleet [github.com/house-of-vanity/OutFleet/]",
|
||||
}
|
||||
else:
|
||||
log.warning(f"Hack attempt! Client secret does not match: {client_provided_secret}")
|
||||
return WRONG_DOOR
|
||||
else:
|
||||
log.warning(f"Hack attempt! Client or server doesn't exist. payload: {hash_secret[0:200]}")
|
||||
return WRONG_DOOR
|
||||
except Exception as e:
|
||||
log.error(f"Dynamic V2 parse error: {e}")
|
||||
return WRONG_DOOR
|
||||
|
||||
|
||||
def dynamic_depticated(server_name, client_id, hash_secret=""):
|
||||
@app.route("/dynamic/<server_name>/<client_id>", methods=["GET"], strict_slashes=False)
|
||||
def dynamic(server_name, client_id):
|
||||
try:
|
||||
client = next(
|
||||
(keys for client, keys in CLIENTS.items() if client == client_id), None
|
||||
@@ -398,15 +342,13 @@ def dynamic_depticated(server_name, client_id, hash_secret=""):
|
||||
if server and client and key:
|
||||
if server.data["local_server_id"] in client["servers"]:
|
||||
log.info(
|
||||
"Client %s has been requested ssconf for %s", client["name"], server.data["name"]
|
||||
"Client %s wants ssconf for %s", client["name"], server.data["name"]
|
||||
)
|
||||
append_to_log(f"User: {client["name"]}. Server: {server.data['name']} client secret string: {hash_secret}")
|
||||
return {
|
||||
"server": server.data["hostname_for_access_keys"],
|
||||
"server_port": key.port,
|
||||
"password": key.password,
|
||||
"method": key.method,
|
||||
"prefix":SS_PREFIX,
|
||||
"info": "Managed by OutFleet [github.com/house-of-vanity/OutFleet/]",
|
||||
}
|
||||
else:
|
||||
@@ -415,17 +357,18 @@ def dynamic_depticated(server_name, client_id, hash_secret=""):
|
||||
client["name"],
|
||||
server.data["name"],
|
||||
)
|
||||
return WRONG_DOOR
|
||||
return "Hey buddy, i think you got the wrong door the leather-club is two blocks down"
|
||||
except:
|
||||
log.warning("Hack attempt! Client or server doesn't exist. SCAM")
|
||||
return WRONG_DOOR
|
||||
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)
|
||||
@app.route("/dynamic/", methods=["GET"], strict_slashes=False)
|
||||
def _dynamic():
|
||||
log.warning("Hack attempt! Client or server doesn't exist. SCAM")
|
||||
return WRONG_DOOR
|
||||
return (
|
||||
"Hey buddy, i think you got the wrong door the leather-club is two blocks down"
|
||||
)
|
||||
|
||||
|
||||
@app.route("/sync", methods=["GET", "POST"])
|
||||
@@ -475,7 +418,6 @@ def sync():
|
||||
return redirect(url_for("sync"))
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_state_thread = threading.Thread(target=update_state)
|
||||
update_state_thread.start()
|
||||
|
||||
@@ -2,5 +2,4 @@ outline-vpn-api
|
||||
kubernetes
|
||||
PyYAML>=6.0.1
|
||||
Flask>=2.3.3
|
||||
flask-cors
|
||||
bcrypt
|
||||
flask-cors
|
||||
@@ -1,11 +1,3 @@
|
||||
:root {
|
||||
--app-h: 100vh;
|
||||
--app-space-1: 8px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* -- BASE STYLES --
|
||||
* Most of these are inherited from Base, but I want to change a few.
|
||||
@@ -30,7 +22,6 @@ a {
|
||||
.button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: #9d2c2c;
|
||||
border: 1px solid #480b0b;
|
||||
@@ -41,8 +32,7 @@ a {
|
||||
* -- LAYOUT STYLES --
|
||||
* This layout consists of three main elements, `#nav` (navigation bar), `#list` (server list), and `#main` (server content). All 3 elements are within `#layout`
|
||||
*/
|
||||
#nav,
|
||||
#main {
|
||||
#layout, #nav, #list, #main {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -55,7 +45,6 @@ a {
|
||||
background: rgb(37, 42, 58);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Show the "Menu" button on phones */
|
||||
#nav .nav-menu-button {
|
||||
display: block;
|
||||
@@ -66,9 +55,8 @@ a {
|
||||
|
||||
/* When "Menu" is clicked, the navbar should be 80% height */
|
||||
#nav.active {
|
||||
height: 100%;
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
/* Don't show the navigation items... */
|
||||
.nav-inner {
|
||||
display: none;
|
||||
@@ -90,22 +78,19 @@ a {
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#nav .pure-menu-link:hover,
|
||||
#nav .pure-menu-link:focus {
|
||||
background: rgb(55, 60, 90);
|
||||
}
|
||||
|
||||
#nav .pure-menu-link {
|
||||
color: #fff;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
#nav .pure-menu-heading {
|
||||
border-bottom: none;
|
||||
font-size: 110%;
|
||||
color: rgb(75, 113, 151);
|
||||
}
|
||||
#nav .pure-menu-link:hover,
|
||||
#nav .pure-menu-link:focus {
|
||||
background: rgb(55, 60, 90);
|
||||
}
|
||||
#nav .pure-menu-link {
|
||||
color: #fff;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
#nav .pure-menu-heading {
|
||||
border-bottom: none;
|
||||
font-size:110%;
|
||||
color: rgb(75, 113, 151);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
@@ -125,118 +110,94 @@ a {
|
||||
margin-right: 0.5em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.server-label-personal {
|
||||
background: #ffc94c;
|
||||
}
|
||||
|
||||
.server-label-work {
|
||||
background: #41ccb4;
|
||||
}
|
||||
|
||||
.server-label-travel {
|
||||
background: #40c365;
|
||||
}
|
||||
|
||||
|
||||
/* server Item Styles */
|
||||
.server-content-title {
|
||||
padding: var(--app-space-1);
|
||||
/* color: #ffffff; */
|
||||
}
|
||||
|
||||
.server-item {
|
||||
padding: var(--app-space-1);
|
||||
/* color: #ffffff; */
|
||||
cursor: pointer;
|
||||
padding: 0.9em 1em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-left: 6px solid transparent;
|
||||
}
|
||||
.server-name {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.server-name,
|
||||
.server-info {
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
}
|
||||
.server-info {
|
||||
color: #999;
|
||||
font-size: 80%;
|
||||
}
|
||||
.server-comment {
|
||||
font-size: 90%;
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
.server-add {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 150%;
|
||||
color: #999;
|
||||
}
|
||||
.server-add:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.server-item-selected {
|
||||
background: #eeeeee;
|
||||
}
|
||||
.server-item-unread {
|
||||
border-left: 6px solid #1b98f8;
|
||||
}
|
||||
.server-item-broken {
|
||||
border-left: 6px solid #880d06;
|
||||
}
|
||||
|
||||
.server-item:hover {
|
||||
background: #d1d0d0;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.server-name,
|
||||
.server-info {
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
color: #999;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.server-comment {
|
||||
font-size: 90%;
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
|
||||
.server-add {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 150%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.server-add:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.server-item-selected {
|
||||
background: #eeeeee;
|
||||
}
|
||||
|
||||
.server-item-unread {
|
||||
border-left: 6px solid #1b98f8;
|
||||
}
|
||||
|
||||
.server-item-broken {
|
||||
border-left: 6px solid #880d06;
|
||||
}
|
||||
|
||||
|
||||
/* server Content Styles */
|
||||
.server-content-header,
|
||||
.server-content-body,
|
||||
.server-content-footer {
|
||||
.server-content-header, .server-content-body, .server-content-footer {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
.server-content-header {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.server-content-header {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.server-content-title {
|
||||
margin: 0.5em 0 0;
|
||||
}
|
||||
.server-content-subtitle {
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
.server-content-subtitle span {
|
||||
color: #999;
|
||||
}
|
||||
.server-content-controls {
|
||||
margin-top: 2em;
|
||||
text-align: right;
|
||||
}
|
||||
.server-content-controls .secondary-button {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.server-content-title {
|
||||
margin: 0.5em 0 0;
|
||||
}
|
||||
|
||||
.server-content-subtitle {
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.server-content-subtitle span {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.server-content-controls {
|
||||
margin-top: 2em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.server-content-controls .secondary-button {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.server-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.server-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
@@ -246,17 +207,22 @@ a {
|
||||
*/
|
||||
@media (min-width: 40em) {
|
||||
|
||||
/* Move the layout over so we can fit the nav + list in on the left */
|
||||
#layout {
|
||||
padding-left:500px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* These are position:fixed; elements that will be in the left 500px of the screen */
|
||||
#nav {
|
||||
#nav, #list {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#nav {
|
||||
/* margin-left: -500px; */
|
||||
width: 150px;
|
||||
margin-left:-500px;
|
||||
width:150px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -271,6 +237,12 @@ a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#list {
|
||||
margin-left: -350px;
|
||||
width: 100%;
|
||||
height: 33%;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#main {
|
||||
position: fixed;
|
||||
@@ -279,8 +251,7 @@ a {
|
||||
bottom: 0;
|
||||
left: 150px;
|
||||
overflow: auto;
|
||||
width: auto;
|
||||
/* so that it's not 100% */
|
||||
width: auto; /* so that it's not 100% */
|
||||
}
|
||||
|
||||
}
|
||||
@@ -293,7 +264,12 @@ a {
|
||||
@media (min-width: 60em) {
|
||||
|
||||
/* This will take up the entire height, and be a little thinner */
|
||||
|
||||
#list {
|
||||
margin-left: -350px;
|
||||
width:350px;
|
||||
height: 100%;
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* This will now take up it's own column, so don't need position: fixed; */
|
||||
#main {
|
||||
@@ -305,171 +281,72 @@ a {
|
||||
|
||||
|
||||
.alert {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
line-height: 1.8;
|
||||
border-radius: 5px;
|
||||
cursor: hand;
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
line-height: 1.8;
|
||||
border-radius: 5px;
|
||||
cursor: hand;
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.alertCheckbox {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
:checked+.alert {
|
||||
display: none;
|
||||
:checked + .alert {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alertText {
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
font-size: 150%;
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.alertClose {
|
||||
float: right;
|
||||
padding-top: 0px;
|
||||
font-size: 120%;
|
||||
float: right;
|
||||
padding-top: 0px;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #DDD;
|
||||
color: #999;
|
||||
background-color: #EEE;
|
||||
border: 1px solid #DDD;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #EFE;
|
||||
border: 1px solid #DED;
|
||||
color: #9A9;
|
||||
background-color: #EFE;
|
||||
border: 1px solid #DED;
|
||||
color: #9A9;
|
||||
}
|
||||
|
||||
.notice {
|
||||
background-color: #EFF;
|
||||
border: 1px solid #DEE;
|
||||
color: #9AA;
|
||||
background-color: #EFF;
|
||||
border: 1px solid #DEE;
|
||||
color: #9AA;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: #FDF7DF;
|
||||
border: 1px solid #FEEC6F;
|
||||
color: #C9971C;
|
||||
background-color: #FDF7DF;
|
||||
border: 1px solid #FEEC6F;
|
||||
color: #C9971C;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #FEE;
|
||||
border: 1px solid #EDD;
|
||||
color: #A66;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#layout {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"menu entety-menu content";
|
||||
height: var(--app-h);
|
||||
justify-content: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#menu {
|
||||
grid-area: menu;
|
||||
width: 150px;
|
||||
height: 100%;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
#entety-menu {
|
||||
grid-area: entety-menu;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
#content {
|
||||
grid-area: content;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.srcollable-list-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
@media (max-width: 60em) {
|
||||
#entety-menu {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 40em) {
|
||||
#layout {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"menu"
|
||||
"entety-menu"
|
||||
"content";
|
||||
grid-template-rows: min-content min-content;
|
||||
}
|
||||
|
||||
#menu {
|
||||
width: 100vw;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
#entety-menu {
|
||||
width: 100vw;
|
||||
height: initial;
|
||||
}
|
||||
|
||||
.srcollable-list-content{
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
column-gap: var(--app-space-1);
|
||||
}
|
||||
|
||||
.srcollable-list-content > * {
|
||||
scroll-snap-align: start;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 250px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.server-checkbox {
|
||||
display: flex;
|
||||
|
||||
}
|
||||
|
||||
.server-checkbox input {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pure-form-message {
|
||||
padding: 0.2em;
|
||||
}
|
||||
#search-form {
|
||||
padding: var(--app-space-1);
|
||||
}
|
||||
#entety-menu-search {
|
||||
width: 100%;
|
||||
background-color: #FEE;
|
||||
border: 1px solid #EDD;
|
||||
color: #A66;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -2,83 +2,67 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="entety-menu">
|
||||
<div id="list">
|
||||
<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 pure-g">
|
||||
<h1 class="server-content-title">Clients</h1>
|
||||
<form id="search-form" class="pure-form">
|
||||
<input placeholder="🔍" type="text" id="entety-menu-search" />
|
||||
</form>
|
||||
<div data-search-box class="srcollable-list-content">
|
||||
{% for client, values in CLIENTS.items() %}
|
||||
<div
|
||||
class="server-item server-item-{% if client == selected_client %}unread{% else %}selected{% endif %} pure-g">
|
||||
<div class="" onclick="location.href='/clients?selected_client={{ client }}';">
|
||||
<h5 data-search class="server-name">{{ values["name"] }}</h5>
|
||||
<h4 class="server-info">{{ values["servers"]|length }} server{% if values["servers"]|length >1
|
||||
%}s{%endif%}</h4>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for client, values in CLIENTS.items() %}
|
||||
<div class="server-item server-item-{% if client == selected_client %}unread{% else %}selected{% endif %} pure-g">
|
||||
<div class="pure-u-3-4" onclick="location.href='/clients?selected_client={{ client }}';">
|
||||
<h5 class="server-name">{{ values["name"] }}</h5>
|
||||
<h4 class="server-info">Allowed {{ values["servers"]|length }} server{% if values["servers"]|length >1 %}s{%endif%}</h4>
|
||||
</div>
|
||||
|
||||
<div onclick="location.href='/clients?add_client=True';" class="server-item server-add pure-g">
|
||||
</div>
|
||||
{% 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>
|
||||
|
||||
{% if add_client %}
|
||||
<div id="content">
|
||||
<div class="">
|
||||
<div class="pure-u-1-3">
|
||||
<div class="server-content-header pure-g">
|
||||
<div class="">
|
||||
<div class="pure-u-1-2">
|
||||
<h1 class="server-content-title">Add new client</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-content-body">
|
||||
<form action="/add_client" class="pure-form pure-form-stacked" method="POST">
|
||||
<fieldset>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 ">
|
||||
<input type="text" class="" name="name" required placeholder="Name" />
|
||||
</div>
|
||||
<div class="pure-u-1 ">
|
||||
<input type="text" class="" name="comment" placeholder="Comment" />
|
||||
</div>
|
||||
<div class="pure-checkbox">
|
||||
{% for server in SERVERS %}
|
||||
<div class="server-checkbox">
|
||||
<input type="checkbox" id="option{{loop.index0}}" name="servers"
|
||||
value="{{server.info()['local_server_id']}}">
|
||||
<label class="pure-checkbox" for="option{{loop.index0}}">{{server.info()["comment"]}}
|
||||
<span class="pure-form-message">ID: {{server.info()['local_server_id'][0:8]}}</span>
|
||||
<span class="pure-form-message">Comment: {{server.info()["name"]}}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
<form action="/add_client" class="pure-form pure-form-stacked" method="POST">
|
||||
<fieldset>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<input type="text" class="pure-u-23-24" name="name" required placeholder="Name"/>
|
||||
</div>
|
||||
<button type="submit" class="pure-button pure-input-1 pure-button-primary">Add</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</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 %}
|
||||
<label class="pure-checkbox" for="option{{loop.index0}}">{{server.info()["name"]}}
|
||||
<input type="checkbox" id="option{{loop.index0}}" name="servers" value="{{server.info()['local_server_id']}}"></label>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button type="submit" class="pure-button pure-input-1 pure-button-primary">Add</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if selected_client and not add_client %}
|
||||
{% set client = CLIENTS[selected_client] %}
|
||||
{% set client = CLIENTS[selected_client] %}
|
||||
|
||||
<div id="content">
|
||||
<div class="">
|
||||
<div class="pure-u-1-2">
|
||||
<div class="server-content-header pure-g">
|
||||
<div class="">
|
||||
<div class="pure-u-1-2">
|
||||
<h1 class="server-content-title">{{client['name']}}</h1>
|
||||
<h4 class="server-info">{{ client['comment'] }}</h4>
|
||||
<h4 class="server-info">id {{ selected_client }}</h4>
|
||||
@@ -86,82 +70,105 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-content-body">
|
||||
<form action="/add_client" class="pure-form pure-form-stacked" method="POST">
|
||||
<fieldset>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 ">
|
||||
<input type="text" class="pure-u-1" name="name" required value="{{client['name']}}" />
|
||||
<input type="hidden" class="pure-u-1" name="old_name" required value="{{client['name']}}" />
|
||||
</div>
|
||||
<div class="pure-u-1 ">
|
||||
<input type="text" class="pure-u-1" name="comment" value="{{client['comment']}}" />
|
||||
</div>
|
||||
<input type="hidden" class="pure-u-1" name="user_id" value="{{selected_client}}" />
|
||||
|
||||
<div class="pure-checkbox">
|
||||
|
||||
<p>Allow access to:</p>
|
||||
|
||||
{% for server in SERVERS %}
|
||||
<div class="server-checkbox">
|
||||
<input {% if server.info()['local_server_id'] in client['servers'] %}checked{%endif%}
|
||||
type="checkbox" id="option{{loop.index0}}" name="servers"
|
||||
value="{{server.info()['local_server_id']}}">
|
||||
<label class="pure-checkbox" for="option{{loop.index0}}">{{server.info()["comment"]}}
|
||||
<span class="pure-form-message">ID: {{server.info()['local_server_id'][0:8]}}</span>
|
||||
<span class="pure-form-message">Comment: {{server.info()["name"]}}</span>
|
||||
<span class="pure-form-message">{% if
|
||||
server.info()['local_server_id'] in client['servers'] %}Usage: {% for key in
|
||||
server.data["keys"] %}{% if key.name == client['name'] %}{{ (key.used_bytes if
|
||||
key.used_bytes else 0) | filesizeformat }}{% endif %}{% endfor
|
||||
%}{%endif%}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form action="/add_client" class="pure-form pure-form-stacked" method="POST">
|
||||
<fieldset>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<input type="text" class="pure-u-1" name="name" required value="{{client['name']}}"/>
|
||||
<input type="hidden" class="pure-u-1" name="old_name" required value="{{client['name']}}"/>
|
||||
</div>
|
||||
<div class="pure-g pure-form pure-form-stacked">
|
||||
<div class="pure-u-1-2">
|
||||
<button type="submit" class="pure-button pure-button-primary button">Save and apply</button>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<input type="text" class="pure-u-1" name="comment" value="{{client['comment']}}"/>
|
||||
</div>
|
||||
<input type="hidden" class="pure-u-1" name="user_id" value="{{selected_client}}"/>
|
||||
|
||||
</fieldset>
|
||||
</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}}" />
|
||||
<button type="submit" class="pure-button pure-button-primary delete-button button">Delete
|
||||
Client</button>
|
||||
<input type="checkbox" id="agree" name="agree" required>
|
||||
</form>
|
||||
<div class="pure-checkbox">
|
||||
<p>Allow access to:</p>
|
||||
{% for server in SERVERS %}
|
||||
<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%}
|
||||
type="checkbox" id="option{{loop.index0}}" name="servers" value="{{server.info()['local_server_id']}}"></label>
|
||||
|
||||
<div>
|
||||
<h3>Invite text</h3>
|
||||
<hr>
|
||||
<p>Install Outline VPN. Copy and paste the keys below into the Outline client.
|
||||
The same keys can be used simultaneously on multiple devices.</p>
|
||||
{% for server in SERVERS -%}
|
||||
{% if server.info()['local_server_id'] in client['servers'] %}
|
||||
{% set salt = bcrypt.gensalt() %}
|
||||
{% set secret_string = server.info()['local_server_id'] + selected_client %}
|
||||
{% set hash_secret = bcrypt.hashpw(
|
||||
password=secret_string.encode('utf-8'),
|
||||
salt=salt).decode('utf-8') %}
|
||||
{% endfor %}
|
||||
|
||||
<p><b>Server location:</b> {{server.info()['name']}}</p>
|
||||
<p><b>Client link:</b> {% for key in server.data["keys"] %}{% if key.key_id == client['name']
|
||||
%}ssconf://{{ dynamic_hostname
|
||||
}}/dynamic/{{server.info()['local_server_id'][0:SECRET_LINK_LENGTH]}}{{selected_client[0:SECRET_LINK_LENGTH]}}{{hash_secret[SECRET_LINK_PREFIX|length:]}}#{{server.info()['comment']}}{%
|
||||
endif %}{% endfor %}</p>
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button type="submit" class="pure-button pure-input-1 pure-button-primary">Save and apply</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<h3>Invite text</h3><hr>
|
||||
<textarea style="width: 100%; rows=10">
|
||||
Install OutLine VPN. Copy and paste below keys to OutLine client.
|
||||
Same keys will work simultaneously on many devices.
|
||||
{% for server in SERVERS -%}
|
||||
|
||||
{% if server.info()['local_server_id'] in client['servers'] %}
|
||||
{{server.info()['name']}}
|
||||
```{% 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 %}```
|
||||
{% endif %}
|
||||
{%- endfor -%}</textarea>
|
||||
</div>
|
||||
<hr>
|
||||
<div style="padding-top: 15px; padding-bottom: 15px">
|
||||
<div class="pure-u-1">
|
||||
<h3>Dynamic Access Keys</h3>
|
||||
<table class="pure-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Server</th>
|
||||
<th>Dynamic</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for server in SERVERS %}
|
||||
{% if server.info()['local_server_id'] in client['servers'] %}
|
||||
<tr>
|
||||
<td>{{ server.info()['name'] }}</td>
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1">
|
||||
<h3>SS Links</h3>
|
||||
<table class="pure-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Server</th>
|
||||
<th>SSlink</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for server in SERVERS %}
|
||||
{% if server.info()['local_server_id'] in client['servers'] %}
|
||||
<tr>
|
||||
<td>{{ server.info()['name'] }}</td>
|
||||
<td>
|
||||
<pre style="font-size: 10pt">{% for key in server.data["keys"] %}{% if key.key_id == client['name'] %}{{ key.access_url }}{% endif %}{% endfor %}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
<form action="/del_client" class="pure-form pure-form-stacked" method="POST">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
|
||||
<div id="entety-menu">
|
||||
<div>
|
||||
<h1 class="server-content-title">Servers</h1>
|
||||
<div class="srcollable-list-content">
|
||||
<div id="list" class="pure-u-1-3" xmlns="http://www.w3.org/1999/html">
|
||||
<div class="server-item pure-g">
|
||||
<h1 class="server-content-title">Servers</h1>
|
||||
</div>
|
||||
{% for server in SERVERS %}
|
||||
{% set total_traffic = namespace(total_bytes=0) %}
|
||||
{% set list_ns = namespace(total_bytes=0) %}
|
||||
{% for key in server.data["keys"] %}
|
||||
{% if key.used_bytes %}
|
||||
{% set total_traffic.total_bytes = total_traffic.total_bytes + key.used_bytes %}
|
||||
{% set list_ns.total_bytes = list_ns.total_bytes + key.used_bytes %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="server-item server-item-{% if loop.index0 == selected_server|int %}unread{% else %}selected{% endif %} pure-g">
|
||||
@@ -21,7 +19,7 @@
|
||||
<h4 class="server-info">{{ '/'.join(server.info()["url"].split('/')[0:-1]) }}</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">Traffic: {{ total_traffic.total_bytes | filesizeformat }}</h4>
|
||||
<h4 class="server-info">Traffic: {{ list_ns.total_bytes | filesizeformat }}</h4>
|
||||
<h4 class="server-info">v.{{ server.info()["version"] }}</h4>
|
||||
<p class="server-comment">
|
||||
{{ server.info()["comment"] }}
|
||||
@@ -29,7 +27,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div onclick="location.href='/?add_server=True';" class="server-item server-add pure-g">
|
||||
<div class="pure-u-1">
|
||||
+
|
||||
@@ -37,37 +34,36 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if add_server %}
|
||||
<div id="content">
|
||||
<div >
|
||||
<div class="pure-u-1-3">
|
||||
<div class="server-content-header pure-g">
|
||||
<div >
|
||||
<div class="pure-u-1-2">
|
||||
<h1 class="server-content-title">Add new server</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-content-body">
|
||||
<form action="/add_server" class="pure-form pure-form-stacked" method="POST">
|
||||
<fieldset>
|
||||
<div >
|
||||
<label for="url">Server management URL</label>
|
||||
<input type="text" class="pure-u-1" name="url" placeholder="https://example.com:5743/KSsdywe6Sdb..."/>
|
||||
<label for="cert">Server management Certificate</label>
|
||||
<input type="text" class="pure-u-1" name="cert" placeholder="B5DD2443DAF..."/>
|
||||
<label for="cert">Server Comment
|
||||
<span class="pure-form-message">This will be exposed to client and will be used as "Server name" in client Outline app</span>
|
||||
</label>
|
||||
<input type="text" class="pure-u-1" name="comment" placeholder="e.g. server location"/>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<input type="text" class="pure-u-23-24" name="url" placeholder="Server management URL"/>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<input type="text"class="pure-u-23-24" name="cert" placeholder="Certificate"/>
|
||||
</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>
|
||||
<button type="submit" class="pure-button pure-input-1 pure-button-primary">Add</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if SERVERS|length != 0 and not add_server %}
|
||||
|
||||
{% if selected_server is none %}
|
||||
@@ -75,12 +71,11 @@
|
||||
{% else %}
|
||||
{% set server = SERVERS[selected_server|int] %}
|
||||
{% endif %}
|
||||
<div id="content">
|
||||
<div >
|
||||
<div id="main" class="pure-u-1">
|
||||
<div class="server-content">
|
||||
<div class="server-content-header pure-g">
|
||||
<div>
|
||||
<h1 class="content-title">{{server.info()["name"]}}</h1>
|
||||
<div class="pure-u-1-2">
|
||||
<h1 class="server-content-title">{{server.info()["name"]}}</h1>
|
||||
<p class="server-content-subtitle">
|
||||
<span>v.{{server.info()["version"]}} {{server.info()["local_server_id"]}}</span>
|
||||
</p>
|
||||
@@ -100,38 +95,35 @@
|
||||
<form class="pure-form pure-form-stacked" method="POST">
|
||||
<fieldset>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 ">
|
||||
<label for="name">Server Name
|
||||
<span class="pure-form-message">This will not be exposed to client</span>
|
||||
</label>
|
||||
<input type="text" id="name" class="pure-u-1" name="name" value="{{server.info()['name']}}"/>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<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>
|
||||
<input type="text" id="name" class="pure-u-23-24" name="name" value="{{server.info()['name']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 ">
|
||||
<label for="comment">Comment</br>
|
||||
<span class="pure-form-message">This will be exposed to client and will be used as "Server name" in client Outline app</span></label>
|
||||
<input type="text" id="comment" class="pure-u-1" name="comment" value="{{server.info()['comment']}}"/>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="comment">Comment</br>This value will be used as "Server name" in client app.</label>
|
||||
<input type="text" id="comment" class="pure-u-23-24" name="comment" value="{{server.info()['comment']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="port_for_new_access_keys">Port For New Access Keys</label>
|
||||
<input type="text" id="port_for_new_access_keys" class="pure-u-1" name="port_for_new_access_keys" value="{{server.info()['port_for_new_access_keys']}}"/>
|
||||
<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']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 ">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="hostname_for_access_keys">Hostname For Access Keys</label>
|
||||
<input type="text" id="hostname_for_access_keys" class="pure-u-1" name="hostname_for_access_keys" value="{{server.info()['hostname_for_access_keys']}}"/>
|
||||
<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']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 ">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="url">Server URL</label>
|
||||
<input type="text" readonly id="url" class="pure-u-1" name="url" value="{{server.info()['url']}}"/>
|
||||
<input type="text" readonly id="url" class="pure-u-23-24" name="url" value="{{server.info()['url']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 ">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="cert">Server Access Certificate</label>
|
||||
<input type="text" readonly id="cert" class="pure-u-1" name="cert" value="{{server.info()['cert']}}"/>
|
||||
<input type="text" readonly id="cert" class="pure-u-23-24" name="cert" value="{{server.info()['cert']}}"/>
|
||||
</div>
|
||||
<div class="pure-u-1 ">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<label for="created_timestamp_ms">Created</label>
|
||||
<input type="text" readonly id="created_timestamp_ms" class="pure-u-1" 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>
|
||||
<input type="hidden" readonly id="server_id" name="server_id" value="{{server.info()['local_server_id']}}"/>
|
||||
<input type="hidden" readonly id="server_id" class="pure-u-23-24" name="server_id" value="{{server.info()['local_server_id']}}"/>
|
||||
</div>
|
||||
<p>Share anonymous metrics</p>
|
||||
<label for="metrics_enabled" class="pure-radio">
|
||||
@@ -152,7 +144,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
$url = Read-Host "Please enter the URL for the JSON configuration"
|
||||
$comment = Read-Host "Comment [server, country, etc]"
|
||||
$port = Read-Host "Please enter the port to use for sslocal"
|
||||
|
||||
$version = "1.21.0"
|
||||
$archiveUrl = "https://github.com/shadowsocks/shadowsocks-rust/releases/download/v${version}/shadowsocks-v${version}.x86_64-pc-windows-gnu.zip"
|
||||
$downloadPath = "$HOME\shadowsocks-rust\shadowsocks.zip"
|
||||
$extractPath = "$HOME\shadowsocks-rust"
|
||||
$scriptUrl = "https://raw.githubusercontent.com/house-of-vanity/OutFleet/refs/heads/master/tools/windows_task.ps1"
|
||||
$cmdFilePath = "$extractPath\run_${comment}.cmd"
|
||||
$taskName = "Shadowsocks_Task_${comment}"
|
||||
$logFile = "$extractPath\Log_${comment}.log"
|
||||
|
||||
|
||||
if ($url -notmatch "^[a-z]+://") {
|
||||
$url = "https://$url"
|
||||
} elseif ($url -like "ssconf://*") {
|
||||
$url = $url -replace "^ssconf://", "https://"
|
||||
}
|
||||
|
||||
function Test-Admin {
|
||||
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
if (-Not (Test-Admin)) {
|
||||
Write-Host "Error: This script requires administrator privileges. Please run PowerShell as administrator." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure the extraction directory exists
|
||||
if (-Not (Test-Path -Path $extractPath)) {
|
||||
New-Item -ItemType Directory -Path $extractPath
|
||||
}
|
||||
|
||||
# Download the archive
|
||||
Invoke-WebRequest -Uri $archiveUrl -OutFile $downloadPath
|
||||
|
||||
# Extract the archive
|
||||
Expand-Archive -Path $downloadPath -DestinationPath $extractPath -Force
|
||||
|
||||
# Check if sslocal.exe exists
|
||||
if (-Not (Test-Path -Path "$extractPath\sslocal.exe")) {
|
||||
Write-Host "Error: sslocal.exe not found in $extractPath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Download the windows_task.ps1 script
|
||||
Invoke-WebRequest -Uri $scriptUrl -OutFile "$extractPath\windows_task.ps1"
|
||||
|
||||
# Build Batch file content
|
||||
$batchContent = @"
|
||||
@echo off
|
||||
set scriptPath=""$extractPath\windows_task.ps1""
|
||||
powershell.exe -ExecutionPolicy Bypass -File %scriptPath% ""$url"" ""$extractPath\sslocal.exe"" ""$port""
|
||||
"@
|
||||
|
||||
|
||||
$batchContent | Set-Content -Path $cmdFilePath
|
||||
|
||||
# Create or update Task Scheduler
|
||||
$action = New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c $cmdFilePath > $logFile"
|
||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||
|
||||
# Check if the task already exists
|
||||
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
|
||||
if ($existingTask) {
|
||||
Write-Host "Task $taskName already exists. Updating the task..."
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
|
||||
}
|
||||
|
||||
# Register the new or updated task
|
||||
Register-ScheduledTask -Action $action -Trigger $trigger -Principal $principal -Settings $settings -TaskName $taskName
|
||||
|
||||
Write-Host "Task $taskName has been created/updated successfully."
|
||||
|
||||
# Optionally, start the task immediately
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "Task $taskName has been started."
|
||||
@@ -1,100 +0,0 @@
|
||||
if ($args.Count -lt 3) {
|
||||
Write-Host "Usage: windows_task.ps1 <url> <sslocal_path> <local_port>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$url = $args[0]
|
||||
$sslocalPath = $args[1]
|
||||
$localPort = $args[2]
|
||||
|
||||
$localAddr = "localhost"
|
||||
$checkInterval = 60
|
||||
$previousPassword = ""
|
||||
|
||||
# Function to get the process ID of the process listening on a specific port
|
||||
function Get-ProcessByPort {
|
||||
param (
|
||||
[int]$port
|
||||
)
|
||||
|
||||
# Use Get-NetTCPConnection to find the process listening on the given port
|
||||
$connection = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue | Where-Object { $_.State -eq 'Listen' }
|
||||
|
||||
if ($connection) {
|
||||
# Get the owning process ID (OwningProcess) from the connection
|
||||
$pid = $connection.OwningProcess
|
||||
return Get-Process -Id $pid -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# Function to start sslocal
|
||||
function Start-SSLocal {
|
||||
param (
|
||||
[string]$method,
|
||||
[string]$password,
|
||||
[string]$server,
|
||||
[int]$serverPort
|
||||
)
|
||||
|
||||
# Form the Shadowsocks connection string
|
||||
$credentials = "${method}:${password}@${server}:${serverPort}"
|
||||
$encodedCredentials = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($credentials))
|
||||
$ssUrl = "ss://$encodedCredentials"
|
||||
|
||||
# Get the process listening on the specified port and kill it if found
|
||||
$process = Get-ProcessByPort -port $localPort
|
||||
if ($process) {
|
||||
Write-Host "Killing process $($process.Id) using port $localPort"
|
||||
Stop-Process -Id $process.Id -Force
|
||||
}
|
||||
|
||||
# Log the sslocal restart
|
||||
Write-Host "Starting sslocal with method: $method, server: $server, port: $serverPort"
|
||||
|
||||
# Start sslocal with the provided arguments
|
||||
Start-Process -NoNewWindow -FilePath $sslocalPath -ArgumentList "--local-addr ${localAddr}:${localPort} --server-url $ssUrl"
|
||||
}
|
||||
|
||||
# Main loop
|
||||
while ($true) {
|
||||
try {
|
||||
if ($url -notmatch "mode=json") {
|
||||
$delimiter = "?"
|
||||
if ($url -match "\?") {
|
||||
$delimiter = "&"
|
||||
}
|
||||
$url = "$url${delimiter}mode=json"
|
||||
}
|
||||
# Download and parse the JSON
|
||||
$jsonContent = Invoke-WebRequest -Uri $url -UseBasicParsing | Select-Object -ExpandProperty Content
|
||||
$json = $jsonContent | ConvertFrom-Json
|
||||
|
||||
# Extract the necessary fields
|
||||
$method = $json.method
|
||||
$password = $json.password
|
||||
$server = $json.server
|
||||
$serverPort = $json.server_port
|
||||
|
||||
# Log current password and server information
|
||||
Write-Host "Checking server: $server, port: $serverPort"
|
||||
|
||||
# Check if the password has changed
|
||||
if ($password -ne $previousPassword) {
|
||||
# Start/restart sslocal
|
||||
Start-SSLocal -method $method -password $password -server $server -serverPort $serverPort
|
||||
$previousPassword = $password
|
||||
Write-Host "Password changed, restarting sslocal."
|
||||
} else {
|
||||
Write-Host "Password has not changed."
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Host "Error occurred: $_"
|
||||
}
|
||||
|
||||
# Wait for the next check
|
||||
Start-Sleep -Seconds $checkInterval
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user