aboutsummaryrefslogblamecommitdiffstats
path: root/clients/ssh/git_upload_pack.go
blob: 4a4021dc62282474ec51abd336000886e8c875ec (plain) (tree)











































                                                                                   

                                                                































































































































































                                                                                                        
// 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(url string) *GitUploadPackService {
	// url ignored
	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
}