Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 39 additions & 16 deletions cmd/XDC/consolecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package main

import (
"fmt"
"net/url"
"slices"
"strings"

Expand Down Expand Up @@ -78,10 +79,11 @@ func localConsole(ctx *cli.Context) error {
// Attach to the newly started node and create the JavaScript console.
client := stack.Attach()
config := console.Config{
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
LocalTransport: true,
Preload: utils.MakeConsolePreloads(ctx),
}
console, err := console.New(config)
if err != nil {
Expand Down Expand Up @@ -134,15 +136,16 @@ func remoteConsole(ctx *cli.Context) error {
endpoint = cfg.IPCEndpoint()
}

client, err := dialRPC(endpoint)
client, localTransport, err := dialRPC(endpoint)
if err != nil {
utils.Fatalf("Unable to attach to remote XDC: %v", err)
}
config := console.Config{
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
LocalTransport: localTransport,
Preload: utils.MakeConsolePreloads(ctx),
}
console, err := console.New(config)
if err != nil {
Expand Down Expand Up @@ -177,13 +180,33 @@ XDC --exec "%s" console`, b.String())
// dialRPC returns a RPC client which connects to the given endpoint.
// The check for empty endpoint implements the defaulting logic
// for "XDC attach" and "XDC monitor" with no argument.
func dialRPC(endpoint string) (*rpc.Client, error) {
func dialRPC(endpoint string) (*rpc.Client, bool, error) {
endpoint, localTransport := resolveConsoleEndpoint(endpoint)
client, err := rpc.Dial(endpoint)
return client, localTransport, err
}

func resolveConsoleEndpoint(endpoint string) (string, bool) {
if endpoint == "" {
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
// Backwards compatibility with geth < 1.5 which required
// these prefixes.
endpoint = endpoint[4:]
return node.DefaultIPCEndpoint(clientIdentifier), true
}
if strings.HasPrefix(endpoint, "ipc:") {
return endpoint[4:], true
}
endpoint = strings.TrimPrefix(endpoint, "rpc:")
if endpoint == "stdio" {
return endpoint, false
}
u, err := url.Parse(endpoint)
if err != nil {
return endpoint, false
}
switch u.Scheme {
case "http", "https", "ws", "wss", "stdio":
return endpoint, false
case "":
return endpoint, true
default:
return endpoint, false
}
return rpc.Dial(endpoint)
}
71 changes: 71 additions & 0 deletions cmd/XDC/consolecmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,77 @@ To exit, press ctrl-d or type exit
attach.ExpectExit()
}

func TestResolveConsoleEndpoint(t *testing.T) {
tests := []struct {
name string
endpoint string
wantEndpoint string
wantLocal bool
}{
{name: "default ipc endpoint", endpoint: "", wantEndpoint: "", wantLocal: true},
{name: "plain ipc path", endpoint: "/tmp/XDC.ipc", wantEndpoint: "/tmp/XDC.ipc", wantLocal: true},
{name: "legacy ipc prefix", endpoint: "ipc:/tmp/XDC.ipc", wantEndpoint: "/tmp/XDC.ipc", wantLocal: true},
{name: "legacy rpc prefix", endpoint: "rpc:/tmp/XDC.ipc", wantEndpoint: "/tmp/XDC.ipc", wantLocal: true},
{name: "windows drive path stays unsupported", endpoint: `C:\\Users\\tester\\XDC.ipc`, wantEndpoint: `C:\\Users\\tester\\XDC.ipc`, wantLocal: false},
{name: "windows drive slash path stays unsupported", endpoint: "C:/Users/tester/XDC.ipc", wantEndpoint: "C:/Users/tester/XDC.ipc", wantLocal: false},
{name: "legacy rpc windows drive path stays unsupported", endpoint: `rpc:C:\\Users\\tester\\XDC.ipc`, wantEndpoint: `C:\\Users\\tester\\XDC.ipc`, wantLocal: false},
{name: "legacy rpc http endpoint", endpoint: "rpc:http://localhost:8545", wantEndpoint: "http://localhost:8545", wantLocal: false},
{name: "legacy rpc ws endpoint", endpoint: "rpc:ws://localhost:8546", wantEndpoint: "ws://localhost:8546", wantLocal: false},
{name: "stdio endpoint", endpoint: "stdio", wantEndpoint: "stdio", wantLocal: false},
{name: "legacy rpc stdio endpoint", endpoint: "rpc:stdio", wantEndpoint: "stdio", wantLocal: false},
{name: "http endpoint", endpoint: "http://localhost:8545", wantEndpoint: "http://localhost:8545", wantLocal: false},
{name: "ws endpoint", endpoint: "ws://localhost:8546", wantEndpoint: "ws://localhost:8546", wantLocal: false},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotEndpoint, gotLocal := resolveConsoleEndpoint(test.endpoint)
if gotLocal != test.wantLocal {
t.Fatalf("unexpected local transport classification: got %v want %v", gotLocal, test.wantLocal)
}
if test.wantEndpoint == "" {
if !strings.HasSuffix(gotEndpoint, "XDC.ipc") {
t.Fatalf("expected default IPC endpoint, got %q", gotEndpoint)
}
return
}
if gotEndpoint != test.wantEndpoint {
t.Fatalf("unexpected resolved endpoint: got %q want %q", gotEndpoint, test.wantEndpoint)
}
})
}
}

func TestDialRPCRejectsWindowsDrivePaths(t *testing.T) {
tests := []struct {
name string
endpoint string
}{
{name: "windows drive path", endpoint: `C:\\Users\\tester\\XDC.ipc`},
{name: "windows drive slash path", endpoint: "C:/Users/tester/XDC.ipc"},
{name: "legacy rpc windows drive path", endpoint: `rpc:C:\\Users\\tester\\XDC.ipc`},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
client, local, err := dialRPC(test.endpoint)
if client != nil {
client.Close()
t.Fatal("expected dialRPC to reject Windows drive-letter path")
}
if err == nil {
t.Fatal("expected dialRPC to fail for Windows drive-letter path")
}
if local {
t.Fatal("expected Windows drive-letter path to stay classified as non-local")
}
if !strings.Contains(err.Error(), `no known transport for URL scheme "c"`) {
t.Fatalf("unexpected dialRPC error: %v", err)
}
})
}
}

// trulyRandInt generates a crypto random integer used by the console tests to
// not clash network ports with other tests running cocurrently.
func trulyRandInt(lo, hi int) int {
Expand Down
81 changes: 66 additions & 15 deletions console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,26 +56,28 @@ const DefaultPrompt = "> "
// Config is the collection of configurations to fine tune the behavior of the
// JavaScript console.
type Config struct {
DataDir string // Data directory to store the console history at
DocRoot string // Filesystem path from where to load JavaScript files from
Client *rpc.Client // RPC client to execute Ethereum requests through
Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
Prompter prompt.UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
Preload []string // Absolute paths to JavaScript files to preload
DataDir string // Data directory to store the console history at
DocRoot string // Filesystem path from where to load JavaScript files from
Client *rpc.Client // RPC client to execute Ethereum requests through
LocalTransport bool // Whether the console is attached over an in-process or IPC transport
Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
Prompter prompt.UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
Preload []string // Absolute paths to JavaScript files to preload
}

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

interactiveStopped chan struct{}
stopInteractiveCh chan struct{}
Expand Down Expand Up @@ -103,6 +105,7 @@ func New(config Config) (*Console, error) {
console := &Console{
client: config.Client,
jsre: jsre.New(config.DocRoot, config.Printer),
localTransport: config.LocalTransport,
prompt: config.Prompt,
prompter: config.Prompter,
printer: config.Printer,
Expand Down Expand Up @@ -235,9 +238,41 @@ func (c *Console) initExtensions() error {
}
}
})
if !c.localTransport {
c.hideUnavailableDebugMethods()
}
return nil
}

func (c *Console) hideUnavailableDebugMethods() {
c.jsre.Do(func(vm *goja.Runtime) {
if _, err := vm.RunString(`
(function() {
function hideMethod(target, name) {
if (!target) {
return;
}
Object.defineProperty(target, name, {
value: undefined,
writable: true,
configurable: true,
enumerable: false
});
}

if (typeof debug !== "undefined") {
hideMethod(debug, "setHead");
}
if (typeof web3 !== "undefined" && web3 && web3.debug) {
hideMethod(web3.debug, "setHead");
}
})();
`); err != nil {
panic(err)
}
})
}

// initAdmin creates additional admin APIs implemented by the bridge.
func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
if admin := getObject(vm, "admin"); admin != nil {
Expand Down Expand Up @@ -288,7 +323,23 @@ func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, str
start++
break
}
return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
return line[:start], c.filterCompletions(c.jsre.CompleteKeywords(line[start:pos])), line[pos:]
}

func (c *Console) filterCompletions(completions []string) []string {
if c.localTransport {
return completions
}
filtered := completions[:0]
for _, completion := range completions {
switch completion {
case "debug.setHead", "debug.setHead(", "debug.setHead.", "web3.debug.setHead", "web3.debug.setHead(", "web3.debug.setHead.":
continue
default:
filtered = append(filtered, completion)
}
}
return filtered
}

// Welcome show summary of current Geth instance and some metadata about the
Expand Down
Loading
Loading