// 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" "context" "crypto/tls" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/url" "strconv" "strings" "sync" "time" 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") var WellKnownRequestTimeout = 30 * time.Second const ProtocolIDForMultistreamSelect = "/http/1.1" const WellKnownProtocols = "/.well-known/libp2p/protocols" // LegacyWellKnownProtocols refer to a the well-known resource used in an early // draft of the libp2p+http spec. Some users have deployed this, and need backwards compatibility. // Hopefully we can phase this out in the future. Context: https://github.com/libp2p/go-libp2p/pull/2797 const LegacyWellKnownProtocols = "/.well-known/libp2p" const peerMetadataLimit = 8 << 10 // 8KB const peerMetadataLRUSize = 256 // How many different peer's metadata to keep in our LRU cache // 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 resource type WellKnownHandler struct { wellknownMapMu sync.Mutex wellKnownMapping PeerMeta wellKnownCache []byte } // streamHostListen returns 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 := h.wellKnownCache var err error if mapping == nil { mapping, err = json.Marshal(h.wellKnownMapping) if err == nil { h.wellKnownCache = mapping } } 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.wellKnownCache = nil h.wellknownMapMu.Unlock() } func (h *WellKnownHandler) RemoveProtocolMeta(p protocol.ID) { h.wellknownMapMu.Lock() if h.wellKnownMapping != nil { delete(h.wellKnownMapping, p) } h.wellKnownCache = nil 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 // 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 // EnableCompatibilityWithLegacyWellKnownEndpoint allows compatibility with // an older version of the spec that defined the well-known resource as: // .well-known/libp2p. // For servers, this means hosting the well-known resource at both the // legacy and current paths. // For clients it means making two parallel requests and picking the first one that succeeds. // // Long term this should be deprecated once enough users have upgraded to a // newer go-libp2p version and we can remove all this code. EnableCompatibilityWithLegacyWellKnownEndpoint bool // peerMetadata is an LRU cache of a peer's well-known protocol map. peerMetadata *lru.Cache[peer.ID, PeerMeta] // createHTTPTransport is used to lazily create the httpTransport in a thread-safe way. 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() { if h.ServeMux == nil { 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(WellKnownProtocols, &h.WellKnownHandler) if h.EnableCompatibilityWithLegacyWellKnownEndpoint { h.ServeMux.Handle(LegacyWellKnownProtocols, &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, connectionCloseHeaderMiddleware(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 resource 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 resource 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() // Do not trim the trailing / from path // This allows us to serve `/a/b` when we mount a handler for `/b` at path `/a` h.ServeMux.Handle(path, http.StripPrefix(strings.TrimSuffix(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) { ctx := context.Background() ctx, cancel := context.WithDeadline(ctx, time.Now().Add(WellKnownRequestTimeout)) defer cancel() return rt.httpHost.getAndStorePeerMetadata(ctx, rt, rt.server) } // RoundTrip implements http.RoundTripper. 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 } // Write connection: close header to ensure the stream is closed after the response r.Header.Add("connection", "close") go func() { defer s.CloseWrite() r.Write(s) if r.Body != nil { r.Body.Close() } }() if deadline, ok := r.Context().Deadline(); ok { s.SetReadDeadline(deadline) } resp, err := http.ReadResponse(bufio.NewReader(s), r) if err != nil { s.Close() 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 } } ctx := context.Background() ctx, cancel := context.WithDeadline(ctx, time.Now().Add(WellKnownRequestTimeout)) defer cancel() wk, err := rt.httpHost.getAndStorePeerMetadata(ctx, rt, rt.server) if err == nil { rt.cachedProtos = wk return wk, nil } 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) { ctx := context.Background() ctx, cancel := context.WithDeadline(ctx, time.Now().Add(WellKnownRequestTimeout)) defer cancel() protos, err := h.getAndStorePeerMetadata(ctx, roundtripper, server) if err != nil { return &namespacedRoundTripper{}, err } 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 } // getAndStorePeerMetadata looks up the protocol path in the well-known mapping and // returns it. Will only store the peer's protocol mapping if the server ID is // provided. func (h *Host) getAndStorePeerMetadata(ctx context.Context, roundtripper http.RoundTripper, server peer.ID) (PeerMeta, error) { if h.peerMetadata == nil { h.peerMetadata = newPeerMetadataCache() } if meta, ok := h.peerMetadata.Get(server); server != "" && ok { return meta, nil } var meta PeerMeta var err error if h.EnableCompatibilityWithLegacyWellKnownEndpoint { type metaAndErr struct { m PeerMeta err error } legacyRespCh := make(chan metaAndErr, 1) wellKnownRespCh := make(chan metaAndErr, 1) ctx, cancel := context.WithCancel(ctx) go func() { meta, err := requestPeerMeta(ctx, roundtripper, LegacyWellKnownProtocols) legacyRespCh <- metaAndErr{meta, err} }() go func() { meta, err := requestPeerMeta(ctx, roundtripper, WellKnownProtocols) wellKnownRespCh <- metaAndErr{meta, err} }() select { case resp := <-legacyRespCh: if resp.err != nil { resp = <-wellKnownRespCh } meta, err = resp.m, resp.err case resp := <-wellKnownRespCh: if resp.err != nil { legacyResp := <-legacyRespCh if legacyResp.err != nil { // If both endpoints error, return the error from the well // known resource (not the legacy well known resource) meta, err = resp.m, resp.err } else { meta, err = legacyResp.m, legacyResp.err } } else { meta, err = resp.m, resp.err } } cancel() } else { meta, err = requestPeerMeta(ctx, roundtripper, WellKnownProtocols) } if err != nil { return nil, err } if server != "" { h.peerMetadata.Add(server, meta) } return meta, nil } func requestPeerMeta(ctx context.Context, roundtripper http.RoundTripper, wellKnownResource string) (PeerMeta, error) { req, err := http.NewRequest("GET", wellKnownResource, nil) if err != nil { return nil, err } 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 } 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) } func connectionCloseHeaderMiddleware(next http.Handler) http.Handler { // Sets connection: close. It's preferable to not reuse streams for HTTP. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Connection", "close") next.ServeHTTP(w, r) }) }