Compare commits
1
Commits
766ac225ef
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3ff896fdf |
@@ -3,7 +3,6 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: ghcr.io/jasonwong1991/easy_proxies:latest
|
|
||||||
container_name: easy-proxies
|
container_name: easy-proxies
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# 方式一:使用主机网络模式(推荐,支持端口自动重分配)
|
# 方式一:使用主机网络模式(推荐,支持端口自动重分配)
|
||||||
|
|||||||
@@ -157,11 +157,52 @@ const emptyVmessFields: VmessFormFields = {
|
|||||||
alpn: 'http/1.1',
|
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 {
|
interface ParsedShareLink {
|
||||||
name?: string
|
name?: string
|
||||||
protocol?: string
|
protocol?: string
|
||||||
valid: boolean
|
valid: boolean
|
||||||
vmess?: VmessFormFields
|
vmess?: VmessFormFields
|
||||||
|
vless?: VlessFormFields
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeBase64Decode(value: string): string | null {
|
function safeBase64Decode(value: string): string | null {
|
||||||
@@ -228,6 +269,63 @@ function buildVmessUri(fields: VmessFormFields): string {
|
|||||||
return `vmess://${safeBase64Encode(JSON.stringify(payload))}`
|
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 {
|
function parseGenericName(uri: string): string | undefined {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(uri)
|
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 }
|
return { protocol, name: parseGenericName(trimmed), valid: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +386,8 @@ export default function ManagePanel() {
|
|||||||
const [parseHint, setParseHint] = useState('')
|
const [parseHint, setParseHint] = useState('')
|
||||||
const [vmessFields, setVmessFields] = useState<VmessFormFields>(emptyVmessFields)
|
const [vmessFields, setVmessFields] = useState<VmessFormFields>(emptyVmessFields)
|
||||||
const [vmessEditorOpen, setVmessEditorOpen] = useState(false)
|
const [vmessEditorOpen, setVmessEditorOpen] = useState(false)
|
||||||
|
const [vlessFields, setVlessFields] = useState<VlessFormFields>(emptyVlessFields)
|
||||||
|
const [vlessEditorOpen, setVlessEditorOpen] = useState(false)
|
||||||
|
|
||||||
// Delete confirm
|
// Delete confirm
|
||||||
const [deleteTarget, setDeleteTarget] = useState<string | null>(null)
|
const [deleteTarget, setDeleteTarget] = useState<string | null>(null)
|
||||||
@@ -470,9 +580,18 @@ export default function ManagePanel() {
|
|||||||
if (parsed.protocol === 'vmess' && parsed.vmess) {
|
if (parsed.protocol === 'vmess' && parsed.vmess) {
|
||||||
setVmessFields(parsed.vmess)
|
setVmessFields(parsed.vmess)
|
||||||
setVmessEditorOpen(true)
|
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()) {
|
} else if (!value.trim()) {
|
||||||
setVmessFields(emptyVmessFields)
|
setVmessFields(emptyVmessFields)
|
||||||
setVmessEditorOpen(false)
|
setVmessEditorOpen(false)
|
||||||
|
setVlessFields(emptyVlessFields)
|
||||||
|
setVlessEditorOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
setForm(prev => {
|
setForm(prev => {
|
||||||
@@ -492,6 +611,8 @@ export default function ManagePanel() {
|
|||||||
setParseHint('')
|
setParseHint('')
|
||||||
setVmessFields(emptyVmessFields)
|
setVmessFields(emptyVmessFields)
|
||||||
setVmessEditorOpen(false)
|
setVmessEditorOpen(false)
|
||||||
|
setVlessFields(emptyVlessFields)
|
||||||
|
setVlessEditorOpen(false)
|
||||||
setModalOpen(true)
|
setModalOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,6 +629,8 @@ export default function ManagePanel() {
|
|||||||
setNameManuallyEdited(false)
|
setNameManuallyEdited(false)
|
||||||
setVmessFields(emptyVmessFields)
|
setVmessFields(emptyVmessFields)
|
||||||
setVmessEditorOpen(false)
|
setVmessEditorOpen(false)
|
||||||
|
setVlessFields(emptyVlessFields)
|
||||||
|
setVlessEditorOpen(false)
|
||||||
applyParsedLink(node.uri, true)
|
applyParsedLink(node.uri, true)
|
||||||
setModalOpen(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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!form.name.trim()) { setFormError('节点名称不能为空'); return }
|
if (!form.name.trim()) { setFormError('节点名称不能为空'); return }
|
||||||
@@ -1155,6 +1292,9 @@ export default function ManagePanel() {
|
|||||||
if (vmessEditorOpen) {
|
if (vmessEditorOpen) {
|
||||||
setVmessFields(prev => ({ ...prev, ps: value }))
|
setVmessFields(prev => ({ ...prev, ps: value }))
|
||||||
}
|
}
|
||||||
|
if (vlessEditorOpen) {
|
||||||
|
setVlessFields(prev => ({ ...prev, name: value }))
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!!editingNode}
|
disabled={!!editingNode}
|
||||||
/>
|
/>
|
||||||
@@ -1168,7 +1308,7 @@ export default function ManagePanel() {
|
|||||||
onChange={(e) => handleUriInputChange(e.target.value)}
|
onChange={(e) => handleUriInputChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="label text-base-content/50 mt-1 flex flex-col items-start gap-1">
|
<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>}
|
{parseHint && <span className="text-info">{parseHint}</span>}
|
||||||
{parseShareLink(form.uri).protocol === 'vmess' && (
|
{parseShareLink(form.uri).protocol === 'vmess' && (
|
||||||
<button
|
<button
|
||||||
@@ -1176,7 +1316,16 @@ export default function ManagePanel() {
|
|||||||
className="btn btn-xs btn-outline btn-info mt-1"
|
className="btn btn-xs btn-outline btn-info mt-1"
|
||||||
onClick={() => setVmessEditorOpen(open => !open)}
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1246,6 +1395,87 @@ export default function ManagePanel() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<fieldset className="fieldset mb-3">
|
||||||
<legend className="fieldset-legend">本地代理端口</legend>
|
<legend className="fieldset-legend">本地代理端口</legend>
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user