hiddifypanel 10.14.0__py3-none-any.whl → 10.15.0.dev1__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 +11 -3
- hiddifypanel/cache.py +43 -25
- hiddifypanel/database.py +9 -0
- hiddifypanel/drivers/singbox_api.py +2 -14
- hiddifypanel/drivers/xray_api.py +0 -4
- 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 +13 -4
- 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 +43 -12
- hiddifypanel/panel/usage.py +28 -15
- hiddifypanel/panel/user/templates/home/usage.html +1 -1
- hiddifypanel/panel/user/templates/new.html +1 -1
- hiddifypanel/static/css/custom.css +13 -0
- hiddifypanel/static/images/hiddify.png +0 -0
- hiddifypanel/static/new/assets/{index-bce9b1a6.js → index-ccb9873c.js} +65 -65
- hiddifypanel/templates/admin-layout.html +24 -40
- hiddifypanel/templates/master.html +23 -41
- hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/en/LC_MESSAGES/messages.po +90 -0
- hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/fa/LC_MESSAGES/messages.po +91 -1
- 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 +90 -0
- hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/zh/LC_MESSAGES/messages.po +92 -2
- hiddifypanel/translations.i18n/en.json +56 -0
- hiddifypanel/translations.i18n/fa.json +57 -1
- hiddifypanel/translations.i18n/pt.json +63 -7
- hiddifypanel/translations.i18n/ru.json +56 -0
- hiddifypanel/translations.i18n/zh.json +58 -2
- {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev1.dist-info}/METADATA +47 -47
- {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev1.dist-info}/RECORD +103 -85
- 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/images/{hiddify1.png → hiddify-old.png} +0 -0
- /hiddifypanel/static/{new/assets/hiddify-logo-noroz-559c8dcb.png → images/hiddify2.png} +0 -0
- {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev1.dist-info}/LICENSE.md +0 -0
- {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev1.dist-info}/WHEEL +0 -0
- {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev1.dist-info}/entry_points.txt +0 -0
- {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev1.dist-info}/top_level.txt +0 -0
hiddifypanel/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
10.
|
1
|
+
10.15.0.dev1
|
hiddifypanel/VERSION.py
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
__version__='10.
|
1
|
+
__version__='10.15.0.dev1'
|
2
2
|
from datetime import datetime
|
3
|
-
__release_date__= datetime.strptime('2024-
|
3
|
+
__release_date__= datetime.strptime('2024-04-06','%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
@@ -14,7 +14,7 @@ import os
|
|
14
14
|
import sys
|
15
15
|
from apiflask import APIFlask
|
16
16
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
17
|
-
|
17
|
+
from loguru import logger
|
18
18
|
from hiddifypanel.panel.init_db import init_db
|
19
19
|
|
20
20
|
|
@@ -23,6 +23,7 @@ def create_app(*args, cli=False, **config):
|
|
23
23
|
app = APIFlask(__name__, static_url_path="/<proxy_path>/static/", instance_relative_config=True, version='2.0.0', title="Hiddify API",
|
24
24
|
openapi_blueprint_url_prefix="/<proxy_path>/api", docs_ui='elements', json_errors=False, enable_openapi=not cli)
|
25
25
|
# app = Flask(__name__, static_url_path="/<proxy_path>/static/", instance_relative_config=True)
|
26
|
+
|
26
27
|
if not cli:
|
27
28
|
from hiddifypanel.cache import redis_client
|
28
29
|
from hiddifypanel import auth
|
@@ -55,6 +56,7 @@ def create_app(*args, cli=False, **config):
|
|
55
56
|
app.config['SESSION_PERMANENT'] = True
|
56
57
|
app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=10)
|
57
58
|
Session(app)
|
59
|
+
|
58
60
|
app.jinja_env.line_statement_prefix = '%'
|
59
61
|
from hiddifypanel import hutils
|
60
62
|
app.jinja_env.filters['b64encode'] = hutils.encode.do_base_64
|
@@ -72,13 +74,19 @@ 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:
|
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
|
@@ -10,23 +10,15 @@ class SingboxApi(DriverABS):
|
|
10
10
|
def is_enabled(self) -> bool: return True
|
11
11
|
|
12
12
|
def get_singbox_client(self):
|
13
|
-
if hconfig(ConfigEnum.is_parent):
|
14
|
-
return
|
15
13
|
return xtlsapi.SingboxClient('127.0.0.1', 10086)
|
16
14
|
|
17
15
|
def get_enabled_users(self):
|
18
|
-
if hconfig(ConfigEnum.is_parent):
|
19
|
-
return
|
20
16
|
config_dir = current_app.config['HIDDIFY_CONFIG_PATH']
|
21
17
|
with open(f"{config_dir}/singbox/configs/01_api.json") as f:
|
22
18
|
json_data = json.load(f)
|
23
19
|
return {u.split("@")[0]: 1 for u in json_data['experimental']['v2ray_api']['stats']['users']}
|
24
|
-
# raise NotImplementedError()
|
25
|
-
#
|
26
20
|
|
27
21
|
def get_inbound_tags(self):
|
28
|
-
if hconfig(ConfigEnum.is_parent):
|
29
|
-
return
|
30
22
|
try:
|
31
23
|
xray_client = self.get_singbox_client()
|
32
24
|
inbounds = [inb.name.split(">>>")[1] for inb in xray_client.stats_query('inbound')]
|
@@ -37,14 +29,10 @@ class SingboxApi(DriverABS):
|
|
37
29
|
return list(set(inbounds))
|
38
30
|
|
39
31
|
def add_client(self, user):
|
40
|
-
|
41
|
-
return
|
42
|
-
# raise NotImplementedError()
|
32
|
+
pass
|
43
33
|
|
44
34
|
def remove_client(self, user):
|
45
|
-
|
46
|
-
return
|
47
|
-
# raise NotImplementedError()
|
35
|
+
pass
|
48
36
|
|
49
37
|
def get_all_usage(self, users):
|
50
38
|
xray_client = self.get_singbox_client()
|
hiddifypanel/drivers/xray_api.py
CHANGED
@@ -41,10 +41,6 @@ class XrayApi(DriverABS):
|
|
41
41
|
return list(set(inbounds))
|
42
42
|
|
43
43
|
def add_client(self, user):
|
44
|
-
if hconfig(ConfigEnum.is_parent):
|
45
|
-
return
|
46
|
-
if hconfig(ConfigEnum.core_type) != "xray":
|
47
|
-
return
|
48
44
|
uuid = user.uuid
|
49
45
|
xray_client = self.get_xray_client()
|
50
46
|
tags = self.get_inbound_tags()
|
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:
|
@@ -236,7 +237,20 @@ def validate_domain_exist(form, field):
|
|
236
237
|
def get_proxy_stats_url():
|
237
238
|
proxy_stats_url = f'{request.host_url}{g.proxy_path}/proxy-stats/'
|
238
239
|
params = f'hostname={proxy_stats_url}api/&port=443&secret=hiddify'
|
239
|
-
return f'{proxy_stats_url}?{params}
|
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
|
|
@@ -6,15 +6,18 @@ from datetime import datetime
|
|
6
6
|
from dateutil.relativedelta import relativedelta
|
7
7
|
from hiddifypanel import hutils
|
8
8
|
from hiddifypanel.models import *
|
9
|
-
from hiddifypanel.database import db
|
9
|
+
from hiddifypanel.database import db, db_execute
|
10
10
|
import os
|
11
|
+
from sqlalchemy import text
|
11
12
|
|
12
13
|
|
13
14
|
def __query_fetch_json(db, query: str, args: Tuple = ()) -> List[Dict[str, Any]]:
|
14
15
|
try:
|
15
|
-
|
16
|
+
|
17
|
+
db_execute(query, args)
|
16
18
|
r = [dict((db.description[i][0], value)
|
17
19
|
for i, value in enumerate(row)) for row in db.fetchall()]
|
20
|
+
connection.close()
|
18
21
|
return r
|
19
22
|
except Exception as err:
|
20
23
|
raise err
|
@@ -0,0 +1,76 @@
|
|
1
|
+
from typing import Optional, Union, Type
|
2
|
+
from apiflask import Schema, fields
|
3
|
+
import traceback
|
4
|
+
import requests
|
5
|
+
from loguru import logger
|
6
|
+
from hiddifypanel.models import hconfig, ConfigEnum
|
7
|
+
|
8
|
+
|
9
|
+
class NodeApiErrorSchema(Schema):
|
10
|
+
msg = fields.String(required=True)
|
11
|
+
stacktrace = fields.String(required=True)
|
12
|
+
code = fields.Integer(required=True)
|
13
|
+
reason = fields.String(required=True)
|
14
|
+
|
15
|
+
|
16
|
+
class NodeApiClient():
|
17
|
+
def __init__(self, base_url: str, apikey: Optional[str] = None, max_retry: int = 3):
|
18
|
+
self.base_url = base_url if base_url.endswith('/') else base_url+'/'
|
19
|
+
self.max_retry = max_retry
|
20
|
+
self.headers = {'Hiddify-API-Key': apikey or hconfig(ConfigEnum.unique_id)}
|
21
|
+
|
22
|
+
def __call(self, method: str, path: str, payload: Optional[Schema], output_schema: Type[Union[Schema, dict]]) -> Union[dict, NodeApiErrorSchema]: # type: ignore
|
23
|
+
retry_count = 1
|
24
|
+
full_url = self.base_url + path.removeprefix('/')
|
25
|
+
while 1:
|
26
|
+
try:
|
27
|
+
# TODO: implement it with aiohttp
|
28
|
+
|
29
|
+
logger.trace(f"Attempting {method} request to node at {full_url}")
|
30
|
+
|
31
|
+
# send request
|
32
|
+
if payload:
|
33
|
+
response = requests.request(method, full_url, json=payload.dump(payload), headers=self.headers)
|
34
|
+
else:
|
35
|
+
response = requests.request(method, full_url, headers=self.headers)
|
36
|
+
|
37
|
+
# parse response
|
38
|
+
response.raise_for_status()
|
39
|
+
resp = response.json()
|
40
|
+
if not resp:
|
41
|
+
err = NodeApiErrorSchema()
|
42
|
+
err.msg = 'Empty response' # type: ignore
|
43
|
+
err.stacktrace = '' # type: ignore
|
44
|
+
err.code = response.status_code # type: ignore
|
45
|
+
err.reason = response.reason # type: ignore
|
46
|
+
with logger.contextualize(payload=payload):
|
47
|
+
logger.warning(f"Received empty response from {full_url} with method {method}")
|
48
|
+
return err
|
49
|
+
|
50
|
+
logger.trace(f"Successfully received response from {full_url}")
|
51
|
+
return resp if isinstance(output_schema, type(dict)) else output_schema().load(resp) # type: ignore
|
52
|
+
|
53
|
+
except requests.HTTPError as e:
|
54
|
+
if retry_count >= self.max_retry:
|
55
|
+
stack_trace = traceback.format_exc()
|
56
|
+
err = NodeApiErrorSchema()
|
57
|
+
err.msg = str(e) # type: ignore
|
58
|
+
err.stacktrace = stack_trace # type: ignore
|
59
|
+
err.code = response.status_code # type: ignore
|
60
|
+
err.reason = response.reason # type: ignore
|
61
|
+
with logger.contextualize(status_code=err.code, reason=err.reason, stack_trace=stack_trace, payload=payload):
|
62
|
+
logger.error(f"HTTP error after {self.max_retry} retries")
|
63
|
+
logger.exception(e)
|
64
|
+
return err
|
65
|
+
|
66
|
+
logger.warning(f"Error occurred: {e} from {full_url} with method {method}, retrying... ({retry_count}/{self.max_retry})")
|
67
|
+
retry_count += 1
|
68
|
+
|
69
|
+
def get(self, path: str, output: Type[Union[Schema, dict]]) -> Union[dict, NodeApiErrorSchema]:
|
70
|
+
return self.__call("GET", path, None, output)
|
71
|
+
|
72
|
+
def post(self, path: str, payload: Optional[Schema], output: Type[Union[Schema, dict]]) -> Union[dict, NodeApiErrorSchema]:
|
73
|
+
return self.__call("POST", path, payload, output)
|
74
|
+
|
75
|
+
def put(self, path: str, payload: Optional[Schema], output: Type[Union[Schema, dict]]) -> Union[dict, NodeApiErrorSchema]:
|
76
|
+
return self.__call("PUT", path, payload, output)
|
@@ -0,0 +1,147 @@
|
|
1
|
+
from loguru import logger
|
2
|
+
import socket
|
3
|
+
|
4
|
+
from hiddifypanel.models import AdminUser, User, hconfig, ConfigEnum, ChildMode, set_hconfig, Domain, Proxy, StrConfig, BoolConfig, Child, ChildMode
|
5
|
+
from hiddifypanel import hutils
|
6
|
+
from hiddifypanel.panel import hiddify
|
7
|
+
from hiddifypanel.panel import usage
|
8
|
+
from hiddifypanel.database import db
|
9
|
+
from hiddifypanel.cache import cache
|
10
|
+
|
11
|
+
# import schmeas
|
12
|
+
from hiddifypanel.panel.commercial.restapi.v2.parent.schema import *
|
13
|
+
from hiddifypanel.panel.commercial.restapi.v2.child.schema import *
|
14
|
+
|
15
|
+
from .api_client import NodeApiClient, NodeApiErrorSchema
|
16
|
+
# region private
|
17
|
+
|
18
|
+
|
19
|
+
def __get_register_data_for_api(name: str, mode: ChildMode) -> RegisterInputSchema:
|
20
|
+
|
21
|
+
register_data = RegisterInputSchema()
|
22
|
+
register_data.unique_id = hconfig(ConfigEnum.unique_id)
|
23
|
+
register_data.name = name # type: ignore
|
24
|
+
register_data.mode = mode # type: ignore
|
25
|
+
|
26
|
+
panel_data = RegisterDataSchema() # type: ignore
|
27
|
+
panel_data.admin_users = [admin_user.to_schema() for admin_user in AdminUser.query.all()] # type: ignore
|
28
|
+
panel_data.users = [user.to_schema() for user in User.query.all()] # type: ignore
|
29
|
+
panel_data.domains = [domain.to_schema() for domain in Domain.query.all()] # type: ignore
|
30
|
+
panel_data.proxies = [proxy.to_schema() for proxy in Proxy.query.all()] # type: ignore
|
31
|
+
panel_data.hconfigs = [*[u.to_schema() for u in StrConfig.query.all()], *[u.to_schema() for u in BoolConfig.query.all()]] # type: ignore
|
32
|
+
register_data.panel_data = panel_data
|
33
|
+
|
34
|
+
return register_data
|
35
|
+
|
36
|
+
|
37
|
+
def __get_sync_data_for_api() -> SyncInputSchema:
|
38
|
+
sync_data = SyncInputSchema()
|
39
|
+
sync_data.domains = [domain.to_schema() for domain in Domain.query.all()] # type: ignore
|
40
|
+
sync_data.proxies = [proxy.to_schema() for proxy in Proxy.query.all()] # type: ignore
|
41
|
+
sync_data.hconfigs = [*[u.to_schema() for u in StrConfig.query.all()], *[u.to_schema() for u in BoolConfig.query.all()]] # type: ignore
|
42
|
+
|
43
|
+
return sync_data
|
44
|
+
|
45
|
+
|
46
|
+
def __get_parent_panel_url() -> str:
|
47
|
+
url = 'https://' + f"{hconfig(ConfigEnum.parent_domain).removesuffix('/')}/{hconfig(ConfigEnum.parent_admin_proxy_path).removesuffix('/')}"
|
48
|
+
return url
|
49
|
+
|
50
|
+
# endregion
|
51
|
+
|
52
|
+
|
53
|
+
def is_registered() -> bool:
|
54
|
+
'''Checks if the current parent registered as a child'''
|
55
|
+
try:
|
56
|
+
logger.debug("Checking if current panel is registered with parent")
|
57
|
+
base_url = __get_parent_panel_url()
|
58
|
+
if not base_url:
|
59
|
+
return False
|
60
|
+
payload = ChildStatusInputSchema()
|
61
|
+
payload.child_unique_id = hconfig(ConfigEnum.unique_id)
|
62
|
+
|
63
|
+
res = NodeApiClient(base_url).post('/api/v2/parent/status/', payload, ChildStatusOutputSchema)
|
64
|
+
if isinstance(res, NodeApiErrorSchema):
|
65
|
+
logger.error(f"Error while checking if current panel is registered with parent: {res.msg}")
|
66
|
+
return False
|
67
|
+
|
68
|
+
if res['existance']:
|
69
|
+
return True
|
70
|
+
return False
|
71
|
+
except Exception as e:
|
72
|
+
logger.error(f"Error while checking if current panel is registered with parent")
|
73
|
+
logger.exception(e)
|
74
|
+
return False
|
75
|
+
|
76
|
+
|
77
|
+
def register_to_parent(name: str, apikey: str, mode: ChildMode = ChildMode.remote) -> bool:
|
78
|
+
# get parent link its format is "https://panel.hiddify.com/<admin_proxy_path>/"
|
79
|
+
p_url = __get_parent_panel_url()
|
80
|
+
if not p_url:
|
81
|
+
logger.error("Parent url is empty")
|
82
|
+
return False
|
83
|
+
|
84
|
+
payload = __get_register_data_for_api(name, mode)
|
85
|
+
res = NodeApiClient(p_url, apikey).put('/api/v2/parent/register/', payload, RegisterOutputSchema)
|
86
|
+
if isinstance(res, NodeApiErrorSchema):
|
87
|
+
logger.error(f"Error while registering to parent: {res.msg}")
|
88
|
+
return False
|
89
|
+
|
90
|
+
# TODO: change the bulk_register and such methods to accept models instead of dict
|
91
|
+
AdminUser.bulk_register(res['admin_users'], commit=False)
|
92
|
+
User.bulk_register(res['users'], commit=False)
|
93
|
+
|
94
|
+
# add new child as parent
|
95
|
+
db.session.add( # type: ignore
|
96
|
+
Child(unique_id=res['parent_unique_id'], name=socket.gethostname() or res['parent_unique_id'], mode=ChildMode.parent)
|
97
|
+
)
|
98
|
+
|
99
|
+
db.session.commit() # type: ignore
|
100
|
+
|
101
|
+
logger.success("Successfully registered to parent")
|
102
|
+
cache.invalidate_all_cached_functions()
|
103
|
+
return True
|
104
|
+
|
105
|
+
|
106
|
+
def sync_with_parent() -> bool:
|
107
|
+
# sync usage first
|
108
|
+
if not sync_users_usage_with_parent():
|
109
|
+
logger.error("Error while syncing with parent: Failed to sync users usage")
|
110
|
+
return False
|
111
|
+
|
112
|
+
p_url = __get_parent_panel_url()
|
113
|
+
if not p_url:
|
114
|
+
logger.error("Error while syncing with parent: Parent url is empty")
|
115
|
+
return False
|
116
|
+
payload = __get_sync_data_for_api()
|
117
|
+
res = NodeApiClient(p_url).put('/api/v2/parent/sync/', payload, SyncOutputSchema)
|
118
|
+
if isinstance(res, NodeApiErrorSchema):
|
119
|
+
logger.error(f"Error while syncing with parent: {res.msg}")
|
120
|
+
return False
|
121
|
+
AdminUser.bulk_register(res['admin_users'], commit=False, remove=True)
|
122
|
+
User.bulk_register(res['users'], commit=False, remove=True)
|
123
|
+
db.session.commit() # type: ignore
|
124
|
+
logger.success("Successfully synced with parent")
|
125
|
+
cache.invalidate_all_cached_functions()
|
126
|
+
return True
|
127
|
+
|
128
|
+
|
129
|
+
def sync_users_usage_with_parent() -> bool:
|
130
|
+
p_url = __get_parent_panel_url()
|
131
|
+
if not p_url:
|
132
|
+
logger.error("Parent url is empty")
|
133
|
+
return False
|
134
|
+
|
135
|
+
payload = hutils.node.get_users_usage_data_for_api()
|
136
|
+
if payload:
|
137
|
+
res = NodeApiClient(p_url).put('/api/v2/parent/usage/', payload, UsageInputOutputSchema) # type: ignore
|
138
|
+
if isinstance(res, NodeApiErrorSchema):
|
139
|
+
logger.error(f"Error while syncing users usage with parent: {res.msg}")
|
140
|
+
return False
|
141
|
+
|
142
|
+
# parse usages data
|
143
|
+
res = hutils.node.convert_usage_api_response_to_dict(res) # type: ignore
|
144
|
+
usage.add_users_usage_uuid(res, hiddify.get_child(None), True)
|
145
|
+
logger.success(f"Successfully synced users usage with parent: {res}")
|
146
|
+
|
147
|
+
return True
|