gateway: make IPNSHostname complete
IPNSHostnameOption() touches the URL path only on the way in, but not on the way out. This commit makes it complete by touching the following URLs in responses: - Heading, file links, back links in directory listings - Redirecting /foo to /foo/ if there's an index.html link - Omit Suborigin header License: MIT Signed-off-by: Lars Gierth <larsg@systemli.org>
Lars Gierth committed
Aug 15, 2015 at 01:41 UTC
09d750172473a7991bca0b0dad9529a5bdbda2fb
3 files changed
+258
-20
core/corehttp/gateway_handler.go
+45
-10
@@ -90,6 +90,19 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
90
91
urlPath := r.URL.Path
92
93
+ // IPNSHostnameOption might have constructed an IPNS path using the Host header.
94
+ // In this case, we need the original path for constructing redirects
95
+ // and links that match the requested URL.
96
+ // For example, http://example.net would become /ipns/example.net, and
97
+ // the redirects and links would end up as http://example.net/ipns/example.net
98
+ originalUrlPath := urlPath
99
+ ipnsHostname := false
100
+ hdr := r.Header["X-IPNS-Original-Path"]
101
+ if len(hdr) > 0 {
102
+ originalUrlPath = hdr[0]
103
+ ipnsHostname = true
104
+ }
105
+
106
if i.config.BlockList != nil && i.config.BlockList.ShouldBlock(urlPath) {
107
w.WriteHeader(http.StatusForbidden)
108
w.Write([]byte("403 - Forbidden"))
@@ -112,10 +125,17 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
125
w.Header().Set("X-IPFS-Path", urlPath)
126
127
// Suborigin header, sandboxes apps from each other in the browser (even
115
- // though they are served from the same gateway domain). NOTE: This is not
116
- // yet widely supported by browsers.
117
- pathRoot := strings.SplitN(urlPath, "/", 4)[2]
118
- w.Header().Set("Suborigin", pathRoot)
128
+ // though they are served from the same gateway domain).
129
+ //
130
+ // Omited if the path was treated by IPNSHostnameOption(), for example
131
+ // a request for http://example.net/ would be changed to /ipns/example.net/,
132
+ // which would turn into an incorrect Suborigin: example.net header.
133
+ //
134
+ // NOTE: This is not yet widely supported by browsers.
135
+ if !ipnsHostname {
136
+ pathRoot := strings.SplitN(urlPath, "/", 4)[2]
137
+ w.Header().Set("Suborigin", pathRoot)
138
+ }
139
140
dr, err := uio.NewDagReader(ctx, nd, i.node.DAG)
141
if err != nil && err != uio.ErrIsDir {
@@ -150,13 +170,16 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
170
foundIndex := false
171
for _, link := range nd.Links {
172
if link.Name == "index.html" {
173
+ log.Debugf("found index.html link for %s", urlPath)
174
+ foundIndex = true
175
+
176
if urlPath[len(urlPath)-1] != '/' {
154
- http.Redirect(w, r, urlPath+"/", 302)
177
+ // See comment above where originalUrlPath is declared.
178
+ http.Redirect(w, r, originalUrlPath+"/", 302)
179
+ log.Debugf("redirect to %s", originalUrlPath+"/")
180
return
181
}
182
158
- log.Debug("found index")
159
- foundIndex = true
183
// return index page instead.
184
nd, err := core.Resolve(ctx, i.node, path.Path(urlPath+"/index.html"))
185
if err != nil {
@@ -177,7 +200,8 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
200
break
201
}
202
180
- di := directoryItem{link.Size, link.Name, gopath.Join(urlPath, link.Name)}
203
+ // See comment above where originalUrlPath is declared.
204
+ di := directoryItem{link.Size, link.Name, gopath.Join(originalUrlPath, link.Name)}
205
dirListing = append(dirListing, di)
206
}
207
@@ -185,7 +209,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
209
if r.Method != "HEAD" {
210
// construct the correct back link
211
// https://github.com/ipfs/go-ipfs/issues/1365
188
- var backLink string = r.URL.Path
212
+ var backLink string = urlPath
213
214
// don't go further up than /ipfs/$hash/
215
pathSplit := strings.Split(backLink, "/")
@@ -205,9 +229,20 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
229
}
230
}
231
232
+ // strip /ipfs/$hash from backlink if IPNSHostnameOption touched the path.
233
+ if ipnsHostname {
234
+ backLink = "/"
235
+ if len(pathSplit) > 5 {
236
+ // also strip the trailing segment, because it's a backlink
237
+ backLinkParts := pathSplit[3 : len(pathSplit)-2]
238
+ backLink += strings.Join(backLinkParts, "/") + "/"
239
+ }
240
+ }
241
+
242
+ // See comment above where originalUrlPath is declared.
243
tplData := listingTemplateData{
244
Listing: dirListing,
210
- Path: urlPath,
245
+ Path: originalUrlPath,
246
BackLink: backLink,
247
}
248
err := listingTemplate.Execute(w, tplData)
core/corehttp/gateway_test.go
+212
-10
@@ -63,26 +63,30 @@ func (dh *delegatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
63
dh.Handler.ServeHTTP(w, r)
64
}
65
66
-func TestGatewayGet(t *testing.T) {
67
- // mock node and namesys
68
- ns := mockNamesys{}
69
- n, err := newNodeWithMockNamesys(ns)
70
- if err != nil {
71
- t.Fatal(err)
66
+func doWithoutRedirect(req *http.Request) (*http.Response, error) {
67
+ tag := "without-redirect"
68
+ c := &http.Client{
69
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
70
+ return errors.New(tag)
71
+ },
72
+ }
73
+ res, err := c.Do(req)
74
+ if err != nil && !strings.Contains(err.Error(), tag) {
75
+ return nil, err
76
}
77
+ return res, nil
78
+}
79
74
- // mock ipfs object
75
- k, err := coreunix.Add(n, strings.NewReader("fnord"))
80
+func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, *core.IpfsNode) {
81
+ n, err := newNodeWithMockNamesys(ns)
82
if err != nil {
83
t.Fatal(err)
84
}
79
- ns["/ipns/example.com"] = path.FromString("/ipfs/" + k)
85
86
// need this variable here since we need to construct handler with
87
// listener, and server with handler. yay cycles.
88
dh := &delegatedHandler{}
89
ts := httptest.NewServer(dh)
85
- defer ts.Close()
90
91
dh.Handler, err = makeHandler(n,
92
ts.Listener,
@@ -93,6 +97,20 @@ func TestGatewayGet(t *testing.T) {
97
t.Fatal(err)
98
}
99
100
+ return ts, n
101
+}
102
+
103
+func TestGatewayGet(t *testing.T) {
104
+ ns := mockNamesys{}
105
+ ts, n := newTestServerAndNode(t, ns)
106
+ defer ts.Close()
107
+
108
+ k, err := coreunix.Add(n, strings.NewReader("fnord"))
109
+ if err != nil {
110
+ t.Fatal(err)
111
+ }
112
+ ns["/ipns/example.com"] = path.FromString("/ipfs/" + k)
113
+
114
t.Log(ts.URL)
115
for _, test := range []struct {
116
host string
@@ -135,3 +153,187 @@ func TestGatewayGet(t *testing.T) {
153
}
154
}
155
}
156
+
157
+func TestIPNSHostnameRedirect(t *testing.T) {
158
+ ns := mockNamesys{}
159
+ ts, n := newTestServerAndNode(t, ns)
160
+ t.Logf("test server url: %s", ts.URL)
161
+ defer ts.Close()
162
+
163
+ // create /ipns/example.net/foo/index.html
164
+ _, dagn1, err := coreunix.AddWrapped(n, strings.NewReader("_"), "_")
165
+ if err != nil {
166
+ t.Fatal(err)
167
+ }
168
+ _, dagn2, err := coreunix.AddWrapped(n, strings.NewReader("_"), "index.html")
169
+ if err != nil {
170
+ t.Fatal(err)
171
+ }
172
+ dagn1.AddNodeLink("foo", dagn2)
173
+ if err != nil {
174
+ t.Fatal(err)
175
+ }
176
+
177
+ err = n.DAG.AddRecursive(dagn1)
178
+ if err != nil {
179
+ t.Fatal(err)
180
+ }
181
+
182
+ k, err := dagn1.Key()
183
+ if err != nil {
184
+ t.Fatal(err)
185
+ }
186
+ t.Logf("k: %s\n", k)
187
+ ns["/ipns/example.net"] = path.FromString("/ipfs/" + k.String())
188
+
189
+ // make request to directory containing index.html
190
+ req, err := http.NewRequest("GET", ts.URL+"/foo", nil)
191
+ if err != nil {
192
+ t.Fatal(err)
193
+ }
194
+ req.Host = "example.net"
195
+
196
+ res, err := doWithoutRedirect(req)
197
+ if err != nil {
198
+ t.Fatal(err)
199
+ }
200
+
201
+ // expect 302 redirect to same path, but with trailing slash
202
+ if res.StatusCode != 302 {
203
+ t.Errorf("status is %d, expected 302", res.StatusCode)
204
+ }
205
+ hdr := res.Header["Location"]
206
+ if len(hdr) < 1 {
207
+ t.Errorf("location header not present")
208
+ } else if hdr[0] != "/foo/" {
209
+ t.Errorf("location header is %v, expected /foo/", hdr[0])
210
+ }
211
+}
212
+
213
+func TestIPNSHostnameBacklinks(t *testing.T) {
214
+ ns := mockNamesys{}
215
+ ts, n := newTestServerAndNode(t, ns)
216
+ t.Logf("test server url: %s", ts.URL)
217
+ defer ts.Close()
218
+
219
+ // create /ipns/example.net/foo/
220
+ _, dagn1, err := coreunix.AddWrapped(n, strings.NewReader("1"), "file.txt")
221
+ if err != nil {
222
+ t.Fatal(err)
223
+ }
224
+ _, dagn2, err := coreunix.AddWrapped(n, strings.NewReader("2"), "file.txt")
225
+ if err != nil {
226
+ t.Fatal(err)
227
+ }
228
+ _, dagn3, err := coreunix.AddWrapped(n, strings.NewReader("3"), "file.txt")
229
+ if err != nil {
230
+ t.Fatal(err)
231
+ }
232
+ dagn2.AddNodeLink("bar", dagn3)
233
+ dagn1.AddNodeLink("foo", dagn2)
234
+ if err != nil {
235
+ t.Fatal(err)
236
+ }
237
+
238
+ err = n.DAG.AddRecursive(dagn1)
239
+ if err != nil {
240
+ t.Fatal(err)
241
+ }
242
+
243
+ k, err := dagn1.Key()
244
+ if err != nil {
245
+ t.Fatal(err)
246
+ }
247
+ t.Logf("k: %s\n", k)
248
+ ns["/ipns/example.net"] = path.FromString("/ipfs/" + k.String())
249
+
250
+ // make request to directory listing
251
+ req, err := http.NewRequest("GET", ts.URL+"/foo/", nil)
252
+ if err != nil {
253
+ t.Fatal(err)
254
+ }
255
+ req.Host = "example.net"
256
+
257
+ res, err := doWithoutRedirect(req)
258
+ if err != nil {
259
+ t.Fatal(err)
260
+ }
261
+
262
+ // expect correct backlinks
263
+ body, err := ioutil.ReadAll(res.Body)
264
+ if err != nil {
265
+ t.Fatalf("error reading response: %s", err)
266
+ }
267
+ s := string(body)
268
+ t.Logf("body: %s\n", string(body))
269
+
270
+ if !strings.Contains(s, "Index of /foo/") {
271
+ t.Fatalf("expected a path in directory listing")
272
+ }
273
+ if !strings.Contains(s, "<a href=\"/\">") {
274
+ t.Fatalf("expected backlink in directory listing")
275
+ }
276
+ if !strings.Contains(s, "<a href=\"/foo/file.txt\">") {
277
+ t.Fatalf("expected file in directory listing")
278
+ }
279
+
280
+ // make request to directory listing
281
+ req, err = http.NewRequest("GET", ts.URL, nil)
282
+ if err != nil {
283
+ t.Fatal(err)
284
+ }
285
+ req.Host = "example.net"
286
+
287
+ res, err = doWithoutRedirect(req)
288
+ if err != nil {
289
+ t.Fatal(err)
290
+ }
291
+
292
+ // expect correct backlinks
293
+ body, err = ioutil.ReadAll(res.Body)
294
+ if err != nil {
295
+ t.Fatalf("error reading response: %s", err)
296
+ }
297
+ s = string(body)
298
+ t.Logf("body: %s\n", string(body))
299
+
300
+ if !strings.Contains(s, "Index of /") {
301
+ t.Fatalf("expected a path in directory listing")
302
+ }
303
+ if !strings.Contains(s, "<a href=\"/\">") {
304
+ t.Fatalf("expected backlink in directory listing")
305
+ }
306
+ if !strings.Contains(s, "<a href=\"/file.txt\">") {
307
+ t.Fatalf("expected file in directory listing")
308
+ }
309
+
310
+ // make request to directory listing
311
+ req, err = http.NewRequest("GET", ts.URL+"/foo/bar/", nil)
312
+ if err != nil {
313
+ t.Fatal(err)
314
+ }
315
+ req.Host = "example.net"
316
+
317
+ res, err = doWithoutRedirect(req)
318
+ if err != nil {
319
+ t.Fatal(err)
320
+ }
321
+
322
+ // expect correct backlinks
323
+ body, err = ioutil.ReadAll(res.Body)
324
+ if err != nil {
325
+ t.Fatalf("error reading response: %s", err)
326
+ }
327
+ s = string(body)
328
+ t.Logf("body: %s\n", string(body))
329
+
330
+ if !strings.Contains(s, "Index of /foo/bar/") {
331
+ t.Fatalf("expected a path in directory listing")
332
+ }
333
+ if !strings.Contains(s, "<a href=\"/foo/\">") {
334
+ t.Fatalf("expected backlink in directory listing")
335
+ }
336
+ if !strings.Contains(s, "<a href=\"/foo/bar/file.txt\">") {
337
+ t.Fatalf("expected file in directory listing")
338
+ }
339
+}
core/corehttp/ipns_hostname.go
+1
@@ -24,6 +24,7 @@ func IPNSHostnameOption() ServeOption {
24
if len(host) > 0 && isd.IsDomain(host) {
25
name := "/ipns/" + host
26
if _, err := n.Namesys.Resolve(ctx, name); err == nil {
27
+ r.Header["X-IPNS-Original-Path"] = []string{r.URL.Path}
28
r.URL.Path = name + r.URL.Path
29
}
30
}