gitea.tea/modules/utils/ssh_host.go
GyeongHo Kim 511884b2af
fix(login): default SSH host to URL hostname
Use the server URL hostname when a login has no explicit SSHHost configured, so an HTTP(S) port from the Gitea URL is not reused as the SSH endpoint.

Add shared SSH host normalization and resolution helpers with tests for host and host:port values.

Signed-off-by: GyeongHo Kim <gyeongho.dev@proton.me>
2026-07-29 14:05:35 +09:00

91 lines
2.4 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package utils
import (
"fmt"
"net"
"net/url"
"strconv"
"strings"
)
// NormalizeSSHHost validates an SSH host setting and returns it trimmed.
// The accepted format is host or host:port. IPv6 addresses must be bracketed.
func NormalizeSSHHost(raw string) (string, error) {
sshHost := strings.TrimSpace(raw)
if sshHost == "" {
return "", nil
}
if strings.ContainsAny(sshHost, "/\\") {
return "", fmt.Errorf("invalid SSH host %q: use host or host:port, not a URL or path", raw)
}
if strings.ContainsAny(sshHost, " \t\r\n") {
return "", fmt.Errorf("invalid SSH host %q: whitespace is not allowed", raw)
}
if strings.Contains(sshHost, "@") {
return "", fmt.Errorf("invalid SSH host %q: use host or host:port without a user", raw)
}
host := sshHost
port := ""
colonCount := strings.Count(sshHost, ":")
portSpecified := false
switch {
case strings.HasPrefix(sshHost, "["):
if strings.Contains(sshHost, "]:") {
portSpecified = true
var err error
host, port, err = net.SplitHostPort(sshHost)
if err != nil {
return "", fmt.Errorf("invalid SSH host %q: %w", raw, err)
}
} else if strings.HasSuffix(sshHost, "]") {
host = strings.TrimSuffix(strings.TrimPrefix(sshHost, "["), "]")
} else {
return "", fmt.Errorf("invalid SSH host %q: malformed IPv6 address", raw)
}
case colonCount == 1:
portSpecified = true
parts := strings.SplitN(sshHost, ":", 2)
host = parts[0]
port = parts[1]
case colonCount > 1:
return "", fmt.Errorf("invalid SSH host %q: IPv6 addresses must be bracketed", raw)
}
if host == "" {
return "", fmt.Errorf("invalid SSH host %q: host is required", raw)
}
if portSpecified {
if port == "" {
return "", fmt.Errorf("invalid SSH host %q: port is required", raw)
}
portNumber, err := strconv.Atoi(port)
if err != nil || portNumber < 1 || portNumber > 65535 {
return "", fmt.Errorf("invalid SSH host %q: port must be between 1 and 65535", raw)
}
}
return sshHost, nil
}
// ResolveSSHHost returns the explicit SSH host when provided, otherwise the
// hostname from the Gitea server URL.
func ResolveSSHHost(serverURL *url.URL, explicitSSHHost string) (string, error) {
sshHost, err := NormalizeSSHHost(explicitSSHHost)
if err != nil {
return "", err
}
if sshHost != "" {
return sshHost, nil
}
if serverURL == nil {
return "", nil
}
return serverURL.Hostname(), nil
}