@cryptotaxi247 / kubo / commits / 09d750172

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 }