refactor: optimize setting view
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 10–24h), so re-run <code>kinit</code> if uploads
|
expire (typically every 10–24h), 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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user