diff --git a/client/rpc/api.go b/client/rpc/api.go index c4b73d387c2..36e691629df 100644 --- a/client/rpc/api.go +++ b/client/rpc/api.go @@ -18,6 +18,7 @@ import ( "github.com/ipfs/go-cid" legacy "github.com/ipfs/go-ipld-legacy" ipfs "github.com/ipfs/kubo" + daemon "github.com/ipfs/kubo/cmd/ipfs/kubo" iface "github.com/ipfs/kubo/core/coreiface" caopts "github.com/ipfs/kubo/core/coreiface/options" "github.com/ipfs/kubo/misc/fsutil" @@ -109,6 +110,7 @@ func NewApi(a ma.Multiaddr) (*HttpApi, error) { return nil, err } if network == "unix" { + address = daemon.NormalizeUnixMultiaddr(address) transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", address) } diff --git a/cmd/ipfs/kubo/daemon.go b/cmd/ipfs/kubo/daemon.go index 94b633f7941..e681149a173 100644 --- a/cmd/ipfs/kubo/daemon.go +++ b/cmd/ipfs/kubo/daemon.go @@ -9,7 +9,9 @@ import ( "net" "net/http" _ "net/http/pprof" + "net/url" "os" + "path/filepath" "regexp" "runtime" "sort" @@ -704,6 +706,24 @@ take effect. return errs } +// TODO: should a version of this live in https://github.com/multiformats/go-multiaddr +// so we dont need to duplicate code here and in client/rpc/api.go ? +func NormalizeUnixMultiaddr(address string) string { + // Support legacy and modern /unix addrs + // https://github.com/multiformats/multiaddr/pull/174 + socketPath, err := url.PathUnescape(address) + if err != nil { + return address // nil, fmt.Errorf("failed to unescape /unix socket path: %w", err) + } + // Ensure the path is absolute + if !strings.HasPrefix(socketPath, string(filepath.Separator)) { + socketPath = string(filepath.Separator) + socketPath + } + // Normalize path + socketPath = filepath.Clean(socketPath) + return socketPath +} + // serveHTTPApi collects options, creates listener, prints status message and starts serving requests. func serveHTTPApi(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, error) { cfg, err := cctx.GetConfig() @@ -730,6 +750,9 @@ func serveHTTPApi(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, error } for _, addr := range apiAddrs { + if strings.HasPrefix(addr, "/unix/") { + addr = NormalizeUnixMultiaddr(addr) + } apiMaddr, err := ma.NewMultiaddr(addr) if err != nil { return nil, fmt.Errorf("serveHTTPApi: invalid API address: %q (err: %s)", addr, err) @@ -919,6 +942,9 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e gatewayAddrs := cfg.Addresses.Gateway for _, addr := range gatewayAddrs { + if strings.HasPrefix(addr, "/unix/") { + addr = NormalizeUnixMultiaddr(addr) + } gatewayMaddr, err := ma.NewMultiaddr(addr) if err != nil { return nil, fmt.Errorf("serveHTTPGateway: invalid gateway address: %q (err: %s)", addr, err) diff --git a/docs/config.md b/docs/config.md index a6e9c699f4d..174aadfbddf 100644 --- a/docs/config.md +++ b/docs/config.md @@ -249,7 +249,7 @@ the local [Kubo RPC API](https://docs.ipfs.tech/reference/kubo/rpc/) (`/api/v0`) Supported Transports: * tcp/ip{4,6} - `/ipN/.../tcp/...` -* unix - `/unix/path/to/socket` +* unix - `/unix/path/to/socket` or `/unix/path%2Fto%2Fsocket` > [!CAUTION] > **NEVER EXPOSE UNPROTECTED ADMIN RPC TO LAN OR THE PUBLIC INTERNET** @@ -276,7 +276,7 @@ the local [HTTP gateway](https://specs.ipfs.tech/http-gateways/) (`/ipfs`, `/ipn Supported Transports: * tcp/ip{4,6} - `/ipN/.../tcp/...` -* unix - `/unix/path/to/socket` +* unix - `/unix/path/to/socket` or `/unix/path%2Fto%2Fsocket` Default: `/ip4/127.0.0.1/tcp/8080` diff --git a/test/cli/rpc_unixsocket_test.go b/test/cli/rpc_unixsocket_test.go index 8cead7388d5..241bb7808b1 100644 --- a/test/cli/rpc_unixsocket_test.go +++ b/test/cli/rpc_unixsocket_test.go @@ -2,7 +2,10 @@ package cli import ( "context" + "net/url" "path" + "path/filepath" + "strings" "testing" rpcapi "github.com/ipfs/kubo/client/rpc" @@ -13,39 +16,72 @@ import ( ) func TestRPCUnixSocket(t *testing.T) { - node := harness.NewT(t).NewNode().Init() + t.Parallel() - sockDir := node.Dir - sockAddr := path.Join("/unix", sockDir, "sock") + testCases := []struct { + name string + getSockMultiaddr func(sockPath string) (unixMultiaddr string) + }{ + { + name: "Legacy /unix: unescaped socket path", + getSockMultiaddr: func(sockDir string) string { + return path.Join("/unix", sockDir, "sock") + }, + }, + { + name: "Spec-compliant /unix: percent-encoded socket path without leading slash", + getSockMultiaddr: func(sockDir string) string { + sockPath := path.Join(sockDir, "sock") + pathWithoutLeadingSlash := strings.TrimPrefix(sockPath, string(filepath.Separator)) + escapedPath := url.PathEscape(pathWithoutLeadingSlash) + return path.Join("/unix", escapedPath) + }, + }, + { + name: "Spec-compliant /unix: percent-encoded socket path with leading slash", + getSockMultiaddr: func(sockDir string) string { + sockPath := path.Join(sockDir, "sock") + escapedPath := url.PathEscape(sockPath) + return path.Join("/unix", escapedPath) + }, + }, + } - node.UpdateConfig(func(cfg *config.Config) { - //cfg.Addresses.API = append(cfg.Addresses.API, sockPath) - cfg.Addresses.API = []string{sockAddr} - }) - t.Log("Starting daemon with unix socket:", sockAddr) - node.StartDaemon() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + node := harness.NewT(t).NewNode().Init() + sockDir := node.Dir + sockAddr := tc.getSockMultiaddr(sockDir) + node.UpdateConfig(func(cfg *config.Config) { + //cfg.Addresses.API = append(cfg.Addresses.API, sockPath) + cfg.Addresses.API = []string{sockAddr} + }) + t.Log("Starting daemon with unix socket:", sockAddr) + node.StartDaemon() - unixMaddr, err := multiaddr.NewMultiaddr(sockAddr) - require.NoError(t, err) + unixMaddr, err := multiaddr.NewMultiaddr(sockAddr) + require.NoError(t, err) - apiClient, err := rpcapi.NewApi(unixMaddr) - require.NoError(t, err) + apiClient, err := rpcapi.NewApi(unixMaddr) + require.NoError(t, err) - var ver struct { - Version string - } - err = apiClient.Request("version").Exec(context.Background(), &ver) - require.NoError(t, err) - require.NotEmpty(t, ver) - t.Log("Got version:", ver.Version) + var ver struct { + Version string + } + err = apiClient.Request("version").Exec(context.Background(), &ver) + require.NoError(t, err) + require.NotEmpty(t, ver) + t.Log("Got version:", ver.Version) - var res struct { - ID string - } - err = apiClient.Request("id").Exec(context.Background(), &res) - require.NoError(t, err) - require.NotEmpty(t, res) - t.Log("Got ID:", res.ID) + var res struct { + ID string + } + err = apiClient.Request("id").Exec(context.Background(), &res) + require.NoError(t, err) + require.NotEmpty(t, res) + t.Log("Got ID:", res.ID) - node.StopDaemon() + node.StopDaemon() + }) + } }