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/transport/http | |
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/transport/http')
-rw-r--r-- | plumbing/transport/http/common.go | 155 | ||||
-rw-r--r-- | plumbing/transport/http/common_test.go | 89 | ||||
-rw-r--r-- | plumbing/transport/http/fetch_pack.go | 155 | ||||
-rw-r--r-- | plumbing/transport/http/fetch_pack_test.go | 122 | ||||
-rw-r--r-- | plumbing/transport/http/send_pack.go | 29 |
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 +} |