diff options
author | Santiago M. Mola <santi@mola.io> | 2016-11-23 15:30:34 +0100 |
---|---|---|
committer | Máximo Cuadros <mcuadros@gmail.com> | 2016-11-23 15:38:12 +0100 |
commit | 08e08d771ef03df80248c80d81475fe7c5ea6fe7 (patch) | |
tree | d12e9befa22409e8cf50c5bbc4895e69fd8a5f48 /plumbing | |
parent | 844169a739fb8bf1f252d416f10d8c7034db9fe2 (diff) | |
download | go-git-08e08d771ef03df80248c80d81475fe7c5ea6fe7.tar.gz |
transport: create Client interface (#132)
* plumbing: move plumbing/client package to plumbing/transport.
* transport: create Client interface.
* A Client can instantiate any client transport service.
* InstallProtocol installs a Client for a given protocol,
instead of just a UploadPackService.
* A Client can open a session for fetch-pack or send-pack
for a specific Endpoint.
* Adapt ssh and http clients to the new client interface.
* updated doc
Diffstat (limited to 'plumbing')
28 files changed, 1440 insertions, 1277 deletions
diff --git a/plumbing/client/common.go b/plumbing/client/common.go deleted file mode 100644 index 1524753..0000000 --- a/plumbing/client/common.go +++ /dev/null @@ -1,46 +0,0 @@ -// Package clients includes the implementation for different transport protocols -// -// go-git needs the packfile and the refs of the repo. The -// `NewGitUploadPackService` function returns an object that allows to -// download them. -// -// go-git supports HTTP and SSH (see `Protocols`) for downloading the packfile -// and the refs, but you can also install your own protocols (see -// `InstallProtocol` below). -// -// Each protocol has its own implementation of -// `NewGitUploadPackService`, but you should generally not use them -// directly, use this package's `NewGitUploadPackService` instead. -package clients - -import ( - "fmt" - - "gopkg.in/src-d/go-git.v4/plumbing/client/common" - "gopkg.in/src-d/go-git.v4/plumbing/client/http" - "gopkg.in/src-d/go-git.v4/plumbing/client/ssh" -) - -// Protocols are the protocols supported by default. -var Protocols = map[string]common.GitUploadPackServiceFactory{ - "http": http.NewGitUploadPackService, - "https": http.NewGitUploadPackService, - "ssh": ssh.NewGitUploadPackService, -} - -// InstallProtocol adds or modifies an existing protocol. -func InstallProtocol(scheme string, f common.GitUploadPackServiceFactory) { - Protocols[scheme] = f -} - -// NewGitUploadPackService returns the appropriate upload pack service -// among of the set of known protocols: HTTP, SSH. See `InstallProtocol` -// to add or modify protocols. -func NewGitUploadPackService(endpoint common.Endpoint) (common.GitUploadPackService, error) { - f, ok := Protocols[endpoint.Scheme] - if !ok { - return nil, fmt.Errorf("unsupported scheme %q", endpoint.Scheme) - } - - return f(endpoint), nil -} diff --git a/plumbing/client/common/common_test.go b/plumbing/client/common/common_test.go deleted file mode 100644 index cf4d871..0000000 --- a/plumbing/client/common/common_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package common - -import ( - "bytes" - "encoding/base64" - "testing" - - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/format/packp" - - . "gopkg.in/check.v1" -) - -func Test(t *testing.T) { TestingT(t) } - -type SuiteCommon struct{} - -var _ = Suite(&SuiteCommon{}) - -func (s *SuiteCommon) TestNewEndpoint(c *C) { - e, err := NewEndpoint("ssh://git@github.com/user/repository.git") - c.Assert(err, IsNil) - c.Assert(e.String(), Equals, "ssh://git@github.com/user/repository.git") -} - -func (s *SuiteCommon) TestNewEndpointSCPLike(c *C) { - e, err := NewEndpoint("git@github.com:user/repository.git") - c.Assert(err, IsNil) - c.Assert(e.String(), Equals, "ssh://git@github.com/user/repository.git") -} - -func (s *SuiteCommon) TestNewEndpointWrongForgat(c *C) { - e, err := NewEndpoint("foo") - c.Assert(err, Not(IsNil)) - c.Assert(e.Host, Equals, "") -} - -const CapabilitiesFixture = "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEADmulti_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress include-tag multi_ack_detailed no-done symref=HEAD:refs/heads/master agent=git/2:2.4.8~dbussink-fix-enterprise-tokens-compilation-1167-gc7006cf" - -func (s *SuiteCommon) TestCapabilitiesSymbolicReference(c *C) { - cap := packp.NewCapabilities() - cap.Decode(CapabilitiesFixture) - c.Assert(cap.SymbolicReference("HEAD"), Equals, "refs/heads/master") -} - -const GitUploadPackInfoFixture = "MDAxZSMgc2VydmljZT1naXQtdXBsb2FkLXBhY2sKMDAwMDAxMGM2ZWNmMGVmMmMyZGZmYjc5NjAzM2U1YTAyMjE5YWY4NmVjNjU4NGU1IEhFQUQAbXVsdGlfYWNrIHRoaW4tcGFjayBzaWRlLWJhbmQgc2lkZS1iYW5kLTY0ayBvZnMtZGVsdGEgc2hhbGxvdyBuby1wcm9ncmVzcyBpbmNsdWRlLXRhZyBtdWx0aV9hY2tfZGV0YWlsZWQgbm8tZG9uZSBzeW1yZWY9SEVBRDpyZWZzL2hlYWRzL21hc3RlciBhZ2VudD1naXQvMjoyLjQuOH5kYnVzc2luay1maXgtZW50ZXJwcmlzZS10b2tlbnMtY29tcGlsYXRpb24tMTE2Ny1nYzcwMDZjZgowMDNmZThkM2ZmYWI1NTI4OTVjMTliOWZjZjdhYTI2NGQyNzdjZGUzMzg4MSByZWZzL2hlYWRzL2JyYW5jaAowMDNmNmVjZjBlZjJjMmRmZmI3OTYwMzNlNWEwMjIxOWFmODZlYzY1ODRlNSByZWZzL2hlYWRzL21hc3RlcgowMDNlYjhlNDcxZjU4YmNiY2E2M2IwN2JkYTIwZTQyODE5MDQwOWMyZGI0NyByZWZzL3B1bGwvMS9oZWFkCjAwMDA=" - -func (s *SuiteCommon) TestGitUploadPackInfo(c *C) { - b, _ := base64.StdEncoding.DecodeString(GitUploadPackInfoFixture) - - i := NewGitUploadPackInfo() - err := i.Decode(bytes.NewBuffer(b)) - c.Assert(err, IsNil) - - name := i.Capabilities.SymbolicReference("HEAD") - c.Assert(name, Equals, "refs/heads/master") - c.Assert(i.Refs, HasLen, 4) - - ref := i.Refs[plumbing.ReferenceName(name)] - c.Assert(ref, NotNil) - c.Assert(ref.Hash().String(), Equals, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5") - - ref = i.Refs[plumbing.HEAD] - c.Assert(ref, NotNil) - c.Assert(ref.Target(), Equals, plumbing.ReferenceName(name)) -} - -const GitUploadPackInfoNoHEADFixture = "MDAxZSMgc2VydmljZT1naXQtdXBsb2FkLXBhY2sKMDAwMDAwYmNkN2UxZmVlMjYxMjM0YmIzYTQzYzA5NmY1NTg3NDhhNTY5ZDc5ZWZmIHJlZnMvaGVhZHMvdjQAbXVsdGlfYWNrIHRoaW4tcGFjayBzaWRlLWJhbmQgc2lkZS1iYW5kLTY0ayBvZnMtZGVsdGEgc2hhbGxvdyBuby1wcm9ncmVzcyBpbmNsdWRlLXRhZyBtdWx0aV9hY2tfZGV0YWlsZWQgbm8tZG9uZSBhZ2VudD1naXQvMS45LjEKMDAwMA==" - -func (s *SuiteCommon) TestGitUploadPackInfoNoHEAD(c *C) { - b, _ := base64.StdEncoding.DecodeString(GitUploadPackInfoNoHEADFixture) - - i := NewGitUploadPackInfo() - err := i.Decode(bytes.NewBuffer(b)) - c.Assert(err, IsNil) - - name := i.Capabilities.SymbolicReference("HEAD") - c.Assert(name, Equals, "") - c.Assert(i.Refs, HasLen, 1) - - ref := i.Refs["refs/heads/v4"] - c.Assert(ref, NotNil) - c.Assert(ref.Hash().String(), Equals, "d7e1fee261234bb3a43c096f558748a569d79eff") -} - -func (s *SuiteCommon) TestGitUploadPackInfoEmpty(c *C) { - b := bytes.NewBuffer(nil) - - i := NewGitUploadPackInfo() - err := i.Decode(b) - c.Assert(err, ErrorMatches, "permanent.*empty.*") -} - -func (s *SuiteCommon) TestGitUploadPackEncode(c *C) { - info := NewGitUploadPackInfo() - info.Capabilities.Add("symref", "HEAD:refs/heads/master") - - ref := plumbing.ReferenceName("refs/heads/master") - hash := plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5") - info.Refs = map[plumbing.ReferenceName]*plumbing.Reference{ - plumbing.HEAD: plumbing.NewSymbolicReference(plumbing.HEAD, ref), - ref: plumbing.NewHashReference(ref, hash), - } - - c.Assert(info.Head(), NotNil) - c.Assert(info.String(), Equals, - "001e# service=git-upload-pack\n"+ - "000000506ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00symref=HEAD:refs/heads/master\n"+ - "003f6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/master\n"+ - "0000", - ) -} - -func (s *SuiteCommon) TestGitUploadPackRequest(c *C) { - r := &GitUploadPackRequest{} - r.Want(plumbing.NewHash("d82f291cde9987322c8a0c81a325e1ba6159684c")) - r.Want(plumbing.NewHash("2b41ef280fdb67a9b250678686a0c3e03b0a9989")) - r.Have(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) - - c.Assert(r.String(), Equals, - "0032want d82f291cde9987322c8a0c81a325e1ba6159684c\n"+ - "0032want 2b41ef280fdb67a9b250678686a0c3e03b0a9989\n"+ - "0032have 6ecf0ef2c2dffb796033e5a02219af86ec6584e5\n0000"+ - "0009done\n", - ) -} diff --git a/plumbing/client/common_test.go b/plumbing/client/common_test.go deleted file mode 100644 index 058c4d3..0000000 --- a/plumbing/client/common_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package clients - -import ( - "fmt" - "io" - "testing" - - "gopkg.in/src-d/go-git.v4/plumbing/client/common" - - . "gopkg.in/check.v1" -) - -func Test(t *testing.T) { TestingT(t) } - -type SuiteCommon struct{} - -var _ = Suite(&SuiteCommon{}) - -func (s *SuiteCommon) TestNewGitUploadPackServiceHTTP(c *C) { - e, err := common.NewEndpoint("http://github.com/src-d/go-git") - c.Assert(err, IsNil) - - output, err := NewGitUploadPackService(e) - c.Assert(err, IsNil) - c.Assert(typeAsString(output), Equals, "*http.GitUploadPackService") - - e, err = common.NewEndpoint("https://github.com/src-d/go-git") - c.Assert(err, IsNil) - - output, err = NewGitUploadPackService(e) - c.Assert(err, IsNil) - c.Assert(typeAsString(output), Equals, "*http.GitUploadPackService") -} - -func (s *SuiteCommon) TestNewGitUploadPackServiceSSH(c *C) { - e, err := common.NewEndpoint("ssh://github.com/src-d/go-git") - c.Assert(err, IsNil) - - output, err := NewGitUploadPackService(e) - c.Assert(err, IsNil) - c.Assert(typeAsString(output), Equals, "*ssh.GitUploadPackService") -} - -func (s *SuiteCommon) TestNewGitUploadPackServiceUnknown(c *C) { - e, err := common.NewEndpoint("unknown://github.com/src-d/go-git") - c.Assert(err, IsNil) - - _, err = NewGitUploadPackService(e) - c.Assert(err, NotNil) -} - -func (s *SuiteCommon) TestInstallProtocol(c *C) { - InstallProtocol("newscheme", newDummyProtocolService) - c.Assert(Protocols["newscheme"], NotNil) -} - -type dummyProtocolService struct{} - -func newDummyProtocolService(common.Endpoint) common.GitUploadPackService { - return &dummyProtocolService{} -} - -func (s *dummyProtocolService) Connect() error { - return nil -} - -func (s *dummyProtocolService) SetAuth(auth common.AuthMethod) error { - return nil -} - -func (s *dummyProtocolService) Info() (*common.GitUploadPackInfo, error) { - return nil, nil -} - -func (s *dummyProtocolService) Fetch(r *common.GitUploadPackRequest) (io.ReadCloser, error) { - return nil, nil -} - -func (s *dummyProtocolService) Disconnect() error { - return nil -} - -func typeAsString(v interface{}) string { - return fmt.Sprintf("%T", v) -} diff --git a/plumbing/client/http/common.go b/plumbing/client/http/common.go deleted file mode 100644 index 2447995..0000000 --- a/plumbing/client/http/common.go +++ /dev/null @@ -1,76 +0,0 @@ -// Package http implements a HTTP client for go-git. -package http - -import ( - "fmt" - "net/http" - - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/client/common" -) - -// AuthMethod is concrete implementation of common.AuthMethod for HTTP services -type AuthMethod interface { - common.AuthMethod - setAuth(r *http.Request) -} - -// BasicAuth represent a HTTP basic auth -type BasicAuth struct { - username, password string -} - -// NewBasicAuth returns a BasicAuth base on the given user and password -func NewBasicAuth(username, password string) *BasicAuth { - return &BasicAuth{username, password} -} - -func (a *BasicAuth) setAuth(r *http.Request) { - 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) -} - -// Err is a dedicated error to return errors based on status code -type Err struct { - Response *http.Response -} - -// NewErr returns a new Err based on a http response -func NewErr(r *http.Response) error { - if r.StatusCode >= http.StatusOK && r.StatusCode < http.StatusMultipleChoices { - return nil - } - - switch r.StatusCode { - case http.StatusUnauthorized: - return common.ErrAuthorizationRequired - case http.StatusNotFound: - return common.ErrRepositoryNotFound - } - - return plumbing.NewUnexpectedError(&Err{r}) -} - -// 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, - ) -} diff --git a/plumbing/client/http/common_test.go b/plumbing/client/http/common_test.go deleted file mode 100644 index 7503d84..0000000 --- a/plumbing/client/http/common_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package http - -import ( - "net/http" - "testing" - - . "gopkg.in/check.v1" -) - -func Test(t *testing.T) { TestingT(t) } - -type SuiteCommon struct{} - -var _ = Suite(&SuiteCommon{}) - -func (s *SuiteCommon) TestNewBasicAuth(c *C) { - a := NewBasicAuth("foo", "qux") - - c.Assert(a.Name(), Equals, "http-basic-auth") - c.Assert(a.String(), Equals, "http-basic-auth - foo:*******") -} - -func (s *SuiteCommon) TestNewErrOK(c *C) { - res := &http.Response{StatusCode: http.StatusOK} - err := NewErr(res) - c.Assert(err, IsNil) -} - -func (s *SuiteCommon) TestNewErrUnauthorized(c *C) { - s.testNewHTTPError(c, http.StatusUnauthorized, "authorization required") -} - -func (s *SuiteCommon) TestNewErrNotFound(c *C) { - s.testNewHTTPError(c, http.StatusNotFound, "repository not found") -} - -func (s *SuiteCommon) TestNewHTTPError40x(c *C) { - s.testNewHTTPError(c, http.StatusPaymentRequired, "unexpected client error.*") -} - -func (s *SuiteCommon) testNewHTTPError(c *C, code int, msg string) { - req, _ := http.NewRequest("GET", "foo", nil) - res := &http.Response{ - StatusCode: code, - Request: req, - } - - err := NewErr(res) - c.Assert(err, NotNil) - c.Assert(err, ErrorMatches, msg) -} diff --git a/plumbing/client/http/git_upload_pack.go b/plumbing/client/http/git_upload_pack.go deleted file mode 100644 index 1ecf299..0000000 --- a/plumbing/client/http/git_upload_pack.go +++ /dev/null @@ -1,202 +0,0 @@ -package http - -import ( - "bufio" - "bytes" - "fmt" - "io" - "net/http" - "strings" - - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/client/common" - "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" -) - -// GitUploadPackService git-upload-pack service over HTTP -type GitUploadPackService struct { - client *http.Client - endpoint common.Endpoint - auth AuthMethod -} - -// NewGitUploadPackService connects to a git-upload-pack service over HTTP, the -// auth is extracted from the URL, or can be provided using the SetAuth method -func NewGitUploadPackService(endpoint common.Endpoint) common.GitUploadPackService { - return newGitUploadPackService(endpoint, http.DefaultClient) -} - -// NewGitUploadPackServiceFactory creates a http client factory with a customizable client -// See `InstallProtocol` to install and override default http client. -// Unless a properly initialized client is given, it will fall back into `http.DefaultClient`. -func NewGitUploadPackServiceFactory(client *http.Client) common.GitUploadPackServiceFactory { - return func(endpoint common.Endpoint) common.GitUploadPackService { - return newGitUploadPackService(endpoint, client) - } -} - -func newGitUploadPackService(endpoint common.Endpoint, client *http.Client) common.GitUploadPackService { - if client == nil { - client = http.DefaultClient - } - s := &GitUploadPackService{ - client: client, - endpoint: endpoint, - } - s.setBasicAuthFromEndpoint() - return s -} - -// Connect has not any effect, is here to satisfy interface -func (s *GitUploadPackService) Connect() error { - return nil -} - -func (s *GitUploadPackService) setBasicAuthFromEndpoint() { - info := s.endpoint.User - if info == nil { - return - } - - p, ok := info.Password() - if !ok { - return - } - - u := info.Username() - s.auth = NewBasicAuth(u, p) -} - -// SetAuth sets the AuthMethod -func (s *GitUploadPackService) SetAuth(auth common.AuthMethod) error { - httpAuth, ok := auth.(AuthMethod) - if !ok { - return common.ErrInvalidAuthMethod - } - - s.auth = httpAuth - return nil -} - -// Info returns the references info and capabilities from the service -func (s *GitUploadPackService) Info() (*common.GitUploadPackInfo, error) { - url := fmt.Sprintf( - "%s/info/refs?service=%s", - s.endpoint.String(), common.GitUploadPackServiceName, - ) - - res, err := s.doRequest("GET", url, nil) - if err != nil { - return nil, err - } - - defer res.Body.Close() - - i := common.NewGitUploadPackInfo() - return i, i.Decode(res.Body) -} - -// Fetch request and returns a reader to a packfile -func (s *GitUploadPackService) Fetch(r *common.GitUploadPackRequest) (io.ReadCloser, error) { - url := fmt.Sprintf( - "%s/%s", - s.endpoint.String(), common.GitUploadPackServiceName, - ) - - res, err := s.doRequest("POST", url, r.Reader()) - if err != nil { - return nil, err - } - - reader := newBufferedReadCloser(res.Body) - if _, err := reader.Peek(1); err != nil { - if err == io.ErrUnexpectedEOF { - return nil, common.ErrEmptyGitUploadPack - } - - return nil, err - } - - if err := discardResponseInfo(reader); err != nil { - return nil, err - } - - return reader, nil -} - -func discardResponseInfo(r io.Reader) error { - s := pktline.NewScanner(r) - for s.Scan() { - if bytes.Equal(s.Bytes(), []byte{'N', 'A', 'K', '\n'}) { - break - } - } - - return s.Err() -} - -func (s *GitUploadPackService) doRequest(method, url string, content *strings.Reader) (*http.Response, error) { - var body io.Reader - if content != nil { - body = content - } - - req, err := http.NewRequest(method, url, body) - if err != nil { - return nil, plumbing.NewPermanentError(err) - } - - s.applyHeadersToRequest(req, content) - s.applyAuthToRequest(req) - - res, err := s.client.Do(req) - if err != nil { - return nil, plumbing.NewUnexpectedError(err) - } - - if err := NewErr(res); err != nil { - _ = res.Body.Close() - return nil, err - } - - return res, nil -} - -func (s *GitUploadPackService) applyHeadersToRequest(req *http.Request, content *strings.Reader) { - req.Header.Add("User-Agent", "git/1.0") - req.Header.Add("Host", "github.com") - - if content == nil { - req.Header.Add("Accept", "*/*") - } else { - req.Header.Add("Accept", "application/x-git-upload-pack-result") - req.Header.Add("Content-Type", "application/x-git-upload-pack-request") - req.Header.Add("Content-Length", string(content.Len())) - } -} - -func (s *GitUploadPackService) applyAuthToRequest(req *http.Request) { - if s.auth == nil { - return - } - - s.auth.setAuth(req) -} - -// Disconnect do nothing -func (s *GitUploadPackService) Disconnect() error { - return nil -} - -type bufferedReadCloser struct { - *bufio.Reader - closer io.Closer -} - -func newBufferedReadCloser(r io.ReadCloser) *bufferedReadCloser { - return &bufferedReadCloser{bufio.NewReader(r), r} -} - -func (r *bufferedReadCloser) Close() error { - return r.closer.Close() -} diff --git a/plumbing/client/http/git_upload_pack_test.go b/plumbing/client/http/git_upload_pack_test.go deleted file mode 100644 index 8010cea..0000000 --- a/plumbing/client/http/git_upload_pack_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package http - -import ( - "crypto/tls" - "io/ioutil" - "net/http" - - . "gopkg.in/check.v1" - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/client/common" -) - -type RemoteSuite struct { - Endpoint common.Endpoint -} - -var _ = Suite(&RemoteSuite{}) - -func (s *RemoteSuite) SetUpSuite(c *C) { - var err error - s.Endpoint, err = common.NewEndpoint("https://github.com/git-fixtures/basic") - c.Assert(err, IsNil) -} - -func (s *RemoteSuite) TestNewGitUploadPackServiceAuth(c *C) { - e, err := common.NewEndpoint("https://foo:bar@github.com/git-fixtures/basic") - c.Assert(err, IsNil) - - r := NewGitUploadPackService(e) - auth := r.(*GitUploadPackService).auth - - c.Assert(auth.String(), Equals, "http-basic-auth - foo:*******") -} - -func (s *RemoteSuite) TestNewGitUploadPackServiceFactory(c *C) { - e, err := common.NewEndpoint("https://foo:bar@github.com/git-fixtures/basic") - c.Assert(err, IsNil) - - roundTripper := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} - client := &http.Client{Transport: roundTripper} - r := NewGitUploadPackServiceFactory(client)(e).(*GitUploadPackService) - - c.Assert(r.auth.String(), Equals, "http-basic-auth - foo:*******") - c.Assert(r.client.Transport, Equals, roundTripper) -} - -func (s *RemoteSuite) TestConnect(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) -} - -func (s *RemoteSuite) TestSetAuth(c *C) { - auth := &BasicAuth{} - r := NewGitUploadPackService(s.Endpoint) - r.SetAuth(auth) - c.Assert(auth, Equals, r.(*GitUploadPackService).auth) -} - -type mockAuth struct{} - -func (*mockAuth) Name() string { return "" } -func (*mockAuth) String() string { return "" } - -func (s *RemoteSuite) TestSetAuthWrongType(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.SetAuth(&mockAuth{}), Equals, common.ErrInvalidAuthMethod) -} - -func (s *RemoteSuite) TestInfoEmpty(c *C) { - endpoint, _ := common.NewEndpoint("https://github.com/git-fixture/empty") - r := NewGitUploadPackService(endpoint) - c.Assert(r.Connect(), IsNil) - - info, err := r.Info() - c.Assert(err, Equals, common.ErrAuthorizationRequired) - c.Assert(info, IsNil) -} - -func (s *RemoteSuite) TestInfoNotExists(c *C) { - endpoint, _ := common.NewEndpoint("https://github.com/git-fixture/not-exists") - r := NewGitUploadPackService(endpoint) - c.Assert(r.Connect(), IsNil) - - info, err := r.Info() - c.Assert(err, Equals, common.ErrAuthorizationRequired) - c.Assert(info, IsNil) -} - -func (s *RemoteSuite) TestDefaultBranch(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - - info, err := r.Info() - c.Assert(err, IsNil) - c.Assert(info.Capabilities.SymbolicReference("HEAD"), Equals, "refs/heads/master") -} - -func (s *RemoteSuite) TestCapabilities(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - - info, err := r.Info() - c.Assert(err, IsNil) - c.Assert(info.Capabilities.Get("agent").Values, HasLen, 1) -} - -func (s *RemoteSuite) TestFetch(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - - req := &common.GitUploadPackRequest{} - req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) - - reader, err := r.Fetch(req) - c.Assert(err, IsNil) - - b, err := ioutil.ReadAll(reader) - c.Assert(err, IsNil) - c.Assert(b, HasLen, 85374) -} - -func (s *RemoteSuite) TestFetchNoChanges(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - - req := &common.GitUploadPackRequest{} - req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) - req.Have(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) - - reader, err := r.Fetch(req) - c.Assert(err, Equals, common.ErrEmptyGitUploadPack) - c.Assert(reader, IsNil) -} - -func (s *RemoteSuite) TestFetchMulti(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - - req := &common.GitUploadPackRequest{} - req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) - req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881")) - - reader, err := r.Fetch(req) - c.Assert(err, IsNil) - - b, err := ioutil.ReadAll(reader) - c.Assert(err, IsNil) - c.Assert(b, HasLen, 85585) -} diff --git a/plumbing/client/ssh/git_upload_pack.go b/plumbing/client/ssh/git_upload_pack.go deleted file mode 100644 index db7fa93..0000000 --- a/plumbing/client/ssh/git_upload_pack.go +++ /dev/null @@ -1,311 +0,0 @@ -// Package ssh implements a ssh client for go-git. -package ssh - -import ( - "bytes" - "errors" - "fmt" - "io" - "strings" - - "gopkg.in/src-d/go-git.v4/plumbing/client/common" - "gopkg.in/src-d/go-git.v4/plumbing/format/packp/advrefs" - "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" - "gopkg.in/src-d/go-git.v4/plumbing/format/packp/ulreq" - - "golang.org/x/crypto/ssh" -) - -// New errors introduced by this package. -var ( - ErrInvalidAuthMethod = errors.New("invalid ssh auth method") - ErrAuthRequired = errors.New("cannot connect: auth required") - ErrNotConnected = errors.New("not connected") - ErrAlreadyConnected = errors.New("already connected") - ErrUploadPackAnswerFormat = errors.New("git-upload-pack bad answer format") - ErrUnsupportedVCS = errors.New("only git is supported") - ErrUnsupportedRepo = errors.New("only github.com is supported") - - nak = []byte("NAK") - eol = []byte("\n") -) - -// GitUploadPackService holds the service information. -// The zero value is safe to use. -type GitUploadPackService struct { - connected bool - endpoint common.Endpoint - client *ssh.Client - auth AuthMethod -} - -// NewGitUploadPackService initialises a GitUploadPackService, -func NewGitUploadPackService(endpoint common.Endpoint) common.GitUploadPackService { - return &GitUploadPackService{endpoint: endpoint} -} - -// Connect connects to the SSH server, unless a AuthMethod was set with SetAuth -// method, by default uses an auth method based on PublicKeysCallback, it -// connects to a SSH agent, using the address stored in the SSH_AUTH_SOCK -// environment var -func (s *GitUploadPackService) Connect() error { - if s.connected { - return ErrAlreadyConnected - } - - if err := s.setAuthFromEndpoint(); err != nil { - return err - } - - var err error - s.client, err = ssh.Dial("tcp", s.getHostWithPort(), s.auth.clientConfig()) - if err != nil { - return err - } - - s.connected = true - return nil -} - -func (s *GitUploadPackService) getHostWithPort() string { - host := s.endpoint.Host - if strings.Index(s.endpoint.Host, ":") == -1 { - host += ":22" - } - - return host -} - -func (s *GitUploadPackService) setAuthFromEndpoint() error { - var u string - if info := s.endpoint.User; info != nil { - u = info.Username() - } - - var err error - s.auth, err = NewSSHAgentAuth(u) - return err -} - -// SetAuth sets the AuthMethod -func (s *GitUploadPackService) SetAuth(auth common.AuthMethod) error { - var ok bool - s.auth, ok = auth.(AuthMethod) - if !ok { - return ErrInvalidAuthMethod - } - - return nil -} - -// Info returns the GitUploadPackInfo of the repository. The client must be -// connected with the repository (using the ConnectWithAuth() method) before -// using this method. -func (s *GitUploadPackService) Info() (*common.GitUploadPackInfo, error) { - if !s.connected { - return nil, ErrNotConnected - } - - session, err := s.client.NewSession() - if err != nil { - return nil, err - } - defer func() { - // the session can be closed by the other endpoint, - // therefore we must ignore a close error. - _ = session.Close() - }() - - out, err := session.Output(s.getCommand()) - if err != nil { - return nil, err - } - - i := common.NewGitUploadPackInfo() - return i, i.Decode(bytes.NewReader(out)) -} - -// Disconnect the SSH client. -func (s *GitUploadPackService) Disconnect() error { - if !s.connected { - return ErrNotConnected - } - s.connected = false - return s.client.Close() -} - -// Fetch returns a packfile for a given upload request. It opens a new -// SSH session on a connected GitUploadPackService, sends the given -// upload request to the server and returns a reader for the received -// packfile. Closing the returned reader will close the SSH session. -func (s *GitUploadPackService) Fetch(req *common.GitUploadPackRequest) (io.ReadCloser, error) { - if !s.connected { - return nil, ErrNotConnected - } - - session, i, o, done, err := openSSHSession(s.client, s.getCommand()) - if err != nil { - return nil, fmt.Errorf("cannot open SSH session: %s", err) - } - - if err := talkPackProtocol(i, o, req); err != nil { - return nil, err - } - - return &fetchSession{ - Reader: o, - session: session, - done: done, - }, nil -} - -func openSSHSession(c *ssh.Client, cmd string) ( - *ssh.Session, io.WriteCloser, io.Reader, <-chan error, error) { - - session, err := c.NewSession() - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("cannot open SSH session: %s", err) - } - - i, err := session.StdinPipe() - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("cannot pipe remote stdin: %s", err) - } - - o, err := session.StdoutPipe() - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("cannot pipe remote stdout: %s", err) - } - - done := make(chan error) - go func() { - done <- session.Run(cmd) - }() - - return session, i, o, done, nil -} - -// TODO support multi_ack mode -// TODO support multi_ack_detailed mode -// TODO support acks for common objects -// TODO build a proper state machine for all these processing options -func talkPackProtocol(w io.WriteCloser, r io.Reader, - req *common.GitUploadPackRequest) error { - - if err := skipAdvRef(r); err != nil { - return fmt.Errorf("skipping advertised-refs: %s", err) - } - - if err := sendUlReq(w, req); err != nil { - return fmt.Errorf("sending upload-req message: %s", err) - } - - if err := sendHaves(w, req); err != nil { - return fmt.Errorf("sending haves message: %s", err) - } - - if err := sendDone(w); err != nil { - return fmt.Errorf("sending done message: %s", err) - } - - if err := w.Close(); err != nil { - return fmt.Errorf("closing input: %s", err) - } - - if err := readNAK(r); err != nil { - return fmt.Errorf("reading NAK: %s", err) - } - - return nil -} - -func skipAdvRef(r io.Reader) error { - d := advrefs.NewDecoder(r) - ar := advrefs.New() - - return d.Decode(ar) -} - -func sendUlReq(w io.Writer, req *common.GitUploadPackRequest) error { - ur := ulreq.New() - ur.Wants = req.Wants - ur.Depth = ulreq.DepthCommits(req.Depth) - e := ulreq.NewEncoder(w) - - return e.Encode(ur) -} - -func sendHaves(w io.Writer, req *common.GitUploadPackRequest) error { - e := pktline.NewEncoder(w) - for _, have := range req.Haves { - if err := e.Encodef("have %s\n", have); err != nil { - return fmt.Errorf("sending haves for %q: %s", have, err) - } - } - - if len(req.Haves) != 0 { - if err := e.Flush(); err != nil { - return fmt.Errorf("sending flush-pkt after haves: %s", err) - } - } - - return nil -} - -func sendDone(w io.Writer) error { - e := pktline.NewEncoder(w) - - return e.Encodef("done\n") -} - -func readNAK(r io.Reader) error { - s := pktline.NewScanner(r) - if !s.Scan() { - return s.Err() - } - - b := s.Bytes() - b = bytes.TrimSuffix(b, eol) - if !bytes.Equal(b, nak) { - return fmt.Errorf("expecting NAK, found %q instead", string(b)) - } - - return nil -} - -type fetchSession struct { - io.Reader - session *ssh.Session - done <-chan error -} - -// Close closes the session and collects the output state of the remote -// SSH command. -// -// If both the remote command and the closing of the session completes -// susccessfully it returns nil. -// -// If the remote command completes unsuccessfully or is interrupted by a -// signal, it returns the corresponding *ExitError. -// -// Otherwise, if clossing the SSH session fails it returns the close -// error. Closing the session when the other has already close it is -// not cosidered an error. -func (f *fetchSession) Close() (err error) { - if err := <-f.done; err != nil { - return err - } - - if err := f.session.Close(); err != nil && err != io.EOF { - return err - } - - return nil -} - -func (s *GitUploadPackService) getCommand() string { - directory := s.endpoint.Path - directory = directory[1:] - - return fmt.Sprintf("git-upload-pack '%s'", directory) -} diff --git a/plumbing/client/ssh/git_upload_pack_test.go b/plumbing/client/ssh/git_upload_pack_test.go deleted file mode 100644 index 4d5b2b1..0000000 --- a/plumbing/client/ssh/git_upload_pack_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package ssh - -import ( - "io/ioutil" - "os" - - . "gopkg.in/check.v1" - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/client/common" -) - -type RemoteSuite struct { - Endpoint common.Endpoint -} - -var _ = Suite(&RemoteSuite{}) - -func (s *RemoteSuite) SetUpSuite(c *C) { - var err error - s.Endpoint, err = common.NewEndpoint("git@github.com:git-fixtures/basic.git") - c.Assert(err, IsNil) - - if os.Getenv("SSH_AUTH_SOCK") == "" { - c.Skip("SSH_AUTH_SOCK is not set") - } -} - -// A mock implementation of client.common.AuthMethod -// to test non ssh auth method detection. -type mockAuth struct{} - -func (*mockAuth) Name() string { return "" } -func (*mockAuth) String() string { return "" } - -func (s *RemoteSuite) TestSetAuthWrongType(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.SetAuth(&mockAuth{}), Equals, ErrInvalidAuthMethod) -} - -func (s *RemoteSuite) TestAlreadyConnected(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - defer func() { - c.Assert(r.Disconnect(), IsNil) - }() - - c.Assert(r.Connect(), Equals, ErrAlreadyConnected) -} - -func (s *RemoteSuite) TestDisconnect(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - c.Assert(r.Disconnect(), IsNil) -} - -func (s *RemoteSuite) TestDisconnectedWhenNonConnected(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Disconnect(), Equals, ErrNotConnected) -} - -func (s *RemoteSuite) TestAlreadyDisconnected(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - c.Assert(r.Disconnect(), IsNil) - c.Assert(r.Disconnect(), Equals, ErrNotConnected) -} - -func (s *RemoteSuite) TestServeralConnections(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - c.Assert(r.Disconnect(), IsNil) - - c.Assert(r.Connect(), IsNil) - c.Assert(r.Disconnect(), IsNil) - - c.Assert(r.Connect(), IsNil) - c.Assert(r.Disconnect(), IsNil) -} - -func (s *RemoteSuite) TestInfoNotConnected(c *C) { - r := NewGitUploadPackService(s.Endpoint) - _, err := r.Info() - c.Assert(err, Equals, ErrNotConnected) -} - -func (s *RemoteSuite) TestDefaultBranch(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - defer func() { c.Assert(r.Disconnect(), IsNil) }() - - info, err := r.Info() - c.Assert(err, IsNil) - c.Assert(info.Capabilities.SymbolicReference("HEAD"), Equals, "refs/heads/master") -} - -func (s *RemoteSuite) TestCapabilities(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - defer func() { c.Assert(r.Disconnect(), IsNil) }() - - info, err := r.Info() - c.Assert(err, IsNil) - c.Assert(info.Capabilities.Get("agent").Values, HasLen, 1) -} - -func (s *RemoteSuite) TestFetchNotConnected(c *C) { - r := NewGitUploadPackService(s.Endpoint) - pr := &common.GitUploadPackRequest{} - pr.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) - _, err := r.Fetch(pr) - c.Assert(err, Equals, ErrNotConnected) -} - -func (s *RemoteSuite) TestFetch(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - defer func() { c.Assert(r.Disconnect(), IsNil) }() - - req := &common.GitUploadPackRequest{} - req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) - req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881")) - reader, err := r.Fetch(req) - c.Assert(err, IsNil) - defer func() { c.Assert(reader.Close(), IsNil) }() - - b, err := ioutil.ReadAll(reader) - c.Assert(err, IsNil) - c.Check(len(b), Equals, 85585) -} - -func (s *RemoteSuite) TestFetchError(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - defer func() { c.Assert(r.Disconnect(), IsNil) }() - - req := &common.GitUploadPackRequest{} - req.Want(plumbing.NewHash("1111111111111111111111111111111111111111")) - - reader, err := r.Fetch(req) - c.Assert(err, IsNil) - - err = reader.Close() - c.Assert(err, Not(IsNil)) -} diff --git a/plumbing/transport/client/client.go b/plumbing/transport/client/client.go new file mode 100644 index 0000000..5c6da05 --- /dev/null +++ b/plumbing/transport/client/client.go @@ -0,0 +1,32 @@ +package client + +import ( + "fmt" + + "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/http" + "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh" +) + +// Protocols are the protocols supported by default. +var Protocols = map[string]transport.Client{ + "http": http.DefaultClient, + "https": http.DefaultClient, + "ssh": ssh.DefaultClient, +} + +// InstallProtocol adds or modifies an existing protocol. +func InstallProtocol(scheme string, c transport.Client) { + Protocols[scheme] = c +} + +// NewClient returns the appropriate client among of the set of known protocols: +// HTTP, SSH. See `InstallProtocol` to add or modify protocols. +func NewClient(endpoint transport.Endpoint) (transport.Client, error) { + f, ok := Protocols[endpoint.Scheme] + if !ok { + return nil, fmt.Errorf("unsupported scheme %q", endpoint.Scheme) + } + + return f, nil +} diff --git a/plumbing/transport/client/client_test.go b/plumbing/transport/client/client_test.go new file mode 100644 index 0000000..90bad57 --- /dev/null +++ b/plumbing/transport/client/client_test.go @@ -0,0 +1,73 @@ +package client + +import ( + "fmt" + "net/http" + "testing" + + "gopkg.in/src-d/go-git.v4/plumbing/transport" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type ClientSuite struct{} + +var _ = Suite(&ClientSuite{}) + +func (s *ClientSuite) TestNewClientHTTP(c *C) { + e, err := transport.NewEndpoint("http://github.com/src-d/go-git") + c.Assert(err, IsNil) + + output, err := NewClient(e) + c.Assert(err, IsNil) + c.Assert(typeAsString(output), Equals, "*http.Client") + + e, err = transport.NewEndpoint("https://github.com/src-d/go-git") + c.Assert(err, IsNil) + + output, err = NewClient(e) + c.Assert(err, IsNil) + c.Assert(typeAsString(output), Equals, "*http.Client") +} + +func (s *ClientSuite) TestNewClientSSH(c *C) { + e, err := transport.NewEndpoint("ssh://github.com/src-d/go-git") + c.Assert(err, IsNil) + + output, err := NewClient(e) + c.Assert(err, IsNil) + c.Assert(typeAsString(output), Equals, "*ssh.Client") +} + +func (s *ClientSuite) TestNewClientUnknown(c *C) { + e, err := transport.NewEndpoint("unknown://github.com/src-d/go-git") + c.Assert(err, IsNil) + + _, err = NewClient(e) + c.Assert(err, NotNil) +} + +func (s *ClientSuite) TestInstallProtocol(c *C) { + InstallProtocol("newscheme", &dummyClient{}) + c.Assert(Protocols["newscheme"], NotNil) +} + +type dummyClient struct { + *http.Client +} + +func (*dummyClient) NewFetchPackSession(transport.Endpoint) ( + transport.FetchPackSession, error) { + return nil, nil +} + +func (*dummyClient) NewSendPackSession(transport.Endpoint) ( + transport.SendPackSession, error) { + return nil, nil +} + +func typeAsString(v interface{}) string { + return fmt.Sprintf("%T", v) +} diff --git a/plumbing/transport/client/example_test.go b/plumbing/transport/client/example_test.go new file mode 100644 index 0000000..3e7a4f0 --- /dev/null +++ b/plumbing/transport/client/example_test.go @@ -0,0 +1,21 @@ +package client_test + +import ( + "crypto/tls" + "net/http" + + "gopkg.in/src-d/go-git.v4/plumbing/transport/client" + githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" +) + +func ExampleInstallProtocol() { + // Create custom net/http client that. + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + // Install it as default client for https URLs. + client.InstallProtocol("https", githttp.NewClient(httpClient)) +} diff --git a/plumbing/transport/common.go b/plumbing/transport/common.go new file mode 100644 index 0000000..cc30564 --- /dev/null +++ b/plumbing/transport/common.go @@ -0,0 +1,121 @@ +// 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 +// appropiate `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 ( + "errors" + "fmt" + "io" + "net/url" + "regexp" + + "gopkg.in/src-d/go-git.v4/plumbing" +) + +var ( + ErrRepositoryNotFound = errors.New("repository not found") + ErrAuthorizationRequired = errors.New("authorization required") + ErrEmptyUploadPackRequest = errors.New("empty git-upload-pack given") + ErrInvalidAuthMethod = errors.New("invalid auth method") +) + +const ( + UploadPackServiceName = "git-upload-pack" +) + +// Client can initiate git-fetch-pack and git-send-pack processes. +type Client interface { + // NewFetchPackSession starts a git-fetch-pack session for an endpoint. + NewFetchPackSession(Endpoint) (FetchPackSession, error) + // NewSendPackSession starts a git-send-pack session for an endpoint. + NewSendPackSession(Endpoint) (SendPackSession, error) +} + +type Session interface { + SetAuth(auth AuthMethod) error + io.Closer +} + +type AuthMethod interface { + fmt.Stringer + Name() string +} + +// FetchPackSession represents a git-fetch-pack session. +// A git-fetch-pack session has two steps: reference discovery +// (`AdvertisedReferences` function) and fetching pack (`FetchPack` function). +// In that order. +type FetchPackSession interface { + Session + // AdvertisedReferences retrieves the advertised references for a + // repository. It should be called before FetchPack, and it cannot be + // called after FetchPack. + AdvertisedReferences() (*UploadPackInfo, error) + // FetchPack takes a request and returns a reader for the packfile + // received from the server. + FetchPack(req *UploadPackRequest) (io.ReadCloser, error) +} + +// FetchPackSession represents a git-send-pack session. +// A git-send-pack session has two steps: reference discovery +// (`AdvertisedReferences` function) and sending pack (`SendPack` function). +// In that order. +type SendPackSession interface { + Session + // AdvertisedReferences retrieves the advertised references for a + // repository. It should be called before FetchPack, and it cannot be + // called after FetchPack. + AdvertisedReferences() (*UploadPackInfo, error) + // UpdateReferences sends an update references request and returns a + // writer to be used for packfile writing. + //TODO: Complete signature. + SendPack() (io.WriteCloser, error) +} + +type Endpoint url.URL + +var ( + isSchemeRegExp = regexp.MustCompile("^[^:]+://") + scpLikeUrlRegExp = regexp.MustCompile("^(?P<user>[^@]+@)?(?P<host>[^:]+):/?(?P<path>.+)$") +) + +func NewEndpoint(endpoint string) (Endpoint, error) { + endpoint = transformSCPLikeIfNeeded(endpoint) + + u, err := url.Parse(endpoint) + if err != nil { + return Endpoint{}, plumbing.NewPermanentError(err) + } + + if !u.IsAbs() { + return Endpoint{}, plumbing.NewPermanentError(fmt.Errorf( + "invalid endpoint: %s", endpoint, + )) + } + + return Endpoint(*u), nil +} + +func (e *Endpoint) String() string { + u := url.URL(*e) + return u.String() +} + +func transformSCPLikeIfNeeded(endpoint string) string { + if !isSchemeRegExp.MatchString(endpoint) && scpLikeUrlRegExp.MatchString(endpoint) { + m := scpLikeUrlRegExp.FindStringSubmatch(endpoint) + return fmt.Sprintf("ssh://%s%s/%s", m[1], m[2], m[3]) + } + + return endpoint +} diff --git a/plumbing/transport/common_test.go b/plumbing/transport/common_test.go new file mode 100644 index 0000000..9ca4459 --- /dev/null +++ b/plumbing/transport/common_test.go @@ -0,0 +1,31 @@ +package transport + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type SuiteCommon struct{} + +var _ = Suite(&SuiteCommon{}) + +func (s *SuiteCommon) TestNewEndpoint(c *C) { + e, err := NewEndpoint("ssh://git@github.com/user/repository.git") + c.Assert(err, IsNil) + c.Assert(e.String(), Equals, "ssh://git@github.com/user/repository.git") +} + +func (s *SuiteCommon) TestNewEndpointSCPLike(c *C) { + e, err := NewEndpoint("git@github.com:user/repository.git") + c.Assert(err, IsNil) + c.Assert(e.String(), Equals, "ssh://git@github.com/user/repository.git") +} + +func (s *SuiteCommon) TestNewEndpointWrongForgat(c *C) { + e, err := NewEndpoint("foo") + c.Assert(err, Not(IsNil)) + c.Assert(e.Host, Equals, "") +} diff --git a/plumbing/client/common/common.go b/plumbing/transport/fetch_pack.go index b2d52e8..3b2a39c 100644 --- a/plumbing/client/common/common.go +++ b/plumbing/transport/fetch_pack.go @@ -1,14 +1,10 @@ -// Package common contains interfaces and non-specific protocol entities -package common +package transport import ( "bytes" - "errors" "fmt" "io" "io/ioutil" - "net/url" - "regexp" "strings" "gopkg.in/src-d/go-git.v4/plumbing" @@ -19,82 +15,20 @@ import ( "gopkg.in/src-d/go-git.v4/storage/memory" ) -var ( - ErrRepositoryNotFound = errors.New("repository not found") - ErrAuthorizationRequired = errors.New("authorization required") - ErrEmptyGitUploadPack = errors.New("empty git-upload-pack given") - ErrInvalidAuthMethod = errors.New("invalid auth method") -) - -const GitUploadPackServiceName = "git-upload-pack" - -type GitUploadPackService interface { - Connect() error - SetAuth(AuthMethod) error - Info() (*GitUploadPackInfo, error) - Fetch(*GitUploadPackRequest) (io.ReadCloser, error) - Disconnect() error -} - -// GitUploadPackServiceFactory is capable of instantiating GitUploadPackService with given endpoint -type GitUploadPackServiceFactory func(Endpoint) GitUploadPackService - -type AuthMethod interface { - Name() string - String() string -} - -type Endpoint url.URL - -var ( - isSchemeRegExp = regexp.MustCompile("^[^:]+://") - scpLikeUrlRegExp = regexp.MustCompile("^(?P<user>[^@]+@)?(?P<host>[^:]+):/?(?P<path>.+)$") -) - -func NewEndpoint(endpoint string) (Endpoint, error) { - endpoint = transformSCPLikeIfNeeded(endpoint) - - u, err := url.Parse(endpoint) - if err != nil { - return Endpoint{}, plumbing.NewPermanentError(err) - } - - if !u.IsAbs() { - return Endpoint{}, plumbing.NewPermanentError(fmt.Errorf( - "invalid endpoint: %s", endpoint, - )) - } - - return Endpoint(*u), nil -} - -func transformSCPLikeIfNeeded(endpoint string) string { - if !isSchemeRegExp.MatchString(endpoint) && scpLikeUrlRegExp.MatchString(endpoint) { - m := scpLikeUrlRegExp.FindStringSubmatch(endpoint) - return fmt.Sprintf("ssh://%s%s/%s", m[1], m[2], m[3]) - } - - return endpoint -} - -func (e *Endpoint) String() string { - u := url.URL(*e) - return u.String() -} - -type GitUploadPackInfo struct { +//TODO: Replace this by advrefs.AdvRefs. +type UploadPackInfo struct { Capabilities *packp.Capabilities Refs memory.ReferenceStorage } -func NewGitUploadPackInfo() *GitUploadPackInfo { - return &GitUploadPackInfo{ +func NewUploadPackInfo() *UploadPackInfo { + return &UploadPackInfo{ Capabilities: packp.NewCapabilities(), Refs: make(memory.ReferenceStorage, 0), } } -func (i *GitUploadPackInfo) Decode(r io.Reader) error { +func (i *UploadPackInfo) Decode(r io.Reader) error { d := advrefs.NewDecoder(r) ar := advrefs.New() if err := d.Decode(ar); err != nil { @@ -113,7 +47,7 @@ func (i *GitUploadPackInfo) Decode(r io.Reader) error { return nil } -func (i *GitUploadPackInfo) addRefs(ar *advrefs.AdvRefs) error { +func (i *UploadPackInfo) addRefs(ar *advrefs.AdvRefs) error { for name, hash := range ar.References { ref := plumbing.NewReferenceFromStrings(name, hash.String()) i.Refs.SetReference(ref) @@ -122,7 +56,7 @@ func (i *GitUploadPackInfo) addRefs(ar *advrefs.AdvRefs) error { return i.addSymbolicRefs(ar) } -func (i *GitUploadPackInfo) addSymbolicRefs(ar *advrefs.AdvRefs) error { +func (i *UploadPackInfo) addSymbolicRefs(ar *advrefs.AdvRefs) error { if !hasSymrefs(ar) { return nil } @@ -146,16 +80,16 @@ func hasSymrefs(ar *advrefs.AdvRefs) bool { return ar.Capabilities.Supports("symref") } -func (i *GitUploadPackInfo) Head() *plumbing.Reference { +func (i *UploadPackInfo) Head() *plumbing.Reference { ref, _ := storer.ResolveReference(i.Refs, plumbing.HEAD) return ref } -func (i *GitUploadPackInfo) String() string { +func (i *UploadPackInfo) String() string { return string(i.Bytes()) } -func (i *GitUploadPackInfo) Bytes() []byte { +func (i *UploadPackInfo) Bytes() []byte { var buf bytes.Buffer e := pktline.NewEncoder(&buf) @@ -180,26 +114,26 @@ func (i *GitUploadPackInfo) Bytes() []byte { return buf.Bytes() } -type GitUploadPackRequest struct { +type UploadPackRequest struct { Wants []plumbing.Hash Haves []plumbing.Hash Depth int } -func (r *GitUploadPackRequest) Want(h ...plumbing.Hash) { +func (r *UploadPackRequest) Want(h ...plumbing.Hash) { r.Wants = append(r.Wants, h...) } -func (r *GitUploadPackRequest) Have(h ...plumbing.Hash) { +func (r *UploadPackRequest) Have(h ...plumbing.Hash) { r.Haves = append(r.Haves, h...) } -func (r *GitUploadPackRequest) String() string { +func (r *UploadPackRequest) String() string { b, _ := ioutil.ReadAll(r.Reader()) return string(b) } -func (r *GitUploadPackRequest) Reader() *strings.Reader { +func (r *UploadPackRequest) Reader() *strings.Reader { var buf bytes.Buffer e := pktline.NewEncoder(&buf) diff --git a/plumbing/transport/fetch_pack_test.go b/plumbing/transport/fetch_pack_test.go new file mode 100644 index 0000000..7a7f86f --- /dev/null +++ b/plumbing/transport/fetch_pack_test.go @@ -0,0 +1,96 @@ +package transport + +import ( + "bytes" + "encoding/base64" + + "gopkg.in/src-d/go-git.v4/plumbing" + + . "gopkg.in/check.v1" +) + +type UploadPackSuite struct{} + +var _ = Suite(&UploadPackSuite{}) + +const UploadPackInfoFixture = "MDAxZSMgc2VydmljZT1naXQtdXBsb2FkLXBhY2sKMDAwMDAxMGM2ZWNmMGVmMmMyZGZmYjc5NjAzM2U1YTAyMjE5YWY4NmVjNjU4NGU1IEhFQUQAbXVsdGlfYWNrIHRoaW4tcGFjayBzaWRlLWJhbmQgc2lkZS1iYW5kLTY0ayBvZnMtZGVsdGEgc2hhbGxvdyBuby1wcm9ncmVzcyBpbmNsdWRlLXRhZyBtdWx0aV9hY2tfZGV0YWlsZWQgbm8tZG9uZSBzeW1yZWY9SEVBRDpyZWZzL2hlYWRzL21hc3RlciBhZ2VudD1naXQvMjoyLjQuOH5kYnVzc2luay1maXgtZW50ZXJwcmlzZS10b2tlbnMtY29tcGlsYXRpb24tMTE2Ny1nYzcwMDZjZgowMDNmZThkM2ZmYWI1NTI4OTVjMTliOWZjZjdhYTI2NGQyNzdjZGUzMzg4MSByZWZzL2hlYWRzL2JyYW5jaAowMDNmNmVjZjBlZjJjMmRmZmI3OTYwMzNlNWEwMjIxOWFmODZlYzY1ODRlNSByZWZzL2hlYWRzL21hc3RlcgowMDNlYjhlNDcxZjU4YmNiY2E2M2IwN2JkYTIwZTQyODE5MDQwOWMyZGI0NyByZWZzL3B1bGwvMS9oZWFkCjAwMDA=" + +func (s *UploadPackSuite) TestUploadPackInfo(c *C) { + b, _ := base64.StdEncoding.DecodeString(UploadPackInfoFixture) + + i := NewUploadPackInfo() + err := i.Decode(bytes.NewBuffer(b)) + c.Assert(err, IsNil) + + name := i.Capabilities.SymbolicReference("HEAD") + c.Assert(name, Equals, "refs/heads/master") + c.Assert(i.Refs, HasLen, 4) + + ref := i.Refs[plumbing.ReferenceName(name)] + c.Assert(ref, NotNil) + c.Assert(ref.Hash().String(), Equals, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5") + + ref = i.Refs[plumbing.HEAD] + c.Assert(ref, NotNil) + c.Assert(ref.Target(), Equals, plumbing.ReferenceName(name)) +} + +const UploadPackInfoNoHEADFixture = "MDAxZSMgc2VydmljZT1naXQtdXBsb2FkLXBhY2sKMDAwMDAwYmNkN2UxZmVlMjYxMjM0YmIzYTQzYzA5NmY1NTg3NDhhNTY5ZDc5ZWZmIHJlZnMvaGVhZHMvdjQAbXVsdGlfYWNrIHRoaW4tcGFjayBzaWRlLWJhbmQgc2lkZS1iYW5kLTY0ayBvZnMtZGVsdGEgc2hhbGxvdyBuby1wcm9ncmVzcyBpbmNsdWRlLXRhZyBtdWx0aV9hY2tfZGV0YWlsZWQgbm8tZG9uZSBhZ2VudD1naXQvMS45LjEKMDAwMA==" + +func (s *UploadPackSuite) TestUploadPackInfoNoHEAD(c *C) { + b, _ := base64.StdEncoding.DecodeString(UploadPackInfoNoHEADFixture) + + i := NewUploadPackInfo() + err := i.Decode(bytes.NewBuffer(b)) + c.Assert(err, IsNil) + + name := i.Capabilities.SymbolicReference("HEAD") + c.Assert(name, Equals, "") + c.Assert(i.Refs, HasLen, 1) + + ref := i.Refs["refs/heads/v4"] + c.Assert(ref, NotNil) + c.Assert(ref.Hash().String(), Equals, "d7e1fee261234bb3a43c096f558748a569d79eff") +} + +func (s *UploadPackSuite) TestUploadPackInfoEmpty(c *C) { + b := bytes.NewBuffer(nil) + + i := NewUploadPackInfo() + err := i.Decode(b) + c.Assert(err, ErrorMatches, "permanent.*empty.*") +} + +func (s *UploadPackSuite) TestUploadPackEncode(c *C) { + info := NewUploadPackInfo() + info.Capabilities.Add("symref", "HEAD:refs/heads/master") + + ref := plumbing.ReferenceName("refs/heads/master") + hash := plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5") + info.Refs = map[plumbing.ReferenceName]*plumbing.Reference{ + plumbing.HEAD: plumbing.NewSymbolicReference(plumbing.HEAD, ref), + ref: plumbing.NewHashReference(ref, hash), + } + + c.Assert(info.Head(), NotNil) + c.Assert(info.String(), Equals, + "001e# service=git-upload-pack\n"+ + "000000506ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00symref=HEAD:refs/heads/master\n"+ + "003f6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/master\n"+ + "0000", + ) +} + +func (s *UploadPackSuite) TestUploadPackRequest(c *C) { + r := &UploadPackRequest{} + r.Want(plumbing.NewHash("d82f291cde9987322c8a0c81a325e1ba6159684c")) + r.Want(plumbing.NewHash("2b41ef280fdb67a9b250678686a0c3e03b0a9989")) + r.Have(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + + c.Assert(r.String(), Equals, + "0032want d82f291cde9987322c8a0c81a325e1ba6159684c\n"+ + "0032want 2b41ef280fdb67a9b250678686a0c3e03b0a9989\n"+ + "0032have 6ecf0ef2c2dffb796033e5a02219af86ec6584e5\n0000"+ + "0009done\n", + ) +} diff --git a/plumbing/transport/http/common.go b/plumbing/transport/http/common.go new file mode 100644 index 0000000..038c469 --- /dev/null +++ b/plumbing/transport/http/common.go @@ -0,0 +1,155 @@ +// Package http implements a HTTP client for go-git. +package http + +import ( + "fmt" + "net/http" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/transport" +) + +type Client struct { + c *http.Client +} + +var DefaultClient = NewClient(nil) + +// NewClient creates a new client with a custom net/http client. +// See `InstallProtocol` to install and override default http client. +// Unless a properly initialized client is given, it will fall back into +// `http.DefaultClient`. +func NewClient(c *http.Client) transport.Client { + if c == nil { + return &Client{http.DefaultClient} + } + + return &Client{ + c: c, + } +} + +func (c *Client) NewFetchPackSession(ep transport.Endpoint) ( + transport.FetchPackSession, error) { + + return newFetchPackSession(c.c, ep), nil +} + +func (c *Client) NewSendPackSession(ep transport.Endpoint) ( + transport.SendPackSession, error) { + + return newSendPackSession(c.c, ep), nil +} + +type session struct { + auth AuthMethod + client *http.Client + endpoint transport.Endpoint +} + +func (s *session) SetAuth(auth transport.AuthMethod) error { + a, ok := auth.(AuthMethod) + if !ok { + return transport.ErrInvalidAuthMethod + } + + s.auth = a + return nil +} + +func (*session) Close() error { + return nil +} + +func (s *session) applyAuthToRequest(req *http.Request) { + if s.auth == nil { + return + } + + s.auth.setAuth(req) +} + +// 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 { + info := ep.User + if info == nil { + return nil + } + + p, ok := info.Password() + if !ok { + return nil + } + + u := info.Username() + return NewBasicAuth(u, p) +} + +// BasicAuth represent a HTTP basic auth +type BasicAuth struct { + username, password string +} + +// NewBasicAuth returns a basicAuth base on the given user and password +func NewBasicAuth(username, password string) *BasicAuth { + return &BasicAuth{username, password} +} + +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) +} + +// Err is a dedicated error to return errors based on status code +type Err struct { + Response *http.Response +} + +// NewErr returns a new Err based on a http response +func NewErr(r *http.Response) error { + if r.StatusCode >= http.StatusOK && r.StatusCode < http.StatusMultipleChoices { + return nil + } + + switch r.StatusCode { + case http.StatusUnauthorized: + return transport.ErrAuthorizationRequired + case http.StatusNotFound: + return transport.ErrRepositoryNotFound + } + + return plumbing.NewUnexpectedError(&Err{r}) +} + +// 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, + ) +} diff --git a/plumbing/transport/http/common_test.go b/plumbing/transport/http/common_test.go new file mode 100644 index 0000000..1d09fba --- /dev/null +++ b/plumbing/transport/http/common_test.go @@ -0,0 +1,89 @@ +package http + +import ( + "crypto/tls" + "net/http" + "testing" + + "gopkg.in/src-d/go-git.v4/plumbing/transport" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type ClientSuite struct { + Endpoint transport.Endpoint +} + +var _ = Suite(&ClientSuite{}) + +func (s *ClientSuite) SetUpSuite(c *C) { + var err error + s.Endpoint, err = transport.NewEndpoint("https://github.com/git-fixtures/basic") + c.Assert(err, IsNil) +} + +func (s *FetchPackSuite) TestNewClient(c *C) { + roundTripper := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + client := &http.Client{Transport: roundTripper} + r := NewClient(client).(*Client) + + c.Assert(r.c, Equals, client) +} + +func (s *ClientSuite) TestNewBasicAuth(c *C) { + a := NewBasicAuth("foo", "qux") + + c.Assert(a.Name(), Equals, "http-basic-auth") + c.Assert(a.String(), Equals, "http-basic-auth - foo:*******") +} + +func (s *ClientSuite) TestNewErrOK(c *C) { + res := &http.Response{StatusCode: http.StatusOK} + err := NewErr(res) + c.Assert(err, IsNil) +} + +func (s *ClientSuite) TestNewErrUnauthorized(c *C) { + s.testNewHTTPError(c, http.StatusUnauthorized, "authorization required") +} + +func (s *ClientSuite) TestNewErrNotFound(c *C) { + s.testNewHTTPError(c, http.StatusNotFound, "repository not found") +} + +func (s *ClientSuite) TestNewHTTPError40x(c *C) { + s.testNewHTTPError(c, http.StatusPaymentRequired, "unexpected client error.*") +} + +func (s *ClientSuite) testNewHTTPError(c *C, code int, msg string) { + req, _ := http.NewRequest("GET", "foo", nil) + res := &http.Response{ + StatusCode: code, + Request: req, + } + + err := NewErr(res) + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, msg) +} + +func (s *ClientSuite) TestSetAuth(c *C) { + auth := &BasicAuth{} + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + r.SetAuth(auth) + c.Assert(auth, Equals, r.(*fetchPackSession).auth) +} + +type mockAuth struct{} + +func (*mockAuth) Name() string { return "" } +func (*mockAuth) String() string { return "" } + +func (s *ClientSuite) TestSetAuthWrongType(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + c.Assert(r.SetAuth(&mockAuth{}), Equals, transport.ErrInvalidAuthMethod) +} diff --git a/plumbing/transport/http/fetch_pack.go b/plumbing/transport/http/fetch_pack.go new file mode 100644 index 0000000..0c32672 --- /dev/null +++ b/plumbing/transport/http/fetch_pack.go @@ -0,0 +1,155 @@ +package http + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net/http" + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" + "gopkg.in/src-d/go-git.v4/plumbing/transport" +) + +type fetchPackSession struct { + *session +} + +func newFetchPackSession(c *http.Client, + ep transport.Endpoint) transport.FetchPackSession { + + return &fetchPackSession{ + session: &session{ + auth: basicAuthFromEndpoint(ep), + client: c, + endpoint: ep, + }, + } +} + +func (s *fetchPackSession) AdvertisedReferences() (*transport.UploadPackInfo, + error) { + + url := fmt.Sprintf( + "%s/info/refs?service=%s", + s.endpoint.String(), transport.UploadPackServiceName, + ) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + s.applyAuthToRequest(req) + s.applyHeadersToRequest(req, nil) + res, err := s.client.Do(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + if res.StatusCode == http.StatusUnauthorized { + return nil, transport.ErrAuthorizationRequired + } + + i := transport.NewUploadPackInfo() + return i, i.Decode(res.Body) +} + +func (s *fetchPackSession) FetchPack(r *transport.UploadPackRequest) (io.ReadCloser, error) { + url := fmt.Sprintf( + "%s/%s", + s.endpoint.String(), transport.UploadPackServiceName, + ) + + res, err := s.doRequest("POST", url, r.Reader()) + if err != nil { + return nil, err + } + + reader := newBufferedReadCloser(res.Body) + if _, err := reader.Peek(1); err != nil { + if err == io.ErrUnexpectedEOF { + return nil, transport.ErrEmptyUploadPackRequest + } + + return nil, err + } + + if err := discardResponseInfo(reader); err != nil { + return nil, err + } + + return reader, nil +} + +// Close does nothing. +func (s *fetchPackSession) Close() error { + return nil +} + +func discardResponseInfo(r io.Reader) error { + s := pktline.NewScanner(r) + for s.Scan() { + if bytes.Equal(s.Bytes(), []byte{'N', 'A', 'K', '\n'}) { + break + } + } + + return s.Err() +} + +func (s *fetchPackSession) doRequest(method, url string, content *strings.Reader) (*http.Response, error) { + var body io.Reader + if content != nil { + body = content + } + + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, plumbing.NewPermanentError(err) + } + + s.applyHeadersToRequest(req, content) + s.applyAuthToRequest(req) + + res, err := s.client.Do(req) + if err != nil { + return nil, plumbing.NewUnexpectedError(err) + } + + if err := NewErr(res); err != nil { + _ = res.Body.Close() + return nil, err + } + + return res, nil +} + +func (s *fetchPackSession) applyHeadersToRequest(req *http.Request, content *strings.Reader) { + req.Header.Add("User-Agent", "git/1.0") + req.Header.Add("Host", "github.com") + + if content == nil { + req.Header.Add("Accept", "*/*") + } else { + req.Header.Add("Accept", "application/x-git-upload-pack-result") + req.Header.Add("Content-Type", "application/x-git-upload-pack-request") + req.Header.Add("Content-Length", string(content.Len())) + } +} + +type bufferedReadCloser struct { + *bufio.Reader + closer io.Closer +} + +func newBufferedReadCloser(r io.ReadCloser) *bufferedReadCloser { + return &bufferedReadCloser{bufio.NewReader(r), r} +} + +func (r *bufferedReadCloser) Close() error { + return r.closer.Close() +} diff --git a/plumbing/transport/http/fetch_pack_test.go b/plumbing/transport/http/fetch_pack_test.go new file mode 100644 index 0000000..5ec9991 --- /dev/null +++ b/plumbing/transport/http/fetch_pack_test.go @@ -0,0 +1,122 @@ +package http + +import ( + "fmt" + "io/ioutil" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + + . "gopkg.in/check.v1" +) + +type FetchPackSuite struct { + Endpoint transport.Endpoint +} + +var _ = Suite(&FetchPackSuite{}) + +func (s *FetchPackSuite) SetUpSuite(c *C) { + fmt.Println("SetUpSuite\n") + var err error + s.Endpoint, err = transport.NewEndpoint("https://github.com/git-fixtures/basic") + c.Assert(err, IsNil) +} + +func (s *FetchPackSuite) TestInfoEmpty(c *C) { + endpoint, _ := transport.NewEndpoint("https://github.com/git-fixture/empty") + r, err := DefaultClient.NewFetchPackSession(endpoint) + c.Assert(err, IsNil) + info, err := r.AdvertisedReferences() + c.Assert(err, Equals, transport.ErrAuthorizationRequired) + c.Assert(info, IsNil) +} + +//TODO: Test this error with HTTP BasicAuth too. +func (s *FetchPackSuite) TestInfoNotExists(c *C) { + endpoint, _ := transport.NewEndpoint("https://github.com/git-fixture/not-exists") + r, err := DefaultClient.NewFetchPackSession(endpoint) + c.Assert(err, IsNil) + info, err := r.AdvertisedReferences() + c.Assert(err, Equals, transport.ErrAuthorizationRequired) + c.Assert(info, IsNil) +} + +func (s *FetchPackSuite) TestDefaultBranch(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + info, err := r.AdvertisedReferences() + c.Assert(err, IsNil) + c.Assert(info.Capabilities.SymbolicReference("HEAD"), Equals, "refs/heads/master") +} + +func (s *FetchPackSuite) TestCapabilities(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + info, err := r.AdvertisedReferences() + c.Assert(err, IsNil) + c.Assert(info.Capabilities.Get("agent").Values, HasLen, 1) +} + +func (s *FetchPackSuite) TestFullFetchPack(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + + info, err := r.AdvertisedReferences() + c.Assert(err, IsNil) + c.Assert(info, NotNil) + + req := &transport.UploadPackRequest{} + req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + + reader, err := r.FetchPack(req) + c.Assert(err, IsNil) + + b, err := ioutil.ReadAll(reader) + c.Assert(err, IsNil) + c.Assert(b, HasLen, 85374) +} + +func (s *FetchPackSuite) TestFetchPack(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + + req := &transport.UploadPackRequest{} + req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + + reader, err := r.FetchPack(req) + c.Assert(err, IsNil) + + b, err := ioutil.ReadAll(reader) + c.Assert(err, IsNil) + c.Assert(b, HasLen, 85374) +} + +func (s *FetchPackSuite) TestFetchPackNoChanges(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + + req := &transport.UploadPackRequest{} + req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + req.Have(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + + reader, err := r.FetchPack(req) + c.Assert(err, Equals, transport.ErrEmptyUploadPackRequest) + c.Assert(reader, IsNil) +} + +func (s *FetchPackSuite) TestFetchPackMulti(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + + req := &transport.UploadPackRequest{} + req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881")) + + reader, err := r.FetchPack(req) + c.Assert(err, IsNil) + + b, err := ioutil.ReadAll(reader) + c.Assert(err, IsNil) + c.Assert(b, HasLen, 85585) +} diff --git a/plumbing/transport/http/send_pack.go b/plumbing/transport/http/send_pack.go new file mode 100644 index 0000000..39be95c --- /dev/null +++ b/plumbing/transport/http/send_pack.go @@ -0,0 +1,29 @@ +package http + +import ( + "errors" + "io" + "net/http" + + "gopkg.in/src-d/go-git.v4/plumbing/transport" +) + +var errSendPackNotSupported = errors.New("send-pack not supported yet") + +type sendPackSession struct{ + *session +} + +func newSendPackSession(c *http.Client, ep transport.Endpoint) transport.SendPackSession { + return &sendPackSession{&session{}} +} + +func (s *sendPackSession) AdvertisedReferences() (*transport.UploadPackInfo, + error) { + + return nil, errSendPackNotSupported +} + +func (s *sendPackSession) SendPack() (io.WriteCloser, error) { + return nil, errSendPackNotSupported +} diff --git a/plumbing/client/ssh/auth_method.go b/plumbing/transport/ssh/auth_method.go index 587f59a..9c3d6f3 100644 --- a/plumbing/client/ssh/auth_method.go +++ b/plumbing/transport/ssh/auth_method.go @@ -7,14 +7,12 @@ import ( "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" - "gopkg.in/src-d/go-git.v4/plumbing/client/common" ) // AuthMethod is the interface all auth methods for the ssh client // must implement. The clientConfig method returns the ssh client // configuration needed to establish an ssh connection. type AuthMethod interface { - common.AuthMethod clientConfig() *ssh.ClientConfig } diff --git a/plumbing/client/ssh/auth_method_test.go b/plumbing/transport/ssh/auth_method_test.go index a87c950..f9e7dec 100644 --- a/plumbing/client/ssh/auth_method_test.go +++ b/plumbing/transport/ssh/auth_method_test.go @@ -2,13 +2,10 @@ package ssh import ( "fmt" - "testing" . "gopkg.in/check.v1" ) -func Test(t *testing.T) { TestingT(t) } - type SuiteCommon struct{} var _ = Suite(&SuiteCommon{}) diff --git a/plumbing/transport/ssh/common.go b/plumbing/transport/ssh/common.go new file mode 100644 index 0000000..6f0f3d4 --- /dev/null +++ b/plumbing/transport/ssh/common.go @@ -0,0 +1,151 @@ +package ssh + +import ( + "errors" + "fmt" + "io" + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing/transport" + + "golang.org/x/crypto/ssh" +) + +// New errors introduced by this package. +var ( + ErrAdvertistedReferencesAlreadyCalled = errors.New("cannot call AdvertisedReference twice") + ErrAlreadyConnected = errors.New("ssh session already created") + ErrAuthRequired = errors.New("cannot connect: auth required") + ErrNotConnected = errors.New("not connected") + ErrUploadPackAnswerFormat = errors.New("git-upload-pack bad answer format") + ErrUnsupportedVCS = errors.New("only git is supported") + ErrUnsupportedRepo = errors.New("only github.com is supported") +) + +type Client struct{} + +var DefaultClient = NewClient() + +func NewClient() transport.Client { + return &Client{} +} + +func (c *Client) NewFetchPackSession(ep transport.Endpoint) ( + transport.FetchPackSession, error) { + + return newFetchPackSession(ep) +} + +func (c *Client) NewSendPackSession(ep transport.Endpoint) ( + transport.SendPackSession, error) { + + return newSendPackSession(ep) +} + +type session struct { + connected bool + endpoint transport.Endpoint + client *ssh.Client + session *ssh.Session + stdin io.WriteCloser + stdout io.Reader + sessionDone chan error + auth AuthMethod +} + +func (s *session) SetAuth(auth transport.AuthMethod) error { + a, ok := auth.(AuthMethod) + if !ok { + return transport.ErrInvalidAuthMethod + } + + s.auth = a + return nil +} + +// Close closes the SSH session. +func (s *session) Close() error { + if !s.connected { + return nil + } + + s.connected = false + return s.client.Close() +} + +// ensureConnected connects to the SSH server, unless a AuthMethod was set with +// SetAuth method, by default uses an auth method based on PublicKeysCallback, +// it connects to a SSH agent, using the address stored in the SSH_AUTH_SOCK +// environment var. +func (s *session) connect() error { + if s.connected { + return ErrAlreadyConnected + } + + if err := s.setAuthFromEndpoint(); err != nil { + return err + } + + var err error + s.client, err = ssh.Dial("tcp", s.getHostWithPort(), s.auth.clientConfig()) + if err != nil { + return err + } + + if err := s.openSSHSession(); err != nil { + _ = s.client.Close() + return err + } + + s.connected = true + return nil +} + +func (s *session) getHostWithPort() string { + host := s.endpoint.Host + if strings.Index(s.endpoint.Host, ":") == -1 { + host += ":22" + } + + return host +} + +func (s *session) setAuthFromEndpoint() error { + var u string + if info := s.endpoint.User; info != nil { + u = info.Username() + } + + var err error + s.auth, err = NewSSHAgentAuth(u) + return err +} + +func (s *session) openSSHSession() error { + var err error + s.session, err = s.client.NewSession() + if err != nil { + return fmt.Errorf("cannot open SSH session: %s", err) + } + + s.stdin, err = s.session.StdinPipe() + if err != nil { + return fmt.Errorf("cannot pipe remote stdin: %s", err) + } + + s.stdout, err = s.session.StdoutPipe() + if err != nil { + return fmt.Errorf("cannot pipe remote stdout: %s", err) + } + + return nil +} + +func (s *session) runCommand(cmd string) chan error { + done := make(chan error) + go func() { + done <- s.session.Run(cmd) + }() + + return done +} diff --git a/plumbing/transport/ssh/common_test.go b/plumbing/transport/ssh/common_test.go new file mode 100644 index 0000000..ac4d03e --- /dev/null +++ b/plumbing/transport/ssh/common_test.go @@ -0,0 +1,17 @@ +package ssh + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type ClientSuite struct{} + +var _ = Suite(&ClientSuite{}) + +func (s *ClientSuite) TestNewClient(c *C) { + c.Assert(DefaultClient, DeepEquals, NewClient()) +} diff --git a/plumbing/transport/ssh/fetch_pack.go b/plumbing/transport/ssh/fetch_pack.go new file mode 100644 index 0000000..bda4edf --- /dev/null +++ b/plumbing/transport/ssh/fetch_pack.go @@ -0,0 +1,202 @@ +// Package ssh implements a ssh client for go-git. +package ssh + +import ( + "bytes" + "fmt" + "io" + + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/ulreq" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + + "golang.org/x/crypto/ssh" +) + +type fetchPackSession struct { + *session + cmdRun bool + advRefsRun bool + done chan error +} + +func newFetchPackSession(ep transport.Endpoint) (*fetchPackSession, error) { + s := &fetchPackSession{ + session: &session{ + endpoint: ep, + }, + } + if err := s.connect(); err != nil { + return nil, err + } + + return s, nil +} + +func (s *fetchPackSession) AdvertisedReferences() (*transport.UploadPackInfo, error) { + if s.advRefsRun { + return nil, ErrAdvertistedReferencesAlreadyCalled + } + + if err := s.ensureRunCommand(); err != nil { + return nil, err + } + + defer func() { s.advRefsRun = true }() + + i := transport.NewUploadPackInfo() + return i, i.Decode(s.stdout) +} + +// FetchPack returns a packfile for a given upload request. +// Closing the returned reader will close the SSH session. +func (s *fetchPackSession) FetchPack(req *transport.UploadPackRequest) ( + io.ReadCloser, error) { + + if !s.advRefsRun { + if _, err := s.AdvertisedReferences(); err != nil { + return nil, err + } + } + + if err := fetchPack(s.stdin, s.stdout, req); err != nil { + return nil, err + } + + return &fetchSession{ + Reader: s.stdout, + session: s.session.session, + done: s.done, + }, nil +} + +func (s *fetchPackSession) ensureRunCommand() error { + if s.cmdRun { + return nil + } + + s.cmdRun = true + s.done = s.runCommand(s.getCommand()) + return nil +} + +type fetchSession struct { + io.Reader + session *ssh.Session + done <-chan error +} + +// Close closes the session and collects the output state of the remote +// SSH command. +// +// If both the remote command and the closing of the session completes +// susccessfully it returns nil. +// +// If the remote command completes unsuccessfully or is interrupted by a +// signal, it returns the corresponding *ExitError. +// +// Otherwise, if clossing the SSH session fails it returns the close +// error. Closing the session when the other has already close it is +// not cosidered an error. +func (f *fetchSession) Close() (err error) { + if err := <-f.done; err != nil { + return err + } + + if err := f.session.Close(); err != nil && err != io.EOF { + return err + } + + return nil +} + +func (s *fetchPackSession) getCommand() string { + directory := s.endpoint.Path + directory = directory[1:] + + return fmt.Sprintf("git-upload-pack '%s'", directory) +} + +var ( + nak = []byte("NAK") + eol = []byte("\n") +) + +// FetchPack implements the git-fetch-pack protocol. +// +// TODO support multi_ack mode +// TODO support multi_ack_detailed mode +// TODO support acks for common objects +// TODO build a proper state machine for all these processing options +func fetchPack(w io.WriteCloser, r io.Reader, + req *transport.UploadPackRequest) error { + + if err := sendUlReq(w, req); err != nil { + return fmt.Errorf("sending upload-req message: %s", err) + } + + if err := sendHaves(w, req); err != nil { + return fmt.Errorf("sending haves message: %s", err) + } + + if err := sendDone(w); err != nil { + return fmt.Errorf("sending done message: %s", err) + } + + if err := w.Close(); err != nil { + return fmt.Errorf("closing input: %s", err) + } + + if err := readNAK(r); err != nil { + return fmt.Errorf("reading NAK: %s", err) + } + + return nil +} + +func sendUlReq(w io.Writer, req *transport.UploadPackRequest) error { + ur := ulreq.New() + ur.Wants = req.Wants + ur.Depth = ulreq.DepthCommits(req.Depth) + e := ulreq.NewEncoder(w) + + return e.Encode(ur) +} + +func sendHaves(w io.Writer, req *transport.UploadPackRequest) error { + e := pktline.NewEncoder(w) + for _, have := range req.Haves { + if err := e.Encodef("have %s\n", have); err != nil { + return fmt.Errorf("sending haves for %q: %s", have, err) + } + } + + if len(req.Haves) != 0 { + if err := e.Flush(); err != nil { + return fmt.Errorf("sending flush-pkt after haves: %s", err) + } + } + + return nil +} + +func sendDone(w io.Writer) error { + e := pktline.NewEncoder(w) + + return e.Encodef("done\n") +} + +func readNAK(r io.Reader) error { + s := pktline.NewScanner(r) + if !s.Scan() { + return s.Err() + } + + b := s.Bytes() + b = bytes.TrimSuffix(b, eol) + if !bytes.Equal(b, nak) { + return fmt.Errorf("expecting NAK, found %q instead", string(b)) + } + + return nil +} diff --git a/plumbing/transport/ssh/fetch_pack_test.go b/plumbing/transport/ssh/fetch_pack_test.go new file mode 100644 index 0000000..3d62e57 --- /dev/null +++ b/plumbing/transport/ssh/fetch_pack_test.go @@ -0,0 +1,100 @@ +package ssh + +import ( + "io/ioutil" + "os" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + + . "gopkg.in/check.v1" +) + +type FetchPackSuite struct { + Endpoint transport.Endpoint +} + +var _ = Suite(&FetchPackSuite{}) + +func (s *FetchPackSuite) SetUpSuite(c *C) { + var err error + s.Endpoint, err = transport.NewEndpoint("git@github.com:git-fixtures/basic.git") + c.Assert(err, IsNil) + + if os.Getenv("SSH_AUTH_SOCK") == "" { + c.Skip("SSH_AUTH_SOCK is not set") + } +} + +func (s *FetchPackSuite) TestDefaultBranch(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + defer func() { c.Assert(r.Close(), IsNil) }() + + info, err := r.AdvertisedReferences() + c.Assert(err, IsNil) + c.Assert(info.Capabilities.SymbolicReference("HEAD"), Equals, "refs/heads/master") +} + +func (s *FetchPackSuite) TestCapabilities(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + defer func() { c.Assert(r.Close(), IsNil) }() + + info, err := r.AdvertisedReferences() + c.Assert(err, IsNil) + c.Assert(info.Capabilities.Get("agent").Values, HasLen, 1) +} + +func (s *FetchPackSuite) TestFullFetchPack(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + defer func() { c.Assert(r.Close(), IsNil) }() + + _, err = r.AdvertisedReferences() + c.Assert(err, IsNil) + + req := &transport.UploadPackRequest{} + req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881")) + reader, err := r.FetchPack(req) + c.Assert(err, IsNil) + + defer func() { c.Assert(reader.Close(), IsNil) }() + + b, err := ioutil.ReadAll(reader) + c.Assert(err, IsNil) + c.Check(len(b), Equals, 85585) +} + +func (s *FetchPackSuite) TestFetchPack(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + defer func() { c.Assert(r.Close(), IsNil) }() + + req := &transport.UploadPackRequest{} + req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881")) + reader, err := r.FetchPack(req) + c.Assert(err, IsNil) + defer func() { c.Assert(reader.Close(), IsNil) }() + + b, err := ioutil.ReadAll(reader) + c.Assert(err, IsNil) + c.Check(len(b), Equals, 85585) +} + +func (s *FetchPackSuite) TestFetchError(c *C) { + r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + defer func() { c.Assert(r.Close(), IsNil) }() + + req := &transport.UploadPackRequest{} + req.Want(plumbing.NewHash("1111111111111111111111111111111111111111")) + + reader, err := r.FetchPack(req) + c.Assert(err, IsNil) + + err = reader.Close() + c.Assert(err, Not(IsNil)) +} diff --git a/plumbing/transport/ssh/send_pack.go b/plumbing/transport/ssh/send_pack.go new file mode 100644 index 0000000..afe7510 --- /dev/null +++ b/plumbing/transport/ssh/send_pack.go @@ -0,0 +1,30 @@ +package ssh + +import ( + "errors" + "io" + + "gopkg.in/src-d/go-git.v4/plumbing/transport" +) + +var errSendPackNotSupported = errors.New("send-pack not supported yet") + +type sendPackSession struct { + *session +} + +func newSendPackSession(ep transport.Endpoint) (transport.SendPackSession, + error) { + + return &sendPackSession{&session{}}, nil +} + +func (s *sendPackSession) AdvertisedReferences() (*transport.UploadPackInfo, + error) { + + return nil, errSendPackNotSupported +} + +func (s *sendPackSession) SendPack() (io.WriteCloser, error) { + return nil, errSendPackNotSupported +} |