Skip to content

Commit

Permalink
Add backwards compatibility with old well-known resource (#2798)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcoPolo committed May 17, 2024
1 parent f8d74ef commit 024293c
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 9 deletions.
102 changes: 93 additions & 9 deletions p2p/http/libp2phttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package libp2phttp

import (
"bufio"
"context"
"crypto/tls"
"encoding/json"
"errors"
Expand All @@ -15,6 +16,7 @@ import (
"strconv"
"strings"
"sync"
"time"

lru "github.com/hashicorp/golang-lru/v2"
logging "github.com/ipfs/go-log/v2"
Expand All @@ -29,8 +31,16 @@ import (

var log = logging.Logger("libp2phttp")

var WellKnownRequestTimeout = 30 * time.Second

const ProtocolIDForMultistreamSelect = "/http/1.1"
const WellKnownProtocols = "/.well-known/libp2p/protocols"

// LegacyWellKnownProtocols refer to a the well-known resource used in an early
// draft of the libp2p+http spec. Some users have deployed this, and need backwards compatibility.
// Hopefully we can phase this out in the future. Context: https://github.com/libp2p/go-libp2p/pull/2797
const LegacyWellKnownProtocols = "/.well-known/libp2p"

const peerMetadataLimit = 8 << 10 // 8KB
const peerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache

Expand Down Expand Up @@ -145,6 +155,17 @@ type Host struct {
// here when a user calls `SetHTTPHandler` or `SetHTTPHandlerAtPath`.
WellKnownHandler WellKnownHandler

// EnableCompatibilityWithLegacyWellKnownEndpoint allows compatibility with
// an older version of the spec that defined the well-known resource as:
// .well-known/libp2p.
// For servers, this means hosting the well-known resource at both the
// legacy and current paths.
// For clients it means making two parallel requests and picking the first one that succeeds.
//
// Long term this should be deprecated once enough users have upgraded to a
// newer go-libp2p version and we can remove all this code.
EnableCompatibilityWithLegacyWellKnownEndpoint bool

// peerMetadata is an LRU cache of a peer's well-known protocol map.
peerMetadata *lru.Cache[peer.ID, PeerMeta]
// createHTTPTransport is used to lazily create the httpTransport in a thread-safe way.
Expand Down Expand Up @@ -272,6 +293,9 @@ func (h *Host) Serve() error {

h.serveMuxInit()
h.ServeMux.Handle(WellKnownProtocols, &h.WellKnownHandler)
if h.EnableCompatibilityWithLegacyWellKnownEndpoint {
h.ServeMux.Handle(LegacyWellKnownProtocols, &h.WellKnownHandler)
}

h.httpTransportInit()

Expand Down Expand Up @@ -405,7 +429,10 @@ func (s *streamReadCloser) Close() error {
}

func (rt *streamRoundTripper) GetPeerMetadata() (PeerMeta, error) {
return rt.httpHost.getAndStorePeerMetadata(rt, rt.server)
ctx := context.Background()
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(WellKnownRequestTimeout))
defer cancel()
return rt.httpHost.getAndStorePeerMetadata(ctx, rt, rt.server)
}

// RoundTrip implements http.RoundTripper.
Expand Down Expand Up @@ -476,7 +503,10 @@ func (rt *roundTripperForSpecificServer) GetPeerMetadata() (PeerMeta, error) {
}
}

wk, err := rt.httpHost.getAndStorePeerMetadata(rt, rt.server)
ctx := context.Background()
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(WellKnownRequestTimeout))
defer cancel()
wk, err := rt.httpHost.getAndStorePeerMetadata(ctx, rt, rt.server)
if err == nil {
rt.cachedProtos = wk
return wk, nil
Expand Down Expand Up @@ -541,7 +571,10 @@ func (rt *namespacedRoundTripper) RoundTrip(r *http.Request) (*http.Response, er

// NamespaceRoundTripper returns an http.RoundTripper that are scoped to the given protocol on the given server.
func (h *Host) NamespaceRoundTripper(roundtripper http.RoundTripper, p protocol.ID, server peer.ID) (*namespacedRoundTripper, error) {
protos, err := h.getAndStorePeerMetadata(roundtripper, server)
ctx := context.Background()
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(WellKnownRequestTimeout))
defer cancel()
protos, err := h.getAndStorePeerMetadata(ctx, roundtripper, server)
if err != nil {
return &namespacedRoundTripper{}, err
}
Expand Down Expand Up @@ -740,18 +773,72 @@ func normalizeHTTPMultiaddr(addr ma.Multiaddr) (ma.Multiaddr, bool) {
return ma.Join(beforeHTTPS, tlsComponent, httpComponent, afterHTTPS), isHTTPMultiaddr
}

// ProtocolPathPrefix looks up the protocol path in the well-known mapping and
// getAndStorePeerMetadata looks up the protocol path in the well-known mapping and
// returns it. Will only store the peer's protocol mapping if the server ID is
// provided.
func (h *Host) getAndStorePeerMetadata(roundtripper http.RoundTripper, server peer.ID) (PeerMeta, error) {
func (h *Host) getAndStorePeerMetadata(ctx context.Context, roundtripper http.RoundTripper, server peer.ID) (PeerMeta, error) {
if h.peerMetadata == nil {
h.peerMetadata = newPeerMetadataCache()
}
if meta, ok := h.peerMetadata.Get(server); server != "" && ok {
return meta, nil
}

req, err := http.NewRequest("GET", WellKnownProtocols, nil)
var meta PeerMeta
var err error
if h.EnableCompatibilityWithLegacyWellKnownEndpoint {
type metaAndErr struct {
m PeerMeta
err error
}
legacyRespCh := make(chan metaAndErr, 1)
wellKnownRespCh := make(chan metaAndErr, 1)
ctx, cancel := context.WithCancel(ctx)
go func() {
meta, err := requestPeerMeta(ctx, roundtripper, LegacyWellKnownProtocols)
legacyRespCh <- metaAndErr{meta, err}
}()
go func() {
meta, err := requestPeerMeta(ctx, roundtripper, WellKnownProtocols)
wellKnownRespCh <- metaAndErr{meta, err}
}()
select {
case resp := <-legacyRespCh:
if resp.err != nil {
resp = <-wellKnownRespCh
}
meta, err = resp.m, resp.err
case resp := <-wellKnownRespCh:
if resp.err != nil {
legacyResp := <-legacyRespCh
if legacyResp.err != nil {
// If both endpoints error, return the error from the well
// known resource (not the legacy well known resource)
meta, err = resp.m, resp.err
} else {
meta, err = legacyResp.m, legacyResp.err
}
} else {
meta, err = resp.m, resp.err
}
}
cancel()
} else {
meta, err = requestPeerMeta(ctx, roundtripper, WellKnownProtocols)
}
if err != nil {
return nil, err
}

if server != "" {
h.peerMetadata.Add(server, meta)
}

return meta, nil
}

func requestPeerMeta(ctx context.Context, roundtripper http.RoundTripper, wellKnownResource string) (PeerMeta, error) {
req, err := http.NewRequest("GET", wellKnownResource, nil)
if err != nil {
return nil, err
}
Expand All @@ -776,9 +863,6 @@ func (h *Host) getAndStorePeerMetadata(roundtripper http.RoundTripper, server pe
if err != nil {
return nil, err
}
if server != "" {
h.peerMetadata.Add(server, meta)
}

return meta, nil
}
Expand Down
98 changes: 98 additions & 0 deletions p2p/http/libp2phttp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"math/big"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"reflect"
Expand Down Expand Up @@ -621,3 +622,100 @@ func TestSetHandlerAtPath(t *testing.T) {
})
}
}

func TestServerLegacyWellKnownResource(t *testing.T) {
mkHTTPServer := func(wellKnown string) ma.Multiaddr {
mux := http.NewServeMux()
wk := libp2phttp.WellKnownHandler{}
mux.Handle(wellKnown, &wk)

mux.Handle("/ping/", httpping.Ping{})
wk.AddProtocolMeta(httpping.PingProtocolID, libp2phttp.ProtocolMeta{Path: "/ping/"})

server := &http.Server{Addr: "127.0.0.1:0", Handler: mux}

l, err := net.Listen("tcp", server.Addr)
require.NoError(t, err)

go server.Serve(l)
t.Cleanup(func() { server.Close() })
addrPort, err := netip.ParseAddrPort(l.Addr().String())
require.NoError(t, err)
return ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%d/http", addrPort.Addr().String(), addrPort.Port()))
}

mkServerlibp2phttp := func(enableLegacyWellKnown bool) ma.Multiaddr {
server := libp2phttp.Host{
EnableCompatibilityWithLegacyWellKnownEndpoint: enableLegacyWellKnown,
ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")},
InsecureAllowHTTP: true,
}
server.SetHTTPHandler(httpping.PingProtocolID, httpping.Ping{})
go server.Serve()
t.Cleanup(func() { server.Close() })
return server.Addrs()[0]
}

type testCase struct {
name string
client libp2phttp.Host
serverAddr ma.Multiaddr
expectErr bool
}

var testCases = []testCase{
{
name: "legacy server, client with compat",
client: libp2phttp.Host{EnableCompatibilityWithLegacyWellKnownEndpoint: true},
serverAddr: mkHTTPServer(libp2phttp.LegacyWellKnownProtocols),
},
{
name: "up-to-date http server, client with compat",
client: libp2phttp.Host{EnableCompatibilityWithLegacyWellKnownEndpoint: true},
serverAddr: mkHTTPServer(libp2phttp.WellKnownProtocols),
},
{
name: "up-to-date http server, client without compat",
client: libp2phttp.Host{},
serverAddr: mkHTTPServer(libp2phttp.WellKnownProtocols),
},
{
name: "libp2phttp server with compat, client with compat",
client: libp2phttp.Host{EnableCompatibilityWithLegacyWellKnownEndpoint: true},
serverAddr: mkServerlibp2phttp(true),
},
{
name: "libp2phttp server without compat, client with compat",
client: libp2phttp.Host{EnableCompatibilityWithLegacyWellKnownEndpoint: true},
serverAddr: mkServerlibp2phttp(false),
},
{
name: "libp2phttp server with compat, client without compat",
client: libp2phttp.Host{},
serverAddr: mkServerlibp2phttp(true),
},
{
name: "legacy server, client without compat",
client: libp2phttp.Host{},
serverAddr: mkHTTPServer(libp2phttp.LegacyWellKnownProtocols),
expectErr: true,
},
}

for i := range testCases {
tc := &testCases[i] // to not copy the lock in libp2phttp.Host
t.Run(tc.name, func(t *testing.T) {
if tc.expectErr {
_, err := tc.client.NamespacedClient(httpping.PingProtocolID, peer.AddrInfo{Addrs: []ma.Multiaddr{tc.serverAddr}})
require.Error(t, err)
return
}
httpClient, err := tc.client.NamespacedClient(httpping.PingProtocolID, peer.AddrInfo{Addrs: []ma.Multiaddr{tc.serverAddr}})
require.NoError(t, err)

err = httpping.SendPing(httpClient)
require.NoError(t, err)
})
}

}

0 comments on commit 024293c

Please sign in to comment.