Skip to content

Commit

Permalink
Merge pull request #541 from jbenet/gateway-server
Browse files Browse the repository at this point in the history
Gateway Server
  • Loading branch information
jbenet committed Jan 12, 2015
2 parents cfbe440 + 2c83cd5 commit b299c42
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 114 deletions.
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

0 comments on commit b299c42

Please sign in to comment.