feat(proxy): HTTP/SOCKS5 代理支持,按会话独立配置 | 15 files +483/-23 | 96/96 tests passing
功能: - 代理类型定义 (ProxyType, ProxyConfig) - 代理工具函数 (验证、解析、构建URL) + 24 测试 - SSH 连接通过 HTTP CONNECT / SOCKS5 隧道 - 数据库 proxy 列持久化 (JSON) - SessionDialog 代理配置 UI (可折叠面板) - 连接池传递 proxy 到 IPC - sessionStore 类型添加 proxy 支持
This commit is contained in:
1 parent
3f636b1b0e
commit
07c4a5b14f
15 files changed
+757
-23
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { X, Server, Key, Lock, User, Globe, Tag, Folder } from 'lucide-react'
|
||||
import { X, Server, Key, Lock, User, Globe, Tag, Folder, Shield } from 'lucide-react'
|
||||
import { useSessionStore, Session } from '../stores'
|
||||
import type { AuthType } from '../../shared/types/session.types'
|
||||
import type { AuthType, ProxyType } from '../../shared/types/session.types'
|
||||
import { createLogger } from '../utils/logger'
|
||||
|
||||
const logger = createLogger('SessionDialog')
|
||||
@@ -23,9 +23,17 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
password: '',
|
||||
privateKey: '~/.ssh/id_rsa',
|
||||
passphrase: '',
|
||||
proxy: {
|
||||
type: 'none' as ProxyType,
|
||||
host: '',
|
||||
port: 0,
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
group: 'default',
|
||||
tags: '',
|
||||
})
|
||||
const [showProxySettings, setShowProxySettings] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
// 当编辑会话时,填充表单数据
|
||||
@@ -40,9 +48,15 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
password: editSession.password || '',
|
||||
privateKey: editSession.privateKey || '',
|
||||
passphrase: editSession.passphrase || '',
|
||||
proxy: editSession.proxy
|
||||
? { type: editSession.proxy.type as ProxyType, host: editSession.proxy.host, port: editSession.proxy.port, username: editSession.proxy.username || '', password: editSession.proxy.password || '' }
|
||||
: { type: 'none' as ProxyType, host: '', port: 0, username: '', password: '' },
|
||||
group: editSession.group || 'default',
|
||||
tags: editSession.tags?.join(', ') || '',
|
||||
})
|
||||
if (editSession.proxy && editSession.proxy.type !== 'none') {
|
||||
setShowProxySettings(true)
|
||||
}
|
||||
} else {
|
||||
// 重置表单
|
||||
setFormData({
|
||||
@@ -54,9 +68,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
password: '',
|
||||
privateKey: '~/.ssh/id_rsa',
|
||||
passphrase: '',
|
||||
proxy: { type: 'none', host: '', port: 0, username: '', password: '' },
|
||||
group: 'default',
|
||||
tags: '',
|
||||
})
|
||||
setShowProxySettings(false)
|
||||
}
|
||||
}, [editSession])
|
||||
|
||||
@@ -69,6 +85,16 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
const proxyConfig = formData.proxy.type !== 'none' && formData.proxy.host
|
||||
? {
|
||||
type: formData.proxy.type,
|
||||
host: formData.proxy.host,
|
||||
port: formData.proxy.port || (formData.proxy.type === 'http' ? 8080 : 1080),
|
||||
username: formData.proxy.username || undefined,
|
||||
password: formData.proxy.password || undefined,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const sessionData = {
|
||||
name: formData.name,
|
||||
host: formData.host,
|
||||
@@ -78,6 +104,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
password: formData.password,
|
||||
privateKey: formData.authType === 'key' ? (formData.privateKey.trim() || '~/.ssh/id_rsa') : formData.privateKey,
|
||||
passphrase: formData.passphrase,
|
||||
proxy: proxyConfig,
|
||||
group: formData.group,
|
||||
tags: formData.tags.split(',').map(t => t.trim()).filter(Boolean),
|
||||
}
|
||||
@@ -102,9 +129,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
password: '',
|
||||
privateKey: '',
|
||||
passphrase: '',
|
||||
proxy: { type: 'none', host: '', port: 0, username: '', password: '' },
|
||||
group: 'default',
|
||||
tags: '',
|
||||
})
|
||||
setShowProxySettings(false)
|
||||
} catch (error) {
|
||||
logger.error(isEditing ? 'Failed to update session:' : 'Failed to create session:', error)
|
||||
} finally {
|
||||
@@ -301,6 +330,124 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Proxy Settings */}
|
||||
<div className="border-t border-dark-700 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowProxySettings(!showProxySettings)}
|
||||
className="flex items-center gap-2 text-sm font-medium text-gray-400 hover:text-gray-200 transition-colors"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
代理设置
|
||||
<span className="text-xs text-gray-600">
|
||||
{formData.proxy.type !== 'none' && formData.proxy.host ? `(已配置 ${formData.proxy.type.toUpperCase()})` : '(未配置)'}
|
||||
</span>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform ${showProxySettings ? 'rotate-180' : ''}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{showProxySettings && (
|
||||
<div className="mt-3 space-y-3 animate-fade-in">
|
||||
{/* Proxy Type */}
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ value: 'none', label: '不使用' },
|
||||
{ value: 'http', label: 'HTTP' },
|
||||
{ value: 'socks5', label: 'SOCKS5' },
|
||||
].map((type) => (
|
||||
<button
|
||||
key={type.value}
|
||||
type="button"
|
||||
onClick={() => setFormData({
|
||||
...formData,
|
||||
proxy: {
|
||||
...formData.proxy,
|
||||
type: type.value as ProxyType,
|
||||
port: formData.proxy.port || (type.value === 'http' ? 8080 : 1080),
|
||||
}
|
||||
})}
|
||||
className={`flex-1 px-3 py-1.5 rounded-lg border text-xs transition-colors ${
|
||||
formData.proxy.type === type.value
|
||||
? 'border-accent-blue bg-accent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700 hover:border-dark-600 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{type.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{formData.proxy.type !== 'none' && (
|
||||
<>
|
||||
{/* Proxy Host & Port */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-xs text-gray-500 mb-1">代理地址</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.proxy.host}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
proxy: { ...formData.proxy, host: e.target.value }
|
||||
})}
|
||||
placeholder="proxy.example.com"
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">端口</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.proxy.port || ''}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
proxy: { ...formData.proxy, port: parseInt(e.target.value) || 0 }
|
||||
})}
|
||||
placeholder={formData.proxy.type === 'http' ? '8080' : '1080'}
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Proxy Auth */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">代理用户名 (可选)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.proxy.username}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
proxy: { ...formData.proxy, username: e.target.value }
|
||||
})}
|
||||
placeholder="username"
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">代理密码 (可选)</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.proxy.password}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
proxy: { ...formData.proxy, password: e.target.value }
|
||||
})}
|
||||
placeholder="password"
|
||||
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Group and tags */}
|
||||
<div className="border-t border-dark-700 pt-4 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
|
||||
Reference in new issue
Block a user