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

improve gateway symlink handling #6680

Merged
merged 9 commits into from
Jan 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 38 additions & 44 deletions core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
}

// write to request
http.ServeContent(w, r, "index.html", modtime, f)
i.serveFile(w, r, "index.html", modtime, f)
return
case resolver.ErrNoLink:
// no index.html; noop
Expand All @@ -306,14 +306,14 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
var dirListing []directoryItem
dirit := dir.Entries()
for dirit.Next() {
// See comment above where originalUrlPath is declared.
s, err := dirit.Node().Size()
if err != nil {
internalWebError(w, err)
return
size := "?"
if s, err := dirit.Node().Size(); err == nil {
// Size may not be defined/supported. Continue anyways.
size = humanize.Bytes(uint64(s))
}

di := directoryItem{humanize.Bytes(uint64(s)), dirit.Name(), gopath.Join(originalUrlPath, dirit.Name())}
// See comment above where originalUrlPath is declared.
di := directoryItem{size, dirit.Name(), gopath.Join(originalUrlPath, dirit.Name())}
dirListing = append(dirListing, di)
}
if dirit.Err() != nil {
Expand Down Expand Up @@ -372,48 +372,42 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
}
}

type sizeReadSeeker interface {
Size() (int64, error)

io.ReadSeeker
}

type sizeSeeker struct {
sizeReadSeeker
}

func (s *sizeSeeker) Seek(offset int64, whence int) (int64, error) {
if whence == io.SeekEnd && offset == 0 {
return s.Size()
func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, file files.File) {
size, err := file.Size()
if err != nil {
http.Error(w, "cannot serve files with unknown sizes", http.StatusBadGateway)
return
}

return s.sizeReadSeeker.Seek(offset, whence)
}

func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) {
if sp, ok := content.(sizeReadSeeker); ok {
content = &sizeSeeker{
sizeReadSeeker: sp,
}
content := &lazySeeker{
size: size,
reader: file,
}

ctype := mime.TypeByExtension(gopath.Ext(name))
if ctype == "" {
buf := make([]byte, 512)
n, _ := io.ReadFull(content, buf[:])
ctype = http.DetectContentType(buf[:n])
_, err := content.Seek(0, io.SeekStart)
if err != nil {
http.Error(w, "seeker can't seek", http.StatusInternalServerError)
return
var ctype string
if _, isSymlink := file.(*files.Symlink); isSymlink {
// We should be smarter about resolving symlinks but this is the
// "most correct" we can be without doing that.
ctype = "inode/symlink"
} else {
ctype = mime.TypeByExtension(gopath.Ext(name))
if ctype == "" {
buf := make([]byte, 512)
n, _ := io.ReadFull(content, buf[:])
ctype = http.DetectContentType(buf[:n])
_, err := content.Seek(0, io.SeekStart)
if err != nil {
http.Error(w, "seeker can't seek", http.StatusInternalServerError)
return
}
}
// Strip the encoding from the HTML Content-Type header and let the
// browser figure it out.
//
// Fixes https://github.com/ipfs/go-ipfs/issues/2203
if strings.HasPrefix(ctype, "text/html;") {
ctype = "text/html"
}
}
// Strip the encoding from the HTML Content-Type header and let the
// browser figure it out.
//
// Fixes https://github.com/ipfs/go-ipfs/issues/2203
if strings.HasPrefix(ctype, "text/html;") {
ctype = "text/html"
}
w.Header().Set("Content-Type", ctype)

Expand Down
60 changes: 60 additions & 0 deletions core/corehttp/lazyseek.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package corehttp

import (
"fmt"
"io"
)

// The HTTP server uses seek to determine the file size. Actually _seeking_ can
// be slow so we wrap the seeker in a _lazy_ seeker.
type lazySeeker struct {
reader io.ReadSeeker

size int64
offset int64
realOffset int64
}

func (s *lazySeeker) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekEnd:
return s.Seek(s.size+offset, io.SeekStart)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't it s.size-offset ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're seeking from the end, the offset will already be negative.

Copy link
Member Author

@Stebalien Stebalien Sep 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this was wrong. It should have been s.size-1+offset. Nvm. Seeking to the end is correctly done with a Seek(-1, SeekEnd).

case io.SeekCurrent:
return s.Seek(s.offset+offset, io.SeekStart)
case io.SeekStart:
if offset < 0 {
return s.offset, fmt.Errorf("invalid seek offset")
}
s.offset = offset
return s.offset, nil
default:
return s.offset, fmt.Errorf("invalid whence: %d", whence)
}
}

func (s *lazySeeker) Read(b []byte) (int, error) {
// If we're past the end, EOF.
if s.offset >= s.size {
return 0, io.EOF
}

// actually seek
for s.offset != s.realOffset {
off, err := s.reader.Seek(s.offset, io.SeekStart)
if err != nil {
return 0, err
}
s.realOffset = off
}
off, err := s.reader.Read(b)
s.realOffset += int64(off)
s.offset += int64(off)
return off, err
}

func (s *lazySeeker) Close() error {
if closer, ok := s.reader.(io.Closer); ok {
return closer.Close()
}
return nil
}
137 changes: 137 additions & 0 deletions core/corehttp/lazyseek_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package corehttp

import (
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
)

type badSeeker struct {
io.ReadSeeker
}

var badSeekErr = fmt.Errorf("I'm a bad seeker")

func (bs badSeeker) Seek(offset int64, whence int) (int64, error) {
off, err := bs.ReadSeeker.Seek(0, io.SeekCurrent)
if err != nil {
panic(err)
}
return off, badSeekErr
}

func TestLazySeekerError(t *testing.T) {
underlyingBuffer := strings.NewReader("fubar")
s := &lazySeeker{
reader: badSeeker{underlyingBuffer},
size: underlyingBuffer.Size(),
}
off, err := s.Seek(0, io.SeekEnd)
if err != nil {
t.Fatal(err)
}
if off != s.size {
t.Fatal("expected to seek to the end")
}

// shouldn't have actually seeked.
b, err := ioutil.ReadAll(s)
if err != nil {
t.Fatal(err)
}
if len(b) != 0 {
t.Fatal("expected to read nothing")
}

// shouldn't need to actually seek.
off, err = s.Seek(0, io.SeekStart)
if err != nil {
t.Fatal(err)
}
if off != 0 {
t.Fatal("expected to seek to the start")
}
b, err = ioutil.ReadAll(s)
if err != nil {
t.Fatal(err)
}
if string(b) != "fubar" {
t.Fatal("expected to read string")
}

// should fail the second time.
off, err = s.Seek(0, io.SeekStart)
if err != nil {
t.Fatal(err)
}
if off != 0 {
t.Fatal("expected to seek to the start")
}
// right here...
b, err = ioutil.ReadAll(s)
if err == nil {
t.Fatalf("expected an error, got output %s", string(b))
}
if err != badSeekErr {
t.Fatalf("expected a bad seek error, got %s", err)
}
if len(b) != 0 {
t.Fatalf("expected to read nothing")
}
}

func TestLazySeeker(t *testing.T) {
underlyingBuffer := strings.NewReader("fubar")
s := &lazySeeker{
reader: underlyingBuffer,
size: underlyingBuffer.Size(),
}
expectByte := func(b byte) {
t.Helper()
var buf [1]byte
n, err := io.ReadFull(s, buf[:])
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("expected to read one byte, read %d", n)
}
if buf[0] != b {
t.Fatalf("expected %b, got %b", b, buf[0])
}
}
expectSeek := func(whence int, off, expOff int64, expErr string) {
t.Helper()
n, err := s.Seek(off, whence)
if expErr == "" {
if err != nil {
t.Fatal("unexpected seek error: ", err)
}
} else {
if err == nil || err.Error() != expErr {
t.Fatalf("expected %s, got %s", err, expErr)
}
}
if n != expOff {
t.Fatalf("expected offset %d, got, %d", expOff, n)
}
}

expectSeek(io.SeekEnd, 0, s.size, "")
b, err := ioutil.ReadAll(s)
if err != nil {
t.Fatal(err)
}
if len(b) != 0 {
t.Fatal("expected to read nothing")
}
expectSeek(io.SeekEnd, -1, s.size-1, "")
expectByte('r')
expectSeek(io.SeekStart, 0, 0, "")
expectByte('f')
expectSeek(io.SeekCurrent, 1, 2, "")
expectByte('b')
expectSeek(io.SeekCurrent, -100, 3, "invalid seek offset")
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ require (
github.com/ipfs/go-ipfs-ds-help v0.0.1
github.com/ipfs/go-ipfs-exchange-interface v0.0.1
github.com/ipfs/go-ipfs-exchange-offline v0.0.1
github.com/ipfs/go-ipfs-files v0.0.4
github.com/ipfs/go-ipfs-files v0.0.6
github.com/ipfs/go-ipfs-pinner v0.0.3
github.com/ipfs/go-ipfs-posinfo v0.0.1
github.com/ipfs/go-ipfs-provider v0.3.0
Expand All @@ -50,7 +50,7 @@ require (
github.com/ipfs/go-metrics-prometheus v0.0.2
github.com/ipfs/go-mfs v0.1.1
github.com/ipfs/go-path v0.0.7
github.com/ipfs/go-unixfs v0.2.1
github.com/ipfs/go-unixfs v0.2.3
github.com/ipfs/go-verifcid v0.0.1
github.com/ipfs/interface-go-ipfs-core v0.2.5
github.com/jbenet/go-is-domain v1.0.3
Expand Down
7 changes: 4 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,9 @@ github.com/ipfs/go-ipfs-exchange-offline v0.0.1 h1:P56jYKZF7lDDOLx5SotVh5KFxoY6C
github.com/ipfs/go-ipfs-exchange-offline v0.0.1/go.mod h1:WhHSFCVYX36H/anEKQboAzpUws3x7UeEGkzQc3iNkM0=
github.com/ipfs/go-ipfs-files v0.0.2/go.mod h1:INEFm0LL2LWXBhNJ2PMIIb2w45hpXgPjNoE7yA8Y1d4=
github.com/ipfs/go-ipfs-files v0.0.3/go.mod h1:INEFm0LL2LWXBhNJ2PMIIb2w45hpXgPjNoE7yA8Y1d4=
github.com/ipfs/go-ipfs-files v0.0.4 h1:WzRCivcybUQch/Qh6v8LBRhKtRsjnwyiuOV09mK7mrE=
github.com/ipfs/go-ipfs-files v0.0.4/go.mod h1:INEFm0LL2LWXBhNJ2PMIIb2w45hpXgPjNoE7yA8Y1d4=
github.com/ipfs/go-ipfs-files v0.0.6 h1:sMRtPiSmDrTA2FEiFTtk1vWgO2Dkg7bxXKJ+s8/cDAc=
github.com/ipfs/go-ipfs-files v0.0.6/go.mod h1:lVYE6sgAdtZN5825beJjSAHibw7WOBNPDWz5LaJeukg=
github.com/ipfs/go-ipfs-flags v0.0.1/go.mod h1:RnXBb9WV53GSfTrSDVK61NLTFKvWc60n+K9EgCDh+rA=
github.com/ipfs/go-ipfs-pinner v0.0.3 h1:ez/yNYYyH1W7DiCF/L29tmp6L7lBO8eqbJtPi2pHicA=
github.com/ipfs/go-ipfs-pinner v0.0.3/go.mod h1:s4kFZWLWGDudN8Jyd/GTpt222A12C2snA2+OTdy/7p8=
Expand Down Expand Up @@ -278,8 +279,8 @@ github.com/ipfs/go-todocounter v0.0.2 h1:9UBngSQhylg2UDcxSAtpkT+rEWFr26hDPXVStE8
github.com/ipfs/go-todocounter v0.0.2/go.mod h1:l5aErvQc8qKE2r7NDMjmq5UNAvuZy0rC8BHOplkWvZ4=
github.com/ipfs/go-unixfs v0.0.4/go.mod h1:eIo/p9ADu/MFOuyxzwU+Th8D6xoxU//r590vUpWyfz8=
github.com/ipfs/go-unixfs v0.1.0/go.mod h1:lysk5ELhOso8+Fed9U1QTGey2ocsfaZ18h0NCO2Fj9s=
github.com/ipfs/go-unixfs v0.2.1 h1:g51t9ODICFZ3F51FPivm8dE7NzYcdAQNUL9wGP5AYa0=
github.com/ipfs/go-unixfs v0.2.1/go.mod h1:IwAAgul1UQIcNZzKPYZWOCijryFBeCV79cNubPzol+k=
github.com/ipfs/go-unixfs v0.2.3 h1:VsZwK3Z6+rjFxha87GBrp3kZHDsztSIuKlsScr3Iw4s=
github.com/ipfs/go-unixfs v0.2.3/go.mod h1:SUdisfUjNoSDzzhGVxvCL9QO/nKdwXdr+gbMUdqcbYw=
github.com/ipfs/go-verifcid v0.0.1 h1:m2HI7zIuR5TFyQ1b79Da5N9dnnCP1vcu2QqawmWlK2E=
github.com/ipfs/go-verifcid v0.0.1/go.mod h1:5Hrva5KBeIog4A+UpqlaIU+DEstipcJYQQZc0g37pY0=
github.com/ipfs/interface-go-ipfs-core v0.2.5 h1:/rspOe8RbIxwtssEXHB+X9JXhOBDCQt8x50d2kFPXL8=
Expand Down
38 changes: 38 additions & 0 deletions test/sharness/t0113-gateway-symlink.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
#
# Copyright (c) Protocol Labs

test_description="Test symlink support on the HTTP gateway"

. lib/test-lib.sh

test_init_ipfs
test_launch_ipfs_daemon


test_expect_success "Create a test directory with symlinks" '
mkdir testfiles &&
echo "content" > testfiles/foo &&
ln -s foo testfiles/bar &&
test_cmp testfiles/foo testfiles/bar
'

test_expect_success "Add the test directory" '
HASH=$(ipfs add -Qr testfiles)
'

test_expect_success "Test the directory listing" '
curl "$GWAY_ADDR/ipfs/$HASH" > list_response &&
test_should_contain ">foo<" list_response &&
test_should_contain ">bar<" list_response
'

test_expect_success "Test the symlink" '
curl "$GWAY_ADDR/ipfs/$HASH/bar" > bar_actual &&
echo -n "foo" > bar_expected &&
test_cmp bar_expected bar_actual
'

test_kill_ipfs_daemon

test_done