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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35f85751c76226260916fb5b417180f8c10a1fe53eb8913295895aec83da8eaa
4
- data.tar.gz: 7808ea07ce71dd2576b168acf9cb1c429c0f9f4dc098570c13ffa81d416e6ffc
3
+ metadata.gz: d8f7823f226f8196c1c33bda916bb5ffde0d6ca22b0c47b3d7b5518ca85e9a8b
4
+ data.tar.gz: f069d75fa0a0e678aff23e9255cdd5ea13e4b3821a93fa6a1a0fd86e5648171c
5
5
  SHA512:
6
- metadata.gz: 933f35d3a0d28a1dfdb87eb826874e88b150738b333bfb2518a9366f61cd72b24c5fb7e64fe30e73a92086320d6d3df8bfb88b3d8047f9565eab9dd995780087
7
- data.tar.gz: 5384de39a95c61302cd37f6a6bcdfbcd54f1f9b047a7588c990afef5b89a8e572007dab3abab3e35547d8dc35c6a43d4f2ec72997122e70ea86ea0a3cb21fc7d
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
- case kind
554
- when :serial
555
- tx = { serial_obj: obj, to: dest, text: text, want_ack: true }
556
- tx[:channel] = radio unless radio.nil?
557
- Meshtastic::Serial.send_text(tx)
558
- when :bluetooth
559
- tx = { bluetooth_obj: obj, to: dest, text: text, want_ack: true }
560
- tx[:channel] = radio unless radio.nil?
561
- Meshtastic::Bluetooth.send_text(tx)
562
- when :tcp
563
- tx = { tcp_obj: obj, to: dest, text: text }
564
- tx[:channel] = radio unless radio.nil?
565
- Meshtastic::TCP.send_text(tx)
566
- else
567
- send_psks = psks
568
- send_psks = mesh_channel_psks(env: env) if send_psks.nil? || send_psks.empty?
569
- Meshtastic::MQTT.send_text(
570
- mqtt_obj: obj,
571
- from: from,
572
- to: dest,
573
- region: mesh_mqtt_region(region: opts[:region], env: env),
574
- topic: mesh_mqtt_topic(env: env, topic: opts[:topic]),
575
- channel: channel,
576
- text: text,
577
- psks: send_psks
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
- nil
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.734'
4
+ VERSION = '0.5.735'
5
5
  end
@@ -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.734
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