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. Plain ENTER submits the full (possibly multi-line) buffer; a newline is inserted (keep editing) by ANY of:

- SHIFT+ENTER   on capable terminals (kitty, wezterm, foot,
              alacritty, xterm, Konsole, iTerm2, Windows
              Terminal; and Terminator via `pwn setup --terminal`)
- ALT+ENTER     terminal-agnostic fallback — every emulator,
              including bare VTE, sends `\e\r` for this.
- trailing `\`  bash/zsh/irb/psql-style continuation: end a line
              with backslash + ENTER to keep composing. The
              backslashes are stripped before submit.

Multi-line pastes work (Reline holds \n in the buffer; ENTER submits). See documentation/pwn-REPL.md § Multi-line input for the terminal support matrix and pwn setup --terminal for the opt-in VTE fix.

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 -s 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  ALT+ENTER — universal fallback (all emulators, incl. VTE)
  [27, 10],                                  # \e\n  ALT+ENTER (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.



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

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

Instance Attribute Details

#line_bufferObject (readonly)

Returns the value of attribute line_buffer.



31
32
33
# File 'lib/pwn/plugins/repl.rb', line 31

def line_buffer
  @line_buffer
end

Instance Method Details

#ensure_tmux_extended_keysObject

tmux gates modifyOtherKeys / kitty-keyboard requests behind its extended-keys server option. When off (the shipped default on many distros / older ~/.tmux.conf), tmux silently drops the ENABLE_EXTENDED_KEYS CSI we emit in #readline and forwards plain 0x0D for BOTH Enter and Shift+Enter — SHIFT_ENTER_SEQS can then never match and Shift+Enter "still just submits".

Detect tmux via $TMUX, read the current server option, and flip it to on (NOT always) so tmux honours the per-read enable/disable we send around Reline.readmultiline. on is scoped: tmux only encodes extended keys while the inner app is requesting them, so this does not affect other panes or the main pwn REPL.

Verified on tmux 3.6b: extended-keys on + \e[>4;1m → S-Enter is delivered as \e[27;2;13~ (matches SHIFT_ENTER_SEQS).



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/pwn/plugins/repl.rb', line 119

def ensure_tmux_extended_keys
  return if self.class.instance_variable_get(:@tmux_extkeys_checked)

  self.class.instance_variable_set(:@tmux_extkeys_checked, true)
  return if ENV['TMUX'].to_s.empty?

  # (1) Inner side: tmux → app. `extended-keys on` makes tmux honour the
  #     ENABLE_EXTENDED_KEYS request we emit in #readline and re-encode
  #     S-Enter to the pane as CSI 27;2;13~ / CSI 13;2u.
  cur = `tmux show -sv extended-keys 2>/dev/null`.to_s.strip
  unless %w[on always].include?(cur)
    if system('tmux', 'set', '-s', 'extended-keys', 'on', out: File::NULL, err: File::NULL)
      warn '[pwn] tmux `extended-keys` was off; auto-enabled (server scope) so SHIFT+ENTER is distinguishable from ENTER.'
      warn '[pwn] Persist it: add `set -s extended-keys on` to ~/.tmux.conf'
    else
      warn '[pwn] tmux `extended-keys` is off and could not be enabled; SHIFT+ENTER will behave like ENTER.'
      warn '[pwn] Fix: run `tmux set -s extended-keys on` (and add `set -s extended-keys on` to ~/.tmux.conf).'
    end
  end

  # (2) Outer side: terminal → tmux. tmux only ASKS the outer terminal
  #     to encode S-Enter distinctly (sends `\e[>4;2m` at attach) if the
  #     client tty has the `extkeys` feature. That comes from the
  #     `terminal-features` server option matched against the client's
  #     $TERM at attach time. No match ⇒ outer emulator keeps sending
  #     0x0D for BOTH Enter and Shift+Enter ⇒ tmux can't disambiguate ⇒
  #     step (1) is moot. Add it for common outer TERMs (and tmux* to
  #     cover `alias tmux='TERM=tmux-256color tmux'` and nested tmux).
  tf = `tmux show -sv terminal-features 2>/dev/null`.to_s
  unless tf.include?('extkeys')
    %w[xterm* tmux* screen*].each do |pat|
      system('tmux', 'set', '-as', 'terminal-features', "#{pat}:extkeys", out: File::NULL, err: File::NULL)
    end
    warn '[pwn] Added `extkeys` to tmux terminal-features (xterm*/tmux*/screen*) so tmux requests extended keys from the OUTER terminal.'
    warn "[pwn] Persist it: add `set -as terminal-features 'xterm*:extkeys'` (and tmux*/screen*) to ~/.tmux.conf"
  end

  # (3) terminal-features is evaluated at CLIENT ATTACH time. If the
  #     current client attached before `extkeys` was present, tmux never
  #     sent the enable CSI to the outer terminal. Detect and warn.
  feats = `tmux display -p '\#{client_termfeatures}' 2>/dev/null`.to_s
  return if feats.include?('extkeys')

  warn '[pwn] This tmux client attached before `extkeys` was configured; the outer terminal is still sending plain 0x0D for SHIFT+ENTER.'
  warn '[pwn] Fix: detach (prefix + d) and reattach (`tmux attach -t <session>`) so tmux re-negotiates extended keys with the terminal.'
rescue StandardError => e
  warn "[pwn] ensure_tmux_extended_keys: #{e.class}: #{e.message}"
end

#ensure_vte_shift_enterObject

VTE-based emulators (Terminator, GNOME Terminal, Tilix, xfce4-terminal, Guake, Ptyxis, MATE Terminal, ...) do not implement xterm modifyOtherKeys (CSI >4;Nm) nor the kitty keyboard protocol (CSI >1u) — see GNOME/vte issues #2601 and #2607. The ENABLE_EXTENDED_KEYS request we emit (and that tmux emits to the outer terminal via Eneks) is silently ignored, so a physical Shift+Enter reaches us as an indistinguishable plain 0x0D. No tmux/Reline configuration can fix that — the modifier was lost at the outer terminal.

This method therefore ONLY detects and hints. It never mutates the user's host. Terminal-agnostic fallbacks (Alt+Enter, trailing \) already work; for real Shift+Enter under Terminator the user can opt in with pwn setup --terminal, which installs third_party/terminator/pwn_shift_enter.py into ~/.config/terminator/plugins/ after asking permission.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/pwn/plugins/repl.rb', line 184

def ensure_vte_shift_enter
  return if self.class.instance_variable_get(:@vte_shift_enter_checked)

  self.class.instance_variable_set(:@vte_shift_enter_checked, true)
  vte_ver = ENV['VTE_VERSION'].to_s
  return if vte_ver.empty?

  in_terminator = !ENV['TERMINATOR_UUID'].to_s.empty? || !ENV['TERMINATOR_DBUS_NAME'].to_s.empty?
  # If the plugin is already installed & enabled, stay silent.
  if in_terminator
    cfg = File.join(Dir.home, '.config', 'terminator', 'config')
    return if File.exist?(cfg) && File.read(cfg).include?('PWNShiftEnter')
  end

  host = if in_terminator then 'Terminator'
         elsif ENV['GNOME_TERMINAL_SCREEN'] || ENV['GNOME_TERMINAL_SERVICE'] then 'GNOME Terminal'
         elsif ENV['TILIX_ID'] then 'Tilix'
         else "a VTE-#{vte_ver} terminal"
         end

  warn "[pwn] #{host} (libvte) can't distinguish SHIFT+ENTER from ENTER — the modifier is dropped at the emulator."
  warn '[pwn] Multi-line input still works: use ALT+ENTER, or end the line with `\` then ENTER.'
  if in_terminator
    warn '[pwn] For real SHIFT+ENTER support here, run: `pwn setup --terminal` (installs a Terminator plugin — opt-in, one-time).'
  else
    warn '[pwn] For native SHIFT+ENTER, use kitty / wezterm / foot / alacritty / xterm / Konsole / iTerm2 / Terminator.'
  end
rescue StandardError => e
  warn "[pwn] ensure_vte_shift_enter: #{e.class}: #{e.message}"
end

#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.



230
231
232
233
234
235
236
237
238
239
240
# File 'lib/pwn/plugins/repl.rb', line 230

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



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/pwn/plugins/repl.rb', line 242

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
    # Plain ENTER submits UNLESS the last non-whitespace char on the
    # last line is `\` (bash/irb/psql-style continuation) — that is
    # the terminal-agnostic fallback for emulators that can't send a
    # distinct SHIFT+ENTER (all VTE hosts). SHIFT+ENTER / ALT+ENTER
    # (matched via SHIFT_ENTER_SEQS) trigger :key_newline directly.
    # Reline handles multi-line pastes by splitting on \n in-buffer.
    @line_buffer = Reline.readmultiline(prompt, true) do |buffer|
      !buffer.split("\n", -1).last.to_s.rstrip.end_with?('\\')
    end || ''
    # Strip the continuation markers before handing off to the caller.
    @line_buffer = @line_buffer.gsub(/\\[ \t]*\n/, "\n")
  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.



97
98
99
100
101
102
# File 'lib/pwn/plugins/repl.rb', line 97

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)


274
275
276
# File 'lib/pwn/plugins/repl.rb', line 274

def tty?
  true
end

#winsizeObject



278
279
280
# File 'lib/pwn/plugins/repl.rb', line 278

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