hiddifypanel 10.20.3__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.
- hiddifypanel/VERSION +1 -1
- hiddifypanel/VERSION.py +2 -2
- hiddifypanel/base.py +17 -8
- hiddifypanel/cache.py +2 -51
- hiddifypanel/drivers/wireguard_api.py +24 -5
- hiddifypanel/hutils/convert.py +1 -1
- hiddifypanel/hutils/flask.py +28 -2
- hiddifypanel/hutils/importer/xui.py +6 -7
- hiddifypanel/hutils/network/__init__.py +1 -0
- hiddifypanel/hutils/network/cf_api.py +84 -0
- hiddifypanel/hutils/network/net.py +26 -49
- hiddifypanel/hutils/node/child.py +25 -7
- hiddifypanel/hutils/node/parent.py +7 -7
- hiddifypanel/hutils/node/shared.py +19 -6
- hiddifypanel/hutils/proxy/clash.py +1 -1
- hiddifypanel/hutils/proxy/shared.py +1 -1
- hiddifypanel/hutils/proxy/singbox.py +2 -3
- hiddifypanel/hutils/proxy/xray.py +12 -10
- hiddifypanel/hutils/proxy/xrayjson.py +26 -49
- hiddifypanel/hutils/utils.py +47 -3
- hiddifypanel/models/__init__.py +1 -1
- hiddifypanel/models/admin.py +9 -2
- hiddifypanel/models/base_account.py +3 -1
- hiddifypanel/models/config.py +5 -7
- hiddifypanel/models/config_enum.py +18 -6
- hiddifypanel/models/domain.py +82 -118
- hiddifypanel/models/user.py +44 -24
- hiddifypanel/panel/admin/Actions.py +6 -11
- hiddifypanel/panel/admin/AdminstratorAdmin.py +3 -9
- hiddifypanel/panel/admin/Backup.py +5 -8
- hiddifypanel/panel/admin/Dashboard.py +3 -4
- hiddifypanel/panel/admin/DomainAdmin.py +20 -15
- hiddifypanel/panel/admin/ProxyAdmin.py +3 -10
- hiddifypanel/panel/admin/QuickSetup.py +1 -1
- hiddifypanel/panel/admin/SettingAdmin.py +7 -5
- hiddifypanel/panel/admin/Terminal.py +0 -1
- hiddifypanel/panel/admin/UserAdmin.py +4 -3
- hiddifypanel/panel/cli.py +36 -23
- hiddifypanel/panel/commercial/ProxyDetailsAdmin.py +2 -4
- hiddifypanel/panel/commercial/restapi/v1/tgbot.py +7 -4
- hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py +17 -13
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +4 -3
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +28 -10
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +2 -19
- hiddifypanel/panel/commercial/restapi/v2/admin/schema.py +27 -4
- hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +28 -9
- hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +1 -21
- hiddifypanel/panel/commercial/restapi/v2/parent/register_api.py +1 -1
- hiddifypanel/panel/commercial/restapi/v2/parent/schema.py +8 -4
- hiddifypanel/panel/commercial/restapi/v2/parent/sync_api.py +19 -3
- hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +48 -42
- hiddifypanel/panel/commercial/telegrambot/Usage.py +1 -1
- hiddifypanel/panel/commercial/telegrambot/admin.py +1 -1
- hiddifypanel/panel/commercial/telegrambot/information.py +1 -1
- hiddifypanel/panel/common.py +5 -11
- hiddifypanel/panel/hiddify.py +9 -20
- hiddifypanel/panel/init_db.py +31 -13
- hiddifypanel/panel/usage.py +38 -9
- hiddifypanel/panel/user/user.py +52 -32
- hiddifypanel/templates/admin-layout.html +2 -2
- hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/en/LC_MESSAGES/messages.po +80 -25
- hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/fa/LC_MESSAGES/messages.po +74 -20
- hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/pt/LC_MESSAGES/messages.po +60 -6
- hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/ru/LC_MESSAGES/messages.po +158 -78
- hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/zh/LC_MESSAGES/messages.po +60 -6
- hiddifypanel/translations.i18n/en.json +62 -22
- hiddifypanel/translations.i18n/fa.json +57 -17
- hiddifypanel/translations.i18n/pt.json +43 -3
- hiddifypanel/translations.i18n/ru.json +112 -72
- hiddifypanel/translations.i18n/zh.json +43 -3
- {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/METADATA +2 -1
- {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/RECORD +81 -81
- {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/WHEEL +1 -1
- hiddifypanel/panel/cf_api.py +0 -37
- {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/LICENSE.md +0 -0
- {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/entry_points.txt +0 -0
- {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/top_level.txt +0 -0
hiddifypanel/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
10.
|
1
|
+
10.30.0.dev0
|
hiddifypanel/VERSION.py
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
__version__='10.
|
1
|
+
__version__='10.30.0.dev0'
|
2
2
|
from datetime import datetime
|
3
|
-
__release_date__= datetime.strptime('2024-
|
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
|
-
|
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,
|
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
|
-
|
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
|
hiddifypanel/hutils/convert.py
CHANGED
@@ -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:
|
hiddifypanel/hutils/flask.py
CHANGED
@@ -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
|
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
|
-
|
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.
|
68
|
+
user.package_days = 3650
|
69
69
|
else:
|
70
|
-
user.
|
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
|
-
|
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:
|
@@ -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,
|
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
|
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
|
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
|
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 =
|
344
|
-
ip_target_asn =
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
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,
|
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
|
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
|
-
|
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
|
-
|
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()
|
@@ -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]
|