hiddifypanel 10.20.4__py3-none-any.whl → 10.30.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 (83) hide show
  1. hiddifypanel/VERSION +1 -1
  2. hiddifypanel/VERSION.py +2 -2
  3. hiddifypanel/base.py +17 -8
  4. hiddifypanel/cache.py +2 -51
  5. hiddifypanel/drivers/wireguard_api.py +24 -5
  6. hiddifypanel/hutils/convert.py +1 -1
  7. hiddifypanel/hutils/flask.py +28 -2
  8. hiddifypanel/hutils/importer/xui.py +6 -7
  9. hiddifypanel/hutils/network/__init__.py +1 -0
  10. hiddifypanel/hutils/network/cf_api.py +84 -0
  11. hiddifypanel/hutils/network/net.py +26 -49
  12. hiddifypanel/hutils/node/child.py +25 -7
  13. hiddifypanel/hutils/node/parent.py +7 -7
  14. hiddifypanel/hutils/node/shared.py +19 -6
  15. hiddifypanel/hutils/proxy/clash.py +1 -1
  16. hiddifypanel/hutils/proxy/shared.py +1 -1
  17. hiddifypanel/hutils/proxy/singbox.py +2 -3
  18. hiddifypanel/hutils/proxy/xray.py +12 -10
  19. hiddifypanel/hutils/proxy/xrayjson.py +26 -49
  20. hiddifypanel/hutils/utils.py +47 -3
  21. hiddifypanel/models/__init__.py +1 -1
  22. hiddifypanel/models/admin.py +9 -2
  23. hiddifypanel/models/base_account.py +3 -1
  24. hiddifypanel/models/config.py +5 -7
  25. hiddifypanel/models/config_enum.py +18 -6
  26. hiddifypanel/models/domain.py +82 -118
  27. hiddifypanel/models/user.py +44 -24
  28. hiddifypanel/panel/admin/Actions.py +6 -11
  29. hiddifypanel/panel/admin/AdminstratorAdmin.py +3 -9
  30. hiddifypanel/panel/admin/Backup.py +5 -8
  31. hiddifypanel/panel/admin/Dashboard.py +3 -4
  32. hiddifypanel/panel/admin/DomainAdmin.py +20 -15
  33. hiddifypanel/panel/admin/ProxyAdmin.py +3 -10
  34. hiddifypanel/panel/admin/QuickSetup.py +1 -1
  35. hiddifypanel/panel/admin/SettingAdmin.py +7 -5
  36. hiddifypanel/panel/admin/Terminal.py +0 -1
  37. hiddifypanel/panel/admin/UserAdmin.py +4 -3
  38. hiddifypanel/panel/cli.py +36 -23
  39. hiddifypanel/panel/commercial/ProxyDetailsAdmin.py +2 -4
  40. hiddifypanel/panel/commercial/restapi/v1/tgbot.py +7 -4
  41. hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py +17 -13
  42. hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +4 -3
  43. hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +28 -10
  44. hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +2 -19
  45. hiddifypanel/panel/commercial/restapi/v2/admin/schema.py +27 -4
  46. hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +28 -9
  47. hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +1 -21
  48. hiddifypanel/panel/commercial/restapi/v2/parent/register_api.py +1 -1
  49. hiddifypanel/panel/commercial/restapi/v2/parent/schema.py +8 -4
  50. hiddifypanel/panel/commercial/restapi/v2/parent/sync_api.py +19 -3
  51. hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +48 -42
  52. hiddifypanel/panel/commercial/telegrambot/Usage.py +1 -1
  53. hiddifypanel/panel/commercial/telegrambot/admin.py +1 -1
  54. hiddifypanel/panel/commercial/telegrambot/information.py +1 -1
  55. hiddifypanel/panel/common.py +5 -11
  56. hiddifypanel/panel/hiddify.py +9 -20
  57. hiddifypanel/panel/init_db.py +31 -13
  58. hiddifypanel/panel/usage.py +38 -9
  59. hiddifypanel/panel/user/user.py +52 -32
  60. hiddifypanel/templates/admin-layout.html +2 -2
  61. hiddifypanel/templates/fake.html +0 -298
  62. hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
  63. hiddifypanel/translations/en/LC_MESSAGES/messages.po +80 -25
  64. hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
  65. hiddifypanel/translations/fa/LC_MESSAGES/messages.po +74 -20
  66. hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
  67. hiddifypanel/translations/pt/LC_MESSAGES/messages.po +60 -6
  68. hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
  69. hiddifypanel/translations/ru/LC_MESSAGES/messages.po +158 -78
  70. hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
  71. hiddifypanel/translations/zh/LC_MESSAGES/messages.po +60 -6
  72. hiddifypanel/translations.i18n/en.json +62 -22
  73. hiddifypanel/translations.i18n/fa.json +57 -17
  74. hiddifypanel/translations.i18n/pt.json +43 -3
  75. hiddifypanel/translations.i18n/ru.json +112 -72
  76. hiddifypanel/translations.i18n/zh.json +43 -3
  77. {hiddifypanel-10.20.4.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/METADATA +2 -1
  78. {hiddifypanel-10.20.4.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/RECORD +82 -82
  79. {hiddifypanel-10.20.4.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/WHEEL +1 -1
  80. hiddifypanel/panel/cf_api.py +0 -37
  81. {hiddifypanel-10.20.4.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/LICENSE.md +0 -0
  82. {hiddifypanel-10.20.4.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/entry_points.txt +0 -0
  83. {hiddifypanel-10.20.4.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/top_level.txt +0 -0
hiddifypanel/VERSION CHANGED
@@ -1 +1 @@
1
- 10.20.4
1
+ 10.30.0.dev0
hiddifypanel/VERSION.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__='10.20.4'
1
+ __version__='10.30.0.dev0'
2
2
  from datetime import datetime
3
- __release_date__= datetime.strptime('2024-04-13','%Y-%m-%d')
3
+ __release_date__= datetime.strptime('2024-06-24','%Y-%m-%d')
hiddifypanel/base.py CHANGED
@@ -18,6 +18,22 @@ from loguru import logger
18
18
  from hiddifypanel.panel.init_db import init_db
19
19
 
20
20
 
21
+ def init_logger():
22
+ def dynamic_formatter(record) -> str:
23
+ fmt = '<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>'
24
+ if record['extra']:
25
+ fmt += ' | <level>{extra}</level>'
26
+ return fmt + '\n'
27
+
28
+ # configure logger
29
+ from hiddifypanel.models import ConfigEnum, hconfig
30
+ logger.remove()
31
+ logger.add(sys.stderr, format=dynamic_formatter, level=hconfig(ConfigEnum.log_level), colorize=True, catch=True, enqueue=True, diagnose=False, backtrace=True)
32
+ # logger.trace('Logger initiated :)')
33
+
34
+
35
+ # TODO: refactor this function
36
+
21
37
  def create_app(*args, cli=False, **config):
22
38
 
23
39
  app = APIFlask(__name__, static_url_path="/<proxy_path>/static/", instance_relative_config=True, version='2.0.0', title="Hiddify API",
@@ -75,14 +91,7 @@ def create_app(*args, cli=False, **config):
75
91
  with app.app_context():
76
92
  init_db()
77
93
 
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)
85
- # @babel.localeselector
94
+ init_logger()
86
95
 
87
96
  def get_locale():
88
97
  # Put your logic here. Application can store locale in
hiddifypanel/cache.py CHANGED
@@ -1,5 +1,4 @@
1
- from redis_cache import RedisCache, CacheDecorator, compact_dump, chunks
2
- from redis_cache import loads as redis_cache_loads
1
+ from redis_cache import RedisCache, chunks
3
2
  import redis
4
3
  from pickle import dumps, loads
5
4
  from loguru import logger
@@ -23,52 +22,4 @@ class CustomRedisCache(RedisCache):
23
22
  return False
24
23
 
25
24
 
26
- cache = CustomRedisCache(redis_client=redis_client, prefix="h", serializer=dumps, deserializer=loads)
27
-
28
-
29
- # cache = RedisCache(redis_client=redis_client, exception_handler=exception_handler)
30
- # cache = RedisCache(redis_client=redis_client, prefix="h", serializer=dumps, deserializer=loads, exception_handler=exception_handler)
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
36
-
37
- # class CacheDecorator:
38
- # def __init__(self, *args, **kwargs):
39
- # pass
40
-
41
- # def __call__(self, fn):
42
- # @wraps(fn)
43
- # def inner(*args, **kwargs):
44
-
45
- # parsed_result = fn(*args, **kwargs)
46
-
47
- # return parsed_result
48
-
49
- # inner.invalidate = self.invalidate
50
- # inner.invalidate_all = self.invalidate_all
51
- # inner.instance = self
52
- # return inner
53
-
54
- # def invalidate(self, *args, **kwargs):
55
- # pass
56
-
57
- # def invalidate_all(self, *args, **kwargs):
58
- # pass
59
-
60
-
61
- # class DisableCache:
62
- # cache = CacheDecorator
63
-
64
-
65
- # cache = DisableCache()
66
- # try:
67
- # @cache.cache()
68
- # def test():
69
- # return 1
70
- # test()
71
- # except Exception as e:
72
- # import sys
73
- # print('Caching Error! Disabling cache', e, file=sys.stderr)
74
- # # cache = DisableCache()
25
+ cache = CustomRedisCache(redis_client=redis_client, prefix="h", serializer=dumps, deserializer=loads)
@@ -9,15 +9,35 @@ from hiddifypanel.panel.run_commander import Command, commander
9
9
  class WireguardApi(DriverABS):
10
10
  def is_enabled(self) -> bool:
11
11
  return hconfig(ConfigEnum.wireguard_enable)
12
- WG_LOCAL_USAGE_FILE_PATH = './hiddify_usages.json'
12
+
13
+ WG_LOCAL_USAGE_FILE_PATH = os.path.join('/opt/hiddify-manager/','hiddify-panel','wireguard_usages.json')
14
+ OLD_WG_LOCAL_USAGE_FILE_PATH = os.path.join('/opt/hiddify-manager/','hiddify-panel','hiddify_usages.json')
13
15
 
14
16
  def __init__(self) -> None:
15
17
  super().__init__()
18
+
19
+ if os.path.isfile(WireguardApi.OLD_WG_LOCAL_USAGE_FILE_PATH) and not os.path.isfile(WireguardApi.WG_LOCAL_USAGE_FILE_PATH):
20
+ os.rename(WireguardApi.OLD_WG_LOCAL_USAGE_FILE_PATH,WireguardApi.WG_LOCAL_USAGE_FILE_PATH)
21
+
22
+ if not self.is_usages_file_exists_and_json():
23
+ self.init_empty_usages_file()
16
24
  # create empty local usage file
17
- if not os.path.isfile(WireguardApi.WG_LOCAL_USAGE_FILE_PATH):
18
- with open(WireguardApi.WG_LOCAL_USAGE_FILE_PATH, 'w+') as f:
19
- json.dump({}, f)
20
25
 
26
+ def is_usages_file_exists_and_json(self) -> bool:
27
+ if os.path.isfile(WireguardApi.WG_LOCAL_USAGE_FILE_PATH):
28
+ try:
29
+ # try to load it as a JSON
30
+ self.__get_local_usage()
31
+ return True
32
+ except json.decoder.JSONDecodeError:
33
+ os.remove(WireguardApi.WG_LOCAL_USAGE_FILE_PATH)
34
+ return False
35
+ return False
36
+ def init_empty_usages_file(self):
37
+ with open(WireguardApi.WG_LOCAL_USAGE_FILE_PATH, 'w+') as f:
38
+ json.dump({}, f)
39
+
40
+
21
41
  def __get_wg_usages(self) -> dict:
22
42
  raw_output = commander(Command.update_wg_usage, run_in_background=False)
23
43
  data = {}
@@ -34,7 +54,6 @@ class WireguardApi(DriverABS):
34
54
  return data
35
55
 
36
56
  def __get_local_usage(self) -> dict:
37
-
38
57
  with open(WireguardApi.WG_LOCAL_USAGE_FILE_PATH, 'r') as f:
39
58
  data = json.load(f)
40
59
  return data
@@ -57,7 +57,7 @@ def json_to_time(time_str: str) -> datetime | str:
57
57
  try:
58
58
  return datetime.strptime(__fix_time_format(time_str), "%Y-%m-%d %H:%M:%S")
59
59
  except BaseException:
60
- return time_str
60
+ return json_to_date(time_str)
61
61
 
62
62
 
63
63
  def format_timedelta(delta: timedelta, add_direction: bool = True, granularity: str = "days") -> str:
@@ -5,7 +5,7 @@ from apiflask import abort as apiflask_abort
5
5
  from flask_babel import gettext as _
6
6
  from flask import url_for # type: ignore
7
7
  from urllib.parse import urlparse
8
- from markupsafe import Markup
8
+ from strenum import StrEnum
9
9
 
10
10
  import user_agents
11
11
  import re
@@ -17,7 +17,7 @@ from hiddifypanel import hutils
17
17
 
18
18
 
19
19
  def flash(message: str, category: str = "message"):
20
- if not isinstance(message,str):
20
+ if not isinstance(message, str):
21
21
  message = str(message)
22
22
  return flask_flash(message, category)
23
23
 
@@ -82,6 +82,8 @@ def __parse_user_agent(ua: str) -> dict:
82
82
  res['is_shadowrocket'] = re.match('^(Shadowrocket)', ua, re.IGNORECASE) and True
83
83
  res['is_v2rayng'] = re.match('^(v2rayNG)', ua, re.IGNORECASE) and True
84
84
 
85
+ if res['is_v2rayng']:
86
+ res['v2rayng_version'] = generic_version
85
87
  if res['is_singbox']:
86
88
  res['singbox_version'] = generic_version
87
89
 
@@ -254,6 +256,30 @@ def extract_parent_info_from_url(url) -> Tuple[str | None, str | None, str | Non
254
256
  return domain, proxy_path, admin_uuid
255
257
  else:
256
258
  return None, None, None
259
+
260
+
261
+ class ClientVersion(StrEnum):
262
+ v2ryang = 'v2rayng_version'
263
+ hiddify_next = 'hiddify_version'
264
+
265
+
266
+ def is_client_version(client: ClientVersion, major_v: int = 0, minor_v: int = 0, patch_v: int = 0) -> bool:
267
+ '''If the user agent version be equals or higher than parameters returns True'''
268
+ if raw_v := g.user_agent.get(client):
269
+ # TODO: probably we don't need these checks and the compare_versions can handle it (need to be test)
270
+ raw_v_len = len(raw_v)
271
+ u_major_v = raw_v[0] if raw_v_len > 0 else 0
272
+ u_minor_v = raw_v[1] if raw_v_len > 1 else 0
273
+ u_patch_v = raw_v[2] if raw_v_len > 2 else 0
274
+
275
+ user_agent_v = f'{u_major_v}.{u_minor_v}.{u_patch_v}'
276
+ needed_version = f'{major_v}.{minor_v}.{patch_v}'
277
+
278
+ res = hutils.utils.compare_versions(user_agent_v, needed_version)
279
+ if res == 0 or res == 1:
280
+ return True
281
+ return False
282
+
257
283
  # region not used
258
284
 
259
285
 
@@ -1,14 +1,14 @@
1
1
  import sqlite3
2
2
  import json
3
- from typing import Any, Dict, List, Tuple
3
+ import os
4
4
  import uuid as uuid_mod
5
+ from typing import Any, Dict, List, Tuple
5
6
  from datetime import datetime
6
7
  from dateutil.relativedelta import relativedelta
8
+
7
9
  from hiddifypanel import hutils
8
10
  from hiddifypanel.models import *
9
11
  from hiddifypanel.database import db, db_execute
10
- import os
11
- from sqlalchemy import text
12
12
 
13
13
 
14
14
  def __query_fetch_json(db, query: str, **kwargs) -> List[Dict[str, Any]]:
@@ -65,9 +65,9 @@ def __create_hiddify_user_from_xui_values(id: str, values: Dict[str, Any]) -> "U
65
65
  user.uuid = id if hutils.auth.is_uuid_valid(id, 4) else uuid_mod.uuid4()
66
66
 
67
67
  if str(values['expiry_time']) == '0':
68
- user.expiry_time = datetime.now() + relativedelta(years=10)
68
+ user.package_days = 3650
69
69
  else:
70
- user.expiry_time = datetime.fromtimestamp(values['expiry_time'] / 1000)
70
+ user.package_days = max(0, (datetime.fromtimestamp(values['expiry_time'] / 1000) - datetime.today()).days)
71
71
 
72
72
  user.usage_limit = values['max_usage_bytes']
73
73
  user.current_usage = values['current_usage_bytes']
@@ -145,8 +145,7 @@ def import_data(db_path: str):
145
145
  User.add_or_update(commit=False, **u)
146
146
 
147
147
  for d in hiddify_domains_dict:
148
- from hiddifypanel.panel import hiddify
149
- hiddify.add_or_update_domain(commit=False, **d)
148
+ Domain.add_or_update(commit=False, **d)
150
149
 
151
150
  db.session.commit() # type: ignore
152
151
  except Exception as err:
@@ -2,3 +2,4 @@
2
2
  from . import auto_ip_selector
3
3
  # from .ip import get_domain_ip, get_socket_public_ip, get_interface_public_ip, get_ips, get_ip
4
4
  from .net import *
5
+ from . import cf_api
@@ -0,0 +1,84 @@
1
+ import CloudFlare
2
+ from hiddifypanel.models import hconfig, ConfigEnum
3
+
4
+ __cf: CloudFlare.CloudFlare = '' # type: ignore
5
+
6
+
7
+ def __prepare_cf_api_client() -> bool:
8
+ '''Prepares cloudflare client if it's not already'''
9
+ global __cf
10
+ if __cf and isinstance(__cf, CloudFlare.CloudFlare):
11
+ return True
12
+
13
+ if hconfig(ConfigEnum.cloudflare):
14
+ __cf = CloudFlare.CloudFlare(token=hconfig(ConfigEnum.cloudflare))
15
+ if __cf and isinstance(__cf, CloudFlare.CloudFlare):
16
+ return True
17
+ return False
18
+
19
+
20
+ def add_or_update_dns_record(domain: str, ip: str, dns_type: str = "A", proxied: bool = True) -> bool:
21
+ '''This function cloud throw an exception'''
22
+ if not __prepare_cf_api_client():
23
+ return False
24
+
25
+ zone_name = __extract_root_domain(domain)
26
+ zone = __get_zone(zone_name)
27
+ if zone:
28
+ record = __get_dns_record(zone, domain)
29
+ dns_name = domain[:-len(zone['name'])].replace('.', '')
30
+ # if the input domain is root itself
31
+ dns_name = '@' if not dns_name else dns_name
32
+ data = {
33
+ 'name': dns_name,
34
+ 'type': dns_type, 'content': ip, 'proxied': proxied
35
+ }
36
+ if not record:
37
+ api_res = __cf.zones.dns_records.post(zone['id'], data=data)
38
+ else:
39
+ api_res = __cf.zones.dns_records.put(zone['id'], record['id'], data=data)
40
+
41
+ # validate api response
42
+ if api_res['name'] == domain and api_res['type'] == dns_type and api_res['content'] == ip:
43
+ return True
44
+ return False
45
+
46
+
47
+ def delete_dns_record(domain: str) -> bool:
48
+ '''Deletes a DNS record from cloudflare panel of user'''
49
+ if not __prepare_cf_api_client():
50
+ return False
51
+
52
+ zone_name = __extract_root_domain(domain)
53
+ zone = __get_zone(zone_name)
54
+ record = __get_dns_record(zone, domain)
55
+ if zone and record:
56
+ api_res = __cf.zones.dns_records.delete(zone['id'], record['id'])
57
+ if api_res['id'] == record['id']:
58
+ return True
59
+ return False
60
+
61
+
62
+ def __get_zone(zone_name: str) -> dict | None:
63
+ zones = __cf.zones.get()
64
+ for z in zones:
65
+ if z['name'] == zone_name:
66
+ return z
67
+ return None
68
+
69
+
70
+ def __get_dns_record(zone, domain: str) -> dict | None:
71
+ '''Returns dns record if exists'''
72
+ dns_records = __cf.zones.dns_records(zone['id'])
73
+ for r in dns_records:
74
+ if r['name'] == domain:
75
+ return r
76
+ return None
77
+
78
+
79
+ def __extract_root_domain(domain: str) -> str:
80
+ domain_parts = domain.split(".")
81
+ if len(domain_parts) > 1:
82
+ return ".".join(domain_parts[-2:])
83
+ else:
84
+ return domain
@@ -1,4 +1,4 @@
1
- from typing import List, Literal, Tuple, Union
1
+ from typing import List, Literal, Union
2
2
  from urllib.parse import urlparse
3
3
  import urllib.request
4
4
  import ipaddress
@@ -136,46 +136,6 @@ def get_ip(version: Literal[4, 6], retry: int = 5) -> ipaddress.IPv4Address | ip
136
136
  ip = get_ip(version, retry=retry - 1)
137
137
  return ip
138
138
 
139
-
140
- def check_connection_to_remote(api_url: str) -> bool:
141
-
142
- path = f"{api_url}/api/v1/hello/"
143
-
144
- try:
145
- _ = requests.get(path, verify=False, timeout=2).json()
146
- return True
147
-
148
- except BaseException:
149
- return False
150
-
151
-
152
- def check_connection_for_domain(domain: str) -> bool:
153
-
154
- proxy_path = hconfig(ConfigEnum.proxy_path_admin)
155
- admin_secret = hconfig(ConfigEnum.admin_secret)
156
- path = f"{proxy_path}/{admin_secret}/api/v1/hello/"
157
- try:
158
- print(f"https://{domain}/{path}")
159
- res = requests.get(
160
- f"https://{domain}/{path}", verify=False, timeout=10).json()
161
- return res['status'] == 200
162
-
163
- except BaseException:
164
- try:
165
- print(f"http://{domain}/{path}")
166
- res = requests.get(
167
- f"http://{domain}/{path}", verify=False, timeout=10).json()
168
- return res['status'] == 200
169
- except BaseException:
170
- try:
171
- print(f"http://{get_domain_ip(domain)}/{path}")
172
- res = requests.get(
173
- f"http://{get_domain_ip(domain)}/{path}", verify=False, timeout=10).json()
174
- return res['status'] == 200
175
- except BaseException:
176
- return False
177
-
178
-
179
139
  def get_random_domains(count: int = 1, retry: int = 3) -> List[str]:
180
140
  try:
181
141
  irurl = "https://api.ooni.io/api/v1/measurements?probe_cc=IR&test_name=web_connectivity&anomaly=false&confirmed=false&failure=false&order_by=test_start_time&limit=1000"
@@ -205,7 +165,7 @@ def is_domain_support_tls_13(domain: str) -> bool:
205
165
  return ssock.version() == "TLSv1.3"
206
166
 
207
167
 
208
- def is_domain_support_h2(sni: str, server: str = '') -> bool:
168
+ def is_domain_support_h2_tls13(sni: str, server: str = '') -> bool:
209
169
  try:
210
170
 
211
171
  context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
@@ -229,11 +189,11 @@ def is_domain_support_h2(sni: str, server: str = '') -> bool:
229
189
 
230
190
 
231
191
  def is_domain_reality_friendly(domain: str) -> bool:
232
- return is_domain_support_h2(domain)
192
+ return is_domain_support_h2_tls13(domain)
233
193
 
234
194
 
235
195
  def fallback_domain_compatible_with_servernames(fallback_domain: str, servername: str) -> bool:
236
- return is_domain_support_h2(servername, fallback_domain)
196
+ return is_domain_support_h2_tls13(servername, fallback_domain)
237
197
 
238
198
 
239
199
  def get_random_decoy_domain() -> str:
@@ -329,10 +289,9 @@ def add_number_to_ipv6(ip: str, number: int) -> str:
329
289
  return modified_ipv6
330
290
 
331
291
 
292
+ @cache.cache(600)
332
293
  def is_in_same_asn(domain_or_ip: str, domain_or_ip_target: str) -> bool:
333
294
  '''Returns True if domain is in panel ASN'''
334
- if not IPASN:
335
- return False
336
295
  try:
337
296
  ip = domain_or_ip if is_ip(domain_or_ip) else get_domain_ip(domain_or_ip)
338
297
  ip_target = domain_or_ip_target if is_ip(domain_or_ip_target) else get_domain_ip(domain_or_ip_target)
@@ -340,8 +299,8 @@ def is_in_same_asn(domain_or_ip: str, domain_or_ip_target: str) -> bool:
340
299
  if not ip or not ip_target:
341
300
  return False
342
301
 
343
- ip_asn = get_ip_asn_name(ip)
344
- ip_target_asn = get_ip_asn_name(ip_target)
302
+ ip_asn = get_ip_asn(ip)
303
+ ip_target_asn = get_ip_asn(ip_target)
345
304
 
346
305
  if not ip_asn or not ip_target_asn:
347
306
  return False
@@ -355,7 +314,10 @@ def is_in_same_asn(domain_or_ip: str, domain_or_ip_target: str) -> bool:
355
314
  # f"<br> Server ASN={asn_ipv4.get('autonomous_system_organization','unknown')}<br>{domain}_ASN={asn_dip.get('autonomous_system_organization','unknown')}", "warning")
356
315
 
357
316
 
358
- def get_ip_asn_name(ip: ipaddress.IPv4Address | ipaddress.IPv6Address | str) -> str:
317
+ @cache.cache(600)
318
+ def get_ip_asn(ip: ipaddress.IPv4Address | ipaddress.IPv6Address | str) -> str:
319
+ if not IPASN:
320
+ return __get_ip_asn_api(ip)
359
321
  try:
360
322
  if asn := IPASN.get(str(ip)):
361
323
  return str(asn.get('autonomous_system_organization', ''))
@@ -364,9 +326,24 @@ def get_ip_asn_name(ip: ipaddress.IPv4Address | ipaddress.IPv6Address | str) ->
364
326
  return ''
365
327
 
366
328
 
329
+ def __get_ip_asn_api(ip: ipaddress.IPv4Address | ipaddress.IPv6Address | str) -> str:
330
+ ip = str(ip)
331
+ if not is_ip(ip):
332
+ return ''
333
+ endpoint = f'https://ipapi.co/{ip}/asn/'
334
+ return str(requests.get(endpoint).content)
335
+
336
+
337
+ @cache.cache(3600)
367
338
  def is_ip(input: str):
368
339
  try:
369
340
  _ = ipaddress.ip_address(input)
370
341
  return True
371
342
  except:
372
343
  return False
344
+
345
+ def resolve_domain_with_api(domain:str) -> str:
346
+ if not domain:
347
+ return ''
348
+ endpoint = f'http://ip-api.com/json/{domain}?fields=query'
349
+ return str(requests.get(endpoint).json().get('query'))
@@ -1,7 +1,9 @@
1
1
  from loguru import logger
2
2
  import socket
3
+ from flask_babel import gettext as _
4
+ from strenum import StrEnum
3
5
 
4
- from hiddifypanel.models import AdminUser, User, hconfig, ConfigEnum, ChildMode, set_hconfig, Domain, Proxy, StrConfig, BoolConfig, Child, ChildMode
6
+ from hiddifypanel.models import AdminUser, User, hconfig, ConfigEnum, ChildMode, Domain, Proxy, StrConfig, BoolConfig, Child, ChildMode
5
7
  from hiddifypanel import hutils
6
8
  from hiddifypanel.panel import hiddify
7
9
  from hiddifypanel.panel import usage
@@ -34,11 +36,27 @@ def __get_register_data_for_api(name: str, mode: ChildMode) -> RegisterInputSche
34
36
  return register_data
35
37
 
36
38
 
37
- def __get_sync_data_for_api() -> SyncInputSchema:
39
+ class SyncFields(StrEnum):
40
+ domains = 'domains'
41
+ proxies = 'proxies'
42
+ hconfigs = 'hconfigs'
43
+
44
+
45
+ def __get_sync_data_for_api(*fields: SyncFields) -> SyncInputSchema:
38
46
  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
47
+ if len(fields) == 0:
48
+ sync_data.domains = [domain.to_schema() for domain in Domain.query.all()] # type: ignore
49
+ sync_data.proxies = [proxy.to_schema() for proxy in Proxy.query.all()] # type: ignore
50
+ sync_data.hconfigs = [*[u.to_schema() for u in StrConfig.query.all()], *[u.to_schema() for u in BoolConfig.query.all()]] # type: ignore
51
+ else:
52
+ for f in fields:
53
+ match f:
54
+ case SyncFields.domains:
55
+ sync_data.domains = [domain.to_schema() for domain in Domain.query.all()] # type: ignore
56
+ case SyncFields.proxies:
57
+ sync_data.proxies = [proxy.to_schema() for proxy in Proxy.query.all()] # type: ignore
58
+ case SyncFields.hconfigs:
59
+ sync_data.hconfigs = [*[u.to_schema() for u in StrConfig.query.all()], *[u.to_schema() for u in BoolConfig.query.all()]] # type: ignore
42
60
 
43
61
  return sync_data
44
62
 
@@ -103,7 +121,7 @@ def register_to_parent(name: str, apikey: str, mode: ChildMode = ChildMode.remot
103
121
  return True
104
122
 
105
123
 
106
- def sync_with_parent() -> bool:
124
+ def sync_with_parent(*fields: SyncFields) -> bool:
107
125
  # sync usage first
108
126
  if not sync_users_usage_with_parent():
109
127
  logger.error("Error while syncing with parent: Failed to sync users usage")
@@ -113,7 +131,7 @@ def sync_with_parent() -> bool:
113
131
  if not p_url:
114
132
  logger.error("Error while syncing with parent: Parent url is empty")
115
133
  return False
116
- payload = __get_sync_data_for_api()
134
+ payload = __get_sync_data_for_api(*fields)
117
135
  res = NodeApiClient(p_url).put('/api/v2/parent/sync/', payload, SyncOutputSchema)
118
136
  if isinstance(res, NodeApiErrorSchema):
119
137
  logger.error(f"Error while syncing with parent: {res.msg}")
@@ -3,7 +3,7 @@ from flask_babel import lazy_gettext as _
3
3
  from typing import List
4
4
  from loguru import logger
5
5
 
6
- from hiddifypanel.models import Child, AdminUser, ConfigEnum, Domain, ChildMode, hconfig, get_panel_link
6
+ from hiddifypanel.models import Child, AdminUser, ConfigEnum, Domain, hconfig, Domain
7
7
  from hiddifypanel import hutils
8
8
  from hiddifypanel.panel.commercial.restapi.v2.child.schema import RegisterWithParentInputSchema
9
9
  from .api_client import NodeApiClient, NodeApiErrorSchema
@@ -15,12 +15,12 @@ def request_childs_to_sync():
15
15
  for c in Child.query.filter(Child.id != 0).all():
16
16
  if not request_child_to_sync(c):
17
17
  logger.error(f'{c.name}: {_("parent.sync-req-failed")}')
18
- hutils.flask.flash(f'{c.name}: ' + _('parent.sync-req-failed'), 'danger')
18
+ hutils.flask.flash(f'{c.name}: ' + _('parent.sync-req-failed'), 'danger') # just for debug
19
19
 
20
20
 
21
21
  def request_child_to_sync(child: Child) -> bool:
22
22
  '''Requests to a child to sync itself with the current panel'''
23
- child_domain = get_panel_link(child.id)
23
+ child_domain = Domain.get_panel_link(child.id) # type:ignore
24
24
  if not child_domain:
25
25
  logger.error(f"Child {child.name} has no valid domain")
26
26
  return False
@@ -49,14 +49,14 @@ def request_chlid_to_register(name: str, child_link: str, apikey: str) -> bool:
49
49
  if not child_link or not apikey:
50
50
  logger.error("Child link or apikey is empty")
51
51
  return False
52
- domain = get_panel_link()
52
+ domain = Domain.get_panel_link()
53
53
  if not domain:
54
54
  logger.error("Domain is empty")
55
55
  return False
56
56
  from hiddifypanel.panel import hiddify
57
57
 
58
58
  payload = RegisterWithParentInputSchema()
59
- payload.parent_panel = hiddify.get_account_panel_link(AdminUser.by_uuid(g.account.uuid), domain.domain) # type: ignore
59
+ payload.parent_panel = hiddify.get_account_panel_link(AdminUser.by_uuid(g.account.uuid), domain) # type: ignore
60
60
  payload.apikey = payload.name = hconfig(ConfigEnum.unique_id)
61
61
 
62
62
  logger.debug(f"Requesting child {name} to register")
@@ -87,14 +87,14 @@ def is_child_domain_active(child: Child, domain: Domain) -> bool:
87
87
 
88
88
  def get_child_active_domains(child: Child) -> List[Domain]:
89
89
  actives = []
90
- for d in child.domains:
90
+ for d in child.domains: # type: ignore
91
91
  if is_child_domain_active(child, d):
92
92
  actives.append(d)
93
93
  return actives
94
94
 
95
95
 
96
96
  def is_child_active(child: Child) -> bool:
97
- for d in child.domains:
97
+ for d in child.domains: # type: ignore
98
98
  if is_child_domain_active(child, d):
99
99
  return True
100
100
  return False
@@ -1,4 +1,7 @@
1
+ import threading
1
2
  from loguru import logger
3
+ from typing import Callable
4
+ from flask import copy_current_request_context
2
5
 
3
6
  from hiddifypanel.models import hconfig, ConfigEnum, PanelMode, User
4
7
  from hiddifypanel.cache import cache
@@ -40,11 +43,13 @@ def convert_usage_api_response_to_dict(data: dict) -> dict:
40
43
 
41
44
  # endregion
42
45
 
46
+ # TODO: use cache for these functions in release
47
+ # @cache.cache(ttl=150)
43
48
 
44
- #@cache.cache(ttl=150)
45
- def is_panel_active(domain: str, proxy_path: str,apikey:str|None = None) -> bool:
49
+
50
+ def is_panel_active(domain: str, proxy_path: str, apikey: str | None = None) -> bool:
46
51
  base_url = f'https://{domain}/{proxy_path}'
47
- res = NodeApiClient(base_url,apikey).get('/api/v2/panel/ping/', dict)
52
+ res = NodeApiClient(base_url, apikey).get('/api/v2/panel/ping/', dict)
48
53
  if isinstance(res, NodeApiErrorSchema):
49
54
  logger.error(f"Error while checking if panel is active: {res.msg}")
50
55
  return False
@@ -55,11 +60,19 @@ def is_panel_active(domain: str, proxy_path: str,apikey:str|None = None) -> bool
55
60
  return False
56
61
 
57
62
 
58
- #@cache.cache(300)
59
- def get_panel_info(domain: str, proxy_path: str,apikey:str|None = None) -> dict | None:
63
+ # @cache.cache(300)
64
+ def get_panel_info(domain: str, proxy_path: str, apikey: str | None = None) -> dict | None:
60
65
  base_url = f'https://{domain}/{proxy_path}'
61
- res = NodeApiClient(base_url,apikey).get('/api/v2/panel/info/', PanelInfoOutputSchema)
66
+ res = NodeApiClient(base_url, apikey).get('/api/v2/panel/info/', PanelInfoOutputSchema)
62
67
  if isinstance(res, NodeApiErrorSchema):
63
68
  logger.error(f"Error while getting panel info from {domain}: {res.msg}")
64
69
  return None
65
70
  return res
71
+
72
+
73
+ def run_node_op_in_bg(op: Callable, *args, **kwargs):
74
+ @copy_current_request_context
75
+ def wrapped_op():
76
+ op(*args, **kwargs)
77
+
78
+ threading.Thread(target=wrapped_op).start()
@@ -1,5 +1,5 @@
1
1
  import yaml
2
- from hiddifypanel.models import Proxy, ProxyCDN, ProxyL3, ProxyProto, ProxyTransport, Domain
2
+ from hiddifypanel.models import ProxyCDN, ProxyL3, ProxyProto, ProxyTransport, Domain
3
3
  from hiddifypanel import hutils
4
4
 
5
5
 
@@ -293,7 +293,7 @@ def make_proxy(hconfigs: dict, proxy: Proxy, domain_db: Domain, phttp=80, ptls=4
293
293
  return base
294
294
 
295
295
  if proxy.proto in [ProxyProto.vmess]:
296
- base['cipher'] = "chacha20-poly1305"
296
+ base['cipher'] = "auto" # "chacha20-poly1305"
297
297
 
298
298
  if l3 in ['reality']:
299
299
  base['reality_short_id'] = random.sample(hconfigs[ConfigEnum.reality_short_ids].split(','), 1)[0]