Files
3x-ui/frontend/src/models/inbound.ts
T
Sanaei dc37f9b731 Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563)
* refactor(frontend): port api/* and reality-targets to TypeScript

Phase 1 of the JS→TS migration: convert three small, isolated files
(axios-init, websocket, reality-targets) to typed sources so future
phases can lean on their interfaces.

- api/axios-init.ts: typed CSRF cache, interceptors, request retry
- api/websocket.ts: typed listener map, message envelope guard,
  reconnect timer
- models/reality-targets.ts: RealityTarget interface, readonly list
- env.d.ts: minimal qs module shim (stringify/parse)
- consumers: drop ".js" extension from @/api imports

* refactor(frontend): port utils/index to TypeScript

Phase 2 of the JS→TS migration: convert the 858-line utility module
that 30+ pages and hooks depend on.

- Msg<T = any> generic with success/msg/obj shape preserved
- HttpUtil get/post/postWithModal generic over response shape
- RandomUtil, Wireguard, Base64 fully typed
- SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed
- ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union
- LanguageManager.supportedLanguages readonly typed
- IntlUtil.formatDate/formatRelativeTime accept null/undefined
- ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped
  to preserve the prior JS contract used by class-instance callers
  (AllSetting.cloneProps(this, data), etc.)

* refactor(frontend): port models/outbound to TypeScript (hybrid typing)

Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and
make it compile under strict mode with a minimal hybrid type pass.

- Enum-like constants kept as typed objects (Protocols, SSMethods, …)
- Top-level DNS helpers strictly typed
- CommonClass gets [key: string]: any so all subclasses can keep their
  loose this.foo = bar assignments without per-field declarations
- Constructor / fromJson / toJson signatures typed as any to preserve
  the prior JS contract used by consumers and parsers
- Outbound declares static fields for the dynamically-attached Settings
  subclasses (Settings, FreedomSettings, VmessSettings, …)
- urlParams.get() results that feed parseInt now use the non-null
  assertion since the surrounding has() check already guards them
- File-level eslint-disable for no-explicit-any/no-var/prefer-const to
  keep the JS-derived code building without churn

* refactor(frontend): port models/inbound to TypeScript (hybrid typing)

Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts:
constants typed strictly, classes get [key: string]: any from
XrayCommonClass, constructor / fromJson / toJson signatures use any.

- XrayCommonClass gains [key: string]: any plus typed static helpers
  (toJsonArray, fallbackToJson, toHeaders, toV2Headers)
- TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound
  declare static fields for their dynamically-attached subclasses
  (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/
  Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings)
- All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask*
  and related helpers explicitly any-typed
- Constructor positional client-args (email, limitIp, totalGB, …) typed
  as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS|
  VLESS|Trojan|Shadowsocks|Hysteria
- File-level eslint-disable for no-explicit-any/prefer-const/
  no-case-declarations/no-array-constructor to silence churn without
  changing behavior

* refactor(frontend): port models/dbinbound to TypeScript

Phase 6 — final phase of the JS→TS migration. Frontend src/ no
longer contains any *.js files.

- DBInbound declares all fields explicitly (id, userId, up, down,
  total, …, nodeId, fallbackParent) with proper types
- _expiryTime getter/setter typed against dayjs.Dayjs
- coerceInboundJsonField takes unknown, returns any
- Private cache fields (_cachedInbound, _clientStatsMap) declared
- Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js"
  extension from @/models/dbinbound imports

* refactor(frontend): drop .js extensions from TS-resolved imports

Cleanup after the JS→TS migration:

- All consumers that imported @/models/{inbound,outbound,dbinbound}.js
  now drop the .js extension (TS module resolution lands on the .ts
  file automatically)
- eslint.config.js: remove the **/*.js block since the only remaining
  JS file under src/ is endpoints.js (build-script consumed only) and
  js.configs.recommended already covers it correctly

* refactor(frontend): tighten inbound.ts cleanup wins

Checkpoint before the full any → typed pass:
- Wrap 15 case bodies in braces (no-case-declarations)
- Convert 14 let → const in genLink helpers (prefer-const)
- new Array() → [] for shadowsocks passwords (no-array-constructor)
- XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces;
  fromJson/toV2Headers/toHeaders typed against them; static methods
  return JsonObject / HeaderEntry[] instead of any
- Reduce file-level eslint-disable scope from 4 rules to just
  no-explicit-any (the only one still needed)

* refactor(frontend): drop eslint-disable from models/dbinbound

Replace `any` with explicit domain types:
- `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects).
- Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types.
- `_cachedInbound: Inbound | null`, `toInbound(): Inbound`.
- `getClientStats(email): ClientStats | undefined`.
- `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks).
- Constructor now accepts `DBInboundInit`.

* refactor(frontend): drop eslint-disable from InboundsPage

Type all callbacks against DBInbound from @/models/dbinbound:
- state setters use DBInbound | null
- helpers (projectChildThroughMaster, checkFallback, findClientIndex,
  exportInboundLinks, etc.) take DBInbound
- drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[]
- introduce ClientMatchTarget for findClientIndex's `client` param
- tighten DBInbound.clientStats to ClientStats[] (default [])
- single boundary cast at <InboundList onRowAction=> to bridge
  InboundList's narrower DBInboundRecord (cleanup belongs with InboundList)

* refactor(frontend): drop file-level eslint-disable from utils/index

- ObjectUtil.clone/deepClone become generic <T>
- cloneProps/delProps accept `object` (cast internally to AnyRecord)
- equals accepts `unknown` with proper narrowing
- ColorUtils.usageColor narrows data/threshold to `number`; total widened
  to `number | { valueOf(): number } | null | undefined` so Dayjs works
- Utils.debounce replaces `const self = this` with lexical arrow
  closure (no-this-alias clean)
- InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null`
- Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil
  generic defaults (idiomatic API envelope; changing default to unknown
  cascades through 34 consumer files)

* refactor(frontend): drop eslint-disable from OutboundFormModal field section

Replace `type OB = any` with `type OB = Outbound`. Body code still
sees protocol fields as `any` via Outbound's inherited [key: string]: any
index signature (CommonClass) — that escape hatch will narrow as
Phase 6 tightens outbound.ts itself.

The intentional `// eslint-disable-next-line` on `useRef<any>(null)`
at line 72 stays — out of scope per plan.

* refactor(frontend): drop file-level eslint-disable from InboundFormModal

Add minimal local interfaces for protocol-specific shapes the form reads:
- StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount,
  WireguardPeer (replace with real exports from inbound.ts as Phase 7
  exports them).
- Props typed as DBInbound | null + DBInbound[].
- Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`,
  `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are
  already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings`
  remain `any` via static field on Inbound (will tighten in Phase 7).
- inboundRef/dbFormRef retain single-line `// eslint-disable-next-line`
  for `useRef<any>(null)` — nullable narrowing across ~30 callsites
  exceeds Phase 5 scope.
- payload locals typed Record<string, unknown>; setAdvancedAllValue
  parses JSON into a narrowed object instead of `let parsed: any`.

* refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only

- Fix all 36 prefer-const violations: convert never-reassigned `let` to
  `const`; for mixed-mutability destructuring (fromParamLink,
  fromHysteriaLink) split into separate `const`/`let` declarations
  by index instead of destructuring.
- Fix both no-var violations: `var stream` / `var settings` → `let`.
- File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */`
  because tightening 223 `any` uses requires removing CommonClass's
  `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached
  subclass patterns into named classes — multi-hour architectural work
  tracked as Phase 7's twin for outbound.

* refactor(frontend): align sub page chrome with login + AntD defaults

- Theme + language buttons now both use AntD `<Button shape="circle"
  size="large" className="toolbar-btn">` with TranslationOutlined and
  the SVG theme icon — identical hover/border behaviour.
- Language popover content switched from hand-rolled `<ul.lang-list>`
  to AntD `<Menu mode="vertical" selectable />`; gains native
  hover/keyboard nav + active highlight.
- Drop `.info-table` `!important` border overrides (8 selectors) so
  Descriptions inherits the AntD theme border colour.
- Drop `.qr-code` padding/background/border-radius overrides; only
  `cursor: pointer` remains (QRCode handles padding/bg itself).
- Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`,
  `.lang-select`, `.settings-popover` rules.

* refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens

- Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>)
  and its unscoped global `.ant-statistic-*` CSS overrides; consumers
  (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD
  `<Statistic>` directly.
- Add Statistic component tokens to ConfigProvider so the title (11px)
  and content (17px) font sizes still apply, without `!important`
  global selectors.
- Move dark / ultra-dark card border colours from `body.dark .ant-card`
  + `html[data-theme='ultra-dark'] .ant-card` selectors into Card
  `colorBorderSecondary` tokens; page-cards.css now only carries the
  custom radius/shadow/transition that has no token equivalent.
- Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot
  keyframe and per-state ring-colour overrides; AntD `<Badge
  status="processing" color={…}>` already pulses the ring in the same
  colour, no extra CSS needed.

* refactor(frontend): modernize login page with AntD primitives

- Theme cycle button switched from `<button.theme-cycle>` + custom CSS
  to AntD `<Button shape="circle" className="toolbar-btn">` (matches
  sub page chrome already established).
- Theme icons switched from hand-rolled inline SVG (sun, moon,
  moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`,
  `<MoonFilled />` for the three light / dark / ultra-dark states.
- Language popover content switched from `<ul.lang-list>` +
  `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />`
  with `selectedKeys=[lang]`; native hover / keyboard nav / active
  highlight come for free.
- Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused).
  `.toolbar-btn` retained since it sizes both circular buttons.

* refactor(frontend): switch sub page theme icons to AntD primitives

Replace the three hand-rolled SVG theme icons (sun, moon, moon+star)
with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />`
for the light / dark / ultra-dark states. Switch the theme `<Button>`
to use the `icon` prop instead of children so it renders the same
way as the language button. Drop `.toolbar-btn svg` CSS — no longer
needed once the icon comes from AntD.

* refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs)

- ClientsPage: pagination size-changer `min-width !important` removed;
  the 3-level selector specificity already beats AntD's defaults.
  Scope `body.dark .client-card` to `.clients-page.is-dark .client-card`
  (avoid leaking into other pages).
- LogModal + XrayLogModal: move the mobile full-screen tweaks
  (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important`
  class rules to the Modal's `style` prop; keep `.ant-modal-content`
  / `.ant-modal-body` overrides as plain CSS via the className.
- SubscriptionFormatsTab: drop `display: block !important` on
  `.nested-block` — div is already block by default.
- TwoFactorModal: drop `padding/background/border-radius !important`
  on `.qr-code`; AntD QRCode handles those itself.

* refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables

Scope page-level dark overrides:
- inbounds/InboundList: scope `.ant-table` border-radius rules and the
  mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global
  and leaked into other pages); scope `.inbound-card` dark variant to
  `.inbounds-page.is-dark`.
- nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`.
- xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`,
  `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`.

Modernize list borders to use AntD CSS vars instead of body.dark forks:
- index/BackupModal, PanelUpdateModal, VersionModal: replace
  hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]`
  override pairs with `var(--ant-color-border-secondary)`; replace
  custom text colours with `var(--ant-color-text)` /
  `var(--ant-color-text-tertiary)`.
- xray/DnsPresetsModal: same border-color treatment.
- xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark`
  pair into a single neutral `rgba(128,128,128,0.06)` that works on
  both themes; scope under `.nord-data-table` / `.warp-data-table`.

* refactor(frontend): switch shared components CSS to AntD CSS variables

Replace body.dark / html[data-theme] forks with AntD CSS variables
in shared components (work in both light and dark, scale to ultra):
- SettingListItem: borders + text colours via
  `--ant-color-border-secondary`, `--ant-color-text`,
  `--ant-color-text-tertiary`.
- InputAddon: bg/border/text via `--ant-color-fill-tertiary`,
  `--ant-color-border`, `--ant-color-text`.
- JsonEditor: host border/bg via `--ant-color-border`,
  `--ant-color-bg-container`; focus border via `--ant-color-primary`.
- Sparkline (SVG): grid/text colours via `--ant-color-text*`
  and `--ant-color-border-secondary`; only the tooltip drop-shadow
  retains a body.dark fork (filter opacity needs explicit value).

* refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart

Replace the 368-line hand-rolled SVG sparkline (with manual
ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip,
custom Y-axis label thinning) with a thin Recharts `<AreaChart>`
wrapper that keeps the same prop API.

- Preserved props: data, labels, height, stroke, strokeWidth,
  maxPoints, showGrid, fillOpacity, showMarker, markerRadius,
  showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax,
  yFormatter, tooltipFormatter.
- Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` —
  Recharts' ResponsiveContainer handles width, and margins are wired
  to whether axes are visible. Removed the unused `vbWidth` prop from
  SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites.
- Tooltip, grid, and axis text now use AntD CSS variables for
  automatic light/dark adaptation; replaced the SVG body.dark forks
  in Sparkline.css with a single 5-line stylesheet.
- Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off
  for less custom chart code to maintain and a more standard API
  for future charts (multi-series, brush, etc.).

* build(frontend): split Recharts + d3 deps into vendor-recharts chunk

Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale
+ victory-vendor deps out of the catch-all vendor chunk so they
load on demand on the three pages that use Sparkline
(SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache
independently from the rest of the panel JS.

* refactor(frontend): drop body.dark forks in favor of AntD CSS variables

- ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use
  var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing
  the body.dark light/dark background pair.
- InboundFormModal: advanced-panel uses --ant-color-border-secondary and
  --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone.
- CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover
  use --ant-color-fill-tertiary/-secondary; body.dark forks gone.
- SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients
  into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary.
- page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to
  page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but
  consistent with the page-scoping convention used elsewhere.

* refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons

- Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text)
  and var(--ant-color-text-secondary) so light/dark adapt automatically.
- Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary)
  and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary).
- Drop all body.dark/html[data-theme='ultra-dark'] color forks for
  .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle,
  .sidebar-donate (CSS variables already adapt).
- Drop the body.dark Drawer background !important pair; AntD's
  colorBgElevated token from the dark algorithm handles it now.
- Replace inline sun/moon SVGs in ThemeCycleButton with AntD's
  SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage.
- Convert .sidebar-theme-cycle hover and the menu item selected/hover
  highlights from hardcoded #4096ff to color-mix on --ant-color-primary,
  keeping !important on menu rules to beat AntD's CSS-in-JS specificity.

* refactor(frontend): swap hardcoded AntD palette colors for CSS variables

The dot/badge/pill styles still hardcoded AntD's default palette values
(#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its
semantic --ant-color-* equivalent so they auto-adapt to any theme
customization through ConfigProvider.

- ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now
  use --ant-color-success / -primary / -error / -warning / -text-quaternary.
  .bulk-count / .client-card / .client-card.is-selected backgrounds use
  color-mix on --ant-color-primary and --ant-color-fill-quaternary, which
  also let the body-dark .client-card fork go away.
- XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now
  build their box-shadow tint via color-mix on --ant-color-success and
  --ant-color-error instead of rgba literals.
- IndexPage: .action-update warning color uses --ant-color-warning.
- OutboundsTab: .outbound-card border, .address-pill background, and
  .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark
  .address-pill fork is gone.
- InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale
  `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and
  switch .danger-icon to --ant-color-error.

The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic
and pill rows are intentionally kept hardcoded — they are brand-specific
shades, not AntD palette colors.

* refactor(frontend): swap neutral gray rgba literals for AntD CSS variables

Across 12 files the same neutral grays kept reappearing — rgba(128,128,128,
0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle
backgrounds. Each maps cleanly to an AntD CSS variable that already
adapts to light/dark and to any theme customization through ConfigProvider:

- 0.12–0.18 borders → var(--ant-color-border-secondary)
- 0.2–0.25 borders → var(--ant-color-border)
- 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary)
- 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary)

Card surfaces (InboundList .inbound-card, NodeList .node-card) had a
light/dark fork pair — the variable covers both, so the .is-dark .card
override is gone.

RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the
inset focus shadow; replaced with var(--ant-color-primary) so reordering
indicators follow the theme primary.

ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16,
#52c41a, rgba gray) for a Badge color prop. Switched to status="error"|
"warning"|"success"|"default" so the dot color now comes from AntD's
semantic palette directly.

* refactor(xray): collapse RoutingTab dark forks into AntD CSS variables

- .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary)
- .xray-page.is-dark .rule-card and .criterion-chip overrides removed;
  the rules already use --bg-card and --ant-color-fill-tertiary that
  adapt to the theme on their own.

* refactor(frontend): inline style hex literals and Alert icon redundancy

- FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline;
  swap for var(--ant-color-error) so they follow theme customization.
- NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes
  switch to var(--ant-color-success) / -error.
- NodeList: ExclamationCircleOutlined warning icons (two callsites) now
  use var(--ant-color-warning).
- BasicsTab: four <Alert type="warning"> blocks shipped a custom
  ExclamationCircleFilled icon styled to match the warning palette —
  exactly the icon and color AntD Alert renders for type="warning" by
  default. Replace the icon prop with showIcon and drop the now-unused
  ExclamationCircleFilled import.
- JsonEditor: focus-within box-shadow tint now uses color-mix on
  --ant-color-primary instead of an rgba(22,119,255,0.1) literal.

* refactor(logs): collapse log-container dark forks to AntD CSS variables

LogModal and XrayLogModal each had a body.dark fork that overrode the
log container's background, border-color, and text color in addition
to the --log-* severity tokens. Background/border/color all map cleanly
to var(--ant-color-fill-tertiary) / var(--ant-color-border) /
var(--ant-color-text) which already adapt to the theme, so only the
severity color tokens remain inside the dark/ultra-dark blocks.

* refactor(xray): drop stale --ant-primary-color fallbacks and hex literals

- RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary)
- OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff
  pair (the old AntD v4 token name with stale fallback) for the v6
  --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error).
- XrayPage .restart-icon: same drop of the --ant-primary-color fallback.

These were all leftovers from the AntD v4 → v6 rename — the v6
--ant-color-primary is already populated by ConfigProvider, so the
fallback hex was dead code that would only trigger if AntD wasn't
mounted.

* refactor(frontend): consolidate margin utility classes into one stylesheet

Page CSS files each carried their own copies of the same atomic margin
utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions
were identical everywhere they appeared, with each file holding only
the subset it happened to need.

Move all of them into a single styles/utils.css imported once from
main.tsx, and delete the per-page copies from InboundFormModal,
CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal,
OutboundFormModal, and WarpModal. The classes are available globally
on the panel app; login.tsx and subpage.tsx entries do not consume any
of them so they stay untouched.

* refactor(frontend): consolidate shared page-shell rules into one stylesheet

Every panel page CSS file repeated the same wrapper boilerplate — the
--bg-page/--bg-card token triples for light/dark/ultra-dark, the
min-height + background root rule, the .ant-layout transparent reset,
the .content-shell transparent reset, and the .loading-spacer min-height.
That's ~30 identical lines duplicated across IndexPage, ClientsPage,
InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage.

Move all of it into styles/page-shell.css and import it once from
main.tsx alongside utils.css and page-cards.css. Each page CSS file
now only contains genuinely page-specific rules (content-area padding
overrides, page-specific tokens like ApiDocs's Swagger --sw-* set).

Also drop the per-page `import '@/styles/page-cards.css'` statements
from the 7 page tsx files now that main.tsx loads it globally.

Net: -211 deleted, +6 inserted in the touched files, plus the new
page-shell.css. .zero-margin (Divider override used by Nord/Warp
modals) folded into utils.css alongside the margin classes.

* refactor(frontend): move default content-area padding to page-shell.css

After page-shell.css landed, six of the seven panel pages still kept an
identical `.X-page .content-area { padding: 24px }` desktop rule, plus
three of them kept an identical `padding: 8px` mobile rule. Hoist both
defaults into page-shell.css under a single 6-page selector group and
delete the per-page copies.

What stays page-specific:
- IndexPage keeps its mobile override (padding 12px + padding-top: 64px
  for the fixed drawer handle clearance).
- ApiDocsPage keeps its tighter desktop padding (16px) and its own
  mobile padding-top: 56px.

Settings .ldap-no-inbounds also switches from #999 to
var(--ant-color-text-tertiary) for theme adaptation.

* refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css

Settings and Xray pages both carried identical .header-row /
.header-actions / .header-info rules and an identical six-rule
.icons-only block that styles tabbed page navigation. Clients, Inbounds,
and Nodes all carried identical .summary-card padding rules with the
same mobile reduction. None of these are page-specific.

Consolidate:
- .header-row family → page-shell scoped to .settings-page, .xray-page
- .icons-only family → page-shell global (the class is a deliberate
  opt-in marker, no scope needed)
- .summary-card → page-shell scoped to .clients-page, .inbounds-page,
  .nodes-page (also fixes InboundsPage's missing scope — its rule was
  global and would have matched stray .summary-card uses elsewhere)

InboundsPage.css and NodesPage.css became empty after the move so the
files and their per-page imports are deleted.

* refactor(frontend): hoist .random-icon to utils.css

Three form modals each carried identical .random-icon styles (small
primary-tinted icon next to randomizable inputs):
  ClientBulkAddModal, InboundFormModal, OutboundFormModal

Single definition lives in utils.css now. ClientBulkAddModal.css was
just this one rule, so the file and its import are deleted along the way.

.danger-icon is left per file — the margin-left differs slightly
between InboundFormModal (6px) and OutboundFormModal (8px), so it
stays as a page-local rule rather than getting averaged into utils.css.

* refactor(frontend): hoist .danger-icon to utils.css and use it everywhere

InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left
8px) each carried their own .danger-icon, and FinalMaskForm wrote the
same color/cursor/marginLeft trio inline five times. Unify on a single
.danger-icon in utils.css with margin-left: 8px — matching the more
generous OutboundFormModal value — and:
- Drop the per-file .danger-icon copies from InboundFormModal.css and
  OutboundFormModal.css.
- Replace the five inline style props in FinalMaskForm.tsx with
  className="danger-icon".

The visible change is a 2px wider gap to the right of the delete icons
on InboundFormModal's protocol/peer dividers.
2026-05-25 14:34:53 +02:00

3360 lines
111 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
import dayjs from 'dayjs';
import { ObjectUtil, RandomUtil, Base64, NumberFormatter, SizeFormatter, Wireguard } from '@/utils';
import { getRandomRealityTarget } from '@/models/reality-targets';
export const Protocols = {
VMESS: 'vmess',
VLESS: 'vless',
TROJAN: 'trojan',
SHADOWSOCKS: 'shadowsocks',
WIREGUARD: 'wireguard',
HYSTERIA: 'hysteria',
MIXED: 'mixed',
HTTP: 'http',
TUNNEL: 'tunnel',
TUN: 'tun',
};
export const SSMethods = {
AES_256_GCM: 'aes-256-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
};
export const TLS_FLOW_CONTROL = {
VISION: "xtls-rprx-vision",
VISION_UDP443: "xtls-rprx-vision-udp443",
};
export const TLS_VERSION_OPTION = {
TLS10: "1.0",
TLS11: "1.1",
TLS12: "1.2",
TLS13: "1.3",
};
export const TLS_CIPHER_OPTION = {
AES_128_GCM: "TLS_AES_128_GCM_SHA256",
AES_256_GCM: "TLS_AES_256_GCM_SHA384",
CHACHA20_POLY1305: "TLS_CHACHA20_POLY1305_SHA256",
ECDHE_ECDSA_AES_128_CBC: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
ECDHE_ECDSA_AES_256_CBC: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
ECDHE_RSA_AES_128_CBC: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
ECDHE_RSA_AES_256_CBC: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
ECDHE_ECDSA_AES_128_GCM: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
ECDHE_ECDSA_AES_256_GCM: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
ECDHE_RSA_AES_128_GCM: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
ECDHE_RSA_AES_256_GCM: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
ECDHE_ECDSA_CHACHA20_POLY1305: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
ECDHE_RSA_CHACHA20_POLY1305: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
};
export const UTLS_FINGERPRINT = {
UTLS_CHROME: "chrome",
UTLS_FIREFOX: "firefox",
UTLS_SAFARI: "safari",
UTLS_IOS: "ios",
UTLS_android: "android",
UTLS_EDGE: "edge",
UTLS_360: "360",
UTLS_QQ: "qq",
UTLS_RANDOM: "random",
UTLS_RANDOMIZED: "randomized",
UTLS_RONDOMIZEDNOALPN: "randomizednoalpn",
UTLS_UNSAFE: "unsafe",
};
export const ALPN_OPTION = {
H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
};
export const SNIFFING_OPTION = {
HTTP: "http",
TLS: "tls",
QUIC: "quic",
FAKEDNS: "fakedns"
};
export const USAGE_OPTION = {
ENCIPHERMENT: "encipherment",
VERIFY: "verify",
ISSUE: "issue",
};
export const DOMAIN_STRATEGY_OPTION = {
AS_IS: "AsIs",
USE_IP: "UseIP",
USE_IPV6V4: "UseIPv6v4",
USE_IPV6: "UseIPv6",
USE_IPV4V6: "UseIPv4v6",
USE_IPV4: "UseIPv4",
FORCE_IP: "ForceIP",
FORCE_IPV6V4: "ForceIPv6v4",
FORCE_IPV6: "ForceIPv6",
FORCE_IPV4V6: "ForceIPv4v6",
FORCE_IPV4: "ForceIPv4",
};
export const TCP_CONGESTION_OPTION = {
BBR: "bbr",
CUBIC: "cubic",
RENO: "reno",
};
export const USERS_SECURITY = {
AES_128_GCM: "aes-128-gcm",
CHACHA20_POLY1305: "chacha20-poly1305",
AUTO: "auto",
NONE: "none",
ZERO: "zero",
};
export const MODE_OPTION = {
AUTO: "auto",
PACKET_UP: "packet-up",
STREAM_UP: "stream-up",
STREAM_ONE: "stream-one",
};
Object.freeze(Protocols);
Object.freeze(SSMethods);
Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(TLS_VERSION_OPTION);
Object.freeze(TLS_CIPHER_OPTION);
Object.freeze(UTLS_FINGERPRINT);
Object.freeze(ALPN_OPTION);
Object.freeze(SNIFFING_OPTION);
Object.freeze(USAGE_OPTION);
Object.freeze(DOMAIN_STRATEGY_OPTION);
Object.freeze(TCP_CONGESTION_OPTION);
Object.freeze(USERS_SECURITY);
Object.freeze(MODE_OPTION);
export type JsonObject = Record<string, unknown>;
export interface HeaderEntry { name: string; value: string }
export interface FallbackEntry {
dest?: string | number;
name?: string;
alpn?: string;
path?: string;
xver?: number | string;
}
export class XrayCommonClass {
[key: string]: any;
static toJsonArray<T extends { toJson(): unknown }>(arr: T[]): unknown[] {
return arr.map((obj) => obj.toJson());
}
static fromJson(..._args: unknown[]): XrayCommonClass | undefined {
return new XrayCommonClass();
}
toJson(): unknown {
return this;
}
static fallbackToJson(fb: FallbackEntry): JsonObject {
const out: JsonObject = { dest: fb.dest };
if (fb.name) out.name = fb.name;
if (fb.alpn) out.alpn = fb.alpn;
if (fb.path) out.path = fb.path;
const xver = Number(fb.xver);
if (Number.isInteger(xver) && xver > 0) out.xver = xver;
return out;
}
toString(format: boolean = true): string {
return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());
}
static toHeaders(v2Headers: unknown): HeaderEntry[] {
const newHeaders: HeaderEntry[] = [];
if (v2Headers && typeof v2Headers === 'object') {
const map = v2Headers as Record<string, string | string[]>;
Object.keys(map).forEach((key: string) => {
const values = map[key];
if (typeof values === 'string') {
newHeaders.push({ name: key, value: values });
} else if (Array.isArray(values)) {
for (let i = 0; i < values.length; ++i) {
newHeaders.push({ name: key, value: values[i] });
}
}
});
}
return newHeaders;
}
static toV2Headers(headers: HeaderEntry[], arr: boolean = true): Record<string, string | string[]> {
const v2Headers: Record<string, string | string[]> = {};
for (let i = 0; i < headers.length; ++i) {
const name = headers[i].name;
const value = headers[i].value;
if (ObjectUtil.isEmpty(name) || ObjectUtil.isEmpty(value)) {
continue;
}
if (!(name in v2Headers)) {
v2Headers[name] = arr ? [value] : value;
} else {
const existing = v2Headers[name];
if (arr && Array.isArray(existing)) {
existing.push(value);
} else {
v2Headers[name] = value;
}
}
}
return v2Headers;
}
}
export class TcpStreamSettings extends XrayCommonClass {
static TcpRequest: any;
static TcpResponse: any;
constructor(
acceptProxyProtocol: any = false,
type: any = 'none',
request: any = new TcpStreamSettings.TcpRequest(),
response = new TcpStreamSettings.TcpResponse(),
) {
super();
this.acceptProxyProtocol = acceptProxyProtocol;
this.type = type;
this.request = request;
this.response = response;
}
static fromJson(json: any = {}) {
let header = json.header;
if (!header) {
header = {};
}
return new TcpStreamSettings(json.acceptProxyProtocol,
header.type,
TcpStreamSettings.TcpRequest.fromJson(header.request),
TcpStreamSettings.TcpResponse.fromJson(header.response),
);
}
toJson() {
const json: any = {};
if (this.acceptProxyProtocol) {
json.acceptProxyProtocol = true;
}
if (this.type === 'http') {
json.header = {
type: 'http',
request: this.request.toJson(),
response: this.response.toJson(),
};
} else if (this.type && this.type !== 'none') {
json.header = { type: this.type };
}
return json;
}
}
TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
constructor(
version = '1.1',
method = 'GET',
path = ['/'],
headers: any[] = [],
) {
super();
this.version = version;
this.method = method;
this.path = path.length === 0 ? ['/'] : path;
this.headers = headers;
}
addPath(path: any) {
this.path.push(path);
}
removePath(index: number) {
this.path.splice(index, 1);
}
addHeader(name: any, value: any) {
this.headers.push({ name: name, value: value });
}
removeHeader(index: number) {
this.headers.splice(index, 1);
}
static fromJson(json: any = {}) {
return new TcpStreamSettings.TcpRequest(
json.version,
json.method,
json.path,
XrayCommonClass.toHeaders(json.headers),
);
}
toJson() {
return {
version: this.version,
method: this.method,
path: ObjectUtil.clone(this.path),
headers: XrayCommonClass.toV2Headers(this.headers),
};
}
};
TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
constructor(
version = '1.1',
status = '200',
reason = 'OK',
headers: any[] = [],
) {
super();
this.version = version;
this.status = status;
this.reason = reason;
this.headers = headers;
}
addHeader(name: any, value: any) {
this.headers.push({ name: name, value: value });
}
removeHeader(index: number) {
this.headers.splice(index, 1);
}
static fromJson(json: any = {}) {
return new TcpStreamSettings.TcpResponse(
json.version,
json.status,
json.reason,
XrayCommonClass.toHeaders(json.headers),
);
}
toJson() {
return {
version: this.version,
status: this.status,
reason: this.reason,
headers: XrayCommonClass.toV2Headers(this.headers),
};
}
};
export class KcpStreamSettings extends XrayCommonClass {
constructor(
mtu = 1350,
tti = 20,
uplinkCapacity = 5,
downlinkCapacity = 20,
cwndMultiplier = 1,
maxSendingWindow = 2097152,
) {
super();
this.mtu = mtu;
this.tti = tti;
this.upCap = uplinkCapacity;
this.downCap = downlinkCapacity;
this.cwndMultiplier = cwndMultiplier;
this.maxSendingWindow = maxSendingWindow;
}
static fromJson(json: any = {}) {
return new KcpStreamSettings(
json.mtu,
json.tti,
json.uplinkCapacity,
json.downlinkCapacity,
json.cwndMultiplier,
json.maxSendingWindow,
);
}
toJson() {
return {
mtu: this.mtu,
tti: this.tti,
uplinkCapacity: this.upCap,
downlinkCapacity: this.downCap,
cwndMultiplier: this.cwndMultiplier,
maxSendingWindow: this.maxSendingWindow,
};
}
}
export class WsStreamSettings extends XrayCommonClass {
constructor(
acceptProxyProtocol: any = false,
path = '/',
host = '',
headers: any[] = [],
heartbeatPeriod = 0,
) {
super();
this.acceptProxyProtocol = acceptProxyProtocol;
this.path = path;
this.host = host;
this.headers = headers;
this.heartbeatPeriod = heartbeatPeriod;
}
addHeader(name: any, value: any) {
this.headers.push({ name: name, value: value });
}
removeHeader(index: number) {
this.headers.splice(index, 1);
}
static fromJson(json: any = {}) {
return new WsStreamSettings(
json.acceptProxyProtocol,
json.path,
json.host,
XrayCommonClass.toHeaders(json.headers),
json.heartbeatPeriod,
);
}
toJson() {
return {
acceptProxyProtocol: this.acceptProxyProtocol,
path: this.path,
host: this.host,
headers: XrayCommonClass.toV2Headers(this.headers, false),
heartbeatPeriod: this.heartbeatPeriod,
};
}
}
export class GrpcStreamSettings extends XrayCommonClass {
constructor(
serviceName = "",
authority = "",
multiMode = false,
) {
super();
this.serviceName = serviceName;
this.authority = authority;
this.multiMode = multiMode;
}
static fromJson(json: any = {}) {
return new GrpcStreamSettings(
json.serviceName,
json.authority,
json.multiMode
);
}
toJson() {
return {
serviceName: this.serviceName,
authority: this.authority,
multiMode: this.multiMode,
}
}
}
export class HTTPUpgradeStreamSettings extends XrayCommonClass {
constructor(
acceptProxyProtocol: any = false,
path = '/',
host = '',
headers: any[] = []
) {
super();
this.acceptProxyProtocol = acceptProxyProtocol;
this.path = path;
this.host = host;
this.headers = headers;
}
addHeader(name: any, value: any) {
this.headers.push({ name: name, value: value });
}
removeHeader(index: number) {
this.headers.splice(index, 1);
}
static fromJson(json: any = {}) {
return new HTTPUpgradeStreamSettings(
json.acceptProxyProtocol,
json.path,
json.host,
XrayCommonClass.toHeaders(json.headers),
);
}
toJson() {
return {
acceptProxyProtocol: this.acceptProxyProtocol,
path: this.path,
host: this.host,
headers: XrayCommonClass.toV2Headers(this.headers, false),
};
}
}
// Mirrors the inbound (server-side) view of Xray-core's SplitHTTPConfig
// (infra/conf/transport_internet.go). Only fields the server actually
// reads at runtime, plus the bidirectional fields the server enforces,
// live here. Most client-only fields (uplinkChunkSize, noGRPCHeader,
// scMinPostsIntervalMs, xmux, downloadSettings) belong on the outbound
// class instead.
//
// `headers` and `uplinkHTTPMethod` are client-only at runtime (xray's
// listener doesn't read them) but we keep them here so the admin can set
// values that get embedded into the share link's `extra` blob.
export class xHTTPStreamSettings extends XrayCommonClass {
constructor(
// Bidirectional — must match between client and server
path = '/',
host = '',
mode = MODE_OPTION.AUTO,
xPaddingBytes = "100-1000",
xPaddingObfsMode = false,
xPaddingKey = '',
xPaddingHeader = '',
xPaddingPlacement = '',
xPaddingMethod = '',
sessionPlacement = '',
sessionKey = '',
seqPlacement = '',
seqKey = '',
uplinkDataPlacement = '',
uplinkDataKey = '',
scMaxEachPostBytes = "1000000",
// Server-side only
noSSEHeader = false,
scMaxBufferedPosts = 30,
scStreamUpServerSecs = "20-80",
serverMaxHeaderBytes = 0,
// URL-share only — embedded in the link's `extra` blob so clients
// pick them up; xray's listener ignores them at runtime.
uplinkHTTPMethod = '',
headers: any[] = [],
) {
super();
this.path = path;
this.host = host;
this.mode = mode;
this.xPaddingBytes = xPaddingBytes;
this.xPaddingObfsMode = xPaddingObfsMode;
this.xPaddingKey = xPaddingKey;
this.xPaddingHeader = xPaddingHeader;
this.xPaddingPlacement = xPaddingPlacement;
this.xPaddingMethod = xPaddingMethod;
this.sessionPlacement = sessionPlacement;
this.sessionKey = sessionKey;
this.seqPlacement = seqPlacement;
this.seqKey = seqKey;
this.uplinkDataPlacement = uplinkDataPlacement;
this.uplinkDataKey = uplinkDataKey;
this.scMaxEachPostBytes = scMaxEachPostBytes;
this.noSSEHeader = noSSEHeader;
this.scMaxBufferedPosts = scMaxBufferedPosts;
this.scStreamUpServerSecs = scStreamUpServerSecs;
this.serverMaxHeaderBytes = serverMaxHeaderBytes;
this.uplinkHTTPMethod = uplinkHTTPMethod;
this.headers = headers;
}
addHeader(name: any, value: any) {
this.headers.push({ name: name, value: value });
}
removeHeader(index: number) {
this.headers.splice(index, 1);
}
static fromJson(json: any = {}) {
return new xHTTPStreamSettings(
json.path,
json.host,
json.mode,
json.xPaddingBytes,
json.xPaddingObfsMode,
json.xPaddingKey,
json.xPaddingHeader,
json.xPaddingPlacement,
json.xPaddingMethod,
json.sessionPlacement,
json.sessionKey,
json.seqPlacement,
json.seqKey,
json.uplinkDataPlacement,
json.uplinkDataKey,
json.scMaxEachPostBytes,
json.noSSEHeader,
json.scMaxBufferedPosts,
json.scStreamUpServerSecs,
json.serverMaxHeaderBytes,
json.uplinkHTTPMethod,
XrayCommonClass.toHeaders(json.headers),
);
}
toJson() {
return {
path: this.path,
host: this.host,
mode: this.mode,
xPaddingBytes: this.xPaddingBytes,
xPaddingObfsMode: this.xPaddingObfsMode,
xPaddingKey: this.xPaddingKey,
xPaddingHeader: this.xPaddingHeader,
xPaddingPlacement: this.xPaddingPlacement,
xPaddingMethod: this.xPaddingMethod,
sessionPlacement: this.sessionPlacement,
sessionKey: this.sessionKey,
seqPlacement: this.seqPlacement,
seqKey: this.seqKey,
uplinkDataPlacement: this.uplinkDataPlacement,
uplinkDataKey: this.uplinkDataKey,
scMaxEachPostBytes: this.scMaxEachPostBytes,
noSSEHeader: this.noSSEHeader,
scMaxBufferedPosts: this.scMaxBufferedPosts,
scStreamUpServerSecs: this.scStreamUpServerSecs,
serverMaxHeaderBytes: this.serverMaxHeaderBytes,
uplinkHTTPMethod: this.uplinkHTTPMethod,
headers: XrayCommonClass.toV2Headers(this.headers, false),
};
}
}
export class HysteriaStreamSettings extends XrayCommonClass {
constructor(
protocol?: any,
version: any = 2,
auth: any = '',
udpIdleTimeout: any = 60,
masquerade?: any,
) {
super();
this.protocol = protocol;
this.version = version;
this.auth = auth;
this.udpIdleTimeout = udpIdleTimeout;
this.masquerade = masquerade;
}
static fromJson(json: any = {}) {
return new HysteriaStreamSettings(
json.protocol,
json.version ?? 2,
json.auth ?? '',
json.udpIdleTimeout ?? 60,
json.masquerade ? HysteriaMasquerade.fromJson(json.masquerade) : undefined,
);
}
toJson() {
return {
protocol: this.protocol,
version: this.version,
auth: this.auth,
udpIdleTimeout: this.udpIdleTimeout,
masquerade: this.masqueradeSwitch ? this.masquerade.toJson() : undefined,
};
}
get masqueradeSwitch() {
return this.masquerade != undefined;
}
set masqueradeSwitch(value) {
this.masquerade = value ? new HysteriaMasquerade() : undefined;
}
};
export class HysteriaMasquerade extends XrayCommonClass {
constructor(
type = 'proxy',
dir = '',
url = '',
rewriteHost = false,
insecure = false,
content = '',
headers: any[] = [],
statusCode = 0,
) {
super();
this.type = type;
this.dir = dir;
this.url = url;
this.rewriteHost = rewriteHost;
this.insecure = insecure;
this.content = content;
this.headers = headers;
this.statusCode = statusCode;
}
addHeader(name: any, value: any) {
this.headers.push({ name: name, value: value });
}
removeHeader(index: number) {
this.headers.splice(index, 1);
}
static fromJson(json: any = {}) {
const type = ['proxy', 'file', 'string'].includes(json.type) ? json.type : 'proxy';
return new HysteriaMasquerade(
type,
json.dir,
json.url,
json.rewriteHost,
json.insecure,
json.content,
XrayCommonClass.toHeaders(json.headers),
json.statusCode,
);
}
toJson() {
return {
type: this.type,
dir: this.dir,
url: this.url,
rewriteHost: this.rewriteHost,
insecure: this.insecure,
content: this.content,
headers: XrayCommonClass.toV2Headers(this.headers, false),
statusCode: this.statusCode,
};
}
};
export class TlsStreamSettings extends XrayCommonClass {
static Cert: any;
static Settings: any;
constructor(
serverName: any = '',
minVersion = TLS_VERSION_OPTION.TLS12,
maxVersion = TLS_VERSION_OPTION.TLS13,
cipherSuites = '',
rejectUnknownSni = false,
disableSystemRoot = false,
enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()],
alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
echServerKeys = '',
settings = new TlsStreamSettings.Settings()
) {
super();
this.sni = serverName;
this.minVersion = minVersion;
this.maxVersion = maxVersion;
this.cipherSuites = cipherSuites;
this.rejectUnknownSni = rejectUnknownSni;
this.disableSystemRoot = disableSystemRoot;
this.enableSessionResumption = enableSessionResumption;
this.certs = certificates;
this.alpn = alpn;
this.echServerKeys = echServerKeys;
this.settings = settings;
}
addCert() {
this.certs.push(new TlsStreamSettings.Cert());
}
removeCert(index: number) {
this.certs.splice(index, 1);
}
static fromJson(json: any = {}) {
let certs;
let settings;
if (!ObjectUtil.isEmpty(json.certificates)) {
certs = json.certificates.map((cert: any) => TlsStreamSettings.Cert.fromJson(cert));
}
if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.fingerprint, json.settings.echConfigList);
}
return new TlsStreamSettings(
json.serverName,
json.minVersion,
json.maxVersion,
json.cipherSuites,
json.rejectUnknownSni,
json.disableSystemRoot,
json.enableSessionResumption,
certs,
json.alpn,
json.echServerKeys,
settings,
);
}
toJson() {
return {
serverName: this.sni,
minVersion: this.minVersion,
maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites,
rejectUnknownSni: this.rejectUnknownSni,
disableSystemRoot: this.disableSystemRoot,
enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn,
echServerKeys: this.echServerKeys,
settings: this.settings,
};
}
}
TlsStreamSettings.Cert = class extends XrayCommonClass {
constructor(
useFile = true,
certificateFile = '',
keyFile = '',
certificate = '',
key = '',
oneTimeLoading = false,
usage = USAGE_OPTION.ENCIPHERMENT,
buildChain = false,
) {
super();
this.useFile = useFile;
this.certFile = certificateFile;
this.keyFile = keyFile;
this.cert = Array.isArray(certificate) ? certificate.join('\n') : certificate;
this.key = Array.isArray(key) ? key.join('\n') : key;
this.oneTimeLoading = oneTimeLoading;
this.usage = usage;
this.buildChain = buildChain
}
static fromJson(json: any = {}) {
if ('certificateFile' in json && 'keyFile' in json) {
return new TlsStreamSettings.Cert(
true,
json.certificateFile,
json.keyFile, '', '',
json.oneTimeLoading,
json.usage,
json.buildChain,
);
} else {
return new TlsStreamSettings.Cert(
false, '', '',
Array.isArray(json.certificate) ? json.certificate.join('\n') : (json.certificate ?? ''),
Array.isArray(json.key) ? json.key.join('\n') : (json.key ?? ''),
json.oneTimeLoading,
json.usage,
json.buildChain,
);
}
}
toJson() {
if (this.useFile) {
return {
certificateFile: this.certFile,
keyFile: this.keyFile,
oneTimeLoading: this.oneTimeLoading,
usage: this.usage,
buildChain: this.buildChain,
};
} else {
return {
certificate: this.cert.split('\n'),
key: this.key.split('\n'),
oneTimeLoading: this.oneTimeLoading,
usage: this.usage,
buildChain: this.buildChain,
};
}
}
};
TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
echConfigList = '',
) {
super();
this.fingerprint = fingerprint;
this.echConfigList = echConfigList;
}
static fromJson(json: any = {}) {
return new TlsStreamSettings.Settings(
json.fingerprint,
json.echConfigList,
);
}
toJson() {
return {
fingerprint: this.fingerprint,
echConfigList: this.echConfigList
};
}
};
export class RealityStreamSettings extends XrayCommonClass {
static Settings: any;
constructor(
show: any = false,
xver = 0,
target = '',
serverNames = '',
privateKey = '',
minClientVer = '',
maxClientVer = '',
maxTimediff = 0,
shortIds = RandomUtil.randomShortIds(),
mldsa65Seed = '',
settings = new RealityStreamSettings.Settings()
) {
super();
// If target/serverNames are not provided, use random values
if (!target && !serverNames) {
const randomTarget = getRandomRealityTarget();
target = randomTarget.target;
serverNames = randomTarget.sni;
}
this.show = show;
this.xver = xver;
this.target = target;
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
this.privateKey = privateKey;
this.minClientVer = minClientVer;
this.maxClientVer = maxClientVer;
this.maxTimediff = maxTimediff;
this.shortIds = Array.isArray(shortIds) ? shortIds.join(",") : shortIds;
this.mldsa65Seed = mldsa65Seed;
this.settings = settings;
}
static fromJson(json: any = {}) {
let settings;
if (!ObjectUtil.isEmpty(json.settings)) {
settings = new RealityStreamSettings.Settings(
json.settings.publicKey,
json.settings.fingerprint,
json.settings.serverName,
json.settings.spiderX,
json.settings.mldsa65Verify,
);
}
return new RealityStreamSettings(
json.show,
json.xver,
json.target,
json.serverNames,
json.privateKey,
json.minClientVer,
json.maxClientVer,
json.maxTimediff,
json.shortIds,
json.mldsa65Seed,
settings,
);
}
toJson() {
return {
show: this.show,
xver: this.xver,
target: this.target,
serverNames: this.serverNames.split(","),
privateKey: this.privateKey,
minClientVer: this.minClientVer,
maxClientVer: this.maxClientVer,
maxTimediff: this.maxTimediff,
shortIds: this.shortIds.split(","),
mldsa65Seed: this.mldsa65Seed,
settings: this.settings,
};
}
}
RealityStreamSettings.Settings = class extends XrayCommonClass {
constructor(
publicKey = '',
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
serverName = '',
spiderX = '/',
mldsa65Verify = ''
) {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.spiderX = spiderX;
this.mldsa65Verify = mldsa65Verify;
}
static fromJson(json: any = {}) {
return new RealityStreamSettings.Settings(
json.publicKey,
json.fingerprint,
json.serverName,
json.spiderX,
json.mldsa65Verify
);
}
toJson() {
return {
publicKey: this.publicKey,
fingerprint: this.fingerprint,
serverName: this.serverName,
spiderX: this.spiderX,
mldsa65Verify: this.mldsa65Verify
};
}
};
export class SockoptStreamSettings extends XrayCommonClass {
constructor(
acceptProxyProtocol: any = false,
tcpFastOpen = false,
mark = 0,
tproxy = "off",
tcpMptcp = false,
penetrate = false,
domainStrategy = DOMAIN_STRATEGY_OPTION.USE_IP,
tcpMaxSeg = 1440,
dialerProxy = "",
tcpKeepAliveInterval = 0,
tcpKeepAliveIdle = 300,
tcpUserTimeout = 10000,
tcpcongestion = TCP_CONGESTION_OPTION.BBR,
V6Only = false,
tcpWindowClamp = 600,
interfaceName = "",
trustedXForwardedFor = [],
) {
super();
this.acceptProxyProtocol = acceptProxyProtocol;
this.tcpFastOpen = tcpFastOpen;
this.mark = mark;
this.tproxy = tproxy;
this.tcpMptcp = tcpMptcp;
this.penetrate = penetrate;
this.domainStrategy = domainStrategy;
this.tcpMaxSeg = tcpMaxSeg;
this.dialerProxy = dialerProxy;
this.tcpKeepAliveInterval = tcpKeepAliveInterval;
this.tcpKeepAliveIdle = tcpKeepAliveIdle;
this.tcpUserTimeout = tcpUserTimeout;
this.tcpcongestion = tcpcongestion;
this.V6Only = V6Only;
this.tcpWindowClamp = tcpWindowClamp;
this.interfaceName = interfaceName;
this.trustedXForwardedFor = trustedXForwardedFor;
}
static fromJson(json: any = {}) {
if (Object.keys(json).length === 0) return undefined;
return new SockoptStreamSettings(
json.acceptProxyProtocol,
json.tcpFastOpen,
json.mark,
json.tproxy,
json.tcpMptcp,
json.penetrate,
json.domainStrategy,
json.tcpMaxSeg,
json.dialerProxy,
json.tcpKeepAliveInterval,
json.tcpKeepAliveIdle,
json.tcpUserTimeout,
json.tcpcongestion,
json.V6Only,
json.tcpWindowClamp,
json.interface,
json.trustedXForwardedFor || [],
);
}
toJson() {
const result: any = {
acceptProxyProtocol: this.acceptProxyProtocol,
tcpFastOpen: this.tcpFastOpen,
mark: this.mark,
tproxy: this.tproxy,
tcpMptcp: this.tcpMptcp,
penetrate: this.penetrate,
domainStrategy: this.domainStrategy,
tcpMaxSeg: this.tcpMaxSeg,
dialerProxy: this.dialerProxy,
tcpKeepAliveInterval: this.tcpKeepAliveInterval,
tcpKeepAliveIdle: this.tcpKeepAliveIdle,
tcpUserTimeout: this.tcpUserTimeout,
tcpcongestion: this.tcpcongestion,
V6Only: this.V6Only,
tcpWindowClamp: this.tcpWindowClamp,
interface: this.interfaceName,
};
if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
result.trustedXForwardedFor = this.trustedXForwardedFor;
}
return result;
}
}
export class UdpMask extends XrayCommonClass {
constructor(type: any = 'salamander', settings: any = {}) {
super();
this.type = type;
this.settings = this._getDefaultSettings(type, settings);
}
_getDefaultSettings(type: any, settings: any = {}): any {
switch (type) {
case 'salamander':
case 'mkcp-aes128gcm':
return { password: settings.password || '' };
case 'header-dns':
return { domain: settings.domain || '' };
case 'xdns':
return { domains: Array.isArray(settings.domains) ? settings.domains : [] };
case 'xicmp':
return { ip: settings.ip || '', id: settings.id ?? 0 };
case 'mkcp-original':
case 'header-dtls':
case 'header-srtp':
case 'header-utp':
case 'header-wechat':
case 'header-wireguard':
return {};
case 'header-custom':
return {
client: Array.isArray(settings.client) ? settings.client : [],
server: Array.isArray(settings.server) ? settings.server : [],
};
case 'noise':
return {
reset: settings.reset ?? 0,
noise: Array.isArray(settings.noise) ? settings.noise : [],
};
default:
return settings;
}
}
static fromJson(json: any = {}) {
return new UdpMask(
json.type || 'salamander',
json.settings || {}
);
}
toJson() {
const cleanItem = (item: any) => {
const out = { ...item };
if (out.type === 'array') {
delete out.packet;
} else {
delete out.rand;
delete out.randRange;
}
return out;
};
let settings = this.settings;
if (this.type === 'noise' && settings && Array.isArray(settings.noise)) {
settings = { ...settings, noise: settings.noise.map(cleanItem) };
} else if (this.type === 'header-custom' && settings) {
settings = {
...settings,
client: Array.isArray(settings.client) ? settings.client.map(cleanItem) : settings.client,
server: Array.isArray(settings.server) ? settings.server.map(cleanItem) : settings.server,
};
}
return {
type: this.type,
settings: (settings && Object.keys(settings).length > 0) ? settings : undefined
};
}
}
export class TcpMask extends XrayCommonClass {
constructor(type: any = 'fragment', settings: any = {}) {
super();
this.type = type;
this.settings = this._getDefaultSettings(type, settings);
}
_getDefaultSettings(type: any, settings: any = {}): any {
switch (type) {
case 'fragment':
return {
packets: settings.packets ?? 'tlshello',
length: settings.length ?? '',
delay: settings.delay ?? '',
maxSplit: settings.maxSplit ?? '',
};
case 'sudoku':
return {
password: settings.password ?? '',
ascii: settings.ascii ?? '',
customTable: settings.customTable ?? '',
customTables: Array.isArray(settings.customTables) ? settings.customTables : [],
paddingMin: settings.paddingMin ?? 0,
paddingMax: settings.paddingMax ?? 0,
};
case 'header-custom':
return {
clients: Array.isArray(settings.clients) ? settings.clients : [],
servers: Array.isArray(settings.servers) ? settings.servers : [],
};
default:
return settings;
}
}
static fromJson(json: any = {}) {
return new TcpMask(
json.type || 'fragment',
json.settings || {}
);
}
toJson() {
const cleanItem = (item: any) => {
const out = { ...item };
if (out.type === 'array') {
delete out.packet;
} else {
delete out.rand;
delete out.randRange;
}
return out;
};
let settings = this.settings;
if (this.type === 'header-custom' && settings) {
const cleanGroup = (group: any) => Array.isArray(group) ? group.map(cleanItem) : group;
settings = {
...settings,
clients: Array.isArray(settings.clients) ? settings.clients.map(cleanGroup) : settings.clients,
servers: Array.isArray(settings.servers) ? settings.servers.map(cleanGroup) : settings.servers,
};
}
return {
type: this.type,
settings: (settings && Object.keys(settings).length > 0) ? settings : undefined
};
}
}
export class QuicParams extends XrayCommonClass {
constructor(
congestion: any = 'bbr',
debug: any = false,
brutalUp: any = 65537,
brutalDown: any = 65537,
udpHop: any = undefined,
initStreamReceiveWindow: any = 8388608,
maxStreamReceiveWindow: any = 8388608,
initConnectionReceiveWindow: any = 20971520,
maxConnectionReceiveWindow: any = 20971520,
maxIdleTimeout: any = 30,
keepAlivePeriod: any = 5,
disablePathMTUDiscovery: any = false,
maxIncomingStreams = 1024,
) {
super();
this.congestion = congestion;
this.debug = debug;
this.brutalUp = brutalUp;
this.brutalDown = brutalDown;
this.udpHop = udpHop;
this.initStreamReceiveWindow = initStreamReceiveWindow;
this.maxStreamReceiveWindow = maxStreamReceiveWindow;
this.initConnectionReceiveWindow = initConnectionReceiveWindow;
this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
this.maxIdleTimeout = maxIdleTimeout;
this.keepAlivePeriod = keepAlivePeriod;
this.disablePathMTUDiscovery = disablePathMTUDiscovery;
this.maxIncomingStreams = maxIncomingStreams;
}
get hasUdpHop() {
return this.udpHop != null;
}
set hasUdpHop(value) {
this.udpHop = value ? (this.udpHop || { ports: '20000-50000', interval: '5-10' }) : undefined;
}
static fromJson(json: any = {}) {
if (!json || Object.keys(json).length === 0) return undefined;
return new QuicParams(
json.congestion,
json.debug,
json.brutalUp,
json.brutalDown,
json.udpHop ? { ports: json.udpHop.ports, interval: json.udpHop.interval } : undefined,
json.initStreamReceiveWindow,
json.maxStreamReceiveWindow,
json.initConnectionReceiveWindow,
json.maxConnectionReceiveWindow,
json.maxIdleTimeout,
json.keepAlivePeriod,
json.disablePathMTUDiscovery,
json.maxIncomingStreams,
);
}
toJson() {
const result: any = { congestion: this.congestion };
if (this.debug) result.debug = this.debug;
if (['brutal', 'force-brutal'].includes(this.congestion)) {
if (this.brutalUp) result.brutalUp = this.brutalUp;
if (this.brutalDown) result.brutalDown = this.brutalDown;
}
if (this.udpHop) result.udpHop = { ports: this.udpHop.ports, interval: this.udpHop.interval };
if (this.initStreamReceiveWindow > 0) result.initStreamReceiveWindow = this.initStreamReceiveWindow;
if (this.maxStreamReceiveWindow > 0) result.maxStreamReceiveWindow = this.maxStreamReceiveWindow;
if (this.initConnectionReceiveWindow > 0) result.initConnectionReceiveWindow = this.initConnectionReceiveWindow;
if (this.maxConnectionReceiveWindow > 0) result.maxConnectionReceiveWindow = this.maxConnectionReceiveWindow;
if (this.maxIdleTimeout !== 30 && this.maxIdleTimeout > 0) result.maxIdleTimeout = this.maxIdleTimeout;
if (this.keepAlivePeriod > 0) result.keepAlivePeriod = this.keepAlivePeriod;
if (this.disablePathMTUDiscovery) result.disablePathMTUDiscovery = this.disablePathMTUDiscovery;
if (this.maxIncomingStreams > 0) result.maxIncomingStreams = this.maxIncomingStreams;
return result;
}
}
export class FinalMaskStreamSettings extends XrayCommonClass {
constructor(tcp: any[] = [], udp: any[] = [], quicParams: any = undefined) {
super();
this.tcp = Array.isArray(tcp) ? tcp.map((t: any) => t instanceof TcpMask ? t : new TcpMask(t.type, t.settings)) : [];
this.udp = Array.isArray(udp) ? udp.map((u: any) => new UdpMask(u.type, u.settings)) : [new UdpMask((udp as any).type, (udp as any).settings)];
this.quicParams = quicParams instanceof QuicParams ? quicParams : (quicParams ? QuicParams.fromJson(quicParams) : undefined);
}
get enableQuicParams() {
return this.quicParams != null;
}
set enableQuicParams(value) {
this.quicParams = value ? (this.quicParams || new QuicParams()) : undefined;
}
static fromJson(json: any = {}) {
return new FinalMaskStreamSettings(
json.tcp || [],
json.udp || [],
json.quicParams ? QuicParams.fromJson(json.quicParams) : undefined,
);
}
toJson() {
const result: any = {} as any;
if (this.tcp && this.tcp.length > 0) {
result.tcp = this.tcp.map((t: any) => t.toJson());
}
if (this.udp && this.udp.length > 0) {
result.udp = this.udp.map((udp: any) => udp.toJson());
}
if (this.quicParams) {
result.quicParams = this.quicParams.toJson();
}
return result;
}
}
export class StreamSettings extends XrayCommonClass {
constructor(network = 'tcp',
security = 'none',
externalProxy = [],
tlsSettings = new TlsStreamSettings(),
realitySettings = new RealityStreamSettings(),
tcpSettings = new TcpStreamSettings(),
kcpSettings = new KcpStreamSettings(),
wsSettings = new WsStreamSettings(),
grpcSettings = new GrpcStreamSettings(),
httpupgradeSettings = new HTTPUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(),
hysteriaSettings = new HysteriaStreamSettings(),
finalmask = new FinalMaskStreamSettings(),
sockopt: any = undefined,
) {
super();
this.network = network;
this.security = security;
this.externalProxy = externalProxy;
this.tls = tlsSettings;
this.reality = realitySettings;
this.tcp = tcpSettings;
this.kcp = kcpSettings;
this.ws = wsSettings;
this.grpc = grpcSettings;
this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings;
this.hysteria = hysteriaSettings;
this.finalmask = finalmask;
this.sockopt = sockopt;
}
addTcpMask(type = 'fragment') {
this.finalmask.tcp.push(new TcpMask(type));
}
delTcpMask(index: number) {
if (this.finalmask.tcp) {
this.finalmask.tcp.splice(index, 1);
}
}
addUdpMask(type = 'salamander') {
this.finalmask.udp.push(new UdpMask(type));
}
delUdpMask(index: number) {
if (this.finalmask.udp) {
this.finalmask.udp.splice(index, 1);
}
}
get hasFinalMask() {
const hasTcp = this.finalmask.tcp && this.finalmask.tcp.length > 0;
const hasUdp = this.finalmask.udp && this.finalmask.udp.length > 0;
const hasQuicParams = this.finalmask.quicParams != null;
return hasTcp || hasUdp || hasQuicParams;
}
get isTls() {
return this.security === "tls";
}
set isTls(isTls) {
if (isTls) {
this.security = 'tls';
} else {
this.security = 'none';
}
}
//for Reality
get isReality() {
return this.security === "reality";
}
set isReality(isReality) {
if (isReality) {
this.security = 'reality';
} else {
this.security = 'none';
}
}
get sockoptSwitch() {
return this.sockopt != undefined;
}
set sockoptSwitch(value) {
this.sockopt = value ? new SockoptStreamSettings() : undefined;
}
static fromJson(json: any = {}) {
return new StreamSettings(
json.network,
json.security,
json.externalProxy,
TlsStreamSettings.fromJson(json.tlsSettings),
RealityStreamSettings.fromJson(json.realitySettings),
TcpStreamSettings.fromJson(json.tcpSettings),
KcpStreamSettings.fromJson(json.kcpSettings),
WsStreamSettings.fromJson(json.wsSettings),
GrpcStreamSettings.fromJson(json.grpcSettings),
HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings),
HysteriaStreamSettings.fromJson(json.hysteriaSettings),
FinalMaskStreamSettings.fromJson(json.finalmask),
SockoptStreamSettings.fromJson(json.sockopt),
);
}
toJson() {
const network = this.network;
return {
network: network,
security: this.security,
externalProxy: Array.isArray(this.externalProxy) && this.externalProxy.length > 0
? this.externalProxy
: undefined,
tlsSettings: this.isTls ? this.tls.toJson() : undefined,
realitySettings: this.isReality ? this.reality.toJson() : undefined,
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
};
}
}
export class Sniffing extends XrayCommonClass {
constructor(
enabled = false,
destOverride = ['http', 'tls', 'quic', 'fakedns'],
metadataOnly = false,
routeOnly = false,
ipsExcluded = [],
domainsExcluded = []) {
super();
this.enabled = enabled;
this.destOverride = Array.isArray(destOverride) && destOverride.length > 0 ? destOverride : ['http', 'tls', 'quic', 'fakedns'];
this.metadataOnly = metadataOnly;
this.routeOnly = routeOnly;
this.ipsExcluded = Array.isArray(ipsExcluded) ? ipsExcluded : [];
this.domainsExcluded = Array.isArray(domainsExcluded) ? domainsExcluded : [];
}
static fromJson(json: any = {}) {
let destOverride = ObjectUtil.clone(json.destOverride);
if (ObjectUtil.isEmpty(destOverride) || ObjectUtil.isArrEmpty(destOverride) || ObjectUtil.isEmpty(destOverride[0])) {
destOverride = ['http', 'tls', 'quic', 'fakedns'];
}
return new Sniffing(
!!json.enabled,
destOverride,
json.metadataOnly,
json.routeOnly,
json.ipsExcluded || [],
json.domainsExcluded || [],
);
}
toJson() {
if (!this.enabled) {
return { enabled: false };
}
return {
enabled: true,
destOverride: this.destOverride,
metadataOnly: this.metadataOnly || undefined,
routeOnly: this.routeOnly || undefined,
ipsExcluded: this.ipsExcluded.length > 0 ? this.ipsExcluded : undefined,
domainsExcluded: this.domainsExcluded.length > 0 ? this.domainsExcluded : undefined,
};
}
}
export class Inbound extends XrayCommonClass {
static Settings: any;
static ClientBase: any;
static VmessSettings: any;
static VLESSSettings: any;
static TrojanSettings: any;
static ShadowsocksSettings: any;
static HysteriaSettings: any;
static TunnelSettings: any;
static MixedSettings: any;
static HttpSettings: any;
static WireguardSettings: any;
static TunSettings: any;
constructor(
port: any = RandomUtil.randomInteger(10000, 60000),
listen = '',
protocol = Protocols.VLESS,
settings = null,
streamSettings = new StreamSettings(),
tag = '',
sniffing = new Sniffing(),
clientStats = '',
) {
super();
this.port = port;
this.listen = listen;
this._protocol = protocol;
this.settings = ObjectUtil.isEmpty(settings) ? Inbound.Settings.getSettings(protocol) : settings;
this.stream = streamSettings;
this.tag = tag;
this.sniffing = sniffing;
this.clientStats = clientStats;
}
getClientStats() {
return this.clientStats;
}
// Looks for a "host"-named entry in xhttp.headers and returns its value,
// or '' if not found. Used as a fallback when xhttp.host is empty so the
// share URL still carries a usable Host hint.
static xhttpHostFallback(xhttp: any): string {
if (!xhttp || !Array.isArray(xhttp.headers)) return '';
for (const h of xhttp.headers) {
if (h && typeof h.name === 'string' && h.name.toLowerCase() === 'host') {
return h.value || '';
}
}
return '';
}
// Build the JSON blob that goes into the URL's `extra` param (or, for
// VMess, into the base64-encoded link object). Carries ONLY the
// bidirectional fields from xray-core's SplitHTTPConfig — i.e. the
// ones the server enforces and the client must match. Strictly
// one-sided fields are excluded:
//
// - server-only (noSSEHeader, scMaxBufferedPosts,
// scStreamUpServerSecs, serverMaxHeaderBytes) — client wouldn't
// read them, so emitting them just bloats the URL.
// - client-only values are included only when present on the inbound
// object. Imported/API-created configs can carry them there, and
// the share link is the only place clients can receive them.
//
// Truthy-only guards keep default inbounds emitting the same compact
// URL they did before this helper grew.
static buildXhttpExtra(xhttp: any): any {
if (!xhttp) return null;
const extra: any = {};
if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) {
extra.xPaddingBytes = xhttp.xPaddingBytes;
}
if (xhttp.xPaddingObfsMode === true) {
extra.xPaddingObfsMode = true;
["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach((k: string) => {
if (typeof xhttp[k] === 'string' && xhttp[k].length > 0) {
extra[k] = xhttp[k];
}
});
}
const stringFields = [
"uplinkHTTPMethod",
"sessionPlacement", "sessionKey",
"seqPlacement", "seqKey",
"uplinkDataPlacement", "uplinkDataKey",
"scMaxEachPostBytes", "scMinPostsIntervalMs",
];
for (const k of stringFields) {
const v = xhttp[k];
if (typeof v === 'string' && v.length > 0) extra[k] = v;
}
const uplinkChunkSize = xhttp.uplinkChunkSize;
if ((typeof uplinkChunkSize === 'number' && uplinkChunkSize !== 0) ||
(typeof uplinkChunkSize === 'string' && uplinkChunkSize.length > 0)) {
extra.uplinkChunkSize = uplinkChunkSize;
}
if (xhttp.noGRPCHeader === true) {
extra.noGRPCHeader = true;
}
for (const k of ["xmux", "downloadSettings"]) {
const v = xhttp[k];
if (v && typeof v === 'object' && Object.keys(v).length > 0) {
extra[k] = v;
}
}
// Headers — emitted as the {name: value} map upstream's struct
// expects. The server runtime ignores this field, but the client
// (consuming the share link) honors it.
if (Array.isArray(xhttp.headers) && xhttp.headers.length > 0) {
const headersMap: any = {};
for (const h of xhttp.headers) {
if (h && h.name && h.name.toLowerCase() !== 'host') {
headersMap[h.name] = h.value || '';
}
}
if (Object.keys(headersMap).length > 0) extra.headers = headersMap;
}
return Object.keys(extra).length > 0 ? extra : null;
}
// Inject the inbound-side xhttp config into URL query params for
// vless/trojan/ss links. Sets path/host/mode at top level (xray's
// Build() always lets these win over `extra`) and packs the
// bidirectional fields into a JSON `extra` param. Also writes the
// flat `x_padding_bytes` param sing-box-family clients understand.
//
// Without this, the admin's custom xPaddingBytes / sessionKey / etc.
// never reach the client and handshakes are silently rejected with
// `invalid padding (...) length: 0`.
static applyXhttpExtraToParams(xhttp: any, params: any): void {
if (!xhttp) return;
params.set("path", xhttp.path);
const host = xhttp.host?.length > 0 ? xhttp.host : Inbound.xhttpHostFallback(xhttp);
params.set("host", host);
params.set("mode", xhttp.mode);
// Flat fallback for sing-box-family clients that don't read `extra`.
if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) {
params.set("x_padding_bytes", xhttp.xPaddingBytes);
}
const extra = Inbound.buildXhttpExtra(xhttp);
if (extra) params.set("extra", JSON.stringify(extra));
}
// VMess variant: VMess links are a base64-encoded JSON object, so we
// copy the same bidirectional fields directly into the JSON instead
// of building a query string. (The base VMess link generator already
// sets net/type/path/host, so we only contribute the SplitHTTPConfig
// extra side here.)
static applyXhttpExtraToObj(xhttp: any, obj: any): void {
if (!xhttp || !obj) return;
if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) {
obj.x_padding_bytes = xhttp.xPaddingBytes;
}
const extra = Inbound.buildXhttpExtra(xhttp);
if (!extra) return;
for (const [k, v] of Object.entries(extra)) {
obj[k] = v;
}
}
static externalProxyAlpn(value: any): any {
if (Array.isArray(value)) return value.filter(Boolean).join(',');
return typeof value === 'string' ? value : '';
}
static applyExternalProxyTLSParams(externalProxy: any, params: any, security: any): void {
if (!externalProxy || security !== 'tls') return;
const sni = externalProxy.sni?.length > 0 ? externalProxy.sni : externalProxy.dest;
if (sni?.length > 0) params.set("sni", sni);
if (externalProxy.fingerprint?.length > 0) params.set("fp", externalProxy.fingerprint);
const alpn = Inbound.externalProxyAlpn(externalProxy.alpn);
if (alpn.length > 0) params.set("alpn", alpn);
}
static applyExternalProxyTLSObj(externalProxy: any, obj: any, security: any): void {
if (!externalProxy || !obj || security !== 'tls') return;
const sni = externalProxy.sni?.length > 0 ? externalProxy.sni : externalProxy.dest;
if (sni?.length > 0) obj.sni = sni;
if (externalProxy.fingerprint?.length > 0) obj.fp = externalProxy.fingerprint;
const alpn = Inbound.externalProxyAlpn(externalProxy.alpn);
if (alpn.length > 0) obj.alpn = alpn;
}
static hasShareableFinalMaskValue(value: any): boolean {
if (value == null) {
return false;
}
if (Array.isArray(value)) {
return value.some((item: any) => Inbound.hasShareableFinalMaskValue(item));
}
if (typeof value === 'object') {
return Object.values(value).some((item: any) => Inbound.hasShareableFinalMaskValue(item));
}
if (typeof value === 'string') {
return value.length > 0;
}
return true;
}
static serializeFinalMask(finalmask: any): any {
if (!finalmask) {
return '';
}
const value = typeof finalmask.toJson === 'function' ? finalmask.toJson() : finalmask;
return Inbound.hasShareableFinalMaskValue(value) ? JSON.stringify(value) : '';
}
// Export finalmask with the same compact JSON payload shape that
// v2rayN-compatible share links use: fm=<json>.
static applyFinalMaskToParams(finalmask: any, params: any): void {
if (!params) return;
const payload = Inbound.serializeFinalMask(finalmask);
if (payload.length > 0) {
params.set("fm", payload);
}
}
// VMess links are a base64 JSON object, so keep the same fm payload
// under a flat property instead of a URL query string.
static applyFinalMaskToObj(finalmask: any, obj: any): void {
if (!obj) return;
const payload = Inbound.serializeFinalMask(finalmask);
if (payload.length > 0) {
obj.fm = payload;
}
}
get clients() {
switch (this.protocol) {
case Protocols.VMESS: return this.settings.vmesses;
case Protocols.VLESS: return this.settings.vlesses;
case Protocols.TROJAN: return this.settings.trojans;
case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;
case Protocols.HYSTERIA: return this.settings.hysterias;
default: return null;
}
}
get protocol() {
return this._protocol;
}
set protocol(protocol) {
this._protocol = protocol;
this.settings = Inbound.Settings.getSettings(protocol);
this.stream = new StreamSettings();
if (protocol === Protocols.TROJAN) {
this.tls = false;
}
if (protocol === Protocols.HYSTERIA) {
this.stream.network = 'hysteria';
this.stream.security = 'tls';
// Hysteria runs over QUIC and must not inherit TCP TLS ALPN defaults.
this.stream.tls.alpn = [ALPN_OPTION.H3];
}
}
get network() {
return this.stream.network;
}
set network(network) {
this.stream.network = network;
}
get isTcp() {
return this.network === "tcp";
}
get isWs() {
return this.network === "ws";
}
get isKcp() {
return this.network === "kcp";
}
get isGrpc() {
return this.network === "grpc";
}
get isHttpupgrade() {
return this.network === "httpupgrade";
}
get isXHTTP() {
return this.network === "xhttp";
}
// Shadowsocks
get method() {
switch (this.protocol) {
case Protocols.SHADOWSOCKS:
return this.settings.method;
default:
return "";
}
}
get isSSMultiUser() {
return this.method != SSMethods.BLAKE3_CHACHA20_POLY1305;
}
get isSS2022() {
return this.method.substring(0, 4) === "2022";
}
get serverName() {
if (this.stream.isTls) return this.stream.tls.sni;
if (this.stream.isReality) return this.stream.reality.serverNames;
return "";
}
getHeader(obj: any, name: any) {
for (const header of obj.headers) {
if (header.name.toLowerCase() === name.toLowerCase()) {
return header.value;
}
}
return "";
}
get host() {
if (this.isTcp) {
return this.getHeader(this.stream.tcp.request, 'host');
} else if (this.isWs) {
return this.stream.ws.host?.length > 0 ? this.stream.ws.host : this.getHeader(this.stream.ws, 'host');
} else if (this.isHttpupgrade) {
return this.stream.httpupgrade.host?.length > 0 ? this.stream.httpupgrade.host : this.getHeader(this.stream.httpupgrade, 'host');
} else if (this.isXHTTP) {
return this.stream.xhttp.host?.length > 0 ? this.stream.xhttp.host : this.getHeader(this.stream.xhttp, 'host');
}
return null;
}
get path() {
if (this.isTcp) {
return this.stream.tcp.request.path[0];
} else if (this.isWs) {
return this.stream.ws.path;
} else if (this.isHttpupgrade) {
return this.stream.httpupgrade.path;
} else if (this.isXHTTP) {
return this.stream.xhttp.path;
}
return null;
}
get serviceName() {
return this.stream.grpc.serviceName;
}
isExpiry(index: number) {
const exp = this.clients[index].expiryTime;
return exp > 0 ? exp < new Date().getTime() : false;
}
canEnableTls() {
if (this.protocol === Protocols.HYSTERIA) return true;
if (![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.network);
}
//this is used for xtls-rprx-vision
canEnableTlsFlow() {
if (((this.stream.security === 'tls') || (this.stream.security === 'reality')) && (this.network === "tcp")) {
return this.protocol === Protocols.VLESS;
}
return false;
}
// Vision seed applies only when the XTLS Vision (TCP/TLS) flow is selected.
// Excludes the UDP variant per spec.
canEnableVisionSeed() {
if (!this.canEnableTlsFlow()) return false;
const clients = this.settings?.vlesses;
if (!Array.isArray(clients)) return false;
return clients.some((c: any) => c?.flow === TLS_FLOW_CONTROL.VISION);
}
canEnableReality() {
if (![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
return ["tcp", "http", "grpc", "xhttp"].includes(this.network);
}
canEnableStream() {
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA].includes(this.protocol);
}
reset() {
this.port = RandomUtil.randomInteger(10000, 60000);
this.listen = '';
this.protocol = Protocols.VMESS;
this.settings = Inbound.Settings.getSettings(Protocols.VMESS);
this.stream = new StreamSettings();
this.tag = '';
this.sniffing = new Sniffing();
}
genVmessLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientId?: any, security?: any, externalProxy: any = null) {
if (this.protocol !== Protocols.VMESS) {
return '';
}
const tls = forceTls == 'same' ? this.stream.security : forceTls;
const obj: any = {
v: '2',
ps: remark,
add: address,
port: port,
id: clientId,
scy: security,
net: this.stream.network,
tls: tls,
};
const network = this.stream.network;
if (network === 'tcp') {
const tcp = this.stream.tcp;
obj.type = tcp.type;
if (tcp.type === 'http') {
const request = tcp.request;
obj.path = request.path.join(',');
const host = this.getHeader(request, 'host');
if (host) obj.host = host;
}
} else if (network === 'kcp') {
const kcp = this.stream.kcp;
obj.mtu = kcp.mtu;
obj.tti = kcp.tti;
} else if (network === 'ws') {
const ws = this.stream.ws;
obj.path = ws.path;
obj.host = ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host');
} else if (network === 'grpc') {
obj.path = this.stream.grpc.serviceName;
obj.authority = this.stream.grpc.authority;
if (this.stream.grpc.multiMode) {
obj.type = 'multi'
}
} else if (network === 'httpupgrade') {
const httpupgrade = this.stream.httpupgrade;
obj.path = httpupgrade.path;
obj.host = httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host');
} else if (network === 'xhttp') {
const xhttp = this.stream.xhttp;
obj.path = xhttp.path;
obj.host = xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host');
obj.type = xhttp.mode;
Inbound.applyXhttpExtraToObj(xhttp, obj);
}
Inbound.applyFinalMaskToObj(this.stream.finalmask, obj);
if (tls === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
obj.sni = this.stream.tls.sni;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)) {
obj.fp = this.stream.tls.settings.fingerprint;
}
if (this.stream.tls.alpn.length > 0) {
obj.alpn = this.stream.tls.alpn.join(',');
}
}
Inbound.applyExternalProxyTLSObj(externalProxy, obj, tls);
return 'vmess://' + Base64.encode(JSON.stringify(obj, null, 2));
}
genVLESSLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientId?: any, flow?: any, externalProxy: any = null) {
const uuid = clientId;
const type = this.stream.network;
const security = forceTls == 'same' ? this.stream.security : forceTls;
const params = new Map();
params.set("type", this.stream.network);
params.set("encryption", this.settings.encryption);
switch (type) {
case "tcp": {
const tcp = this.stream.tcp;
if (tcp.type === 'http') {
const request = tcp.request;
params.set("path", request.path.join(','));
const index = request.headers.findIndex((header: any) => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = request.headers[index].value;
params.set("host", host);
}
params.set("headerType", 'http');
}
break;
}
case "kcp": {
const kcp = this.stream.kcp;
params.set("mtu", kcp.mtu);
params.set("tti", kcp.tti);
break;
}
case "ws": {
const ws = this.stream.ws;
params.set("path", ws.path);
params.set("host", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host'));
break;
}
case "grpc": {
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
params.set("authority", grpc.authority);
if (grpc.multiMode) {
params.set("mode", "multi");
}
break;
}
case "httpupgrade": {
const httpupgrade = this.stream.httpupgrade;
params.set("path", httpupgrade.path);
params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));
break;
}
case "xhttp":
Inbound.applyXhttpExtraToParams(this.stream.xhttp, params);
break;
}
Inbound.applyFinalMaskToParams(this.stream.finalmask, params);
if (security === 'tls') {
params.set("security", "tls");
if (this.stream.isTls) {
params.set("fp", this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn);
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni);
}
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (type == "tcp" && !ObjectUtil.isEmpty(flow)) {
params.set("flow", flow);
}
}
Inbound.applyExternalProxyTLSParams(externalProxy, params, security);
}
else if (security === 'reality') {
params.set("security", "reality");
params.set("pbk", this.stream.reality.settings.publicKey);
params.set("fp", this.stream.reality.settings.fingerprint);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.mldsa65Verify)) {
params.set("pqv", this.stream.reality.settings.mldsa65Verify);
}
if (type == 'tcp' && !ObjectUtil.isEmpty(flow)) {
params.set("flow", flow);
}
}
else {
params.set("security", "none");
}
const link = `vless://${uuid}@${address}:${port}`;
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value)
}
url.hash = encodeURIComponent(remark);
return url.toString();
}
genSSLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientPassword?: any, externalProxy: any = null) {
const settings = this.settings;
const type = this.stream.network;
const security = forceTls == 'same' ? this.stream.security : forceTls;
const params = new Map();
params.set("type", this.stream.network);
switch (type) {
case "tcp": {
const tcp = this.stream.tcp;
if (tcp.type === 'http') {
const request = tcp.request;
params.set("path", request.path.join(','));
const index = request.headers.findIndex((header: any) => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = request.headers[index].value;
params.set("host", host);
}
params.set("headerType", 'http');
}
break;
}
case "kcp": {
const kcp = this.stream.kcp;
params.set("mtu", kcp.mtu);
params.set("tti", kcp.tti);
break;
}
case "ws": {
const ws = this.stream.ws;
params.set("path", ws.path);
params.set("host", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host'));
break;
}
case "grpc": {
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
params.set("authority", grpc.authority);
if (grpc.multiMode) {
params.set("mode", "multi");
}
break;
}
case "httpupgrade": {
const httpupgrade = this.stream.httpupgrade;
params.set("path", httpupgrade.path);
params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));
break;
}
case "xhttp":
Inbound.applyXhttpExtraToParams(this.stream.xhttp, params);
break;
}
Inbound.applyFinalMaskToParams(this.stream.finalmask, params);
if (security === 'tls') {
params.set("security", "tls");
if (this.stream.isTls) {
params.set("fp", this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn);
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni);
}
}
Inbound.applyExternalProxyTLSParams(externalProxy, params, security);
}
const password: string[] = [];
if (this.isSS2022) password.push(settings.password);
if (this.isSSMultiUser) password.push(clientPassword);
const link = `ss://${Base64.encode(`${settings.method}:${password.join(':')}`, true)}@${address}:${port}`;
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value)
}
url.hash = encodeURIComponent(remark);
return url.toString();
}
genTrojanLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientPassword?: any, externalProxy: any = null) {
const security = forceTls == 'same' ? this.stream.security : forceTls;
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
switch (type) {
case "tcp": {
const tcp = this.stream.tcp;
if (tcp.type === 'http') {
const request = tcp.request;
params.set("path", request.path.join(','));
const index = request.headers.findIndex((header: any) => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = request.headers[index].value;
params.set("host", host);
}
params.set("headerType", 'http');
}
break;
}
case "kcp": {
const kcp = this.stream.kcp;
params.set("mtu", kcp.mtu);
params.set("tti", kcp.tti);
break;
}
case "ws": {
const ws = this.stream.ws;
params.set("path", ws.path);
params.set("host", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host'));
break;
}
case "grpc": {
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
params.set("authority", grpc.authority);
if (grpc.multiMode) {
params.set("mode", "multi");
}
break;
}
case "httpupgrade": {
const httpupgrade = this.stream.httpupgrade;
params.set("path", httpupgrade.path);
params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));
break;
}
case "xhttp":
Inbound.applyXhttpExtraToParams(this.stream.xhttp, params);
break;
}
Inbound.applyFinalMaskToParams(this.stream.finalmask, params);
if (security === 'tls') {
params.set("security", "tls");
if (this.stream.isTls) {
params.set("fp", this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn);
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni);
}
}
Inbound.applyExternalProxyTLSParams(externalProxy, params, security);
}
else if (security === 'reality') {
params.set("security", "reality");
params.set("pbk", this.stream.reality.settings.publicKey);
params.set("fp", this.stream.reality.settings.fingerprint);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.mldsa65Verify)) {
params.set("pqv", this.stream.reality.settings.mldsa65Verify);
}
}
else {
params.set("security", "none");
}
const link = `trojan://${clientPassword}@${address}:${port}`;
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value)
}
url.hash = encodeURIComponent(remark);
return url.toString();
}
genHysteriaLink(address: any = '', port: any = this.port, remark: any = '', clientAuth?: any) {
const protocol = this.settings.version == 2 ? "hysteria2" : "hysteria";
const link = `${protocol}://${clientAuth}@${address}:${port}`;
const params = new Map();
params.set("security", "tls");
if (this.stream.tls.settings.fingerprint?.length > 0) params.set("fp", this.stream.tls.settings.fingerprint);
if (this.stream.tls.alpn?.length > 0) params.set("alpn", this.stream.tls.alpn);
if (this.stream.tls.settings.allowInsecure) params.set("insecure", "1");
if (this.stream.tls.settings.echConfigList?.length > 0) params.set("ech", this.stream.tls.settings.echConfigList);
if (this.stream.tls.sni?.length > 0) params.set("sni", this.stream.tls.sni);
const udpMasks = this.stream?.finalmask?.udp;
if (Array.isArray(udpMasks)) {
const salamanderMask = udpMasks.find((mask: any) => mask?.type === 'salamander');
const obfsPassword = salamanderMask?.settings?.password;
if (typeof obfsPassword === 'string' && obfsPassword.length > 0) {
params.set("obfs", "salamander");
params.set("obfs-password", obfsPassword);
}
}
Inbound.applyFinalMaskToParams(this.stream.finalmask, params);
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value);
}
url.hash = encodeURIComponent(remark);
return url.toString();
}
getWireguardTxt(address: any, port: any, remark: any, peerId: any) {
let txt = `[Interface]\n`
txt += `PrivateKey = ${this.settings.peers[peerId].privateKey}\n`
txt += `Address = ${this.settings.peers[peerId].allowedIPs[0]}\n`
txt += `DNS = 1.1.1.1, 1.0.0.1\n`
if (this.settings.mtu) {
txt += `MTU = ${this.settings.mtu}\n`
}
txt += `\n# ${remark}\n`
txt += `[Peer]\n`
txt += `PublicKey = ${this.settings.pubKey}\n`
txt += `AllowedIPs = 0.0.0.0/0, ::/0\n`
txt += `Endpoint = ${address}:${port}`
if (this.settings.peers[peerId].psk) {
txt += `\nPresharedKey = ${this.settings.peers[peerId].psk}`
}
if (this.settings.peers[peerId].keepAlive) {
txt += `\nPersistentKeepalive = ${this.settings.peers[peerId].keepAlive}\n`
}
return txt;
}
getWireguardLink(address: any, port: any, remark: any, peerId: any) {
const peer = this.settings?.peers?.[peerId];
if (!peer) return '';
const link = `wireguard://${address}:${port}`;
const url = new URL(link);
url.username = peer.privateKey || '';
if (this.settings?.pubKey) {
url.searchParams.set("publickey", this.settings.pubKey);
}
if (Array.isArray(peer.allowedIPs) && peer.allowedIPs.length > 0 && peer.allowedIPs[0]) {
url.searchParams.set("address", peer.allowedIPs[0]);
}
if (this.settings?.mtu) {
url.searchParams.set("mtu", this.settings.mtu);
}
url.hash = encodeURIComponent(remark);
return url.toString();
}
// resolveAddr picks the host that goes into share/sub links. Order:
// 1. hostOverride (caller supplies node address for node-managed inbounds)
// 2. inbound's bind listen (when explicit, not 0.0.0.0)
// 3. browser's location.hostname (single-panel default)
// Centralised so genAllLinks/genInboundLinks/genWireguard*
// all share the same chain — pre-Phase 3 we had four duplicated lines.
_resolveAddr(hostOverride = '') {
if (hostOverride) return hostOverride;
if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") return this.listen;
return location.hostname;
}
genWireguardLinks(remark = '', remarkModel = '-ieo', hostOverride = '') {
const addr = this._resolveAddr(hostOverride);
const separationChar = remarkModel.charAt(0);
const links: any[] = [];
this.settings.peers.forEach((_p: any, index: number) => {
links.push(this.getWireguardLink(addr, this.port, remark + separationChar + (index + 1), index));
});
return links.join('\r\n');
}
genWireguardConfigs(remark = '', remarkModel = '-ieo', hostOverride = '') {
const addr = this._resolveAddr(hostOverride);
const separationChar = remarkModel.charAt(0);
const links: any[] = [];
this.settings.peers.forEach((_p: any, index: number) => {
links.push(this.getWireguardTxt(addr, this.port, remark + separationChar + (index + 1), index));
});
return links.join('\r\n');
}
genLink(address: any = '', port: any = this.port, forceTls: any = 'same', remark: any = '', client?: any, externalProxy: any = null) {
switch (this.protocol) {
case Protocols.VMESS:
return this.genVmessLink(address, port, forceTls, remark, client.id, client.security, externalProxy);
case Protocols.VLESS:
return this.genVLESSLink(address, port, forceTls, remark, client.id, client.flow, externalProxy);
case Protocols.SHADOWSOCKS:
return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : '', externalProxy);
case Protocols.TROJAN:
return this.genTrojanLink(address, port, forceTls, remark, client.password, externalProxy);
case Protocols.HYSTERIA:
return this.genHysteriaLink(address, port, remark, client.auth.length > 0 ? client.auth : this.stream.hysteria.auth);
default: return '';
}
}
genAllLinks(remark: any = '', remarkModel: any = '-ieo', client?: any, hostOverride: any = '') {
const result: any[] = [];
const email = client ? client.email : '';
const addr = this._resolveAddr(hostOverride);
const port = this.port;
const separationChar = remarkModel.charAt(0);
const orderChars = remarkModel.slice(1);
const orders: any = {
'i': remark,
'e': email,
'o': '',
};
if (ObjectUtil.isArrEmpty(this.stream.externalProxy)) {
const r = orderChars.split('').map((char: string) => orders[char]).filter((x: any) => x.length > 0).join(separationChar);
result.push({
remark: r,
link: this.genLink(addr, port, 'same', r, client)
});
} else {
this.stream.externalProxy.forEach((ep: any) => {
orders['o'] = ep.remark;
const r = orderChars.split('').map((char: string) => orders[char]).filter((x: any) => x.length > 0).join(separationChar);
result.push({
remark: r,
link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client, ep)
});
});
}
return result;
}
genInboundLinks(remark = '', remarkModel = '-ieo', hostOverride = '') {
const addr = this._resolveAddr(hostOverride);
if (this.clients) {
const links: any[] = [];
this.clients.forEach((client: any) => {
this.genAllLinks(remark, remarkModel, client, hostOverride).forEach((l: any) => {
links.push(l.link);
})
});
return links.join('\r\n');
} else {
if (this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(addr, this.port, 'same', remark);
if (this.protocol == Protocols.WIREGUARD) {
return this.genWireguardConfigs(remark, remarkModel, hostOverride);
}
return '';
}
}
static fromJson(json: any = {}) {
return new Inbound(
json.port,
json.listen,
json.protocol,
Inbound.Settings.fromJson(json.protocol, json.settings),
StreamSettings.fromJson(json.streamSettings),
json.tag,
Sniffing.fromJson(json.sniffing),
json.clientStats
)
}
toJson() {
// Only these protocols use streamSettings
const streamProtocols = [Protocols.VLESS, Protocols.VMESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA];
const result: any = {
port: this.port,
listen: this.listen,
protocol: this.protocol,
settings: this.settings instanceof XrayCommonClass ? this.settings.toJson() : this.settings,
tag: this.tag,
sniffing: this.sniffing.toJson(),
clientStats: this.clientStats
};
// Only add streamSettings if protocol supports it
if (streamProtocols.includes(this.protocol)) {
result.streamSettings = this.stream.toJson();
}
return result;
}
}
Inbound.Settings = class extends XrayCommonClass {
constructor(protocol: any) {
super();
this.protocol = protocol;
}
static getSettings(protocol: any): any {
switch (protocol) {
case Protocols.VMESS: return new Inbound.VmessSettings(protocol);
case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
case Protocols.TUN: return new Inbound.TunSettings(protocol);
case Protocols.HYSTERIA: return new Inbound.HysteriaSettings(protocol);
default: return null;
}
}
static fromJson(protocol: any, json: any): any {
switch (protocol) {
case Protocols.VMESS: return Inbound.VmessSettings.fromJson(json);
case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
case Protocols.HYSTERIA: return Inbound.HysteriaSettings.fromJson(json);
default: return null;
}
}
toJson() {
return {};
}
};
/** Shared user-quota fields and UI helpers for multi-user protocol clients. */
Inbound.ClientBase = class extends XrayCommonClass {
constructor(
email: any = RandomUtil.randomLowerAndNum(8),
limitIp: any = 0,
totalGB: any = 0,
expiryTime: any = 0,
enable: any = true,
tgId: any = '',
subId: any = RandomUtil.randomLowerAndNum(16),
comment: any = '',
reset: any = 0,
created_at: any = undefined,
updated_at: any = undefined,
) {
super();
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
this.comment = comment;
this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
}
static commonArgsFromJson(json: any = {}) {
return [
json.email,
json.limitIp,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
];
}
_clientBaseToJson() {
return {
email: this.email,
limitIp: this.limitIp,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
comment: this.comment,
reset: this.reset,
created_at: this.created_at,
updated_at: this.updated_at,
};
}
get _expiryTime() {
if (this.expiryTime === 0 || this.expiryTime === '') {
return null;
}
if (this.expiryTime < 0) {
return this.expiryTime / -86400000;
}
return dayjs(this.expiryTime);
}
set _expiryTime(t: any) {
if (t == null || t === '') {
this.expiryTime = 0;
} else {
this.expiryTime = t.valueOf();
}
}
get _totalGB() {
return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
}
};
Inbound.VmessSettings = class extends Inbound.Settings {
constructor(protocol: any,
vmesses: any[] = []) {
super(protocol);
this.vmesses = vmesses;
}
indexOfVmessById(id: any) {
return this.vmesses.findIndex((VMESS: any) => VMESS.id === id);
}
addVmess(VMESS: any) {
if (this.indexOfVmessById(VMESS.id) >= 0) {
return false;
}
this.vmesses.push(VMESS);
}
delVmess(VMESS: any) {
const i = this.indexOfVmessById(VMESS.id);
if (i >= 0) {
this.vmesses.splice(i, 1);
}
}
static fromJson(json: any = {}) {
return new Inbound.VmessSettings(
Protocols.VMESS,
(json.clients || []).map((client: any) => Inbound.VmessSettings.VMESS.fromJson(client)),
);
}
toJson() {
return {
clients: Inbound.VmessSettings.toJsonArray(this.vmesses),
};
}
};
Inbound.VmessSettings.VMESS = class extends Inbound.ClientBase {
constructor(
id: any = RandomUtil.randomUUID(),
security: any = USERS_SECURITY.AUTO,
email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any,
) {
super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
this.id = id;
this.security = security;
}
static fromJson(json: any = {}) {
return new Inbound.VmessSettings.VMESS(
json.id,
json.security,
...Inbound.ClientBase.commonArgsFromJson(json),
);
}
toJson() {
return {
id: this.id,
security: this.security,
...this._clientBaseToJson(),
};
}
};
Inbound.VLESSSettings = class extends Inbound.Settings {
constructor(
protocol: any,
vlesses: any[] = [],
decryption: any = "none",
encryption: any = "none",
fallbacks: any[] = [],
testseed: any[] = [],
) {
super(protocol);
this.vlesses = vlesses;
this.decryption = decryption;
this.encryption = encryption;
this.fallbacks = fallbacks;
this.testseed = testseed;
}
addFallback() {
this.fallbacks.push(new Inbound.VLESSSettings.Fallback());
}
delFallback(index: number) {
this.fallbacks.splice(index, 1);
}
// Empty array means "use server defaults" (won't be sent).
// Anything else must be exactly 4 positive integers.
static isValidTestseed(arr: any): boolean {
if (!Array.isArray(arr) || arr.length === 0) return true;
if (arr.length !== 4) return false;
return arr.every((v: any) => Number.isInteger(v) && v > 0);
}
static fromJson(json: any = {}) {
// Preserve a saved testseed only if it's a valid 4-positive-int array; otherwise leave empty
// so toJson omits it and the form falls back to placeholder defaults.
const saved = json.testseed;
const testseed = (Array.isArray(saved)
&& saved.length === 4
&& saved.every((v: any) => Number.isInteger(v) && v > 0))
? saved
: [];
const obj = new Inbound.VLESSSettings(
Protocols.VLESS,
(json.clients || []).map((client: any) => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption,
json.encryption,
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
testseed,
);
return obj;
}
toJson() {
const json: any = {
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
};
if (this.decryption) {
json.decryption = this.decryption;
}
if (this.encryption) {
json.encryption = this.encryption;
}
if (this.fallbacks && this.fallbacks.length > 0) {
json.fallbacks = Inbound.VLESSSettings.toJsonArray(this.fallbacks);
}
// testseed is only meaningful for the exact xtls-rprx-vision flow, and only when
// the user supplied a complete 4-positive-int array. Otherwise omit and let the
// backend fall back to its safe defaults.
const hasVisionFlow = this.vlesses && this.vlesses.some((v: any) => v.flow === TLS_FLOW_CONTROL.VISION);
if (hasVisionFlow
&& Array.isArray(this.testseed)
&& this.testseed.length === 4
&& this.testseed.every((v: any) => Number.isInteger(v) && v > 0)) {
json.testseed = this.testseed;
}
return json;
}
};
Inbound.VLESSSettings.VLESS = class extends Inbound.ClientBase {
constructor(
id: any = RandomUtil.randomUUID(),
flow: any = '',
reverseTag: any = '',
reverseSniffing: any = new Sniffing(),
email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any,
) {
super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
this.id = id;
this.flow = flow;
this.reverseTag = reverseTag;
this.reverseSniffing = reverseSniffing;
}
static fromJson(json: any = {}) {
return new Inbound.VLESSSettings.VLESS(
json.id,
json.flow,
json.reverse?.tag ?? '',
Sniffing.fromJson(json.reverse?.sniffing || {}),
...Inbound.ClientBase.commonArgsFromJson(json),
);
}
toJson() {
const json: any = {
id: this.id,
flow: this.flow,
...this._clientBaseToJson(),
};
if (this.reverseTag) {
json.reverse = {
tag: this.reverseTag,
};
}
return json;
}
};
Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
constructor(name = "", alpn = '', path = '', dest = '', xver = 0) {
super();
this.name = name;
this.alpn = alpn;
this.path = path;
this.dest = dest;
this.xver = xver;
}
toJson() {
return XrayCommonClass.fallbackToJson(this as unknown as FallbackEntry);
}
static fromJson(json: any = []) {
return (json || []).map((f: any) => new Inbound.VLESSSettings.Fallback(
f.name, f.alpn, f.path, f.dest, f.xver,
));
}
};
Inbound.TrojanSettings = class extends Inbound.Settings {
constructor(protocol: any,
trojans: any[] = [],
fallbacks: any[] = [],) {
super(protocol);
this.trojans = trojans;
this.fallbacks = fallbacks;
}
addFallback() {
this.fallbacks.push(new Inbound.TrojanSettings.Fallback());
}
delFallback(index: number) {
this.fallbacks.splice(index, 1);
}
static fromJson(json: any = {}) {
return new Inbound.TrojanSettings(
Protocols.TROJAN,
(json.clients || []).map((client: any) => Inbound.TrojanSettings.Trojan.fromJson(client)),
Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);
}
toJson() {
const json: any = {
clients: Inbound.TrojanSettings.toJsonArray(this.trojans),
};
if (this.fallbacks && this.fallbacks.length > 0) {
json.fallbacks = Inbound.TrojanSettings.toJsonArray(this.fallbacks);
}
return json;
}
};
Inbound.TrojanSettings.Trojan = class extends Inbound.ClientBase {
constructor(
password = RandomUtil.randomSeq(10),
email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any,
) {
super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
this.password = password;
}
toJson() {
return {
password: this.password,
...this._clientBaseToJson(),
};
}
static fromJson(json: any = {}) {
return new Inbound.TrojanSettings.Trojan(
json.password,
...Inbound.ClientBase.commonArgsFromJson(json),
);
}
};
Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
constructor(name = "", alpn = '', path = '', dest = '', xver = 0) {
super();
this.name = name;
this.alpn = alpn;
this.path = path;
this.dest = dest;
this.xver = xver;
}
toJson() {
return XrayCommonClass.fallbackToJson(this as unknown as FallbackEntry);
}
static fromJson(json: any = []) {
return (json || []).map((f: any) => new Inbound.TrojanSettings.Fallback(
f.name, f.alpn, f.path, f.dest, f.xver,
));
}
};
Inbound.ShadowsocksSettings = class extends Inbound.Settings {
constructor(protocol: any,
method: any = SSMethods.BLAKE3_AES_256_GCM,
password: any = RandomUtil.randomShadowsocksPassword(),
network: any = 'tcp',
shadowsockses: any[] = [],
ivCheck = false,
) {
super(protocol);
this.method = method;
this.password = password;
this.network = network;
this.shadowsockses = shadowsockses;
this.ivCheck = ivCheck;
}
static fromJson(json: any = {}) {
return new Inbound.ShadowsocksSettings(
Protocols.SHADOWSOCKS,
json.method,
json.password,
json.network,
(json.clients || []).map((client: any) => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)),
json.ivCheck,
);
}
toJson() {
return {
method: this.method,
password: this.password,
network: this.network,
clients: Inbound.ShadowsocksSettings.toJsonArray(this.shadowsockses),
ivCheck: this.ivCheck,
};
}
};
Inbound.ShadowsocksSettings.Shadowsocks = class extends Inbound.ClientBase {
constructor(
method = '',
password = RandomUtil.randomShadowsocksPassword(),
email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any,
) {
super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
this.method = method;
this.password = password;
}
toJson() {
return {
method: this.method,
password: this.password,
...this._clientBaseToJson(),
};
}
static fromJson(json: any = {}) {
return new Inbound.ShadowsocksSettings.Shadowsocks(
json.method,
json.password,
...Inbound.ClientBase.commonArgsFromJson(json),
);
}
};
Inbound.HysteriaSettings = class extends Inbound.Settings {
constructor(protocol: any, version: any = 2, hysterias: any[] = []) {
super(protocol);
this.version = version;
this.hysterias = hysterias;
}
static fromJson(json: any = {}) {
return new Inbound.HysteriaSettings(
Protocols.HYSTERIA,
json.version ?? 2,
(json.clients || []).map((client: any) => Inbound.HysteriaSettings.Hysteria.fromJson(client)),
);
}
toJson() {
return {
version: this.version,
clients: Inbound.HysteriaSettings.toJsonArray(this.hysterias),
};
}
};
Inbound.HysteriaSettings.Hysteria = class extends Inbound.ClientBase {
constructor(
auth = RandomUtil.randomSeq(10),
email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any,
) {
super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
this.auth = auth;
}
toJson() {
return {
auth: this.auth,
...this._clientBaseToJson(),
};
}
static fromJson(json: any = {}) {
return new Inbound.HysteriaSettings.Hysteria(
json.auth,
...Inbound.ClientBase.commonArgsFromJson(json),
);
}
};
Inbound.TunnelSettings = class extends Inbound.Settings {
constructor(
protocol: any,
rewriteAddress?: any,
rewritePort?: any,
portMap: any[] = [],
allowedNetwork: any = 'tcp,udp',
followRedirect: any = false
) {
super(protocol);
this.rewriteAddress = rewriteAddress;
this.rewritePort = rewritePort;
this.portMap = portMap;
this.allowedNetwork = allowedNetwork;
this.followRedirect = followRedirect;
}
addPortMap(port = '', target = '') {
this.portMap.push({ name: port, value: target });
}
removePortMap(index: number) {
this.portMap.splice(index, 1);
}
static fromJson(json: any = {}) {
return new Inbound.TunnelSettings(
Protocols.TUNNEL,
json.rewriteAddress,
json.rewritePort,
XrayCommonClass.toHeaders(json.portMap),
json.allowedNetwork,
json.followRedirect,
);
}
toJson() {
return {
rewriteAddress: this.rewriteAddress,
rewritePort: this.rewritePort,
portMap: XrayCommonClass.toV2Headers(this.portMap, false),
allowedNetwork: this.allowedNetwork,
followRedirect: this.followRedirect,
};
}
};
Inbound.MixedSettings = class extends Inbound.Settings {
constructor(protocol: any, auth: any = 'password', accounts: any[] = [new Inbound.MixedSettings.SocksAccount()], udp: any = false, ip: any = '127.0.0.1') {
super(protocol);
this.auth = auth;
this.accounts = accounts;
this.udp = udp;
this.ip = ip;
}
addAccount(account: any) {
this.accounts.push(account);
}
delAccount(index: number) {
this.accounts.splice(index, 1);
}
static fromJson(json: any = {}) {
let accounts;
if (json.auth === 'password') {
accounts = json.accounts.map(
(account: any) => Inbound.MixedSettings.SocksAccount.fromJson(account)
)
}
return new Inbound.MixedSettings(
Protocols.MIXED,
json.auth,
accounts,
json.udp,
json.ip,
);
}
toJson() {
return {
auth: this.auth,
accounts: this.auth === 'password' ? this.accounts.map((account: any) => account.toJson()) : undefined,
udp: this.udp,
ip: this.ip,
};
}
};
Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
super();
this.user = user;
this.pass = pass;
}
static fromJson(json: any = {}) {
return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);
}
};
Inbound.HttpSettings = class extends Inbound.Settings {
constructor(
protocol: any,
accounts: any[] = [new Inbound.HttpSettings.HttpAccount()],
allowTransparent: any = false,
) {
super(protocol);
this.accounts = accounts;
this.allowTransparent = allowTransparent;
}
addAccount(account: any) {
this.accounts.push(account);
}
delAccount(index: number) {
this.accounts.splice(index, 1);
}
static fromJson(json: any = {}) {
return new Inbound.HttpSettings(
Protocols.HTTP,
json.accounts.map((account: any) => Inbound.HttpSettings.HttpAccount.fromJson(account)),
json.allowTransparent,
);
}
toJson() {
return {
accounts: Inbound.HttpSettings.toJsonArray(this.accounts),
allowTransparent: this.allowTransparent,
};
}
};
Inbound.HttpSettings.HttpAccount = class extends XrayCommonClass {
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
super();
this.user = user;
this.pass = pass;
}
static fromJson(json: any = {}) {
return new Inbound.HttpSettings.HttpAccount(json.user, json.pass);
}
};
Inbound.WireguardSettings = class extends XrayCommonClass {
constructor(
protocol?: any,
mtu: any = 1420,
secretKey: any = Wireguard.generateKeypair().privateKey,
peers: any[] = [new Inbound.WireguardSettings.Peer()],
noKernelTun: any = false
) {
super();
this.protocol = protocol;
this.mtu = mtu;
this.secretKey = secretKey;
this.pubKey = secretKey.length > 0 ? Wireguard.generateKeypair(secretKey).publicKey : '';
this.peers = peers;
this.noKernelTun = noKernelTun;
}
addPeer() {
this.peers.push(new Inbound.WireguardSettings.Peer(null, null, '', ['10.0.0.' + (this.peers.length + 2)]));
}
delPeer(index: number) {
this.peers.splice(index, 1);
}
static fromJson(json: any = {}) {
return new Inbound.WireguardSettings(
Protocols.WIREGUARD,
json.mtu,
json.secretKey,
json.peers.map((peer: any) => Inbound.WireguardSettings.Peer.fromJson(peer)),
json.noKernelTun,
);
}
toJson() {
return {
mtu: this.mtu ?? undefined,
secretKey: this.secretKey,
peers: Inbound.WireguardSettings.Peer.toJsonArray(this.peers),
noKernelTun: this.noKernelTun,
};
}
};
Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
constructor(privateKey?: any, publicKey?: any, psk: any = '', allowedIPs: any[] = ['10.0.0.2/32'], keepAlive: any = 0) {
super();
this.privateKey = privateKey
this.publicKey = publicKey;
if (!this.publicKey) {
[this.publicKey, this.privateKey] = Object.values(Wireguard.generateKeypair())
}
this.psk = psk;
allowedIPs.forEach((a: any, index: number) => {
if (a.length > 0 && !a.includes('/')) allowedIPs[index] += '/32';
})
this.allowedIPs = allowedIPs;
this.keepAlive = keepAlive;
}
static fromJson(json: any = {}) {
return new Inbound.WireguardSettings.Peer(
json.privateKey,
json.publicKey,
json.preSharedKey,
json.allowedIPs,
json.keepAlive
);
}
toJson() {
this.allowedIPs.forEach((a: any, index: number) => {
if (a.length > 0 && !a.includes('/')) this.allowedIPs[index] += '/32';
});
return {
privateKey: this.privateKey,
publicKey: this.publicKey,
preSharedKey: this.psk.length > 0 ? this.psk : undefined,
allowedIPs: this.allowedIPs,
keepAlive: this.keepAlive ?? undefined,
};
}
};
Inbound.TunSettings = class extends Inbound.Settings {
constructor(
protocol: any,
name: any = 'xray0',
mtu: any = 1500,
gateway: any[] = [],
dns: any[] = [],
userLevel: any = 0,
autoSystemRoutingTable: any[] = [],
autoOutboundsInterface = 'auto'
) {
super(protocol);
this.name = name;
this.mtu = Number(mtu) || 1500;
this.gateway = Array.isArray(gateway) ? gateway : [];
this.dns = Array.isArray(dns) ? dns : [];
this.userLevel = userLevel;
this.autoSystemRoutingTable = Array.isArray(autoSystemRoutingTable) ? autoSystemRoutingTable : [];
this.autoOutboundsInterface = autoOutboundsInterface;
}
static fromJson(json: any = {}) {
const rawMtu = json.mtu ?? json.MTU;
const mtu = Array.isArray(rawMtu) ? rawMtu[0] : rawMtu;
return new Inbound.TunSettings(
Protocols.TUN,
json.name ?? 'xray0',
mtu ?? 1500,
json.gateway ?? json.Gateway ?? [],
json.dns ?? json.DNS ?? [],
json.userLevel ?? 0,
json.autoSystemRoutingTable ?? [],
Object.prototype.hasOwnProperty.call(json, 'autoOutboundsInterface') ? json.autoOutboundsInterface : 'auto'
);
}
toJson() {
return {
name: this.name || 'xray0',
mtu: Number(this.mtu) || 1500,
gateway: this.gateway,
dns: this.dns,
userLevel: this.userLevel || 0,
autoSystemRoutingTable: this.autoSystemRoutingTable,
autoOutboundsInterface: this.autoOutboundsInterface,
};
}
};