aboutsummaryrefslogtreecommitdiffstats
path: root/clients/ssh
diff options
context:
space:
mode:
Diffstat (limited to 'clients/ssh')
-rw-r--r--clients/ssh/auth_method.go136
-rw-r--r--clients/ssh/auth_method_test.go94
-rw-r--r--clients/ssh/git_upload_pack.go205
-rw-r--r--clients/ssh/git_upload_pack_test.go235
4 files changed, 670 insertions, 0 deletions
diff --git a/clients/ssh/auth_method.go b/clients/ssh/auth_method.go
new file mode 100644
index 0000000..445d502
--- /dev/null
+++ b/clients/ssh/auth_method.go
@@ -0,0 +1,136 @@
+package ssh
+
+import (
+ "fmt"
+
+ "golang.org/x/crypto/ssh"
+ "gopkg.in/src-d/go-git.v2/clients/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)},
+ }
+}
diff --git a/clients/ssh/auth_method_test.go b/clients/ssh/auth_method_test.go
new file mode 100644
index 0000000..ca4558d
--- /dev/null
+++ b/clients/ssh/auth_method_test.go
@@ -0,0 +1,94 @@
+package ssh
+
+import (
+ "fmt"
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type SuiteCommon struct{}
+
+var _ = Suite(&SuiteCommon{})
+
+func (s *SuiteRemote) TestKeyboardInteractiveName(c *C) {
+ a := &KeyboardInteractive{
+ User: "test",
+ Challenge: nil,
+ }
+ c.Assert(a.Name(), Equals, KeyboardInteractiveName)
+}
+
+func (s *SuiteRemote) TestKeyboardInteractiveString(c *C) {
+ a := &KeyboardInteractive{
+ User: "test",
+ Challenge: nil,
+ }
+ c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", KeyboardInteractiveName))
+}
+
+func (s *SuiteRemote) TestPasswordName(c *C) {
+ a := &Password{
+ User: "test",
+ Pass: "",
+ }
+ c.Assert(a.Name(), Equals, PasswordName)
+}
+
+func (s *SuiteRemote) TestPasswordString(c *C) {
+ a := &Password{
+ User: "test",
+ Pass: "",
+ }
+ c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PasswordName))
+}
+
+func (s *SuiteRemote) TestPasswordCallbackName(c *C) {
+ a := &PasswordCallback{
+ User: "test",
+ Callback: nil,
+ }
+ c.Assert(a.Name(), Equals, PasswordCallbackName)
+}
+
+func (s *SuiteRemote) TestPasswordCallbackString(c *C) {
+ a := &PasswordCallback{
+ User: "test",
+ Callback: nil,
+ }
+ c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PasswordCallbackName))
+}
+
+func (s *SuiteRemote) TestPublicKeysName(c *C) {
+ a := &PublicKeys{
+ User: "test",
+ Signer: nil,
+ }
+ c.Assert(a.Name(), Equals, PublicKeysName)
+}
+
+func (s *SuiteRemote) TestPublicKeysString(c *C) {
+ a := &PublicKeys{
+ User: "test",
+ Signer: nil,
+ }
+ c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PublicKeysName))
+}
+
+func (s *SuiteRemote) TestPublicKeysCallbackName(c *C) {
+ a := &PublicKeysCallback{
+ User: "test",
+ Callback: nil,
+ }
+ c.Assert(a.Name(), Equals, PublicKeysCallbackName)
+}
+
+func (s *SuiteRemote) TestPublicKeysCallbackString(c *C) {
+ a := &PublicKeysCallback{
+ User: "test",
+ Callback: nil,
+ }
+ c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PublicKeysCallbackName))
+}
diff --git a/clients/ssh/git_upload_pack.go b/clients/ssh/git_upload_pack.go
new file mode 100644
index 0000000..09cb5ab
--- /dev/null
+++ b/clients/ssh/git_upload_pack.go
@@ -0,0 +1,205 @@
+// Package ssh implements a ssh client for go-git.
+//
+// The Connect() method is not allowed in ssh, use ConnectWithAuth() instead.
+package ssh
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/url"
+
+ "gopkg.in/src-d/go-git.v2/clients/common"
+ "gopkg.in/src-d/go-git.v2/formats/pktline"
+
+ "github.com/sourcegraph/go-vcsurl"
+ "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")
+)
+
+// GitUploadPackService holds the service information.
+// The zero value is safe to use.
+// TODO: remove NewGitUploadPackService().
+type GitUploadPackService struct {
+ connected bool
+ vcs *vcsurl.RepoInfo
+ client *ssh.Client
+ auth AuthMethod
+}
+
+// NewGitUploadPackService initialises a GitUploadPackService.
+// TODO: remove this, as the struct is zero-value safe.
+func NewGitUploadPackService() *GitUploadPackService {
+ return &GitUploadPackService{}
+}
+
+// Connect cannot be used with SSH clients and always return
+// ErrAuthRequired. Use ConnectWithAuth instead.
+func (s *GitUploadPackService) Connect(ep common.Endpoint) (err error) {
+ return ErrAuthRequired
+}
+
+// ConnectWithAuth connects to ep using SSH. Authentication is handled
+// by auth.
+func (s *GitUploadPackService) ConnectWithAuth(ep common.Endpoint, auth common.AuthMethod) (err error) {
+ if s.connected {
+ return ErrAlreadyConnected
+ }
+
+ s.vcs, err = vcsurl.Parse(string(ep))
+ if err != nil {
+ return err
+ }
+
+ url, err := vcsToURL(s.vcs)
+ if err != nil {
+ return
+ }
+
+ var ok bool
+ s.auth, ok = auth.(AuthMethod)
+ if !ok {
+ return ErrInvalidAuthMethod
+ }
+
+ s.client, err = ssh.Dial("tcp", url.Host, s.auth.clientConfig())
+ if err != nil {
+ return err
+ }
+
+ s.connected = true
+ return
+}
+
+func vcsToURL(vcs *vcsurl.RepoInfo) (u *url.URL, err error) {
+ if vcs.VCS != vcsurl.Git {
+ return nil, ErrUnsupportedVCS
+ }
+ if vcs.RepoHost != vcsurl.GitHub {
+ return nil, ErrUnsupportedRepo
+ }
+ s := "ssh://git@" + string(vcs.RepoHost) + ":22/" + vcs.FullName
+ u, err = url.Parse(s)
+ return
+}
+
+// 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() (i *common.GitUploadPackInfo, err 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("git-upload-pack " + s.vcs.FullName + ".git")
+ if err != nil {
+ return nil, err
+ }
+
+ i = common.NewGitUploadPackInfo()
+ return i, i.Decode(pktline.NewDecoder(bytes.NewReader(out)))
+}
+
+// Disconnect the SSH client.
+func (s *GitUploadPackService) Disconnect() (err error) {
+ if !s.connected {
+ return ErrNotConnected
+ }
+ s.connected = false
+ return s.client.Close()
+}
+
+// Fetch retrieves the GitUploadPack form the repository.
+// You must be connected to the repository before using this method
+// (using the ConnectWithAuth() method).
+// TODO: fetch should really reuse the info session instead of openning a new
+// one
+func (s *GitUploadPackService) Fetch(r *common.GitUploadPackRequest) (rc io.ReadCloser, err 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()
+ }()
+
+ si, err := session.StdinPipe()
+ if err != nil {
+ return nil, err
+ }
+
+ so, err := session.StdoutPipe()
+ if err != nil {
+ return nil, err
+ }
+
+ go func() {
+ fmt.Fprintln(si, r.String())
+ err = si.Close()
+ }()
+
+ err = session.Start("git-upload-pack " + s.vcs.FullName + ".git")
+ if err != nil {
+ return nil, err
+ }
+ // TODO: inestigate this *ExitError type (command fails or
+ // doesn't complete successfully), as it is happenning all
+ // the time, but everyting seems to work fine.
+ err = session.Wait()
+ if err != nil {
+ if _, ok := err.(*ssh.ExitError); !ok {
+ return nil, err
+ }
+ }
+
+ // read until the header of the second answer
+ soBuf := bufio.NewReader(so)
+ token := "0000"
+ for {
+ var line string
+ line, err = soBuf.ReadString('\n')
+ if err == io.EOF {
+ return nil, ErrUploadPackAnswerFormat
+ }
+ if line[0:len(token)] == token {
+ break
+ }
+ }
+
+ data, err := ioutil.ReadAll(soBuf)
+ if err != nil {
+ return nil, err
+ }
+ buf := bytes.NewBuffer(data)
+ return ioutil.NopCloser(buf), nil
+}
diff --git a/clients/ssh/git_upload_pack_test.go b/clients/ssh/git_upload_pack_test.go
new file mode 100644
index 0000000..62a8596
--- /dev/null
+++ b/clients/ssh/git_upload_pack_test.go
@@ -0,0 +1,235 @@
+package ssh
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+
+ "golang.org/x/crypto/ssh/agent"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-git.v2/clients/common"
+ "gopkg.in/src-d/go-git.v2/core"
+)
+
+type SuiteRemote struct{}
+
+var _ = Suite(&SuiteRemote{})
+
+const (
+ fixRepo = "git@github.com:tyba/git-fixture.git"
+ fixRepoBadVcs = "www.example.com"
+ fixRepoNonGit = "https://code.google.com/p/go"
+ fixGitRepoNonGithub = "https://bitbucket.org/user/repo.git"
+)
+
+func (s *SuiteRemote) TestConnect(c *C) {
+ fmt.Println("TestConnect")
+ r := NewGitUploadPackService()
+ c.Assert(r.Connect(fixRepo), Equals, ErrAuthRequired)
+}
+
+// We will use a running ssh agent for testing
+// ssh authentication.
+type sshAgentConn struct {
+ pipe net.Conn
+ auth *PublicKeysCallback
+}
+
+// Opens a pipe with the ssh agent and uses the pipe
+// as the implementer of the public key callback function.
+func newSSHAgentConn() (*sshAgentConn, error) {
+ pipe, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
+ if err != nil {
+ return nil, err
+ }
+ return &sshAgentConn{
+ pipe: pipe,
+ auth: &PublicKeysCallback{
+ User: "git",
+ Callback: agent.NewClient(pipe).Signers,
+ },
+ }, nil
+}
+
+// Closes the pipe with the ssh agent
+func (c *sshAgentConn) close() error {
+ return c.pipe.Close()
+}
+
+func (s *SuiteRemote) TestConnectWithPublicKeysCallback(c *C) {
+ fmt.Println("TestConnectWithPublicKeysCallback")
+ agent, err := newSSHAgentConn()
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(agent.close(), IsNil) }()
+
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), IsNil)
+ defer func() { c.Assert(r.Disconnect(), IsNil) }()
+ c.Assert(r.connected, Equals, true)
+ c.Assert(r.auth, Equals, agent.auth)
+}
+
+func (s *SuiteRemote) TestConnectBadVcs(c *C) {
+ fmt.Println("TestConnectBadVcs")
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepoBadVcs, nil), ErrorMatches, fmt.Sprintf(".*%s.*", fixRepoBadVcs))
+}
+
+func (s *SuiteRemote) TestConnectNonGit(c *C) {
+ fmt.Println("TestConnectNonGit")
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepoNonGit, nil), Equals, ErrUnsupportedVCS)
+}
+
+func (s *SuiteRemote) TestConnectNonGithub(c *C) {
+ fmt.Println("TestConnectNonGit")
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixGitRepoNonGithub, nil), Equals, ErrUnsupportedRepo)
+}
+
+// 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 *SuiteRemote) TestConnectWithAuthWrongType(c *C) {
+ fmt.Println("TestConnectWithAuthWrongType")
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepo, &mockAuth{}), Equals, ErrInvalidAuthMethod)
+ c.Assert(r.connected, Equals, false)
+}
+
+func (s *SuiteRemote) TestAlreadyConnected(c *C) {
+ fmt.Println("TestAlreadyConnected")
+ agent, err := newSSHAgentConn()
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(agent.close(), IsNil) }()
+
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), IsNil)
+ defer func() { c.Assert(r.Disconnect(), IsNil) }()
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), Equals, ErrAlreadyConnected)
+ c.Assert(r.connected, Equals, true)
+}
+
+func (s *SuiteRemote) TestDisconnect(c *C) {
+ fmt.Println("TestDisconnect")
+ agent, err := newSSHAgentConn()
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(agent.close(), IsNil) }()
+
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), IsNil)
+ c.Assert(r.Disconnect(), IsNil)
+ c.Assert(r.connected, Equals, false)
+}
+
+func (s *SuiteRemote) TestDisconnectedWhenNonConnected(c *C) {
+ fmt.Println("TestDisconnectedWhenNonConnected")
+ r := NewGitUploadPackService()
+ c.Assert(r.Disconnect(), Equals, ErrNotConnected)
+}
+
+func (s *SuiteRemote) TestAlreadyDisconnected(c *C) {
+ fmt.Println("TestAlreadyDisconnected")
+ agent, err := newSSHAgentConn()
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(agent.close(), IsNil) }()
+
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), IsNil)
+ c.Assert(r.Disconnect(), IsNil)
+ c.Assert(r.Disconnect(), Equals, ErrNotConnected)
+ c.Assert(r.connected, Equals, false)
+}
+
+func (s *SuiteRemote) TestServeralConnections(c *C) {
+ fmt.Println("TestServeralConnections")
+ agent, err := newSSHAgentConn()
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(agent.close(), IsNil) }()
+
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), IsNil)
+ c.Assert(r.Disconnect(), IsNil)
+
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), IsNil)
+ c.Assert(r.connected, Equals, true)
+ c.Assert(r.Disconnect(), IsNil)
+ c.Assert(r.connected, Equals, false)
+
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), IsNil)
+ c.Assert(r.connected, Equals, true)
+ c.Assert(r.Disconnect(), IsNil)
+ c.Assert(r.connected, Equals, false)
+}
+
+func (s *SuiteRemote) TestInfoNotConnected(c *C) {
+ fmt.Println("TestInfoNotConnected")
+ r := NewGitUploadPackService()
+ _, err := r.Info()
+ c.Assert(err, Equals, ErrNotConnected)
+}
+
+func (s *SuiteRemote) TestDefaultBranch(c *C) {
+ fmt.Println("TestDefaultBranch")
+ agent, err := newSSHAgentConn()
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(agent.close(), IsNil) }()
+
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), 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 *SuiteRemote) TestCapabilities(c *C) {
+ fmt.Println("TestCapabilities")
+ agent, err := newSSHAgentConn()
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(agent.close(), IsNil) }()
+
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), 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 *SuiteRemote) TestFetchNotConnected(c *C) {
+ fmt.Println("TestFetchNotConnected")
+ r := NewGitUploadPackService()
+ pr := &common.GitUploadPackRequest{}
+ pr.Want(core.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ _, err := r.Fetch(pr)
+ c.Assert(err, Equals, ErrNotConnected)
+}
+
+func (s *SuiteRemote) TestFetch(c *C) {
+ fmt.Println("TestFetch")
+ agent, err := newSSHAgentConn()
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(agent.close(), IsNil) }()
+
+ r := NewGitUploadPackService()
+ c.Assert(r.ConnectWithAuth(fixRepo, agent.auth), IsNil)
+ defer func() { c.Assert(r.Disconnect(), IsNil) }()
+
+ pr := &common.GitUploadPackRequest{}
+ pr.Want(core.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ reader, err := r.Fetch(pr)
+ c.Assert(err, IsNil)
+
+ b, err := ioutil.ReadAll(reader)
+ c.Assert(err, IsNil)
+ c.Assert(b, HasLen, 85374)
+}