lanet 0.1.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/index.html ADDED
@@ -0,0 +1,233 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Lanet - Lightweight LAN Communication Tool for Networking</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Arial', sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ margin: 0;
13
+ padding: 0;
14
+ background-color: #f4f4f4;
15
+ }
16
+ .container {
17
+ max-width: 1200px;
18
+ margin: 0 auto;
19
+ padding: 20px;
20
+ }
21
+ header {
22
+ background: linear-gradient(90deg, #007BFF, #0056b3);
23
+ color: white;
24
+ padding: 40px 0;
25
+ text-align: center;
26
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
27
+ }
28
+ header h1 {
29
+ font-size: 3em;
30
+ margin: 0;
31
+ }
32
+ header p {
33
+ font-size: 1.2em;
34
+ margin-top: 10px;
35
+ }
36
+ .badges {
37
+ margin-top: 20px;
38
+ }
39
+ .badges img {
40
+ margin: 0 10px;
41
+ }
42
+ section {
43
+ margin: 40px 0;
44
+ padding: 20px;
45
+ background-color: white;
46
+ border-radius: 8px;
47
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
48
+ }
49
+ h2 {
50
+ color: #007BFF;
51
+ font-size: 2em;
52
+ margin-bottom: 20px;
53
+ }
54
+ h3 {
55
+ color: #0056b3;
56
+ font-size: 1.5em;
57
+ margin-top: 30px;
58
+ }
59
+ code {
60
+ background-color: #f4f4f4;
61
+ padding: 2px 4px;
62
+ border-radius: 4px;
63
+ font-family: 'Courier New', monospace;
64
+ }
65
+ pre {
66
+ background-color: #f4f4f4;
67
+ padding: 15px;
68
+ border-radius: 8px;
69
+ overflow-x: auto;
70
+ }
71
+ .feature-list {
72
+ list-style-type: none;
73
+ padding: 0;
74
+ }
75
+ .feature-list li {
76
+ margin: 10px 0;
77
+ font-size: 1.1em;
78
+ }
79
+ .feature-list li::before {
80
+ content: '🚀';
81
+ margin-right: 10px;
82
+ }
83
+ .cli-example, .api-example {
84
+ margin: 20px 0;
85
+ }
86
+ .cli-example h4, .api-example h4 {
87
+ margin-bottom: 10px;
88
+ font-size: 1.2em;
89
+ }
90
+ footer {
91
+ text-align: center;
92
+ padding: 20px;
93
+ background-color: #007BFF;
94
+ color: white;
95
+ margin-top: 40px;
96
+ }
97
+ footer a {
98
+ color: white;
99
+ text-decoration: none;
100
+ }
101
+ </style>
102
+ </head>
103
+ <body>
104
+ <header>
105
+ <div class="container">
106
+ <h1>Lanet</h1>
107
+ <p>A lightweight, powerful LAN communication tool for Networking</p>
108
+ <div class="badges">
109
+ <a href="https://badge.fury.io/rb/lanet"><img src="https://badge.fury.io/rb/lanet.svg" alt="Gem Version"></a>
110
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
111
+ </div>
112
+ </div>
113
+ </header>
114
+
115
+ <div class="container">
116
+ <section id="about">
117
+ <h2>About Lanet</h2>
118
+ <p>Lanet is a lightweight and powerful LAN communication tool for Ruby that enables secure message exchange between devices on the same network. It simplifies peer-to-peer networking with built-in encryption, network discovery, and both targeted and broadcast messaging capabilities.</p>
119
+ </section>
120
+
121
+ <section id="features">
122
+ <h2>Features</h2>
123
+ <ul class="feature-list">
124
+ <li>Simple API - An intuitive Ruby interface for straightforward network communication.</li>
125
+ <li>Built-in encryption - Optional message encryption with AES-256-GCM for confidentiality.</li>
126
+ <li>Network scanning - Automatically discover active devices on your local network.</li>
127
+ <li>Targeted messaging - Send messages to specific IP addresses.</li>
128
+ <li>Broadcasting - Send messages to all devices on the network.</li>
129
+ <li>Host pinging - Check host availability and measure response times (with a familiar <code>ping</code> interface).</li>
130
+ <li>Command-line interface - Perform common network operations directly from your terminal.</li>
131
+ <li>Extensible - Easily build custom tools and integrations using the Lanet API.</li>
132
+ <li>Configurable - Adjust port settings, encryption keys, and network scan ranges.</li>
133
+ </ul>
134
+ </section>
135
+
136
+ <section id="installation">
137
+ <h2>Installation</h2>
138
+ <p>Add this line to your application's Gemfile:</p>
139
+ <pre><code>gem 'lanet'</code></pre>
140
+ <p>And then execute:</p>
141
+ <pre><code>bundle install</code></pre>
142
+ <p>Or install it yourself as:</p>
143
+ <pre><code>gem install lanet</code></pre>
144
+ </section>
145
+
146
+ <section id="usage">
147
+ <h2>Usage</h2>
148
+ <h3>Command Line Interface (CLI)</h3>
149
+ <div class="cli-example">
150
+ <h4>Scanning the network</h4>
151
+ <pre><code>lanet scan --range 192.168.1.0/24</code></pre>
152
+ <p>With verbose output (shows hostname, MAC address, open ports, and response time):</p>
153
+ <pre><code>lanet scan --range 192.168.1.0/24 --verbose</code></pre>
154
+ </div>
155
+ <div class="cli-example">
156
+ <h4>Sending a message to a specific target</h4>
157
+ <pre><code>lanet send --target 192.168.1.5 --message "Hello there!"</code></pre>
158
+ <p>Sending an encrypted message:</p>
159
+ <pre><code>lanet send --target 192.168.1.5 --message "Secret message" --key "my_secret_key"</code></pre>
160
+ </div>
161
+ <div class="cli-example">
162
+ <h4>Broadcasting a message to all devices</h4>
163
+ <pre><code>lanet broadcast --message "Announcement for everyone!"</code></pre>
164
+ </div>
165
+ <div class="cli-example">
166
+ <h4>Pinging a specific host</h4>
167
+ <pre><code>lanet ping --host 192.168.1.5</code></pre>
168
+ </div>
169
+
170
+ <h3>Ruby API</h3>
171
+ <div class="api-example">
172
+ <h4>Scanning the network</h4>
173
+ <pre><code>require 'lanet'
174
+
175
+ scanner = Lanet.scanner
176
+ active_ips = scanner.scan('192.168.1.0/24')
177
+ puts "Found devices: #{active_ips.join(', ')}"</code></pre>
178
+ </div>
179
+ <div class="api-example">
180
+ <h4>Sending a message</h4>
181
+ <pre><code>sender = Lanet.sender
182
+ sender.send_to('192.168.1.5', 'Hello from Ruby!')</code></pre>
183
+ <p>Broadcasting a message:</p>
184
+ <pre><code>sender.broadcast('Announcement to all devices!')</code></pre>
185
+ </div>
186
+ <div class="api-example">
187
+ <h4>Listening for messages</h4>
188
+ <pre><code>receiver = Lanet.receiver
189
+ receiver.listen do |data, ip|
190
+ puts "Received from #{ip}: #{data}"
191
+ end</code></pre>
192
+ </div>
193
+ </section>
194
+
195
+ <section id="configuration">
196
+ <h2>Configuration</h2>
197
+ <p>Lanet can be configured with several options:</p>
198
+ <ul>
199
+ <li><strong>Port</strong>: Default is 5000, but can be changed for both sending and receiving.</li>
200
+ <li><strong>Encryption Keys</strong>: Use your own encryption keys for secure communication.</li>
201
+ <li><strong>Custom Ranges</strong>: Scan specific network ranges to discover devices.</li>
202
+ </ul>
203
+ </section>
204
+
205
+ <section id="development">
206
+ <h2>Development</h2>
207
+ <p>After checking out the repo, run <code>bin/setup</code> to install dependencies. Then, run <code>rake spec</code> to run the tests.</p>
208
+ <p>To install this gem locally, run <code>bundle exec rake install</code>.</p>
209
+ </section>
210
+
211
+ <section id="contributing">
212
+ <h2>Contributing</h2>
213
+ <p>Bug reports and pull requests are welcome on GitHub at <a href="https://github.com/davidesantangelo/lanet">https://github.com/davidesantangelo/lanet</a>. Follow these steps:</p>
214
+ <ol>
215
+ <li>Fork the repository</li>
216
+ <li>Create your feature branch (<code>git checkout -b my-new-feature</code>)</li>
217
+ <li>Commit your changes (<code>git commit -am 'Add some feature'</code>)</li>
218
+ <li>Push to the branch (<code>git push origin my-new-feature</code>)</li>
219
+ <li>Create a new Pull Request</li>
220
+ </ol>
221
+ </section>
222
+
223
+ <section id="license">
224
+ <h2>License</h2>
225
+ <p>The gem is available under the <a href="https://opensource.org/licenses/MIT">MIT License</a>.</p>
226
+ </section>
227
+ </div>
228
+
229
+ <footer>
230
+ <p>&copy; 2025 Lanet. All rights reserved. | <a href="https://github.com/davidesantangelo/lanet">View on GitHub</a></p>
231
+ </footer>
232
+ </body>
233
+ </html>
data/lib/lanet/cli.rb ADDED
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "lanet/sender"
5
+ require "lanet/receiver"
6
+ require "lanet/scanner"
7
+ require "lanet/ping"
8
+ require "lanet/encryptor"
9
+
10
+ module Lanet
11
+ class CLI < Thor
12
+ # Add this method to silence Thor errors and disable exit on failure
13
+ def self.exit_on_failure?
14
+ false
15
+ end
16
+
17
+ desc "send --target IP --message MSG [--key KEY] [--port PORT]", "Send a message to a specific target"
18
+ option :target, required: true
19
+ option :message, required: true
20
+ option :key
21
+ option :port, type: :numeric, default: 5000
22
+ def send
23
+ sender = Sender.new(options[:port])
24
+ message = Encryptor.prepare_message(options[:message], options[:key])
25
+ sender.send_to(options[:target], message)
26
+ puts "Message sent to #{options[:target]}"
27
+ end
28
+
29
+ desc "broadcast --message MSG [--key KEY] [--port PORT]", "Broadcast a message to all devices"
30
+ option :message, required: true
31
+ option :key
32
+ option :port, type: :numeric, default: 5000
33
+ def broadcast
34
+ sender = Sender.new(options[:port])
35
+ message = Encryptor.prepare_message(options[:message], options[:key])
36
+ sender.broadcast(message)
37
+ puts "Message broadcasted"
38
+ end
39
+
40
+ desc "scan --range CIDR [--timeout TIMEOUT] [--threads THREADS] [--verbose]",
41
+ "Scan for active devices in the given range"
42
+ option :range, required: true
43
+ option :timeout, type: :numeric, default: 1
44
+ option :threads, type: :numeric, default: 32
45
+ option :verbose, type: :boolean, default: false
46
+ def scan
47
+ scanner = Scanner.new
48
+ results = scanner.scan(
49
+ options[:range],
50
+ options[:timeout],
51
+ options[:threads],
52
+ options[:verbose]
53
+ )
54
+
55
+ puts "\nActive devices:"
56
+
57
+ if options[:verbose]
58
+ results.each do |host|
59
+ puts "─" * 50
60
+ puts "IP: #{host[:ip]}"
61
+ puts "Hostname: #{host[:hostname]}" if host[:hostname]
62
+ puts "MAC: #{host[:mac]}" if host[:mac] # Add this line to display MAC addresses
63
+ puts "Response time: #{host[:response_time]}ms" if host[:response_time]
64
+ puts "Detection method: #{host[:detection_method]}" if host[:detection_method]
65
+
66
+ next unless host[:ports] && !host[:ports].empty?
67
+
68
+ puts "Open ports:"
69
+ host[:ports].each do |port, service|
70
+ puts " - #{port}: #{service}"
71
+ end
72
+ end
73
+ puts "─" * 50
74
+ puts "Found #{results.size} active hosts."
75
+ else
76
+ results.each { |ip| puts ip }
77
+ end
78
+ end
79
+
80
+ desc "listen [--port PORT] [--key KEY]", "Listen for incoming messages"
81
+ option :port, type: :numeric, default: 5000
82
+ option :key
83
+ def listen
84
+ receiver = Receiver.new(options[:port])
85
+ puts "Listening on port #{options[:port]}..."
86
+ receiver.listen do |data, ip|
87
+ message = Encryptor.process_message(data, options[:key])
88
+ puts "From #{ip}: #{message}"
89
+ end
90
+ end
91
+
92
+ desc "ping [HOST]", "Ping a host or multiple hosts with real-time output"
93
+ option :host, type: :string, desc: "Single host to ping"
94
+ option :hosts, type: :string, desc: "Comma-separated list of hosts to ping"
95
+ option :timeout, type: :numeric, default: 1, desc: "Ping timeout in seconds"
96
+ option :count, type: :numeric, default: 5, desc: "Number of ping packets to send"
97
+ option :quiet, type: :boolean, default: false, desc: "Only display summary"
98
+ option :continuous, type: :boolean, default: false, desc: "Ping continuously until interrupted"
99
+ def ping(target_host = nil)
100
+ # Support both traditional command (lanet ping 192.168.1.1) and option-style (--host)
101
+ target = target_host || options[:host] || options[:hosts]
102
+
103
+ unless target
104
+ puts "Error: Missing host to ping"
105
+ puts "Usage: lanet ping HOST"
106
+ puts " or: lanet ping --host HOST"
107
+ puts " or: lanet ping --hosts HOST1,HOST2,HOST3"
108
+ return
109
+ end
110
+
111
+ pinger = Lanet::Ping.new(timeout: options[:timeout], count: options[:count])
112
+
113
+ if target_host || options[:host]
114
+ # For a single host, we use real-time output unless quiet is specified
115
+ host = target_host || options[:host]
116
+ if options[:quiet]
117
+ result = pinger.ping_host(host, false, options[:continuous])
118
+ display_ping_summary(host, result)
119
+ else
120
+ pinger.ping_host(host, true, options[:continuous]) # Real-time output with optional continuous mode
121
+ end
122
+ else
123
+ hosts = options[:hosts].split(",").map(&:strip)
124
+
125
+ if options[:quiet]
126
+ results = pinger.ping_hosts(hosts, false, options[:continuous])
127
+ hosts.each do |host|
128
+ display_ping_summary(host, results[host])
129
+ puts "\n" unless host == hosts.last
130
+ end
131
+ else
132
+ # Real-time output for multiple hosts
133
+ pinger.ping_hosts(hosts, true, options[:continuous])
134
+ end
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def display_ping_details(host, result)
141
+ # Display header like standard ping command
142
+ puts "PING #{host} (#{host}): 56 data bytes"
143
+
144
+ if result[:status]
145
+ # Display individual ping responses
146
+ unless options[:quiet]
147
+ result[:responses].each do |response|
148
+ puts "64 bytes from #{host}: icmp_seq=#{response[:seq]} ttl=#{response[:ttl]} time=#{response[:time]} ms"
149
+ end
150
+ end
151
+
152
+ # Display summary
153
+ transmitted = options[:count]
154
+ received = result[:responses].size
155
+ loss_pct = ((transmitted - received) / transmitted.to_f * 100).round(1)
156
+
157
+ puts "\n--- #{host} ping statistics ---"
158
+ puts "#{transmitted} packets transmitted, #{received} packets received, #{loss_pct}% packet loss"
159
+
160
+ if received.positive?
161
+ times = result[:responses].map { |r| r[:time] }
162
+ min = times.min
163
+ avg = times.sum / times.size
164
+ max = times.max
165
+ mdev = Math.sqrt(times.map { |t| (t - avg)**2 }.sum / times.size).round(3)
166
+
167
+ puts "round-trip min/avg/max/stddev = #{min}/#{avg.round(3)}/#{max}/#{mdev} ms"
168
+ end
169
+ else
170
+ puts "No response from #{host}"
171
+ puts result[:output] if result[:output].to_s.strip != ""
172
+ end
173
+ end
174
+
175
+ # Display only the summary portion
176
+ def display_ping_summary(host, result)
177
+ if result[:status]
178
+ transmitted = options[:count]
179
+ received = result[:responses].size
180
+ loss_pct = ((transmitted - received) / transmitted.to_f * 100).round(1)
181
+
182
+ puts "--- #{host} ping statistics ---"
183
+ puts "#{transmitted} packets transmitted, #{received} packets received, #{loss_pct}% packet loss"
184
+
185
+ if received.positive?
186
+ times = result[:responses].map { |r| r[:time] }
187
+ min = times.min
188
+ avg = times.sum / times.size
189
+ max = times.max
190
+ mdev = Math.sqrt(times.map { |t| (t - avg)**2 }.sum / times.size).round(3)
191
+
192
+ puts "round-trip min/avg/max/stddev = #{min}/#{avg.round(3)}/#{max}/#{mdev} ms"
193
+ end
194
+ else
195
+ puts "No response from #{host}"
196
+ end
197
+ end
198
+
199
+ def display_ping_result(host, result)
200
+ # Keep the old method for backward compatibility
201
+ puts "Host: #{host}"
202
+ puts "Status: #{result[:status] ? "reachable" : "unreachable"}"
203
+
204
+ if result[:status]
205
+ puts "Response time: #{result[:response_time]}ms"
206
+ puts "Packet loss: #{result[:packet_loss]}%"
207
+ end
208
+
209
+ return unless options[:verbose]
210
+
211
+ puts "\nOutput:"
212
+ puts result[:output]
213
+ end
214
+
215
+ # Override method_missing to provide helpful error messages for common mistakes
216
+ def method_missing(method, *args)
217
+ if method.to_s == "ping" && args.any?
218
+ invoke "ping", [], { host: args.first, timeout: options[:timeout], count: options[:count],
219
+ quiet: options[:quiet], continuous: options[:continuous] }
220
+ else
221
+ super
222
+ end
223
+ end
224
+
225
+ def respond_to_missing?(method, include_private = false)
226
+ method.to_s == "ping" || super
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "digest"
5
+ require "base64"
6
+
7
+ module Lanet
8
+ class Encryptor
9
+ # Constants
10
+ CIPHER_ALGORITHM = "AES-256-CBC"
11
+ ENCRYPTED_PREFIX = "E"
12
+ PLAINTEXT_PREFIX = "P"
13
+ IV_SIZE = 16
14
+
15
+ # Error class for encryption/decryption failures
16
+ class Error < StandardError; end
17
+
18
+ # Encrypts a message if key is provided, otherwise marks it as plaintext
19
+ # @param message [String] the message to prepare
20
+ # @param key [String, nil] encryption key or nil for plaintext
21
+ # @return [String] prepared message with appropriate prefix
22
+ def self.prepare_message(message, key)
23
+ return PLAINTEXT_PREFIX + message.to_s if key.nil? || key.empty?
24
+
25
+ begin
26
+ cipher = OpenSSL::Cipher.new("AES-128-CBC")
27
+ cipher.encrypt
28
+ cipher.key = derive_key(key)
29
+ iv = cipher.random_iv
30
+ encrypted = cipher.update(message.to_s) + cipher.final
31
+ encoded = Base64.strict_encode64(iv + encrypted)
32
+ "#{ENCRYPTED_PREFIX}#{encoded}"
33
+ rescue StandardError => e
34
+ raise Error, "Encryption failed: #{e.message}"
35
+ end
36
+ end
37
+
38
+ # Processes a message, decrypting if necessary
39
+ # @param data [String] the data to process
40
+ # @param key [String, nil] decryption key or nil
41
+ # @return [String] processed message
42
+ def self.process_message(data, key)
43
+ return "[Empty message]" if data.nil? || data.empty?
44
+
45
+ prefix = data[0]
46
+ content = data[1..]
47
+
48
+ case prefix
49
+ when ENCRYPTED_PREFIX
50
+ if key.nil? || key.strip.empty?
51
+ "[Encrypted message received, but no key provided]"
52
+ else
53
+ begin
54
+ decode_encrypted_message(content, key)
55
+ rescue StandardError => e
56
+ "Decryption failed: #{e.message}"
57
+ end
58
+ end
59
+ when PLAINTEXT_PREFIX
60
+ content
61
+ else
62
+ "[Invalid message format]"
63
+ end
64
+ end
65
+
66
+ def self.derive_key(key)
67
+ digest = OpenSSL::Digest.new("SHA256")
68
+ OpenSSL::PKCS5.pbkdf2_hmac(key, "salt", 1000, 16, digest)
69
+ end
70
+
71
+ def self.decode_encrypted_message(content, key)
72
+ decoded = Base64.strict_decode64(content)
73
+ iv = decoded[0...16]
74
+ ciphertext = decoded[16..]
75
+
76
+ decipher = OpenSSL::Cipher.new("AES-128-CBC")
77
+ decipher.decrypt
78
+ decipher.key = derive_key(key)
79
+ decipher.iv = iv
80
+
81
+ decipher.update(ciphertext) + decipher.final
82
+ end
83
+ end
84
+ end