lanet 0.2.0 → 0.3.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.
@@ -0,0 +1,308 @@
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)
126
+ FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
127
+ receiver = UDPSocket.new
128
+ receiver.bind("0.0.0.0", @port)
129
+ active_transfers = {}
130
+
131
+ begin
132
+ loop do
133
+ data, addr = receiver.recvfrom(65_536) # Large buffer for chunks
134
+ sender_ip = addr[3]
135
+ result = Lanet::Encryptor.process_message(data, encryption_key, public_key)
136
+ next unless result[:content]&.length&.> 2
137
+
138
+ message_type = result[:content][0..1]
139
+ message_data = result[:content][2..]
140
+
141
+ case message_type
142
+ when FILE_HEADER
143
+ handle_file_header(sender_ip, message_data, active_transfers, encryption_key, progress_callback)
144
+ when FILE_CHUNK
145
+ handle_file_chunk(sender_ip, message_data, active_transfers, progress_callback, encryption_key)
146
+ when FILE_END
147
+ handle_file_end(sender_ip, message_data, active_transfers, output_dir, encryption_key, progress_callback)
148
+ when FILE_ERROR
149
+ handle_file_error(sender_ip, message_data, active_transfers, progress_callback)
150
+ end
151
+ end
152
+ rescue Interrupt
153
+ puts "\nFile receiver stopped."
154
+ ensure
155
+ cleanup_transfers(active_transfers)
156
+ receiver.close
157
+ end
158
+ end
159
+
160
+ ### Cancel Transfer
161
+ def cancel_transfer
162
+ @cancellation_requested = true
163
+ end
164
+
165
+ private
166
+
167
+ ### Helper Methods
168
+
169
+ def calculate_file_checksum(file_path)
170
+ Digest::SHA256.file(file_path).hexdigest
171
+ end
172
+
173
+ def send_error(target_ip, transfer_id, message, encryption_key, private_key = nil)
174
+ error_data = { id: transfer_id, message: message, timestamp: Time.now.to_i }.to_json
175
+ error_message = Lanet::Encryptor.prepare_message("#{FILE_ERROR}#{error_data}", encryption_key, private_key)
176
+ @sender.send_to(target_ip, error_message)
177
+ end
178
+
179
+ def wait_for_ack(receiver, target_ip, transfer_id, encryption_key, context)
180
+ Timeout.timeout(TIMEOUT) do
181
+ data, addr = receiver.recvfrom(1024)
182
+ sender_ip = addr[3]
183
+ if sender_ip == target_ip
184
+ result = Lanet::Encryptor.process_message(data, encryption_key)
185
+ return if result[:content]&.start_with?(FILE_ACK) && result[:content][2..] == transfer_id
186
+
187
+ # Valid ACK received
188
+
189
+ raise Error, "Invalid #{context} ACK received: #{result[:content]}"
190
+
191
+ end
192
+ end
193
+ rescue Timeout::Error
194
+ raise Error, "Timeout waiting for #{context} transfer acknowledgment"
195
+ end
196
+
197
+ def handle_file_header(sender_ip, message_data, active_transfers, encryption_key, callback)
198
+ header = JSON.parse(message_data)
199
+ transfer_id = header["id"]
200
+ active_transfers[transfer_id] = {
201
+ sender_ip: sender_ip,
202
+ file_name: header["name"],
203
+ file_size: header["size"],
204
+ expected_checksum: header["checksum"],
205
+ temp_file: Tempfile.new([File.basename(header["name"], ".*"), File.extname(header["name"])]),
206
+ chunks_received: 0,
207
+ timestamp: Time.now
208
+ }
209
+ ack_message = Lanet::Encryptor.prepare_message("#{FILE_ACK}#{transfer_id}", encryption_key)
210
+ @sender.send_to(sender_ip, ack_message)
211
+ callback&.call(:start, {
212
+ transfer_id: transfer_id,
213
+ sender_ip: sender_ip,
214
+ file_name: header["name"],
215
+ file_size: header["size"]
216
+ })
217
+ rescue JSON::ParserError => e
218
+ send_error(sender_ip, "unknown", "Invalid header format: #{e.message}", encryption_key)
219
+ end
220
+
221
+ def handle_file_chunk(sender_ip, message_data, active_transfers, callback, encryption_key)
222
+ chunk = JSON.parse(message_data)
223
+ transfer_id = chunk["id"]
224
+ transfer = active_transfers[transfer_id]
225
+ if transfer && transfer[:sender_ip] == sender_ip
226
+ chunk_data = Base64.strict_decode64(chunk["data"])
227
+ transfer[:temp_file].write(chunk_data)
228
+ transfer[:chunks_received] += 1
229
+ bytes_received = transfer[:temp_file].size
230
+ progress = (bytes_received.to_f / transfer[:file_size] * 100).round(2)
231
+ callback&.call(:progress, {
232
+ transfer_id: transfer_id,
233
+ sender_ip: sender_ip,
234
+ file_name: transfer[:file_name],
235
+ progress: progress,
236
+ bytes_received: bytes_received,
237
+ total_bytes: transfer[:file_size]
238
+ })
239
+ end
240
+ rescue JSON::ParserError => e
241
+ send_error(sender_ip, "unknown", "Invalid chunk format: #{e.message}", encryption_key)
242
+ end
243
+
244
+ def handle_file_end(sender_ip, message_data, active_transfers, output_dir, encryption_key, callback)
245
+ end_data = JSON.parse(message_data)
246
+ transfer_id = end_data["id"]
247
+ transfer = active_transfers[transfer_id]
248
+ if transfer && transfer[:sender_ip] == sender_ip
249
+ transfer[:temp_file].close
250
+ calculated_checksum = calculate_file_checksum(transfer[:temp_file].path)
251
+ if calculated_checksum == transfer[:expected_checksum]
252
+ final_path = File.join(output_dir, transfer[:file_name])
253
+ FileUtils.mv(transfer[:temp_file].path, final_path)
254
+ ack_message = Lanet::Encryptor.prepare_message("#{FILE_ACK}#{transfer_id}", encryption_key)
255
+ @sender.send_to(sender_ip, ack_message)
256
+ callback&.call(:complete, {
257
+ transfer_id: transfer_id,
258
+ sender_ip: sender_ip,
259
+ file_name: transfer[:file_name],
260
+ file_path: final_path
261
+ })
262
+ else
263
+ error_msg = "Checksum verification failed"
264
+ send_error(sender_ip, transfer_id, error_msg, encryption_key)
265
+ callback&.call(:error, {
266
+ transfer_id: transfer_id,
267
+ sender_ip: sender_ip,
268
+ error: error_msg
269
+ })
270
+ end
271
+ transfer[:temp_file].unlink
272
+ active_transfers.delete(transfer_id)
273
+ end
274
+ rescue JSON::ParserError => e
275
+ send_error(sender_ip, "unknown", "Invalid end marker format: #{e.message}", encryption_key)
276
+ end
277
+
278
+ def handle_file_error(sender_ip, message_data, active_transfers, callback)
279
+ error_data = JSON.parse(message_data)
280
+ transfer_id = error_data["id"]
281
+ if callback && active_transfers[transfer_id]
282
+ callback.call(:error, {
283
+ transfer_id: transfer_id,
284
+ sender_ip: sender_ip,
285
+ error: error_data["message"]
286
+ })
287
+ if active_transfers[transfer_id]
288
+ active_transfers[transfer_id][:temp_file].close
289
+ active_transfers[transfer_id][:temp_file].unlink
290
+ active_transfers.delete(transfer_id)
291
+ end
292
+ end
293
+ rescue JSON::ParserError
294
+ # Ignore malformed error messages
295
+ end
296
+
297
+ def cleanup_transfers(active_transfers)
298
+ active_transfers.each_value do |transfer|
299
+ transfer[:temp_file].close
300
+ begin
301
+ transfer[:temp_file].unlink
302
+ rescue StandardError
303
+ nil
304
+ end
305
+ end
306
+ end
307
+ end
308
+ end
data/lib/lanet/scanner.rb CHANGED
@@ -25,55 +25,54 @@ module Lanet
25
25
  8443 => "HTTPS-ALT"
26
26
  }.freeze
27
27
 
28
- # Ports to check during scan
29
28
  QUICK_CHECK_PORTS = [80, 443, 22, 445, 139, 8080].freeze
30
29
 
31
30
  def initialize
32
31
  @hosts = []
33
32
  @mutex = Mutex.new
33
+ @arp_cache = {}
34
34
  end
35
35
 
36
- # Scan network and return active hosts
37
36
  def scan(cidr, timeout = 1, max_threads = 32, verbose = false)
38
37
  @verbose = verbose
39
38
  @timeout = timeout
40
-
41
- # Clear previous scan results
42
39
  @hosts = []
43
-
44
- # Get the range of IP addresses to scan
45
40
  range = IPAddr.new(cidr).to_range
46
-
47
- # Create a queue of IPs to scan
48
41
  queue = Queue.new
49
42
  range.each { |ip| queue << ip.to_s }
50
-
51
43
  total_ips = queue.size
52
44
  completed = 0
53
45
 
54
- # Pre-populate ARP table to improve MAC address resolution
55
- update_arp_table(cidr, max_threads)
46
+ # Initial ARP cache population
47
+ @arp_cache = parse_arp_table
56
48
 
57
- # Create worker threads to process the queue
58
49
  threads = Array.new([max_threads, total_ips].min) do
59
50
  Thread.new do
60
- while (ip = begin
61
- queue.pop(true)
62
- rescue ThreadError
63
- nil
64
- end)
51
+ loop do
52
+ begin
53
+ ip = queue.pop(true)
54
+ rescue ThreadError
55
+ break
56
+ end
65
57
  scan_host(ip)
66
58
  @mutex.synchronize do
67
59
  completed += 1
68
60
  if total_ips < 100 || (completed % 10).zero? || completed == total_ips
69
- print_progress(completed,
70
- total_ips)
61
+ print_progress(completed, total_ips)
71
62
  end
72
63
  end
73
64
  end
74
65
  end
75
66
  end
76
67
 
68
+ # Periodically update ARP cache
69
+ arp_updater = Thread.new do
70
+ while threads.any?(&:alive?)
71
+ sleep 5
72
+ @mutex.synchronize { @arp_cache = parse_arp_table }
73
+ end
74
+ end
75
+
77
76
  begin
78
77
  threads.each(&:join)
79
78
  print_progress(total_ips, total_ips)
@@ -82,6 +81,8 @@ module Lanet
82
81
  rescue Interrupt
83
82
  puts "\nScan interrupted. Returning partial results..."
84
83
  @verbose ? @hosts : @hosts.map { |h| h[:ip] }
84
+ ensure
85
+ arp_updater.kill if arp_updater.alive?
85
86
  end
86
87
  end
87
88
 
@@ -92,47 +93,62 @@ module Lanet
92
93
  print "\rScanning network: #{percent}% complete (#{completed}/#{total})"
93
94
  end
94
95
 
95
- def update_arp_table(cidr, max_threads = 10)
96
- # Use fast ping method to update ARP table
97
- range = IPAddr.new(cidr).to_range
98
- queue = Queue.new
99
- range.each { |ip| queue << ip.to_s }
96
+ def parse_arp_table
97
+ cmd = RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/ ? "arp -a" : "arp -a"
98
+ output = `#{cmd}`
99
+ arp_cache = {}
100
100
 
101
- total = queue.size
102
- processed = 0
101
+ case RbConfig::CONFIG["host_os"]
102
+ when /darwin/
103
+ output.each_line do |line|
104
+ next unless line =~ /\((\d+\.\d+\.\d+\.\d+)\) at ([0-9a-f:]+) on/
103
105
 
104
- threads = Array.new([max_threads, total].min) do
105
- Thread.new do
106
- while (ip = begin
107
- queue.pop(true)
108
- rescue ThreadError
109
- nil
110
- end)
111
- # Use system ping to update ARP table
112
- system("ping -c 1 -W 1 #{ip} > /dev/null 2>&1 &")
113
- sleep 0.01 # Small delay to prevent overwhelming the system
114
- processed += 1
115
- end
106
+ ip = ::Regexp.last_match(1)
107
+ mac = ::Regexp.last_match(2).downcase
108
+ arp_cache[ip] = mac unless mac == "(incomplete)"
116
109
  end
117
- end
110
+ when /linux/
111
+ output.each_line do |line|
112
+ next unless line =~ /^(\d+\.\d+\.\d+\.\d+)\s+\w+\s+([0-9a-f:]+)\s+/
113
+
114
+ ip = ::Regexp.last_match(1)
115
+ mac = ::Regexp.last_match(2).downcase
116
+ arp_cache[ip] = mac unless mac == "00:00:00:00:00:00"
117
+ end
118
+ when /mswin|mingw|cygwin/
119
+ output.each_line do |line|
120
+ next unless line =~ /^\s*(\d+\.\d+\.\d+\.\d+)\s+([0-9a-f-]+)\s+/
118
121
 
119
- # Wait for ping operations to complete
120
- threads.each(&:join)
121
- sleep 1 # Give the system time to update ARP table
122
+ ip = ::Regexp.last_match(1)
123
+ mac = ::Regexp.last_match(2).gsub("-", ":").downcase
124
+ arp_cache[ip] = mac
125
+ end
126
+ end
127
+ arp_cache
122
128
  end
123
129
 
124
130
  def scan_host(ip)
125
- # Skip special addresses to save time
126
- return if ip.end_with?(".0") && !ip.end_with?(".0.0") # Skip network addresses except 0.0.0.0
131
+ # Handle broadcast addresses immediately
132
+ if ip.end_with?(".255") || ip == "255.255.255.255"
133
+ host_info = { ip: ip, mac: "ff:ff:ff:ff:ff:ff", response_time: 0, detection_method: "Broadcast" }
134
+ if @verbose
135
+ host_info[:hostname] = "Broadcast"
136
+ host_info[:ports] = {}
137
+ end
138
+ @mutex.synchronize { @hosts << host_info }
139
+ return
140
+ end
141
+
142
+ # Skip network addresses
143
+ return if ip.end_with?(".0") && !ip.end_with?(".0.0")
127
144
 
128
- # Use multiple methods to detect if a host is alive
129
145
  is_active = false
130
146
  detection_method = nil
131
147
  response_time = nil
132
148
  start_time = Time.now
133
149
  open_ports = []
134
150
 
135
- # Method 1: Try TCP port scan (most reliable)
151
+ # TCP port scan
136
152
  tcp_result = tcp_port_scan(ip, QUICK_CHECK_PORTS)
137
153
  if tcp_result[:active]
138
154
  is_active = true
@@ -140,116 +156,86 @@ module Lanet
140
156
  open_ports = tcp_result[:open_ports]
141
157
  end
142
158
 
143
- # Method 2: Try ICMP ping if TCP didn't work
144
- unless is_active
145
- ping_result = ping_check(ip)
146
- if ping_result
147
- is_active = true
148
- detection_method = "ICMP"
149
- end
159
+ # ICMP ping
160
+ if !is_active && ping_check(ip)
161
+ is_active = true
162
+ detection_method = "ICMP"
150
163
  end
151
164
 
152
- # Method 3: If host is a common network device (e.g., router), check with UDP
153
- if !is_active && (ip.end_with?(".1") || ip.end_with?(".254") || ip.end_with?(".255"))
154
- udp_result = udp_check(ip)
155
- if udp_result
156
- is_active = true
157
- detection_method = "UDP"
158
- end
165
+ # UDP check for common network devices
166
+ if !is_active && (ip.end_with?(".1") || ip.end_with?(".254")) && udp_check(ip)
167
+ is_active = true
168
+ detection_method = "UDP"
159
169
  end
160
170
 
161
- # Method 4: ARP Check - if we have a MAC, the host is likely active
171
+ # ARP check
162
172
  unless is_active
163
- mac_address = get_mac_address(ip)
164
- if mac_address && mac_address != "(incomplete)"
173
+ mac = get_mac_address(ip)
174
+ if mac && mac != "(incomplete)"
165
175
  is_active = true
166
176
  detection_method = "ARP"
167
177
  end
168
178
  end
169
179
 
170
- # For broadcast addresses, always consider them active
171
- if ip.end_with?(".255") || ip == "255.255.255.255"
172
- is_active = true
173
- detection_method = "Broadcast"
174
- end
175
-
176
- # Calculate response time
177
180
  response_time = ((Time.now - start_time) * 1000).round(2) if is_active
178
-
179
181
  return unless is_active
180
182
 
181
- # For active hosts, collect more information if in verbose mode
182
- host_info = {
183
- ip: ip,
184
- mac: get_mac_address(ip),
185
- response_time: response_time,
186
- detection_method: detection_method
187
- }
183
+ host_info = { ip: ip, mac: get_mac_address(ip), response_time: response_time, detection_method: detection_method }
188
184
 
189
185
  if @verbose
190
- # For verbose mode, try to resolve hostname
191
- begin
192
- Timeout.timeout(1) do
193
- host_info[:hostname] = Resolv.getname(ip)
194
- end
195
- rescue Resolv::ResolvError, Timeout::Error
196
- host_info[:hostname] = "Unknown"
186
+ host_info[:hostname] = begin
187
+ Timeout.timeout(1) { Resolv.getname(ip) }
188
+ rescue StandardError
189
+ "Unknown"
197
190
  end
198
-
199
- # For verbose mode, scan more ports if TCP detection method was successful
200
191
  if detection_method == "TCP"
201
192
  extra_ports = tcp_port_scan(ip, COMMON_PORTS.keys - QUICK_CHECK_PORTS)[:open_ports]
202
193
  open_ports += extra_ports
203
194
  end
204
-
205
195
  host_info[:ports] = open_ports.map { |port| [port, COMMON_PORTS[port] || "Unknown"] }.to_h
206
196
  end
207
197
 
208
198
  @mutex.synchronize { @hosts << host_info }
209
- rescue StandardError => e
210
- puts "\nError scanning host #{ip}: #{e.message}" if $DEBUG
211
199
  end
212
200
 
213
201
  def tcp_port_scan(ip, ports)
214
202
  open_ports = []
215
203
  is_active = false
204
+ threads = ports.map do |port|
205
+ Thread.new(port) do |p|
206
+ Timeout.timeout(@timeout) do
207
+ socket = TCPSocket.new(ip, p)
208
+ Thread.current[:open] = p
209
+ socket.close
210
+ end
211
+ rescue Errno::ECONNREFUSED
212
+ Thread.current[:active] = true
213
+ rescue StandardError
214
+ # Port closed or filtered
215
+ end
216
+ end
216
217
 
217
- ports.each do |port|
218
- Timeout.timeout(@timeout) do
219
- socket = TCPSocket.new(ip, port)
218
+ threads.each do |thread|
219
+ thread.join
220
+ if thread[:open]
221
+ open_ports << thread[:open]
222
+ is_active = true
223
+ elsif thread[:active]
220
224
  is_active = true
221
- open_ports << port
222
- socket.close
223
225
  end
224
- rescue Errno::ECONNREFUSED
225
- # Connection refused means host is up but port is closed
226
- is_active = true
227
- rescue StandardError
228
- # Other errors mean port is probably closed or filtered
229
226
  end
230
227
 
231
228
  { active: is_active, open_ports: open_ports }
232
229
  end
233
230
 
234
231
  def ping_check(ip)
235
- cmd = case RbConfig::CONFIG["host_os"]
236
- when /darwin/
237
- "ping -c 1 -W 1 #{ip}"
238
- when /linux/
239
- "ping -c 1 -W 1 #{ip}"
240
- when /mswin|mingw|cygwin/
241
- "ping -n 1 -w 1000 #{ip}"
242
- else
243
- "ping -c 1 -W 1 #{ip}"
244
- end
245
-
232
+ cmd = RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/ ? "ping -n 1 -w 1000 #{ip}" : "ping -c 1 -W 1 #{ip}"
246
233
  system("#{cmd} > /dev/null 2>&1")
247
234
  $CHILD_STATUS.exitstatus.zero?
248
235
  end
249
236
 
250
237
  def udp_check(ip)
251
238
  common_udp_ports = [53, 67, 68, 123, 137, 138, 1900, 5353]
252
-
253
239
  common_udp_ports.each do |port|
254
240
  Timeout.timeout(0.5) do
255
241
  socket = UDPSocket.new
@@ -259,35 +245,15 @@ module Lanet
259
245
  return true
260
246
  end
261
247
  rescue Errno::ECONNREFUSED
262
- return true # Connection refused means host is up
248
+ return true
263
249
  rescue StandardError
264
- # Try next port
250
+ next
265
251
  end
266
252
  false
267
253
  end
268
254
 
269
255
  def get_mac_address(ip)
270
- return "ff:ff:ff:ff:ff:ff" if ip.end_with?(".255") # Special case for broadcast
271
-
272
- # Get MAC from ARP table
273
- cmd = case RbConfig::CONFIG["host_os"]
274
- when /darwin/
275
- "arp -n #{ip}"
276
- when /linux/
277
- "arp -n #{ip}"
278
- when /mswin|mingw|cygwin/
279
- "arp -a #{ip}"
280
- else
281
- "arp -n #{ip}"
282
- end
283
-
284
- output = `#{cmd}`
285
-
286
- if output =~ /([0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2})/
287
- ::Regexp.last_match(1).downcase
288
- else
289
- "(incomplete)"
290
- end
256
+ @mutex.synchronize { @arp_cache[ip] || "(incomplete)" }
291
257
  end
292
258
  end
293
259
  end
data/lib/lanet/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lanet
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end