aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing/client/http
diff options
context:
space:
mode:
Diffstat (limited to 'plumbing/client/http')
-rw-r--r--plumbing/client/http/common.go77
-rw-r--r--plumbing/client/http/common_test.go52
-rw-r--r--plumbing/client/http/git_upload_pack.go186
-rw-r--r--plumbing/client/http/git_upload_pack_test.go135
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)
+}