Initial commit: xray-manager v0.2 with Debian 13 support
This commit is contained in:
+221
@@ -0,0 +1,221 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[codz]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
.pybuild
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py.cover
|
||||||
|
*.lcov
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
# Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
# poetry.lock
|
||||||
|
# poetry.toml
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||||
|
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||||
|
# pdm.lock
|
||||||
|
# pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# pixi
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||||
|
# pixi.lock
|
||||||
|
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||||
|
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||||
|
.pixi/*
|
||||||
|
!.pixi/config.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule*
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
*.rdb
|
||||||
|
*.aof
|
||||||
|
*.pid
|
||||||
|
|
||||||
|
# RabbitMQ
|
||||||
|
mnesia/
|
||||||
|
rabbitmq/
|
||||||
|
rabbitmq-data/
|
||||||
|
|
||||||
|
# ActiveMQ
|
||||||
|
activemq-data/
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.envrc
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
# .idea/
|
||||||
|
|
||||||
|
# Abstra
|
||||||
|
# Abstra is an AI-powered process automation framework.
|
||||||
|
# Ignore directories containing user credentials, local state, and settings.
|
||||||
|
# Learn more at https://abstra.io/docs
|
||||||
|
.abstra/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
|
# you could uncomment the following to ignore the entire vscode folder
|
||||||
|
# .vscode/
|
||||||
|
# Temporary file for partial code execution
|
||||||
|
tempCodeRunnerFile.py
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Marimo
|
||||||
|
marimo/_static/
|
||||||
|
marimo/_lsp/
|
||||||
|
__marimo__/
|
||||||
|
|
||||||
|
# Streamlit
|
||||||
|
.streamlit/secrets.toml
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"xray_config_folder": "/usr/local/etc/xray/default",
|
||||||
|
"profiles": {
|
||||||
|
"host": "profiles-site-dns.com",
|
||||||
|
"base_path": "/var/lib/xray-manager/profiles",
|
||||||
|
"file_name": "list.txt",
|
||||||
|
"folder_mapping": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
python3-xray-manager (0.2-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* Initial release.
|
||||||
|
|
||||||
|
-- Vladimir Khvan <thisisnotablownfuse@gmail.com> Thu, 28 Jun 2026 20:00:00 +0300
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
Source: python3-xray-manager
|
||||||
|
Section: python
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: vkh <vkh@casket.ddns.net>
|
||||||
|
Build-Depends: debhelper-compat (=13),
|
||||||
|
dh-python,
|
||||||
|
pybuild-plugin-pyproject,
|
||||||
|
python3-all,
|
||||||
|
python3-setuptools,
|
||||||
|
python3-build
|
||||||
|
Standards-Version: 4.6.2
|
||||||
|
|
||||||
|
Package: python3-xray-manager
|
||||||
|
Architecture: all
|
||||||
|
Depends: ${python3:Depends}, ${misc:Depends}
|
||||||
|
Description: Utility for managing Xray configuration and user profiles
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
data/config.json etc/xray-manager/
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@ --with python3 --buildsystem=pybuild
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "xray-manager"
|
||||||
|
version = "0.2"
|
||||||
|
description = "Utility for managing Xray configuration and user profiles"
|
||||||
|
dependencies = [
|
||||||
|
"cryptography>=43.0.0",
|
||||||
|
"qrcode>=8.2",
|
||||||
|
"tabulate>=0.9.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
xray-manager = "xray_manager.cli:main"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
cryptography==43.0.0
|
||||||
|
qrcode==8.2
|
||||||
|
tabulate==0.9.0
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import handlers
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(prog="xray-manager")
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest="group", required=True)
|
||||||
|
|
||||||
|
# ---------- user ----------
|
||||||
|
user_parser = subparsers.add_parser("user")
|
||||||
|
user_subparser = user_parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# user add
|
||||||
|
user_add_parser = user_subparser.add_parser("add")
|
||||||
|
user_add_parser.add_argument("username")
|
||||||
|
user_add_parser.add_argument(
|
||||||
|
"-c",
|
||||||
|
"--connection",
|
||||||
|
nargs="+",
|
||||||
|
default=[],
|
||||||
|
help="Connections to add, e.g. shadowsocks:ru1 vless:us1",
|
||||||
|
)
|
||||||
|
user_add_parser.set_defaults(handler=handlers.user_add)
|
||||||
|
|
||||||
|
# user show
|
||||||
|
user_show_parser = user_subparser.add_parser("show")
|
||||||
|
user_show_parser.add_argument("username", nargs="?")
|
||||||
|
user_show_parser.add_argument("-a", "--all", action="store_true")
|
||||||
|
user_show_parser.set_defaults(handler=handlers.user_show)
|
||||||
|
|
||||||
|
# user modify
|
||||||
|
user_modify_parser = user_subparser.add_parser("modify")
|
||||||
|
user_modify_parser.add_argument("username")
|
||||||
|
user_modify_parser.add_argument(
|
||||||
|
"-a",
|
||||||
|
"--add",
|
||||||
|
dest="to_add",
|
||||||
|
nargs="+",
|
||||||
|
default=[],
|
||||||
|
metavar="SPEC",
|
||||||
|
help="Connections to add",
|
||||||
|
)
|
||||||
|
user_modify_parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--delete",
|
||||||
|
dest="to_delete",
|
||||||
|
nargs="+",
|
||||||
|
default=[],
|
||||||
|
metavar="SPEC",
|
||||||
|
help="Connections to delete",
|
||||||
|
)
|
||||||
|
user_modify_parser.set_defaults(handler=handlers.user_modify)
|
||||||
|
|
||||||
|
# user delete
|
||||||
|
user_delete_parser = user_subparser.add_parser("delete")
|
||||||
|
group = user_delete_parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument("username", nargs="?")
|
||||||
|
group.add_argument("-a", "--all", action="store_true")
|
||||||
|
user_delete_parser.set_defaults(handler=handlers.user_delete)
|
||||||
|
|
||||||
|
# -------- profile ---------
|
||||||
|
profile_parser = subparsers.add_parser("profile")
|
||||||
|
profile_subparser = profile_parser.add_subparsers(
|
||||||
|
dest="command", required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# profile build
|
||||||
|
profile_build_parser = profile_subparser.add_parser("build")
|
||||||
|
group = profile_build_parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument("username", nargs="?")
|
||||||
|
group.add_argument("-a", "--all", action="store_true")
|
||||||
|
profile_build_parser.set_defaults(handler=handlers.profile_build)
|
||||||
|
|
||||||
|
# profile show
|
||||||
|
profile_show_parser = profile_subparser.add_parser("show")
|
||||||
|
group = profile_show_parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument("username", nargs="?")
|
||||||
|
group.add_argument("-a", "--all", action="store_true")
|
||||||
|
profile_show_parser.set_defaults(handler=handlers.profile_show)
|
||||||
|
|
||||||
|
# profile clear
|
||||||
|
profile_clear_parser = profile_subparser.add_parser("clear")
|
||||||
|
group = profile_clear_parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument("username", nargs="?")
|
||||||
|
group.add_argument("-a", "--all", action="store_true")
|
||||||
|
profile_clear_parser.set_defaults(handler=handlers.profile_clear)
|
||||||
|
|
||||||
|
# profile delete
|
||||||
|
profile_delete_parser = profile_subparser.add_parser("delete")
|
||||||
|
group = profile_delete_parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument("username", nargs="?")
|
||||||
|
group.add_argument("-a", "--all", action="store_true")
|
||||||
|
profile_delete_parser.set_defaults(handler=handlers.profile_delete)
|
||||||
|
|
||||||
|
# profile qrcode
|
||||||
|
profile_qrcode_parser = profile_subparser.add_parser("qrcode")
|
||||||
|
group = profile_qrcode_parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument("username", nargs="?")
|
||||||
|
group.add_argument("-a", "--all", action="store_true")
|
||||||
|
profile_qrcode_parser.set_defaults(handler=handlers.profile_qrcode)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = build_parser()
|
||||||
|
|
||||||
|
try:
|
||||||
|
args = parser.parse_args()
|
||||||
|
func = args.handler
|
||||||
|
kwargs = vars(args)
|
||||||
|
kwargs.pop("handler")
|
||||||
|
func(**kwargs)
|
||||||
|
except ValueError as e:
|
||||||
|
print(e, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .core import (
|
||||||
|
ConnectionFactory,
|
||||||
|
ConnectionKey,
|
||||||
|
Profile,
|
||||||
|
ProfileFactory,
|
||||||
|
ProfileStorage,
|
||||||
|
User,
|
||||||
|
UserFactory,
|
||||||
|
XrayConfig,
|
||||||
|
XrayManagerConfig,
|
||||||
|
)
|
||||||
|
from .utils import print_qrcode, print_table
|
||||||
|
|
||||||
|
DEFAULT_CONFIG_PATH = "/etc/xray-manager/config.json"
|
||||||
|
DEV_CONFIG_PATH = "tests/mock_data/etc/xray-manager/config.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_config_path() -> str:
|
||||||
|
env_path = os.getenv("XRAY_MANAGER_CONFIG")
|
||||||
|
if env_path:
|
||||||
|
return env_path
|
||||||
|
|
||||||
|
if os.getenv("XRAY_ENV") == "development":
|
||||||
|
return DEV_CONFIG_PATH
|
||||||
|
|
||||||
|
if os.path.exists(DEFAULT_CONFIG_PATH):
|
||||||
|
return DEFAULT_CONFIG_PATH
|
||||||
|
|
||||||
|
return DEV_CONFIG_PATH
|
||||||
|
|
||||||
|
|
||||||
|
xray_manager_config = XrayManagerConfig(_resolve_config_path())
|
||||||
|
|
||||||
|
xray_config = XrayConfig(
|
||||||
|
xray_manager_config.xray_config_folder, xray_manager_config.host
|
||||||
|
)
|
||||||
|
|
||||||
|
storage = ProfileStorage(xray_manager_config)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_users(username: str | None, all: bool) -> list[User]:
|
||||||
|
if all:
|
||||||
|
return xray_config.get_users()
|
||||||
|
if username:
|
||||||
|
user = xray_config.get_user(username)
|
||||||
|
if not isinstance(user, User):
|
||||||
|
raise ValueError(f"User not found: {username}")
|
||||||
|
return [user]
|
||||||
|
raise ValueError("Either username or --all must be specified")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_profiles(username: str | None, all: bool) -> list[Profile]:
|
||||||
|
if all:
|
||||||
|
return storage.load_profiles()
|
||||||
|
if username:
|
||||||
|
try:
|
||||||
|
profile = storage.load_profile(username)
|
||||||
|
except (ValueError, FileNotFoundError) as e:
|
||||||
|
raise ValueError(f"Cannot load profile for {username}: {e}")
|
||||||
|
return [profile]
|
||||||
|
raise ValueError("Either username or --all must be specified")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_connection_spec(spec: str) -> ConnectionKey:
|
||||||
|
protocol, exit_point = spec.split(":", 1)
|
||||||
|
if protocol not in ConnectionFactory.get_protocols():
|
||||||
|
raise ValueError(f"Unsupported protocol: {protocol}")
|
||||||
|
if exit_point not in xray_config.get_exit_points():
|
||||||
|
raise ValueError(f"Unsupported exit point: {exit_point}")
|
||||||
|
return ConnectionKey(protocol, exit_point)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_user_rows(users: list[User]) -> list[list[str]]:
|
||||||
|
rows = []
|
||||||
|
for index, user in enumerate(users, start=1):
|
||||||
|
first = True
|
||||||
|
for conn in user.connections:
|
||||||
|
if first:
|
||||||
|
row = [index, user.username, conn.exit_point, conn.protocol]
|
||||||
|
first = False
|
||||||
|
else:
|
||||||
|
row = ["", "", conn.exit_point, conn.protocol]
|
||||||
|
rows.append(row)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _build_profile_rows(profiles: list[Profile]) -> list[list[str]]:
|
||||||
|
rows = []
|
||||||
|
for index, profile in enumerate(profiles, start=1):
|
||||||
|
first = True
|
||||||
|
for conn in profile.user.connections:
|
||||||
|
if first:
|
||||||
|
row = [
|
||||||
|
index,
|
||||||
|
profile.user.username,
|
||||||
|
profile.folder_name,
|
||||||
|
conn.exit_point,
|
||||||
|
conn.protocol,
|
||||||
|
]
|
||||||
|
first = False
|
||||||
|
else:
|
||||||
|
row = ["", "", "", conn.exit_point, conn.protocol]
|
||||||
|
rows.append(row)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def user_add(username: str, connection: list[str], **_):
|
||||||
|
user = xray_config.get_user(username)
|
||||||
|
|
||||||
|
if isinstance(user, User):
|
||||||
|
raise ValueError(f"User already exists: {username}")
|
||||||
|
|
||||||
|
keys: list[ConnectionKey] = [
|
||||||
|
_parse_connection_spec(spec) for spec in connection
|
||||||
|
]
|
||||||
|
|
||||||
|
user = UserFactory.from_spec(
|
||||||
|
username=username, keys=keys, xray_config=xray_config
|
||||||
|
)
|
||||||
|
|
||||||
|
xray_config.add_user(user)
|
||||||
|
|
||||||
|
|
||||||
|
def user_show(username: str | None, all: bool, **_):
|
||||||
|
users = _resolve_users(username, all)
|
||||||
|
|
||||||
|
headers = ["#", "Username", "Exit Point", "Protocol"]
|
||||||
|
rows = _build_user_rows(users)
|
||||||
|
|
||||||
|
print_table(headers, rows)
|
||||||
|
|
||||||
|
|
||||||
|
def user_modify(username: str, to_add: list[str], to_delete: list[str], **_):
|
||||||
|
user = xray_config.get_user(username)
|
||||||
|
|
||||||
|
if not isinstance(user, User):
|
||||||
|
raise ValueError(f"User not found: {username}")
|
||||||
|
|
||||||
|
add_key = [_parse_connection_spec(spec) for spec in to_add]
|
||||||
|
del_key = [_parse_connection_spec(spec) for spec in to_delete]
|
||||||
|
|
||||||
|
user.modify_connection_by_key(xray_config, add_key, del_key)
|
||||||
|
xray_config.modify_user(user)
|
||||||
|
|
||||||
|
|
||||||
|
def user_delete(username: str | None, all: bool, **_):
|
||||||
|
users = _resolve_users(username, all)
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
xray_config.delete_user(user.username)
|
||||||
|
|
||||||
|
|
||||||
|
def profile_build(username: str | None, all: bool, **_):
|
||||||
|
users = _resolve_users(username, all)
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
try:
|
||||||
|
profile = storage.load_profile(user.username)
|
||||||
|
profile = ProfileFactory.from_user(user, profile.folder_name)
|
||||||
|
except (ValueError, FileNotFoundError) as e:
|
||||||
|
print(f"Cannot load profile for {user.username}: {e}")
|
||||||
|
profile = ProfileFactory.from_user(user)
|
||||||
|
|
||||||
|
storage.save_profile(profile)
|
||||||
|
|
||||||
|
|
||||||
|
def profile_show(username: str | None, all: bool, **_):
|
||||||
|
profiles = _resolve_profiles(username, all)
|
||||||
|
|
||||||
|
headers = ["#", "Username", "Profile", "Exit Point", "Protocol"]
|
||||||
|
rows = _build_profile_rows(profiles)
|
||||||
|
|
||||||
|
print_table(headers, rows)
|
||||||
|
|
||||||
|
|
||||||
|
def profile_clear(username: str | None, all: bool, **_):
|
||||||
|
profiles = _resolve_profiles(username, all)
|
||||||
|
|
||||||
|
for profile in profiles:
|
||||||
|
storage.clear_profile(profile.user.username)
|
||||||
|
|
||||||
|
|
||||||
|
def profile_delete(username: str | None, all: bool, **_):
|
||||||
|
profiles = _resolve_profiles(username, all)
|
||||||
|
|
||||||
|
for profile in profiles:
|
||||||
|
storage.delete_profile(profile.user.username)
|
||||||
|
|
||||||
|
|
||||||
|
def profile_qrcode(username: str | None, all: bool, **_):
|
||||||
|
profiles = _resolve_profiles(username, all)
|
||||||
|
|
||||||
|
for profile in profiles:
|
||||||
|
host = xray_manager_config.profile_host
|
||||||
|
folder_name = profile.folder_name
|
||||||
|
filename = xray_manager_config.profile_file_name
|
||||||
|
|
||||||
|
url = f"https://{host}/{folder_name}/{filename}"
|
||||||
|
print_qrcode(url)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import x25519
|
||||||
|
from qrcode import ERROR_CORRECT_L, QRCode
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
|
||||||
|
def print_table(headers: list[str], rows: list[list[str]]) -> None:
|
||||||
|
table = tabulate(rows, headers=headers, tablefmt="rounded_grid")
|
||||||
|
print(table)
|
||||||
|
|
||||||
|
|
||||||
|
def print_qrcode(url: str) -> None:
|
||||||
|
qr = QRCode(version=1, error_correction=ERROR_CORRECT_L)
|
||||||
|
qr.add_data(url)
|
||||||
|
qr.make()
|
||||||
|
|
||||||
|
print(f"{url}\n")
|
||||||
|
qr.print_ascii()
|
||||||
|
|
||||||
|
|
||||||
|
def private_to_public(private_key_b64: str) -> str:
|
||||||
|
def b64decode(s: str) -> bytes:
|
||||||
|
padding = "=" * (-len(s) % 4)
|
||||||
|
return base64.urlsafe_b64decode(s + padding)
|
||||||
|
|
||||||
|
def b64encode(b: bytes) -> str:
|
||||||
|
return base64.urlsafe_b64encode(b).decode().rstrip("=")
|
||||||
|
|
||||||
|
private_bytes = b64decode(private_key_b64)
|
||||||
|
|
||||||
|
if len(private_bytes) != 32:
|
||||||
|
raise ValueError("Invalid private key length (expected 32 bytes)")
|
||||||
|
|
||||||
|
private_key = x25519.X25519PrivateKey.from_private_bytes(private_bytes)
|
||||||
|
public_key = private_key.public_key()
|
||||||
|
|
||||||
|
public_bytes = public_key.public_bytes(
|
||||||
|
encoding=serialization.Encoding.Raw,
|
||||||
|
format=serialization.PublicFormat.Raw,
|
||||||
|
)
|
||||||
|
|
||||||
|
return b64encode(public_bytes)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"xray_config_folder": "tests/mock_data/usr/local/etc/xray/default",
|
||||||
|
"profiles": {
|
||||||
|
"host": "localhost.internal",
|
||||||
|
"base_path": "tests/mock_data/var/lib/xray-manager/profiles",
|
||||||
|
"file_name": "list.txt",
|
||||||
|
"folder_mapping": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"loglevel": "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"tag": "shadowsocks",
|
||||||
|
"xrm": true,
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"port": 8443,
|
||||||
|
"protocol": "shadowsocks",
|
||||||
|
"settings": {
|
||||||
|
"method": "2022-blake3-aes-256-gcm",
|
||||||
|
"password": "Z8WtMeWzZVh1F6/5URGrs8vWdB3CLN5y7A9D1U0Q65E=",
|
||||||
|
"clients": [],
|
||||||
|
"network": "tcp,udp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"tag": "in-vless",
|
||||||
|
"xrm": true,
|
||||||
|
"listen": "127.0.0.1",
|
||||||
|
"port": 443,
|
||||||
|
"protocol": "vless",
|
||||||
|
"settings": {
|
||||||
|
"clients": [],
|
||||||
|
"decryption": "none"
|
||||||
|
},
|
||||||
|
"streamSettings": {
|
||||||
|
"network": "tcp",
|
||||||
|
"security": "reality",
|
||||||
|
"realitySettings": {
|
||||||
|
"show": false,
|
||||||
|
"dest": "google.com:443",
|
||||||
|
"xver": 0,
|
||||||
|
"serverNames": [
|
||||||
|
"google.com"
|
||||||
|
],
|
||||||
|
"privateKey": "yHJQq57MNRi-0UQm7ymiy2DI17iO_Ovu2HF5EV1ViGg",
|
||||||
|
"minClientVer": "",
|
||||||
|
"maxClientVer": "",
|
||||||
|
"maxTimeDiff": 0,
|
||||||
|
"shortIds": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"tag": "out-relay",
|
||||||
|
"protocol": "freedom"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"tag": "out-us1",
|
||||||
|
"protocol": "vless",
|
||||||
|
"settings": {
|
||||||
|
"vnext": [
|
||||||
|
{
|
||||||
|
"address": "127.0.0.1",
|
||||||
|
"port": 443,
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "-",
|
||||||
|
"encryption": "none",
|
||||||
|
"flow": "xtls-rprx-vision"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"streamSettings": {
|
||||||
|
"network": "tcp",
|
||||||
|
"security": "reality",
|
||||||
|
"realitySettings": {
|
||||||
|
"fingerprint": "chrome",
|
||||||
|
"serverName": "google.com",
|
||||||
|
"publicKey": "-",
|
||||||
|
"spiderX": "",
|
||||||
|
"shortId": "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"tag": "out-us2",
|
||||||
|
"protocol": "vless",
|
||||||
|
"settings": {
|
||||||
|
"vnext": [
|
||||||
|
{
|
||||||
|
"address": "127.0.0.1",
|
||||||
|
"port": 443,
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "-",
|
||||||
|
"encryption": "none",
|
||||||
|
"flow": "xtls-rprx-vision"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"streamSettings": {
|
||||||
|
"network": "tcp",
|
||||||
|
"security": "reality",
|
||||||
|
"realitySettings": {
|
||||||
|
"fingerprint": "chrome",
|
||||||
|
"serverName": "google.com",
|
||||||
|
"publicKey": "-",
|
||||||
|
"spiderX": "",
|
||||||
|
"shortId": "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"tag": "out-fr1",
|
||||||
|
"protocol": "socks",
|
||||||
|
"settings": {
|
||||||
|
"address": "127.0.0.1",
|
||||||
|
"port": 1080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"routing": {
|
||||||
|
"domainStrategy": "AsIs",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"user": [
|
||||||
|
],
|
||||||
|
"outboundTag": "out-fr1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"user": [
|
||||||
|
],
|
||||||
|
"outboundTag": "out-us1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"user": [
|
||||||
|
],
|
||||||
|
"outboundTag": "out-us2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"user": [
|
||||||
|
],
|
||||||
|
"outboundTag": "out-relay"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user