增强 SSH/SFTP 稳定性并完善安全校验与前端交互
This commit is contained in:
@@ -20,35 +20,59 @@ const username = ref('')
|
||||
const authType = ref<AuthType>('PASSWORD')
|
||||
const password = ref('')
|
||||
const privateKey = ref('')
|
||||
const privateKeyFileName = ref('')
|
||||
const privateKeyInputRef = ref<HTMLInputElement | null>(null)
|
||||
const passphrase = ref('')
|
||||
|
||||
const isEdit = computed(() => !!props.connection)
|
||||
|
||||
const hostError = computed(() => {
|
||||
const h = host.value.trim()
|
||||
if (!h) return ''
|
||||
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
|
||||
const hostnameRegex = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
|
||||
if (ipv4Regex.test(h)) {
|
||||
const parts = h.match(ipv4Regex)
|
||||
if (parts && parts.slice(1, 5).every(p => parseInt(p) <= 255)) return ''
|
||||
return 'IP地址格式无效'
|
||||
}
|
||||
if (hostnameRegex.test(h)) return ''
|
||||
return '主机名格式无效'
|
||||
})
|
||||
|
||||
const portError = computed(() => {
|
||||
const p = port.value
|
||||
if (p < 1 || p > 65535) return '端口号必须在1-65535之间'
|
||||
return ''
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.connection,
|
||||
(c) => {
|
||||
() => props.connection,
|
||||
(c) => {
|
||||
if (c) {
|
||||
name.value = c.name
|
||||
host.value = c.host
|
||||
port.value = c.port
|
||||
username.value = c.username
|
||||
authType.value = c.authType
|
||||
password.value = ''
|
||||
privateKey.value = ''
|
||||
passphrase.value = ''
|
||||
} else {
|
||||
password.value = ''
|
||||
privateKey.value = ''
|
||||
privateKeyFileName.value = ''
|
||||
passphrase.value = ''
|
||||
} else {
|
||||
name.value = ''
|
||||
host.value = ''
|
||||
port.value = 22
|
||||
username.value = ''
|
||||
authType.value = 'PASSWORD'
|
||||
password.value = ''
|
||||
privateKey.value = ''
|
||||
passphrase.value = ''
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
password.value = ''
|
||||
privateKey.value = ''
|
||||
privateKeyFileName.value = ''
|
||||
passphrase.value = ''
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
@@ -68,9 +92,53 @@ function handleBackdropMouseUp() {
|
||||
function handleDialogMouseDown() {
|
||||
backdropPressed.value = false
|
||||
}
|
||||
|
||||
function normalizeKeyText(text: string) {
|
||||
return text.replace(/\r\n?/g, '\n').trim() + '\n'
|
||||
}
|
||||
|
||||
async function handlePrivateKeyFileChange(e: Event) {
|
||||
error.value = ''
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// Keep it generous; OpenSSH keys are usually a few KB.
|
||||
const MAX_SIZE = 256 * 1024
|
||||
if (file.size > MAX_SIZE) {
|
||||
error.value = '私钥文件过大(>256KB),请检查是否选错文件'
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await file.text()
|
||||
privateKey.value = normalizeKeyText(text)
|
||||
privateKeyFileName.value = file.name
|
||||
} catch {
|
||||
error.value = '读取私钥文件失败'
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function clearPrivateKeyFile() {
|
||||
privateKey.value = ''
|
||||
privateKeyFileName.value = ''
|
||||
if (privateKeyInputRef.value) privateKeyInputRef.value.value = ''
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
error.value = ''
|
||||
const hostErr = hostError.value
|
||||
const portErr = portError.value
|
||||
if (hostErr) {
|
||||
error.value = hostErr
|
||||
return
|
||||
}
|
||||
if (portErr) {
|
||||
error.value = portErr
|
||||
return
|
||||
}
|
||||
if (!name.value.trim()) {
|
||||
error.value = '请填写名称'
|
||||
return
|
||||
@@ -169,6 +237,7 @@ async function handleSubmit() {
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-slate-700 border border-slate-600 text-slate-100 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
placeholder="192.168.1.1"
|
||||
/>
|
||||
<p v-if="hostError" class="mt-1 text-xs text-red-400">{{ hostError }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="port" class="block text-sm font-medium text-slate-300 mb-1">端口</label>
|
||||
@@ -180,6 +249,7 @@ async function handleSubmit() {
|
||||
max="65535"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-slate-700 border border-slate-600 text-slate-100 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
/>
|
||||
<p v-if="portError" class="mt-1 text-xs text-red-400">{{ portError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -217,25 +287,36 @@ async function handleSubmit() {
|
||||
:placeholder="isEdit ? '••••••••' : ''"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="authType === 'PRIVATE_KEY'" class="space-y-2">
|
||||
<label for="privateKey" class="block text-sm font-medium text-slate-300 mb-1">
|
||||
私钥 {{ isEdit ? '(留空则不修改)' : '' }}
|
||||
</label>
|
||||
<textarea
|
||||
id="privateKey"
|
||||
v-model="privateKey"
|
||||
rows="6"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-slate-700 border border-slate-600 text-slate-100 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ... -----END OPENSSH PRIVATE KEY-----"
|
||||
></textarea>
|
||||
<label for="passphrase" class="block text-sm font-medium text-slate-300 mb-1">私钥口令(可选)</label>
|
||||
<input
|
||||
id="passphrase"
|
||||
v-model="passphrase"
|
||||
type="password"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-slate-700 border border-slate-600 text-slate-100 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="authType === 'PRIVATE_KEY'" class="space-y-2">
|
||||
<label for="privateKey" class="block text-sm font-medium text-slate-300 mb-1">
|
||||
私钥 {{ isEdit ? '(留空则不修改)' : '' }}
|
||||
</label>
|
||||
<input
|
||||
ref="privateKeyInputRef"
|
||||
id="privateKey"
|
||||
type="file"
|
||||
accept=".pem,.key,.ppk,.txt,application/x-pem-file"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-slate-700 border border-slate-600 text-slate-100 focus:outline-none focus:ring-2 focus:ring-cyan-500 file:mr-4 file:rounded-md file:border-0 file:bg-slate-600 file:px-3 file:py-2 file:text-slate-100 hover:file:bg-slate-500"
|
||||
@change="handlePrivateKeyFileChange"
|
||||
/>
|
||||
<div v-if="privateKeyFileName" class="flex items-center justify-between gap-3">
|
||||
<p class="text-xs text-slate-400 truncate">已选择:{{ privateKeyFileName }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-slate-300 hover:text-slate-100 hover:underline"
|
||||
@click="clearPrivateKeyFile"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
<label for="passphrase" class="block text-sm font-medium text-slate-300 mb-1">私钥口令(可选)</label>
|
||||
<input
|
||||
id="passphrase"
|
||||
v-model="passphrase"
|
||||
type="password"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-slate-700 border border-slate-600 text-slate-100 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="error" class="text-sm text-red-400">{{ error }}</p>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user