mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-27 08:55:04 +00:00
d42128d71d
https://github.com/go-gitea/gitea/pull/38406 stopped setting `ServerName` on the internal API client, which https://github.com/go-gitea/gitea/pull/5820 had added for ACME. Internal requests to a local `LOCAL_ROOT_URL` now send SNI `localhost` (or none for IPs). The ACME listener selects its certificate by SNI, finds none and aborts the handshake with `tls: internal error`, breaking SSH access and git hooks. Send the `ROOT_URL` host as SNI again for local targets, and treat unspecified addresses (`0.0.0.0`, `::`) as local since dialing them reaches the local host. Remote targets are still verified against their own hostname. Fixes: https://github.com/go-gitea/gitea/issues/38903 --------- Co-authored-by: silverwind <me@silverwind.io>
119 lines
3.5 KiB
Go
119 lines
3.5 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package private
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitea.dev/modules/httplib"
|
|
"gitea.dev/modules/json"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/proxyprotocol"
|
|
"gitea.dev/modules/setting"
|
|
)
|
|
|
|
// Response is used for internal request response (for user message and error message)
|
|
type Response struct {
|
|
Err string `json:"err,omitempty"` // server-side error log message, it won't be exposed to end users
|
|
UserMsg string `json:"user_msg,omitempty"` // meaningful error message for end users, it will be shown in git client's output.
|
|
}
|
|
|
|
func getClientIP() string {
|
|
sshConnEnv := strings.TrimSpace(os.Getenv("SSH_CONNECTION"))
|
|
if len(sshConnEnv) == 0 {
|
|
return "127.0.0.1"
|
|
}
|
|
return strings.Fields(sshConnEnv)[0]
|
|
}
|
|
|
|
func dialContextInternalAPI(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
|
d := net.Dialer{Timeout: 10 * time.Second}
|
|
if setting.Protocol == setting.HTTPUnix {
|
|
conn, err = d.DialContext(ctx, "unix", setting.HTTPAddr)
|
|
} else {
|
|
conn, err = d.DialContext(ctx, network, address)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if setting.LocalUseProxyProtocol {
|
|
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
// unspecified IPs dial the local host too, other targets are network hops that must be verified to protect INTERNAL_TOKEN
|
|
func internalAPIConnectionIsLocal(protocol setting.Scheme, localURL string) bool {
|
|
if protocol == setting.HTTPUnix {
|
|
return true
|
|
}
|
|
u, err := url.Parse(localURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
host := u.Hostname()
|
|
if host == "localhost" {
|
|
return true
|
|
}
|
|
ip := net.ParseIP(host)
|
|
return ip != nil && (ip.IsLoopback() || ip.IsUnspecified())
|
|
}
|
|
|
|
func internalAPITLSConfig(protocol setting.Scheme, localURL, domain string) *tls.Config {
|
|
if internalAPIConnectionIsLocal(protocol, localURL) {
|
|
// the ACME listener selects its certificate by SNI, so send the public domain instead of the local host
|
|
return &tls.Config{InsecureSkipVerify: true, ServerName: domain}
|
|
}
|
|
return &tls.Config{}
|
|
}
|
|
|
|
var internalAPITransport = sync.OnceValue(func() http.RoundTripper {
|
|
return &http.Transport{
|
|
DialContext: dialContextInternalAPI,
|
|
TLSClientConfig: internalAPITLSConfig(setting.Protocol, setting.LocalURL, setting.AppDomain),
|
|
}
|
|
})
|
|
|
|
func NewInternalRequest(ctx context.Context, url, method string) *httplib.ClientRequest {
|
|
if setting.InternalToken == "" {
|
|
log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q.
|
|
Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf)
|
|
}
|
|
|
|
if !strings.HasPrefix(url, setting.LocalURL) {
|
|
log.Fatal("Invalid internal request URL: %q", url)
|
|
}
|
|
|
|
return httplib.NewClientRequest(method, url).
|
|
SetContext(ctx).
|
|
SetTransport(internalAPITransport()).
|
|
Header("X-Real-IP", getClientIP()).
|
|
Header("X-Gitea-Internal-Auth", "Bearer "+setting.InternalToken)
|
|
}
|
|
|
|
func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) *httplib.ClientRequest {
|
|
req := NewInternalRequest(ctx, url, method)
|
|
if len(body) == 1 {
|
|
req.Header("Content-Type", "application/json")
|
|
jsonBytes, _ := json.Marshal(body[0])
|
|
req.Body(jsonBytes)
|
|
} else if len(body) > 1 {
|
|
log.Fatal("Too many arguments for newInternalRequestAPI")
|
|
}
|
|
|
|
req.SetReadWriteTimeout(60 * time.Second)
|
|
return req
|
|
}
|