28 Commits

Author SHA1 Message Date
Alexandr Bogomyakov
26fd9c3e85 Update windows_task.ps1
Some checks failed
Docker hub build / docker (push) Has been cancelled
2025-02-26 11:55:30 +02:00
Alexandr Bogomyakov
eb9f199d57 Add log
Some checks failed
Docker hub build / docker (push) Has been cancelled
2024-10-26 11:27:51 +03:00
Alexandr Bogomyakov
c9c21f1baa log keys 2024-10-26 10:45:33 +03:00
Alexandr Bogomyakov
9680ce802d Update README.md
Some checks failed
Docker hub build / docker (push) Has been cancelled
2024-09-28 22:04:27 +03:00
Alexandr Bogomyakov
e4fd6ea5d7 Update README.md 2024-09-28 22:03:07 +03:00
Alexandr Bogomyakov
22cca991fc Update README.md 2024-09-28 21:50:23 +03:00
Alexandr Bogomyakov
dd5f0c4e2f Update windows-helper.ps1 2024-09-28 21:37:42 +03:00
Alexandr Bogomyakov
98d993423d Update windows-helper.ps1 2024-09-28 21:19:42 +03:00
Alexandr Bogomyakov
db382f2b27 Rename windows_task.ps1 to tools/windows_task.ps1 2024-09-28 21:19:28 +03:00
Alexandr Bogomyakov
7e08bd465b Create windows-helper.ps1 2024-09-28 21:19:09 +03:00
Alexandr Bogomyakov
f7ce671427 Update windows_task.ps1 2024-09-28 21:17:34 +03:00
Alexandr Bogomyakov
dceb07137a Update windows_task.ps1 2024-09-28 20:44:08 +03:00
Alexandr Bogomyakov
e41febe061 Update windows_task.ps1 2024-09-28 20:42:36 +03:00
Alexandr Bogomyakov
2397a05a08 Update windows_task.ps1 2024-09-28 20:36:27 +03:00
Alexandr Bogomyakov
c940e9f38b Update windows_task.ps1 2024-09-28 20:31:25 +03:00
Alexandr Bogomyakov
315be97354 Update windows_task.ps1 2024-09-28 19:53:01 +03:00
Alexandr Bogomyakov
8a5e1d2d69 Update and rename windows_service.ps1 to windows_task.ps1 2024-09-28 19:32:30 +03:00
Alexandr Bogomyakov
22eb5ec7af Create windows_service.ps1 2024-09-28 18:15:43 +03:00
Alexandr Bogomyakov
3da1d4f5f7 Bump version 2024-06-12 12:21:03 +03:00
Alexandr Bogomyakov
c8dcd4439c Fix logging 2024-06-12 12:20:09 +03:00
Alexandr Bogomyakov
b01d86251c fix logging 2024-06-12 12:08:41 +03:00
AB
58be345610 New UI fix
Co-authored-by: XakPlant <xakplant@users.noreply.github.com>
2024-04-28 15:06:58 +03:00
AB
48521cb8a3 New UI
Co-authored-by: XakPlant <xakplant@users.noreply.github.com>
2024-04-28 13:15:07 +03:00
35f57de110 Reworked dynamic keys link generation. 2024-04-23 19:30:14 +03:00
423c408893 new links 2024-04-19 20:10:31 +03:00
Alexandr Bogomyakov
788797f3ef Merge pull request #10 from Sanapach/master
Outfleet::fix empty config
2024-04-17 20:16:39 +03:00
c9ae1bbbbd Outfleet::fix empty config 2024-04-17 20:15:09 +03:00
5cc32b18af Outfleet::fix empty config 2024-04-17 20:13:10 +03:00
10 changed files with 795 additions and 330 deletions

View File

@@ -82,6 +82,15 @@ 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. 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 ## Authors

11
lib.py
View File

@@ -33,10 +33,19 @@ def get_config():
try: try:
with open(args.config, "r") as file: with open(args.config, "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
if config == None:
config = {
"servers": {},
"clients": {}
}
except: except:
try: try:
with open(args.config, "w"): with open(args.config, "w"):
pass config = {
"servers": {},
"clients": {}
}
yaml.safe_dump(config, file)
except Exception as exp: except Exception as exp:
log.error(f"Couldn't create config. {exp}") log.error(f"Couldn't create config. {exp}")
return None return None

107
main.py
View File

@@ -7,13 +7,24 @@ import random
import string import string
import argparse import argparse
import uuid import uuid
import bcrypt
import k8s 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 werkzeug.routing import BaseConverter
from lib import Server, write_config, get_config, args, lock 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) logging.getLogger("werkzeug").setLevel(logging.ERROR)
@@ -30,14 +41,20 @@ formatter = logging.Formatter(
) )
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
log.addHandler(file_handler) log.addHandler(file_handler)
duplicate_filter = DuplicateFilter()
log.addFilter(duplicate_filter)
CFG_PATH = args.config CFG_PATH = args.config
NAMESPACE = k8s.NAMESPACE NAMESPACE = k8s.NAMESPACE
SERVERS = list() SERVERS = list()
BROKEN_SERVERS = list() BROKEN_SERVERS = list()
CLIENTS = dict() CLIENTS = dict()
VERSION = '5' VERSION = '8.1'
SECRET_LINK_LENGTH = 8
SECRET_LINK_PREFIX = '$2b$12$'
SS_PREFIX = "\u0005\u00DC\u005F\u00E0\u0001\u0020"
HOSTNAME = "" HOSTNAME = ""
WRONG_DOOR = "Hey buddy, i think you got the wrong door the leather-club is two blocks down"
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
@@ -52,9 +69,7 @@ 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(timer=40): def update_state(timer=40):
while True: while True:
with lock: with lock:
global SERVERS global SERVERS
@@ -136,8 +151,11 @@ def clients():
return render_template( return render_template(
"clients.html", "clients.html",
SERVERS=SERVERS, SERVERS=SERVERS,
bcrypt=bcrypt,
CLIENTS=CLIENTS, CLIENTS=CLIENTS,
VERSION=VERSION, VERSION=VERSION,
SECRET_LINK_LENGTH=SECRET_LINK_LENGTH,
SECRET_LINK_PREFIX=SECRET_LINK_PREFIX,
K8S_NAMESPACE=k8s.NAMESPACE, K8S_NAMESPACE=k8s.NAMESPACE,
nt=request.args.get("nt"), nt=request.args.get("nt"),
nl=request.args.get("nl"), nl=request.args.get("nl"),
@@ -181,6 +199,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":
@@ -302,8 +321,70 @@ 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"], strict_slashes=False) def append_to_log(log_entry):
def dynamic(server_name, client_id): 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=""):
try: try:
client = next( client = next(
(keys for client, keys in CLIENTS.items() if client == client_id), None (keys for client, keys in CLIENTS.items() if client == client_id), None
@@ -317,13 +398,15 @@ def dynamic(server_name, client_id):
if server and client and key: if server and client and key:
if server.data["local_server_id"] in client["servers"]: if server.data["local_server_id"] in client["servers"]:
log.info( log.info(
"Client %s wants ssconf for %s", client["name"], server.data["name"] "Client %s has been requested 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 { return {
"server": server.data["hostname_for_access_keys"], "server": server.data["hostname_for_access_keys"],
"server_port": key.port, "server_port": key.port,
"password": key.password, "password": key.password,
"method": key.method, "method": key.method,
"prefix":SS_PREFIX,
"info": "Managed by OutFleet [github.com/house-of-vanity/OutFleet/]", "info": "Managed by OutFleet [github.com/house-of-vanity/OutFleet/]",
} }
else: else:
@@ -332,18 +415,17 @@ def dynamic(server_name, client_id):
client["name"], client["name"],
server.data["name"], server.data["name"],
) )
return "Hey buddy, i think you got the wrong door the leather-club is two blocks down" return WRONG_DOOR
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 WRONG_DOOR
@app.route("/dynamic/", methods=["GET"], strict_slashes=False)
@app.route("/dynamic", methods=["GET"], strict_slashes=False)
def _dynamic(): def _dynamic():
log.warning("Hack attempt! Client or server doesn't exist. SCAM") log.warning("Hack attempt! Client or server doesn't exist. SCAM")
return ( return WRONG_DOOR
"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"])
@@ -393,6 +475,7 @@ def sync():
return redirect(url_for("sync")) return redirect(url_for("sync"))
if __name__ == "__main__": if __name__ == "__main__":
update_state_thread = threading.Thread(target=update_state) update_state_thread = threading.Thread(target=update_state)
update_state_thread.start() update_state_thread.start()

View File

@@ -3,3 +3,4 @@ kubernetes
PyYAML>=6.0.1 PyYAML>=6.0.1
Flask>=2.3.3 Flask>=2.3.3
flask-cors flask-cors
bcrypt

View File

@@ -1,3 +1,11 @@
:root {
--app-h: 100vh;
--app-space-1: 8px;
}
/* /*
* -- BASE STYLES -- * -- BASE STYLES --
* Most of these are inherited from Base, but I want to change a few. * Most of these are inherited from Base, but I want to change a few.
@@ -22,6 +30,7 @@ a {
.button { .button {
border-radius: 4px; border-radius: 4px;
} }
.delete-button { .delete-button {
background: #9d2c2c; background: #9d2c2c;
border: 1px solid #480b0b; border: 1px solid #480b0b;
@@ -32,7 +41,8 @@ a {
* -- LAYOUT STYLES -- * -- 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` * This layout consists of three main elements, `#nav` (navigation bar), `#list` (server list), and `#main` (server content). All 3 elements are within `#layout`
*/ */
#layout, #nav, #list, #main { #nav,
#main {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
@@ -45,6 +55,7 @@ a {
background: rgb(37, 42, 58); background: rgb(37, 42, 58);
text-align: center; text-align: center;
} }
/* Show the "Menu" button on phones */ /* Show the "Menu" button on phones */
#nav .nav-menu-button { #nav .nav-menu-button {
display: block; display: block;
@@ -55,8 +66,9 @@ a {
/* When "Menu" is clicked, the navbar should be 80% height */ /* When "Menu" is clicked, the navbar should be 80% height */
#nav.active { #nav.active {
height: 80%; height: 100%;
} }
/* Don't show the navigation items... */ /* Don't show the navigation items... */
.nav-inner { .nav-inner {
display: none; display: none;
@@ -78,19 +90,22 @@ a {
border: none; border: none;
text-align: left; text-align: left;
} }
#nav .pure-menu-link:hover,
#nav .pure-menu-link:focus { #nav .pure-menu-link:hover,
background: rgb(55, 60, 90); #nav .pure-menu-link:focus {
} background: rgb(55, 60, 90);
#nav .pure-menu-link { }
color: #fff;
margin-left: 0.5em; #nav .pure-menu-link {
} color: #fff;
#nav .pure-menu-heading { margin-left: 0.5em;
border-bottom: none; }
font-size:110%;
color: rgb(75, 113, 151); #nav .pure-menu-heading {
} border-bottom: none;
font-size: 110%;
color: rgb(75, 113, 151);
}
/* /*
@@ -110,94 +125,118 @@ a {
margin-right: 0.5em; margin-right: 0.5em;
border-radius: 3px; border-radius: 3px;
} }
.server-label-personal { .server-label-personal {
background: #ffc94c; background: #ffc94c;
} }
.server-label-work { .server-label-work {
background: #41ccb4; background: #41ccb4;
} }
.server-label-travel { .server-label-travel {
background: #40c365; background: #40c365;
} }
/* server Item Styles */ /* server Item Styles */
.server-item { .server-content-title {
padding: 0.9em 1em; padding: var(--app-space-1);
border-bottom: 1px solid #ddd; /* color: #ffffff; */
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 { .server-item {
background: #eeeeee; padding: var(--app-space-1);
} /* color: #ffffff; */
.server-item-unread { cursor: pointer;
border-left: 6px solid #1b98f8;
}
.server-item-broken {
border-left: 6px solid #880d06;
} }
.server-item:hover { .server-item:hover {
background: #d1d0d0; 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 Styles */
.server-content-header, .server-content-body, .server-content-footer { .server-content-header,
.server-content-body,
.server-content-footer {
padding: 1em 2em; padding: 1em 2em;
} }
.server-content-header {
border-bottom: 1px solid #ddd;
}
.server-content-title { .server-content-header {
margin: 0.5em 0 0; border-bottom: 1px solid #ddd;
} }
.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 { .server-content-title {
width: 40px; margin: 0.5em 0 0;
height: 40px; }
}
.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;
}
/* /*
@@ -207,22 +246,17 @@ a {
*/ */
@media (min-width: 40em) { @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 */ /* These are position:fixed; elements that will be in the left 500px of the screen */
#nav, #list { #nav {
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
overflow: auto; overflow: auto;
} }
#nav { #nav {
margin-left:-500px; /* margin-left: -500px; */
width:150px; width: 150px;
height: 100%; height: 100%;
} }
@@ -237,12 +271,6 @@ a {
display: none; display: none;
} }
#list {
margin-left: -350px;
width: 100%;
height: 33%;
border-bottom: 1px solid #ddd;
}
#main { #main {
position: fixed; position: fixed;
@@ -251,7 +279,8 @@ a {
bottom: 0; bottom: 0;
left: 150px; left: 150px;
overflow: auto; overflow: auto;
width: auto; /* so that it's not 100% */ width: auto;
/* so that it's not 100% */
} }
} }
@@ -264,12 +293,7 @@ a {
@media (min-width: 60em) { @media (min-width: 60em) {
/* This will take up the entire height, and be a little thinner */ /* 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; */ /* This will now take up it's own column, so don't need position: fixed; */
#main { #main {
@@ -281,72 +305,171 @@ a {
.alert { .alert {
position: absolute; position: absolute;
top: 1em; top: 1em;
right: 1em; right: 1em;
width: auto; width: auto;
height: auto; height: auto;
padding: 10px; padding: 10px;
margin: 10px; margin: 10px;
line-height: 1.8; line-height: 1.8;
border-radius: 5px; border-radius: 5px;
cursor: hand; cursor: hand;
cursor: pointer; cursor: pointer;
font-family: sans-serif; font-family: sans-serif;
font-weight: 400; font-weight: 400;
} }
.alertCheckbox { .alertCheckbox {
display: none; display: none;
} }
:checked + .alert { :checked+.alert {
display: none; display: none;
} }
.alertText { .alertText {
display: table; display: table;
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
font-size: 150%; font-size: 150%;
} }
.alertClose { .alertClose {
float: right; float: right;
padding-top: 0px; padding-top: 0px;
font-size: 120%; font-size: 120%;
} }
.clear { .clear {
clear: both; clear: both;
} }
.info { .info {
background-color: #EEE; background-color: #EEE;
border: 1px solid #DDD; border: 1px solid #DDD;
color: #999; color: #999;
} }
.success { .success {
background-color: #EFE; background-color: #EFE;
border: 1px solid #DED; border: 1px solid #DED;
color: #9A9; color: #9A9;
} }
.notice { .notice {
background-color: #EFF; background-color: #EFF;
border: 1px solid #DEE; border: 1px solid #DEE;
color: #9AA; color: #9AA;
} }
.warning { .warning {
background-color: #FDF7DF; background-color: #FDF7DF;
border: 1px solid #FEEC6F; border: 1px solid #FEEC6F;
color: #C9971C; color: #C9971C;
} }
.error { .error {
background-color: #FEE; background-color: #FEE;
border: 1px solid #EDD; border: 1px solid #EDD;
color: #A66; 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%;
} }

File diff suppressed because one or more lines are too long

View File

@@ -2,67 +2,83 @@
{% block content %} {% block content %}
<div id="list" class="pure-u-1-3" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html"> <div id="entety-menu">
<div class="server-item pure-g"> <div id="list">
<h1 class="server-content-title">Clients</h1> <h1 class="server-content-title">Clients</h1>
</div> <form id="search-form" class="pure-form">
{% for client, values in CLIENTS.items() %} <input placeholder="&#128269;" type="text" id="entety-menu-search" />
<div class="server-item server-item-{% if client == selected_client %}unread{% else %}selected{% endif %} pure-g"> </form>
<div class="pure-u-3-4" onclick="location.href='/clients?selected_client={{ client }}';"> <div data-search-box class="srcollable-list-content">
<h5 class="server-name">{{ values["name"] }}</h5> {% for client, values in CLIENTS.items() %}
<h4 class="server-info">Allowed {{ values["servers"]|length }} server{% if values["servers"]|length >1 %}s{%endif%}</h4> <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> </div>
</div>
{% endfor %} <div onclick="location.href='/clients?add_client=True';" class="server-item server-add pure-g">
<div onclick="location.href='/clients?add_client=True';" class="server-item server-add pure-g">
<div class="pure-u-1"> <div class="pure-u-1">
+ +
</div> </div>
</div> </div>
</div>
</div> </div>
{% if add_client %} {% if add_client %}
<div class="pure-u-1-3"> <div id="content">
<div class="">
<div class="server-content-header pure-g"> <div class="server-content-header pure-g">
<div class="pure-u-1-2"> <div class="">
<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>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 ">
<input type="text" class="pure-u-23-24" name="name" required placeholder="Name"/> <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>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <button type="submit" class="pure-button pure-input-1 pure-button-primary">Add</button>
<input type="text" class="pure-u-23-24" name="comment" placeholder="Comment"/> </fieldset>
</div> </form>
<div class="pure-checkbox"> </div>
{% 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>
</div>
{% endif %} {% 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-u-1-2"> <div id="content">
<div class="">
<div class="server-content-header pure-g"> <div class="server-content-header pure-g">
<div class="pure-u-1-2"> <div class="">
<h1 class="server-content-title">{{client['name']}}</h1> <h1 class="server-content-title">{{client['name']}}</h1>
<h4 class="server-info">{{ client['comment'] }}</h4> <h4 class="server-info">{{ client['comment'] }}</h4>
<h4 class="server-info">id {{ selected_client }}</h4> <h4 class="server-info">id {{ selected_client }}</h4>
@@ -70,105 +86,82 @@
</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>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 ">
<input type="text" class="pure-u-1" name="name" required value="{{client['name']}}"/> <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']}}"/> <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>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-g pure-form pure-form-stacked">
<input type="text" class="pure-u-1" name="comment" value="{{client['comment']}}"/> <div class="pure-u-1-2">
<button type="submit" class="pure-button pure-button-primary button">Save and apply</button>
</div>
</div> </div>
<input type="hidden" class="pure-u-1" name="user_id" value="{{selected_client}}"/>
<div class="pure-checkbox"> </fieldset>
<p>Allow access to:</p> </form>
{% for server in SERVERS %} <form action="/del_client" class="pure-form pure-form-stacked" method="POST">
<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 type="hidden" name="name" required value="{{client['name']}}" />
<input <input type="hidden" name="user_id" value="{{selected_client}}" />
{% if server.info()['local_server_id'] in client['servers'] %}checked{%endif%} <button type="submit" class="pure-button pure-button-primary delete-button button">Delete
type="checkbox" id="option{{loop.index0}}" name="servers" value="{{server.info()['local_server_id']}}"></label> Client</button>
<input type="checkbox" id="agree" name="agree" required>
</form>
{% endfor %} <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') %}
</div> <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>
<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> </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>
</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -2,15 +2,17 @@
{% block content %} {% block 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 id="entety-menu">
</div> <div>
<h1 class="server-content-title">Servers</h1>
<div class="srcollable-list-content">
{% for server in SERVERS %} {% for server in SERVERS %}
{% set list_ns = namespace(total_bytes=0) %} {% set total_traffic = namespace(total_bytes=0) %}
{% for key in server.data["keys"] %} {% for key in server.data["keys"] %}
{% if key.used_bytes %} {% if key.used_bytes %}
{% set list_ns.total_bytes = list_ns.total_bytes + key.used_bytes %} {% set total_traffic.total_bytes = total_traffic.total_bytes + key.used_bytes %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<div class="server-item server-item-{% if loop.index0 == selected_server|int %}unread{% else %}selected{% endif %} pure-g"> <div class="server-item server-item-{% if loop.index0 == selected_server|int %}unread{% else %}selected{% endif %} pure-g">
@@ -19,7 +21,7 @@
<h4 class="server-info">{{ '/'.join(server.info()["url"].split('/')[0:-1]) }}</h4> <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">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: {{ total_traffic.total_bytes | filesizeformat }}</h4>
<h4 class="server-info">v.{{ 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"] }}
@@ -27,6 +29,7 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div>
<div onclick="location.href='/?add_server=True';" class="server-item server-add pure-g"> <div onclick="location.href='/?add_server=True';" class="server-item server-add pure-g">
<div class="pure-u-1"> <div class="pure-u-1">
+ +
@@ -34,36 +37,37 @@
</div> </div>
</div> </div>
</div>
{% if add_server %} {% if add_server %}
<div class="pure-u-1-3"> <div id="content">
<div >
<div class="server-content-header pure-g"> <div class="server-content-header pure-g">
<div class="pure-u-1-2"> <div >
<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="pure-g"> <div >
<div class="pure-u-1 pure-u-md-1-3"> <label for="url">Server management URL</label>
<input type="text" class="pure-u-23-24" name="url" placeholder="Server management URL"/> <input type="text" class="pure-u-1" name="url" placeholder="https://example.com:5743/KSsdywe6Sdb..."/>
</div> <label for="cert">Server management Certificate</label>
<div class="pure-u-1 pure-u-md-1-3"> <input type="text" class="pure-u-1" name="cert" placeholder="B5DD2443DAF..."/>
<input type="text"class="pure-u-23-24" name="cert" placeholder="Certificate"/> <label for="cert">Server Comment
</div> <span class="pure-form-message">This will be exposed to client and will be used as "Server name" in client Outline app</span>
<div class="pure-u-1 pure-u-md-1-3"> </label>
<input type="text" class="pure-u-23-24" name="comment" placeholder="Comment"/> <input type="text" class="pure-u-1" name="comment" placeholder="e.g. server location"/>
</div>
</div> </div>
<button type="submit" class="pure-button pure-input-1 pure-button-primary">Add</button> <button type="submit" class="pure-button pure-input-1 pure-button-primary">Add</button>
</fieldset> </fieldset>
</form> </form>
</div> </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 %}
@@ -71,11 +75,12 @@
{% else %} {% else %}
{% set server = SERVERS[selected_server|int] %} {% set server = SERVERS[selected_server|int] %}
{% endif %} {% endif %}
<div id="main" class="pure-u-1"> <div id="content">
<div >
<div class="server-content"> <div class="server-content">
<div class="server-content-header pure-g"> <div class="server-content-header pure-g">
<div class="pure-u-1-2"> <div>
<h1 class="server-content-title">{{server.info()["name"]}}</h1> <h1 class="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>
</p> </p>
@@ -95,35 +100,38 @@
<form class="pure-form pure-form-stacked" method="POST"> <form class="pure-form pure-form-stacked" method="POST">
<fieldset> <fieldset>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 ">
<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> <label for="name">Server Name
<input type="text" id="name" class="pure-u-23-24" name="name" value="{{server.info()['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> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 ">
<label for="comment">Comment</br>This value will be used as "Server name" in client app.</label> <label for="comment">Comment</br>
<input type="text" id="comment" class="pure-u-23-24" name="comment" value="{{server.info()['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" id="comment" class="pure-u-1" name="comment" value="{{server.info()['comment']}}"/>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1">
<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 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']}}"/> <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']}}"/>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 ">
<label for="hostname_for_access_keys">Hostname For Access Keys</label> <label for="hostname_for_access_keys">Hostname For Access Keys</label>
<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']}}"/> <input type="text" id="hostname_for_access_keys" class="pure-u-1" name="hostname_for_access_keys" value="{{server.info()['hostname_for_access_keys']}}"/>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 ">
<label for="url">Server URL</label> <label for="url">Server URL</label>
<input type="text" readonly id="url" class="pure-u-23-24" name="url" value="{{server.info()['url']}}"/> <input type="text" readonly id="url" class="pure-u-1" name="url" value="{{server.info()['url']}}"/>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 ">
<label for="cert">Server Access Certificate</label> <label for="cert">Server Access Certificate</label>
<input type="text" readonly id="cert" class="pure-u-23-24" name="cert" value="{{server.info()['cert']}}"/> <input type="text" readonly id="cert" class="pure-u-1" name="cert" value="{{server.info()['cert']}}"/>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 ">
<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-1" 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()['local_server_id']}}"/> <input type="hidden" readonly id="server_id" 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">
@@ -144,6 +152,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
{% endif %} {% endif %}

83
tools/windows-helper.ps1 Normal file
View File

@@ -0,0 +1,83 @@
$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."

100
tools/windows_task.ps1 Normal file
View File

@@ -0,0 +1,100 @@
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
}