1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-07-01 02:30:39 +08:00

Merge pull request #541 from jbenet/gateway-server

Gateway Server
This commit is contained in:
Juan Batiz-Benet
2015-01-12 01:10:21 -08:00
5 changed files with 269 additions and 114 deletions

View File

@ -24,7 +24,10 @@ const (
ipnsMountKwd = "mount-ipns" ipnsMountKwd = "mount-ipns"
// apiAddrKwd = "address-api" // apiAddrKwd = "address-api"
// swarmAddrKwd = "address-swarm" // swarmAddrKwd = "address-swarm"
originEnvKey = "API_ORIGIN" originEnvKey = "API_ORIGIN"
webuiPath = "/ipfs/QmTWvqK9dYvqjAMAcCeUun8b45Fwu7wPhEN9B9TsGbkXfJ"
) )
var daemonCmd = &cmds.Command{ var daemonCmd = &cmds.Command{
@ -108,6 +111,16 @@ func daemonFunc(req cmds.Request) (interface{}, error) {
return nil, err 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 if the user provided the --mount flag
mount, _, err := req.Option(mountKwd).Bool() mount, _, err := req.Option(mountKwd).Bool()
if err != nil { if err != nil {
@ -138,32 +151,54 @@ func daemonFunc(req cmds.Request) (interface{}, error) {
fmt.Printf("IPNS mounted at: %s\n", nsdir) fmt.Printf("IPNS mounted at: %s\n", nsdir)
} }
if gatewayMaddr != nil {
listenAndServeGateway(node, gatewayMaddr)
}
return nil, listenAndServeAPI(node, req, apiMaddr) return nil, listenAndServeAPI(node, req, apiMaddr)
} }
func listenAndServeAPI(node *core.IpfsNode, req cmds.Request, addr ma.Multiaddr) error { 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) _, host, err := manet.DialArgs(addr)
if err != nil { if err != nil {
return err return err
} }
origin := os.Getenv(originEnvKey)
server := manners.NewServer() 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 // if the server exits beforehand
var serverError error var serverError error
serverExited := make(chan struct{}) serverExited := make(chan struct{})
go func() { 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) serverError = server.ListenAndServe(host, mux)
close(serverExited) 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 // if node being closed before server exits, close server
case <-node.Closing(): case <-node.Closing():
log.Infof("daemon at %s terminating...", addr) log.Infof("server at %s terminating...", addr)
server.Shutdown <- true 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 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
cmd/ipfs/gatewayHandler.go Normal file
View File

@ -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>
`

View File

@ -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/<path>)
// (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()))
}

View File

@ -42,6 +42,7 @@ type Datastore struct {
type Addresses struct { type Addresses struct {
Swarm []string // addresses for the swarm network Swarm []string // addresses for the swarm network
API string // address for the local API (RPC) 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 // Mounts stores the (string) mount points

View File

@ -88,7 +88,7 @@ test_launch_ipfs_daemon() {
test_expect_success FUSE "'ipfs daemon' output looks good" ' test_expect_success FUSE "'ipfs daemon' output looks good" '
IPFS_PID=$! && 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 || test_cmp_repeat_10_sec expected actual ||
fsh cat daemon_err fsh cat daemon_err
' '