pwn 0.5.740 → 0.5.741

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4abf337af5fb9173890e87ee2d6d8523f93f62c6d9453d1fec9431f650f04a3e
4
- data.tar.gz: 2639a24b9b3b1895e392b8168a604270b9f43118c743919c7b8348ec8f4dd929
3
+ metadata.gz: 3eb74de135994d78ce8af6b31729ef0622495f0cbe6ff72ddb5ecce5662a5aa7
4
+ data.tar.gz: 3f2d855056ef2cce6424ac399393e38492970850baa36fde37419d5e798ca2f8
5
5
  SHA512:
6
- metadata.gz: e383699557934dab9374bef09204e3e37c0e4b255a3dd3c13ef1597c265fd46b7b71bb79dd8094d2fb42aea71512e01b5a7bce9eae08a3f74d8dfb7639f8ff00
7
- data.tar.gz: 6f1aea7cd79a34738923014fbc13024797c614c33e28058ab8b565402f665ccb464aa440c148291a385f4ad6b2617412d42b3c526df5d2f6cdff349149872c9d
6
+ metadata.gz: 995de4dfb6c55750ed90c2f8c282c4d6ca14e876f41ec207b71aecdc1d162ab69359a20d3d479115c4aea9b9eee7d04d9c3bd1678ff73d1e1c06dbefcbef7c46
7
+ data.tar.gz: 30c2d6602caa5d1fef740508751d4457d69c7f51d861a6260e57f65251c5050d2425c21dad3e946181aa1776345f413e2a035ec3bcf411ae4eedce21441d61d4
data/Gemfile CHANGED
@@ -22,7 +22,7 @@ gem 'brakeman', '8.0.6'
22
22
  gem 'bson', '5.2.0'
23
23
  gem 'bundler', '>=4.0.21'
24
24
  gem 'bundler-audit', '>=0.9.3'
25
- gem 'bunny', '3.3.0'
25
+ gem 'bunny', '3.4.0'
26
26
  gem 'colorize', '1.1.0'
27
27
  gem 'credit_card_validations', '9.0.0'
28
28
  gem 'curses', '1.7.0'
@@ -28,13 +28,20 @@ Class methods take `(opts = {})` and read `opts`.
28
28
 
29
29
  ```ruby
30
30
  PWNSandboxDriver.help
31
- PWNSandboxDriver.help(opts)
31
+ PWNSandboxDriver.available?(opts)
32
32
  ```
33
33
 
34
34
  ## Public methods
35
35
 
36
36
  - `authors`
37
37
  - `help`
38
+ - `available?`
39
+ - `bounded`
40
+ - `execute`
41
+ - `fuzz`
42
+ - `inside`
43
+ - `main`
44
+ - `snapshot`
38
45
 
39
46
  ## Source
40
47
 
@@ -6,6 +6,7 @@ require 'json'
6
6
  require 'base64'
7
7
  require 'pry'
8
8
  require 'reline'
9
+ require 'io/wait'
9
10
 
10
11
  module PWN
11
12
  module Plugins
@@ -134,10 +135,12 @@ module PWN
134
135
 
135
136
  # Curses owns input and rendering until this command returns to Pry.
136
137
  PWN.const_set(:MeshEvents, Queue.new)
137
- PWN::Plugins::REPL.send(:mesh_start_rx!, env: meshtastic_env, obj: mesh_obj)
138
- PWN::Plugins::REPL.send(:mesh_refresh_ui!, env: meshtastic_env)
139
- PWN::Plugins::REPL.send(:mesh_ui_puts, text: 'Ready. /menu opens settings. Incoming channel messages appear here.')
140
- PWN::Plugins::REPL.send(:mesh_console_loop, pry: pi)
138
+ PWN::Plugins::REPL.send(:mesh_capture_output, run: proc {
139
+ PWN::Plugins::REPL.send(:mesh_start_rx!, env: meshtastic_env, obj: mesh_obj)
140
+ PWN::Plugins::REPL.send(:mesh_refresh_ui!, env: meshtastic_env)
141
+ PWN::Plugins::REPL.send(:mesh_ui_puts, text: 'Ready. /menu opens settings. Incoming channel messages appear here.')
142
+ PWN::Plugins::REPL.send(:mesh_console_loop, pry: pi)
143
+ })
141
144
  rescue StandardError => e
142
145
  raise e
143
146
  ensure
@@ -146,6 +149,15 @@ module PWN
146
149
  end
147
150
  end
148
151
  PWN_MESH_TRANSPORTS = %i[serial bluetooth tcp mqtt].freeze
152
+ # meshtastic 0.0.184 loses protobuf defaults in Data#to_h. Empty
153
+ # decoded data is not an unsupported application payload.
154
+ EMPTY_DATA_DECODER = Module.new do
155
+ define_method(:decode_payload) do |opts = {}|
156
+ return nil if opts[:payload].nil? && opts[:msg_type].nil?
157
+
158
+ super(opts)
159
+ end
160
+ end
149
161
 
150
162
  # Meshtastic transport for pwn-mesh: auto (probe order) or a pinned kind.
151
163
  class << PWN::Plugins::REPL # rubocop:disable Metrics/ClassLength
@@ -557,6 +569,7 @@ module PWN
557
569
 
558
570
  # Subscribe for inbound TEXT_MESSAGE_APP frames.
559
571
  def mesh_subscribe(opts = {})
572
+ Meshtastic::MeshInterface.prepend(Mesh::EMPTY_DATA_DECODER) unless Meshtastic::MeshInterface.ancestors.include?(Mesh::EMPTY_DATA_DECODER)
560
573
  env = opts[:env] || {}
561
574
  obj = opts[:obj]
562
575
  psks = opts[:psks]
@@ -1002,7 +1015,13 @@ module PWN
1002
1015
  event = PWN::MeshEvents.pop(true)
1003
1016
  next if event[:obj] && (!PWN.const_defined?(:MeshObj) || !event[:obj].equal?(PWN::MeshObj))
1004
1017
 
1005
- event[:msg] ? mesh_handle_rx(msg: event[:msg]) : mesh_ui_puts(text: event[:text])
1018
+ if event[:notice]
1019
+ mesh_notice(text: event[:notice])
1020
+ elsif event[:msg]
1021
+ mesh_handle_rx(msg: event[:msg])
1022
+ else
1023
+ mesh_ui_puts(text: event[:text])
1024
+ end
1006
1025
  end
1007
1026
  rescue ThreadError
1008
1027
  nil
@@ -1059,6 +1078,10 @@ module PWN
1059
1078
 
1060
1079
  def mesh_ui_puts(opts = {})
1061
1080
  text = opts[:text].to_s
1081
+ if PWN.const_defined?(:MeshEvents) && text.match?(/warning|error|failed|disconnect|persist skipped|unknown command/i)
1082
+ PWN::MeshEvents << { notice: text }
1083
+ return text
1084
+ end
1062
1085
  if PWN.const_defined?(:MeshRxBodyWin) && PWN.const_defined?(:MeshMutex)
1063
1086
  win = PWN.const_get(:MeshRxBodyWin)
1064
1087
  mutex = PWN.const_get(:MeshMutex)
@@ -1079,6 +1102,79 @@ module PWN
1079
1102
  text
1080
1103
  end
1081
1104
 
1105
+ def mesh_capture_output(opts = {})
1106
+ original_out = $stdout
1107
+ original_err = $stderr
1108
+ input, output = IO.pipe
1109
+ output.sync = true
1110
+ events = PWN::MeshEvents
1111
+ collector = Thread.new do
1112
+ loop do
1113
+ text = input.readpartial(4096)
1114
+ begin
1115
+ text << input.readpartial(4096) while text.bytesize < 16_384 && input.wait_readable(0.02)
1116
+ rescue EOFError
1117
+ # Flush the last diagnostic even when the TUI is closing.
1118
+ end
1119
+ events << { notice: text.force_encoding(Encoding::UTF_8).scrub }
1120
+ end
1121
+ rescue IOError
1122
+ nil
1123
+ end
1124
+ $stdout = output
1125
+ $stderr = output
1126
+ opts[:run].call
1127
+ ensure
1128
+ $stdout = original_out
1129
+ $stderr = original_err
1130
+ output&.close
1131
+ collector&.join(1)
1132
+ collector&.kill
1133
+ input&.close
1134
+ end
1135
+
1136
+ def mesh_notice(opts = {})
1137
+ width = [Curses.cols - 2, 80].min
1138
+ lines = mesh_wrap_text(text: opts[:text].to_s, width: [width - 4, 1].max)
1139
+ height = [lines.size + 5, Curses.lines - 2].min
1140
+ win = Curses::Window.new(height, width, (Curses.lines - height) / 2, (Curses.cols - width) / 2)
1141
+ win.keypad(true)
1142
+ offset = 0
1143
+ loop do
1144
+ win.erase
1145
+ mesh_box!(win: win)
1146
+ win.setpos(0, 2)
1147
+ win.attron(Curses.color_pair(20) | Curses::A_BOLD)
1148
+ win.addstr(' NOTICE ')
1149
+ win.attroff(Curses.color_pair(20) | Curses::A_BOLD)
1150
+ lines.drop(offset).first([height - 4, 1].max).each_with_index do |line, index|
1151
+ win.setpos(index + 1, 2)
1152
+ win.addstr(line)
1153
+ end
1154
+ win.setpos(height - 2, (width - 2) / 2)
1155
+ win.attron(Curses.color_pair(20) | Curses::A_BOLD | Curses::A_REVERSE)
1156
+ win.addstr('Ok')
1157
+ win.attroff(Curses.color_pair(20) | Curses::A_BOLD | Curses::A_REVERSE)
1158
+ win.refresh
1159
+ key = opts[:getch] ? opts[:getch].call : win.getch
1160
+ break if [10, 13, "\n", "\r", Curses::KEY_ENTER].include?(key)
1161
+
1162
+ offset = [offset - 1, 0].max if key == Curses::KEY_UP
1163
+ offset += 1 if key == Curses::KEY_DOWN && offset < lines.size - (height - 4)
1164
+ end
1165
+ ensure
1166
+ win&.close
1167
+ %i[MeshRxHeaderWin MeshRxFrameWin MeshRxBodyWin MeshTxWin].each do |name|
1168
+ next unless PWN.const_defined?(name)
1169
+
1170
+ pane = PWN.const_get(name)
1171
+ pane.touch
1172
+ pane.refresh
1173
+ end
1174
+ end
1175
+
1176
+ private :mesh_capture_output, :mesh_notice
1177
+
1082
1178
  # Drop the submitted TX buffer so the next prompt is empty (Reline keeps the old line).
1083
1179
  def mesh_reset_input!(opts = {})
1084
1180
  return opts if PWN.const_defined?(:MeshEvents)
@@ -1264,7 +1360,25 @@ module PWN
1264
1360
  psks = mesh_active_psks(env: env) if psks.empty?
1265
1361
  PWN.const_set(:MeshRxState, { last_from: nil, last_line: nil }) unless PWN.const_defined?(:MeshRxState)
1266
1362
  events = PWN.const_defined?(:MeshEvents) ? PWN::MeshEvents : Queue.new
1363
+ kind = mesh_bound_transport(env: env)
1364
+ interval = Float(opts.fetch(:heartbeat_interval, 30))
1365
+ raise ArgumentError, 'heartbeat interval must be positive and finite' unless interval.positive? && interval.finite?
1366
+
1267
1367
  thread = Thread.new do
1368
+ heartbeat = if obj.is_a?(Hash) && %i[serial bluetooth tcp].include?(kind)
1369
+ Thread.new do
1370
+ transport = { serial: Meshtastic::Serial, bluetooth: Meshtastic::Bluetooth, tcp: Meshtastic::TCP }.fetch(kind)
1371
+ bytes = Meshtastic::ToRadio.new(heartbeat: Meshtastic::Heartbeat.new(nonce: 0)).to_proto
1372
+ loop do
1373
+ break if obj[:closing]
1374
+
1375
+ transport.send_to_radio({ "#{kind}_obj": obj, to_radio: bytes })
1376
+ sleep interval
1377
+ end
1378
+ rescue StandardError => e
1379
+ events << { text: "RX keepalive failed: #{e.class}: #{e.message}", obj: obj }
1380
+ end
1381
+ end
1268
1382
  mesh_subscribe(
1269
1383
  env: env,
1270
1384
  obj: obj,
@@ -1276,6 +1390,9 @@ module PWN
1276
1390
  events << { text: 'RX stopped: transport stream closed. Use /transport to reconnect.', obj: obj }
1277
1391
  rescue StandardError => e
1278
1392
  events << { text: "RX failed: #{e.class}: #{e.message}", obj: obj }
1393
+ ensure
1394
+ heartbeat&.kill
1395
+ heartbeat&.join
1279
1396
  end
1280
1397
  PWN.send(:remove_const, :MeshSubThread) if PWN.const_defined?(:MeshSubThread)
1281
1398
  PWN.const_set(:MeshSubThread, thread)
@@ -1546,13 +1663,17 @@ module PWN
1546
1663
  if decoded[:emoji].to_i.zero?
1547
1664
  messages[[scope, id]] = { from: opts[:from], text: text } if id.positive?
1548
1665
  messages.shift while messages.size > 1000
1549
- return text
1666
+ return text unless decoded[:reply_id].to_i.positive?
1550
1667
  end
1551
1668
 
1552
1669
  original = messages[[scope, decoded[:reply_id].to_i]]
1553
1670
  original ||= mesh_reaction_original(reply_id: decoded[:reply_id], index: opts[:index])
1554
1671
  target = original ? "#{original[:from]} >> #{original[:text]}" : "original message unavailable (packet #{decoded[:reply_id].to_i})"
1555
- "Reacted to: \"#{target}\" with: #{text}."
1672
+ if decoded[:emoji].to_i.positive?
1673
+ "Reacted to: \"#{target}\" with: #{text}."
1674
+ else
1675
+ "Replied to: \"#{target}\" with: #{text}"
1676
+ end
1556
1677
  end
1557
1678
 
1558
1679
  def mesh_reaction_original(opts = {})
@@ -2058,7 +2179,7 @@ module PWN
2058
2179
  end
2059
2180
  true
2060
2181
  rescue StandardError => e
2061
- warn "[pwn-mesh] persist skipped: #{e.class}: #{e.message}"
2182
+ mesh_ui_puts(text: "[pwn-mesh] persist skipped: #{e.class}: #{e.message}")
2062
2183
  false
2063
2184
  end
2064
2185
 
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.740'
4
+ VERSION = '0.5.741'
5
5
  end
@@ -80,6 +80,10 @@ describe PWN::Plugins::REPL, 'mesh packet channel routing' do
80
80
  expect(win).to have_received(:addstr).with(" Reacted to: \"!aabbccdd >> Meet at noon\" with: 👍.\n")
81
81
  expect(win).to have_received(:attron).with((local ? 23 : 21) | Curses::A_BOLD).at_least(:once)
82
82
  expect(described_class).to have_received(:mesh_maybe_dispatch_to_pwn_ai).once
83
+ reply = Meshtastic::MeshPacket.new(id: 125, from: 0xb0b, to: 0xffffffff, channel: 1,
84
+ decoded: { portnum: :TEXT_MESSAGE_APP, payload: 'See you there!', reply_id: 123 }).to_h
85
+ described_class.send(:mesh_handle_rx, local: local, msg: { packet: reply })
86
+ expect(win).to have_received(:addstr).with(" Replied to: \"!aabbccdd >> Meet at noon\" with: See you there!\n")
83
87
  end
84
88
  end
85
89
 
@@ -140,6 +144,8 @@ describe PWN::Plugins::REPL, 'mesh packet channel routing' do
140
144
  text = described_class.send(:mesh_reaction_text, packet: packet, text: '👍', channel: 'LongFast', index: 1)
141
145
  expect(text).to eq('Reacted to: "original message unavailable (packet 999)" with: 👍.')
142
146
  packet[:decoded][:emoji] = 0
147
+ expect(described_class.send(:mesh_reaction_text, packet: packet, text: 'Reply')).to eq('Replied to: "original message unavailable (packet 999)" with: Reply')
148
+ packet[:decoded].delete(:reply_id)
143
149
  expect(described_class.send(:mesh_reaction_text, packet: packet, text: '👍')).to eq('👍')
144
150
  end
145
151
 
@@ -1,8 +1,130 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'meshtastic'
4
5
 
5
6
  describe PWN::Plugins::REPL, 'mesh compose history' do
7
+ it 'does not warn when the real subscriber sees an empty protobuf Data message' do
8
+ stub_const('PWN::MeshTransport', :serial)
9
+ queue = Queue.new
10
+ encoded = Meshtastic::FromRadio.new(packet: { decoded: Meshtastic::Data.new }).to_proto
11
+ queue << Meshtastic::FromRadio.decode(encoded)
12
+ queue << Meshtastic::FromRadio.new(queueStatus: { free: 1 })
13
+ queue.close
14
+ received = []
15
+ output = StringIO.new
16
+ old = $stdout
17
+ $stdout = output
18
+ described_class.send(:mesh_subscribe, obj: { from_radio_queue: queue }, psks: {}, on_message: proc { |message| received << message })
19
+ expect(received.size).to eq(2)
20
+ expect(output.string).not_to include("Can't decode")
21
+ Meshtastic::MeshInterface.new.decode_payload(payload: 'unsupported', msg_type: :PRIVATE_APP)
22
+ expect(output.string).to include("Can't decode", 'unsupported')
23
+ ensure
24
+ $stdout = old
25
+ end
26
+
27
+ it 'queues warnings for acknowledgement instead of writing through the TUI' do
28
+ events = Queue.new
29
+ stub_const('PWN::MeshEvents', events)
30
+ expect(described_class).not_to receive(:puts)
31
+ described_class.send(:mesh_ui_puts, text: 'WARNING: fixture warning')
32
+ expect(events.pop(true)).to eq(notice: 'WARNING: fixture warning')
33
+ end
34
+
35
+ it 'displays queued diagnostics on the UI thread' do
36
+ events = Queue.new
37
+ stub_const('PWN::MeshEvents', events)
38
+ events << { notice: 'RX failed: fixture' }
39
+ expect(described_class).to receive(:mesh_notice).with(text: 'RX failed: fixture')
40
+ described_class.send(:mesh_drain_events)
41
+ end
42
+
43
+ it 'captures dependency stdout and stderr and restores both streams on failure' do
44
+ events = Queue.new
45
+ stub_const('PWN::MeshEvents', events)
46
+ original = [$stdout, $stderr]
47
+ expect do
48
+ described_class.send(:mesh_capture_output, run: proc {
49
+ puts 'WARNING: decoder fixture'
50
+ warn 'ERROR: transport fixture'
51
+ raise IOError, 'fixture'
52
+ })
53
+ end.to raise_error(IOError, 'fixture')
54
+ expect([$stdout, $stderr]).to eq(original)
55
+ output = []
56
+ output << events.pop(true)[:notice] until events.empty?
57
+ expect(output.join).to include('WARNING: decoder fixture', 'ERROR: transport fixture')
58
+ end
59
+
60
+ it 'centers a reverse-colored Ok button and repaints the underlying panes' do
61
+ win = double('notice', maxx: 78, maxy: 6)
62
+ %i[keypad erase setpos addstr refresh close attron attroff].each { |method| allow(win).to receive(method) }
63
+ allow(Curses).to receive(:color_pair).with(20).and_return(5120)
64
+ allow(Curses).to receive(:cols).and_return(80)
65
+ allow(Curses).to receive(:lines).and_return(24)
66
+ allow(Curses::Window).to receive(:new).and_return(win)
67
+ pane = double('pane', touch: nil, refresh: nil)
68
+ stub_const('PWN::MeshTxWin', pane)
69
+ keys = ["\e", 'q', "\n"]
70
+ described_class.send(:mesh_notice, text: 'Decoder warning', getch: proc { keys.shift })
71
+ expect(keys).to be_empty
72
+ expect(win).to have_received(:addstr).with('Ok').exactly(3).times
73
+ expect(win).not_to have_received(:addstr).with(a_string_matching(/Acknowledged|Enter/))
74
+ expect(win).to have_received(:setpos).with(4, 38).exactly(3).times
75
+ expect(win).to have_received(:attron).with(5120 | Curses::A_BOLD | Curses::A_REVERSE).exactly(3).times
76
+ expect(win).to have_received(:attroff).with(5120 | Curses::A_BOLD | Curses::A_REVERSE).exactly(3).times
77
+ expect(win).to have_received(:close)
78
+ expect(pane).to have_received(:touch)
79
+ end
80
+
81
+ it 'keeps an idle PhoneAPI session alive and stops its heartbeat with the subscriber' do
82
+ events = Queue.new
83
+ beats = Queue.new
84
+ release = Queue.new
85
+ obj = {}
86
+ stub_const('PWN::MeshEvents', events)
87
+ stub_const('PWN::MeshObj', obj)
88
+ stub_const('PWN::MeshTransport', :serial)
89
+ allow(Meshtastic::Serial).to receive(:send_to_radio) do |args|
90
+ beats << [Thread.current, Meshtastic::ToRadio.decode(args[:to_radio])]
91
+ end
92
+ allow(described_class).to receive(:mesh_subscribe) do |args|
93
+ release.pop
94
+ args[:on_message].call(packet: { decoded: { portnum: 1, payload: 'after idle' } })
95
+ end
96
+ thread = described_class.send(:mesh_start_rx!, env: { channel: {} }, obj: obj, heartbeat_interval: 0.01)
97
+ first = beats.pop(timeout: 1)
98
+ second = beats.pop(timeout: 1)
99
+ expect(first).not_to be_nil
100
+ expect(second).not_to be_nil
101
+ expect(second.last.payload_variant).to eq(:heartbeat)
102
+ expect(second.last.heartbeat.nonce).to eq(0)
103
+ release << true
104
+ expect(thread.join(1)).to eq(thread)
105
+ expect(first.first).not_to be_alive
106
+ expect(events.pop[:msg].dig(:packet, :decoded, :payload)).to eq('after idle')
107
+ ensure
108
+ thread&.kill
109
+ thread&.join(1)
110
+ end
111
+
112
+ it 'drains incoming messages during idle input polls without a keypress' do
113
+ pi = double('pry', config: double('config', pwn_mesh: true))
114
+ events = Queue.new
115
+ stub_const('PWN::MeshEvents', events)
116
+ message = { packet: { decoded: { portnum: 1, payload: 'idle RX' } } }
117
+ polls = 0
118
+ allow(described_class).to receive(:mesh_draw_input)
119
+ expect(described_class).to receive(:mesh_handle_rx).with(msg: message)
120
+ reader = proc do
121
+ polls += 1
122
+ events << { msg: message } if polls == 20
123
+ polls > 20 ? "\u0004" : nil
124
+ end
125
+ described_class.send(:mesh_console_loop, pry: pi, getch: reader)
126
+ end
127
+
6
128
  it 'recalls submissions with Up/Down and restores the unfinished draft and cursor' do
7
129
  config = double('config', pwn_mesh: true)
8
130
  pry = double('pry', config: config)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pwn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.740
4
+ version: 0.5.741
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -169,14 +169,14 @@ dependencies:
169
169
  requirements:
170
170
  - - '='
171
171
  - !ruby/object:Gem::Version
172
- version: 3.3.0
172
+ version: 3.4.0
173
173
  type: :runtime
174
174
  prerelease: false
175
175
  version_requirements: !ruby/object:Gem::Requirement
176
176
  requirements:
177
177
  - - '='
178
178
  - !ruby/object:Gem::Version
179
- version: 3.3.0
179
+ version: 3.4.0
180
180
  - !ruby/object:Gem::Dependency
181
181
  name: colorize
182
182
  requirement: !ruby/object:Gem::Requirement