keenetic 0.2.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fa7552177bec3d537db352ed748bcd24e07bee6156272569a121b7cb18c6a2f
4
- data.tar.gz: e48b1c10d4cc62466ebd5e34b200f399e28e8261e0e1a83a1a4b1ee86efc4ee3
3
+ metadata.gz: 55da30f5dcac15dad86d5d93cd6dd35cd8d9f63ee2dfb63f73a8e8b32d66a4bb
4
+ data.tar.gz: 7be5b4c51ca2b764c7bc2cc68e1bb69402377d3c19106f0e288a15103847bbcd
5
5
  SHA512:
6
- metadata.gz: f801f8df36437d0f2ea76f6765beaeee3a68750b2974b8b3fc4eed9145be5a5c123940245b56566c690ef906d791eaf8223ae3f016b4bb0472c1a2a3476f77df
7
- data.tar.gz: 984b84f49fb83117ab0c9c89b60a0b88718ccbbd92258c69987be8103bec4d2fd66554c4413c001e45167396735c634a578e839c35cce74b0124d2d3c829f930
6
+ metadata.gz: 655fd847b64bec33b24534d23d8f7459cad8cede50af9e78b5289f615612e334b8bf53e74990569d52e3ee6276ac49e01e83894b35c4fd64c8a8418668fd17da
7
+ data.tar.gz: 14e3d21ee47872269d4a13bb0794e06d9926eb111e919788122cc76f6d2a6f7cb9af225c39cb9e397883c1194b3e32faa53e7fc58f8619f2bf2314c2052c0f13
data/README.md CHANGED
@@ -20,62 +20,48 @@ 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
29
+ client.system.info # model, firmware
30
+ client.system.resources # CPU, memory, uptime
31
+
32
+ # Connected devices
33
+ client.devices.all # all registered devices
34
+ client.devices.active # currently connected
37
35
 
38
36
  # Network
39
- client.network.interfaces
40
-
41
- # WiFi
42
- client.wifi.access_points
43
- client.wifi.clients
44
-
45
- # Internet
46
- client.internet.status
47
- client.internet.speed
48
-
49
- # Ports
50
- client.ports.all
51
-
52
- # Static Routes
53
- client.routes.all
54
- client.routes.add(host: '1.2.3.4', interface: 'Wireguard0', comment: 'VPN host')
55
- client.routes.add(network: '10.0.0.0/24', interface: 'Wireguard0', comment: 'VPN network')
56
- client.routes.add_batch([
57
- { host: '1.2.3.4', interface: 'Wireguard0', comment: 'Host 1' },
58
- { network: '10.0.0.0/24', interface: 'Wireguard0', comment: 'Network 1' }
59
- ])
60
- client.routes.delete(host: '1.2.3.4')
61
- client.routes.delete(network: '10.0.0.0/24')
62
-
63
- # Hotspot / Policies
64
- client.hotspot.policies # all IP policies
65
- client.hotspot.hosts # all registered hosts
66
- client.hotspot.set_host_policy(mac: 'AA:BB:CC:DD:EE:FF', policy: 'Policy0')
67
- client.hotspot.set_host_policy(mac: 'AA:BB:CC:DD:EE:FF', policy: nil) # remove policy
37
+ client.network.interfaces # all interfaces
38
+ client.wifi.access_points # Wi-Fi networks
39
+ client.internet.status # internet connectivity
40
+
41
+ # Static routes
42
+ client.routes.all # configured static routes
43
+ client.routes.add(network: '10.0.0.0/24', interface: 'Wireguard0', comment: 'VPN')
44
+
45
+ # DNS-based routes
46
+ client.dns_routes.domain_groups # FQDN domain groups
47
+ client.dns_routes.routes # DNS-based route mappings
48
+ client.dns_routes.create_domain_group(name: 'domain-list0', description: 'YouTube',
49
+ domains: ['youtube.com', 'googlevideo.com'])
50
+ client.dns_routes.add_route(group: 'domain-list0', interface: 'Wireguard0')
51
+ client.dns_routes.delete_route(index: 'abc123...')
52
+
53
+ # Port forwarding
54
+ client.nat.rules # NAT rules
55
+ client.nat.add_forward(index: 1, protocol: 'tcp', port: 8080,
56
+ to_host: '192.168.1.100', to_port: 80)
57
+
58
+ # VPN
59
+ client.vpn.status # VPN server status
60
+ client.vpn.clients # connected VPN clients
68
61
 
69
62
  # Configuration
70
63
  client.system_config.save # save to flash
71
- client.system_config.download # download startup-config.txt
72
-
73
- # Raw RCI Access (for custom commands)
74
- client.rci({ 'show' => { 'system' => {} } })
75
- client.rci([
76
- { 'show' => { 'system' => {} } },
77
- { 'show' => { 'version' => {} } }
78
- ])
64
+ client.system_config.download # backup config
79
65
  ```
80
66
 
81
67
  ## Error Handling
@@ -89,18 +75,18 @@ rescue Keenetic::ConnectionError
89
75
  # router unreachable
90
76
  rescue Keenetic::TimeoutError
91
77
  # request timed out
92
- rescue Keenetic::NotFoundError
93
- # resource not found
94
78
  rescue Keenetic::ApiError => e
95
- # Other API errors
79
+ # other API errors
96
80
  e.status_code
97
81
  e.response_body
98
82
  end
99
83
  ```
100
84
 
101
- ## API Reference
85
+ ## Documentation
102
86
 
103
- See [KEENETIC_API.md](KEENETIC_API.md) for complete API documentation.
87
+ - [Complete API Reference](docs/API_REFERENCE.md) - All features with detailed examples
88
+ - [API Coverage](API_PROGRESS.md) - Implementation status
89
+ - [Keenetic API Specification](KEENETIC_API.md) - Raw API documentation
104
90
 
105
91
  ## Requirements
106
92
 
@@ -117,6 +117,76 @@ module Keenetic
117
117
  @system_config ||= Resources::Config.new(self)
118
118
  end
119
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
+ # @return [Resources::DnsRoutes] DNS-based routes and FQDN domain groups resource
186
+ def dns_routes
187
+ @dns_routes ||= Resources::DnsRoutes.new(self)
188
+ end
189
+
120
190
  # Execute arbitrary RCI command(s).
121
191
  #
122
192
  # Provides raw access to the Keenetic RCI (Remote Command Interface).
@@ -211,6 +281,19 @@ module Keenetic
211
281
  request(:post, '/rci/', body: commands)
212
282
  end
213
283
 
284
+ # Make a POST request with raw body content (non-JSON).
285
+ #
286
+ # Used for file uploads like configuration restore.
287
+ #
288
+ # @param path [String] API path
289
+ # @param content [String] Raw content to send
290
+ # @param content_type [String] Content-Type header (default: text/plain)
291
+ # @return [String, nil] Response body
292
+ #
293
+ def post_raw(path, content, content_type: 'text/plain')
294
+ request(:post, path, raw_body: content, content_type: content_type)
295
+ end
296
+
214
297
  # Check if client is authenticated.
215
298
  # @return [Boolean]
216
299
  def authenticated?
@@ -240,6 +323,25 @@ module Keenetic
240
323
  def request(method, path, options = {})
241
324
  authenticate! unless @authenticated || path == '/auth'
242
325
 
326
+ response = execute_request(method, path, options)
327
+
328
+ # Handle 401 by re-authenticating and retrying once
329
+ if response.code == 401 && path != '/auth'
330
+ config.logger.debug { "Keenetic: Got 401, re-authenticating..." }
331
+ @authenticated = false
332
+ @mutex.synchronize { perform_authentication }
333
+
334
+ response = execute_request(method, path, options)
335
+
336
+ if response.code == 401
337
+ raise AuthenticationError, "Request unauthorized after re-authentication (HTTP 401)"
338
+ end
339
+ end
340
+
341
+ handle_response(response)
342
+ end
343
+
344
+ def execute_request(method, path, options)
243
345
  url = "#{config.base_url}#{path}"
244
346
 
245
347
  request_options = {
@@ -254,16 +356,17 @@ module Keenetic
254
356
  url += "?#{URI.encode_www_form(options[:params])}"
255
357
  end
256
358
 
257
- if options[:body]
359
+ if options[:raw_body]
360
+ request_options[:body] = options[:raw_body]
361
+ request_options[:headers]['Content-Type'] = options[:content_type] || 'text/plain'
362
+ elsif options[:body]
258
363
  request_options[:body] = options[:body].to_json
259
364
  request_options[:headers]['Content-Type'] = 'application/json'
260
365
  end
261
366
 
262
367
  config.logger.debug { "Keenetic: #{method.upcase} #{url}" }
263
368
 
264
- response = Typhoeus::Request.new(url, request_options).run
265
-
266
- handle_response(response)
369
+ Typhoeus::Request.new(url, request_options).run
267
370
  end
268
371
 
269
372
  def build_headers
@@ -297,7 +400,7 @@ module Keenetic
297
400
  end
298
401
  end
299
402
 
300
- def handle_response(response)
403
+ def handle_response(response, allow_401: false)
301
404
  parse_cookies(response)
302
405
 
303
406
  if response.timed_out?
@@ -308,7 +411,7 @@ module Keenetic
308
411
  raise ConnectionError, "Connection failed: #{response.return_message}"
309
412
  end
310
413
 
311
- unless response.success? || response.code == 401
414
+ unless response.success? || (allow_401 && response.code == 401)
312
415
  if response.code == 404
313
416
  raise NotFoundError, "Resource not found"
314
417
  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
@@ -13,6 +13,11 @@ module Keenetic
13
13
  # GET /ci/startup-config.txt
14
14
  # Returns: Plain text configuration file
15
15
  #
16
+ # === Upload Configuration
17
+ # POST /ci/startup-config.txt
18
+ # Body: Plain text configuration file
19
+ # Restores configuration from backup
20
+ #
16
21
  class Config < Base
17
22
  # Save current configuration to persistent storage.
18
23
  #
@@ -42,12 +47,43 @@ module Keenetic
42
47
  #
43
48
  # @return [String] Configuration file content
44
49
  # @example
45
- # config_text = client.config.download
50
+ # config_text = client.system_config.download
46
51
  # File.write('router-backup.txt', config_text)
47
52
  #
48
53
  def download
49
54
  get('/ci/startup-config.txt')
50
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
51
87
  end
52
88
  end
53
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