2018-11-27 21:52:20 +00:00
|
|
|
// Package transport includes the implementation for different transport
|
|
|
|
// protocols.
|
|
|
|
//
|
|
|
|
// `Client` can be used to fetch and send packfiles to a git server.
|
|
|
|
// The `client` package provides higher level functions to instantiate the
|
|
|
|
// appropriate `Client` based on the repository URL.
|
|
|
|
//
|
|
|
|
// go-git supports HTTP and SSH (see `Protocols`), but you can also install
|
|
|
|
// your own protocols (see the `client` package).
|
|
|
|
//
|
|
|
|
// Each protocol has its own implementation of `Client`, but you should
|
|
|
|
// generally not use them directly, use `client.NewClient` instead.
|
|
|
|
package transport
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2019-04-17 02:04:23 +00:00
|
|
|
giturl "gopkg.in/src-d/go-git.v4/internal/url"
|
2018-11-27 21:52:20 +00:00
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp"
|
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrRepositoryNotFound = errors.New("repository not found")
|
|
|
|
ErrEmptyRemoteRepository = errors.New("remote repository is empty")
|
|
|
|
ErrAuthenticationRequired = errors.New("authentication required")
|
|
|
|
ErrAuthorizationFailed = errors.New("authorization failed")
|
|
|
|
ErrEmptyUploadPackRequest = errors.New("empty git-upload-pack given")
|
|
|
|
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
|
|
|
ErrAlreadyConnected = errors.New("session already established")
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
UploadPackServiceName = "git-upload-pack"
|
|
|
|
ReceivePackServiceName = "git-receive-pack"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Transport can initiate git-upload-pack and git-receive-pack processes.
|
|
|
|
// It is implemented both by the client and the server, making this a RPC.
|
|
|
|
type Transport interface {
|
|
|
|
// NewUploadPackSession starts a git-upload-pack session for an endpoint.
|
|
|
|
NewUploadPackSession(*Endpoint, AuthMethod) (UploadPackSession, error)
|
|
|
|
// NewReceivePackSession starts a git-receive-pack session for an endpoint.
|
|
|
|
NewReceivePackSession(*Endpoint, AuthMethod) (ReceivePackSession, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
type Session interface {
|
|
|
|
// AdvertisedReferences retrieves the advertised references for a
|
|
|
|
// repository.
|
|
|
|
// If the repository does not exist, returns ErrRepositoryNotFound.
|
|
|
|
// If the repository exists, but is empty, returns ErrEmptyRemoteRepository.
|
|
|
|
AdvertisedReferences() (*packp.AdvRefs, error)
|
|
|
|
io.Closer
|
|
|
|
}
|
|
|
|
|
|
|
|
type AuthMethod interface {
|
|
|
|
fmt.Stringer
|
|
|
|
Name() string
|
|
|
|
}
|
|
|
|
|
|
|
|
// UploadPackSession represents a git-upload-pack session.
|
|
|
|
// A git-upload-pack session has two steps: reference discovery
|
|
|
|
// (AdvertisedReferences) and uploading pack (UploadPack).
|
|
|
|
type UploadPackSession interface {
|
|
|
|
Session
|
|
|
|
// UploadPack takes a git-upload-pack request and returns a response,
|
|
|
|
// including a packfile. Don't be confused by terminology, the client
|
|
|
|
// side of a git-upload-pack is called git-fetch-pack, although here
|
|
|
|
// the same interface is used to make it RPC-like.
|
|
|
|
UploadPack(context.Context, *packp.UploadPackRequest) (*packp.UploadPackResponse, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReceivePackSession represents a git-receive-pack session.
|
|
|
|
// A git-receive-pack session has two steps: reference discovery
|
|
|
|
// (AdvertisedReferences) and receiving pack (ReceivePack).
|
|
|
|
// In that order.
|
|
|
|
type ReceivePackSession interface {
|
|
|
|
Session
|
|
|
|
// ReceivePack sends an update references request and a packfile
|
|
|
|
// reader and returns a ReportStatus and error. Don't be confused by
|
|
|
|
// terminology, the client side of a git-receive-pack is called
|
|
|
|
// git-send-pack, although here the same interface is used to make it
|
|
|
|
// RPC-like.
|
|
|
|
ReceivePack(context.Context, *packp.ReferenceUpdateRequest) (*packp.ReportStatus, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Endpoint represents a Git URL in any supported protocol.
|
|
|
|
type Endpoint struct {
|
|
|
|
// Protocol is the protocol of the endpoint (e.g. git, https, file).
|
|
|
|
Protocol string
|
|
|
|
// User is the user.
|
|
|
|
User string
|
|
|
|
// Password is the password.
|
|
|
|
Password string
|
|
|
|
// Host is the host.
|
|
|
|
Host string
|
|
|
|
// Port is the port to connect, if 0 the default port for the given protocol
|
|
|
|
// wil be used.
|
|
|
|
Port int
|
|
|
|
// Path is the repository path.
|
|
|
|
Path string
|
|
|
|
}
|
|
|
|
|
|
|
|
var defaultPorts = map[string]int{
|
|
|
|
"http": 80,
|
|
|
|
"https": 443,
|
|
|
|
"git": 9418,
|
|
|
|
"ssh": 22,
|
|
|
|
}
|
|
|
|
|
|
|
|
// String returns a string representation of the Git URL.
|
|
|
|
func (u *Endpoint) String() string {
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if u.Protocol != "" {
|
|
|
|
buf.WriteString(u.Protocol)
|
|
|
|
buf.WriteByte(':')
|
|
|
|
}
|
|
|
|
|
|
|
|
if u.Protocol != "" || u.Host != "" || u.User != "" || u.Password != "" {
|
|
|
|
buf.WriteString("//")
|
|
|
|
|
|
|
|
if u.User != "" || u.Password != "" {
|
|
|
|
buf.WriteString(url.PathEscape(u.User))
|
|
|
|
if u.Password != "" {
|
|
|
|
buf.WriteByte(':')
|
|
|
|
buf.WriteString(url.PathEscape(u.Password))
|
|
|
|
}
|
|
|
|
|
|
|
|
buf.WriteByte('@')
|
|
|
|
}
|
|
|
|
|
|
|
|
if u.Host != "" {
|
|
|
|
buf.WriteString(u.Host)
|
|
|
|
|
|
|
|
if u.Port != 0 {
|
|
|
|
port, ok := defaultPorts[strings.ToLower(u.Protocol)]
|
|
|
|
if !ok || ok && port != u.Port {
|
|
|
|
fmt.Fprintf(&buf, ":%d", u.Port)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if u.Path != "" && u.Path[0] != '/' && u.Host != "" {
|
|
|
|
buf.WriteByte('/')
|
|
|
|
}
|
|
|
|
|
|
|
|
buf.WriteString(u.Path)
|
|
|
|
return buf.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewEndpoint(endpoint string) (*Endpoint, error) {
|
|
|
|
if e, ok := parseSCPLike(endpoint); ok {
|
|
|
|
return e, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if e, ok := parseFile(endpoint); ok {
|
|
|
|
return e, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return parseURL(endpoint)
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseURL(endpoint string) (*Endpoint, error) {
|
|
|
|
u, err := url.Parse(endpoint)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !u.IsAbs() {
|
|
|
|
return nil, plumbing.NewPermanentError(fmt.Errorf(
|
|
|
|
"invalid endpoint: %s", endpoint,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
var user, pass string
|
|
|
|
if u.User != nil {
|
|
|
|
user = u.User.Username()
|
|
|
|
pass, _ = u.User.Password()
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Endpoint{
|
|
|
|
Protocol: u.Scheme,
|
|
|
|
User: user,
|
|
|
|
Password: pass,
|
|
|
|
Host: u.Hostname(),
|
|
|
|
Port: getPort(u),
|
|
|
|
Path: getPath(u),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getPort(u *url.URL) int {
|
|
|
|
p := u.Port()
|
|
|
|
if p == "" {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
i, err := strconv.Atoi(p)
|
|
|
|
if err != nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
|
|
|
|
func getPath(u *url.URL) string {
|
|
|
|
var res string = u.Path
|
|
|
|
if u.RawQuery != "" {
|
|
|
|
res += "?" + u.RawQuery
|
|
|
|
}
|
|
|
|
|
|
|
|
if u.Fragment != "" {
|
|
|
|
res += "#" + u.Fragment
|
|
|
|
}
|
|
|
|
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseSCPLike(endpoint string) (*Endpoint, bool) {
|
2019-04-17 02:04:23 +00:00
|
|
|
if giturl.MatchesScheme(endpoint) || !giturl.MatchesScpLike(endpoint) {
|
2018-11-27 21:52:20 +00:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
2019-04-17 02:04:23 +00:00
|
|
|
user, host, portStr, path := giturl.FindScpLikeComponents(endpoint)
|
|
|
|
port, err := strconv.Atoi(portStr)
|
2018-11-27 21:52:20 +00:00
|
|
|
if err != nil {
|
|
|
|
port = 22
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Endpoint{
|
|
|
|
Protocol: "ssh",
|
2019-04-17 02:04:23 +00:00
|
|
|
User: user,
|
|
|
|
Host: host,
|
2018-11-27 21:52:20 +00:00
|
|
|
Port: port,
|
2019-04-17 02:04:23 +00:00
|
|
|
Path: path,
|
2018-11-27 21:52:20 +00:00
|
|
|
}, true
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseFile(endpoint string) (*Endpoint, bool) {
|
2019-04-17 02:04:23 +00:00
|
|
|
if giturl.MatchesScheme(endpoint) {
|
2018-11-27 21:52:20 +00:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
path := endpoint
|
|
|
|
return &Endpoint{
|
|
|
|
Protocol: "file",
|
|
|
|
Path: path,
|
|
|
|
}, true
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnsupportedCapabilities are the capabilities not supported by any client
|
|
|
|
// implementation
|
|
|
|
var UnsupportedCapabilities = []capability.Capability{
|
|
|
|
capability.MultiACK,
|
|
|
|
capability.MultiACKDetailed,
|
|
|
|
capability.ThinPack,
|
|
|
|
}
|
|
|
|
|
|
|
|
// FilterUnsupportedCapabilities it filter out all the UnsupportedCapabilities
|
|
|
|
// from a capability.List, the intended usage is on the client implementation
|
|
|
|
// to filter the capabilities from an AdvRefs message.
|
|
|
|
func FilterUnsupportedCapabilities(list *capability.List) {
|
|
|
|
for _, c := range UnsupportedCapabilities {
|
|
|
|
list.Delete(c)
|
|
|
|
}
|
|
|
|
}
|