aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing/client/ssh
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/client/ssh
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/client/ssh')
-rw-r--r--plumbing/client/ssh/auth_method.go159
-rw-r--r--plumbing/client/ssh/auth_method_test.go94
-rw-r--r--plumbing/client/ssh/git_upload_pack.go311
-rw-r--r--plumbing/client/ssh/git_upload_pack_test.go144
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))
-}