// 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" giturl "gopkg.in/src-d/go-git.v4/internal/url" "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) { if giturl.MatchesScheme(endpoint) || !giturl.MatchesScpLike(endpoint) { return nil, false } user, host, portStr, path := giturl.FindScpLikeComponents(endpoint) port, err := strconv.Atoi(portStr) if err != nil { port = 22 } return &Endpoint{ Protocol: "ssh", User: user, Host: host, Port: port, Path: path, }, true } func parseFile(endpoint string) (*Endpoint, bool) { if giturl.MatchesScheme(endpoint) { 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) } }