diff --git a/p2p/http/example_test.go b/p2p/http/example_test.go new file mode 100644 index 0000000000..f0b49b0e9d --- /dev/null +++ b/p2p/http/example_test.go @@ -0,0 +1,353 @@ +package libp2phttp_test + +import ( + "fmt" + "io" + "log" + "net" + "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" +) + +func ExampleHost_withAStockGoHTTPClient() { + server := libp2phttp.Host{ + InsecureAllowHTTP: 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 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.Host{ + 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, + } + 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 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.Host{ + 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.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 + // 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 +} + +func ExampleHost_Serve() { + server := libp2phttp.Host{ + InsecureAllowHTTP: 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 ExampleHost_SetHTTPHandler() { + server := libp2phttp.Host{ + InsecureAllowHTTP: 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 ExampleHost_SetHTTPHandlerAtPath() { + server := libp2phttp.Host{ + InsecureAllowHTTP: 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 ExampleHost_NamespacedClient() { + var client libp2phttp.Host + + // Create the server + server := libp2phttp.Host{ + InsecureAllowHTTP: 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 ExampleHost_NamespaceRoundTripper() { + var client libp2phttp.Host + + // Create the server + server := libp2phttp.Host{ + InsecureAllowHTTP: 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.NewConstrainedRoundTripper(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 ExampleHost_NewConstrainedRoundTripper() { + var client libp2phttp.Host + + // Create the server + server := libp2phttp.Host{ + InsecureAllowHTTP: 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.NewConstrainedRoundTripper(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 +} + +func ExampleWellKnownHandler() { + var h libp2phttp.WellKnownHandler + h.AddProtocolMeta("/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 libp2phttp.Host. + 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/"}} + +} diff --git a/p2p/http/libp2phttp.go b/p2p/http/libp2phttp.go new file mode 100644 index 0000000000..42c4333b85 --- /dev/null +++ b/p2p/http/libp2phttp.go @@ -0,0 +1,804 @@ +// 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](https://github.com/libp2p/specs/pull/508). +package libp2phttp + +import ( + "bufio" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "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" + "github.com/libp2p/go-libp2p/core/peerstore" + "github.com/libp2p/go-libp2p/core/protocol" + gostream "github.com/libp2p/go-libp2p/p2p/net/gostream" + 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 + +// ProtocolMeta is metadata about a protocol. +type ProtocolMeta struct { + // Path defines the HTTP Path prefix used for this protocol + Path string `json:"path"` +} + +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 PeerMeta +} + +// 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 (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) + return + } + + if r.Method != http.MethodGet { + http.Error(w, "Only GET requests are supported", http.StatusMethodNotAllowed) + return + } + + // 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, "Marshal error", http.StatusInternalServerError) + return + } + w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Length", strconv.Itoa(len(mapping))) + w.Write(mapping) +} + +func (h *WellKnownHandler) AddProtocolMeta(p protocol.ID, protocolMeta ProtocolMeta) { + h.wellknownMapMu.Lock() + if h.wellKnownMapping == nil { + h.wellKnownMapping = make(map[protocol.ID]ProtocolMeta) + } + h.wellKnownMapping[p] = protocolMeta + h.wellknownMapMu.Unlock() +} + +func (h *WellKnownHandler) RemoveProtocolMeta(p protocol.ID) { + h.wellknownMapMu.Lock() + if h.wellKnownMapping != nil { + delete(h.wellKnownMapping, p) + } + h.wellknownMapMu.Unlock() +} + +// 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 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 + // 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 + // 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, 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 + + // 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` + // resource. It is responsible for sharing this node's protocol metadata + // 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. + peerMetadata *lru.Cache[peer.ID, PeerMeta] + // createHTTPTransport is used to lazily create the httpTransport in a thread-safe way. + createHTTPTransport sync.Once + // createDefaultClientRoundTripper is used to lazily create the default + // client round tripper in a thread-safe way. + createDefaultClientRoundTripper sync.Once + httpTransport *httpTransport +} + +type httpTransport struct { + listenAddrs []ma.Multiaddr + listeners []net.Listener + closeListeners chan struct{} + waitingForListeners chan struct{} +} + +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) + } + return peerMetadata +} + +func (h *Host) httpTransportInit() { + h.createHTTPTransport.Do(func() { + h.httpTransport = &httpTransport{ + closeListeners: make(chan struct{}), + waitingForListeners: make(chan struct{}), + } + }) +} + +func (h *Host) serveMuxInit() { + h.initializeServeMux.Do(func() { + h.ServeMux = http.NewServeMux() + }) +} + +func (h *Host) Addrs() []ma.Multiaddr { + h.httpTransportInit() + <-h.httpTransport.waitingForListeners + 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 *Host) PeerID() peer.ID { + if h.StreamHost != nil { + return h.StreamHost.ID() + } + return "" +} + +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 { + // 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.serveMuxInit() + h.ServeMux.Handle("/.well-known/libp2p", &h.WellKnownHandler) + + h.httpTransportInit() + + closedWaitingForListeners := false + defer func() { + if !closedWaitingForListeners { + close(h.httpTransport.waitingForListeners) + } + }() + + if len(h.ListenAddrs) == 0 && h.StreamHost == nil { + return ErrNoListeners + } + + 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()) + } + 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 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.ServeMux) + }() + } + + closeAllListeners := func() { + for _, l := range h.httpTransport.listeners { + l.Close() + } + } + + err := h.setupListeners(errCh) + if err != nil { + closeAllListeners() + return err + } + + 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) + 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 *Host) Close() error { + h.httpTransportInit() + close(h.httpTransport.closeListeners) + return nil +} + +// 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 +// its prefix path. +func (h *Host) 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 +// its prefix path. +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 += "/" + } + h.WellKnownHandler.AddProtocolMeta(p, ProtocolMeta{Path: path}) + h.serveMuxInit() + h.ServeMux.Handle(path, http.StripPrefix(path, handler)) +} + +// PeerMetadataGetter lets RoundTrippers implement a specific way of caching a peer's protocol mapping. +type PeerMetadataGetter interface { + GetPeerMetadata() (PeerMeta, error) +} + +type streamRoundTripper struct { + 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 +// 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 +} + +func (s *streamReadCloser) Close() error { + s.s.Close() + return s.ReadCloser.Close() +} + +func (rt *streamRoundTripper) GetPeerMetadata() (PeerMeta, error) { + return rt.httpHost.getAndStorePeerMetadata(rt, rt.server) +} + +// 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 + } + + go func() { + defer s.CloseWrite() + r.Write(s) + if r.Body != nil { + r.Body.Close() + } + }() + + // TODO: Adhere to the request.Context + resp, err := http.ReadResponse(bufio.NewReader(s), r) + if err != nil { + return nil, err + } + resp.Body = &streamReadCloser{resp.Body, s} + + return resp, nil +} + +// 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 + ownRoundtripper bool + httpHost *Host + server peer.ID + targetServerAddr string + sni string + scheme string + cachedProtos PeerMeta +} + +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 GetPeerMetadata, use that + if g, ok := rt.RoundTripper.(PeerMetadataGetter); ok { + wk, err := g.GetPeerMetadata() + if err == nil { + rt.cachedProtos = wk + return wk, nil + } + } + + wk, err := rt.httpHost.getAndStorePeerMetadata(rt, rt.server) + if err == nil { + rt.cachedProtos = wk + return wk, nil + } + return wk, err +} + +// 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.sni + return rt.RoundTripper.RoundTrip(r) +} + +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 +} + +// 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 + protocolPrefixRaw string +} + +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 GetPeerMetadata") +} + +// 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 *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 + } + + v, ok := protos[p] + if !ok { + return &namespacedRoundTripper{}, fmt.Errorf("no protocol %s for server %s", p, server) + } + + 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) + } + + return &namespacedRoundTripper{ + RoundTripper: roundtripper, + protocolPrefix: u.Path, + protocolPrefixRaw: u.RawPath, + }, nil +} + +// 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 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.NewConstrainedRoundTripper(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 +} + +// 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 == "" { + return nil, fmt.Errorf("server must authenticate peer ID, but no peer ID provided") + } + + 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 != "" && h.StreamHost != nil { + existingStreamConn = len(h.StreamHost.Network().ConnsToPeer(server.ID)) > 0 + } + + // 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 { + scheme = "https" + } + + h.createDefaultClientRoundTripper.Do(func() { + if h.DefaultClientRoundTripper == nil { + h.DefaultClientRoundTripper = &http.Transport{} + } + }) + rt := h.DefaultClientRoundTripper + 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.ServerName = parsed.sni + ownRoundtripper = true + } + + return &roundTripperForSpecificServer{ + RoundTripper: rt, + ownRoundtripper: ownRoundtripper, + httpHost: h, + server: server.ID, + targetServerAddr: parsed.host + ":" + parsed.port, + sni: parsed.sni, + scheme: scheme, + }, nil + } + + // Otherwise use a stream based transport + 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") + } + } + + return &streamRoundTripper{h: h.StreamHost, server: server.ID, serverAddrs: nonHTTPAddrs, httpHost: h}, 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 { + switch c.Protocol().Code { + case ma.P_IP4, ma.P_IP6, ma.P_DNS, ma.P_DNS4, ma.P_DNS6: + out.host = c.Value() + case ma.P_TCP, ma.P_UDP: + out.port = c.Value() + case ma.P_TLS, ma.P_HTTPS: + out.useHTTPS = true + case 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 +} + +var httpComponent, _ = 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. +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) + if afterHTTPS == nil { + return ma.Join(beforeHTTPS, tlsComponent, httpComponent), isHTTPMultiaddr + } + + return ma.Join(beforeHTTPS, tlsComponent, httpComponent, afterHTTPS), isHTTPMultiaddr +} + +// 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 *Host) getAndStorePeerMetadata(roundtripper http.RoundTripper, server peer.ID) (PeerMeta, error) { + if h.peerMetadata == nil { + h.peerMetadata = newPeerMetadataCache() + } + if meta, ok := h.peerMetadata.Get(server); server != "" && ok { + return meta, nil + } + + req, err := http.NewRequest("GET", "/.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) + } + + meta := PeerMeta{} + err = json.NewDecoder(&io.LimitedReader{ + R: resp.Body, + N: peerMetadataLimit, + }).Decode(&meta) + if err != nil { + return nil, err + } + if server != "" { + h.peerMetadata.Add(server, meta) + } + + return meta, nil +} + +// 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) 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 { + return nil, false + } + return h.peerMetadata.Get(server) +} + +// RemovePeerMetadata removes a peer's protocol metadata from the http host +func (h *Host) RemovePeerMetadata(server peer.ID) { + if h.peerMetadata == nil { + return + } + h.peerMetadata.Remove(server) +} diff --git a/p2p/http/libp2phttp_test.go b/p2p/http/libp2phttp_test.go new file mode 100644 index 0000000000..1114b5a9d9 --- /dev/null +++ b/p2p/http/libp2phttp_test.go @@ -0,0 +1,392 @@ +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" + "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" +) + +func TestHTTPOverStreams(t *testing.T) { + serverHost, err := libp2p.New( + libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/quic-v1"), + ) + require.NoError(t, err) + + httpHost := libp2phttp.Host{StreamHost: serverHost} + + httpHost.SetHTTPHandler("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello")) + })) + + // Start server + go httpHost.Serve() + defer httpHost.Close() + + // 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, err := (&libp2phttp.Host{StreamHost: clientHost}).NewConstrainedRoundTripper(peer.AddrInfo{ID: serverHost.ID()}) + require.NoError(t, err) + + 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) + + httpHost := libp2phttp.Host{ + InsecureAllowHTTP: 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) { + w.Write([]byte("hello")) + })) + + // Start stream based server + go httpHost.Serve() + defer httpHost.Close() + + serverMultiaddrs := httpHost.Addrs() + serverHTTPAddr := serverMultiaddrs[1] + + testCases := []struct { + name string + 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.Host) http.RoundTripper { + rt, err := clientHTTPHost.NewConstrainedRoundTripper(peer.AddrInfo{ + ID: serverHost.ID(), + Addrs: serverMultiaddrs, + }, libp2phttp.PreferHTTPTransport) + require.NoError(t, err) + return rt + }, + }, + { + name: "HTTP first", + setupRoundTripper: func(t *testing.T, clientStreamHost host.Host, clientHTTPHost *libp2phttp.Host) http.RoundTripper { + rt, err := clientHTTPHost.NewConstrainedRoundTripper(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.Host) http.RoundTripper { + rt, err := clientHTTPHost.NewConstrainedRoundTripper(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.Host) http.RoundTripper { + rt, err := clientHTTPHost.NewConstrainedRoundTripper(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.Host) http.RoundTripper { + clientStreamHost.Connect(context.Background(), peer.AddrInfo{ + ID: serverHost.ID(), + Addrs: serverHost.Addrs(), + }) + rt, err := clientHTTPHost.NewConstrainedRoundTripper(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 + clientStreamHost, err := libp2p.New(libp2p.NoListenAddrs) + require.NoError(t, err) + defer clientStreamHost.Close() + + clientHttpHost := &libp2phttp.Host{StreamHost: clientStreamHost} + + rt := tc.setupRoundTripper(t, clientStreamHost, 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 { + var h libp2phttp.Host + 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("/") + 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 := rt.(libp2phttp.PeerMetadataGetter).GetPeerMetadata() + require.NoError(t, err) + + expectedMap := make(libp2phttp.PeerMeta) + expectedMap["/hello"] = libp2phttp.ProtocolMeta{Path: "/hello/"} + require.Equal(t, expectedMap, wk) + }) + } +} + +func TestPlainOldHTTPServer(t *testing.T) { + mux := http.NewServeMux() + wk := libp2phttp.WellKnownHandler{} + mux.Handle("/.well-known/libp2p", &wk) + + mux.Handle("/ping/", httpping.Ping{}) + wk.AddProtocolMeta(httpping.PingProtocolID, libp2phttp.ProtocolMeta{Path: "/ping/"}) + + server := &http.Server{Addr: "127.0.0.1:0", Handler: mux} + + l, err := net.Listen("tcp", server.Addr) + require.NoError(t, err) + + go server.Serve(l) + 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.PeerMeta, error) + }{ + { + name: "using libp2phttp", + do: func(t *testing.T, request *http.Request) (*http.Response, error) { + var clientHttpHost libp2phttp.Host + 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} + return client.Do(request) + }, + getWellKnown: func(t *testing.T) (libp2phttp.PeerMeta, error) { + var clientHttpHost libp2phttp.Host + 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() + }, + }, + { + 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.PeerMeta, 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.PeerMeta + 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.PeerMeta) + expectedMap[httpping.PingProtocolID] = libp2phttp.ProtocolMeta{Path: "/ping/"} + require.Equal(t, expectedMap, protoMap) + }) + } +} + +func TestHostZeroValue(t *testing.T) { + server := libp2phttp.Host{ + 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")) })) + go func() { + server.Serve() + }() + defer server.Close() + + c := libp2phttp.Host{} + 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") +} + +func TestHTTPS(t *testing.T) { + server := libp2phttp.Host{ + TLSConfig: selfSignedTLSConfig(t), + ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/https")}, + } + 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.Host{ + 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 +} diff --git a/p2p/http/options.go b/p2p/http/options.go new file mode 100644 index 0000000000..0062e59319 --- /dev/null +++ b/p2p/http/options.go @@ -0,0 +1,24 @@ +package libp2phttp + +type RoundTripperOption func(o roundTripperOpts) roundTripperOpts + +type roundTripperOpts struct { + preferHTTPTransport bool + 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 +} diff --git a/p2p/http/ping/ping.go b/p2p/http/ping/ping.go new file mode 100644 index 0000000000..2c2ad80fbf --- /dev/null +++ b/p2p/http/ping/ping.go @@ -0,0 +1,67 @@ +package httpping + +import ( + "bytes" + "crypto/rand" + "errors" + "fmt" + "io" + "net/http" + "strconv" +) + +const pingSize = 32 +const PingProtocolID = "/http-ping/1" + +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 { + w.WriteHeader(http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Length", strconv.Itoa(pingSize)) + 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 := [pingSize]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 + } + defer resp.Body.Close() + + 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 +}