lanet 0.2.1 → 0.4.0

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.
data/lib/lanet/cli.rb CHANGED
@@ -178,11 +178,194 @@ module Lanet
178
178
  puts "Share your public key with others who need to verify your messages."
179
179
  end
180
180
 
181
+ desc "send-file", "Send a file to a specific target"
182
+ option :target, type: :string, required: true, desc: "Target IP address"
183
+ option :file, type: :string, required: true, desc: "File to send"
184
+ option :key, type: :string, desc: "Encryption key (optional)"
185
+ option :private_key_file, type: :string, desc: "Path to private key file for signing (optional)"
186
+ option :port, type: :numeric, default: 5001, desc: "Port number"
187
+ def send_file
188
+ unless File.exist?(options[:file]) && File.file?(options[:file])
189
+ puts "Error: File not found or is not a regular file: #{options[:file]}"
190
+ return
191
+ end
192
+
193
+ private_key = nil
194
+ if options[:private_key_file]
195
+ begin
196
+ private_key = File.read(options[:private_key_file])
197
+ puts "File will be digitally signed"
198
+ rescue StandardError => e
199
+ puts "Error reading private key file: #{e.message}"
200
+ return
201
+ end
202
+ end
203
+
204
+ file_transfer = Lanet::FileTransfer.new(options[:port])
205
+
206
+ puts "Sending file #{File.basename(options[:file])} to #{options[:target]}..."
207
+ puts "File size: #{File.size(options[:file])} bytes"
208
+
209
+ begin
210
+ file_transfer.send_file(
211
+ options[:target],
212
+ options[:file],
213
+ options[:key],
214
+ private_key
215
+ ) do |progress, bytes, total|
216
+ # Update progress bar
217
+ print "\rProgress: #{progress}% (#{bytes}/#{total} bytes)"
218
+ end
219
+
220
+ puts "\nFile sent successfully!"
221
+ rescue Lanet::FileTransfer::Error => e
222
+ puts "\nError: #{e.message}"
223
+ end
224
+ end
225
+
226
+ desc "receive-file", "Listen for incoming files"
227
+ option :output, type: :string, default: "./received", desc: "Output directory for received files"
228
+ option :encryption_key, type: :string, desc: "Encryption key (optional)"
229
+ option :public_key_file, type: :string, desc: "Path to public key file for verification (optional)"
230
+ option :port, type: :numeric, default: 5001, desc: "Port number"
231
+ def receive_file
232
+ public_key = nil
233
+ if options[:public_key_file]
234
+ begin
235
+ public_key = File.read(options[:public_key_file])
236
+ puts "Digital signature verification enabled"
237
+ rescue StandardError => e
238
+ puts "Error reading public key file: #{e.message}"
239
+ return
240
+ end
241
+ end
242
+
243
+ output_dir = File.expand_path(options[:output])
244
+ FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
245
+
246
+ puts "Listening for incoming files on port #{options[:port]}..."
247
+ puts "Files will be saved to #{output_dir}"
248
+ puts "Press Ctrl+C to stop"
249
+
250
+ file_transfer = Lanet::FileTransfer.new(options[:port])
251
+
252
+ begin
253
+ file_transfer.receive_file(
254
+ output_dir,
255
+ options[:encryption_key],
256
+ public_key
257
+ ) do |event, data|
258
+ case event
259
+ when :start
260
+ puts "\nReceiving file: #{data[:file_name]} from #{data[:sender_ip]}"
261
+ puts "Size: #{data[:file_size]} bytes"
262
+ puts "Transfer ID: #{data[:transfer_id]}"
263
+ when :progress
264
+ print "\rProgress: #{data[:progress]}% (#{data[:bytes_received]}/#{data[:total_bytes]} bytes)"
265
+ when :complete
266
+ puts "\nFile received and saved to: #{data[:file_path]}"
267
+ when :error
268
+ puts "\nError during file transfer: #{data[:error]}"
269
+ end
270
+ end
271
+ rescue Interrupt
272
+ puts "\nFile receiver stopped."
273
+ end
274
+ end
275
+
181
276
  desc "version", "Display the version of Lanet"
182
277
  def version
183
278
  puts "Lanet version #{Lanet::VERSION}"
184
279
  end
185
280
 
281
+ desc "mesh start", "Start a mesh network node"
282
+ option :port, type: :numeric, default: 5050, desc: "Port for mesh communication"
283
+ option :max_hops, type: :numeric, default: 10, desc: "Maximum number of hops for message routing"
284
+ def mesh_start
285
+ mesh = Lanet::Mesh.new(options[:port], options[:max_hops])
286
+
287
+ puts "Starting mesh network node with ID: #{mesh.node_id}"
288
+ puts "Listening on port: #{options[:port]}"
289
+ puts "Press Ctrl+C to stop"
290
+
291
+ mesh.start
292
+
293
+ # Keep the process running
294
+ begin
295
+ loop do
296
+ sleep 1
297
+ end
298
+ rescue Interrupt
299
+ puts "\nStopping mesh network node..."
300
+ mesh.stop
301
+ end
302
+ end
303
+
304
+ desc "mesh send", "Send a message through the mesh network"
305
+ option :target, type: :string, required: true, desc: "Target node ID"
306
+ option :message, type: :string, required: true, desc: "Message to send"
307
+ option :key, type: :string, desc: "Encryption key (optional)"
308
+ option :private_key_file, type: :string, desc: "Path to private key file for signing (optional)"
309
+ option :port, type: :numeric, default: 5050, desc: "Port for mesh communication"
310
+ def mesh_send
311
+ mesh = Lanet::Mesh.new(options[:port])
312
+
313
+ private_key = nil
314
+ if options[:private_key_file]
315
+ begin
316
+ private_key = File.read(options[:private_key_file])
317
+ puts "Message will be digitally signed"
318
+ rescue StandardError => e
319
+ puts "Error reading private key file: #{e.message}"
320
+ return
321
+ end
322
+ end
323
+
324
+ # Initialize the mesh network
325
+ mesh.start
326
+
327
+ begin
328
+ message_id = mesh.send_message(
329
+ options[:target],
330
+ options[:message],
331
+ options[:key],
332
+ private_key
333
+ )
334
+
335
+ puts "Message sent through mesh network"
336
+ puts "Message ID: #{message_id}"
337
+ rescue Lanet::Mesh::Error => e
338
+ puts "Error sending mesh message: #{e.message}"
339
+ ensure
340
+ mesh.stop
341
+ end
342
+ end
343
+
344
+ desc "mesh info", "Display information about the mesh network"
345
+ option :port, type: :numeric, default: 5050, desc: "Port for mesh communication"
346
+ def mesh_info
347
+ mesh = Lanet::Mesh.new(options[:port])
348
+
349
+ # Load state if available
350
+ mesh.start
351
+
352
+ puts "Mesh Node ID: #{mesh.node_id}"
353
+ puts "\nConnected nodes:"
354
+
355
+ if mesh.connections.empty?
356
+ puts " No direct connections"
357
+ else
358
+ mesh.connections.each do |node_id, info|
359
+ last_seen = Time.now.to_i - info[:last_seen]
360
+ puts " #{node_id} (#{info[:ip]}, last seen #{last_seen}s ago)"
361
+ end
362
+ end
363
+
364
+ puts "\nMessage cache: #{mesh.message_cache.size} messages"
365
+
366
+ mesh.stop
367
+ end
368
+
186
369
  private
187
370
 
188
371
  def display_ping_details(host, result)
@@ -259,19 +442,5 @@ module Lanet
259
442
  puts "\nOutput:"
260
443
  puts result[:output]
261
444
  end
262
-
263
- # Override method_missing to provide helpful error messages for common mistakes
264
- def method_missing(method, *args)
265
- if method.to_s == "ping" && args.any?
266
- invoke "ping", [], { host: args.first, timeout: options[:timeout], count: options[:count],
267
- quiet: options[:quiet], continuous: options[:continuous] }
268
- else
269
- super
270
- end
271
- end
272
-
273
- def respond_to_missing?(method, include_private = false)
274
- method.to_s == "ping" || super
275
- end
276
445
  end
277
446
  end
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "tempfile"
6
+ require "zlib"
7
+ require "securerandom"
8
+ require "base64"
9
+ require "json"
10
+ require "socket"
11
+ require "timeout"
12
+
13
+ module Lanet
14
+ class FileTransfer
15
+ # Constants
16
+ # Use smaller chunks in tests to avoid "Message too long" errors
17
+ CHUNK_SIZE = if ENV["LANET_TEST_CHUNK_SIZE"]
18
+ ENV["LANET_TEST_CHUNK_SIZE"].to_i
19
+ elsif ENV["RACK_ENV"] == "test"
20
+ 8192 # 8KB in test environment
21
+ else
22
+ 65_536 # 64KB in production
23
+ end
24
+
25
+ MAX_RETRIES = 3
26
+ TIMEOUT = ENV["RACK_ENV"] == "test" ? 2 : 10 # Seconds
27
+
28
+ # Message types
29
+ FILE_HEADER = "FH" # File metadata
30
+ FILE_CHUNK = "FC" # File data chunk
31
+ FILE_END = "FE" # End of transfer
32
+ FILE_ACK = "FA" # Acknowledgment
33
+ FILE_ERROR = "FR" # Error message
34
+
35
+ # Custom error class
36
+ class Error < StandardError; end
37
+
38
+ # Attributes for tracking progress
39
+ attr_reader :progress, :file_size, :transferred_bytes
40
+
41
+ ### Initialization
42
+ def initialize(port = nil)
43
+ @port = port || 5001 # Default port for file transfers
44
+ @progress = 0.0
45
+ @file_size = 0
46
+ @transferred_bytes = 0
47
+ @sender = Lanet::Sender.new(@port) # Assumes Lanet::Sender is defined elsewhere
48
+ @cancellation_requested = false
49
+ end
50
+
51
+ ### Send File Method
52
+ def send_file(target_ip, file_path, encryption_key = nil, private_key = nil, progress_callback = nil)
53
+ # Validate file
54
+ unless File.exist?(file_path) && File.file?(file_path)
55
+ raise Error, "File not found or is not a regular file: #{file_path}"
56
+ end
57
+
58
+ # Initialize transfer state
59
+ @file_size = File.size(file_path)
60
+ @transferred_bytes = 0
61
+ @progress = 0.0
62
+ @cancellation_requested = false
63
+ transfer_id = SecureRandom.uuid
64
+ chunk_index = 0
65
+
66
+ receiver = nil
67
+
68
+ begin
69
+ # Send file header
70
+ file_name = File.basename(file_path)
71
+ file_checksum = calculate_file_checksum(file_path)
72
+ header_data = {
73
+ id: transfer_id,
74
+ name: file_name,
75
+ size: @file_size,
76
+ checksum: file_checksum,
77
+ timestamp: Time.now.to_i
78
+ }.to_json
79
+ header_message = Lanet::Encryptor.prepare_message("#{FILE_HEADER}#{header_data}", encryption_key, private_key)
80
+ @sender.send_to(target_ip, header_message)
81
+
82
+ # Wait for initial ACK
83
+ receiver = UDPSocket.new
84
+ receiver.bind("0.0.0.0", @port)
85
+ wait_for_ack(receiver, target_ip, transfer_id, encryption_key, "initial")
86
+
87
+ # Send file chunks
88
+ File.open(file_path, "rb") do |file|
89
+ until file.eof? || @cancellation_requested
90
+ chunk = file.read(CHUNK_SIZE)
91
+ chunk_data = {
92
+ id: transfer_id,
93
+ index: chunk_index,
94
+ data: Base64.strict_encode64(chunk)
95
+ }.to_json
96
+ chunk_message = Lanet::Encryptor.prepare_message("#{FILE_CHUNK}#{chunk_data}", encryption_key, private_key)
97
+ @sender.send_to(target_ip, chunk_message)
98
+
99
+ chunk_index += 1
100
+ @transferred_bytes += chunk.bytesize
101
+ @progress = (@transferred_bytes.to_f / @file_size * 100).round(2)
102
+ progress_callback&.call(@progress, @transferred_bytes, @file_size)
103
+
104
+ sleep(0.01) # Prevent overwhelming the receiver
105
+ end
106
+ end
107
+
108
+ # Send end marker and wait for final ACK
109
+ unless @cancellation_requested
110
+ end_data = { id: transfer_id, total_chunks: chunk_index }.to_json
111
+ end_message = Lanet::Encryptor.prepare_message("#{FILE_END}#{end_data}", encryption_key, private_key)
112
+ @sender.send_to(target_ip, end_message)
113
+ wait_for_ack(receiver, target_ip, transfer_id, encryption_key, "final")
114
+ true # Transfer successful
115
+ end
116
+ rescue StandardError => e
117
+ send_error(target_ip, transfer_id, e.message, encryption_key, private_key)
118
+ raise Error, "File transfer failed: #{e.message}"
119
+ ensure
120
+ receiver&.close
121
+ end
122
+ end
123
+
124
+ ### Receive File Method
125
+ def receive_file(output_dir, encryption_key = nil, public_key = nil, progress_callback = nil, &block)
126
+ # Use the block parameter if provided and progress_callback is nil
127
+ progress_callback = block if block_given? && progress_callback.nil?
128
+
129
+ FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
130
+ receiver = UDPSocket.new
131
+ receiver.bind("0.0.0.0", @port)
132
+ active_transfers = {}
133
+
134
+ begin
135
+ loop do
136
+ data, addr = receiver.recvfrom(65_536) # Large buffer for chunks
137
+
138
+ # Skip if we received nil data or address
139
+ next if addr.nil? || data.nil?
140
+
141
+ sender_ip = addr[3]
142
+ result = Lanet::Encryptor.process_message(data, encryption_key, public_key)
143
+ next unless result[:content]&.length&.> 2
144
+
145
+ message_type = result[:content][0..1]
146
+ message_data = result[:content][2..]
147
+
148
+ case message_type
149
+ when FILE_HEADER
150
+ handle_file_header(sender_ip, message_data, active_transfers, encryption_key, progress_callback)
151
+ when FILE_CHUNK
152
+ handle_file_chunk(sender_ip, message_data, active_transfers, progress_callback, encryption_key)
153
+ when FILE_END
154
+ handle_file_end(sender_ip, message_data, active_transfers, output_dir, encryption_key, progress_callback)
155
+ when FILE_ERROR
156
+ handle_file_error(sender_ip, message_data, active_transfers, progress_callback)
157
+ end
158
+ end
159
+ rescue Interrupt
160
+ puts "\nFile receiver stopped."
161
+ ensure
162
+ cleanup_transfers(active_transfers)
163
+ receiver.close
164
+ end
165
+ end
166
+
167
+ ### Cancel Transfer
168
+ def cancel_transfer
169
+ @cancellation_requested = true
170
+ end
171
+
172
+ private
173
+
174
+ ### Helper Methods
175
+
176
+ def calculate_file_checksum(file_path)
177
+ Digest::SHA256.file(file_path).hexdigest
178
+ end
179
+
180
+ def send_error(target_ip, transfer_id, message, encryption_key, private_key = nil)
181
+ error_data = { id: transfer_id, message: message, timestamp: Time.now.to_i }.to_json
182
+ error_message = Lanet::Encryptor.prepare_message("#{FILE_ERROR}#{error_data}", encryption_key, private_key)
183
+ @sender.send_to(target_ip, error_message)
184
+ end
185
+
186
+ def wait_for_ack(receiver, target_ip, transfer_id, encryption_key, context)
187
+ Timeout.timeout(TIMEOUT) do
188
+ data, addr = receiver.recvfrom(1024)
189
+ sender_ip = addr[3]
190
+ if sender_ip == target_ip
191
+ result = Lanet::Encryptor.process_message(data, encryption_key)
192
+ return if result[:content]&.start_with?(FILE_ACK) && result[:content][2..] == transfer_id
193
+
194
+ # Valid ACK received
195
+
196
+ raise Error, "Invalid #{context} ACK received: #{result[:content]}"
197
+
198
+ end
199
+ end
200
+ rescue Timeout::Error
201
+ raise Error, "Timeout waiting for #{context} transfer acknowledgment"
202
+ end
203
+
204
+ def handle_file_header(sender_ip, message_data, active_transfers, encryption_key, callback)
205
+ header = JSON.parse(message_data)
206
+ transfer_id = header["id"]
207
+ active_transfers[transfer_id] = {
208
+ sender_ip: sender_ip,
209
+ file_name: header["name"],
210
+ file_size: header["size"],
211
+ expected_checksum: header["checksum"],
212
+ temp_file: Tempfile.new([File.basename(header["name"], ".*"), File.extname(header["name"])]),
213
+ chunks_received: 0,
214
+ timestamp: Time.now
215
+ }
216
+ ack_message = Lanet::Encryptor.prepare_message("#{FILE_ACK}#{transfer_id}", encryption_key)
217
+ @sender.send_to(sender_ip, ack_message)
218
+ callback&.call(:start, {
219
+ transfer_id: transfer_id,
220
+ sender_ip: sender_ip,
221
+ file_name: header["name"],
222
+ file_size: header["size"]
223
+ })
224
+ rescue JSON::ParserError => e
225
+ send_error(sender_ip, "unknown", "Invalid header format: #{e.message}", encryption_key)
226
+ end
227
+
228
+ def handle_file_chunk(sender_ip, message_data, active_transfers, callback, encryption_key)
229
+ chunk = JSON.parse(message_data)
230
+ transfer_id = chunk["id"]
231
+ transfer = active_transfers[transfer_id]
232
+ if transfer && transfer[:sender_ip] == sender_ip
233
+ chunk_data = Base64.strict_decode64(chunk["data"])
234
+ transfer[:temp_file].write(chunk_data)
235
+ transfer[:chunks_received] += 1
236
+ bytes_received = transfer[:temp_file].size
237
+ progress = (bytes_received.to_f / transfer[:file_size] * 100).round(2)
238
+ callback&.call(:progress, {
239
+ transfer_id: transfer_id,
240
+ sender_ip: sender_ip,
241
+ file_name: transfer[:file_name],
242
+ progress: progress,
243
+ bytes_received: bytes_received,
244
+ total_bytes: transfer[:file_size]
245
+ })
246
+ end
247
+ rescue JSON::ParserError => e
248
+ send_error(sender_ip, "unknown", "Invalid chunk format: #{e.message}", encryption_key)
249
+ end
250
+
251
+ def handle_file_end(sender_ip, message_data, active_transfers, output_dir, encryption_key, callback)
252
+ end_data = JSON.parse(message_data)
253
+ transfer_id = end_data["id"]
254
+ transfer = active_transfers[transfer_id]
255
+ if transfer && transfer[:sender_ip] == sender_ip
256
+ transfer[:temp_file].close
257
+ calculated_checksum = calculate_file_checksum(transfer[:temp_file].path)
258
+ if calculated_checksum == transfer[:expected_checksum]
259
+ final_path = File.join(output_dir, transfer[:file_name])
260
+ FileUtils.mv(transfer[:temp_file].path, final_path)
261
+ ack_message = Lanet::Encryptor.prepare_message("#{FILE_ACK}#{transfer_id}", encryption_key)
262
+ @sender.send_to(sender_ip, ack_message)
263
+ callback&.call(:complete, {
264
+ transfer_id: transfer_id,
265
+ sender_ip: sender_ip,
266
+ file_name: transfer[:file_name],
267
+ file_path: final_path
268
+ })
269
+ else
270
+ error_msg = "Checksum verification failed"
271
+ send_error(sender_ip, transfer_id, error_msg, encryption_key)
272
+ callback&.call(:error, {
273
+ transfer_id: transfer_id,
274
+ sender_ip: sender_ip,
275
+ error: error_msg
276
+ })
277
+ end
278
+ transfer[:temp_file].unlink
279
+ active_transfers.delete(transfer_id)
280
+ end
281
+ rescue JSON::ParserError => e
282
+ send_error(sender_ip, "unknown", "Invalid end marker format: #{e.message}", encryption_key)
283
+ end
284
+
285
+ def handle_file_error(sender_ip, message_data, active_transfers, callback)
286
+ error_data = JSON.parse(message_data)
287
+ transfer_id = error_data["id"]
288
+ if callback && active_transfers[transfer_id]
289
+ callback.call(:error, {
290
+ transfer_id: transfer_id,
291
+ sender_ip: sender_ip,
292
+ error: error_data["message"]
293
+ })
294
+ if active_transfers[transfer_id]
295
+ active_transfers[transfer_id][:temp_file].close
296
+ active_transfers[transfer_id][:temp_file].unlink
297
+ active_transfers.delete(transfer_id)
298
+ end
299
+ end
300
+ rescue JSON::ParserError
301
+ # Ignore malformed error messages
302
+ end
303
+
304
+ def cleanup_transfers(active_transfers)
305
+ active_transfers.each_value do |transfer|
306
+ transfer[:temp_file].close
307
+ begin
308
+ transfer[:temp_file].unlink
309
+ rescue StandardError
310
+ nil
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end