From fbd76ebb5b5390a5037340003e77f06e04e3e87a Mon Sep 17 00:00:00 2001 From: Kevin Wallace Date: Mon, 2 Feb 2015 22:50:13 -0800 Subject: [PATCH 1/4] corehttp: ServeOption supports chaining muxes Each option now additionally returns the mux to be used by future options. If every options returns the mux it was passed, the current behavior is unchanged. However, if the option returns an a new mux, it can mediate requests to handlers provided by future options: return func(n *core.IpfsNode, mux *http.ServeMux) (*http.ServeMux, error) { childMux := http.NewServeMux() mux.Handle("/", handlerThatDelegatesToChildMux) return childMux, nil } License: MIT Signed-off-by: Kevin Wallace --- core/corehttp/commands.go | 4 ++-- core/corehttp/corehttp.go | 18 ++++++++++-------- core/corehttp/gateway.go | 10 +++++----- core/corehttp/redirect.go | 4 ++-- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/core/corehttp/commands.go b/core/corehttp/commands.go index 3b9ac262c..6128ed717 100644 --- a/core/corehttp/commands.go +++ b/core/corehttp/commands.go @@ -16,10 +16,10 @@ const ( ) func CommandsOption(cctx commands.Context) ServeOption { - return func(n *core.IpfsNode, mux *http.ServeMux) error { + return func(n *core.IpfsNode, mux *http.ServeMux) (*http.ServeMux, error) { origin := os.Getenv(originEnvKey) cmdHandler := cmdsHttp.NewHandler(cctx, corecommands.Root, origin) mux.Handle(cmdsHttp.ApiPath+"/", cmdHandler) - return nil + return mux, nil } } diff --git a/core/corehttp/corehttp.go b/core/corehttp/corehttp.go index 104b6566f..4e5dce98b 100644 --- a/core/corehttp/corehttp.go +++ b/core/corehttp/corehttp.go @@ -12,11 +12,11 @@ import ( var log = eventlog.Logger("core/server") -const ( -// TODO rename -) - -type ServeOption func(*core.IpfsNode, *http.ServeMux) error +// ServeOption registers any HTTP handlers it provides on the given mux. +// It returns the mux to expose to future options, which may be a new mux if it +// is interested in mediating requests to future options, or the same mux +// initially passed in if not. +type ServeOption func(*core.IpfsNode, *http.ServeMux) (*http.ServeMux, error) // ListenAndServe runs an HTTP server listening at |listeningMultiAddr| with // the given serve options. The address must be provided in multiaddr format. @@ -29,13 +29,15 @@ func ListenAndServe(n *core.IpfsNode, listeningMultiAddr string, options ...Serv if err != nil { return err } - mux := http.NewServeMux() + topMux := http.NewServeMux() + mux := topMux for _, option := range options { - if err := option(n, mux); err != nil { + mux, err = option(n, mux) + if err != nil { return err } } - return listenAndServe(n, addr, mux) + return listenAndServe(n, addr, topMux) } func listenAndServe(node *core.IpfsNode, addr ma.Multiaddr, mux *http.ServeMux) error { diff --git a/core/corehttp/gateway.go b/core/corehttp/gateway.go index 9c20cd5a9..139c317b2 100644 --- a/core/corehttp/gateway.go +++ b/core/corehttp/gateway.go @@ -24,14 +24,14 @@ func NewGateway(conf GatewayConfig) *Gateway { } func (g *Gateway) ServeOption() ServeOption { - return func(n *core.IpfsNode, mux *http.ServeMux) error { + return func(n *core.IpfsNode, mux *http.ServeMux) (*http.ServeMux, error) { gateway, err := newGatewayHandler(n, g.Config) if err != nil { - return err + return nil, err } mux.Handle("/ipfs/", gateway) mux.Handle("/ipns/", gateway) - return nil + return mux, nil } } @@ -47,8 +47,8 @@ func GatewayOption(writable bool) ServeOption { type Decider func(string) bool type BlockList struct { - mu sync.RWMutex - Decider Decider + mu sync.RWMutex + Decider Decider } func (b *BlockList) ShouldAllow(s string) bool { diff --git a/core/corehttp/redirect.go b/core/corehttp/redirect.go index 249d8801b..0048e1786 100644 --- a/core/corehttp/redirect.go +++ b/core/corehttp/redirect.go @@ -8,9 +8,9 @@ import ( func RedirectOption(path string, redirect string) ServeOption { handler := &redirectHandler{redirect} - return func(n *core.IpfsNode, mux *http.ServeMux) error { + return func(n *core.IpfsNode, mux *http.ServeMux) (*http.ServeMux, error) { mux.Handle("/"+path, handler) - return nil + return mux, nil } } From 084cdc3ed842c84da357bf0ea441c9207c08c3ca Mon Sep 17 00:00:00 2001 From: Kevin Wallace Date: Mon, 2 Feb 2015 22:55:23 -0800 Subject: [PATCH 2/4] gateway: attempt to resolve hostname to ipfs path This allows someone to host a static site by pointing a TXT record at their content in IPFS, and a CNAME record at an IPFS gateway. Note that such a setup technically violates RFC1912 (section 2.4; "A CNAME record is not allowed to coexist with any other data."), but tends to work in practice. We may want to consider changing the DNS->IPFS resolution scheme to allow this scenario to be RFC-compliant (e.g. store the mapping on a well-known subdomain to allow CNAME records on the domain itself). License: MIT Signed-off-by: Kevin Wallace --- cmd/ipfs/daemon.go | 5 ++++- core/corehttp/ipns_hostname.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 core/corehttp/ipns_hostname.go diff --git a/cmd/ipfs/daemon.go b/cmd/ipfs/daemon.go index 2dcf48e4d..0ad20c9dc 100644 --- a/cmd/ipfs/daemon.go +++ b/cmd/ipfs/daemon.go @@ -178,7 +178,10 @@ func daemonFunc(req cmds.Request, res cmds.Response) { if gatewayMaddr != nil { go func() { - var opts = []corehttp.ServeOption{corehttp.GatewayOption(writable)} + var opts = []corehttp.ServeOption{ + corehttp.IPNSHostnameOption(), + corehttp.GatewayOption(writable), + } if rootRedirect != nil { opts = append(opts, rootRedirect) } diff --git a/core/corehttp/ipns_hostname.go b/core/corehttp/ipns_hostname.go new file mode 100644 index 000000000..27d683250 --- /dev/null +++ b/core/corehttp/ipns_hostname.go @@ -0,0 +1,29 @@ +package corehttp + +import ( + "net/http" + "strings" + + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context" + "github.com/jbenet/go-ipfs/core" +) + +// IPNSHostnameOption rewrites an incoming request if its Host: header contains +// an IPNS name. +// The rewritten request points at the resolved name on the gateway handler. +func IPNSHostnameOption() ServeOption { + return func(n *core.IpfsNode, mux *http.ServeMux) (*http.ServeMux, error) { + childMux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithCancel(n.Context()) + defer cancel() + + host := strings.SplitN(r.Host, ":", 2)[0] + if k, err := n.Namesys.Resolve(ctx, host); err == nil { + r.URL.Path = "/ipfs/" + k.Pretty() + r.URL.Path + } + childMux.ServeHTTP(w, r) + }) + return childMux, nil + } +} From 794b7b7b3e82168ed02600cf76669a05b6970729 Mon Sep 17 00:00:00 2001 From: Kevin Wallace Date: Sun, 8 Feb 2015 11:33:16 -0800 Subject: [PATCH 3/4] corehttp: tear off makeHandler, for tests License: MIT Signed-off-by: Kevin Wallace --- core/corehttp/corehttp.go | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/core/corehttp/corehttp.go b/core/corehttp/corehttp.go index 4e5dce98b..6deac03ac 100644 --- a/core/corehttp/corehttp.go +++ b/core/corehttp/corehttp.go @@ -18,6 +18,21 @@ var log = eventlog.Logger("core/server") // initially passed in if not. type ServeOption func(*core.IpfsNode, *http.ServeMux) (*http.ServeMux, error) +// makeHandler turns a list of ServeOptions into a http.Handler that implements +// all of the given options, in order. +func makeHandler(n *core.IpfsNode, options ...ServeOption) (http.Handler, error) { + topMux := http.NewServeMux() + mux := topMux + for _, option := range options { + var err error + mux, err = option(n, mux) + if err != nil { + return nil, err + } + } + return topMux, nil +} + // ListenAndServe runs an HTTP server listening at |listeningMultiAddr| with // the given serve options. The address must be provided in multiaddr format. // @@ -29,18 +44,14 @@ func ListenAndServe(n *core.IpfsNode, listeningMultiAddr string, options ...Serv if err != nil { return err } - topMux := http.NewServeMux() - mux := topMux - for _, option := range options { - mux, err = option(n, mux) - if err != nil { - return err - } + handler, err := makeHandler(n, options...) + if err != nil { + return err } - return listenAndServe(n, addr, topMux) + return listenAndServe(n, addr, handler) } -func listenAndServe(node *core.IpfsNode, addr ma.Multiaddr, mux *http.ServeMux) error { +func listenAndServe(node *core.IpfsNode, addr ma.Multiaddr, handler http.Handler) error { _, host, err := manet.DialArgs(addr) if err != nil { return err @@ -53,7 +64,7 @@ func listenAndServe(node *core.IpfsNode, addr ma.Multiaddr, mux *http.ServeMux) serverExited := make(chan struct{}) go func() { - serverError = server.ListenAndServe(host, mux) + serverError = server.ListenAndServe(host, handler) close(serverExited) }() From e5abf0764c9c178bbce1b1ba47ddd3efedc40066 Mon Sep 17 00:00:00 2001 From: Kevin Wallace Date: Sun, 8 Feb 2015 12:49:21 -0800 Subject: [PATCH 4/4] corehttp: add test for gateway with mocked namesys License: MIT Signed-off-by: Kevin Wallace --- core/corehttp/gateway_test.go | 125 ++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 core/corehttp/gateway_test.go diff --git a/core/corehttp/gateway_test.go b/core/corehttp/gateway_test.go new file mode 100644 index 000000000..74bb3af92 --- /dev/null +++ b/core/corehttp/gateway_test.go @@ -0,0 +1,125 @@ +package corehttp + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + context "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context" + b58 "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-base58" + core "github.com/jbenet/go-ipfs/core" + coreunix "github.com/jbenet/go-ipfs/core/coreunix" + namesys "github.com/jbenet/go-ipfs/namesys" + ci "github.com/jbenet/go-ipfs/p2p/crypto" + repo "github.com/jbenet/go-ipfs/repo" + config "github.com/jbenet/go-ipfs/repo/config" + u "github.com/jbenet/go-ipfs/util" + testutil "github.com/jbenet/go-ipfs/util/testutil" +) + +type mockNamesys map[string]string + +func (m mockNamesys) Resolve(ctx context.Context, name string) (value u.Key, err error) { + enc, ok := m[name] + if !ok { + return "", namesys.ErrResolveFailed + } + dec := b58.Decode(enc) + if len(dec) == 0 { + return "", fmt.Errorf("invalid b58 string for name %q: %q", name, enc) + } + return u.Key(dec), nil +} + +func (m mockNamesys) CanResolve(name string) bool { + _, ok := m[name] + return ok +} + +func (m mockNamesys) Publish(ctx context.Context, name ci.PrivKey, value u.Key) error { + return errors.New("not implemented for mockNamesys") +} + +func newNodeWithMockNamesys(t *testing.T, ns mockNamesys) *core.IpfsNode { + c := config.Config{ + Identity: config.Identity{ + PeerID: "Qmfoo", // required by offline node + }, + } + r := &repo.Mock{ + C: c, + D: testutil.ThreadSafeCloserMapDatastore(), + } + n, err := core.NewIPFSNode(context.Background(), core.Offline(r)) + if err != nil { + t.Fatal(err) + } + n.Namesys = ns + return n +} + +func TestGatewayGet(t *testing.T) { + ns := mockNamesys{} + n := newNodeWithMockNamesys(t, ns) + k, err := coreunix.Add(n, strings.NewReader("fnord")) + if err != nil { + t.Fatal(err) + } + ns["example.com"] = k + + h, err := makeHandler(n, + IPNSHostnameOption(), + GatewayOption(false), + ) + if err != nil { + t.Fatal(err) + } + + ts := httptest.NewServer(h) + defer ts.Close() + + for _, test := range []struct { + host string + path string + status int + text string + }{ + {"localhost:5001", "/", http.StatusNotFound, "404 page not found\n"}, + {"localhost:5001", "/" + k, http.StatusNotFound, "404 page not found\n"}, + {"localhost:5001", "/ipfs/" + k, http.StatusOK, "fnord"}, + {"localhost:5001", "/ipns/nxdomain.example.com", http.StatusBadRequest, namesys.ErrResolveFailed.Error()}, + {"localhost:5001", "/ipns/example.com", http.StatusOK, "fnord"}, + {"example.com", "/", http.StatusOK, "fnord"}, + } { + var c http.Client + r, err := http.NewRequest("GET", ts.URL+test.path, nil) + if err != nil { + t.Fatal(err) + } + r.Host = test.host + resp, err := c.Do(r) + + urlstr := "http://" + test.host + test.path + if err != nil { + t.Errorf("error requesting %s: %s", urlstr, err) + continue + } + defer resp.Body.Close() + if resp.StatusCode != test.status { + t.Errorf("got %d, expected %d from %s", resp.StatusCode, test.status, urlstr) + continue + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("error reading response from %s: %s", urlstr, err) + } + if string(body) != test.text { + t.Errorf("unexpected response body from %s: expected %q; got %q", urlstr, test.text, body) + continue + } + } +}