aboutsummaryrefslogblamecommitdiffstats
path: root/clients/common/common.go
blob: df1c233daa210b5c1ce26ec5b8e9f7693c52d438 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                                                        


              
                
             
            
                   
                 
                

                 
                                       
                                                        
                                                 

 
     



                                                                            

 

                                                  
                                     
                       
                                 
                                          
                                                           
                          






                           
                     
 




                                                                                                  
                                                     

                                                     
                                     
                       
                                                              

         



                                                                     

         
                                

 








                                                                                            


                                    



                                                                                           



                                
 




                                            
 




                                                    

 

                                           








                                              
                                  


         






                                                                 



                                         


                                    





                                                                 
 

                             

         
                                                                          

 


                                                         


                 
                                                                         

                                                             


                         
                                                       












                                                   







                                        
 
                              
                                                  




                                        

                                                              







                                                   




                           
                               
                                  
                                            

 



                                                                  

                                                              
                                                 
                                                          

                 
                                                   

         
                  

 
                                                            
                       
                                                 



















                                                                                                


                                

                                                        

                 



                               
                                            

         
                      

 

                                           

 















                                                               

         


                                                                                              


                                                    
                                                          
                  






                                             







                                                                                             




                                                     
 

                                                                     

         



                                                          
                


                                  

                         
                 







                                                     







                                                         

                              
                                      
                                                                           

         
                                      
                                                                           

         
                         
                                                                                

         




                                                          
 
                                           
 
// Package common contains interfaces and non-specific protocol entities
package common

import (
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/url"
	"regexp"
	"strings"

	"gopkg.in/src-d/go-git.v4/core"
	"gopkg.in/src-d/go-git.v4/formats/packp/pktline"
	"gopkg.in/src-d/go-git.v4/storage/memory"
)

var (
	ErrRepositoryNotFound    = errors.New("repository not found")
	ErrAuthorizationRequired = errors.New("authorization required")
	ErrEmptyGitUploadPack    = errors.New("empty git-upload-pack given")
	ErrInvalidAuthMethod     = errors.New("invalid auth method")
)

const GitUploadPackServiceName = "git-upload-pack"

type GitUploadPackService interface {
	Connect() error
	SetAuth(AuthMethod) error
	Info() (*GitUploadPackInfo, error)
	Fetch(*GitUploadPackRequest) (io.ReadCloser, error)
	Disconnect() error
}

type AuthMethod interface {
	Name() string
	String() string
}

type Endpoint url.URL

var (
	isSchemeRegExp   = regexp.MustCompile("^[^:]+://")
	scpLikeUrlRegExp = regexp.MustCompile("^(?P<user>[^@]+@)?(?P<host>[^:]+):/?(?P<path>.+)$")
)

func NewEndpoint(endpoint string) (Endpoint, error) {
	endpoint = transformSCPLikeIfNeeded(endpoint)

	u, err := url.Parse(endpoint)
	if err != nil {
		return Endpoint{}, core.NewPermanentError(err)
	}

	if !u.IsAbs() {
		return Endpoint{}, core.NewPermanentError(fmt.Errorf(
			"invalid endpoint: %s", endpoint,
		))
	}

	return Endpoint(*u), nil
}

func transformSCPLikeIfNeeded(endpoint string) string {
	if !isSchemeRegExp.MatchString(endpoint) && scpLikeUrlRegExp.MatchString(endpoint) {
		m := scpLikeUrlRegExp.FindStringSubmatch(endpoint)
		return fmt.Sprintf("ssh://%s%s/%s", m[1], m[2], m[3])
	}

	return endpoint
}

func (e *Endpoint) String() string {
	u := url.URL(*e)
	return u.String()
}

// Capabilities contains all the server capabilities
// https://github.com/git/git/blob/master/Documentation/technical/protocol-capabilities.txt
type Capabilities struct {
	m map[string]*Capability
	o []string
}

// Capability represents a server capability
type Capability struct {
	Name   string
	Values []string
}

// NewCapabilities returns a new Capabilities struct
func NewCapabilities() *Capabilities {
	return &Capabilities{
		m: make(map[string]*Capability, 0),
	}
}

// Decode decodes a string
func (c *Capabilities) Decode(raw string) {
	params := strings.Split(raw, " ")
	for _, p := range params {
		s := strings.SplitN(p, "=", 2)

		var value string
		if len(s) == 2 {
			value = s[1]
		}

		c.Add(s[0], value)
	}
}

// Get returns the values for a capability
func (c *Capabilities) Get(capability string) *Capability {
	return c.m[capability]
}

// Set sets a capability removing the values
func (c *Capabilities) Set(capability string, values ...string) {
	if _, ok := c.m[capability]; ok {
		delete(c.m, capability)
	}

	c.Add(capability, values...)
}

// Add adds a capability, values are optional
func (c *Capabilities) Add(capability string, values ...string) {
	if !c.Supports(capability) {
		c.m[capability] = &Capability{Name: capability}
		c.o = append(c.o, capability)
	}

	if len(values) == 0 {
		return
	}

	c.m[capability].Values = append(c.m[capability].Values, values...)
}

// Supports returns true if capability is present
func (c *Capabilities) Supports(capability string) bool {
	_, ok := c.m[capability]
	return ok
}

// SymbolicReference returns the reference for a given symbolic reference
func (c *Capabilities) SymbolicReference(sym string) string {
	if !c.Supports("symref") {
		return ""
	}

	for _, symref := range c.Get("symref").Values {
		parts := strings.Split(symref, ":")
		if len(parts) != 2 {
			continue
		}

		if parts[0] == sym {
			return parts[1]
		}
	}

	return ""
}

func (c *Capabilities) String() string {
	if len(c.o) == 0 {
		return ""
	}

	var o string
	for _, key := range c.o {
		cap := c.m[key]

		added := false
		for _, value := range cap.Values {
			if value == "" {
				continue
			}

			added = true
			o += fmt.Sprintf("%s=%s ", key, value)
		}

		if len(cap.Values) == 0 || !added {
			o += key + " "
		}
	}

	if len(o) == 0 {
		return o
	}

	return o[:len(o)-1]
}

type GitUploadPackInfo struct {
	Capabilities *Capabilities
	Refs         memory.ReferenceStorage
}

func NewGitUploadPackInfo() *GitUploadPackInfo {
	return &GitUploadPackInfo{Capabilities: NewCapabilities()}
}

func (r *GitUploadPackInfo) Decode(s *pktline.Scanner) error {
	if err := r.read(s); err != nil {
		if err == ErrEmptyGitUploadPack {
			return core.NewPermanentError(err)
		}

		return core.NewUnexpectedError(err)
	}

	return nil
}

func (r *GitUploadPackInfo) read(s *pktline.Scanner) error {
	isEmpty := true
	r.Refs = make(memory.ReferenceStorage, 0)
	smartCommentIgnore := false
	for s.Scan() {
		line := string(s.Bytes())

		if smartCommentIgnore {
			// some servers like Github add a flush-pkt after the smart http comment
			// that we must ignore to prevent a premature termination of the read.
			if len(line) == 0 {
				continue
			}
			smartCommentIgnore = false
		}

		// exit on first flush-pkt
		if len(line) == 0 {
			break
		}

		if isSmartHttpComment(line) {
			smartCommentIgnore = true
			continue
		}

		if err := r.readLine(line); err != nil {
			return err
		}

		isEmpty = false
	}

	if isEmpty {
		return ErrEmptyGitUploadPack
	}

	return s.Err()
}

func isSmartHttpComment(line string) bool {
	return line[0] == '#'
}

func (r *GitUploadPackInfo) readLine(line string) error {
	hashEnd := strings.Index(line, " ")
	hash := line[:hashEnd]

	zeroID := strings.Index(line, string([]byte{0}))
	if zeroID == -1 {
		name := line[hashEnd+1 : len(line)-1]
		ref := core.NewReferenceFromStrings(name, hash)
		return r.Refs.Set(ref)
	}

	name := line[hashEnd+1 : zeroID]
	r.Capabilities.Decode(line[zeroID+1 : len(line)-1])
	if !r.Capabilities.Supports("symref") {
		ref := core.NewReferenceFromStrings(name, hash)
		return r.Refs.Set(ref)
	}

	target := r.Capabilities.SymbolicReference(name)
	ref := core.NewSymbolicReference(core.ReferenceName(name), core.ReferenceName(target))
	return r.Refs.Set(ref)
}

func (r *GitUploadPackInfo) Head() *core.Reference {
	ref, _ := core.ResolveReference(r.Refs, core.HEAD)
	return ref
}

func (r *GitUploadPackInfo) String() string {
	return string(r.Bytes())
}

func (r *GitUploadPackInfo) Bytes() []byte {
	payloads := []string{}
	payloads = append(payloads, "# service=git-upload-pack\n")
	// inserting a flush-pkt here violates the protocol spec, but some
	// servers do it, like Github.com
	payloads = append(payloads, "")

	firstLine := fmt.Sprintf("%s HEAD\x00%s\n", r.Head().Hash(), r.Capabilities.String())
	payloads = append(payloads, firstLine)

	for _, ref := range r.Refs {
		if ref.Type() != core.HashReference {
			continue
		}

		ref := fmt.Sprintf("%s %s\n", ref.Hash(), ref.Name())
		payloads = append(payloads, ref)
	}

	payloads = append(payloads, "")
	pktlines, _ := pktline.NewFromStrings(payloads...)
	b, _ := ioutil.ReadAll(pktlines)

	return b
}

type GitUploadPackRequest struct {
	Wants []core.Hash
	Haves []core.Hash
	Depth int
}

func (r *GitUploadPackRequest) Want(h ...core.Hash) {
	r.Wants = append(r.Wants, h...)
}

func (r *GitUploadPackRequest) Have(h ...core.Hash) {
	r.Haves = append(r.Haves, h...)
}

func (r *GitUploadPackRequest) String() string {
	b, _ := ioutil.ReadAll(r.Reader())
	return string(b)
}

func (r *GitUploadPackRequest) Reader() *strings.Reader {
	payloads := []string{}

	for _, want := range r.Wants {
		payloads = append(payloads, fmt.Sprintf("want %s\n", want))
	}

	for _, have := range r.Haves {
		payloads = append(payloads, fmt.Sprintf("have %s\n", have))
	}

	if r.Depth != 0 {
		payloads = append(payloads, fmt.Sprintf("deepen %d\n", r.Depth))
	}

	payloads = append(payloads, "")
	payloads = append(payloads, "done\n")

	pktlines, _ := pktline.NewFromStrings(payloads...)
	b, _ := ioutil.ReadAll(pktlines)

	return strings.NewReader(string(b))
}