Enable returning partial HTTPResponses

This commit is contained in:
Marius 2021-11-30 17:03:58 +01:00
parent 47c61b02f7
commit f4ccd53ba5
5 changed files with 84 additions and 30 deletions

View File

@ -2,7 +2,6 @@ package cli
import ( import (
"errors" "errors"
"fmt"
"strconv" "strconv"
"strings" "strings"
@ -151,14 +150,14 @@ func invokeHookSync(typ hooks.HookType, event handler.HookEvent) (httpRes handle
}) })
if err != nil { if err != nil {
err = fmt.Errorf("%s hook failed: %s", typ, err) //err = fmt.Errorf("%s hook failed: %s", typ, err)
logEv(stderr, "HookInvocationError", "type", string(typ), "id", id, "error", err.Error()) logEv(stderr, "HookInvocationError", "type", string(typ), "id", id, "error", err.Error())
MetricsHookErrorsTotal.WithLabelValues(string(typ)).Add(1) MetricsHookErrorsTotal.WithLabelValues(string(typ)).Add(1)
} else if Flags.VerboseOutput { } else if Flags.VerboseOutput {
logEv(stdout, "HookInvocationFinish", "type", string(typ), "id", id) logEv(stdout, "HookInvocationFinish", "type", string(typ), "id", id)
} }
// IDEA: PreHooks work like this: error return value does not carry HTTP response information // IDEA: PreHooks work like this: error return value does carry HTTP response information for error
// Instead the additional HTTP response return value // Instead the additional HTTP response return value
httpRes = hookRes.HTTPResponse httpRes = hookRes.HTTPResponse
@ -171,12 +170,10 @@ func invokeHookSync(typ hooks.HookType, event handler.HookEvent) (httpRes handle
// If the hook response includes the instruction to reject the upload, reuse the error code // If the hook response includes the instruction to reject the upload, reuse the error code
// and message from ErrUploadRejectedByServer, but also include custom HTTP response values // and message from ErrUploadRejectedByServer, but also include custom HTTP response values
if typ == hooks.HookPreCreate && hookRes.RejectUpload { if typ == hooks.HookPreCreate && hookRes.RejectUpload {
// TODO: Merge httpRes with default of ErrUploadRejected, so we always have a response code. err := handler.ErrUploadRejectedByServer
return httpRes, handler.Error{ err.HTTPResponse = err.HTTPResponse.MergeWith(httpRes)
ErrorCode: handler.ErrUploadRejectedByServer.ErrorCode,
Message: handler.ErrUploadRejectedByServer.Message, return httpRes, err
HTTPResponse: httpRes,
}
} }
if typ == hooks.HookPostReceive && hookRes.StopUpload { if typ == hooks.HookPostReceive && hookRes.StopUpload {

View File

@ -1,5 +1,7 @@
package hooks package hooks
// TODO: Move hooks into a package in /pkg
import ( import (
"github.com/tus/tusd/pkg/handler" "github.com/tus/tusd/pkg/handler"
) )

View File

@ -7,8 +7,12 @@ import (
"os/exec" "os/exec"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
"github.com/tus/tusd/pkg/handler"
) )
// TODO: When the tusd process stops, the plugin does not get properly killed
// and lives on as a zombie process.
type PluginHook struct { type PluginHook struct {
Path string Path string
@ -54,8 +58,8 @@ func (h *PluginHook) InvokeHook(req HookRequest) (HookResponse, error) {
// directory. It is a UX feature, not a security feature. // directory. It is a UX feature, not a security feature.
var handshakeConfig = plugin.HandshakeConfig{ var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1, ProtocolVersion: 1,
MagicCookieKey: "BASIC_PLUGIN", MagicCookieKey: "TUSD_PLUGIN",
MagicCookieValue: "hello", MagicCookieValue: "yes",
} }
// pluginMap is the map of plugins we can dispense. // pluginMap is the map of plugins we can dispense.
@ -63,19 +67,35 @@ var pluginMap = map[string]plugin.Plugin{
"hookHandler": &HookHandlerPlugin{}, "hookHandler": &HookHandlerPlugin{},
} }
// TODO: Explain, mention that it is internal only
// TODO: Do we actually need this? Maybe not...
type InvokeHookRPCAnswer struct {
HookResponse HookResponse
TusdError *handler.Error // Why is TusdError a pointer
}
// Here is an implementation that talks over RPC // Here is an implementation that talks over RPC
type HookHandlerRPC struct{ client *rpc.Client } type HookHandlerRPC struct{ client *rpc.Client }
func (g *HookHandlerRPC) Setup() error { func (g *HookHandlerRPC) Setup() error {
var res interface{} var res interface{}
err := g.client.Call("Plugin.Setup", new(interface{}), &res) err := g.client.Call("Plugin.Setup", new(interface{}), &res)
fmt.Println("after Setup")
return err return err
} }
func (g *HookHandlerRPC) InvokeHook(req HookRequest) (res HookResponse, err error) { func (g *HookHandlerRPC) InvokeHook(req HookRequest) (HookResponse, error) {
err = g.client.Call("Plugin.InvokeHook", req, &res) var answer InvokeHookRPCAnswer
return res, err err := g.client.Call("Plugin.InvokeHook", req, &answer)
fmt.Printf("Client: %#v\n", answer.TusdError)
if err != nil {
return answer.HookResponse, err
}
if answer.TusdError != nil {
return answer.HookResponse, *answer.TusdError
}
return answer.HookResponse, nil
} }
// Here is the RPC server that HookHandlerRPC talks to, conforming to // Here is the RPC server that HookHandlerRPC talks to, conforming to
@ -89,9 +109,21 @@ func (s *HookHandlerRPCServer) Setup(args interface{}, resp *interface{}) error
return s.Impl.Setup() return s.Impl.Setup()
} }
func (s *HookHandlerRPCServer) InvokeHook(args HookRequest, resp *HookResponse) (err error) { func (s *HookHandlerRPCServer) InvokeHook(args HookRequest, answer *InvokeHookRPCAnswer) error {
*resp, err = s.Impl.InvokeHook(args) resp, err := s.Impl.InvokeHook(args)
return err if err != nil {
if tusdErr, ok := err.(handler.Error); ok {
answer.TusdError = &tusdErr
return nil
} else {
return err
}
}
answer.HookResponse = resp
return nil
} }
// This is the implementation of plugin.Plugin so we can serve/consume this // This is the implementation of plugin.Plugin so we can serve/consume this

View File

@ -26,6 +26,14 @@ func (g *MyHookHandler) InvokeHook(req hooks.HookRequest) (res hooks.HookRespons
if req.Type == hooks.HookPreCreate { if req.Type == hooks.HookPreCreate {
res.HTTPResponse.Headers["X-From-Pre-Create"] = "hello" res.HTTPResponse.Headers["X-From-Pre-Create"] = "hello"
if req.Event.Upload.Size > 10 {
res.HTTPResponse.StatusCode = 413
res.HTTPResponse.Body = `{"error":"upload size is too large"}`
res.RejectUpload = true
return res, nil
}
} }
if req.Type == hooks.HookPreFinish { if req.Type == hooks.HookPreFinish {
@ -42,8 +50,8 @@ func (g *MyHookHandler) InvokeHook(req hooks.HookRequest) (res hooks.HookRespons
// directory. It is a UX feature, not a security feature. // directory. It is a UX feature, not a security feature.
var handshakeConfig = plugin.HandshakeConfig{ var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1, ProtocolVersion: 1,
MagicCookieKey: "BASIC_PLUGIN", MagicCookieKey: "TUSD_PLUGIN",
MagicCookieValue: "hello", MagicCookieValue: "yes",
} }
func main() { func main() {

View File

@ -118,18 +118,33 @@ func (resp HTTPResponse) writeTo(w http.ResponseWriter) {
} }
} }
func (resp *HTTPResponse) MergeWith(resp2 HTTPResponse) { func (resp1 HTTPResponse) MergeWith(resp2 HTTPResponse) HTTPResponse {
if resp2.StatusCode != 0 { // Clone the response 1 and use it as a basis
resp.StatusCode = resp2.StatusCode newResp := resp1
}
for key, value := range resp2.Headers { // Take the status code and body from response 2 to
resp.Headers[key] = value // overwrite values from response 1.
if resp2.StatusCode != 0 {
newResp.StatusCode = resp2.StatusCode
} }
if len(resp2.Body) > 0 { if len(resp2.Body) > 0 {
resp.Body = resp2.Body newResp.Body = resp2.Body
} }
// For the headers, me must make a new map to avoid writing
// into the header map from response 1.
newResp.Headers = make(HTTPHeaders, len(resp1.Headers)+len(resp2.Headers))
for key, value := range resp1.Headers {
newResp.Headers[key] = value
}
for key, value := range resp2.Headers {
newResp.Headers[key] = value
}
return newResp
} }
// HookEvent represents an event from tusd which can be handled by the application. // HookEvent represents an event from tusd which can be handled by the application.
@ -400,7 +415,7 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
handler.sendError(w, r, err) handler.sendError(w, r, err)
return return
} }
resp.MergeWith(resp2) resp = resp.MergeWith(resp2)
} }
upload, err := handler.composer.Core.NewUpload(ctx, info) upload, err := handler.composer.Core.NewUpload(ctx, info)
@ -770,7 +785,7 @@ func (handler *UnroutedHandler) finishUploadIfComplete(ctx context.Context, uplo
if err != nil { if err != nil {
return resp, err return resp, err
} }
resp.MergeWith(resp2) resp = resp.MergeWith(resp2)
} }
} }
@ -1035,7 +1050,7 @@ func (handler *UnroutedHandler) sendError(w http.ResponseWriter, r *http.Request
func (handler *UnroutedHandler) sendResp(w http.ResponseWriter, r *http.Request, resp HTTPResponse) { func (handler *UnroutedHandler) sendResp(w http.ResponseWriter, r *http.Request, resp HTTPResponse) {
resp.writeTo(w) resp.writeTo(w)
handler.log("ResponseOutgoing", "status", strconv.Itoa(resp.StatusCode), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r)) handler.log("ResponseOutgoing", "status", strconv.Itoa(resp.StatusCode), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r), "body", resp.Body)
} }
// Make an absolute URLs to the given upload id. If the base path is absolute // Make an absolute URLs to the given upload id. If the base path is absolute