hiddifypanel 9.0.0.dev92__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.
- hiddifypanel/VERSION +1 -1
- hiddifypanel/VERSION.py +2 -2
- hiddifypanel/auth.py +30 -9
- hiddifypanel/base.py +58 -50
- hiddifypanel/cache.py +43 -25
- hiddifypanel/database.py +9 -0
- hiddifypanel/drivers/abstract_driver.py +2 -0
- hiddifypanel/drivers/singbox_api.py +17 -15
- hiddifypanel/drivers/ssh_liberty_bridge_api.py +3 -1
- hiddifypanel/drivers/user_driver.py +12 -6
- hiddifypanel/drivers/wireguard_api.py +7 -2
- hiddifypanel/drivers/xray_api.py +14 -9
- hiddifypanel/hutils/__init__.py +4 -0
- hiddifypanel/hutils/convert.py +13 -2
- hiddifypanel/hutils/crypto.py +48 -0
- hiddifypanel/hutils/encode.py +4 -1
- hiddifypanel/hutils/flask.py +38 -5
- hiddifypanel/hutils/github_issue.py +1 -1
- hiddifypanel/hutils/importer/xui.py +5 -2
- hiddifypanel/{models/utils.py → hutils/model.py} +14 -4
- hiddifypanel/hutils/network/auto_ip_selector.py +2 -0
- hiddifypanel/hutils/network/net.py +46 -2
- hiddifypanel/hutils/node/__init__.py +3 -0
- hiddifypanel/hutils/node/api_client.py +76 -0
- hiddifypanel/hutils/node/child.py +147 -0
- hiddifypanel/hutils/node/parent.py +100 -0
- hiddifypanel/hutils/node/shared.py +65 -0
- hiddifypanel/hutils/proxy/__init__.py +5 -0
- hiddifypanel/hutils/proxy/clash.py +161 -0
- hiddifypanel/hutils/proxy/shared.py +434 -0
- hiddifypanel/hutils/proxy/singbox.py +339 -0
- hiddifypanel/hutils/proxy/xray.py +235 -0
- hiddifypanel/hutils/proxy/xrayjson.py +391 -0
- hiddifypanel/hutils/random.py +4 -0
- hiddifypanel/hutils/utils.py +4 -1
- hiddifypanel/models/__init__.py +2 -2
- hiddifypanel/models/admin.py +31 -17
- hiddifypanel/models/base_account.py +7 -7
- hiddifypanel/models/child.py +30 -16
- hiddifypanel/models/config.py +45 -16
- hiddifypanel/models/config_enum.py +68 -17
- hiddifypanel/models/domain.py +28 -20
- hiddifypanel/models/parent_domain.py +2 -2
- hiddifypanel/models/proxy.py +29 -20
- hiddifypanel/models/report.py +2 -3
- hiddifypanel/models/usage.py +2 -2
- hiddifypanel/models/user.py +33 -22
- hiddifypanel/panel/admin/Actions.py +13 -19
- hiddifypanel/panel/admin/AdminstratorAdmin.py +14 -3
- hiddifypanel/panel/admin/Dashboard.py +5 -10
- hiddifypanel/panel/admin/DomainAdmin.py +35 -48
- hiddifypanel/panel/admin/NodeAdmin.py +6 -2
- hiddifypanel/panel/admin/ProxyAdmin.py +6 -5
- hiddifypanel/panel/admin/QuickSetup.py +21 -20
- hiddifypanel/panel/admin/SettingAdmin.py +107 -62
- hiddifypanel/panel/admin/UserAdmin.py +22 -21
- hiddifypanel/panel/admin/templates/index.html +1 -1
- hiddifypanel/panel/admin/templates/model/user_list.html +44 -20
- hiddifypanel/panel/admin/templates/parent_dash.html +2 -4
- hiddifypanel/panel/admin/templates/result.html +2 -3
- hiddifypanel/panel/cf_api.py +1 -2
- hiddifypanel/panel/cli.py +16 -16
- hiddifypanel/panel/commercial/ProxyDetailsAdmin.py +16 -12
- hiddifypanel/panel/commercial/__init__.py +7 -5
- hiddifypanel/panel/commercial/restapi/v1/__init__.py +1 -1
- hiddifypanel/panel/commercial/restapi/v1/tgbot.py +1 -1
- hiddifypanel/panel/commercial/restapi/v1/tgmsg.py +14 -10
- hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py +0 -5
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +2 -2
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py +4 -5
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +8 -25
- hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +4 -4
- hiddifypanel/panel/commercial/restapi/v2/admin/schema.py +157 -0
- hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py +3 -3
- hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +9 -66
- hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +1 -1
- hiddifypanel/panel/commercial/restapi/v2/child/__init__.py +18 -0
- hiddifypanel/panel/commercial/restapi/v2/child/actions.py +63 -0
- hiddifypanel/panel/commercial/restapi/v2/child/register_parent_api.py +34 -0
- hiddifypanel/panel/commercial/restapi/v2/child/schema.py +7 -0
- hiddifypanel/panel/commercial/restapi/v2/child/sync_parent_api.py +21 -0
- hiddifypanel/panel/commercial/restapi/v2/panel/__init__.py +13 -0
- hiddifypanel/panel/commercial/restapi/v2/panel/info.py +18 -0
- hiddifypanel/panel/commercial/restapi/v2/panel/ping_pong.py +23 -0
- hiddifypanel/panel/commercial/restapi/v2/panel/schema.py +7 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/__init__.py +16 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/register_api.py +65 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/schema.py +115 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/status_api.py +26 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/sync_api.py +53 -0
- hiddifypanel/panel/commercial/restapi/v2/parent/usage_api.py +57 -0
- hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +17 -23
- hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +23 -26
- hiddifypanel/panel/commercial/telegrambot/admin.py +1 -2
- hiddifypanel/panel/common.py +25 -8
- hiddifypanel/panel/common_bp/login.py +2 -2
- hiddifypanel/panel/hiddify.py +22 -185
- hiddifypanel/panel/init_db.py +102 -55
- hiddifypanel/panel/usage.py +33 -18
- hiddifypanel/panel/user/__init__.py +0 -1
- hiddifypanel/panel/user/templates/all_configs copy.txt +2 -2
- hiddifypanel/panel/user/templates/all_configs.txt +2 -2
- hiddifypanel/panel/user/templates/base_singbox_config.json.j2 +2 -1
- hiddifypanel/panel/user/templates/base_xray_config.json.j2 +125 -0
- hiddifypanel/panel/user/templates/clash_config copy.yml +1 -1
- hiddifypanel/panel/user/templates/clash_config.yml +4 -4
- hiddifypanel/panel/user/templates/clash_proxies.yml +1 -1
- hiddifypanel/panel/user/templates/home/all-configs.html +2 -2
- hiddifypanel/panel/user/templates/home/all-configs_old.html +1 -1
- hiddifypanel/panel/user/templates/home/ios copy.html +2 -2
- hiddifypanel/panel/user/templates/home/usage.html +1 -1
- hiddifypanel/panel/user/templates/new.html +2 -2
- hiddifypanel/panel/user/user.py +56 -50
- hiddifypanel/static/css/custom.css +31 -0
- hiddifypanel/static/images/favicon.ico +0 -0
- hiddifypanel/static/images/hiddify-old.png +0 -0
- hiddifypanel/static/images/hiddify.png +0 -0
- hiddifypanel/static/images/hiddify2.png +0 -0
- hiddifypanel/static/new/assets/{index-1b891a7c.js → index-ccb9873c.js} +56 -56
- hiddifypanel/static/new/assets/index-fa00de9a.css +1 -0
- hiddifypanel/static/new/i18n/en.json +6 -6
- hiddifypanel/static/new/i18n/fa.json +2 -2
- hiddifypanel/templates/admin-layout.html +30 -43
- hiddifypanel/templates/fake.html +0 -4
- hiddifypanel/templates/flaskadmin-layout.html +7 -3
- hiddifypanel/templates/master.html +11 -6
- hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/en/LC_MESSAGES/messages.po +2082 -1977
- hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/fa/LC_MESSAGES/messages.po +2035 -1924
- hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/pt/LC_MESSAGES/messages.po +1911 -1848
- hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/ru/LC_MESSAGES/messages.po +2019 -1874
- hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/zh/LC_MESSAGES/messages.po +1873 -1742
- hiddifypanel/translations.i18n/en.json +992 -933
- hiddifypanel/translations.i18n/fa.json +994 -935
- hiddifypanel/translations.i18n/pt.json +1031 -972
- hiddifypanel/translations.i18n/ru.json +994 -935
- hiddifypanel/translations.i18n/zh.json +971 -912
- {hiddifypanel-9.0.0.dev92.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/METADATA +47 -47
- {hiddifypanel-9.0.0.dev92.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/RECORD +147 -120
- {hiddifypanel-9.0.0.dev92.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/WHEEL +1 -1
- hiddifypanel/panel/commercial/restapi/v2/DTO.py +0 -9
- hiddifypanel/panel/commercial/restapi/v2/hello/__init__.py +0 -16
- hiddifypanel/panel/commercial/restapi/v2/hello/hello.py +0 -32
- hiddifypanel/panel/user/link_maker.py +0 -1083
- hiddifypanel/static/new/assets/index-669b32c8.css +0 -1
- {hiddifypanel-9.0.0.dev92.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/LICENSE.md +0 -0
- {hiddifypanel-9.0.0.dev92.dist-info → hiddifypanel-10.5.0.dev0.dist-info}/entry_points.txt +0 -0
- {hiddifypanel-9.0.0.dev92.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}
|
hiddifypanel/hutils/encode.py
CHANGED
@@ -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:
|
hiddifypanel/hutils/flask.py
CHANGED
@@ -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
|
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
|
-
|
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
|
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,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
|