diff options
Diffstat (limited to 'plumbing/client/http')
-rw-r--r-- | plumbing/client/http/common.go | 77 | ||||
-rw-r--r-- | plumbing/client/http/common_test.go | 52 | ||||
-rw-r--r-- | plumbing/client/http/git_upload_pack.go | 186 | ||||
-rw-r--r-- | plumbing/client/http/git_upload_pack_test.go | 135 |
4 files changed, 450 insertions, 0 deletions
diff --git a/plumbing/client/http/common.go b/plumbing/client/http/common.go new file mode 100644 index 0000000..4c07876 --- /dev/null +++ b/plumbing/client/http/common.go @@ -0,0 +1,77 @@ +// 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" +) + +// HTTPAuthMethod concrete implementation of common.AuthMethod for HTTP services +type HTTPAuthMethod 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 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) +} + +// HTTPError a dedicated error to return errors bases on status codes +type HTTPError struct { + Response *http.Response +} + +// NewHTTPError returns a new HTTPError based on a http response +func NewHTTPError(r *http.Response) error { + if r.StatusCode >= 200 && r.StatusCode < 300 { + return nil + } + + switch r.StatusCode { + case 401: + return common.ErrAuthorizationRequired + case 404: + return common.ErrRepositoryNotFound + } + + err := &HTTPError{r} + return plumbing.NewUnexpectedError(err) +} + +// StatusCode returns the status code of the response +func (e *HTTPError) StatusCode() int { + return e.Response.StatusCode +} + +func (e *HTTPError) 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 new file mode 100644 index 0000000..287897d --- /dev/null +++ b/plumbing/client/http/common_test.go @@ -0,0 +1,52 @@ +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) TestNewHTTPError200(c *C) { + res := &http.Response{StatusCode: 200} + res.StatusCode = 200 + err := NewHTTPError(res) + c.Assert(err, IsNil) +} + +func (s *SuiteCommon) TestNewHTTPError401(c *C) { + s.testNewHTTPError(c, 401, "authorization required") +} + +func (s *SuiteCommon) TestNewHTTPError404(c *C) { + s.testNewHTTPError(c, 404, "repository not found") +} + +func (s *SuiteCommon) TestNewHTTPError40x(c *C) { + s.testNewHTTPError(c, 402, "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 := NewHTTPError(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 new file mode 100644 index 0000000..c1f4a0b --- /dev/null +++ b/plumbing/client/http/git_upload_pack.go @@ -0,0 +1,186 @@ +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-upoad-pack service over HTTP +type GitUploadPackService struct { + client *http.Client + endpoint common.Endpoint + auth HTTPAuthMethod +} + +// 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 { + s := &GitUploadPackService{ + client: http.DefaultClient, + endpoint: endpoint, + } + + s.setBasicAuthFromEndpoint() + return s +} + +// Connect has not any effect, is here just for meet the 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.(HTTPAuthMethod) + 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 := NewHTTPError(res); err != nil { + 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() (err 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 new file mode 100644 index 0000000..a50dbdf --- /dev/null +++ b/plumbing/client/http/git_upload_pack_test.go @@ -0,0 +1,135 @@ +package http + +import ( + "io/ioutil" + + . "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) 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) +} |