Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for deserialized response #1625

Closed
Closed
90 changes: 11 additions & 79 deletions cmd/booster-http/gateway_handler.go
Original file line number Diff line number Diff line change
@@ -1,105 +1,37 @@
package main

import (
"fmt"
"mime"

"net/http"
"strings"

"github.com/ipfs/boxo/gateway"
)

type gatewayHandler struct {
gwh http.Handler
supportedFormats map[string]struct{}
}

func newGatewayHandler(gw *gateway.BlocksBackend, supportedFormats []string) http.Handler {
func newGatewayHandler(gw *gateway.BlocksBackend, serveGateway string) http.Handler {
headers := map[string][]string{}
var deserializedResponse bool
gateway.AddAccessControlHeaders(headers)

fmtsMap := make(map[string]struct{}, len(supportedFormats))
for _, f := range supportedFormats {
fmtsMap[f] = struct{}{}
if serveGateway == "none" {
return &gatewayHandler{}
}
if serveGateway == "all" {
deserializedResponse = true
} else if serveGateway == "verifiable" {
deserializedResponse = false
}

return &gatewayHandler{
gwh: gateway.NewHandler(gateway.Config{
Headers: headers,
DeserializedResponses: true,
DeserializedResponses: deserializedResponse,
}, gw),
supportedFormats: fmtsMap,
dirkmc marked this conversation as resolved.
Show resolved Hide resolved
}
}

func (h *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
dirkmc marked this conversation as resolved.
Show resolved Hide resolved
responseFormat, _, err := customResponseFormat(r)
if err != nil {
webError(w, fmt.Errorf("error while processing the Accept header: %w", err), http.StatusBadRequest)
return
}

if _, ok := h.supportedFormats[responseFormat]; !ok {
if responseFormat == "" {
responseFormat = "unixfs"
}
webError(w, fmt.Errorf("unsupported response format: %s", responseFormat), http.StatusBadRequest)
return
}

h.gwh.ServeHTTP(w, r)
}

func webError(w http.ResponseWriter, err error, code int) {
http.Error(w, err.Error(), code)
}

// Unfortunately this function is not exported from go-libipfs so we need to copy it here.
// return explicit response format if specified in request as query parameter or via Accept HTTP header
func customResponseFormat(r *http.Request) (mediaType string, params map[string]string, err error) {
if formatParam := r.URL.Query().Get("format"); formatParam != "" {
// translate query param to a content type
switch formatParam {
case "raw":
return "application/vnd.ipld.raw", nil, nil
case "car":
return "application/vnd.ipld.car", nil, nil
case "tar":
return "application/x-tar", nil, nil
case "json":
return "application/json", nil, nil
case "cbor":
return "application/cbor", nil, nil
case "dag-json":
return "application/vnd.ipld.dag-json", nil, nil
case "dag-cbor":
return "application/vnd.ipld.dag-cbor", nil, nil
case "ipns-record":
return "application/vnd.ipfs.ipns-record", nil, nil
}
}
// Browsers and other user agents will send Accept header with generic types like:
// Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
// We only care about explicit, vendor-specific content-types and respond to the first match (in order).
// TODO: make this RFC compliant and respect weights (eg. return CAR for Accept:application/vnd.ipld.dag-json;q=0.1,application/vnd.ipld.car;q=0.2)
for _, header := range r.Header.Values("Accept") {
for _, value := range strings.Split(header, ",") {
accept := strings.TrimSpace(value)
// respond to the very first matching content type
if strings.HasPrefix(accept, "application/vnd.ipld") ||
strings.HasPrefix(accept, "application/x-tar") ||
strings.HasPrefix(accept, "application/json") ||
strings.HasPrefix(accept, "application/cbor") ||
strings.HasPrefix(accept, "application/vnd.ipfs") {
mediatype, params, err := mime.ParseMediaType(accept)
if err != nil {
return "", nil, err
}
return mediatype, params, nil
}
}
}
// If none of special-cased content types is found, return empty string
// to indicate default, implicit UnixFS response should be prepared
return "", nil, nil
}
65 changes: 25 additions & 40 deletions cmd/booster-http/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"errors"
"fmt"
"net/http"
_ "net/http/pprof"
Expand Down Expand Up @@ -76,20 +75,11 @@ var runCmd = &cli.Command{
Usage: "enables serving raw pieces",
Value: true,
},
&cli.BoolFlag{
Name: "serve-blocks",
Usage: "serve blocks with the ipfs gateway API",
Value: true,
},
&cli.BoolFlag{
Name: "serve-cars",
Usage: "serve CAR files with the ipfs gateway API",
Value: true,
},
&cli.BoolFlag{
Name: "serve-files",
Usage: "serve original files (eg jpg, mov) with the ipfs gateway API",
Value: false,
&cli.StringFlag{
Name: "serve-gateway",
Usage: "This flag enables serving deserialized responses with the ipfs gateway API. " +
"Values: 'verifiable' (default) - only serve verifiable responses, 'all' - serve all responses, 'none' - disable serving responses with the ipfs gateway API",
Value: "verifiable",
},
&cli.BoolFlag{
Name: "tracing",
Expand Down Expand Up @@ -117,10 +107,9 @@ var runCmd = &cli.Command{
},
Action: func(cctx *cli.Context) error {
servePieces := cctx.Bool("serve-pieces")
responseFormats := parseSupportedResponseFormats(cctx)
enableIpfsGateway := len(responseFormats) > 0
if !servePieces && !enableIpfsGateway {
return errors.New("one of --serve-pieces, --serve-blocks, etc must be enabled")
enableIpfsGateway, err := shouldEnableIPFSGateway(cctx)
if err != nil {
return err
}

if cctx.Bool("pprof") {
Expand All @@ -136,7 +125,7 @@ var runCmd = &cli.Command{
ctx := lcli.ReqContext(cctx)
cl := bdclient.NewStore()
defer cl.Close(ctx)
err := cl.Dial(ctx, cctx.String("api-lid"))
err = cl.Dial(ctx, cctx.String("api-lid"))
if err != nil {
return fmt.Errorf("connecting to local index directory service: %w", err)
}
Expand Down Expand Up @@ -177,9 +166,10 @@ var runCmd = &cli.Command{
pr := &piecedirectory.SectorAccessorAsPieceReader{SectorAccessor: sa}
pd := piecedirectory.NewPieceDirectory(cl, pr, cctx.Int("add-index-throttle"))

serveGateway := cctx.String("serve-gateway")
opts := &HttpServerOptions{
ServePieces: servePieces,
SupportedResponseFormats: responseFormats,
ServeGateway: serveGateway,
}
if enableIpfsGateway {
repoDir, err := createRepoDir(cctx.String(FlagRepo.Name))
Expand Down Expand Up @@ -262,32 +252,27 @@ var runCmd = &cli.Command{
},
}

func parseSupportedResponseFormats(cctx *cli.Context) []string {
fmts := []string{}
if cctx.Bool("serve-blocks") {
fmts = append(fmts, "application/vnd.ipld.raw")
}
if cctx.Bool("serve-cars") {
fmts = append(fmts, "application/vnd.ipld.car")
func shouldEnableIPFSGateway(cctx *cli.Context) (bool, error) {
switch gatewayType := cctx.String("serve-gateway"); gatewayType {
case "verifiable":
return true, nil
case "all":
return true, nil
case "none":
return false, nil
default:
return false, fmt.Errorf("invalid value for serve-gateway: %s", gatewayType)
}
if cctx.Bool("serve-files") {
// Allow the user to not specify a specific response format.
// In that case the gateway will respond with any kind of file
// (eg jpg, mov etc)
fmts = append(fmts, "")
}
return fmts
}


func ipfsGatewayMsg(cctx *cli.Context, ipfsBasePath string) string {
fmts := []string{}
if cctx.Bool("serve-blocks") {
switch cctx.String("serve-gateway") {
case "verifiable":
fmts = append(fmts, "blocks")
}
if cctx.Bool("serve-cars") {
fmts = append(fmts, "CARs")
}
if cctx.Bool("serve-files") {
case "all":
fmts = append(fmts, "files")
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/booster-http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ type HttpServerApi interface {
type HttpServerOptions struct {
Blockstore blockstore.Blockstore
ServePieces bool
SupportedResponseFormats []string
ServeGateway string
}

func NewHttpServer(path string, listenAddr string, port int, api HttpServerApi, opts *HttpServerOptions) *HttpServer {
Expand Down Expand Up @@ -106,7 +106,7 @@ func (s *HttpServer) Start(ctx context.Context) error {
if err != nil {
return fmt.Errorf("creating blocks gateway: %w", err)
}
handler.Handle(s.ipfsBasePath(), newGatewayHandler(gw, s.opts.SupportedResponseFormats))
handler.Handle(s.ipfsBasePath(), newGatewayHandler(gw, s.opts.ServeGateway))
}

handler.HandleFunc("/", s.handleIndex)
Expand Down
2 changes: 1 addition & 1 deletion docker/devnet/booster-http/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ echo $MINER_API_INFO
echo $LID_API_INFO

echo Starting booster-http...
exec booster-http run --serve-files=true --api-lid=$LID_API_INFO --api-fullnode=$FULLNODE_API_INFO --api-storage=$MINER_API_INFO --tracing
exec booster-http run --serve-gateway=verifiable --api-lid=$LID_API_INFO --api-fullnode=$FULLNODE_API_INFO --api-storage=$MINER_API_INFO --tracing