From 841abfb7dc640755c443432064252907e3e55c95 Mon Sep 17 00:00:00 2001 From: "Santiago M. Mola" Date: Wed, 4 Jan 2017 11:18:41 +0100 Subject: server: add git server implementation (#190) * server: add generic server implementation (transport-independent), both for git-upload-pack and git-receive-pack. * server: move internal functions to internal/common. * cli: add git-receive-pack and git-upload-pack implementations. * format/packfile: add UpdateObjectStorage function, extracted from Remote. * transport: implement tranport RPC-like, only with git-upload-pack and git-receive-pack methods. Client renamed to Transport. * storer: add storer.Storer interface. * protocol/packp: add UploadPackResponse constructor with packfile. * protocol/packp: fix UploadPackResponse encoding, add tests. * protocol/packp/capability: implement All. --- plumbing/transport/http/common.go | 14 +-- plumbing/transport/http/common_test.go | 8 +- plumbing/transport/http/fetch_pack.go | 174 ---------------------------- plumbing/transport/http/fetch_pack_test.go | 59 ---------- plumbing/transport/http/receive_pack.go | 30 +++++ plumbing/transport/http/send_pack.go | 30 ----- plumbing/transport/http/upload_pack.go | 174 ++++++++++++++++++++++++++++ plumbing/transport/http/upload_pack_test.go | 60 ++++++++++ 8 files changed, 275 insertions(+), 274 deletions(-) delete mode 100644 plumbing/transport/http/fetch_pack.go delete mode 100644 plumbing/transport/http/fetch_pack_test.go create mode 100644 plumbing/transport/http/receive_pack.go delete mode 100644 plumbing/transport/http/send_pack.go create mode 100644 plumbing/transport/http/upload_pack.go create mode 100644 plumbing/transport/http/upload_pack_test.go (limited to 'plumbing/transport/http') diff --git a/plumbing/transport/http/common.go b/plumbing/transport/http/common.go index aa3425e..957fd07 100644 --- a/plumbing/transport/http/common.go +++ b/plumbing/transport/http/common.go @@ -25,7 +25,7 @@ var DefaultClient = NewClient(nil) // Note that for HTTP client cannot distinguist between private repositories and // unexistent repositories on GitHub. So it returns `ErrAuthorizationRequired` // for both. -func NewClient(c *http.Client) transport.Client { +func NewClient(c *http.Client) transport.Transport { if c == nil { return &client{http.DefaultClient} } @@ -35,16 +35,16 @@ func NewClient(c *http.Client) transport.Client { } } -func (c *client) NewFetchPackSession(ep transport.Endpoint) ( - transport.FetchPackSession, error) { +func (c *client) NewUploadPackSession(ep transport.Endpoint) ( + transport.UploadPackSession, error) { - return newFetchPackSession(c.c, ep), nil + return newUploadPackSession(c.c, ep), nil } -func (c *client) NewSendPackSession(ep transport.Endpoint) ( - transport.SendPackSession, error) { +func (c *client) NewReceivePackSession(ep transport.Endpoint) ( + transport.ReceivePackSession, error) { - return newSendPackSession(c.c, ep), nil + return newReceivePackSession(c.c, ep), nil } type session struct { diff --git a/plumbing/transport/http/common_test.go b/plumbing/transport/http/common_test.go index 432bd07..217999d 100644 --- a/plumbing/transport/http/common_test.go +++ b/plumbing/transport/http/common_test.go @@ -26,7 +26,7 @@ func (s *ClientSuite) SetUpSuite(c *C) { c.Assert(err, IsNil) } -func (s *FetchPackSuite) TestNewClient(c *C) { +func (s *UploadPackSuite) TestNewClient(c *C) { roundTripper := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } @@ -76,10 +76,10 @@ func (s *ClientSuite) testNewHTTPError(c *C, code int, msg string) { func (s *ClientSuite) TestSetAuth(c *C) { auth := &BasicAuth{} - r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + r, err := DefaultClient.NewUploadPackSession(s.Endpoint) c.Assert(err, IsNil) r.SetAuth(auth) - c.Assert(auth, Equals, r.(*fetchPackSession).auth) + c.Assert(auth, Equals, r.(*upSession).auth) } type mockAuth struct{} @@ -88,7 +88,7 @@ func (*mockAuth) Name() string { return "" } func (*mockAuth) String() string { return "" } func (s *ClientSuite) TestSetAuthWrongType(c *C) { - r, err := DefaultClient.NewFetchPackSession(s.Endpoint) + r, err := DefaultClient.NewUploadPackSession(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 deleted file mode 100644 index 0c85be4..0000000 --- a/plumbing/transport/http/fetch_pack.go +++ /dev/null @@ -1,174 +0,0 @@ -package http - -import ( - "bytes" - "fmt" - "io" - "net/http" - "strconv" - - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/format/pktline" - "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" - "gopkg.in/src-d/go-git.v4/plumbing/transport" - "gopkg.in/src-d/go-git.v4/plumbing/transport/internal/common" - "gopkg.in/src-d/go-git.v4/utils/ioutil" -) - -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() (*packp.AdvRefs, error) { - if s.advRefs != nil { - return s.advRefs, nil - } - - 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 - } - - ar := packp.NewAdvRefs() - if err := ar.Decode(res.Body); err != nil { - if err == packp.ErrEmptyAdvRefs { - err = transport.ErrEmptyRemoteRepository - } - - return nil, err - } - - transport.FilterUnsupportedCapabilities(ar.Capabilities) - s.advRefs = ar - return ar, nil -} - -func (s *fetchPackSession) FetchPack(req *packp.UploadPackRequest) (*packp.UploadPackResponse, error) { - if req.IsEmpty() { - return nil, transport.ErrEmptyUploadPackRequest - } - - if err := req.Validate(); err != nil { - return nil, err - } - - url := fmt.Sprintf( - "%s/%s", - s.endpoint.String(), transport.UploadPackServiceName, - ) - - content, err := uploadPackRequestToReader(req) - if err != nil { - return nil, err - } - - res, err := s.doRequest(http.MethodPost, url, content) - if err != nil { - return nil, err - } - - r, err := ioutil.NonEmptyReader(res.Body) - if err != nil { - if err == ioutil.ErrEmptyReader || err == io.ErrUnexpectedEOF { - return nil, transport.ErrEmptyUploadPackRequest - } - - return nil, err - } - - rc := ioutil.NewReadCloser(r, res.Body) - return common.DecodeUploadPackResponse(rc, req) -} - -// Close does nothing. -func (s *fetchPackSession) Close() error { - return nil -} - -func (s *fetchPackSession) doRequest(method, url string, content *bytes.Buffer) (*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 -} - -// it requires a bytes.Buffer, because we need to know the length -func (s *fetchPackSession) applyHeadersToRequest(req *http.Request, content *bytes.Buffer) { - req.Header.Add("User-Agent", "git/1.0") - req.Header.Add("Host", s.endpoint.Host) - - if content == nil { - req.Header.Add("Accept", "*/*") - return - } - - 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", strconv.Itoa(content.Len())) -} - -func uploadPackRequestToReader(req *packp.UploadPackRequest) (*bytes.Buffer, error) { - buf := bytes.NewBuffer(nil) - e := pktline.NewEncoder(buf) - - if err := req.UploadRequest.Encode(buf); err != nil { - return nil, fmt.Errorf("sending upload-req message: %s", err) - } - - if err := req.UploadHaves.Encode(buf); err != nil { - return nil, fmt.Errorf("sending haves message: %s", err) - } - - _ = e.EncodeString("done\n") - return buf, nil -} diff --git a/plumbing/transport/http/fetch_pack_test.go b/plumbing/transport/http/fetch_pack_test.go deleted file mode 100644 index 6c40e60..0000000 --- a/plumbing/transport/http/fetch_pack_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package http - -import ( - "io/ioutil" - - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" - "gopkg.in/src-d/go-git.v4/plumbing/transport" - "gopkg.in/src-d/go-git.v4/plumbing/transport/test" - - . "gopkg.in/check.v1" -) - -type FetchPackSuite struct { - test.FetchPackSuite -} - -var _ = Suite(&FetchPackSuite{}) - -func (s *FetchPackSuite) SetUpSuite(c *C) { - s.FetchPackSuite.Client = DefaultClient - - ep, err := transport.NewEndpoint("https://github.com/git-fixtures/basic.git") - c.Assert(err, IsNil) - s.FetchPackSuite.Endpoint = ep - - ep, err = transport.NewEndpoint("https://github.com/git-fixtures/empty.git") - c.Assert(err, IsNil) - s.FetchPackSuite.EmptyEndpoint = ep - - ep, err = transport.NewEndpoint("https://github.com/git-fixtures/non-existent.git") - c.Assert(err, IsNil) - s.FetchPackSuite.NonExistentEndpoint = ep -} - -func (s *FetchPackSuite) TestInfoNotExists(c *C) { - r, err := s.Client.NewFetchPackSession(s.NonExistentEndpoint) - c.Assert(err, IsNil) - info, err := r.AdvertisedReferences() - c.Assert(err, Equals, transport.ErrAuthorizationRequired) - c.Assert(info, IsNil) -} - -func (s *FetchPackSuite) TestuploadPackRequestToReader(c *C) { - r := packp.NewUploadPackRequest() - r.Wants = append(r.Wants, plumbing.NewHash("d82f291cde9987322c8a0c81a325e1ba6159684c")) - r.Wants = append(r.Wants, plumbing.NewHash("2b41ef280fdb67a9b250678686a0c3e03b0a9989")) - r.Haves = append(r.Haves, plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) - - sr, err := uploadPackRequestToReader(r) - c.Assert(err, IsNil) - b, _ := ioutil.ReadAll(sr) - c.Assert(string(b), Equals, - "0032want 2b41ef280fdb67a9b250678686a0c3e03b0a9989\n"+ - "0032want d82f291cde9987322c8a0c81a325e1ba6159684c\n0000"+ - "0032have 6ecf0ef2c2dffb796033e5a02219af86ec6584e5\n0000"+ - "0009done\n", - ) -} diff --git a/plumbing/transport/http/receive_pack.go b/plumbing/transport/http/receive_pack.go new file mode 100644 index 0000000..a8384c7 --- /dev/null +++ b/plumbing/transport/http/receive_pack.go @@ -0,0 +1,30 @@ +package http + +import ( + "errors" + "net/http" + + "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" + "gopkg.in/src-d/go-git.v4/plumbing/transport" +) + +var errReceivePackNotSupported = errors.New("receive-pack not supported yet") + +type rpSession struct { + *session +} + +func newReceivePackSession(c *http.Client, ep transport.Endpoint) transport.ReceivePackSession { + return &rpSession{&session{}} +} + +func (s *rpSession) AdvertisedReferences() (*packp.AdvRefs, error) { + + return nil, errReceivePackNotSupported +} + +func (s *rpSession) ReceivePack(*packp.ReferenceUpdateRequest) ( + *packp.ReportStatus, error) { + + return nil, errReceivePackNotSupported +} diff --git a/plumbing/transport/http/send_pack.go b/plumbing/transport/http/send_pack.go deleted file mode 100644 index 43464ae..0000000 --- a/plumbing/transport/http/send_pack.go +++ /dev/null @@ -1,30 +0,0 @@ -package http - -import ( - "errors" - "net/http" - - "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" - "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() (*packp.AdvRefs, error) { - - return nil, errSendPackNotSupported -} - -func (s *sendPackSession) SendPack(*packp.ReferenceUpdateRequest) ( - *packp.ReportStatus, error) { - - return nil, errSendPackNotSupported -} diff --git a/plumbing/transport/http/upload_pack.go b/plumbing/transport/http/upload_pack.go new file mode 100644 index 0000000..26257f5 --- /dev/null +++ b/plumbing/transport/http/upload_pack.go @@ -0,0 +1,174 @@ +package http + +import ( + "bytes" + "fmt" + "io" + "net/http" + "strconv" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/pktline" + "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/internal/common" + "gopkg.in/src-d/go-git.v4/utils/ioutil" +) + +type upSession struct { + *session +} + +func newUploadPackSession(c *http.Client, + ep transport.Endpoint) transport.UploadPackSession { + + return &upSession{ + session: &session{ + auth: basicAuthFromEndpoint(ep), + client: c, + endpoint: ep, + }, + } +} + +func (s *upSession) AdvertisedReferences() (*packp.AdvRefs, error) { + if s.advRefs != nil { + return s.advRefs, nil + } + + 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 + } + + ar := packp.NewAdvRefs() + if err := ar.Decode(res.Body); err != nil { + if err == packp.ErrEmptyAdvRefs { + err = transport.ErrEmptyRemoteRepository + } + + return nil, err + } + + transport.FilterUnsupportedCapabilities(ar.Capabilities) + s.advRefs = ar + return ar, nil +} + +func (s *upSession) UploadPack(req *packp.UploadPackRequest) (*packp.UploadPackResponse, error) { + if req.IsEmpty() { + return nil, transport.ErrEmptyUploadPackRequest + } + + if err := req.Validate(); err != nil { + return nil, err + } + + url := fmt.Sprintf( + "%s/%s", + s.endpoint.String(), transport.UploadPackServiceName, + ) + + content, err := uploadPackRequestToReader(req) + if err != nil { + return nil, err + } + + res, err := s.doRequest(http.MethodPost, url, content) + if err != nil { + return nil, err + } + + r, err := ioutil.NonEmptyReader(res.Body) + if err != nil { + if err == ioutil.ErrEmptyReader || err == io.ErrUnexpectedEOF { + return nil, transport.ErrEmptyUploadPackRequest + } + + return nil, err + } + + rc := ioutil.NewReadCloser(r, res.Body) + return common.DecodeUploadPackResponse(rc, req) +} + +// Close does nothing. +func (s *upSession) Close() error { + return nil +} + +func (s *upSession) doRequest(method, url string, content *bytes.Buffer) (*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 +} + +// it requires a bytes.Buffer, because we need to know the length +func (s *upSession) applyHeadersToRequest(req *http.Request, content *bytes.Buffer) { + req.Header.Add("User-Agent", "git/1.0") + req.Header.Add("Host", s.endpoint.Host) + + if content == nil { + req.Header.Add("Accept", "*/*") + return + } + + 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", strconv.Itoa(content.Len())) +} + +func uploadPackRequestToReader(req *packp.UploadPackRequest) (*bytes.Buffer, error) { + buf := bytes.NewBuffer(nil) + e := pktline.NewEncoder(buf) + + if err := req.UploadRequest.Encode(buf); err != nil { + return nil, fmt.Errorf("sending upload-req message: %s", err) + } + + if err := req.UploadHaves.Encode(buf); err != nil { + return nil, fmt.Errorf("sending haves message: %s", err) + } + + _ = e.EncodeString("done\n") + return buf, nil +} diff --git a/plumbing/transport/http/upload_pack_test.go b/plumbing/transport/http/upload_pack_test.go new file mode 100644 index 0000000..d3e4989 --- /dev/null +++ b/plumbing/transport/http/upload_pack_test.go @@ -0,0 +1,60 @@ +package http + +import ( + "io/ioutil" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/test" + + . "gopkg.in/check.v1" +) + +type UploadPackSuite struct { + test.UploadPackSuite +} + +var _ = Suite(&UploadPackSuite{}) + +func (s *UploadPackSuite) SetUpSuite(c *C) { + s.UploadPackSuite.Client = DefaultClient + + ep, err := transport.NewEndpoint("https://github.com/git-fixtures/basic.git") + c.Assert(err, IsNil) + s.UploadPackSuite.Endpoint = ep + + ep, err = transport.NewEndpoint("https://github.com/git-fixtures/empty.git") + c.Assert(err, IsNil) + s.UploadPackSuite.EmptyEndpoint = ep + + ep, err = transport.NewEndpoint("https://github.com/git-fixtures/non-existent.git") + c.Assert(err, IsNil) + s.UploadPackSuite.NonExistentEndpoint = ep +} + +// Overwritten, different behaviour for HTTP. +func (s *UploadPackSuite) TestAdvertisedReferencesNotExists(c *C) { + r, err := s.Client.NewUploadPackSession(s.NonExistentEndpoint) + c.Assert(err, IsNil) + info, err := r.AdvertisedReferences() + c.Assert(err, Equals, transport.ErrAuthorizationRequired) + c.Assert(info, IsNil) +} + +func (s *UploadPackSuite) TestuploadPackRequestToReader(c *C) { + r := packp.NewUploadPackRequest() + r.Wants = append(r.Wants, plumbing.NewHash("d82f291cde9987322c8a0c81a325e1ba6159684c")) + r.Wants = append(r.Wants, plumbing.NewHash("2b41ef280fdb67a9b250678686a0c3e03b0a9989")) + r.Haves = append(r.Haves, plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + + sr, err := uploadPackRequestToReader(r) + c.Assert(err, IsNil) + b, _ := ioutil.ReadAll(sr) + c.Assert(string(b), Equals, + "0032want 2b41ef280fdb67a9b250678686a0c3e03b0a9989\n"+ + "0032want d82f291cde9987322c8a0c81a325e1ba6159684c\n0000"+ + "0032have 6ecf0ef2c2dffb796033e5a02219af86ec6584e5\n0000"+ + "0009done\n", + ) +} -- cgit