- commit
- 741d106cbfcf999deb12dda48b43356ccf4b0bda
- parent
- 7d30ccdf656cc6881f36e27661401c11b7ac51c0
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 2 | +- |
| M | internal/ | 28 | +++++++++++++++++++++++----- |
| M | internal/ | 24 | ++++++++++++------------ |
| M | internal/ | 26 | +++++++++++++++++++++++--- |
@@ -99,7 +99,7 @@ type Service struct {
9999 multipartFinalizing map[string]bool100100 // verifyMultipartHook is used by package tests to pause finalization101101 // without allocating a GiB fixture. It is nil in production.102- verifyMultipartHook func(string, string, int64) error102+ verifyMultipartHook func(context.Context, string, string, int64) error103103 }104104 105105 var ErrBlocked = errors.New("blocked: this blob was removed by a moderator")@@ -340,7 +340,7 @@ func (s *Service) commitMultipart(ctx context.Context, part multipartRequest, ch
340340 if s.verifyMultipartHook != nil {341341 verify = s.verifyMultipartHook342342 }343- if err := verify(candidate.path, part.hash, part.length); err != nil {343+ if err := verify(ctx, candidate.path, part.hash, part.length); err != nil {344344 s.multipartMu.Lock()345345 delete(s.multipartFinalizing, part.id)346346 // A canceled request must not throw away an otherwise complete session:@@ -428,16 +428,34 @@ func (s *Service) recordMultipartRanges(ctx context.Context, id string, ranges [
428428 return err429429 })430430 }431-func verifyMultipart(path, hash string, size int64) error {431+432+// verifyMultipart hashes the assembled file outside the tenant lock. It checks433+// the request context between reads so a disconnected client stops a long434+// hash instead of holding the session until the hash completes.435+func verifyMultipart(ctx context.Context, path, hash string, size int64) error {432436 file, err := os.Open(path)433437 if err != nil {434438 return err435439 }436440 defer file.Close()437441 hasher := sha256.New()438- count, err := io.Copy(hasher, file)439- if err != nil {440- return err442+ var count int64443+ buffer := make([]byte, 1<<20)444+ for {445+ if err := ctx.Err(); err != nil {446+ return err447+ }448+ n, readErr := file.Read(buffer)449+ if n > 0 {450+ hasher.Write(buffer[:n])451+ count += int64(n)452+ }453+ if errors.Is(readErr, io.EOF) {454+ break455+ }456+ if readErr != nil {457+ return readErr458+ }441459 }442460 if count != size || hex.EncodeToString(hasher.Sum(nil)) != hash {443461 return ErrHashMismatch@@ -114,10 +114,10 @@ func TestMultipartReviewStalledHashDoesNotBlockOrdinaryUpload(t *testing.T) {
114114 sha := hex.EncodeToString(digest[:])115115 started := make(chan struct{})116116 release := make(chan struct{})117- s.verifyMultipartHook = func(path, hash string, size int64) error {117+ s.verifyMultipartHook = func(ctx context.Context, path, hash string, size int64) error {118118 close(started)119119 <-release120- return verifyMultipart(path, hash, size)120+ return verifyMultipart(ctx, path, hash, size)121121 }122122 result := make(chan *httptest.ResponseRecorder, 1)123123 r := multipartReviewRequest(sha, int64(len(body)), 0, body)@@ -160,10 +160,10 @@ func TestMultipartReviewStalledHashDoesNotBlockAnotherSession(t *testing.T) {
160160 release := make(chan struct{})161161 var unblock sync.Once162162 defer unblock.Do(func() { close(release) })163- s.verifyMultipartHook = func(path, hash string, size int64) error {163+ s.verifyMultipartHook = func(ctx context.Context, path, hash string, size int64) error {164164 close(started)165165 <-release166- return verifyMultipart(path, hash, size)166+ return verifyMultipart(ctx, path, hash, size)167167 }168168 first := make(chan *httptest.ResponseRecorder, 1)169169 go func() { first <- record(s.Handler(), multipartReviewRequest(sha, int64(len(body)), 0, body)) }()@@ -207,10 +207,10 @@ func TestMultipartReviewFinalizingSessionRejectsConcurrentChunk(t *testing.T) {
207207 sha := hex.EncodeToString(digest[:])208208 started := make(chan struct{})209209 release := make(chan struct{})210- s.verifyMultipartHook = func(path, hash string, size int64) error {210+ s.verifyMultipartHook = func(ctx context.Context, path, hash string, size int64) error {211211 close(started)212212 <-release213- return verifyMultipart(path, hash, size)213+ return verifyMultipart(ctx, path, hash, size)214214 }215215 first := make(chan *httptest.ResponseRecorder, 1)216216 go func() { first <- record(s.Handler(), multipartReviewRequest(sha, int64(len(body)), 0, body)) }()@@ -236,10 +236,10 @@ func TestMultipartReviewCleanupDoesNotRemoveActiveFinalization(t *testing.T) {
236236 sha := hex.EncodeToString(digest[:])237237 started := make(chan struct{})238238 release := make(chan struct{})239- s.verifyMultipartHook = func(path, hash string, size int64) error {239+ s.verifyMultipartHook = func(ctx context.Context, path, hash string, size int64) error {240240 close(started)241241 <-release242- return verifyMultipart(path, hash, size)242+ return verifyMultipart(ctx, path, hash, size)243243 }244244 result := make(chan *httptest.ResponseRecorder, 1)245245 go func() { result <- record(s.Handler(), multipartReviewRequest(sha, int64(len(body)), 0, body)) }()@@ -278,10 +278,10 @@ func TestMultipartReviewPolicyRecheckedAfterHash(t *testing.T) {
278278 sha := hex.EncodeToString(digest[:])279279 started := make(chan struct{})280280 release := make(chan struct{})281- s.verifyMultipartHook = func(path, hash string, size int64) error {281+ s.verifyMultipartHook = func(ctx context.Context, path, hash string, size int64) error {282282 close(started)283283 <-release284- return verifyMultipart(path, hash, size)284+ return verifyMultipart(ctx, path, hash, size)285285 }286286 s.config.Limits = func() Limits { return Limits{MaxFileBytes: int64(len(body))} }287287 result := make(chan *httptest.ResponseRecorder, 1)@@ -304,10 +304,10 @@ func TestMultipartReviewCanceledHashCanBeRetried(t *testing.T) {
304304 sha := hex.EncodeToString(digest[:])305305 started := make(chan struct{})306306 release := make(chan struct{})307- s.verifyMultipartHook = func(path, hash string, size int64) error {307+ s.verifyMultipartHook = func(ctx context.Context, path, hash string, size int64) error {308308 close(started)309309 <-release310- return verifyMultipart(path, hash, size)310+ return verifyMultipart(ctx, path, hash, size)311311 }312312 ctx, cancel := context.WithCancel(context.Background())313313 r := multipartReviewRequest(sha, int64(len(body)), 0, body).WithContext(ctx)@@ -3,6 +3,10 @@ package gitrelay
33 import (44 "bufio"55 "context"6+ "crypto/rand"7+ "crypto/subtle"8+ "encoding/base64"9+ "encoding/hex"610 "errors"711 "io"812 "net"@@ -29,7 +33,11 @@ func (b *byteBudget) take(wanted, limit int64) int64 {
2933 }3034 3135 type repairProxy struct {32- listener net.Listener36+ listener net.Listener37+ // credential is the Proxy-Authorization value Git presents. The listener38+ // is loopback only, but other local processes cannot borrow the tunnel.39+ credential string40+ url string3341 ctx context.Context3442 cancel context.CancelFunc3543 budget *byteBudget@@ -47,14 +55,22 @@ func newRepairProxy(ctx context.Context, targetIP, targetHost, targetPort string
4755 if err != nil {4856 return nil, err4957 }58+ var secret [16]byte59+ if _, err := rand.Read(secret[:]); err != nil {60+ _ = listener.Close()61+ return nil, err62+ }5063 proxyCtx, cancel := context.WithCancel(ctx)51- p := &repairProxy{listener: listener, ctx: proxyCtx, cancel: cancel, budget: budget, limit: limit, targetIP: targetIP, targetHost: targetHost, targetPort: targetPort, connections: make(map[net.Conn]struct{})}64+ token := hex.EncodeToString(secret[:])65+ credential := "Basic " + base64.StdEncoding.EncodeToString([]byte("tiny:"+token))66+ p := &repairProxy{listener: listener, credential: credential, ctx: proxyCtx, cancel: cancel, budget: budget, limit: limit, targetIP: targetIP, targetHost: targetHost, targetPort: targetPort, connections: make(map[net.Conn]struct{})}67+ p.url = "http://tiny:" + token + "@" + listener.Addr().String()5268 context.AfterFunc(proxyCtx, func() { _ = p.Close() })5369 go p.serve()5470 return p, nil5571 }5672 57-func (p *repairProxy) URL() string { return "http://" + p.listener.Addr().String() }73+func (p *repairProxy) URL() string { return p.url }5874 5975 func (p *repairProxy) Close() error {6076 p.closeOnce.Do(func() {@@ -109,6 +125,10 @@ func (p *repairProxy) handle(client net.Conn) {
109125 if request.Method != http.MethodConnect && request.Method != http.MethodGet && request.Method != http.MethodPost {110126 return111127 }128+ if subtle.ConstantTimeCompare([]byte(request.Header.Get("Proxy-Authorization")), []byte(p.credential)) != 1 {129+ _, _ = io.WriteString(client, "HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"tiny\"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")130+ return131+ }112132 host := request.URL.Hostname()113133 port := request.URL.Port()114134 if port == "" {