diff --git a/handler_test.go b/handler_test.go new file mode 100644 index 0000000..2db09ac --- /dev/null +++ b/handler_test.go @@ -0,0 +1,86 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +type rpcRedirectTest struct { + url string + location string + status int +} + +func TestRPCRedirectsToGateway(t *testing.T) { + rpcHandler := newKuboRPCHandler([]string{"http://example.com"}) + + tests := []rpcRedirectTest{ + {"/api/v0/cat?arg=bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", "/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", http.StatusSeeOther}, + {"/api/v0/cat?arg=/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", "/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", http.StatusSeeOther}, + {"/api/v0/cat?arg=/ipns/k51qzi5uqu5dgutdk6i1ynyzgkqngpha5xpgia3a5qqp4jsh0u4csozksxel2r", "/ipns/k51qzi5uqu5dgutdk6i1ynyzgkqngpha5xpgia3a5qqp4jsh0u4csozksxel2r", http.StatusSeeOther}, + + {"/api/v0/dag/get?arg=bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", "/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e?format=dag-json", http.StatusSeeOther}, + {"/api/v0/dag/get?arg=/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", "/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e?format=dag-json", http.StatusSeeOther}, + {"/api/v0/dag/get?arg=/ipns/k51qzi5uqu5dgutdk6i1ynyzgkqngpha5xpgia3a5qqp4jsh0u4csozksxel2r", "/ipns/k51qzi5uqu5dgutdk6i1ynyzgkqngpha5xpgia3a5qqp4jsh0u4csozksxel2r?format=dag-json", http.StatusSeeOther}, + {"/api/v0/dag/get?arg=bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e&output-codec=dag-cbor", "/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e?format=dag-cbor", http.StatusSeeOther}, + + {"/api/v0/dag/export?arg=bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", "/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e?format=car", http.StatusSeeOther}, + {"/api/v0/dag/export?arg=/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", "/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e?format=car", http.StatusSeeOther}, + {"/api/v0/dag/export?arg=/ipns/k51qzi5uqu5dgutdk6i1ynyzgkqngpha5xpgia3a5qqp4jsh0u4csozksxel2r", "/ipns/k51qzi5uqu5dgutdk6i1ynyzgkqngpha5xpgia3a5qqp4jsh0u4csozksxel2r?format=car", http.StatusSeeOther}, + + {"/api/v0/block/get?arg=bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", "/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e?format=raw", http.StatusSeeOther}, + {"/api/v0/block/get?arg=/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", "/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e?format=raw", http.StatusSeeOther}, + {"/api/v0/block/get?arg=/ipns/k51qzi5uqu5dgutdk6i1ynyzgkqngpha5xpgia3a5qqp4jsh0u4csozksxel2r", "/ipns/k51qzi5uqu5dgutdk6i1ynyzgkqngpha5xpgia3a5qqp4jsh0u4csozksxel2r?format=raw", http.StatusSeeOther}, + } + + for _, test := range tests { + for _, method := range []string{http.MethodGet, http.MethodPost} { + req, err := http.NewRequest(method, "http://127.0.0.1"+test.url, nil) + assert.Nil(t, err) + resp := httptest.NewRecorder() + rpcHandler.ServeHTTP(resp, req) + + assert.Equal(t, test.status, resp.Code) + assert.Equal(t, test.location, resp.Header().Get("Location")) + } + } +} + +func TestRPCRedirectsToKubo(t *testing.T) { + rpcHandler := newKuboRPCHandler([]string{"http://example.com"}) + + tests := []rpcRedirectTest{ + {"/api/v0/name/resolve?arg=some-arg", "http://example.com/api/v0/name/resolve?arg=some-arg", http.StatusTemporaryRedirect}, + {"/api/v0/resolve?arg=some-arg", "http://example.com/api/v0/resolve?arg=some-arg", http.StatusTemporaryRedirect}, + {"/api/v0/dag/resolve?arg=some-arg", "http://example.com/api/v0/dag/resolve?arg=some-arg", http.StatusTemporaryRedirect}, + {"/api/v0/dns?arg=some-arg", "http://example.com/api/v0/dns?arg=some-arg", http.StatusTemporaryRedirect}, + } + + for _, test := range tests { + for _, method := range []string{http.MethodGet, http.MethodPost} { + req, err := http.NewRequest(method, "http://127.0.0.1"+test.url, nil) + assert.Nil(t, err) + resp := httptest.NewRecorder() + rpcHandler.ServeHTTP(resp, req) + + assert.Equal(t, test.status, resp.Code) + assert.Equal(t, test.location, resp.Header().Get("Location")) + } + } +} + +func TestRPCNotImplemented(t *testing.T) { + rpcHandler := newKuboRPCHandler([]string{"http://example.com"}) + + for _, method := range []string{http.MethodGet, http.MethodPost} { + req, err := http.NewRequest(method, "http://127.0.0.1/api/v0/ping", nil) + assert.Nil(t, err) + resp := httptest.NewRecorder() + rpcHandler.ServeHTTP(resp, req) + + assert.Equal(t, http.StatusNotImplemented, resp.Code) + } +} diff --git a/handlers.go b/handlers.go index d47560a..08f28ec 100644 --- a/handlers.go +++ b/handlers.go @@ -11,6 +11,7 @@ import ( "github.com/ipfs/go-blockservice" offline "github.com/ipfs/go-ipfs-exchange-offline" "github.com/ipfs/go-libipfs/gateway" + "github.com/ipfs/interface-go-ipfs-core/path" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -60,7 +61,7 @@ func makeGatewayHandler(saturnOrchestrator, saturnLogger string, kuboRPC []strin mux := http.NewServeMux() mux.Handle("/ipfs/", gwHandler) mux.Handle("/ipns/", gwHandler) - mux.Handle("/api/v0/", newAPIHandler(kuboRPC)) + mux.Handle("/api/v0/", newKuboRPCHandler(kuboRPC)) // Note: in the future we may want to make this more configurable. noDNSLink := false @@ -102,47 +103,47 @@ func makeGatewayHandler(saturnOrchestrator, saturnLogger string, kuboRPC []strin }, nil } -func newAPIHandler(endpoints []string) http.Handler { +func newKuboRPCHandler(endpoints []string) http.Handler { mux := http.NewServeMux() // Endpoints that can be redirected to the gateway itself as they can be handled - // by the path gateway. - mux.HandleFunc("/api/v0/cat", func(w http.ResponseWriter, r *http.Request) { - cid := r.URL.Query().Get("arg") - url := fmt.Sprintf("/ipfs/%s", cid) - http.Redirect(w, r, url, http.StatusFound) - }) + // by the path gateway. We use 303 See Other here to ensure that the API requests + // are transformed to GET requests to the gateway. + // - https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303 + redirectToGateway := func(format string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + path := path.New(r.URL.Query().Get("arg")) + url := path.String() + if format != "" { + url += "?format=" + format + } + http.Redirect(w, r, url, http.StatusSeeOther) + } + } + mux.HandleFunc("/api/v0/cat", redirectToGateway("")) + mux.HandleFunc("/api/v0/dag/export", redirectToGateway("car")) + mux.HandleFunc("/api/v0/block/get", redirectToGateway("raw")) mux.HandleFunc("/api/v0/dag/get", func(w http.ResponseWriter, r *http.Request) { - cid := r.URL.Query().Get("arg") + path := path.New(r.URL.Query().Get("arg")) codec := r.URL.Query().Get("output-codec") if codec == "" { codec = "dag-json" } - url := fmt.Sprintf("/ipfs/%s?format=%s", cid, codec) - http.Redirect(w, r, url, http.StatusFound) - }) - - mux.HandleFunc("/api/v0/dag/export", func(w http.ResponseWriter, r *http.Request) { - cid := r.URL.Query().Get("arg") - url := fmt.Sprintf("/ipfs/%s?format=car", cid) - http.Redirect(w, r, url, http.StatusFound) - }) - - mux.HandleFunc("/api/v0/block/get", func(w http.ResponseWriter, r *http.Request) { - cid := r.URL.Query().Get("arg") - url := fmt.Sprintf("/ipfs/%s?format=raw", cid) - http.Redirect(w, r, url, http.StatusFound) + url := fmt.Sprintf("%s?format=%s", path.String(), codec) + http.Redirect(w, r, url, http.StatusSeeOther) }) // Endpoints that have high traffic volume. We will keep redirecting these - // for now to Kubo endpoints that are able to handle these requests. + // for now to Kubo endpoints that are able to handle these requests. We use + // 307 Temporary Redirect in order to preserve the original HTTP Method. + // - https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307 s := rand.NewSource(time.Now().Unix()) rand := rand.New(s) redirectToKubo := func(w http.ResponseWriter, r *http.Request) { // Naively choose one of the Kubo RPC clients. endpoint := endpoints[rand.Intn(len(endpoints))] - http.Redirect(w, r, endpoint+r.URL.Path+"?"+r.URL.RawQuery, http.StatusFound) + http.Redirect(w, r, endpoint+r.URL.Path+"?"+r.URL.RawQuery, http.StatusTemporaryRedirect) } mux.HandleFunc("/api/v0/name/resolve", redirectToKubo)