// 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 ( "context" "errors" "fmt" "io" "net/url" "regexp" "strconv" "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 interface { // Protocol returns the protocol (e.g. git, https, file). It should never // return the empty string. Protocol() string // User returns the user or an empty string if none is given. User() string // Password returns the password or an empty string if none is given. Password() string // Host returns the host or an empty string if none is given. Host() string // Port returns the port or 0 if there is no port or a default should be // used. Port() int // Path returns the repository path. Path() string // String returns a string representation of the Git URL. String() 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 } u, err := url.Parse(endpoint) if err != nil { return nil, plumbing.NewPermanentError(err) } if !u.IsAbs() { return nil, plumbing.NewPermanentError(fmt.Errorf( "invalid endpoint: %s", endpoint, )) } return urlEndpoint{u}, nil } type urlEndpoint struct { *url.URL } func (e urlEndpoint) Protocol() string { return e.URL.Scheme } func (e urlEndpoint) Host() string { return e.URL.Hostname() } func (e urlEndpoint) User() string { if e.URL.User == nil { return "" } return e.URL.User.Username() } func (e urlEndpoint) Password() string { if e.URL.User == nil { return "" } p, _ := e.URL.User.Password() return p } func (e urlEndpoint) Port() int { p := e.URL.Port() if p == "" { return 0 } i, err := strconv.Atoi(e.URL.Port()) if err != nil { return 0 } return i } func (e urlEndpoint) Path() string { var res string = e.URL.Path if e.URL.RawQuery != "" { res += "?" + e.URL.RawQuery } if e.URL.Fragment != "" { res += "#" + e.URL.Fragment } return res } type scpEndpoint struct { user string host string port string path string } func (e *scpEndpoint) Protocol() string { return "ssh" } func (e *scpEndpoint) User() string { return e.user } func (e *scpEndpoint) Password() string { return "" } func (e *scpEndpoint) Host() string { return e.host } func (e *scpEndpoint) Path() string { return e.path } func (e *scpEndpoint) Port() int { i, err := strconv.Atoi(e.port) if err != nil { return 22 } return i } func (e *scpEndpoint) String() string { var user string if e.user != "" { user = fmt.Sprintf("%s@", e.user) } return fmt.Sprintf("%s%s:%s", user, e.host, e.path) } type fileEndpoint struct { path string } func (e *fileEndpoint) Protocol() string { return "file" } func (e *fileEndpoint) User() string { return "" } func (e *fileEndpoint) Password() string { return "" } func (e *fileEndpoint) Host() string { return "" } func (e *fileEndpoint) Port() int { return 0 } func (e *fileEndpoint) Path() string { return e.path } func (e *fileEndpoint) String() string { return e.path } var ( isSchemeRegExp = regexp.MustCompile(`^[^:]+://`) scpLikeUrlRegExp = regexp.MustCompile(`^(?:(?P[^@]+)@)?(?P[^:\s]+):(?:(?P[0-9]{1,5})/)?(?P[^\\].*)$`) ) func parseSCPLike(endpoint string) (Endpoint, bool) { if isSchemeRegExp.MatchString(endpoint) || !scpLikeUrlRegExp.MatchString(endpoint) { return nil, false } m := scpLikeUrlRegExp.FindStringSubmatch(endpoint) return &scpEndpoint{ user: m[1], host: m[2], port: m[3], path: m[4], }, true } func parseFile(endpoint string) (Endpoint, bool) { if isSchemeRegExp.MatchString(endpoint) { return nil, false } path := endpoint return &fileEndpoint{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) } }