Skip to content

Commit e0405d9

Browse files
committed
fix(internal/ethapi,console,node,rpc): restrict debug_setHead to local transports
Move debug_setHead out of the public debug API and expose it only through local transports by introducing a local-only RPC API classification. This keeps debug_setHead available to in-process clients, IPC, and the local console while removing it from HTTP and WebSocket JSON-RPC exposure. It also updates the console and node tests to cover the new visibility rules.
1 parent f5fe86c commit e0405d9

12 files changed

Lines changed: 450 additions & 57 deletions

File tree

cmd/XDC/consolecmd.go

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818

1919
import (
2020
"fmt"
21+
"net/url"
2122
"slices"
2223
"strings"
2324

@@ -78,10 +79,11 @@ func localConsole(ctx *cli.Context) error {
7879
// Attach to the newly started node and create the JavaScript console.
7980
client := stack.Attach()
8081
config := console.Config{
81-
DataDir: utils.MakeDataDir(ctx),
82-
DocRoot: ctx.String(utils.JSpathFlag.Name),
83-
Client: client,
84-
Preload: utils.MakeConsolePreloads(ctx),
82+
DataDir: utils.MakeDataDir(ctx),
83+
DocRoot: ctx.String(utils.JSpathFlag.Name),
84+
Client: client,
85+
LocalTransport: true,
86+
Preload: utils.MakeConsolePreloads(ctx),
8587
}
8688
console, err := console.New(config)
8789
if err != nil {
@@ -134,15 +136,16 @@ func remoteConsole(ctx *cli.Context) error {
134136
endpoint = cfg.IPCEndpoint()
135137
}
136138

137-
client, err := dialRPC(endpoint)
139+
client, localTransport, err := dialRPC(endpoint)
138140
if err != nil {
139141
utils.Fatalf("Unable to attach to remote XDC: %v", err)
140142
}
141143
config := console.Config{
142-
DataDir: utils.MakeDataDir(ctx),
143-
DocRoot: ctx.String(utils.JSpathFlag.Name),
144-
Client: client,
145-
Preload: utils.MakeConsolePreloads(ctx),
144+
DataDir: utils.MakeDataDir(ctx),
145+
DocRoot: ctx.String(utils.JSpathFlag.Name),
146+
Client: client,
147+
LocalTransport: localTransport,
148+
Preload: utils.MakeConsolePreloads(ctx),
146149
}
147150
console, err := console.New(config)
148151
if err != nil {
@@ -177,13 +180,50 @@ XDC --exec "%s" console`, b.String())
177180
// dialRPC returns a RPC client which connects to the given endpoint.
178181
// The check for empty endpoint implements the defaulting logic
179182
// for "XDC attach" and "XDC monitor" with no argument.
180-
func dialRPC(endpoint string) (*rpc.Client, error) {
183+
func dialRPC(endpoint string) (*rpc.Client, bool, error) {
184+
endpoint, localTransport := resolveConsoleEndpoint(endpoint)
185+
client, err := rpc.Dial(endpoint)
186+
return client, localTransport, err
187+
}
188+
189+
func resolveConsoleEndpoint(endpoint string) (string, bool) {
181190
if endpoint == "" {
182-
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
183-
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
184-
// Backwards compatibility with geth < 1.5 which required
185-
// these prefixes.
186-
endpoint = endpoint[4:]
191+
return node.DefaultIPCEndpoint(clientIdentifier), true
192+
}
193+
if strings.HasPrefix(endpoint, "ipc:") {
194+
return endpoint[4:], true
195+
}
196+
endpoint = strings.TrimPrefix(endpoint, "rpc:")
197+
if endpoint == "stdio" {
198+
return endpoint, false
199+
}
200+
if isWindowsIPCPath(endpoint) {
201+
return endpoint, true
202+
}
203+
u, err := url.Parse(endpoint)
204+
if err != nil {
205+
return endpoint, false
206+
}
207+
switch u.Scheme {
208+
case "http", "https", "ws", "wss", "stdio":
209+
return endpoint, false
210+
case "":
211+
return endpoint, true
212+
default:
213+
return endpoint, false
214+
}
215+
}
216+
217+
func isWindowsIPCPath(endpoint string) bool {
218+
if len(endpoint) < 3 {
219+
return false
220+
}
221+
drive := endpoint[0]
222+
if (drive < 'A' || drive > 'Z') && (drive < 'a' || drive > 'z') {
223+
return false
224+
}
225+
if endpoint[1] != ':' {
226+
return false
187227
}
188-
return rpc.Dial(endpoint)
228+
return endpoint[2] == '\\' || endpoint[2] == '/'
189229
}

cmd/XDC/consolecmd_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,47 @@ To exit, press ctrl-d or type exit
168168
attach.ExpectExit()
169169
}
170170

171+
func TestResolveConsoleEndpoint(t *testing.T) {
172+
tests := []struct {
173+
name string
174+
endpoint string
175+
wantEndpoint string
176+
wantLocal bool
177+
}{
178+
{name: "default ipc endpoint", endpoint: "", wantEndpoint: "", wantLocal: true},
179+
{name: "plain ipc path", endpoint: "/tmp/XDC.ipc", wantEndpoint: "/tmp/XDC.ipc", wantLocal: true},
180+
{name: "legacy ipc prefix", endpoint: "ipc:/tmp/XDC.ipc", wantEndpoint: "/tmp/XDC.ipc", wantLocal: true},
181+
{name: "legacy rpc prefix", endpoint: "rpc:/tmp/XDC.ipc", wantEndpoint: "/tmp/XDC.ipc", wantLocal: true},
182+
{name: "windows drive ipc path", endpoint: `C:\\Users\\tester\\XDC.ipc`, wantEndpoint: `C:\\Users\\tester\\XDC.ipc`, wantLocal: true},
183+
{name: "windows drive ipc slash path", endpoint: "C:/Users/tester/XDC.ipc", wantEndpoint: "C:/Users/tester/XDC.ipc", wantLocal: true},
184+
{name: "legacy rpc windows drive ipc path", endpoint: `rpc:C:\\Users\\tester\\XDC.ipc`, wantEndpoint: `C:\\Users\\tester\\XDC.ipc`, wantLocal: true},
185+
{name: "legacy rpc http endpoint", endpoint: "rpc:http://localhost:8545", wantEndpoint: "http://localhost:8545", wantLocal: false},
186+
{name: "legacy rpc ws endpoint", endpoint: "rpc:ws://localhost:8546", wantEndpoint: "ws://localhost:8546", wantLocal: false},
187+
{name: "stdio endpoint", endpoint: "stdio", wantEndpoint: "stdio", wantLocal: false},
188+
{name: "legacy rpc stdio endpoint", endpoint: "rpc:stdio", wantEndpoint: "stdio", wantLocal: false},
189+
{name: "http endpoint", endpoint: "http://localhost:8545", wantEndpoint: "http://localhost:8545", wantLocal: false},
190+
{name: "ws endpoint", endpoint: "ws://localhost:8546", wantEndpoint: "ws://localhost:8546", wantLocal: false},
191+
}
192+
193+
for _, test := range tests {
194+
t.Run(test.name, func(t *testing.T) {
195+
gotEndpoint, gotLocal := resolveConsoleEndpoint(test.endpoint)
196+
if gotLocal != test.wantLocal {
197+
t.Fatalf("unexpected local transport classification: got %v want %v", gotLocal, test.wantLocal)
198+
}
199+
if test.wantEndpoint == "" {
200+
if !strings.HasSuffix(gotEndpoint, "XDC.ipc") {
201+
t.Fatalf("expected default IPC endpoint, got %q", gotEndpoint)
202+
}
203+
return
204+
}
205+
if gotEndpoint != test.wantEndpoint {
206+
t.Fatalf("unexpected resolved endpoint: got %q want %q", gotEndpoint, test.wantEndpoint)
207+
}
208+
})
209+
}
210+
}
211+
171212
// trulyRandInt generates a crypto random integer used by the console tests to
172213
// not clash network ports with other tests running cocurrently.
173214
func trulyRandInt(lo, hi int) int {

console/console.go

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,26 +56,28 @@ const DefaultPrompt = "> "
5656
// Config is the collection of configurations to fine tune the behavior of the
5757
// JavaScript console.
5858
type Config struct {
59-
DataDir string // Data directory to store the console history at
60-
DocRoot string // Filesystem path from where to load JavaScript files from
61-
Client *rpc.Client // RPC client to execute Ethereum requests through
62-
Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
63-
Prompter prompt.UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
64-
Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
65-
Preload []string // Absolute paths to JavaScript files to preload
59+
DataDir string // Data directory to store the console history at
60+
DocRoot string // Filesystem path from where to load JavaScript files from
61+
Client *rpc.Client // RPC client to execute Ethereum requests through
62+
LocalTransport bool // Whether the console is attached over an in-process or IPC transport
63+
Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
64+
Prompter prompt.UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
65+
Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
66+
Preload []string // Absolute paths to JavaScript files to preload
6667
}
6768

6869
// Console is a JavaScript interpreted runtime environment. It is a fully fleged
6970
// JavaScript console attached to a running node via an external or in-process RPC
7071
// client.
7172
type Console struct {
72-
client *rpc.Client // RPC client to execute Ethereum requests through
73-
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
74-
prompt string // Input prompt prefix string
75-
prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
76-
histPath string // Absolute path to the console scrollback history
77-
history []string // Scroll history maintained by the console
78-
printer io.Writer // Output writer to serialize any display strings to
73+
client *rpc.Client // RPC client to execute Ethereum requests through
74+
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
75+
localTransport bool // Whether the connected transport is in-process or IPC
76+
prompt string // Input prompt prefix string
77+
prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
78+
histPath string // Absolute path to the console scrollback history
79+
history []string // Scroll history maintained by the console
80+
printer io.Writer // Output writer to serialize any display strings to
7981

8082
interactiveStopped chan struct{}
8183
stopInteractiveCh chan struct{}
@@ -103,6 +105,7 @@ func New(config Config) (*Console, error) {
103105
console := &Console{
104106
client: config.Client,
105107
jsre: jsre.New(config.DocRoot, config.Printer),
108+
localTransport: config.LocalTransport,
106109
prompt: config.Prompt,
107110
prompter: config.Prompter,
108111
printer: config.Printer,
@@ -235,9 +238,25 @@ func (c *Console) initExtensions() error {
235238
}
236239
}
237240
})
241+
if !c.localTransport {
242+
c.hideUnavailableDebugMethods()
243+
}
238244
return nil
239245
}
240246

247+
func (c *Console) hideUnavailableDebugMethods() {
248+
c.jsre.Do(func(vm *goja.Runtime) {
249+
if debug := getObject(vm, "debug"); debug != nil {
250+
debug.Set("setHead", goja.Undefined())
251+
}
252+
if web3 := getObject(vm, "web3"); web3 != nil {
253+
if debug := web3.Get("debug"); debug != nil && !goja.IsUndefined(debug) && !goja.IsNull(debug) {
254+
debug.ToObject(vm).Set("setHead", goja.Undefined())
255+
}
256+
}
257+
})
258+
}
259+
241260
// initAdmin creates additional admin APIs implemented by the bridge.
242261
func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
243262
if admin := getObject(vm, "admin"); admin != nil {
@@ -288,7 +307,23 @@ func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, str
288307
start++
289308
break
290309
}
291-
return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
310+
return line[:start], c.filterCompletions(c.jsre.CompleteKeywords(line[start:pos])), line[pos:]
311+
}
312+
313+
func (c *Console) filterCompletions(completions []string) []string {
314+
if c.localTransport {
315+
return completions
316+
}
317+
filtered := completions[:0]
318+
for _, completion := range completions {
319+
switch completion {
320+
case "debug.setHead", "debug.setHead(", "web3.debug.setHead", "web3.debug.setHead(":
321+
continue
322+
default:
323+
filtered = append(filtered, completion)
324+
}
325+
}
326+
return filtered
292327
}
293328

294329
// Welcome show summary of current Geth instance and some metadata about the

console/console_test.go

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@ import (
2828
"github.com/XinFinOrg/XDPoSChain/XDCx"
2929
"github.com/XinFinOrg/XDPoSChain/XDCxlending"
3030
"github.com/XinFinOrg/XDPoSChain/common"
31+
"github.com/XinFinOrg/XDPoSChain/common/hexutil"
3132
"github.com/XinFinOrg/XDPoSChain/console/prompt"
3233
"github.com/XinFinOrg/XDPoSChain/core"
3334
"github.com/XinFinOrg/XDPoSChain/eth"
3435
"github.com/XinFinOrg/XDPoSChain/eth/ethconfig"
3536
"github.com/XinFinOrg/XDPoSChain/internal/jsre"
3637
"github.com/XinFinOrg/XDPoSChain/miner"
3738
"github.com/XinFinOrg/XDPoSChain/node"
39+
"github.com/XinFinOrg/XDPoSChain/rpc"
40+
"github.com/dop251/goja"
3841
)
3942

4043
const (
@@ -123,12 +126,13 @@ func newTester(t *testing.T, confOverride func(*ethconfig.Config)) *tester {
123126
printer := new(bytes.Buffer)
124127

125128
console, err := New(Config{
126-
DataDir: stack.DataDir(),
127-
DocRoot: "testdata",
128-
Client: client,
129-
Prompter: prompter,
130-
Printer: printer,
131-
Preload: []string{"preload.js"},
129+
DataDir: stack.DataDir(),
130+
DocRoot: "testdata",
131+
Client: client,
132+
LocalTransport: true,
133+
Prompter: prompter,
134+
Printer: printer,
135+
Preload: []string{"preload.js"},
132136
})
133137
if err != nil {
134138
t.Fatalf("failed to create JavaScript console: %v", err)
@@ -193,6 +197,95 @@ func TestEvaluate(t *testing.T) {
193197
}
194198
}
195199

200+
type debugPrintAndSetHeadRPC struct{}
201+
202+
func (debugPrintAndSetHeadRPC) PrintBlock(uint64) (string, error) {
203+
return "ok", nil
204+
}
205+
206+
func (debugPrintAndSetHeadRPC) SetHead(hexutil.Uint64) error {
207+
return nil
208+
}
209+
210+
func TestConsoleHidesUnavailableDebugSetHead(t *testing.T) {
211+
t.Run("hidden on remote transport", func(t *testing.T) {
212+
console := newRPCConsole(t, debugPrintAndSetHeadRPC{}, false)
213+
defer stopConsole(t, console)
214+
assertDebugSetHeadVisible(t, console, false)
215+
assertDebugSetHeadCompletion(t, console, false)
216+
})
217+
218+
t.Run("kept on local transport", func(t *testing.T) {
219+
console := newRPCConsole(t, debugPrintAndSetHeadRPC{}, true)
220+
defer stopConsole(t, console)
221+
assertDebugSetHeadVisible(t, console, true)
222+
assertDebugSetHeadCompletion(t, console, true)
223+
})
224+
}
225+
226+
func newRPCConsole(t *testing.T, debugService interface{}, localTransport bool) *Console {
227+
t.Helper()
228+
229+
server := rpc.NewServer()
230+
if err := server.RegisterName("debug", debugService); err != nil {
231+
t.Fatalf("failed to register debug service: %v", err)
232+
}
233+
client := rpc.DialInProc(server)
234+
t.Cleanup(func() {
235+
client.Close()
236+
})
237+
238+
console, err := New(Config{
239+
DataDir: t.TempDir(),
240+
DocRoot: "testdata",
241+
Client: client,
242+
LocalTransport: localTransport,
243+
Printer: new(bytes.Buffer),
244+
})
245+
if err != nil {
246+
t.Fatalf("failed to create console: %v", err)
247+
}
248+
return console
249+
}
250+
251+
func stopConsole(t *testing.T, console *Console) {
252+
t.Helper()
253+
if err := console.Stop(false); err != nil {
254+
t.Fatalf("failed to stop console: %v", err)
255+
}
256+
}
257+
258+
func assertDebugSetHeadVisible(t *testing.T, console *Console, want bool) {
259+
t.Helper()
260+
261+
console.jsre.Do(func(vm *goja.Runtime) {
262+
debug := getObject(vm, "debug")
263+
if debug == nil {
264+
t.Fatal("debug object is not available")
265+
}
266+
got := !goja.IsUndefined(debug.Get("setHead"))
267+
if got != want {
268+
t.Fatalf("unexpected debug.setHead visibility: got %v want %v", got, want)
269+
}
270+
})
271+
}
272+
273+
func assertDebugSetHeadCompletion(t *testing.T, console *Console, want bool) {
274+
t.Helper()
275+
276+
_, completions, _ := console.AutoCompleteInput("debug.setH", len("debug.setH"))
277+
got := false
278+
for _, completion := range completions {
279+
if completion == "debug.setHead" || completion == "debug.setHead(" {
280+
got = true
281+
break
282+
}
283+
}
284+
if got != want {
285+
t.Fatalf("unexpected debug.setHead completion visibility: got %v want %v (completions=%v)", got, want, completions)
286+
}
287+
}
288+
196289
// Tests that the console can be used in interactive mode.
197290
func TestInteractive(t *testing.T) {
198291
// Create a tester and run an interactive console in the background

0 commit comments

Comments
 (0)