Skip to content
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
88 changes: 58 additions & 30 deletions p2p/http/libp2phttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,19 +523,18 @@ func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error)
}
resp.Body = &streamReadCloser{resp.Body, s}

locUrl, err := resp.Location()
if err == nil {
// Location url in response. Is this a multiaddr uri? and is it relative?
// If it's relative we want to convert it to an absolute multiaddr uri
// so that the next request knows how to reach the endpoint.
if locUrl.Scheme == "multiaddr" && resp.Request.URL.Scheme == "multiaddr" {
// Check if it's a relative URI and turn it into an absolute one
u, err := relativeMultiaddrURIToAbs(resp.Request.URL, locUrl)
if err == nil {
// It was a relative URI and we were able to convert it to an absolute one
// Update the location header to be an absolute multiaddr uri
resp.Header.Set("Location", u.String())
if r.URL.Scheme == "multiaddr" {
// This was a multiaddr uri, we may need to convert relative URI
// references to absolute multiaddr ones so that the next request
// knows how to reach the endpoint.
locationHeader := resp.Header.Get("Location")
if locationHeader != "" {
Comment on lines +530 to +531
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious, why didn't this work in the previous version where we used
resp.Location
This under the hood resolves the relative path(or at least claims to)
https://github.com/golang/go/blob/master/src/net/http/response.go#L137

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It does, but it doesn't work with how we encode http paths in the
multiaddr URI. For multiaddr URIs in general we only use URI path components. A relative
URI reference should be relative to the http-path component not the whole
multiaddr. For example multiaddr:/ip4/1.2.3.4/http-path/foo with relative reference /bar
should resolve to multiaddr:/ip4/1.2.3.4/http-path/bar not multiaddr:/bar.

u, err := locationHeaderToMultiaddrURI(r.URL, locationHeader)
if err != nil {
return nil, fmt.Errorf("failed to convert location header (%s) from request (%s) to multiaddr uri: %w", locationHeader, r.URL, err)
}
// Update the location header to be an absolute multiaddr uri
resp.Header.Set("Location", u.String())
}
}

Expand All @@ -544,39 +543,68 @@ func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error)
return resp, nil
}

var errNotRelative = errors.New("not relative")

// relativeMultiaddrURIToAbs takes a relative multiaddr URI and turns it into an
// absolute one. Useful, for example, when a server gives us a relative URI for a redirect.
// It allows the following request (the one after redirected) to reach the correct server.
func relativeMultiaddrURIToAbs(original *url.URL, relative *url.URL) (*url.URL, error) {
// Is this a relative uri? We know if it is because non-relative URI's of the form:
// "multiaddr:/ip4/1.2.3.4/tcp/9899" when parsed by Go's url package will have url.OmitHost == true
// But if it is relative (just a path to an http resource e.g. /here-instead)
// a redirect will inherit the multiaddr scheme, but set url.OmitHost == false. It will also stringify as something like
// multiaddr://here-instead.
if relative.OmitHost {
// Not relative (at least we can't tell). Nothing we can do here
return nil, errNotRelative
// locationHeaderToMultiaddrURI takes our original URL and the response's Location header
// and, if the location header is relative, turns it into an absolute multiaddr uri.
// Refer to https://www.rfc-editor.org/rfc/rfc3986#section-4.2 for the
// definition of a Relative Reference.
func locationHeaderToMultiaddrURI(original *url.URL, locationHeader string) (*url.URL, error) {
if locationHeader == "" {
return nil, errors.New("location header is empty")
}
if strings.HasPrefix(locationHeader, "//") {
// This is a network path reference. We don't support these.
return nil, errors.New("network path reference not supported")
}

firstSegment := strings.SplitN(locationHeader, "/", 2)[0]
if strings.Contains(firstSegment, ":") {
// This location contains a scheme, so it's an absolute uri.
return url.Parse(locationHeader)
}

// It's a relative reference. We need to resolve it against the original URL.
if original.Scheme != "multiaddr" {
return nil, errors.New("original uri is not a multiaddr")
}

// Parse the original multiaddr
originalStr := original.RawPath
if originalStr == "" {
originalStr = original.Path
}
originalMa, err := ma.NewMultiaddr(originalStr)
if err != nil {
return nil, errors.New("original uri is not a multiaddr")
return nil, fmt.Errorf("original uri is not a valid multiaddr: %w", err)
}

// Get the target http path
var targetHTTPPath string
for _, c := range originalMa {
if c.Protocol().Code == ma.P_HTTP_PATH {
targetHTTPPath = string(c.RawValue())
break
}
}

// Resolve reference from targetURL and relativeURL
targetURL := url.URL{Path: targetHTTPPath}
relativeURL := url.URL{Path: locationHeader}
resolved := targetURL.ResolveReference(&relativeURL)

resolvedHTTPPath := resolved.Path
if len(resolvedHTTPPath) > 0 && resolvedHTTPPath[0] == '/' {
resolvedHTTPPath = resolvedHTTPPath[1:] // trim leading slash. It's implied by the http-path component
}

relativePathComponent, err := ma.NewComponent("http-path", relative.Path)
resolvedHTTPPathComponent, err := ma.NewComponent("http-path", resolvedHTTPPath)
if err != nil {
return nil, errors.New("relative path is not a valid http-path")
return nil, fmt.Errorf("relative path is not a valid http-path: %w", err)
}

withoutPath, afterAndIncludingPath := ma.SplitFunc(originalMa, func(c ma.Component) bool {
return c.Protocol().Code == ma.P_HTTP_PATH
})
withNewPath := withoutPath.AppendComponent(relativePathComponent)
withNewPath := withoutPath.AppendComponent(resolvedHTTPPathComponent)
if len(afterAndIncludingPath) > 1 {
// Include after path since it may include other parts
withNewPath = append(withNewPath, afterAndIncludingPath[1:]...)
Expand Down
18 changes: 17 additions & 1 deletion p2p/http/libp2phttp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,14 @@ func TestRedirects(t *testing.T) {
w.Write([]byte("hello"))
}))

serverHttpHost.SetHTTPHandlerAtPath("/redirect-1/0.0.1", "/foo/bar/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "../baz/")
w.WriteHeader(http.StatusMovedPermanently)
}))
serverHttpHost.SetHTTPHandlerAtPath("/redirect-1/0.0.1", "/foo/baz/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}))

clientStreamHost, err := libp2p.New(libp2p.NoListenAddrs, libp2p.Transport(libp2pquic.NewTransport))
require.NoError(t, err)
client := http.Client{Transport: &libp2phttp.Host{StreamHost: clientStreamHost}}
Expand All @@ -879,9 +887,17 @@ func TestRedirects(t *testing.T) {
u := fmt.Sprintf("multiaddr:%s/http-path/a%%2f", a)
f := fmt.Sprintf("http://127.0.0.1:%s/d/", port)
testCases = append(testCases, testCase{u, f})

u = fmt.Sprintf("multiaddr:%s/http-path/foo%%2Fbar", a)
f = fmt.Sprintf("http://127.0.0.1:%s/foo/baz/", port)
testCases = append(testCases, testCase{u, f})
} else {
u := fmt.Sprintf("multiaddr:%s/p2p/%s/http-path/a%%2f", a, serverHost.ID())
f := fmt.Sprintf("multiaddr:%s/p2p/%s/http-path/%%2Fd%%2F", a, serverHost.ID())
f := fmt.Sprintf("multiaddr:%s/p2p/%s/http-path/d%%2F", a, serverHost.ID())
testCases = append(testCases, testCase{u, f})

u = fmt.Sprintf("multiaddr:%s/p2p/%s/http-path/foo%%2Fbar", a, serverHost.ID())
f = fmt.Sprintf("multiaddr:%s/p2p/%s/http-path/foo%%2Fbaz%%2F", a, serverHost.ID())
testCases = append(testCases, testCase{u, f})
}
}
Expand Down
Loading