diff --git a/cmd/ipfs/daemon.go b/cmd/ipfs/daemon.go index e0a6663cf..bbf880f4d 100644 --- a/cmd/ipfs/daemon.go +++ b/cmd/ipfs/daemon.go @@ -24,7 +24,10 @@ const ( ipnsMountKwd = "mount-ipns" // apiAddrKwd = "address-api" // swarmAddrKwd = "address-swarm" + originEnvKey = "API_ORIGIN" + + webuiPath = "/ipfs/QmTWvqK9dYvqjAMAcCeUun8b45Fwu7wPhEN9B9TsGbkXfJ" ) var daemonCmd = &cmds.Command{ @@ -108,6 +111,16 @@ func daemonFunc(req cmds.Request) (interface{}, error) { return nil, err } + var gatewayMaddr ma.Multiaddr + if len(cfg.Addresses.Gateway) > 0 { + // ignore error for gateway address + // if there is an error (invalid address), then don't run the gateway + gatewayMaddr, _ = ma.NewMultiaddr(cfg.Addresses.Gateway) + if gatewayMaddr == nil { + log.Errorf("Invalid gateway address: %s", cfg.Addresses.Gateway) + } + } + // mount if the user provided the --mount flag mount, _, err := req.Option(mountKwd).Bool() if err != nil { @@ -138,32 +151,54 @@ func daemonFunc(req cmds.Request) (interface{}, error) { fmt.Printf("IPNS mounted at: %s\n", nsdir) } + if gatewayMaddr != nil { + listenAndServeGateway(node, gatewayMaddr) + } + return nil, listenAndServeAPI(node, req, apiMaddr) } func listenAndServeAPI(node *core.IpfsNode, req cmds.Request, addr ma.Multiaddr) error { + origin := os.Getenv(originEnvKey) + cmdHandler := cmdsHttp.NewHandler(*req.Context(), commands.Root, origin) + gateway, err := NewGatewayHandler(node) + if err != nil { + return err + } + mux := http.NewServeMux() + mux.Handle(cmdsHttp.ApiPath+"/", cmdHandler) + mux.Handle("/ipfs/", gateway) + mux.Handle("/webui/", &redirectHandler{webuiPath}) + return listenAndServe("API", node, addr, mux) +} + +// the gateway also listens on its own address:port in addition to the API listener +func listenAndServeGateway(node *core.IpfsNode, addr ma.Multiaddr) error { + gateway, err := NewGatewayHandler(node) + if err != nil { + return err + } + + mux := http.NewServeMux() + mux.Handle("/ipfs/", gateway) + return listenAndServe("gateway", node, addr, mux) +} + +func listenAndServe(name string, node *core.IpfsNode, addr ma.Multiaddr, mux *http.ServeMux) error { _, host, err := manet.DialArgs(addr) if err != nil { return err } - origin := os.Getenv(originEnvKey) - server := manners.NewServer() - mux := http.NewServeMux() - cmdHandler := cmdsHttp.NewHandler(*req.Context(), commands.Root, origin) - mux.Handle(cmdsHttp.ApiPath+"/", cmdHandler) - - ifpsHandler := &ipfsHandler{node} - mux.Handle("/ipfs/", ifpsHandler) // if the server exits beforehand var serverError error serverExited := make(chan struct{}) go func() { - fmt.Printf("daemon listening on %s\n", addr) + fmt.Printf("%s server listening on %s\n", name, addr) serverError = server.ListenAndServe(host, mux) close(serverExited) }() @@ -174,11 +209,19 @@ func listenAndServeAPI(node *core.IpfsNode, req cmds.Request, addr ma.Multiaddr) // if node being closed before server exits, close server case <-node.Closing(): - log.Infof("daemon at %s terminating...", addr) + log.Infof("server at %s terminating...", addr) server.Shutdown <- true - <-serverExited // now, DO wait until server exits + <-serverExited // now, DO wait until server exit } - log.Infof("daemon at %s terminated", addr) + log.Infof("server at %s terminated", addr) return serverError } + +type redirectHandler struct { + path string +} + +func (i *redirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, i.path, 302) +} diff --git a/cmd/ipfs/gatewayHandler.go b/cmd/ipfs/gatewayHandler.go new file mode 100644 index 000000000..26422902a --- /dev/null +++ b/cmd/ipfs/gatewayHandler.go @@ -0,0 +1,210 @@ +package main + +import ( + "html/template" + "io" + "mime" + "net/http" + "strings" + + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context" + mh "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash" + + core "github.com/jbenet/go-ipfs/core" + "github.com/jbenet/go-ipfs/importer" + chunk "github.com/jbenet/go-ipfs/importer/chunk" + dag "github.com/jbenet/go-ipfs/merkledag" + "github.com/jbenet/go-ipfs/routing" + uio "github.com/jbenet/go-ipfs/unixfs/io" + u "github.com/jbenet/go-ipfs/util" +) + +type gateway interface { + ResolvePath(string) (*dag.Node, error) + NewDagFromReader(io.Reader) (*dag.Node, error) + AddNodeToDAG(nd *dag.Node) (u.Key, error) + NewDagReader(nd *dag.Node) (io.Reader, error) +} + +// shortcut for templating +type webHandler map[string]interface{} + +// struct for directory listing +type directoryItem struct { + Size uint64 + Name string +} + +// gatewayHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/) +// (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link) +type gatewayHandler struct { + node *core.IpfsNode + dirList *template.Template +} + +func NewGatewayHandler(node *core.IpfsNode) (*gatewayHandler, error) { + i := &gatewayHandler{ + node: node, + } + err := i.loadTemplate() + if err != nil { + return nil, err + } + return i, nil +} + +// Load the directroy list template +func (i *gatewayHandler) loadTemplate() error { + t, err := template.New("dir").Parse(listingTemplate) + if err != nil { + return err + } + i.dirList = t + return nil +} + +func (i *gatewayHandler) ResolvePath(path string) (*dag.Node, error) { + return i.node.Resolver.ResolvePath(path) +} + +func (i *gatewayHandler) NewDagFromReader(r io.Reader) (*dag.Node, error) { + return importer.BuildDagFromReader( + r, i.node.DAG, i.node.Pinning.GetManual(), chunk.DefaultSplitter) +} + +func (i *gatewayHandler) AddNodeToDAG(nd *dag.Node) (u.Key, error) { + return i.node.DAG.Add(nd) +} + +func (i *gatewayHandler) NewDagReader(nd *dag.Node) (io.Reader, error) { + return uio.NewDagReader(nd, i.node.DAG) +} + +func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path[5:] + log := log.Prefix("serving %s", path) + + nd, err := i.ResolvePath(path) + if err != nil { + if err == routing.ErrNotFound { + w.WriteHeader(http.StatusNotFound) + } else if err == context.DeadlineExceeded { + w.WriteHeader(http.StatusRequestTimeout) + } else { + w.WriteHeader(http.StatusBadRequest) + } + + log.Error(err) + w.Write([]byte(err.Error())) + return + } + + extensionIndex := strings.LastIndex(path, ".") + if extensionIndex != -1 { + extension := path[extensionIndex:] + mimeType := mime.TypeByExtension(extension) + if len(mimeType) > 0 { + w.Header().Add("Content-Type", mimeType) + } + } + + dr, err := i.NewDagReader(nd) + if err == nil { + io.Copy(w, dr) + return + } + + if err != uio.ErrIsDir { + // not a directory and still an error + internalWebError(w, err) + return + } + + log.Debug("listing directory") + if path[len(path)-1:] != "/" { + log.Debug("missing trailing slash, redirect") + http.Redirect(w, r, "/ipfs/"+path+"/", 307) + return + } + + // storage for directory listing + var dirListing []directoryItem + // loop through files + for _, link := range nd.Links { + if link.Name != "index.html" { + dirListing = append(dirListing, directoryItem{link.Size, link.Name}) + continue + } + + log.Debug("found index") + // return index page instead. + nd, err := i.ResolvePath(path + "/index.html") + if err != nil { + internalWebError(w, err) + return + } + dr, err := i.NewDagReader(nd) + if err != nil { + internalWebError(w, err) + return + } + // write to request + io.Copy(w, dr) + } + + // template and return directory listing + hndlr := webHandler{"listing": dirListing, "path": path} + if err := i.dirList.Execute(w, hndlr); err != nil { + internalWebError(w, err) + return + } +} + +func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) { + nd, err := i.NewDagFromReader(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Error(err) + w.Write([]byte(err.Error())) + return + } + + k, err := i.AddNodeToDAG(nd) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Error(err) + w.Write([]byte(err.Error())) + return + } + + //TODO: return json representation of list instead + w.WriteHeader(http.StatusCreated) + w.Write([]byte(mh.Multihash(k).B58String())) +} + +// return a 500 error and log +func internalWebError(w http.ResponseWriter, err error) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + log.Error("%s", err) +} + +// Directory listing template +var listingTemplate = ` + + + + + {{ .path }} + + +

Index of {{ .path }}

+ + + +` diff --git a/cmd/ipfs/ipfsHandler.go b/cmd/ipfs/ipfsHandler.go deleted file mode 100644 index 623531239..000000000 --- a/cmd/ipfs/ipfsHandler.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "io" - "net/http" - - "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context" - mh "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash" - - core "github.com/jbenet/go-ipfs/core" - "github.com/jbenet/go-ipfs/importer" - chunk "github.com/jbenet/go-ipfs/importer/chunk" - dag "github.com/jbenet/go-ipfs/merkledag" - "github.com/jbenet/go-ipfs/routing" - uio "github.com/jbenet/go-ipfs/unixfs/io" - u "github.com/jbenet/go-ipfs/util" -) - -type ipfs interface { - ResolvePath(string) (*dag.Node, error) - NewDagFromReader(io.Reader) (*dag.Node, error) - AddNodeToDAG(nd *dag.Node) (u.Key, error) - NewDagReader(nd *dag.Node) (io.Reader, error) -} - -// ipfsHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/) -// (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link) -type ipfsHandler struct { - node *core.IpfsNode -} - -func (i *ipfsHandler) ResolvePath(path string) (*dag.Node, error) { - return i.node.Resolver.ResolvePath(path) -} - -func (i *ipfsHandler) NewDagFromReader(r io.Reader) (*dag.Node, error) { - return importer.BuildDagFromReader( - r, i.node.DAG, i.node.Pinning.GetManual(), chunk.DefaultSplitter) -} - -func (i *ipfsHandler) AddNodeToDAG(nd *dag.Node) (u.Key, error) { - return i.node.DAG.Add(nd) -} - -func (i *ipfsHandler) NewDagReader(nd *dag.Node) (io.Reader, error) { - return uio.NewDagReader(nd, i.node.DAG) -} - -func (i *ipfsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path[5:] - - nd, err := i.ResolvePath(path) - if err != nil { - if err == routing.ErrNotFound { - w.WriteHeader(http.StatusNotFound) - } else if err == context.DeadlineExceeded { - w.WriteHeader(http.StatusRequestTimeout) - } else { - w.WriteHeader(http.StatusBadRequest) - } - - log.Error(err) - w.Write([]byte(err.Error())) - return - } - - dr, err := i.NewDagReader(nd) - if err != nil { - // TODO: return json object containing the tree data if it's a directory (err == ErrIsDir) - w.WriteHeader(http.StatusInternalServerError) - log.Error(err) - w.Write([]byte(err.Error())) - return - } - - io.Copy(w, dr) -} - -func (i *ipfsHandler) postHandler(w http.ResponseWriter, r *http.Request) { - nd, err := i.NewDagFromReader(r.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - log.Error(err) - w.Write([]byte(err.Error())) - return - } - - k, err := i.AddNodeToDAG(nd) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - log.Error(err) - w.Write([]byte(err.Error())) - return - } - - //TODO: return json representation of list instead - w.WriteHeader(http.StatusCreated) - w.Write([]byte(mh.Multihash(k).B58String())) -} diff --git a/config/config.go b/config/config.go index 3aca889ef..d33e4d36d 100644 --- a/config/config.go +++ b/config/config.go @@ -40,8 +40,9 @@ type Datastore struct { // Addresses stores the (string) multiaddr addresses for the node. type Addresses struct { - Swarm []string // addresses for the swarm network - API string // address for the local API (RPC) + Swarm []string // addresses for the swarm network + API string // address for the local API (RPC) + Gateway string // address to listen on for IPFS HTTP object gateway } // Mounts stores the (string) mount points diff --git a/test/sharness/lib/test-lib.sh b/test/sharness/lib/test-lib.sh index 968627be5..43bd1397d 100644 --- a/test/sharness/lib/test-lib.sh +++ b/test/sharness/lib/test-lib.sh @@ -88,7 +88,7 @@ test_launch_ipfs_daemon() { test_expect_success FUSE "'ipfs daemon' output looks good" ' IPFS_PID=$! && - echo "daemon listening on /ip4/127.0.0.1/tcp/5001" >expected && + echo "API server listening on /ip4/127.0.0.1/tcp/5001" >expected && test_cmp_repeat_10_sec expected actual || fsh cat daemon_err '