// Package http implements the HTTP transport protocol.
package http
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"sync"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/utils/ioutil"
"github.com/golang/groupcache/lru"
)
// it requires a bytes.Buffer, because we need to know the length
func applyHeadersToRequest(req *http.Request, content *bytes.Buffer, host string, requestType string) {
req.Header.Add("User-Agent", "git/1.0")
req.Header.Add("Host", host) // host:port
if content == nil {
req.Header.Add("Accept", "*/*")
return
}
req.Header.Add("Accept", fmt.Sprintf("application/x-%s-result", requestType))
req.Header.Add("Content-Type", fmt.Sprintf("application/x-%s-request", requestType))
req.Header.Add("Content-Length", strconv.Itoa(content.Len()))
}
const infoRefsPath = "/info/refs"
func advertisedReferences(ctx context.Context, s *session, serviceName string) (ref *packp.AdvRefs, err error) {
url := fmt.Sprintf(
"%s%s?service=%s",
s.endpoint.String(), infoRefsPath, serviceName,
)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
s.ApplyAuthToRequest(req)
applyHeadersToRequest(req, nil, s.endpoint.Host, serviceName)
res, err := s.client.Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
s.ModifyEndpointIfRedirect(res)
defer ioutil.CheckClose(res.Body, &err)
if err = NewErr(res); err != nil {
return nil, err
}
ar := packp.NewAdvRefs()
if err = ar.Decode(res.Body); err != nil {
if err == packp.ErrEmptyAdvRefs {
err = transport.ErrEmptyRemoteRepository
}
return nil, err
}
// Git 2.41+ returns a zero-id plus capabilities when an empty
// repository is being cloned. This skips the existing logic within
// advrefs_decode.decodeFirstHash, which expects a flush-pkt instead.
//
// This logic aligns with plumbing/transport/internal/common/common.go.
if ar.IsEmpty() &&
// Empty repositories are valid for git-receive-pack.
transport.ReceivePackServiceName != serviceName {
return nil, transport.ErrEmptyRemoteRepository
}
transport.FilterUnsupportedCapabilities(ar.Capabilities)
s.advRefs = ar
return ar, nil
}
type client struct {
client *http.Client
transports *lru.Cache
mutex sync.RWMutex
}
// ClientOptions holds user configurable options for the client.
type ClientOptions struct {
// CacheMaxEntries is the max no. of entries that the transport objects
// cache will hold at any given point of time. It must be a positive integer.
// Calling `client.addTransport()` after the cache has reached the specified
// size, will result in the least recently used transport getting deleted
// before the provided transport is added to the cache.
CacheMaxEntries int
}
var (
// defaultTransportCacheSize is the default capacity of the transport objects cache.
// Its value is 0 because transport caching is turned off by default and is an
// opt-in feature.
defaultTransportCacheSize = 0
// DefaultClient is the default HTTP client, which uses a net/http client configured
// with http.DefaultTransport.
DefaultClient = NewClient(nil)
)
// NewClient creates a new client with a custom net/http client.
// See `InstallProtocol` to install and override default http client.
// If the net/http client is nil or empty, it will use a net/http client configured
// with http.DefaultTransport.
//
// Note that for HTTP client cannot distinguish between private repositories and
// unexistent repositories on GitHub. So it returns `ErrAuthorizationRequired`
// for both.
func NewClient(c *http.Client) transport.Transport {
if c == nil {
c = &http.Client{
Transport: http.DefaultTransport,
}
}
return NewClientWithOptions(c, &ClientOptions{
CacheMaxEntries: defaultTransportCacheSize,
})
}
// NewClientWithOptions returns a new client configured with the provided net/http client
// and other custom options specific to the client.
// If the net/http client is nil or empty, it will use a net/http client configured
// with http.DefaultTransport.
func NewClientWithOptions(c *http.Client, opts *ClientOptions) transport.Transport {
if c == nil {
c = &http.Client{
Transport: http.DefaultTransport,
}
}
cl := &client{
client: c,
}
if opts != nil {
if opts.CacheMaxEntries > 0 {
cl.transports = lru.New(opts.CacheMaxEntries)
}
}
return cl
}
func (c *client) NewUploadPackSession(ep *transport.Endpoint, auth transport.AuthMethod) (
transport.UploadPackSession, error) {
return newUploadPackSession(c, ep, auth)
}
func (c *client) NewReceivePackSession(ep *transport.Endpoint, auth transport.AuthMethod) (
transport.ReceivePackSession, error) {
return newReceivePackSession(c, ep, auth)
}
type session struct {
auth AuthMethod
client *http.Client
endpoint *transport.Endpoint
advRefs *packp.AdvRefs
}
func transportWithInsecureTLS(transport *http.Transport) {
if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{}
}
transport.TLSClientConfig.InsecureSkipVerify = true
}
func transportWithCABundle(transport *http.Transport, caBundle []byte) error {
rootCAs, err := x509.SystemCertPool()
if err != nil {
return err
}
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}
rootCAs.AppendCertsFromPEM(caBundle)
if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{}
}
transport.TLSClientConfig.RootCAs = rootCAs
return nil
}
func transportWithProxy(transport *http.Transport, proxyURL *url.URL) {
transport.Proxy = http.ProxyURL(proxyURL)
}
func configureTransport(transport *http.Transport, ep *transport.Endpoint) error {
if len(ep.CaBundle) > 0 {
if err := transportWithCABundle(transport, ep.CaBundle); err != nil {
return err
}
}
if ep.InsecureSkipTLS {
transportWithInsecureTLS(transport)
}
if ep.Proxy.URL != "" {
proxyURL, err := ep.Proxy.FullURL()
if err != nil {
return err
}
transportWithProxy(transport, proxyURL)
}
return nil
}
func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (*session, error) {
var httpClient *http.Client
// We need to configure the http transport if there are transport specific
// options present in the endpoint.
if len(ep.CaBundle) > 0 || ep.InsecureSkipTLS || ep.Proxy.URL != "" {
var transport *http.Transport
// if the client wasn't configured to have a cache for transports then just configure
// the transport and use it directly, otherwise try to use the cache.
if c.transports == nil {
tr, ok := c.client.Transport.(*http.Transport)
if !ok {
return nil, fmt.Errorf("expected underlying client transport to be of type: %s; got: %s",
reflect.TypeOf(transport), reflect.TypeOf(c.client.Transport))
}
transport = tr.Clone()
configureTransport(transport, ep)
} else {
transportOpts := transportOptions{
caBundle: string(ep.CaBundle),
insecureSkipTLS: ep.InsecureSkipTLS,
}
if ep.Proxy.URL != "" {
proxyURL, err := ep.Proxy.FullURL()
if err != nil {
return nil, err
}
transportOpts.proxyURL = *proxyURL
}
var found bool
transport, found = c.fetchTransport(transportOpts)
if !found {
transport = c.client.Transport.(*http.Transport).Clone()
configureTransport(transport, ep)
c.addTransport(transportOpts, transport)
}
}
httpClient = &http.Client{
Transport: transport,
CheckRedirect: c.client.CheckRedirect,
Jar: c.client.Jar,
Timeout: c.client.Timeout,
}
} else {
httpClient = c.client
}
s := &session{
auth: basicAuthFromEndpoint(ep),
client: httpClient,
endpoint: ep,
}
if auth != nil {
a, ok := auth.(AuthMethod)
if !ok {
return nil, transport.ErrInvalidAuthMethod
}
s.auth = a
}
return s, nil
}
func (s *session) ApplyAuthToRequest(req *http.Request) {
if s.auth == nil {
return
}
s.auth.SetAuth(req)
}
func (s *session) ModifyEndpointIfRedirect(res *http.Response) {
if res.Request == nil {
return
}
r := res.Request
if !strings.HasSuffix(r.URL.Path, infoRefsPath) {
return
}
h, p, err := net.SplitHostPort(r.URL.Host)
if err != nil {
h = r.URL.Host
}
if p != "" {
port, err := strconv.Atoi(p)
if err == nil {
s.endpoint.Port = port
}
}
s.endpoint.Host = h
s.endpoint.Protocol = r.URL.Scheme
s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)]
}
func (*session) Close() error {
return nil
}
// AuthMethod is concrete implementation of common.AuthMethod for HTTP services
type AuthMethod interface {
transport.AuthMethod
SetAuth(r *http.Request)
}
func basicAuthFromEndpoint(ep *transport.Endpoint) *BasicAuth {
u := ep.User
if u == "" {
return nil
}
return &BasicAuth{u, ep.Password}
}
// BasicAuth represent a HTTP basic auth
type BasicAuth struct {
Username, Password string
}
func (a *BasicAuth) SetAuth(r *http.Request) {
if a == nil {
return
}
r.SetBasicAuth(a.Username, a.Password)
}
// Name is name of the auth
func (a *BasicAuth) Name() string {
return "http-basic-auth"
}
func (a *BasicAuth) String() string {
masked := "*******"
if a.Password == "" {
masked = "<empty>"
}
return fmt.Sprintf("%s - %s:%s", a.Name(), a.Username, masked)
}
// TokenAuth implements an http.AuthMethod that can be used with http transport
// to authenticate with HTTP token authentication (also known as bearer
// authentication).
//
// IMPORTANT: If you are looking to use OAuth tokens with popular servers (e.g.
// GitHub, Bitbucket, GitLab) you should use BasicAuth instead. These servers
// use basic HTTP authentication, with the OAuth token as user or password.
// Check the documentation of your git server for details.
type TokenAuth struct {
Token string
}
func (a *TokenAuth) SetAuth(r *http.Request) {
if a == nil {
return
}
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", a.Token))
}
// Name is name of the auth
func (a *TokenAuth) Name() string {
return "http-token-auth"
}
func (a *TokenAuth) String() string {
masked := "*******"
if a.Token == "" {
masked = "<empty>"
}
return fmt.Sprintf("%s - %s", a.Name(), masked)
}
// Err is a dedicated error to return errors based on status code
type Err struct {
Response *http.Response
Reason string
}
// NewErr returns a new Err based on a http response and closes response body
// if needed
func NewErr(r *http.Response) error {
if r.StatusCode >= http.StatusOK && r.StatusCode < http.StatusMultipleChoices {
return nil
}
var reason string
// If a response message is present, add it to error
var messageBuffer bytes.Buffer
if r.Body != nil {
messageLength, _ := messageBuffer.ReadFrom(r.Body)
if messageLength > 0 {
reason = messageBuffer.String()
}
_ = r.Body.Close()
}
switch r.StatusCode {
case http.StatusUnauthorized:
return transport.ErrAuthenticationRequired
case http.StatusForbidden:
return transport.ErrAuthorizationFailed
case http.StatusNotFound:
return transport.ErrRepositoryNotFound
}
return plumbing.NewUnexpectedError(&Err{r, reason})
}
// StatusCode returns the status code of the response
func (e *Err) StatusCode() int {
return e.Response.StatusCode
}
func (e *Err) Error() string {
return fmt.Sprintf("unexpected requesting %q status code: %d",
e.Response.Request.URL, e.Response.StatusCode,
)
}