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

Gateway Server #541

Merged
merged 11 commits into from
Jan 12, 2015
67 changes: 55 additions & 12 deletions cmd/ipfs/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ const (
ipnsMountKwd = "mount-ipns"
// apiAddrKwd = "address-api"
// swarmAddrKwd = "address-swarm"

originEnvKey = "API_ORIGIN"

webuiPath = "/ipfs/QmTWvqK9dYvqjAMAcCeUun8b45Fwu7wPhEN9B9TsGbkXfJ"
)

var daemonCmd = &cmds.Command{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {

_, host, err := manet.DialArgs(addr)
origin := os.Getenv(originEnvKey)
cmdHandler := cmdsHttp.NewHandler(*req.Context(), commands.Root, origin)
gateway, err := NewGatewayHandler(node)
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)
mux.Handle("/ipfs/", gateway)
mux.Handle("/webui/", &redirectHandler{webuiPath})
return listenAndServe("API", node, addr, mux)
}

ifpsHandler := &ipfsHandler{node}
mux.Handle("/ipfs/", ifpsHandler)
// 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
}

server := manners.NewServer()

// 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)
}()
Expand All @@ -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)
}
210 changes: 210 additions & 0 deletions cmd/ipfs/gatewayHandler.go
Original file line number Diff line number Diff line change
@@ -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/<path>)
// (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 = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{{ .path }}</title>
</head>
<body>
<h2>Index of {{ .path }}</h2>
<ul>
<li><a href="./..">..</a></li>
{{ range $item := .listing }}
<li><a href="./{{ $item.Name }}">{{ $item.Name }}</a> - {{ $item.Size }} bytes</li>
{{ end }}
</ul>
</body>
</html>
`
Loading