From deef00e408fa663947ee4d2f6e97bf9655836ec6 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 20 Jul 2023 10:04:06 -0700 Subject: [PATCH 01/72] Add SNI and HTTP_libp2p_token to Noise extensions --- p2p/security/noise/pb/payload.pb.go | 54 ++++++++++++++++++++--------- p2p/security/noise/pb/payload.proto | 2 ++ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/p2p/security/noise/pb/payload.pb.go b/p2p/security/noise/pb/payload.pb.go index 8e3a805a58..b35475aba2 100644 --- a/p2p/security/noise/pb/payload.pb.go +++ b/p2p/security/noise/pb/payload.pb.go @@ -27,6 +27,8 @@ type NoiseExtensions struct { WebtransportCerthashes [][]byte `protobuf:"bytes,1,rep,name=webtransport_certhashes,json=webtransportCerthashes" json:"webtransport_certhashes,omitempty"` StreamMuxers []string `protobuf:"bytes,2,rep,name=stream_muxers,json=streamMuxers" json:"stream_muxers,omitempty"` + SNI *string `protobuf:"bytes,3,opt,name=SNI" json:"SNI,omitempty"` + HTTPLibp2PToken *string `protobuf:"bytes,4,opt,name=HTTP_libp2p_token,json=HTTPLibp2pToken" json:"HTTP_libp2p_token,omitempty"` } func (x *NoiseExtensions) Reset() { @@ -75,6 +77,20 @@ func (x *NoiseExtensions) GetStreamMuxers() []string { return nil } +func (x *NoiseExtensions) GetSNI() string { + if x != nil && x.SNI != nil { + return *x.SNI + } + return "" +} + +func (x *NoiseExtensions) GetHTTPLibp2PToken() string { + if x != nil && x.HTTPLibp2PToken != nil { + return *x.HTTPLibp2PToken + } + return "" +} + type NoiseHandshakePayload struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -142,23 +158,27 @@ var File_pb_payload_proto protoreflect.FileDescriptor var file_pb_payload_proto_rawDesc = []byte{ 0x0a, 0x10, 0x70, 0x62, 0x2f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62, 0x22, 0x6f, 0x0a, 0x0f, 0x4e, 0x6f, 0x69, 0x73, 0x65, 0x45, - 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x17, 0x77, 0x65, 0x62, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x68, 0x61, - 0x73, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x16, 0x77, 0x65, 0x62, 0x74, - 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x65, 0x72, 0x74, 0x68, 0x61, 0x73, 0x68, - 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x6d, 0x75, 0x78, - 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x74, 0x72, 0x65, 0x61, - 0x6d, 0x4d, 0x75, 0x78, 0x65, 0x72, 0x73, 0x22, 0x92, 0x01, 0x0a, 0x15, 0x4e, 0x6f, 0x69, 0x73, - 0x65, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x5f, 0x73, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x53, 0x69, 0x67, 0x12, 0x33, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, - 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, - 0x2e, 0x4e, 0x6f, 0x69, 0x73, 0x65, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62, 0x22, 0xad, 0x01, 0x0a, 0x0f, 0x4e, 0x6f, 0x69, 0x73, 0x65, + 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x17, 0x77, 0x65, + 0x62, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x68, + 0x61, 0x73, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x16, 0x77, 0x65, 0x62, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x65, 0x72, 0x74, 0x68, 0x61, 0x73, + 0x68, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x6d, 0x75, + 0x78, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x4d, 0x75, 0x78, 0x65, 0x72, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x53, 0x4e, 0x49, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x53, 0x4e, 0x49, 0x12, 0x2a, 0x0a, 0x11, 0x48, 0x54, + 0x54, 0x50, 0x5f, 0x6c, 0x69, 0x62, 0x70, 0x32, 0x70, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x48, 0x54, 0x54, 0x50, 0x4c, 0x69, 0x62, 0x70, 0x32, + 0x70, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x92, 0x01, 0x0a, 0x15, 0x4e, 0x6f, 0x69, 0x73, 0x65, + 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, + 0x73, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x53, 0x69, 0x67, 0x12, 0x33, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, + 0x4e, 0x6f, 0x69, 0x73, 0x65, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, } var ( diff --git a/p2p/security/noise/pb/payload.proto b/p2p/security/noise/pb/payload.proto index ff303b0daf..a3ccfa6052 100644 --- a/p2p/security/noise/pb/payload.proto +++ b/p2p/security/noise/pb/payload.proto @@ -4,6 +4,8 @@ package pb; message NoiseExtensions { repeated bytes webtransport_certhashes = 1; repeated string stream_muxers = 2; + optional string SNI = 3; + optional string HTTP_libp2p_token = 4; } message NoiseHandshakePayload { From d92727bc580bbdad764a5f2232257db685e03755 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 20 Jul 2023 10:04:45 -0700 Subject: [PATCH 02/72] Initial libp2phttp work --- p2p/http/auth.go | 259 ++++++++++++++++++ p2p/http/auth_test.go | 146 +++++++++++ p2p/http/libp2phttp.go | 448 ++++++++++++++++++++++++++++++++ p2p/http/libp2phttp_test.go | 310 ++++++++++++++++++++++ p2p/http/ping.go | 23 ++ p2p/http/responsewriter.go | 138 ++++++++++ p2p/http/responsewriter_test.go | 62 +++++ 7 files changed, 1386 insertions(+) create mode 100644 p2p/http/auth.go create mode 100644 p2p/http/auth_test.go create mode 100644 p2p/http/libp2phttp.go create mode 100644 p2p/http/libp2phttp_test.go create mode 100644 p2p/http/ping.go create mode 100644 p2p/http/responsewriter.go create mode 100644 p2p/http/responsewriter_test.go diff --git a/p2p/http/auth.go b/p2p/http/auth.go new file mode 100644 index 0000000000..4b549bec44 --- /dev/null +++ b/p2p/http/auth.go @@ -0,0 +1,259 @@ +package libp2phttp + +import ( + "crypto/rand" + "crypto/sha256" + "errors" + "fmt" + "hash" + "net/http" + "strings" + + "github.com/flynn/noise" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/security/noise/pb" + "github.com/multiformats/go-multibase" + "google.golang.org/protobuf/proto" +) + +const payloadSigPrefix = "noise-libp2p-static-key:" + +type minioSHAFn struct{} + +func (h minioSHAFn) Hash() hash.Hash { return sha256.New() } +func (h minioSHAFn) HashName() string { return "SHA256" } + +var shaHashFn noise.HashFunc = minioSHAFn{} +var cipherSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, shaHashFn) + +type AuthState struct { + hs *noise.HandshakeState +} + +func WithNoiseAuthentication(hostKey crypto.PrivKey, requestHeader http.Header) (AuthState, error) { + s := AuthState{} + kp, err := noise.DH25519.GenerateKeypair(rand.Reader) + if err != nil { + return s, fmt.Errorf("error generating static keypair: %w", err) + } + + cfg := noise.Config{ + CipherSuite: cipherSuite, + Pattern: noise.HandshakeIX, + Initiator: true, + StaticKeypair: kp, + Prologue: nil, + } + + s.hs, err = noise.NewHandshakeState(cfg) + if err != nil { + return s, fmt.Errorf("error initializing handshake state: %w", err) + } + + payload, err := generateNoisePayload(hostKey, kp, nil) + if err != nil { + return s, fmt.Errorf("error generating noise payload: %w", err) + } + + // Allocate a buffer on the stack for the handshake message + hbuf := [2 << 10]byte{} + authMsg, _, _, err := s.hs.WriteMessage(hbuf[:0], payload) + if err != nil { + return s, fmt.Errorf("error writing handshake message: %w", err) + } + authMsgEncoded, err := multibase.Encode(multibase.Encodings["base32"], authMsg) + if err != nil { + return s, fmt.Errorf("error encoding handshake message: %w", err) + } + + requestHeader.Set("Authorization", "Libp2p-Noise-IX "+authMsgEncoded) + + return s, nil +} + +// AuthenticateClient verifies the Authorization header of the request and sets +// the Authentication-Info response header to allow the client to authenticate +// the server. Returns the peer.ID of the authenticated client. +// Returns an empty peer.ID if the client did not authenticate itself either by sending +// sending a `Libp2p-Noise-NX` Authorization header, or by not sending a `Libp2p-Noise-IX` Authorization header. +func AuthenticateClient(hostKey crypto.PrivKey, responseHeader http.Header, request *http.Request) (peer.ID, error) { + authValue := request.Header.Get("Authorization") + authMethod := strings.SplitN(authValue, " ", 2) + if len(authMethod) != 2 || authMethod[0] != "Libp2p-Noise-IX" { + return "", nil + } + + // Decode the handshake message + _, authMsg, err := multibase.Decode(authMethod[1]) + if err != nil { + return "", fmt.Errorf("error decoding handshake message: %w", err) + } + + kp, err := noise.DH25519.GenerateKeypair(rand.Reader) + if err != nil { + return "", fmt.Errorf("error generating static keypair: %w", err) + } + + cfg := noise.Config{ + CipherSuite: cipherSuite, + Pattern: noise.HandshakeIX, + Initiator: false, + StaticKeypair: kp, + Prologue: nil, + } + + hs, err := noise.NewHandshakeState(cfg) + if err != nil { + return "", fmt.Errorf("error initializing handshake state: %w", err) + } + + // Allocate a buffer on the stack for the payload + hbuf := [2 << 10]byte{} + + payload, _, _, err := hs.ReadMessage(hbuf[:0], authMsg) + if err != nil { + return "", fmt.Errorf("error reading handshake message: %w", err) + } + + // TODO handle the peer not sending a Static key (handle Libp2p-Noise-NX) + remotePeer, _, err := handleRemoteHandshakePayload(payload, hs.PeerStatic()) + if err != nil { + return "", fmt.Errorf("error handling remote handshake payload: %w", err) + } + + sni := "" + if request.TLS != nil { + sni = request.TLS.ServerName + } + + payload, err = generateNoisePayload(hostKey, kp, &pb.NoiseExtensions{ + SNI: &sni, + }) + if err != nil { + return "", fmt.Errorf("error generating noise payload: %w", err) + } + + authInfoMsg, _, _, err := hs.WriteMessage(hbuf[:0], payload) + if err != nil { + return "", fmt.Errorf("error writing handshake message: %w", err) + } + + authInfoMsgEncoded, err := multibase.Encode(multibase.Encodings["base32"], authInfoMsg) + if err != nil { + return "", fmt.Errorf("error encoding handshake message: %w", err) + } + responseHeader.Set("Authentication-Info", authMethod[0]+" "+authInfoMsgEncoded) + + return remotePeer, nil +} + +// AuthenticateServer returns the peer.ID of the server. It returns an error if the response does not include authentication info +func (s AuthState) AuthenticateServer(expectedSNI string, responseHeader http.Header) (peer.ID, error) { + authValue := responseHeader.Get("Authentication-Info") + authMethod := strings.SplitN(authValue, " ", 2) + if len(authMethod) != 2 || authMethod[0] != "Libp2p-Noise-IX" { + return "", errors.New("response does not include noise authentication info") + } + + // Decode the handshake message + _, authMsg, err := multibase.Decode(authMethod[1]) + if err != nil { + return "", fmt.Errorf("error decoding handshake message: %w", err) + } + + // Allocate a buffer on the stack for the payload + hbuf := [2 << 10]byte{} + + payload, cs1, cs2, err := s.hs.ReadMessage(hbuf[:0], authMsg) + if err != nil { + return "", fmt.Errorf("error reading handshake message: %w", err) + } + + if cs1 == nil || cs2 == nil { + return "", errors.New("expected ciphersuites to be present") + } + + server, extensions, err := handleRemoteHandshakePayload(payload, s.hs.PeerStatic()) + if err != nil { + return "", fmt.Errorf("error handling remote handshake payload: %w", err) + } + + if expectedSNI != "" { + if extensions == nil { + return "", errors.New("server is missing noise extensions") + } + + if extensions.SNI == nil { + return "", errors.New("server is missing SNI in noise extensions") + } + + if *extensions.SNI != expectedSNI { + return "", errors.New("server SNI in noise extension does not match expected SNI") + } + } + + return server, nil + +} + +func generateNoisePayload(hostKey crypto.PrivKey, localStatic noise.DHKey, ext *pb.NoiseExtensions) ([]byte, error) { + // obtain the public key from the handshake session, so we can sign it with + // our libp2p secret key. + localKeyRaw, err := crypto.MarshalPublicKey(hostKey.GetPublic()) + if err != nil { + return nil, fmt.Errorf("error serializing libp2p identity key: %w", err) + } + + // prepare payload to sign; perform signature. + toSign := append([]byte(payloadSigPrefix), localStatic.Public...) + signedPayload, err := hostKey.Sign(toSign) + if err != nil { + return nil, fmt.Errorf("error sigining handshake payload: %w", err) + } + + // create payload + payloadEnc, err := proto.Marshal(&pb.NoiseHandshakePayload{ + IdentityKey: localKeyRaw, + IdentitySig: signedPayload, + Extensions: ext, + }) + if err != nil { + return nil, fmt.Errorf("error marshaling handshake payload: %w", err) + } + return payloadEnc, nil +} + +// handleRemoteHandshakePayload unmarshals the handshake payload object sent +// by the remote peer and validates the signature against the peer's static Noise key. +// It returns the data attached to the payload. +func handleRemoteHandshakePayload(payload []byte, remoteStatic []byte) (peer.ID, *pb.NoiseExtensions, error) { + // unmarshal payload + nhp := new(pb.NoiseHandshakePayload) + err := proto.Unmarshal(payload, nhp) + if err != nil { + return "", nil, fmt.Errorf("error unmarshaling remote handshake payload: %w", err) + } + + // unpack remote peer's public libp2p key + remotePubKey, err := crypto.UnmarshalPublicKey(nhp.GetIdentityKey()) + if err != nil { + return "", nil, err + } + id, err := peer.IDFromPublicKey(remotePubKey) + if err != nil { + return "", nil, err + } + + // verify payload is signed by asserted remote libp2p key. + sig := nhp.GetIdentitySig() + msg := append([]byte(payloadSigPrefix), remoteStatic...) + ok, err := remotePubKey.Verify(msg, sig) + if err != nil { + return "", nil, fmt.Errorf("error verifying signature: %w", err) + } else if !ok { + return "", nil, fmt.Errorf("handshake signature invalid") + } + + return id, nhp.Extensions, nil +} diff --git a/p2p/http/auth_test.go b/p2p/http/auth_test.go new file mode 100644 index 0000000000..99e32ea8cb --- /dev/null +++ b/p2p/http/auth_test.go @@ -0,0 +1,146 @@ +package libp2phttp + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "testing" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/test" + "github.com/stretchr/testify/require" +) + +func TestHappyPathIX(t *testing.T) { + req, err := http.NewRequest("GET", "http://example.com", nil) + require.NoError(t, err) + respHeader := make(http.Header) + + clientKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) + require.NoError(t, err) + serverKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) + require.NoError(t, err) + + // Client starts a request + authState, err := WithNoiseAuthentication(clientKey, req.Header) + require.NoError(t, err) + + // Server responds to the request and sets the appropriate headers + clientPeerID, err := AuthenticateClient(serverKey, respHeader, req) + require.NoError(t, err) + require.NotEmpty(t, clientPeerID) + require.NotEmpty(t, respHeader.Get("Authentication-Info")) + + expectedClientKey, err := peer.IDFromPrivateKey(clientKey) + require.NoError(t, err) + require.Equal(t, expectedClientKey, clientPeerID) + + // Client receives the response and validates the auth info + serverPeerID, err := authState.AuthenticateServer("", respHeader) + require.NoError(t, err) + + expectedServerID, err := peer.IDFromPrivateKey(serverKey) + require.NoError(t, err) + require.Equal(t, expectedServerID, serverPeerID) +} + +func TestServerHandlerRequiresAuth(t *testing.T) { + clientKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) + require.NoError(t, err) + serverKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) + require.NoError(t, err) + + // Start the server + server := New() + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + + server.SetHttpHandlerAtPath("/my-app/1", "/my-app/1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // This handler requires libp2p auth + clientID, err := AuthenticateClient(serverKey, w.Header(), r) + + if err != nil || clientID == "" { + w.Header().Set("WWW-Authenticate", "Libp2p-Noise-IX") + w.WriteHeader(http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf("Hello, %s", clientID))) + })) + + // Middleware example + type Libp2pAuthKey struct{} + var libp2pAuthKey = Libp2pAuthKey{} + libp2pAuthMiddleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // This middleware requires libp2p auth + clientID, err := AuthenticateClient(serverKey, w.Header(), r) + + if err != nil || clientID == "" { + w.Header().Set("WWW-Authenticate", "Libp2p-Noise-IX") + w.WriteHeader(http.StatusUnauthorized) + return + } + + r = r.WithContext(context.WithValue(r.Context(), libp2pAuthKey, clientID)) + next.ServeHTTP(w, r) + }) + } + + server.SetHttpHandler("/my-app-middleware/1", libp2pAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf("Hello, %s", r.Context().Value(libp2pAuthKey)))) + }))) + + go server.Serve(l) + + client := http.Client{} + + pathsToTry := []string{ + fmt.Sprintf("http://%s/my-app/1", l.Addr().String()), + fmt.Sprintf("http://%s/my-app-middleware/1", l.Addr().String()), + } + + for _, path := range pathsToTry { + // Try without auth + resp, err := client.Get(path) + require.NoError(t, err) + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + + // Try with auth + req, err := http.NewRequest("GET", path, nil) + require.NoError(t, err) + + // Set the Authorization header + authState, err := WithNoiseAuthentication(clientKey, req.Header) + require.NoError(t, err) + + // Make the request + resp, err = client.Do(req) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, resp.StatusCode) + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + resp.Body.Close() + + expectedClientKey, err := peer.IDFromPrivateKey(clientKey) + require.NoError(t, err) + require.Equal(t, fmt.Sprintf("Hello, %s", expectedClientKey), string(respBody)) + + // Client receives the response and validates the auth info + serverPeerID, err := authState.AuthenticateServer("", resp.Header) + require.NoError(t, err) + + expectedServerID, err := peer.IDFromPrivateKey(serverKey) + require.NoError(t, err) + require.Equal(t, expectedServerID, serverPeerID) + } +} + +// TODO test with TLS + sni diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go new file mode 100644 index 0000000000..31b6fe33d3 --- /dev/null +++ b/p2p/http/libp2phttp.go @@ -0,0 +1,448 @@ +package libp2phttp + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + + lru "github.com/hashicorp/golang-lru/v2" + host "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + gostream "github.com/libp2p/go-libp2p/p2p/gostream" + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" +) + +const ProtocolIDForMultistreamSelect = "/http/1.1" +const PeerMetadataLimit = 8 << 10 // 8KB +const PeerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache + +// TODOs: +// - integrate with the conn gater and resource manager +// - skip client auth +// - Support listenAddr option to accept a multiaddr to listen on (could handle h3 as well) +// - Support withStreamHost option to accept a streamHost to listen on + +type WellKnownProtocolMeta struct { + Path string `json:"path"` +} + +type WellKnownProtoMap map[protocol.ID]WellKnownProtocolMeta + +type wellKnownHandler struct { + wellknownMapMu sync.Mutex + wellKnownMapping WellKnownProtoMap +} + +// StreamHostListen retuns a net.Listener that listens on libp2p streams for HTTP/1.1 messages. +func StreamHostListen(streamHost host.Host) (net.Listener, error) { + return gostream.Listen(streamHost, ProtocolIDForMultistreamSelect) +} + +func NewWellKnownHandler() wellKnownHandler { + return wellKnownHandler{wellKnownMapping: make(map[protocol.ID]WellKnownProtocolMeta)} +} + +func (h *wellKnownHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Check if the requests accepts JSON + accepts := r.Header.Get("Accept") + if accepts != "" && !(strings.Contains(accepts, "application/json") || strings.Contains(accepts, "*/*")) { + http.Error(w, "Only application/json is supported", http.StatusNotAcceptable) + } + + if r.Method != "GET" { + http.Error(w, "Only GET requests are supported", http.StatusMethodNotAllowed) + } + + // Return a JSON object with the well-known protocols + h.wellknownMapMu.Lock() + mapping, err := json.Marshal(h.wellKnownMapping) + h.wellknownMapMu.Unlock() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Length", strconv.Itoa(len(mapping))) + w.Write(mapping) +} + +func (h *wellKnownHandler) AddProtocolMapping(p protocol.ID, path string) { + h.wellknownMapMu.Lock() + h.wellKnownMapping[p] = WellKnownProtocolMeta{Path: path} + h.wellknownMapMu.Unlock() +} + +func (h *wellKnownHandler) RmProtocolMapping(p protocol.ID, path string) { + h.wellknownMapMu.Lock() + delete(h.wellKnownMapping, p) + h.wellknownMapMu.Unlock() +} + +type HTTPHost struct { + rootHandler http.ServeMux + wk wellKnownHandler + httpRoundTripper http.RoundTripper + recentHTTPAddrs *lru.Cache[peer.ID, httpAddr] + peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] +} + +type httpAddr struct { + addr string + scheme string +} + +// New creates a new HTTPHost. Use HTTPHost.Serve to start serving HTTP requests (both over libp2p streams and HTTP transport). +func New() *HTTPHost { + recentConnsLimit := http.DefaultTransport.(*http.Transport).MaxIdleConns + if recentConnsLimit < 1 { + recentConnsLimit = 32 + } + + recentHTTP, err := lru.New[peer.ID, httpAddr](recentConnsLimit) + peerMetadata, err2 := lru.New[peer.ID, WellKnownProtoMap](PeerMetadataLRUSize) + if err != nil || err2 != nil { + // Only happens if size is < 1. We set it to 32, so this should never happen. + panic(err) + } + + h := &HTTPHost{ + wk: NewWellKnownHandler(), + rootHandler: http.ServeMux{}, + httpRoundTripper: http.DefaultTransport, + recentHTTPAddrs: recentHTTP, + peerMetadata: peerMetadata, + } + h.rootHandler.Handle("/.well-known/libp2p", &h.wk) + + return h +} + +// Serve starts serving HTTP requests using the given listener. You may call this method multiple times with different listeners. +func (h *HTTPHost) Serve(l net.Listener) error { + return http.Serve(l, &h.rootHandler) +} + +// ServeTLS starts serving TLS+HTTP requests using the given listener. You may call this method multiple times with different listeners. +func (h *HTTPHost) ServeTLS(l net.Listener, certFile, keyFile string) error { + return http.ServeTLS(l, &h.rootHandler, certFile, keyFile) +} + +// SetHttpHandler sets the HTTP handler for a given protocol. Automatically +// manages the .well-known/libp2p mapping. +func (h *HTTPHost) SetHttpHandler(p protocol.ID, handler http.Handler) { + path := string(p) + "/" + h.SetHttpHandlerAtPath(p, path, handler) +} + +// SetHttpHandlerAtPath sets the HTTP handler for a given protocol using the +// given path. Automatically manages the .well-known/libp2p mapping. +func (h *HTTPHost) SetHttpHandlerAtPath(p protocol.ID, path string, handler http.Handler) { + h.wk.AddProtocolMapping(p, path) + h.rootHandler.Handle(path, handler) +} + +// TODO do we need this? Kind of complicated. We could the same with http.Serve and a custom libp2p listener. +// SetCustomHTTPHandler sets a custom HTTP handler for all HTTP over libp2p +// streams. It is up to the user to make sure the well-known mapping is set up +// correctly (NewWellKnownHandler could be helpful). This is useful if you're +// bringing in an existing HTTP ServeMux (or similar) to be used on top of +// libp2p streams. +// Use host.RemoveStreamHandler(libp2phttp.ProtocolIDForMultistreamSelect) to remove this handler. +func SetCustomHTTPHandler(streamHost host.Host, handler http.Handler) { + streamHost.SetStreamHandler(ProtocolIDForMultistreamSelect, func(s network.Stream) { + defer s.Close() + ServeReadWriter(s, handler) + }) +} + +type roundTripperOpts struct { + // todo SkipClientAuth bool + preferHTTPTransport bool +} + +type streamRoundTripper struct { + server peer.ID + h host.Host +} + +type streamReadCloser struct { + io.ReadCloser + s network.Stream +} + +func (s *streamReadCloser) Close() error { + s.s.Close() + return s.ReadCloser.Close() +} + +// RoundTrip implements http.RoundTripper. +func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + s, err := rt.h.NewStream(r.Context(), rt.server, ProtocolIDForMultistreamSelect) + if err != nil { + return nil, err + } + + go func() { + defer s.CloseWrite() + r.Write(s) + if r.Body != nil { + r.Body.Close() + } + }() + + resp, err := http.ReadResponse(bufio.NewReader(s), r) + if err != nil { + return nil, err + } + resp.Body = &streamReadCloser{resp.Body, s} + + return resp, nil +} + +// roundTripperForSpecificHost is an http.RoundTripper targets a specific server. Still reuses the underlying RoundTripper for the requests. +type roundTripperForSpecificServer struct { + http.RoundTripper + httpHost *HTTPHost + server peer.ID + targetServerAddr string + scheme string +} + +// RoundTrip implements http.RoundTripper. +func (rt *roundTripperForSpecificServer) RoundTrip(r *http.Request) (*http.Response, error) { + if (r.URL.Scheme != "" && r.URL.Scheme != rt.scheme) || (r.URL.Host != "" && r.URL.Host != rt.targetServerAddr) { + return nil, fmt.Errorf("this transport is only for requests to %s://%s", rt.scheme, rt.targetServerAddr) + } + r.URL.Scheme = rt.scheme + r.URL.Host = rt.targetServerAddr + r.Host = rt.targetServerAddr + resp, err := rt.RoundTripper.RoundTrip(r) + if err == nil && rt.server != "" { + rt.httpHost.recentHTTPAddrs.Add(rt.server, httpAddr{addr: rt.targetServerAddr, scheme: rt.scheme}) + } + return resp, err +} + +type namespacedRoundTripper struct { + http.RoundTripper + protocolPrefix string + protocolPrefixRaw string +} + +// RoundTrip implements http.RoundTripper. +func (rt *namespacedRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + if !strings.HasPrefix(r.URL.Path, rt.protocolPrefix) { + r.URL.Path = rt.protocolPrefix + r.URL.Path + } + if !strings.HasPrefix(r.URL.RawPath, rt.protocolPrefixRaw) { + r.URL.RawPath = rt.protocolPrefixRaw + r.URL.Path + } + + return rt.RoundTripper.RoundTrip(r) +} + +// NamespaceRoundTripper returns an http.RoundTripper that are scoped to the given protocol on the given server. +func (h *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p protocol.ID, server peer.ID) (namespacedRoundTripper, error) { + protos, err := h.GetAndStorePeerProtoMap(roundtripper, server) + if err != nil { + return namespacedRoundTripper{}, err + } + + v, ok := protos[p] + if !ok { + return namespacedRoundTripper{}, fmt.Errorf("no protocol %s for server %s", p, server) + } + + u, err := url.Parse(v.Path) + if err != nil { + return namespacedRoundTripper{}, fmt.Errorf("invalid path %s for protocol %s for server %s", v.Path, p, server) + } + + return namespacedRoundTripper{ + RoundTripper: roundtripper, + protocolPrefix: u.Path, + protocolPrefixRaw: u.RawPath, + }, nil +} + +func RoundTripperPreferHTTPTransport(o roundTripperOpts) roundTripperOpts { + o.preferHTTPTransport = true + return o +} + +type RoundTripperOptsFn func(o roundTripperOpts) roundTripperOpts + +func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.RoundTripper, error) { + options := roundTripperOpts{} + for _, o := range opts { + options = o(options) + } + + // Do we have a recent HTTP transport connection to this peer? + if a, ok := h.recentHTTPAddrs.Get(server.ID); server.ID != "" && ok { + return &roundTripperForSpecificServer{ + RoundTripper: h.httpRoundTripper, + httpHost: h, + server: server.ID, + targetServerAddr: a.addr, + scheme: a.scheme, + }, nil + } + + httpAddrs := make([]ma.Multiaddr, 0, 1) // The common case of a single http address + nonHttpAddrs := make([]ma.Multiaddr, 0, len(server.Addrs)) + + firstAddrIsHTTP := false + + for i, addr := range server.Addrs { + addr, isHttp := normalizeHTTPMultiaddr(addr) + if isHttp { + if i == 0 { + firstAddrIsHTTP = true + } + httpAddrs = append(httpAddrs, addr) + } else { + nonHttpAddrs = append(nonHttpAddrs, addr) + } + } + + // Do we have an existing connection to this peer? + existingStreamConn := false + if server.ID != "" { + existingStreamConn = len(streamHost.Network().ConnsToPeer(server.ID)) > 0 + } + + if len(httpAddrs) > 0 && (options.preferHTTPTransport || (firstAddrIsHTTP && !existingStreamConn)) { + useHTTPS := false + withoutHTTP, _ := ma.SplitFunc(httpAddrs[0], func(c ma.Component) bool { + return c.Protocol().Code == ma.P_HTTP + }) + // Check if we need to pop the TLS component at the end as well + maybeWithoutTLS, maybeTLS := ma.SplitLast(withoutHTTP) + if maybeTLS != nil && maybeTLS.Protocol().Code == ma.P_TLS { + useHTTPS = true + withoutHTTP = maybeWithoutTLS + } + na, err := manet.ToNetAddr(withoutHTTP) + if err != nil { + return nil, err + } + scheme := "http" + if useHTTPS { + scheme = "https" + } + + return &roundTripperForSpecificServer{ + RoundTripper: http.DefaultTransport, + httpHost: h, + server: server.ID, + targetServerAddr: na.String(), + scheme: scheme, + }, nil + } + + // Otherwise use a stream based transport + if !existingStreamConn { + if server.ID == "" { + return nil, fmt.Errorf("no http addresses for peer, and no server peer ID provided") + } + err := streamHost.Connect(context.Background(), peer.AddrInfo{ID: server.ID, Addrs: nonHttpAddrs}) + if err != nil { + return nil, fmt.Errorf("failed to connect to peer: %w", err) + } + } + + return NewStreamRoundTripper(streamHost, server.ID), nil +} + +func NewStreamRoundTripper(streamHost host.Host, server peer.ID) http.RoundTripper { + return &streamRoundTripper{h: streamHost, server: server} +} + +var httpComponent, _ = ma.NewComponent("http", "") +var tlsComponent, _ = ma.NewComponent("http", "") + +// normalizeHTTPMultiaddr converts an https multiaddr to a tls/http one. +// Returns a bool indicating if the input multiaddr has an http (or https) component. +func normalizeHTTPMultiaddr(addr ma.Multiaddr) (ma.Multiaddr, bool) { + isHTTPMultiaddr := false + beforeHTTPS, afterIncludingHTTPS := ma.SplitFunc(addr, func(c ma.Component) bool { + if c.Protocol().Code == ma.P_HTTP { + isHTTPMultiaddr = true + } + + if c.Protocol().Code == ma.P_HTTPS { + isHTTPMultiaddr = true + return true + } + return false + }) + + if afterIncludingHTTPS == nil { + // No HTTPS component, just return the original + return addr, isHTTPMultiaddr + } + + _, afterHTTPS := ma.SplitFirst(afterIncludingHTTPS) + + return ma.Join(beforeHTTPS, tlsComponent, httpComponent, afterHTTPS), isHTTPMultiaddr +} + +// ProtocolPathPrefix looks up the protocol path in the well-known mapping and returns it +func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, server peer.ID) (WellKnownProtoMap, error) { + if meta, ok := h.peerMetadata.Get(server); server != "" && ok { + return meta, nil + } + + req, err := http.NewRequest("GET", "/.well-known/libp2p", nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/json") + + client := &http.Client{Transport: roundtripper} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + body := [PeerMetadataLimit]byte{} + bytesRead := 0 + for { + n, err := resp.Body.Read(body[bytesRead:]) + bytesRead += n + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + if bytesRead >= PeerMetadataLimit { + return nil, fmt.Errorf("peer metadata too large") + } + } + + meta := WellKnownProtoMap{} + json.Unmarshal(body[:bytesRead], &meta) + if server != "" { + h.peerMetadata.Add(server, meta) + } + + return meta, nil +} diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go new file mode 100644 index 0000000000..c3246b097d --- /dev/null +++ b/p2p/http/libp2phttp_test.go @@ -0,0 +1,310 @@ +package libp2phttp_test + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/json" + "io" + "net" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/libp2p/go-libp2p" + host "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + libp2phttp "github.com/libp2p/go-libp2p/p2p/http" + ma "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" +) + +func TestHTTPOverStreams(t *testing.T) { + serverHost, err := libp2p.New( + libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1"), + ) + require.NoError(t, err) + + streamListener, err := libp2phttp.StreamHostListen(serverHost) + require.NoError(t, err) + defer streamListener.Close() + + httpHost := libp2phttp.New() + + httpHost.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello")) + })) + + // Start server + go httpHost.Serve(streamListener) + + // Start client + clientHost, err := libp2p.New(libp2p.NoListenAddrs) + require.NoError(t, err) + clientHost.Connect(context.Background(), peer.AddrInfo{ + ID: serverHost.ID(), + Addrs: serverHost.Addrs(), + }) + + clientRT := libp2phttp.NewStreamRoundTripper(clientHost, serverHost.ID()) + + client := &http.Client{Transport: clientRT} + + resp, err := client.Get("/hello") + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Equal(t, "hello", string(body)) +} + +func TestRoundTrippers(t *testing.T) { + serverHost, err := libp2p.New( + libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1"), + ) + require.NoError(t, err) + + streamListener, err := libp2phttp.StreamHostListen(serverHost) + require.NoError(t, err) + defer streamListener.Close() + + httpHost := libp2phttp.New() + + httpHost.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello")) + })) + + // Start stream based server + go httpHost.Serve(streamListener) + // Start HTTP transport based server + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + go httpHost.Serve(l) + + serverHTTPAddrParts := strings.Split(l.Addr().String(), ":") + require.Equal(t, 2, len(serverHTTPAddrParts)) + serverHTTPAddr := ma.StringCast("/ip4/" + serverHTTPAddrParts[0] + "/tcp/" + serverHTTPAddrParts[1] + "/http") + serverMultiaddrs := serverHost.Addrs() + serverMultiaddrs = append(serverMultiaddrs, serverHTTPAddr) + + testCases := []struct { + name string + setupRoundTripper func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper + expectStreamRoundTripper bool + }{ + { + name: "HTTP preferred", + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { + rt, err := clientHTTPHost.NewRoundTripper(clientStreamHost, peer.AddrInfo{ + ID: serverHost.ID(), + Addrs: serverMultiaddrs, + }, libp2phttp.RoundTripperPreferHTTPTransport) + require.NoError(t, err) + return rt + }, + }, + { + name: "HTTP first", + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { + rt, err := clientHTTPHost.NewRoundTripper(clientStreamHost, peer.AddrInfo{ + ID: serverHost.ID(), + Addrs: []ma.Multiaddr{serverHTTPAddr, serverHost.Addrs()[0]}, + }) + require.NoError(t, err) + return rt + }, + }, + { + name: "No HTTP transport", + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { + rt, err := clientHTTPHost.NewRoundTripper(clientStreamHost, peer.AddrInfo{ + ID: serverHost.ID(), + Addrs: []ma.Multiaddr{serverHost.Addrs()[0]}, + }) + require.NoError(t, err) + return rt + }, + expectStreamRoundTripper: true, + }, + { + name: "Stream transport first", + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { + rt, err := clientHTTPHost.NewRoundTripper(clientStreamHost, peer.AddrInfo{ + ID: serverHost.ID(), + Addrs: []ma.Multiaddr{serverHost.Addrs()[0], serverHTTPAddr}, + }) + require.NoError(t, err) + return rt + }, + expectStreamRoundTripper: true, + }, + { + name: "Existing stream transport connection", + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { + clientStreamHost.Connect(context.Background(), peer.AddrInfo{ + ID: serverHost.ID(), + Addrs: serverHost.Addrs(), + }) + rt, err := clientHTTPHost.NewRoundTripper(clientStreamHost, peer.AddrInfo{ + ID: serverHost.ID(), + Addrs: []ma.Multiaddr{serverHTTPAddr, serverHost.Addrs()[0]}, + }) + require.NoError(t, err) + return rt + }, + expectStreamRoundTripper: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Start client + clientHost, err := libp2p.New(libp2p.NoListenAddrs) + require.NoError(t, err) + defer clientHost.Close() + + clientHttpHost := libp2phttp.New() + + rt := tc.setupRoundTripper(t, clientHost, clientHttpHost) + if tc.expectStreamRoundTripper { + // Hack to get the private type of this roundtripper + typ := reflect.TypeOf(rt).String() + require.Contains(t, typ, "streamRoundTripper", "Expected stream based round tripper") + } + + for _, tc := range []bool{true, false} { + name := "" + if tc { + name = "with namespaced roundtripper" + } + t.Run(name, func(t *testing.T) { + var resp *http.Response + var err error + if tc { + nrt, err := (libp2phttp.New()).NamespaceRoundTripper(rt, "/hello", serverHost.ID()) + require.NoError(t, err) + client := &http.Client{Transport: &nrt} + resp, err = client.Get("/") + require.NoError(t, err) + } else { + client := &http.Client{Transport: rt} + resp, err = client.Get("/hello/") + require.NoError(t, err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "hello", string(body)) + }) + } + + // Read the .well-known/libp2p resource + wk, err := clientHttpHost.GetAndStorePeerProtoMap(rt, serverHost.ID()) + require.NoError(t, err) + + expectedMap := make(libp2phttp.WellKnownProtoMap) + expectedMap["/hello"] = libp2phttp.WellKnownProtocolMeta{Path: "/hello/"} + require.Equal(t, expectedMap, wk) + }) + } +} + +// TODO test with a native Go HTTP server +func TestPlainOldHTTPServer(t *testing.T) { + mux := http.NewServeMux() + wk := libp2phttp.NewWellKnownHandler() + mux.Handle("/.well-known/libp2p", &wk) + + mux.HandleFunc("/ping/", libp2phttp.Ping) + wk.AddProtocolMapping("/ping", "/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) + defer server.Close() + + // That's all for the server, now the client: + + serverAddrParts := strings.Split(l.Addr().String(), ":") + + testCases := []struct { + name string + do func(*testing.T, *http.Request) (*http.Response, error) + getWellKnown func(*testing.T) (libp2phttp.WellKnownProtoMap, error) + }{ + { + name: "using libp2phttp", + do: func(t *testing.T, request *http.Request) (*http.Response, error) { + clientHttpHost := libp2phttp.New() + rt, err := clientHttpHost.NewRoundTripper(nil, peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) + require.NoError(t, err) + + client := &http.Client{Transport: rt} + return client.Do(request) + }, + getWellKnown: func(t *testing.T) (libp2phttp.WellKnownProtoMap, error) { + clientHttpHost := libp2phttp.New() + rt, err := clientHttpHost.NewRoundTripper(nil, peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) + require.NoError(t, err) + return clientHttpHost.GetAndStorePeerProtoMap(rt, "") + }, + }, + { + name: "using stock http client", + do: func(t *testing.T, request *http.Request) (*http.Response, error) { + request.URL.Scheme = "http" + request.URL.Host = l.Addr().String() + request.Host = l.Addr().String() + + client := http.Client{} + return client.Do(request) + }, + getWellKnown: func(t *testing.T) (libp2phttp.WellKnownProtoMap, error) { + client := http.Client{} + resp, err := client.Get("http://" + l.Addr().String() + "/.well-known/libp2p") + require.NoError(t, err) + + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + var out libp2phttp.WellKnownProtoMap + err = json.Unmarshal(b, &out) + return out, err + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + body := [32]byte{} + _, err = rand.Reader.Read(body[:]) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPost, "/ping/", bytes.NewReader(body[:])) + require.NoError(t, err) + resp, err := tc.do(t, req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + rBody := [32]byte{} + _, err = io.ReadFull(resp.Body, rBody[:]) + require.NoError(t, err) + require.Equal(t, body, rBody) + + // Make sure we can get the well known resource + protoMap, err := tc.getWellKnown(t) + require.NoError(t, err) + + expectedMap := make(libp2phttp.WellKnownProtoMap) + expectedMap["/ping"] = libp2phttp.WellKnownProtocolMeta{Path: "/ping/"} + require.Equal(t, expectedMap, protoMap) + }) + } +} + +// TODO test with tls diff --git a/p2p/http/ping.go b/p2p/http/ping.go new file mode 100644 index 0000000000..16c7e67cf0 --- /dev/null +++ b/p2p/http/ping.go @@ -0,0 +1,23 @@ +package libp2phttp + +import ( + "io" + "net/http" + "strconv" +) + +const pingSize = 32 + +func Ping(w http.ResponseWriter, r *http.Request) { + body := [pingSize]byte{} + _, err := io.ReadFull(r.Body, body[:]) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Length", strconv.Itoa(pingSize)) + w.WriteHeader(http.StatusOK) + w.Write(body[:]) +} diff --git a/p2p/http/responsewriter.go b/p2p/http/responsewriter.go new file mode 100644 index 0000000000..ad037038f1 --- /dev/null +++ b/p2p/http/responsewriter.go @@ -0,0 +1,138 @@ +package libp2phttp + +import ( + "bufio" + "fmt" + "io" + "net/http" + "strconv" + "sync" + + logging "github.com/ipfs/go-log/v2" +) + +var bufWriterPool = sync.Pool{ + New: func() any { + return bufio.NewWriterSize(nil, 4<<10) + }, +} + +var bufReaderPool = sync.Pool{ + New: func() any { + return bufio.NewReaderSize(nil, 4<<10) + }, +} + +var log = logging.Logger("p2phttp") + +var _ http.ResponseWriter = (*httpResponseWriter)(nil) + +type httpResponseWriter struct { + w *bufio.Writer + directWriter io.Writer + header http.Header + wroteHeader bool + inferredContentLength bool +} + +// Header implements http.ResponseWriter +func (w *httpResponseWriter) Header() http.Header { + return w.header +} + +// Write implements http.ResponseWriter +func (w *httpResponseWriter) Write(b []byte) (int, error) { + if !w.wroteHeader { + if w.header.Get("Content-Type") == "" { + contentType := http.DetectContentType(b) + w.header.Set("Content-Type", contentType) + } + + if w.w.Available() > len(b) { + return w.w.Write(b) + } + } + + // Ran out of buffered space, We should check if we need to write the headers. + if !w.wroteHeader { + if w.header.Get("Content-Length") == "" && w.header.Get("Transfer-Encoding") == "" { + log.Error("Handler did not set the Content-Length or Transfer-Encoding header. Clients may fail to parse this response") + } + w.WriteHeader(http.StatusOK) + } else if w.inferredContentLength { + log.Error("Tried to infer content length, but another write happened, so content length is wrong and headers are already written. This response may fail to parse by clients") + } + + return w.w.Write(b) +} + +func (w *httpResponseWriter) flush() { + if !w.wroteHeader { + // Be nice for small things + if w.header.Get("Content-Length") == "" { + w.inferredContentLength = true + w.header.Set("Content-Length", strconv.Itoa(w.w.Buffered())) + } + + // If WriteHeader has not yet been called, Write calls + // WriteHeader(http.StatusOK) before writing the data. + w.WriteHeader(http.StatusOK) + } + w.w.Flush() +} + +// WriteHeader implements http.ResponseWriter +func (w *httpResponseWriter) WriteHeader(statusCode int) { + if w.wroteHeader { + log.Errorf("multiple WriteHeader calls dropping %d", statusCode) + return + } + w.wroteHeader = true + w.writeStatusLine(statusCode) + w.header.Write(w.directWriter) + w.directWriter.Write([]byte("\r\n")) +} + +// Copied from Go stdlib https://cs.opensource.google/go/go/+/refs/tags/go1.20.2:src/net/http/server.go;drc=ea4631cc0cf301c824bd665a7980c13289ab5c9d;l=1533 +func (w *httpResponseWriter) writeStatusLine(code int) { + // Stack allocated + scratch := [4]byte{} + // Always HTTP/1.1 + w.directWriter.Write([]byte("HTTP/1.1 ")) + + if text := http.StatusText(code); text != "" { + w.directWriter.Write(strconv.AppendInt(scratch[:0], int64(code), 10)) + w.directWriter.Write([]byte(" ")) + w.directWriter.Write([]byte(text)) + w.directWriter.Write([]byte("\r\n")) + } else { + // don't worry about performance + fmt.Fprintf(w.directWriter, "%03d status code %d\r\n", code, code) + } +} + +func ServeReadWriter(rw io.ReadWriter, handler http.Handler) { + r := bufReaderPool.Get().(*bufio.Reader) + r.Reset(rw) + defer bufReaderPool.Put(r) + + buffedWriter := bufWriterPool.Get().(*bufio.Writer) + buffedWriter.Reset(rw) + defer bufWriterPool.Put(buffedWriter) + w := httpResponseWriter{ + w: buffedWriter, + directWriter: rw, + header: make(http.Header), + } + defer w.flush() + + req, err := http.ReadRequest(r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.w.Flush() + log.Errorf("error reading request: %s", err) + return + } + + handler.ServeHTTP(&w, req) +} diff --git a/p2p/http/responsewriter_test.go b/p2p/http/responsewriter_test.go new file mode 100644 index 0000000000..2eb4007d0a --- /dev/null +++ b/p2p/http/responsewriter_test.go @@ -0,0 +1,62 @@ +package libp2phttp + +import ( + "bufio" + "bytes" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestResponseLooksCorrect(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost/", bytes.NewReader([]byte(""))) + require.NoError(t, err) + reqBuf := bytes.Buffer{} + req.Write(&reqBuf) + + resp := bytes.Buffer{} + respWriter := bufio.NewWriter(&resp) + s := bufio.NewReadWriter(bufio.NewReader(&reqBuf), respWriter) + + ServeReadWriter(s, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello world")) + })) + + respWriter.Flush() + parsedResponse, err := http.ReadResponse(bufio.NewReader(&resp), nil) + require.NoError(t, err) + respBody, err := io.ReadAll(parsedResponse.Body) + require.NoError(t, err) + require.Equal(t, "Hello world", string(respBody)) + require.Equal(t, len("Hello world"), int(parsedResponse.ContentLength)) +} + +func TestMultipleWritesButSmallResponseLooksCorrect(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost/", bytes.NewReader([]byte(""))) + require.NoError(t, err) + reqBuf := bytes.Buffer{} + req.Write(&reqBuf) + + resp := bytes.Buffer{} + respWriter := bufio.NewWriter(&resp) + s := bufio.NewReadWriter(bufio.NewReader(&reqBuf), respWriter) + + ServeReadWriter(s, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello world 1 ")) + w.Write([]byte("2 ")) + w.Write([]byte("3 ")) + w.Write([]byte("4 ")) + w.Write([]byte("5 ")) + w.Write([]byte("6 ")) + })) + + respWriter.Flush() + parsedResponse, err := http.ReadResponse(bufio.NewReader(&resp), nil) + require.NoError(t, err) + respBody, err := io.ReadAll(parsedResponse.Body) + require.NoError(t, err) + require.Equal(t, "Hello world 1 2 3 4 5 6 ", string(respBody)) + require.Equal(t, len("Hello world 1 2 3 4 5 6 "), int(parsedResponse.ContentLength)) +} From e814a07f11aff3dd2bb0ef1ec45619261c099f48 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 20 Jul 2023 10:05:24 -0700 Subject: [PATCH 03/72] Remove custom response writer --- p2p/http/libp2phttp.go | 14 ---- p2p/http/responsewriter.go | 138 -------------------------------- p2p/http/responsewriter_test.go | 62 -------------- 3 files changed, 214 deletions(-) delete mode 100644 p2p/http/responsewriter.go delete mode 100644 p2p/http/responsewriter_test.go diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 31b6fe33d3..9da308c4d1 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -153,20 +153,6 @@ func (h *HTTPHost) SetHttpHandlerAtPath(p protocol.ID, path string, handler http h.rootHandler.Handle(path, handler) } -// TODO do we need this? Kind of complicated. We could the same with http.Serve and a custom libp2p listener. -// SetCustomHTTPHandler sets a custom HTTP handler for all HTTP over libp2p -// streams. It is up to the user to make sure the well-known mapping is set up -// correctly (NewWellKnownHandler could be helpful). This is useful if you're -// bringing in an existing HTTP ServeMux (or similar) to be used on top of -// libp2p streams. -// Use host.RemoveStreamHandler(libp2phttp.ProtocolIDForMultistreamSelect) to remove this handler. -func SetCustomHTTPHandler(streamHost host.Host, handler http.Handler) { - streamHost.SetStreamHandler(ProtocolIDForMultistreamSelect, func(s network.Stream) { - defer s.Close() - ServeReadWriter(s, handler) - }) -} - type roundTripperOpts struct { // todo SkipClientAuth bool preferHTTPTransport bool diff --git a/p2p/http/responsewriter.go b/p2p/http/responsewriter.go deleted file mode 100644 index ad037038f1..0000000000 --- a/p2p/http/responsewriter.go +++ /dev/null @@ -1,138 +0,0 @@ -package libp2phttp - -import ( - "bufio" - "fmt" - "io" - "net/http" - "strconv" - "sync" - - logging "github.com/ipfs/go-log/v2" -) - -var bufWriterPool = sync.Pool{ - New: func() any { - return bufio.NewWriterSize(nil, 4<<10) - }, -} - -var bufReaderPool = sync.Pool{ - New: func() any { - return bufio.NewReaderSize(nil, 4<<10) - }, -} - -var log = logging.Logger("p2phttp") - -var _ http.ResponseWriter = (*httpResponseWriter)(nil) - -type httpResponseWriter struct { - w *bufio.Writer - directWriter io.Writer - header http.Header - wroteHeader bool - inferredContentLength bool -} - -// Header implements http.ResponseWriter -func (w *httpResponseWriter) Header() http.Header { - return w.header -} - -// Write implements http.ResponseWriter -func (w *httpResponseWriter) Write(b []byte) (int, error) { - if !w.wroteHeader { - if w.header.Get("Content-Type") == "" { - contentType := http.DetectContentType(b) - w.header.Set("Content-Type", contentType) - } - - if w.w.Available() > len(b) { - return w.w.Write(b) - } - } - - // Ran out of buffered space, We should check if we need to write the headers. - if !w.wroteHeader { - if w.header.Get("Content-Length") == "" && w.header.Get("Transfer-Encoding") == "" { - log.Error("Handler did not set the Content-Length or Transfer-Encoding header. Clients may fail to parse this response") - } - w.WriteHeader(http.StatusOK) - } else if w.inferredContentLength { - log.Error("Tried to infer content length, but another write happened, so content length is wrong and headers are already written. This response may fail to parse by clients") - } - - return w.w.Write(b) -} - -func (w *httpResponseWriter) flush() { - if !w.wroteHeader { - // Be nice for small things - if w.header.Get("Content-Length") == "" { - w.inferredContentLength = true - w.header.Set("Content-Length", strconv.Itoa(w.w.Buffered())) - } - - // If WriteHeader has not yet been called, Write calls - // WriteHeader(http.StatusOK) before writing the data. - w.WriteHeader(http.StatusOK) - } - w.w.Flush() -} - -// WriteHeader implements http.ResponseWriter -func (w *httpResponseWriter) WriteHeader(statusCode int) { - if w.wroteHeader { - log.Errorf("multiple WriteHeader calls dropping %d", statusCode) - return - } - w.wroteHeader = true - w.writeStatusLine(statusCode) - w.header.Write(w.directWriter) - w.directWriter.Write([]byte("\r\n")) -} - -// Copied from Go stdlib https://cs.opensource.google/go/go/+/refs/tags/go1.20.2:src/net/http/server.go;drc=ea4631cc0cf301c824bd665a7980c13289ab5c9d;l=1533 -func (w *httpResponseWriter) writeStatusLine(code int) { - // Stack allocated - scratch := [4]byte{} - // Always HTTP/1.1 - w.directWriter.Write([]byte("HTTP/1.1 ")) - - if text := http.StatusText(code); text != "" { - w.directWriter.Write(strconv.AppendInt(scratch[:0], int64(code), 10)) - w.directWriter.Write([]byte(" ")) - w.directWriter.Write([]byte(text)) - w.directWriter.Write([]byte("\r\n")) - } else { - // don't worry about performance - fmt.Fprintf(w.directWriter, "%03d status code %d\r\n", code, code) - } -} - -func ServeReadWriter(rw io.ReadWriter, handler http.Handler) { - r := bufReaderPool.Get().(*bufio.Reader) - r.Reset(rw) - defer bufReaderPool.Put(r) - - buffedWriter := bufWriterPool.Get().(*bufio.Writer) - buffedWriter.Reset(rw) - defer bufWriterPool.Put(buffedWriter) - w := httpResponseWriter{ - w: buffedWriter, - directWriter: rw, - header: make(http.Header), - } - defer w.flush() - - req, err := http.ReadRequest(r) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.w.Flush() - log.Errorf("error reading request: %s", err) - return - } - - handler.ServeHTTP(&w, req) -} diff --git a/p2p/http/responsewriter_test.go b/p2p/http/responsewriter_test.go deleted file mode 100644 index 2eb4007d0a..0000000000 --- a/p2p/http/responsewriter_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package libp2phttp - -import ( - "bufio" - "bytes" - "io" - "net/http" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestResponseLooksCorrect(t *testing.T) { - req, err := http.NewRequest("GET", "http://localhost/", bytes.NewReader([]byte(""))) - require.NoError(t, err) - reqBuf := bytes.Buffer{} - req.Write(&reqBuf) - - resp := bytes.Buffer{} - respWriter := bufio.NewWriter(&resp) - s := bufio.NewReadWriter(bufio.NewReader(&reqBuf), respWriter) - - ServeReadWriter(s, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Hello world")) - })) - - respWriter.Flush() - parsedResponse, err := http.ReadResponse(bufio.NewReader(&resp), nil) - require.NoError(t, err) - respBody, err := io.ReadAll(parsedResponse.Body) - require.NoError(t, err) - require.Equal(t, "Hello world", string(respBody)) - require.Equal(t, len("Hello world"), int(parsedResponse.ContentLength)) -} - -func TestMultipleWritesButSmallResponseLooksCorrect(t *testing.T) { - req, err := http.NewRequest("GET", "http://localhost/", bytes.NewReader([]byte(""))) - require.NoError(t, err) - reqBuf := bytes.Buffer{} - req.Write(&reqBuf) - - resp := bytes.Buffer{} - respWriter := bufio.NewWriter(&resp) - s := bufio.NewReadWriter(bufio.NewReader(&reqBuf), respWriter) - - ServeReadWriter(s, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Hello world 1 ")) - w.Write([]byte("2 ")) - w.Write([]byte("3 ")) - w.Write([]byte("4 ")) - w.Write([]byte("5 ")) - w.Write([]byte("6 ")) - })) - - respWriter.Flush() - parsedResponse, err := http.ReadResponse(bufio.NewReader(&resp), nil) - require.NoError(t, err) - respBody, err := io.ReadAll(parsedResponse.Body) - require.NoError(t, err) - require.Equal(t, "Hello world 1 2 3 4 5 6 ", string(respBody)) - require.Equal(t, len("Hello world 1 2 3 4 5 6 "), int(parsedResponse.ContentLength)) -} From 9d9426bfac4829c477a254fe30400ea8e04e6bf6 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 20 Jul 2023 10:35:14 -0700 Subject: [PATCH 04/72] Initial self review --- p2p/http/auth.go | 14 +++++++------ p2p/http/libp2phttp.go | 39 ++++++++++++++++++++++++------------- p2p/http/libp2phttp_test.go | 2 +- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/p2p/http/auth.go b/p2p/http/auth.go index 4b549bec44..458b45bb1a 100644 --- a/p2p/http/auth.go +++ b/p2p/http/auth.go @@ -134,11 +134,15 @@ func AuthenticateClient(hostKey crypto.PrivKey, responseHeader http.Header, requ return "", fmt.Errorf("error generating noise payload: %w", err) } - authInfoMsg, _, _, err := hs.WriteMessage(hbuf[:0], payload) + authInfoMsg, cs1, cs2, err := hs.WriteMessage(hbuf[:0], payload) if err != nil { return "", fmt.Errorf("error writing handshake message: %w", err) } + if cs1 == nil || cs2 == nil { + return "", errors.New("expected handshake to be complete") + } + authInfoMsgEncoded, err := multibase.Encode(multibase.Encodings["base32"], authInfoMsg) if err != nil { return "", fmt.Errorf("error encoding handshake message: %w", err) @@ -171,7 +175,7 @@ func (s AuthState) AuthenticateServer(expectedSNI string, responseHeader http.He } if cs1 == nil || cs2 == nil { - return "", errors.New("expected ciphersuites to be present") + return "", errors.New("expected handshake to be complete") } server, extensions, err := handleRemoteHandshakePayload(payload, s.hs.PeerStatic()) @@ -198,9 +202,7 @@ func (s AuthState) AuthenticateServer(expectedSNI string, responseHeader http.He } func generateNoisePayload(hostKey crypto.PrivKey, localStatic noise.DHKey, ext *pb.NoiseExtensions) ([]byte, error) { - // obtain the public key from the handshake session, so we can sign it with - // our libp2p secret key. - localKeyRaw, err := crypto.MarshalPublicKey(hostKey.GetPublic()) + localPubKeyRaw, err := crypto.MarshalPublicKey(hostKey.GetPublic()) if err != nil { return nil, fmt.Errorf("error serializing libp2p identity key: %w", err) } @@ -214,7 +216,7 @@ func generateNoisePayload(hostKey crypto.PrivKey, localStatic noise.DHKey, ext * // create payload payloadEnc, err := proto.Marshal(&pb.NoiseHandshakePayload{ - IdentityKey: localKeyRaw, + IdentityKey: localPubKeyRaw, IdentitySig: signedPayload, Extensions: ext, }) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 9da308c4d1..e5a7e27168 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -1,3 +1,5 @@ +// HTTP semantics with libp2p. Can use a libp2p stream transport or stock HTTP +// transports. This API is experimental and will likely change soon. Implements libp2p spec #508. package libp2phttp import ( @@ -39,7 +41,8 @@ type WellKnownProtocolMeta struct { type WellKnownProtoMap map[protocol.ID]WellKnownProtocolMeta -type wellKnownHandler struct { +// WellKnownHandler is an http.Handler that serves the .well-known/libp2p resource +type WellKnownHandler struct { wellknownMapMu sync.Mutex wellKnownMapping WellKnownProtoMap } @@ -49,11 +52,7 @@ func StreamHostListen(streamHost host.Host) (net.Listener, error) { return gostream.Listen(streamHost, ProtocolIDForMultistreamSelect) } -func NewWellKnownHandler() wellKnownHandler { - return wellKnownHandler{wellKnownMapping: make(map[protocol.ID]WellKnownProtocolMeta)} -} - -func (h *wellKnownHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *WellKnownHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Check if the requests accepts JSON accepts := r.Header.Get("Accept") if accepts != "" && !(strings.Contains(accepts, "application/json") || strings.Contains(accepts, "*/*")) { @@ -78,21 +77,29 @@ func (h *wellKnownHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write(mapping) } -func (h *wellKnownHandler) AddProtocolMapping(p protocol.ID, path string) { +func (h *WellKnownHandler) AddProtocolMapping(p protocol.ID, path string) { h.wellknownMapMu.Lock() + if h.wellKnownMapping == nil { + h.wellKnownMapping = make(map[protocol.ID]WellKnownProtocolMeta) + } h.wellKnownMapping[p] = WellKnownProtocolMeta{Path: path} h.wellknownMapMu.Unlock() } -func (h *wellKnownHandler) RmProtocolMapping(p protocol.ID, path string) { +func (h *WellKnownHandler) RmProtocolMapping(p protocol.ID, path string) { h.wellknownMapMu.Lock() - delete(h.wellKnownMapping, p) + if h.wellKnownMapping != nil { + delete(h.wellKnownMapping, p) + } h.wellknownMapMu.Unlock() } +// HTTPHost is a libp2p host for request/responses with HTTP semantics. This is +// in contrast to a stream-oriented host like the host.Host interface. Warning, +// this is experimental. The API will likely change. type HTTPHost struct { rootHandler http.ServeMux - wk wellKnownHandler + wk WellKnownHandler httpRoundTripper http.RoundTripper recentHTTPAddrs *lru.Cache[peer.ID, httpAddr] peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] @@ -113,12 +120,12 @@ func New() *HTTPHost { recentHTTP, err := lru.New[peer.ID, httpAddr](recentConnsLimit) peerMetadata, err2 := lru.New[peer.ID, WellKnownProtoMap](PeerMetadataLRUSize) if err != nil || err2 != nil { - // Only happens if size is < 1. We set it to 32, so this should never happen. + // Only happens if size is < 1. We make sure to not do that, so this should never happen. panic(err) } h := &HTTPHost{ - wk: NewWellKnownHandler(), + wk: WellKnownHandler{}, rootHandler: http.ServeMux{}, httpRoundTripper: http.DefaultTransport, recentHTTPAddrs: recentHTTP, @@ -270,6 +277,9 @@ func RoundTripperPreferHTTPTransport(o roundTripperOpts) roundTripperOpts { type RoundTripperOptsFn func(o roundTripperOpts) roundTripperOpts +// NewRoundTripper returns an http.RoundTripper that can fulfill and HTTP +// request to the given server. It may use an HTTP transport or a stream based +// transport. It is valid to pass an empty server.ID and a nil streamHost. func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.RoundTripper, error) { options := roundTripperOpts{} for _, o := range opts { @@ -340,6 +350,9 @@ func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, o } // Otherwise use a stream based transport + if streamHost == nil { + return nil, fmt.Errorf("no http addresses for peer, and no stream host provided") + } if !existingStreamConn { if server.ID == "" { return nil, fmt.Errorf("no http addresses for peer, and no server peer ID provided") @@ -398,7 +411,7 @@ func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, serve } req.Header.Set("Accept", "application/json") - client := &http.Client{Transport: roundtripper} + client := http.Client{Transport: roundtripper} resp, err := client.Do(req) if err != nil { return nil, err diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index c3246b097d..6fcbe7c6e2 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -216,7 +216,7 @@ func TestRoundTrippers(t *testing.T) { // TODO test with a native Go HTTP server func TestPlainOldHTTPServer(t *testing.T) { mux := http.NewServeMux() - wk := libp2phttp.NewWellKnownHandler() + wk := libp2phttp.WellKnownHandler{} mux.Handle("/.well-known/libp2p", &wk) mux.HandleFunc("/ping/", libp2phttp.Ping) From de3cdd49baacace5349bfa65e5edc17c28a89006 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 20 Jul 2023 21:39:55 -0700 Subject: [PATCH 05/72] Add client for PingHTTP --- p2p/http/ping.go | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/p2p/http/ping.go b/p2p/http/ping.go index 16c7e67cf0..e0575f638f 100644 --- a/p2p/http/ping.go +++ b/p2p/http/ping.go @@ -1,14 +1,24 @@ package libp2phttp import ( + "bytes" + "crypto/rand" + "errors" + "fmt" "io" "net/http" "strconv" ) const pingSize = 32 +const PingProtocolID = "/http-ping/1" -func Ping(w http.ResponseWriter, r *http.Request) { +type Ping struct{} + +var _ http.Handler = Ping{} + +// ServeHTTP implements http.Handler. +func (Ping) ServeHTTP(w http.ResponseWriter, r *http.Request) { body := [pingSize]byte{} _, err := io.ReadFull(r.Body, body[:]) if err != nil { @@ -21,3 +31,37 @@ func Ping(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(body[:]) } + +// SendPing send an ping request over HTTP. The provided client should be namespaced to the Ping protocol. +func SendPing(client http.Client) error { + body := [32]byte{} + _, err := io.ReadFull(rand.Reader, body[:]) + if err != nil { + return err + } + req, err := http.NewRequest("POST", "/", bytes.NewReader(body[:])) + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Content-Length", strconv.Itoa(pingSize)) + if err != nil { + return err + } + resp, err := client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + rBody := [pingSize]byte{} + _, err = io.ReadFull(resp.Body, rBody[:]) + if err != nil { + return err + } + + if !bytes.Equal(body[:], rBody[:]) { + return errors.New("ping body mismatch") + } + return nil +} From 61957ef6fe90eacb565444dbe60e4ef0c08ff7c6 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 2 Aug 2023 14:48:58 -0700 Subject: [PATCH 06/72] Support using a different sni from host --- p2p/http/libp2phttp.go | 138 ++++++++++++++++++++++++++++-------- p2p/http/libp2phttp_test.go | 25 ++++--- 2 files changed, 124 insertions(+), 39 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index e5a7e27168..ee195aa179 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -5,6 +5,7 @@ package libp2phttp import ( "bufio" "context" + "crypto/tls" "encoding/json" "fmt" "io" @@ -22,7 +23,6 @@ import ( "github.com/libp2p/go-libp2p/core/protocol" gostream "github.com/libp2p/go-libp2p/p2p/gostream" ma "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr/net" ) const ProtocolIDForMultistreamSelect = "/http/1.1" @@ -100,7 +100,7 @@ func (h *WellKnownHandler) RmProtocolMapping(p protocol.ID, path string) { type HTTPHost struct { rootHandler http.ServeMux wk WellKnownHandler - httpRoundTripper http.RoundTripper + httpRoundTripper *http.Transport recentHTTPAddrs *lru.Cache[peer.ID, httpAddr] peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] } @@ -108,11 +108,24 @@ type HTTPHost struct { type httpAddr struct { addr string scheme string + sni string + rt http.RoundTripper // optional, if this needed its own transport +} + +type HTTPHostOption func(*HTTPHost) error + +// WithTLSClientConfig sets the TLS client config for the native HTTP transport. +func WithTLSClientConfig(tlsConfig *tls.Config) HTTPHostOption { + return func(h *HTTPHost) error { + h.httpRoundTripper.TLSClientConfig = tlsConfig + return nil + } } // New creates a new HTTPHost. Use HTTPHost.Serve to start serving HTTP requests (both over libp2p streams and HTTP transport). -func New() *HTTPHost { - recentConnsLimit := http.DefaultTransport.(*http.Transport).MaxIdleConns +func New(opts ...HTTPHostOption) (*HTTPHost, error) { + httpRoundTripper := http.DefaultTransport.(*http.Transport).Clone() + recentConnsLimit := httpRoundTripper.MaxIdleConns if recentConnsLimit < 1 { recentConnsLimit = 32 } @@ -127,13 +140,19 @@ func New() *HTTPHost { h := &HTTPHost{ wk: WellKnownHandler{}, rootHandler: http.ServeMux{}, - httpRoundTripper: http.DefaultTransport, + httpRoundTripper: httpRoundTripper, recentHTTPAddrs: recentHTTP, peerMetadata: peerMetadata, } h.rootHandler.Handle("/.well-known/libp2p", &h.wk) + for _, opt := range opts { + err := opt(h) + if err != nil { + return nil, err + } + } - return h + return h, nil } // Serve starts serving HTTP requests using the given listener. You may call this method multiple times with different listeners. @@ -142,15 +161,18 @@ func (h *HTTPHost) Serve(l net.Listener) error { } // ServeTLS starts serving TLS+HTTP requests using the given listener. You may call this method multiple times with different listeners. -func (h *HTTPHost) ServeTLS(l net.Listener, certFile, keyFile string) error { - return http.ServeTLS(l, &h.rootHandler, certFile, keyFile) +func (h *HTTPHost) ServeTLS(l net.Listener, c *tls.Config) error { + srv := http.Server{ + Handler: &h.rootHandler, + TLSConfig: c, + } + return srv.ServeTLS(l, "", "") } // SetHttpHandler sets the HTTP handler for a given protocol. Automatically // manages the .well-known/libp2p mapping. func (h *HTTPHost) SetHttpHandler(p protocol.ID, handler http.Handler) { - path := string(p) + "/" - h.SetHttpHandlerAtPath(p, path, handler) + h.SetHttpHandlerAtPath(p, string(p)+"/", handler) } // SetHttpHandlerAtPath sets the HTTP handler for a given protocol using the @@ -207,9 +229,11 @@ func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) // roundTripperForSpecificHost is an http.RoundTripper targets a specific server. Still reuses the underlying RoundTripper for the requests. type roundTripperForSpecificServer struct { http.RoundTripper + ownRoundtripper bool httpHost *HTTPHost server peer.ID targetServerAddr string + sni string scheme string } @@ -220,10 +244,14 @@ func (rt *roundTripperForSpecificServer) RoundTrip(r *http.Request) (*http.Respo } r.URL.Scheme = rt.scheme r.URL.Host = rt.targetServerAddr - r.Host = rt.targetServerAddr + r.Host = rt.sni resp, err := rt.RoundTripper.RoundTrip(r) if err == nil && rt.server != "" { - rt.httpHost.recentHTTPAddrs.Add(rt.server, httpAddr{addr: rt.targetServerAddr, scheme: rt.scheme}) + ha := httpAddr{addr: rt.targetServerAddr, scheme: rt.scheme, sni: rt.sni} + if rt.ownRoundtripper { + ha.rt = rt.RoundTripper + } + rt.httpHost.recentHTTPAddrs.Add(rt.server, ha) } return resp, err } @@ -258,7 +286,13 @@ func (h *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p proto return namespacedRoundTripper{}, fmt.Errorf("no protocol %s for server %s", p, server) } - u, err := url.Parse(v.Path) + path := v.Path + if path[len(path)-1] == '/' { + // Trim the trailing slash, since it's common to make requests starting with a leading forward slash for the path + path = path[:len(path)-1] + } + + u, err := url.Parse(path) if err != nil { return namespacedRoundTripper{}, fmt.Errorf("invalid path %s for protocol %s for server %s", v.Path, p, server) } @@ -270,6 +304,21 @@ func (h *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p proto }, nil } +// NamespacedClient returns an http.Client that is scoped to the given protocol on the given server. +func (h *HTTPHost) NamespacedClient(streamHost host.Host, p protocol.ID, server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.Client, error) { + rt, err := h.NewRoundTripper(streamHost, server, opts...) + if err != nil { + return http.Client{}, err + } + + nrt, err := h.NamespaceRoundTripper(rt, p, server.ID) + if err != nil { + return http.Client{}, err + } + + return http.Client{Transport: &nrt}, nil +} + func RoundTripperPreferHTTPTransport(o roundTripperOpts) roundTripperOpts { o.preferHTTPTransport = true return o @@ -288,12 +337,20 @@ func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, o // Do we have a recent HTTP transport connection to this peer? if a, ok := h.recentHTTPAddrs.Get(server.ID); server.ID != "" && ok { + var rt http.RoundTripper = h.httpRoundTripper + ownRoundtripper := false + if a.rt != nil { + ownRoundtripper = true + rt = a.rt + } return &roundTripperForSpecificServer{ - RoundTripper: h.httpRoundTripper, + RoundTripper: rt, + ownRoundtripper: ownRoundtripper, httpHost: h, server: server.ID, targetServerAddr: a.addr, scheme: a.scheme, + sni: a.sni, }, nil } @@ -316,35 +373,56 @@ func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, o // Do we have an existing connection to this peer? existingStreamConn := false - if server.ID != "" { + if server.ID != "" && streamHost != nil { existingStreamConn = len(streamHost.Network().ConnsToPeer(server.ID)) > 0 } if len(httpAddrs) > 0 && (options.preferHTTPTransport || (firstAddrIsHTTP && !existingStreamConn)) { - useHTTPS := false - withoutHTTP, _ := ma.SplitFunc(httpAddrs[0], func(c ma.Component) bool { - return c.Protocol().Code == ma.P_HTTP + var useHTTPS bool + var host string + var port string + var sni string + + ma.ForEach(httpAddrs[0], func(c ma.Component) bool { + p := c.Protocol() + if p.Code == ma.P_IP4 || p.Code == ma.P_IP6 || p.Code == ma.P_DNS || p.Code == ma.P_DNS4 || p.Code == ma.P_DNS6 { + host = c.Value() + } else if p.Code == ma.P_TCP || p.Code == ma.P_UDP { + port = c.Value() + } else if p.Code == ma.P_TLS { + useHTTPS = true + } else if p.Code == ma.P_SNI { + sni = c.Value() + } + + return host == "" || port == "" || !useHTTPS || sni == "" }) - // Check if we need to pop the TLS component at the end as well - maybeWithoutTLS, maybeTLS := ma.SplitLast(withoutHTTP) - if maybeTLS != nil && maybeTLS.Protocol().Code == ma.P_TLS { - useHTTPS = true - withoutHTTP = maybeWithoutTLS - } - na, err := manet.ToNetAddr(withoutHTTP) - if err != nil { - return nil, err - } scheme := "http" if useHTTPS { scheme = "https" + if sni == "" { + sni = host + } + } + + rt := h.httpRoundTripper + ownRoundtripper := false + if sni != host { + // We have a different host and SNI (e.g. using an IP address but specifying a SNI) + // We need to make our own transport to support this. + rt = rt.Clone() + rt.TLSClientConfig = h.httpRoundTripper.TLSClientConfig.Clone() + rt.TLSClientConfig.ServerName = sni + ownRoundtripper = true } return &roundTripperForSpecificServer{ - RoundTripper: http.DefaultTransport, + RoundTripper: rt, + ownRoundtripper: ownRoundtripper, httpHost: h, server: server.ID, - targetServerAddr: na.String(), + targetServerAddr: host + ":" + port, + sni: sni, scheme: scheme, }, nil } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 6fcbe7c6e2..a4d2235271 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -30,7 +30,8 @@ func TestHTTPOverStreams(t *testing.T) { require.NoError(t, err) defer streamListener.Close() - httpHost := libp2phttp.New() + httpHost, err := libp2phttp.New() + require.NoError(t, err) httpHost.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) @@ -71,7 +72,8 @@ func TestRoundTrippers(t *testing.T) { require.NoError(t, err) defer streamListener.Close() - httpHost := libp2phttp.New() + httpHost, err := libp2phttp.New() + require.NoError(t, err) httpHost.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) @@ -166,7 +168,8 @@ func TestRoundTrippers(t *testing.T) { require.NoError(t, err) defer clientHost.Close() - clientHttpHost := libp2phttp.New() + clientHttpHost, err := libp2phttp.New() + require.NoError(t, err) rt := tc.setupRoundTripper(t, clientHost, clientHttpHost) if tc.expectStreamRoundTripper { @@ -184,7 +187,9 @@ func TestRoundTrippers(t *testing.T) { var resp *http.Response var err error if tc { - nrt, err := (libp2phttp.New()).NamespaceRoundTripper(rt, "/hello", serverHost.ID()) + h, err := libp2phttp.New() + require.NoError(t, err) + nrt, err := h.NamespaceRoundTripper(rt, "/hello", serverHost.ID()) require.NoError(t, err) client := &http.Client{Transport: &nrt} resp, err = client.Get("/") @@ -219,8 +224,8 @@ func TestPlainOldHTTPServer(t *testing.T) { wk := libp2phttp.WellKnownHandler{} mux.Handle("/.well-known/libp2p", &wk) - mux.HandleFunc("/ping/", libp2phttp.Ping) - wk.AddProtocolMapping("/ping", "/ping/") + mux.Handle("/ping/", libp2phttp.Ping{}) + wk.AddProtocolMapping(libp2phttp.PingProtocolID, "/ping/") server := &http.Server{Addr: "127.0.0.1:0", Handler: mux} @@ -242,7 +247,8 @@ func TestPlainOldHTTPServer(t *testing.T) { { name: "using libp2phttp", do: func(t *testing.T, request *http.Request) (*http.Response, error) { - clientHttpHost := libp2phttp.New() + clientHttpHost, err := libp2phttp.New() + require.NoError(t, err) rt, err := clientHttpHost.NewRoundTripper(nil, peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) @@ -250,7 +256,8 @@ func TestPlainOldHTTPServer(t *testing.T) { return client.Do(request) }, getWellKnown: func(t *testing.T) (libp2phttp.WellKnownProtoMap, error) { - clientHttpHost := libp2phttp.New() + clientHttpHost, err := libp2phttp.New() + require.NoError(t, err) rt, err := clientHttpHost.NewRoundTripper(nil, peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) return clientHttpHost.GetAndStorePeerProtoMap(rt, "") @@ -301,7 +308,7 @@ func TestPlainOldHTTPServer(t *testing.T) { require.NoError(t, err) expectedMap := make(libp2phttp.WellKnownProtoMap) - expectedMap["/ping"] = libp2phttp.WellKnownProtocolMeta{Path: "/ping/"} + expectedMap[libp2phttp.PingProtocolID] = libp2phttp.WellKnownProtocolMeta{Path: "/ping/"} require.Equal(t, expectedMap, protoMap) }) } From 587839f83a1f9fb94ccf721fafa170dae222ade3 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 2 Aug 2023 14:51:07 -0700 Subject: [PATCH 07/72] Add WIP auth support --- p2p/http/auth_handler.go | 62 +++++++++++++++++++ p2p/http/auth_test.go | 128 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 p2p/http/auth_handler.go diff --git a/p2p/http/auth_handler.go b/p2p/http/auth_handler.go new file mode 100644 index 0000000000..28310d3384 --- /dev/null +++ b/p2p/http/auth_handler.go @@ -0,0 +1,62 @@ +package libp2phttp + +import ( + "errors" + "fmt" + "net/http" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" +) + +const AuthHandlerProtocolID = "/http-noise-auth/1.0.0" + +type AuthHandler struct { + hostKey crypto.PrivKey +} + +var _ http.Handler = (*AuthHandler)(nil) + +// ServeHTTP implements http.Handler. +func (h *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + clientID, err := AuthenticateClient(h.hostKey, w.Header(), r) + + if err != nil || clientID == "" { + w.Header().Set("WWW-Authenticate", "Libp2p-Noise-IX") + w.WriteHeader(http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) +} + +// DoAuth sends an auth request over HTTP. The provided client should be +// namespaced to the AuthHandler protocol. +// +// Returns the server's peer id. +func DoAuth(client http.Client, clientKey crypto.PrivKey) (peer.ID, error) { + req, err := http.NewRequest("POST", "/", nil) + if err != nil { + return "", err + } + a, err := WithNoiseAuthentication(clientKey, req.Header) + if err != nil { + return "", err + } + + resp, err := client.Do(req) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + if resp.TLS == nil { + return "", errors.New("expected TLS connection") + } + + expectedHost := resp.TLS.ServerName + return a.AuthenticateServer(expectedHost, resp.Header) +} diff --git a/p2p/http/auth_test.go b/p2p/http/auth_test.go index 99e32ea8cb..2fda011d8f 100644 --- a/p2p/http/auth_test.go +++ b/p2p/http/auth_test.go @@ -2,15 +2,27 @@ package libp2phttp import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "errors" "fmt" "io" + "math/big" "net" "net/http" "testing" + "time" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/test" + "github.com/multiformats/go-multiaddr" + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" "github.com/stretchr/testify/require" ) @@ -54,7 +66,8 @@ func TestServerHandlerRequiresAuth(t *testing.T) { require.NoError(t, err) // Start the server - server := New() + server, err := New() + require.NoError(t, err) l, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) defer l.Close() @@ -143,4 +156,115 @@ func TestServerHandlerRequiresAuth(t *testing.T) { } } -// TODO test with TLS + sni +func TestSNIIsUsed(t *testing.T) { + serverKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) + require.NoError(t, err) + serverID, err := peer.IDFromPrivateKey(serverKey) + require.NoError(t, err) + + serverHTTPHost, err := New() + require.NoError(t, err) + + l, err := net.Listen("tcp", "127.0.0.1:0") + serverHTTPHost.SetHttpHandler(AuthHandlerProtocolID, &AuthHandler{serverKey}) + require.NoError(t, err) + defer l.Close() + tlsConf := getTLSConf(t, net.IPv4(127, 0, 0, 1), time.Now(), time.Now().Add(time.Hour), "example.com") + go serverHTTPHost.ServeTLS(l, tlsConf) + + serverMaSNI, err := manet.FromNetAddr(l.Addr()) + require.NoError(t, err) + serverMaWrongSNI := ma.Join(serverMaSNI, multiaddr.StringCast("/tls/sni/wrong.com/http")) + serverMaSNI = ma.Join(serverMaSNI, multiaddr.StringCast("/tls/sni/example.com/http")) + serverMaDNS := multiaddr.StringCast("/dns4/example.com/tcp/443/tls/http") + + testCases := []struct { + name string + ma ma.Multiaddr + shouldFail bool + }{ + { + name: "With sni: " + serverMaSNI.String(), + ma: serverMaSNI, + shouldFail: false, + }, + { + name: "With wrong sni: " + serverMaWrongSNI.String(), + ma: serverMaWrongSNI, + shouldFail: true, + }, + { + name: "With dns: " + serverMaDNS.String(), + ma: serverMaDNS, + shouldFail: false, + }, + } + + for _, tc := range testCases { + t.Run("With multiaddr "+tc.name, func(t *testing.T) { + clientHttpHost, err := New(WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true})) + require.NoError(t, err) + + tr := clientHttpHost.httpRoundTripper.Clone() + // Resolve example.com to the server's IP in our test + clientHttpHost.httpRoundTripper.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + // Returns our listener's addr + return tr.DialContext(ctx, l.Addr().Network(), l.Addr().String()) + } + client, err := clientHttpHost.NamespacedClient(nil, AuthHandlerProtocolID, peer.AddrInfo{ID: serverID, Addrs: []multiaddr.Multiaddr{tc.ma}}) + if tc.shouldFail { + require.Error(t, err) + } else { + + require.NoError(t, err) + } + + clientKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) + require.NoError(t, err) + + observedServerID, err := DoAuth(client, clientKey) + if tc.shouldFail { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, serverID, observedServerID) + } + }) + } +} + +func getTLSConf(t *testing.T, ip net.IP, start, end time.Time, expectSNI string) *tls.Config { + t.Helper() + certTempl := &x509.Certificate{ + DNSNames: []string{expectSNI}, + SerialNumber: big.NewInt(1234), + Subject: pkix.Name{Organization: []string{"https-test"}}, + NotBefore: start, + NotAfter: end, + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, &priv.PublicKey, priv) + require.NoError(t, err) + cert, err := x509.ParseCertificate(caBytes) + require.NoError(t, err) + var c *tls.Config + c = &tls.Config{ + Certificates: []tls.Certificate{{ + Certificate: [][]byte{cert.Raw}, + PrivateKey: priv, + Leaf: cert, + }}, + GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { + if hello.ServerName != expectSNI { + return nil, errors.New("unexpected SNI") + } + return c, nil + }, + } + return c +} From 4baeba3580c87494707246e9a0773109d272c098 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 2 Aug 2023 14:51:10 -0700 Subject: [PATCH 08/72] Revert "Add WIP auth support" This reverts commit 8a648d94f7cd8707e10626a3333e99d6dad99c21. Since current spec doesn't use libp2p-noise-?X auth anymore. --- p2p/http/auth_handler.go | 62 ------------------- p2p/http/auth_test.go | 128 +-------------------------------------- 2 files changed, 2 insertions(+), 188 deletions(-) delete mode 100644 p2p/http/auth_handler.go diff --git a/p2p/http/auth_handler.go b/p2p/http/auth_handler.go deleted file mode 100644 index 28310d3384..0000000000 --- a/p2p/http/auth_handler.go +++ /dev/null @@ -1,62 +0,0 @@ -package libp2phttp - -import ( - "errors" - "fmt" - "net/http" - - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" -) - -const AuthHandlerProtocolID = "/http-noise-auth/1.0.0" - -type AuthHandler struct { - hostKey crypto.PrivKey -} - -var _ http.Handler = (*AuthHandler)(nil) - -// ServeHTTP implements http.Handler. -func (h *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - clientID, err := AuthenticateClient(h.hostKey, w.Header(), r) - - if err != nil || clientID == "" { - w.Header().Set("WWW-Authenticate", "Libp2p-Noise-IX") - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.WriteHeader(http.StatusOK) -} - -// DoAuth sends an auth request over HTTP. The provided client should be -// namespaced to the AuthHandler protocol. -// -// Returns the server's peer id. -func DoAuth(client http.Client, clientKey crypto.PrivKey) (peer.ID, error) { - req, err := http.NewRequest("POST", "/", nil) - if err != nil { - return "", err - } - a, err := WithNoiseAuthentication(clientKey, req.Header) - if err != nil { - return "", err - } - - resp, err := client.Do(req) - if err != nil { - return "", err - } - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - if resp.TLS == nil { - return "", errors.New("expected TLS connection") - } - - expectedHost := resp.TLS.ServerName - return a.AuthenticateServer(expectedHost, resp.Header) -} diff --git a/p2p/http/auth_test.go b/p2p/http/auth_test.go index 2fda011d8f..99e32ea8cb 100644 --- a/p2p/http/auth_test.go +++ b/p2p/http/auth_test.go @@ -2,27 +2,15 @@ package libp2phttp import ( "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "errors" "fmt" "io" - "math/big" "net" "net/http" "testing" - "time" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/test" - "github.com/multiformats/go-multiaddr" - ma "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr/net" "github.com/stretchr/testify/require" ) @@ -66,8 +54,7 @@ func TestServerHandlerRequiresAuth(t *testing.T) { require.NoError(t, err) // Start the server - server, err := New() - require.NoError(t, err) + server := New() l, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) defer l.Close() @@ -156,115 +143,4 @@ func TestServerHandlerRequiresAuth(t *testing.T) { } } -func TestSNIIsUsed(t *testing.T) { - serverKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) - require.NoError(t, err) - serverID, err := peer.IDFromPrivateKey(serverKey) - require.NoError(t, err) - - serverHTTPHost, err := New() - require.NoError(t, err) - - l, err := net.Listen("tcp", "127.0.0.1:0") - serverHTTPHost.SetHttpHandler(AuthHandlerProtocolID, &AuthHandler{serverKey}) - require.NoError(t, err) - defer l.Close() - tlsConf := getTLSConf(t, net.IPv4(127, 0, 0, 1), time.Now(), time.Now().Add(time.Hour), "example.com") - go serverHTTPHost.ServeTLS(l, tlsConf) - - serverMaSNI, err := manet.FromNetAddr(l.Addr()) - require.NoError(t, err) - serverMaWrongSNI := ma.Join(serverMaSNI, multiaddr.StringCast("/tls/sni/wrong.com/http")) - serverMaSNI = ma.Join(serverMaSNI, multiaddr.StringCast("/tls/sni/example.com/http")) - serverMaDNS := multiaddr.StringCast("/dns4/example.com/tcp/443/tls/http") - - testCases := []struct { - name string - ma ma.Multiaddr - shouldFail bool - }{ - { - name: "With sni: " + serverMaSNI.String(), - ma: serverMaSNI, - shouldFail: false, - }, - { - name: "With wrong sni: " + serverMaWrongSNI.String(), - ma: serverMaWrongSNI, - shouldFail: true, - }, - { - name: "With dns: " + serverMaDNS.String(), - ma: serverMaDNS, - shouldFail: false, - }, - } - - for _, tc := range testCases { - t.Run("With multiaddr "+tc.name, func(t *testing.T) { - clientHttpHost, err := New(WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true})) - require.NoError(t, err) - - tr := clientHttpHost.httpRoundTripper.Clone() - // Resolve example.com to the server's IP in our test - clientHttpHost.httpRoundTripper.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - // Returns our listener's addr - return tr.DialContext(ctx, l.Addr().Network(), l.Addr().String()) - } - client, err := clientHttpHost.NamespacedClient(nil, AuthHandlerProtocolID, peer.AddrInfo{ID: serverID, Addrs: []multiaddr.Multiaddr{tc.ma}}) - if tc.shouldFail { - require.Error(t, err) - } else { - - require.NoError(t, err) - } - - clientKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) - require.NoError(t, err) - - observedServerID, err := DoAuth(client, clientKey) - if tc.shouldFail { - require.Error(t, err) - } else { - require.NoError(t, err) - require.Equal(t, serverID, observedServerID) - } - }) - } -} - -func getTLSConf(t *testing.T, ip net.IP, start, end time.Time, expectSNI string) *tls.Config { - t.Helper() - certTempl := &x509.Certificate{ - DNSNames: []string{expectSNI}, - SerialNumber: big.NewInt(1234), - Subject: pkix.Name{Organization: []string{"https-test"}}, - NotBefore: start, - NotAfter: end, - IsCA: true, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - BasicConstraintsValid: true, - } - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, &priv.PublicKey, priv) - require.NoError(t, err) - cert, err := x509.ParseCertificate(caBytes) - require.NoError(t, err) - var c *tls.Config - c = &tls.Config{ - Certificates: []tls.Certificate{{ - Certificate: [][]byte{cert.Raw}, - PrivateKey: priv, - Leaf: cert, - }}, - GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { - if hello.ServerName != expectSNI { - return nil, errors.New("unexpected SNI") - } - return c, nil - }, - } - return c -} +// TODO test with TLS + sni From 89e78feb8af95b6f020755722475d880e30137c5 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 2 Aug 2023 14:56:43 -0700 Subject: [PATCH 09/72] Remove libp2p-noise auth (removed from spec) --- p2p/http/auth.go | 261 ------------------------------------------ p2p/http/auth_test.go | 146 ----------------------- 2 files changed, 407 deletions(-) delete mode 100644 p2p/http/auth.go delete mode 100644 p2p/http/auth_test.go diff --git a/p2p/http/auth.go b/p2p/http/auth.go deleted file mode 100644 index 458b45bb1a..0000000000 --- a/p2p/http/auth.go +++ /dev/null @@ -1,261 +0,0 @@ -package libp2phttp - -import ( - "crypto/rand" - "crypto/sha256" - "errors" - "fmt" - "hash" - "net/http" - "strings" - - "github.com/flynn/noise" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/p2p/security/noise/pb" - "github.com/multiformats/go-multibase" - "google.golang.org/protobuf/proto" -) - -const payloadSigPrefix = "noise-libp2p-static-key:" - -type minioSHAFn struct{} - -func (h minioSHAFn) Hash() hash.Hash { return sha256.New() } -func (h minioSHAFn) HashName() string { return "SHA256" } - -var shaHashFn noise.HashFunc = minioSHAFn{} -var cipherSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, shaHashFn) - -type AuthState struct { - hs *noise.HandshakeState -} - -func WithNoiseAuthentication(hostKey crypto.PrivKey, requestHeader http.Header) (AuthState, error) { - s := AuthState{} - kp, err := noise.DH25519.GenerateKeypair(rand.Reader) - if err != nil { - return s, fmt.Errorf("error generating static keypair: %w", err) - } - - cfg := noise.Config{ - CipherSuite: cipherSuite, - Pattern: noise.HandshakeIX, - Initiator: true, - StaticKeypair: kp, - Prologue: nil, - } - - s.hs, err = noise.NewHandshakeState(cfg) - if err != nil { - return s, fmt.Errorf("error initializing handshake state: %w", err) - } - - payload, err := generateNoisePayload(hostKey, kp, nil) - if err != nil { - return s, fmt.Errorf("error generating noise payload: %w", err) - } - - // Allocate a buffer on the stack for the handshake message - hbuf := [2 << 10]byte{} - authMsg, _, _, err := s.hs.WriteMessage(hbuf[:0], payload) - if err != nil { - return s, fmt.Errorf("error writing handshake message: %w", err) - } - authMsgEncoded, err := multibase.Encode(multibase.Encodings["base32"], authMsg) - if err != nil { - return s, fmt.Errorf("error encoding handshake message: %w", err) - } - - requestHeader.Set("Authorization", "Libp2p-Noise-IX "+authMsgEncoded) - - return s, nil -} - -// AuthenticateClient verifies the Authorization header of the request and sets -// the Authentication-Info response header to allow the client to authenticate -// the server. Returns the peer.ID of the authenticated client. -// Returns an empty peer.ID if the client did not authenticate itself either by sending -// sending a `Libp2p-Noise-NX` Authorization header, or by not sending a `Libp2p-Noise-IX` Authorization header. -func AuthenticateClient(hostKey crypto.PrivKey, responseHeader http.Header, request *http.Request) (peer.ID, error) { - authValue := request.Header.Get("Authorization") - authMethod := strings.SplitN(authValue, " ", 2) - if len(authMethod) != 2 || authMethod[0] != "Libp2p-Noise-IX" { - return "", nil - } - - // Decode the handshake message - _, authMsg, err := multibase.Decode(authMethod[1]) - if err != nil { - return "", fmt.Errorf("error decoding handshake message: %w", err) - } - - kp, err := noise.DH25519.GenerateKeypair(rand.Reader) - if err != nil { - return "", fmt.Errorf("error generating static keypair: %w", err) - } - - cfg := noise.Config{ - CipherSuite: cipherSuite, - Pattern: noise.HandshakeIX, - Initiator: false, - StaticKeypair: kp, - Prologue: nil, - } - - hs, err := noise.NewHandshakeState(cfg) - if err != nil { - return "", fmt.Errorf("error initializing handshake state: %w", err) - } - - // Allocate a buffer on the stack for the payload - hbuf := [2 << 10]byte{} - - payload, _, _, err := hs.ReadMessage(hbuf[:0], authMsg) - if err != nil { - return "", fmt.Errorf("error reading handshake message: %w", err) - } - - // TODO handle the peer not sending a Static key (handle Libp2p-Noise-NX) - remotePeer, _, err := handleRemoteHandshakePayload(payload, hs.PeerStatic()) - if err != nil { - return "", fmt.Errorf("error handling remote handshake payload: %w", err) - } - - sni := "" - if request.TLS != nil { - sni = request.TLS.ServerName - } - - payload, err = generateNoisePayload(hostKey, kp, &pb.NoiseExtensions{ - SNI: &sni, - }) - if err != nil { - return "", fmt.Errorf("error generating noise payload: %w", err) - } - - authInfoMsg, cs1, cs2, err := hs.WriteMessage(hbuf[:0], payload) - if err != nil { - return "", fmt.Errorf("error writing handshake message: %w", err) - } - - if cs1 == nil || cs2 == nil { - return "", errors.New("expected handshake to be complete") - } - - authInfoMsgEncoded, err := multibase.Encode(multibase.Encodings["base32"], authInfoMsg) - if err != nil { - return "", fmt.Errorf("error encoding handshake message: %w", err) - } - responseHeader.Set("Authentication-Info", authMethod[0]+" "+authInfoMsgEncoded) - - return remotePeer, nil -} - -// AuthenticateServer returns the peer.ID of the server. It returns an error if the response does not include authentication info -func (s AuthState) AuthenticateServer(expectedSNI string, responseHeader http.Header) (peer.ID, error) { - authValue := responseHeader.Get("Authentication-Info") - authMethod := strings.SplitN(authValue, " ", 2) - if len(authMethod) != 2 || authMethod[0] != "Libp2p-Noise-IX" { - return "", errors.New("response does not include noise authentication info") - } - - // Decode the handshake message - _, authMsg, err := multibase.Decode(authMethod[1]) - if err != nil { - return "", fmt.Errorf("error decoding handshake message: %w", err) - } - - // Allocate a buffer on the stack for the payload - hbuf := [2 << 10]byte{} - - payload, cs1, cs2, err := s.hs.ReadMessage(hbuf[:0], authMsg) - if err != nil { - return "", fmt.Errorf("error reading handshake message: %w", err) - } - - if cs1 == nil || cs2 == nil { - return "", errors.New("expected handshake to be complete") - } - - server, extensions, err := handleRemoteHandshakePayload(payload, s.hs.PeerStatic()) - if err != nil { - return "", fmt.Errorf("error handling remote handshake payload: %w", err) - } - - if expectedSNI != "" { - if extensions == nil { - return "", errors.New("server is missing noise extensions") - } - - if extensions.SNI == nil { - return "", errors.New("server is missing SNI in noise extensions") - } - - if *extensions.SNI != expectedSNI { - return "", errors.New("server SNI in noise extension does not match expected SNI") - } - } - - return server, nil - -} - -func generateNoisePayload(hostKey crypto.PrivKey, localStatic noise.DHKey, ext *pb.NoiseExtensions) ([]byte, error) { - localPubKeyRaw, err := crypto.MarshalPublicKey(hostKey.GetPublic()) - if err != nil { - return nil, fmt.Errorf("error serializing libp2p identity key: %w", err) - } - - // prepare payload to sign; perform signature. - toSign := append([]byte(payloadSigPrefix), localStatic.Public...) - signedPayload, err := hostKey.Sign(toSign) - if err != nil { - return nil, fmt.Errorf("error sigining handshake payload: %w", err) - } - - // create payload - payloadEnc, err := proto.Marshal(&pb.NoiseHandshakePayload{ - IdentityKey: localPubKeyRaw, - IdentitySig: signedPayload, - Extensions: ext, - }) - if err != nil { - return nil, fmt.Errorf("error marshaling handshake payload: %w", err) - } - return payloadEnc, nil -} - -// handleRemoteHandshakePayload unmarshals the handshake payload object sent -// by the remote peer and validates the signature against the peer's static Noise key. -// It returns the data attached to the payload. -func handleRemoteHandshakePayload(payload []byte, remoteStatic []byte) (peer.ID, *pb.NoiseExtensions, error) { - // unmarshal payload - nhp := new(pb.NoiseHandshakePayload) - err := proto.Unmarshal(payload, nhp) - if err != nil { - return "", nil, fmt.Errorf("error unmarshaling remote handshake payload: %w", err) - } - - // unpack remote peer's public libp2p key - remotePubKey, err := crypto.UnmarshalPublicKey(nhp.GetIdentityKey()) - if err != nil { - return "", nil, err - } - id, err := peer.IDFromPublicKey(remotePubKey) - if err != nil { - return "", nil, err - } - - // verify payload is signed by asserted remote libp2p key. - sig := nhp.GetIdentitySig() - msg := append([]byte(payloadSigPrefix), remoteStatic...) - ok, err := remotePubKey.Verify(msg, sig) - if err != nil { - return "", nil, fmt.Errorf("error verifying signature: %w", err) - } else if !ok { - return "", nil, fmt.Errorf("handshake signature invalid") - } - - return id, nhp.Extensions, nil -} diff --git a/p2p/http/auth_test.go b/p2p/http/auth_test.go deleted file mode 100644 index 99e32ea8cb..0000000000 --- a/p2p/http/auth_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package libp2phttp - -import ( - "context" - "fmt" - "io" - "net" - "net/http" - "testing" - - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/test" - "github.com/stretchr/testify/require" -) - -func TestHappyPathIX(t *testing.T) { - req, err := http.NewRequest("GET", "http://example.com", nil) - require.NoError(t, err) - respHeader := make(http.Header) - - clientKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) - require.NoError(t, err) - serverKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) - require.NoError(t, err) - - // Client starts a request - authState, err := WithNoiseAuthentication(clientKey, req.Header) - require.NoError(t, err) - - // Server responds to the request and sets the appropriate headers - clientPeerID, err := AuthenticateClient(serverKey, respHeader, req) - require.NoError(t, err) - require.NotEmpty(t, clientPeerID) - require.NotEmpty(t, respHeader.Get("Authentication-Info")) - - expectedClientKey, err := peer.IDFromPrivateKey(clientKey) - require.NoError(t, err) - require.Equal(t, expectedClientKey, clientPeerID) - - // Client receives the response and validates the auth info - serverPeerID, err := authState.AuthenticateServer("", respHeader) - require.NoError(t, err) - - expectedServerID, err := peer.IDFromPrivateKey(serverKey) - require.NoError(t, err) - require.Equal(t, expectedServerID, serverPeerID) -} - -func TestServerHandlerRequiresAuth(t *testing.T) { - clientKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) - require.NoError(t, err) - serverKey, _, err := test.RandTestKeyPair(crypto.Ed25519, 256) - require.NoError(t, err) - - // Start the server - server := New() - l, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - defer l.Close() - - server.SetHttpHandlerAtPath("/my-app/1", "/my-app/1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // This handler requires libp2p auth - clientID, err := AuthenticateClient(serverKey, w.Header(), r) - - if err != nil || clientID == "" { - w.Header().Set("WWW-Authenticate", "Libp2p-Noise-IX") - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte(fmt.Sprintf("Hello, %s", clientID))) - })) - - // Middleware example - type Libp2pAuthKey struct{} - var libp2pAuthKey = Libp2pAuthKey{} - libp2pAuthMiddleware := func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // This middleware requires libp2p auth - clientID, err := AuthenticateClient(serverKey, w.Header(), r) - - if err != nil || clientID == "" { - w.Header().Set("WWW-Authenticate", "Libp2p-Noise-IX") - w.WriteHeader(http.StatusUnauthorized) - return - } - - r = r.WithContext(context.WithValue(r.Context(), libp2pAuthKey, clientID)) - next.ServeHTTP(w, r) - }) - } - - server.SetHttpHandler("/my-app-middleware/1", libp2pAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(fmt.Sprintf("Hello, %s", r.Context().Value(libp2pAuthKey)))) - }))) - - go server.Serve(l) - - client := http.Client{} - - pathsToTry := []string{ - fmt.Sprintf("http://%s/my-app/1", l.Addr().String()), - fmt.Sprintf("http://%s/my-app-middleware/1", l.Addr().String()), - } - - for _, path := range pathsToTry { - // Try without auth - resp, err := client.Get(path) - require.NoError(t, err) - require.Equal(t, http.StatusUnauthorized, resp.StatusCode) - - // Try with auth - req, err := http.NewRequest("GET", path, nil) - require.NoError(t, err) - - // Set the Authorization header - authState, err := WithNoiseAuthentication(clientKey, req.Header) - require.NoError(t, err) - - // Make the request - resp, err = client.Do(req) - require.NoError(t, err) - - require.Equal(t, http.StatusOK, resp.StatusCode) - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) - resp.Body.Close() - - expectedClientKey, err := peer.IDFromPrivateKey(clientKey) - require.NoError(t, err) - require.Equal(t, fmt.Sprintf("Hello, %s", expectedClientKey), string(respBody)) - - // Client receives the response and validates the auth info - serverPeerID, err := authState.AuthenticateServer("", resp.Header) - require.NoError(t, err) - - expectedServerID, err := peer.IDFromPrivateKey(serverKey) - require.NoError(t, err) - require.Equal(t, expectedServerID, serverPeerID) - } -} - -// TODO test with TLS + sni From 30d348a86d70fb2bd65d1d3af8239ce19af22cd9 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 2 Aug 2023 16:18:59 -0700 Subject: [PATCH 10/72] wip notes --- p2p/http/libp2phttp.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index ee195aa179..5c002c6a93 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -31,10 +31,13 @@ const PeerMetadataLRUSize = 256 // How many different peer's metadata to keep // TODOs: // - integrate with the conn gater and resource manager -// - skip client auth // - Support listenAddr option to accept a multiaddr to listen on (could handle h3 as well) // - Support withStreamHost option to accept a streamHost to listen on +// Dev notes +// Would be nice to have an .Addrs method on the httpHost +// Would be nice to have the httpHost manage the listener (listenAddr option above) + type WellKnownProtocolMeta struct { Path string `json:"path"` } From 464205022d08182e158a3305915a5900d9574862 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 3 Aug 2023 12:02:13 -0700 Subject: [PATCH 11/72] Fix ordering of header writes --- p2p/http/libp2phttp.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 5c002c6a93..9c5234aa2b 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -74,9 +74,9 @@ func (h *WellKnownHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - w.WriteHeader(http.StatusOK) w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Length", strconv.Itoa(len(mapping))) + w.WriteHeader(http.StatusOK) w.Write(mapping) } @@ -181,6 +181,10 @@ func (h *HTTPHost) SetHttpHandler(p protocol.ID, handler http.Handler) { // SetHttpHandlerAtPath sets the HTTP handler for a given protocol using the // given path. Automatically manages the .well-known/libp2p mapping. func (h *HTTPHost) SetHttpHandlerAtPath(p protocol.ID, path string, handler http.Handler) { + if path[len(path)-1] != '/' { + // We are nesting this handler under this path, so it should end with a slash. + path += "/" + } h.wk.AddProtocolMapping(p, path) h.rootHandler.Handle(path, handler) } From 25555021b161adfbfb2d5b97e59732d3bcf56c57 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 3 Aug 2023 18:32:20 -0700 Subject: [PATCH 12/72] Change api to have the host do more --- p2p/http/libp2phttp.go | 219 ++++++++++++++++++++++++++++-------- p2p/http/libp2phttp_test.go | 27 ++--- 2 files changed, 183 insertions(+), 63 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 9c5234aa2b..acd017ad78 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -7,6 +7,7 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "io" "net" @@ -106,6 +107,17 @@ type HTTPHost struct { httpRoundTripper *http.Transport recentHTTPAddrs *lru.Cache[peer.ID, httpAddr] peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] + streamHost host.Host // may be nil + httpTransport *httpTransport +} + +type httpTransport struct { + requestedListenAddrs []ma.Multiaddr + listenAddrs []ma.Multiaddr + tlsConfig *tls.Config + listeners []net.Listener + closeListeners chan struct{} + waitingForListeners chan struct{} } type httpAddr struct { @@ -115,16 +127,6 @@ type httpAddr struct { rt http.RoundTripper // optional, if this needed its own transport } -type HTTPHostOption func(*HTTPHost) error - -// WithTLSClientConfig sets the TLS client config for the native HTTP transport. -func WithTLSClientConfig(tlsConfig *tls.Config) HTTPHostOption { - return func(h *HTTPHost) error { - h.httpRoundTripper.TLSClientConfig = tlsConfig - return nil - } -} - // New creates a new HTTPHost. Use HTTPHost.Serve to start serving HTTP requests (both over libp2p streams and HTTP transport). func New(opts ...HTTPHostOption) (*HTTPHost, error) { httpRoundTripper := http.DefaultTransport.(*http.Transport).Clone() @@ -146,6 +148,10 @@ func New(opts ...HTTPHostOption) (*HTTPHost, error) { httpRoundTripper: httpRoundTripper, recentHTTPAddrs: recentHTTP, peerMetadata: peerMetadata, + httpTransport: &httpTransport{ + closeListeners: make(chan struct{}), + waitingForListeners: make(chan struct{}), + }, } h.rootHandler.Handle("/.well-known/libp2p", &h.wk) for _, opt := range opts { @@ -158,18 +164,130 @@ func New(opts ...HTTPHostOption) (*HTTPHost, error) { return h, nil } -// Serve starts serving HTTP requests using the given listener. You may call this method multiple times with different listeners. -func (h *HTTPHost) Serve(l net.Listener) error { - return http.Serve(l, &h.rootHandler) +func (h *HTTPHost) Addrs() []ma.Multiaddr { + <-h.httpTransport.waitingForListeners + return h.httpTransport.listenAddrs } -// ServeTLS starts serving TLS+HTTP requests using the given listener. You may call this method multiple times with different listeners. -func (h *HTTPHost) ServeTLS(l net.Listener, c *tls.Config) error { - srv := http.Server{ - Handler: &h.rootHandler, - TLSConfig: c, +var ErrNoListeners = errors.New("nothing to listen on") + +// Serve starts the HTTP transport listeners. Always returns a non-nil error. +// If there are no listeners, returns ErrNoListeners. +func (h *HTTPHost) Serve() error { + closedWaitingForListeners := false + defer func() { + if !closedWaitingForListeners { + close(h.httpTransport.waitingForListeners) + } + }() + + if len(h.httpTransport.requestedListenAddrs) == 0 && h.streamHost == nil { + return ErrNoListeners + } + + h.httpTransport.listeners = make([]net.Listener, 0, len(h.httpTransport.requestedListenAddrs)+1) // +1 for stream host + + streamHostAddrsCount := 0 + if h.streamHost != nil { + streamHostAddrsCount = len(h.streamHost.Addrs()) + } + h.httpTransport.listenAddrs = make([]ma.Multiaddr, 0, len(h.httpTransport.requestedListenAddrs)+streamHostAddrsCount) + + errCh := make(chan error) + + if h.streamHost != nil { + listener, err := StreamHostListen(h.streamHost) + if err != nil { + return err + } + h.httpTransport.listeners = append(h.httpTransport.listeners, listener) + h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, h.streamHost.Addrs()...) + + go func() { + errCh <- http.Serve(listener, &h.rootHandler) + }() } - return srv.ServeTLS(l, "", "") + + closeAllListeners := func() { + for _, l := range h.httpTransport.listeners { + l.Close() + } + } + + for _, addr := range h.httpTransport.requestedListenAddrs { + parsedAddr := parseMultiaddr(addr) + // resolve the host + ipaddr, err := net.ResolveIPAddr("ip", parsedAddr.host) + if err != nil { + closeAllListeners() + return err + } + + host := ipaddr.String() + l, err := net.Listen("tcp", host+":"+parsedAddr.port) + fmt.Println("HTTPHost.Serve", err) + if err != nil { + closeAllListeners() + return err + } + h.httpTransport.listeners = append(h.httpTransport.listeners, l) + + // get resolved port + _, port, err := net.SplitHostPort(l.Addr().String()) + + var listenAddr ma.Multiaddr + if parsedAddr.useHTTPS && parsedAddr.sni != "" && parsedAddr.sni != host { + listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/tls/sni/%s/http", host, port, parsedAddr.sni)) + } else { + scheme := "http" + if parsedAddr.useHTTPS { + scheme = "https" + } + listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/%s", host, port, scheme)) + + } + + h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) + + if parsedAddr.useHTTPS { + go func() { + srv := http.Server{ + Handler: &h.rootHandler, + TLSConfig: h.httpTransport.tlsConfig, + } + errCh <- srv.ServeTLS(l, "", "") + }() + } else { + go func() { + errCh <- http.Serve(l, &h.rootHandler) + }() + } + } + + close(h.httpTransport.waitingForListeners) + closedWaitingForListeners = true + + expectedErrCount := len(h.httpTransport.listeners) + var err error + select { + case <-h.httpTransport.closeListeners: + case err = <-errCh: + expectedErrCount-- + } + + // Close all listeners + closeAllListeners() + for i := 0; i < expectedErrCount; i++ { + <-errCh + } + close(errCh) + + return err +} + +func (h *HTTPHost) Close() error { + close(h.httpTransport.closeListeners) + return nil } // SetHttpHandler sets the HTTP handler for a given protocol. Automatically @@ -385,41 +503,20 @@ func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, o } if len(httpAddrs) > 0 && (options.preferHTTPTransport || (firstAddrIsHTTP && !existingStreamConn)) { - var useHTTPS bool - var host string - var port string - var sni string - - ma.ForEach(httpAddrs[0], func(c ma.Component) bool { - p := c.Protocol() - if p.Code == ma.P_IP4 || p.Code == ma.P_IP6 || p.Code == ma.P_DNS || p.Code == ma.P_DNS4 || p.Code == ma.P_DNS6 { - host = c.Value() - } else if p.Code == ma.P_TCP || p.Code == ma.P_UDP { - port = c.Value() - } else if p.Code == ma.P_TLS { - useHTTPS = true - } else if p.Code == ma.P_SNI { - sni = c.Value() - } - - return host == "" || port == "" || !useHTTPS || sni == "" - }) + parsed := parseMultiaddr(httpAddrs[0]) scheme := "http" - if useHTTPS { + if parsed.useHTTPS { scheme = "https" - if sni == "" { - sni = host - } } rt := h.httpRoundTripper ownRoundtripper := false - if sni != host { + if parsed.sni != parsed.host { // We have a different host and SNI (e.g. using an IP address but specifying a SNI) // We need to make our own transport to support this. rt = rt.Clone() rt.TLSClientConfig = h.httpRoundTripper.TLSClientConfig.Clone() - rt.TLSClientConfig.ServerName = sni + rt.TLSClientConfig.ServerName = parsed.sni ownRoundtripper = true } @@ -428,8 +525,8 @@ func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, o ownRoundtripper: ownRoundtripper, httpHost: h, server: server.ID, - targetServerAddr: host + ":" + port, - sni: sni, + targetServerAddr: parsed.host + ":" + parsed.port, + sni: parsed.sni, scheme: scheme, }, nil } @@ -451,6 +548,36 @@ func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, o return NewStreamRoundTripper(streamHost, server.ID), nil } +type httpMultiaddr struct { + useHTTPS bool + host string + port string + sni string +} + +func parseMultiaddr(addr ma.Multiaddr) httpMultiaddr { + out := httpMultiaddr{} + ma.ForEach(addr, func(c ma.Component) bool { + p := c.Protocol() + if p.Code == ma.P_IP4 || p.Code == ma.P_IP6 || p.Code == ma.P_DNS || p.Code == ma.P_DNS4 || p.Code == ma.P_DNS6 { + out.host = c.Value() + } else if p.Code == ma.P_TCP || p.Code == ma.P_UDP { + out.port = c.Value() + } else if p.Code == ma.P_TLS { + out.useHTTPS = true + } else if p.Code == ma.P_SNI { + out.sni = c.Value() + } + + return out.host == "" || out.port == "" || !out.useHTTPS || out.sni == "" + }) + + if out.useHTTPS && out.sni == "" { + out.sni = out.host + } + return out +} + func NewStreamRoundTripper(streamHost host.Host, server peer.ID) http.RoundTripper { return &streamRoundTripper{h: streamHost, server: server} } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index a4d2235271..3a28e7ef5a 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -26,11 +26,7 @@ func TestHTTPOverStreams(t *testing.T) { ) require.NoError(t, err) - streamListener, err := libp2phttp.StreamHostListen(serverHost) - require.NoError(t, err) - defer streamListener.Close() - - httpHost, err := libp2phttp.New() + httpHost, err := libp2phttp.New(libp2phttp.StreamHost(serverHost)) require.NoError(t, err) httpHost.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -38,7 +34,8 @@ func TestHTTPOverStreams(t *testing.T) { })) // Start server - go httpHost.Serve(streamListener) + go httpHost.Serve() + defer httpHost.Close() // Start client clientHost, err := libp2p.New(libp2p.NoListenAddrs) @@ -72,7 +69,9 @@ func TestRoundTrippers(t *testing.T) { require.NoError(t, err) defer streamListener.Close() - httpHost, err := libp2phttp.New() + httpHost, err := libp2phttp.New( + libp2phttp.StreamHost(serverHost), + libp2phttp.ListenAddrs([]ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")})) require.NoError(t, err) httpHost.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -80,17 +79,11 @@ func TestRoundTrippers(t *testing.T) { })) // Start stream based server - go httpHost.Serve(streamListener) - // Start HTTP transport based server - l, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - go httpHost.Serve(l) + go httpHost.Serve() + defer httpHost.Close() - serverHTTPAddrParts := strings.Split(l.Addr().String(), ":") - require.Equal(t, 2, len(serverHTTPAddrParts)) - serverHTTPAddr := ma.StringCast("/ip4/" + serverHTTPAddrParts[0] + "/tcp/" + serverHTTPAddrParts[1] + "/http") - serverMultiaddrs := serverHost.Addrs() - serverMultiaddrs = append(serverMultiaddrs, serverHTTPAddr) + serverMultiaddrs := httpHost.Addrs() + serverHTTPAddr := serverMultiaddrs[1] testCases := []struct { name string From f183e2eace376ad27fa41b369d3642382f577811 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 3 Aug 2023 18:38:35 -0700 Subject: [PATCH 13/72] Add options --- p2p/http/options.go | 50 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 p2p/http/options.go diff --git a/p2p/http/options.go b/p2p/http/options.go new file mode 100644 index 0000000000..a1bd9e27b4 --- /dev/null +++ b/p2p/http/options.go @@ -0,0 +1,50 @@ +package libp2phttp + +import ( + "crypto/tls" + "fmt" + + "github.com/libp2p/go-libp2p/core/host" + ma "github.com/multiformats/go-multiaddr" +) + +type HTTPHostOption func(*HTTPHost) error + +// WithStreamHost sets the stream host to use for HTTP over libp2p streams. +func ListenAddrs(addrs []ma.Multiaddr) HTTPHostOption { + return func(h *HTTPHost) error { + // assert that each addr contains a /http component + for _, addr := range addrs { + if _, err := addr.ValueForProtocol(ma.P_HTTP); err != nil { + return fmt.Errorf("address %s does not contain a /http component", addr) + } + } + + h.httpTransport.requestedListenAddrs = addrs + + return nil + } +} + +func TLSConfig(tlsConfig *tls.Config) HTTPHostOption { + return func(h *HTTPHost) error { + h.httpTransport.tlsConfig = tlsConfig + return nil + } +} + +// StreamHost sets the stream host to use for HTTP over libp2p streams. +func StreamHost(streamHost host.Host) HTTPHostOption { + return func(h *HTTPHost) error { + h.streamHost = streamHost + return nil + } +} + +// WithTLSClientConfig sets the TLS client config for the native HTTP transport. +func WithTLSClientConfig(tlsConfig *tls.Config) HTTPHostOption { + return func(h *HTTPHost) error { + h.httpRoundTripper.TLSClientConfig = tlsConfig + return nil + } +} From aeeaeacaef469c8914919639f818d89a14e0508f Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 4 Aug 2023 11:29:15 -0700 Subject: [PATCH 14/72] Use stream host from option instead of parameter --- p2p/http/libp2phttp.go | 20 ++++++++++++-------- p2p/http/libp2phttp_test.go | 22 +++++++++++----------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index acd017ad78..66d52fa14b 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -234,6 +234,10 @@ func (h *HTTPHost) Serve() error { // get resolved port _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + closeAllListeners() + return err + } var listenAddr ma.Multiaddr if parsedAddr.useHTTPS && parsedAddr.sni != "" && parsedAddr.sni != host { @@ -430,8 +434,8 @@ func (h *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p proto } // NamespacedClient returns an http.Client that is scoped to the given protocol on the given server. -func (h *HTTPHost) NamespacedClient(streamHost host.Host, p protocol.ID, server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.Client, error) { - rt, err := h.NewRoundTripper(streamHost, server, opts...) +func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.Client, error) { + rt, err := h.NewRoundTripper(server, opts...) if err != nil { return http.Client{}, err } @@ -454,7 +458,7 @@ type RoundTripperOptsFn func(o roundTripperOpts) roundTripperOpts // NewRoundTripper returns an http.RoundTripper that can fulfill and HTTP // request to the given server. It may use an HTTP transport or a stream based // transport. It is valid to pass an empty server.ID and a nil streamHost. -func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.RoundTripper, error) { +func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.RoundTripper, error) { options := roundTripperOpts{} for _, o := range opts { options = o(options) @@ -498,8 +502,8 @@ func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, o // Do we have an existing connection to this peer? existingStreamConn := false - if server.ID != "" && streamHost != nil { - existingStreamConn = len(streamHost.Network().ConnsToPeer(server.ID)) > 0 + if server.ID != "" && h.streamHost != nil { + existingStreamConn = len(h.streamHost.Network().ConnsToPeer(server.ID)) > 0 } if len(httpAddrs) > 0 && (options.preferHTTPTransport || (firstAddrIsHTTP && !existingStreamConn)) { @@ -532,20 +536,20 @@ func (h *HTTPHost) NewRoundTripper(streamHost host.Host, server peer.AddrInfo, o } // Otherwise use a stream based transport - if streamHost == nil { + if h.streamHost == nil { return nil, fmt.Errorf("no http addresses for peer, and no stream host provided") } if !existingStreamConn { if server.ID == "" { return nil, fmt.Errorf("no http addresses for peer, and no server peer ID provided") } - err := streamHost.Connect(context.Background(), peer.AddrInfo{ID: server.ID, Addrs: nonHttpAddrs}) + err := h.streamHost.Connect(context.Background(), peer.AddrInfo{ID: server.ID, Addrs: nonHttpAddrs}) if err != nil { return nil, fmt.Errorf("failed to connect to peer: %w", err) } } - return NewStreamRoundTripper(streamHost, server.ID), nil + return NewStreamRoundTripper(h.streamHost, server.ID), nil } type httpMultiaddr struct { diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 3a28e7ef5a..1adb812eb4 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -93,7 +93,7 @@ func TestRoundTrippers(t *testing.T) { { name: "HTTP preferred", setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { - rt, err := clientHTTPHost.NewRoundTripper(clientStreamHost, peer.AddrInfo{ + rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: serverMultiaddrs, }, libp2phttp.RoundTripperPreferHTTPTransport) @@ -104,7 +104,7 @@ func TestRoundTrippers(t *testing.T) { { name: "HTTP first", setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { - rt, err := clientHTTPHost.NewRoundTripper(clientStreamHost, peer.AddrInfo{ + rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHTTPAddr, serverHost.Addrs()[0]}, }) @@ -115,7 +115,7 @@ func TestRoundTrippers(t *testing.T) { { name: "No HTTP transport", setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { - rt, err := clientHTTPHost.NewRoundTripper(clientStreamHost, peer.AddrInfo{ + rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHost.Addrs()[0]}, }) @@ -127,7 +127,7 @@ func TestRoundTrippers(t *testing.T) { { name: "Stream transport first", setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { - rt, err := clientHTTPHost.NewRoundTripper(clientStreamHost, peer.AddrInfo{ + rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHost.Addrs()[0], serverHTTPAddr}, }) @@ -143,7 +143,7 @@ func TestRoundTrippers(t *testing.T) { ID: serverHost.ID(), Addrs: serverHost.Addrs(), }) - rt, err := clientHTTPHost.NewRoundTripper(clientStreamHost, peer.AddrInfo{ + rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHTTPAddr, serverHost.Addrs()[0]}, }) @@ -157,14 +157,14 @@ func TestRoundTrippers(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Start client - clientHost, err := libp2p.New(libp2p.NoListenAddrs) + clientStreamHost, err := libp2p.New(libp2p.NoListenAddrs) require.NoError(t, err) - defer clientHost.Close() + defer clientStreamHost.Close() - clientHttpHost, err := libp2phttp.New() + clientHttpHost, err := libp2phttp.New(libp2phttp.StreamHost(clientStreamHost)) require.NoError(t, err) - rt := tc.setupRoundTripper(t, clientHost, clientHttpHost) + rt := tc.setupRoundTripper(t, clientStreamHost, clientHttpHost) if tc.expectStreamRoundTripper { // Hack to get the private type of this roundtripper typ := reflect.TypeOf(rt).String() @@ -242,7 +242,7 @@ func TestPlainOldHTTPServer(t *testing.T) { do: func(t *testing.T, request *http.Request) (*http.Response, error) { clientHttpHost, err := libp2phttp.New() require.NoError(t, err) - rt, err := clientHttpHost.NewRoundTripper(nil, peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) + rt, err := clientHttpHost.NewRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) client := &http.Client{Transport: rt} @@ -251,7 +251,7 @@ func TestPlainOldHTTPServer(t *testing.T) { getWellKnown: func(t *testing.T) (libp2phttp.WellKnownProtoMap, error) { clientHttpHost, err := libp2phttp.New() require.NoError(t, err) - rt, err := clientHttpHost.NewRoundTripper(nil, peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) + rt, err := clientHttpHost.NewRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) return clientHttpHost.GetAndStorePeerProtoMap(rt, "") }, From b7ab53837183b5c02b6262194d508969806dd1c1 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 4 Aug 2023 13:02:40 -0700 Subject: [PATCH 15/72] Nits --- p2p/http/libp2phttp.go | 15 ++++++++------- p2p/http/ping.go | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 66d52fa14b..3cd044f485 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -562,17 +562,17 @@ type httpMultiaddr struct { func parseMultiaddr(addr ma.Multiaddr) httpMultiaddr { out := httpMultiaddr{} ma.ForEach(addr, func(c ma.Component) bool { - p := c.Protocol() - if p.Code == ma.P_IP4 || p.Code == ma.P_IP6 || p.Code == ma.P_DNS || p.Code == ma.P_DNS4 || p.Code == ma.P_DNS6 { + switch c.Protocol().Code { + case ma.P_IP4, ma.P_IP6, ma.P_DNS, ma.P_DNS4, ma.P_DNS6: out.host = c.Value() - } else if p.Code == ma.P_TCP || p.Code == ma.P_UDP { + case ma.P_TCP, ma.P_UDP: out.port = c.Value() - } else if p.Code == ma.P_TLS { + case ma.P_TLS: out.useHTTPS = true - } else if p.Code == ma.P_SNI { + case ma.P_SNI: out.sni = c.Value() - } + } return out.host == "" || out.port == "" || !out.useHTTPS || out.sni == "" }) @@ -645,7 +645,8 @@ func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, serve bytesRead += n if err == io.EOF { break - } else if err != nil { + } + if err != nil { return nil, err } if bytesRead >= PeerMetadataLimit { diff --git a/p2p/http/ping.go b/p2p/http/ping.go index e0575f638f..f6b5734c59 100644 --- a/p2p/http/ping.go +++ b/p2p/http/ping.go @@ -49,6 +49,7 @@ func SendPing(client http.Client) error { if err != nil { return err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status code: %d", resp.StatusCode) From 44f330a1b25b52cdc1d80ef5300f0247c8af0f20 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 7 Aug 2023 15:34:30 -0700 Subject: [PATCH 16/72] Add AddPeerMetadata --- p2p/http/libp2phttp.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 3cd044f485..b7245b3f93 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -662,3 +662,14 @@ func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, serve return meta, nil } + +// AddPeerMetadata adds a peer's protocol metadata to the http host. Useful if +// you have out-of-band knowledge of a peer's protocol mapping. +func (h *HTTPHost) AddPeerMetadata(server peer.ID, meta WellKnownProtoMap) { + h.peerMetadata.Add(server, meta) +} + +// RmPeerMetadata removes a peer's protocol metadata from the http host +func (h *HTTPHost) RmPeerMetadata(server peer.ID, meta WellKnownProtoMap) { + h.peerMetadata.Remove(server) +} From 6f18bdb11ec7f6bbef18c055a2b76d1b0de53014 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 8 Aug 2023 15:31:57 -0700 Subject: [PATCH 17/72] Add CustomRootHandler option --- p2p/http/options.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/p2p/http/options.go b/p2p/http/options.go index a1bd9e27b4..2e095d1b86 100644 --- a/p2p/http/options.go +++ b/p2p/http/options.go @@ -3,6 +3,7 @@ package libp2phttp import ( "crypto/tls" "fmt" + "net/http" "github.com/libp2p/go-libp2p/core/host" ma "github.com/multiformats/go-multiaddr" @@ -10,7 +11,7 @@ import ( type HTTPHostOption func(*HTTPHost) error -// WithStreamHost sets the stream host to use for HTTP over libp2p streams. +// ListenAddrs sets the listen addresses for the HTTP transport. func ListenAddrs(addrs []ma.Multiaddr) HTTPHostOption { return func(h *HTTPHost) error { // assert that each addr contains a /http component @@ -26,6 +27,7 @@ func ListenAddrs(addrs []ma.Multiaddr) HTTPHostOption { } } +// TLSConfig sets the server TLS config for the HTTP transport. func TLSConfig(tlsConfig *tls.Config) HTTPHostOption { return func(h *HTTPHost) error { h.httpTransport.tlsConfig = tlsConfig @@ -48,3 +50,14 @@ func WithTLSClientConfig(tlsConfig *tls.Config) HTTPHostOption { return nil } } + +// CustomRootHandler sets the root handler for the HTTP transport. It *does not* +// set up a .well-known/libp2p handler. Users assume the responsibility of +// handling .well-known/libp2p, as well as keeping it up to date. +func CustomRootHandler(mux http.Handler) HTTPHostOption { + return func(h *HTTPHost) error { + h.rootHandler = http.ServeMux{} + h.rootHandler.Handle("/", mux) + return nil + } +} From 63cfaf1d823d1d02340da2317437fc075f0c8b97 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 8 Aug 2023 15:32:28 -0700 Subject: [PATCH 18/72] Remove old todos --- p2p/http/libp2phttp.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index b7245b3f93..683cd9de24 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -32,12 +32,6 @@ const PeerMetadataLRUSize = 256 // How many different peer's metadata to keep // TODOs: // - integrate with the conn gater and resource manager -// - Support listenAddr option to accept a multiaddr to listen on (could handle h3 as well) -// - Support withStreamHost option to accept a streamHost to listen on - -// Dev notes -// Would be nice to have an .Addrs method on the httpHost -// Would be nice to have the httpHost manage the listener (listenAddr option above) type WellKnownProtocolMeta struct { Path string `json:"path"` From 8753458e02b3a69279b75e7a53d0059995f733ad Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 8 Aug 2023 15:33:17 -0700 Subject: [PATCH 19/72] Undo Noise changes --- p2p/security/noise/pb/payload.pb.go | 54 +++++++++-------------------- p2p/security/noise/pb/payload.proto | 2 -- 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/p2p/security/noise/pb/payload.pb.go b/p2p/security/noise/pb/payload.pb.go index b35475aba2..8e3a805a58 100644 --- a/p2p/security/noise/pb/payload.pb.go +++ b/p2p/security/noise/pb/payload.pb.go @@ -27,8 +27,6 @@ type NoiseExtensions struct { WebtransportCerthashes [][]byte `protobuf:"bytes,1,rep,name=webtransport_certhashes,json=webtransportCerthashes" json:"webtransport_certhashes,omitempty"` StreamMuxers []string `protobuf:"bytes,2,rep,name=stream_muxers,json=streamMuxers" json:"stream_muxers,omitempty"` - SNI *string `protobuf:"bytes,3,opt,name=SNI" json:"SNI,omitempty"` - HTTPLibp2PToken *string `protobuf:"bytes,4,opt,name=HTTP_libp2p_token,json=HTTPLibp2pToken" json:"HTTP_libp2p_token,omitempty"` } func (x *NoiseExtensions) Reset() { @@ -77,20 +75,6 @@ func (x *NoiseExtensions) GetStreamMuxers() []string { return nil } -func (x *NoiseExtensions) GetSNI() string { - if x != nil && x.SNI != nil { - return *x.SNI - } - return "" -} - -func (x *NoiseExtensions) GetHTTPLibp2PToken() string { - if x != nil && x.HTTPLibp2PToken != nil { - return *x.HTTPLibp2PToken - } - return "" -} - type NoiseHandshakePayload struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -158,27 +142,23 @@ var File_pb_payload_proto protoreflect.FileDescriptor var file_pb_payload_proto_rawDesc = []byte{ 0x0a, 0x10, 0x70, 0x62, 0x2f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62, 0x22, 0xad, 0x01, 0x0a, 0x0f, 0x4e, 0x6f, 0x69, 0x73, 0x65, - 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x17, 0x77, 0x65, - 0x62, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x68, - 0x61, 0x73, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x16, 0x77, 0x65, 0x62, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x65, 0x72, 0x74, 0x68, 0x61, 0x73, - 0x68, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x6d, 0x75, - 0x78, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x74, 0x72, 0x65, - 0x61, 0x6d, 0x4d, 0x75, 0x78, 0x65, 0x72, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x53, 0x4e, 0x49, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x53, 0x4e, 0x49, 0x12, 0x2a, 0x0a, 0x11, 0x48, 0x54, - 0x54, 0x50, 0x5f, 0x6c, 0x69, 0x62, 0x70, 0x32, 0x70, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x48, 0x54, 0x54, 0x50, 0x4c, 0x69, 0x62, 0x70, 0x32, - 0x70, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x92, 0x01, 0x0a, 0x15, 0x4e, 0x6f, 0x69, 0x73, 0x65, - 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, - 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, - 0x73, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x53, 0x69, 0x67, 0x12, 0x33, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, - 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, - 0x4e, 0x6f, 0x69, 0x73, 0x65, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, - 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62, 0x22, 0x6f, 0x0a, 0x0f, 0x4e, 0x6f, 0x69, 0x73, 0x65, 0x45, + 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x17, 0x77, 0x65, 0x62, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x68, 0x61, + 0x73, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x16, 0x77, 0x65, 0x62, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x65, 0x72, 0x74, 0x68, 0x61, 0x73, 0x68, + 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x6d, 0x75, 0x78, + 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x4d, 0x75, 0x78, 0x65, 0x72, 0x73, 0x22, 0x92, 0x01, 0x0a, 0x15, 0x4e, 0x6f, 0x69, 0x73, + 0x65, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x5f, 0x73, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x53, 0x69, 0x67, 0x12, 0x33, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, + 0x2e, 0x4e, 0x6f, 0x69, 0x73, 0x65, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, } var ( diff --git a/p2p/security/noise/pb/payload.proto b/p2p/security/noise/pb/payload.proto index a3ccfa6052..ff303b0daf 100644 --- a/p2p/security/noise/pb/payload.proto +++ b/p2p/security/noise/pb/payload.proto @@ -4,8 +4,6 @@ package pb; message NoiseExtensions { repeated bytes webtransport_certhashes = 1; repeated string stream_muxers = 2; - optional string SNI = 3; - optional string HTTP_libp2p_token = 4; } message NoiseHandshakePayload { From 50704079fc0eda43ca87a719c6f23eb480a2042e Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 8 Aug 2023 15:37:48 -0700 Subject: [PATCH 20/72] Add comments --- p2p/http/libp2phttp.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 683cd9de24..e5c68d5272 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -99,10 +99,15 @@ type HTTPHost struct { rootHandler http.ServeMux wk WellKnownHandler httpRoundTripper *http.Transport - recentHTTPAddrs *lru.Cache[peer.ID, httpAddr] - peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] - streamHost host.Host // may be nil - httpTransport *httpTransport + // recentHTTPAddrs is an lru cache of recently used HTTP addresses. This + // lets us know if we've recently connected to an HTTP endpoint and might + // have a warm idle connection for it (managed by the underlying HTTP + // roundtripper). In some cases, this lets us reuse our existing custom roundtripper (i.e. SNI != host). + recentHTTPAddrs *lru.Cache[peer.ID, httpAddr] + // peerMetadata is an lru cache of a peer's well-known protocol map. + peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] + streamHost host.Host // may be nil + httpTransport *httpTransport } type httpTransport struct { From b7b3a1ac8d96c0b2212d061e9b0a85c3c958b41c Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 9 Aug 2023 11:29:58 -0700 Subject: [PATCH 21/72] Implement CloseIdleConnections to protect from surprising behavior --- p2p/http/libp2phttp.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index e5c68d5272..65a9e8fdef 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -384,6 +384,22 @@ func (rt *roundTripperForSpecificServer) RoundTrip(r *http.Request) (*http.Respo return resp, err } +func (rt *roundTripperForSpecificServer) CloseIdleConnections() { + if rt.ownRoundtripper { + // Safe to close idle connections, since we own the RoundTripper. We + // aren't closing other's idle connections. + type closeIdler interface { + CloseIdleConnections() + } + if tr, ok := rt.RoundTripper.(closeIdler); ok { + tr.CloseIdleConnections() + } + } + // No-op, since we don't want users thinking they are closing idle + // connections for this server, when in fact they are closing all idle + // connections +} + type namespacedRoundTripper struct { http.RoundTripper protocolPrefix string From 67cd05e78a2db788fb32ac453561f608383459b3 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 9 Aug 2023 11:57:31 -0700 Subject: [PATCH 22/72] Add todo --- p2p/http/libp2phttp.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 65a9e8fdef..7dca4295c6 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -31,6 +31,7 @@ const PeerMetadataLimit = 8 << 10 // 8KB const PeerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache // TODOs: +// - Error if we try to connect to a server over an HTTP transport and specify the server ID. We haven't implemented server peer id Auth yet. // - integrate with the conn gater and resource manager type WellKnownProtocolMeta struct { @@ -472,7 +473,7 @@ type RoundTripperOptsFn func(o roundTripperOpts) roundTripperOpts // NewRoundTripper returns an http.RoundTripper that can fulfill and HTTP // request to the given server. It may use an HTTP transport or a stream based -// transport. It is valid to pass an empty server.ID and a nil streamHost. +// transport. It is valid to pass an empty server.ID. func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.RoundTripper, error) { options := roundTripperOpts{} for _, o := range opts { From b60680d7dfe076c3768dbf3114e3e030d5741c86 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 11 Aug 2023 11:17:20 -0700 Subject: [PATCH 23/72] Add ServerMustAuthenticatePeerID option --- p2p/http/libp2phttp.go | 32 ++++++++++++++------------------ p2p/http/options.go | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 7dca4295c6..9599da414e 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -31,7 +31,6 @@ const PeerMetadataLimit = 8 << 10 // 8KB const PeerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache // TODOs: -// - Error if we try to connect to a server over an HTTP transport and specify the server ID. We haven't implemented server peer id Auth yet. // - integrate with the conn gater and resource manager type WellKnownProtocolMeta struct { @@ -125,6 +124,10 @@ type httpAddr struct { scheme string sni string rt http.RoundTripper // optional, if this needed its own transport + // This is a temporary flag that lets us know if this round tripper can authenticate the server. + // Currently HTTP peer ID auth is not implemented, so if this roundtripper + // is a native HTTP transport, we can not authenticate the server's peer ID. + rtCanAuthenticateServer bool } // New creates a new HTTPHost. Use HTTPHost.Serve to start serving HTTP requests (both over libp2p streams and HTTP transport). @@ -311,11 +314,6 @@ func (h *HTTPHost) SetHttpHandlerAtPath(p protocol.ID, path string, handler http h.rootHandler.Handle(path, handler) } -type roundTripperOpts struct { - // todo SkipClientAuth bool - preferHTTPTransport bool -} - type streamRoundTripper struct { server peer.ID h host.Host @@ -464,13 +462,6 @@ func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts .. return http.Client{Transport: &nrt}, nil } -func RoundTripperPreferHTTPTransport(o roundTripperOpts) roundTripperOpts { - o.preferHTTPTransport = true - return o -} - -type RoundTripperOptsFn func(o roundTripperOpts) roundTripperOpts - // NewRoundTripper returns an http.RoundTripper that can fulfill and HTTP // request to the given server. It may use an HTTP transport or a stream based // transport. It is valid to pass an empty server.ID. @@ -480,8 +471,12 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt options = o(options) } - // Do we have a recent HTTP transport connection to this peer? - if a, ok := h.recentHTTPAddrs.Get(server.ID); server.ID != "" && ok { + if options.ServerMustAuthenticatePeerID && server.ID == "" { + return nil, fmt.Errorf("server must authenticate peer ID, but no peer ID provided") + } + + // Do we have a recent HTTP transport connection to this peer? And is it compatible with the ServerMustAuthenticate option? + if a, ok := h.recentHTTPAddrs.Get(server.ID); server.ID != "" && ok && (!options.ServerMustAuthenticatePeerID || (options.ServerMustAuthenticatePeerID && a.rtCanAuthenticateServer)) { var rt http.RoundTripper = h.httpRoundTripper ownRoundtripper := false if a.rt != nil { @@ -522,7 +517,8 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt existingStreamConn = len(h.streamHost.Network().ConnsToPeer(server.ID)) > 0 } - if len(httpAddrs) > 0 && (options.preferHTTPTransport || (firstAddrIsHTTP && !existingStreamConn)) { + // Currently the HTTP transport can not authenticate peer IDs. + if !options.ServerMustAuthenticatePeerID && len(httpAddrs) > 0 && (options.preferHTTPTransport || (firstAddrIsHTTP && !existingStreamConn)) { parsed := parseMultiaddr(httpAddrs[0]) scheme := "http" if parsed.useHTTPS { @@ -553,11 +549,11 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt // Otherwise use a stream based transport if h.streamHost == nil { - return nil, fmt.Errorf("no http addresses for peer, and no stream host provided") + return nil, fmt.Errorf("can not use the HTTP transport (either no address or PeerID auth is required), and no stream host provided") } if !existingStreamConn { if server.ID == "" { - return nil, fmt.Errorf("no http addresses for peer, and no server peer ID provided") + return nil, fmt.Errorf("can not use the HTTP transport, and no server peer ID provided") } err := h.streamHost.Connect(context.Background(), peer.AddrInfo{ID: server.ID, Addrs: nonHttpAddrs}) if err != nil { diff --git a/p2p/http/options.go b/p2p/http/options.go index 2e095d1b86..fea8c9fb6e 100644 --- a/p2p/http/options.go +++ b/p2p/http/options.go @@ -61,3 +61,21 @@ func CustomRootHandler(mux http.Handler) HTTPHostOption { return nil } } + +type RoundTripperOptsFn func(o roundTripperOpts) roundTripperOpts + +type roundTripperOpts struct { + // todo SkipClientAuth bool + preferHTTPTransport bool + ServerMustAuthenticatePeerID bool +} + +func RoundTripperPreferHTTPTransport(o roundTripperOpts) roundTripperOpts { + o.preferHTTPTransport = true + return o +} + +func ServerMustAuthenticatePeerID(o roundTripperOpts) roundTripperOpts { + o.ServerMustAuthenticatePeerID = true + return o +} From 9dad142bb27477e355de11409dfd687d22f64dce Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 11 Aug 2023 14:30:30 -0700 Subject: [PATCH 24/72] WIP work on recent roundtripper logic --- p2p/http/libp2phttp.go | 123 +++++++++++++++++++++++++----------- p2p/http/libp2phttp_test.go | 2 + 2 files changed, 88 insertions(+), 37 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 9599da414e..129357e932 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -30,6 +30,9 @@ const ProtocolIDForMultistreamSelect = "/http/1.1" const PeerMetadataLimit = 8 << 10 // 8KB const PeerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache +// The scheme we use internally to name a stream based roundtripper. This is only used locally. +const libp2pStreamScheme = "libp2p-stream" + // TODOs: // - integrate with the conn gater and resource manager @@ -92,6 +95,14 @@ func (h *WellKnownHandler) RmProtocolMapping(p protocol.ID, path string) { h.wellknownMapMu.Unlock() } +// peerKey is the key used for a peer in the lru cache. +type peerKey struct { + peerID peer.ID + addr string // host:port + scheme string + sni string +} + // HTTPHost is a libp2p host for request/responses with HTTP semantics. This is // in contrast to a stream-oriented host like the host.Host interface. Warning, // this is experimental. The API will likely change. @@ -99,13 +110,14 @@ type HTTPHost struct { rootHandler http.ServeMux wk WellKnownHandler httpRoundTripper *http.Transport - // recentHTTPAddrs is an lru cache of recently used HTTP addresses. This - // lets us know if we've recently connected to an HTTP endpoint and might - // have a warm idle connection for it (managed by the underlying HTTP - // roundtripper). In some cases, this lets us reuse our existing custom roundtripper (i.e. SNI != host). - recentHTTPAddrs *lru.Cache[peer.ID, httpAddr] + // recentRoundTrippers is an lru cache of recently used roundtrippers for a + // peer. This lets us know if we've recently connected to an HTTP endpoint + // and might have a warm idle connection for it (managed by the underlying + // HTTP roundtripper). In some cases, this lets us reuse our existing custom + // roundtripper (i.e. SNI != host). + recentRoundTrippers *lru.Cache[peerKey, roundTripperForServer] // peerMetadata is an lru cache of a peer's well-known protocol map. - peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] + peerMetadata *lru.Cache[peerKey, WellKnownProtoMap] streamHost host.Host // may be nil httpTransport *httpTransport } @@ -119,11 +131,11 @@ type httpTransport struct { waitingForListeners chan struct{} } -type httpAddr struct { +type roundTripperForServer struct { addr string scheme string sni string - rt http.RoundTripper // optional, if this needed its own transport + rt http.RoundTripper // optional, if nil it's the hosts' http.RoundTripper // This is a temporary flag that lets us know if this round tripper can authenticate the server. // Currently HTTP peer ID auth is not implemented, so if this roundtripper // is a native HTTP transport, we can not authenticate the server's peer ID. @@ -138,19 +150,19 @@ func New(opts ...HTTPHostOption) (*HTTPHost, error) { recentConnsLimit = 32 } - recentHTTP, err := lru.New[peer.ID, httpAddr](recentConnsLimit) - peerMetadata, err2 := lru.New[peer.ID, WellKnownProtoMap](PeerMetadataLRUSize) + recentHTTP, err := lru.New[peerKey, roundTripperForServer](recentConnsLimit) + peerMetadata, err2 := lru.New[peerKey, WellKnownProtoMap](PeerMetadataLRUSize) if err != nil || err2 != nil { // Only happens if size is < 1. We make sure to not do that, so this should never happen. panic(err) } h := &HTTPHost{ - wk: WellKnownHandler{}, - rootHandler: http.ServeMux{}, - httpRoundTripper: httpRoundTripper, - recentHTTPAddrs: recentHTTP, - peerMetadata: peerMetadata, + wk: WellKnownHandler{}, + rootHandler: http.ServeMux{}, + httpRoundTripper: httpRoundTripper, + recentRoundTrippers: recentHTTP, + peerMetadata: peerMetadata, httpTransport: &httpTransport{ closeListeners: make(chan struct{}), waitingForListeners: make(chan struct{}), @@ -354,6 +366,7 @@ func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) } // roundTripperForSpecificHost is an http.RoundTripper targets a specific server. Still reuses the underlying RoundTripper for the requests. +// The underlying RoundTripper MUST be an HTTP Transport. type roundTripperForSpecificServer struct { http.RoundTripper ownRoundtripper bool @@ -362,6 +375,10 @@ type roundTripperForSpecificServer struct { targetServerAddr string sni string scheme string + // This is a temporary flag that lets us know if this round tripper can authenticate the server. + // Currently HTTP peer ID auth is not implemented, so if this roundtripper + // is a native HTTP transport, we can not authenticate the server's peer ID. + rtCanAuthenticateServer bool } // RoundTrip implements http.RoundTripper. @@ -373,12 +390,22 @@ func (rt *roundTripperForSpecificServer) RoundTrip(r *http.Request) (*http.Respo r.URL.Host = rt.targetServerAddr r.Host = rt.sni resp, err := rt.RoundTripper.RoundTrip(r) - if err == nil && rt.server != "" { - ha := httpAddr{addr: rt.targetServerAddr, scheme: rt.scheme, sni: rt.sni} + if err == nil { + ha := roundTripperForServer{ + addr: rt.targetServerAddr, + scheme: rt.scheme, + sni: rt.sni, + rtCanAuthenticateServer: rt.rtCanAuthenticateServer, + } if rt.ownRoundtripper { ha.rt = rt.RoundTripper } - rt.httpHost.recentHTTPAddrs.Add(rt.server, ha) + rt.httpHost.recentRoundTrippers.Add(peerKey{ + peerID: rt.server, + addr: rt.targetServerAddr, + scheme: rt.scheme, + sni: rt.sni, + }, ha) } return resp, err } @@ -475,25 +502,6 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt return nil, fmt.Errorf("server must authenticate peer ID, but no peer ID provided") } - // Do we have a recent HTTP transport connection to this peer? And is it compatible with the ServerMustAuthenticate option? - if a, ok := h.recentHTTPAddrs.Get(server.ID); server.ID != "" && ok && (!options.ServerMustAuthenticatePeerID || (options.ServerMustAuthenticatePeerID && a.rtCanAuthenticateServer)) { - var rt http.RoundTripper = h.httpRoundTripper - ownRoundtripper := false - if a.rt != nil { - ownRoundtripper = true - rt = a.rt - } - return &roundTripperForSpecificServer{ - RoundTripper: rt, - ownRoundtripper: ownRoundtripper, - httpHost: h, - server: server.ID, - targetServerAddr: a.addr, - scheme: a.scheme, - sni: a.sni, - }, nil - } - httpAddrs := make([]ma.Multiaddr, 0, 1) // The common case of a single http address nonHttpAddrs := make([]ma.Multiaddr, 0, len(server.Addrs)) @@ -501,13 +509,54 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt for i, addr := range server.Addrs { addr, isHttp := normalizeHTTPMultiaddr(addr) + var pk peerKey if isHttp { if i == 0 { firstAddrIsHTTP = true } httpAddrs = append(httpAddrs, addr) + + parsed := parseMultiaddr(addr) + pk = peerKey{ + peerID: server.ID, + addr: parsed.host + ":" + parsed.port, + scheme: "http", + sni: parsed.sni, + } + if parsed.useHTTPS { + pk.scheme = "https" + } } else { nonHttpAddrs = append(nonHttpAddrs, addr) + + pk = peerKey{ + peerID: server.ID, + scheme: libp2pStreamScheme, + } + } + + // Do we have recentRt recent transport for this peer? and is it compatible with the options? + if recentRt, ok := h.recentRoundTrippers.Get(pk); ok && (!options.ServerMustAuthenticatePeerID || (options.ServerMustAuthenticatePeerID && recentRt.rtCanAuthenticateServer)) { + var rt http.RoundTripper = h.httpRoundTripper + ownRoundtripper := false + if recentRt.rt != nil { + ownRoundtripper = true + rt = recentRt.rt + } + if recentRt.scheme == libp2pStreamScheme { + // This is a stream based roundtripper + return rt, nil + } + return &roundTripperForSpecificServer{ + RoundTripper: rt, + ownRoundtripper: ownRoundtripper, + httpHost: h, + server: server.ID, + targetServerAddr: recentRt.addr, + scheme: recentRt.scheme, + sni: recentRt.sni, + rtCanAuthenticateServer: recentRt.rtCanAuthenticateServer, + }, nil } } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 1adb812eb4..33e24c8c37 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -307,4 +307,6 @@ func TestPlainOldHTTPServer(t *testing.T) { } } +// TODO test the recent roundtripper code + // TODO test with tls From 24d61f3685b239d40bbaef3955070a4182d18fdd Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 11 Aug 2023 14:49:13 -0700 Subject: [PATCH 25/72] Remove recentHTTPAddrs. We don't need it --- p2p/http/libp2phttp.go | 166 +++++++++++++----------------------- p2p/http/libp2phttp_test.go | 2 - 2 files changed, 59 insertions(+), 109 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 129357e932..6419b23c88 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -95,14 +95,6 @@ func (h *WellKnownHandler) RmProtocolMapping(p protocol.ID, path string) { h.wellknownMapMu.Unlock() } -// peerKey is the key used for a peer in the lru cache. -type peerKey struct { - peerID peer.ID - addr string // host:port - scheme string - sni string -} - // HTTPHost is a libp2p host for request/responses with HTTP semantics. This is // in contrast to a stream-oriented host like the host.Host interface. Warning, // this is experimental. The API will likely change. @@ -110,14 +102,8 @@ type HTTPHost struct { rootHandler http.ServeMux wk WellKnownHandler httpRoundTripper *http.Transport - // recentRoundTrippers is an lru cache of recently used roundtrippers for a - // peer. This lets us know if we've recently connected to an HTTP endpoint - // and might have a warm idle connection for it (managed by the underlying - // HTTP roundtripper). In some cases, this lets us reuse our existing custom - // roundtripper (i.e. SNI != host). - recentRoundTrippers *lru.Cache[peerKey, roundTripperForServer] // peerMetadata is an lru cache of a peer's well-known protocol map. - peerMetadata *lru.Cache[peerKey, WellKnownProtoMap] + peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] streamHost host.Host // may be nil httpTransport *httpTransport } @@ -131,38 +117,20 @@ type httpTransport struct { waitingForListeners chan struct{} } -type roundTripperForServer struct { - addr string - scheme string - sni string - rt http.RoundTripper // optional, if nil it's the hosts' http.RoundTripper - // This is a temporary flag that lets us know if this round tripper can authenticate the server. - // Currently HTTP peer ID auth is not implemented, so if this roundtripper - // is a native HTTP transport, we can not authenticate the server's peer ID. - rtCanAuthenticateServer bool -} - // New creates a new HTTPHost. Use HTTPHost.Serve to start serving HTTP requests (both over libp2p streams and HTTP transport). func New(opts ...HTTPHostOption) (*HTTPHost, error) { httpRoundTripper := http.DefaultTransport.(*http.Transport).Clone() - recentConnsLimit := httpRoundTripper.MaxIdleConns - if recentConnsLimit < 1 { - recentConnsLimit = 32 - } - - recentHTTP, err := lru.New[peerKey, roundTripperForServer](recentConnsLimit) - peerMetadata, err2 := lru.New[peerKey, WellKnownProtoMap](PeerMetadataLRUSize) - if err != nil || err2 != nil { + peerMetadata, err := lru.New[peer.ID, WellKnownProtoMap](PeerMetadataLRUSize) + if err != nil { // Only happens if size is < 1. We make sure to not do that, so this should never happen. panic(err) } h := &HTTPHost{ - wk: WellKnownHandler{}, - rootHandler: http.ServeMux{}, - httpRoundTripper: httpRoundTripper, - recentRoundTrippers: recentHTTP, - peerMetadata: peerMetadata, + wk: WellKnownHandler{}, + rootHandler: http.ServeMux{}, + httpRoundTripper: httpRoundTripper, + peerMetadata: peerMetadata, httpTransport: &httpTransport{ closeListeners: make(chan struct{}), waitingForListeners: make(chan struct{}), @@ -326,9 +294,15 @@ func (h *HTTPHost) SetHttpHandlerAtPath(p protocol.ID, path string, handler http h.rootHandler.Handle(path, handler) } +// getPeerProtoMap lets RoundTrippers implement a specific way of caching a peer's protocol mapping. +type getPeerProtoMap interface { + GetPeerProtoMap() (WellKnownProtoMap, error) +} + type streamRoundTripper struct { - server peer.ID - h host.Host + server peer.ID + h host.Host + httpHost *HTTPHost } type streamReadCloser struct { @@ -341,6 +315,10 @@ func (s *streamReadCloser) Close() error { return s.ReadCloser.Close() } +func (rt *streamRoundTripper) GetPeerProtoMap(server peer.ID) (WellKnownProtoMap, error) { + return rt.httpHost.GetAndStorePeerProtoMap(rt, rt.server) +} + // RoundTrip implements http.RoundTripper. func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { s, err := rt.h.NewStream(r.Context(), rt.server, ProtocolIDForMultistreamSelect) @@ -375,10 +353,30 @@ type roundTripperForSpecificServer struct { targetServerAddr string sni string scheme string - // This is a temporary flag that lets us know if this round tripper can authenticate the server. - // Currently HTTP peer ID auth is not implemented, so if this roundtripper - // is a native HTTP transport, we can not authenticate the server's peer ID. - rtCanAuthenticateServer bool + cachedProtos WellKnownProtoMap +} + +func (rt *roundTripperForSpecificServer) GetPeerProtoMap() (WellKnownProtoMap, error) { + // Do we already have the peer's protocol mapping? + if rt.cachedProtos != nil { + return rt.cachedProtos, nil + } + + // if the underlying roundtripper implements getPeerProtoMap, use that + if g, ok := rt.RoundTripper.(getPeerProtoMap); ok { + wk, err := g.GetPeerProtoMap() + if err == nil { + rt.cachedProtos = wk + return wk, nil + } + } + + wk, err := rt.httpHost.GetAndStorePeerProtoMap(rt, rt.server) + if err == nil { + rt.cachedProtos = wk + return wk, nil + } + return wk, err } // RoundTrip implements http.RoundTripper. @@ -389,25 +387,7 @@ func (rt *roundTripperForSpecificServer) RoundTrip(r *http.Request) (*http.Respo r.URL.Scheme = rt.scheme r.URL.Host = rt.targetServerAddr r.Host = rt.sni - resp, err := rt.RoundTripper.RoundTrip(r) - if err == nil { - ha := roundTripperForServer{ - addr: rt.targetServerAddr, - scheme: rt.scheme, - sni: rt.sni, - rtCanAuthenticateServer: rt.rtCanAuthenticateServer, - } - if rt.ownRoundtripper { - ha.rt = rt.RoundTripper - } - rt.httpHost.recentRoundTrippers.Add(peerKey{ - peerID: rt.server, - addr: rt.targetServerAddr, - scheme: rt.scheme, - sni: rt.sni, - }, ha) - } - return resp, err + return rt.RoundTripper.RoundTrip(r) } func (rt *roundTripperForSpecificServer) CloseIdleConnections() { @@ -432,6 +412,14 @@ type namespacedRoundTripper struct { protocolPrefixRaw string } +func (rt *namespacedRoundTripper) GetPeerProtoMap() (WellKnownProtoMap, error) { + if g, ok := rt.RoundTripper.(getPeerProtoMap); ok { + return g.GetPeerProtoMap() + } + + return nil, fmt.Errorf("can not get peer protocol map. Inner roundtripper does not implement getPeerProtoMap") +} + // RoundTrip implements http.RoundTripper. func (rt *namespacedRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { if !strings.HasPrefix(r.URL.Path, rt.protocolPrefix) { @@ -474,7 +462,10 @@ func (h *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p proto }, nil } -// NamespacedClient returns an http.Client that is scoped to the given protocol on the given server. +// NamespacedClient returns an http.Client that is scoped to the given protocol +// on the given server. It creates a new RoundTripper for each call. If you are +// creating many namespaced clients, consider creating a round tripper directly +// and namespacing that yourself. func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.Client, error) { rt, err := h.NewRoundTripper(server, opts...) if err != nil { @@ -509,54 +500,13 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt for i, addr := range server.Addrs { addr, isHttp := normalizeHTTPMultiaddr(addr) - var pk peerKey if isHttp { if i == 0 { firstAddrIsHTTP = true } httpAddrs = append(httpAddrs, addr) - - parsed := parseMultiaddr(addr) - pk = peerKey{ - peerID: server.ID, - addr: parsed.host + ":" + parsed.port, - scheme: "http", - sni: parsed.sni, - } - if parsed.useHTTPS { - pk.scheme = "https" - } } else { nonHttpAddrs = append(nonHttpAddrs, addr) - - pk = peerKey{ - peerID: server.ID, - scheme: libp2pStreamScheme, - } - } - - // Do we have recentRt recent transport for this peer? and is it compatible with the options? - if recentRt, ok := h.recentRoundTrippers.Get(pk); ok && (!options.ServerMustAuthenticatePeerID || (options.ServerMustAuthenticatePeerID && recentRt.rtCanAuthenticateServer)) { - var rt http.RoundTripper = h.httpRoundTripper - ownRoundtripper := false - if recentRt.rt != nil { - ownRoundtripper = true - rt = recentRt.rt - } - if recentRt.scheme == libp2pStreamScheme { - // This is a stream based roundtripper - return rt, nil - } - return &roundTripperForSpecificServer{ - RoundTripper: rt, - ownRoundtripper: ownRoundtripper, - httpHost: h, - server: server.ID, - targetServerAddr: recentRt.addr, - scheme: recentRt.scheme, - sni: recentRt.sni, - rtCanAuthenticateServer: recentRt.rtCanAuthenticateServer, - }, nil } } @@ -676,7 +626,9 @@ 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 returns it +// ProtocolPathPrefix 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 *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, server peer.ID) (WellKnownProtoMap, error) { if meta, ok := h.peerMetadata.Get(server); server != "" && ok { return meta, nil diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 33e24c8c37..1adb812eb4 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -307,6 +307,4 @@ func TestPlainOldHTTPServer(t *testing.T) { } } -// TODO test the recent roundtripper code - // TODO test with tls From c53875f9562e8130188dc7db8f1f9bfc85da4727 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 10:16:00 -0700 Subject: [PATCH 26/72] Move http ping to separate package --- p2p/http/libp2phttp_test.go | 10 ++++------ p2p/http/{ => ping}/ping.go | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) rename p2p/http/{ => ping}/ping.go (98%) diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 1adb812eb4..68ed2d7230 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -16,6 +16,7 @@ import ( host "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" libp2phttp "github.com/libp2p/go-libp2p/p2p/http" + httpping "github.com/libp2p/go-libp2p/p2p/http/ping" ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" ) @@ -211,14 +212,13 @@ func TestRoundTrippers(t *testing.T) { } } -// TODO test with a native Go HTTP server func TestPlainOldHTTPServer(t *testing.T) { mux := http.NewServeMux() wk := libp2phttp.WellKnownHandler{} mux.Handle("/.well-known/libp2p", &wk) - mux.Handle("/ping/", libp2phttp.Ping{}) - wk.AddProtocolMapping(libp2phttp.PingProtocolID, "/ping/") + mux.Handle("/ping/", httpping.Ping{}) + wk.AddProtocolMapping(httpping.PingProtocolID, "/ping/") server := &http.Server{Addr: "127.0.0.1:0", Handler: mux} @@ -301,10 +301,8 @@ func TestPlainOldHTTPServer(t *testing.T) { require.NoError(t, err) expectedMap := make(libp2phttp.WellKnownProtoMap) - expectedMap[libp2phttp.PingProtocolID] = libp2phttp.WellKnownProtocolMeta{Path: "/ping/"} + expectedMap[httpping.PingProtocolID] = libp2phttp.WellKnownProtocolMeta{Path: "/ping/"} require.Equal(t, expectedMap, protoMap) }) } } - -// TODO test with tls diff --git a/p2p/http/ping.go b/p2p/http/ping/ping.go similarity index 98% rename from p2p/http/ping.go rename to p2p/http/ping/ping.go index f6b5734c59..77e08b8a07 100644 --- a/p2p/http/ping.go +++ b/p2p/http/ping/ping.go @@ -1,4 +1,4 @@ -package libp2phttp +package httpping import ( "bytes" From 6ca42d9dbbb29ace8ee4da2c5d452b0aba5ead57 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 10:16:36 -0700 Subject: [PATCH 27/72] Hide internal constants --- p2p/http/libp2phttp.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 6419b23c88..f1a1b36100 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -27,11 +27,8 @@ import ( ) const ProtocolIDForMultistreamSelect = "/http/1.1" -const PeerMetadataLimit = 8 << 10 // 8KB -const PeerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache - -// The scheme we use internally to name a stream based roundtripper. This is only used locally. -const libp2pStreamScheme = "libp2p-stream" +const peerMetadataLimit = 8 << 10 // 8KB +const peerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache // TODOs: // - integrate with the conn gater and resource manager @@ -120,7 +117,7 @@ type httpTransport struct { // New creates a new HTTPHost. Use HTTPHost.Serve to start serving HTTP requests (both over libp2p streams and HTTP transport). func New(opts ...HTTPHostOption) (*HTTPHost, error) { httpRoundTripper := http.DefaultTransport.(*http.Transport).Clone() - peerMetadata, err := lru.New[peer.ID, WellKnownProtoMap](PeerMetadataLRUSize) + peerMetadata, err := lru.New[peer.ID, WellKnownProtoMap](peerMetadataLRUSize) if err != nil { // Only happens if size is < 1. We make sure to not do that, so this should never happen. panic(err) @@ -651,7 +648,7 @@ func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, serve return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } - body := [PeerMetadataLimit]byte{} + body := [peerMetadataLimit]byte{} bytesRead := 0 for { n, err := resp.Body.Read(body[bytesRead:]) @@ -662,7 +659,7 @@ func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, serve if err != nil { return nil, err } - if bytesRead >= PeerMetadataLimit { + if bytesRead >= peerMetadataLimit { return nil, fmt.Errorf("peer metadata too large") } } From f2a2914b17cfc9a42c9df63a769294caab67f0fe Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 10:56:58 -0700 Subject: [PATCH 28/72] HTTPHost has a valid zero-value. Remove constructor and options --- p2p/http/libp2phttp.go | 149 +++++++++++++++++++++--------------- p2p/http/libp2phttp_test.go | 46 +++++++---- p2p/http/options.go | 62 --------------- 3 files changed, 122 insertions(+), 135 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index f1a1b36100..9aedb48afa 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -93,58 +93,59 @@ func (h *WellKnownHandler) RmProtocolMapping(p protocol.ID, path string) { } // HTTPHost is a libp2p host for request/responses with HTTP semantics. This is -// in contrast to a stream-oriented host like the host.Host interface. Warning, -// this is experimental. The API will likely change. +// in contrast to a stream-oriented host like the host.Host interface. Its +// zero-value (&HTTPHost{}) is usable. Do not copy by value. +// See examples for usage. +// +// Warning, this is experimental. The API will likely change. type HTTPHost struct { - rootHandler http.ServeMux - wk WellKnownHandler - httpRoundTripper *http.Transport + // StreamHost is a stream based libp2p host used to do HTTP over libp2p streams. May be nil + StreamHost host.Host + // ListenAddrs are the requested addresses to listen on. Multiaddrs must be a valid HTTP(s) multiaddr. + ListenAddrs []ma.Multiaddr + // TLSConfig is the TLS config for the server to use + TLSConfig *tls.Config + // ServeMux is the http.ServeMux used by the server to serve requests + ServeMux http.ServeMux + + // DefaultClientRoundTripper is the default http.RoundTripper for clients + DefaultClientRoundTripper *http.Transport + + wk WellKnownHandler // peerMetadata is an lru cache of a peer's well-known protocol map. - peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] - streamHost host.Host // may be nil - httpTransport *httpTransport + peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] + // createHttpTransport is used to lazily create the httpTransport in a thread-safe way. + createHttpTransport sync.Once + httpTransport *httpTransport } type httpTransport struct { - requestedListenAddrs []ma.Multiaddr - listenAddrs []ma.Multiaddr - tlsConfig *tls.Config - listeners []net.Listener - closeListeners chan struct{} - waitingForListeners chan struct{} + listenAddrs []ma.Multiaddr + listeners []net.Listener + closeListeners chan struct{} + waitingForListeners chan struct{} } -// New creates a new HTTPHost. Use HTTPHost.Serve to start serving HTTP requests (both over libp2p streams and HTTP transport). -func New(opts ...HTTPHostOption) (*HTTPHost, error) { - httpRoundTripper := http.DefaultTransport.(*http.Transport).Clone() +func newHttpTransport() *httpTransport { + return &httpTransport{ + closeListeners: make(chan struct{}), + waitingForListeners: make(chan struct{}), + } +} + +func newPeerMetadataCache() *lru.Cache[peer.ID, WellKnownProtoMap] { peerMetadata, err := lru.New[peer.ID, WellKnownProtoMap](peerMetadataLRUSize) if err != nil { // Only happens if size is < 1. We make sure to not do that, so this should never happen. panic(err) } - - h := &HTTPHost{ - wk: WellKnownHandler{}, - rootHandler: http.ServeMux{}, - httpRoundTripper: httpRoundTripper, - peerMetadata: peerMetadata, - httpTransport: &httpTransport{ - closeListeners: make(chan struct{}), - waitingForListeners: make(chan struct{}), - }, - } - h.rootHandler.Handle("/.well-known/libp2p", &h.wk) - for _, opt := range opts { - err := opt(h) - if err != nil { - return nil, err - } - } - - return h, nil + return peerMetadata } func (h *HTTPHost) Addrs() []ma.Multiaddr { + h.createHttpTransport.Do(func() { + h.httpTransport = newHttpTransport() + }) <-h.httpTransport.waitingForListeners return h.httpTransport.listenAddrs } @@ -154,6 +155,20 @@ var ErrNoListeners = errors.New("nothing to listen on") // Serve starts the HTTP transport listeners. Always returns a non-nil error. // If there are no listeners, returns ErrNoListeners. func (h *HTTPHost) Serve() error { + // assert that each addr contains a /http component + for _, addr := range h.ListenAddrs { + _, isHTTP := normalizeHTTPMultiaddr(addr) + if !isHTTP { + return fmt.Errorf("address %s does not contain a /http or /https component", addr) + } + } + + h.ServeMux.Handle("/.well-known/libp2p", &h.wk) + + h.createHttpTransport.Do(func() { + h.httpTransport = newHttpTransport() + }) + closedWaitingForListeners := false defer func() { if !closedWaitingForListeners { @@ -161,30 +176,30 @@ func (h *HTTPHost) Serve() error { } }() - if len(h.httpTransport.requestedListenAddrs) == 0 && h.streamHost == nil { + if len(h.ListenAddrs) == 0 && h.StreamHost == nil { return ErrNoListeners } - h.httpTransport.listeners = make([]net.Listener, 0, len(h.httpTransport.requestedListenAddrs)+1) // +1 for stream host + h.httpTransport.listeners = make([]net.Listener, 0, len(h.ListenAddrs)+1) // +1 for stream host streamHostAddrsCount := 0 - if h.streamHost != nil { - streamHostAddrsCount = len(h.streamHost.Addrs()) + if h.StreamHost != nil { + streamHostAddrsCount = len(h.StreamHost.Addrs()) } - h.httpTransport.listenAddrs = make([]ma.Multiaddr, 0, len(h.httpTransport.requestedListenAddrs)+streamHostAddrsCount) + h.httpTransport.listenAddrs = make([]ma.Multiaddr, 0, len(h.ListenAddrs)+streamHostAddrsCount) errCh := make(chan error) - if h.streamHost != nil { - listener, err := StreamHostListen(h.streamHost) + if h.StreamHost != nil { + listener, err := StreamHostListen(h.StreamHost) if err != nil { return err } h.httpTransport.listeners = append(h.httpTransport.listeners, listener) - h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, h.streamHost.Addrs()...) + h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, h.StreamHost.Addrs()...) go func() { - errCh <- http.Serve(listener, &h.rootHandler) + errCh <- http.Serve(listener, &h.ServeMux) }() } @@ -194,7 +209,7 @@ func (h *HTTPHost) Serve() error { } } - for _, addr := range h.httpTransport.requestedListenAddrs { + for _, addr := range h.ListenAddrs { parsedAddr := parseMultiaddr(addr) // resolve the host ipaddr, err := net.ResolveIPAddr("ip", parsedAddr.host) @@ -205,7 +220,6 @@ func (h *HTTPHost) Serve() error { host := ipaddr.String() l, err := net.Listen("tcp", host+":"+parsedAddr.port) - fmt.Println("HTTPHost.Serve", err) if err != nil { closeAllListeners() return err @@ -236,14 +250,14 @@ func (h *HTTPHost) Serve() error { if parsedAddr.useHTTPS { go func() { srv := http.Server{ - Handler: &h.rootHandler, - TLSConfig: h.httpTransport.tlsConfig, + Handler: &h.ServeMux, + TLSConfig: h.TLSConfig, } errCh <- srv.ServeTLS(l, "", "") }() } else { go func() { - errCh <- http.Serve(l, &h.rootHandler) + errCh <- http.Serve(l, &h.ServeMux) }() } } @@ -270,14 +284,18 @@ func (h *HTTPHost) Serve() error { } func (h *HTTPHost) Close() error { + h.createHttpTransport.Do(func() { + h.httpTransport = newHttpTransport() + }) close(h.httpTransport.closeListeners) return nil } // SetHttpHandler sets the HTTP handler for a given protocol. Automatically // manages the .well-known/libp2p mapping. +// TODO should this strip the prefix? I think so func (h *HTTPHost) SetHttpHandler(p protocol.ID, handler http.Handler) { - h.SetHttpHandlerAtPath(p, string(p)+"/", handler) + h.SetHttpHandlerAtPath(p, string(p), handler) } // SetHttpHandlerAtPath sets the HTTP handler for a given protocol using the @@ -288,7 +306,7 @@ func (h *HTTPHost) SetHttpHandlerAtPath(p protocol.ID, path string, handler http path += "/" } h.wk.AddProtocolMapping(p, path) - h.rootHandler.Handle(path, handler) + h.ServeMux.Handle(path, handler) } // getPeerProtoMap lets RoundTrippers implement a specific way of caching a peer's protocol mapping. @@ -509,8 +527,8 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt // Do we have an existing connection to this peer? existingStreamConn := false - if server.ID != "" && h.streamHost != nil { - existingStreamConn = len(h.streamHost.Network().ConnsToPeer(server.ID)) > 0 + if server.ID != "" && h.StreamHost != nil { + existingStreamConn = len(h.StreamHost.Network().ConnsToPeer(server.ID)) > 0 } // Currently the HTTP transport can not authenticate peer IDs. @@ -521,13 +539,15 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt scheme = "https" } - rt := h.httpRoundTripper + rt := h.DefaultClientRoundTripper + if rt == nil { + rt = http.DefaultTransport.(*http.Transport) + } ownRoundtripper := false if parsed.sni != parsed.host { // We have a different host and SNI (e.g. using an IP address but specifying a SNI) // We need to make our own transport to support this. rt = rt.Clone() - rt.TLSClientConfig = h.httpRoundTripper.TLSClientConfig.Clone() rt.TLSClientConfig.ServerName = parsed.sni ownRoundtripper = true } @@ -544,20 +564,20 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt } // Otherwise use a stream based transport - if h.streamHost == nil { + if h.StreamHost == nil { return nil, fmt.Errorf("can not use the HTTP transport (either no address or PeerID auth is required), and no stream host provided") } if !existingStreamConn { if server.ID == "" { return nil, fmt.Errorf("can not use the HTTP transport, and no server peer ID provided") } - err := h.streamHost.Connect(context.Background(), peer.AddrInfo{ID: server.ID, Addrs: nonHttpAddrs}) + err := h.StreamHost.Connect(context.Background(), peer.AddrInfo{ID: server.ID, Addrs: nonHttpAddrs}) if err != nil { return nil, fmt.Errorf("failed to connect to peer: %w", err) } } - return NewStreamRoundTripper(h.streamHost, server.ID), nil + return NewStreamRoundTripper(h.StreamHost, server.ID), nil } type httpMultiaddr struct { @@ -627,6 +647,9 @@ func normalizeHTTPMultiaddr(addr ma.Multiaddr) (ma.Multiaddr, bool) { // returns it. Will only store the peer's protocol mapping if the server ID is // provided. func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, server peer.ID) (WellKnownProtoMap, error) { + if h.peerMetadata == nil { + h.peerMetadata = newPeerMetadataCache() + } if meta, ok := h.peerMetadata.Get(server); server != "" && ok { return meta, nil } @@ -676,10 +699,16 @@ func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, serve // AddPeerMetadata adds a peer's protocol metadata to the http host. Useful if // you have out-of-band knowledge of a peer's protocol mapping. func (h *HTTPHost) AddPeerMetadata(server peer.ID, meta WellKnownProtoMap) { + if h.peerMetadata == nil { + h.peerMetadata = newPeerMetadataCache() + } h.peerMetadata.Add(server, meta) } // RmPeerMetadata removes a peer's protocol metadata from the http host func (h *HTTPHost) RmPeerMetadata(server peer.ID, meta WellKnownProtoMap) { + if h.peerMetadata == nil { + return + } h.peerMetadata.Remove(server) } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 68ed2d7230..1e2d3dbe3c 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -27,8 +27,7 @@ func TestHTTPOverStreams(t *testing.T) { ) require.NoError(t, err) - httpHost, err := libp2phttp.New(libp2phttp.StreamHost(serverHost)) - require.NoError(t, err) + httpHost := libp2phttp.HTTPHost{StreamHost: serverHost} httpHost.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) @@ -70,10 +69,10 @@ func TestRoundTrippers(t *testing.T) { require.NoError(t, err) defer streamListener.Close() - httpHost, err := libp2phttp.New( - libp2phttp.StreamHost(serverHost), - libp2phttp.ListenAddrs([]ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")})) - require.NoError(t, err) + httpHost := libp2phttp.HTTPHost{ + StreamHost: serverHost, + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, + } httpHost.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) @@ -162,8 +161,7 @@ func TestRoundTrippers(t *testing.T) { require.NoError(t, err) defer clientStreamHost.Close() - clientHttpHost, err := libp2phttp.New(libp2phttp.StreamHost(clientStreamHost)) - require.NoError(t, err) + clientHttpHost := &libp2phttp.HTTPHost{StreamHost: clientStreamHost} rt := tc.setupRoundTripper(t, clientStreamHost, clientHttpHost) if tc.expectStreamRoundTripper { @@ -181,7 +179,7 @@ func TestRoundTrippers(t *testing.T) { var resp *http.Response var err error if tc { - h, err := libp2phttp.New() + var h libp2phttp.HTTPHost require.NoError(t, err) nrt, err := h.NamespaceRoundTripper(rt, "/hello", serverHost.ID()) require.NoError(t, err) @@ -240,8 +238,7 @@ func TestPlainOldHTTPServer(t *testing.T) { { name: "using libp2phttp", do: func(t *testing.T, request *http.Request) (*http.Response, error) { - clientHttpHost, err := libp2phttp.New() - require.NoError(t, err) + var clientHttpHost libp2phttp.HTTPHost rt, err := clientHttpHost.NewRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) @@ -249,8 +246,7 @@ func TestPlainOldHTTPServer(t *testing.T) { return client.Do(request) }, getWellKnown: func(t *testing.T) (libp2phttp.WellKnownProtoMap, error) { - clientHttpHost, err := libp2phttp.New() - require.NoError(t, err) + var clientHttpHost libp2phttp.HTTPHost rt, err := clientHttpHost.NewRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) return clientHttpHost.GetAndStorePeerProtoMap(rt, "") @@ -306,3 +302,27 @@ func TestPlainOldHTTPServer(t *testing.T) { }) } } + +func TestHTTPHostNoConstructor(t *testing.T) { + server := libp2phttp.HTTPHost{ + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, + } + server.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) })) + go func() { + server.Serve() + }() + defer server.Close() + + c := libp2phttp.HTTPHost{} + client, err := c.NamespacedClient("/hello", peer.AddrInfo{Addrs: server.Addrs()}) + require.NoError(t, err) + resp, err := client.Get("/") + require.NoError(t, err) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Equal(t, "hello", string(body), "expected response from server") +} + +// TODO tls test diff --git a/p2p/http/options.go b/p2p/http/options.go index fea8c9fb6e..cb1b2c80e6 100644 --- a/p2p/http/options.go +++ b/p2p/http/options.go @@ -1,67 +1,5 @@ package libp2phttp -import ( - "crypto/tls" - "fmt" - "net/http" - - "github.com/libp2p/go-libp2p/core/host" - ma "github.com/multiformats/go-multiaddr" -) - -type HTTPHostOption func(*HTTPHost) error - -// ListenAddrs sets the listen addresses for the HTTP transport. -func ListenAddrs(addrs []ma.Multiaddr) HTTPHostOption { - return func(h *HTTPHost) error { - // assert that each addr contains a /http component - for _, addr := range addrs { - if _, err := addr.ValueForProtocol(ma.P_HTTP); err != nil { - return fmt.Errorf("address %s does not contain a /http component", addr) - } - } - - h.httpTransport.requestedListenAddrs = addrs - - return nil - } -} - -// TLSConfig sets the server TLS config for the HTTP transport. -func TLSConfig(tlsConfig *tls.Config) HTTPHostOption { - return func(h *HTTPHost) error { - h.httpTransport.tlsConfig = tlsConfig - return nil - } -} - -// StreamHost sets the stream host to use for HTTP over libp2p streams. -func StreamHost(streamHost host.Host) HTTPHostOption { - return func(h *HTTPHost) error { - h.streamHost = streamHost - return nil - } -} - -// WithTLSClientConfig sets the TLS client config for the native HTTP transport. -func WithTLSClientConfig(tlsConfig *tls.Config) HTTPHostOption { - return func(h *HTTPHost) error { - h.httpRoundTripper.TLSClientConfig = tlsConfig - return nil - } -} - -// CustomRootHandler sets the root handler for the HTTP transport. It *does not* -// set up a .well-known/libp2p handler. Users assume the responsibility of -// handling .well-known/libp2p, as well as keeping it up to date. -func CustomRootHandler(mux http.Handler) HTTPHostOption { - return func(h *HTTPHost) error { - h.rootHandler = http.ServeMux{} - h.rootHandler.Handle("/", mux) - return nil - } -} - type RoundTripperOptsFn func(o roundTripperOpts) roundTripperOpts type roundTripperOpts struct { From 8156bbb80a624e7b815f3824f4f417beeab1e44a Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 11:01:55 -0700 Subject: [PATCH 29/72] Add https test --- p2p/http/libp2phttp_test.go | 69 +++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 1e2d3dbe3c..7a23359546 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -3,14 +3,21 @@ package libp2phttp_test import ( "bytes" "context" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "encoding/json" "io" + "math/big" "net" "net/http" "reflect" "strings" "testing" + "time" "github.com/libp2p/go-libp2p" host "github.com/libp2p/go-libp2p/core/host" @@ -303,7 +310,7 @@ func TestPlainOldHTTPServer(t *testing.T) { } } -func TestHTTPHostNoConstructor(t *testing.T) { +func TestHTTPHostZeroValue(t *testing.T) { server := libp2phttp.HTTPHost{ ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } @@ -325,4 +332,62 @@ func TestHTTPHostNoConstructor(t *testing.T) { require.Equal(t, "hello", string(body), "expected response from server") } -// TODO tls test +func TestHTTPS(t *testing.T) { + server := libp2phttp.HTTPHost{ + TLSConfig: selfSignedTLSConfig(t), + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, + } + server.SetHttpHandler(httpping.PingProtocolID, httpping.Ping{}) + go func() { + server.Serve() + }() + defer server.Close() + + clientTransport := http.DefaultTransport.(*http.Transport).Clone() + clientTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + client := libp2phttp.HTTPHost{ + DefaultClientRoundTripper: clientTransport, + } + httpClient, err := client.NamespacedClient(httpping.PingProtocolID, peer.AddrInfo{Addrs: server.Addrs()}) + require.NoError(t, err) + err = httpping.SendPing(httpClient) + require.NoError(t, err) +} + +func selfSignedTLSConfig(t *testing.T) *tls.Config { + t.Helper() + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + notBefore := time.Now() + notAfter := notBefore.Add(365 * 24 * time.Hour) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + require.NoError(t, err) + + certTemplate := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Test"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &priv.PublicKey, priv) + require.NoError(t, err) + + cert := tls.Certificate{ + Certificate: [][]byte{derBytes}, + PrivateKey: priv, + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + return tlsConfig +} From 5e2e19eed28a30b5ca6c1e9b1138a782bd6d806a Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 11:04:10 -0700 Subject: [PATCH 30/72] Rename to following naming convention --- p2p/http/libp2phttp.go | 38 ++++++++++++++++++------------------- p2p/http/libp2phttp_test.go | 8 ++++---- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 9aedb48afa..4db5a97181 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -114,8 +114,8 @@ type HTTPHost struct { wk WellKnownHandler // peerMetadata is an lru cache of a peer's well-known protocol map. peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] - // createHttpTransport is used to lazily create the httpTransport in a thread-safe way. - createHttpTransport sync.Once + // createHTTPTransport is used to lazily create the httpTransport in a thread-safe way. + createHTTPTransport sync.Once httpTransport *httpTransport } @@ -126,7 +126,7 @@ type httpTransport struct { waitingForListeners chan struct{} } -func newHttpTransport() *httpTransport { +func newHTTPTransport() *httpTransport { return &httpTransport{ closeListeners: make(chan struct{}), waitingForListeners: make(chan struct{}), @@ -143,8 +143,8 @@ func newPeerMetadataCache() *lru.Cache[peer.ID, WellKnownProtoMap] { } func (h *HTTPHost) Addrs() []ma.Multiaddr { - h.createHttpTransport.Do(func() { - h.httpTransport = newHttpTransport() + h.createHTTPTransport.Do(func() { + h.httpTransport = newHTTPTransport() }) <-h.httpTransport.waitingForListeners return h.httpTransport.listenAddrs @@ -165,8 +165,8 @@ func (h *HTTPHost) Serve() error { h.ServeMux.Handle("/.well-known/libp2p", &h.wk) - h.createHttpTransport.Do(func() { - h.httpTransport = newHttpTransport() + h.createHTTPTransport.Do(func() { + h.httpTransport = newHTTPTransport() }) closedWaitingForListeners := false @@ -284,23 +284,23 @@ func (h *HTTPHost) Serve() error { } func (h *HTTPHost) Close() error { - h.createHttpTransport.Do(func() { - h.httpTransport = newHttpTransport() + h.createHTTPTransport.Do(func() { + h.httpTransport = newHTTPTransport() }) close(h.httpTransport.closeListeners) return nil } -// SetHttpHandler sets the HTTP handler for a given protocol. Automatically +// SetHTTPHandler sets the HTTP handler for a given protocol. Automatically // manages the .well-known/libp2p mapping. // TODO should this strip the prefix? I think so -func (h *HTTPHost) SetHttpHandler(p protocol.ID, handler http.Handler) { - h.SetHttpHandlerAtPath(p, string(p), handler) +func (h *HTTPHost) SetHTTPHandler(p protocol.ID, handler http.Handler) { + h.SetHTTPHandlerAtPath(p, string(p), handler) } -// SetHttpHandlerAtPath sets the HTTP handler for a given protocol using the +// SetHTTPHandlerAtPath sets the HTTP handler for a given protocol using the // given path. Automatically manages the .well-known/libp2p mapping. -func (h *HTTPHost) SetHttpHandlerAtPath(p protocol.ID, path string, handler http.Handler) { +func (h *HTTPHost) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http.Handler) { if path[len(path)-1] != '/' { // We are nesting this handler under this path, so it should end with a slash. path += "/" @@ -509,19 +509,19 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt } httpAddrs := make([]ma.Multiaddr, 0, 1) // The common case of a single http address - nonHttpAddrs := make([]ma.Multiaddr, 0, len(server.Addrs)) + nonHTTPAddrs := make([]ma.Multiaddr, 0, len(server.Addrs)) firstAddrIsHTTP := false for i, addr := range server.Addrs { - addr, isHttp := normalizeHTTPMultiaddr(addr) - if isHttp { + addr, isHTTP := normalizeHTTPMultiaddr(addr) + if isHTTP { if i == 0 { firstAddrIsHTTP = true } httpAddrs = append(httpAddrs, addr) } else { - nonHttpAddrs = append(nonHttpAddrs, addr) + nonHTTPAddrs = append(nonHTTPAddrs, addr) } } @@ -571,7 +571,7 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt if server.ID == "" { return nil, fmt.Errorf("can not use the HTTP transport, and no server peer ID provided") } - err := h.StreamHost.Connect(context.Background(), peer.AddrInfo{ID: server.ID, Addrs: nonHttpAddrs}) + err := h.StreamHost.Connect(context.Background(), peer.AddrInfo{ID: server.ID, Addrs: nonHTTPAddrs}) if err != nil { return nil, fmt.Errorf("failed to connect to peer: %w", err) } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 7a23359546..a49f7ecaac 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -36,7 +36,7 @@ func TestHTTPOverStreams(t *testing.T) { httpHost := libp2phttp.HTTPHost{StreamHost: serverHost} - httpHost.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpHost.SetHTTPHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) })) @@ -81,7 +81,7 @@ func TestRoundTrippers(t *testing.T) { ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } - httpHost.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpHost.SetHTTPHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) })) @@ -314,7 +314,7 @@ func TestHTTPHostZeroValue(t *testing.T) { server := libp2phttp.HTTPHost{ ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } - server.SetHttpHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) })) + server.SetHTTPHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) })) go func() { server.Serve() }() @@ -337,7 +337,7 @@ func TestHTTPS(t *testing.T) { TLSConfig: selfSignedTLSConfig(t), ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } - server.SetHttpHandler(httpping.PingProtocolID, httpping.Ping{}) + server.SetHTTPHandler(httpping.PingProtocolID, httpping.Ping{}) go func() { server.Serve() }() From 5b5db8898088841982fbbef2261d2c51289e1c27 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 11:27:57 -0700 Subject: [PATCH 31/72] Add flag for insecure http --- p2p/http/libp2phttp.go | 26 +++++++++++++++++++++----- p2p/http/libp2phttp_test.go | 10 ++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 4db5a97181..e573a15626 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -18,6 +18,7 @@ import ( "sync" lru "github.com/hashicorp/golang-lru/v2" + logging "github.com/ipfs/go-log/v2" host "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" @@ -26,6 +27,8 @@ import ( ma "github.com/multiformats/go-multiaddr" ) +var log = logging.Logger("libp2phttp") + const ProtocolIDForMultistreamSelect = "/http/1.1" const peerMetadataLimit = 8 << 10 // 8KB const peerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache @@ -105,6 +108,8 @@ type HTTPHost struct { ListenAddrs []ma.Multiaddr // TLSConfig is the TLS config for the server to use TLSConfig *tls.Config + // ServeInsecureHTTP indicates if the server should serve unencrypted HTTP requests over TCP. + ServeInsecureHTTP bool // ServeMux is the http.ServeMux used by the server to serve requests ServeMux http.ServeMux @@ -245,8 +250,6 @@ func (h *HTTPHost) Serve() error { } - h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) - if parsedAddr.useHTTPS { go func() { srv := http.Server{ @@ -255,16 +258,26 @@ func (h *HTTPHost) Serve() error { } errCh <- srv.ServeTLS(l, "", "") }() - } else { + h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) + } else if h.ServeInsecureHTTP { go func() { errCh <- http.Serve(l, &h.ServeMux) }() + h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) + } else { + // We are not serving insecure HTTP + log.Warnf("Not serving insecure HTTP on %s. Prefer an HTTPS endpoint.", listenAddr) } } close(h.httpTransport.waitingForListeners) closedWaitingForListeners = true + if len(h.httpTransport.listeners) == 0 || len(h.httpTransport.listenAddrs) == 0 { + closeAllListeners() + return ErrNoListeners + } + expectedErrCount := len(h.httpTransport.listeners) var err error select { @@ -595,7 +608,7 @@ func parseMultiaddr(addr ma.Multiaddr) httpMultiaddr { out.host = c.Value() case ma.P_TCP, ma.P_UDP: out.port = c.Value() - case ma.P_TLS: + case ma.P_TLS, ma.P_HTTPS: out.useHTTPS = true case ma.P_SNI: out.sni = c.Value() @@ -615,7 +628,7 @@ func NewStreamRoundTripper(streamHost host.Host, server peer.ID) http.RoundTripp } var httpComponent, _ = ma.NewComponent("http", "") -var tlsComponent, _ = ma.NewComponent("http", "") +var tlsComponent, _ = ma.NewComponent("tls", "") // normalizeHTTPMultiaddr converts an https multiaddr to a tls/http one. // Returns a bool indicating if the input multiaddr has an http (or https) component. @@ -639,6 +652,9 @@ func normalizeHTTPMultiaddr(addr ma.Multiaddr) (ma.Multiaddr, bool) { } _, afterHTTPS := ma.SplitFirst(afterIncludingHTTPS) + if afterHTTPS == nil { + return ma.Join(beforeHTTPS, tlsComponent, httpComponent), isHTTPMultiaddr + } return ma.Join(beforeHTTPS, tlsComponent, httpComponent, afterHTTPS), isHTTPMultiaddr } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index a49f7ecaac..0d9e922094 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -77,8 +77,9 @@ func TestRoundTrippers(t *testing.T) { defer streamListener.Close() httpHost := libp2phttp.HTTPHost{ - StreamHost: serverHost, - ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, + ServeInsecureHTTP: true, + StreamHost: serverHost, + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } httpHost.SetHTTPHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -312,7 +313,8 @@ func TestPlainOldHTTPServer(t *testing.T) { func TestHTTPHostZeroValue(t *testing.T) { server := libp2phttp.HTTPHost{ - ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, + ServeInsecureHTTP: true, + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } server.SetHTTPHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) })) go func() { @@ -335,7 +337,7 @@ func TestHTTPHostZeroValue(t *testing.T) { func TestHTTPS(t *testing.T) { server := libp2phttp.HTTPHost{ TLSConfig: selfSignedTLSConfig(t), - ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/https")}, } server.SetHTTPHandler(httpping.PingProtocolID, httpping.Ping{}) go func() { From 58577579295cc059c54ad0e8e2d04a1d165e0b75 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 11:33:28 -0700 Subject: [PATCH 32/72] Return after error --- p2p/http/libp2phttp.go | 2 ++ p2p/http/ping/ping.go | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index e573a15626..9a8aa8bafd 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -58,10 +58,12 @@ func (h *WellKnownHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { accepts := r.Header.Get("Accept") if accepts != "" && !(strings.Contains(accepts, "application/json") || strings.Contains(accepts, "*/*")) { http.Error(w, "Only application/json is supported", http.StatusNotAcceptable) + return } if r.Method != "GET" { http.Error(w, "Only GET requests are supported", http.StatusMethodNotAllowed) + return } // Return a JSON object with the well-known protocols diff --git a/p2p/http/ping/ping.go b/p2p/http/ping/ping.go index 77e08b8a07..c911421de1 100644 --- a/p2p/http/ping/ping.go +++ b/p2p/http/ping/ping.go @@ -28,7 +28,6 @@ func (Ping) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Length", strconv.Itoa(pingSize)) - w.WriteHeader(http.StatusOK) w.Write(body[:]) } From eee5836bab30d3df520b6d218a619f46a660b541 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 11:33:37 -0700 Subject: [PATCH 33/72] Rename Rm to Remove --- p2p/http/libp2phttp.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 9a8aa8bafd..1375dc44a3 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -71,25 +71,24 @@ func (h *WellKnownHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { mapping, err := json.Marshal(h.wellKnownMapping) h.wellknownMapMu.Unlock() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, "Marshal error", http.StatusInternalServerError) return } w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Length", strconv.Itoa(len(mapping))) - w.WriteHeader(http.StatusOK) w.Write(mapping) } -func (h *WellKnownHandler) AddProtocolMapping(p protocol.ID, path string) { +func (h *WellKnownHandler) AddProtocolMapping(p protocol.ID, protocolMeta WellKnownProtocolMeta) { h.wellknownMapMu.Lock() if h.wellKnownMapping == nil { h.wellKnownMapping = make(map[protocol.ID]WellKnownProtocolMeta) } - h.wellKnownMapping[p] = WellKnownProtocolMeta{Path: path} + h.wellKnownMapping[p] = protocolMeta h.wellknownMapMu.Unlock() } -func (h *WellKnownHandler) RmProtocolMapping(p protocol.ID, path string) { +func (h *WellKnownHandler) RemoveProtocolMapping(p protocol.ID) { h.wellknownMapMu.Lock() if h.wellKnownMapping != nil { delete(h.wellKnownMapping, p) @@ -723,8 +722,8 @@ func (h *HTTPHost) AddPeerMetadata(server peer.ID, meta WellKnownProtoMap) { h.peerMetadata.Add(server, meta) } -// RmPeerMetadata removes a peer's protocol metadata from the http host -func (h *HTTPHost) RmPeerMetadata(server peer.ID, meta WellKnownProtoMap) { +// RemovePeerMetadata removes a peer's protocol metadata from the http host +func (h *HTTPHost) RemovePeerMetadata(server peer.ID, meta WellKnownProtoMap) { if h.peerMetadata == nil { return } From a6cadd98bf3aca8d36981ade6a6fa3605bfe08a1 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 11:34:06 -0700 Subject: [PATCH 34/72] Rename --- p2p/http/libp2phttp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 1375dc44a3..498e5b6c7a 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -61,7 +61,7 @@ func (h *WellKnownHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if r.Method != "GET" { + if r.Method != http.MethodGet { http.Error(w, "Only GET requests are supported", http.StatusMethodNotAllowed) return } From 1ce1915136cfae822c5f1a22024f374a3c33754e Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 12:44:59 -0700 Subject: [PATCH 35/72] Refactor to always call closeAllListeners --- p2p/http/libp2phttp.go | 101 +++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 498e5b6c7a..b75dfcae15 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -215,60 +215,64 @@ func (h *HTTPHost) Serve() error { } } - for _, addr := range h.ListenAddrs { - parsedAddr := parseMultiaddr(addr) - // resolve the host - ipaddr, err := net.ResolveIPAddr("ip", parsedAddr.host) - if err != nil { - closeAllListeners() - return err - } - - host := ipaddr.String() - l, err := net.Listen("tcp", host+":"+parsedAddr.port) - if err != nil { - closeAllListeners() - return err - } - h.httpTransport.listeners = append(h.httpTransport.listeners, l) - - // get resolved port - _, port, err := net.SplitHostPort(l.Addr().String()) - if err != nil { - closeAllListeners() - return err - } + err := func() error { + for _, addr := range h.ListenAddrs { + parsedAddr := parseMultiaddr(addr) + // resolve the host + ipaddr, err := net.ResolveIPAddr("ip", parsedAddr.host) + if err != nil { + return err + } - var listenAddr ma.Multiaddr - if parsedAddr.useHTTPS && parsedAddr.sni != "" && parsedAddr.sni != host { - listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/tls/sni/%s/http", host, port, parsedAddr.sni)) - } else { - scheme := "http" - if parsedAddr.useHTTPS { - scheme = "https" + host := ipaddr.String() + l, err := net.Listen("tcp", host+":"+parsedAddr.port) + if err != nil { + return err } - listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/%s", host, port, scheme)) + h.httpTransport.listeners = append(h.httpTransport.listeners, l) - } + // get resolved port + _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + return err + } - if parsedAddr.useHTTPS { - go func() { - srv := http.Server{ - Handler: &h.ServeMux, - TLSConfig: h.TLSConfig, + var listenAddr ma.Multiaddr + if parsedAddr.useHTTPS && parsedAddr.sni != "" && parsedAddr.sni != host { + listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/tls/sni/%s/http", host, port, parsedAddr.sni)) + } else { + scheme := "http" + if parsedAddr.useHTTPS { + scheme = "https" } - errCh <- srv.ServeTLS(l, "", "") - }() - h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) - } else if h.ServeInsecureHTTP { - go func() { - errCh <- http.Serve(l, &h.ServeMux) - }() - h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) - } else { - // We are not serving insecure HTTP - log.Warnf("Not serving insecure HTTP on %s. Prefer an HTTPS endpoint.", listenAddr) + listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/%s", host, port, scheme)) + + } + + if parsedAddr.useHTTPS { + go func() { + srv := http.Server{ + Handler: &h.ServeMux, + TLSConfig: h.TLSConfig, + } + errCh <- srv.ServeTLS(l, "", "") + }() + h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) + } else if h.ServeInsecureHTTP { + go func() { + errCh <- http.Serve(l, &h.ServeMux) + }() + h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) + } else { + // We are not serving insecure HTTP + log.Warnf("Not serving insecure HTTP on %s. Prefer an HTTPS endpoint.", listenAddr) + } } + return nil + }() + if err != nil { + closeAllListeners() + return err } close(h.httpTransport.waitingForListeners) @@ -280,7 +284,6 @@ func (h *HTTPHost) Serve() error { } expectedErrCount := len(h.httpTransport.listeners) - var err error select { case <-h.httpTransport.closeListeners: case err = <-errCh: From 37c79a6edf40140d7540e32cab6bd95fcc7cbc14 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 12:45:04 -0700 Subject: [PATCH 36/72] Rename --- p2p/http/libp2phttp_test.go | 2 +- p2p/http/options.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 0d9e922094..b2452d36ec 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -104,7 +104,7 @@ func TestRoundTrippers(t *testing.T) { rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: serverMultiaddrs, - }, libp2phttp.RoundTripperPreferHTTPTransport) + }, libp2phttp.PreferHTTPTransport) require.NoError(t, err) return rt }, diff --git a/p2p/http/options.go b/p2p/http/options.go index cb1b2c80e6..3bf0356f81 100644 --- a/p2p/http/options.go +++ b/p2p/http/options.go @@ -8,7 +8,7 @@ type roundTripperOpts struct { ServerMustAuthenticatePeerID bool } -func RoundTripperPreferHTTPTransport(o roundTripperOpts) roundTripperOpts { +func PreferHTTPTransport(o roundTripperOpts) roundTripperOpts { o.preferHTTPTransport = true return o } From e05ea11a7ba7623277e3d94af4cc3d41870b5ee5 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 12:50:42 -0700 Subject: [PATCH 37/72] Rename --- p2p/http/libp2phttp.go | 46 ++++++++++++++++++++++--------------- p2p/http/libp2phttp_test.go | 18 +++++++-------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index b75dfcae15..b231b0b168 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -36,16 +36,18 @@ const peerMetadataLRUSize = 256 // How many different peer's metadata to keep // TODOs: // - integrate with the conn gater and resource manager -type WellKnownProtocolMeta struct { +// ProtocolMeta is metadata about a protocol. +type ProtocolMeta struct { + // Path defines the HTTP Path prefix used for this protocol Path string `json:"path"` } -type WellKnownProtoMap map[protocol.ID]WellKnownProtocolMeta +type ProtocolMetaMap map[protocol.ID]ProtocolMeta // WellKnownHandler is an http.Handler that serves the .well-known/libp2p resource type WellKnownHandler struct { wellknownMapMu sync.Mutex - wellKnownMapping WellKnownProtoMap + wellKnownMapping ProtocolMetaMap } // StreamHostListen retuns a net.Listener that listens on libp2p streams for HTTP/1.1 messages. @@ -79,10 +81,10 @@ func (h *WellKnownHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write(mapping) } -func (h *WellKnownHandler) AddProtocolMapping(p protocol.ID, protocolMeta WellKnownProtocolMeta) { +func (h *WellKnownHandler) AddProtocolMapping(p protocol.ID, protocolMeta ProtocolMeta) { h.wellknownMapMu.Lock() if h.wellKnownMapping == nil { - h.wellKnownMapping = make(map[protocol.ID]WellKnownProtocolMeta) + h.wellKnownMapping = make(map[protocol.ID]ProtocolMeta) } h.wellKnownMapping[p] = protocolMeta h.wellknownMapMu.Unlock() @@ -119,7 +121,7 @@ type HTTPHost struct { wk WellKnownHandler // peerMetadata is an lru cache of a peer's well-known protocol map. - peerMetadata *lru.Cache[peer.ID, WellKnownProtoMap] + peerMetadata *lru.Cache[peer.ID, ProtocolMetaMap] // createHTTPTransport is used to lazily create the httpTransport in a thread-safe way. createHTTPTransport sync.Once httpTransport *httpTransport @@ -139,8 +141,8 @@ func newHTTPTransport() *httpTransport { } } -func newPeerMetadataCache() *lru.Cache[peer.ID, WellKnownProtoMap] { - peerMetadata, err := lru.New[peer.ID, WellKnownProtoMap](peerMetadataLRUSize) +func newPeerMetadataCache() *lru.Cache[peer.ID, ProtocolMetaMap] { + peerMetadata, err := lru.New[peer.ID, ProtocolMetaMap](peerMetadataLRUSize) if err != nil { // Only happens if size is < 1. We make sure to not do that, so this should never happen. panic(err) @@ -322,13 +324,13 @@ func (h *HTTPHost) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http // We are nesting this handler under this path, so it should end with a slash. path += "/" } - h.wk.AddProtocolMapping(p, path) + h.wk.AddProtocolMapping(p, ProtocolMeta{Path: path}) h.ServeMux.Handle(path, handler) } // getPeerProtoMap lets RoundTrippers implement a specific way of caching a peer's protocol mapping. type getPeerProtoMap interface { - GetPeerProtoMap() (WellKnownProtoMap, error) + GetPeerProtoMap() (ProtocolMetaMap, error) } type streamRoundTripper struct { @@ -347,7 +349,7 @@ func (s *streamReadCloser) Close() error { return s.ReadCloser.Close() } -func (rt *streamRoundTripper) GetPeerProtoMap(server peer.ID) (WellKnownProtoMap, error) { +func (rt *streamRoundTripper) GetPeerProtoMap(server peer.ID) (ProtocolMetaMap, error) { return rt.httpHost.GetAndStorePeerProtoMap(rt, rt.server) } @@ -385,10 +387,10 @@ type roundTripperForSpecificServer struct { targetServerAddr string sni string scheme string - cachedProtos WellKnownProtoMap + cachedProtos ProtocolMetaMap } -func (rt *roundTripperForSpecificServer) GetPeerProtoMap() (WellKnownProtoMap, error) { +func (rt *roundTripperForSpecificServer) GetPeerProtoMap() (ProtocolMetaMap, error) { // Do we already have the peer's protocol mapping? if rt.cachedProtos != nil { return rt.cachedProtos, nil @@ -444,7 +446,7 @@ type namespacedRoundTripper struct { protocolPrefixRaw string } -func (rt *namespacedRoundTripper) GetPeerProtoMap() (WellKnownProtoMap, error) { +func (rt *namespacedRoundTripper) GetPeerProtoMap() (ProtocolMetaMap, error) { if g, ok := rt.RoundTripper.(getPeerProtoMap); ok { return g.GetPeerProtoMap() } @@ -666,7 +668,7 @@ func normalizeHTTPMultiaddr(addr ma.Multiaddr) (ma.Multiaddr, bool) { // ProtocolPathPrefix 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 *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, server peer.ID) (WellKnownProtoMap, error) { +func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, server peer.ID) (ProtocolMetaMap, error) { if h.peerMetadata == nil { h.peerMetadata = newPeerMetadataCache() } @@ -707,7 +709,7 @@ func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, serve } } - meta := WellKnownProtoMap{} + meta := ProtocolMetaMap{} json.Unmarshal(body[:bytesRead], &meta) if server != "" { h.peerMetadata.Add(server, meta) @@ -718,15 +720,23 @@ func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, serve // AddPeerMetadata adds a peer's protocol metadata to the http host. Useful if // you have out-of-band knowledge of a peer's protocol mapping. -func (h *HTTPHost) AddPeerMetadata(server peer.ID, meta WellKnownProtoMap) { +func (h *HTTPHost) AddPeerMetadata(server peer.ID, meta ProtocolMetaMap) { if h.peerMetadata == nil { h.peerMetadata = newPeerMetadataCache() } h.peerMetadata.Add(server, meta) } +// GetPeerMetadata gets a peer's cached protocol metadata from the http host. +func (h *HTTPHost) GetPeerMetadata(server peer.ID) (ProtocolMetaMap, bool) { + if h.peerMetadata == nil { + return nil, false + } + return h.peerMetadata.Get(server) +} + // RemovePeerMetadata removes a peer's protocol metadata from the http host -func (h *HTTPHost) RemovePeerMetadata(server peer.ID, meta WellKnownProtoMap) { +func (h *HTTPHost) RemovePeerMetadata(server peer.ID, meta ProtocolMetaMap) { if h.peerMetadata == nil { return } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index b2452d36ec..0fb096b9df 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -211,8 +211,8 @@ func TestRoundTrippers(t *testing.T) { wk, err := clientHttpHost.GetAndStorePeerProtoMap(rt, serverHost.ID()) require.NoError(t, err) - expectedMap := make(libp2phttp.WellKnownProtoMap) - expectedMap["/hello"] = libp2phttp.WellKnownProtocolMeta{Path: "/hello/"} + expectedMap := make(libp2phttp.ProtocolMetaMap) + expectedMap["/hello"] = libp2phttp.ProtocolMeta{Path: "/hello/"} require.Equal(t, expectedMap, wk) }) } @@ -224,7 +224,7 @@ func TestPlainOldHTTPServer(t *testing.T) { mux.Handle("/.well-known/libp2p", &wk) mux.Handle("/ping/", httpping.Ping{}) - wk.AddProtocolMapping(httpping.PingProtocolID, "/ping/") + wk.AddProtocolMapping(httpping.PingProtocolID, libp2phttp.ProtocolMeta{Path: "/ping/"}) server := &http.Server{Addr: "127.0.0.1:0", Handler: mux} @@ -241,7 +241,7 @@ func TestPlainOldHTTPServer(t *testing.T) { testCases := []struct { name string do func(*testing.T, *http.Request) (*http.Response, error) - getWellKnown func(*testing.T) (libp2phttp.WellKnownProtoMap, error) + getWellKnown func(*testing.T) (libp2phttp.ProtocolMetaMap, error) }{ { name: "using libp2phttp", @@ -253,7 +253,7 @@ func TestPlainOldHTTPServer(t *testing.T) { client := &http.Client{Transport: rt} return client.Do(request) }, - getWellKnown: func(t *testing.T) (libp2phttp.WellKnownProtoMap, error) { + getWellKnown: func(t *testing.T) (libp2phttp.ProtocolMetaMap, error) { var clientHttpHost libp2phttp.HTTPHost rt, err := clientHttpHost.NewRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) @@ -270,7 +270,7 @@ func TestPlainOldHTTPServer(t *testing.T) { client := http.Client{} return client.Do(request) }, - getWellKnown: func(t *testing.T) (libp2phttp.WellKnownProtoMap, error) { + getWellKnown: func(t *testing.T) (libp2phttp.ProtocolMetaMap, error) { client := http.Client{} resp, err := client.Get("http://" + l.Addr().String() + "/.well-known/libp2p") require.NoError(t, err) @@ -278,7 +278,7 @@ func TestPlainOldHTTPServer(t *testing.T) { b, err := io.ReadAll(resp.Body) require.NoError(t, err) - var out libp2phttp.WellKnownProtoMap + var out libp2phttp.ProtocolMetaMap err = json.Unmarshal(b, &out) return out, err }, @@ -304,8 +304,8 @@ func TestPlainOldHTTPServer(t *testing.T) { protoMap, err := tc.getWellKnown(t) require.NoError(t, err) - expectedMap := make(libp2phttp.WellKnownProtoMap) - expectedMap[httpping.PingProtocolID] = libp2phttp.WellKnownProtocolMeta{Path: "/ping/"} + expectedMap := make(libp2phttp.ProtocolMetaMap) + expectedMap[httpping.PingProtocolID] = libp2phttp.ProtocolMeta{Path: "/ping/"} require.Equal(t, expectedMap, protoMap) }) } From 17c08edf775e2bf3a570d62078bc02f704f9c501 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 12:54:10 -0700 Subject: [PATCH 38/72] Automatically strip prefix when using SetHTTPHandler* --- p2p/http/libp2phttp.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index b231b0b168..ecc7a54baf 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -312,20 +312,23 @@ func (h *HTTPHost) Close() error { // SetHTTPHandler sets the HTTP handler for a given protocol. Automatically // manages the .well-known/libp2p mapping. -// TODO should this strip the prefix? I think so +// http.StripPrefix is called on the handler, so the handler will be unaware of +// it's prefix path. func (h *HTTPHost) SetHTTPHandler(p protocol.ID, handler http.Handler) { h.SetHTTPHandlerAtPath(p, string(p), handler) } // SetHTTPHandlerAtPath sets the HTTP handler for a given protocol using the // given path. Automatically manages the .well-known/libp2p mapping. +// http.StripPrefix is called on the handler, so the handler will be unaware of +// it's prefix path. func (h *HTTPHost) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http.Handler) { if path[len(path)-1] != '/' { // We are nesting this handler under this path, so it should end with a slash. path += "/" } h.wk.AddProtocolMapping(p, ProtocolMeta{Path: path}) - h.ServeMux.Handle(path, handler) + h.ServeMux.Handle(path, http.StripPrefix(path, handler)) } // getPeerProtoMap lets RoundTrippers implement a specific way of caching a peer's protocol mapping. From b1fc4c15e60e2ed1619df09cdbe4d674cabe80aa Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 12:57:13 -0700 Subject: [PATCH 39/72] Hide streamHostListen --- p2p/http/libp2phttp.go | 6 +++--- p2p/http/libp2phttp_test.go | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index ecc7a54baf..a6d0ce6f6d 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -50,8 +50,8 @@ type WellKnownHandler struct { wellKnownMapping ProtocolMetaMap } -// StreamHostListen retuns a net.Listener that listens on libp2p streams for HTTP/1.1 messages. -func StreamHostListen(streamHost host.Host) (net.Listener, error) { +// streamHostListen retuns a net.Listener that listens on libp2p streams for HTTP/1.1 messages. +func streamHostListen(streamHost host.Host) (net.Listener, error) { return gostream.Listen(streamHost, ProtocolIDForMultistreamSelect) } @@ -199,7 +199,7 @@ func (h *HTTPHost) Serve() error { errCh := make(chan error) if h.StreamHost != nil { - listener, err := StreamHostListen(h.StreamHost) + listener, err := streamHostListen(h.StreamHost) if err != nil { return err } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 0fb096b9df..1125c71396 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -72,10 +72,6 @@ func TestRoundTrippers(t *testing.T) { ) require.NoError(t, err) - streamListener, err := libp2phttp.StreamHostListen(serverHost) - require.NoError(t, err) - defer streamListener.Close() - httpHost := libp2phttp.HTTPHost{ ServeInsecureHTTP: true, StreamHost: serverHost, From d56ee6ef3ac25a74c8d917e2e6b3cdd6afc58edc Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 13:23:17 -0700 Subject: [PATCH 40/72] Cleanup public types and add docs --- p2p/http/libp2phttp.go | 64 ++++++++++++++++++------------------- p2p/http/libp2phttp_test.go | 19 +++++------ p2p/http/options.go | 8 ++++- 3 files changed, 48 insertions(+), 43 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index a6d0ce6f6d..c450ef3d9f 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -1,5 +1,5 @@ // HTTP semantics with libp2p. Can use a libp2p stream transport or stock HTTP -// transports. This API is experimental and will likely change soon. Implements libp2p spec #508. +// transports. This API is experimental and will likely change soon. Implements [libp2p spec #508](https://github.com/libp2p/specs/pull/508). package libp2phttp import ( @@ -42,12 +42,12 @@ type ProtocolMeta struct { Path string `json:"path"` } -type ProtocolMetaMap map[protocol.ID]ProtocolMeta +type PeerMeta map[protocol.ID]ProtocolMeta // WellKnownHandler is an http.Handler that serves the .well-known/libp2p resource type WellKnownHandler struct { wellknownMapMu sync.Mutex - wellKnownMapping ProtocolMetaMap + wellKnownMapping PeerMeta } // streamHostListen retuns a net.Listener that listens on libp2p streams for HTTP/1.1 messages. @@ -121,7 +121,7 @@ type HTTPHost struct { wk WellKnownHandler // peerMetadata is an lru cache of a peer's well-known protocol map. - peerMetadata *lru.Cache[peer.ID, ProtocolMetaMap] + peerMetadata *lru.Cache[peer.ID, PeerMeta] // createHTTPTransport is used to lazily create the httpTransport in a thread-safe way. createHTTPTransport sync.Once httpTransport *httpTransport @@ -141,8 +141,8 @@ func newHTTPTransport() *httpTransport { } } -func newPeerMetadataCache() *lru.Cache[peer.ID, ProtocolMetaMap] { - peerMetadata, err := lru.New[peer.ID, ProtocolMetaMap](peerMetadataLRUSize) +func newPeerMetadataCache() *lru.Cache[peer.ID, PeerMeta] { + peerMetadata, err := lru.New[peer.ID, PeerMeta](peerMetadataLRUSize) if err != nil { // Only happens if size is < 1. We make sure to not do that, so this should never happen. panic(err) @@ -331,9 +331,9 @@ func (h *HTTPHost) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http h.ServeMux.Handle(path, http.StripPrefix(path, handler)) } -// getPeerProtoMap lets RoundTrippers implement a specific way of caching a peer's protocol mapping. -type getPeerProtoMap interface { - GetPeerProtoMap() (ProtocolMetaMap, error) +// PeerMetadataGetter lets RoundTrippers implement a specific way of caching a peer's protocol mapping. +type PeerMetadataGetter interface { + GetPeerMetadata() (PeerMeta, error) } type streamRoundTripper struct { @@ -352,8 +352,8 @@ func (s *streamReadCloser) Close() error { return s.ReadCloser.Close() } -func (rt *streamRoundTripper) GetPeerProtoMap(server peer.ID) (ProtocolMetaMap, error) { - return rt.httpHost.GetAndStorePeerProtoMap(rt, rt.server) +func (rt *streamRoundTripper) GetPeerProtoMap() (PeerMeta, error) { + return rt.httpHost.getAndStorePeerMetadata(rt, rt.server) } // RoundTrip implements http.RoundTripper. @@ -390,25 +390,25 @@ type roundTripperForSpecificServer struct { targetServerAddr string sni string scheme string - cachedProtos ProtocolMetaMap + cachedProtos PeerMeta } -func (rt *roundTripperForSpecificServer) GetPeerProtoMap() (ProtocolMetaMap, error) { +func (rt *roundTripperForSpecificServer) GetPeerProtoMap() (PeerMeta, error) { // Do we already have the peer's protocol mapping? if rt.cachedProtos != nil { return rt.cachedProtos, nil } // if the underlying roundtripper implements getPeerProtoMap, use that - if g, ok := rt.RoundTripper.(getPeerProtoMap); ok { - wk, err := g.GetPeerProtoMap() + if g, ok := rt.RoundTripper.(PeerMetadataGetter); ok { + wk, err := g.GetPeerMetadata() if err == nil { rt.cachedProtos = wk return wk, nil } } - wk, err := rt.httpHost.GetAndStorePeerProtoMap(rt, rt.server) + wk, err := rt.httpHost.getAndStorePeerMetadata(rt, rt.server) if err == nil { rt.cachedProtos = wk return wk, nil @@ -449,9 +449,9 @@ type namespacedRoundTripper struct { protocolPrefixRaw string } -func (rt *namespacedRoundTripper) GetPeerProtoMap() (ProtocolMetaMap, error) { - if g, ok := rt.RoundTripper.(getPeerProtoMap); ok { - return g.GetPeerProtoMap() +func (rt *namespacedRoundTripper) GetPeerProtoMap() (PeerMeta, error) { + if g, ok := rt.RoundTripper.(PeerMetadataGetter); ok { + return g.GetPeerMetadata() } return nil, fmt.Errorf("can not get peer protocol map. Inner roundtripper does not implement getPeerProtoMap") @@ -471,7 +471,7 @@ 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 *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p protocol.ID, server peer.ID) (namespacedRoundTripper, error) { - protos, err := h.GetAndStorePeerProtoMap(roundtripper, server) + protos, err := h.getAndStorePeerMetadata(roundtripper, server) if err != nil { return namespacedRoundTripper{}, err } @@ -502,8 +502,9 @@ func (h *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p proto // NamespacedClient returns an http.Client that is scoped to the given protocol // on the given server. It creates a new RoundTripper for each call. If you are // creating many namespaced clients, consider creating a round tripper directly -// and namespacing that yourself. -func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.Client, error) { +// and namespacing the roundripper yourself, then creating clients from the +// namespace round tripper. +func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...roundTripperOptsFn) (http.Client, error) { rt, err := h.NewRoundTripper(server, opts...) if err != nil { return http.Client{}, err @@ -520,7 +521,7 @@ func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts .. // NewRoundTripper returns an http.RoundTripper that can fulfill and HTTP // request to the given server. It may use an HTTP transport or a stream based // transport. It is valid to pass an empty server.ID. -func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOptsFn) (http.RoundTripper, error) { +func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...roundTripperOptsFn) (http.RoundTripper, error) { options := roundTripperOpts{} for _, o := range opts { options = o(options) @@ -599,7 +600,7 @@ func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOpt } } - return NewStreamRoundTripper(h.StreamHost, server.ID), nil + return &streamRoundTripper{h: h.StreamHost, server: server.ID, httpHost: h}, nil } type httpMultiaddr struct { @@ -632,10 +633,6 @@ func parseMultiaddr(addr ma.Multiaddr) httpMultiaddr { return out } -func NewStreamRoundTripper(streamHost host.Host, server peer.ID) http.RoundTripper { - return &streamRoundTripper{h: streamHost, server: server} -} - var httpComponent, _ = ma.NewComponent("http", "") var tlsComponent, _ = ma.NewComponent("tls", "") @@ -671,7 +668,8 @@ func normalizeHTTPMultiaddr(addr ma.Multiaddr) (ma.Multiaddr, bool) { // ProtocolPathPrefix 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 *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, server peer.ID) (ProtocolMetaMap, error) { +func (h *HTTPHost) getAndStorePeerMetadata(roundtripper http.RoundTripper, server peer.ID) (PeerMeta, error) { + fmt.Println(h) if h.peerMetadata == nil { h.peerMetadata = newPeerMetadataCache() } @@ -712,7 +710,7 @@ func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, serve } } - meta := ProtocolMetaMap{} + meta := PeerMeta{} json.Unmarshal(body[:bytesRead], &meta) if server != "" { h.peerMetadata.Add(server, meta) @@ -723,7 +721,7 @@ func (h *HTTPHost) GetAndStorePeerProtoMap(roundtripper http.RoundTripper, serve // AddPeerMetadata adds a peer's protocol metadata to the http host. Useful if // you have out-of-band knowledge of a peer's protocol mapping. -func (h *HTTPHost) AddPeerMetadata(server peer.ID, meta ProtocolMetaMap) { +func (h *HTTPHost) AddPeerMetadata(server peer.ID, meta PeerMeta) { if h.peerMetadata == nil { h.peerMetadata = newPeerMetadataCache() } @@ -731,7 +729,7 @@ func (h *HTTPHost) AddPeerMetadata(server peer.ID, meta ProtocolMetaMap) { } // GetPeerMetadata gets a peer's cached protocol metadata from the http host. -func (h *HTTPHost) GetPeerMetadata(server peer.ID) (ProtocolMetaMap, bool) { +func (h *HTTPHost) GetPeerMetadata(server peer.ID) (PeerMeta, bool) { if h.peerMetadata == nil { return nil, false } @@ -739,7 +737,7 @@ func (h *HTTPHost) GetPeerMetadata(server peer.ID) (ProtocolMetaMap, bool) { } // RemovePeerMetadata removes a peer's protocol metadata from the http host -func (h *HTTPHost) RemovePeerMetadata(server peer.ID, meta ProtocolMetaMap) { +func (h *HTTPHost) RemovePeerMetadata(server peer.ID, meta PeerMeta) { if h.peerMetadata == nil { return } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 1125c71396..6cf65da5f2 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -52,7 +52,8 @@ func TestHTTPOverStreams(t *testing.T) { Addrs: serverHost.Addrs(), }) - clientRT := libp2phttp.NewStreamRoundTripper(clientHost, serverHost.ID()) + clientRT, err := (&libp2phttp.HTTPHost{StreamHost: clientHost}).NewRoundTripper(peer.AddrInfo{ID: serverHost.ID()}) + require.NoError(t, err) client := &http.Client{Transport: clientRT} @@ -204,10 +205,10 @@ func TestRoundTrippers(t *testing.T) { } // Read the .well-known/libp2p resource - wk, err := clientHttpHost.GetAndStorePeerProtoMap(rt, serverHost.ID()) + wk, err := rt.(libp2phttp.PeerMetadataGetter).GetPeerMetadata() require.NoError(t, err) - expectedMap := make(libp2phttp.ProtocolMetaMap) + expectedMap := make(libp2phttp.PeerMeta) expectedMap["/hello"] = libp2phttp.ProtocolMeta{Path: "/hello/"} require.Equal(t, expectedMap, wk) }) @@ -237,7 +238,7 @@ func TestPlainOldHTTPServer(t *testing.T) { testCases := []struct { name string do func(*testing.T, *http.Request) (*http.Response, error) - getWellKnown func(*testing.T) (libp2phttp.ProtocolMetaMap, error) + getWellKnown func(*testing.T) (libp2phttp.PeerMeta, error) }{ { name: "using libp2phttp", @@ -249,11 +250,11 @@ func TestPlainOldHTTPServer(t *testing.T) { client := &http.Client{Transport: rt} return client.Do(request) }, - getWellKnown: func(t *testing.T) (libp2phttp.ProtocolMetaMap, error) { + getWellKnown: func(t *testing.T) (libp2phttp.PeerMeta, error) { var clientHttpHost libp2phttp.HTTPHost rt, err := clientHttpHost.NewRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) - return clientHttpHost.GetAndStorePeerProtoMap(rt, "") + return rt.(libp2phttp.PeerMetadataGetter).GetPeerMetadata() }, }, { @@ -266,7 +267,7 @@ func TestPlainOldHTTPServer(t *testing.T) { client := http.Client{} return client.Do(request) }, - getWellKnown: func(t *testing.T) (libp2phttp.ProtocolMetaMap, error) { + getWellKnown: func(t *testing.T) (libp2phttp.PeerMeta, error) { client := http.Client{} resp, err := client.Get("http://" + l.Addr().String() + "/.well-known/libp2p") require.NoError(t, err) @@ -274,7 +275,7 @@ func TestPlainOldHTTPServer(t *testing.T) { b, err := io.ReadAll(resp.Body) require.NoError(t, err) - var out libp2phttp.ProtocolMetaMap + var out libp2phttp.PeerMeta err = json.Unmarshal(b, &out) return out, err }, @@ -300,7 +301,7 @@ func TestPlainOldHTTPServer(t *testing.T) { protoMap, err := tc.getWellKnown(t) require.NoError(t, err) - expectedMap := make(libp2phttp.ProtocolMetaMap) + expectedMap := make(libp2phttp.PeerMeta) expectedMap[httpping.PingProtocolID] = libp2phttp.ProtocolMeta{Path: "/ping/"} require.Equal(t, expectedMap, protoMap) }) diff --git a/p2p/http/options.go b/p2p/http/options.go index 3bf0356f81..6d3fd5caf4 100644 --- a/p2p/http/options.go +++ b/p2p/http/options.go @@ -1,6 +1,6 @@ package libp2phttp -type RoundTripperOptsFn func(o roundTripperOpts) roundTripperOpts +type roundTripperOptsFn func(o roundTripperOpts) roundTripperOpts type roundTripperOpts struct { // todo SkipClientAuth bool @@ -8,11 +8,17 @@ type roundTripperOpts struct { ServerMustAuthenticatePeerID bool } +// PreferHTTPTransport tells the roundtripper constructor to prefer using an +// HTTP transport (as opposed to a libp2p stream transport). Useful, for +// example, if you want to attempt to leverage HTTP caching. func PreferHTTPTransport(o roundTripperOpts) roundTripperOpts { o.preferHTTPTransport = true return o } +// ServerMustAuthenticatePeerID tells the roundtripper constructor that we MUST +// authenticate the Server's PeerID. Note: this currently means we can not use a +// native HTTP transport (HTTP peer id authentication is not yet implemented: https://github.com/libp2p/specs/pull/564). func ServerMustAuthenticatePeerID(o roundTripperOpts) roundTripperOpts { o.ServerMustAuthenticatePeerID = true return o From 6cb071d7a654a7b63a344001b35a05deebce8959 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 13:34:12 -0700 Subject: [PATCH 41/72] Fix interface rename --- p2p/http/libp2phttp.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index c450ef3d9f..95405cb4c7 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -352,7 +352,7 @@ func (s *streamReadCloser) Close() error { return s.ReadCloser.Close() } -func (rt *streamRoundTripper) GetPeerProtoMap() (PeerMeta, error) { +func (rt *streamRoundTripper) GetPeerMetadata() (PeerMeta, error) { return rt.httpHost.getAndStorePeerMetadata(rt, rt.server) } @@ -393,13 +393,13 @@ type roundTripperForSpecificServer struct { cachedProtos PeerMeta } -func (rt *roundTripperForSpecificServer) GetPeerProtoMap() (PeerMeta, error) { +func (rt *roundTripperForSpecificServer) GetPeerMetadata() (PeerMeta, error) { // Do we already have the peer's protocol mapping? if rt.cachedProtos != nil { return rt.cachedProtos, nil } - // if the underlying roundtripper implements getPeerProtoMap, use that + // if the underlying roundtripper implements GetPeerMetadata, use that if g, ok := rt.RoundTripper.(PeerMetadataGetter); ok { wk, err := g.GetPeerMetadata() if err == nil { @@ -449,12 +449,12 @@ type namespacedRoundTripper struct { protocolPrefixRaw string } -func (rt *namespacedRoundTripper) GetPeerProtoMap() (PeerMeta, error) { +func (rt *namespacedRoundTripper) GetPeerMetadata() (PeerMeta, error) { if g, ok := rt.RoundTripper.(PeerMetadataGetter); ok { return g.GetPeerMetadata() } - return nil, fmt.Errorf("can not get peer protocol map. Inner roundtripper does not implement getPeerProtoMap") + return nil, fmt.Errorf("can not get peer protocol map. Inner roundtripper does not implement GetPeerMetadata") } // RoundTrip implements http.RoundTripper. From 00e8f1786103933e5cc6e573e50102921765b9a5 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 16 Aug 2023 13:37:58 -0700 Subject: [PATCH 42/72] Remove debug --- p2p/http/libp2phttp.go | 1 - 1 file changed, 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 95405cb4c7..d7438d276e 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -669,7 +669,6 @@ func normalizeHTTPMultiaddr(addr ma.Multiaddr) (ma.Multiaddr, bool) { // returns it. Will only store the peer's protocol mapping if the server ID is // provided. func (h *HTTPHost) getAndStorePeerMetadata(roundtripper http.RoundTripper, server peer.ID) (PeerMeta, error) { - fmt.Println(h) if h.peerMetadata == nil { h.peerMetadata = newPeerMetadataCache() } From 7eaadb41e6f0820f3bf50287a72524a074da6b8b Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 17 Aug 2023 10:46:48 -0700 Subject: [PATCH 43/72] PR comments --- p2p/http/libp2phttp.go | 2 +- p2p/http/ping/ping.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index d7438d276e..c260de638a 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -380,7 +380,7 @@ func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) return resp, nil } -// roundTripperForSpecificHost is an http.RoundTripper targets a specific server. Still reuses the underlying RoundTripper for the requests. +// roundTripperForSpecificServer is an http.RoundTripper targets a specific server. Still reuses the underlying RoundTripper for the requests. // The underlying RoundTripper MUST be an HTTP Transport. type roundTripperForSpecificServer struct { http.RoundTripper diff --git a/p2p/http/ping/ping.go b/p2p/http/ping/ping.go index c911421de1..2c2ad80fbf 100644 --- a/p2p/http/ping/ping.go +++ b/p2p/http/ping/ping.go @@ -33,7 +33,7 @@ func (Ping) ServeHTTP(w http.ResponseWriter, r *http.Request) { // SendPing send an ping request over HTTP. The provided client should be namespaced to the Ping protocol. func SendPing(client http.Client) error { - body := [32]byte{} + body := [pingSize]byte{} _, err := io.ReadFull(rand.Reader, body[:]) if err != nil { return err From b5417c3489652bc1d9a79de3d0331e96954f5ae9 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 18 Aug 2023 15:17:01 -0700 Subject: [PATCH 44/72] Add examples --- p2p/http/example_test.go | 125 +++++++++++++++++++++++++++++++++++++++ p2p/http/libp2phttp.go | 12 +++- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 p2p/http/example_test.go diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go new file mode 100644 index 0000000000..3fc0dda5e8 --- /dev/null +++ b/p2p/http/example_test.go @@ -0,0 +1,125 @@ +package libp2phttp_test + +import ( + "fmt" + "io" + "log" + "net/http" + "strings" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/peer" + libp2phttp "github.com/libp2p/go-libp2p/p2p/http" + ma "github.com/multiformats/go-multiaddr" +) + +type IPFSGatewayHandler struct{} + +func (IPFSGatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func ExampleHTTPHost_withAStockGoHTTPClient() { + server := libp2phttp.HTTPHost{ + ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, + } + + // A server with a simple echo protocol + server.SetHTTPHandler("/echo/1.0.0", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/octet-stream") + io.Copy(w, r.Body) + })) + go server.Serve() + defer server.Close() + + var serverHTTPPort string + var err error + for _, a := range server.Addrs() { + serverHTTPPort, err = a.ValueForProtocol(ma.P_TCP) + if err == nil { + break + } + } + if err != nil { + log.Fatal(err) + } + + // Make an HTTP request using the Go standard library. + resp, err := http.Post("http://127.0.0.1:"+serverHTTPPort+"/echo/1.0.0/", "application/octet-stream", strings.NewReader("Hello HTTP")) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + fmt.Println(string(body)) + + // Output: Hello HTTP +} + +func ExampleHTTPHost_ListenOnHTTPTransportAndStreams() { + serverStreamHost, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/50124/quic-v1")) + if err != nil { + log.Fatal(err) + } + server := libp2phttp.HTTPHost{ + ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50124/http")}, + StreamHost: serverStreamHost, + } + go server.Serve() + defer server.Close() + + fmt.Println("Server listening on:", server.Addrs()) + // Output: Server listening on: [/ip4/127.0.0.1/udp/50124/quic-v1 /ip4/127.0.0.1/tcp/50124/http] +} + +func ExampleHTTPHost_overLibp2pStreams() { + serverStreamHost, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1")) + if err != nil { + log.Fatal(err) + } + + server := libp2phttp.HTTPHost{ + StreamHost: serverStreamHost, + } + + // A server with a simple echo protocol + server.SetHTTPHandler("/echo/1.0.0", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/octet-stream") + io.Copy(w, r.Body) + })) + go server.Serve() + defer server.Close() + + clientStreamHost, err := libp2p.New(libp2p.NoListenAddrs) + if err != nil { + log.Fatal(err) + } + + client := libp2phttp.HTTPHost{StreamHost: clientStreamHost} + + // Make an HTTP request using the Go standard library, but over libp2p + // streams. If the server were listening on an HTTP transport, this could + // also make the request over the HTTP transport. + httpClient, err := client.NamespacedClient("/echo/1.0.0", peer.AddrInfo{ID: server.PeerID(), Addrs: server.Addrs()}) + + // Only need to Post to "/" because this client is namespaced to the "/echo/1.0.0" protocol. + resp, err := httpClient.Post("/", "application/octet-stream", strings.NewReader("Hello HTTP")) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + fmt.Println(string(body)) + + // Output: Hello HTTP +} diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index c260de638a..944bea38e3 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -107,7 +107,9 @@ func (h *WellKnownHandler) RemoveProtocolMapping(p protocol.ID) { type HTTPHost struct { // StreamHost is a stream based libp2p host used to do HTTP over libp2p streams. May be nil StreamHost host.Host - // ListenAddrs are the requested addresses to listen on. Multiaddrs must be a valid HTTP(s) multiaddr. + // ListenAddrs are the requested addresses to listen on. Multiaddrs must be + // a valid HTTP(s) multiaddr. Only multiaddrs for an HTTP transport are + // supported (must end with /http or /https). ListenAddrs []ma.Multiaddr // TLSConfig is the TLS config for the server to use TLSConfig *tls.Config @@ -158,6 +160,14 @@ func (h *HTTPHost) Addrs() []ma.Multiaddr { return h.httpTransport.listenAddrs } +// ID returns the peer ID of the underlying stream host, or the zero value if there is no stream host. +func (h *HTTPHost) PeerID() peer.ID { + if h.StreamHost != nil { + return h.StreamHost.ID() + } + return "" +} + var ErrNoListeners = errors.New("nothing to listen on") // Serve starts the HTTP transport listeners. Always returns a non-nil error. From c3873dd7ac3ea0c261d4204b2e73e9f1895b9131 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 18 Aug 2023 15:22:50 -0700 Subject: [PATCH 45/72] Fix example name --- p2p/http/example_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go index 3fc0dda5e8..b2f14c1c6d 100644 --- a/p2p/http/example_test.go +++ b/p2p/http/example_test.go @@ -61,7 +61,7 @@ func ExampleHTTPHost_withAStockGoHTTPClient() { // Output: Hello HTTP } -func ExampleHTTPHost_ListenOnHTTPTransportAndStreams() { +func ExampleHTTPHost_listenOnHTTPTransportAndStreams() { serverStreamHost, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/50124/quic-v1")) if err != nil { log.Fatal(err) From b5dcff16bab02355315e925c0b08e32e3e46541d Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 18 Aug 2023 15:23:05 -0700 Subject: [PATCH 46/72] Cleanup unused example --- p2p/http/example_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go index b2f14c1c6d..3c96f12b58 100644 --- a/p2p/http/example_test.go +++ b/p2p/http/example_test.go @@ -13,12 +13,6 @@ import ( ma "github.com/multiformats/go-multiaddr" ) -type IPFSGatewayHandler struct{} - -func (IPFSGatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) -} - func ExampleHTTPHost_withAStockGoHTTPClient() { server := libp2phttp.HTTPHost{ ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP From 6906148e768d8ab0dad17cecead1ea303c485a4d Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 18 Aug 2023 15:40:50 -0700 Subject: [PATCH 47/72] Add more examples --- p2p/http/example_test.go | 202 ++++++++++++++++++++++++++++++++++++ p2p/http/libp2phttp.go | 12 +-- p2p/http/libp2phttp_test.go | 2 +- 3 files changed, 209 insertions(+), 7 deletions(-) diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go index 3c96f12b58..8726388405 100644 --- a/p2p/http/example_test.go +++ b/p2p/http/example_test.go @@ -117,3 +117,205 @@ func ExampleHTTPHost_overLibp2pStreams() { // Output: Hello HTTP } + +func ExampleHTTPHost_Serve() { + server := libp2phttp.HTTPHost{ + ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50221/http")}, + } + + go server.Serve() + defer server.Close() + + fmt.Println(server.Addrs()) + + // Output: [/ip4/127.0.0.1/tcp/50221/http] +} + +func ExampleHTTPHost_SetHTTPHandler() { + server := libp2phttp.HTTPHost{ + ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50222/http")}, + } + + server.SetHTTPHandler("/hello/1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/plain") + w.Write([]byte("Hello World")) + })) + + go server.Serve() + defer server.Close() + + port, err := server.Addrs()[0].ValueForProtocol(ma.P_TCP) + if err != nil { + log.Fatal(err) + } + + resp, err := http.Get("http://127.0.0.1:" + port + "/hello/1/") + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(respBody)) + + // Output: Hello World +} + +func ExampleHTTPHost_SetHTTPHandlerAtPath() { + server := libp2phttp.HTTPHost{ + ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50224/http")}, + } + + server.SetHTTPHandlerAtPath("/hello/1", "/other-place/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/plain") + w.Write([]byte("Hello World")) + })) + + go server.Serve() + defer server.Close() + + port, err := server.Addrs()[0].ValueForProtocol(ma.P_TCP) + if err != nil { + log.Fatal(err) + } + + resp, err := http.Get("http://127.0.0.1:" + port + "/other-place/") + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(respBody)) + + // Output: Hello World +} + +func ExampleHTTPHost_NamespacedClient() { + var client libp2phttp.HTTPHost + + // Create the server + server := libp2phttp.HTTPHost{ + ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50221/http")}, + } + + server.SetHTTPHandlerAtPath("/hello/1", "/other-place/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/plain") + w.Write([]byte("Hello World")) + })) + + go server.Serve() + defer server.Close() + + // Create an http.Client that is namespaced to this protocol. + httpClient, err := client.NamespacedClient("/hello/1", peer.AddrInfo{ID: server.PeerID(), Addrs: server.Addrs()}) + if err != nil { + log.Fatal(err) + } + + resp, err := httpClient.Get("/") + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(respBody)) + + // Output: Hello World +} + +func ExampleHTTPHost_NamespaceRoundTripper() { + var client libp2phttp.HTTPHost + + // Create the server + server := libp2phttp.HTTPHost{ + ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50223/http")}, + } + + server.SetHTTPHandler("/hello/1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/plain") + w.Write([]byte("Hello World")) + })) + + go server.Serve() + defer server.Close() + + // Create an http.Roundtripper for the server + rt, err := client.NewRoundTripper(peer.AddrInfo{ID: server.PeerID(), Addrs: server.Addrs()}) + if err != nil { + log.Fatal(err) + } + + // Namespace this roundtripper to a specific protocol + rt, err = client.NamespaceRoundTripper(rt, "/hello/1", server.PeerID()) + if err != nil { + log.Fatal(err) + } + + resp, err := (&http.Client{Transport: rt}).Get("/") + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(respBody)) + + // Output: Hello World +} + +func ExampleHTTPHost_NewRoundTripper() { + var client libp2phttp.HTTPHost + + // Create the server + server := libp2phttp.HTTPHost{ + ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50225/http")}, + } + + server.SetHTTPHandler("/hello/1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/plain") + w.Write([]byte("Hello World")) + })) + + go server.Serve() + defer server.Close() + + // Create an http.Roundtripper for the server + rt, err := client.NewRoundTripper(peer.AddrInfo{ID: server.PeerID(), Addrs: server.Addrs()}) + if err != nil { + log.Fatal(err) + } + + resp, err := (&http.Client{Transport: rt}).Get("/hello/1") + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(respBody)) + + // Output: Hello World +} diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 944bea38e3..5cf592316a 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -480,15 +480,15 @@ 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 *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p protocol.ID, server peer.ID) (namespacedRoundTripper, error) { +func (h *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p protocol.ID, server peer.ID) (*namespacedRoundTripper, error) { protos, err := h.getAndStorePeerMetadata(roundtripper, server) if err != nil { - return namespacedRoundTripper{}, err + return &namespacedRoundTripper{}, err } v, ok := protos[p] if !ok { - return namespacedRoundTripper{}, fmt.Errorf("no protocol %s for server %s", p, server) + return &namespacedRoundTripper{}, fmt.Errorf("no protocol %s for server %s", p, server) } path := v.Path @@ -499,10 +499,10 @@ func (h *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p proto u, err := url.Parse(path) if err != nil { - return namespacedRoundTripper{}, fmt.Errorf("invalid path %s for protocol %s for server %s", v.Path, p, server) + return &namespacedRoundTripper{}, fmt.Errorf("invalid path %s for protocol %s for server %s", v.Path, p, server) } - return namespacedRoundTripper{ + return &namespacedRoundTripper{ RoundTripper: roundtripper, protocolPrefix: u.Path, protocolPrefixRaw: u.RawPath, @@ -525,7 +525,7 @@ func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts .. return http.Client{}, err } - return http.Client{Transport: &nrt}, nil + return http.Client{Transport: nrt}, nil } // NewRoundTripper returns an http.RoundTripper that can fulfill and HTTP diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 6cf65da5f2..2e171c0acc 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -188,7 +188,7 @@ func TestRoundTrippers(t *testing.T) { require.NoError(t, err) nrt, err := h.NamespaceRoundTripper(rt, "/hello", serverHost.ID()) require.NoError(t, err) - client := &http.Client{Transport: &nrt} + client := &http.Client{Transport: nrt} resp, err = client.Get("/") require.NoError(t, err) } else { From eea1710a92ad07ebb7e4f0629f1c19454f4ae97f Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 18 Aug 2023 19:25:46 -0700 Subject: [PATCH 48/72] Add well known handler example --- p2p/http/example_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go index 8726388405..54d791f87c 100644 --- a/p2p/http/example_test.go +++ b/p2p/http/example_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "log" + "net" "net/http" "strings" @@ -319,3 +320,34 @@ func ExampleHTTPHost_NewRoundTripper() { // Output: Hello World } + +func ExampleWellKnownHandler() { + var h libp2phttp.WellKnownHandler + h.AddProtocolMapping("/hello/1", libp2phttp.ProtocolMeta{ + Path: "/hello-path/", + }) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + log.Fatal(err) + } + + defer listener.Close() + // Serve `.well-known/libp2p`. Note, this is handled automatically if you use the HTTPHost. + go http.Serve(listener, &h) + + // Get the `.well-known/libp2p` resource + resp, err := http.Get("http://" + listener.Addr().String() + "/.well-known/libp2p") + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(respBody)) + // Output: {"/hello/1":{"path":"/hello-path/"}} + +} From e756bad1756ef16c8b1e9da0c593a48055e856ec Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 18 Aug 2023 19:26:13 -0700 Subject: [PATCH 49/72] Handle empty path --- p2p/http/libp2phttp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 5cf592316a..ce7d320694 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -333,7 +333,7 @@ func (h *HTTPHost) SetHTTPHandler(p protocol.ID, handler http.Handler) { // http.StripPrefix is called on the handler, so the handler will be unaware of // it's prefix path. func (h *HTTPHost) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http.Handler) { - if path[len(path)-1] != '/' { + if path == "" || path[len(path)-1] != '/' { // We are nesting this handler under this path, so it should end with a slash. path += "/" } From b8c138096f5953832caebe839bb7c73bfc6f1789 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 18 Aug 2023 19:26:41 -0700 Subject: [PATCH 50/72] Fix typo --- p2p/http/libp2phttp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index ce7d320694..be24efc1ab 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -323,7 +323,7 @@ func (h *HTTPHost) Close() error { // SetHTTPHandler sets the HTTP handler for a given protocol. Automatically // manages the .well-known/libp2p mapping. // http.StripPrefix is called on the handler, so the handler will be unaware of -// it's prefix path. +// its prefix path. func (h *HTTPHost) SetHTTPHandler(p protocol.ID, handler http.Handler) { h.SetHTTPHandlerAtPath(p, string(p), handler) } @@ -331,7 +331,7 @@ func (h *HTTPHost) SetHTTPHandler(p protocol.ID, handler http.Handler) { // SetHTTPHandlerAtPath sets the HTTP handler for a given protocol using the // given path. Automatically manages the .well-known/libp2p mapping. // http.StripPrefix is called on the handler, so the handler will be unaware of -// it's prefix path. +// its prefix path. func (h *HTTPHost) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http.Handler) { if path == "" || path[len(path)-1] != '/' { // We are nesting this handler under this path, so it should end with a slash. From 647653d7f64c72ebda80bee2c01a8256c0b47128 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 21 Aug 2023 13:08:22 -0700 Subject: [PATCH 51/72] Make RoundTripperOption public so users can allocate a slice of options --- p2p/http/libp2phttp.go | 4 ++-- p2p/http/options.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index be24efc1ab..5cb65b409e 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -514,7 +514,7 @@ func (h *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p proto // creating many namespaced clients, consider creating a round tripper directly // and namespacing the roundripper yourself, then creating clients from the // namespace round tripper. -func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...roundTripperOptsFn) (http.Client, error) { +func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...RoundTripperOption) (http.Client, error) { rt, err := h.NewRoundTripper(server, opts...) if err != nil { return http.Client{}, err @@ -531,7 +531,7 @@ func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts .. // NewRoundTripper returns an http.RoundTripper that can fulfill and HTTP // request to the given server. It may use an HTTP transport or a stream based // transport. It is valid to pass an empty server.ID. -func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...roundTripperOptsFn) (http.RoundTripper, error) { +func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOption) (http.RoundTripper, error) { options := roundTripperOpts{} for _, o := range opts { options = o(options) diff --git a/p2p/http/options.go b/p2p/http/options.go index 6d3fd5caf4..a4d926431d 100644 --- a/p2p/http/options.go +++ b/p2p/http/options.go @@ -1,6 +1,6 @@ package libp2phttp -type roundTripperOptsFn func(o roundTripperOpts) roundTripperOpts +type RoundTripperOption func(o roundTripperOpts) roundTripperOpts type roundTripperOpts struct { // todo SkipClientAuth bool From 1f12a58d584ca8514d7a207cc607ee772b29f084 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 21 Aug 2023 13:10:03 -0700 Subject: [PATCH 52/72] Rename HTTPHost to Host --- p2p/http/example_test.go | 46 ++++++++++++++++++------------------- p2p/http/libp2phttp.go | 38 +++++++++++++++--------------- p2p/http/libp2phttp_test.go | 36 ++++++++++++++--------------- 3 files changed, 60 insertions(+), 60 deletions(-) diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go index 54d791f87c..48b2c28a35 100644 --- a/p2p/http/example_test.go +++ b/p2p/http/example_test.go @@ -14,8 +14,8 @@ import ( ma "github.com/multiformats/go-multiaddr" ) -func ExampleHTTPHost_withAStockGoHTTPClient() { - server := libp2phttp.HTTPHost{ +func ExampleHost_withAStockGoHTTPClient() { + server := libp2phttp.Host{ ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } @@ -56,12 +56,12 @@ func ExampleHTTPHost_withAStockGoHTTPClient() { // Output: Hello HTTP } -func ExampleHTTPHost_listenOnHTTPTransportAndStreams() { +func ExampleHost_listenOnHTTPTransportAndStreams() { serverStreamHost, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/50124/quic-v1")) if err != nil { log.Fatal(err) } - server := libp2phttp.HTTPHost{ + server := libp2phttp.Host{ ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50124/http")}, StreamHost: serverStreamHost, @@ -73,13 +73,13 @@ func ExampleHTTPHost_listenOnHTTPTransportAndStreams() { // Output: Server listening on: [/ip4/127.0.0.1/udp/50124/quic-v1 /ip4/127.0.0.1/tcp/50124/http] } -func ExampleHTTPHost_overLibp2pStreams() { +func ExampleHost_overLibp2pStreams() { serverStreamHost, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1")) if err != nil { log.Fatal(err) } - server := libp2phttp.HTTPHost{ + server := libp2phttp.Host{ StreamHost: serverStreamHost, } @@ -96,7 +96,7 @@ func ExampleHTTPHost_overLibp2pStreams() { log.Fatal(err) } - client := libp2phttp.HTTPHost{StreamHost: clientStreamHost} + client := libp2phttp.Host{StreamHost: clientStreamHost} // Make an HTTP request using the Go standard library, but over libp2p // streams. If the server were listening on an HTTP transport, this could @@ -119,8 +119,8 @@ func ExampleHTTPHost_overLibp2pStreams() { // Output: Hello HTTP } -func ExampleHTTPHost_Serve() { - server := libp2phttp.HTTPHost{ +func ExampleHost_Serve() { + server := libp2phttp.Host{ ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50221/http")}, } @@ -133,8 +133,8 @@ func ExampleHTTPHost_Serve() { // Output: [/ip4/127.0.0.1/tcp/50221/http] } -func ExampleHTTPHost_SetHTTPHandler() { - server := libp2phttp.HTTPHost{ +func ExampleHost_SetHTTPHandler() { + server := libp2phttp.Host{ ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50222/http")}, } @@ -167,8 +167,8 @@ func ExampleHTTPHost_SetHTTPHandler() { // Output: Hello World } -func ExampleHTTPHost_SetHTTPHandlerAtPath() { - server := libp2phttp.HTTPHost{ +func ExampleHost_SetHTTPHandlerAtPath() { + server := libp2phttp.Host{ ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50224/http")}, } @@ -201,11 +201,11 @@ func ExampleHTTPHost_SetHTTPHandlerAtPath() { // Output: Hello World } -func ExampleHTTPHost_NamespacedClient() { - var client libp2phttp.HTTPHost +func ExampleHost_NamespacedClient() { + var client libp2phttp.Host // Create the server - server := libp2phttp.HTTPHost{ + server := libp2phttp.Host{ ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50221/http")}, } @@ -239,11 +239,11 @@ func ExampleHTTPHost_NamespacedClient() { // Output: Hello World } -func ExampleHTTPHost_NamespaceRoundTripper() { - var client libp2phttp.HTTPHost +func ExampleHost_NamespaceRoundTripper() { + var client libp2phttp.Host // Create the server - server := libp2phttp.HTTPHost{ + server := libp2phttp.Host{ ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50223/http")}, } @@ -283,11 +283,11 @@ func ExampleHTTPHost_NamespaceRoundTripper() { // Output: Hello World } -func ExampleHTTPHost_NewRoundTripper() { - var client libp2phttp.HTTPHost +func ExampleHost_NewRoundTripper() { + var client libp2phttp.Host // Create the server - server := libp2phttp.HTTPHost{ + server := libp2phttp.Host{ ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50225/http")}, } @@ -333,7 +333,7 @@ func ExampleWellKnownHandler() { } defer listener.Close() - // Serve `.well-known/libp2p`. Note, this is handled automatically if you use the HTTPHost. + // Serve `.well-known/libp2p`. Note, this is handled automatically if you use the libp2phttp.Host. go http.Serve(listener, &h) // Get the `.well-known/libp2p` resource diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 5cb65b409e..2f5bfabb0a 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -98,13 +98,13 @@ func (h *WellKnownHandler) RemoveProtocolMapping(p protocol.ID) { h.wellknownMapMu.Unlock() } -// HTTPHost is a libp2p host for request/responses with HTTP semantics. This is -// in contrast to a stream-oriented host like the host.Host interface. Its -// zero-value (&HTTPHost{}) is usable. Do not copy by value. +// Host is a libp2p host for request/responses with HTTP semantics. This is +// in contrast to a stream-oriented host like the core host.Host interface. Its +// zero-value (&Host{}) is usable. Do not copy by value. // See examples for usage. // // Warning, this is experimental. The API will likely change. -type HTTPHost struct { +type Host struct { // StreamHost is a stream based libp2p host used to do HTTP over libp2p streams. May be nil StreamHost host.Host // ListenAddrs are the requested addresses to listen on. Multiaddrs must be @@ -152,7 +152,7 @@ func newPeerMetadataCache() *lru.Cache[peer.ID, PeerMeta] { return peerMetadata } -func (h *HTTPHost) Addrs() []ma.Multiaddr { +func (h *Host) Addrs() []ma.Multiaddr { h.createHTTPTransport.Do(func() { h.httpTransport = newHTTPTransport() }) @@ -161,7 +161,7 @@ func (h *HTTPHost) Addrs() []ma.Multiaddr { } // ID returns the peer ID of the underlying stream host, or the zero value if there is no stream host. -func (h *HTTPHost) PeerID() peer.ID { +func (h *Host) PeerID() peer.ID { if h.StreamHost != nil { return h.StreamHost.ID() } @@ -172,7 +172,7 @@ var ErrNoListeners = errors.New("nothing to listen on") // Serve starts the HTTP transport listeners. Always returns a non-nil error. // If there are no listeners, returns ErrNoListeners. -func (h *HTTPHost) Serve() error { +func (h *Host) Serve() error { // assert that each addr contains a /http component for _, addr := range h.ListenAddrs { _, isHTTP := normalizeHTTPMultiaddr(addr) @@ -312,7 +312,7 @@ func (h *HTTPHost) Serve() error { return err } -func (h *HTTPHost) Close() error { +func (h *Host) Close() error { h.createHTTPTransport.Do(func() { h.httpTransport = newHTTPTransport() }) @@ -324,7 +324,7 @@ func (h *HTTPHost) Close() error { // manages the .well-known/libp2p mapping. // http.StripPrefix is called on the handler, so the handler will be unaware of // its prefix path. -func (h *HTTPHost) SetHTTPHandler(p protocol.ID, handler http.Handler) { +func (h *Host) SetHTTPHandler(p protocol.ID, handler http.Handler) { h.SetHTTPHandlerAtPath(p, string(p), handler) } @@ -332,7 +332,7 @@ func (h *HTTPHost) SetHTTPHandler(p protocol.ID, handler http.Handler) { // given path. Automatically manages the .well-known/libp2p mapping. // http.StripPrefix is called on the handler, so the handler will be unaware of // its prefix path. -func (h *HTTPHost) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http.Handler) { +func (h *Host) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http.Handler) { if path == "" || path[len(path)-1] != '/' { // We are nesting this handler under this path, so it should end with a slash. path += "/" @@ -349,7 +349,7 @@ type PeerMetadataGetter interface { type streamRoundTripper struct { server peer.ID h host.Host - httpHost *HTTPHost + httpHost *Host } type streamReadCloser struct { @@ -395,7 +395,7 @@ func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) type roundTripperForSpecificServer struct { http.RoundTripper ownRoundtripper bool - httpHost *HTTPHost + httpHost *Host server peer.ID targetServerAddr string sni string @@ -480,7 +480,7 @@ 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 *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p protocol.ID, server peer.ID) (*namespacedRoundTripper, error) { +func (h *Host) NamespaceRoundTripper(roundtripper http.RoundTripper, p protocol.ID, server peer.ID) (*namespacedRoundTripper, error) { protos, err := h.getAndStorePeerMetadata(roundtripper, server) if err != nil { return &namespacedRoundTripper{}, err @@ -514,7 +514,7 @@ func (h *HTTPHost) NamespaceRoundTripper(roundtripper http.RoundTripper, p proto // creating many namespaced clients, consider creating a round tripper directly // and namespacing the roundripper yourself, then creating clients from the // namespace round tripper. -func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...RoundTripperOption) (http.Client, error) { +func (h *Host) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...RoundTripperOption) (http.Client, error) { rt, err := h.NewRoundTripper(server, opts...) if err != nil { return http.Client{}, err @@ -531,7 +531,7 @@ func (h *HTTPHost) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts .. // NewRoundTripper returns an http.RoundTripper that can fulfill and HTTP // request to the given server. It may use an HTTP transport or a stream based // transport. It is valid to pass an empty server.ID. -func (h *HTTPHost) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOption) (http.RoundTripper, error) { +func (h *Host) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOption) (http.RoundTripper, error) { options := roundTripperOpts{} for _, o := range opts { options = o(options) @@ -678,7 +678,7 @@ func normalizeHTTPMultiaddr(addr ma.Multiaddr) (ma.Multiaddr, bool) { // ProtocolPathPrefix 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 *HTTPHost) getAndStorePeerMetadata(roundtripper http.RoundTripper, server peer.ID) (PeerMeta, error) { +func (h *Host) getAndStorePeerMetadata(roundtripper http.RoundTripper, server peer.ID) (PeerMeta, error) { if h.peerMetadata == nil { h.peerMetadata = newPeerMetadataCache() } @@ -730,7 +730,7 @@ func (h *HTTPHost) getAndStorePeerMetadata(roundtripper http.RoundTripper, serve // AddPeerMetadata adds a peer's protocol metadata to the http host. Useful if // you have out-of-band knowledge of a peer's protocol mapping. -func (h *HTTPHost) AddPeerMetadata(server peer.ID, meta PeerMeta) { +func (h *Host) AddPeerMetadata(server peer.ID, meta PeerMeta) { if h.peerMetadata == nil { h.peerMetadata = newPeerMetadataCache() } @@ -738,7 +738,7 @@ func (h *HTTPHost) AddPeerMetadata(server peer.ID, meta PeerMeta) { } // GetPeerMetadata gets a peer's cached protocol metadata from the http host. -func (h *HTTPHost) GetPeerMetadata(server peer.ID) (PeerMeta, bool) { +func (h *Host) GetPeerMetadata(server peer.ID) (PeerMeta, bool) { if h.peerMetadata == nil { return nil, false } @@ -746,7 +746,7 @@ func (h *HTTPHost) GetPeerMetadata(server peer.ID) (PeerMeta, bool) { } // RemovePeerMetadata removes a peer's protocol metadata from the http host -func (h *HTTPHost) RemovePeerMetadata(server peer.ID, meta PeerMeta) { +func (h *Host) RemovePeerMetadata(server peer.ID, meta PeerMeta) { if h.peerMetadata == nil { return } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 2e171c0acc..16d7bd8a16 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -34,7 +34,7 @@ func TestHTTPOverStreams(t *testing.T) { ) require.NoError(t, err) - httpHost := libp2phttp.HTTPHost{StreamHost: serverHost} + httpHost := libp2phttp.Host{StreamHost: serverHost} httpHost.SetHTTPHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) @@ -52,7 +52,7 @@ func TestHTTPOverStreams(t *testing.T) { Addrs: serverHost.Addrs(), }) - clientRT, err := (&libp2phttp.HTTPHost{StreamHost: clientHost}).NewRoundTripper(peer.AddrInfo{ID: serverHost.ID()}) + clientRT, err := (&libp2phttp.Host{StreamHost: clientHost}).NewRoundTripper(peer.AddrInfo{ID: serverHost.ID()}) require.NoError(t, err) client := &http.Client{Transport: clientRT} @@ -73,7 +73,7 @@ func TestRoundTrippers(t *testing.T) { ) require.NoError(t, err) - httpHost := libp2phttp.HTTPHost{ + httpHost := libp2phttp.Host{ ServeInsecureHTTP: true, StreamHost: serverHost, ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, @@ -92,12 +92,12 @@ func TestRoundTrippers(t *testing.T) { testCases := []struct { name string - setupRoundTripper func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper + setupRoundTripper func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper expectStreamRoundTripper bool }{ { name: "HTTP preferred", - setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper { rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: serverMultiaddrs, @@ -108,7 +108,7 @@ func TestRoundTrippers(t *testing.T) { }, { name: "HTTP first", - setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper { rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHTTPAddr, serverHost.Addrs()[0]}, @@ -119,7 +119,7 @@ func TestRoundTrippers(t *testing.T) { }, { name: "No HTTP transport", - setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper { rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHost.Addrs()[0]}, @@ -131,7 +131,7 @@ func TestRoundTrippers(t *testing.T) { }, { name: "Stream transport first", - setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper { rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHost.Addrs()[0], serverHTTPAddr}, @@ -143,7 +143,7 @@ func TestRoundTrippers(t *testing.T) { }, { name: "Existing stream transport connection", - setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.HTTPHost) http.RoundTripper { + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper { clientStreamHost.Connect(context.Background(), peer.AddrInfo{ ID: serverHost.ID(), Addrs: serverHost.Addrs(), @@ -166,7 +166,7 @@ func TestRoundTrippers(t *testing.T) { require.NoError(t, err) defer clientStreamHost.Close() - clientHttpHost := &libp2phttp.HTTPHost{StreamHost: clientStreamHost} + clientHttpHost := &libp2phttp.Host{StreamHost: clientStreamHost} rt := tc.setupRoundTripper(t, clientStreamHost, clientHttpHost) if tc.expectStreamRoundTripper { @@ -184,7 +184,7 @@ func TestRoundTrippers(t *testing.T) { var resp *http.Response var err error if tc { - var h libp2phttp.HTTPHost + var h libp2phttp.Host require.NoError(t, err) nrt, err := h.NamespaceRoundTripper(rt, "/hello", serverHost.ID()) require.NoError(t, err) @@ -243,7 +243,7 @@ func TestPlainOldHTTPServer(t *testing.T) { { name: "using libp2phttp", do: func(t *testing.T, request *http.Request) (*http.Response, error) { - var clientHttpHost libp2phttp.HTTPHost + var clientHttpHost libp2phttp.Host rt, err := clientHttpHost.NewRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) @@ -251,7 +251,7 @@ func TestPlainOldHTTPServer(t *testing.T) { return client.Do(request) }, getWellKnown: func(t *testing.T) (libp2phttp.PeerMeta, error) { - var clientHttpHost libp2phttp.HTTPHost + var clientHttpHost libp2phttp.Host rt, err := clientHttpHost.NewRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) return rt.(libp2phttp.PeerMetadataGetter).GetPeerMetadata() @@ -308,8 +308,8 @@ func TestPlainOldHTTPServer(t *testing.T) { } } -func TestHTTPHostZeroValue(t *testing.T) { - server := libp2phttp.HTTPHost{ +func TestHostZeroValue(t *testing.T) { + server := libp2phttp.Host{ ServeInsecureHTTP: true, ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } @@ -319,7 +319,7 @@ func TestHTTPHostZeroValue(t *testing.T) { }() defer server.Close() - c := libp2phttp.HTTPHost{} + c := libp2phttp.Host{} client, err := c.NamespacedClient("/hello", peer.AddrInfo{Addrs: server.Addrs()}) require.NoError(t, err) resp, err := client.Get("/") @@ -332,7 +332,7 @@ func TestHTTPHostZeroValue(t *testing.T) { } func TestHTTPS(t *testing.T) { - server := libp2phttp.HTTPHost{ + server := libp2phttp.Host{ TLSConfig: selfSignedTLSConfig(t), ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/https")}, } @@ -344,7 +344,7 @@ func TestHTTPS(t *testing.T) { clientTransport := http.DefaultTransport.(*http.Transport).Clone() clientTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - client := libp2phttp.HTTPHost{ + client := libp2phttp.Host{ DefaultClientRoundTripper: clientTransport, } httpClient, err := client.NamespacedClient(httpping.PingProtocolID, peer.AddrInfo{Addrs: server.Addrs()}) From c31efcd7ba1a3067a6a165d3ea6a337ee3e8ec37 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 21 Aug 2023 13:38:56 -0700 Subject: [PATCH 53/72] Make the host.WellKnownHandler public --- p2p/http/libp2phttp.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 2f5bfabb0a..d164a0b604 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -121,7 +121,9 @@ type Host struct { // DefaultClientRoundTripper is the default http.RoundTripper for clients DefaultClientRoundTripper *http.Transport - wk WellKnownHandler + // WellKnownHandler is the http handler for the `.well-known/libp2p` resource + WellKnownHandler WellKnownHandler + // 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. @@ -181,7 +183,7 @@ func (h *Host) Serve() error { } } - h.ServeMux.Handle("/.well-known/libp2p", &h.wk) + h.ServeMux.Handle("/.well-known/libp2p", &h.WellKnownHandler) h.createHTTPTransport.Do(func() { h.httpTransport = newHTTPTransport() @@ -337,7 +339,7 @@ func (h *Host) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http.Han // We are nesting this handler under this path, so it should end with a slash. path += "/" } - h.wk.AddProtocolMapping(p, ProtocolMeta{Path: path}) + h.WellKnownHandler.AddProtocolMapping(p, ProtocolMeta{Path: path}) h.ServeMux.Handle(path, http.StripPrefix(path, handler)) } From ac47208407cb79a59ef89987f8eadfde72a17770 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 23 Aug 2023 09:38:40 -0700 Subject: [PATCH 54/72] Make Add merge PeerMetadata. Introduce SetPeerMetadata --- p2p/http/libp2phttp.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index d164a0b604..36b6fe96ea 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -730,15 +730,32 @@ func (h *Host) getAndStorePeerMetadata(roundtripper http.RoundTripper, server pe return meta, nil } -// AddPeerMetadata adds a peer's protocol metadata to the http host. Useful if +// SetPeerMetadata adds a peer's protocol metadata to the http host. Useful if // you have out-of-band knowledge of a peer's protocol mapping. -func (h *Host) AddPeerMetadata(server peer.ID, meta PeerMeta) { +func (h *Host) SetPeerMetadata(server peer.ID, meta PeerMeta) { if h.peerMetadata == nil { h.peerMetadata = newPeerMetadataCache() } h.peerMetadata.Add(server, meta) } +// AddPeerMetadata merges the given peer's protocol metadata to the http host. Useful if +// you have out-of-band knowledge of a peer's protocol mapping. +func (h *Host) AddPeerMetadata(server peer.ID, meta PeerMeta) { + if h.peerMetadata == nil { + h.peerMetadata = newPeerMetadataCache() + } + origMeta, ok := h.peerMetadata.Get(server) + if !ok { + h.peerMetadata.Add(server, meta) + return + } + for proto, m := range meta { + origMeta[proto] = m + } + h.peerMetadata.Add(server, origMeta) +} + // GetPeerMetadata gets a peer's cached protocol metadata from the http host. func (h *Host) GetPeerMetadata(server peer.ID) (PeerMeta, bool) { if h.peerMetadata == nil { @@ -748,7 +765,7 @@ func (h *Host) GetPeerMetadata(server peer.ID) (PeerMeta, bool) { } // RemovePeerMetadata removes a peer's protocol metadata from the http host -func (h *Host) RemovePeerMetadata(server peer.ID, meta PeerMeta) { +func (h *Host) RemovePeerMetadata(server peer.ID) { if h.peerMetadata == nil { return } From 403b28207240a52741518a1da4a39dab25707f72 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 23 Aug 2023 09:50:17 -0700 Subject: [PATCH 55/72] Rename AddProtocolMapping to AddProtocolMeta. Expand comment --- p2p/http/example_test.go | 2 +- p2p/http/libp2phttp.go | 15 ++++++++++----- p2p/http/libp2phttp_test.go | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go index 48b2c28a35..426b1f749c 100644 --- a/p2p/http/example_test.go +++ b/p2p/http/example_test.go @@ -323,7 +323,7 @@ func ExampleHost_NewRoundTripper() { func ExampleWellKnownHandler() { var h libp2phttp.WellKnownHandler - h.AddProtocolMapping("/hello/1", libp2phttp.ProtocolMeta{ + h.AddProtocolMeta("/hello/1", libp2phttp.ProtocolMeta{ Path: "/hello-path/", }) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 36b6fe96ea..aff3477b81 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -81,7 +81,7 @@ func (h *WellKnownHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write(mapping) } -func (h *WellKnownHandler) AddProtocolMapping(p protocol.ID, protocolMeta ProtocolMeta) { +func (h *WellKnownHandler) AddProtocolMeta(p protocol.ID, protocolMeta ProtocolMeta) { h.wellknownMapMu.Lock() if h.wellKnownMapping == nil { h.wellKnownMapping = make(map[protocol.ID]ProtocolMeta) @@ -90,7 +90,7 @@ func (h *WellKnownHandler) AddProtocolMapping(p protocol.ID, protocolMeta Protoc h.wellknownMapMu.Unlock() } -func (h *WellKnownHandler) RemoveProtocolMapping(p protocol.ID) { +func (h *WellKnownHandler) RemoveProtocolMeta(p protocol.ID) { h.wellknownMapMu.Lock() if h.wellKnownMapping != nil { delete(h.wellKnownMapping, p) @@ -115,13 +115,18 @@ type Host struct { TLSConfig *tls.Config // ServeInsecureHTTP indicates if the server should serve unencrypted HTTP requests over TCP. ServeInsecureHTTP bool - // ServeMux is the http.ServeMux used by the server to serve requests + // ServeMux is the http.ServeMux used by the server to serve requests. The + // zero value is a new serve mux. Users may manually add handlers to this + // mux instead of using `SetHTTPHandler`, but if they do, they should also + // update the WellKnownHandler's protocol mapping. ServeMux http.ServeMux // DefaultClientRoundTripper is the default http.RoundTripper for clients DefaultClientRoundTripper *http.Transport - // WellKnownHandler is the http handler for the `.well-known/libp2p` resource + // WellKnownHandler is the http handler for the `.well-known/libp2p` + // resource. It is responsible for sharing this node's protocol metadata + // with other nodes. WellKnownHandler WellKnownHandler // peerMetadata is an lru cache of a peer's well-known protocol map. @@ -339,7 +344,7 @@ func (h *Host) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http.Han // We are nesting this handler under this path, so it should end with a slash. path += "/" } - h.WellKnownHandler.AddProtocolMapping(p, ProtocolMeta{Path: path}) + h.WellKnownHandler.AddProtocolMeta(p, ProtocolMeta{Path: path}) h.ServeMux.Handle(path, http.StripPrefix(path, handler)) } diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 16d7bd8a16..1a7dbc001f 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -221,7 +221,7 @@ func TestPlainOldHTTPServer(t *testing.T) { mux.Handle("/.well-known/libp2p", &wk) mux.Handle("/ping/", httpping.Ping{}) - wk.AddProtocolMapping(httpping.PingProtocolID, libp2phttp.ProtocolMeta{Path: "/ping/"}) + wk.AddProtocolMeta(httpping.PingProtocolID, libp2phttp.ProtocolMeta{Path: "/ping/"}) server := &http.Server{Addr: "127.0.0.1:0", Handler: mux} From 8b9e11df44ee4445cc9e81ed4394d5e6137be438 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 23 Aug 2023 09:58:17 -0700 Subject: [PATCH 56/72] Expand comment on DefaultClientRoundTripper And don't use the http.DefaultRoundTripper and cast --- p2p/http/example_test.go | 16 ++++++++-------- p2p/http/libp2phttp.go | 26 ++++++++++++++++++-------- p2p/http/libp2phttp_test.go | 4 ++-- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go index 426b1f749c..6d25e548d7 100644 --- a/p2p/http/example_test.go +++ b/p2p/http/example_test.go @@ -16,7 +16,7 @@ import ( func ExampleHost_withAStockGoHTTPClient() { server := libp2phttp.Host{ - ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + InsecureAllowHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } @@ -62,7 +62,7 @@ func ExampleHost_listenOnHTTPTransportAndStreams() { log.Fatal(err) } server := libp2phttp.Host{ - ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + InsecureAllowHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50124/http")}, StreamHost: serverStreamHost, } @@ -121,7 +121,7 @@ func ExampleHost_overLibp2pStreams() { func ExampleHost_Serve() { server := libp2phttp.Host{ - ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + InsecureAllowHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50221/http")}, } @@ -135,7 +135,7 @@ func ExampleHost_Serve() { func ExampleHost_SetHTTPHandler() { server := libp2phttp.Host{ - ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + InsecureAllowHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50222/http")}, } @@ -169,7 +169,7 @@ func ExampleHost_SetHTTPHandler() { func ExampleHost_SetHTTPHandlerAtPath() { server := libp2phttp.Host{ - ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + InsecureAllowHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50224/http")}, } @@ -206,7 +206,7 @@ func ExampleHost_NamespacedClient() { // Create the server server := libp2phttp.Host{ - ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + InsecureAllowHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50221/http")}, } @@ -244,7 +244,7 @@ func ExampleHost_NamespaceRoundTripper() { // Create the server server := libp2phttp.Host{ - ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + InsecureAllowHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50223/http")}, } @@ -288,7 +288,7 @@ func ExampleHost_NewRoundTripper() { // Create the server server := libp2phttp.Host{ - ServeInsecureHTTP: true, // For our example, we'll allow insecure HTTP + InsecureAllowHTTP: true, // For our example, we'll allow insecure HTTP ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/50225/http")}, } diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index aff3477b81..067461829a 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -113,15 +113,20 @@ type Host struct { ListenAddrs []ma.Multiaddr // TLSConfig is the TLS config for the server to use TLSConfig *tls.Config - // ServeInsecureHTTP indicates if the server should serve unencrypted HTTP requests over TCP. - ServeInsecureHTTP bool + // InsecureAllowHTTP indicates if the server is allowed to serve unencrypted + // HTTP requests over TCP. + InsecureAllowHTTP bool // ServeMux is the http.ServeMux used by the server to serve requests. The // zero value is a new serve mux. Users may manually add handlers to this // mux instead of using `SetHTTPHandler`, but if they do, they should also // update the WellKnownHandler's protocol mapping. ServeMux http.ServeMux - // DefaultClientRoundTripper is the default http.RoundTripper for clients + // DefaultClientRoundTripper is the default http.RoundTripper for clients to + // use when making requests over an HTTP transport. This must be an + // `*http.Transport` type so that the transport can be cloned and the + // `TLSClientConfig` field can be configured. If unset, it will create a new + // `http.Transport` on first use. DefaultClientRoundTripper *http.Transport // WellKnownHandler is the http handler for the `.well-known/libp2p` @@ -133,7 +138,10 @@ type Host struct { peerMetadata *lru.Cache[peer.ID, PeerMeta] // createHTTPTransport is used to lazily create the httpTransport in a thread-safe way. createHTTPTransport sync.Once - httpTransport *httpTransport + // createDefaultClientRoundTripper is used to lazily create the default + // client round tripper in a thread-safe way. + createDefaultClientRoundTripper sync.Once + httpTransport *httpTransport } type httpTransport struct { @@ -277,7 +285,7 @@ func (h *Host) Serve() error { errCh <- srv.ServeTLS(l, "", "") }() h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) - } else if h.ServeInsecureHTTP { + } else if h.InsecureAllowHTTP { go func() { errCh <- http.Serve(l, &h.ServeMux) }() @@ -579,10 +587,12 @@ func (h *Host) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOption) scheme = "https" } + h.createDefaultClientRoundTripper.Do(func() { + if h.DefaultClientRoundTripper == nil { + h.DefaultClientRoundTripper = &http.Transport{} + } + }) rt := h.DefaultClientRoundTripper - if rt == nil { - rt = http.DefaultTransport.(*http.Transport) - } ownRoundtripper := false if parsed.sni != parsed.host { // We have a different host and SNI (e.g. using an IP address but specifying a SNI) diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 1a7dbc001f..481748d395 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -74,7 +74,7 @@ func TestRoundTrippers(t *testing.T) { require.NoError(t, err) httpHost := libp2phttp.Host{ - ServeInsecureHTTP: true, + InsecureAllowHTTP: true, StreamHost: serverHost, ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } @@ -310,7 +310,7 @@ func TestPlainOldHTTPServer(t *testing.T) { func TestHostZeroValue(t *testing.T) { server := libp2phttp.Host{ - ServeInsecureHTTP: true, + InsecureAllowHTTP: true, ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")}, } server.SetHTTPHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) })) From eb9aa17f46336f549f2c816fca5331f57ce522ae Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 23 Aug 2023 10:13:07 -0700 Subject: [PATCH 57/72] Remove todo Covered by https://github.com/libp2p/go-libp2p/issues/2511 --- p2p/http/libp2phttp.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 067461829a..6add46fede 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -33,9 +33,6 @@ const ProtocolIDForMultistreamSelect = "/http/1.1" const peerMetadataLimit = 8 << 10 // 8KB const peerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache -// TODOs: -// - integrate with the conn gater and resource manager - // ProtocolMeta is metadata about a protocol. type ProtocolMeta struct { // Path defines the HTTP Path prefix used for this protocol From 791e0f3b9b11fb16bd29aa518a19e5e1fa02aa14 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 23 Aug 2023 10:13:33 -0700 Subject: [PATCH 58/72] Fix comment typo --- p2p/http/libp2phttp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 6add46fede..140c6f784c 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -105,7 +105,7 @@ type Host struct { // StreamHost is a stream based libp2p host used to do HTTP over libp2p streams. May be nil StreamHost host.Host // ListenAddrs are the requested addresses to listen on. Multiaddrs must be - // a valid HTTP(s) multiaddr. Only multiaddrs for an HTTP transport are + // valid HTTP(s) multiaddr. Only multiaddrs for an HTTP transport are // supported (must end with /http or /https). ListenAddrs []ma.Multiaddr // TLSConfig is the TLS config for the server to use From e53fa35ee70da5eb86127ae567831939ad354d4f Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 23 Aug 2023 10:16:27 -0700 Subject: [PATCH 59/72] Fix comment typo --- p2p/http/libp2phttp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 140c6f784c..18b9cef6f8 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -131,7 +131,7 @@ type Host struct { // with other nodes. WellKnownHandler WellKnownHandler - // peerMetadata is an lru cache of a peer's well-known protocol map. + // 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. createHTTPTransport sync.Once From 419660b7cec91fad216a364699025317e0a3dfcb Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 23 Aug 2023 10:16:43 -0700 Subject: [PATCH 60/72] Create helper init fn --- p2p/http/libp2phttp.go | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 18b9cef6f8..edc75b7888 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -148,13 +148,6 @@ type httpTransport struct { waitingForListeners chan struct{} } -func newHTTPTransport() *httpTransport { - return &httpTransport{ - closeListeners: make(chan struct{}), - waitingForListeners: make(chan struct{}), - } -} - func newPeerMetadataCache() *lru.Cache[peer.ID, PeerMeta] { peerMetadata, err := lru.New[peer.ID, PeerMeta](peerMetadataLRUSize) if err != nil { @@ -164,10 +157,17 @@ func newPeerMetadataCache() *lru.Cache[peer.ID, PeerMeta] { return peerMetadata } -func (h *Host) Addrs() []ma.Multiaddr { +func (h *Host) httpTransportInit() { h.createHTTPTransport.Do(func() { - h.httpTransport = newHTTPTransport() + h.httpTransport = &httpTransport{ + closeListeners: make(chan struct{}), + waitingForListeners: make(chan struct{}), + } }) +} + +func (h *Host) Addrs() []ma.Multiaddr { + h.httpTransportInit() <-h.httpTransport.waitingForListeners return h.httpTransport.listenAddrs } @@ -195,9 +195,7 @@ func (h *Host) Serve() error { h.ServeMux.Handle("/.well-known/libp2p", &h.WellKnownHandler) - h.createHTTPTransport.Do(func() { - h.httpTransport = newHTTPTransport() - }) + h.httpTransportInit() closedWaitingForListeners := false defer func() { @@ -325,9 +323,7 @@ func (h *Host) Serve() error { } func (h *Host) Close() error { - h.createHTTPTransport.Do(func() { - h.httpTransport = newHTTPTransport() - }) + h.httpTransportInit() close(h.httpTransport.closeListeners) return nil } From 80fbb7dc6e6abdcb14e3f5fc6c3406a6bf2dc5db Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 24 Aug 2023 11:55:45 -0700 Subject: [PATCH 61/72] Rename NewRoundTripper to NewConstrainedRoundTripper --- p2p/http/example_test.go | 6 +++--- p2p/http/libp2phttp.go | 6 +++--- p2p/http/libp2phttp_test.go | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go index 6d25e548d7..f0b49b0e9d 100644 --- a/p2p/http/example_test.go +++ b/p2p/http/example_test.go @@ -257,7 +257,7 @@ func ExampleHost_NamespaceRoundTripper() { defer server.Close() // Create an http.Roundtripper for the server - rt, err := client.NewRoundTripper(peer.AddrInfo{ID: server.PeerID(), Addrs: server.Addrs()}) + rt, err := client.NewConstrainedRoundTripper(peer.AddrInfo{ID: server.PeerID(), Addrs: server.Addrs()}) if err != nil { log.Fatal(err) } @@ -283,7 +283,7 @@ func ExampleHost_NamespaceRoundTripper() { // Output: Hello World } -func ExampleHost_NewRoundTripper() { +func ExampleHost_NewConstrainedRoundTripper() { var client libp2phttp.Host // Create the server @@ -301,7 +301,7 @@ func ExampleHost_NewRoundTripper() { defer server.Close() // Create an http.Roundtripper for the server - rt, err := client.NewRoundTripper(peer.AddrInfo{ID: server.PeerID(), Addrs: server.Addrs()}) + rt, err := client.NewConstrainedRoundTripper(peer.AddrInfo{ID: server.PeerID(), Addrs: server.Addrs()}) if err != nil { log.Fatal(err) } diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index edc75b7888..a973aab1b2 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -523,7 +523,7 @@ func (h *Host) NamespaceRoundTripper(roundtripper http.RoundTripper, p protocol. // and namespacing the roundripper yourself, then creating clients from the // namespace round tripper. func (h *Host) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...RoundTripperOption) (http.Client, error) { - rt, err := h.NewRoundTripper(server, opts...) + rt, err := h.NewConstrainedRoundTripper(server, opts...) if err != nil { return http.Client{}, err } @@ -536,10 +536,10 @@ func (h *Host) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...Rou return http.Client{Transport: nrt}, nil } -// NewRoundTripper returns an http.RoundTripper that can fulfill and HTTP +// NewConstrainedRoundTripper returns an http.RoundTripper that can fulfill and HTTP // request to the given server. It may use an HTTP transport or a stream based // transport. It is valid to pass an empty server.ID. -func (h *Host) NewRoundTripper(server peer.AddrInfo, opts ...RoundTripperOption) (http.RoundTripper, error) { +func (h *Host) NewConstrainedRoundTripper(server peer.AddrInfo, opts ...RoundTripperOption) (http.RoundTripper, error) { options := roundTripperOpts{} for _, o := range opts { options = o(options) diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go index 481748d395..1114b5a9d9 100644 --- a/p2p/http/libp2phttp_test.go +++ b/p2p/http/libp2phttp_test.go @@ -52,7 +52,7 @@ func TestHTTPOverStreams(t *testing.T) { Addrs: serverHost.Addrs(), }) - clientRT, err := (&libp2phttp.Host{StreamHost: clientHost}).NewRoundTripper(peer.AddrInfo{ID: serverHost.ID()}) + clientRT, err := (&libp2phttp.Host{StreamHost: clientHost}).NewConstrainedRoundTripper(peer.AddrInfo{ID: serverHost.ID()}) require.NoError(t, err) client := &http.Client{Transport: clientRT} @@ -98,7 +98,7 @@ func TestRoundTrippers(t *testing.T) { { name: "HTTP preferred", setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper { - rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ + rt, err := clientHTTPHost.NewConstrainedRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: serverMultiaddrs, }, libp2phttp.PreferHTTPTransport) @@ -109,7 +109,7 @@ func TestRoundTrippers(t *testing.T) { { name: "HTTP first", setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper { - rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ + rt, err := clientHTTPHost.NewConstrainedRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHTTPAddr, serverHost.Addrs()[0]}, }) @@ -120,7 +120,7 @@ func TestRoundTrippers(t *testing.T) { { name: "No HTTP transport", setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper { - rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ + rt, err := clientHTTPHost.NewConstrainedRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHost.Addrs()[0]}, }) @@ -132,7 +132,7 @@ func TestRoundTrippers(t *testing.T) { { name: "Stream transport first", setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper { - rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ + rt, err := clientHTTPHost.NewConstrainedRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHost.Addrs()[0], serverHTTPAddr}, }) @@ -148,7 +148,7 @@ func TestRoundTrippers(t *testing.T) { ID: serverHost.ID(), Addrs: serverHost.Addrs(), }) - rt, err := clientHTTPHost.NewRoundTripper(peer.AddrInfo{ + rt, err := clientHTTPHost.NewConstrainedRoundTripper(peer.AddrInfo{ ID: serverHost.ID(), Addrs: []ma.Multiaddr{serverHTTPAddr, serverHost.Addrs()[0]}, }) @@ -244,7 +244,7 @@ func TestPlainOldHTTPServer(t *testing.T) { name: "using libp2phttp", do: func(t *testing.T, request *http.Request) (*http.Response, error) { var clientHttpHost libp2phttp.Host - rt, err := clientHttpHost.NewRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) + rt, err := clientHttpHost.NewConstrainedRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) client := &http.Client{Transport: rt} @@ -252,7 +252,7 @@ func TestPlainOldHTTPServer(t *testing.T) { }, getWellKnown: func(t *testing.T) (libp2phttp.PeerMeta, error) { var clientHttpHost libp2phttp.Host - rt, err := clientHttpHost.NewRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) + rt, err := clientHttpHost.NewConstrainedRoundTripper(peer.AddrInfo{Addrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/" + serverAddrParts[1] + "/http")}}) require.NoError(t, err) return rt.(libp2phttp.PeerMetadataGetter).GetPeerMetadata() }, From 8c18fe4d074a9321155bb07f437dcce987ba88c3 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 25 Aug 2023 15:27:10 -0700 Subject: [PATCH 62/72] Use pointer for Host.ServeMux --- p2p/http/libp2phttp.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index a973aab1b2..9bece2db72 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -113,11 +113,12 @@ type Host struct { // InsecureAllowHTTP indicates if the server is allowed to serve unencrypted // HTTP requests over TCP. InsecureAllowHTTP bool - // ServeMux is the http.ServeMux used by the server to serve requests. The - // zero value is a new serve mux. Users may manually add handlers to this + // ServeMux is the http.ServeMux used by the server to serve requests. If nil, + // new serve mux will be allocated. Users may manually add handlers to this // mux instead of using `SetHTTPHandler`, but if they do, they should also // update the WellKnownHandler's protocol mapping. - ServeMux http.ServeMux + ServeMux *http.ServeMux + initializeServeMux sync.Once // DefaultClientRoundTripper is the default http.RoundTripper for clients to // use when making requests over an HTTP transport. This must be an @@ -166,6 +167,12 @@ func (h *Host) httpTransportInit() { }) } +func (h *Host) serveMuxInit() { + h.initializeServeMux.Do(func() { + h.ServeMux = http.NewServeMux() + }) +} + func (h *Host) Addrs() []ma.Multiaddr { h.httpTransportInit() <-h.httpTransport.waitingForListeners @@ -193,6 +200,7 @@ func (h *Host) Serve() error { } } + h.serveMuxInit() h.ServeMux.Handle("/.well-known/libp2p", &h.WellKnownHandler) h.httpTransportInit() @@ -227,7 +235,7 @@ func (h *Host) Serve() error { h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, h.StreamHost.Addrs()...) go func() { - errCh <- http.Serve(listener, &h.ServeMux) + errCh <- http.Serve(listener, h.ServeMux) }() } @@ -274,7 +282,7 @@ func (h *Host) Serve() error { if parsedAddr.useHTTPS { go func() { srv := http.Server{ - Handler: &h.ServeMux, + Handler: h.ServeMux, TLSConfig: h.TLSConfig, } errCh <- srv.ServeTLS(l, "", "") @@ -282,7 +290,7 @@ func (h *Host) Serve() error { h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) } else if h.InsecureAllowHTTP { go func() { - errCh <- http.Serve(l, &h.ServeMux) + errCh <- http.Serve(l, h.ServeMux) }() h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) } else { @@ -346,6 +354,7 @@ func (h *Host) SetHTTPHandlerAtPath(p protocol.ID, path string, handler http.Han path += "/" } h.WellKnownHandler.AddProtocolMeta(p, ProtocolMeta{Path: path}) + h.serveMuxInit() h.ServeMux.Handle(path, http.StripPrefix(path, handler)) } From c65d655b4c1b27d241e38b819b5f14ce858a3fbf Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 28 Aug 2023 11:20:50 -0700 Subject: [PATCH 63/72] Don't ignore err --- p2p/http/libp2phttp.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 9bece2db72..7f4288df35 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -739,7 +739,10 @@ func (h *Host) getAndStorePeerMetadata(roundtripper http.RoundTripper, server pe } meta := PeerMeta{} - json.Unmarshal(body[:bytesRead], &meta) + err = json.Unmarshal(body[:bytesRead], &meta) + if err != nil { + return nil, err + } if server != "" { h.peerMetadata.Add(server, meta) } From 34c5818e79c419907a2582be1682aa4959b147a9 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 28 Aug 2023 11:24:23 -0700 Subject: [PATCH 64/72] json decode from reader --- p2p/http/libp2phttp.go | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 7f4288df35..d2348f7239 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -722,24 +722,11 @@ func (h *Host) getAndStorePeerMetadata(roundtripper http.RoundTripper, server pe return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } - body := [peerMetadataLimit]byte{} - bytesRead := 0 - for { - n, err := resp.Body.Read(body[bytesRead:]) - bytesRead += n - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - if bytesRead >= peerMetadataLimit { - return nil, fmt.Errorf("peer metadata too large") - } - } - meta := PeerMeta{} - err = json.Unmarshal(body[:bytesRead], &meta) + err = json.NewDecoder(&io.LimitedReader{ + R: resp.Body, + N: 8 << 10, + }).Decode(&meta) if err != nil { return nil, err } From a4de205df991ce927e50da8840743cf49c75ec97 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 28 Aug 2023 11:33:18 -0700 Subject: [PATCH 65/72] Nits --- p2p/http/libp2phttp.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index d2348f7239..9adbfc2100 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -113,10 +113,10 @@ type Host struct { // InsecureAllowHTTP indicates if the server is allowed to serve unencrypted // HTTP requests over TCP. InsecureAllowHTTP bool - // ServeMux is the http.ServeMux used by the server to serve requests. If nil, - // new serve mux will be allocated. Users may manually add handlers to this - // mux instead of using `SetHTTPHandler`, but if they do, they should also - // update the WellKnownHandler's protocol mapping. + // ServeMux is the http.ServeMux used by the server to serve requests. If + // nil, a new serve mux will be created. Users may manually add handlers to + // this mux instead of using `SetHTTPHandler`, but if they do, they should + // also update the WellKnownHandler's protocol mapping. ServeMux *http.ServeMux initializeServeMux sync.Once @@ -129,7 +129,9 @@ type Host struct { // WellKnownHandler is the http handler for the `.well-known/libp2p` // resource. It is responsible for sharing this node's protocol metadata - // with other nodes. + // with other nodes. Users only care about this if they set their own + // ServeMux with pre-existing routes. By default, new protocols are added + // here when a user calls `SetHTTPHandler` or `SetHTTPHandlerAtPath`. WellKnownHandler WellKnownHandler // peerMetadata is an LRU cache of a peer's well-known protocol map. @@ -276,7 +278,6 @@ func (h *Host) Serve() error { scheme = "https" } listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/%s", host, port, scheme)) - } if parsedAddr.useHTTPS { From a6176a71e048cede8773612cb101a378e6a157c6 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 28 Aug 2023 11:38:31 -0700 Subject: [PATCH 66/72] Move setupListeners to method --- p2p/http/libp2phttp.go | 110 +++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 9adbfc2100..321fa29cfb 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -191,6 +191,61 @@ func (h *Host) PeerID() peer.ID { var ErrNoListeners = errors.New("nothing to listen on") +func (h *Host) setupListeners(listenerErrCh chan error) error { + for _, addr := range h.ListenAddrs { + parsedAddr := parseMultiaddr(addr) + // resolve the host + ipaddr, err := net.ResolveIPAddr("ip", parsedAddr.host) + if err != nil { + return err + } + + host := ipaddr.String() + l, err := net.Listen("tcp", host+":"+parsedAddr.port) + if err != nil { + return err + } + h.httpTransport.listeners = append(h.httpTransport.listeners, l) + + // get resolved port + _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + return err + } + + var listenAddr ma.Multiaddr + if parsedAddr.useHTTPS && parsedAddr.sni != "" && parsedAddr.sni != host { + listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/tls/sni/%s/http", host, port, parsedAddr.sni)) + } else { + scheme := "http" + if parsedAddr.useHTTPS { + scheme = "https" + } + listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/%s", host, port, scheme)) + } + + if parsedAddr.useHTTPS { + go func() { + srv := http.Server{ + Handler: h.ServeMux, + TLSConfig: h.TLSConfig, + } + listenerErrCh <- srv.ServeTLS(l, "", "") + }() + h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) + } else if h.InsecureAllowHTTP { + go func() { + listenerErrCh <- http.Serve(l, h.ServeMux) + }() + h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) + } else { + // We are not serving insecure HTTP + log.Warnf("Not serving insecure HTTP on %s. Prefer an HTTPS endpoint.", listenAddr) + } + } + return nil +} + // Serve starts the HTTP transport listeners. Always returns a non-nil error. // If there are no listeners, returns ErrNoListeners. func (h *Host) Serve() error { @@ -247,60 +302,7 @@ func (h *Host) Serve() error { } } - err := func() error { - for _, addr := range h.ListenAddrs { - parsedAddr := parseMultiaddr(addr) - // resolve the host - ipaddr, err := net.ResolveIPAddr("ip", parsedAddr.host) - if err != nil { - return err - } - - host := ipaddr.String() - l, err := net.Listen("tcp", host+":"+parsedAddr.port) - if err != nil { - return err - } - h.httpTransport.listeners = append(h.httpTransport.listeners, l) - - // get resolved port - _, port, err := net.SplitHostPort(l.Addr().String()) - if err != nil { - return err - } - - var listenAddr ma.Multiaddr - if parsedAddr.useHTTPS && parsedAddr.sni != "" && parsedAddr.sni != host { - listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/tls/sni/%s/http", host, port, parsedAddr.sni)) - } else { - scheme := "http" - if parsedAddr.useHTTPS { - scheme = "https" - } - listenAddr = ma.StringCast(fmt.Sprintf("/ip4/%s/tcp/%s/%s", host, port, scheme)) - } - - if parsedAddr.useHTTPS { - go func() { - srv := http.Server{ - Handler: h.ServeMux, - TLSConfig: h.TLSConfig, - } - errCh <- srv.ServeTLS(l, "", "") - }() - h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) - } else if h.InsecureAllowHTTP { - go func() { - errCh <- http.Serve(l, h.ServeMux) - }() - h.httpTransport.listenAddrs = append(h.httpTransport.listenAddrs, listenAddr) - } else { - // We are not serving insecure HTTP - log.Warnf("Not serving insecure HTTP on %s. Prefer an HTTPS endpoint.", listenAddr) - } - } - return nil - }() + err := h.setupListeners(errCh) if err != nil { closeAllListeners() return err From fcd5c05e0be4715ca03cbc2f2be9e6ef2dc6abaf Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 28 Aug 2023 11:53:10 -0700 Subject: [PATCH 67/72] Add comment for streamReadCloser --- p2p/http/libp2phttp.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 321fa29cfb..94362ca37d 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -372,6 +372,11 @@ type streamRoundTripper struct { httpHost *Host } +// streamReadCloser wraps an io.ReadCloser and closes the underlying stream when +// closed (as well as closing the wrapped ReadCloser). This is necessary because +// we have two things to close, the body and the stream. The stream isn't closed +// by the body automatically, as hinted at by the fact that `http.ReadResponse` +// takes a bufio.Reader. type streamReadCloser struct { io.ReadCloser s network.Stream From 39adff6159c0f8ecc44a937f76b4b9b7ddd0b997 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 28 Aug 2023 11:58:59 -0700 Subject: [PATCH 68/72] Add more comments --- p2p/http/libp2phttp.go | 13 +++++++++++-- p2p/http/options.go | 5 ++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 94362ca37d..9ee8b056ff 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -478,6 +478,8 @@ func (rt *roundTripperForSpecificServer) CloseIdleConnections() { // connections } +// namespacedRoundTripper is a round tripper that prefixes all requests with a +// given path prefix. It is used to namespace requests to a specific protocol. type namespacedRoundTripper struct { http.RoundTripper protocolPrefix string @@ -556,13 +558,20 @@ func (h *Host) NamespacedClient(p protocol.ID, server peer.AddrInfo, opts ...Rou // NewConstrainedRoundTripper returns an http.RoundTripper that can fulfill and HTTP // request to the given server. It may use an HTTP transport or a stream based // transport. It is valid to pass an empty server.ID. +// If there are multiple addresses for the server, it will pick the best +// transport (stream vs standard HTTP) using the following rules: +// - If PreferHTTPTransport is set, use the HTTP transport. +// - If ServerMustAuthenticatePeerID is set, use the stream transport, as the +// HTTP transport does not do peer id auth yet. +// - If we already have a connection on a stream transport, use that. +// - Otherwise, if we have both, use the HTTP transport. func (h *Host) NewConstrainedRoundTripper(server peer.AddrInfo, opts ...RoundTripperOption) (http.RoundTripper, error) { options := roundTripperOpts{} for _, o := range opts { options = o(options) } - if options.ServerMustAuthenticatePeerID && server.ID == "" { + if options.serverMustAuthenticatePeerID && server.ID == "" { return nil, fmt.Errorf("server must authenticate peer ID, but no peer ID provided") } @@ -590,7 +599,7 @@ func (h *Host) NewConstrainedRoundTripper(server peer.AddrInfo, opts ...RoundTri } // Currently the HTTP transport can not authenticate peer IDs. - if !options.ServerMustAuthenticatePeerID && len(httpAddrs) > 0 && (options.preferHTTPTransport || (firstAddrIsHTTP && !existingStreamConn)) { + if !options.serverMustAuthenticatePeerID && len(httpAddrs) > 0 && (options.preferHTTPTransport || (firstAddrIsHTTP && !existingStreamConn)) { parsed := parseMultiaddr(httpAddrs[0]) scheme := "http" if parsed.useHTTPS { diff --git a/p2p/http/options.go b/p2p/http/options.go index a4d926431d..0062e59319 100644 --- a/p2p/http/options.go +++ b/p2p/http/options.go @@ -3,9 +3,8 @@ package libp2phttp type RoundTripperOption func(o roundTripperOpts) roundTripperOpts type roundTripperOpts struct { - // todo SkipClientAuth bool preferHTTPTransport bool - ServerMustAuthenticatePeerID bool + serverMustAuthenticatePeerID bool } // PreferHTTPTransport tells the roundtripper constructor to prefer using an @@ -20,6 +19,6 @@ func PreferHTTPTransport(o roundTripperOpts) roundTripperOpts { // authenticate the Server's PeerID. Note: this currently means we can not use a // native HTTP transport (HTTP peer id authentication is not yet implemented: https://github.com/libp2p/specs/pull/564). func ServerMustAuthenticatePeerID(o roundTripperOpts) roundTripperOpts { - o.ServerMustAuthenticatePeerID = true + o.serverMustAuthenticatePeerID = true return o } From 5f75f4e975d34d0ebe2fd433cef31cb5ab284d07 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 28 Aug 2023 12:04:16 -0700 Subject: [PATCH 69/72] Add todo --- p2p/http/libp2phttp.go | 1 + 1 file changed, 1 insertion(+) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 9ee8b056ff..4b69702596 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -406,6 +406,7 @@ func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) } }() + // TODO: Adhere to the request.Context resp, err := http.ReadResponse(bufio.NewReader(s), r) if err != nil { return nil, err From 9bca68a8e0951f8703716ebce5016d8733e64186 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 28 Aug 2023 12:13:56 -0700 Subject: [PATCH 70/72] Defer connect until the round trip --- p2p/http/libp2phttp.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 4b69702596..55eb3d497e 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -4,7 +4,6 @@ package libp2phttp import ( "bufio" - "context" "crypto/tls" "encoding/json" "errors" @@ -22,6 +21,7 @@ import ( host "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/peerstore" "github.com/libp2p/go-libp2p/core/protocol" gostream "github.com/libp2p/go-libp2p/p2p/gostream" ma "github.com/multiformats/go-multiaddr" @@ -367,9 +367,11 @@ type PeerMetadataGetter interface { } type streamRoundTripper struct { - server peer.ID - h host.Host - httpHost *Host + server peer.ID + addrsAdded sync.Once + serverAddrs []ma.Multiaddr + h host.Host + httpHost *Host } // streamReadCloser wraps an io.ReadCloser and closes the underlying stream when @@ -393,6 +395,14 @@ func (rt *streamRoundTripper) GetPeerMetadata() (PeerMeta, error) { // RoundTrip implements http.RoundTripper. func (rt *streamRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + // Add the addresses we learned about for this server + rt.addrsAdded.Do(func() { + if len(rt.serverAddrs) > 0 { + rt.h.Peerstore().AddAddrs(rt.server, rt.serverAddrs, peerstore.TempAddrTTL) + } + rt.serverAddrs = nil // may as well cleanup + }) + s, err := rt.h.NewStream(r.Context(), rt.server, ProtocolIDForMultistreamSelect) if err != nil { return nil, err @@ -641,13 +651,9 @@ func (h *Host) NewConstrainedRoundTripper(server peer.AddrInfo, opts ...RoundTri if server.ID == "" { return nil, fmt.Errorf("can not use the HTTP transport, and no server peer ID provided") } - err := h.StreamHost.Connect(context.Background(), peer.AddrInfo{ID: server.ID, Addrs: nonHTTPAddrs}) - if err != nil { - return nil, fmt.Errorf("failed to connect to peer: %w", err) - } } - return &streamRoundTripper{h: h.StreamHost, server: server.ID, httpHost: h}, nil + return &streamRoundTripper{h: h.StreamHost, server: server.ID, serverAddrs: nonHTTPAddrs, httpHost: h}, nil } type httpMultiaddr struct { From 1ecf64826fca4c5a62000c02e1b673680f0a314d Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 28 Aug 2023 13:10:40 -0700 Subject: [PATCH 71/72] Rebase gostream --- p2p/http/libp2phttp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 55eb3d497e..28f5c7a82d 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -23,7 +23,7 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" "github.com/libp2p/go-libp2p/core/protocol" - gostream "github.com/libp2p/go-libp2p/p2p/gostream" + gostream "github.com/libp2p/go-libp2p/p2p/net/gostream" ma "github.com/multiformats/go-multiaddr" ) From 04ec0c0f671fce448bc5876dcf9ee8621eb9370c Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 28 Aug 2023 15:41:15 -0700 Subject: [PATCH 72/72] Update p2p/http/libp2phttp.go Co-authored-by: Andrew Gillis --- p2p/http/libp2phttp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go index 28f5c7a82d..42c4333b85 100644 --- a/p2p/http/libp2phttp.go +++ b/p2p/http/libp2phttp.go @@ -749,7 +749,7 @@ func (h *Host) getAndStorePeerMetadata(roundtripper http.RoundTripper, server pe meta := PeerMeta{} err = json.NewDecoder(&io.LimitedReader{ R: resp.Body, - N: 8 << 10, + N: peerMetadataLimit, }).Decode(&meta) if err != nil { return nil, err