hiddifypanel 10.14.0__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.
Files changed (107) hide show
  1. hiddifypanel/VERSION +1 -1
  2. hiddifypanel/VERSION.py +2 -2
  3. hiddifypanel/auth.py +15 -4
  4. hiddifypanel/base.py +11 -3
  5. hiddifypanel/cache.py +43 -25
  6. hiddifypanel/database.py +9 -0
  7. hiddifypanel/drivers/singbox_api.py +2 -14
  8. hiddifypanel/drivers/xray_api.py +0 -4
  9. hiddifypanel/hutils/__init__.py +1 -0
  10. hiddifypanel/hutils/convert.py +13 -2
  11. hiddifypanel/hutils/crypto.py +21 -2
  12. hiddifypanel/hutils/flask.py +18 -4
  13. hiddifypanel/hutils/importer/xui.py +5 -2
  14. hiddifypanel/hutils/node/__init__.py +3 -0
  15. hiddifypanel/hutils/node/api_client.py +76 -0
  16. hiddifypanel/hutils/node/child.py +147 -0
  17. hiddifypanel/hutils/node/parent.py +100 -0
  18. hiddifypanel/hutils/node/shared.py +65 -0
  19. hiddifypanel/hutils/proxy/shared.py +15 -3
  20. hiddifypanel/models/__init__.py +2 -2
  21. hiddifypanel/models/admin.py +14 -2
  22. hiddifypanel/models/base_account.py +3 -3
  23. hiddifypanel/models/child.py +30 -16
  24. hiddifypanel/models/config.py +39 -15
  25. hiddifypanel/models/config_enum.py +55 -8
  26. hiddifypanel/models/domain.py +28 -20
  27. hiddifypanel/models/parent_domain.py +2 -2
  28. hiddifypanel/models/proxy.py +13 -4
  29. hiddifypanel/models/report.py +2 -3
  30. hiddifypanel/models/usage.py +2 -2
  31. hiddifypanel/models/user.py +13 -4
  32. hiddifypanel/panel/admin/Actions.py +4 -6
  33. hiddifypanel/panel/admin/AdminstratorAdmin.py +13 -2
  34. hiddifypanel/panel/admin/Dashboard.py +5 -10
  35. hiddifypanel/panel/admin/DomainAdmin.py +12 -11
  36. hiddifypanel/panel/admin/NodeAdmin.py +6 -2
  37. hiddifypanel/panel/admin/ProxyAdmin.py +4 -3
  38. hiddifypanel/panel/admin/SettingAdmin.py +60 -21
  39. hiddifypanel/panel/admin/UserAdmin.py +10 -2
  40. hiddifypanel/panel/admin/templates/index.html +1 -1
  41. hiddifypanel/panel/admin/templates/parent_dash.html +2 -4
  42. hiddifypanel/panel/cli.py +16 -16
  43. hiddifypanel/panel/commercial/ProxyDetailsAdmin.py +10 -5
  44. hiddifypanel/panel/commercial/__init__.py +7 -5
  45. hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py +0 -5
  46. hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +2 -2
  47. hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py +4 -5
  48. hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +8 -35
  49. hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +4 -4
  50. hiddifypanel/panel/commercial/restapi/v2/admin/schema.py +157 -0
  51. hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py +3 -3
  52. hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +9 -73
  53. hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +1 -1
  54. hiddifypanel/panel/commercial/restapi/v2/child/__init__.py +18 -0
  55. hiddifypanel/panel/commercial/restapi/v2/child/actions.py +63 -0
  56. hiddifypanel/panel/commercial/restapi/v2/child/register_parent_api.py +34 -0
  57. hiddifypanel/panel/commercial/restapi/v2/child/schema.py +7 -0
  58. hiddifypanel/panel/commercial/restapi/v2/child/sync_parent_api.py +21 -0
  59. hiddifypanel/panel/commercial/restapi/v2/panel/__init__.py +13 -0
  60. hiddifypanel/panel/commercial/restapi/v2/panel/info.py +18 -0
  61. hiddifypanel/panel/commercial/restapi/v2/panel/ping_pong.py +23 -0
  62. hiddifypanel/panel/commercial/restapi/v2/panel/schema.py +7 -0
  63. hiddifypanel/panel/commercial/restapi/v2/parent/__init__.py +16 -0
  64. hiddifypanel/panel/commercial/restapi/v2/parent/register_api.py +65 -0
  65. hiddifypanel/panel/commercial/restapi/v2/parent/schema.py +115 -0
  66. hiddifypanel/panel/commercial/restapi/v2/parent/status_api.py +26 -0
  67. hiddifypanel/panel/commercial/restapi/v2/parent/sync_api.py +53 -0
  68. hiddifypanel/panel/commercial/restapi/v2/parent/usage_api.py +57 -0
  69. hiddifypanel/panel/commercial/telegrambot/admin.py +1 -2
  70. hiddifypanel/panel/common.py +21 -6
  71. hiddifypanel/panel/hiddify.py +9 -80
  72. hiddifypanel/panel/init_db.py +43 -12
  73. hiddifypanel/panel/usage.py +28 -15
  74. hiddifypanel/panel/user/templates/home/usage.html +1 -1
  75. hiddifypanel/panel/user/templates/new.html +1 -1
  76. hiddifypanel/static/css/custom.css +13 -0
  77. hiddifypanel/static/images/hiddify.png +0 -0
  78. hiddifypanel/static/new/assets/{index-bce9b1a6.js → index-ccb9873c.js} +65 -65
  79. hiddifypanel/templates/admin-layout.html +24 -40
  80. hiddifypanel/templates/fake.html +298 -0
  81. hiddifypanel/templates/master.html +23 -41
  82. hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
  83. hiddifypanel/translations/en/LC_MESSAGES/messages.po +90 -0
  84. hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
  85. hiddifypanel/translations/fa/LC_MESSAGES/messages.po +91 -1
  86. hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
  87. hiddifypanel/translations/pt/LC_MESSAGES/messages.po +98 -6
  88. hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
  89. hiddifypanel/translations/ru/LC_MESSAGES/messages.po +90 -0
  90. hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
  91. hiddifypanel/translations/zh/LC_MESSAGES/messages.po +92 -2
  92. hiddifypanel/translations.i18n/en.json +56 -0
  93. hiddifypanel/translations.i18n/fa.json +57 -1
  94. hiddifypanel/translations.i18n/pt.json +63 -7
  95. hiddifypanel/translations.i18n/ru.json +56 -0
  96. hiddifypanel/translations.i18n/zh.json +58 -2
  97. {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/METADATA +47 -47
  98. {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/RECORD +104 -86
  99. hiddifypanel/panel/commercial/restapi/v2/DTO.py +0 -9
  100. hiddifypanel/panel/commercial/restapi/v2/hello/__init__.py +0 -16
  101. hiddifypanel/panel/commercial/restapi/v2/hello/hello.py +0 -32
  102. /hiddifypanel/static/images/{hiddify1.png → hiddify-old.png} +0 -0
  103. /hiddifypanel/static/{new/assets/hiddify-logo-noroz-559c8dcb.png → images/hiddify2.png} +0 -0
  104. {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/LICENSE.md +0 -0
  105. {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/WHEEL +0 -0
  106. {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/entry_points.txt +0 -0
  107. {hiddifypanel-10.14.0.dist-info → hiddifypanel-10.15.0.dev0.dist-info}/top_level.txt +0 -0
hiddifypanel/VERSION CHANGED
@@ -1 +1 @@
1
- 10.14.0
1
+ 10.15.0.dev0
hiddifypanel/VERSION.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__='10.14.0'
1
+ __version__='10.15.0.dev0'
2
2
  from datetime import datetime
3
- __release_date__= datetime.strptime('2024-03-25','%Y-%m-%d')
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 current_account:
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 functools import wraps
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
- def exception_handler(e, original_fn, args, kwargs):
9
- print("cache exception occur", e, original_fn, args, kwargs)
10
- return original_fn(*args, **kwargs)
11
- pass
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
- def __init__(self, *args, **kwargs):
21
- pass
37
+ # class CacheDecorator:
38
+ # def __init__(self, *args, **kwargs):
39
+ # pass
22
40
 
23
- def __call__(self, fn):
24
- @wraps(fn)
25
- def inner(*args, **kwargs):
41
+ # def __call__(self, fn):
42
+ # @wraps(fn)
43
+ # def inner(*args, **kwargs):
26
44
 
27
- parsed_result = fn(*args, **kwargs)
45
+ # parsed_result = fn(*args, **kwargs)
28
46
 
29
- return parsed_result
47
+ # return parsed_result
30
48
 
31
- inner.invalidate = self.invalidate
32
- inner.invalidate_all = self.invalidate_all
33
- inner.instance = self
34
- return inner
49
+ # inner.invalidate = self.invalidate
50
+ # inner.invalidate_all = self.invalidate_all
51
+ # inner.instance = self
52
+ # return inner
35
53
 
36
- def invalidate(self, *args, **kwargs):
37
- pass
54
+ # def invalidate(self, *args, **kwargs):
55
+ # pass
38
56
 
39
- def invalidate_all(self, *args, **kwargs):
40
- pass
57
+ # def invalidate_all(self, *args, **kwargs):
58
+ # pass
41
59
 
42
60
 
43
- class DisableCache:
44
- cache = CacheDecorator
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
- if hconfig(ConfigEnum.is_parent):
41
- return
42
- # raise NotImplementedError()
32
+ pass
43
33
 
44
34
  def remove_client(self, user):
45
- if hconfig(ConfigEnum.is_parent):
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()
@@ -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()
@@ -11,3 +11,4 @@ from . import utils
11
11
  from . import model
12
12
  from . import crypto
13
13
  from . import proxy
14
+ from . import node
@@ -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
- return d.strftime("%Y-%m-%d %H:%M:%S") if d else None
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
 
@@ -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}
@@ -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, Markup # type: ignore
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:
@@ -237,6 +238,19 @@ 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
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
- db.execute(query, args) if args else db.execute(query)
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,3 @@
1
+ from .shared import *
2
+ from . import child
3
+ from . import parent
@@ -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