hiddifypanel 10.80.0.dev13__py3-none-any.whl → 10.80.0.dev15__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 (33) hide show
  1. hiddifypanel/VERSION +1 -1
  2. hiddifypanel/VERSION.py +1 -1
  3. hiddifypanel/hutils/proxy/__init__.py +1 -0
  4. hiddifypanel/hutils/proxy/clash.py +2 -2
  5. hiddifypanel/hutils/proxy/shared.py +6 -6
  6. hiddifypanel/hutils/proxy/singbox.py +1 -1
  7. hiddifypanel/hutils/proxy/wireguard.py +34 -0
  8. hiddifypanel/hutils/proxy/xray.py +2 -2
  9. hiddifypanel/hutils/proxy/xrayjson.py +4 -4
  10. hiddifypanel/models/config_enum.py +2 -2
  11. hiddifypanel/models/proxy.py +1 -1
  12. hiddifypanel/panel/admin/AdminstratorAdmin.py +7 -8
  13. hiddifypanel/panel/admin/DomainAdmin.py +131 -98
  14. hiddifypanel/panel/admin/QuickSetup.py +8 -8
  15. hiddifypanel/panel/admin/UserAdmin.py +58 -33
  16. hiddifypanel/panel/admin/templates/index.html +6 -4
  17. hiddifypanel/panel/admin/templates/model/user_list.html +11 -3
  18. hiddifypanel/panel/commercial/restapi/v1/tgbot.py +19 -1
  19. hiddifypanel/panel/common.py +1 -1
  20. hiddifypanel/panel/init_db.py +19 -16
  21. hiddifypanel/panel/user/user.py +6 -18
  22. hiddifypanel/translations.i18n/en.json +342 -1
  23. hiddifypanel/translations.i18n/fa.json +342 -1
  24. hiddifypanel/translations.i18n/fr.json +2 -2
  25. hiddifypanel/translations.i18n/my.json +2 -2
  26. hiddifypanel/translations.i18n/pt.json +342 -1
  27. hiddifypanel/translations.i18n/ru.json +342 -1
  28. hiddifypanel/translations.i18n/zh.json +342 -1
  29. {hiddifypanel-10.80.0.dev13.dist-info → hiddifypanel-10.80.0.dev15.dist-info}/METADATA +1 -1
  30. {hiddifypanel-10.80.0.dev13.dist-info → hiddifypanel-10.80.0.dev15.dist-info}/RECORD +33 -32
  31. {hiddifypanel-10.80.0.dev13.dist-info → hiddifypanel-10.80.0.dev15.dist-info}/LICENSE.md +0 -0
  32. {hiddifypanel-10.80.0.dev13.dist-info → hiddifypanel-10.80.0.dev15.dist-info}/WHEEL +0 -0
  33. {hiddifypanel-10.80.0.dev13.dist-info → hiddifypanel-10.80.0.dev15.dist-info}/entry_points.txt +0 -0
hiddifypanel/VERSION CHANGED
@@ -1 +1 @@
1
- 10.80.0.dev13
1
+ 10.80.0.dev15
hiddifypanel/VERSION.py CHANGED
@@ -2,5 +2,5 @@ import importlib.metadata
2
2
  from datetime import datetime
3
3
 
4
4
  __version__ = importlib.metadata.version(__package__ or __name__)
5
- __release_time__= datetime.strptime('2024-11-16T22:55:11','%Y-%m-%dT%H:%M:%S')
5
+ __release_time__= datetime.strptime('2024-12-08T23:50:34','%Y-%m-%dT%H:%M:%S')
6
6
  is_released_version=True
@@ -3,3 +3,4 @@ from . import xray
3
3
  from . import xrayjson
4
4
  from . import singbox
5
5
  from . import clash
6
+ from . import wireguard
@@ -33,7 +33,7 @@ def to_clash(proxy, meta_or_normal):
33
33
 
34
34
  if proxy['l3'] in ["kcp", ProxyL3.h3_quic]:
35
35
  return {'name': name, 'msg': f"clash does not support {proxy['l3']}", 'type': 'debug'}
36
- if proxy['transport'] in [ProxyTransport.splithttp, ProxyTransport.httpupgrade]:
36
+ if proxy['transport'] in [ProxyTransport.xhttp, ProxyTransport.httpupgrade]:
37
37
  return {'name': name, 'msg': f"clash does not support {proxy['transport']}", 'type': 'debug'}
38
38
  # if proxy['proto'] in [Proxy.shado]:
39
39
 
@@ -44,7 +44,7 @@ def to_clash(proxy, meta_or_normal):
44
44
  return {'name': name, 'msg': f"clash does not support {proxy['proto']}", 'type': 'debug'}
45
45
  if proxy['proto'] in ["vless", 'tuic', 'hysteria2']:
46
46
  return {'name': name, 'msg': f"{proxy['proto']} not supported in clash", 'type': 'debug'}
47
- if proxy['transport'] in ["shadowtls", "splithttp"]:
47
+ if proxy['transport'] in ["shadowtls", "xhttp"]:
48
48
  return {'name': name, 'msg': f"{proxy['transport']} not supported in clash", 'type': 'debug'}
49
49
  if proxy['l3'] == ProxyL3.tls_h2 and proxy['proto'] in [ProxyProto.vmess, ProxyProto.vless] and proxy['dbe'].cdn == ProxyCDN.direct:
50
50
  return {'name': name, 'msg': "bug tls_h2 vmess and vless in clash meta", 'type': 'warning'}
@@ -134,8 +134,8 @@ def get_proxies(child_id: int = 0, only_enabled=False) -> list['Proxy']:
134
134
  proxies = [c for c in proxies if 'trojan' not in c.proto]
135
135
  if not hconfig(ConfigEnum.httpupgrade_enable, child_id):
136
136
  proxies = [c for c in proxies if ProxyTransport.httpupgrade not in c.transport]
137
- if not hconfig(ConfigEnum.splithttp_enable, child_id):
138
- proxies = [c for c in proxies if ProxyTransport.splithttp not in c.transport]
137
+ if not hconfig(ConfigEnum.xhttp_enable, child_id):
138
+ proxies = [c for c in proxies if ProxyTransport.xhttp not in c.transport]
139
139
  if not hconfig(ConfigEnum.ws_enable, child_id):
140
140
  proxies = [c for c in proxies if ProxyTransport.WS not in c.transport]
141
141
  # if not hconfig(ConfigEnum.xtls_enable, child_id):
@@ -189,7 +189,7 @@ def get_valid_proxies(domains: list[Domain]) -> list[dict]:
189
189
  noDomainProxies = False
190
190
  if proxy.proto in [ProxyProto.ssh, ProxyProto.wireguard]:
191
191
  noDomainProxies = True
192
- if proxy.proto in [ProxyProto.ss] and proxy.transport not in [ProxyTransport.grpc, ProxyTransport.h2, ProxyTransport.WS, ProxyTransport.httpupgrade, ProxyTransport.splithttp]:
192
+ if proxy.proto in [ProxyProto.ss] and proxy.transport not in [ProxyTransport.grpc, ProxyTransport.h2, ProxyTransport.WS, ProxyTransport.httpupgrade, ProxyTransport.xhttp]:
193
193
  noDomainProxies = True
194
194
  options = []
195
195
  key = f'{proxy.proto}{proxy.transport}{proxy.cdn}{proxy.l3}'
@@ -413,9 +413,9 @@ def make_proxy(hconfigs: dict, proxy: Proxy, domain_db: Domain, phttp=80, ptls=4
413
413
  base['path'] = f'/{path[base["proto"]]}{hconfigs[ConfigEnum.path_httpupgrade]}'
414
414
  base["host"] = domain
415
415
  return base
416
- if proxy.transport in [ProxyTransport.splithttp]:
417
- base['transport'] = 'splithttp'
418
- base['path'] = f'/{path[base["proto"]]}{hconfigs[ConfigEnum.path_splithttp]}'
416
+ if proxy.transport in [ProxyTransport.xhttp]:
417
+ base['transport'] = 'xhttp'
418
+ base['path'] = f'/{path[base["proto"]]}{hconfigs[ConfigEnum.path_xhttp]}'
419
419
  # if 0 and 'h2' in base['alpn'] or 'h3' in base['alpn']:
420
420
  # base['path'] += "2"
421
421
  # else:
@@ -47,7 +47,7 @@ def configs_as_json(domains: list[Domain], **kwargs) -> str:
47
47
  def is_xray_proxy(proxy: dict):
48
48
  if g.user_agent.get('is_hiddify_prefere_xray'):
49
49
  return True
50
- if proxy['transport'] == ProxyTransport.splithttp:
50
+ if proxy['transport'] == ProxyTransport.xhttp:
51
51
  return True
52
52
  return False
53
53
 
@@ -0,0 +1,34 @@
1
+
2
+ def generate_wireguard_config(proxy: dict) -> str:
3
+ """
4
+ Generates a WireGuard configuration from a given proxy dictionary.
5
+
6
+ Args:
7
+ proxy (dict): Dictionary containing WireGuard and proxy details.
8
+
9
+ Returns:
10
+ str: A WireGuard configuration string.
11
+ """
12
+ name=f'{proxy["extra_info"]} {proxy["name"]}'
13
+ addrs = f"{proxy['wg_ipv4']}/32"
14
+ if proxy['wg_ipv6']:
15
+ addrs += f", {proxy['wg_ipv6']}/128"
16
+ config = f"""[Interface]
17
+ # Name = {name}
18
+ Address= {addrs}
19
+ PrivateKey = {proxy["wg_pk"]}
20
+ MTU = {proxy.get("mtu", 1380)}
21
+ DNS = {proxy.get("dns", "1.1.1.1")}
22
+
23
+ [Peer]
24
+ # Name = Public Peer for {name}
25
+ Endpoint = {proxy["server"]}:{proxy["port"]}
26
+ PublicKey = {proxy["wg_server_pub"]}
27
+ PresharedKey = {proxy['wg_psk']}
28
+ #PersistentKeepalive = {proxy.get("keep_alive", 25)}
29
+ """
30
+
31
+ #Address = {proxy.get("wg_ipv4", "0.0.0.0/32")}
32
+ #AllowedIPs = {proxy.get("allowed_ips", "0.0.0.0/0")}
33
+
34
+ return config
@@ -126,10 +126,10 @@ def to_link(proxy: dict) -> str | dict:
126
126
  baseurl += "&encryption=none"
127
127
  if proxy.get('fingerprint', 'none') != 'none':
128
128
  baseurl += "&fp=" + proxy['fingerprint']
129
- if proxy.get('transport') in {ProxyTransport.splithttp}:
129
+ if proxy.get('transport') in {ProxyTransport.xhttp}:
130
130
  baseurl += "&core=xray"
131
131
  if proxy['l3'] != 'quic':
132
- if proxy.get('l3') != ProxyL3.reality and (proxy.get('transport') in {ProxyTransport.tcp, ProxyTransport.httpupgrade, ProxyTransport.splithttp}) and proxy['proto'] in [ProxyProto.vless, ProxyProto.trojan]:
132
+ if proxy.get('l3') != ProxyL3.reality and (proxy.get('transport') in {ProxyTransport.tcp, ProxyTransport.httpupgrade, ProxyTransport.xhttp}) and proxy['proto'] in [ProxyProto.vless, ProxyProto.trojan]:
133
133
  baseurl += '&headerType=http'
134
134
  else:
135
135
  baseurl += '&headerType=None'
@@ -262,9 +262,9 @@ def add_stream_settings(base: dict, proxy: dict):
262
262
  if proxy['transport'] == ProxyTransport.httpupgrade:
263
263
  ss['network'] = proxy['transport']
264
264
  add_httpupgrade_stream(ss, proxy)
265
- if proxy['transport'] == ProxyTransport.splithttp:
265
+ if proxy['transport'] == ProxyTransport.xhttp:
266
266
  ss['network'] = proxy['transport']
267
- add_splithttp_stream(ss, proxy)
267
+ add_xhttp_stream(ss, proxy)
268
268
  if proxy['transport'] == 'ws':
269
269
  ss['network'] = proxy['transport']
270
270
  add_ws_stream(ss, proxy)
@@ -338,8 +338,8 @@ def add_httpupgrade_stream(ss: dict, proxy: dict):
338
338
  }
339
339
 
340
340
 
341
- def add_splithttp_stream(ss: dict, proxy: dict):
342
- ss['splithttpSettings'] = {
341
+ def add_xhttp_stream(ss: dict, proxy: dict):
342
+ ss['xhttpSettings'] = {
343
343
  'path': proxy['path'],
344
344
  'host': proxy['host'],
345
345
  "headers": {
@@ -239,7 +239,7 @@ class ConfigEnum(metaclass=FastEnum):
239
239
  ws_enable = _BoolConfigDscr(ConfigCategory.proxies, ApplyMode.apply_config)
240
240
  grpc_enable = _BoolConfigDscr(ConfigCategory.proxies, ApplyMode.apply_config)
241
241
  httpupgrade_enable = _BoolConfigDscr(ConfigCategory.proxies, ApplyMode.apply_config)
242
- splithttp_enable = _BoolConfigDscr(ConfigCategory.proxies, ApplyMode.apply_config)
242
+ xhttp_enable = _BoolConfigDscr(ConfigCategory.proxies, ApplyMode.apply_config)
243
243
 
244
244
  vless_enable = _BoolConfigDscr(ConfigCategory.proxies, ApplyMode.apply_config)
245
245
  trojan_enable = _BoolConfigDscr(ConfigCategory.proxies, ApplyMode.apply_config)
@@ -262,7 +262,7 @@ class ConfigEnum(metaclass=FastEnum):
262
262
  path_v2ray = _StrConfigDscr(ConfigCategory.hidden, ApplyMode.apply_config, hide_in_virtual_child=True) # deprecated
263
263
  path_ss = _StrConfigDscr(ConfigCategory.hidden, ApplyMode.apply_config, hide_in_virtual_child=True)
264
264
 
265
- path_splithttp = _StrConfigDscr(ConfigCategory.too_advanced, ApplyMode.apply_config, hide_in_virtual_child=True)
265
+ path_xhttp = _StrConfigDscr(ConfigCategory.too_advanced, ApplyMode.apply_config, hide_in_virtual_child=True)
266
266
  path_httpupgrade = _StrConfigDscr(ConfigCategory.too_advanced, ApplyMode.apply_config, hide_in_virtual_child=True)
267
267
  path_ws = _StrConfigDscr(ConfigCategory.too_advanced, ApplyMode.apply_config, hide_in_virtual_child=True)
268
268
  path_tcp = _StrConfigDscr(ConfigCategory.too_advanced, ApplyMode.apply_config, hide_in_virtual_child=True)
@@ -20,7 +20,7 @@ class ProxyTransport(StrEnum):
20
20
  tcp = auto()
21
21
  ssh = auto()
22
22
  httpupgrade = auto()
23
- splithttp = auto()
23
+ xhttp = auto()
24
24
  custom = auto()
25
25
  shadowsocks = auto()
26
26
 
@@ -143,19 +143,18 @@ class AdminstratorAdmin(AdminLTEModelView):
143
143
  """)
144
144
 
145
145
  def _max_active_users_formatter(view, context, model, name):
146
-
147
- actives = [u for u in model.recursive_users_query().all() if u.is_active]
148
- u = len(actives)
146
+ """Optimized user count formatter using database queries"""
147
+ active_count = model.recursive_users_query().filter(User.is_active == True).count()
149
148
  if model.mode == AdminMode.super_admin:
150
- return f"{u} / ∞"
149
+ return f"{active_count} / ∞"
151
150
  t = model.max_active_users
152
- rate = round(u * 100 / (t + 0.000001))
153
- state = "danger" if u >= t else ('warning' if rate > 80 else 'success')
154
- color = "#ff7e7e" if u >= t else ('#ffc107' if rate > 80 else '#9ee150')
151
+ rate = round(active_count * 100 / (t + 0.000001))
152
+ color = "#ff7e7e" if active_count >= t else ('#ffc107' if rate > 80 else '#9ee150')
153
+
155
154
  return Markup(f"""
156
155
  <div class="progress progress-lg position-relative" style="min-width: 100px;">
157
156
  <div class="progress-bar progress-bar-striped" role="progressbar" style="width: {rate}%;background-color: {color};" aria-valuenow="{rate}" aria-valuemin="0" aria-valuemax="100"></div>
158
- <span class='badge position-absolute' style="left:auto;right:auto;width: 100%;font-size:1em">{u} {_('user.home.usage.from')} {t}</span>
157
+ <span class='badge position-absolute' style="left:auto;right:auto;width: 100%;font-size:1em">{active_count} {_('user.home.usage.from')} {t}</span>
159
158
 
160
159
  </div>
161
160
  """)
@@ -16,6 +16,7 @@ from hiddifypanel.panel import hiddify, custom_widgets
16
16
  from .adminlte import AdminLTEModelView
17
17
  from hiddifypanel import hutils
18
18
 
19
+ from loguru import logger
19
20
  from flask import current_app
20
21
  # Define a custom field type for the related domains
21
22
 
@@ -152,132 +153,164 @@ class DomainAdmin(AdminLTEModelView):
152
153
 
153
154
  # TODO: refactor this function
154
155
  def on_model_change(self, form, model, is_created):
155
- model.domain = (model.domain or '').lower()
156
+ # Sanitize domain input
157
+ model.domain = (model.domain or '').lower().strip()
158
+
159
+ # Basic validation
156
160
  if model.domain == '' and model.mode != DomainType.fake:
157
161
  raise ValidationError(_("domain.empty.allowed_for_fake_only"))
158
- configs = get_hconfigs()
159
- for c in configs:
160
- if "domain" in c and c not in [ConfigEnum.decoy_domain, ConfigEnum.reality_fallback_domain] and c.category != 'hidden':
161
- if model.domain == configs[c]:
162
- raise ValidationError(_("You have used this domain in: ") + _(f"config.{c}.label"))
163
-
164
- for td in Domain.query.filter(Domain.mode == DomainType.reality, Domain.domain != model.domain).all():
165
- # print(td)
166
- if td.servernames and (model.domain in td.servernames.split(",")):
167
- raise ValidationError(_("You have used this domain in: ") + _(f"config.reality_server_names.label") + td.domain)
168
-
169
- if is_created and Domain.query.filter(Domain.domain == model.domain, Domain.child_id == model.child_id).count() > 1:
170
- raise ValidationError(_("You have used this domain in: "))
171
162
 
163
+ self._validate_not_used_before(model,is_created)
172
164
  ipv4_list = hutils.network.get_ips(4)
173
165
  ipv6_list = hutils.network.get_ips(6)
166
+ server_ips = [*ipv4_list, *ipv6_list]
174
167
 
175
- if not ipv4_list and not ipv6_list:
168
+ if not server_ips:
176
169
  raise ValidationError(_("Couldn't find your ip addresses"))
177
170
 
171
+ # Validate domain based on mode
178
172
  if "*" in model.domain and model.mode not in [DomainType.cdn, DomainType.auto_cdn_ip]:
179
173
  raise ValidationError(_("Domain can not be resolved! there is a problem in your domain"))
180
174
 
181
- skip_check = "*" in model.domain or model.domain == ""
175
+ cloudflare_updated=self._update_cloudflare(model, ipv4_list,ipv6_list)
176
+
177
+ if not (cloudflare_updated or "*" in model.domain or model.domain == ""):
178
+ self._validate_domain_ips(model, server_ips)
179
+
180
+ # Handle CDN IP settings
181
+ if model.mode == DomainType.direct and model.cdn_ip:
182
+ model.cdn_ip = ""
183
+ raise ValidationError(_("Specifying CDN IP is only valid for CDN mode"))
184
+
185
+ if model.mode == DomainType.fake and not model.cdn_ip:
186
+ model.cdn_ip = str(server_ips[0])
187
+
188
+ if model.cdn_ip:
189
+ try:
190
+ hutils.network.auto_ip_selector.get_clean_ip(str(model.cdn_ip))
191
+ except Exception:
192
+ raise ValidationError(_("Error in auto cdn format"))
193
+
194
+ # Update show domains
195
+ if len(model.show_domains) == Domain.query.count():
196
+ model.show_domains = []
197
+
198
+ # Handle mode-specific settings
199
+ if model.mode == DomainType.old_xtls_direct and not hconfig(ConfigEnum.xtls_enable):
200
+ set_hconfig(ConfigEnum.xtls_enable, True)
201
+ hutils.proxy.get_proxies().invalidate_all()
202
+ elif model.mode == DomainType.reality:
203
+ self._validate_reality_settings(model, server_ips)
204
+
205
+ # Signal config update if needed
206
+ old_db_domain = Domain.by_domain(model.domain)
207
+ if is_created or not old_db_domain or old_db_domain.mode != model.mode:
208
+ # return hiddify.reinstall_action(complete_install=False, domain_changed=True)
209
+ hutils.flask.flash_config_success(restart_mode=ApplyMode.apply_config, domain_changed=True)
210
+
211
+
212
+
213
+ def _update_cloudflare(self, model, ipv4_list,ipv6_list):
182
214
  if hconfig(ConfigEnum.cloudflare) and model.mode not in [DomainType.fake, DomainType.relay, DomainType.reality]:
183
215
  try:
184
216
  proxied = model.mode in [DomainType.cdn, DomainType.auto_cdn_ip]
185
- hutils.network.cf_api.add_or_update_dns_record(model.domain, str(ipv4_list[0]), "A", proxied=proxied)
217
+ if ipv4_list:
218
+ hutils.network.cf_api.add_or_update_dns_record(model.domain, str(ipv4_list[0]), "A", proxied=proxied)
186
219
  if ipv6_list:
187
220
  hutils.network.cf_api.add_or_update_dns_record(model.domain, str(ipv6_list[0]), "AAAA", proxied=proxied)
188
-
189
- skip_check = True
221
+ return True
190
222
  except Exception as e:
191
223
  raise ValidationError(__("cloudflare.error") + f' {e}')
192
- # elif model.mode==DomainType.auto_cdn_ip:
193
- # if model.alias and not model.alias.replace("_", "").isalnum():
194
- # hutils.flask.flash(__("Using alias with special charachters may cause problem in some clients like FairVPN."), 'warning')
195
- # raise ValidationError(_("You have to add your cloudflare api key to use this feature: "))
224
+ return False
196
225
 
197
- dips = hutils.network.get_domain_ips(model.domain)
198
- server_ips = [*ipv4_list, *ipv6_list]
199
- if model.sub_link_only:
200
- if not dips:
201
- raise ValidationError(_("Domain can not be resolved! there is a problem in your domain")) # type: ignore
202
- elif not skip_check:
203
- if not dips:
204
- raise ValidationError(_("Domain can not be resolved! there is a problem in your domain")) # type: ignore
205
-
206
- domain_ip_is_same_as_panel = False
207
-
208
- for mip in server_ips:
209
- domain_ip_is_same_as_panel |= mip in dips
210
- server_ips_str = ', '.join(list(map(str, server_ips)))
211
- dips_str = ', '.join(list(map(str, dips)))
212
-
213
- if model.mode == DomainType.direct and not domain_ip_is_same_as_panel:
214
- # hutils.flask.flash(__(f"Domain IP={dip} is not matched with your ip={', '.join(list(map(str, ipv4_list)))} which is required in direct mode"),category='error')
215
- raise ValidationError(
216
- __("Domain IP=%(domain_ip)s is not matched with your ip=%(server_ip)s which is required in direct mode", server_ip=server_ips_str, domain_ip=dips_str)) # type: ignore
217
-
218
- if domain_ip_is_same_as_panel and model.mode in [DomainType.cdn, DomainType.relay, DomainType.fake, DomainType.auto_cdn_ip]:
219
- # # hutils.flask.flash(__(f"In CDN mode, Domain IP={dip} should be different to your ip={', '.join(list(map(str, ipv4_list)))}"), 'warning')
220
- raise ValidationError(__("In CDN mode, Domain IP=%(domain_ip)s should be different to your ip=%(server_ip)s",
221
- server_ip=server_ips_str, domain_ip=dips_str)) # type: ignore
222
-
223
- # if model.mode in [DomainType.ss_faketls, DomainType.telegram_faketls]:
224
- # if len(Domain.query.filter(Domain.mode==model.mode and Domain.id!=model.id).all())>0:
225
- # ValidationError(f"another {model.mode} is exist")
226
-
227
- model.domain = model.domain.lower()
228
- if model.mode == DomainType.direct and model.cdn_ip:
229
- model.cdn_ip = ""
230
- raise ValidationError(f"Specifying CDN IP is only valid for CDN mode")
226
+ def _validate_reality_settings(self, model, server_ips):
227
+ """Validate REALITY protocol settings with proper error handling"""
228
+ if not hconfig(ConfigEnum.reality_enable):
229
+ set_hconfig(ConfigEnum.reality_enable, True)
230
+ hutils.proxy.get_proxies().invalidate_all()
231
231
 
232
- if model.mode == DomainType.fake and not model.cdn_ip:
233
- model.cdn_ip = str(server_ips[0])
232
+ model.servernames = (model.servernames or model.domain).lower().strip()
233
+ domains_to_check = set()
234
+ for v in [model.domain, model.servernames]:
235
+ domains_to_check.update(d.strip() for d in v.split(",") if d.strip())
234
236
 
235
- # if model.mode==DomainType.fake and model.cdn_ip!=myip:
236
- # raise ValidationError(f"Specifying CDN IP is only valid for CDN mode")
237
+ for d in domains_to_check:
238
+ # Check REALITY compatibility
239
+ if not hutils.network.is_domain_reality_friendly(d):
240
+ raise ValidationError(_("Domain is not REALITY friendly!") + f' {d}')
237
241
 
238
- # # Update the many-to-many relationship
239
- if len(model.show_domains) == Domain.query.count():
240
- model.show_domains = []
241
- if model.mode == DomainType.old_xtls_direct:
242
- if not hconfig(ConfigEnum.xtls_enable):
243
- set_hconfig(ConfigEnum.xtls_enable, True)
244
- hutils.proxy.get_proxies().invalidate_all()
245
- elif model.mode == DomainType.reality:
246
- if not hconfig(ConfigEnum.reality_enable):
247
- set_hconfig(ConfigEnum.reality_enable, True)
248
- hutils.proxy.get_proxies().invalidate_all()
249
- model.servernames = (model.servernames or model.domain).lower()
250
- for v in set([model.domain, model.servernames]):
251
- for d in v.split(","):
252
- if not d:
253
- continue
254
- if not hutils.network.is_domain_reality_friendly(d): # the minimum requirement for the REALITY protocol is to have tls1.3 and h2
255
- raise ValidationError(_("Domain is not REALITY friendly!") + f' {d}')
256
-
257
- if not hutils.network.is_in_same_asn(d, server_ips[0]):
258
- dip = next(iter(dips))
242
+ try:
243
+ if not hutils.network.is_in_same_asn(d, server_ips[0]):
244
+ domain_ips = hutils.network.get_domain_ips(d)
245
+ if domain_ips:
246
+ dip = next(iter(domain_ips))
259
247
  server_asn = hutils.network.get_ip_asn(server_ips[0])
260
- domain_asn = hutils.network.get_ip_asn(dip) # type: ignore
261
- msg = _("domain.reality.asn_issue") + \
262
- (f"<br> Server ASN={server_asn}<br>{d}_ASN={domain_asn}" if server_asn or domain_asn else "")
248
+ domain_asn = hutils.network.get_ip_asn(dip)
249
+ msg = _("domain.reality.asn_issue")
250
+ if server_asn or domain_asn:
251
+ msg += f"<br> Server ASN={server_asn}<br>{d}_ASN={domain_asn}"
263
252
  hutils.flask.flash(msg, 'warning')
253
+ except Exception as e:
254
+ logger.warning(f"ASN check failed for domain {d}: {str(e)}")
264
255
 
265
- for d in model.servernames.split(","):
266
- if not hutils.network.fallback_domain_compatible_with_servernames(model.domain, d):
267
- msg = _("REALITY Fallback domain is not compaitble with server names!") + f' {d} != {model.domain}'
268
- hutils.flask.flash(msg, 'warning')
256
+ # Check fallback compatibility
257
+ for d in model.servernames.split(","):
258
+ if d.strip() and not hutils.network.fallback_domain_compatible_with_servernames(model.domain, d):
259
+ msg = _("REALITY Fallback domain is not compatible with server names!") + f' {d} != {model.domain}'
260
+ hutils.flask.flash(msg, 'warning')
269
261
 
270
- if (model.cdn_ip):
271
- try:
272
- hutils.network.auto_ip_selector.get_clean_ip(str(model.cdn_ip))
273
- except BaseException:
274
- raise ValidationError(_("Error in auto cdn format"))
275
262
 
276
- old_db_domain = Domain.by_domain(model.domain)
277
- if is_created or not old_db_domain or old_db_domain.mode != model.mode:
278
- # return hiddify.reinstall_action(complete_install=False, domain_changed=True)
279
- hutils.flask.flash_config_success(restart_mode=ApplyMode.apply_config, domain_changed=True)
263
+ def _validate_not_used_before(self, model,is_created):
264
+ configs = get_hconfigs()
265
+ for c in configs:
266
+ if "domain" in c and c not in [ConfigEnum.decoy_domain, ConfigEnum.reality_fallback_domain] and c.category != 'hidden':
267
+ if model.domain == configs[c]:
268
+ raise ValidationError(_("You have used this domain in: ") + _(f"config.{c}.label"))
269
+
270
+ for td in Domain.query.filter(Domain.mode == DomainType.reality, Domain.domain != model.domain).all():
271
+ # print(td)
272
+ if td.servernames and (model.domain in td.servernames.split(",")):
273
+ raise ValidationError(_("You have used this domain in: ") + _(f"config.reality_server_names.label") + td.domain)
280
274
 
275
+ if is_created and Domain.query.filter(Domain.domain == model.domain, Domain.child_id == model.child_id).count() > 1:
276
+ raise ValidationError(_("You have used this domain in: "))
277
+
278
+ def _validate_domain_ips(self, model, server_ips):
279
+ """Validate domain IP resolution and matching"""
280
+
281
+ # Skip validation for wildcard or empty domains
282
+ if model.domain.startswith('*') or not model.domain:
283
+ return True
284
+
285
+ # Resolve domain IPs with timeout
286
+ try:
287
+ dips = hutils.network.get_domain_ips(model.domain, timeout=10)
288
+ except Exception as e:
289
+ logger.error(f"Error resolving domain {model.domain}: {str(e)}")
290
+ raise ValidationError(_("Domain cannot be resolved! Please check DNS settings"))
291
+
292
+ # Validate resolution success
293
+ if not dips:
294
+ raise ValidationError(_("Domain cannot be resolved! Please check DNS settings"))
295
+
296
+ # Check IP matching based on mode
297
+ domain_ip_matches_server = any(ip in dips for ip in server_ips)
298
+ server_ips_str = ', '.join(map(str, server_ips))
299
+ dips_str = ', '.join(map(str, dips))
300
+
301
+ if not domain_ip_matches_server and model.mode in [DomainType.direct]:
302
+ raise ValidationError(
303
+ __("Domain IP=%(domain_ip)s is not matched with your ip=%(server_ip)s which is required in direct mode",
304
+ server_ip=server_ips_str, domain_ip=dips_str))
305
+
306
+ if domain_ip_matches_server and model.mode in [DomainType.cdn, DomainType.relay, DomainType.fake, DomainType.auto_cdn_ip]:
307
+ raise ValidationError(
308
+ __("In CDN mode, Domain IP=%(domain_ip)s should be different to your ip=%(server_ip)s",
309
+ server_ip=server_ips_str, domain_ip=dips_str))
310
+
311
+ return True
312
+
313
+
281
314
  # def after_model_change(self,form, model, is_created):
282
315
  # if model.show_domains.count==0:
283
316
  # db.session.bulk_save_objects(ShowDomain(model.id,model.id))
@@ -72,7 +72,7 @@ def get_lang_form(empty=False):
72
72
  default=hconfig(ConfigEnum.admin_lang))
73
73
  # lang=wtf.SelectField(_("config.lang.label"),choices=[("en",_("lang.en")),("fa",_("lang.fa"))],description=_("config.lang.description"),default=hconfig(ConfigEnum.lang))
74
74
  country = wtf.SelectField(
75
- _("config.country.label"), choices=[("ir", _("Iran")), ("zh", _("China")), ("other", "Others")],
75
+ _("config.country.label"), choices=[("ir", _("Iran")), ("zh", _("China")), ("ru", _("Russia")), ("other", "Others")],
76
76
  description=_("config.country.description"),
77
77
  default=hconfig(ConfigEnum.country))
78
78
  lang_submit = wtf.SubmitField(_('Submit'))
@@ -251,11 +251,11 @@ def validate_domain(form, field):
251
251
  if dip is None:
252
252
  raise ValidationError(_("Domain can not be resolved! there is a problem in your domain"))
253
253
 
254
- myip = hutils.network.get_ip(4)
255
- myip6 = hutils.network.get_ip(4)
256
- if dip and myip != dip and (not myip6 or myip6 != dip):
254
+ myips = hutils.network.get_ips()
255
+ # Fixed: Changed from get_ip(4) to get_ip(6)
256
+ if dip not in myips:
257
257
  raise ValidationError(_("Domain (%(domain)s)-> IP=%(domain_ip)s is not matched with your ip=%(server_ip)s which is required in direct mode",
258
- server_ip=myip, domain_ip=dip, domain=domain))
258
+ server_ip=myips, domain_ip=dip, domain=domain))
259
259
 
260
260
 
261
261
  def validate_domain_cdn(form, field):
@@ -266,10 +266,10 @@ def validate_domain_cdn(form, field):
266
266
  if dip is None:
267
267
  raise ValidationError(_("Domain can not be resolved! there is a problem in your domain"))
268
268
 
269
- myip = hutils.network.get_ip(4)
270
- if myip == dip:
269
+ myips = hutils.network.get_ips()
270
+ if dip in myips:
271
271
  raise ValidationError(_("In CDN mode, Domain IP=%(domain_ip)s should be different to your ip=%(server_ip)s",
272
- server_ip=myip, domain_ip=dip, domain=domain))
272
+ server_ip=myips, domain_ip=dip, domain=domain))
273
273
 
274
274
 
275
275
  def admin_link():