From 10a86a301e3a84128f430cdd4befd256bb95d40c Mon Sep 17 00:00:00 2001 From: GyeongHo Kim Date: Sun, 14 Jun 2026 02:20:47 +0900 Subject: [PATCH] fix(login): configure SSH host explicitly Add a --ssh-host option to tea login add and pass it through token and OAuth login creation. Expose the same SSH host setting in the interactive login optional settings so users can configure host or host:port when the SSH endpoint differs from the web URL. Update the generated CLI docs and integration helper for the new login creation parameter. Fixes #1032 Signed-off-by: GyeongHo Kim --- cmd/login/add.go | 6 +++ docs/CLI.md | 2 + modules/auth/oauth.go | 14 +++++-- modules/interact/login.go | 64 +++++++++++++++++++++++++++++-- modules/task/login_create.go | 11 ++++-- tests/integration/helpers_test.go | 2 +- 6 files changed, 88 insertions(+), 11 deletions(-) diff --git a/cmd/login/add.go b/cmd/login/add.go index bcc71659..03318d85 100644 --- a/cmd/login/add.go +++ b/cmd/login/add.go @@ -78,6 +78,10 @@ token. Equivalent to running 'tea login helper setup' afterwards.`, Aliases: []string{"s"}, Usage: "Path to a SSH key/certificate to use, overrides auto-discovery", }, + &cli.StringFlag{ + Name: "ssh-host", + Usage: "SSH host, optionally with port, e.g. git.example.com or git.example.com:2222", + }, &cli.BoolFlag{ Name: "insecure", Aliases: []string{"i"}, @@ -129,6 +133,7 @@ func runLoginAdd(requestCtx context.Context, cmd *cli.Command) error { opts := auth.OAuthOptions{ Name: cmd.String("name"), URL: cmd.String("url"), + SSHHost: cmd.String("ssh-host"), Insecure: cmd.Bool("insecure"), } @@ -161,6 +166,7 @@ func runLoginAdd(requestCtx context.Context, cmd *cli.Command) error { cmd.String("scopes"), cmd.String("ssh-key"), cmd.String("url"), + cmd.String("ssh-host"), cmd.String("ssh-agent-principal"), cmd.String("ssh-agent-key"), cmd.Bool("insecure"), diff --git a/docs/CLI.md b/docs/CLI.md index 3f74a38e..f9e7dfaa 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -63,6 +63,8 @@ Add a Gitea login **--ssh-agent-principal, -c**="": Use SSH certificate with specified principal to login (needs a running ssh-agent with certificate loaded) +**--ssh-host**="": SSH host, optionally with port, e.g. git.example.com or git.example.com:2222 + **--ssh-key, -s**="": Path to a SSH key/certificate to use, overrides auto-discovery **--token, -t**="": Access token. Can be obtained from Settings > Applications diff --git a/modules/auth/oauth.go b/modules/auth/oauth.go index c98a22fa..06ed9fdf 100644 --- a/modules/auth/oauth.go +++ b/modules/auth/oauth.go @@ -46,6 +46,7 @@ const ( type OAuthOptions struct { Name string URL string + SSHHost string Insecure bool ClientID string RedirectURL string @@ -72,12 +73,16 @@ func OAuthLoginWithOptions(ctx context.Context, name, giteaURL string, insecure // OAuthLoginWithFullOptions performs an OAuth2 PKCE login flow with full options control func OAuthLoginWithFullOptions(ctx context.Context, opts OAuthOptions) error { + if _, err := utils.NormalizeSSHHost(opts.SSHHost); err != nil { + return err + } + serverURL, token, err := performBrowserOAuthFlow(ctx, opts) if err != nil { return err } - return createLoginFromToken(ctx, opts.Name, serverURL, token, opts.Insecure) + return createLoginFromToken(ctx, opts.Name, serverURL, token, opts.SSHHost, opts.Insecure) } // performBrowserOAuthFlow performs the browser-based OAuth2 PKCE flow and returns the token. @@ -370,7 +375,7 @@ var openBrowser = func(url string) error { } // createLoginFromToken creates a login entry using the obtained access token -func createLoginFromToken(ctx context.Context, name, serverURL string, token *oauth2.Token, insecure bool) error { +func createLoginFromToken(ctx context.Context, name, serverURL string, token *oauth2.Token, sshHost string, insecure bool) error { if name == "" { var err error name, err = task.GenerateLoginName(serverURL, "") @@ -408,7 +413,10 @@ func createLoginFromToken(ctx context.Context, name, serverURL string, token *oa if err != nil { return err } - login.SSHHost = parsedURL.Host + login.SSHHost, err = utils.ResolveSSHHost(parsedURL, sshHost) + if err != nil { + return err + } // Save tokens and add login to config if err := config.AddOAuthLogin(&login, token.AccessToken, token.RefreshToken, token.Expiry); err != nil { diff --git a/modules/interact/login.go b/modules/interact/login.go index b59d24b9..932f5a9c 100644 --- a/modules/interact/login.go +++ b/modules/interact/login.go @@ -18,6 +18,7 @@ import ( "gitea.dev/tea/modules/config" "gitea.dev/tea/modules/task" "gitea.dev/tea/modules/theme" + "gitea.dev/tea/modules/utils" "charm.land/huh/v2" ) @@ -25,8 +26,8 @@ import ( // CreateLogin create an login interactive func CreateLogin(ctx context.Context) error { var ( - name, token, user, passwd, otp, scopes, sshKey, sshCertPrincipal, sshKeyFingerprint string - insecure, sshAgent, versionCheck, helper bool + name, token, user, passwd, otp, scopes, sshKey, sshHost, sshCertPrincipal, sshKeyFingerprint string + insecure, sshAgent, versionCheck, helper bool ) versionCheck = true @@ -105,7 +106,16 @@ func CreateLogin(ctx context.Context) error { } printTitleAndContent("Allow Insecure connections:", strconv.FormatBool(insecure)) - return auth.OAuthLoginWithOptions(ctx, name, giteaURL, insecure) + if err := promptOAuthOptionalSettings(giteaURL, &sshHost); err != nil { + return err + } + + return auth.OAuthLoginWithFullOptions(ctx, auth.OAuthOptions{ + Name: name, + URL: giteaURL, + SSHHost: sshHost, + Insecure: insecure, + }) default: // token var hasToken bool if err := huh.NewConfirm(). @@ -243,6 +253,10 @@ func CreateLogin(ctx context.Context) error { } printTitleAndContent("SSH Key Path (leave empty for auto-discovery):", sshKey) + if err := promptSSHHost(giteaURL, &sshHost); err != nil { + return err + } + if err := huh.NewConfirm(). Title("Allow Insecure connections:"). Value(&insecure). @@ -271,7 +285,49 @@ func CreateLogin(ctx context.Context) error { printTitleAndContent("Check version of Gitea instance:", strconv.FormatBool(versionCheck)) } - return task.CreateLogin(ctx, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint, insecure, sshAgent, versionCheck, helper) + return task.CreateLogin(ctx, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshHost, sshCertPrincipal, sshKeyFingerprint, insecure, sshAgent, versionCheck, helper) +} + +func promptOAuthOptionalSettings(giteaURL string, sshHost *string) error { + var optSettings bool + if err := huh.NewConfirm(). + Title("Set Optional settings:"). + Value(&optSettings). + WithTheme(theme.GetTheme()). + Run(); err != nil { + return err + } + printTitleAndContent("Set Optional settings:", strconv.FormatBool(optSettings)) + + if !optSettings { + return nil + } + + return promptSSHHost(giteaURL, sshHost) +} + +func promptSSHHost(giteaURL string, sshHost *string) error { + if *sshHost == "" { + serverURL, err := utils.NormalizeURL(giteaURL) + if err == nil { + *sshHost = serverURL.Hostname() + } + } + + if err := huh.NewInput(). + Title("SSH Host (host or host:port, leave empty to use Gitea URL hostname):"). + Value(sshHost). + Validate(func(s string) error { + _, err := utils.NormalizeSSHHost(s) + return err + }). + WithTheme(theme.GetTheme()). + Run(); err != nil { + return err + } + printTitleAndContent("SSH Host (host or host:port, leave empty to use Gitea URL hostname):", *sshHost) + + return nil } var tokenScopeOpts = []string{ diff --git a/modules/task/login_create.go b/modules/task/login_create.go index 2a0e1050..a436db20 100644 --- a/modules/task/login_create.go +++ b/modules/task/login_create.go @@ -49,7 +49,7 @@ func SetupHelper(login config.Login) (ok bool, err error) { } // CreateLogin create a login to be stored in config -func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint string, insecure, sshAgent, versionCheck, addHelper bool) error { +func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshHost, sshCertPrincipal, sshKeyFingerprint string, insecure, sshAgent, versionCheck, addHelper bool) error { // checks ... // ... if we have a url if len(giteaURL) == 0 { @@ -86,6 +86,11 @@ func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, ssh return err } + resolvedSSHHost, err := utils.ResolveSSHHost(serverURL, sshHost) + if err != nil { + return err + } + // check if it's a certificate the principal doesn't matter as the user // has explicitly selected this private key if _, err := os.Stat(sshKey + "-cert.pub"); err == nil { @@ -127,8 +132,8 @@ func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, ssh } // we do not have a method to get SSH config from api, - // so we just use the host - login.SSHHost = serverURL.Host + // so we use the URL hostname unless explicitly configured + login.SSHHost = resolvedSSHHost if len(sshKey) == 0 { login.SSHKey, err = findSSHKey(ctx, client) diff --git a/tests/integration/helpers_test.go b/tests/integration/helpers_test.go index 2f131aae..b0b651d6 100644 --- a/tests/integration/helpers_test.go +++ b/tests/integration/helpers_test.go @@ -96,7 +96,7 @@ func createIntegrationLogin(t *testing.T) *config.Login { require.NotEmpty(t, integrationToken, "integration token setup failed") - require.NoError(t, task.CreateLogin(t.Context(), "integration", integrationToken, "", "", "", "", "", integrationGiteaURL, "", "", true, false, false, false)) + require.NoError(t, task.CreateLogin(t.Context(), "integration", integrationToken, "", "", "", "", "", integrationGiteaURL, "", "", "", true, false, false, false)) login, err := config.GetLoginByName("integration") require.NoError(t, err)