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. // a one-line change. The label values match the user-facing tab names.
const sidebarItems: Array<{ id: SettingsTab; label: string }> = [ const sidebarItems: Array<{ id: SettingsTab; label: string }> = [
{ id: 'general', label: 'General' }, { id: 'general', label: 'General' },
{ id: 's3', label: 'S3-Conf' }, { id: 's3', label: 'S3' },
{ id: 'ssh', label: 'SSH-Conf' }, { id: 'ssh', label: 'SSH' },
] ]
interface OverlayPayload { interface OverlayPayload {
+14 -8
View File
@@ -78,7 +78,7 @@ function defaultConfig(): AppConfig {
host: '', host: '',
port: 22, port: 22,
user: '', user: '',
authMethod: 'builtin', authMethod: 'password',
password: '', password: '',
pathPrefix: 'snapgo/', pathPrefix: 'snapgo/',
strictHostKey: false, strictHostKey: false,
@@ -286,7 +286,8 @@ onMounted(load)
<label class="field"> <label class="field">
<span>Auth method</span> <span>Auth method</span>
<select v-model="config.ssh.authMethod"> <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> <option value="kerberos">Kerberos</option>
</select> </select>
</label> </label>
@@ -295,10 +296,10 @@ onMounted(load)
<input v-model="config.ssh.user" placeholder="ubuntu" /> <input v-model="config.ssh.user" placeholder="ubuntu" />
</label> </label>
<label <label
v-if="config.ssh.authMethod !== 'kerberos'" v-if="config.ssh.authMethod === 'password'"
class="field" class="field"
> >
<span>Password (optional)</span> <span>Password</span>
<input v-model="config.ssh.password" type="password" /> <input v-model="config.ssh.password" type="password" />
</label> </label>
</div> </div>
@@ -340,16 +341,21 @@ onMounted(load)
</div> </div>
<p v-if="config.ssh.authMethod === 'kerberos'" class="hint"> <p v-if="config.ssh.authMethod === 'kerberos'" class="hint">
Kerberos mode delegates to the system <code>ssh</code>/<code>scp</code>. 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 expire (typically every 1024h), so re-run <code>kinit</code> if uploads
start failing. Verify with <code>klist</code>. start failing. Verify with <code>klist</code>.
</p> </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 <p
v-else-if="!config.ssh.password" v-else-if="config.ssh.authMethod === 'password' && !config.ssh.password"
class="hint warn" class="hint warn"
> >
Password is empty please make sure password-less SSH is configured on Password is empty enter the remote login password, or switch the auth
this machine (e.g. via <code>ssh-copy-id</code> or your <code>ssh-agent</code>). method to SSH-Key.
</p> </p>
<div class="actions"> <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. // - Host / Port form the destination endpoint. Port defaults to 22 when 0.
// - User is the SSH login name. // - User is the SSH login name.
// - AuthMethod selects how the connection authenticates: // - AuthMethod selects how the connection authenticates:
// "" / "builtin" → password / ssh-agent / ~/.ssh/id_* via the // "password" → password only, via the in-process Go SSH client.
// in-process golang.org/x/crypto/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 // "kerberos" → delegate to the system /usr/bin/ssh binary so the
// existing Kerberos (GSSAPI) credential cache from // existing Kerberos (GSSAPI) credential cache from
// `kinit` is reused. Native GSSAPI is required here // `kinit` is reused. Native GSSAPI is required here
// because macOS stores tickets in an API: ccache // because macOS stores tickets in an API: ccache
// that pure-Go SSH libraries cannot read. // 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 // - 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 // local SSH agent or ~/.ssh/id_* keys (i.e. the user is responsible for
// having password-less access already configured on this machine). // having password-less access already configured on this machine).
@@ -70,9 +74,14 @@ type SSHConfig struct {
// SSH authentication method identifiers stored in SSHConfig.AuthMethod. // SSH authentication method identifiers stored in SSHConfig.AuthMethod.
const ( const (
// SSHAuthBuiltin uses the in-process Go SSH client (password / agent / // SSHAuthBuiltin is the legacy combined method (password agent → key
// key files). This is the zero-value default. // files). Retained for backward compatibility with older config files;
// new configs use the explicit methods below.
SSHAuthBuiltin = "builtin" 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 // SSHAuthKerberos delegates to the system ssh binary so an existing
// Kerberos credential cache (populated by `kinit`) is reused via GSSAPI. // Kerberos credential cache (populated by `kinit`) is reused via GSSAPI.
SSHAuthKerberos = "kerberos" SSHAuthKerberos = "kerberos"
+52 -34
View File
@@ -385,7 +385,14 @@ func shellQuote(s string) string {
} }
// buildAuthMethods chooses authentication methods given the supplied // 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 // Returns a human-readable summary alongside the method slice so the
// caller can include "password+agent+ed25519" (and similar) in its dial // 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) { func buildAuthMethods(cfg domain.SSHConfig) ([]ssh.AuthMethod, string, error) {
var methods []ssh.AuthMethod var methods []ssh.AuthMethod
var sources []string 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)) methods = append(methods, ssh.Password(cfg.Password))
sources = append(sources, "password") sources = append(sources, "password")
} }
if sock := os.Getenv("SSH_AUTH_SOCK"); sock != "" { if allowKey {
if conn, err := net.Dial("unix", sock); err == nil { if sock := os.Getenv("SSH_AUTH_SOCK"); sock != "" {
// 关键: 仅在 agent 真正持有 key 时才把它加入 auth methods. if conn, err := net.Dial("unix", sock); err == nil {
// // 关键: 仅在 agent 真正持有 key 时才把它加入 auth methods.
// macOS 的 launchd ssh-agent 即便没有任何 identity 也会响应, //
// 此时若仍注册 PublicKeysCallback, x/crypto 会把它当作一次空的 // macOS 的 launchd ssh-agent 即便没有任何 identity 也会响应,
// publickey 尝试. 配合服务器的 MaxAuthTries 计数, 这次"空尝试" // 此时若仍注册 PublicKeysCallback, x/crypto 会把它当作一次空的
// 会挤占后续基于磁盘私钥的认证机会, 最终导致 // publickey 尝试. 配合服务器的 MaxAuthTries 计数, 这次"空尝试"
// "[none publickey] no supported methods remain" —— 即便磁盘上 // 会挤占后续基于磁盘私钥的认证机会, 最终导致
// 的 key 本身完全可用. 因此空 agent 必须跳过. // "[none publickey] no supported methods remain" —— 即便磁盘上
ag := agent.NewClient(conn) // 的 key 本身完全可用. 因此空 agent 必须跳过.
if keys, lerr := ag.List(); lerr == nil && len(keys) > 0 { ag := agent.NewClient(conn)
methods = append(methods, ssh.PublicKeysCallback(ag.Signers)) if keys, lerr := ag.List(); lerr == nil && len(keys) > 0 {
sources = append(sources, fmt.Sprintf("agent(%d)", len(keys))) 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 { } else {
sshLog().Debug("ssh-agent has no identities; skipping", sshLog().Debug("ssh-agent dial failed", "sock", sock, "err", err)
"sock", sock, "list_err", lerr) }
}
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 { } 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 { if len(sources) == 0 {
return methods, "none", nil return methods, "none", nil
} }