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/client/ssh | |
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/client/ssh')
-rw-r--r-- | plumbing/client/ssh/auth_method.go | 159 | ||||
-rw-r--r-- | plumbing/client/ssh/auth_method_test.go | 94 | ||||
-rw-r--r-- | plumbing/client/ssh/git_upload_pack.go | 311 | ||||
-rw-r--r-- | plumbing/client/ssh/git_upload_pack_test.go | 144 |
4 files changed, 0 insertions, 708 deletions
diff --git a/plumbing/client/ssh/auth_method.go b/plumbing/client/ssh/auth_method.go deleted file mode 100644 index 587f59a..0000000 --- a/plumbing/client/ssh/auth_method.go +++ /dev/null @@ -1,159 +0,0 @@ -package ssh - -import ( - "fmt" - "net" - "os" - - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" - "gopkg.in/src-d/go-git.v4/plumbing/client/common" -) - -// AuthMethod is the interface all auth methods for the ssh client -// must implement. The clientConfig method returns the ssh client -// configuration needed to establish an ssh connection. -type AuthMethod interface { - common.AuthMethod - clientConfig() *ssh.ClientConfig -} - -// The names of the AuthMethod implementations. To be returned by the -// Name() method. Most git servers only allow PublicKeysName and -// PublicKeysCallbackName. -const ( - KeyboardInteractiveName = "ssh-keyboard-interactive" - PasswordName = "ssh-password" - PasswordCallbackName = "ssh-password-callback" - PublicKeysName = "ssh-public-keys" - PublicKeysCallbackName = "ssh-public-key-callback" -) - -// KeyboardInteractive implements AuthMethod by using a -// prompt/response sequence controlled by the server. -type KeyboardInteractive struct { - User string - Challenge ssh.KeyboardInteractiveChallenge -} - -func (a *KeyboardInteractive) Name() string { - return KeyboardInteractiveName -} - -func (a *KeyboardInteractive) String() string { - return fmt.Sprintf("user: %s, name: %s", a.User, a.Name()) -} - -func (a *KeyboardInteractive) clientConfig() *ssh.ClientConfig { - return &ssh.ClientConfig{ - User: a.User, - Auth: []ssh.AuthMethod{ssh.KeyboardInteractiveChallenge(a.Challenge)}, - } -} - -// Password implements AuthMethod by using the given password. -type Password struct { - User string - Pass string -} - -func (a *Password) Name() string { - return PasswordName -} - -func (a *Password) String() string { - return fmt.Sprintf("user: %s, name: %s", a.User, a.Name()) -} - -func (a *Password) clientConfig() *ssh.ClientConfig { - return &ssh.ClientConfig{ - User: a.User, - Auth: []ssh.AuthMethod{ssh.Password(a.Pass)}, - } -} - -// PasswordCallback implements AuthMethod by using a callback -// to fetch the password. -type PasswordCallback struct { - User string - Callback func() (pass string, err error) -} - -func (a *PasswordCallback) Name() string { - return PasswordCallbackName -} - -func (a *PasswordCallback) String() string { - return fmt.Sprintf("user: %s, name: %s", a.User, a.Name()) -} - -func (a *PasswordCallback) clientConfig() *ssh.ClientConfig { - return &ssh.ClientConfig{ - User: a.User, - Auth: []ssh.AuthMethod{ssh.PasswordCallback(a.Callback)}, - } -} - -// PublicKeys implements AuthMethod by using the given -// key pairs. -type PublicKeys struct { - User string - Signer ssh.Signer -} - -func (a *PublicKeys) Name() string { - return PublicKeysName -} - -func (a *PublicKeys) String() string { - return fmt.Sprintf("user: %s, name: %s", a.User, a.Name()) -} - -func (a *PublicKeys) clientConfig() *ssh.ClientConfig { - return &ssh.ClientConfig{ - User: a.User, - Auth: []ssh.AuthMethod{ssh.PublicKeys(a.Signer)}, - } -} - -// PublicKeysCallback implements AuthMethod by asking a -// ssh.agent.Agent to act as a signer. -type PublicKeysCallback struct { - User string - Callback func() (signers []ssh.Signer, err error) -} - -func (a *PublicKeysCallback) Name() string { - return PublicKeysCallbackName -} - -func (a *PublicKeysCallback) String() string { - return fmt.Sprintf("user: %s, name: %s", a.User, a.Name()) -} - -func (a *PublicKeysCallback) clientConfig() *ssh.ClientConfig { - return &ssh.ClientConfig{ - User: a.User, - Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(a.Callback)}, - } -} - -const DefaultSSHUsername = "git" - -// Opens a pipe with the ssh agent and uses the pipe -// as the implementer of the public key callback function. -func NewSSHAgentAuth(user string) (*PublicKeysCallback, error) { - if user == "" { - user = DefaultSSHUsername - } - - pipe, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) - if err != nil { - return nil, err - } - - return &PublicKeysCallback{ - User: user, - Callback: agent.NewClient(pipe).Signers, - }, nil -} diff --git a/plumbing/client/ssh/auth_method_test.go b/plumbing/client/ssh/auth_method_test.go deleted file mode 100644 index a87c950..0000000 --- a/plumbing/client/ssh/auth_method_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package ssh - -import ( - "fmt" - "testing" - - . "gopkg.in/check.v1" -) - -func Test(t *testing.T) { TestingT(t) } - -type SuiteCommon struct{} - -var _ = Suite(&SuiteCommon{}) - -func (s *SuiteCommon) TestKeyboardInteractiveName(c *C) { - a := &KeyboardInteractive{ - User: "test", - Challenge: nil, - } - c.Assert(a.Name(), Equals, KeyboardInteractiveName) -} - -func (s *SuiteCommon) TestKeyboardInteractiveString(c *C) { - a := &KeyboardInteractive{ - User: "test", - Challenge: nil, - } - c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", KeyboardInteractiveName)) -} - -func (s *SuiteCommon) TestPasswordName(c *C) { - a := &Password{ - User: "test", - Pass: "", - } - c.Assert(a.Name(), Equals, PasswordName) -} - -func (s *SuiteCommon) TestPasswordString(c *C) { - a := &Password{ - User: "test", - Pass: "", - } - c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PasswordName)) -} - -func (s *SuiteCommon) TestPasswordCallbackName(c *C) { - a := &PasswordCallback{ - User: "test", - Callback: nil, - } - c.Assert(a.Name(), Equals, PasswordCallbackName) -} - -func (s *SuiteCommon) TestPasswordCallbackString(c *C) { - a := &PasswordCallback{ - User: "test", - Callback: nil, - } - c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PasswordCallbackName)) -} - -func (s *SuiteCommon) TestPublicKeysName(c *C) { - a := &PublicKeys{ - User: "test", - Signer: nil, - } - c.Assert(a.Name(), Equals, PublicKeysName) -} - -func (s *SuiteCommon) TestPublicKeysString(c *C) { - a := &PublicKeys{ - User: "test", - Signer: nil, - } - c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PublicKeysName)) -} - -func (s *SuiteCommon) TestPublicKeysCallbackName(c *C) { - a := &PublicKeysCallback{ - User: "test", - Callback: nil, - } - c.Assert(a.Name(), Equals, PublicKeysCallbackName) -} - -func (s *SuiteCommon) TestPublicKeysCallbackString(c *C) { - a := &PublicKeysCallback{ - User: "test", - Callback: nil, - } - c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PublicKeysCallbackName)) -} diff --git a/plumbing/client/ssh/git_upload_pack.go b/plumbing/client/ssh/git_upload_pack.go deleted file mode 100644 index db7fa93..0000000 --- a/plumbing/client/ssh/git_upload_pack.go +++ /dev/null @@ -1,311 +0,0 @@ -// Package ssh implements a ssh client for go-git. -package ssh - -import ( - "bytes" - "errors" - "fmt" - "io" - "strings" - - "gopkg.in/src-d/go-git.v4/plumbing/client/common" - "gopkg.in/src-d/go-git.v4/plumbing/format/packp/advrefs" - "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" - "gopkg.in/src-d/go-git.v4/plumbing/format/packp/ulreq" - - "golang.org/x/crypto/ssh" -) - -// New errors introduced by this package. -var ( - ErrInvalidAuthMethod = errors.New("invalid ssh auth method") - ErrAuthRequired = errors.New("cannot connect: auth required") - ErrNotConnected = errors.New("not connected") - ErrAlreadyConnected = errors.New("already connected") - ErrUploadPackAnswerFormat = errors.New("git-upload-pack bad answer format") - ErrUnsupportedVCS = errors.New("only git is supported") - ErrUnsupportedRepo = errors.New("only github.com is supported") - - nak = []byte("NAK") - eol = []byte("\n") -) - -// GitUploadPackService holds the service information. -// The zero value is safe to use. -type GitUploadPackService struct { - connected bool - endpoint common.Endpoint - client *ssh.Client - auth AuthMethod -} - -// NewGitUploadPackService initialises a GitUploadPackService, -func NewGitUploadPackService(endpoint common.Endpoint) common.GitUploadPackService { - return &GitUploadPackService{endpoint: endpoint} -} - -// Connect connects to the SSH server, unless a AuthMethod was set with SetAuth -// method, by default uses an auth method based on PublicKeysCallback, it -// connects to a SSH agent, using the address stored in the SSH_AUTH_SOCK -// environment var -func (s *GitUploadPackService) Connect() error { - if s.connected { - return ErrAlreadyConnected - } - - if err := s.setAuthFromEndpoint(); err != nil { - return err - } - - var err error - s.client, err = ssh.Dial("tcp", s.getHostWithPort(), s.auth.clientConfig()) - if err != nil { - return err - } - - s.connected = true - return nil -} - -func (s *GitUploadPackService) getHostWithPort() string { - host := s.endpoint.Host - if strings.Index(s.endpoint.Host, ":") == -1 { - host += ":22" - } - - return host -} - -func (s *GitUploadPackService) setAuthFromEndpoint() error { - var u string - if info := s.endpoint.User; info != nil { - u = info.Username() - } - - var err error - s.auth, err = NewSSHAgentAuth(u) - return err -} - -// SetAuth sets the AuthMethod -func (s *GitUploadPackService) SetAuth(auth common.AuthMethod) error { - var ok bool - s.auth, ok = auth.(AuthMethod) - if !ok { - return ErrInvalidAuthMethod - } - - return nil -} - -// Info returns the GitUploadPackInfo of the repository. The client must be -// connected with the repository (using the ConnectWithAuth() method) before -// using this method. -func (s *GitUploadPackService) Info() (*common.GitUploadPackInfo, error) { - if !s.connected { - return nil, ErrNotConnected - } - - session, err := s.client.NewSession() - if err != nil { - return nil, err - } - defer func() { - // the session can be closed by the other endpoint, - // therefore we must ignore a close error. - _ = session.Close() - }() - - out, err := session.Output(s.getCommand()) - if err != nil { - return nil, err - } - - i := common.NewGitUploadPackInfo() - return i, i.Decode(bytes.NewReader(out)) -} - -// Disconnect the SSH client. -func (s *GitUploadPackService) Disconnect() error { - if !s.connected { - return ErrNotConnected - } - s.connected = false - return s.client.Close() -} - -// Fetch returns a packfile for a given upload request. It opens a new -// SSH session on a connected GitUploadPackService, sends the given -// upload request to the server and returns a reader for the received -// packfile. Closing the returned reader will close the SSH session. -func (s *GitUploadPackService) Fetch(req *common.GitUploadPackRequest) (io.ReadCloser, error) { - if !s.connected { - return nil, ErrNotConnected - } - - session, i, o, done, err := openSSHSession(s.client, s.getCommand()) - if err != nil { - return nil, fmt.Errorf("cannot open SSH session: %s", err) - } - - if err := talkPackProtocol(i, o, req); err != nil { - return nil, err - } - - return &fetchSession{ - Reader: o, - session: session, - done: done, - }, nil -} - -func openSSHSession(c *ssh.Client, cmd string) ( - *ssh.Session, io.WriteCloser, io.Reader, <-chan error, error) { - - session, err := c.NewSession() - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("cannot open SSH session: %s", err) - } - - i, err := session.StdinPipe() - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("cannot pipe remote stdin: %s", err) - } - - o, err := session.StdoutPipe() - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("cannot pipe remote stdout: %s", err) - } - - done := make(chan error) - go func() { - done <- session.Run(cmd) - }() - - return session, i, o, done, nil -} - -// TODO support multi_ack mode -// TODO support multi_ack_detailed mode -// TODO support acks for common objects -// TODO build a proper state machine for all these processing options -func talkPackProtocol(w io.WriteCloser, r io.Reader, - req *common.GitUploadPackRequest) error { - - if err := skipAdvRef(r); err != nil { - return fmt.Errorf("skipping advertised-refs: %s", err) - } - - if err := sendUlReq(w, req); err != nil { - return fmt.Errorf("sending upload-req message: %s", err) - } - - if err := sendHaves(w, req); err != nil { - return fmt.Errorf("sending haves message: %s", err) - } - - if err := sendDone(w); err != nil { - return fmt.Errorf("sending done message: %s", err) - } - - if err := w.Close(); err != nil { - return fmt.Errorf("closing input: %s", err) - } - - if err := readNAK(r); err != nil { - return fmt.Errorf("reading NAK: %s", err) - } - - return nil -} - -func skipAdvRef(r io.Reader) error { - d := advrefs.NewDecoder(r) - ar := advrefs.New() - - return d.Decode(ar) -} - -func sendUlReq(w io.Writer, req *common.GitUploadPackRequest) error { - ur := ulreq.New() - ur.Wants = req.Wants - ur.Depth = ulreq.DepthCommits(req.Depth) - e := ulreq.NewEncoder(w) - - return e.Encode(ur) -} - -func sendHaves(w io.Writer, req *common.GitUploadPackRequest) error { - e := pktline.NewEncoder(w) - for _, have := range req.Haves { - if err := e.Encodef("have %s\n", have); err != nil { - return fmt.Errorf("sending haves for %q: %s", have, err) - } - } - - if len(req.Haves) != 0 { - if err := e.Flush(); err != nil { - return fmt.Errorf("sending flush-pkt after haves: %s", err) - } - } - - return nil -} - -func sendDone(w io.Writer) error { - e := pktline.NewEncoder(w) - - return e.Encodef("done\n") -} - -func readNAK(r io.Reader) error { - s := pktline.NewScanner(r) - if !s.Scan() { - return s.Err() - } - - b := s.Bytes() - b = bytes.TrimSuffix(b, eol) - if !bytes.Equal(b, nak) { - return fmt.Errorf("expecting NAK, found %q instead", string(b)) - } - - return nil -} - -type fetchSession struct { - io.Reader - session *ssh.Session - done <-chan error -} - -// Close closes the session and collects the output state of the remote -// SSH command. -// -// If both the remote command and the closing of the session completes -// susccessfully it returns nil. -// -// If the remote command completes unsuccessfully or is interrupted by a -// signal, it returns the corresponding *ExitError. -// -// Otherwise, if clossing the SSH session fails it returns the close -// error. Closing the session when the other has already close it is -// not cosidered an error. -func (f *fetchSession) Close() (err error) { - if err := <-f.done; err != nil { - return err - } - - if err := f.session.Close(); err != nil && err != io.EOF { - return err - } - - return nil -} - -func (s *GitUploadPackService) getCommand() string { - directory := s.endpoint.Path - directory = directory[1:] - - return fmt.Sprintf("git-upload-pack '%s'", directory) -} diff --git a/plumbing/client/ssh/git_upload_pack_test.go b/plumbing/client/ssh/git_upload_pack_test.go deleted file mode 100644 index 4d5b2b1..0000000 --- a/plumbing/client/ssh/git_upload_pack_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package ssh - -import ( - "io/ioutil" - "os" - - . "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("git@github.com:git-fixtures/basic.git") - c.Assert(err, IsNil) - - if os.Getenv("SSH_AUTH_SOCK") == "" { - c.Skip("SSH_AUTH_SOCK is not set") - } -} - -// A mock implementation of client.common.AuthMethod -// to test non ssh auth method detection. -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, ErrInvalidAuthMethod) -} - -func (s *RemoteSuite) TestAlreadyConnected(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - defer func() { - c.Assert(r.Disconnect(), IsNil) - }() - - c.Assert(r.Connect(), Equals, ErrAlreadyConnected) -} - -func (s *RemoteSuite) TestDisconnect(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - c.Assert(r.Disconnect(), IsNil) -} - -func (s *RemoteSuite) TestDisconnectedWhenNonConnected(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Disconnect(), Equals, ErrNotConnected) -} - -func (s *RemoteSuite) TestAlreadyDisconnected(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - c.Assert(r.Disconnect(), IsNil) - c.Assert(r.Disconnect(), Equals, ErrNotConnected) -} - -func (s *RemoteSuite) TestServeralConnections(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - c.Assert(r.Disconnect(), IsNil) - - c.Assert(r.Connect(), IsNil) - c.Assert(r.Disconnect(), IsNil) - - c.Assert(r.Connect(), IsNil) - c.Assert(r.Disconnect(), IsNil) -} - -func (s *RemoteSuite) TestInfoNotConnected(c *C) { - r := NewGitUploadPackService(s.Endpoint) - _, err := r.Info() - c.Assert(err, Equals, ErrNotConnected) -} - -func (s *RemoteSuite) TestDefaultBranch(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - defer func() { c.Assert(r.Disconnect(), 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) - defer func() { c.Assert(r.Disconnect(), IsNil) }() - - info, err := r.Info() - c.Assert(err, IsNil) - c.Assert(info.Capabilities.Get("agent").Values, HasLen, 1) -} - -func (s *RemoteSuite) TestFetchNotConnected(c *C) { - r := NewGitUploadPackService(s.Endpoint) - pr := &common.GitUploadPackRequest{} - pr.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) - _, err := r.Fetch(pr) - c.Assert(err, Equals, ErrNotConnected) -} - -func (s *RemoteSuite) TestFetch(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - defer func() { c.Assert(r.Disconnect(), IsNil) }() - - req := &common.GitUploadPackRequest{} - req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) - req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881")) - reader, err := r.Fetch(req) - c.Assert(err, IsNil) - defer func() { c.Assert(reader.Close(), IsNil) }() - - b, err := ioutil.ReadAll(reader) - c.Assert(err, IsNil) - c.Check(len(b), Equals, 85585) -} - -func (s *RemoteSuite) TestFetchError(c *C) { - r := NewGitUploadPackService(s.Endpoint) - c.Assert(r.Connect(), IsNil) - defer func() { c.Assert(r.Disconnect(), IsNil) }() - - req := &common.GitUploadPackRequest{} - req.Want(plumbing.NewHash("1111111111111111111111111111111111111111")) - - reader, err := r.Fetch(req) - c.Assert(err, IsNil) - - err = reader.Close() - c.Assert(err, Not(IsNil)) -} |