Compare commits
1
Commits
766ac225ef
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3ff896fdf |
@@ -3,7 +3,6 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: ghcr.io/jasonwong1991/easy_proxies:latest
|
||||
container_name: easy-proxies
|
||||
restart: unless-stopped
|
||||
# 方式一:使用主机网络模式(推荐,支持端口自动重分配)
|
||||
|
||||
@@ -157,11 +157,52 @@ const emptyVmessFields: VmessFormFields = {
|
||||
alpn: 'http/1.1',
|
||||
}
|
||||
|
||||
interface VlessFormFields {
|
||||
name: string
|
||||
add: string
|
||||
port: string
|
||||
id: string
|
||||
encryption: string
|
||||
flow: string
|
||||
net: string
|
||||
type: string
|
||||
host: string
|
||||
path: string
|
||||
security: string
|
||||
sni: string
|
||||
alpn: string
|
||||
fp: string
|
||||
pbk: string
|
||||
sid: string
|
||||
allowInsecure: string
|
||||
}
|
||||
|
||||
const emptyVlessFields: VlessFormFields = {
|
||||
name: '',
|
||||
add: '',
|
||||
port: '443',
|
||||
id: '',
|
||||
encryption: 'none',
|
||||
flow: '',
|
||||
net: 'tcp',
|
||||
type: 'none',
|
||||
host: '',
|
||||
path: '',
|
||||
security: 'tls',
|
||||
sni: '',
|
||||
alpn: '',
|
||||
fp: '',
|
||||
pbk: '',
|
||||
sid: '',
|
||||
allowInsecure: '',
|
||||
}
|
||||
|
||||
interface ParsedShareLink {
|
||||
name?: string
|
||||
protocol?: string
|
||||
valid: boolean
|
||||
vmess?: VmessFormFields
|
||||
vless?: VlessFormFields
|
||||
}
|
||||
|
||||
function safeBase64Decode(value: string): string | null {
|
||||
@@ -228,6 +269,63 @@ function buildVmessUri(fields: VmessFormFields): string {
|
||||
return `vmess://${safeBase64Encode(JSON.stringify(payload))}`
|
||||
}
|
||||
|
||||
function parseVlessUri(uri: string): VlessFormFields | null {
|
||||
try {
|
||||
const parsed = new URL(uri)
|
||||
const uuid = parsed.username
|
||||
if (!uuid) return null
|
||||
|
||||
const server = parsed.hostname
|
||||
const port = parsed.port || '443'
|
||||
const query = parsed.searchParams
|
||||
const name = parsed.hash ? decodeURIComponent(parsed.hash.slice(1)) : ''
|
||||
|
||||
return {
|
||||
name,
|
||||
add: server,
|
||||
port,
|
||||
id: uuid,
|
||||
encryption: query.get('encryption') || 'none',
|
||||
flow: query.get('flow') || '',
|
||||
net: query.get('type') || 'tcp',
|
||||
type: query.get('type') || 'none',
|
||||
host: query.get('host') || '',
|
||||
path: query.get('path') || '',
|
||||
security: query.get('security') || 'tls',
|
||||
sni: query.get('sni') || '',
|
||||
alpn: query.get('alpn') || '',
|
||||
fp: query.get('fp') || '',
|
||||
pbk: query.get('pbk') || '',
|
||||
sid: query.get('sid') || '',
|
||||
allowInsecure: query.get('allowInsecure') || '',
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function buildVlessUri(fields: VlessFormFields): string {
|
||||
const params = new URLSearchParams()
|
||||
params.set('encryption', fields.encryption || 'none')
|
||||
if (fields.flow) params.set('flow', fields.flow)
|
||||
if (fields.net && fields.net !== 'tcp') params.set('type', fields.net)
|
||||
if (fields.host) params.set('host', fields.host)
|
||||
if (fields.path) params.set('path', fields.path)
|
||||
if (fields.security && fields.security !== 'tls') {
|
||||
params.set('security', fields.security)
|
||||
}
|
||||
if (fields.sni) params.set('sni', fields.sni)
|
||||
if (fields.alpn) params.set('alpn', fields.alpn)
|
||||
if (fields.fp) params.set('fp', fields.fp)
|
||||
if (fields.pbk) params.set('pbk', fields.pbk)
|
||||
if (fields.sid) params.set('sid', fields.sid)
|
||||
if (fields.allowInsecure) params.set('allowInsecure', fields.allowInsecure)
|
||||
|
||||
const encodedParams = params.toString()
|
||||
const uri = `vless://${fields.id}@${fields.add}:${fields.port || '443'}?${encodedParams}#${encodeURIComponent(fields.name)}`
|
||||
return uri
|
||||
}
|
||||
|
||||
function parseGenericName(uri: string): string | undefined {
|
||||
try {
|
||||
const parsed = new URL(uri)
|
||||
@@ -255,6 +353,16 @@ function parseShareLink(uri: string): ParsedShareLink {
|
||||
}
|
||||
}
|
||||
|
||||
if (protocol === 'vless') {
|
||||
const vless = parseVlessUri(trimmed)
|
||||
return {
|
||||
protocol,
|
||||
vless: vless || undefined,
|
||||
name: vless?.name?.trim() || undefined,
|
||||
valid: !!vless,
|
||||
}
|
||||
}
|
||||
|
||||
return { protocol, name: parseGenericName(trimmed), valid: true }
|
||||
}
|
||||
|
||||
@@ -278,6 +386,8 @@ export default function ManagePanel() {
|
||||
const [parseHint, setParseHint] = useState('')
|
||||
const [vmessFields, setVmessFields] = useState<VmessFormFields>(emptyVmessFields)
|
||||
const [vmessEditorOpen, setVmessEditorOpen] = useState(false)
|
||||
const [vlessFields, setVlessFields] = useState<VlessFormFields>(emptyVlessFields)
|
||||
const [vlessEditorOpen, setVlessEditorOpen] = useState(false)
|
||||
|
||||
// Delete confirm
|
||||
const [deleteTarget, setDeleteTarget] = useState<string | null>(null)
|
||||
@@ -470,9 +580,18 @@ export default function ManagePanel() {
|
||||
if (parsed.protocol === 'vmess' && parsed.vmess) {
|
||||
setVmessFields(parsed.vmess)
|
||||
setVmessEditorOpen(true)
|
||||
setVlessFields(emptyVlessFields)
|
||||
setVlessEditorOpen(false)
|
||||
} else if (parsed.protocol === 'vless' && parsed.vless) {
|
||||
setVlessFields(parsed.vless)
|
||||
setVlessEditorOpen(true)
|
||||
setVmessFields(emptyVmessFields)
|
||||
setVmessEditorOpen(false)
|
||||
} else if (!value.trim()) {
|
||||
setVmessFields(emptyVmessFields)
|
||||
setVmessEditorOpen(false)
|
||||
setVlessFields(emptyVlessFields)
|
||||
setVlessEditorOpen(false)
|
||||
}
|
||||
|
||||
setForm(prev => {
|
||||
@@ -492,6 +611,8 @@ export default function ManagePanel() {
|
||||
setParseHint('')
|
||||
setVmessFields(emptyVmessFields)
|
||||
setVmessEditorOpen(false)
|
||||
setVlessFields(emptyVlessFields)
|
||||
setVlessEditorOpen(false)
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
@@ -508,6 +629,8 @@ export default function ManagePanel() {
|
||||
setNameManuallyEdited(false)
|
||||
setVmessFields(emptyVmessFields)
|
||||
setVmessEditorOpen(false)
|
||||
setVlessFields(emptyVlessFields)
|
||||
setVlessEditorOpen(false)
|
||||
applyParsedLink(node.uri, true)
|
||||
setModalOpen(true)
|
||||
}
|
||||
@@ -530,6 +653,20 @@ export default function ManagePanel() {
|
||||
})
|
||||
}
|
||||
|
||||
const updateVlessFields = (patch: Partial<VlessFormFields>) => {
|
||||
setVlessFields(prev => {
|
||||
const next = { ...prev, ...patch }
|
||||
const nextUri = buildVlessUri(next)
|
||||
setForm(current => ({
|
||||
...current,
|
||||
uri: nextUri,
|
||||
name: !nameManuallyEdited ? (next.name || current.name) : current.name,
|
||||
}))
|
||||
setParseHint(`已识别协议:vless${next.name ? `,建议名称:${next.name}` : ''}`)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!form.name.trim()) { setFormError('节点名称不能为空'); return }
|
||||
@@ -1155,6 +1292,9 @@ export default function ManagePanel() {
|
||||
if (vmessEditorOpen) {
|
||||
setVmessFields(prev => ({ ...prev, ps: value }))
|
||||
}
|
||||
if (vlessEditorOpen) {
|
||||
setVlessFields(prev => ({ ...prev, name: value }))
|
||||
}
|
||||
}}
|
||||
disabled={!!editingNode}
|
||||
/>
|
||||
@@ -1168,7 +1308,7 @@ export default function ManagePanel() {
|
||||
onChange={(e) => handleUriInputChange(e.target.value)}
|
||||
/>
|
||||
<div className="label text-base-content/50 mt-1 flex flex-col items-start gap-1">
|
||||
<span>粘贴分享链接后会自动解析并回填字段;vmess 支持结构化编辑,新增/编辑都支持。</span>
|
||||
<span>粘贴分享链接后会自动解析并回填字段;vmess/vless 支持结构化编辑,新增/编辑都支持。</span>
|
||||
{parseHint && <span className="text-info">{parseHint}</span>}
|
||||
{parseShareLink(form.uri).protocol === 'vmess' && (
|
||||
<button
|
||||
@@ -1176,7 +1316,16 @@ export default function ManagePanel() {
|
||||
className="btn btn-xs btn-outline btn-info mt-1"
|
||||
onClick={() => setVmessEditorOpen(open => !open)}
|
||||
>
|
||||
{vmessEditorOpen ? '收起结构化字段' : '展开结构化字段'}
|
||||
{vmessEditorOpen ? '收起 VMess 结构化字段' : '展开 VMess 结构化字段'}
|
||||
</button>
|
||||
)}
|
||||
{parseShareLink(form.uri).protocol === 'vless' && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-outline btn-warning mt-1"
|
||||
onClick={() => setVlessEditorOpen(open => !open)}
|
||||
>
|
||||
{vlessEditorOpen ? '收起 VLESS 结构化字段' : '展开 VLESS 结构化字段'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1246,6 +1395,87 @@ export default function ManagePanel() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{vlessEditorOpen && parseShareLink(form.uri).protocol === 'vless' && (
|
||||
<div className="rounded-xl border border-warning/20 bg-warning/5 p-4 mb-4 space-y-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="font-semibold text-sm">VLESS 结构化字段</div>
|
||||
<div className="text-xs text-base-content/50">修改下面字段时,会自动重新生成上方 URI。</div>
|
||||
</div>
|
||||
<button type="button" className="btn btn-xs btn-ghost" onClick={() => setVlessEditorOpen(false)}>收起</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">节点名称</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.name} onChange={(e) => updateVlessFields({ name: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">地址</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.add} onChange={(e) => updateVlessFields({ add: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">端口</legend>
|
||||
<input type="number" className="input input-sm w-full" value={vlessFields.port} onChange={(e) => updateVlessFields({ port: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">UUID</legend>
|
||||
<input type="text" className="input input-sm w-full font-mono text-xs" value={vlessFields.id} onChange={(e) => updateVlessFields({ id: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">加密</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.encryption} onChange={(e) => updateVlessFields({ encryption: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Flow</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.flow} onChange={(e) => updateVlessFields({ flow: e.target.value })} placeholder="如 xtls-rprx-vision" />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">传输</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.net} onChange={(e) => updateVlessFields({ net: e.target.value })} placeholder="tcp/ws/grpc/httpupgrade" />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">type</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.type} onChange={(e) => updateVlessFields({ type: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Host</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.host} onChange={(e) => updateVlessFields({ host: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Path</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.path} onChange={(e) => updateVlessFields({ path: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Security</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.security} onChange={(e) => updateVlessFields({ security: e.target.value })} placeholder="tls/reality/none" />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">SNI</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.sni} onChange={(e) => updateVlessFields({ sni: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">ALPN</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.alpn} onChange={(e) => updateVlessFields({ alpn: e.target.value })} placeholder="如 http/1.1 或 h2,http/1.1" />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Fingerprint</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.fp} onChange={(e) => updateVlessFields({ fp: e.target.value })} placeholder="如 chrome/firefox" />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Public Key (reality)</legend>
|
||||
<input type="text" className="input input-sm w-full font-mono text-xs" value={vlessFields.pbk} onChange={(e) => updateVlessFields({ pbk: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Short ID (reality)</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.sid} onChange={(e) => updateVlessFields({ sid: e.target.value })} />
|
||||
</fieldset>
|
||||
<fieldset className="fieldset col-span-2">
|
||||
<legend className="fieldset-legend">Allow Insecure</legend>
|
||||
<input type="text" className="input input-sm w-full" value={vlessFields.allowInsecure} onChange={(e) => updateVlessFields({ allowInsecure: e.target.value })} placeholder="1/true 或留空" />
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<fieldset className="fieldset mb-3">
|
||||
<legend className="fieldset-legend">本地代理端口</legend>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user