refactor: optimize setting view

This commit is contained in:
2026-06-12 11:05:04 +08:00
parent 4576653b4e
commit 3cd92945ac
4 changed files with 81 additions and 48 deletions
+2 -2
View File
@@ -46,8 +46,8 @@ const activeTab = ref<SettingsTab>('general')
// a one-line change. The label values match the user-facing tab names.
const sidebarItems: Array<{ id: SettingsTab; label: string }> = [
{ id: 'general', label: 'General' },
{ id: 's3', label: 'S3-Conf' },
{ id: 'ssh', label: 'SSH-Conf' },
{ id: 's3', label: 'S3' },
{ id: 'ssh', label: 'SSH' },
]
interface OverlayPayload {
+14 -8
View File
@@ -78,7 +78,7 @@ function defaultConfig(): AppConfig {
host: '',
port: 22,
user: '',
authMethod: 'builtin',
authMethod: 'password',
password: '',
pathPrefix: 'snapgo/',
strictHostKey: false,
@@ -286,7 +286,8 @@ onMounted(load)
<label class="field">
<span>Auth method</span>
<select v-model="config.ssh.authMethod">
<option value="builtin">Password / Key</option>
<option value="password">Password</option>
<option value="key">SSH-Key</option>
<option value="kerberos">Kerberos</option>
</select>
</label>
@@ -295,10 +296,10 @@ onMounted(load)
<input v-model="config.ssh.user" placeholder="ubuntu" />
</label>
<label
v-if="config.ssh.authMethod !== 'kerberos'"
v-if="config.ssh.authMethod === 'password'"
class="field"
>
<span>Password (optional)</span>
<span>Password</span>
<input v-model="config.ssh.password" type="password" />
</label>
</div>
@@ -340,16 +341,21 @@ onMounted(load)
</div>
<p v-if="config.ssh.authMethod === 'kerberos'" class="hint">
Kerberos mode delegates to the system <code>ssh</code>/<code>scp</code>.
Run <code>kinit {{ config.ssh.user || 'user' }}@BYTEDANCE.COM</code> in a terminal first; tickets
Run <code>kinit your.name@BYTEDANCE.COM</code> in a terminal first; tickets
expire (typically every 1024h), so re-run <code>kinit</code> if uploads
start failing. Verify with <code>klist</code>.
</p>
<p v-else-if="config.ssh.authMethod === 'key'" class="hint">
SSH-Key mode uses your <code>ssh-agent</code> and <code>~/.ssh/id_*</code>
keys. Make sure password-less login already works (e.g. via
<code>ssh-copy-id</code>).
</p>
<p
v-else-if="!config.ssh.password"
v-else-if="config.ssh.authMethod === 'password' && !config.ssh.password"
class="hint warn"
>
Password is empty please make sure password-less SSH is configured on
this machine (e.g. via <code>ssh-copy-id</code> or your <code>ssh-agent</code>).
Password is empty enter the remote login password, or switch the auth
method to SSH-Key.
</p>
<div class="actions">
+13 -4
View File
@@ -34,13 +34,17 @@ type S3Config struct {
// - Host / Port form the destination endpoint. Port defaults to 22 when 0.
// - User is the SSH login name.
// - AuthMethod selects how the connection authenticates:
// "" / "builtin" → password / ssh-agent / ~/.ssh/id_* via the
// in-process golang.org/x/crypto/ssh client.
// "password" → password only, via the in-process Go SSH client.
// "key" → ssh-agent + ~/.ssh/id_* key files (no password),
// via the in-process Go SSH client.
// "kerberos" → delegate to the system /usr/bin/ssh binary so the
// existing Kerberos (GSSAPI) credential cache from
// `kinit` is reused. Native GSSAPI is required here
// because macOS stores tickets in an API: ccache
// that pure-Go SSH libraries cannot read.
// "" / "builtin" → legacy combined behaviour (password → agent → key
// files) kept only for backward compatibility with
// configs written before the method was split.
// - Password is optional; when empty the implementation falls back to the
// local SSH agent or ~/.ssh/id_* keys (i.e. the user is responsible for
// having password-less access already configured on this machine).
@@ -70,9 +74,14 @@ type SSHConfig struct {
// SSH authentication method identifiers stored in SSHConfig.AuthMethod.
const (
// SSHAuthBuiltin uses the in-process Go SSH client (password / agent /
// key files). This is the zero-value default.
// SSHAuthBuiltin is the legacy combined method (password agent → key
// files). Retained for backward compatibility with older config files;
// new configs use the explicit methods below.
SSHAuthBuiltin = "builtin"
// SSHAuthPassword authenticates with the password field only.
SSHAuthPassword = "password"
// SSHAuthKey authenticates with ssh-agent / ~/.ssh/id_* key files only.
SSHAuthKey = "key"
// SSHAuthKerberos delegates to the system ssh binary so an existing
// Kerberos credential cache (populated by `kinit`) is reused via GSSAPI.
SSHAuthKerberos = "kerberos"
+52 -34
View File
@@ -385,7 +385,14 @@ func shellQuote(s string) string {
}
// buildAuthMethods chooses authentication methods given the supplied
// configuration. See Dial's docstring for the priority order.
// configuration.
//
// The method set is gated by cfg.AuthMethod:
// - SSHAuthPassword → password only.
// - SSHAuthKey → ssh-agent + ~/.ssh/id_* key files only.
// - "" / SSHAuthBuiltin (legacy) → password (if set) + agent + key files.
//
// (Kerberos never reaches here; it uses the system ssh binary instead.)
//
// Returns a human-readable summary alongside the method slice so the
// caller can include "password+agent+ed25519" (and similar) in its dial
@@ -393,47 +400,58 @@ func shellQuote(s string) string {
func buildAuthMethods(cfg domain.SSHConfig) ([]ssh.AuthMethod, string, error) {
var methods []ssh.AuthMethod
var sources []string
if cfg.Password != "" {
// Decide which families are allowed for the selected method. An empty /
// "builtin" value preserves the original combined behaviour so configs
// written before the split keep working.
allowPassword := cfg.AuthMethod == domain.SSHAuthPassword ||
cfg.AuthMethod == domain.SSHAuthBuiltin || cfg.AuthMethod == ""
allowKey := cfg.AuthMethod == domain.SSHAuthKey ||
cfg.AuthMethod == domain.SSHAuthBuiltin || cfg.AuthMethod == ""
if allowPassword && cfg.Password != "" {
methods = append(methods, ssh.Password(cfg.Password))
sources = append(sources, "password")
}
if sock := os.Getenv("SSH_AUTH_SOCK"); sock != "" {
if conn, err := net.Dial("unix", sock); err == nil {
// 关键: 仅在 agent 真正持有 key 时才把它加入 auth methods.
//
// macOS 的 launchd ssh-agent 即便没有任何 identity 也会响应,
// 此时若仍注册 PublicKeysCallback, x/crypto 会把它当作一次空的
// publickey 尝试. 配合服务器的 MaxAuthTries 计数, 这次"空尝试"
// 会挤占后续基于磁盘私钥的认证机会, 最终导致
// "[none publickey] no supported methods remain" —— 即便磁盘上
// 的 key 本身完全可用. 因此空 agent 必须跳过.
ag := agent.NewClient(conn)
if keys, lerr := ag.List(); lerr == nil && len(keys) > 0 {
methods = append(methods, ssh.PublicKeysCallback(ag.Signers))
sources = append(sources, fmt.Sprintf("agent(%d)", len(keys)))
if allowKey {
if sock := os.Getenv("SSH_AUTH_SOCK"); sock != "" {
if conn, err := net.Dial("unix", sock); err == nil {
// 关键: 仅在 agent 真正持有 key 时才把它加入 auth methods.
//
// macOS 的 launchd ssh-agent 即便没有任何 identity 也会响应,
// 此时若仍注册 PublicKeysCallback, x/crypto 会把它当作一次空的
// publickey 尝试. 配合服务器的 MaxAuthTries 计数, 这次"空尝试"
// 会挤占后续基于磁盘私钥的认证机会, 最终导致
// "[none publickey] no supported methods remain" —— 即便磁盘上
// 的 key 本身完全可用. 因此空 agent 必须跳过.
ag := agent.NewClient(conn)
if keys, lerr := ag.List(); lerr == nil && len(keys) > 0 {
methods = append(methods, ssh.PublicKeysCallback(ag.Signers))
sources = append(sources, fmt.Sprintf("agent(%d)", len(keys)))
} else {
sshLog().Debug("ssh-agent has no identities; skipping",
"sock", sock, "list_err", lerr)
}
} else {
sshLog().Debug("ssh-agent has no identities; skipping",
"sock", sock, "list_err", lerr)
sshLog().Debug("ssh-agent dial failed", "sock", sock, "err", err)
}
}
home, err := os.UserHomeDir()
if err == nil {
// Probe the common default keys; any unreadable / missing file is
// silently skipped so the user never sees noise about keys they did
// not set up.
for _, name := range []string{"id_ed25519", "id_rsa", "id_ecdsa"} {
signer, err := loadPrivateKey(path.Join(home, ".ssh", name))
if err == nil && signer != nil {
methods = append(methods, ssh.PublicKeys(signer))
sources = append(sources, name)
}
}
} else {
sshLog().Debug("ssh-agent dial failed", "sock", sock, "err", err)
sshLog().Debug("home dir unavailable for ssh keys", "err", err)
}
}
home, err := os.UserHomeDir()
if err == nil {
// Probe the common default keys; any unreadable / missing file is
// silently skipped so the user never sees noise about keys they did
// not set up.
for _, name := range []string{"id_ed25519", "id_rsa", "id_ecdsa"} {
signer, err := loadPrivateKey(path.Join(home, ".ssh", name))
if err == nil && signer != nil {
methods = append(methods, ssh.PublicKeys(signer))
sources = append(sources, name)
}
}
} else {
sshLog().Debug("home dir unavailable for ssh keys", "err", err)
}
if len(sources) == 0 {
return methods, "none", nil
}