aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Rutger Broekhoff2024-01-02 00:41:25 +0100
committerLibravatar Rutger Broekhoff2024-01-02 00:41:25 +0100
commit9e8463e8dbc4bffcd22f9dd1896176829385dfde (patch)
tree449b5d4ce09c53fcd606c8b30f688c701db93607
parent9a5376c9023098d0c6bedb27ce672bc6b083d76a (diff)
downloadgitolfs3-9e8463e8dbc4bffcd22f9dd1896176829385dfde.tar.gz
gitolfs3-9e8463e8dbc4bffcd22f9dd1896176829385dfde.zip
Upload validation by proxying
Yes, the code is a mess
-rw-r--r--cmd/git-lfs-server/main.go321
1 files changed, 263 insertions, 58 deletions
diff --git a/cmd/git-lfs-server/main.go b/cmd/git-lfs-server/main.go
index b886557..fe62724 100644
--- a/cmd/git-lfs-server/main.go
+++ b/cmd/git-lfs-server/main.go
@@ -4,11 +4,14 @@ import (
4 "bytes" 4 "bytes"
5 "context" 5 "context"
6 "crypto/ed25519" 6 "crypto/ed25519"
7 "crypto/sha256"
7 "encoding/base64" 8 "encoding/base64"
8 "encoding/hex" 9 "encoding/hex"
9 "encoding/json" 10 "encoding/json"
10 "errors" 11 "errors"
11 "fmt" 12 "fmt"
13 "hash"
14 "io"
12 "mime" 15 "mime"
13 "net/http" 16 "net/http"
14 "net/http/cgi" 17 "net/http/cgi"
@@ -48,7 +51,7 @@ type batchRef struct {
48 51
49type batchRequestObject struct { 52type batchRequestObject struct {
50 OID string `json:"oid"` 53 OID string `json:"oid"`
51 Size uint64 `json:"size"` 54 Size int64 `json:"size"`
52} 55}
53 56
54type batchRequest struct { 57type batchRequest struct {
@@ -75,7 +78,7 @@ type batchError struct {
75 78
76type batchResponseObject struct { 79type batchResponseObject struct {
77 OID string `json:"oid"` 80 OID string `json:"oid"`
78 Size uint64 `json:"size"` 81 Size int64 `json:"size"`
79 Authenticated *bool `json:"authenticated"` 82 Authenticated *bool `json:"authenticated"`
80 Actions map[operation]batchAction `json:"actions,omitempty"` 83 Actions map[operation]batchAction `json:"actions,omitempty"`
81 Error *batchError `json:"error,omitempty"` 84 Error *batchError `json:"error,omitempty"`
@@ -92,10 +95,10 @@ type handler struct {
92 bucket string 95 bucket string
93 anonUser string 96 anonUser string
94 gitolitePath string 97 gitolitePath string
95 publicKey ed25519.PublicKey 98 privateKey ed25519.PrivateKey
99 baseURL *url.URL
96} 100}
97 101
98// Requires lowercase hash
99func isValidSHA256Hash(hash string) bool { 102func isValidSHA256Hash(hash string) bool {
100 if len(hash) != 64 { 103 if len(hash) != 64 {
101 return false 104 return false
@@ -161,7 +164,7 @@ func (h *handler) handleDownloadObject(ctx context.Context, repo string, obj par
161 if info.ChecksumSHA256 != "" && strings.ToLower(info.ChecksumSHA256) != obj.fullHash { 164 if info.ChecksumSHA256 != "" && strings.ToLower(info.ChecksumSHA256) != obj.fullHash {
162 return makeObjError(obj, "Corrupted file", http.StatusUnprocessableEntity) 165 return makeObjError(obj, "Corrupted file", http.StatusUnprocessableEntity)
163 } 166 }
164 if uint64(info.Size) != obj.size { 167 if info.Size != obj.size {
165 return makeObjError(obj, "Incorrect size specified for object", http.StatusUnprocessableEntity) 168 return makeObjError(obj, "Incorrect size specified for object", http.StatusUnprocessableEntity)
166 } 169 }
167 170
@@ -187,42 +190,198 @@ func (h *handler) handleDownloadObject(ctx context.Context, repo string, obj par
187 } 190 }
188} 191}
189 192
190func (h *handler) handleUploadObject(ctx context.Context, repo string, obj parsedBatchObject) batchResponseObject { 193type uploadObjectGitolfs3Claims struct {
194 Repository string `json:"repository"`
195 OID string `json:"oid"`
196 Size int64 `json:"size"`
197}
198
199type uploadObjectCustomClaims struct {
200 Gitolfs3 uploadObjectGitolfs3Claims `json:"gitolfs3"`
201 *jwt.RegisteredClaims
202}
203
204// Return nil when the object already exists
205func (h *handler) handleUploadObject(ctx context.Context, repo string, obj parsedBatchObject) *batchResponseObject {
191 fullPath := path.Join(repo+".git", "lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash) 206 fullPath := path.Join(repo+".git", "lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash)
207 _, err := h.mc.StatObject(ctx, h.bucket, fullPath, minio.GetObjectOptions{})
208 if err == nil {
209 // The object exists
210 return nil
211 }
212
213 var resp minio.ErrorResponse
214 if !errors.As(err, &resp) || resp.StatusCode != http.StatusNotFound {
215 // TODO: consider not making this an object-specific, but rather a
216 // generic error such that the entire Batch API request fails.
217 reqlog(ctx, "Failed to generate action href (full path: %s): %s", fullPath, err)
218 objErr := makeObjError(obj, "Failed to generate action href", http.StatusInternalServerError)
219 return &objErr
220 }
221
192 expiresIn := time.Hour * 24 222 expiresIn := time.Hour * 24
223 claims := uploadObjectCustomClaims{
224 Gitolfs3: uploadObjectGitolfs3Claims{
225 Repository: repo,
226 OID: obj.fullHash,
227 Size: obj.size,
228 },
229 RegisteredClaims: &jwt.RegisteredClaims{
230 IssuedAt: jwt.NewNumericDate(time.Now()),
231 ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)),
232 },
233 }
193 234
194 presigned, err := h.mc.Presign(ctx, http.MethodPut, h.bucket, fullPath, expiresIn, url.Values{ 235 token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
195 "x-amz-sdk-checksum-algorithm": {"sha256"}, 236 ss, err := token.SignedString(h.privateKey)
196 "x-amz-checksum-sha256": {sha256AsBase64(obj.fullHash)},
197 "x-amz-content-sha256": {obj.fullHash},
198 "Content-Length": {strconv.FormatUint(obj.size, 10)},
199 })
200 if err != nil { 237 if err != nil {
201 // TODO: consider not making this an object-specific, but rather a 238 // TODO: consider not making this an object-specific, but rather a
202 // generic error such that the entire Batch API request fails. 239 // generic error such that the entire Batch API request fails.
203 reqlog(ctx, "Failed to generate action href (full path: %s): %s", fullPath, err) 240 reqlog(ctx, "Fatal: failed to generate JWT: %s", err)
204 return makeObjError(obj, "Failed to generate action href", http.StatusInternalServerError) 241 objErr := makeObjError(obj, "Failed to generate token", http.StatusInternalServerError)
242 return &objErr
205 } 243 }
206 244
245 uploadPath := path.Join(repo+".git", "info/lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash)
246 uploadHRef := h.baseURL.ResolveReference(&url.URL{Path: uploadPath}).String()
247 // The object does not exist.
207 authenticated := true 248 authenticated := true
208 return batchResponseObject{ 249 return &batchResponseObject{
209 OID: obj.fullHash, 250 OID: obj.fullHash,
210 Size: obj.size, 251 Size: obj.size,
211 Authenticated: &authenticated, 252 Authenticated: &authenticated,
212 Actions: map[operation]batchAction{ 253 Actions: map[operation]batchAction{
213 operationUpload: { 254 operationUpload: {
214 HRef: presigned.String(), 255 Header: map[string]string{
256 "Authorization": "Bearer " + ss,
257 },
258 HRef: uploadHRef,
215 ExpiresIn: int64(expiresIn.Seconds()), 259 ExpiresIn: int64(expiresIn.Seconds()),
216 }, 260 },
217 }, 261 },
218 } 262 }
219} 263}
220 264
265type validatingReader struct {
266 promisedSize int64
267 promisedSha256 []byte
268
269 reader io.Reader
270 bytesRead int64
271 current hash.Hash
272 err error
273}
274
275func newValidatingReader(promisedSize int64, promisedSha256 []byte, r io.Reader) *validatingReader {
276 return &validatingReader{
277 promisedSize: promisedSize,
278 promisedSha256: promisedSha256,
279 reader: r,
280 current: sha256.New(),
281 }
282}
283
284var errTooBig = errors.New("validator: uploaded file bigger than indicated")
285var errTooSmall = errors.New("validator: uploaded file smaller than indicated")
286var errBadSum = errors.New("validator: bad checksum provided or file corrupted")
287
288func (i *validatingReader) Read(b []byte) (int, error) {
289 if i.err != nil {
290 return 0, i.err
291 }
292 n, err := i.reader.Read(b)
293 i.bytesRead += int64(n)
294 if i.bytesRead > i.promisedSize {
295 i.err = errTooBig
296 return 0, i.err
297 }
298 if err != nil && errors.Is(err, io.EOF) {
299 if i.bytesRead < i.promisedSize {
300 i.err = errTooSmall
301 return n, i.err
302 }
303 }
304 // According to the documentation, Hash.Write never returns an error
305 i.current.Write(b[:n])
306 if i.bytesRead == i.promisedSize {
307 if !bytes.Equal(i.promisedSha256, i.current.Sum(nil)) {
308 i.err = errBadSum
309 return 0, i.err
310 }
311 }
312 return n, err
313}
314
315func (h *handler) handlePutObject(w http.ResponseWriter, r *http.Request, repo, oid string) {
316 ctx := r.Context()
317
318 authz := r.Header.Get("Authorization")
319 if authz == "" {
320 makeRespError(ctx, w, "Missing Authorization header", http.StatusBadRequest)
321 return
322 }
323 if !strings.HasPrefix(authz, "Bearer ") {
324 makeRespError(ctx, w, "Invalid Authorization header", http.StatusBadRequest)
325 return
326 }
327 authz = strings.TrimPrefix(authz, "Bearer ")
328
329 var claims uploadObjectCustomClaims
330 _, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) {
331 if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
332 return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"])
333 }
334 return h.privateKey.Public(), nil
335 })
336 if err != nil {
337 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
338 return
339 }
340 if claims.Gitolfs3.Repository != repo {
341 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
342 return
343 }
344 if claims.Gitolfs3.OID != oid {
345 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
346 return
347 }
348
349 // Check with claims
350 if lengthStr := r.Header.Get("Content-Length"); lengthStr != "" {
351 length, err := strconv.ParseInt(lengthStr, 10, 64)
352 if err != nil {
353 makeRespError(ctx, w, "Bad Content-Length format", http.StatusBadRequest)
354 return
355 }
356 if length != claims.Gitolfs3.Size {
357 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
358 return
359 }
360 }
361
362 sha256Raw, err := hex.DecodeString(oid)
363 if err != nil || len(sha256Raw) != sha256.Size {
364 makeRespError(ctx, w, "Invalid OID", http.StatusBadRequest)
365 return
366 }
367
368 reader := newValidatingReader(claims.Gitolfs3.Size, sha256Raw, r.Body)
369
370 fullPath := path.Join(repo+".git", "lfs/objects", oid[:2], oid[2:4], oid)
371 _, err = h.mc.PutObject(ctx, h.bucket, fullPath, reader, int64(claims.Gitolfs3.Size), minio.PutObjectOptions{
372 SendContentMd5: true,
373 })
374 if err != nil {
375 makeRespError(ctx, w, "Failed to upload object", http.StatusInternalServerError)
376 return
377 }
378}
379
221type parsedBatchObject struct { 380type parsedBatchObject struct {
222 firstByte string 381 firstByte string
223 secondByte string 382 secondByte string
224 fullHash string 383 fullHash string
225 size uint64 384 size int64
226} 385}
227 386
228func isLFSMediaType(t string) bool { 387func isLFSMediaType(t string) bool {
@@ -236,20 +395,21 @@ func isLFSMediaType(t string) bool {
236 return false 395 return false
237} 396}
238 397
239var re = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/batch$`) 398var reBatchAPI = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/batch$`)
399var reObjUpload = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/([0-9a-f]{2})/([0-9a-f]{2})/([0-9a-f]{2}){64}$`)
240 400
241type requestID struct{} 401type requestID struct{}
242 402
243var requestIDKey requestID 403var requestIDKey requestID
244 404
245// TODO: make a shared package for this 405// TODO: make a shared package for this
246type gitolfs3Claims struct { 406type lfsAuthGitolfs3Claims struct {
247 Repository string `json:"repository"` 407 Repository string `json:"repository"`
248 Permission operation `json:"permission"` 408 Permission operation `json:"permission"`
249} 409}
250 410
251type customClaims struct { 411type lfsAuthCustomClaims struct {
252 Gitolfs3 gitolfs3Claims `json:"gitolfs3"` 412 Gitolfs3 lfsAuthGitolfs3Claims `json:"gitolfs3"`
253 *jwt.RegisteredClaims 413 *jwt.RegisteredClaims
254} 414}
255 415
@@ -278,8 +438,9 @@ func (h *handler) getGitoliteAccess(repo, user, gitolitePerm string, refspec *st
278 return true, nil 438 return true, nil
279} 439}
280 440
281func (h *handler) authorize(ctx context.Context, w http.ResponseWriter, r *http.Request, or operationRequest) bool { 441func (h *handler) authorize(w http.ResponseWriter, r *http.Request, or operationRequest) bool {
282 user := h.anonUser 442 user := h.anonUser
443 ctx := r.Context()
283 444
284 if authz := r.Header.Get("Authorization"); authz != "" { 445 if authz := r.Header.Get("Authorization"); authz != "" {
285 if !strings.HasPrefix(authz, "Bearer ") { 446 if !strings.HasPrefix(authz, "Bearer ") {
@@ -288,12 +449,12 @@ func (h *handler) authorize(ctx context.Context, w http.ResponseWriter, r *http.
288 } 449 }
289 authz = strings.TrimPrefix(authz, "Bearer ") 450 authz = strings.TrimPrefix(authz, "Bearer ")
290 451
291 var claims customClaims 452 var claims lfsAuthCustomClaims
292 _, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) { 453 _, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) {
293 if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok { 454 if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
294 return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"]) 455 return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"])
295 } 456 }
296 return h.publicKey, nil 457 return h.privateKey.Public(), nil
297 }) 458 })
298 if err != nil { 459 if err != nil {
299 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) 460 makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
@@ -339,29 +500,8 @@ func (h *handler) authorize(ctx context.Context, w http.ResponseWriter, r *http.
339 return true 500 return true
340} 501}
341 502
342func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 503func (h *handler) handleBatchAPI(w http.ResponseWriter, r *http.Request, repo string) {
343 ctx := context.WithValue(r.Context(), requestIDKey, xid.New().String()) 504 ctx := r.Context()
344
345 if r.Method != http.MethodPost {
346 makeRespError(ctx, w, "Method not allowed", http.StatusMethodNotAllowed)
347 return
348 }
349
350 reqPath := os.Getenv("PATH_INFO")
351 if reqPath == "" {
352 reqPath = r.URL.Path
353 }
354 reqlog(ctx, "reqPath: %s", reqPath)
355 reqPath = strings.TrimPrefix(path.Clean(reqPath), "/")
356 reqlog(ctx, "Cleaned reqPath: %s", reqPath)
357 submatches := re.FindStringSubmatch(reqPath)
358 if len(submatches) != 2 {
359 reqlog(ctx, "Got path: %s, did not match regex", reqPath)
360 makeRespError(ctx, w, "Not found", http.StatusNotFound)
361 return
362 }
363 repo := strings.TrimPrefix(path.Clean(submatches[1]), "/")
364 reqlog(ctx, "Repository: %s", repo)
365 505
366 if !slices.ContainsFunc(r.Header.Values("Accept"), isLFSMediaType) { 506 if !slices.ContainsFunc(r.Header.Values("Accept"), isLFSMediaType) {
367 makeRespError(ctx, w, "Expected "+lfsMIME+" (with UTF-8 charset) in list of acceptable response media types", http.StatusNotAcceptable) 507 makeRespError(ctx, w, "Expected "+lfsMIME+" (with UTF-8 charset) in list of acceptable response media types", http.StatusNotAcceptable)
@@ -389,7 +529,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
389 if body.Ref != nil { 529 if body.Ref != nil {
390 or.refspec = &body.Ref.Name 530 or.refspec = &body.Ref.Name
391 } 531 }
392 if !h.authorize(ctx, w, r, or) { 532 if !h.authorize(w, r.WithContext(ctx), or) {
393 return 533 return
394 } 534 }
395 535
@@ -427,7 +567,9 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
427 case operationDownload: 567 case operationDownload:
428 resp.Objects = append(resp.Objects, h.handleDownloadObject(ctx, repo, obj)) 568 resp.Objects = append(resp.Objects, h.handleDownloadObject(ctx, repo, obj))
429 case operationUpload: 569 case operationUpload:
430 resp.Objects = append(resp.Objects, h.handleUploadObject(ctx, repo, obj)) 570 if respObj := h.handleUploadObject(ctx, repo, obj); respObj != nil {
571 resp.Objects = append(resp.Objects, *respObj)
572 }
431 } 573 }
432 } 574 }
433 575
@@ -436,6 +578,53 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
436 json.NewEncoder(w).Encode(resp) 578 json.NewEncoder(w).Encode(resp)
437} 579}
438 580
581func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
582 ctx := context.WithValue(r.Context(), requestIDKey, xid.New().String())
583
584 reqPath := os.Getenv("PATH_INFO")
585 if reqPath == "" {
586 reqPath = r.URL.Path
587 }
588 reqPath = strings.TrimPrefix(path.Clean(reqPath), "/")
589
590 if submatches := reBatchAPI.FindStringSubmatch(reqPath); len(submatches) == 2 {
591 repo := strings.TrimPrefix(path.Clean(submatches[1]), "/")
592 reqlog(ctx, "Repository: %s", repo)
593
594 if r.Method != http.MethodPost {
595 makeRespError(ctx, w, "Method not allowed", http.StatusMethodNotAllowed)
596 return
597 }
598
599 h.handleBatchAPI(w, r.WithContext(ctx), repo)
600 return
601 }
602
603 if submatches := reObjUpload.FindStringSubmatch(reqPath); len(submatches) == 5 {
604 repo := strings.TrimPrefix(path.Clean(submatches[1]), "/")
605 oid0, oid1, oid := submatches[2], submatches[3], submatches[4]
606
607 if !isValidSHA256Hash(oid) {
608 panic("Regex should only allow valid SHA256 hashes")
609 }
610 if oid0 != oid[:2] || oid1 != oid[2:4] {
611 makeRespError(ctx, w, "Bad URL format: malformed OID pattern", http.StatusBadRequest)
612 return
613 }
614 reqlog(ctx, "Repository: %s; OID: %s", repo, oid)
615
616 if r.Method != http.MethodPost {
617 makeRespError(ctx, w, "Method not allowed", http.StatusMethodNotAllowed)
618 return
619 }
620
621 h.handleBatchAPI(w, r.WithContext(ctx), repo)
622 return
623 }
624
625 makeRespError(ctx, w, "Not found", http.StatusNotFound)
626}
627
439func reqlog(ctx context.Context, msg string, args ...any) { 628func reqlog(ctx context.Context, msg string, args ...any) {
440 fmt.Fprint(os.Stderr, "[gitolfs3] ") 629 fmt.Fprint(os.Stderr, "[gitolfs3] ")
441 if val := ctx.Value(requestIDKey); val != nil { 630 if val := ctx.Value(requestIDKey); val != nil {
@@ -460,31 +649,38 @@ func die(msg string, args ...any) {
460 os.Exit(1) 649 os.Exit(1)
461} 650}
462 651
463func loadPublicKey(path string) ed25519.PublicKey { 652func loadPrivateKey(path string) ed25519.PrivateKey {
464 raw, err := os.ReadFile(path) 653 raw, err := os.ReadFile(path)
465 if err != nil { 654 if err != nil {
466 die("Failed to open specified public key: %s", err) 655 die("Failed to open specified public key: %s", err)
467 } 656 }
468 raw = bytes.TrimSpace(raw) 657 raw = bytes.TrimSpace(raw)
469 658
470 if hex.DecodedLen(len(raw)) != ed25519.PublicKeySize { 659 if hex.DecodedLen(len(raw)) != ed25519.SeedSize {
471 die("Specified public key file does not contain key of appropriate length") 660 die("Specified public key file does not contain key (seed) of appropriate length")
472 } 661 }
473 decoded := make([]byte, hex.DecodedLen(len(raw))) 662 decoded := make([]byte, hex.DecodedLen(len(raw)))
474 if _, err = hex.Decode(decoded, raw); err != nil { 663 if _, err = hex.Decode(decoded, raw); err != nil {
475 die("Failed to decode specified public key: %s", err) 664 die("Failed to decode specified public key: %s", err)
476 } 665 }
477 return decoded 666 return ed25519.NewKeyFromSeed(decoded)
667}
668
669func wipe(b []byte) {
670 for i := range b {
671 b[i] = 0
672 }
478} 673}
479 674
480func main() { 675func main() {
481 anonUser := os.Getenv("ANON_USER") 676 anonUser := os.Getenv("ANON_USER")
482 publicKeyPath := os.Getenv("GITOLFS3_PUBLIC_KEY_PATH") 677 privateKeyPath := os.Getenv("GITOLFS3_PRIVATE_KEY_PATH")
483 endpoint := os.Getenv("S3_ENDPOINT") 678 endpoint := os.Getenv("S3_ENDPOINT")
484 bucket := os.Getenv("S3_BUCKET") 679 bucket := os.Getenv("S3_BUCKET")
485 accessKeyIDFile := os.Getenv("S3_ACCESS_KEY_ID_FILE") 680 accessKeyIDFile := os.Getenv("S3_ACCESS_KEY_ID_FILE")
486 secretAccessKeyFile := os.Getenv("S3_SECRET_ACCESS_KEY_FILE") 681 secretAccessKeyFile := os.Getenv("S3_SECRET_ACCESS_KEY_FILE")
487 gitolitePath := os.Getenv("GITOLITE_PATH") 682 gitolitePath := os.Getenv("GITOLITE_PATH")
683 baseURLStr := os.Getenv("BASE_URL")
488 684
489 if gitolitePath == "" { 685 if gitolitePath == "" {
490 gitolitePath = "gitolite" 686 gitolitePath = "gitolite"
@@ -493,8 +689,11 @@ func main() {
493 if anonUser == "" { 689 if anonUser == "" {
494 die("Fatal: expected environment variable ANON_USER to be set") 690 die("Fatal: expected environment variable ANON_USER to be set")
495 } 691 }
496 if publicKeyPath == "" { 692 if privateKeyPath == "" {
497 die("Fatal: expected environment variable GITOLFS3_PUBLIC_KEY_PATH to be set") 693 die("Fatal: expected environment variable GITOLFS3_PRIVATE_KEY_PATH to be set")
694 }
695 if baseURLStr == "" {
696 die("Fatal: expected environment variable BASE_URL to be set")
498 } 697 }
499 if endpoint == "" { 698 if endpoint == "" {
500 die("Fatal: expected environment variable S3_ENDPOINT to be set") 699 die("Fatal: expected environment variable S3_ENDPOINT to be set")
@@ -519,7 +718,13 @@ func main() {
519 die("Fatal: failed to read secret access key from specified file: %s", err) 718 die("Fatal: failed to read secret access key from specified file: %s", err)
520 } 719 }
521 720
522 publicKey := loadPublicKey(publicKeyPath) 721 privateKey := loadPrivateKey(privateKeyPath)
722 defer wipe(privateKey)
723
724 baseURL, err := url.Parse(baseURLStr)
725 if err != nil {
726 die("Fatal: provided BASE_URL has bad format: %s", err)
727 }
523 728
524 mc, err := minio.New(endpoint, &minio.Options{ 729 mc, err := minio.New(endpoint, &minio.Options{
525 Creds: credentials.NewStaticV4(string(accessKeyID), string(secretAccessKey), ""), 730 Creds: credentials.NewStaticV4(string(accessKeyID), string(secretAccessKey), ""),
@@ -529,7 +734,7 @@ func main() {
529 die("Fatal: failed to create S3 client: %s", err) 734 die("Fatal: failed to create S3 client: %s", err)
530 } 735 }
531 736
532 if err = cgi.Serve(&handler{mc, bucket, anonUser, gitolitePath, publicKey}); err != nil { 737 if err = cgi.Serve(&handler{mc, bucket, anonUser, gitolitePath, privateKey, baseURL}); err != nil {
533 die("Fatal: failed to serve CGI: %s", err) 738 die("Fatal: failed to serve CGI: %s", err)
534 } 739 }
535} 740}