gitea.tea/modules/git/types.go
Suhaib 8240b385b1 feat(login): support TLS client certificates for mutual TLS servers
Gitea instances behind a reverse proxy that requires mutual TLS (nginx'
ssl_verify_client, or the equivalent in Traefik or HAProxy) reject every
tea request at the proxy, before it reaches Gitea. The user sees an opaque
403/400 from the proxy rather than an authentication error, and there was
no way to make tea present a certificate: only full verification and
--insecure were configurable.

Add an optional client certificate to a login, stored as client_cert and
client_key next to the existing insecure flag:

    tea login add --url https://git.example.com --token ... \
        --client-cert ~/.certs/client.crt \
        --client-key ~/.certs/client.key

The pair is loaded by Login.TLSConfig(), which now builds the TLS config
for every outgoing connection: SDK API calls, the OAuth authorization and
token refresh flows, and the raw 'tea api' client. Because the certificate
lives on the login, all commands using it present it automatically. Paths
support ~ expansion so they can be written into the config file by hand,
and clones pass the same material to git as http.sslCert/http.sslKey.

Both fields must be set together; setting only one, or pointing at a
certificate that cannot be loaded, is a hard error rather than a silent
fallback to a plain connection, which would fail at the proxy with the
same opaque error the certificate is meant to avoid.

Requests without either setting keep Go's default TLS behaviour: the
config is only overridden when the login asks for it.

Fixes #451

Signed-off-by: Suhaib <suhaib.abdulquddos@gmail.com>
2026-09-04 21:21:44 -07:00

169 lines
4.7 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"errors"
"strings"
)
var (
// ErrRepositoryNotExists indicates the requested path is not inside a git repository.
ErrRepositoryNotExists = errors.New("repository does not exist")
// ErrBranchExists indicates the requested branch already exists locally.
ErrBranchExists = errors.New("branch already exists")
)
// AuthMethod carries backend-agnostic authentication information for git operations.
type AuthMethod struct {
Scheme string
Username string
Password string
KeyFile string
KeyPassphrase string
}
// CloneOptions describes repository clone behavior.
type CloneOptions struct {
Depth int
Insecure bool
// ClientCert and ClientKey are paths to a PEM encoded TLS client
// certificate and its key, passed to git as http.sslCert/http.sslKey so
// clones from mutual-TLS protected servers succeed.
ClientCert string
ClientKey string
}
// RepositoryBackend is the backend abstraction used by TeaRepo.
type RepositoryBackend interface {
WorkTree() string
Config() (*Config, error)
Head() (*Reference, error)
AddRemote(name, remoteURL string) error
SetBranchUpstream(branchName, remoteName, remoteBranch string) error
Fetch(remoteName string, refspecs []string, auth *AuthMethod) error
CreateTrackingBranch(localBranchName, remoteBranchName, remoteName string) error
Checkout(ref ReferenceName) error
DeleteLocalBranch(branchName string) error
DeleteRemoteBranch(remoteName, remoteBranch string, auth *AuthMethod) error
ListReferences(prefixes ...string) ([]*Reference, error)
PushToAgitFlowPR(remoteName, head, base, topic string, pushOptions map[string]string, auth *AuthMethod) error
}
// Backend opens and clones repositories using a concrete git implementation.
type Backend interface {
Name() string
Open(path string) (RepositoryBackend, error)
Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (RepositoryBackend, error)
}
// Config mirrors the repository config fields tea needs.
type Config struct {
Remotes map[string]*RemoteConfig
Branches map[string]*Branch
}
// RemoteConfig stores remote configuration.
type RemoteConfig struct {
Name string
URLs []string
}
// Branch stores branch configuration.
type Branch struct {
Name string
Remote string
Merge ReferenceName
}
// Validate checks whether the branch contains the fields tea needs.
func (b *Branch) Validate() error {
if b == nil || b.Name == "" {
return errors.New("branch name is required")
}
return nil
}
// Remote wraps a configured git remote.
type Remote struct {
repo *TeaRepo
config *RemoteConfig
}
// Config returns the remote configuration.
func (r *Remote) Config() *RemoteConfig {
if r == nil {
return nil
}
return r.config
}
// ReferenceName identifies a git reference.
type ReferenceName string
func (r ReferenceName) String() string { return string(r) }
// Short returns the short display name for the reference.
func (r ReferenceName) Short() string {
s := string(r)
switch {
case strings.HasPrefix(s, "refs/heads/"):
return strings.TrimPrefix(s, "refs/heads/")
case strings.HasPrefix(s, "refs/remotes/"):
return strings.TrimPrefix(s, "refs/remotes/")
case strings.HasPrefix(s, "refs/tags/"):
return strings.TrimPrefix(s, "refs/tags/")
default:
return s
}
}
// IsBranch reports whether the reference points to a local branch.
func (r ReferenceName) IsBranch() bool {
return strings.HasPrefix(string(r), "refs/heads/")
}
// IsRemote reports whether the reference points to a remote-tracking branch.
func (r ReferenceName) IsRemote() bool {
return strings.HasPrefix(string(r), "refs/remotes/")
}
// IsTag reports whether the reference points to a tag.
func (r ReferenceName) IsTag() bool {
return strings.HasPrefix(string(r), "refs/tags/")
}
// Hash wraps a git object id.
type Hash string
func (h Hash) String() string { return string(h) }
// Reference stores a resolved git ref and its hash.
type Reference struct {
name ReferenceName
hash Hash
}
// Name returns the reference name.
func (r *Reference) Name() ReferenceName { return r.name }
// Hash returns the reference hash.
func (r *Reference) Hash() Hash { return r.hash }
// NewBranchReferenceName constructs a local branch ref name.
func NewBranchReferenceName(name string) ReferenceName {
if strings.HasPrefix(name, "refs/") {
return ReferenceName(name)
}
return ReferenceName("refs/heads/" + name)
}
// NewRemoteReferenceName constructs a remote-tracking ref name.
func NewRemoteReferenceName(remote, name string) ReferenceName {
if strings.HasPrefix(name, "refs/") {
return ReferenceName(name)
}
return ReferenceName("refs/remotes/" + remote + "/" + name)
}