diff options
| author | Rutger Broekhoff | 2023-12-30 00:21:31 +0100 |
|---|---|---|
| committer | Rutger Broekhoff | 2023-12-30 00:21:31 +0100 |
| commit | 9f09bc87d6613d365e0eb39881a9c96ffb8e130a (patch) | |
| tree | 8af16ec8eb570829714d53fd97fb646841b840d2 /cmd/git-lfs-server | |
| parent | b33f4447af7e22dbfcec3c3e7f07563f758a0e20 (diff) | |
| download | gitolfs3-9f09bc87d6613d365e0eb39881a9c96ffb8e130a.tar.gz gitolfs3-9f09bc87d6613d365e0eb39881a9c96ffb8e130a.zip | |
Request IDs!
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...) |