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 <gyeongho.dev@proton.me>
This commit is contained in:
GyeongHo Kim 2026-06-14 02:20:47 +09:00 committed by GyeongHo Kim
parent 511884b2af
commit 10a86a301e
No known key found for this signature in database
GPG key ID: 54111101FD01C8BD
6 changed files with 88 additions and 11 deletions

View file

@ -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"),

View file

@ -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

View file

@ -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 {

View file

@ -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{

View file

@ -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)

View file

@ -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)