diff --git a/multiaddr_test.go b/multiaddr_test.go index b33e94d..6d160b4 100644 --- a/multiaddr_test.go +++ b/multiaddr_test.go @@ -192,6 +192,11 @@ var good = []string{ "/ip4/127.0.0.1/tcp/127/wss", "/ip4/127.0.0.1/tcp/127/webrtc-direct", "/ip4/127.0.0.1/tcp/127/webrtc", + "/http-path/tmp%2Fbar", + "/http-path/tmp%2Fbar%2Fbaz", + "/http-path/foo", + "/ip4/127.0.0.1/tcp/0/p2p/12D3KooWCryG7Mon9orvQxcS1rYZjotPgpwoJNHHKcLLfE4Hf5mV/http-path/foo", + "/ip4/127.0.0.1/tcp/443/tls/sni/example.com/http/http-path/foo", } func TestConstructSucceeds(t *testing.T) { @@ -627,6 +632,7 @@ func TestRoundTrip(t *testing.T) { "/ip4/127.0.0.1/udp/1234/quic-v1/webtransport/certhash/uEiDDq4_xNyDorZBH3TlGazyJdOWSwvo4PUo5YHFMrvDE8g", "/p2p/QmbHVEEepCi7rn7VL7Exxpd2Ci9NNB6ifvqwhsrbRMgQFP", "/p2p/QmbHVEEepCi7rn7VL7Exxpd2Ci9NNB6ifvqwhsrbRMgQFP/unix/a/b/c", + "/http-path/tmp%2Fbar", } { ma, err := NewMultiaddr(s) if err != nil { @@ -923,3 +929,48 @@ func TestDNS(t *testing.T) { t.Fatal("expected equality") } } + +func TestHTTPPath(t *testing.T) { + t.Run("bad addr", func(t *testing.T) { + badAddr := "/http-path/thisIsMissingAfullByte%f" + _, err := NewMultiaddr(badAddr) + require.Error(t, err) + }) + + t.Run("only reads the http-path part", func(t *testing.T) { + addr := "/http-path/tmp%2Fbar/p2p-circuit" // The http-path only reference the part immediately after it. It does not include the rest of the multiaddr (like the /path component sometimes does) + m, err := NewMultiaddr(addr) + require.NoError(t, err) + m.ValueForProtocol(P_HTTP_PATH) + v, err := m.ValueForProtocol(P_HTTP_PATH) + require.NoError(t, err) + require.Equal(t, "tmp%2Fbar", v) + }) + + t.Run("round trip", func(t *testing.T) { + cases := []string{ + "/http-path/tmp%2Fbar", + "/http-path/tmp%2Fbar%2Fbaz", + "/http-path/foo", + "/ip4/127.0.0.1/tcp/0/p2p/12D3KooWCryG7Mon9orvQxcS1rYZjotPgpwoJNHHKcLLfE4Hf5mV/http-path/foo", + "/ip4/127.0.0.1/tcp/443/tls/sni/example.com/http/http-path/foo", + } + for _, c := range cases { + m, err := NewMultiaddr(c) + require.NoError(t, err) + require.Equal(t, c, m.String()) + } + }) + + t.Run("value for protocol", func(t *testing.T) { + m := StringCast("/http-path/tmp%2Fbar") + v, err := m.ValueForProtocol(P_HTTP_PATH) + require.NoError(t, err) + // This gives us the url escaped version + require.Equal(t, "tmp%2Fbar", v) + + // If we want the raw unescaped version, we can use the component and read it + _, component := SplitLast(m) + require.Equal(t, "tmp/bar", string(component.RawValue())) + }) +} diff --git a/protocols.go b/protocols.go index b01e6cb..d3117b2 100644 --- a/protocols.go +++ b/protocols.go @@ -26,6 +26,7 @@ const ( P_P2P = 421 P_IPFS = P_P2P // alias for backwards compatibility P_HTTP = 480 + P_HTTP_PATH = 481 P_HTTPS = 443 // deprecated alias for /tls/http P_ONION = 444 // also for backwards compatibility P_ONION3 = 445 @@ -206,6 +207,13 @@ var ( Code: P_HTTP, VCode: CodeToVarint(P_HTTP), } + protoHTTPPath = Protocol{ + Name: "http-path", + Code: P_HTTP_PATH, + VCode: CodeToVarint(P_HTTP_PATH), + Size: LengthPrefixedVarSize, + Transcoder: TranscoderHTTPPath, + } protoHTTPS = Protocol{ Name: "https", Code: P_HTTPS, @@ -301,6 +309,7 @@ func init() { protoWEBTRANSPORT, protoCERTHASH, protoHTTP, + protoHTTPPath, protoHTTPS, protoP2P, protoUNIX, diff --git a/transcoders.go b/transcoders.go index 5387740..0d0327e 100644 --- a/transcoders.go +++ b/transcoders.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net" + "net/url" "strconv" "strings" @@ -454,3 +455,30 @@ func validateCertHash(b []byte) error { _, err := mh.Decode(b) return err } + +var TranscoderHTTPPath = NewTranscoderFromFunctions(httpPathStB, httpPathBtS, validateHTTPPath) + +func httpPathStB(s string) ([]byte, error) { + unescaped, err := url.QueryUnescape(s) + if err != nil { + return nil, err + } + if len(unescaped) == 0 { + return nil, fmt.Errorf("empty http path is not allowed") + } + return []byte(unescaped), err +} + +func httpPathBtS(b []byte) (string, error) { + if len(b) == 0 { + return "", fmt.Errorf("empty http path is not allowed") + } + return url.QueryEscape(string(b)), nil +} + +func validateHTTPPath(b []byte) error { + if len(b) == 0 { + return fmt.Errorf("empty http path is not allowed") + } + return nil // We can represent any byte slice when we escape it. +}