lanet 0.5.1 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/Gemfile.lock +1 -1
- data/README.md +70 -17
- data/index.html +359 -209
- data/lib/lanet/cli.rb +1 -5
- data/lib/lanet/encryptor.rb +138 -79
- data/lib/lanet/file_transfer.rb +64 -72
- data/lib/lanet/receiver.rb +62 -6
- data/lib/lanet/sender.rb +35 -3
- data/lib/lanet/version.rb +1 -1
- data/lib/lanet.rb +2 -0
- metadata +6 -3
data/lib/lanet/encryptor.rb
CHANGED
|
@@ -3,71 +3,93 @@
|
|
|
3
3
|
require "openssl"
|
|
4
4
|
require "digest"
|
|
5
5
|
require "base64"
|
|
6
|
+
require_relative "config"
|
|
6
7
|
require_relative "signer"
|
|
7
8
|
|
|
8
9
|
module Lanet
|
|
10
|
+
# Encryptor class for message encryption/decryption and signing
|
|
9
11
|
class Encryptor
|
|
10
|
-
#
|
|
11
|
-
CIPHER_ALGORITHM = "AES-256-CBC"
|
|
12
|
+
# Message type prefixes
|
|
12
13
|
ENCRYPTED_PREFIX = "E"
|
|
13
14
|
PLAINTEXT_PREFIX = "P"
|
|
14
15
|
SIGNED_ENCRYPTED_PREFIX = "SE"
|
|
15
16
|
SIGNED_PLAINTEXT_PREFIX = "SP"
|
|
16
|
-
|
|
17
|
+
|
|
18
|
+
# Delimiters and sizes
|
|
17
19
|
SIGNATURE_DELIMITER = "||SIG||"
|
|
18
|
-
MAX_KEY_LENGTH = 64
|
|
19
20
|
|
|
20
21
|
# Error class for encryption/decryption failures
|
|
21
22
|
class Error < StandardError; end
|
|
22
23
|
|
|
24
|
+
# Message type enumeration
|
|
25
|
+
module MessageType
|
|
26
|
+
ENCRYPTED = :encrypted
|
|
27
|
+
PLAINTEXT = :plaintext
|
|
28
|
+
SIGNED_ENCRYPTED = :signed_encrypted
|
|
29
|
+
SIGNED_PLAINTEXT = :signed_plaintext
|
|
30
|
+
end
|
|
31
|
+
|
|
23
32
|
# Prepares a message with encryption and/or signing
|
|
24
33
|
# @param message [String] the message to prepare
|
|
25
34
|
# @param encryption_key [String, nil] encryption key or nil for plaintext
|
|
26
35
|
# @param private_key [String, nil] PEM-encoded private key for signing or nil for unsigned
|
|
27
36
|
# @return [String] prepared message with appropriate prefix
|
|
28
37
|
def self.prepare_message(message, encryption_key, private_key = nil)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
message_str = message.to_s
|
|
39
|
+
has_encryption = !encryption_key.nil? && !encryption_key.empty?
|
|
40
|
+
has_signature = !private_key.nil? && !private_key.empty?
|
|
41
|
+
|
|
42
|
+
case [has_signature, has_encryption]
|
|
43
|
+
when [false, false]
|
|
44
|
+
prepare_plaintext(message_str)
|
|
45
|
+
when [false, true]
|
|
46
|
+
prepare_encrypted(message_str, encryption_key)
|
|
47
|
+
when [true, false]
|
|
48
|
+
prepare_signed_plaintext(message_str, private_key)
|
|
49
|
+
when [true, true]
|
|
50
|
+
prepare_signed_encrypted(message_str, encryption_key, private_key)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
35
53
|
|
|
36
|
-
|
|
54
|
+
# Prepare a plaintext message
|
|
55
|
+
def self.prepare_plaintext(message)
|
|
56
|
+
"#{PLAINTEXT_PREFIX}#{message}"
|
|
57
|
+
end
|
|
37
58
|
|
|
38
|
-
|
|
59
|
+
# Prepare an encrypted but unsigned message
|
|
60
|
+
def self.prepare_encrypted(message, key)
|
|
61
|
+
encrypted_data = encrypt_data(message, key)
|
|
62
|
+
"#{ENCRYPTED_PREFIX}#{encrypted_data}"
|
|
63
|
+
end
|
|
39
64
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
encrypted = cipher.update(message_with_signature) + cipher.final
|
|
47
|
-
encoded = Base64.strict_encode64(iv + encrypted)
|
|
48
|
-
"#{SIGNED_ENCRYPTED_PREFIX}#{encoded}"
|
|
49
|
-
rescue StandardError => e
|
|
50
|
-
raise Error, "Encryption failed: #{e.message}"
|
|
51
|
-
end
|
|
65
|
+
# Prepare a signed but unencrypted message
|
|
66
|
+
def self.prepare_signed_plaintext(message, private_key)
|
|
67
|
+
signature = Signer.sign(message, private_key)
|
|
68
|
+
message_with_signature = "#{message}#{SIGNATURE_DELIMITER}#{signature}"
|
|
69
|
+
"#{SIGNED_PLAINTEXT_PREFIX}#{message_with_signature}"
|
|
70
|
+
end
|
|
52
71
|
|
|
53
|
-
|
|
72
|
+
# Prepare a signed and encrypted message
|
|
73
|
+
def self.prepare_signed_encrypted(message, encryption_key, private_key)
|
|
74
|
+
signature = Signer.sign(message, private_key)
|
|
75
|
+
message_with_signature = "#{message}#{SIGNATURE_DELIMITER}#{signature}"
|
|
76
|
+
encrypted_data = encrypt_data(message_with_signature, encryption_key)
|
|
77
|
+
"#{SIGNED_ENCRYPTED_PREFIX}#{encrypted_data}"
|
|
54
78
|
end
|
|
55
79
|
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
raise Error, "Encryption failed: #{e.message}"
|
|
70
|
-
end
|
|
80
|
+
# Encrypt data with the given key
|
|
81
|
+
# @param data [String] data to encrypt
|
|
82
|
+
# @param key [String] encryption key
|
|
83
|
+
# @return [String] base64-encoded encrypted data with IV
|
|
84
|
+
def self.encrypt_data(data, key)
|
|
85
|
+
cipher = OpenSSL::Cipher.new(Config::CIPHER_ALGORITHM)
|
|
86
|
+
cipher.encrypt
|
|
87
|
+
cipher.key = derive_key(key)
|
|
88
|
+
iv = cipher.random_iv
|
|
89
|
+
encrypted = cipher.update(data) + cipher.final
|
|
90
|
+
Base64.strict_encode64(iv + encrypted)
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
raise Error, "Encryption failed: #{e.message}"
|
|
71
93
|
end
|
|
72
94
|
|
|
73
95
|
# Processes a message, decrypting and verifying if necessary
|
|
@@ -78,42 +100,68 @@ module Lanet
|
|
|
78
100
|
def self.process_message(data, encryption_key = nil, public_key = nil)
|
|
79
101
|
return { content: "[Empty message]", verified: false } if data.nil? || data.empty?
|
|
80
102
|
|
|
81
|
-
|
|
82
|
-
prefix = data[0..1] if data.length > 1 && %w[SE SP].include?(data[0..1]) # Two characters for complex prefixes
|
|
83
|
-
content = data[prefix.length..]
|
|
103
|
+
message_type, content = parse_message_type(data)
|
|
84
104
|
|
|
85
|
-
case
|
|
86
|
-
when
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
else
|
|
90
|
-
begin
|
|
91
|
-
decrypted = decode_encrypted_message(content, encryption_key)
|
|
92
|
-
{ content: decrypted, verified: false }
|
|
93
|
-
rescue StandardError => e
|
|
94
|
-
{ content: "Decryption failed: #{e.message}", verified: false }
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
when PLAINTEXT_PREFIX
|
|
105
|
+
case message_type
|
|
106
|
+
when MessageType::ENCRYPTED
|
|
107
|
+
process_encrypted_message(content, encryption_key)
|
|
108
|
+
when MessageType::PLAINTEXT
|
|
98
109
|
{ content: content, verified: false }
|
|
99
|
-
when
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
else
|
|
103
|
-
begin
|
|
104
|
-
decrypted = decode_encrypted_message(content, encryption_key)
|
|
105
|
-
process_signed_content(decrypted, public_key)
|
|
106
|
-
rescue StandardError => e
|
|
107
|
-
{ content: "Processing signed encrypted message failed: #{e.message}", verified: false }
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
when SIGNED_PLAINTEXT_PREFIX
|
|
110
|
+
when MessageType::SIGNED_ENCRYPTED
|
|
111
|
+
process_signed_encrypted_message(content, encryption_key, public_key)
|
|
112
|
+
when MessageType::SIGNED_PLAINTEXT
|
|
111
113
|
process_signed_content(content, public_key)
|
|
112
114
|
else
|
|
113
115
|
{ content: "[Invalid message format]", verified: false }
|
|
114
116
|
end
|
|
115
117
|
end
|
|
116
118
|
|
|
119
|
+
# Parse the message type from the prefix
|
|
120
|
+
# @param data [String] the raw message data
|
|
121
|
+
# @return [Array<Symbol, String>] message type and content
|
|
122
|
+
def self.parse_message_type(data)
|
|
123
|
+
# Check for two-character prefixes first
|
|
124
|
+
if data.length > 1 && data[0..1] == SIGNED_ENCRYPTED_PREFIX
|
|
125
|
+
[MessageType::SIGNED_ENCRYPTED, data[2..]]
|
|
126
|
+
elsif data.length > 1 && data[0..1] == SIGNED_PLAINTEXT_PREFIX
|
|
127
|
+
[MessageType::SIGNED_PLAINTEXT, data[2..]]
|
|
128
|
+
elsif data[0] == ENCRYPTED_PREFIX
|
|
129
|
+
[MessageType::ENCRYPTED, data[1..]]
|
|
130
|
+
elsif data[0] == PLAINTEXT_PREFIX
|
|
131
|
+
[MessageType::PLAINTEXT, data[1..]]
|
|
132
|
+
else
|
|
133
|
+
[nil, data]
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Process an encrypted message
|
|
138
|
+
def self.process_encrypted_message(content, encryption_key)
|
|
139
|
+
if encryption_key.nil? || encryption_key.strip.empty?
|
|
140
|
+
{ content: "[Encrypted message received, but no key provided]", verified: false }
|
|
141
|
+
else
|
|
142
|
+
begin
|
|
143
|
+
decrypted = decrypt_data(content, encryption_key)
|
|
144
|
+
{ content: decrypted, verified: false }
|
|
145
|
+
rescue Error => e
|
|
146
|
+
{ content: "Decryption failed: #{e.message}", verified: false }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Process a signed and encrypted message
|
|
152
|
+
def self.process_signed_encrypted_message(content, encryption_key, public_key)
|
|
153
|
+
if encryption_key.nil? || encryption_key.strip.empty?
|
|
154
|
+
{ content: "[Signed encrypted message received, but no encryption key provided]", verified: false }
|
|
155
|
+
else
|
|
156
|
+
begin
|
|
157
|
+
decrypted = decrypt_data(content, encryption_key)
|
|
158
|
+
process_signed_content(decrypted, public_key)
|
|
159
|
+
rescue Error => e
|
|
160
|
+
{ content: "Processing signed encrypted message failed: #{e.message}", verified: false }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
117
165
|
# Process content that contains a signature
|
|
118
166
|
def self.process_signed_content(content, public_key)
|
|
119
167
|
if content.include?(SIGNATURE_DELIMITER)
|
|
@@ -135,28 +183,39 @@ module Lanet
|
|
|
135
183
|
end
|
|
136
184
|
end
|
|
137
185
|
|
|
186
|
+
# Derive a key from the provided password
|
|
187
|
+
# @param key [String] the password/key to derive from
|
|
188
|
+
# @return [String] derived key of appropriate size
|
|
138
189
|
def self.derive_key(key)
|
|
139
|
-
|
|
140
|
-
if key && key.length > MAX_KEY_LENGTH
|
|
141
|
-
raise Error,
|
|
142
|
-
"Encryption key is too long (maximum #{MAX_KEY_LENGTH} characters)"
|
|
143
|
-
end
|
|
144
|
-
|
|
190
|
+
validate_key_length(key)
|
|
145
191
|
digest = OpenSSL::Digest.new("SHA256")
|
|
146
|
-
OpenSSL::PKCS5.pbkdf2_hmac(key, "salt", 1000,
|
|
192
|
+
OpenSSL::PKCS5.pbkdf2_hmac(key, "salt", 1000, Config::KEY_SIZE, digest)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Validate key length
|
|
196
|
+
def self.validate_key_length(key)
|
|
197
|
+
return unless key && key.length > Config::MAX_KEY_LENGTH
|
|
198
|
+
|
|
199
|
+
raise Error, "Encryption key is too long (maximum #{Config::MAX_KEY_LENGTH} characters)"
|
|
147
200
|
end
|
|
148
201
|
|
|
149
|
-
|
|
202
|
+
# Decrypt encrypted content
|
|
203
|
+
# @param content [String] base64-encoded encrypted data with IV
|
|
204
|
+
# @param key [String] decryption key
|
|
205
|
+
# @return [String] decrypted data
|
|
206
|
+
def self.decrypt_data(content, key)
|
|
150
207
|
decoded = Base64.strict_decode64(content)
|
|
151
|
-
iv = decoded[0...
|
|
152
|
-
ciphertext = decoded[
|
|
208
|
+
iv = decoded[0...Config::IV_SIZE]
|
|
209
|
+
ciphertext = decoded[Config::IV_SIZE..]
|
|
153
210
|
|
|
154
|
-
decipher = OpenSSL::Cipher.new(
|
|
211
|
+
decipher = OpenSSL::Cipher.new(Config::CIPHER_ALGORITHM)
|
|
155
212
|
decipher.decrypt
|
|
156
213
|
decipher.key = derive_key(key)
|
|
157
214
|
decipher.iv = iv
|
|
158
215
|
|
|
159
216
|
decipher.update(ciphertext) + decipher.final
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
raise Error, "Decryption failed: #{e.message}"
|
|
160
219
|
end
|
|
161
220
|
end
|
|
162
221
|
end
|
data/lib/lanet/file_transfer.rb
CHANGED
|
@@ -9,21 +9,16 @@ require "base64"
|
|
|
9
9
|
require "json"
|
|
10
10
|
require "socket"
|
|
11
11
|
require "timeout"
|
|
12
|
+
require_relative "config"
|
|
13
|
+
require_relative "transfer_state"
|
|
12
14
|
|
|
13
15
|
module Lanet
|
|
16
|
+
# FileTransfer handles secure file transmission over the network
|
|
14
17
|
class FileTransfer
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
18
|
+
# Use configuration constants
|
|
19
|
+
CHUNK_SIZE = Config::CHUNK_SIZE
|
|
20
|
+
MAX_RETRIES = Config::MAX_RETRIES
|
|
21
|
+
TIMEOUT = Config::FILE_TRANSFER_TIMEOUT
|
|
27
22
|
|
|
28
23
|
# Message types
|
|
29
24
|
FILE_HEADER = "FH" # File metadata
|
|
@@ -204,17 +199,19 @@ module Lanet
|
|
|
204
199
|
def handle_file_header(sender_ip, message_data, active_transfers, encryption_key, callback)
|
|
205
200
|
header = JSON.parse(message_data)
|
|
206
201
|
transfer_id = header["id"]
|
|
207
|
-
|
|
202
|
+
|
|
203
|
+
# Create new transfer state
|
|
204
|
+
active_transfers[transfer_id] = TransferState.new(
|
|
205
|
+
transfer_id: transfer_id,
|
|
208
206
|
sender_ip: sender_ip,
|
|
209
207
|
file_name: header["name"],
|
|
210
208
|
file_size: header["size"],
|
|
211
|
-
expected_checksum: header["checksum"]
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
timestamp: Time.now
|
|
215
|
-
}
|
|
209
|
+
expected_checksum: header["checksum"]
|
|
210
|
+
)
|
|
211
|
+
|
|
216
212
|
ack_message = Lanet::Encryptor.prepare_message("#{FILE_ACK}#{transfer_id}", encryption_key)
|
|
217
213
|
@sender.send_to(sender_ip, ack_message)
|
|
214
|
+
|
|
218
215
|
callback&.call(:start, {
|
|
219
216
|
transfer_id: transfer_id,
|
|
220
217
|
sender_ip: sender_ip,
|
|
@@ -229,19 +226,18 @@ module Lanet
|
|
|
229
226
|
chunk = JSON.parse(message_data)
|
|
230
227
|
transfer_id = chunk["id"]
|
|
231
228
|
transfer = active_transfers[transfer_id]
|
|
232
|
-
|
|
229
|
+
|
|
230
|
+
if transfer && transfer.sender_ip == sender_ip
|
|
233
231
|
chunk_data = Base64.strict_decode64(chunk["data"])
|
|
234
|
-
transfer
|
|
235
|
-
|
|
236
|
-
bytes_received = transfer[:temp_file].size
|
|
237
|
-
progress = (bytes_received.to_f / transfer[:file_size] * 100).round(2)
|
|
232
|
+
transfer.write_chunk(chunk_data)
|
|
233
|
+
|
|
238
234
|
callback&.call(:progress, {
|
|
239
235
|
transfer_id: transfer_id,
|
|
240
236
|
sender_ip: sender_ip,
|
|
241
|
-
file_name: transfer
|
|
242
|
-
progress: progress,
|
|
243
|
-
bytes_received: bytes_received,
|
|
244
|
-
total_bytes: transfer
|
|
237
|
+
file_name: transfer.file_name,
|
|
238
|
+
progress: transfer.progress,
|
|
239
|
+
bytes_received: transfer.bytes_received,
|
|
240
|
+
total_bytes: transfer.file_size
|
|
245
241
|
})
|
|
246
242
|
end
|
|
247
243
|
rescue JSON::ParserError => e
|
|
@@ -252,32 +248,34 @@ module Lanet
|
|
|
252
248
|
end_data = JSON.parse(message_data)
|
|
253
249
|
transfer_id = end_data["id"]
|
|
254
250
|
transfer = active_transfers[transfer_id]
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
active_transfers.delete(transfer_id)
|
|
251
|
+
|
|
252
|
+
return unless transfer && transfer.sender_ip == sender_ip
|
|
253
|
+
|
|
254
|
+
if transfer.verify_checksum
|
|
255
|
+
final_path = File.join(output_dir, transfer.file_name)
|
|
256
|
+
FileUtils.mv(transfer.temp_file.path, final_path)
|
|
257
|
+
|
|
258
|
+
ack_message = Lanet::Encryptor.prepare_message("#{FILE_ACK}#{transfer_id}", encryption_key)
|
|
259
|
+
@sender.send_to(sender_ip, ack_message)
|
|
260
|
+
|
|
261
|
+
callback&.call(:complete, {
|
|
262
|
+
transfer_id: transfer_id,
|
|
263
|
+
sender_ip: sender_ip,
|
|
264
|
+
file_name: transfer.file_name,
|
|
265
|
+
file_path: final_path
|
|
266
|
+
})
|
|
267
|
+
else
|
|
268
|
+
error_msg = "Checksum verification failed"
|
|
269
|
+
send_error(sender_ip, transfer_id, error_msg, encryption_key)
|
|
270
|
+
callback&.call(:error, {
|
|
271
|
+
transfer_id: transfer_id,
|
|
272
|
+
sender_ip: sender_ip,
|
|
273
|
+
error: error_msg
|
|
274
|
+
})
|
|
280
275
|
end
|
|
276
|
+
|
|
277
|
+
transfer.cleanup
|
|
278
|
+
active_transfers.delete(transfer_id)
|
|
281
279
|
rescue JSON::ParserError => e
|
|
282
280
|
send_error(sender_ip, "unknown", "Invalid end marker format: #{e.message}", encryption_key)
|
|
283
281
|
end
|
|
@@ -285,31 +283,25 @@ module Lanet
|
|
|
285
283
|
def handle_file_error(sender_ip, message_data, active_transfers, callback)
|
|
286
284
|
error_data = JSON.parse(message_data)
|
|
287
285
|
transfer_id = error_data["id"]
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
286
|
+
transfer = active_transfers[transfer_id]
|
|
287
|
+
|
|
288
|
+
return unless callback && transfer
|
|
289
|
+
|
|
290
|
+
callback.call(:error, {
|
|
291
|
+
transfer_id: transfer_id,
|
|
292
|
+
sender_ip: sender_ip,
|
|
293
|
+
error: error_data["message"]
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
transfer.cleanup
|
|
297
|
+
active_transfers.delete(transfer_id)
|
|
300
298
|
rescue JSON::ParserError
|
|
301
299
|
# Ignore malformed error messages
|
|
302
300
|
end
|
|
303
301
|
|
|
304
302
|
def cleanup_transfers(active_transfers)
|
|
305
|
-
active_transfers.each_value
|
|
306
|
-
|
|
307
|
-
begin
|
|
308
|
-
transfer[:temp_file].unlink
|
|
309
|
-
rescue StandardError
|
|
310
|
-
nil
|
|
311
|
-
end
|
|
312
|
-
end
|
|
303
|
+
active_transfers.each_value(&:cleanup)
|
|
304
|
+
active_transfers.clear
|
|
313
305
|
end
|
|
314
306
|
end
|
|
315
307
|
end
|
data/lib/lanet/receiver.rb
CHANGED
|
@@ -1,21 +1,77 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "socket"
|
|
4
|
+
require_relative "config"
|
|
4
5
|
|
|
5
6
|
module Lanet
|
|
7
|
+
# Receiver class for UDP message reception
|
|
6
8
|
class Receiver
|
|
9
|
+
class ReceiveError < StandardError; end
|
|
10
|
+
|
|
11
|
+
attr_reader :port
|
|
12
|
+
|
|
7
13
|
def initialize(port)
|
|
8
14
|
@port = port
|
|
9
|
-
@socket =
|
|
10
|
-
@
|
|
15
|
+
@socket = nil
|
|
16
|
+
@closed = false
|
|
17
|
+
@running = false
|
|
18
|
+
initialize_socket
|
|
11
19
|
end
|
|
12
20
|
|
|
13
|
-
def listen(&block)
|
|
21
|
+
def listen(buffer_size: Config::SMALL_BUFFER, &block)
|
|
22
|
+
raise ReceiveError, "Receiver is closed" if @closed
|
|
23
|
+
raise ArgumentError, "Block is required" unless block_given?
|
|
24
|
+
|
|
25
|
+
@running = true
|
|
26
|
+
|
|
14
27
|
loop do
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
break unless @running
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
data, addr = @socket.recvfrom(buffer_size)
|
|
32
|
+
ip = addr[3]
|
|
33
|
+
block.call(data, ip) if data && ip
|
|
34
|
+
rescue IOError, Errno::EBADF => e
|
|
35
|
+
break if @closed
|
|
36
|
+
|
|
37
|
+
raise ReceiveError, "Socket error: #{e.message}"
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
Config.logger.error("Error receiving message: #{e.message}")
|
|
40
|
+
# Continue listening despite errors
|
|
41
|
+
end
|
|
18
42
|
end
|
|
43
|
+
rescue Interrupt
|
|
44
|
+
Config.logger.info("Receiver interrupted")
|
|
45
|
+
stop
|
|
46
|
+
ensure
|
|
47
|
+
close unless @closed
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stop
|
|
51
|
+
@running = false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def close
|
|
55
|
+
return if @closed
|
|
56
|
+
|
|
57
|
+
@running = false
|
|
58
|
+
@socket&.close
|
|
59
|
+
@closed = true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def closed?
|
|
63
|
+
@closed
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def initialize_socket
|
|
69
|
+
@socket = UDPSocket.new
|
|
70
|
+
@socket.bind("0.0.0.0", @port)
|
|
71
|
+
rescue Errno::EADDRINUSE
|
|
72
|
+
raise ReceiveError, "Port #{@port} is already in use"
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
raise ReceiveError, "Failed to initialize socket: #{e.message}"
|
|
19
75
|
end
|
|
20
76
|
end
|
|
21
77
|
end
|
data/lib/lanet/sender.rb
CHANGED
|
@@ -3,19 +3,51 @@
|
|
|
3
3
|
require "socket"
|
|
4
4
|
|
|
5
5
|
module Lanet
|
|
6
|
+
# Sender class for UDP message transmission
|
|
6
7
|
class Sender
|
|
8
|
+
class SendError < StandardError; end
|
|
9
|
+
|
|
7
10
|
def initialize(port)
|
|
8
11
|
@port = port
|
|
9
|
-
@socket =
|
|
10
|
-
@
|
|
12
|
+
@socket = nil
|
|
13
|
+
@closed = false
|
|
14
|
+
initialize_socket
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def send_to(target_ip, message)
|
|
18
|
+
raise SendError, "Sender is closed" if @closed
|
|
19
|
+
raise ArgumentError, "Invalid IP address" if target_ip.nil? || target_ip.empty?
|
|
20
|
+
raise ArgumentError, "Message cannot be nil" if message.nil?
|
|
21
|
+
|
|
14
22
|
@socket.send(message, 0, target_ip, @port)
|
|
23
|
+
rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH => e
|
|
24
|
+
raise SendError, "Network unreachable: #{e.message}"
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
raise SendError, "Failed to send message: #{e.message}"
|
|
15
27
|
end
|
|
16
28
|
|
|
17
29
|
def broadcast(message)
|
|
18
|
-
|
|
30
|
+
send_to("255.255.255.255", message)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def close
|
|
34
|
+
return if @closed
|
|
35
|
+
|
|
36
|
+
@socket&.close
|
|
37
|
+
@closed = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def closed?
|
|
41
|
+
@closed
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def initialize_socket
|
|
47
|
+
@socket = UDPSocket.new
|
|
48
|
+
@socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
raise SendError, "Failed to initialize socket: #{e.message}"
|
|
19
51
|
end
|
|
20
52
|
end
|
|
21
53
|
end
|
data/lib/lanet/version.rb
CHANGED
data/lib/lanet.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "lanet/version"
|
|
4
|
+
require "lanet/config"
|
|
4
5
|
require "lanet/sender"
|
|
5
6
|
require "lanet/receiver"
|
|
6
7
|
require "lanet/scanner"
|
|
7
8
|
require "lanet/encryptor"
|
|
9
|
+
require "lanet/transfer_state"
|
|
8
10
|
require "lanet/cli"
|
|
9
11
|
require "lanet/ping"
|
|
10
12
|
require "lanet/file_transfer"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lanet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Davide Santangelo
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-10-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|
|
@@ -68,7 +68,10 @@ dependencies:
|
|
|
68
68
|
version: '3.0'
|
|
69
69
|
description: Lanet provides a simple yet powerful API for LAN device discovery, secure
|
|
70
70
|
messaging, and real-time network monitoring. Features include encrypted communications,
|
|
71
|
-
network scanning, targeted and broadcast messaging,
|
|
71
|
+
network scanning, targeted and broadcast messaging, host pinging, secure file transfers,
|
|
72
|
+
mesh networking, and advanced traceroute capabilities. Version 1.0.0 brings significant
|
|
73
|
+
architectural improvements with better resource management, enhanced error handling,
|
|
74
|
+
and improved code quality while maintaining 100% backward compatibility.
|
|
72
75
|
email:
|
|
73
76
|
- davide.santangelo@example.com
|
|
74
77
|
executables:
|