Skip to content

Commit 688c4b9

Browse files
authored
Merge pull request ipfs/kubo#6680 from ipfs/fix/symlink-size
improve gateway symlink handling This commit was moved from ipfs/kubo@ca2767a
2 parents 2e46fb7 + 1fb1df9 commit 688c4b9

File tree

3 files changed

+235
-44
lines changed

3 files changed

+235
-44
lines changed

gateway/core/corehttp/gateway_handler.go

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
289289
}
290290

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

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

375-
type sizeReadSeeker interface {
376-
Size() (int64, error)
377-
378-
io.ReadSeeker
379-
}
380-
381-
type sizeSeeker struct {
382-
sizeReadSeeker
383-
}
384-
385-
func (s *sizeSeeker) Seek(offset int64, whence int) (int64, error) {
386-
if whence == io.SeekEnd && offset == 0 {
387-
return s.Size()
375+
func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, file files.File) {
376+
size, err := file.Size()
377+
if err != nil {
378+
http.Error(w, "cannot serve files with unknown sizes", http.StatusBadGateway)
379+
return
388380
}
389381

390-
return s.sizeReadSeeker.Seek(offset, whence)
391-
}
392-
393-
func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) {
394-
if sp, ok := content.(sizeReadSeeker); ok {
395-
content = &sizeSeeker{
396-
sizeReadSeeker: sp,
397-
}
382+
content := &lazySeeker{
383+
size: size,
384+
reader: file,
398385
}
399386

400-
ctype := mime.TypeByExtension(gopath.Ext(name))
401-
if ctype == "" {
402-
buf := make([]byte, 512)
403-
n, _ := io.ReadFull(content, buf[:])
404-
ctype = http.DetectContentType(buf[:n])
405-
_, err := content.Seek(0, io.SeekStart)
406-
if err != nil {
407-
http.Error(w, "seeker can't seek", http.StatusInternalServerError)
408-
return
387+
var ctype string
388+
if _, isSymlink := file.(*files.Symlink); isSymlink {
389+
// We should be smarter about resolving symlinks but this is the
390+
// "most correct" we can be without doing that.
391+
ctype = "inode/symlink"
392+
} else {
393+
ctype = mime.TypeByExtension(gopath.Ext(name))
394+
if ctype == "" {
395+
buf := make([]byte, 512)
396+
n, _ := io.ReadFull(content, buf[:])
397+
ctype = http.DetectContentType(buf[:n])
398+
_, err := content.Seek(0, io.SeekStart)
399+
if err != nil {
400+
http.Error(w, "seeker can't seek", http.StatusInternalServerError)
401+
return
402+
}
403+
}
404+
// Strip the encoding from the HTML Content-Type header and let the
405+
// browser figure it out.
406+
//
407+
// Fixes https://github.com/ipfs/go-ipfs/issues/2203
408+
if strings.HasPrefix(ctype, "text/html;") {
409+
ctype = "text/html"
409410
}
410-
}
411-
// Strip the encoding from the HTML Content-Type header and let the
412-
// browser figure it out.
413-
//
414-
// Fixes https://github.com/ipfs/go-ipfs/issues/2203
415-
if strings.HasPrefix(ctype, "text/html;") {
416-
ctype = "text/html"
417411
}
418412
w.Header().Set("Content-Type", ctype)
419413

gateway/core/corehttp/lazyseek.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package corehttp
2+
3+
import (
4+
"fmt"
5+
"io"
6+
)
7+
8+
// The HTTP server uses seek to determine the file size. Actually _seeking_ can
9+
// be slow so we wrap the seeker in a _lazy_ seeker.
10+
type lazySeeker struct {
11+
reader io.ReadSeeker
12+
13+
size int64
14+
offset int64
15+
realOffset int64
16+
}
17+
18+
func (s *lazySeeker) Seek(offset int64, whence int) (int64, error) {
19+
switch whence {
20+
case io.SeekEnd:
21+
return s.Seek(s.size+offset, io.SeekStart)
22+
case io.SeekCurrent:
23+
return s.Seek(s.offset+offset, io.SeekStart)
24+
case io.SeekStart:
25+
if offset < 0 {
26+
return s.offset, fmt.Errorf("invalid seek offset")
27+
}
28+
s.offset = offset
29+
return s.offset, nil
30+
default:
31+
return s.offset, fmt.Errorf("invalid whence: %d", whence)
32+
}
33+
}
34+
35+
func (s *lazySeeker) Read(b []byte) (int, error) {
36+
// If we're past the end, EOF.
37+
if s.offset >= s.size {
38+
return 0, io.EOF
39+
}
40+
41+
// actually seek
42+
for s.offset != s.realOffset {
43+
off, err := s.reader.Seek(s.offset, io.SeekStart)
44+
if err != nil {
45+
return 0, err
46+
}
47+
s.realOffset = off
48+
}
49+
off, err := s.reader.Read(b)
50+
s.realOffset += int64(off)
51+
s.offset += int64(off)
52+
return off, err
53+
}
54+
55+
func (s *lazySeeker) Close() error {
56+
if closer, ok := s.reader.(io.Closer); ok {
57+
return closer.Close()
58+
}
59+
return nil
60+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package corehttp
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"io/ioutil"
7+
"strings"
8+
"testing"
9+
)
10+
11+
type badSeeker struct {
12+
io.ReadSeeker
13+
}
14+
15+
var badSeekErr = fmt.Errorf("I'm a bad seeker")
16+
17+
func (bs badSeeker) Seek(offset int64, whence int) (int64, error) {
18+
off, err := bs.ReadSeeker.Seek(0, io.SeekCurrent)
19+
if err != nil {
20+
panic(err)
21+
}
22+
return off, badSeekErr
23+
}
24+
25+
func TestLazySeekerError(t *testing.T) {
26+
underlyingBuffer := strings.NewReader("fubar")
27+
s := &lazySeeker{
28+
reader: badSeeker{underlyingBuffer},
29+
size: underlyingBuffer.Size(),
30+
}
31+
off, err := s.Seek(0, io.SeekEnd)
32+
if err != nil {
33+
t.Fatal(err)
34+
}
35+
if off != s.size {
36+
t.Fatal("expected to seek to the end")
37+
}
38+
39+
// shouldn't have actually seeked.
40+
b, err := ioutil.ReadAll(s)
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
if len(b) != 0 {
45+
t.Fatal("expected to read nothing")
46+
}
47+
48+
// shouldn't need to actually seek.
49+
off, err = s.Seek(0, io.SeekStart)
50+
if err != nil {
51+
t.Fatal(err)
52+
}
53+
if off != 0 {
54+
t.Fatal("expected to seek to the start")
55+
}
56+
b, err = ioutil.ReadAll(s)
57+
if err != nil {
58+
t.Fatal(err)
59+
}
60+
if string(b) != "fubar" {
61+
t.Fatal("expected to read string")
62+
}
63+
64+
// should fail the second time.
65+
off, err = s.Seek(0, io.SeekStart)
66+
if err != nil {
67+
t.Fatal(err)
68+
}
69+
if off != 0 {
70+
t.Fatal("expected to seek to the start")
71+
}
72+
// right here...
73+
b, err = ioutil.ReadAll(s)
74+
if err == nil {
75+
t.Fatalf("expected an error, got output %s", string(b))
76+
}
77+
if err != badSeekErr {
78+
t.Fatalf("expected a bad seek error, got %s", err)
79+
}
80+
if len(b) != 0 {
81+
t.Fatalf("expected to read nothing")
82+
}
83+
}
84+
85+
func TestLazySeeker(t *testing.T) {
86+
underlyingBuffer := strings.NewReader("fubar")
87+
s := &lazySeeker{
88+
reader: underlyingBuffer,
89+
size: underlyingBuffer.Size(),
90+
}
91+
expectByte := func(b byte) {
92+
t.Helper()
93+
var buf [1]byte
94+
n, err := io.ReadFull(s, buf[:])
95+
if err != nil {
96+
t.Fatal(err)
97+
}
98+
if n != 1 {
99+
t.Fatalf("expected to read one byte, read %d", n)
100+
}
101+
if buf[0] != b {
102+
t.Fatalf("expected %b, got %b", b, buf[0])
103+
}
104+
}
105+
expectSeek := func(whence int, off, expOff int64, expErr string) {
106+
t.Helper()
107+
n, err := s.Seek(off, whence)
108+
if expErr == "" {
109+
if err != nil {
110+
t.Fatal("unexpected seek error: ", err)
111+
}
112+
} else {
113+
if err == nil || err.Error() != expErr {
114+
t.Fatalf("expected %s, got %s", err, expErr)
115+
}
116+
}
117+
if n != expOff {
118+
t.Fatalf("expected offset %d, got, %d", expOff, n)
119+
}
120+
}
121+
122+
expectSeek(io.SeekEnd, 0, s.size, "")
123+
b, err := ioutil.ReadAll(s)
124+
if err != nil {
125+
t.Fatal(err)
126+
}
127+
if len(b) != 0 {
128+
t.Fatal("expected to read nothing")
129+
}
130+
expectSeek(io.SeekEnd, -1, s.size-1, "")
131+
expectByte('r')
132+
expectSeek(io.SeekStart, 0, 0, "")
133+
expectByte('f')
134+
expectSeek(io.SeekCurrent, 1, 2, "")
135+
expectByte('b')
136+
expectSeek(io.SeekCurrent, -100, 3, "invalid seek offset")
137+
}

0 commit comments

Comments
 (0)