aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing/transport/http
diff options
context:
space:
mode:
authorSantiago M. Mola <santi@mola.io>2016-11-23 15:30:34 +0100
committerMáximo Cuadros <mcuadros@gmail.com>2016-11-23 15:38:12 +0100
commit08e08d771ef03df80248c80d81475fe7c5ea6fe7 (patch)
treed12e9befa22409e8cf50c5bbc4895e69fd8a5f48 /plumbing/transport/http
parent844169a739fb8bf1f252d416f10d8c7034db9fe2 (diff)
downloadgo-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/transport/http')
-rw-r--r--plumbing/transport/http/common.go155
-rw-r--r--plumbing/transport/http/common_test.go89
-rw-r--r--plumbing/transport/http/fetch_pack.go155
-rw-r--r--plumbing/transport/http/fetch_pack_test.go122
-rw-r--r--plumbing/transport/http/send_pack.go29
5 files changed, 550 insertions, 0 deletions
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
+}