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)