mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-26 07:08:01 +00:00
feat(inbound): Advanced XHTTP and external TLS proxy settings (#4491)
* ✨ Introduce extended XHTTP and external proxy settings * ✨ Add custom SNI for proxy * ✨ Add previous changes into React version of app * fix(sub): isolate per-proxy tlsSettings during external-proxy iteration cloneMap (Clash) is shallow and `newStream := stream` (JSON) is an alias, so tlsSettings was shared across iterations. The new applyExternalProxyTLSToStream mutates it, leaking one proxy's serverName/fingerprint/alpn into the next (only overwritten when the next proxy explicitly sets the same field). Add cloneStreamForExternalProxy: shallow clones the top-level stream plus deep clones tlsSettings and tlsSettings.settings. Regression test locks in that proxy B does not inherit proxy A's fingerprint/alpn when B leaves them unset.
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>3X-UI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="message"></div>
|
||||
|
||||
@@ -499,14 +499,13 @@ export class HTTPUpgradeStreamSettings extends XrayCommonClass {
|
||||
// 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. Client-only fields (uplinkHTTPMethod, uplinkChunkSize,
|
||||
// noGRPCHeader, scMinPostsIntervalMs, xmux, downloadSettings) belong on
|
||||
// the outbound class instead.
|
||||
// live here. Most client-only fields (uplinkChunkSize, noGRPCHeader,
|
||||
// scMinPostsIntervalMs, xmux, downloadSettings) belong on the outbound
|
||||
// class instead.
|
||||
//
|
||||
// `headers` is technically client-only at runtime (xray's listener
|
||||
// doesn't read it) but we keep it here so the admin can set request
|
||||
// headers that get embedded into the share link's `extra` blob — the
|
||||
// client picks them up from there.
|
||||
// `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
|
||||
@@ -533,6 +532,7 @@ export class xHTTPStreamSettings extends XrayCommonClass {
|
||||
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 = [],
|
||||
) {
|
||||
super();
|
||||
@@ -556,6 +556,7 @@ export class xHTTPStreamSettings extends XrayCommonClass {
|
||||
this.scMaxBufferedPosts = scMaxBufferedPosts;
|
||||
this.scStreamUpServerSecs = scStreamUpServerSecs;
|
||||
this.serverMaxHeaderBytes = serverMaxHeaderBytes;
|
||||
this.uplinkHTTPMethod = uplinkHTTPMethod;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@@ -589,6 +590,7 @@ export class xHTTPStreamSettings extends XrayCommonClass {
|
||||
json.scMaxBufferedPosts,
|
||||
json.scStreamUpServerSecs,
|
||||
json.serverMaxHeaderBytes,
|
||||
json.uplinkHTTPMethod,
|
||||
XrayCommonClass.toHeaders(json.headers),
|
||||
);
|
||||
}
|
||||
@@ -615,6 +617,7 @@ export class xHTTPStreamSettings extends XrayCommonClass {
|
||||
scMaxBufferedPosts: this.scMaxBufferedPosts,
|
||||
scStreamUpServerSecs: this.scStreamUpServerSecs,
|
||||
serverMaxHeaderBytes: this.serverMaxHeaderBytes,
|
||||
uplinkHTTPMethod: this.uplinkHTTPMethod,
|
||||
headers: XrayCommonClass.toV2Headers(this.headers, false),
|
||||
};
|
||||
}
|
||||
@@ -1584,10 +1587,9 @@ export class Inbound extends XrayCommonClass {
|
||||
// - server-only (noSSEHeader, scMaxBufferedPosts,
|
||||
// scStreamUpServerSecs, serverMaxHeaderBytes) — client wouldn't
|
||||
// read them, so emitting them just bloats the URL.
|
||||
// - client-only (headers, uplinkHTTPMethod, uplinkChunkSize,
|
||||
// noGRPCHeader, scMinPostsIntervalMs, xmux, downloadSettings) —
|
||||
// not on the inbound class at all; the client configures them
|
||||
// locally.
|
||||
// - 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.
|
||||
@@ -1607,21 +1609,35 @@ export class Inbound extends XrayCommonClass {
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof xhttp.mode === 'string' && xhttp.mode.length > 0) {
|
||||
extra.mode = xhttp.mode;
|
||||
}
|
||||
|
||||
const stringFields = [
|
||||
"uplinkHTTPMethod",
|
||||
"sessionPlacement", "sessionKey",
|
||||
"seqPlacement", "seqKey",
|
||||
"uplinkDataPlacement", "uplinkDataKey",
|
||||
"scMaxEachPostBytes",
|
||||
"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.
|
||||
@@ -1680,6 +1696,29 @@ export class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
static externalProxyAlpn(value) {
|
||||
if (Array.isArray(value)) return value.filter(Boolean).join(',');
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
|
||||
static applyExternalProxyTLSParams(externalProxy, params, security) {
|
||||
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, obj, security) {
|
||||
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) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
@@ -1894,7 +1933,7 @@ export class Inbound extends XrayCommonClass {
|
||||
this.sniffing = new Sniffing();
|
||||
}
|
||||
|
||||
genVmessLink(address = '', port = this.port, forceTls, remark = '', clientId, security) {
|
||||
genVmessLink(address = '', port = this.port, forceTls, remark = '', clientId, security, externalProxy = null) {
|
||||
if (this.protocol !== Protocols.VMESS) {
|
||||
return '';
|
||||
}
|
||||
@@ -1958,11 +1997,12 @@ export class Inbound extends XrayCommonClass {
|
||||
obj.alpn = this.stream.tls.alpn.join(',');
|
||||
}
|
||||
}
|
||||
Inbound.applyExternalProxyTLSObj(externalProxy, obj, tls);
|
||||
|
||||
return 'vmess://' + Base64.encode(JSON.stringify(obj, null, 2));
|
||||
}
|
||||
|
||||
genVLESSLink(address = '', port = this.port, forceTls, remark = '', clientId, flow) {
|
||||
genVLESSLink(address = '', port = this.port, forceTls, remark = '', clientId, flow, externalProxy = null) {
|
||||
const uuid = clientId;
|
||||
const type = this.stream.network;
|
||||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
@@ -2028,6 +2068,7 @@ export class Inbound extends XrayCommonClass {
|
||||
params.set("flow", flow);
|
||||
}
|
||||
}
|
||||
Inbound.applyExternalProxyTLSParams(externalProxy, params, security);
|
||||
}
|
||||
|
||||
else if (security === 'reality') {
|
||||
@@ -2064,7 +2105,7 @@ export class Inbound extends XrayCommonClass {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
genSSLink(address = '', port = this.port, forceTls, remark = '', clientPassword) {
|
||||
genSSLink(address = '', port = this.port, forceTls, remark = '', clientPassword, externalProxy = null) {
|
||||
let settings = this.settings;
|
||||
const type = this.stream.network;
|
||||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
@@ -2126,6 +2167,7 @@ export class Inbound extends XrayCommonClass {
|
||||
params.set("sni", this.stream.tls.sni);
|
||||
}
|
||||
}
|
||||
Inbound.applyExternalProxyTLSParams(externalProxy, params, security);
|
||||
}
|
||||
|
||||
|
||||
@@ -2142,7 +2184,7 @@ export class Inbound extends XrayCommonClass {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
genTrojanLink(address = '', port = this.port, forceTls, remark = '', clientPassword) {
|
||||
genTrojanLink(address = '', port = this.port, forceTls, remark = '', clientPassword, externalProxy = null) {
|
||||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
const type = this.stream.network;
|
||||
const params = new Map();
|
||||
@@ -2203,6 +2245,7 @@ export class Inbound extends XrayCommonClass {
|
||||
params.set("sni", this.stream.tls.sni);
|
||||
}
|
||||
}
|
||||
Inbound.applyExternalProxyTLSParams(externalProxy, params, security);
|
||||
}
|
||||
|
||||
else if (security === 'reality') {
|
||||
@@ -2344,16 +2387,16 @@ export class Inbound extends XrayCommonClass {
|
||||
return links.join('\r\n');
|
||||
}
|
||||
|
||||
genLink(address = '', port = this.port, forceTls = 'same', remark = '', client) {
|
||||
genLink(address = '', port = this.port, forceTls = 'same', remark = '', client, externalProxy = null) {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
return this.genVmessLink(address, port, forceTls, remark, client.id, client.security);
|
||||
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);
|
||||
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 : '');
|
||||
return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : '', externalProxy);
|
||||
case Protocols.TROJAN:
|
||||
return this.genTrojanLink(address, port, forceTls, remark, client.password);
|
||||
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 '';
|
||||
@@ -2384,7 +2427,7 @@ export class Inbound extends XrayCommonClass {
|
||||
let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar);
|
||||
result.push({
|
||||
remark: r,
|
||||
link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client)
|
||||
link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client, ep)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1407,10 +1407,24 @@ export class Outbound extends CommonClass {
|
||||
});
|
||||
}
|
||||
// Bidirectional string fields carried in the extra block
|
||||
const xFields = ["sessionPlacement", "sessionKey", "seqPlacement", "seqKey", "uplinkDataPlacement", "uplinkDataKey", "scMaxEachPostBytes"];
|
||||
const xFields = [
|
||||
"uplinkHTTPMethod",
|
||||
"sessionPlacement", "sessionKey",
|
||||
"seqPlacement", "seqKey",
|
||||
"uplinkDataPlacement", "uplinkDataKey",
|
||||
"scMaxEachPostBytes", "scMinPostsIntervalMs",
|
||||
];
|
||||
xFields.forEach(k => {
|
||||
if (typeof json[k] === 'string' && json[k]) xh[k] = json[k];
|
||||
});
|
||||
if (typeof json.uplinkChunkSize === 'number' && json.uplinkChunkSize !== 0) xh.uplinkChunkSize = json.uplinkChunkSize;
|
||||
if (typeof json.uplinkChunkSize === 'string' && json.uplinkChunkSize) xh.uplinkChunkSize = json.uplinkChunkSize;
|
||||
if (json.noGRPCHeader === true) xh.noGRPCHeader = true;
|
||||
if (json.xmux && typeof json.xmux === 'object') {
|
||||
xh.xmux = json.xmux;
|
||||
xh.enableXmux = true;
|
||||
}
|
||||
if (json.downloadSettings && typeof json.downloadSettings === 'object') xh.downloadSettings = json.downloadSettings;
|
||||
// Headers — VMess extra emits them as a {name: value} map
|
||||
if (json.headers && typeof json.headers === 'object' && !Array.isArray(json.headers)) {
|
||||
xh.headers = Object.entries(json.headers).map(([name, value]) => ({ name, value }));
|
||||
@@ -1487,10 +1501,24 @@ export class Outbound extends CommonClass {
|
||||
});
|
||||
if (!xh.mode && typeof extra.mode === 'string' && extra.mode) xh.mode = extra.mode;
|
||||
// Bidirectional string fields carried inside the extra block
|
||||
const xFields = ["sessionPlacement", "sessionKey", "seqPlacement", "seqKey", "uplinkDataPlacement", "uplinkDataKey", "scMaxEachPostBytes"];
|
||||
const xFields = [
|
||||
"uplinkHTTPMethod",
|
||||
"sessionPlacement", "sessionKey",
|
||||
"seqPlacement", "seqKey",
|
||||
"uplinkDataPlacement", "uplinkDataKey",
|
||||
"scMaxEachPostBytes", "scMinPostsIntervalMs",
|
||||
];
|
||||
xFields.forEach(k => {
|
||||
if (typeof extra[k] === 'string' && extra[k]) xh[k] = extra[k];
|
||||
});
|
||||
if (typeof extra.uplinkChunkSize === 'number' && extra.uplinkChunkSize !== 0) xh.uplinkChunkSize = extra.uplinkChunkSize;
|
||||
if (typeof extra.uplinkChunkSize === 'string' && extra.uplinkChunkSize) xh.uplinkChunkSize = extra.uplinkChunkSize;
|
||||
if (extra.noGRPCHeader === true) xh.noGRPCHeader = true;
|
||||
if (extra.xmux && typeof extra.xmux === 'object') {
|
||||
xh.xmux = extra.xmux;
|
||||
xh.enableXmux = true;
|
||||
}
|
||||
if (extra.downloadSettings && typeof extra.downloadSettings === 'object') xh.downloadSettings = extra.downloadSettings;
|
||||
// Headers — extra emits them as a {name: value} map
|
||||
if (extra.headers && typeof extra.headers === 'object' && !Array.isArray(extra.headers)) {
|
||||
xh.headers = Object.entries(extra.headers).map(([name, value]) => ({ name, value }));
|
||||
@@ -2354,4 +2382,4 @@ Outbound.HysteriaSettings = class extends CommonClass {
|
||||
version: this.version
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -319,6 +319,9 @@ export default function InboundFormModal({
|
||||
dest: window.location.hostname,
|
||||
port: ib.port,
|
||||
remark: '',
|
||||
sni: '',
|
||||
fingerprint: '',
|
||||
alpn: [],
|
||||
}];
|
||||
} else {
|
||||
ib.stream.externalProxy = [];
|
||||
@@ -1617,6 +1620,14 @@ export default function InboundFormModal({
|
||||
)}
|
||||
<Form.Item label="Server Max Header Bytes"><InputNumber value={ib.stream.xhttp.serverMaxHeaderBytes} min={0} placeholder="0 (default)" onChange={(v) => { ib.stream.xhttp.serverMaxHeaderBytes = Number(v) || 0; refresh(); }} /></Form.Item>
|
||||
<Form.Item label="Padding Bytes"><Input value={ib.stream.xhttp.xPaddingBytes} onChange={(e) => { ib.stream.xhttp.xPaddingBytes = e.target.value; refresh(); }} /></Form.Item>
|
||||
<Form.Item label="Uplink HTTP Method">
|
||||
<Select value={ib.stream.xhttp.uplinkHTTPMethod || ''} onChange={(v) => { ib.stream.xhttp.uplinkHTTPMethod = v; refresh(); }}>
|
||||
<Select.Option value="">Default (POST)</Select.Option>
|
||||
<Select.Option value="POST">POST</Select.Option>
|
||||
<Select.Option value="PUT">PUT</Select.Option>
|
||||
<Select.Option value="GET" disabled={ib.stream.xhttp.mode !== 'packet-up'}>GET (packet-up only)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="Padding Obfs Mode"><Switch checked={!!ib.stream.xhttp.xPaddingObfsMode} onChange={(v) => { ib.stream.xhttp.xPaddingObfsMode = v; refresh(); }} /></Form.Item>
|
||||
{ib.stream.xhttp.xPaddingObfsMode && (
|
||||
<>
|
||||
@@ -1686,34 +1697,51 @@ export default function InboundFormModal({
|
||||
<Switch checked={externalProxyOn} onChange={setExternalProxy} />
|
||||
{externalProxyOn && (
|
||||
<Button size="small" type="primary" style={{ marginLeft: 10 }}
|
||||
onClick={() => { ib.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '' }); refresh(); }}>
|
||||
onClick={() => { ib.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '', sni: '', fingerprint: '', alpn: [] }); refresh(); }}>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
)}
|
||||
</Form.Item>
|
||||
{externalProxyOn && (
|
||||
<Form.Item wrapperCol={{ span: 24 }}>
|
||||
{(ib.stream.externalProxy as { forceTls: string; dest: string; port: number; remark: string }[]).map((row, idx) => (
|
||||
<Space.Compact key={`ep-${idx}`} style={{ margin: '8px 0' }} block>
|
||||
<Tooltip title="Force TLS">
|
||||
<Select value={row.forceTls} style={{ width: '20%' }} onChange={(v) => { row.forceTls = v; refresh(); }}>
|
||||
<Select.Option value="same">{t('pages.inbounds.same')}</Select.Option>
|
||||
<Select.Option value="none">{t('none')}</Select.Option>
|
||||
<Select.Option value="tls">TLS</Select.Option>
|
||||
</Select>
|
||||
</Tooltip>
|
||||
<Input style={{ width: '30%' }} value={row.dest} placeholder={t('host')}
|
||||
onChange={(e) => { row.dest = e.target.value; refresh(); }} />
|
||||
<Tooltip title={t('pages.inbounds.port')}>
|
||||
<InputNumber value={row.port} style={{ width: '15%' }} min={1} max={65535}
|
||||
onChange={(v) => { row.port = Number(v) || 0; refresh(); }} />
|
||||
</Tooltip>
|
||||
<Input style={{ width: '25%' }} value={row.remark} placeholder={t('pages.inbounds.remark')}
|
||||
onChange={(e) => { row.remark = e.target.value; refresh(); }} />
|
||||
<InputAddon onClick={() => { ib.stream.externalProxy.splice(idx, 1); refresh(); }}>
|
||||
<MinusOutlined />
|
||||
</InputAddon>
|
||||
</Space.Compact>
|
||||
{(ib.stream.externalProxy as { forceTls: string; dest: string; port: number; remark: string; sni?: string; fingerprint?: string; alpn?: string[] }[]).map((row, idx) => (
|
||||
<div key={`ep-${idx}`} style={{ margin: '8px 0' }}>
|
||||
<Space.Compact block>
|
||||
<Tooltip title="Force TLS">
|
||||
<Select value={row.forceTls} style={{ width: '20%' }} onChange={(v) => { row.forceTls = v; refresh(); }}>
|
||||
<Select.Option value="same">{t('pages.inbounds.same')}</Select.Option>
|
||||
<Select.Option value="none">{t('none')}</Select.Option>
|
||||
<Select.Option value="tls">TLS</Select.Option>
|
||||
</Select>
|
||||
</Tooltip>
|
||||
<Input style={{ width: '30%' }} value={row.dest} placeholder={t('host')}
|
||||
onChange={(e) => { row.dest = e.target.value; refresh(); }} />
|
||||
<Tooltip title={t('pages.inbounds.port')}>
|
||||
<InputNumber value={row.port} style={{ width: '15%' }} min={1} max={65535}
|
||||
onChange={(v) => { row.port = Number(v) || 0; refresh(); }} />
|
||||
</Tooltip>
|
||||
<Input style={{ width: '25%' }} value={row.remark} placeholder={t('pages.inbounds.remark')}
|
||||
onChange={(e) => { row.remark = e.target.value; refresh(); }} />
|
||||
<InputAddon onClick={() => { ib.stream.externalProxy.splice(idx, 1); refresh(); }}>
|
||||
<MinusOutlined />
|
||||
</InputAddon>
|
||||
</Space.Compact>
|
||||
{row.forceTls === 'tls' && (
|
||||
<Space.Compact style={{ marginTop: 6 }} block>
|
||||
<Input style={{ width: '30%' }} value={row.sni || ''} placeholder="SNI (defaults to host)"
|
||||
onChange={(e) => { row.sni = e.target.value; refresh(); }} />
|
||||
<Select value={row.fingerprint || ''} style={{ width: '30%' }} placeholder="Fingerprint"
|
||||
onChange={(v) => { row.fingerprint = v; refresh(); }}>
|
||||
<Select.Option value="">Default</Select.Option>
|
||||
{FINGERPRINTS.map((fp) => <Select.Option key={fp} value={fp}>{fp}</Select.Option>)}
|
||||
</Select>
|
||||
<Select mode="multiple" value={row.alpn || []} style={{ width: '40%' }} placeholder="ALPN"
|
||||
onChange={(v) => { row.alpn = v; refresh(); }}>
|
||||
{ALPNS.map((alpn) => <Select.Option key={alpn} value={alpn}>{alpn}</Select.Option>)}
|
||||
</Select>
|
||||
</Space.Compact>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user