Skip to content

Commit 00207c2

Browse files
committed
fix(console,internal/ethapi,node,rpc): restrict debug_setHead to local transports
Add a local-only RPC API classification and use it to keep debug_setHead available over in-process RPC and IPC while hiding it from HTTP and WebSocket transports. This also updates the admin RPC startup path and adds regression coverage for console visibility, transport exposure, and local-only API leakage.
1 parent 146252a commit 00207c2

11 files changed

Lines changed: 397 additions & 55 deletions

File tree

cmd/XDC/consolecmd.go

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

1919
import (
2020
"fmt"
21+
"net/url"
2122
"os"
2223
"os/signal"
2324
"path/filepath"
@@ -85,10 +86,11 @@ func localConsole(ctx *cli.Context) error {
8586
utils.Fatalf("Failed to attach to the inproc XDC: %v", err)
8687
}
8788
config := console.Config{
88-
DataDir: utils.MakeDataDir(ctx),
89-
DocRoot: ctx.String(utils.JSpathFlag.Name),
90-
Client: client,
91-
Preload: utils.MakeConsolePreloads(ctx),
89+
DataDir: utils.MakeDataDir(ctx),
90+
DocRoot: ctx.String(utils.JSpathFlag.Name),
91+
Client: client,
92+
LocalTransport: true,
93+
Preload: utils.MakeConsolePreloads(ctx),
9294
}
9395

9496
console, err := console.New(config)
@@ -132,15 +134,16 @@ func remoteConsole(ctx *cli.Context) error {
132134
endpoint = fmt.Sprintf("%s/XDC.ipc", path)
133135
}
134136

135-
client, err := dialRPC(endpoint)
137+
client, localTransport, err := dialRPC(endpoint)
136138
if err != nil {
137139
utils.Fatalf("Unable to attach to remote XDC: %v", err)
138140
}
139141
config := console.Config{
140-
DataDir: utils.MakeDataDir(ctx),
141-
DocRoot: ctx.String(utils.JSpathFlag.Name),
142-
Client: client,
143-
Preload: utils.MakeConsolePreloads(ctx),
142+
DataDir: utils.MakeDataDir(ctx),
143+
DocRoot: ctx.String(utils.JSpathFlag.Name),
144+
Client: client,
145+
LocalTransport: localTransport,
146+
Preload: utils.MakeConsolePreloads(ctx),
144147
}
145148

146149
console, err := console.New(config)
@@ -164,15 +167,32 @@ func remoteConsole(ctx *cli.Context) error {
164167
// dialRPC returns a RPC client which connects to the given endpoint.
165168
// The check for empty endpoint implements the defaulting logic
166169
// for "XDC attach" and "XDC monitor" with no argument.
167-
func dialRPC(endpoint string) (*rpc.Client, error) {
170+
func dialRPC(endpoint string) (*rpc.Client, bool, error) {
171+
endpoint, localTransport := resolveConsoleEndpoint(endpoint)
172+
client, err := rpc.Dial(endpoint)
173+
return client, localTransport, err
174+
}
175+
176+
func resolveConsoleEndpoint(endpoint string) (string, bool) {
168177
if endpoint == "" {
169-
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
170-
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
171-
// Backwards compatibility with geth < 1.5 which required
172-
// these prefixes.
173-
endpoint = endpoint[4:]
178+
return node.DefaultIPCEndpoint(clientIdentifier), true
179+
}
180+
if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
181+
// Backwards compatibility with geth < 1.5 which required these prefixes.
182+
return endpoint[4:], true
183+
}
184+
u, err := url.Parse(endpoint)
185+
if err != nil {
186+
return endpoint, false
187+
}
188+
switch u.Scheme {
189+
case "http", "https", "ws", "wss", "stdio":
190+
return endpoint, false
191+
case "":
192+
return endpoint, true
193+
default:
194+
return endpoint, false
174195
}
175-
return rpc.Dial(endpoint)
176196
}
177197

178198
// ephemeralConsole starts a new XDC node, attaches an ephemeral JavaScript
@@ -190,10 +210,11 @@ func ephemeralConsole(ctx *cli.Context) error {
190210
utils.Fatalf("Failed to attach to the inproc XDC: %v", err)
191211
}
192212
config := console.Config{
193-
DataDir: utils.MakeDataDir(ctx),
194-
DocRoot: ctx.String(utils.JSpathFlag.Name),
195-
Client: client,
196-
Preload: utils.MakeConsolePreloads(ctx),
213+
DataDir: utils.MakeDataDir(ctx),
214+
DocRoot: ctx.String(utils.JSpathFlag.Name),
215+
Client: client,
216+
LocalTransport: true,
217+
Preload: utils.MakeConsolePreloads(ctx),
197218
}
198219

199220
console, err := console.New(config)

cmd/XDC/consolecmd_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,40 @@ at block: 0 ({{niltime}}){{if ipc}}
160160
attach.ExpectExit()
161161
}
162162

163+
func TestResolveConsoleEndpoint(t *testing.T) {
164+
tests := []struct {
165+
name string
166+
endpoint string
167+
wantLocal bool
168+
wantPrefix string
169+
}{
170+
{name: "default ipc endpoint", endpoint: "", wantLocal: true, wantPrefix: ""},
171+
{name: "explicit ipc path", endpoint: "/tmp/XDC.ipc", wantLocal: true, wantPrefix: "/tmp/XDC.ipc"},
172+
{name: "legacy ipc prefix", endpoint: "ipc:/tmp/XDC.ipc", wantLocal: true, wantPrefix: "/tmp/XDC.ipc"},
173+
{name: "legacy rpc prefix", endpoint: "rpc:/tmp/XDC.ipc", wantLocal: true, wantPrefix: "/tmp/XDC.ipc"},
174+
{name: "http endpoint", endpoint: "http://localhost:8545", wantLocal: false, wantPrefix: "http://localhost:8545"},
175+
{name: "ws endpoint", endpoint: "ws://localhost:8546", wantLocal: false, wantPrefix: "ws://localhost:8546"},
176+
}
177+
178+
for _, test := range tests {
179+
t.Run(test.name, func(t *testing.T) {
180+
gotEndpoint, gotLocal := resolveConsoleEndpoint(test.endpoint)
181+
if gotLocal != test.wantLocal {
182+
t.Fatalf("unexpected local transport classification: got %v want %v", gotLocal, test.wantLocal)
183+
}
184+
if test.wantPrefix == "" {
185+
if !strings.HasSuffix(gotEndpoint, "XDC.ipc") {
186+
t.Fatalf("expected default IPC endpoint, got %q", gotEndpoint)
187+
}
188+
return
189+
}
190+
if gotEndpoint != test.wantPrefix {
191+
t.Fatalf("unexpected resolved endpoint: got %q want %q", gotEndpoint, test.wantPrefix)
192+
}
193+
})
194+
}
195+
}
196+
163197
// trulyRandInt generates a crypto random integer used by the console tests to
164198
// not clash network ports with other tests running cocurrently.
165199
func trulyRandInt(lo, hi int) int {

console/console.go

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

6364
// Console is a JavaScript interpreted runtime environment. It is a fully fleged
6465
// JavaScript console attached to a running node via an external or in-process RPC
6566
// client.
6667
type Console struct {
67-
client *rpc.Client // RPC client to execute Ethereum requests through
68-
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
69-
prompt string // Input prompt prefix string
70-
prompter UserPrompter // Input prompter to allow interactive user feedback
71-
histPath string // Absolute path to the console scrollback history
72-
history []string // Scroll history maintained by the console
73-
printer io.Writer // Output writer to serialize any display strings to
68+
client *rpc.Client // RPC client to execute Ethereum requests through
69+
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
70+
localTransport bool // Whether the connected transport is in-process or IPC
71+
prompt string // Input prompt prefix string
72+
prompter UserPrompter // Input prompter to allow interactive user feedback
73+
histPath string // Absolute path to the console scrollback history
74+
history []string // Scroll history maintained by the console
75+
printer io.Writer // Output writer to serialize any display strings to
7476
}
7577

7678
// New initializes a JavaScript interpreted runtime environment and sets defaults
@@ -89,12 +91,13 @@ func New(config Config) (*Console, error) {
8991

9092
// Initialize the console and return
9193
console := &Console{
92-
client: config.Client,
93-
jsre: jsre.New(config.DocRoot, config.Printer),
94-
prompt: config.Prompt,
95-
prompter: config.Prompter,
96-
printer: config.Printer,
97-
histPath: filepath.Join(config.DataDir, HistoryFile),
94+
client: config.Client,
95+
jsre: jsre.New(config.DocRoot, config.Printer),
96+
localTransport: config.LocalTransport,
97+
prompt: config.Prompt,
98+
prompter: config.Prompter,
99+
printer: config.Printer,
100+
histPath: filepath.Join(config.DataDir, HistoryFile),
98101
}
99102
if err := os.MkdirAll(config.DataDir, 0700); err != nil {
100103
return nil, err
@@ -207,9 +210,25 @@ func (c *Console) initExtensions() error {
207210
}
208211
}
209212
})
213+
if !c.localTransport {
214+
c.hideUnavailableDebugMethods()
215+
}
210216
return nil
211217
}
212218

219+
func (c *Console) hideUnavailableDebugMethods() {
220+
c.jsre.Do(func(vm *goja.Runtime) {
221+
if debug := getObject(vm, "debug"); debug != nil {
222+
debug.Set("setHead", goja.Undefined())
223+
}
224+
if web3 := getObject(vm, "web3"); web3 != nil {
225+
if debug := web3.Get("debug"); debug != nil && !goja.IsUndefined(debug) && !goja.IsNull(debug) {
226+
debug.ToObject(vm).Set("setHead", goja.Undefined())
227+
}
228+
}
229+
})
230+
}
231+
213232
// initAdmin creates additional admin APIs implemented by the bridge.
214233
func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
215234
if admin := getObject(vm, "admin"); admin != nil {

console/console_test.go

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ import (
2828
"github.com/XinFinOrg/XDPoSChain/XDCxlending"
2929

3030
"github.com/XinFinOrg/XDPoSChain/common"
31+
"github.com/XinFinOrg/XDPoSChain/common/hexutil"
3132
"github.com/XinFinOrg/XDPoSChain/consensus/ethash"
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/node"
38+
"github.com/XinFinOrg/XDPoSChain/rpc"
39+
"github.com/dop251/goja"
3740
)
3841

3942
const (
@@ -120,12 +123,13 @@ func newTester(t *testing.T, confOverride func(*ethconfig.Config)) *tester {
120123
printer := new(bytes.Buffer)
121124

122125
console, err := New(Config{
123-
DataDir: stack.DataDir(),
124-
DocRoot: "testdata",
125-
Client: client,
126-
Prompter: prompter,
127-
Printer: printer,
128-
Preload: []string{"preload.js"},
126+
DataDir: stack.DataDir(),
127+
DocRoot: "testdata",
128+
Client: client,
129+
LocalTransport: true,
130+
Prompter: prompter,
131+
Printer: printer,
132+
Preload: []string{"preload.js"},
129133
})
130134
if err != nil {
131135
t.Fatalf("failed to create JavaScript console: %v", err)
@@ -164,6 +168,84 @@ func TestEvaluate(t *testing.T) {
164168
}
165169
}
166170

171+
type debugPrintOnlyRPC struct{}
172+
173+
func (debugPrintOnlyRPC) PrintBlock(uint64) (string, error) {
174+
return "ok", nil
175+
}
176+
177+
type debugPrintAndSetHeadRPC struct{}
178+
179+
func (debugPrintAndSetHeadRPC) PrintBlock(uint64) (string, error) {
180+
return "ok", nil
181+
}
182+
183+
func (debugPrintAndSetHeadRPC) SetHead(hexutil.Uint64) error {
184+
return nil
185+
}
186+
187+
func TestConsoleHidesUnavailableDebugSetHead(t *testing.T) {
188+
t.Run("hidden on remote transport", func(t *testing.T) {
189+
console := newRPCConsole(t, debugPrintAndSetHeadRPC{}, false)
190+
defer stopConsole(t, console)
191+
assertDebugSetHeadVisible(t, console, false)
192+
})
193+
194+
t.Run("kept on local transport", func(t *testing.T) {
195+
console := newRPCConsole(t, debugPrintAndSetHeadRPC{}, true)
196+
defer stopConsole(t, console)
197+
assertDebugSetHeadVisible(t, console, true)
198+
})
199+
}
200+
201+
func newRPCConsole(t *testing.T, debugService interface{}, localTransport bool) *Console {
202+
t.Helper()
203+
204+
server := rpc.NewServer()
205+
if err := server.RegisterName("debug", debugService); err != nil {
206+
t.Fatalf("failed to register debug service: %v", err)
207+
}
208+
client := rpc.DialInProc(server)
209+
210+
console, err := New(Config{
211+
DataDir: t.TempDir(),
212+
DocRoot: "testdata",
213+
Client: client,
214+
LocalTransport: localTransport,
215+
Printer: new(bytes.Buffer),
216+
})
217+
if err != nil {
218+
client.Close()
219+
t.Fatalf("failed to create console: %v", err)
220+
}
221+
t.Cleanup(func() {
222+
client.Close()
223+
})
224+
return console
225+
}
226+
227+
func stopConsole(t *testing.T, console *Console) {
228+
t.Helper()
229+
if err := console.Stop(false); err != nil {
230+
t.Fatalf("failed to stop console: %v", err)
231+
}
232+
}
233+
234+
func assertDebugSetHeadVisible(t *testing.T, console *Console, want bool) {
235+
t.Helper()
236+
237+
console.jsre.Do(func(vm *goja.Runtime) {
238+
debug := getObject(vm, "debug")
239+
if debug == nil {
240+
t.Fatal("debug object is not available")
241+
}
242+
got := !goja.IsUndefined(debug.Get("setHead"))
243+
if got != want {
244+
t.Fatalf("unexpected debug.setHead visibility: got %v want %v", got, want)
245+
}
246+
})
247+
}
248+
167249
// Tests that the console can be used in interactive mode.
168250
func TestInteractive(t *testing.T) {
169251
// Create a tester and run an interactive console in the background

internal/ethapi/api.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2374,6 +2374,18 @@ func NewPrivateDebugAPI(b Backend) *PrivateDebugAPI {
23742374
return &PrivateDebugAPI{b: b}
23752375
}
23762376

2377+
// LocalDebugAPI is the collection of Ethereum debug APIs exposed only over
2378+
// local transports.
2379+
type LocalDebugAPI struct {
2380+
b Backend
2381+
}
2382+
2383+
// NewLocalDebugAPI creates a new API definition for the local-only debug
2384+
// methods of the Ethereum service.
2385+
func NewLocalDebugAPI(b Backend) *LocalDebugAPI {
2386+
return &LocalDebugAPI{b: b}
2387+
}
2388+
23772389
// ChaindbProperty returns leveldb properties of the key-value database.
23782390
func (api *PrivateDebugAPI) ChaindbProperty(property string) (string, error) {
23792391
if property == "" {
@@ -2406,7 +2418,7 @@ func (api *PrivateDebugAPI) ChaindbCompact() error {
24062418
}
24072419

24082420
// SetHead rewinds the head of the blockchain to a previous block.
2409-
func (api *PrivateDebugAPI) SetHead(number hexutil.Uint64) error {
2421+
func (api *LocalDebugAPI) SetHead(number hexutil.Uint64) error {
24102422
header := api.b.CurrentHeader()
24112423
if header == nil {
24122424
return errors.New("current header is not available")

0 commit comments

Comments
 (0)