hiddifypanel 10.85.0b20__py3-none-any.whl → 10.86.0b1__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 (43) hide show
  1. hiddifypanel/VERSION +1 -1
  2. hiddifypanel/VERSION.py +2 -2
  3. hiddifypanel/hutils/network/auto_ip_selector.py +19 -10
  4. hiddifypanel/hutils/network/net.py +25 -5
  5. hiddifypanel/hutils/proxy/clash.py +1 -1
  6. hiddifypanel/hutils/proxy/shared.py +118 -56
  7. hiddifypanel/hutils/proxy/xray.py +55 -40
  8. hiddifypanel/hutils/proxy/xrayjson.py +48 -28
  9. hiddifypanel/models/config.py +1 -1
  10. hiddifypanel/models/config_enum.py +4 -0
  11. hiddifypanel/models/domain.py +33 -8
  12. hiddifypanel/models/proxy.py +6 -2
  13. hiddifypanel/panel/admin/DomainAdmin.py +22 -25
  14. hiddifypanel/panel/commercial/ProxyDetailsAdmin.py +13 -4
  15. hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +1 -1
  16. hiddifypanel/panel/custom_widgets.py +45 -0
  17. hiddifypanel/panel/init_db.py +102 -184
  18. hiddifypanel/panel/user/user.py +12 -12
  19. hiddifypanel/static/js/custom-rtl.js +1 -1
  20. hiddifypanel/templates/fake.html +11 -2
  21. hiddifypanel/templates/flaskadmin-layout.html +11 -9
  22. hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
  23. hiddifypanel/translations/en/LC_MESSAGES/messages.po +33 -0
  24. hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
  25. hiddifypanel/translations/fa/LC_MESSAGES/messages.po +33 -0
  26. hiddifypanel/translations/my/LC_MESSAGES/messages.mo +0 -0
  27. hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
  28. hiddifypanel/translations/pt/LC_MESSAGES/messages.po +33 -0
  29. hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
  30. hiddifypanel/translations/ru/LC_MESSAGES/messages.po +33 -0
  31. hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
  32. hiddifypanel/translations/zh/LC_MESSAGES/messages.po +33 -0
  33. hiddifypanel/translations.i18n/en.json +19 -0
  34. hiddifypanel/translations.i18n/fa.json +19 -0
  35. hiddifypanel/translations.i18n/pt.json +19 -0
  36. hiddifypanel/translations.i18n/ru.json +19 -0
  37. hiddifypanel/translations.i18n/zh.json +19 -0
  38. {hiddifypanel-10.85.0b20.dist-info → hiddifypanel-10.86.0b1.dist-info}/METADATA +1 -1
  39. {hiddifypanel-10.85.0b20.dist-info → hiddifypanel-10.86.0b1.dist-info}/RECORD +43 -43
  40. {hiddifypanel-10.85.0b20.dist-info → hiddifypanel-10.86.0b1.dist-info}/WHEEL +0 -0
  41. {hiddifypanel-10.85.0b20.dist-info → hiddifypanel-10.86.0b1.dist-info}/entry_points.txt +0 -0
  42. {hiddifypanel-10.85.0b20.dist-info → hiddifypanel-10.86.0b1.dist-info}/licenses/LICENSE.md +0 -0
  43. {hiddifypanel-10.85.0b20.dist-info → hiddifypanel-10.86.0b1.dist-info}/top_level.txt +0 -0
@@ -87,8 +87,7 @@ def configs_as_json(domains: list[Domain], user: User, expire_days: int, remarks
87
87
  if not all_configs:
88
88
  return ''
89
89
 
90
- json_configs = json.dumps(all_configs, indent=2,
91
- cls=hutils.proxy.ProxyJsonEncoder)
90
+ json_configs = json.dumps(all_configs, indent=2, cls=hutils.proxy.ProxyJsonEncoder)
92
91
  return json_configs
93
92
 
94
93
 
@@ -162,7 +161,7 @@ def add_vless_settings(base: dict, proxy: dict):
162
161
  'id': proxy['uuid'],
163
162
  'encryption': 'none',
164
163
  # 'security': 'auto',
165
- 'flow': 'xtls-rprx-vision' if (proxy['transport'] == ProxyTransport.XTLS or base['streamSettings']['security'] == 'reality') else '',
164
+ 'flow': proxy.get('flow',''),
166
165
  'level': OUTBOUND_LEVEL
167
166
  }
168
167
  ]
@@ -216,14 +215,17 @@ def add_shadowsocks_settings(base: dict, proxy: dict):
216
215
 
217
216
  # region stream settings
218
217
 
219
- def add_stream_settings(base: dict, proxy: dict):
220
- ss = base['streamSettings']
218
+ def _add_security(base_dict, proxy, tls_info=None):
219
+ if not tls_info:
220
+ tls_info = proxy
221
+
222
+ ss = base_dict
221
223
  ss['security'] = 'none' # default
222
224
 
223
225
  # security
224
- if proxy['l3'] == ProxyL3.reality:
226
+ if 'reality' in tls_info['mode']:
225
227
  ss['security'] = 'reality'
226
- elif proxy['l3'] in [ProxyL3.tls, ProxyL3.tls_h2, ProxyL3.tls_h2_h1, ProxyL3.h3_quic]:
228
+ elif proxy['l3'] in [ProxyL3.tls, ProxyL3.tls_h2, ProxyL3.tls_h2_h1, ProxyL3.h3_quic, ProxyL3.reality]:
227
229
  ss['security'] = 'tls'
228
230
 
229
231
  # network and transport settings
@@ -231,15 +233,14 @@ def add_stream_settings(base: dict, proxy: dict):
231
233
  # ss['security'] == 'tls' or 'xtls' -----> ss['security'] in ['tls','xtls']
232
234
  # TODO: FIX THE CONDITION AND TEST CONFIGS ON THE CLIENT SIDE
233
235
  if ss['security'] == 'reality':
234
- ss['network'] = proxy['transport']
235
- add_reality_stream(ss, proxy)
236
+ # ss['network'] = proxy['transport']
237
+ add_reality_stream(ss, proxy, tls_info)
236
238
  elif ss['security'] in ['tls', "xtls"] and proxy['proto'] != ProxyProto.ss:
237
-
238
239
  ss['tlsSettings'] = {
239
- 'serverName': proxy['sni'],
240
- 'allowInsecure': proxy['allow_insecure'],
240
+ 'serverName': tls_info['sni'],
241
+ 'allowInsecure': tls_info['allow_insecure'],
241
242
  'fingerprint': proxy['fingerprint'],
242
- 'alpn': [proxy['alpn']],
243
+ 'alpn': [tls_info['alpn']],
243
244
  # 'minVersion': '1.2',
244
245
  # 'disableSystemRoot': '',
245
246
  # 'enableSessionResumption': '',
@@ -250,6 +251,11 @@ def add_stream_settings(base: dict, proxy: dict):
250
251
  # 'rejectUnknownSni': '', # default is false
251
252
  }
252
253
 
254
+
255
+ def add_stream_settings(base: dict, proxy: dict):
256
+ ss = base['streamSettings']
257
+
258
+ _add_security(ss, proxy, proxy)
253
259
  if proxy['l3'] == ProxyL3.kcp:
254
260
  ss['network'] = 'kcp'
255
261
  add_kcp_stream(ss, proxy)
@@ -299,7 +305,7 @@ def add_tcp_stream(ss: dict, proxy: dict):
299
305
  "Connection": "keep-alive",
300
306
  "Pragma": "no-cache"
301
307
  },
302
-
308
+
303
309
  }
304
310
  }
305
311
  }
@@ -358,23 +364,37 @@ def add_httpupgrade_stream(ss: dict, proxy: dict):
358
364
 
359
365
 
360
366
  def add_xhttp_stream(ss: dict, proxy: dict):
361
- if ss['transport'] == "xhttp" and not hutils.flask.is_client_version(hutils.flask.ClientVersion.hiddify_next, 3, 0, 0):
367
+ if ss['transport'] == "xhttp" and g.user_agent.get(hutils.flask.ClientVersion.hiddify_next) and not hutils.flask.is_client_version(hutils.flask.ClientVersion.hiddify_next, 3, 0, 0):
362
368
  ss['transport'] = "splithttp"
363
369
  ss['splithttpSettings'] = {
364
370
  'path': proxy['path'],
365
371
  'host': proxy['host'],
366
- "headers": {
367
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
368
- }
372
+ "headers": proxy['params'].get('headers', {})
369
373
  }
370
374
  else:
371
- ss['xhttpSettings'] = {
372
- 'path': proxy['path'],
373
- 'host': proxy['host'],
374
- "headers": {
375
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
376
- }
375
+ _add_xhttp_details(ss, proxy)
376
+
377
+
378
+
379
+
380
+ def _add_xhttp_details(ss: dict, proxy: dict):
381
+ ss['network'] = "xhttp"
382
+ ss['xhttpSettings'] = {
383
+ 'path': proxy['path'],
384
+ 'host': proxy['host'],
385
+ 'mode':proxy['xhttp_mode'],
386
+ "extra": {
387
+ "headers": proxy['params'].get('headers', {})
377
388
  }
389
+ }
390
+ if proxy.get("download"):
391
+ ss['xhttpSettings']['extra']['downloadSettings'] = {
392
+ "address": proxy['download'].get("server"),
393
+ "port": proxy['port'],
394
+ }
395
+
396
+ _add_xhttp_details(ss['xhttpSettings']['extra']['downloadSettings'], proxy['download'])
397
+ _add_security(ss['xhttpSettings']['extra']['downloadSettings'], proxy, proxy['download'])
378
398
 
379
399
 
380
400
  def add_kcp_stream(ss: dict, proxy: dict):
@@ -409,12 +429,12 @@ def add_quic_stream(ss: dict, proxy: dict):
409
429
  }
410
430
 
411
431
 
412
- def add_reality_stream(ss: dict, proxy: dict):
432
+ def add_reality_stream(ss: dict, proxy: dict, domain_info: dict):
413
433
  ss['realitySettings'] = {
414
- 'serverName': proxy['sni'],
434
+ 'serverName': domain_info['sni'],
415
435
  'fingerprint': proxy['fingerprint'],
416
- 'shortId': proxy['reality_short_id'],
417
- 'publicKey': proxy['reality_pbk'],
436
+ 'shortId': domain_info['reality_short_id'],
437
+ 'publicKey': domain_info['reality_pbk'],
418
438
  'show': False,
419
439
  }
420
440
 
@@ -80,7 +80,7 @@ def hconfig(key: ConfigEnum, child_id: Optional[int] = None): # -> str | int |
80
80
  except BaseException:
81
81
  logger.exception(f'{key} error!')
82
82
  raise
83
- if value != None:
83
+ if value is not None:
84
84
  if key.type == int:
85
85
  return int(value)
86
86
  elif hasattr(key.type, 'from_str'):
@@ -128,6 +128,7 @@ class ConfigEnum(metaclass=FastEnum):
128
128
  reality_private_key = _StrConfigDscr(ConfigCategory.reality, ApplyMode.apply_config, hide_in_virtual_child=True)
129
129
  reality_public_key = _StrConfigDscr(ConfigCategory.reality, ApplyMode.apply_config, hide_in_virtual_child=True)
130
130
  reality_port = _StrConfigDscr(ConfigCategory.reality, ApplyMode.apply_config, hide_in_virtual_child=True)
131
+ special_port = _StrConfigDscr(ConfigCategory.reality, ApplyMode.apply_config, hide_in_virtual_child=True)
131
132
 
132
133
  restls1_2_domain = _StrConfigDscr(ConfigCategory.hidden)
133
134
  restls1_3_domain = _StrConfigDscr(ConfigCategory.hidden)
@@ -161,6 +162,8 @@ class ConfigEnum(metaclass=FastEnum):
161
162
  admin_lang = _TypedConfigDscr(Lang, ConfigCategory.admin)
162
163
  admin_secret = _StrConfigDscr(ConfigCategory.hidden) # removed
163
164
 
165
+ default_useragent_string = _StrConfigDscr(ConfigCategory.general)
166
+ use_ip_in_config=_BoolConfigDscr(ConfigCategory.general)
164
167
  # tls
165
168
  tls_ports = _StrConfigDscr(ConfigCategory.tls, ApplyMode.apply_config)
166
169
 
@@ -291,6 +294,7 @@ class ConfigEnum(metaclass=FastEnum):
291
294
  ssh_host_dsa_pub = _StrConfigDscr(ConfigCategory.hidden)
292
295
 
293
296
 
297
+
294
298
 
295
299
  hiddifycli_enable = _BoolConfigDscr(ConfigCategory.hidden, ApplyMode.reinstall)
296
300
 
@@ -8,7 +8,6 @@ from sqlalchemy.orm import backref
8
8
  from strenum import StrEnum
9
9
 
10
10
 
11
-
12
11
  from hiddifypanel.database import db
13
12
  from hiddifypanel.models.config import hconfig
14
13
  from .child import Child
@@ -21,11 +20,16 @@ class DomainType(StrEnum):
21
20
  cdn = auto()
22
21
  auto_cdn_ip = auto()
23
22
  relay = auto()
24
- reality = auto()
25
- old_xtls_direct = auto()
26
23
  worker = auto()
27
24
  fake = auto()
28
25
 
26
+ reality = auto() #deprecated
27
+ special_reality_tcp = auto()
28
+ special_reality_xhttp = auto()
29
+ special_reality_grpc = auto()
30
+ old_xtls_direct = auto() #deprecated
31
+ # special_shadowtls = auto()
32
+
29
33
  # fake_cdn = "fake_cdn"
30
34
  # telegram_faketls = "telegram_faketls"
31
35
  # ss_faketls = "ss_faketls"
@@ -37,6 +41,7 @@ ShowDomain = db.Table('show_domain',
37
41
  )
38
42
 
39
43
 
44
+
40
45
  class Domain(db.Model):
41
46
  id = db.Column(db.Integer, primary_key=True, autoincrement=True)
42
47
  child_id = db.Column(db.Integer, db.ForeignKey('child.id'), default=0)
@@ -54,6 +59,8 @@ class Domain(db.Model):
54
59
  secondaryjoin=id == ShowDomain.c.related_id,
55
60
  backref=backref('showed_by_domains', lazy='dynamic')
56
61
  )
62
+ download_domain_id= db.Column(db.Integer, db.ForeignKey('domain.id', ondelete='SET NULL'), default=None,nullable=True)
63
+ download_domain = db.relationship('Domain',remote_side=[id], foreign_keys=[download_domain_id])
57
64
  extra_params = db.Column(db.String(200), nullable=True, default='')
58
65
 
59
66
  def __repr__(self):
@@ -79,6 +86,7 @@ class Domain(db.Model):
79
86
  'cdn_ip': self.cdn_ip,
80
87
  'servernames': self.servernames,
81
88
  'grpc': self.grpc,
89
+ 'download_domain':self.download_domain.domain if self.download_domain else "",
82
90
  'show_domains': [dd.domain for dd in self.show_domains], # type: ignore
83
91
  }
84
92
  if dump_child_id:
@@ -86,7 +94,7 @@ class Domain(db.Model):
86
94
  if dump_ports:
87
95
  data["internal_port_hysteria2"] = self.internal_port_hysteria2
88
96
  data["internal_port_tuic"] = self.internal_port_tuic
89
- data["internal_port_reality"] = self.internal_port_reality
97
+ data["internal_port_special"] = self.internal_port_special
90
98
  data["need_valid_ssl"] = self.need_valid_ssl
91
99
 
92
100
  return data
@@ -100,6 +108,13 @@ class Domain(db.Model):
100
108
  from hiddifypanel.panel.commercial.restapi.v2.parent.schema import DomainSchema
101
109
  return DomainSchema().load(domain_dict)
102
110
 
111
+
112
+ def auto_cdn_ip(self):
113
+ from hiddifypanel import hutils
114
+ if self.cdn_ip:
115
+ return hutils.network.auto_ip_selector.get_clean_ip(self.cdn_ip)
116
+ return None
117
+
103
118
  @property
104
119
  def need_valid_ssl(self):
105
120
  return self.mode in [DomainType.direct, DomainType.cdn, DomainType.worker, DomainType.relay, DomainType.auto_cdn_ip, DomainType.old_xtls_direct, DomainType.sub_link_only]
@@ -124,11 +139,11 @@ class Domain(db.Model):
124
139
  return int(hconfig(ConfigEnum.tuic_port, self.child_id)) + self.port_index
125
140
 
126
141
  @property
127
- def internal_port_reality(self):
128
- if self.mode != DomainType.reality:
142
+ def internal_port_special(self):
143
+ if self.mode != DomainType.reality and "special" not in self.mode.value:
129
144
  return 0
130
145
  # TODO: check validity of the range of the port
131
- return int(hconfig(ConfigEnum.reality_port, self.child_id)) + self.port_index
146
+ return int(hconfig(ConfigEnum.special_port, self.child_id)) + self.port_index
132
147
 
133
148
  @classmethod
134
149
  def by_mode(cls, mode: DomainType) -> List['Domain']:
@@ -163,7 +178,7 @@ class Domain(db.Model):
163
178
  domains = []
164
179
  domains = db.session.query(Domain).filter(Domain.mode == DomainType.sub_link_only, Domain.child_id == Child.current().id).all()
165
180
  if not len(domains) or always_add_all_domains:
166
- domains = db.session.query(Domain).filter(Domain.mode.notin_([DomainType.fake, DomainType.reality])).all()
181
+ domains = db.session.query(Domain).filter(Domain.mode.notin_([DomainType.fake, DomainType.reality,DomainType.special_reality_tcp,DomainType.special_reality_xhttp,DomainType.special_reality_grpc])).all()
167
182
 
168
183
  if len(domains) == 0 and request:
169
184
  domains = [Domain(domain=request.host)] # type: ignore
@@ -188,6 +203,16 @@ class Domain(db.Model):
188
203
  dbdomain.servernames = domain.get('servernames', '')
189
204
  show_domains = domain.get('show_domains', [])
190
205
  dbdomain.show_domains = Domain.query.filter(Domain.domain.in_(show_domains)).all()
206
+ dl_domain=domain.get("download_domain")
207
+ if dl_domain:
208
+ dbdldomain = Domain.query.filter(Domain.domain == dl_domain).first()
209
+ if not dbdldomain:
210
+ dbdldomain = Domain(domain=dl_domain) # type: ignore
211
+ db.session.add(dbdldomain)
212
+ db.session.commit()
213
+ dbdldomain=Domain.query.filter(Domain.domain == dl_domain).first()
214
+ assert dbdldomain
215
+ dbdomain.download_domain_id=dbdldomain.id
191
216
  if commit:
192
217
  db.session.commit()
193
218
 
@@ -5,12 +5,13 @@ from sqlalchemy import Column, String, Integer, Boolean, Enum, ForeignKey
5
5
 
6
6
  from hiddifypanel.database import db
7
7
 
8
+ from sqlalchemy.types import JSON
8
9
 
9
10
 
10
11
  class ProxyTransport(StrEnum):
11
12
  h2 = auto()
12
13
  grpc = auto()
13
- XTLS = auto()
14
+ # XTLS = auto()
14
15
  faketls = auto()
15
16
  shadowtls = auto()
16
17
  restls1_2 = auto()
@@ -68,6 +69,7 @@ class Proxy(db.Model): # type: ignore
68
69
  l3 = Column(Enum(ProxyL3), nullable=False)
69
70
  transport = Column(Enum(ProxyTransport), nullable=False)
70
71
  cdn = Column(Enum(ProxyCDN), nullable=False)
72
+ params = Column(JSON,default={})
71
73
 
72
74
  @property
73
75
  def enabled(self):
@@ -81,7 +83,8 @@ class Proxy(db.Model): # type: ignore
81
83
  'l3': self.l3,
82
84
  'transport': self.transport,
83
85
  'cdn': self.cdn,
84
- 'child_unique_id': self.child.unique_id if self.child else ''
86
+ 'child_unique_id': self.child.unique_id if self.child else '',
87
+ 'params': self.params
85
88
  }
86
89
 
87
90
  def __str__(self):
@@ -101,6 +104,7 @@ class Proxy(db.Model): # type: ignore
101
104
  dbproxy.transport = proxy['transport']
102
105
  dbproxy.cdn = proxy['cdn']
103
106
  dbproxy.l3 = proxy['l3']
107
+ dbproxy.params=proxy['params']
104
108
  dbproxy.child_id = child_id
105
109
  if commit:
106
110
  db.session.commit() # type: ignore
@@ -46,39 +46,31 @@ class DomainAdmin(AdminLTEModelView):
46
46
  domain=_("domain.description"),
47
47
  mode=_("Direct mode means you want to use your server directly (for usual use), CDN means that you use your server on behind of a CDN provider."),
48
48
  cdn_ip=_("config.cdn_forced_host.description"),
49
- show_domains=_(
50
- 'domain.show_domains_description'),
49
+ show_domains=_('domain.show_domains_description'),
51
50
  alias=_('The name shown in the configs for this domain.'),
52
51
  servernames=_('config.reality_server_names.description'),
53
52
  sub_link_only=_('This can be used for giving your users a permanent non blockable links.'),
54
- grpc=_('grpc-proxy.description'))
55
-
53
+ grpc=_('grpc-proxy.description'),
54
+ download_domain=_('download_domain.description')
55
+ )
56
56
  # create_modal = True
57
57
  can_export = False
58
- form_widget_args = {'show_domains': {'class': 'form-control ltr'}}
58
+ form_widget_args = {'show_domains': {'class': 'form-control ltr'},'download_domain': {'class': 'form-control ltr'}}
59
+
59
60
  form_args = {
60
- 'mode': {
61
- 'enum': DomainType},
61
+ 'mode': {'enum': DomainType},
62
62
  'show_domains': {
63
- 'query_factory': lambda: Domain.query.filter(
64
- Domain.sub_link_only == False),
63
+ 'query_factory': lambda: Domain.query.filter( Domain.sub_link_only == False),
65
64
  },
66
65
  'domain': {
67
66
  'validators': [
68
- Regexp(
69
- r'^(\*\.)?([A-Za-z0-9\-\.]+\.[a-zA-Z]{2,})$|^$',
70
- message=__("Should be a valid domain"))]},
67
+ Regexp(r'^(\*\.)?([A-Za-z0-9\-\.]+\.[a-zA-Z]{2,})$|^$',message=__("Should be a valid domain"))]},
71
68
  "cdn_ip": {
72
69
  'validators': [
73
- Regexp(
74
- r"(((((25[0-5]|(2[0-4]|1\d|[1-9]|)\d).){3}(25[0-5]|(2[0-4]|1\d|[1-9]|)\d))|^([A-Za-z0-9\-\.]+\.[a-zA-Z]{2,}))[ \t\n,;]*\w{3}[ \t\n,;]*)*",
75
- message=__("Invalid IP or domain"))]},
70
+ Regexp(r"(((((25[0-5]|(2[0-4]|1\d|[1-9]|)\d).){3}(25[0-5]|(2[0-4]|1\d|[1-9]|)\d))|^([A-Za-z0-9\-\.]+\.[a-zA-Z]{2,}))[ \t\n,;]*\w{3}[ \t\n,;]*)*",message=__("Invalid IP or domain"))]},
76
71
  "servernames": {
77
72
  'validators': [
78
- Regexp(
79
- r"^([\w-]+\.)+[\w-]+(,\s*([\w-]+\.)+[\w-]+)*$",
80
- re.IGNORECASE,
81
- _("Invalid REALITY hostnames"))]}}
73
+ Regexp(r"^([\w-]+\.)+[\w-]+(,\s*([\w-]+\.)+[\w-]+)*$",re.IGNORECASE,_("Invalid REALITY hostnames"))]}}
82
74
  column_list = ["domain", "alias", "mode", "domain_ip", "show_domains"]
83
75
  column_editable_list = ["alias"]
84
76
  # column_filters=["domain","mode"]
@@ -93,10 +85,11 @@ class DomainAdmin(AdminLTEModelView):
93
85
  'servernames': _('config.reality_server_names.label'),
94
86
  'show_domains': _('Show Domains'),
95
87
  'alias': _('Alias'),
96
- 'grpc': _('gRPC')
88
+ 'grpc': _('gRPC'),
89
+ "download_domain":_('Download Domain')
97
90
  }
98
91
 
99
- form_columns = ['mode', 'domain', 'alias', 'servernames', 'grpc', 'cdn_ip', 'show_domains']
92
+ form_columns = ['mode', 'domain','download_domain', 'alias', 'servernames', 'cdn_ip', 'show_domains']
100
93
 
101
94
  def _domain_admin_link(view, context, model, name):
102
95
  if model.mode == DomainType.fake:
@@ -155,7 +148,9 @@ class DomainAdmin(AdminLTEModelView):
155
148
  def on_model_change(self, form, model, is_created):
156
149
  # Sanitize domain input
157
150
  model.domain = (model.domain or '').lower().strip()
158
-
151
+ if model.domain==model.download_domain.domain:
152
+ model.download_domain_id=None
153
+ model.download_domain=None
159
154
  # Basic validation
160
155
  if model.domain == '' and model.mode != DomainType.fake:
161
156
  raise ValidationError(_("domain.empty.allowed_for_fake_only"))
@@ -199,7 +194,7 @@ class DomainAdmin(AdminLTEModelView):
199
194
  if model.mode == DomainType.old_xtls_direct and not hconfig(ConfigEnum.xtls_enable):
200
195
  set_hconfig(ConfigEnum.xtls_enable, True)
201
196
  hutils.proxy.get_proxies().invalidate_all()
202
- elif model.mode == DomainType.reality:
197
+ elif "reality" in model.mode:
203
198
  self._validate_reality_settings(model, server_ips)
204
199
 
205
200
  # Signal config update if needed
@@ -267,7 +262,7 @@ class DomainAdmin(AdminLTEModelView):
267
262
  if model.domain == configs[c]:
268
263
  raise ValidationError(_("You have used this domain in: ") + _(f"config.{c}.label"))
269
264
 
270
- for td in Domain.query.filter(Domain.mode == DomainType.reality, Domain.domain != model.domain).all():
265
+ for td in Domain.query.filter(Domain.mode._in([DomainType.reality,DomainType.special_reality_xhttp,DomainType.special_reality_grpc,DomainType.special_reality_tcp]) == DomainType.reality, Domain.domain != model.domain).all():
271
266
  # print(td)
272
267
  if td.servernames and (model.domain in td.servernames.split(",")):
273
268
  raise ValidationError(_("You have used this domain in: ") + _(f"config.reality_server_names.label") + td.domain)
@@ -283,6 +278,8 @@ class DomainAdmin(AdminLTEModelView):
283
278
  return True
284
279
  if model.mode in [DomainType.fake, DomainType.reality, DomainType.relay]:
285
280
  return True
281
+ if "special" in model.mode:
282
+ return True
286
283
  # Resolve domain IPs with timeout
287
284
  try:
288
285
  dips = hutils.network.get_domain_ips(model.domain)
@@ -319,7 +316,7 @@ class DomainAdmin(AdminLTEModelView):
319
316
  def on_model_delete(self, model):
320
317
  if len(Domain.query.all()) <= 1:
321
318
  raise ValidationError(f"at least one domain should exist")
322
- if hconfig(ConfigEnum.cloudflare) and model.mode not in [DomainType.fake, DomainType.reality, DomainType.relay]:
319
+ if hconfig(ConfigEnum.cloudflare) and model.mode not in [DomainType.fake, DomainType.reality, DomainType.relay] and "special" not in model.mode:
323
320
  if not hutils.network.cf_api.delete_dns_record(model.domain):
324
321
  hutils.flask.flash(_('cf-delete.failed'), 'warning') # type: ignore
325
322
  model.showed_by_domains = []
@@ -8,11 +8,13 @@ from hiddifypanel.auth import login_required
8
8
  from flask_admin.actions import action
9
9
  from flask_admin.contrib.sqla import form, filters as sqla_filters, tools
10
10
  from flask_admin import expose
11
-
11
+ from hiddifypanel.panel import custom_widgets
12
12
 
13
13
  # Define a custom field type for the related domains
14
14
  from hiddifypanel import hutils
15
15
 
16
+ from wtforms.widgets import TextArea
17
+ import json
16
18
 
17
19
  class ProxyDetailsAdmin(AdminLTEModelView):
18
20
  list_template = 'model/proxydetail_list.html'
@@ -22,7 +24,12 @@ class ProxyDetailsAdmin(AdminLTEModelView):
22
24
  column_exclude_list = ['child']
23
25
  column_searchable_list = ['name', 'proto', 'transport', 'l3', 'cdn']
24
26
  column_editable_list = ['name']
25
-
27
+ form_extra_fields = {
28
+ 'params': custom_widgets.JSONField(label='Proxy Params')
29
+ }
30
+ form_overrides={
31
+ 'params': custom_widgets.JSONField
32
+ }
26
33
  @expose('reset_proxies')
27
34
  def reset_proxies(self):
28
35
  from hiddifypanel.panel.init_db import get_proxy_rows_v1
@@ -70,7 +77,8 @@ class ProxyDetailsAdmin(AdminLTEModelView):
70
77
  if login_required(roles={Role.super_admin, Role.admin})(lambda: True)() != True:
71
78
  return False
72
79
  return True
73
-
80
+ def _params_formatter(view, context, model, name):
81
+ return str(model.params)
74
82
  def _enable_formatter(view, context, model, name):
75
83
  if model.enable:
76
84
  link = '<i class="fa-solid fa-circle-check text-success"></i> '
@@ -79,5 +87,6 @@ class ProxyDetailsAdmin(AdminLTEModelView):
79
87
  return Markup(link)
80
88
  column_formatters = {
81
89
 
82
- "enable": _enable_formatter
90
+ "enable": _enable_formatter,
91
+ "params": _params_formatter
83
92
  }
@@ -130,7 +130,7 @@ class AllConfigsAPI(MethodView):
130
130
  items.append(
131
131
  create_item(
132
132
  pinfo["name"].replace("_", " "),
133
- f"{'Auto ' if pinfo['dbdomain'].has_auto_ip else ''}{pinfo['mode']}",
133
+ f"{pinfo['mode']}",
134
134
  pinfo['server'],
135
135
  pinfo['proto'],
136
136
  pinfo['transport'],
@@ -6,6 +6,11 @@ from wtforms import TextAreaField
6
6
  from wtforms.fields import IntegerField, SelectField, DecimalField
7
7
  from wtforms.widgets import TextArea
8
8
 
9
+ from wtforms import Field
10
+ from wtforms.validators import ValidationError
11
+ from wtforms.widgets import TextArea
12
+ import json
13
+
9
14
  from hiddifypanel.models import *
10
15
 
11
16
 
@@ -89,3 +94,43 @@ class UsageField(DecimalField):
89
94
  self.data = int(float(valuelist[0]) * ONE_GIG)
90
95
  else:
91
96
  self.data = None
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+ class JSONWidget(TextArea):
105
+ def __call__(self, field, **kwargs):
106
+ if isinstance(field.data, dict):
107
+ try:
108
+ field.data = json.dumps(field.data, indent=2)
109
+ except Exception:
110
+ pass
111
+ if kwargs.get('class'):
112
+ kwargs['class'] += ' ltr json-editor'
113
+ else:
114
+ kwargs.setdefault('class', 'ltr json-editor')
115
+
116
+ return super().__call__(field, **kwargs)
117
+
118
+ class JSONField(Field):
119
+ widget = JSONWidget()
120
+
121
+ def _value(self):
122
+ if self.data is None:
123
+ return ''
124
+ if isinstance(self.data, str):
125
+ return self.data
126
+ try:
127
+ return json.dumps(self.data, indent=2)
128
+ except Exception:
129
+ return str(self.data)
130
+
131
+ def process_formdata(self, valuelist):
132
+ if valuelist:
133
+ try:
134
+ self.data = json.loads(valuelist[0])
135
+ except Exception as e:
136
+ raise ValidationError(f'Invalid JSON: {e}')