提交 2a29413c 作者: Juan Batiz-Benet

Merge pull request #674 from jbenet/gateway

Gateway Changes
......@@ -153,9 +153,18 @@ func daemonFunc(req cmds.Request, res cmds.Response) {
fmt.Printf("IPNS mounted at: %s\n", nsdir)
}
var rootRedirect corehttp.ServeOption
if len(cfg.Gateway.RootRedirect) > 0 {
rootRedirect = corehttp.RedirectOption("", cfg.Gateway.RootRedirect)
}
if gatewayMaddr != nil {
go func() {
err := corehttp.ListenAndServe(node, gatewayMaddr.String(), corehttp.GatewayOption)
var opts = []corehttp.ServeOption{corehttp.GatewayOption}
if rootRedirect != nil {
opts = append(opts, rootRedirect)
}
err := corehttp.ListenAndServe(node, gatewayMaddr.String(), opts...)
if err != nil {
log.Error(err)
}
......@@ -167,6 +176,9 @@ func daemonFunc(req cmds.Request, res cmds.Response) {
corehttp.WebUIOption,
corehttp.GatewayOption,
}
if rootRedirect != nil {
opts = append(opts, rootRedirect)
}
if err := corehttp.ListenAndServe(node, apiMaddr.String(), opts...); err != nil {
res.SetError(err, cmds.ErrNormal)
return
......
......@@ -12,5 +12,6 @@ func GatewayOption(n *core.IpfsNode, mux *http.ServeMux) error {
return err
}
mux.Handle("/ipfs/", gateway)
mux.Handle("/ipns/", gateway)
return nil
}
......@@ -3,9 +3,10 @@ package corehttp
import (
"html/template"
"io"
"mime"
"net/http"
"path"
"strings"
"time"
"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"
......@@ -19,11 +20,16 @@ import (
u "github.com/jbenet/go-ipfs/util"
)
const (
IpfsPathPrefix = "/ipfs/"
IpnsPathPrefix = "/ipns/"
)
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)
NewDagReader(nd *dag.Node) (uio.ReadSeekCloser, error)
}
// shortcut for templating
......@@ -33,6 +39,7 @@ type webHandler map[string]interface{}
type directoryItem struct {
Size uint64
Name string
Path string
}
// gatewayHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/<path>)
......@@ -63,8 +70,29 @@ func (i *gatewayHandler) loadTemplate() error {
return nil
}
func (i *gatewayHandler) ResolvePath(path string) (*dag.Node, error) {
return i.node.Resolver.ResolvePath(path)
func (i *gatewayHandler) ResolvePath(ctx context.Context, p string) (*dag.Node, string, error) {
p = path.Clean(p)
if strings.HasPrefix(p, IpnsPathPrefix) {
elements := strings.Split(p[len(IpnsPathPrefix):], "/")
hash := elements[0]
k, err := i.node.Namesys.Resolve(ctx, hash)
if err != nil {
return nil, "", err
}
elements[0] = k.Pretty()
p = path.Join(elements...)
}
if !strings.HasPrefix(p, IpfsPathPrefix) {
p = path.Join(IpfsPathPrefix, p)
}
node, err := i.node.Resolver.ResolvePath(p)
if err != nil {
return nil, "", err
}
return node, p, err
}
func (i *gatewayHandler) NewDagFromReader(r io.Reader) (*dag.Node, error) {
......@@ -76,14 +104,17 @@ 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) {
func (i *gatewayHandler) NewDagReader(nd *dag.Node) (uio.ReadSeekCloser, error) {
return uio.NewDagReader(i.node.Context(), nd, i.node.DAG)
}
func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path[5:]
ctx, cancel := context.WithCancel(i.node.Context())
defer cancel()
urlPath := r.URL.Path
nd, err := i.ResolvePath(path)
nd, p, err := i.ResolvePath(ctx, urlPath)
if err != nil {
if err == routing.ErrNotFound {
w.WriteHeader(http.StatusNotFound)
......@@ -98,31 +129,32 @@ func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
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)
etag := path.Base(p)
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
if err != uio.ErrIsDir {
w.Header().Set("X-IPFS-Path", p)
dr, err := i.NewDagReader(nd)
if err != nil && 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)
// set these headers _after_ the error, for we may just not have it
// and dont want the client to cache a 500 response...
w.Header().Set("Etag", etag)
w.Header().Set("Cache-Control", "public, max-age=29030400")
if err == nil {
defer dr.Close()
_, name := path.Split(urlPath)
// set modtime to a really long time ago, since files are immutable and should stay cached
modtime := time.Unix(1, 0)
http.ServeContent(w, r, name, modtime, dr)
return
}
......@@ -132,10 +164,15 @@ func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
foundIndex := false
for _, link := range nd.Links {
if link.Name == "index.html" {
if urlPath[len(urlPath)-1] != '/' {
http.Redirect(w, r, urlPath+"/", 302)
return
}
log.Debug("found index")
foundIndex = true
// return index page instead.
nd, err := i.ResolvePath(path + "/index.html")
nd, _, err := i.ResolvePath(ctx, urlPath+"/index.html")
if err != nil {
internalWebError(w, err)
return
......@@ -145,17 +182,22 @@ func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
internalWebError(w, err)
return
}
defer dr.Close()
// write to request
io.Copy(w, dr)
break
}
dirListing = append(dirListing, directoryItem{link.Size, link.Name})
di := directoryItem{link.Size, link.Name, path.Join(urlPath, link.Name)}
dirListing = append(dirListing, di)
}
if !foundIndex {
// template and return directory listing
hndlr := webHandler{"listing": dirListing, "path": path}
hndlr := webHandler{
"listing": dirListing,
"path": urlPath,
}
if err := i.dirList.Execute(w, hndlr); err != nil {
internalWebError(w, err)
return
......@@ -204,8 +246,8 @@ var listingTemplate = `
<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>
{{ range .listing }}
<li><a href="{{ .Path }}">{{ .Name }}</a> - {{ .Size }} bytes</li>
{{ end }}
</ul>
</body>
......
package corehttp
import (
"net/http"
core "github.com/jbenet/go-ipfs/core"
)
func RedirectOption(path string, redirect string) ServeOption {
handler := &redirectHandler{redirect}
return func(n *core.IpfsNode, mux *http.ServeMux) error {
mux.Handle("/"+path, handler)
return nil
}
}
type redirectHandler struct {
path string
}
func (i *redirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, i.path, 302)
}
package corehttp
import (
"net/http"
// TODO: move to IPNS
const webuiPath = "/ipfs/QmctngrQAt9fjpQUZr7Bx3BsXUcif52eZGTizWhvcShsjz"
core "github.com/jbenet/go-ipfs/core"
)
const (
// TODO rename
webuiPath = "/ipfs/QmTWvqK9dYvqjAMAcCeUun8b45Fwu7wPhEN9B9TsGbkXfJ"
)
func WebUIOption(n *core.IpfsNode, mux *http.ServeMux) error {
mux.Handle("/webui/", &redirectHandler{webuiPath})
return nil
}
type redirectHandler struct {
path string
}
func (i *redirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, i.path, 302)
}
var WebUIOption = RedirectOption("webui", webuiPath)
......@@ -164,8 +164,8 @@ func TestDialBackoff(t *testing.T) {
defer s1.Close()
defer s2.Close()
s1.dialT = time.Millisecond * 500 // lower timeout for tests.
s2.dialT = time.Millisecond * 500 // lower timeout for tests.
s1.dialT = time.Second // lower timeout for tests.
s2.dialT = time.Second // lower timeout for tests.
s2addrs, err := s2.InterfaceListenAddresses()
if err != nil {
......@@ -229,8 +229,8 @@ func TestDialBackoff(t *testing.T) {
// when all dials should be done by:
dialTimeout1x := time.After(s1.dialT)
dialTimeout1Ax := time.After(s1.dialT * dialAttempts)
dialTimeout10Ax := time.After(s1.dialT * dialAttempts * 10)
// dialTimeout1Ax := time.After(s1.dialT * 2) // dialAttempts)
dialTimeout10Ax := time.After(s1.dialT * 2 * 10) // dialAttempts * 10)
// 2) all dials should hang
select {
......@@ -265,7 +265,7 @@ func TestDialBackoff(t *testing.T) {
}
// 4) s1->s3 should not (and should place s3 on backoff)
// N-1 should finish before dialTimeout1Ax
// N-1 should finish before dialTimeout1x * 2
for i := 0; i < N; i++ {
select {
case <-s2done:
......@@ -274,11 +274,11 @@ func TestDialBackoff(t *testing.T) {
if r {
t.Error("s3 should not succeed")
}
case <-dialTimeout1Ax:
case <-(dialTimeout1x):
if i < (N - 1) {
t.Fatal("s3 took too long")
}
t.Log("dialTimeout1Ax hit for last peer")
t.Log("dialTimeout1x * 1.3 hit for last peer")
case <-dialTimeout10Ax:
t.Fatal("s3 took too long")
}
......@@ -313,8 +313,8 @@ func TestDialBackoff(t *testing.T) {
// when all dials should be done by:
dialTimeout1x := time.After(s1.dialT)
dialTimeout1Ax := time.After(s1.dialT * dialAttempts)
dialTimeout10Ax := time.After(s1.dialT * dialAttempts * 10)
// dialTimeout1Ax := time.After(s1.dialT * 2) // dialAttempts)
dialTimeout10Ax := time.After(s1.dialT * 2 * 10) // dialAttempts * 10)
// 7) s3 dials should all return immediately (except 1)
for i := 0; i < N-1; i++ {
......@@ -338,7 +338,7 @@ func TestDialBackoff(t *testing.T) {
t.Error("s2 should succeed")
}
// case <-s3done:
case <-dialTimeout1Ax:
case <-(dialTimeout1x):
t.Fatal("s3 took too long")
}
}
......
......@@ -23,6 +23,7 @@ type Config struct {
Version Version // local node's version management
Bootstrap []BootstrapPeer // local nodes's bootstrap peers
Tour Tour // local node's tour position
Gateway Gateway // local node's gateway server options
}
const (
......
package config
// Gateway contains options for the HTTP gateway server.
type Gateway struct {
RootRedirect string
}
......@@ -50,6 +50,10 @@ func Init(nBitsForKeypair int) (*Config, error) {
// tracking ipfs version used to generate the init folder and adding
// update checker default setting.
Version: VersionDefaultValue(),
Gateway: Gateway{
RootRedirect: "",
},
}
return conf, nil
......
#!/bin/sh
#
# Copyright (c) 2015 Matt Bell
# MIT Licensed; see the LICENSE file in this repository.
#
test_description="Test HTTP gateway"
. lib/test-lib.sh
test_init_ipfs
test_launch_ipfs_daemon
test_expect_success "GET IPFS path succeeds" '
echo "Hello Worlds!" > expected &&
HASH=`ipfs add -q expected` &&
wget "http://127.0.0.1:5001/ipfs/$HASH" -O actual
'
test_expect_success "GET IPFS path output looks good" '
test_cmp expected actual &&
rm actual
'
test_expect_success "GET IPFS directory path succeeds" '
mkdir dir &&
echo "12345" > dir/test &&
HASH2=`ipfs add -r -q dir | tail -n 1` &&
wget "http://127.0.0.1:5001/ipfs/$HASH2"
'
test_expect_success "GET IPFS directory file succeeds" '
wget "http://127.0.0.1:5001/ipfs/$HASH2/test" -O actual
'
test_expect_success "GET IPFS directory file output looks good" '
test_cmp dir/test actual
'
test_expect_failure "GET IPNS path succeeds" '
ipfs name publish "$HASH" &&
NAME=`ipfs config Identity.PeerID` &&
wget "http://127.0.0.1:5001/ipns/$NAME" -O actual
'
test_expect_failure "GET IPNS path output looks good" '
test_cmp expected actual
'
test_expect_success "GET invalid IPFS path errors" '
test_must_fail wget http://127.0.0.1:5001/ipfs/12345
'
test_expect_success "GET invalid path errors" '
test_must_fail wget http://127.0.0.1:5001/12345
'
test_kill_ipfs_daemon
test_done
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论