feat(dir2config): defaults

This commit is contained in:
Mikaël Cluseau
2019-02-28 19:27:09 +11:00
parent d2b212ae6b
commit ea6fce68e1
383 changed files with 74236 additions and 41 deletions

View File

@ -0,0 +1,48 @@
// Package client contains helper function to deal with the different client
// protocols.
package client
import (
"fmt"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/plumbing/transport/file"
"gopkg.in/src-d/go-git.v4/plumbing/transport/git"
"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
"gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
)
// Protocols are the protocols supported by default.
var Protocols = map[string]transport.Transport{
"http": http.DefaultClient,
"https": http.DefaultClient,
"ssh": ssh.DefaultClient,
"git": git.DefaultClient,
"file": file.DefaultClient,
}
// InstallProtocol adds or modifies an existing protocol.
func InstallProtocol(scheme string, c transport.Transport) {
if c == nil {
delete(Protocols, scheme)
return
}
Protocols[scheme] = c
}
// NewClient returns the appropriate client among of the set of known protocols:
// http://, https://, ssh:// and file://.
// See `InstallProtocol` to add or modify protocols.
func NewClient(endpoint *transport.Endpoint) (transport.Transport, error) {
f, ok := Protocols[endpoint.Protocol]
if !ok {
return nil, fmt.Errorf("unsupported scheme %q", endpoint.Protocol)
}
if f == nil {
return nil, fmt.Errorf("malformed client for scheme %q, client is defined as nil", endpoint.Protocol)
}
return f, nil
}

View File

@ -0,0 +1,274 @@
// Package transport includes the implementation for different transport
// protocols.
//
// `Client` can be used to fetch and send packfiles to a git server.
// The `client` package provides higher level functions to instantiate the
// appropriate `Client` based on the repository URL.
//
// go-git supports HTTP and SSH (see `Protocols`), but you can also install
// your own protocols (see the `client` package).
//
// Each protocol has its own implementation of `Client`, but you should
// generally not use them directly, use `client.NewClient` instead.
package transport
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/url"
"strconv"
"strings"
giturl "gopkg.in/src-d/go-git.v4/internal/url"
"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/protocol/packp/capability"
)
var (
ErrRepositoryNotFound = errors.New("repository not found")
ErrEmptyRemoteRepository = errors.New("remote repository is empty")
ErrAuthenticationRequired = errors.New("authentication required")
ErrAuthorizationFailed = errors.New("authorization failed")
ErrEmptyUploadPackRequest = errors.New("empty git-upload-pack given")
ErrInvalidAuthMethod = errors.New("invalid auth method")
ErrAlreadyConnected = errors.New("session already established")
)
const (
UploadPackServiceName = "git-upload-pack"
ReceivePackServiceName = "git-receive-pack"
)
// Transport can initiate git-upload-pack and git-receive-pack processes.
// It is implemented both by the client and the server, making this a RPC.
type Transport interface {
// NewUploadPackSession starts a git-upload-pack session for an endpoint.
NewUploadPackSession(*Endpoint, AuthMethod) (UploadPackSession, error)
// NewReceivePackSession starts a git-receive-pack session for an endpoint.
NewReceivePackSession(*Endpoint, AuthMethod) (ReceivePackSession, error)
}
type Session interface {
// AdvertisedReferences retrieves the advertised references for a
// repository.
// If the repository does not exist, returns ErrRepositoryNotFound.
// If the repository exists, but is empty, returns ErrEmptyRemoteRepository.
AdvertisedReferences() (*packp.AdvRefs, error)
io.Closer
}
type AuthMethod interface {
fmt.Stringer
Name() string
}
// UploadPackSession represents a git-upload-pack session.
// A git-upload-pack session has two steps: reference discovery
// (AdvertisedReferences) and uploading pack (UploadPack).
type UploadPackSession interface {
Session
// UploadPack takes a git-upload-pack request and returns a response,
// including a packfile. Don't be confused by terminology, the client
// side of a git-upload-pack is called git-fetch-pack, although here
// the same interface is used to make it RPC-like.
UploadPack(context.Context, *packp.UploadPackRequest) (*packp.UploadPackResponse, error)
}
// ReceivePackSession represents a git-receive-pack session.
// A git-receive-pack session has two steps: reference discovery
// (AdvertisedReferences) and receiving pack (ReceivePack).
// In that order.
type ReceivePackSession interface {
Session
// ReceivePack sends an update references request and a packfile
// reader and returns a ReportStatus and error. Don't be confused by
// terminology, the client side of a git-receive-pack is called
// git-send-pack, although here the same interface is used to make it
// RPC-like.
ReceivePack(context.Context, *packp.ReferenceUpdateRequest) (*packp.ReportStatus, error)
}
// Endpoint represents a Git URL in any supported protocol.
type Endpoint struct {
// Protocol is the protocol of the endpoint (e.g. git, https, file).
Protocol string
// User is the user.
User string
// Password is the password.
Password string
// Host is the host.
Host string
// Port is the port to connect, if 0 the default port for the given protocol
// wil be used.
Port int
// Path is the repository path.
Path string
}
var defaultPorts = map[string]int{
"http": 80,
"https": 443,
"git": 9418,
"ssh": 22,
}
// String returns a string representation of the Git URL.
func (u *Endpoint) String() string {
var buf bytes.Buffer
if u.Protocol != "" {
buf.WriteString(u.Protocol)
buf.WriteByte(':')
}
if u.Protocol != "" || u.Host != "" || u.User != "" || u.Password != "" {
buf.WriteString("//")
if u.User != "" || u.Password != "" {
buf.WriteString(url.PathEscape(u.User))
if u.Password != "" {
buf.WriteByte(':')
buf.WriteString(url.PathEscape(u.Password))
}
buf.WriteByte('@')
}
if u.Host != "" {
buf.WriteString(u.Host)
if u.Port != 0 {
port, ok := defaultPorts[strings.ToLower(u.Protocol)]
if !ok || ok && port != u.Port {
fmt.Fprintf(&buf, ":%d", u.Port)
}
}
}
}
if u.Path != "" && u.Path[0] != '/' && u.Host != "" {
buf.WriteByte('/')
}
buf.WriteString(u.Path)
return buf.String()
}
func NewEndpoint(endpoint string) (*Endpoint, error) {
if e, ok := parseSCPLike(endpoint); ok {
return e, nil
}
if e, ok := parseFile(endpoint); ok {
return e, nil
}
return parseURL(endpoint)
}
func parseURL(endpoint string) (*Endpoint, error) {
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
if !u.IsAbs() {
return nil, plumbing.NewPermanentError(fmt.Errorf(
"invalid endpoint: %s", endpoint,
))
}
var user, pass string
if u.User != nil {
user = u.User.Username()
pass, _ = u.User.Password()
}
return &Endpoint{
Protocol: u.Scheme,
User: user,
Password: pass,
Host: u.Hostname(),
Port: getPort(u),
Path: getPath(u),
}, nil
}
func getPort(u *url.URL) int {
p := u.Port()
if p == "" {
return 0
}
i, err := strconv.Atoi(p)
if err != nil {
return 0
}
return i
}
func getPath(u *url.URL) string {
var res string = u.Path
if u.RawQuery != "" {
res += "?" + u.RawQuery
}
if u.Fragment != "" {
res += "#" + u.Fragment
}
return res
}
func parseSCPLike(endpoint string) (*Endpoint, bool) {
if giturl.MatchesScheme(endpoint) || !giturl.MatchesScpLike(endpoint) {
return nil, false
}
user, host, portStr, path := giturl.FindScpLikeComponents(endpoint)
port, err := strconv.Atoi(portStr)
if err != nil {
port = 22
}
return &Endpoint{
Protocol: "ssh",
User: user,
Host: host,
Port: port,
Path: path,
}, true
}
func parseFile(endpoint string) (*Endpoint, bool) {
if giturl.MatchesScheme(endpoint) {
return nil, false
}
path := endpoint
return &Endpoint{
Protocol: "file",
Path: path,
}, true
}
// UnsupportedCapabilities are the capabilities not supported by any client
// implementation
var UnsupportedCapabilities = []capability.Capability{
capability.MultiACK,
capability.MultiACKDetailed,
capability.ThinPack,
}
// FilterUnsupportedCapabilities it filter out all the UnsupportedCapabilities
// from a capability.List, the intended usage is on the client implementation
// to filter the capabilities from an AdvRefs message.
func FilterUnsupportedCapabilities(list *capability.List) {
for _, c := range UnsupportedCapabilities {
list.Delete(c)
}
}

View File

@ -0,0 +1,156 @@
// Package file implements the file transport protocol.
package file
import (
"bufio"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/plumbing/transport/internal/common"
)
// DefaultClient is the default local client.
var DefaultClient = NewClient(
transport.UploadPackServiceName,
transport.ReceivePackServiceName,
)
type runner struct {
UploadPackBin string
ReceivePackBin string
}
// NewClient returns a new local client using the given git-upload-pack and
// git-receive-pack binaries.
func NewClient(uploadPackBin, receivePackBin string) transport.Transport {
return common.NewClient(&runner{
UploadPackBin: uploadPackBin,
ReceivePackBin: receivePackBin,
})
}
func prefixExecPath(cmd string) (string, error) {
// Use `git --exec-path` to find the exec path.
execCmd := exec.Command("git", "--exec-path")
stdout, err := execCmd.StdoutPipe()
if err != nil {
return "", err
}
stdoutBuf := bufio.NewReader(stdout)
err = execCmd.Start()
if err != nil {
return "", err
}
execPathBytes, isPrefix, err := stdoutBuf.ReadLine()
if err != nil {
return "", err
}
if isPrefix {
return "", errors.New("Couldn't read exec-path line all at once")
}
err = execCmd.Wait()
if err != nil {
return "", err
}
execPath := string(execPathBytes)
execPath = strings.TrimSpace(execPath)
cmd = filepath.Join(execPath, cmd)
// Make sure it actually exists.
_, err = exec.LookPath(cmd)
if err != nil {
return "", err
}
return cmd, nil
}
func (r *runner) Command(cmd string, ep *transport.Endpoint, auth transport.AuthMethod,
) (common.Command, error) {
switch cmd {
case transport.UploadPackServiceName:
cmd = r.UploadPackBin
case transport.ReceivePackServiceName:
cmd = r.ReceivePackBin
}
_, err := exec.LookPath(cmd)
if err != nil {
if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound {
cmd, err = prefixExecPath(cmd)
if err != nil {
return nil, err
}
} else {
return nil, err
}
}
return &command{cmd: exec.Command(cmd, ep.Path)}, nil
}
type command struct {
cmd *exec.Cmd
stderrCloser io.Closer
closed bool
}
func (c *command) Start() error {
return c.cmd.Start()
}
func (c *command) StderrPipe() (io.Reader, error) {
// Pipe returned by Command.StderrPipe has a race with Read + Command.Wait.
// We use an io.Pipe and close it after the command finishes.
r, w := io.Pipe()
c.cmd.Stderr = w
c.stderrCloser = r
return r, nil
}
func (c *command) StdinPipe() (io.WriteCloser, error) {
return c.cmd.StdinPipe()
}
func (c *command) StdoutPipe() (io.Reader, error) {
return c.cmd.StdoutPipe()
}
func (c *command) Kill() error {
c.cmd.Process.Kill()
return c.Close()
}
// Close waits for the command to exit.
func (c *command) Close() error {
if c.closed {
return nil
}
defer func() {
c.closed = true
_ = c.stderrCloser.Close()
}()
err := c.cmd.Wait()
if _, ok := err.(*os.PathError); ok {
return nil
}
// When a repository does not exist, the command exits with code 128.
if _, ok := err.(*exec.ExitError); ok {
return nil
}
return err
}

View File

@ -0,0 +1,53 @@
package file
import (
"fmt"
"os"
"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/plumbing/transport/server"
"gopkg.in/src-d/go-git.v4/utils/ioutil"
)
// ServeUploadPack serves a git-upload-pack request using standard output, input
// and error. This is meant to be used when implementing a git-upload-pack
// command.
func ServeUploadPack(path string) error {
ep, err := transport.NewEndpoint(path)
if err != nil {
return err
}
// TODO: define and implement a server-side AuthMethod
s, err := server.DefaultServer.NewUploadPackSession(ep, nil)
if err != nil {
return fmt.Errorf("error creating session: %s", err)
}
return common.ServeUploadPack(srvCmd, s)
}
// ServeReceivePack serves a git-receive-pack request using standard output,
// input and error. This is meant to be used when implementing a
// git-receive-pack command.
func ServeReceivePack(path string) error {
ep, err := transport.NewEndpoint(path)
if err != nil {
return err
}
// TODO: define and implement a server-side AuthMethod
s, err := server.DefaultServer.NewReceivePackSession(ep, nil)
if err != nil {
return fmt.Errorf("error creating session: %s", err)
}
return common.ServeReceivePack(srvCmd, s)
}
var srvCmd = common.ServerCommand{
Stdin: os.Stdin,
Stdout: ioutil.WriteNopCloser(os.Stdout),
Stderr: os.Stderr,
}

View File

@ -0,0 +1,109 @@
// Package git implements the git transport protocol.
package git
import (
"fmt"
"io"
"net"
"gopkg.in/src-d/go-git.v4/plumbing/format/pktline"
"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"
)
// DefaultClient is the default git client.
var DefaultClient = common.NewClient(&runner{})
const DefaultPort = 9418
type runner struct{}
// Command returns a new Command for the given cmd in the given Endpoint
func (r *runner) Command(cmd string, ep *transport.Endpoint, auth transport.AuthMethod) (common.Command, error) {
// auth not allowed since git protocol doesn't support authentication
if auth != nil {
return nil, transport.ErrInvalidAuthMethod
}
c := &command{command: cmd, endpoint: ep}
if err := c.connect(); err != nil {
return nil, err
}
return c, nil
}
type command struct {
conn net.Conn
connected bool
command string
endpoint *transport.Endpoint
}
// Start executes the command sending the required message to the TCP connection
func (c *command) Start() error {
cmd := endpointToCommand(c.command, c.endpoint)
e := pktline.NewEncoder(c.conn)
return e.Encode([]byte(cmd))
}
func (c *command) connect() error {
if c.connected {
return transport.ErrAlreadyConnected
}
var err error
c.conn, err = net.Dial("tcp", c.getHostWithPort())
if err != nil {
return err
}
c.connected = true
return nil
}
func (c *command) getHostWithPort() string {
host := c.endpoint.Host
port := c.endpoint.Port
if port <= 0 {
port = DefaultPort
}
return fmt.Sprintf("%s:%d", host, port)
}
// StderrPipe git protocol doesn't have any dedicated error channel
func (c *command) StderrPipe() (io.Reader, error) {
return nil, nil
}
// StdinPipe return the underlying connection as WriteCloser, wrapped to prevent
// call to the Close function from the connection, a command execution in git
// protocol can't be closed or killed
func (c *command) StdinPipe() (io.WriteCloser, error) {
return ioutil.WriteNopCloser(c.conn), nil
}
// StdoutPipe return the underlying connection as Reader
func (c *command) StdoutPipe() (io.Reader, error) {
return c.conn, nil
}
func endpointToCommand(cmd string, ep *transport.Endpoint) string {
host := ep.Host
if ep.Port != DefaultPort {
host = fmt.Sprintf("%s:%d", ep.Host, ep.Port)
}
return fmt.Sprintf("%s %s%chost=%s%c", cmd, ep.Path, 0, host, 0)
}
// Close closes the TCP connection and connection.
func (c *command) Close() error {
if !c.connected {
return nil
}
c.connected = false
return c.conn.Close()
}

View File

@ -0,0 +1,281 @@
// Package http implements the HTTP transport protocol.
package http
import (
"bytes"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"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/utils/ioutil"
)
// it requires a bytes.Buffer, because we need to know the length
func applyHeadersToRequest(req *http.Request, content *bytes.Buffer, host string, requestType string) {
req.Header.Add("User-Agent", "git/1.0")
req.Header.Add("Host", host) // host:port
if content == nil {
req.Header.Add("Accept", "*/*")
return
}
req.Header.Add("Accept", fmt.Sprintf("application/x-%s-result", requestType))
req.Header.Add("Content-Type", fmt.Sprintf("application/x-%s-request", requestType))
req.Header.Add("Content-Length", strconv.Itoa(content.Len()))
}
const infoRefsPath = "/info/refs"
func advertisedReferences(s *session, serviceName string) (ref *packp.AdvRefs, err error) {
url := fmt.Sprintf(
"%s%s?service=%s",
s.endpoint.String(), infoRefsPath, serviceName,
)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
s.ApplyAuthToRequest(req)
applyHeadersToRequest(req, nil, s.endpoint.Host, serviceName)
res, err := s.client.Do(req)
if err != nil {
return nil, err
}
s.ModifyEndpointIfRedirect(res)
defer ioutil.CheckClose(res.Body, &err)
if err = NewErr(res); err != nil {
return nil, err
}
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
}
type client struct {
c *http.Client
}
// DefaultClient is the default HTTP client, which uses `http.DefaultClient`.
var DefaultClient = NewClient(nil)
// NewClient creates a new client with a custom net/http client.
// See `InstallProtocol` to install and override default http client.
// Unless a properly initialized client is given, it will fall back into
// `http.DefaultClient`.
//
// 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.Transport {
if c == nil {
return &client{http.DefaultClient}
}
return &client{
c: c,
}
}
func (c *client) NewUploadPackSession(ep *transport.Endpoint, auth transport.AuthMethod) (
transport.UploadPackSession, error) {
return newUploadPackSession(c.c, ep, auth)
}
func (c *client) NewReceivePackSession(ep *transport.Endpoint, auth transport.AuthMethod) (
transport.ReceivePackSession, error) {
return newReceivePackSession(c.c, ep, auth)
}
type session struct {
auth AuthMethod
client *http.Client
endpoint *transport.Endpoint
advRefs *packp.AdvRefs
}
func newSession(c *http.Client, ep *transport.Endpoint, auth transport.AuthMethod) (*session, error) {
s := &session{
auth: basicAuthFromEndpoint(ep),
client: c,
endpoint: ep,
}
if auth != nil {
a, ok := auth.(AuthMethod)
if !ok {
return nil, transport.ErrInvalidAuthMethod
}
s.auth = a
}
return s, nil
}
func (s *session) ApplyAuthToRequest(req *http.Request) {
if s.auth == nil {
return
}
s.auth.setAuth(req)
}
func (s *session) ModifyEndpointIfRedirect(res *http.Response) {
if res.Request == nil {
return
}
r := res.Request
if !strings.HasSuffix(r.URL.Path, infoRefsPath) {
return
}
h, p, err := net.SplitHostPort(r.URL.Host)
if err != nil {
h = r.URL.Host
}
if p != "" {
port, err := strconv.Atoi(p)
if err == nil {
s.endpoint.Port = port
}
}
s.endpoint.Host = h
s.endpoint.Protocol = r.URL.Scheme
s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)]
}
func (*session) Close() error {
return nil
}
// AuthMethod is concrete implementation of common.AuthMethod for HTTP services
type AuthMethod interface {
transport.AuthMethod
setAuth(r *http.Request)
}
func basicAuthFromEndpoint(ep *transport.Endpoint) *BasicAuth {
u := ep.User
if u == "" {
return nil
}
return &BasicAuth{u, ep.Password}
}
// BasicAuth represent a HTTP basic auth
type BasicAuth struct {
Username, Password string
}
func (a *BasicAuth) setAuth(r *http.Request) {
if a == nil {
return
}
r.SetBasicAuth(a.Username, a.Password)
}
// Name is name of the auth
func (a *BasicAuth) Name() string {
return "http-basic-auth"
}
func (a *BasicAuth) String() string {
masked := "*******"
if a.Password == "" {
masked = "<empty>"
}
return fmt.Sprintf("%s - %s:%s", a.Name(), a.Username, masked)
}
// TokenAuth implements an http.AuthMethod that can be used with http transport
// to authenticate with HTTP token authentication (also known as bearer
// authentication).
//
// IMPORTANT: If you are looking to use OAuth tokens with popular servers (e.g.
// GitHub, Bitbucket, GitLab) you should use BasicAuth instead. These servers
// use basic HTTP authentication, with the OAuth token as user or password.
// Check the documentation of your git server for details.
type TokenAuth struct {
Token string
}
func (a *TokenAuth) setAuth(r *http.Request) {
if a == nil {
return
}
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", a.Token))
}
// Name is name of the auth
func (a *TokenAuth) Name() string {
return "http-token-auth"
}
func (a *TokenAuth) String() string {
masked := "*******"
if a.Token == "" {
masked = "<empty>"
}
return fmt.Sprintf("%s - %s", a.Name(), masked)
}
// Err is a dedicated error to return errors based on status code
type Err struct {
Response *http.Response
}
// NewErr returns a new Err based on a http response
func NewErr(r *http.Response) error {
if r.StatusCode >= http.StatusOK && r.StatusCode < http.StatusMultipleChoices {
return nil
}
switch r.StatusCode {
case http.StatusUnauthorized:
return transport.ErrAuthenticationRequired
case http.StatusForbidden:
return transport.ErrAuthorizationFailed
case http.StatusNotFound:
return transport.ErrRepositoryNotFound
}
return plumbing.NewUnexpectedError(&Err{r})
}
// StatusCode returns the status code of the response
func (e *Err) StatusCode() int {
return e.Response.StatusCode
}
func (e *Err) Error() string {
return fmt.Sprintf("unexpected requesting %q status code: %d",
e.Response.Request.URL, e.Response.StatusCode,
)
}

View File

@ -0,0 +1,106 @@
package http
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"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/protocol/packp/capability"
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/sideband"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/utils/ioutil"
)
type rpSession struct {
*session
}
func newReceivePackSession(c *http.Client, ep *transport.Endpoint, auth transport.AuthMethod) (transport.ReceivePackSession, error) {
s, err := newSession(c, ep, auth)
return &rpSession{s}, err
}
func (s *rpSession) AdvertisedReferences() (*packp.AdvRefs, error) {
return advertisedReferences(s.session, transport.ReceivePackServiceName)
}
func (s *rpSession) ReceivePack(ctx context.Context, req *packp.ReferenceUpdateRequest) (
*packp.ReportStatus, error) {
url := fmt.Sprintf(
"%s/%s",
s.endpoint.String(), transport.ReceivePackServiceName,
)
buf := bytes.NewBuffer(nil)
if err := req.Encode(buf); err != nil {
return nil, err
}
res, err := s.doRequest(ctx, http.MethodPost, url, buf)
if err != nil {
return nil, err
}
r, err := ioutil.NonEmptyReader(res.Body)
if err == ioutil.ErrEmptyReader {
return nil, nil
}
if err != nil {
return nil, err
}
var d *sideband.Demuxer
if req.Capabilities.Supports(capability.Sideband64k) {
d = sideband.NewDemuxer(sideband.Sideband64k, r)
} else if req.Capabilities.Supports(capability.Sideband) {
d = sideband.NewDemuxer(sideband.Sideband, r)
}
if d != nil {
d.Progress = req.Progress
r = d
}
rc := ioutil.NewReadCloser(r, res.Body)
report := packp.NewReportStatus()
if err := report.Decode(rc); err != nil {
return nil, err
}
return report, report.Error()
}
func (s *rpSession) doRequest(
ctx context.Context, 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)
}
applyHeadersToRequest(req, content, s.endpoint.Host, transport.ReceivePackServiceName)
s.ApplyAuthToRequest(req)
res, err := s.client.Do(req.WithContext(ctx))
if err != nil {
return nil, plumbing.NewUnexpectedError(err)
}
if err := NewErr(res); err != nil {
_ = res.Body.Close()
return nil, err
}
return res, nil
}

View File

@ -0,0 +1,123 @@
package http
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"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, auth transport.AuthMethod) (transport.UploadPackSession, error) {
s, err := newSession(c, ep, auth)
return &upSession{s}, err
}
func (s *upSession) AdvertisedReferences() (*packp.AdvRefs, error) {
return advertisedReferences(s.session, transport.UploadPackServiceName)
}
func (s *upSession) UploadPack(
ctx context.Context, 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(ctx, 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(
ctx context.Context, 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)
}
applyHeadersToRequest(req, content, s.endpoint.Host, transport.UploadPackServiceName)
s.ApplyAuthToRequest(req)
res, err := s.client.Do(req.WithContext(ctx))
if err != nil {
return nil, plumbing.NewUnexpectedError(err)
}
if err := NewErr(res); err != nil {
_ = res.Body.Close()
return nil, err
}
return res, nil
}
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, false); err != nil {
return nil, fmt.Errorf("sending haves message: %s", err)
}
if err := e.EncodeString("done\n"); err != nil {
return nil, err
}
return buf, nil
}

View File

@ -0,0 +1,467 @@
// Package common implements the git pack protocol with a pluggable transport.
// This is a low-level package to implement new transports. Use a concrete
// implementation instead (e.g. http, file, ssh).
//
// A simple example of usage can be found in the file package.
package common
import (
"bufio"
"context"
"errors"
"fmt"
"io"
stdioutil "io/ioutil"
"strings"
"time"
"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/protocol/packp/capability"
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/sideband"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/utils/ioutil"
)
const (
readErrorSecondsTimeout = 10
)
var (
ErrTimeoutExceeded = errors.New("timeout exceeded")
)
// Commander creates Command instances. This is the main entry point for
// transport implementations.
type Commander interface {
// Command creates a new Command for the given git command and
// endpoint. cmd can be git-upload-pack or git-receive-pack. An
// error should be returned if the endpoint is not supported or the
// command cannot be created (e.g. binary does not exist, connection
// cannot be established).
Command(cmd string, ep *transport.Endpoint, auth transport.AuthMethod) (Command, error)
}
// Command is used for a single command execution.
// This interface is modeled after exec.Cmd and ssh.Session in the standard
// library.
type Command interface {
// StderrPipe returns a pipe that will be connected to the command's
// standard error when the command starts. It should not be called after
// Start.
StderrPipe() (io.Reader, error)
// StdinPipe returns a pipe that will be connected to the command's
// standard input when the command starts. It should not be called after
// Start. The pipe should be closed when no more input is expected.
StdinPipe() (io.WriteCloser, error)
// StdoutPipe returns a pipe that will be connected to the command's
// standard output when the command starts. It should not be called after
// Start.
StdoutPipe() (io.Reader, error)
// Start starts the specified command. It does not wait for it to
// complete.
Start() error
// Close closes the command and releases any resources used by it. It
// will block until the command exits.
Close() error
}
// CommandKiller expands the Command interface, enableing it for being killed.
type CommandKiller interface {
// Kill and close the session whatever the state it is. It will block until
// the command is terminated.
Kill() error
}
type client struct {
cmdr Commander
}
// NewClient creates a new client using the given Commander.
func NewClient(runner Commander) transport.Transport {
return &client{runner}
}
// NewUploadPackSession creates a new UploadPackSession.
func (c *client) NewUploadPackSession(ep *transport.Endpoint, auth transport.AuthMethod) (
transport.UploadPackSession, error) {
return c.newSession(transport.UploadPackServiceName, ep, auth)
}
// NewReceivePackSession creates a new ReceivePackSession.
func (c *client) NewReceivePackSession(ep *transport.Endpoint, auth transport.AuthMethod) (
transport.ReceivePackSession, error) {
return c.newSession(transport.ReceivePackServiceName, ep, auth)
}
type session struct {
Stdin io.WriteCloser
Stdout io.Reader
Command Command
isReceivePack bool
advRefs *packp.AdvRefs
packRun bool
finished bool
firstErrLine chan string
}
func (c *client) newSession(s string, ep *transport.Endpoint, auth transport.AuthMethod) (*session, error) {
cmd, err := c.cmdr.Command(s, ep, auth)
if err != nil {
return nil, err
}
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
return nil, err
}
return &session{
Stdin: stdin,
Stdout: stdout,
Command: cmd,
firstErrLine: c.listenFirstError(stderr),
isReceivePack: s == transport.ReceivePackServiceName,
}, nil
}
func (c *client) listenFirstError(r io.Reader) chan string {
if r == nil {
return nil
}
errLine := make(chan string, 1)
go func() {
s := bufio.NewScanner(r)
if s.Scan() {
errLine <- s.Text()
} else {
close(errLine)
}
_, _ = io.Copy(stdioutil.Discard, r)
}()
return errLine
}
// AdvertisedReferences retrieves the advertised references from the server.
func (s *session) AdvertisedReferences() (*packp.AdvRefs, error) {
if s.advRefs != nil {
return s.advRefs, nil
}
ar := packp.NewAdvRefs()
if err := ar.Decode(s.Stdout); err != nil {
if err := s.handleAdvRefDecodeError(err); err != nil {
return nil, err
}
}
transport.FilterUnsupportedCapabilities(ar.Capabilities)
s.advRefs = ar
return ar, nil
}
func (s *session) handleAdvRefDecodeError(err error) error {
// If repository is not found, we get empty stdout and server writes an
// error to stderr.
if err == packp.ErrEmptyInput {
s.finished = true
if err := s.checkNotFoundError(); err != nil {
return err
}
return io.ErrUnexpectedEOF
}
// For empty (but existing) repositories, we get empty advertised-references
// message. But valid. That is, it includes at least a flush.
if err == packp.ErrEmptyAdvRefs {
// Empty repositories are valid for git-receive-pack.
if s.isReceivePack {
return nil
}
if err := s.finish(); err != nil {
return err
}
return transport.ErrEmptyRemoteRepository
}
// Some server sends the errors as normal content (git protocol), so when
// we try to decode it fails, we need to check the content of it, to detect
// not found errors
if uerr, ok := err.(*packp.ErrUnexpectedData); ok {
if isRepoNotFoundError(string(uerr.Data)) {
return transport.ErrRepositoryNotFound
}
}
return err
}
// UploadPack performs a request to the server to fetch a packfile. A reader is
// returned with the packfile content. The reader must be closed after reading.
func (s *session) UploadPack(ctx context.Context, req *packp.UploadPackRequest) (*packp.UploadPackResponse, error) {
if req.IsEmpty() {
return nil, transport.ErrEmptyUploadPackRequest
}
if err := req.Validate(); err != nil {
return nil, err
}
if _, err := s.AdvertisedReferences(); err != nil {
return nil, err
}
s.packRun = true
in := s.StdinContext(ctx)
out := s.StdoutContext(ctx)
if err := uploadPack(in, out, req); err != nil {
return nil, err
}
r, err := ioutil.NonEmptyReader(out)
if err == ioutil.ErrEmptyReader {
if c, ok := s.Stdout.(io.Closer); ok {
_ = c.Close()
}
return nil, transport.ErrEmptyUploadPackRequest
}
if err != nil {
return nil, err
}
rc := ioutil.NewReadCloser(r, s)
return DecodeUploadPackResponse(rc, req)
}
func (s *session) StdinContext(ctx context.Context) io.WriteCloser {
return ioutil.NewWriteCloserOnError(
ioutil.NewContextWriteCloser(ctx, s.Stdin),
s.onError,
)
}
func (s *session) StdoutContext(ctx context.Context) io.Reader {
return ioutil.NewReaderOnError(
ioutil.NewContextReader(ctx, s.Stdout),
s.onError,
)
}
func (s *session) onError(err error) {
if k, ok := s.Command.(CommandKiller); ok {
_ = k.Kill()
}
_ = s.Close()
}
func (s *session) ReceivePack(ctx context.Context, req *packp.ReferenceUpdateRequest) (*packp.ReportStatus, error) {
if _, err := s.AdvertisedReferences(); err != nil {
return nil, err
}
s.packRun = true
w := s.StdinContext(ctx)
if err := req.Encode(w); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
if !req.Capabilities.Supports(capability.ReportStatus) {
// If we don't have report-status, we can only
// check return value error.
return nil, s.Command.Close()
}
r := s.StdoutContext(ctx)
var d *sideband.Demuxer
if req.Capabilities.Supports(capability.Sideband64k) {
d = sideband.NewDemuxer(sideband.Sideband64k, r)
} else if req.Capabilities.Supports(capability.Sideband) {
d = sideband.NewDemuxer(sideband.Sideband, r)
}
if d != nil {
d.Progress = req.Progress
r = d
}
report := packp.NewReportStatus()
if err := report.Decode(r); err != nil {
return nil, err
}
if err := report.Error(); err != nil {
defer s.Close()
return report, err
}
return report, s.Command.Close()
}
func (s *session) finish() error {
if s.finished {
return nil
}
s.finished = true
// If we did not run a upload/receive-pack, we close the connection
// gracefully by sending a flush packet to the server. If the server
// operates correctly, it will exit with status 0.
if !s.packRun {
_, err := s.Stdin.Write(pktline.FlushPkt)
return err
}
return nil
}
func (s *session) Close() (err error) {
err = s.finish()
defer ioutil.CheckClose(s.Command, &err)
return
}
func (s *session) checkNotFoundError() error {
t := time.NewTicker(time.Second * readErrorSecondsTimeout)
defer t.Stop()
select {
case <-t.C:
return ErrTimeoutExceeded
case line, ok := <-s.firstErrLine:
if !ok {
return nil
}
if isRepoNotFoundError(line) {
return transport.ErrRepositoryNotFound
}
return fmt.Errorf("unknown error: %s", line)
}
}
var (
githubRepoNotFoundErr = "ERROR: Repository not found."
bitbucketRepoNotFoundErr = "conq: repository does not exist."
localRepoNotFoundErr = "does not appear to be a git repository"
gitProtocolNotFoundErr = "ERR \n Repository not found."
gitProtocolNoSuchErr = "ERR no such repository"
gitProtocolAccessDeniedErr = "ERR access denied"
gogsAccessDeniedErr = "Gogs: Repository does not exist or you do not have access"
)
func isRepoNotFoundError(s string) bool {
if strings.HasPrefix(s, githubRepoNotFoundErr) {
return true
}
if strings.HasPrefix(s, bitbucketRepoNotFoundErr) {
return true
}
if strings.HasSuffix(s, localRepoNotFoundErr) {
return true
}
if strings.HasPrefix(s, gitProtocolNotFoundErr) {
return true
}
if strings.HasPrefix(s, gitProtocolNoSuchErr) {
return true
}
if strings.HasPrefix(s, gitProtocolAccessDeniedErr) {
return true
}
if strings.HasPrefix(s, gogsAccessDeniedErr) {
return true
}
return false
}
var (
nak = []byte("NAK")
eol = []byte("\n")
)
// uploadPack implements the git-upload-pack protocol.
func uploadPack(w io.WriteCloser, r io.Reader, req *packp.UploadPackRequest) error {
// 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
if err := req.UploadRequest.Encode(w); err != nil {
return fmt.Errorf("sending upload-req message: %s", err)
}
if err := req.UploadHaves.Encode(w, true); 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)
}
return nil
}
func sendDone(w io.Writer) error {
e := pktline.NewEncoder(w)
return e.Encodef("done\n")
}
// DecodeUploadPackResponse decodes r into a new packp.UploadPackResponse
func DecodeUploadPackResponse(r io.ReadCloser, req *packp.UploadPackRequest) (
*packp.UploadPackResponse, error,
) {
res := packp.NewUploadPackResponse(req)
if err := res.Decode(r); err != nil {
return nil, fmt.Errorf("error decoding upload-pack response: %s", err)
}
return res, nil
}

View File

@ -0,0 +1,73 @@
package common
import (
"context"
"fmt"
"io"
"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/utils/ioutil"
)
// ServerCommand is used for a single server command execution.
type ServerCommand struct {
Stderr io.Writer
Stdout io.WriteCloser
Stdin io.Reader
}
func ServeUploadPack(cmd ServerCommand, s transport.UploadPackSession) (err error) {
ioutil.CheckClose(cmd.Stdout, &err)
ar, err := s.AdvertisedReferences()
if err != nil {
return err
}
if err := ar.Encode(cmd.Stdout); err != nil {
return err
}
req := packp.NewUploadPackRequest()
if err := req.Decode(cmd.Stdin); err != nil {
return err
}
var resp *packp.UploadPackResponse
resp, err = s.UploadPack(context.TODO(), req)
if err != nil {
return err
}
return resp.Encode(cmd.Stdout)
}
func ServeReceivePack(cmd ServerCommand, s transport.ReceivePackSession) error {
ar, err := s.AdvertisedReferences()
if err != nil {
return fmt.Errorf("internal error in advertised references: %s", err)
}
if err := ar.Encode(cmd.Stdout); err != nil {
return fmt.Errorf("error in advertised references encoding: %s", err)
}
req := packp.NewReferenceUpdateRequest()
if err := req.Decode(cmd.Stdin); err != nil {
return fmt.Errorf("error decoding: %s", err)
}
rs, err := s.ReceivePack(context.TODO(), req)
if rs != nil {
if err := rs.Encode(cmd.Stdout); err != nil {
return fmt.Errorf("error in encoding report status %s", err)
}
}
if err != nil {
return fmt.Errorf("error in receive pack: %s", err)
}
return nil
}

View File

@ -0,0 +1,64 @@
package server
import (
"gopkg.in/src-d/go-git.v4/plumbing/cache"
"gopkg.in/src-d/go-git.v4/plumbing/storer"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/storage/filesystem"
"gopkg.in/src-d/go-billy.v4"
"gopkg.in/src-d/go-billy.v4/osfs"
)
// DefaultLoader is a filesystem loader ignoring host and resolving paths to /.
var DefaultLoader = NewFilesystemLoader(osfs.New(""))
// Loader loads repository's storer.Storer based on an optional host and a path.
type Loader interface {
// Load loads a storer.Storer given a transport.Endpoint.
// Returns transport.ErrRepositoryNotFound if the repository does not
// exist.
Load(ep *transport.Endpoint) (storer.Storer, error)
}
type fsLoader struct {
base billy.Filesystem
}
// NewFilesystemLoader creates a Loader that ignores host and resolves paths
// with a given base filesystem.
func NewFilesystemLoader(base billy.Filesystem) Loader {
return &fsLoader{base}
}
// Load looks up the endpoint's path in the base file system and returns a
// storer for it. Returns transport.ErrRepositoryNotFound if a repository does
// not exist in the given path.
func (l *fsLoader) Load(ep *transport.Endpoint) (storer.Storer, error) {
fs, err := l.base.Chroot(ep.Path)
if err != nil {
return nil, err
}
if _, err := fs.Stat("config"); err != nil {
return nil, transport.ErrRepositoryNotFound
}
return filesystem.NewStorage(fs, cache.NewObjectLRUDefault()), nil
}
// MapLoader is a Loader that uses a lookup map of storer.Storer by
// transport.Endpoint.
type MapLoader map[string]storer.Storer
// Load returns a storer.Storer for given a transport.Endpoint by looking it up
// in the map. Returns transport.ErrRepositoryNotFound if the endpoint does not
// exist.
func (l MapLoader) Load(ep *transport.Endpoint) (storer.Storer, error) {
s, ok := l[ep.String()]
if !ok {
return nil, transport.ErrRepositoryNotFound
}
return s, nil
}

View File

@ -0,0 +1,427 @@
// Package server implements the git server protocol. For most use cases, the
// transport-specific implementations should be used.
package server
import (
"context"
"errors"
"fmt"
"io"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/format/packfile"
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp"
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability"
"gopkg.in/src-d/go-git.v4/plumbing/revlist"
"gopkg.in/src-d/go-git.v4/plumbing/storer"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/utils/ioutil"
)
var DefaultServer = NewServer(DefaultLoader)
type server struct {
loader Loader
handler *handler
}
// NewServer returns a transport.Transport implementing a git server,
// independent of transport. Each transport must wrap this.
func NewServer(loader Loader) transport.Transport {
return &server{
loader,
&handler{asClient: false},
}
}
// NewClient returns a transport.Transport implementing a client with an
// embedded server.
func NewClient(loader Loader) transport.Transport {
return &server{
loader,
&handler{asClient: true},
}
}
func (s *server) NewUploadPackSession(ep *transport.Endpoint, auth transport.AuthMethod) (transport.UploadPackSession, error) {
sto, err := s.loader.Load(ep)
if err != nil {
return nil, err
}
return s.handler.NewUploadPackSession(sto)
}
func (s *server) NewReceivePackSession(ep *transport.Endpoint, auth transport.AuthMethod) (transport.ReceivePackSession, error) {
sto, err := s.loader.Load(ep)
if err != nil {
return nil, err
}
return s.handler.NewReceivePackSession(sto)
}
type handler struct {
asClient bool
}
func (h *handler) NewUploadPackSession(s storer.Storer) (transport.UploadPackSession, error) {
return &upSession{
session: session{storer: s, asClient: h.asClient},
}, nil
}
func (h *handler) NewReceivePackSession(s storer.Storer) (transport.ReceivePackSession, error) {
return &rpSession{
session: session{storer: s, asClient: h.asClient},
cmdStatus: map[plumbing.ReferenceName]error{},
}, nil
}
type session struct {
storer storer.Storer
caps *capability.List
asClient bool
}
func (s *session) Close() error {
return nil
}
func (s *session) SetAuth(transport.AuthMethod) error {
//TODO: deprecate
return nil
}
func (s *session) checkSupportedCapabilities(cl *capability.List) error {
for _, c := range cl.All() {
if !s.caps.Supports(c) {
return fmt.Errorf("unsupported capability: %s", c)
}
}
return nil
}
type upSession struct {
session
}
func (s *upSession) AdvertisedReferences() (*packp.AdvRefs, error) {
ar := packp.NewAdvRefs()
if err := s.setSupportedCapabilities(ar.Capabilities); err != nil {
return nil, err
}
s.caps = ar.Capabilities
if err := setReferences(s.storer, ar); err != nil {
return nil, err
}
if err := setHEAD(s.storer, ar); err != nil {
return nil, err
}
if s.asClient && len(ar.References) == 0 {
return nil, transport.ErrEmptyRemoteRepository
}
return ar, nil
}
func (s *upSession) UploadPack(ctx context.Context, req *packp.UploadPackRequest) (*packp.UploadPackResponse, error) {
if req.IsEmpty() {
return nil, transport.ErrEmptyUploadPackRequest
}
if err := req.Validate(); err != nil {
return nil, err
}
if s.caps == nil {
s.caps = capability.NewList()
if err := s.setSupportedCapabilities(s.caps); err != nil {
return nil, err
}
}
if err := s.checkSupportedCapabilities(req.Capabilities); err != nil {
return nil, err
}
s.caps = req.Capabilities
if len(req.Shallows) > 0 {
return nil, fmt.Errorf("shallow not supported")
}
objs, err := s.objectsToUpload(req)
if err != nil {
return nil, err
}
pr, pw := io.Pipe()
e := packfile.NewEncoder(pw, s.storer, false)
go func() {
// TODO: plumb through a pack window.
_, err := e.Encode(objs, 10)
pw.CloseWithError(err)
}()
return packp.NewUploadPackResponseWithPackfile(req,
ioutil.NewContextReadCloser(ctx, pr),
), nil
}
func (s *upSession) objectsToUpload(req *packp.UploadPackRequest) ([]plumbing.Hash, error) {
haves, err := revlist.Objects(s.storer, req.Haves, nil)
if err != nil {
return nil, err
}
return revlist.Objects(s.storer, req.Wants, haves)
}
func (*upSession) setSupportedCapabilities(c *capability.List) error {
if err := c.Set(capability.Agent, capability.DefaultAgent); err != nil {
return err
}
if err := c.Set(capability.OFSDelta); err != nil {
return err
}
return nil
}
type rpSession struct {
session
cmdStatus map[plumbing.ReferenceName]error
firstErr error
unpackErr error
}
func (s *rpSession) AdvertisedReferences() (*packp.AdvRefs, error) {
ar := packp.NewAdvRefs()
if err := s.setSupportedCapabilities(ar.Capabilities); err != nil {
return nil, err
}
s.caps = ar.Capabilities
if err := setReferences(s.storer, ar); err != nil {
return nil, err
}
if err := setHEAD(s.storer, ar); err != nil {
return nil, err
}
return ar, nil
}
var (
ErrUpdateReference = errors.New("failed to update ref")
)
func (s *rpSession) ReceivePack(ctx context.Context, req *packp.ReferenceUpdateRequest) (*packp.ReportStatus, error) {
if s.caps == nil {
s.caps = capability.NewList()
if err := s.setSupportedCapabilities(s.caps); err != nil {
return nil, err
}
}
if err := s.checkSupportedCapabilities(req.Capabilities); err != nil {
return nil, err
}
s.caps = req.Capabilities
//TODO: Implement 'atomic' update of references.
r := ioutil.NewContextReadCloser(ctx, req.Packfile)
if err := s.writePackfile(r); err != nil {
s.unpackErr = err
s.firstErr = err
return s.reportStatus(), err
}
s.updateReferences(req)
return s.reportStatus(), s.firstErr
}
func (s *rpSession) updateReferences(req *packp.ReferenceUpdateRequest) {
for _, cmd := range req.Commands {
exists, err := referenceExists(s.storer, cmd.Name)
if err != nil {
s.setStatus(cmd.Name, err)
continue
}
switch cmd.Action() {
case packp.Create:
if exists {
s.setStatus(cmd.Name, ErrUpdateReference)
continue
}
ref := plumbing.NewHashReference(cmd.Name, cmd.New)
err := s.storer.SetReference(ref)
s.setStatus(cmd.Name, err)
case packp.Delete:
if !exists {
s.setStatus(cmd.Name, ErrUpdateReference)
continue
}
err := s.storer.RemoveReference(cmd.Name)
s.setStatus(cmd.Name, err)
case packp.Update:
if !exists {
s.setStatus(cmd.Name, ErrUpdateReference)
continue
}
if err != nil {
s.setStatus(cmd.Name, err)
continue
}
ref := plumbing.NewHashReference(cmd.Name, cmd.New)
err := s.storer.SetReference(ref)
s.setStatus(cmd.Name, err)
}
}
}
func (s *rpSession) writePackfile(r io.ReadCloser) error {
if r == nil {
return nil
}
if err := packfile.UpdateObjectStorage(s.storer, r); err != nil {
_ = r.Close()
return err
}
return r.Close()
}
func (s *rpSession) setStatus(ref plumbing.ReferenceName, err error) {
s.cmdStatus[ref] = err
if s.firstErr == nil && err != nil {
s.firstErr = err
}
}
func (s *rpSession) reportStatus() *packp.ReportStatus {
if !s.caps.Supports(capability.ReportStatus) {
return nil
}
rs := packp.NewReportStatus()
rs.UnpackStatus = "ok"
if s.unpackErr != nil {
rs.UnpackStatus = s.unpackErr.Error()
}
if s.cmdStatus == nil {
return rs
}
for ref, err := range s.cmdStatus {
msg := "ok"
if err != nil {
msg = err.Error()
}
status := &packp.CommandStatus{
ReferenceName: ref,
Status: msg,
}
rs.CommandStatuses = append(rs.CommandStatuses, status)
}
return rs
}
func (*rpSession) setSupportedCapabilities(c *capability.List) error {
if err := c.Set(capability.Agent, capability.DefaultAgent); err != nil {
return err
}
if err := c.Set(capability.OFSDelta); err != nil {
return err
}
if err := c.Set(capability.DeleteRefs); err != nil {
return err
}
return c.Set(capability.ReportStatus)
}
func setHEAD(s storer.Storer, ar *packp.AdvRefs) error {
ref, err := s.Reference(plumbing.HEAD)
if err == plumbing.ErrReferenceNotFound {
return nil
}
if err != nil {
return err
}
if ref.Type() == plumbing.SymbolicReference {
if err := ar.AddReference(ref); err != nil {
return nil
}
ref, err = storer.ResolveReference(s, ref.Target())
if err == plumbing.ErrReferenceNotFound {
return nil
}
if err != nil {
return err
}
}
if ref.Type() != plumbing.HashReference {
return plumbing.ErrInvalidType
}
h := ref.Hash()
ar.Head = &h
return nil
}
func setReferences(s storer.Storer, ar *packp.AdvRefs) error {
//TODO: add peeled references.
iter, err := s.IterReferences()
if err != nil {
return err
}
return iter.ForEach(func(ref *plumbing.Reference) error {
if ref.Type() != plumbing.HashReference {
return nil
}
ar.References[ref.Name().String()] = ref.Hash()
return nil
})
}
func referenceExists(s storer.ReferenceStorer, n plumbing.ReferenceName) (bool, error) {
_, err := s.Reference(n)
if err == plumbing.ErrReferenceNotFound {
return false, nil
}
return err == nil, err
}

View File

@ -0,0 +1,322 @@
package ssh
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"github.com/mitchellh/go-homedir"
"github.com/xanzy/ssh-agent"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
const DefaultUsername = "git"
// 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 {
transport.AuthMethod
// ClientConfig should return a valid ssh.ClientConfig to be used to create
// a connection to the SSH server.
ClientConfig() (*ssh.ClientConfig, error)
}
// 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
HostKeyCallbackHelper
}
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, error) {
return a.SetHostKeyCallback(&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
Password string
HostKeyCallbackHelper
}
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, error) {
return a.SetHostKeyCallback(&ssh.ClientConfig{
User: a.User,
Auth: []ssh.AuthMethod{ssh.Password(a.Password)},
})
}
// PasswordCallback implements AuthMethod by using a callback
// to fetch the password.
type PasswordCallback struct {
User string
Callback func() (pass string, err error)
HostKeyCallbackHelper
}
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, error) {
return a.SetHostKeyCallback(&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
HostKeyCallbackHelper
}
// NewPublicKeys returns a PublicKeys from a PEM encoded private key. An
// encryption password should be given if the pemBytes contains a password
// encrypted PEM block otherwise password should be empty. It supports RSA
// (PKCS#1), DSA (OpenSSL), and ECDSA private keys.
func NewPublicKeys(user string, pemBytes []byte, password string) (*PublicKeys, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("invalid PEM data")
}
if x509.IsEncryptedPEMBlock(block) {
key, err := x509.DecryptPEMBlock(block, []byte(password))
if err != nil {
return nil, err
}
block = &pem.Block{Type: block.Type, Bytes: key}
pemBytes = pem.EncodeToMemory(block)
}
signer, err := ssh.ParsePrivateKey(pemBytes)
if err != nil {
return nil, err
}
return &PublicKeys{User: user, Signer: signer}, nil
}
// NewPublicKeysFromFile returns a PublicKeys from a file containing a PEM
// encoded private key. An encryption password should be given if the pemBytes
// contains a password encrypted PEM block otherwise password should be empty.
func NewPublicKeysFromFile(user, pemFile, password string) (*PublicKeys, error) {
bytes, err := ioutil.ReadFile(pemFile)
if err != nil {
return nil, err
}
return NewPublicKeys(user, bytes, password)
}
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, error) {
return a.SetHostKeyCallback(&ssh.ClientConfig{
User: a.User,
Auth: []ssh.AuthMethod{ssh.PublicKeys(a.Signer)},
})
}
func username() (string, error) {
var username string
if user, err := user.Current(); err == nil {
username = user.Username
} else {
username = os.Getenv("USER")
}
if username == "" {
return "", errors.New("failed to get username")
}
return username, nil
}
// 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)
HostKeyCallbackHelper
}
// NewSSHAgentAuth returns a PublicKeysCallback based on a SSH agent, it opens
// a pipe with the SSH agent and uses the pipe as the implementer of the public
// key callback function.
func NewSSHAgentAuth(u string) (*PublicKeysCallback, error) {
var err error
if u == "" {
u, err = username()
if err != nil {
return nil, err
}
}
a, _, err := sshagent.New()
if err != nil {
return nil, fmt.Errorf("error creating SSH agent: %q", err)
}
return &PublicKeysCallback{
User: u,
Callback: a.Signers,
}, nil
}
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, error) {
return a.SetHostKeyCallback(&ssh.ClientConfig{
User: a.User,
Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(a.Callback)},
})
}
// NewKnownHostsCallback returns ssh.HostKeyCallback based on a file based on a
// known_hosts file. http://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT
//
// If list of files is empty, then it will be read from the SSH_KNOWN_HOSTS
// environment variable, example:
// /home/foo/custom_known_hosts_file:/etc/custom_known/hosts_file
//
// If SSH_KNOWN_HOSTS is not set the following file locations will be used:
// ~/.ssh/known_hosts
// /etc/ssh/ssh_known_hosts
func NewKnownHostsCallback(files ...string) (ssh.HostKeyCallback, error) {
var err error
if len(files) == 0 {
if files, err = getDefaultKnownHostsFiles(); err != nil {
return nil, err
}
}
if files, err = filterKnownHostsFiles(files...); err != nil {
return nil, err
}
return knownhosts.New(files...)
}
func getDefaultKnownHostsFiles() ([]string, error) {
files := filepath.SplitList(os.Getenv("SSH_KNOWN_HOSTS"))
if len(files) != 0 {
return files, nil
}
homeDirPath, err := homedir.Dir()
if err != nil {
return nil, err
}
return []string{
filepath.Join(homeDirPath, "/.ssh/known_hosts"),
"/etc/ssh/ssh_known_hosts",
}, nil
}
func filterKnownHostsFiles(files ...string) ([]string, error) {
var out []string
for _, file := range files {
_, err := os.Stat(file)
if err == nil {
out = append(out, file)
continue
}
if !os.IsNotExist(err) {
return nil, err
}
}
if len(out) == 0 {
return nil, fmt.Errorf("unable to find any valid known_hosts file, set SSH_KNOWN_HOSTS env variable")
}
return out, nil
}
// HostKeyCallbackHelper is a helper that provides common functionality to
// configure HostKeyCallback into a ssh.ClientConfig.
type HostKeyCallbackHelper struct {
// HostKeyCallback is the function type used for verifying server keys.
// If nil default callback will be create using NewKnownHostsCallback
// without argument.
HostKeyCallback ssh.HostKeyCallback
}
// SetHostKeyCallback sets the field HostKeyCallback in the given cfg. If
// HostKeyCallback is empty a default callback is created using
// NewKnownHostsCallback.
func (m *HostKeyCallbackHelper) SetHostKeyCallback(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) {
var err error
if m.HostKeyCallback == nil {
if m.HostKeyCallback, err = NewKnownHostsCallback(); err != nil {
return cfg, err
}
}
cfg.HostKeyCallback = m.HostKeyCallback
return cfg, nil
}

View File

@ -0,0 +1,203 @@
// Package ssh implements the SSH transport protocol.
package ssh
import (
"fmt"
"reflect"
"strconv"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/plumbing/transport/internal/common"
"github.com/kevinburke/ssh_config"
"golang.org/x/crypto/ssh"
)
// DefaultClient is the default SSH client.
var DefaultClient = NewClient(nil)
// DefaultSSHConfig is the reader used to access parameters stored in the
// system's ssh_config files. If nil all the ssh_config are ignored.
var DefaultSSHConfig sshConfig = ssh_config.DefaultUserSettings
type sshConfig interface {
Get(alias, key string) string
}
// NewClient creates a new SSH client with an optional *ssh.ClientConfig.
func NewClient(config *ssh.ClientConfig) transport.Transport {
return common.NewClient(&runner{config: config})
}
// DefaultAuthBuilder is the function used to create a default AuthMethod, when
// the user doesn't provide any.
var DefaultAuthBuilder = func(user string) (AuthMethod, error) {
return NewSSHAgentAuth(user)
}
const DefaultPort = 22
type runner struct {
config *ssh.ClientConfig
}
func (r *runner) Command(cmd string, ep *transport.Endpoint, auth transport.AuthMethod) (common.Command, error) {
c := &command{command: cmd, endpoint: ep, config: r.config}
if auth != nil {
c.setAuth(auth)
}
if err := c.connect(); err != nil {
return nil, err
}
return c, nil
}
type command struct {
*ssh.Session
connected bool
command string
endpoint *transport.Endpoint
client *ssh.Client
auth AuthMethod
config *ssh.ClientConfig
}
func (c *command) setAuth(auth transport.AuthMethod) error {
a, ok := auth.(AuthMethod)
if !ok {
return transport.ErrInvalidAuthMethod
}
c.auth = a
return nil
}
func (c *command) Start() error {
return c.Session.Start(endpointToCommand(c.command, c.endpoint))
}
// Close closes the SSH session and connection.
func (c *command) Close() error {
if !c.connected {
return nil
}
c.connected = false
//XXX: If did read the full packfile, then the session might be already
// closed.
_ = c.Session.Close()
return c.client.Close()
}
// 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 (c *command) connect() error {
if c.connected {
return transport.ErrAlreadyConnected
}
if c.auth == nil {
if err := c.setAuthFromEndpoint(); err != nil {
return err
}
}
var err error
config, err := c.auth.ClientConfig()
if err != nil {
return err
}
overrideConfig(c.config, config)
c.client, err = ssh.Dial("tcp", c.getHostWithPort(), config)
if err != nil {
return err
}
c.Session, err = c.client.NewSession()
if err != nil {
_ = c.client.Close()
return err
}
c.connected = true
return nil
}
func (c *command) getHostWithPort() string {
if addr, found := c.doGetHostWithPortFromSSHConfig(); found {
return addr
}
host := c.endpoint.Host
port := c.endpoint.Port
if port <= 0 {
port = DefaultPort
}
return fmt.Sprintf("%s:%d", host, port)
}
func (c *command) doGetHostWithPortFromSSHConfig() (addr string, found bool) {
if DefaultSSHConfig == nil {
return
}
host := c.endpoint.Host
port := c.endpoint.Port
configHost := DefaultSSHConfig.Get(c.endpoint.Host, "Hostname")
if configHost != "" {
host = configHost
found = true
}
if !found {
return
}
configPort := DefaultSSHConfig.Get(c.endpoint.Host, "Port")
if configPort != "" {
if i, err := strconv.Atoi(configPort); err == nil {
port = i
}
}
addr = fmt.Sprintf("%s:%d", host, port)
return
}
func (c *command) setAuthFromEndpoint() error {
var err error
c.auth, err = DefaultAuthBuilder(c.endpoint.User)
return err
}
func endpointToCommand(cmd string, ep *transport.Endpoint) string {
return fmt.Sprintf("%s '%s'", cmd, ep.Path)
}
func overrideConfig(overrides *ssh.ClientConfig, c *ssh.ClientConfig) {
if overrides == nil {
return
}
t := reflect.TypeOf(*c)
vc := reflect.ValueOf(c).Elem()
vo := reflect.ValueOf(overrides).Elem()
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
vcf := vc.FieldByName(f.Name)
vof := vo.FieldByName(f.Name)
vcf.Set(vof)
}
*c = vc.Interface().(ssh.ClientConfig)
}