Class: PWN::Plugins::REPL::PWNMultiLineInput

Inherits:
Object
  • Object
show all
Defined in:
lib/pwn/plugins/repl.rb

Overview

Custom input handler for pwn-ai and pwn-asm to support multi-line submissions:

  • Use only SHIFT+ENTER to insert a newline (continue editing).

  • Plain ENTER submits the full (possibly multi-line) buffer.

  • Multi-line pastes are supported (Reline handles n in buffer; submit with ENTER).

Strict SHIFT+ENTER only — no Ctrl+J, Alt-Enter, or other fallbacks (per requirements).

Constant Summary collapse

SHIFT_ENTER_SEQS =

SHIFT+ENTER escape sequences (byte arrays). These are terminal-dependent. Listed common ones for xterm, VTE (terminator), kitty, wezterm, etc. (with modifyOtherKeys / extended-keys enabled).

For tmux + terminator (or similar):

In ~/.tmux.conf (then `tmux kill-server` + new session):
  set -g extended-keys on
  set -g xterm-keys on
Use TERM=xterm-256color (or equivalent that supports the CSI) in your terminal profile.

The bindings make matching sequences produce :key_newline (insert n without submit).

If after typing text + SHIFT+ENTER it still submits instead of newline:

1. Apply the tmux.conf + TERM changes above and fully restart tmux.
2. In your *real* terminal (the one running `pwn`), run a capture script from /tmp ONLY:
     ruby /tmp/capture_keys.rb
   (Debugging scripts must live in /tmp per user rule; never commit them to /opt/pwn.)
3. Paste the exact bytes array for the SHIFT+ENTER press here so it can be added to the list.
[
  [27, 91, 49, 51, 59, 50, 126],             # \e[13;2~
  [27, 91, 50, 55, 59, 50, 59, 49, 51, 126], # \e[27;2;13~
  [27, 91, 49, 51, 59, 50, 117],             # \e[13;2u (CSI u)
  [27, 91, 50, 55, 59, 50, 59, 49, 51, 117], # \e[27;2;13u
  [27, 91, 49, 59, 50, 126],                 # \e[1;2~
  [27, 13],                                  # \e\r (ESC+CR variant)
  [27, 10],                                  # \e\n (ESC+LF variant)
  [27, 91, 13, 59, 50, 126],                 # \e[13;2~ alt numeric
  [27, 91, 49, 59, 50, 117],                 # \e[1;2u
  [27, 91, 50, 55, 59, 50, 13, 126],         # \e[27;2;13~ variant
  [27, 79, 77]                               # \eOM (application-keypad Enter; some emulators emit this for S-Enter)
].freeze
ENABLE_EXTENDED_KEYS =

CSI sequences that ask the terminal to start/stop encoding Shift+Enter (and other modified keys) distinctly from plain Enter. Without one of these active, most emulators send the SAME byte (0x0D) for both, so SHIFT_ENTER_SEQS can never match.

\e[>4;1m / \e[>4;0m   xterm modifyOtherKeys on/off (level 1 —
                      disambiguates Shift+Enter without altering
                      Ctrl-C). xterm, VTE/Terminator, iTerm2,
                      Konsole. tmux ≥3.2 with `extended-keys on`
                      honours this request and re-encodes as
                      CSI-u to the inner app.
\e[>1u   / \e[<u      kitty keyboard protocol push/pop, flags=1
                      "disambiguate escape codes". kitty, wezterm,
                      foot, ghostty, alacritty, recent tmux.

Emitting both is harmless on terminals that support neither —they’re DEC-private CSIs and get silently ignored.

"\e[>4;1m\e[>1u"
DISABLE_EXTENDED_KEYS =
"\e[<u\e[>4;0m"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pry_instance) ⇒ PWNMultiLineInput

Returns a new instance of PWNMultiLineInput.



77
78
79
80
81
# File 'lib/pwn/plugins/repl.rb', line 77

def initialize(pry_instance)
  @line_buffer = ''
  pry_instance.config.pwn_ai_original_input = Pry.input
  install_shift_enter_bindings
end

Instance Attribute Details

#line_bufferObject (readonly)

Returns the value of attribute line_buffer.



23
24
25
# File 'lib/pwn/plugins/repl.rb', line 23

def line_buffer
  @line_buffer
end

Instance Method Details

#install_shift_enter_bindingsObject

Register SHIFT+ENTER → :key_newline on Reline’s default keymaps.

IMPORTANT: do NOT use add_oneshot_key_binding for this. Reline’s LineEditor#input_key calls reset_oneshot_key_bindings on EVERY keystroke (it’s designed for dialog trap-keys = “next keypress only”), so oneshot bindings are wiped the moment the user types their first character — Shift+Enter then falls through as an unrecognised CSI and is silently swallowed. Default-keymap bindings persist for the life of the Config object.

Scoping is handled by the input-handler swap, not the binding lifetime: outside pwn-ai/pwn-asm, Pry uses its own input, PWNMultiLineInput#readline never runs, ENABLE_EXTENDED_KEYS is never emitted, the terminal sends plain 0x0D for Shift+Enter, and these bindings never match. So registering once at construction is safe.



109
110
111
112
113
114
115
116
117
118
119
# File 'lib/pwn/plugins/repl.rb', line 109

def install_shift_enter_bindings
  return if self.class.instance_variable_get(:@shift_enter_installed)

  cfg = reline_config
  %i[emacs vi_insert].each do |keymap|
    SHIFT_ENTER_SEQS.each do |seq|
      cfg.add_default_key_binding_by_keymap(keymap, seq, :key_newline)
    end
  end
  self.class.instance_variable_set(:@shift_enter_installed, true)
end

#readline(prompt) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/pwn/plugins/repl.rb', line 121

def readline(prompt)
  # Ask the terminal to encode Shift+Enter distinctly from Enter for
  # the duration of this read. Without this, most emulators send 0x0D
  # for both and SHIFT_ENTER_SEQS can never match. Reset in `ensure`.
  tty = $stdout.respond_to?(:tty?) && $stdout.tty?
  if tty
    $stdout.write(ENABLE_EXTENDED_KEYS)
    $stdout.flush
  end

  begin
    # readmultiline with confirm block that *always* returns true:
    #   => default (plain) ENTER triggers finish/submit of the (multi-line) buffer
    # SHIFT+ENTER (matched seq) triggers :key_newline (insert \n, stay in edit mode)
    # Reline handles multi-line pastes by splitting on \n in the buffer.
    @line_buffer = Reline.readmultiline(prompt, true) { |_buffer| true } || ''
  ensure
    if tty
      $stdout.write(DISABLE_EXTENDED_KEYS)
      $stdout.flush
    end
  end
  @line_buffer
end

#reline_configObject

Reline ≤ 0.5.x exposed a top-level ‘Reline.config` delegator. Reline ≥ 0.6.x removed it; the Config object now lives only on the (private) singleton `Reline.core`. Probe in order of preference so the same code works across both.



87
88
89
90
91
92
# File 'lib/pwn/plugins/repl.rb', line 87

def reline_config
  return Reline.config if Reline.respond_to?(:config)
  return Reline.core.config if Reline.respond_to?(:core)

  Reline.send(:core).config
end

#tty?Boolean

Compatibility with Pry input expectations

Returns:

  • (Boolean)


147
148
149
# File 'lib/pwn/plugins/repl.rb', line 147

def tty?
  true
end

#winsizeObject



151
152
153
# File 'lib/pwn/plugins/repl.rb', line 151

def winsize
  [TTY::Screen.rows || 24, TTY::Screen.columns || 80]
end