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 -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 (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
82
# File 'lib/pwn/plugins/repl.rb', line 77

def initialize(pry_instance)
  @line_buffer = ''
  pry_instance.config.pwn_ai_original_input = Pry.input
  ensure_tmux_extended_keys
  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

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



110
111
112
113
114
115
116
117
118
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
# File 'lib/pwn/plugins/repl.rb', line 110

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

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



174
175
176
177
178
179
180
181
182
183
184
# File 'lib/pwn/plugins/repl.rb', line 174

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



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

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.



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

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)


212
213
214
# File 'lib/pwn/plugins/repl.rb', line 212

def tty?
  true
end

#winsizeObject



216
217
218
# File 'lib/pwn/plugins/repl.rb', line 216

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