upstream.go 5.44 KB
Newer Older
1
/*
2
The upstream type implements http.Handler.
3

Jacob Vosmaer's avatar
Jacob Vosmaer committed
4
In this file we handle request routing and interaction with the authBackend.
5 6 7 8 9 10 11 12 13 14
*/

package main

import (
	"io"
	"log"
	"net/http"
	"os"
	"path"
15
	"regexp"
16 17
)

Kamil Trzcinski's avatar
Kamil Trzcinski committed
18 19
type serviceHandleFunc func(w http.ResponseWriter, r *gitRequest)

20
type upstream struct {
21 22 23 24 25
	httpClient  *http.Client
	authBackend string
}

type gitService struct {
Kamil Trzcinski's avatar
Kamil Trzcinski committed
26 27 28
	method     string
	regex      *regexp.Regexp
	handleFunc serviceHandleFunc
29 30
}

Kamil Trzcinski's avatar
Kamil Trzcinski committed
31
type authorizationResponse struct {
32 33
	// GL_ID is an environment variable used by gitlab-shell hooks during 'git
	// push' and 'git pull'
34
	GL_ID string
35 36 37 38 39 40 41 42
	// RepoPath is the full path on disk to the Git repository the request is
	// about
	RepoPath string
	// ArchivePath is the full path where we should find/create a cached copy
	// of a requested archive
	ArchivePath string
	// ArchivePrefix is used to put extracted archive contents in a
	// subdirectory
43
	ArchivePrefix string
44
	// CommitId is used do prevent race conditions between the 'time of check'
Jacob Vosmaer's avatar
Jacob Vosmaer committed
45
	// in the GitLab Rails app and the 'time of use' in gitlab-workhorse.
46
	CommitId string
47 48
	// StoreLFSPath is provided by the GitLab Rails application
	// to mark where the tmp file should be placed
49
	StoreLFSPath string
50 51 52
	// LFS object id
	LfsOid string
	// LFS object size
53
	LfsSize int64
54 55 56
	// TmpPath is the path where we should store temporary files
	// This is set by authorization middleware
	TempPath string
57 58
}

Kamil Trzcinski's avatar
Kamil Trzcinski committed
59 60 61 62 63
// A gitReqest is an *http.Request decorated with attributes returned by the
// GitLab Rails application.
type gitRequest struct {
	*http.Request
	authorizationResponse
64
	u *upstream
Kamil Trzcinski's avatar
Kamil Trzcinski committed
65 66
}

67 68
// Routing table
var gitServices = [...]gitService{
Kamil Trzcinski's avatar
Kamil Trzcinski committed
69
	gitService{"GET", regexp.MustCompile(`/info/refs\z`), repoPreAuthorizeHandler(handleGetInfoRefs)},
70 71
	gitService{"POST", regexp.MustCompile(`/git-upload-pack\z`), repoPreAuthorizeHandler(contentEncodingHandler(handlePostRPC))},
	gitService{"POST", regexp.MustCompile(`/git-receive-pack\z`), repoPreAuthorizeHandler(contentEncodingHandler(handlePostRPC))},
Kamil Trzcinski's avatar
Kamil Trzcinski committed
72 73 74 75 76 77
	gitService{"GET", regexp.MustCompile(`/repository/archive\z`), repoPreAuthorizeHandler(handleGetArchive)},
	gitService{"GET", regexp.MustCompile(`/repository/archive.zip\z`), repoPreAuthorizeHandler(handleGetArchive)},
	gitService{"GET", regexp.MustCompile(`/repository/archive.tar\z`), repoPreAuthorizeHandler(handleGetArchive)},
	gitService{"GET", regexp.MustCompile(`/repository/archive.tar.gz\z`), repoPreAuthorizeHandler(handleGetArchive)},
	gitService{"GET", regexp.MustCompile(`/repository/archive.tar.bz2\z`), repoPreAuthorizeHandler(handleGetArchive)},
	gitService{"GET", regexp.MustCompile(`/uploads/`), handleSendFile},
78 79

	// Git LFS
80
	gitService{"PUT", regexp.MustCompile(`/gitlab-lfs/objects/([0-9a-f]{64})/([0-9]+)\z`), lfsAuthorizeHandler(handleStoreLfsObject)},
81
	gitService{"GET", regexp.MustCompile(`/gitlab-lfs/objects/([0-9a-f]{64})\z`), handleSendFile},
82 83 84 85 86 87

	// CI artifacts
	gitService{"GET", regexp.MustCompile(`/builds/download\z`), handleSendFile},
	gitService{"GET", regexp.MustCompile(`/ci/api/v1/builds/[0-9]+/artifacts\z`), handleSendFile},
	gitService{"POST", regexp.MustCompile(`/ci/api/v1/builds/[0-9]+/artifacts\z`), artifactsAuthorizeHandler(contentEncodingHandler(handleFileUploads))},
	gitService{"DELETE", regexp.MustCompile(`/ci/api/v1/builds/[0-9]+/artifacts\z`), proxyRequest},
88 89
}

90 91
func newUpstream(authBackend string, authTransport http.RoundTripper) *upstream {
	return &upstream{&http.Client{Transport: authTransport}, authBackend}
92 93
}

94
func (u *upstream) ServeHTTP(w http.ResponseWriter, r *http.Request) {
95 96
	var g gitService

97
	log.Printf("%s %q", r.Method, r.URL)
98 99 100 101

	// Look for a matching Git service
	foundService := false
	for _, g = range gitServices {
102
		if r.Method == g.method && g.regex.MatchString(r.URL.Path) {
103 104 105 106 107 108 109 110 111 112 113
			foundService = true
			break
		}
	}
	if !foundService {
		// The protocol spec in git/Documentation/technical/http-protocol.txt
		// says we must return 403 if no matching service is found.
		http.Error(w, "Forbidden", 403)
		return
	}

Kamil Trzcinski's avatar
Kamil Trzcinski committed
114 115 116
	request := gitRequest{
		Request: r,
		u:       u,
117 118
	}

Kamil Trzcinski's avatar
Kamil Trzcinski committed
119
	g.handleFunc(w, &request)
120 121 122 123 124 125 126 127 128 129 130
}

func looksLikeRepo(p string) bool {
	// If /path/to/foo.git/objects exists then let's assume it is a valid Git
	// repository.
	if _, err := os.Stat(path.Join(p, "objects")); err != nil {
		log.Print(err)
		return false
	}
	return true
}
131

Kamil Trzcinski's avatar
Kamil Trzcinski committed
132 133 134
func (u *upstream) newUpstreamRequest(r *http.Request, body io.Reader, suffix string) (*http.Request, error) {
	url := u.authBackend + r.URL.RequestURI() + suffix
	authReq, err := http.NewRequest(r.Method, url, body)
135 136 137 138 139 140 141 142
	if err != nil {
		return nil, err
	}
	// Forward all headers from our client to the auth backend. This includes
	// HTTP Basic authentication credentials (the 'Authorization' header).
	for k, v := range r.Header {
		authReq.Header[k] = v
	}
Kamil Trzcinski's avatar
Kamil Trzcinski committed
143 144 145 146 147 148

	// Clean some headers when issuing a new request without body
	if body == nil {
		authReq.Header.Del("Content-Type")
		authReq.Header.Del("Content-Encoding")
		authReq.Header.Del("Content-Length")
149
		authReq.Header.Del("Content-Disposition")
Kamil Trzcinski's avatar
Kamil Trzcinski committed
150 151 152 153
		authReq.Header.Del("Accept-Encoding")
		authReq.Header.Del("Transfer-Encoding")
	}

154 155 156 157 158 159
	// Also forward the Host header, which is excluded from the Header map by the http libary.
	// This allows the Host header received by the backend to be consistent with other
	// requests not going through gitlab-workhorse.
	authReq.Host = r.Host
	// Set a custom header for the request. This can be used in some
	// configurations (Passenger) to solve auth request routing problems.
160
	authReq.Header.Set("Gitlab-Workhorse", Version)
161 162 163

	return authReq, nil
}