pwn 0.5.734 → 0.5.735
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 +4 -4
- data/lib/pwn/plugins/repl/mesh.rb +150 -45
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/plugins/repl_mesh_chunk_spec.rb +111 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8f7823f226f8196c1c33bda916bb5ffde0d6ca22b0c47b3d7b5518ca85e9a8b
|
|
4
|
+
data.tar.gz: f069d75fa0a0e678aff23e9255cdd5ea13e4b3821a93fa6a1a0fd86e5648171c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4795ce1673e60efc6919b72aa25cd0d1058a740234fdccb55efb104708d9940a32ca77d0f600195662dd543d5d20403435313d7c3746266d01fc761b3af8e4ae
|
|
7
|
+
data.tar.gz: 1fb653643bcd942b89a7e220009a5b05714950382f1598581bd859e8c2f8c9f64c5c60e37570a02d2c6d4e31831a8978dead7793766c529a48373fe6acd5b31b
|
|
@@ -536,10 +536,107 @@ module PWN
|
|
|
536
536
|
end
|
|
537
537
|
|
|
538
538
|
# Send a text frame on the active mesh transport.
|
|
539
|
+
def mesh_text_payload_max(opts = {})
|
|
540
|
+
return 0 unless opts.is_a?(Hash)
|
|
541
|
+
|
|
542
|
+
cap = opts[:max] || Meshtastic::Constants::DATA_PAYLOAD_LEN
|
|
543
|
+
# DATA_PAYLOAD_LEN is the protobuf Data.payload max. A MeshPacket
|
|
544
|
+
# around a full-size payload is ~260 bytes and will not fit a 256-byte
|
|
545
|
+
# LoRa frame, so the radio accepts ToRadio then silently drops TX.
|
|
546
|
+
lora = 256
|
|
547
|
+
overhead = 32
|
|
548
|
+
[cap, lora - overhead].min
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def mesh_payload_fits?(opts = {})
|
|
552
|
+
return false unless opts.is_a?(Hash)
|
|
553
|
+
|
|
554
|
+
text = opts[:text].to_s
|
|
555
|
+
max = opts[:max] || mesh_text_payload_max
|
|
556
|
+
text.length <= max && text.bytesize <= max
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def mesh_tx_row_ready?(opts = {})
|
|
560
|
+
return false unless opts.is_a?(Hash)
|
|
561
|
+
|
|
562
|
+
row = opts[:row]
|
|
563
|
+
return false unless row.is_a?(Hash)
|
|
564
|
+
|
|
565
|
+
packet = row[:packet] || row['packet']
|
|
566
|
+
return false unless packet.is_a?(Hash)
|
|
567
|
+
|
|
568
|
+
decoded = packet[:decoded] || packet['decoded']
|
|
569
|
+
return false unless decoded.is_a?(Hash)
|
|
570
|
+
|
|
571
|
+
port = decoded[:portnum] || decoded['portnum']
|
|
572
|
+
%w[5 ROUTING_APP].include?(port.to_s) || port == :ROUTING_APP
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def mesh_wait_tx_slot(opts = {})
|
|
576
|
+
return unless opts.is_a?(Hash)
|
|
577
|
+
|
|
578
|
+
obj = opts[:obj]
|
|
579
|
+
return unless obj.is_a?(Hash)
|
|
580
|
+
|
|
581
|
+
timeout = opts[:timeout]
|
|
582
|
+
timeout = 8 if timeout.nil?
|
|
583
|
+
timeout = Float(timeout)
|
|
584
|
+
return if timeout <= 0
|
|
585
|
+
|
|
586
|
+
since = opts[:since].to_i
|
|
587
|
+
deadline = Time.now + timeout
|
|
588
|
+
loop do
|
|
589
|
+
rows = if obj[:rx_mutex]
|
|
590
|
+
obj[:rx_mutex].synchronize { Array(obj[:proto_data]).dup }
|
|
591
|
+
else
|
|
592
|
+
Array(obj[:proto_data])
|
|
593
|
+
end
|
|
594
|
+
ready = rows.drop(since).any? do |row|
|
|
595
|
+
mesh_tx_row_ready?(row: row)
|
|
596
|
+
end
|
|
597
|
+
return if ready
|
|
598
|
+
return if Time.now >= deadline
|
|
599
|
+
|
|
600
|
+
sleep 0.05
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def mesh_text_chunks(opts = {})
|
|
605
|
+
return [] unless opts.is_a?(Hash)
|
|
606
|
+
|
|
607
|
+
text = opts[:text].to_s
|
|
608
|
+
max = mesh_text_payload_max
|
|
609
|
+
return [text] if mesh_payload_fits?(text: text, max: max)
|
|
610
|
+
|
|
611
|
+
n = 2
|
|
612
|
+
loop do
|
|
613
|
+
prefix = "(#{n}/#{n}) "
|
|
614
|
+
body_max = max - prefix.length
|
|
615
|
+
raise ArgumentError, "DATA_PAYLOAD_LEN #{max} cannot hold a chunk prefix" if body_max < 1
|
|
616
|
+
|
|
617
|
+
bodies = []
|
|
618
|
+
offset = 0
|
|
619
|
+
while offset < text.length
|
|
620
|
+
take = body_max
|
|
621
|
+
piece = text[offset, take].to_s
|
|
622
|
+
while !mesh_payload_fits?(text: "#{prefix}#{piece}", max: max) && take > 1
|
|
623
|
+
take -= 1
|
|
624
|
+
piece = text[offset, take].to_s
|
|
625
|
+
end
|
|
626
|
+
bodies << piece
|
|
627
|
+
offset += piece.length
|
|
628
|
+
end
|
|
629
|
+
if bodies.size <= n
|
|
630
|
+
total = bodies.size
|
|
631
|
+
return bodies.each_with_index.map { |body, i| "(#{i + 1}/#{total}) #{body}" }
|
|
632
|
+
end
|
|
633
|
+
n = bodies.size
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
539
637
|
def mesh_send_text(opts = {})
|
|
540
638
|
env = opts[:env] || {}
|
|
541
639
|
obj = opts[:obj]
|
|
542
|
-
text = opts[:text]
|
|
543
640
|
from = opts[:from]
|
|
544
641
|
channel = opts[:channel]
|
|
545
642
|
psks = opts[:psks]
|
|
@@ -550,49 +647,57 @@ module PWN
|
|
|
550
647
|
radio = opts[:radio]
|
|
551
648
|
radio = mesh_radio_index_for_name(env: env, obj: obj, name: channel_name) if radio.nil? && !channel_name.empty?
|
|
552
649
|
radio = mesh_radio_channel(env: env, obj: obj) if radio.nil? && %i[serial bluetooth tcp].include?(kind)
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
650
|
+
chunks = mesh_text_chunks(text: opts[:text])
|
|
651
|
+
chunks.each_with_index do |piece, idx|
|
|
652
|
+
since = obj.is_a?(Hash) ? Array(obj[:proto_data]).size : 0
|
|
653
|
+
case kind
|
|
654
|
+
when :serial
|
|
655
|
+
tx = { serial_obj: obj, to: dest, text: piece, want_ack: true }
|
|
656
|
+
tx[:channel] = radio unless radio.nil?
|
|
657
|
+
Meshtastic::Serial.send_text(tx)
|
|
658
|
+
when :bluetooth
|
|
659
|
+
tx = { bluetooth_obj: obj, to: dest, text: piece, want_ack: true }
|
|
660
|
+
tx[:channel] = radio unless radio.nil?
|
|
661
|
+
Meshtastic::Bluetooth.send_text(tx)
|
|
662
|
+
when :tcp
|
|
663
|
+
tx = { tcp_obj: obj, to: dest, text: piece }
|
|
664
|
+
tx[:channel] = radio unless radio.nil?
|
|
665
|
+
Meshtastic::TCP.send_text(tx)
|
|
666
|
+
else
|
|
667
|
+
send_psks = psks
|
|
668
|
+
send_psks = mesh_channel_psks(env: env) if send_psks.nil? || send_psks.empty?
|
|
669
|
+
Meshtastic::MQTT.send_text(
|
|
670
|
+
mqtt_obj: obj,
|
|
671
|
+
from: from,
|
|
672
|
+
to: dest,
|
|
673
|
+
region: mesh_mqtt_region(region: opts[:region], env: env),
|
|
674
|
+
topic: mesh_mqtt_topic(env: env, topic: opts[:topic]),
|
|
675
|
+
channel: channel,
|
|
676
|
+
text: piece,
|
|
677
|
+
psks: send_psks
|
|
678
|
+
)
|
|
679
|
+
end
|
|
680
|
+
from_id = from.to_s
|
|
681
|
+
from_id = mesh_self_node_id(env: env, obj: obj) if from_id.empty?
|
|
682
|
+
PWN.send(:remove_const, :MeshLastTx) if PWN.const_defined?(:MeshLastTx)
|
|
683
|
+
PWN.const_set(:MeshLastTx, { from: from_id, to: dest, text: piece.to_s, at: Time.now })
|
|
684
|
+
mesh_handle_rx(
|
|
685
|
+
local: true,
|
|
686
|
+
channel_name: channel_name,
|
|
687
|
+
msg: {
|
|
688
|
+
packet: {
|
|
689
|
+
channel: radio,
|
|
690
|
+
node_id_from: from_id,
|
|
691
|
+
node_id_to: dest,
|
|
692
|
+
decoded: { portnum: :TEXT_MESSAGE_APP, payload: piece.to_s }
|
|
693
|
+
}
|
|
694
|
+
}
|
|
578
695
|
)
|
|
696
|
+
next unless idx + 1 < chunks.size
|
|
697
|
+
next unless %i[serial bluetooth tcp].include?(kind)
|
|
698
|
+
|
|
699
|
+
mesh_wait_tx_slot(obj: obj, since: since)
|
|
579
700
|
end
|
|
580
|
-
from_id = from.to_s
|
|
581
|
-
from_id = mesh_self_node_id(env: env, obj: obj) if from_id.empty?
|
|
582
|
-
PWN.send(:remove_const, :MeshLastTx) if PWN.const_defined?(:MeshLastTx)
|
|
583
|
-
PWN.const_set(:MeshLastTx, { from: from_id, to: dest, text: text.to_s, at: Time.now })
|
|
584
|
-
mesh_handle_rx(
|
|
585
|
-
local: true,
|
|
586
|
-
channel_name: channel_name,
|
|
587
|
-
msg: {
|
|
588
|
-
packet: {
|
|
589
|
-
channel: radio,
|
|
590
|
-
node_id_from: from_id,
|
|
591
|
-
node_id_to: dest,
|
|
592
|
-
decoded: { portnum: :TEXT_MESSAGE_APP, payload: text.to_s }
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
)
|
|
596
701
|
end
|
|
597
702
|
|
|
598
703
|
# Close the Meshtastic session opened by #mesh_connect.
|
|
@@ -1268,8 +1373,8 @@ module PWN
|
|
|
1268
1373
|
channel: slot[:channel_num] || channel_name,
|
|
1269
1374
|
psks: mesh_channel_psks(env: env)
|
|
1270
1375
|
)
|
|
1271
|
-
rescue StandardError
|
|
1272
|
-
|
|
1376
|
+
rescue StandardError => e
|
|
1377
|
+
mesh_ui_puts(text: "TX failed: #{e.class}: #{e.message}")
|
|
1273
1378
|
ensure
|
|
1274
1379
|
PWN.send(:remove_const, :MeshDispatchLock) if PWN.const_defined?(:MeshDispatchLock)
|
|
1275
1380
|
end
|
|
@@ -1705,7 +1810,7 @@ module PWN
|
|
|
1705
1810
|
|
|
1706
1811
|
private :mesh_transport, :mesh_bound_transport, :mesh_box!, :mesh_mqtt_region, :mesh_mqtt_topic, :mesh_active_psks, :mesh_device_channel_meta, :mesh_device_channels
|
|
1707
1812
|
private :mesh_radio_channel, :mesh_radio_index_for_name, :mesh_channel_name_for_index, :mesh_unassigned_slot_map, :mesh_psk_b64, :mesh_psk_same?, :mesh_env_channel_name_for_psk, :mesh_whitelist_name_for_index, :mesh_channel_name_from_topic, :mesh_link_label
|
|
1708
|
-
private :mesh_connect_one, :mesh_connect, :mesh_mqtt_tls?, :mesh_subscribe, :mesh_send_text, :mesh_disconnect, :mesh_compose_send, :mesh_channel_names, :mesh_env_hash
|
|
1813
|
+
private :mesh_connect_one, :mesh_connect, :mesh_mqtt_tls?, :mesh_subscribe, :mesh_text_payload_max, :mesh_payload_fits?, :mesh_text_chunks, :mesh_tx_row_ready?, :mesh_wait_tx_slot, :mesh_send_text, :mesh_disconnect, :mesh_compose_send, :mesh_channel_names, :mesh_env_hash
|
|
1709
1814
|
private :mesh_submit, :mesh_console_loop, :mesh_drain_events, :mesh_draw_input, :mesh_wrap_text, :mesh_ui_puts, :mesh_menu_root, :mesh_list_devices
|
|
1710
1815
|
private :mesh_start_rx!, :mesh_channel_psks, :mesh_text_app?, :mesh_rx_text, :mesh_self_node_id, :mesh_decorate_local_id, :mesh_public_psk?, :mesh_channel_securely_encrypted?
|
|
1711
1816
|
private :mesh_ai_whitelisted?, :mesh_ai_prompt, :mesh_broadcast?, :mesh_reply_target_label, :mesh_handle_rx, :mesh_maybe_dispatch_to_pwn_ai, :mesh_refresh_ui!, :mesh_reconnect!
|
data/lib/pwn/version.rb
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'meshtastic'
|
|
5
|
+
|
|
6
|
+
describe PWN::Plugins::REPL, 'mesh payload chunking' do
|
|
7
|
+
let(:max) { Meshtastic::Constants::DATA_PAYLOAD_LEN }
|
|
8
|
+
let(:env) { { transport: 'serial' } }
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
PWN.send(:remove_const, :MeshTransport) if PWN.const_defined?(:MeshTransport)
|
|
12
|
+
PWN.const_set(:MeshTransport, :serial)
|
|
13
|
+
allow(described_class).to receive(:mesh_handle_rx)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
after do
|
|
17
|
+
PWN.send(:remove_const, :MeshTransport) if PWN.const_defined?(:MeshTransport)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'sends text that fits DATA_PAYLOAD_LEN as a single unprefixed payload' do
|
|
21
|
+
expect(Meshtastic::Serial).to receive(:send_text).once.with(
|
|
22
|
+
hash_including(text: 'hi', to: '!ffffffff')
|
|
23
|
+
)
|
|
24
|
+
described_class.send(:mesh_send_text, env: env, obj: :serial_obj, text: 'hi', to: '!ffffffff')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'chunks text longer than DATA_PAYLOAD_LEN as (i/N) payloads until complete' do
|
|
28
|
+
body = 'A' * (max + 10)
|
|
29
|
+
sent = []
|
|
30
|
+
allow(Meshtastic::Serial).to receive(:send_text) { |h| sent << h[:text] }
|
|
31
|
+
|
|
32
|
+
described_class.send(:mesh_send_text, env: env, obj: :serial_obj, text: body, to: '!ffffffff')
|
|
33
|
+
|
|
34
|
+
expect(sent.size).to be > 1
|
|
35
|
+
n = sent.size
|
|
36
|
+
expect(sent[0]).to start_with("(1/#{n}) ")
|
|
37
|
+
expect(sent[-1]).to start_with("(#{n}/#{n}) ")
|
|
38
|
+
expect(sent).to all(satisfy { |piece| piece.length <= max && piece.bytesize <= max })
|
|
39
|
+
reconstructed = sent.map { |piece| piece.sub(%r{\A\(\d+/\d+\) }, '') }.join
|
|
40
|
+
expect(reconstructed).to eq(body)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'keeps each chunk small enough that the encoded ToRadio fits a 256-byte LoRa frame' do
|
|
44
|
+
body = 'A' * max
|
|
45
|
+
chunks = described_class.send(:mesh_text_chunks, text: body)
|
|
46
|
+
expect(chunks.size).to be > 1
|
|
47
|
+
mui = Meshtastic::MeshInterface.new
|
|
48
|
+
chunks.each do |piece|
|
|
49
|
+
expect(piece.bytesize).to be <= max
|
|
50
|
+
proto = mui.send_text(
|
|
51
|
+
from: '!00000b0b',
|
|
52
|
+
to: '!ffffffff',
|
|
53
|
+
channel: 0,
|
|
54
|
+
text: piece,
|
|
55
|
+
want_ack: true,
|
|
56
|
+
psks: nil,
|
|
57
|
+
via: :radio
|
|
58
|
+
)
|
|
59
|
+
body_bytes = proto.is_a?(String) ? proto.b.bytesize : proto.to_proto.bytesize
|
|
60
|
+
expect(body_bytes).to be <= 256
|
|
61
|
+
end
|
|
62
|
+
reconstructed = chunks.map { |piece| piece.sub(%r{\A\(\d+/\d+\) }, '') }.join
|
|
63
|
+
expect(reconstructed).to eq(body)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'sends chunked frames with want_ack so the radio reports when each chunk is on the air' do
|
|
67
|
+
body = 'A' * (max + 10)
|
|
68
|
+
sent = []
|
|
69
|
+
allow(Meshtastic::Serial).to receive(:send_text) { |h| sent << h[:want_ack] }
|
|
70
|
+
described_class.send(:mesh_send_text, env: env, obj: :serial_obj, text: body, to: '!ffffffff')
|
|
71
|
+
expect(sent.size).to be > 1
|
|
72
|
+
expect(sent).to all(eq(true))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'waits for a free device TX slot before sending the next chunk' do
|
|
76
|
+
body = 'A' * (max + 10)
|
|
77
|
+
obj = { proto_data: [], rx_mutex: Mutex.new }
|
|
78
|
+
allow(Meshtastic::Serial).to receive(:send_text)
|
|
79
|
+
allow(described_class).to receive(:mesh_wait_tx_slot)
|
|
80
|
+
described_class.send(:mesh_send_text, env: env, obj: obj, text: body, to: '!ffffffff')
|
|
81
|
+
n = described_class.send(:mesh_text_chunks, text: body).size
|
|
82
|
+
expect(described_class).to have_received(:mesh_wait_tx_slot).exactly(n - 1).times
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'does not wait for a TX slot after a single payload' do
|
|
86
|
+
allow(Meshtastic::Serial).to receive(:send_text)
|
|
87
|
+
allow(described_class).to receive(:mesh_wait_tx_slot)
|
|
88
|
+
described_class.send(:mesh_send_text, env: env, obj: { proto_data: [] }, text: 'hi', to: '!ffffffff')
|
|
89
|
+
expect(described_class).not_to have_received(:mesh_wait_tx_slot)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'does not treat QueueStatus free as the chunk being on the air' do
|
|
93
|
+
obj = { proto_data: [{ queueStatus: { free: 15, maxlen: 16 } }], rx_mutex: Mutex.new }
|
|
94
|
+
t = Time.now
|
|
95
|
+
described_class.send(:mesh_wait_tx_slot, obj: obj, since: 0, timeout: 0.25)
|
|
96
|
+
expect(Time.now - t).to be >= 0.2
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'treats a ROUTING_APP FromRadio as a TX slot without a fixed sleep' do
|
|
100
|
+
obj = { proto_data: [], rx_mutex: Mutex.new }
|
|
101
|
+
Thread.new do
|
|
102
|
+
sleep 0.05
|
|
103
|
+
obj[:rx_mutex].synchronize do
|
|
104
|
+
obj[:proto_data] << { packet: { decoded: { portnum: :ROUTING_APP, request_id: 1 } } }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
t = Time.now
|
|
108
|
+
described_class.send(:mesh_wait_tx_slot, obj: obj, since: 0, timeout: 1)
|
|
109
|
+
expect(Time.now - t).to be < 0.5
|
|
110
|
+
end
|
|
111
|
+
end
|
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.
|
|
4
|
+
version: 0.5.735
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 0day Inc.
|
|
@@ -6911,6 +6911,7 @@ files:
|
|
|
6911
6911
|
- spec/lib/pwn/plugins/repl/mesh_spec.rb
|
|
6912
6912
|
- spec/lib/pwn/plugins/repl/vault_spec.rb
|
|
6913
6913
|
- spec/lib/pwn/plugins/repl_mesh_channel_route_spec.rb
|
|
6914
|
+
- spec/lib/pwn/plugins/repl_mesh_chunk_spec.rb
|
|
6914
6915
|
- spec/lib/pwn/plugins/repl_mesh_console_spec.rb
|
|
6915
6916
|
- spec/lib/pwn/plugins/repl_mesh_dispatch_spec.rb
|
|
6916
6917
|
- spec/lib/pwn/plugins/repl_mesh_dm_spec.rb
|