ctx, cancel := contextForResponse(context.TODO(), resp)
defer cancel()
+ // Intervening proxies must not return a cached GET response
+ // to a prior request if a X-Keep-Signature request header has
+ // been added or changed.
+ resp.Header().Add("Vary", "X-Keep-Signature")
+
locator := req.URL.Path[1:]
if strings.Contains(locator, "+R") && !strings.Contains(locator, "+A") {
- rtr.remoteProxy.Get(resp, req, rtr.cluster)
+ rtr.remoteProxy.Get(ctx, resp, req, rtr.cluster)
return
}
package main
import (
+ "context"
+ "errors"
"io"
"net/http"
+ "regexp"
"strings"
"sync"
+ "time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
mtx sync.Mutex
}
-func (rp *remoteProxy) Get(w http.ResponseWriter, r *http.Request, cluster *arvados.Cluster) {
+func (rp *remoteProxy) Get(ctx context.Context, w http.ResponseWriter, r *http.Request, cluster *arvados.Cluster) {
+ token := GetAPIToken(r)
+ if token == "" {
+ http.Error(w, "no token provided in Authorization header", http.StatusUnauthorized)
+ return
+ }
+ if sign := r.Header.Get("X-Keep-Signature"); sign != "" {
+ buf, err := getBufferWithContext(ctx, bufs, BlockSize)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusServiceUnavailable)
+ return
+ }
+ defer bufs.Put(buf)
+ rrc := &remoteResponseCacher{
+ Locator: r.URL.Path[1:],
+ Token: token,
+ Buffer: buf[:0],
+ ResponseWriter: w,
+ }
+ defer rrc.Flush(ctx)
+ w = rrc
+ }
var remoteClient *keepclient.KeepClient
var parts []string
for i, part := range strings.Split(r.URL.Path[1:], "+") {
http.Error(w, "remote cluster not configured", http.StatusBadGateway)
return
}
- token := GetAPIToken(r)
- if token == "" {
- http.Error(w, "no token provided in Authorization header", http.StatusUnauthorized)
- return
- }
kc, err := rp.remoteClient(remoteID, remote, token)
if err == auth.ErrObsoleteToken {
http.Error(w, err.Error(), http.StatusBadRequest)
kccopy.Arvados.ApiToken = token
return &kccopy, nil
}
+
+var localOrRemoteSignature = regexp.MustCompile(`\+[AR][^\+]*`)
+
+// remoteResponseCacher wraps http.ResponseWriter. It buffers the
+// response data in the provided buffer, writes/touches a copy on a
+// local volume, adds a response header with a locally-signed locator,
+// and finally writes the data through.
+type remoteResponseCacher struct {
+ Locator string
+ Token string
+ Buffer []byte
+ http.ResponseWriter
+ statusCode int
+}
+
+func (rrc *remoteResponseCacher) Write(p []byte) (int, error) {
+ if len(rrc.Buffer)+len(p) > cap(rrc.Buffer) {
+ return 0, errors.New("buffer full")
+ }
+ rrc.Buffer = append(rrc.Buffer, p...)
+ return len(p), nil
+}
+
+func (rrc *remoteResponseCacher) WriteHeader(statusCode int) {
+ rrc.statusCode = statusCode
+}
+
+func (rrc *remoteResponseCacher) Flush(ctx context.Context) {
+ if rrc.statusCode == 0 {
+ rrc.statusCode = http.StatusOK
+ } else if rrc.statusCode != http.StatusOK {
+ rrc.ResponseWriter.WriteHeader(rrc.statusCode)
+ rrc.ResponseWriter.Write(rrc.Buffer)
+ return
+ }
+ _, err := PutBlock(ctx, rrc.Buffer, rrc.Locator[:32])
+ if err == RequestHashError {
+ http.Error(rrc.ResponseWriter, "checksum mismatch in remote response", http.StatusBadGateway)
+ return
+ }
+ if err, ok := err.(*KeepError); ok {
+ http.Error(rrc.ResponseWriter, err.Error(), err.HTTPCode)
+ return
+ }
+ if err != nil {
+ http.Error(rrc.ResponseWriter, err.Error(), http.StatusBadGateway)
+ return
+ }
+
+ unsigned := localOrRemoteSignature.ReplaceAllLiteralString(rrc.Locator, "")
+ signed := SignLocator(unsigned, rrc.Token, time.Now().Add(theConfig.BlobSignatureTTL.Duration()))
+ if signed == unsigned {
+ http.Error(rrc.ResponseWriter, "could not sign locator", http.StatusInternalServerError)
+ return
+ }
+ rrc.Header().Set("X-Keep-Locator", signed)
+ rrc.ResponseWriter.WriteHeader(rrc.statusCode)
+ rrc.ResponseWriter.Write(rrc.Buffer)
+}
KeepVM = s.vm
theConfig = DefaultConfig()
theConfig.systemAuthToken = arvadostest.DataManagerToken
+ theConfig.blobSigningKey = []byte(knownKey)
theConfig.Start()
s.rtr = MakeRESTRouter(s.cluster)
}
for _, trial := range []struct {
label string
+ method string
token string
+ xKeepSignature string
expectRemoteReqs int64
expectCode int
+ expectSignature bool
}{
{
- label: "happy path",
+ label: "GET only",
+ method: "GET",
token: arvadostest.ActiveTokenV2,
expectRemoteReqs: 1,
expectCode: http.StatusOK,
},
{
label: "obsolete token",
+ method: "GET",
token: arvadostest.ActiveToken,
expectRemoteReqs: 0,
expectCode: http.StatusBadRequest,
},
{
label: "bad token",
+ method: "GET",
token: arvadostest.ActiveTokenV2[:len(arvadostest.ActiveTokenV2)-3] + "xxx",
expectRemoteReqs: 1,
expectCode: http.StatusNotFound,
},
+ {
+ label: "HEAD only",
+ method: "HEAD",
+ token: arvadostest.ActiveTokenV2,
+ expectRemoteReqs: 1,
+ expectCode: http.StatusOK,
+ },
+ {
+ label: "HEAD with local signature",
+ method: "HEAD",
+ xKeepSignature: "local, time=" + time.Now().Format(time.RFC3339),
+ token: arvadostest.ActiveTokenV2,
+ expectRemoteReqs: 1,
+ expectCode: http.StatusOK,
+ expectSignature: true,
+ },
+ {
+ label: "GET with local signature",
+ method: "GET",
+ xKeepSignature: "local, time=" + time.Now().Format(time.RFC3339),
+ token: arvadostest.ActiveTokenV2,
+ expectRemoteReqs: 1,
+ expectCode: http.StatusOK,
+ expectSignature: true,
+ },
} {
c.Logf("trial: %s", trial.label)
var req *http.Request
var resp *httptest.ResponseRecorder
- req = httptest.NewRequest("GET", path, nil)
+ req = httptest.NewRequest(trial.method, path, nil)
req.Header.Set("Authorization", "Bearer "+trial.token)
+ if trial.xKeepSignature != "" {
+ req.Header.Set("X-Keep-Signature", trial.xKeepSignature)
+ }
resp = httptest.NewRecorder()
s.rtr.ServeHTTP(resp, req)
c.Check(s.remoteKeepRequests, check.Equals, trial.expectRemoteReqs)
} else {
c.Check(resp.Body.String(), check.Not(check.Equals), string(data))
}
+
+ c.Check(resp.Header().Get("Vary"), check.Matches, `(.*, )?X-Keep-Signature(, .*)?`)
+
+ locHdr := resp.Header().Get("X-Keep-Locator")
+ if !trial.expectSignature {
+ c.Check(locHdr, check.Equals, "")
+ continue
+ }
+
+ c.Check(locHdr, check.Not(check.Equals), "")
+ c.Check(locHdr, check.Not(check.Matches), `.*\+R.*`)
+ c.Check(VerifySignature(locHdr, trial.token), check.IsNil)
+
+ // Ensure block can be requested using new signature
+ req = httptest.NewRequest("GET", "/"+locHdr, nil)
+ req.Header.Set("Authorization", "Bearer "+trial.token)
+ resp = httptest.NewRecorder()
+ s.rtr.ServeHTTP(resp, req)
+ c.Check(resp.Code, check.Equals, http.StatusOK)
+ c.Check(s.remoteKeepRequests, check.Equals, trial.expectRemoteReqs)
}
}