hiddifypanel 10.12.1__py3-none-any.whl → 10.15.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hiddifypanel/VERSION +1 -1
- hiddifypanel/VERSION.py +2 -2
- hiddifypanel/auth.py +15 -4
- hiddifypanel/base.py +58 -50
- hiddifypanel/cache.py +43 -25
- hiddifypanel/database.py +9 -0
- hiddifypanel/drivers/abstract_driver.py +2 -0
- hiddifypanel/drivers/singbox_api.py +17 -15
- hiddifypanel/drivers/ssh_liberty_bridge_api.py +2 -0
- hiddifypanel/drivers/user_driver.py +12 -6
- hiddifypanel/drivers/wireguard_api.py +2 -0
- hiddifypanel/drivers/xray_api.py +14 -9
- hiddifypanel/hutils/__init__.py +1 -0
- hiddifypanel/hutils/convert.py +13 -2
- hiddifypanel/hutils/crypto.py +21 -2
- hiddifypanel/hutils/flask.py +19 -5
- hiddifypanel/hutils/importer/xui.py +5 -2
- hiddifypanel/hutils/node/__init__.py +3 -0
- hiddifypanel/hutils/node/api_client.py +76 -0
- hiddifypanel/hutils/node/child.py +147 -0
- hiddifypanel/hutils/node/parent.py +100 -0
- hiddifypanel/hutils/node/shared.py +65 -0
- hiddifypanel/hutils/proxy/shared.py +15 -3
- hiddifypanel/models/__init__.py +2 -2
- hiddifypanel/models/admin.py +14 -2
- hiddifypanel/models/base_account.py +3 -3
- hiddifypanel/models/child.py +30 -16
- hiddifypanel/models/config.py +39 -15
- hiddifypanel/models/config_enum.py +55 -8
- hiddifypanel/models/domain.py +28 -20
- hiddifypanel/models/parent_domain.py +2 -2
- hiddifypanel/models/proxy.py +13 -4
- hiddifypanel/models/report.py +2 -3
- hiddifypanel/models/usage.py +2 -2
- hiddifypanel/models/user.py +18 -9
- hiddifypanel/panel/admin/Actions.py +4 -6
- hiddifypanel/panel/admin/AdminstratorAdmin.py +13 -2
- hiddifypanel/panel/admin/Dashboard.py +5 -10
- hiddifypanel/panel/admin/DomainAdmin.py +12 -11
- hiddifypanel/panel/admin/NodeAdmin.py +6 -2
- hiddifypanel/panel/admin/ProxyAdmin.py +4 -3
- hiddifypanel/panel/admin/SettingAdmin.py +60 -21
- hiddifypanel/panel/admin/UserAdmin.py +10 -2
- hiddifypanel/panel/admin/templates/index.html +1 -1
- hiddifypanel/panel/admin/templates/parent_dash.html +2 -4
- hiddifypanel/panel/cli.py +16 -16
- hiddifypanel/panel/commercial/ProxyDetailsAdmin.py +10 -5
- hiddifypanel/panel/commercial/__init__.py +7 -5
- hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py +0 -5
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +2 -2
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py +4 -5
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +8 -35
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +4 -4
- hiddifypanel/panel/commercial/restapi/v2/admin/schema.py +157 -0
- hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py +3 -3
- hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +9 -73
- hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +1 -1
- hiddifypanel/panel/commercial/restapi/v2/child/__init__.py +18 -0
- hiddifypanel/panel/commercial/restapi/v2/child/actions.py +63 -0
- hiddifypanel/panel/commercial/restapi/v2/child/register_parent_api.py +34 -0
- hiddifypanel/panel/commercial/restapi/v2/child/schema.py +7 -0
- hiddifypanel/panel/commercial/restapi/v2/child/sync_parent_api.py +21 -0
- hiddifypanel/panel/commercial/restapi/v2/panel/__init__.py +13 -0
- hiddifypanel/panel/commercial/restapi/v2/panel/info.py +18 -0
- hiddifypanel/panel/commercial/restapi/v2/panel/ping_pong.py +23 -0
- hiddifypanel/panel/commercial/restapi/v2/panel/schema.py +7 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/__init__.py +16 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/register_api.py +65 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/schema.py +115 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/status_api.py +26 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/sync_api.py +53 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/usage_api.py +57 -0
- hiddifypanel/panel/commercial/telegrambot/admin.py +1 -2
- hiddifypanel/panel/common.py +21 -6
- hiddifypanel/panel/hiddify.py +9 -80
- hiddifypanel/panel/init_db.py +84 -38
- hiddifypanel/panel/usage.py +33 -18
- hiddifypanel/panel/user/templates/home/usage.html +1 -1
- hiddifypanel/panel/user/templates/new.html +2 -2
- hiddifypanel/static/css/custom.css +13 -0
- hiddifypanel/static/images/hiddify.png +0 -0
- hiddifypanel/static/images/hiddify2.png +0 -0
- hiddifypanel/static/new/assets/hiddify-logo-7617d937.png +0 -0
- hiddifypanel/static/new/assets/{index-4510b616.js → index-ccb9873c.js} +2 -2
- hiddifypanel/static/new/assets/index-fa00de9a.css +1 -0
- hiddifypanel/static/new/i18n/en.json +6 -6
- hiddifypanel/static/new/i18n/fa.json +1 -1
- hiddifypanel/templates/admin-layout.html +24 -40
- hiddifypanel/templates/fake.html +22 -0
- hiddifypanel/templates/master.html +24 -42
- hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/en/LC_MESSAGES/messages.po +95 -5
- hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/fa/LC_MESSAGES/messages.po +96 -4
- hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/pt/LC_MESSAGES/messages.po +98 -6
- hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/ru/LC_MESSAGES/messages.po +91 -1
- hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/zh/LC_MESSAGES/messages.po +92 -2
- hiddifypanel/translations.i18n/en.json +61 -5
- hiddifypanel/translations.i18n/fa.json +60 -4
- hiddifypanel/translations.i18n/pt.json +63 -7
- hiddifypanel/translations.i18n/ru.json +57 -1
- hiddifypanel/translations.i18n/zh.json +58 -2
- {hiddifypanel-10.12.1.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/METADATA +47 -47
- {hiddifypanel-10.12.1.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/RECORD +112 -94
- hiddifypanel/panel/commercial/restapi/v2/DTO.py +0 -9
- hiddifypanel/panel/commercial/restapi/v2/hello/__init__.py +0 -16
- hiddifypanel/panel/commercial/restapi/v2/hello/hello.py +0 -32
- hiddifypanel/static/new/assets/hiddify-logo-7617d937_old.png +0 -0
- hiddifypanel/static/new/assets/index-669b32c8.css +0 -1
- /hiddifypanel/static/images/{hiddify1.png → hiddify-old.png} +0 -0
- {hiddifypanel-10.12.1.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/LICENSE.md +0 -0
- {hiddifypanel-10.12.1.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/WHEEL +0 -0
- {hiddifypanel-10.12.1.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/entry_points.txt +0 -0
- {hiddifypanel-10.12.1.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/top_level.txt +0 -0
hiddifypanel/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
10.
|
1
|
+
10.15.0.dev0
|
hiddifypanel/VERSION.py
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
__version__='10.
|
1
|
+
__version__='10.15.0.dev0'
|
2
2
|
from datetime import datetime
|
3
|
-
__release_date__= datetime.strptime('2024-
|
3
|
+
__release_date__= datetime.strptime('2024-04-05','%Y-%m-%d')
|
hiddifypanel/auth.py
CHANGED
@@ -4,7 +4,7 @@ from hiddifypanel.hutils.flask import hurl_for
|
|
4
4
|
from flask_login.utils import _get_user
|
5
5
|
from functools import wraps
|
6
6
|
from hiddifypanel.models import *
|
7
|
-
|
7
|
+
from apiflask import abort as json_abort
|
8
8
|
from hiddifypanel import hutils
|
9
9
|
from werkzeug.local import LocalProxy
|
10
10
|
current_account: "BaseAccount" = LocalProxy(lambda: _get_user())
|
@@ -97,14 +97,17 @@ def login_user(user: AdminUser | User, remember=False, duration=None, force=Fals
|
|
97
97
|
return True
|
98
98
|
|
99
99
|
|
100
|
-
def login_required(roles: set[Role] | None = None):
|
100
|
+
def login_required(roles: set[Role] | None = None, node_auth: bool = False):
|
101
|
+
'''When both roles and node_auth is set, means authentication can be done by either uuid or unique_id'''
|
101
102
|
def wrapper(fn):
|
102
103
|
@wraps(fn)
|
103
104
|
def decorated_view(*args, **kwargs):
|
104
105
|
# print('xxxx', current_account)
|
105
|
-
if not
|
106
|
+
if node_auth and not Child.node and not roles:
|
107
|
+
json_abort(403, 'Unauthorized node')
|
108
|
+
if not current_account and not node_auth:
|
106
109
|
return redirect_to_login() # type: ignore
|
107
|
-
if roles:
|
110
|
+
if roles and not Child.node:
|
108
111
|
account_role = current_account.role
|
109
112
|
if account_role not in roles:
|
110
113
|
return redirect_to_login() # type: ignore
|
@@ -151,6 +154,12 @@ def auth_before_request():
|
|
151
154
|
|
152
155
|
elif apikey := request.headers.get("Hiddify-API-Key"):
|
153
156
|
account = get_account_by_api_key(apikey, is_admin_path)
|
157
|
+
if not account:
|
158
|
+
# when parent/child panel needs to call another parent/child api, it will pass its unique id in the header as apikey
|
159
|
+
if node := Child.by_unique_id(apikey):
|
160
|
+
g.node = node
|
161
|
+
return
|
162
|
+
|
154
163
|
if not account:
|
155
164
|
return logout_redirect()
|
156
165
|
elif request.authorization:
|
@@ -201,6 +210,8 @@ def logout_redirect():
|
|
201
210
|
|
202
211
|
|
203
212
|
def redirect_to_login():
|
213
|
+
if hutils.flask.is_api_call(request.path):
|
214
|
+
json_abort(403, 'Unathorized')
|
204
215
|
# if g.user_agent['is_browser']:
|
205
216
|
# return redirect(hurl_for('common_bp.LoginView:basic_0', force=1, next=request.path))
|
206
217
|
return redirect(hurl_for('common_bp.LoginView:index', force=1, next=request.path))
|
hiddifypanel/base.py
CHANGED
@@ -11,55 +11,57 @@ import datetime
|
|
11
11
|
|
12
12
|
from dotenv import dotenv_values
|
13
13
|
import os
|
14
|
+
import sys
|
14
15
|
from apiflask import APIFlask
|
15
16
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
16
|
-
|
17
|
+
from loguru import logger
|
17
18
|
from hiddifypanel.panel.init_db import init_db
|
18
|
-
from hiddifypanel.cache import redis_client
|
19
|
-
from hiddifypanel import auth
|
20
|
-
from hiddifypanel.panel import hiddify
|
21
|
-
from hiddifypanel import hutils
|
22
19
|
|
23
20
|
|
24
21
|
def create_app(*args, cli=False, **config):
|
25
22
|
|
26
23
|
app = APIFlask(__name__, static_url_path="/<proxy_path>/static/", instance_relative_config=True, version='2.0.0', title="Hiddify API",
|
27
|
-
openapi_blueprint_url_prefix="/<proxy_path>/api", docs_ui='elements', json_errors=False, enable_openapi=
|
24
|
+
openapi_blueprint_url_prefix="/<proxy_path>/api", docs_ui='elements', json_errors=False, enable_openapi=not cli)
|
28
25
|
# app = Flask(__name__, static_url_path="/<proxy_path>/static/", instance_relative_config=True)
|
29
26
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
'
|
43
|
-
'
|
44
|
-
'
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
27
|
+
if not cli:
|
28
|
+
from hiddifypanel.cache import redis_client
|
29
|
+
from hiddifypanel import auth
|
30
|
+
app.config["PREFERRED_URL_SCHEME"] = "https"
|
31
|
+
app.wsgi_app = ProxyFix(
|
32
|
+
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1,
|
33
|
+
)
|
34
|
+
app.servers = {
|
35
|
+
'name': 'current',
|
36
|
+
'url': '',
|
37
|
+
} # type: ignore
|
38
|
+
app.info = {
|
39
|
+
'description': 'Hiddify is a free and open source software. It is as it is.',
|
40
|
+
'termsOfService': 'https://hiddify.com',
|
41
|
+
'contact': {
|
42
|
+
'name': 'API Support',
|
43
|
+
'url': 'https://www.hiddify.com/support',
|
44
|
+
'email': 'panel@hiddify.com'
|
45
|
+
},
|
46
|
+
'license': {
|
47
|
+
'name': 'Creative Commons Zero v1.0 Universal',
|
48
|
+
'url': 'https://github.com/hiddify/Hiddify-Manager/blob/main/LICENSE'
|
49
|
+
}
|
49
50
|
}
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
51
|
+
# setup flask server-side session
|
52
|
+
# app.config['APPLICATION_ROOT'] = './'
|
53
|
+
# app.config['SESSION_COOKIE_DOMAIN'] = '/'
|
54
|
+
app.config['SESSION_TYPE'] = 'redis'
|
55
|
+
app.config['SESSION_REDIS'] = redis_client
|
56
|
+
app.config['SESSION_PERMANENT'] = True
|
57
|
+
app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=10)
|
58
|
+
Session(app)
|
59
|
+
|
60
|
+
app.jinja_env.line_statement_prefix = '%'
|
61
|
+
from hiddifypanel import hutils
|
62
|
+
app.jinja_env.filters['b64encode'] = hutils.encode.do_base_64
|
63
|
+
app.view_functions['admin.static'] = {} # fix bug in apiflask
|
64
|
+
flask_bootstrap.Bootstrap4(app)
|
63
65
|
|
64
66
|
for c, v in dotenv_values(os.environ.get("HIDDIFY_CFG_PATH", 'app.cfg')).items():
|
65
67
|
if v.isdecimal():
|
@@ -72,27 +74,34 @@ def create_app(*args, cli=False, **config):
|
|
72
74
|
hiddifypanel.database.init_app(app)
|
73
75
|
with app.app_context():
|
74
76
|
init_db()
|
75
|
-
# flaskbabel = FlaskBabel(app)
|
76
77
|
|
78
|
+
# configure logger
|
79
|
+
from hiddifypanel.models import ConfigEnum, hconfig
|
80
|
+
logger.remove()
|
81
|
+
log_format = '<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level> | <level>{extra}</level>'
|
82
|
+
logger.add(sys.stderr, format=log_format, level=hconfig(ConfigEnum.log_level), colorize=True, catch=True, enqueue=True, diagnose=False, backtrace=True)
|
83
|
+
|
84
|
+
# flaskbabel = FlaskBabel(app)
|
77
85
|
# @babel.localeselector
|
86
|
+
|
78
87
|
def get_locale():
|
79
88
|
# Put your logic here. Application can store locale in
|
80
89
|
# user profile, cookie, session, etc.
|
81
|
-
from hiddifypanel.models import ConfigEnum, hconfig
|
82
90
|
if "admin" in request.base_url:
|
83
91
|
g.locale = auth.current_account.lang or hconfig(ConfigEnum.admin_lang) or 'fa'
|
84
92
|
else:
|
85
93
|
g.locale = auth.current_account.lang or hconfig(ConfigEnum.lang) or 'fa'
|
86
94
|
return g.locale
|
95
|
+
app.jinja_env.globals['get_locale'] = get_locale
|
87
96
|
babel = Babel(app, locale_selector=get_locale)
|
97
|
+
if not cli:
|
98
|
+
hiddifypanel.panel.common.init_app(app)
|
99
|
+
hiddifypanel.panel.common_bp.init_app(app)
|
88
100
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
admin.init_app(app)
|
94
|
-
user.init_app(app)
|
95
|
-
commercial.init_app(app)
|
101
|
+
from hiddifypanel.panel import user, commercial, admin
|
102
|
+
admin.init_app(app)
|
103
|
+
user.init_app(app)
|
104
|
+
commercial.init_app(app)
|
96
105
|
|
97
106
|
app.config.update(config) # Override with passed config
|
98
107
|
# app.config['WTF_CSRF_CHECK_DEFAULT'] = False
|
@@ -115,8 +124,6 @@ def create_app(*args, cli=False, **config):
|
|
115
124
|
# return
|
116
125
|
# csrf.protect()
|
117
126
|
|
118
|
-
app.jinja_env.globals['get_locale'] = get_locale
|
119
|
-
|
120
127
|
hiddifypanel.panel.cli.init_app(app)
|
121
128
|
return app
|
122
129
|
|
@@ -126,7 +133,8 @@ def create_app_wsgi(*args, **kwargs):
|
|
126
133
|
# that doesn't allow **config
|
127
134
|
# to be passed to create_app
|
128
135
|
# https://github.com/pallets/flask/issues/4170
|
129
|
-
|
136
|
+
cli = ("hiddifypanel" in sys.argv[0]) or (sys.argv[1] in ["update-usage", "all-configs", "admin_links", "admin_path"])
|
137
|
+
app = create_app(cli=cli)
|
130
138
|
return app
|
131
139
|
|
132
140
|
|
hiddifypanel/cache.py
CHANGED
@@ -1,47 +1,65 @@
|
|
1
|
-
from redis_cache import RedisCache
|
2
|
-
from
|
1
|
+
from redis_cache import RedisCache, CacheDecorator, compact_dump, chunks
|
2
|
+
from redis_cache import loads as redis_cache_loads
|
3
3
|
import redis
|
4
4
|
from pickle import dumps, loads
|
5
|
+
from loguru import logger
|
6
|
+
|
5
7
|
redis_client = redis.from_url('unix:///opt/hiddify-manager/other/redis/run.sock?db=0')
|
6
8
|
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
class CustomRedisCache(RedisCache):
|
11
|
+
|
12
|
+
def invalidate_all_cached_functions(self):
|
13
|
+
try:
|
14
|
+
logger.info("Invalidating all cached functions")
|
15
|
+
chunks_gen = chunks(f'{self.prefix}*', 500)
|
16
|
+
for keys in chunks_gen:
|
17
|
+
self.client.delete(*keys)
|
18
|
+
logger.success("Successfully invalidated all cached functions")
|
19
|
+
return True
|
20
|
+
except Exception as err:
|
21
|
+
with logger.contextualize(error=err):
|
22
|
+
logger.error("Failed to invalidate all cached functions")
|
23
|
+
return False
|
24
|
+
|
25
|
+
|
26
|
+
cache = CustomRedisCache(redis_client=redis_client, prefix="h", serializer=dumps, deserializer=loads)
|
12
27
|
|
13
28
|
|
14
29
|
# cache = RedisCache(redis_client=redis_client, exception_handler=exception_handler)
|
15
30
|
# cache = RedisCache(redis_client=redis_client, prefix="h", serializer=dumps, deserializer=loads, exception_handler=exception_handler)
|
16
|
-
cache = RedisCache(redis_client=redis_client, prefix="h", serializer=dumps, deserializer=loads)
|
17
31
|
|
32
|
+
# def exception_handler(e, original_fn, args, kwargs):
|
33
|
+
# print("cache exception occur", e, original_fn, args, kwargs)
|
34
|
+
# return original_fn(*args, **kwargs)
|
35
|
+
# pass
|
18
36
|
|
19
|
-
class CacheDecorator:
|
20
|
-
|
21
|
-
|
37
|
+
# class CacheDecorator:
|
38
|
+
# def __init__(self, *args, **kwargs):
|
39
|
+
# pass
|
22
40
|
|
23
|
-
|
24
|
-
|
25
|
-
|
41
|
+
# def __call__(self, fn):
|
42
|
+
# @wraps(fn)
|
43
|
+
# def inner(*args, **kwargs):
|
26
44
|
|
27
|
-
|
45
|
+
# parsed_result = fn(*args, **kwargs)
|
28
46
|
|
29
|
-
|
47
|
+
# return parsed_result
|
30
48
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
49
|
+
# inner.invalidate = self.invalidate
|
50
|
+
# inner.invalidate_all = self.invalidate_all
|
51
|
+
# inner.instance = self
|
52
|
+
# return inner
|
35
53
|
|
36
|
-
|
37
|
-
|
54
|
+
# def invalidate(self, *args, **kwargs):
|
55
|
+
# pass
|
38
56
|
|
39
|
-
|
40
|
-
|
57
|
+
# def invalidate_all(self, *args, **kwargs):
|
58
|
+
# pass
|
41
59
|
|
42
60
|
|
43
|
-
class DisableCache:
|
44
|
-
|
61
|
+
# class DisableCache:
|
62
|
+
# cache = CacheDecorator
|
45
63
|
|
46
64
|
|
47
65
|
# cache = DisableCache()
|
hiddifypanel/database.py
CHANGED
@@ -2,6 +2,8 @@ from flask_sqlalchemy import SQLAlchemy
|
|
2
2
|
from sqlalchemy_utils import UUIDType
|
3
3
|
import re
|
4
4
|
import os
|
5
|
+
from sqlalchemy import text
|
6
|
+
|
5
7
|
|
6
8
|
db: SQLAlchemy = SQLAlchemy()
|
7
9
|
db.UUID = UUIDType # type: ignore
|
@@ -10,3 +12,10 @@ db.UUID = UUIDType # type: ignore
|
|
10
12
|
def init_app(app):
|
11
13
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
|
12
14
|
db.init_app(app)
|
15
|
+
|
16
|
+
|
17
|
+
def db_execute(query: str, **params: dict):
|
18
|
+
with db.engine.connect() as connection:
|
19
|
+
res = connection.execute(text(query), params)
|
20
|
+
connection.commit()
|
21
|
+
return res
|
@@ -3,27 +3,22 @@ from hiddifypanel.models import *
|
|
3
3
|
from .abstract_driver import DriverABS
|
4
4
|
from flask import current_app
|
5
5
|
import json
|
6
|
+
from collections import defaultdict
|
6
7
|
|
7
8
|
|
8
9
|
class SingboxApi(DriverABS):
|
10
|
+
def is_enabled(self) -> bool: return True
|
11
|
+
|
9
12
|
def get_singbox_client(self):
|
10
|
-
if hconfig(ConfigEnum.is_parent):
|
11
|
-
return
|
12
13
|
return xtlsapi.SingboxClient('127.0.0.1', 10086)
|
13
14
|
|
14
15
|
def get_enabled_users(self):
|
15
|
-
if hconfig(ConfigEnum.is_parent):
|
16
|
-
return
|
17
16
|
config_dir = current_app.config['HIDDIFY_CONFIG_PATH']
|
18
17
|
with open(f"{config_dir}/singbox/configs/01_api.json") as f:
|
19
18
|
json_data = json.load(f)
|
20
19
|
return {u.split("@")[0]: 1 for u in json_data['experimental']['v2ray_api']['stats']['users']}
|
21
|
-
# raise NotImplementedError()
|
22
|
-
#
|
23
20
|
|
24
21
|
def get_inbound_tags(self):
|
25
|
-
if hconfig(ConfigEnum.is_parent):
|
26
|
-
return
|
27
22
|
try:
|
28
23
|
xray_client = self.get_singbox_client()
|
29
24
|
inbounds = [inb.name.split(">>>")[1] for inb in xray_client.stats_query('inbound')]
|
@@ -34,17 +29,24 @@ class SingboxApi(DriverABS):
|
|
34
29
|
return list(set(inbounds))
|
35
30
|
|
36
31
|
def add_client(self, user):
|
37
|
-
|
38
|
-
return
|
39
|
-
# raise NotImplementedError()
|
32
|
+
pass
|
40
33
|
|
41
34
|
def remove_client(self, user):
|
42
|
-
|
43
|
-
return
|
44
|
-
# raise NotImplementedError()
|
35
|
+
pass
|
45
36
|
|
46
37
|
def get_all_usage(self, users):
|
47
|
-
|
38
|
+
xray_client = self.get_singbox_client()
|
39
|
+
usages = xray_client.stats_query('user', reset=True)
|
40
|
+
uuid_user_map = {u.uuid: u for u in users}
|
41
|
+
res = defaultdict(int)
|
42
|
+
for use in usages:
|
43
|
+
if "user>>>" not in use.name:
|
44
|
+
continue
|
45
|
+
# print(use.name, use.value)
|
46
|
+
uuid = use.name.split(">>>")[1].split("@")[0]
|
47
|
+
res[uuid_user_map[uuid]] += use.value # uplink + downlink
|
48
|
+
return res
|
49
|
+
# return {u: self.get_usage_imp(u.uuid) for u in users}
|
48
50
|
|
49
51
|
def get_usage_imp(self, uuid):
|
50
52
|
xray_client = self.get_singbox_client()
|
@@ -4,19 +4,25 @@ from .singbox_api import SingboxApi
|
|
4
4
|
from .wireguard_api import WireguardApi
|
5
5
|
from hiddifypanel.models import *
|
6
6
|
from hiddifypanel.panel import hiddify
|
7
|
+
from collections import defaultdict
|
8
|
+
|
7
9
|
drivers = [XrayApi(), SingboxApi(), SSHLibertyBridgeApi(), WireguardApi()]
|
8
10
|
|
9
11
|
|
12
|
+
def enabled_drivers():
|
13
|
+
return [d for d in drivers if d.is_enabled()]
|
14
|
+
|
15
|
+
|
10
16
|
def get_users_usage(reset=True):
|
11
17
|
res = {}
|
12
18
|
users = list(User.query.all())
|
13
|
-
res =
|
14
|
-
for driver in
|
19
|
+
res = defaultdict(lambda: {'usage': 0, 'devices': ''})
|
20
|
+
for driver in enabled_drivers():
|
15
21
|
all_usage = driver.get_all_usage(users)
|
16
22
|
for user, usage in all_usage.items():
|
17
23
|
if usage:
|
18
24
|
res[user]['usage'] += usage
|
19
|
-
# res[user]['
|
25
|
+
# res[user]['devices'] +=usage
|
20
26
|
return res
|
21
27
|
|
22
28
|
|
@@ -24,7 +30,7 @@ def get_enabled_users():
|
|
24
30
|
from collections import defaultdict
|
25
31
|
d = defaultdict(int)
|
26
32
|
total = 0
|
27
|
-
for driver in
|
33
|
+
for driver in enabled_drivers():
|
28
34
|
try:
|
29
35
|
for u, v in driver.get_enabled_users().items():
|
30
36
|
if not v:
|
@@ -41,7 +47,7 @@ def get_enabled_users():
|
|
41
47
|
|
42
48
|
|
43
49
|
def add_client(user: User):
|
44
|
-
for driver in
|
50
|
+
for driver in enabled_drivers():
|
45
51
|
try:
|
46
52
|
driver.add_client(user)
|
47
53
|
except Exception as e:
|
@@ -49,7 +55,7 @@ def add_client(user: User):
|
|
49
55
|
|
50
56
|
|
51
57
|
def remove_client(user: User):
|
52
|
-
for driver in
|
58
|
+
for driver in enabled_drivers():
|
53
59
|
try:
|
54
60
|
driver.remove_client(user)
|
55
61
|
except Exception as e:
|
@@ -7,6 +7,8 @@ from hiddifypanel.panel.run_commander import Command, commander
|
|
7
7
|
|
8
8
|
|
9
9
|
class WireguardApi(DriverABS):
|
10
|
+
def is_enabled(self) -> bool:
|
11
|
+
return hconfig(ConfigEnum.wireguard_enable)
|
10
12
|
WG_LOCAL_USAGE_FILE_PATH = './hiddify_usages.json'
|
11
13
|
|
12
14
|
def __init__(self) -> None:
|
hiddifypanel/drivers/xray_api.py
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
import xtlsapi
|
2
2
|
from hiddifypanel.models import *
|
3
3
|
from .abstract_driver import DriverABS
|
4
|
+
from collections import defaultdict
|
4
5
|
|
5
6
|
|
6
7
|
class XrayApi(DriverABS):
|
8
|
+
def is_enabled(self) -> bool:
|
9
|
+
return hconfig(ConfigEnum.core_type) == "xray"
|
10
|
+
|
7
11
|
def get_xray_client(self):
|
8
|
-
if hconfig(ConfigEnum.is_parent):
|
9
|
-
return
|
10
12
|
return xtlsapi.XrayClient('127.0.0.1', 10085)
|
11
13
|
|
12
14
|
def get_enabled_users(self):
|
13
|
-
if hconfig(ConfigEnum.is_parent):
|
14
|
-
return
|
15
15
|
xray_client = self.get_xray_client()
|
16
16
|
users = User.query.all()
|
17
17
|
t = "xtls"
|
@@ -31,8 +31,6 @@ class XrayApi(DriverABS):
|
|
31
31
|
return enabled
|
32
32
|
|
33
33
|
def get_inbound_tags(self):
|
34
|
-
if hconfig(ConfigEnum.is_parent):
|
35
|
-
return
|
36
34
|
try:
|
37
35
|
xray_client = self.get_xray_client()
|
38
36
|
inbounds = [inb.name.split(">>>")[1] for inb in xray_client.stats_query('inbound')]
|
@@ -43,8 +41,6 @@ class XrayApi(DriverABS):
|
|
43
41
|
return list(set(inbounds))
|
44
42
|
|
45
43
|
def add_client(self, user):
|
46
|
-
if hconfig(ConfigEnum.is_parent):
|
47
|
-
return
|
48
44
|
uuid = user.uuid
|
49
45
|
xray_client = self.get_xray_client()
|
50
46
|
tags = self.get_inbound_tags()
|
@@ -97,7 +93,16 @@ class XrayApi(DriverABS):
|
|
97
93
|
pass
|
98
94
|
|
99
95
|
def get_all_usage(self, users):
|
100
|
-
|
96
|
+
xray_client = self.get_xray_client()
|
97
|
+
usages = xray_client.stats_query('user', reset=True)
|
98
|
+
uuid_user_map = {u.uuid: u for u in users}
|
99
|
+
res = defaultdict(int)
|
100
|
+
for use in usages:
|
101
|
+
if "user>>>" not in use.name:
|
102
|
+
continue
|
103
|
+
uuid = use.name.split(">>>")[1].split("@")[0]
|
104
|
+
res[uuid_user_map[uuid]] += use.value
|
105
|
+
return res
|
101
106
|
|
102
107
|
def get_usage_imp(self, uuid):
|
103
108
|
xray_client = self.get_xray_client()
|
hiddifypanel/hutils/__init__.py
CHANGED
hiddifypanel/hutils/convert.py
CHANGED
@@ -38,13 +38,24 @@ def json_to_date(date_str: str) -> datetime | str:
|
|
38
38
|
|
39
39
|
|
40
40
|
def time_to_json(d: datetime) -> str | None:
|
41
|
+
return __fix_time_format(d.strftime("%Y-%m-%d %H:%M:%S")) if d else None
|
41
42
|
|
42
|
-
|
43
|
+
|
44
|
+
def __fix_time_format(time_str):
|
45
|
+
'Convert "1-00-00 00:00:00" to "0001-00-00 00:00:00"'
|
46
|
+
t = time_str
|
47
|
+
char_index = t.find('-')
|
48
|
+
year_part = t[:char_index]
|
49
|
+
|
50
|
+
if len(year_part) < 4:
|
51
|
+
t = year_part.zfill(4) + t[char_index:]
|
52
|
+
|
53
|
+
return t
|
43
54
|
|
44
55
|
|
45
56
|
def json_to_time(time_str: str) -> datetime | str:
|
46
57
|
try:
|
47
|
-
return datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
|
58
|
+
return datetime.strptime(__fix_time_format(time_str), "%Y-%m-%d %H:%M:%S")
|
48
59
|
except BaseException:
|
49
60
|
return time_str
|
50
61
|
|
hiddifypanel/hutils/crypto.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
import subprocess
|
2
|
+
from cryptography.hazmat.primitives import serialization
|
3
|
+
from cryptography.hazmat.primitives.asymmetric import x25519, ed25519
|
2
4
|
|
3
5
|
|
4
6
|
def get_ed25519_private_public_pair():
|
5
|
-
from cryptography.hazmat.primitives.asymmetric import ed25519
|
6
|
-
from cryptography.hazmat.primitives import serialization
|
7
7
|
privkey = ed25519.Ed25519PrivateKey.generate()
|
8
8
|
pubkey = privkey.public_key()
|
9
9
|
priv_bytes = privkey.private_bytes(
|
@@ -27,3 +27,22 @@ def get_wg_private_public_psk_pair():
|
|
27
27
|
except subprocess.CalledProcessError as e:
|
28
28
|
print(f"Error: {e}")
|
29
29
|
return None, None, None
|
30
|
+
|
31
|
+
|
32
|
+
def generate_x25519_keys():
|
33
|
+
priv = x25519.X25519PrivateKey.generate()
|
34
|
+
pub = priv.public_key()
|
35
|
+
priv_bytes = priv.private_bytes(
|
36
|
+
encoding=serialization.Encoding.Raw,
|
37
|
+
format=serialization.PrivateFormat.Raw,
|
38
|
+
encryption_algorithm=serialization.NoEncryption()
|
39
|
+
)
|
40
|
+
pub_bytes = pub.public_bytes(
|
41
|
+
encoding=serialization.Encoding.Raw,
|
42
|
+
format=serialization.PublicFormat.Raw
|
43
|
+
)
|
44
|
+
import base64
|
45
|
+
pub_str = base64.urlsafe_b64encode(pub_bytes).decode()[:-1]
|
46
|
+
priv_str = base64.urlsafe_b64encode(priv_bytes).decode()[:-1]
|
47
|
+
|
48
|
+
return {'private_key': priv_str, 'public_key': pub_str}
|
hiddifypanel/hutils/flask.py
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
from typing import List
|
1
|
+
from typing import List, Tuple
|
2
2
|
from flask import current_app, flash as flask_flash, g, request
|
3
3
|
from wtforms.validators import ValidationError
|
4
4
|
from apiflask import abort as apiflask_abort
|
5
5
|
from flask_babel import lazy_gettext as _
|
6
|
-
from flask import url_for
|
6
|
+
from flask import url_for # type: ignore
|
7
7
|
from urllib.parse import urlparse
|
8
|
+
from markupsafe import Markup
|
9
|
+
|
8
10
|
import user_agents
|
9
11
|
import re
|
10
12
|
import os
|
@@ -15,7 +17,6 @@ from hiddifypanel import hutils
|
|
15
17
|
|
16
18
|
|
17
19
|
def flash(message: str, category: str = "message"):
|
18
|
-
# print(message)
|
19
20
|
return flask_flash(Markup(message), category)
|
20
21
|
|
21
22
|
|
@@ -35,7 +36,7 @@ def static_url_for(**values):
|
|
35
36
|
|
36
37
|
|
37
38
|
def hurl_for(endpoint, **values):
|
38
|
-
if Child.current.id != 0:
|
39
|
+
if Child.current().id != 0:
|
39
40
|
|
40
41
|
new_endpoint = "child_" + endpoint
|
41
42
|
if new_endpoint in current_app.view_functions:
|
@@ -235,8 +236,21 @@ def validate_domain_exist(form, field):
|
|
235
236
|
|
236
237
|
def get_proxy_stats_url():
|
237
238
|
proxy_stats_url = f'{request.host_url}{g.proxy_path}/proxy-stats/'
|
238
|
-
params = f'hostname={proxy_stats_url}api/&port=443'
|
239
|
+
params = f'hostname={proxy_stats_url}api/&port=443&secret=hiddify'
|
239
240
|
return f'{proxy_stats_url}?{params}/'
|
241
|
+
|
242
|
+
|
243
|
+
def extract_parent_info_from_url(url) -> Tuple[str | None, str | None, str | None]:
|
244
|
+
pattern = r'^https?://([^/]+)/([^/]+)/([^/]+)/.*$'
|
245
|
+
match = re.match(pattern, url)
|
246
|
+
|
247
|
+
if match:
|
248
|
+
domain = match.group(1)
|
249
|
+
proxy_path = match.group(2)
|
250
|
+
admin_uuid = match.group(3)
|
251
|
+
return domain, proxy_path, admin_uuid
|
252
|
+
else:
|
253
|
+
return None, None, None
|
240
254
|
# region not used
|
241
255
|
|
242
256
|
|