keenetic 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 150b29909b27e3c0c7704dead364af88b95152e02c48ad237dd2bbdaf5d6471e
4
- data.tar.gz: dde2d8a08098c812502fb07cb3cad0262d004ff6c431be3b8d11b21f2216491a
3
+ metadata.gz: cc71a113a4147e3ffd1035596420469ca8c7d9e9639623fd14172b2137d902f6
4
+ data.tar.gz: 7fcb0cbd7b8a1219715288354b58bfa7a8f15ed7900334be7fd4014a1de9e788
5
5
  SHA512:
6
- metadata.gz: 12e93dc8b6b71b665fd88994236ef984c042dc2cbf16b3108456f9615e55c5751a76b657adaf7ff970c4a83ffd3d4aa49ade8800b772666abc27c23e082bc630
7
- data.tar.gz: 7921327abfc55e5f1d2c4846602ecad522fe69478b8d78ddf2a3519ebe9b15aff87950618eeef2a8fe4a6e889c08e8b350931bfe2bc430549bb12a36668e8d95
6
+ metadata.gz: e03cfc9b96ac18afa8ec3526d735970e5381983325920aaf0f771c1e31337f4c0800158a320081c696ad973b75eab29846b961ea5ea76d2c12b6209516a894e4
7
+ data.tar.gz: fba387ac8b75676298f75715b22c1c348d3ca749fe44234b2c0dbd8d01ff6b9554f78ddf19fcea2b4dcfbee17ccadc253f91a5c777c7c91427d89140fab5335e
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Anton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -20,34 +20,36 @@ Keenetic.configure do |config|
20
20
  end
21
21
  ```
22
22
 
23
- ## Usage
23
+ ## Quick Start
24
24
 
25
25
  ```ruby
26
26
  client = Keenetic.client
27
27
 
28
- # Connected devices
29
- client.devices.all
30
- client.devices.active
31
- client.devices.find(mac: 'AA:BB:CC:DD:EE:FF')
32
- client.devices.update(mac: 'AA:BB:CC:DD:EE:FF', name: 'My Phone')
33
-
34
28
  # System info
35
- client.system.info # model, firmware
36
- client.system.resources # CPU, memory, uptime
37
-
38
- # Network
39
- client.network.interfaces
29
+ client.system.info # model, firmware
30
+ client.system.resources # CPU, memory, uptime
40
31
 
41
- # WiFi
42
- client.wifi.access_points
43
- client.wifi.clients
44
-
45
- # Internet
46
- client.internet.status
47
- client.internet.speed
32
+ # Connected devices
33
+ client.devices.all # all registered devices
34
+ client.devices.active # currently connected
48
35
 
49
- # Ports
50
- client.ports.all
36
+ # Network
37
+ client.network.interfaces # all interfaces
38
+ client.wifi.access_points # Wi-Fi networks
39
+ client.internet.status # internet connectivity
40
+
41
+ # Port forwarding
42
+ client.nat.rules # NAT rules
43
+ client.nat.add_forward(index: 1, protocol: 'tcp', port: 8080,
44
+ to_host: '192.168.1.100', to_port: 80)
45
+
46
+ # VPN
47
+ client.vpn.status # VPN server status
48
+ client.vpn.clients # connected VPN clients
49
+
50
+ # Configuration
51
+ client.system_config.save # save to flash
52
+ client.system_config.download # backup config
51
53
  ```
52
54
 
53
55
  ## Error Handling
@@ -61,18 +63,18 @@ rescue Keenetic::ConnectionError
61
63
  # router unreachable
62
64
  rescue Keenetic::TimeoutError
63
65
  # request timed out
64
- rescue Keenetic::NotFoundError
65
- # resource not found
66
66
  rescue Keenetic::ApiError => e
67
- # Other API errors
67
+ # other API errors
68
68
  e.status_code
69
69
  e.response_body
70
70
  end
71
71
  ```
72
72
 
73
- ## API Reference
73
+ ## Documentation
74
74
 
75
- See [KEENETIC_API.md](KEENETIC_API.md) for complete API documentation.
75
+ - [Complete API Reference](docs/API_REFERENCE.md) - All features with detailed examples
76
+ - [API Coverage](API_PROGRESS.md) - Implementation status
77
+ - [Keenetic API Specification](KEENETIC_API.md) - Raw API documentation
76
78
 
77
79
  ## Requirements
78
80
 
@@ -102,6 +102,120 @@ module Keenetic
102
102
  @logs ||= Resources::Logs.new(self)
103
103
  end
104
104
 
105
+ # @return [Resources::Routes] Static routes resource
106
+ def routes
107
+ @routes ||= Resources::Routes.new(self)
108
+ end
109
+
110
+ # @return [Resources::Hotspot] Hotspot hosts and policies resource
111
+ def hotspot
112
+ @hotspot ||= Resources::Hotspot.new(self)
113
+ end
114
+
115
+ # @return [Resources::Config] Configuration management resource
116
+ def system_config
117
+ @system_config ||= Resources::Config.new(self)
118
+ end
119
+
120
+ # @return [Resources::Nat] NAT and port forwarding resource
121
+ def nat
122
+ @nat ||= Resources::Nat.new(self)
123
+ end
124
+
125
+ # @return [Resources::Vpn] VPN server resource
126
+ def vpn
127
+ @vpn ||= Resources::Vpn.new(self)
128
+ end
129
+
130
+ # @return [Resources::Diagnostics] Network diagnostics resource
131
+ def diagnostics
132
+ @diagnostics ||= Resources::Diagnostics.new(self)
133
+ end
134
+
135
+ # @return [Resources::Firewall] Firewall resource
136
+ def firewall
137
+ @firewall ||= Resources::Firewall.new(self)
138
+ end
139
+
140
+ # @return [Resources::Mesh] Mesh Wi-Fi system resource
141
+ def mesh
142
+ @mesh ||= Resources::Mesh.new(self)
143
+ end
144
+
145
+ # @return [Resources::Usb] USB devices and storage resource
146
+ def usb
147
+ @usb ||= Resources::Usb.new(self)
148
+ end
149
+
150
+ # @return [Resources::Dns] DNS settings and cache resource
151
+ def dns
152
+ @dns ||= Resources::Dns.new(self)
153
+ end
154
+
155
+ # @return [Resources::Dyndns] Dynamic DNS resource
156
+ def dyndns
157
+ @dyndns ||= Resources::Dyndns.new(self)
158
+ end
159
+
160
+ # @return [Resources::Schedule] Access schedules resource
161
+ def schedule
162
+ @schedule ||= Resources::Schedule.new(self)
163
+ end
164
+
165
+ # @return [Resources::Users] User accounts resource
166
+ def users
167
+ @users ||= Resources::Users.new(self)
168
+ end
169
+
170
+ # @return [Resources::Components] System components resource
171
+ def components
172
+ @components ||= Resources::Components.new(self)
173
+ end
174
+
175
+ # @return [Resources::Qos] QoS and traffic control resource
176
+ def qos
177
+ @qos ||= Resources::Qos.new(self)
178
+ end
179
+
180
+ # @return [Resources::Ipv6] IPv6 network resource
181
+ def ipv6
182
+ @ipv6 ||= Resources::Ipv6.new(self)
183
+ end
184
+
185
+ # Execute arbitrary RCI command(s).
186
+ #
187
+ # Provides raw access to the Keenetic RCI (Remote Command Interface).
188
+ # Use this for custom commands not covered by the gem's resources.
189
+ #
190
+ # == Keenetic API
191
+ # POST http://<host>/rci/
192
+ # Content-Type: application/json
193
+ # Body: Array or Hash of RCI commands
194
+ #
195
+ # @param body [Hash, Array<Hash>] RCI command(s) to execute
196
+ # @return [Hash, Array, nil] Parsed JSON response
197
+ #
198
+ # @example Execute single command
199
+ # client.rci({ 'show' => { 'system' => {} } })
200
+ # # => { "show" => { "system" => { ... } } }
201
+ #
202
+ # @example Execute batch commands
203
+ # client.rci([
204
+ # { 'show' => { 'system' => {} } },
205
+ # { 'show' => { 'version' => {} } }
206
+ # ])
207
+ # # => [{ "show" => { "system" => { ... } } }, { "show" => { "version" => { ... } } }]
208
+ #
209
+ # @example Execute write command
210
+ # client.rci([
211
+ # { 'ip' => { 'hotspot' => { 'host' => { 'mac' => 'aa:bb:cc:dd:ee:ff', 'permit' => true } } } }
212
+ # ])
213
+ #
214
+ def rci(body)
215
+ commands = body.is_a?(Array) ? body : [body]
216
+ batch(commands)
217
+ end
218
+
105
219
  # Make a GET request to the router API.
106
220
  #
107
221
  # == Keenetic API
@@ -162,6 +276,19 @@ module Keenetic
162
276
  request(:post, '/rci/', body: commands)
163
277
  end
164
278
 
279
+ # Make a POST request with raw body content (non-JSON).
280
+ #
281
+ # Used for file uploads like configuration restore.
282
+ #
283
+ # @param path [String] API path
284
+ # @param content [String] Raw content to send
285
+ # @param content_type [String] Content-Type header (default: text/plain)
286
+ # @return [String, nil] Response body
287
+ #
288
+ def post_raw(path, content, content_type: 'text/plain')
289
+ request(:post, path, raw_body: content, content_type: content_type)
290
+ end
291
+
165
292
  # Check if client is authenticated.
166
293
  # @return [Boolean]
167
294
  def authenticated?
@@ -205,7 +332,10 @@ module Keenetic
205
332
  url += "?#{URI.encode_www_form(options[:params])}"
206
333
  end
207
334
 
208
- if options[:body]
335
+ if options[:raw_body]
336
+ request_options[:body] = options[:raw_body]
337
+ request_options[:headers]['Content-Type'] = options[:content_type] || 'text/plain'
338
+ elsif options[:body]
209
339
  request_options[:body] = options[:body].to_json
210
340
  request_options[:headers]['Content-Type'] = 'application/json'
211
341
  end
@@ -17,6 +17,10 @@ module Keenetic
17
17
  client.post(path, body)
18
18
  end
19
19
 
20
+ def post_raw(path, content, content_type: 'text/plain')
21
+ client.post_raw(path, content, content_type: content_type)
22
+ end
23
+
20
24
  # Convert kebab-case keys to snake_case symbols
21
25
  # @param hash [Hash] Hash with string keys
22
26
  # @return [Hash] Hash with symbolized snake_case keys
@@ -0,0 +1,88 @@
1
+ module Keenetic
2
+ module Resources
3
+ # Components resource for managing installable router components.
4
+ #
5
+ # == API Endpoints Used
6
+ #
7
+ # === Installed Components
8
+ # GET /rci/show/components
9
+ #
10
+ # === Available Components
11
+ # GET /rci/show/components/available
12
+ #
13
+ # === Install Component
14
+ # POST /rci/components/install
15
+ #
16
+ # === Remove Component
17
+ # POST /rci/components/remove
18
+ #
19
+ class Components < Base
20
+ # Get installed components.
21
+ #
22
+ # @return [Array<Hash>] List of installed components
23
+ # @example
24
+ # installed = client.components.installed
25
+ #
26
+ def installed
27
+ response = get('/rci/show/components')
28
+ normalize_components(response)
29
+ end
30
+
31
+ # Get available components for installation.
32
+ #
33
+ # @return [Array<Hash>] List of available components
34
+ # @example
35
+ # available = client.components.available
36
+ #
37
+ def available
38
+ response = get('/rci/show/components/available')
39
+ normalize_components(response)
40
+ end
41
+
42
+ # Install a component.
43
+ #
44
+ # @param name [String] Component name
45
+ # @return [Hash, nil] API response
46
+ # @example
47
+ # client.components.install(name: 'transmission')
48
+ #
49
+ def install(name:)
50
+ post('/rci/components/install', { 'name' => name })
51
+ end
52
+
53
+ # Remove a component.
54
+ #
55
+ # @param name [String] Component name
56
+ # @return [Hash, nil] API response
57
+ # @example
58
+ # client.components.remove(name: 'transmission')
59
+ #
60
+ def remove(name:)
61
+ post('/rci/components/remove', { 'name' => name })
62
+ end
63
+
64
+ private
65
+
66
+ def normalize_components(response)
67
+ components_data = case response
68
+ when Array
69
+ response
70
+ when Hash
71
+ response['component'] || response['components'] || []
72
+ else
73
+ []
74
+ end
75
+
76
+ return [] unless components_data.is_a?(Array)
77
+
78
+ components_data.map { |component| normalize_component(component) }.compact
79
+ end
80
+
81
+ def normalize_component(data)
82
+ return nil unless data.is_a?(Hash)
83
+
84
+ deep_normalize_keys(data)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,89 @@
1
+ module Keenetic
2
+ module Resources
3
+ # Manages router configuration operations.
4
+ #
5
+ # == API Endpoints Used
6
+ #
7
+ # === Save Configuration
8
+ # POST /rci/ (batch format)
9
+ # Body: [{"system": {"configuration": {"save": {}}}}]
10
+ # Saves current configuration to persistent storage
11
+ #
12
+ # === Download Configuration
13
+ # GET /ci/startup-config.txt
14
+ # Returns: Plain text configuration file
15
+ #
16
+ # === Upload Configuration
17
+ # POST /ci/startup-config.txt
18
+ # Body: Plain text configuration file
19
+ # Restores configuration from backup
20
+ #
21
+ class Config < Base
22
+ # Save current configuration to persistent storage.
23
+ #
24
+ # == Keenetic API Request
25
+ # POST /rci/ (batch format)
26
+ # Body: [{"system": {"configuration": {"save": {}}}}]
27
+ #
28
+ # Configuration changes are typically auto-saved, but this method
29
+ # forces an immediate save to flash storage.
30
+ #
31
+ # @return [Array<Hash>] API response
32
+ # @example
33
+ # client.config.save
34
+ # # => [{ "system" => { "configuration" => { "save" => {} } } }]
35
+ #
36
+ def save
37
+ client.batch([{ 'system' => { 'configuration' => { 'save' => {} } } }])
38
+ end
39
+
40
+ # Download the startup configuration file.
41
+ #
42
+ # == Keenetic API Request
43
+ # GET /ci/startup-config.txt
44
+ #
45
+ # Returns the full router configuration as a text file.
46
+ # This is the same format used for backup/restore operations.
47
+ #
48
+ # @return [String] Configuration file content
49
+ # @example
50
+ # config_text = client.system_config.download
51
+ # File.write('router-backup.txt', config_text)
52
+ #
53
+ def download
54
+ get('/ci/startup-config.txt')
55
+ end
56
+
57
+ # Upload and restore a configuration file.
58
+ #
59
+ # == Keenetic API Request
60
+ # POST /ci/startup-config.txt
61
+ # Content-Type: text/plain
62
+ # Body: Configuration file content
63
+ #
64
+ # Uploads a configuration file to the router. The configuration
65
+ # will be applied after a reboot.
66
+ #
67
+ # == Warning
68
+ # This is a potentially destructive operation. Uploading an
69
+ # invalid or incompatible configuration may make the router
70
+ # inaccessible. Always ensure you have physical access to the
71
+ # router before restoring configuration.
72
+ #
73
+ # @param content [String] Configuration file content
74
+ # @return [String, nil] Response from the router
75
+ # @example Upload from string
76
+ # client.system_config.upload(config_text)
77
+ #
78
+ # @example Upload from file
79
+ # config_text = File.read('router-backup.txt')
80
+ # client.system_config.upload(config_text)
81
+ #
82
+ def upload(content)
83
+ raise ArgumentError, 'Configuration content cannot be empty' if content.nil? || content.strip.empty?
84
+
85
+ post_raw('/ci/startup-config.txt', content)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,102 @@
1
+ module Keenetic
2
+ module Resources
3
+ # Diagnostics resource for network troubleshooting tools.
4
+ #
5
+ # == API Endpoints Used
6
+ #
7
+ # === Ping
8
+ # POST /rci/tools/ping
9
+ # Body: { host, count }
10
+ #
11
+ # === Traceroute
12
+ # POST /rci/tools/traceroute
13
+ # Body: { host }
14
+ #
15
+ # === DNS Lookup
16
+ # POST /rci/tools/nslookup
17
+ # Body: { host }
18
+ #
19
+ class Diagnostics < Base
20
+ # Ping a host from the router.
21
+ #
22
+ # == Keenetic API Request
23
+ # POST /rci/tools/ping
24
+ # Body: { "host": "8.8.8.8", "count": 4 }
25
+ #
26
+ # @param host [String] Hostname or IP address to ping
27
+ # @param count [Integer] Number of ping packets (default: 4)
28
+ # @return [Hash] Ping results
29
+ #
30
+ # @example Ping Google DNS
31
+ # result = client.diagnostics.ping('8.8.8.8')
32
+ # # => { host: "8.8.8.8", transmitted: 4, received: 4, loss: 0, ... }
33
+ #
34
+ # @example Ping with custom count
35
+ # result = client.diagnostics.ping('google.com', count: 10)
36
+ #
37
+ def ping(host, count: 4)
38
+ response = post('/rci/tools/ping', { 'host' => host, 'count' => count })
39
+ normalize_ping_result(response)
40
+ end
41
+
42
+ # Trace route to a host from the router.
43
+ #
44
+ # == Keenetic API Request
45
+ # POST /rci/tools/traceroute
46
+ # Body: { "host": "google.com" }
47
+ #
48
+ # @param host [String] Hostname or IP address to trace
49
+ # @return [Hash] Traceroute results with hops
50
+ #
51
+ # @example Trace route to Google
52
+ # result = client.diagnostics.traceroute('google.com')
53
+ # # => { host: "google.com", hops: [{ hop: 1, ip: "192.168.1.1", time: 1 }, ...] }
54
+ #
55
+ def traceroute(host)
56
+ response = post('/rci/tools/traceroute', { 'host' => host })
57
+ normalize_traceroute_result(response)
58
+ end
59
+
60
+ # Perform DNS lookup from the router.
61
+ #
62
+ # == Keenetic API Request
63
+ # POST /rci/tools/nslookup
64
+ # Body: { "host": "google.com" }
65
+ #
66
+ # @param host [String] Hostname to resolve
67
+ # @return [Hash] DNS lookup results
68
+ #
69
+ # @example Lookup Google
70
+ # result = client.diagnostics.nslookup('google.com')
71
+ # # => { host: "google.com", addresses: ["142.250.185.46", ...], ... }
72
+ #
73
+ def nslookup(host)
74
+ response = post('/rci/tools/nslookup', { 'host' => host })
75
+ normalize_nslookup_result(response)
76
+ end
77
+
78
+ # Alias for nslookup
79
+ alias dns_lookup nslookup
80
+
81
+ private
82
+
83
+ def normalize_ping_result(response)
84
+ return {} unless response.is_a?(Hash)
85
+
86
+ deep_normalize_keys(response)
87
+ end
88
+
89
+ def normalize_traceroute_result(response)
90
+ return {} unless response.is_a?(Hash)
91
+
92
+ deep_normalize_keys(response)
93
+ end
94
+
95
+ def normalize_nslookup_result(response)
96
+ return {} unless response.is_a?(Hash)
97
+
98
+ deep_normalize_keys(response)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,95 @@
1
+ module Keenetic
2
+ module Resources
3
+ # DNS resource for managing DNS settings and cache.
4
+ #
5
+ # == API Endpoints Used
6
+ #
7
+ # === Reading DNS Servers
8
+ # GET /rci/show/ip/name-server
9
+ # Returns: Configured DNS servers
10
+ #
11
+ # === Reading DNS Cache
12
+ # GET /rci/show/dns/cache
13
+ # Returns: DNS cache entries
14
+ #
15
+ # === Reading DNS Proxy Settings
16
+ # GET /rci/show/dns/proxy
17
+ # Returns: DNS proxy configuration
18
+ #
19
+ # === Clear DNS Cache
20
+ # POST /rci/dns/cache/clear
21
+ # Body: {}
22
+ #
23
+ class Dns < Base
24
+ # Get configured DNS servers.
25
+ #
26
+ # == Keenetic API Request
27
+ # GET /rci/show/ip/name-server
28
+ #
29
+ # @return [Hash] DNS servers configuration
30
+ # @example
31
+ # servers = client.dns.servers
32
+ #
33
+ def servers
34
+ response = get('/rci/show/ip/name-server')
35
+ normalize_response(response)
36
+ end
37
+
38
+ # Alias for servers
39
+ alias name_servers servers
40
+
41
+ # Get DNS cache entries.
42
+ #
43
+ # == Keenetic API Request
44
+ # GET /rci/show/dns/cache
45
+ #
46
+ # @return [Hash] DNS cache data
47
+ # @example
48
+ # cache = client.dns.cache
49
+ #
50
+ def cache
51
+ response = get('/rci/show/dns/cache')
52
+ normalize_response(response)
53
+ end
54
+
55
+ # Get DNS proxy settings.
56
+ #
57
+ # == Keenetic API Request
58
+ # GET /rci/show/dns/proxy
59
+ #
60
+ # @return [Hash] DNS proxy configuration
61
+ # @example
62
+ # proxy = client.dns.proxy
63
+ #
64
+ def proxy
65
+ response = get('/rci/show/dns/proxy')
66
+ normalize_response(response)
67
+ end
68
+
69
+ # Alias for proxy
70
+ alias proxy_settings proxy
71
+
72
+ # Clear DNS cache.
73
+ #
74
+ # == Keenetic API Request
75
+ # POST /rci/dns/cache/clear
76
+ # Body: {}
77
+ #
78
+ # @return [Hash, nil] API response
79
+ # @example
80
+ # client.dns.clear_cache
81
+ #
82
+ def clear_cache
83
+ post('/rci/dns/cache/clear', {})
84
+ end
85
+
86
+ private
87
+
88
+ def normalize_response(response)
89
+ return {} unless response.is_a?(Hash)
90
+
91
+ deep_normalize_keys(response)
92
+ end
93
+ end
94
+ end
95
+ end