git@cosmocr.at/gitweb.git @ HEAD · summary refs log
HEAD / internal / server / githttp.go /
internal/server/githttp.go · 5.1 KiB · raw
view: Source ☐ force
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package server

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"path"
	"strings"

	"gitweb/internal/githttp"
	"gitweb/internal/gitrepo"
)

// gitKind identifies which git smart-HTTP endpoint a request hit.
type gitKind int

const (
	gitInfoRefs gitKind = iota
	gitUploadPack
	gitReceivePack
)

// gitReq describes a matched git smart-HTTP request.
type gitReq struct {
	kind    gitKind
	service githttp.Service // the effective service (upload-pack or receive-pack)
	urlPath string          // URL path with the git suffix stripped, e.g. "/u@d.com/x.git"
}

// gitEndpoint recognizes the three git smart-HTTP endpoints by method and path
// suffix. It returns ok=false for ordinary browse requests.
func gitEndpoint(r *http.Request) (gitReq, bool) {
	p := r.URL.Path
	switch {
	case r.Method == http.MethodGet && strings.HasSuffix(p, "/info/refs"):
		svc := r.URL.Query().Get("service")
		switch githttp.Service(svc) {
		case githttp.UploadPack, githttp.ReceivePack:
			return gitReq{gitInfoRefs, githttp.Service(svc), strings.TrimSuffix(p, "/info/refs")}, true
		}
		// info/refs without a smart service param: fall through to browse (this
		// also lets someone view a repo's literal info/refs file).
		return gitReq{}, false
	case r.Method == http.MethodPost && strings.HasSuffix(p, "/git-upload-pack"):
		return gitReq{gitUploadPack, githttp.UploadPack, strings.TrimSuffix(p, "/git-upload-pack")}, true
	case r.Method == http.MethodPost && strings.HasSuffix(p, "/git-receive-pack"):
		return gitReq{gitReceivePack, githttp.ReceivePack, strings.TrimSuffix(p, "/git-receive-pack")}, true
	}
	return gitReq{}, false
}

// handleGitHTTP serves a git smart-HTTP request: authenticate, resolve the repo,
// authorize (read for clone/fetch, write for push), then run the protocol.
func (s *Server) handleGitHTTP(w http.ResponseWriter, r *http.Request, ep gitReq) {
	user, clean, ok := parseUpspinPath(ep.urlPath)
	if !ok {
		s.notFound(w)
		return
	}

	authUser, ok := s.authenticate(w, r)
	if !ok {
		return // 401 already written
	}

	repoPath, rest, isRepo, err := gitrepo.Locate(s.store, user, clean)
	if err != nil {
		s.storeErr(w, err)
		return
	}
	// Endpoints live only at a repo root.
	if !isRepo || rest != "" {
		s.denyRead(w, r, authUser)
		return
	}

	writing := ep.service == githttp.ReceivePack
	if writing {
		// Push always requires an authenticated, authorized writer.
		if authUser == "" {
			s.challenge(w)
			return
		}
		can, err := s.authz.CanWrite(authUser, repoPath)
		if err != nil {
			s.serverErr(w, err)
			return
		}
		if !can {
			// The caller is known; distinguish "can't read" (hide it) from
			// "can read but not write" (explicit 403).
			readable, rerr := s.authz.CanRead(authUser, repoPath)
			if rerr != nil {
				s.serverErr(w, rerr)
				return
			}
			if !readable {
				s.notFound(w)
				return
			}
			http.Error(w, "403 forbidden: no write access", http.StatusForbidden)
			return
		}
	} else {
		can, err := s.authz.CanRead(authUser, repoPath)
		if err != nil {
			s.serverErr(w, err)
			return
		}
		if !can {
			s.denyRead(w, r, authUser)
			return
		}
	}

	switch ep.kind {
	case gitInfoRefs:
		storer, release, err := s.repos.GetStorer(repoPath)
		if err != nil {
			s.storeErr(w, err)
			return
		}
		defer release()
		if err := githttp.InfoRefs(w, storer, ep.service); err != nil {
			log.Printf("gitweb: info/refs %s: %v", repoPath, err)
		}
	case gitUploadPack:
		// upload-pack streams the packfile from the storer for the whole
		// response; the deferred release keeps the repo's on-disk backing
		// alive until ServeUploadPack returns.
		storer, release, err := s.repos.GetStorer(repoPath)
		if err != nil {
			s.storeErr(w, err)
			return
		}
		defer release()
		if err := githttp.ServeUploadPack(w, r, storer); err != nil {
			log.Printf("gitweb: upload-pack %s: %v", repoPath, err)
		}
	case gitReceivePack:
		req, err := githttp.DecodeReceivePack(r)
		if errors.Is(err, githttp.ErrProbe) {
			// git checking the endpoint before streaming a large push.
			if err := githttp.WriteReceivePackResult(w, nil); err != nil {
				log.Printf("gitweb: receive-pack probe %s: %v", repoPath, err)
			}
			return
		}
		if err != nil {
			log.Printf("gitweb: receive-pack decode %s: %v", repoPath, err)
			var tooBig *http.MaxBytesError
			if errors.As(err, &tooBig) {
				http.Error(w, fmt.Sprintf("push too large: the limit is %d MiB", tooBig.Limit>>20),
					http.StatusRequestEntityTooLarge)
				return
			}
			http.Error(w, "malformed receive-pack request: "+err.Error(), http.StatusBadRequest)
			return
		}
		rs, err := s.pusher.ReceivePack(r.Context(), repoPath, req)
		if err != nil {
			log.Printf("gitweb: receive-pack %s: %v", repoPath, err)
			if rs == nil {
				s.serverErr(w, err)
				return
			}
			// rs carries per-ref failure detail; report it to the client.
		}
		if err := githttp.WriteReceivePackResult(w, rs); err != nil {
			log.Printf("gitweb: receive-pack result %s: %v", repoPath, err)
		}
	}
}

// cleanRest cleans a URL remainder into an upspin path suffix beginning with
// "/". path.Clean collapses any ".." so it cannot escape the user's tree.
func cleanRest(rest string) string {
	return path.Clean("/" + rest)
}