hiddifypanel 9.0.0.dev90__py3-none-any.whl → 10.5.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 (152) hide show
  1. hiddifypanel/VERSION +1 -1
  2. hiddifypanel/VERSION.py +2 -2
  3. hiddifypanel/auth.py +30 -9
  4. hiddifypanel/base.py +60 -52
  5. hiddifypanel/cache.py +43 -25
  6. hiddifypanel/database.py +9 -0
  7. hiddifypanel/drivers/abstract_driver.py +2 -0
  8. hiddifypanel/drivers/singbox_api.py +17 -15
  9. hiddifypanel/drivers/ssh_liberty_bridge_api.py +3 -1
  10. hiddifypanel/drivers/user_driver.py +12 -6
  11. hiddifypanel/drivers/wireguard_api.py +7 -2
  12. hiddifypanel/drivers/xray_api.py +14 -9
  13. hiddifypanel/hutils/__init__.py +4 -0
  14. hiddifypanel/hutils/convert.py +13 -2
  15. hiddifypanel/hutils/crypto.py +48 -0
  16. hiddifypanel/hutils/encode.py +4 -1
  17. hiddifypanel/hutils/flask.py +38 -5
  18. hiddifypanel/hutils/github_issue.py +1 -1
  19. hiddifypanel/hutils/importer/xui.py +5 -2
  20. hiddifypanel/{models/utils.py → hutils/model.py} +14 -4
  21. hiddifypanel/hutils/network/auto_ip_selector.py +2 -0
  22. hiddifypanel/hutils/network/net.py +46 -2
  23. hiddifypanel/hutils/node/__init__.py +3 -0
  24. hiddifypanel/hutils/node/api_client.py +76 -0
  25. hiddifypanel/hutils/node/child.py +147 -0
  26. hiddifypanel/hutils/node/parent.py +100 -0
  27. hiddifypanel/hutils/node/shared.py +65 -0
  28. hiddifypanel/hutils/proxy/__init__.py +5 -0
  29. hiddifypanel/hutils/proxy/clash.py +161 -0
  30. hiddifypanel/hutils/proxy/shared.py +434 -0
  31. hiddifypanel/hutils/proxy/singbox.py +339 -0
  32. hiddifypanel/hutils/proxy/xray.py +235 -0
  33. hiddifypanel/hutils/proxy/xrayjson.py +391 -0
  34. hiddifypanel/hutils/random.py +4 -0
  35. hiddifypanel/hutils/utils.py +4 -1
  36. hiddifypanel/models/__init__.py +2 -2
  37. hiddifypanel/models/admin.py +31 -17
  38. hiddifypanel/models/base_account.py +7 -7
  39. hiddifypanel/models/child.py +30 -16
  40. hiddifypanel/models/config.py +45 -16
  41. hiddifypanel/models/config_enum.py +68 -17
  42. hiddifypanel/models/domain.py +28 -20
  43. hiddifypanel/models/parent_domain.py +2 -2
  44. hiddifypanel/models/proxy.py +29 -20
  45. hiddifypanel/models/report.py +2 -3
  46. hiddifypanel/models/usage.py +2 -2
  47. hiddifypanel/models/user.py +33 -22
  48. hiddifypanel/panel/admin/Actions.py +13 -19
  49. hiddifypanel/panel/admin/AdminstratorAdmin.py +14 -3
  50. hiddifypanel/panel/admin/Dashboard.py +5 -10
  51. hiddifypanel/panel/admin/DomainAdmin.py +35 -48
  52. hiddifypanel/panel/admin/NodeAdmin.py +6 -2
  53. hiddifypanel/panel/admin/ProxyAdmin.py +6 -5
  54. hiddifypanel/panel/admin/QuickSetup.py +21 -20
  55. hiddifypanel/panel/admin/SettingAdmin.py +107 -62
  56. hiddifypanel/panel/admin/UserAdmin.py +22 -21
  57. hiddifypanel/panel/admin/templates/index.html +1 -1
  58. hiddifypanel/panel/admin/templates/model/user_list.html +44 -20
  59. hiddifypanel/panel/admin/templates/parent_dash.html +2 -4
  60. hiddifypanel/panel/admin/templates/result.html +2 -3
  61. hiddifypanel/panel/cf_api.py +1 -2
  62. hiddifypanel/panel/cli.py +16 -16
  63. hiddifypanel/panel/commercial/ProxyDetailsAdmin.py +16 -12
  64. hiddifypanel/panel/commercial/__init__.py +7 -5
  65. hiddifypanel/panel/commercial/restapi/v1/__init__.py +1 -1
  66. hiddifypanel/panel/commercial/restapi/v1/tgbot.py +1 -1
  67. hiddifypanel/panel/commercial/restapi/v1/tgmsg.py +14 -10
  68. hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py +0 -5
  69. hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +2 -2
  70. hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py +4 -5
  71. hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +8 -25
  72. hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +4 -4
  73. hiddifypanel/panel/commercial/restapi/v2/admin/schema.py +157 -0
  74. hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py +3 -3
  75. hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +9 -66
  76. hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +1 -1
  77. hiddifypanel/panel/commercial/restapi/v2/child/__init__.py +18 -0
  78. hiddifypanel/panel/commercial/restapi/v2/child/actions.py +63 -0
  79. hiddifypanel/panel/commercial/restapi/v2/child/register_parent_api.py +34 -0
  80. hiddifypanel/panel/commercial/restapi/v2/child/schema.py +7 -0
  81. hiddifypanel/panel/commercial/restapi/v2/child/sync_parent_api.py +21 -0
  82. hiddifypanel/panel/commercial/restapi/v2/panel/__init__.py +13 -0
  83. hiddifypanel/panel/commercial/restapi/v2/panel/info.py +18 -0
  84. hiddifypanel/panel/commercial/restapi/v2/panel/ping_pong.py +23 -0
  85. hiddifypanel/panel/commercial/restapi/v2/panel/schema.py +7 -0
  86. hiddifypanel/panel/commercial/restapi/v2/parent/__init__.py +16 -0
  87. hiddifypanel/panel/commercial/restapi/v2/parent/register_api.py +65 -0
  88. hiddifypanel/panel/commercial/restapi/v2/parent/schema.py +115 -0
  89. hiddifypanel/panel/commercial/restapi/v2/parent/status_api.py +26 -0
  90. hiddifypanel/panel/commercial/restapi/v2/parent/sync_api.py +53 -0
  91. hiddifypanel/panel/commercial/restapi/v2/parent/usage_api.py +57 -0
  92. hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +17 -23
  93. hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +23 -26
  94. hiddifypanel/panel/commercial/telegrambot/admin.py +1 -2
  95. hiddifypanel/panel/common.py +25 -8
  96. hiddifypanel/panel/common_bp/login.py +2 -2
  97. hiddifypanel/panel/hiddify.py +22 -185
  98. hiddifypanel/panel/init_db.py +102 -55
  99. hiddifypanel/panel/usage.py +33 -18
  100. hiddifypanel/panel/user/__init__.py +0 -1
  101. hiddifypanel/panel/user/templates/all_configs copy.txt +2 -2
  102. hiddifypanel/panel/user/templates/all_configs.txt +2 -2
  103. hiddifypanel/panel/user/templates/base_singbox_config.json.j2 +2 -1
  104. hiddifypanel/panel/user/templates/base_xray_config.json.j2 +125 -0
  105. hiddifypanel/panel/user/templates/clash_config copy.yml +1 -1
  106. hiddifypanel/panel/user/templates/clash_config.yml +4 -4
  107. hiddifypanel/panel/user/templates/clash_proxies.yml +1 -1
  108. hiddifypanel/panel/user/templates/home/all-configs.html +2 -2
  109. hiddifypanel/panel/user/templates/home/all-configs_old.html +1 -1
  110. hiddifypanel/panel/user/templates/home/ios copy.html +2 -2
  111. hiddifypanel/panel/user/templates/home/usage.html +1 -1
  112. hiddifypanel/panel/user/templates/new.html +2 -2
  113. hiddifypanel/panel/user/user.py +56 -50
  114. hiddifypanel/static/css/custom.css +31 -0
  115. hiddifypanel/static/images/favicon.ico +0 -0
  116. hiddifypanel/static/images/hiddify-old.png +0 -0
  117. hiddifypanel/static/images/hiddify.png +0 -0
  118. hiddifypanel/static/images/hiddify2.png +0 -0
  119. hiddifypanel/static/new/assets/{index-1b891a7c.js → index-ccb9873c.js} +56 -56
  120. hiddifypanel/static/new/assets/index-fa00de9a.css +1 -0
  121. hiddifypanel/static/new/i18n/en.json +6 -6
  122. hiddifypanel/static/new/i18n/fa.json +2 -2
  123. hiddifypanel/templates/admin-layout.html +30 -43
  124. hiddifypanel/templates/fake.html +0 -4
  125. hiddifypanel/templates/flaskadmin-layout.html +7 -3
  126. hiddifypanel/templates/master.html +11 -6
  127. hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
  128. hiddifypanel/translations/en/LC_MESSAGES/messages.po +2082 -1977
  129. hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
  130. hiddifypanel/translations/fa/LC_MESSAGES/messages.po +2035 -1924
  131. hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
  132. hiddifypanel/translations/pt/LC_MESSAGES/messages.po +1911 -1840
  133. hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
  134. hiddifypanel/translations/ru/LC_MESSAGES/messages.po +2036 -1881
  135. hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
  136. hiddifypanel/translations/zh/LC_MESSAGES/messages.po +1857 -1720
  137. hiddifypanel/translations.i18n/en.json +992 -933
  138. hiddifypanel/translations.i18n/fa.json +994 -935
  139. hiddifypanel/translations.i18n/pt.json +994 -935
  140. hiddifypanel/translations.i18n/ru.json +994 -935
  141. hiddifypanel/translations.i18n/zh.json +971 -912
  142. {hiddifypanel-9.0.0.dev90.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/METADATA +47 -47
  143. {hiddifypanel-9.0.0.dev90.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/RECORD +147 -120
  144. {hiddifypanel-9.0.0.dev90.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/WHEEL +1 -1
  145. hiddifypanel/panel/commercial/restapi/v2/DTO.py +0 -9
  146. hiddifypanel/panel/commercial/restapi/v2/hello/__init__.py +0 -16
  147. hiddifypanel/panel/commercial/restapi/v2/hello/hello.py +0 -32
  148. hiddifypanel/panel/user/link_maker.py +0 -1083
  149. hiddifypanel/static/new/assets/index-669b32c8.css +0 -1
  150. {hiddifypanel-9.0.0.dev90.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/LICENSE.md +0 -0
  151. {hiddifypanel-9.0.0.dev90.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/entry_points.txt +0 -0
  152. {hiddifypanel-9.0.0.dev90.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,48 @@
1
+ import subprocess
2
+ from cryptography.hazmat.primitives import serialization
3
+ from cryptography.hazmat.primitives.asymmetric import x25519, ed25519
4
+
5
+
6
+ def get_ed25519_private_public_pair():
7
+ privkey = ed25519.Ed25519PrivateKey.generate()
8
+ pubkey = privkey.public_key()
9
+ priv_bytes = privkey.private_bytes(
10
+ encoding=serialization.Encoding.PEM,
11
+ format=serialization.PrivateFormat.OpenSSH,
12
+ encryption_algorithm=serialization.NoEncryption(),
13
+ )
14
+ pub_bytes = pubkey.public_bytes(
15
+ encoding=serialization.Encoding.OpenSSH,
16
+ format=serialization.PublicFormat.OpenSSH,
17
+ )
18
+ return priv_bytes.decode(), pub_bytes.decode()
19
+
20
+
21
+ def get_wg_private_public_psk_pair():
22
+ try:
23
+ private_key = subprocess.run(["wg", "genkey"], capture_output=True, text=True, check=True).stdout.strip()
24
+ public_key = subprocess.run(["wg", "pubkey"], input=private_key, capture_output=True, text=True, check=True).stdout.strip()
25
+ psk = subprocess.run(["wg", "genpsk"], capture_output=True, text=True, check=True).stdout.strip()
26
+ return private_key, public_key, psk
27
+ except subprocess.CalledProcessError as e:
28
+ print(f"Error: {e}")
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,7 +1,6 @@
1
1
  import urllib.parse
2
2
  import base64
3
3
  import uuid
4
- import string
5
4
  from slugify import slugify
6
5
 
7
6
 
@@ -26,6 +25,10 @@ def is_valid_uuid(val: str, version: int | None = None) -> bool:
26
25
 
27
26
  return True
28
27
 
28
+
29
+ def convert_dict_to_url(dict):
30
+ return '&' + '&'.join([f'{k}={v}' for k, v in dict.items()]) if len(dict) else ''
31
+
29
32
  # not used
30
33
  # def is_assci_alphanumeric(str):
31
34
  # for c in str:
@@ -1,18 +1,22 @@
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
+ from wtforms.validators import ValidationError
3
4
  from apiflask import abort as apiflask_abort
4
5
  from flask_babel import lazy_gettext as _
5
- from flask import url_for, Markup # type: ignore
6
+ from flask import url_for # type: ignore
6
7
  from urllib.parse import urlparse
8
+ from markupsafe import Markup
9
+
7
10
  import user_agents
8
11
  import re
9
12
  import os
13
+
10
14
  from hiddifypanel.cache import cache
11
15
  from hiddifypanel.models import *
16
+ from hiddifypanel import hutils
12
17
 
13
18
 
14
19
  def flash(message: str, category: str = "message"):
15
- # print(message)
16
20
  return flask_flash(Markup(message), category)
17
21
 
18
22
 
@@ -32,7 +36,7 @@ def static_url_for(**values):
32
36
 
33
37
 
34
38
  def hurl_for(endpoint, **values):
35
- if Child.current.id != 0:
39
+ if Child.current().id != 0:
36
40
 
37
41
  new_endpoint = "child_" + endpoint
38
42
  if new_endpoint in current_app.view_functions:
@@ -217,7 +221,36 @@ def proxy_path_validator(proxy_path: str) -> None:
217
221
 
218
222
 
219
223
  def list_dir_files(dir_path: str) -> List[str]:
220
- return [f for f in os.listdir(dir_path) if os.path.isfile(os.path.join(dir_path, f))]
224
+ return sorted([f for f in os.listdir(dir_path) if os.path.isfile(os.path.join(dir_path, f))])
225
+
226
+
227
+ def validate_domain_exist(form, field):
228
+ domain = field.data
229
+ if not domain:
230
+ return
231
+ dip = hutils.network.get_domain_ip(domain)
232
+ if dip is None:
233
+ raise ValidationError(
234
+ _("Domain can not be resolved! there is a problem in your domain")) # type: ignore
235
+
236
+
237
+ def get_proxy_stats_url():
238
+ proxy_stats_url = f'{request.host_url}{g.proxy_path}/proxy-stats/'
239
+ params = f'hostname={proxy_stats_url}api/&port=443&secret=hiddify'
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
221
254
  # region not used
222
255
 
223
256
 
@@ -12,7 +12,6 @@ from flask import g, request, render_template
12
12
  import hiddifypanel
13
13
  from hiddifypanel.models.config import hconfig
14
14
  from hiddifypanel.models.config_enum import ConfigEnum
15
- from hiddifypanel.auth import current_account
16
15
 
17
16
 
18
17
  class __IssueUrl:
@@ -136,6 +135,7 @@ def __github_issue_details() -> dict:
136
135
 
137
136
 
138
137
  def __remove_sensetive_data_from_github_issue_link(issue_link: str):
138
+ from hiddifypanel.auth import current_account
139
139
  if current_account.uuid:
140
140
  issue_link.replace(f'{current_account.uuid}', '*******************')
141
141
 
@@ -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
@@ -1,6 +1,4 @@
1
-
2
-
3
- def fill_username(model) -> None:
1
+ def gen_username(model) -> None:
4
2
  from hiddifypanel import hutils
5
3
  if model.username:
6
4
  return
@@ -19,8 +17,20 @@ def fill_username(model) -> None:
19
17
  model.username += rand_str
20
18
 
21
19
 
22
- def fill_password(model) -> None:
20
+ def gen_password(model) -> None:
23
21
  from hiddifypanel import hutils
24
22
  # TODO: hash the password
25
23
  if not model.password or len(model.password) < 16:
26
24
  model.password = hutils.random.get_random_password(length=16)
25
+
26
+
27
+ def gen_wg_keys(model) -> None:
28
+ from hiddifypanel import hutils
29
+ if not model.wg_pk or not model.wg_pub or not model.wg_psk:
30
+ model.wg_pk, model.wg_pub, model.wg_psk = hutils.crypto.get_wg_private_public_psk_pair()
31
+
32
+
33
+ def gen_ed25519_keys(model) -> None:
34
+ from hiddifypanel import hutils
35
+ if not model.ed25519_private_key or not model.ed25519_public_key:
36
+ model.ed25519_private_key, model.ed25519_public_key = hutils.crypto.get_ed25519_private_public_pair()
@@ -116,6 +116,8 @@ def get_real_user_ip_debug(user_ip: str = '') -> str:
116
116
 
117
117
  @cache.cache()
118
118
  def __get_real_user_ip_debug_imp(user_ip) -> str:
119
+ if type(user_ip) is str and ',' in user_ip:
120
+ user_ip = user_ip.split(',')[0]
119
121
  asnres = IPASN.get(user_ip) or {}
120
122
  asn = f"{asnres.get('autonomous_system_number','unknown')}" if asnres else "unknown"
121
123
  asn_dscr = f"{asnres.get('autonomous_system_organization','unknown')}" if asnres else "unknown"
@@ -1,7 +1,8 @@
1
- from typing import List, Literal, Union
1
+ from typing import List, Literal, Tuple, Union
2
2
  from urllib.parse import urlparse
3
3
  import urllib.request
4
4
  import ipaddress
5
+ from hiddifypanel.hutils.network.auto_ip_selector import IPASN
5
6
  import netifaces
6
7
  import requests
7
8
  import random
@@ -16,7 +17,7 @@ from hiddifypanel.models import *
16
17
  from hiddifypanel.cache import cache
17
18
 
18
19
 
19
- def get_domain_ip(domain: str, retry: int = 3, version: Literal[4, 6] = None) -> Union[ipaddress.IPv4Address, ipaddress.IPv6Address, None]:
20
+ def get_domain_ip(domain: str, retry: int = 3, version: Literal[4, 6] | None = None) -> Union[ipaddress.IPv4Address, ipaddress.IPv6Address, None]:
20
21
  res = None
21
22
  if not version:
22
23
  try:
@@ -326,3 +327,46 @@ def add_number_to_ipv6(ip: str, number: int) -> str:
326
327
  modified_ipv6 = ":".join(segments)
327
328
 
328
329
  return modified_ipv6
330
+
331
+
332
+ def is_in_same_asn(domain_or_ip: str, domain_or_ip_target: str) -> bool:
333
+ '''Returns True if domain is in panel ASN'''
334
+ if not IPASN:
335
+ return False
336
+ try:
337
+ ip = domain_or_ip if is_ip(domain_or_ip) else get_domain_ip(domain_or_ip)
338
+ ip_target = domain_or_ip_target if is_ip(domain_or_ip_target) else get_domain_ip(domain_or_ip_target)
339
+
340
+ if not ip or not ip_target:
341
+ return False
342
+
343
+ ip_asn = get_ip_asn_name(ip)
344
+ ip_target_asn = get_ip_asn_name(ip_target)
345
+
346
+ if not ip_asn or not ip_target_asn:
347
+ return False
348
+
349
+ return ip_asn == ip_target_asn
350
+ except Exception as e:
351
+ print(f"An error occurred: {e}")
352
+ return False
353
+
354
+ # hutils.flask.flash(_("selected domain for REALITY is not in the same ASN. To better use of the protocol, it is better to find a domain in the same ASN.") +
355
+ # f"<br> Server ASN={asn_ipv4.get('autonomous_system_organization','unknown')}<br>{domain}_ASN={asn_dip.get('autonomous_system_organization','unknown')}", "warning")
356
+
357
+
358
+ def get_ip_asn_name(ip: ipaddress.IPv4Address | ipaddress.IPv6Address | str) -> str:
359
+ try:
360
+ if asn := IPASN.get(str(ip)):
361
+ return str(asn.get('autonomous_system_organization', ''))
362
+ return ''
363
+ except:
364
+ return ''
365
+
366
+
367
+ def is_ip(input: str):
368
+ try:
369
+ _ = ipaddress.ip_address(input)
370
+ return True
371
+ except:
372
+ return False
@@ -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
@@ -0,0 +1,100 @@
1
+ from flask import g
2
+ from flask_babel import lazy_gettext as _
3
+ from typing import List
4
+ from loguru import logger
5
+
6
+ from hiddifypanel.models import Child, AdminUser, ConfigEnum, Domain, ChildMode, hconfig, get_panel_link
7
+ from hiddifypanel import hutils
8
+ from hiddifypanel.panel.commercial.restapi.v2.child.schema import RegisterWithParentInputSchema
9
+ from .api_client import NodeApiClient, NodeApiErrorSchema
10
+
11
+ from hiddifypanel.cache import cache
12
+
13
+
14
+ def request_childs_to_sync():
15
+ for c in Child.query.filter(Child.id != 0).all():
16
+ if not request_child_to_sync(c):
17
+ logger.error(f'{c.name}: {_("parent.sync-req-failed")}')
18
+ hutils.flask.flash(f'{c.name}: ' + _('parent.sync-req-failed'), 'danger')
19
+
20
+
21
+ def request_child_to_sync(child: Child) -> bool:
22
+ '''Requests to a child to sync itself with the current panel'''
23
+ child_domain = get_panel_link(child.id)
24
+ if not child_domain:
25
+ logger.error(f"Child {child.name} has no valid domain")
26
+ return False
27
+
28
+ child_admin_proxy_path = hconfig(ConfigEnum.proxy_path_admin, child.id)
29
+ base_url = f'https://{child_domain}/{child_admin_proxy_path}'
30
+ path = '/api/v2/child/sync-parent/'
31
+ res = NodeApiClient(base_url).post(path, payload=None, output=dict)
32
+ if isinstance(res, NodeApiErrorSchema):
33
+ logger.error(f"Error while requesting child {child.name} to sync: {res.msg}")
34
+ return False
35
+ if res['msg'] == 'ok':
36
+ logger.success(f"Successfully requested child {child.name} to sync")
37
+ cache.invalidate_all_cached_functions()
38
+ return True
39
+
40
+ logger.error(f"Request to child {child.name} to sync failed")
41
+ return False
42
+
43
+ # before using this function should check child version
44
+
45
+
46
+ # TODO: not used
47
+ def request_chlid_to_register(name: str, child_link: str, apikey: str) -> bool:
48
+ '''Requests to a child to register itself with the current panel'''
49
+ if not child_link or not apikey:
50
+ logger.error("Child link or apikey is empty")
51
+ return False
52
+ domain = get_panel_link()
53
+ if not domain:
54
+ logger.error("Domain is empty")
55
+ return False
56
+ from hiddifypanel.panel import hiddify
57
+
58
+ payload = RegisterWithParentInputSchema()
59
+ payload.parent_panel = hiddify.get_account_panel_link(AdminUser.by_uuid(g.account.uuid), domain.domain) # type: ignore
60
+ payload.apikey = payload.name = hconfig(ConfigEnum.unique_id)
61
+
62
+ logger.debug(f"Requesting child {name} to register")
63
+ res = NodeApiClient(child_link, apikey).post('/api/v2/child/register-parent/', payload, dict)
64
+ if isinstance(res, NodeApiErrorSchema):
65
+ logger.error(f"Error while requesting child {name} to register: {res.msg}")
66
+ return False
67
+
68
+ if res['msg'] == 'ok':
69
+ logger.success(f"Successfully requested child {name} to register")
70
+ cache.invalidate_all_cached_functions()
71
+ return True
72
+
73
+ logger.error(f"Request to child {name} to register failed")
74
+ return False
75
+
76
+
77
+ def is_child_domain_active(child: Child, domain: Domain) -> bool:
78
+ '''Checks whether a child's domain is responsive'''
79
+ if not domain.need_valid_ssl:
80
+ return False
81
+ child_admin_proxy_path = hconfig(ConfigEnum.proxy_path_admin, child.id)
82
+ if not child_admin_proxy_path:
83
+ return False
84
+
85
+ return hutils.node.is_panel_active(domain.domain, child_admin_proxy_path)
86
+
87
+
88
+ def get_child_active_domains(child: Child) -> List[Domain]:
89
+ actives = []
90
+ for d in child.domains:
91
+ if is_child_domain_active(child, d):
92
+ actives.append(d)
93
+ return actives
94
+
95
+
96
+ def is_child_active(child: Child) -> bool:
97
+ for d in child.domains:
98
+ if is_child_domain_active(child, d):
99
+ return True
100
+ return False
@@ -0,0 +1,65 @@
1
+ from loguru import logger
2
+
3
+ from hiddifypanel.models import hconfig, ConfigEnum, PanelMode, User
4
+ from hiddifypanel.cache import cache
5
+ from hiddifypanel.panel.commercial.restapi.v2.parent.schema import UsageInputOutputSchema, UsageData
6
+ from hiddifypanel.panel.commercial.restapi.v2.panel.schema import PanelInfoOutputSchema
7
+ from .api_client import NodeApiClient, NodeApiErrorSchema
8
+
9
+
10
+ def is_child() -> bool:
11
+ return hconfig(ConfigEnum.panel_mode) == PanelMode.child
12
+
13
+
14
+ def is_parent() -> bool:
15
+ return hconfig(ConfigEnum.panel_mode) == PanelMode.parent
16
+
17
+ # region usage
18
+
19
+
20
+ def get_users_usage_data_for_api() -> UsageInputOutputSchema:
21
+ res = UsageInputOutputSchema()
22
+ res.usages = [] # type: ignore
23
+ for u in User.query.all():
24
+ usage_data = UsageData()
25
+ usage_data.uuid = u.uuid
26
+ usage_data.usage = u.current_usage
27
+ usage_data.devices = u.devices
28
+ res.usages.append(usage_data) # type: ignore
29
+ return res
30
+
31
+
32
+ def convert_usage_api_response_to_dict(data: dict) -> dict:
33
+ converted = {}
34
+ for i in data['usages']: # type: ignore
35
+ converted[str(i['uuid'])] = {
36
+ 'usage': i['usage'],
37
+ 'devices': ','.join(i['devices']) # type: ignore
38
+ }
39
+ return converted
40
+
41
+ # endregion
42
+
43
+
44
+ #@cache.cache(ttl=150)
45
+ def is_panel_active(domain: str, proxy_path: str,apikey:str|None = None) -> bool:
46
+ base_url = f'https://{domain}/{proxy_path}'
47
+ res = NodeApiClient(base_url,apikey).get('/api/v2/panel/ping/', dict)
48
+ if isinstance(res, NodeApiErrorSchema):
49
+ logger.error(f"Error while checking if panel is active: {res.msg}")
50
+ return False
51
+ if 'PONG' in res['msg']:
52
+ logger.debug(f"Panel is active: {res['msg']}")
53
+ return True
54
+ logger.debug("Panel is not active")
55
+ return False
56
+
57
+
58
+ #@cache.cache(300)
59
+ def get_panel_info(domain: str, proxy_path: str,apikey:str|None = None) -> dict | None:
60
+ base_url = f'https://{domain}/{proxy_path}'
61
+ res = NodeApiClient(base_url,apikey).get('/api/v2/panel/info/', PanelInfoOutputSchema)
62
+ if isinstance(res, NodeApiErrorSchema):
63
+ logger.error(f"Error while getting panel info from {domain}: {res.msg}")
64
+ return None
65
+ return res