clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

stop multipart hashing on disconnect and require a credential on the repair proxy

Dami committed 741d106
commit
741d106cbfcf999deb12dda48b43356ccf4b0bda
parent
7d30ccdf656cc6881f36e27661401c11b7ac51c0
author
Dami <dami.etoile@gmail.com>
4 files changed, +59 -21
Minternal/blob/blob.go2+-
Minternal/blob/multipart.go28+++++++++++++++++++++++-----
Minternal/blob/multipart_review_test.go24++++++++++++------------
Minternal/gitrelay/pr_budget_proxy.go26+++++++++++++++++++++++---
M internal/blob/blob.go +1 -1
M internal/blob/multipart.go +23 -5
@@ -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
M internal/blob/multipart_review_test.go +12 -12
@@ -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)
M internal/gitrelay/pr_budget_proxy.go +23 -3
@@ -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 == "" {