diff options
Diffstat (limited to 'cmd/git-lfs-server')
-rw-r--r-- | cmd/git-lfs-server/main.go | 60 |
1 files changed, 40 insertions, 20 deletions
diff --git a/cmd/git-lfs-server/main.go b/cmd/git-lfs-server/main.go index aa2a0c6..4ff62f6 100644 --- a/cmd/git-lfs-server/main.go +++ b/cmd/git-lfs-server/main.go | |||
@@ -21,6 +21,7 @@ import ( | |||
21 | 21 | ||
22 | "github.com/minio/minio-go/v7" | 22 | "github.com/minio/minio-go/v7" |
23 | "github.com/minio/minio-go/v7/pkg/credentials" | 23 | "github.com/minio/minio-go/v7/pkg/credentials" |
24 | "github.com/rs/xid" | ||
24 | ) | 25 | ) |
25 | 26 | ||
26 | type operation string | 27 | type operation string |
@@ -123,10 +124,14 @@ type lfsError struct { | |||
123 | RequestID string `json:"request_id,omitempty"` | 124 | RequestID string `json:"request_id,omitempty"` |
124 | } | 125 | } |
125 | 126 | ||
126 | func makeRespError(w http.ResponseWriter, message string, code int) { | 127 | func makeRespError(ctx context.Context, w http.ResponseWriter, message string, code int) { |
128 | err := lfsError{Message: message} | ||
129 | if val := ctx.Value(requestIDKey); val != nil { | ||
130 | err.RequestID = val.(string) | ||
131 | } | ||
127 | w.Header().Set("Content-Type", lfsMIME+"; charset=utf-8") | 132 | w.Header().Set("Content-Type", lfsMIME+"; charset=utf-8") |
128 | w.WriteHeader(code) | 133 | w.WriteHeader(code) |
129 | json.NewEncoder(w).Encode(lfsError{Message: message}) | 134 | json.NewEncoder(w).Encode(err) |
130 | } | 135 | } |
131 | 136 | ||
132 | func makeObjError(obj parsedBatchObject, message string, code int) batchResponseObject { | 137 | func makeObjError(obj parsedBatchObject, message string, code int) batchResponseObject { |
@@ -153,7 +158,7 @@ func (h *handler) handleDownloadObject(ctx context.Context, repo string, obj par | |||
153 | } | 158 | } |
154 | // TODO: consider not making this an object-specific, but rather a | 159 | // TODO: consider not making this an object-specific, but rather a |
155 | // generic error such that the entire Batch API request fails. | 160 | // generic error such that the entire Batch API request fails. |
156 | log("Failed to query object information (full path: %s): %s", fullPath, err) | 161 | reqlog(ctx, "Failed to query object information (full path: %s): %s", fullPath, err) |
157 | return makeObjError(obj, "Failed to query object information", http.StatusInternalServerError) | 162 | return makeObjError(obj, "Failed to query object information", http.StatusInternalServerError) |
158 | } | 163 | } |
159 | if info.ChecksumSHA256 != "" && strings.ToLower(info.ChecksumSHA256) != obj.fullHash { | 164 | if info.ChecksumSHA256 != "" && strings.ToLower(info.ChecksumSHA256) != obj.fullHash { |
@@ -167,7 +172,7 @@ func (h *handler) handleDownloadObject(ctx context.Context, repo string, obj par | |||
167 | if err != nil { | 172 | if err != nil { |
168 | // TODO: consider not making this an object-specific, but rather a | 173 | // TODO: consider not making this an object-specific, but rather a |
169 | // generic error such that the entire Batch API request fails. | 174 | // generic error such that the entire Batch API request fails. |
170 | log("Failed to generate action href (full path: %s): %s", fullPath, err) | 175 | reqlog(ctx, "Failed to generate action href (full path: %s): %s", fullPath, err) |
171 | return makeObjError(obj, "Failed to generate action href", http.StatusInternalServerError) | 176 | return makeObjError(obj, "Failed to generate action href", http.StatusInternalServerError) |
172 | } | 177 | } |
173 | 178 | ||
@@ -205,52 +210,58 @@ func isLFSMediaType(t string) bool { | |||
205 | 210 | ||
206 | var re = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/batch$`) | 211 | var re = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/batch$`) |
207 | 212 | ||
213 | type requestID struct{} | ||
214 | |||
215 | var requestIDKey requestID | ||
216 | |||
208 | func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | 217 | func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
218 | ctx := context.WithValue(r.Context(), requestIDKey, xid.New().String()) | ||
219 | |||
209 | reqPath := os.Getenv("PATH_INFO") | 220 | reqPath := os.Getenv("PATH_INFO") |
210 | if reqPath == "" { | 221 | if reqPath == "" { |
211 | reqPath = r.URL.Path | 222 | reqPath = r.URL.Path |
212 | } | 223 | } |
213 | log("reqPath: %s", reqPath) | 224 | reqlog(ctx, "reqPath: %s", reqPath) |
214 | reqPath = strings.TrimPrefix(path.Clean(reqPath), "/") | 225 | reqPath = strings.TrimPrefix(path.Clean(reqPath), "/") |
215 | log("Cleaned reqPath: %s", reqPath) | 226 | reqlog(ctx, "Cleaned reqPath: %s", reqPath) |
216 | submatches := re.FindStringSubmatch(reqPath) | 227 | submatches := re.FindStringSubmatch(reqPath) |
217 | if len(submatches) != 2 { | 228 | if len(submatches) != 2 { |
218 | log("Got path: %s, did not match regex", reqPath) | 229 | reqlog(ctx, "Got path: %s, did not match regex", reqPath) |
219 | makeRespError(w, "Not found", http.StatusNotFound) | 230 | makeRespError(ctx, w, "Not found", http.StatusNotFound) |
220 | return | 231 | return |
221 | } | 232 | } |
222 | repo := strings.TrimPrefix(path.Clean(submatches[1]), "/") | 233 | repo := strings.TrimPrefix(path.Clean(submatches[1]), "/") |
223 | log("Repository: %s", repo) | 234 | reqlog(ctx, "Repository: %s", repo) |
224 | 235 | ||
225 | if !slices.ContainsFunc(r.Header.Values("Accept"), isLFSMediaType) { | 236 | if !slices.ContainsFunc(r.Header.Values("Accept"), isLFSMediaType) { |
226 | makeRespError(w, "Expected "+lfsMIME+" (with UTF-8 charset) in list of acceptable response media types", http.StatusNotAcceptable) | 237 | makeRespError(ctx, w, "Expected "+lfsMIME+" (with UTF-8 charset) in list of acceptable response media types", http.StatusNotAcceptable) |
227 | return | 238 | return |
228 | } | 239 | } |
229 | if !isLFSMediaType(r.Header.Get("Content-Type")) { | 240 | if !isLFSMediaType(r.Header.Get("Content-Type")) { |
230 | makeRespError(w, "Expected request Content-Type to be "+lfsMIME+" (with UTF-8 charset)", http.StatusUnsupportedMediaType) | 241 | makeRespError(ctx, w, "Expected request Content-Type to be "+lfsMIME+" (with UTF-8 charset)", http.StatusUnsupportedMediaType) |
231 | return | 242 | return |
232 | } | 243 | } |
233 | 244 | ||
234 | var body batchRequest | 245 | var body batchRequest |
235 | if err := json.NewDecoder(r.Body).Decode(&body); err != nil { | 246 | if err := json.NewDecoder(r.Body).Decode(&body); err != nil { |
236 | makeRespError(w, "Failed to parse request body as JSON", http.StatusBadRequest) | 247 | makeRespError(ctx, w, "Failed to parse request body as JSON", http.StatusBadRequest) |
237 | return | 248 | return |
238 | } | 249 | } |
239 | 250 | ||
240 | if body.HashAlgo != hashAlgoSHA256 { | 251 | if body.HashAlgo != hashAlgoSHA256 { |
241 | makeRespError(w, "Unsupported hash algorithm specified", http.StatusConflict) | 252 | makeRespError(ctx, w, "Unsupported hash algorithm specified", http.StatusConflict) |
242 | return | 253 | return |
243 | } | 254 | } |
244 | 255 | ||
245 | // TODO: handle authentication | 256 | // TODO: handle authentication |
246 | // right now, we're just trying to make everything publically accessible | 257 | // right now, we're just trying to make everything publically accessible |
247 | if body.Operation == operationUpload { | 258 | if body.Operation == operationUpload { |
248 | makeRespError(w, "Upload operations are currently not supported", http.StatusForbidden) | 259 | makeRespError(ctx, w, "Upload operations are currently not supported", http.StatusForbidden) |
249 | return | 260 | return |
250 | } | 261 | } |
251 | 262 | ||
252 | if len(body.Transfers) != 0 && !slices.Contains(body.Transfers, transferAdapterBasic) { | 263 | if len(body.Transfers) != 0 && !slices.Contains(body.Transfers, transferAdapterBasic) { |
253 | makeRespError(w, "Unsupported transfer adapter specified (supported: basic)", http.StatusConflict) | 264 | makeRespError(ctx, w, "Unsupported transfer adapter specified (supported: basic)", http.StatusConflict) |
254 | return | 265 | return |
255 | } | 266 | } |
256 | 267 | ||
@@ -263,15 +274,15 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
263 | permGranted := err == nil | 274 | permGranted := err == nil |
264 | var exitErr *exec.ExitError | 275 | var exitErr *exec.ExitError |
265 | if err != nil && !errors.As(err, &exitErr) { | 276 | if err != nil && !errors.As(err, &exitErr) { |
266 | log("Error checking access info (running %s): %s", cmd, err) | 277 | reqlog(ctx, "Error checking access info (running %s): %s", cmd, err) |
267 | makeRespError(w, "Failed to query access information", http.StatusInternalServerError) | 278 | makeRespError(ctx, w, "Failed to query access information", http.StatusInternalServerError) |
268 | return | 279 | return |
269 | } | 280 | } |
270 | if !permGranted { | 281 | if !permGranted { |
271 | // TODO: when handling authorization, make sure to return 403 Forbidden | 282 | // TODO: when handling authorization, make sure to return 403 Forbidden |
272 | // here when the user *does* have read permissions, but is not allowed | 283 | // here when the user *does* have read permissions, but is not allowed |
273 | // to write when requesting an upload operation. | 284 | // to write when requesting an upload operation. |
274 | makeRespError(w, "Repository not found", http.StatusNotFound) | 285 | makeRespError(ctx, w, "Repository not found", http.StatusNotFound) |
275 | return | 286 | return |
276 | } | 287 | } |
277 | 288 | ||
@@ -279,7 +290,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
279 | for _, obj := range body.Objects { | 290 | for _, obj := range body.Objects { |
280 | oid := strings.ToLower(obj.OID) | 291 | oid := strings.ToLower(obj.OID) |
281 | if !isValidSHA256Hash(oid) { | 292 | if !isValidSHA256Hash(oid) { |
282 | makeRespError(w, "Invalid hash format in object ID", http.StatusBadRequest) | 293 | makeRespError(ctx, w, "Invalid hash format in object ID", http.StatusBadRequest) |
283 | return | 294 | return |
284 | } | 295 | } |
285 | objects = append(objects, parsedBatchObject{ | 296 | objects = append(objects, parsedBatchObject{ |
@@ -295,7 +306,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
295 | HashAlgo: hashAlgoSHA256, | 306 | HashAlgo: hashAlgoSHA256, |
296 | } | 307 | } |
297 | for _, obj := range objects { | 308 | for _, obj := range objects { |
298 | resp.Objects = append(resp.Objects, h.handleDownloadObject(r.Context(), repo, obj)) | 309 | resp.Objects = append(resp.Objects, h.handleDownloadObject(ctx, repo, obj)) |
299 | } | 310 | } |
300 | 311 | ||
301 | w.Header().Set("Content-Type", lfsMIME) | 312 | w.Header().Set("Content-Type", lfsMIME) |
@@ -303,6 +314,15 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
303 | json.NewEncoder(w).Encode(resp) | 314 | json.NewEncoder(w).Encode(resp) |
304 | } | 315 | } |
305 | 316 | ||
317 | func reqlog(ctx context.Context, msg string, args ...any) { | ||
318 | fmt.Fprint(os.Stderr, "[gitolfs3] ") | ||
319 | if val := ctx.Value(requestIDKey); val != nil { | ||
320 | fmt.Fprintf(os.Stderr, "[%s] ", val.(string)) | ||
321 | } | ||
322 | fmt.Fprintf(os.Stderr, msg, args...) | ||
323 | fmt.Fprint(os.Stderr, "\n") | ||
324 | } | ||
325 | |||
306 | func log(msg string, args ...any) { | 326 | func log(msg string, args ...any) { |
307 | fmt.Fprint(os.Stderr, "[gitolfs3] ") | 327 | fmt.Fprint(os.Stderr, "[gitolfs3] ") |
308 | fmt.Fprintf(os.Stderr, msg, args...) | 328 | fmt.Fprintf(os.Stderr, msg, args...) |