Skip to content

Commit c512c7c

Browse files
committed
Add upload-source registry and handler foundation
1 parent d8e7892 commit c512c7c

5 files changed

Lines changed: 1201 additions & 0 deletions

File tree

internal/uploadsource/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package uploadsource holds live public PutObject bodies until a private
2+
// connector claims and streams them exactly once.
3+
package uploadsource

internal/uploadsource/handler.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package uploadsource
2+
3+
import (
4+
"errors"
5+
"io"
6+
"net/http"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/terion-name/air3/internal/ingest"
11+
)
12+
13+
const defaultStreamCopyBufferBytes = 32 * 1024
14+
15+
type HandlerOptions struct {
16+
Registry *Registry
17+
AllowedConnectorIdentities []string
18+
StreamCopyBufferBytes int
19+
}
20+
21+
type Handler struct {
22+
registry *Registry
23+
authorizer ingest.ConnectorAuthorizer
24+
streamCopyBufferBytes int
25+
}
26+
27+
func NewHandler(opts HandlerOptions) (*Handler, error) {
28+
if opts.Registry == nil {
29+
return nil, errors.New("upload source registry is required")
30+
}
31+
streamCopyBufferBytes := opts.StreamCopyBufferBytes
32+
if streamCopyBufferBytes <= 0 {
33+
streamCopyBufferBytes = defaultStreamCopyBufferBytes
34+
}
35+
return &Handler{
36+
registry: opts.Registry,
37+
authorizer: ingest.NewConnectorAuthorizer(opts.AllowedConnectorIdentities),
38+
streamCopyBufferBytes: streamCopyBufferBytes,
39+
}, nil
40+
}
41+
42+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
43+
if r.Method != http.MethodGet {
44+
w.Header().Set("Allow", http.MethodGet)
45+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
46+
return
47+
}
48+
requestID, ok := requestIDFromPath(r.URL.Path)
49+
if !ok {
50+
http.NotFound(w, r)
51+
return
52+
}
53+
if err := h.authorizePeer(r); err != nil {
54+
http.Error(w, "unauthorized connector", http.StatusUnauthorized)
55+
return
56+
}
57+
58+
claim, err := h.registry.Claim(requestID, r.Header.Get(TokenHeader))
59+
if err != nil {
60+
http.Error(w, "upload source rejected", statusForError(err))
61+
return
62+
}
63+
64+
if contentLength := claim.ContentLength(); contentLength != nil {
65+
w.Header().Set("Content-Length", strconv.FormatInt(*contentLength, 10))
66+
}
67+
if contentType := claim.ContentType(); contentType != "" {
68+
w.Header().Set("Content-Type", contentType)
69+
}
70+
71+
tracked := &trackingResponseWriter{ResponseWriter: w}
72+
ctxDone := make(chan struct{})
73+
defer close(ctxDone)
74+
go func() {
75+
select {
76+
case <-r.Context().Done():
77+
_ = claim.CloseWithError(r.Context().Err())
78+
case <-ctxDone:
79+
}
80+
}()
81+
82+
var copyErr error
83+
defer func() { _ = claim.CloseWithError(copyErr) }()
84+
_, copyErr = io.CopyBuffer(tracked, claim, make([]byte, h.streamCopyBufferBytes))
85+
if copyErr != nil && tracked.bytes == 0 {
86+
w.Header().Del("Content-Length")
87+
w.Header().Del("Content-Type")
88+
http.Error(w, "upload source stream failed", http.StatusBadGateway)
89+
}
90+
}
91+
92+
func requestIDFromPath(path string) (string, bool) {
93+
if !strings.HasPrefix(path, PathPrefix) {
94+
return "", false
95+
}
96+
id := strings.TrimPrefix(path, PathPrefix)
97+
if id == "" || strings.Contains(id, "/") || !safeToken(id) {
98+
return "", false
99+
}
100+
return id, true
101+
}
102+
103+
func (h *Handler) authorizePeer(r *http.Request) error {
104+
if r.TLS == nil {
105+
return h.authorizer.AuthorizePeerCertificates(nil)
106+
}
107+
return h.authorizer.AuthorizePeerCertificates(r.TLS.PeerCertificates)
108+
}
109+
110+
func statusForError(err error) int {
111+
switch {
112+
case errors.Is(err, ErrInvalidToken):
113+
return http.StatusUnauthorized
114+
case errors.Is(err, ErrNotFound):
115+
return http.StatusNotFound
116+
case errors.Is(err, ErrExpired), errors.Is(err, ErrCanceled), errors.Is(err, ErrReplayed):
117+
return http.StatusConflict
118+
case errors.Is(err, ErrInvalidSource):
119+
return http.StatusBadRequest
120+
default:
121+
return http.StatusBadGateway
122+
}
123+
}
124+
125+
type trackingResponseWriter struct {
126+
http.ResponseWriter
127+
bytes int64
128+
}
129+
130+
func (w *trackingResponseWriter) Write(p []byte) (int, error) {
131+
n, err := w.ResponseWriter.Write(p)
132+
w.bytes += int64(n)
133+
return n, err
134+
}

0 commit comments

Comments
 (0)