diff --git a/cmd/ipfs/daemon.go b/cmd/ipfs/daemon.go index cbde96d19..1c9aae7aa 100644 --- a/cmd/ipfs/daemon.go +++ b/cmd/ipfs/daemon.go @@ -180,7 +180,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/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..6deac03ac 100644 --- a/core/corehttp/corehttp.go +++ b/core/corehttp/corehttp.go @@ -12,11 +12,26 @@ import ( var log = eventlog.Logger("core/server") -const ( -// TODO rename -) +// 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) -type ServeOption func(*core.IpfsNode, *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,16 +44,14 @@ func ListenAndServe(n *core.IpfsNode, listeningMultiAddr string, options ...Serv if err != nil { return err } - mux := http.NewServeMux() - for _, option := range options { - if err := option(n, mux); err != nil { - return err - } + handler, err := makeHandler(n, options...) + if err != nil { + return err } - return listenAndServe(n, addr, mux) + 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 @@ -51,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) }() 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/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 + } + } +} 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 + } +} 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 } }