keenetic 1.0.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 +4 -4
- data/README.md +13 -1
- data/lib/keenetic/client.rb +27 -5
- data/lib/keenetic/resources/dns_routes.rb +298 -0
- data/lib/keenetic/version.rb +1 -1
- data/lib/keenetic.rb +1 -0
- metadata +4 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55da30f5dcac15dad86d5d93cd6dd35cd8d9f63ee2dfb63f73a8e8b32d66a4bb
|
|
4
|
+
data.tar.gz: 7be5b4c51ca2b764c7bc2cc68e1bb69402377d3c19106f0e288a15103847bbcd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 655fd847b64bec33b24534d23d8f7459cad8cede50af9e78b5289f615612e334b8bf53e74990569d52e3ee6276ac49e01e83894b35c4fd64c8a8418668fd17da
|
|
7
|
+
data.tar.gz: 14e3d21ee47872269d4a13bb0794e06d9926eb111e919788122cc76f6d2a6f7cb9af225c39cb9e397883c1194b3e32faa53e7fc58f8619f2bf2314c2052c0f13
|
data/README.md
CHANGED
|
@@ -38,9 +38,21 @@ client.network.interfaces # all interfaces
|
|
|
38
38
|
client.wifi.access_points # Wi-Fi networks
|
|
39
39
|
client.internet.status # internet connectivity
|
|
40
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
|
+
|
|
41
53
|
# Port forwarding
|
|
42
54
|
client.nat.rules # NAT rules
|
|
43
|
-
client.nat.add_forward(index: 1, protocol: 'tcp', port: 8080,
|
|
55
|
+
client.nat.add_forward(index: 1, protocol: 'tcp', port: 8080,
|
|
44
56
|
to_host: '192.168.1.100', to_port: 80)
|
|
45
57
|
|
|
46
58
|
# VPN
|
data/lib/keenetic/client.rb
CHANGED
|
@@ -182,6 +182,11 @@ module Keenetic
|
|
|
182
182
|
@ipv6 ||= Resources::Ipv6.new(self)
|
|
183
183
|
end
|
|
184
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
|
+
|
|
185
190
|
# Execute arbitrary RCI command(s).
|
|
186
191
|
#
|
|
187
192
|
# Provides raw access to the Keenetic RCI (Remote Command Interface).
|
|
@@ -318,6 +323,25 @@ module Keenetic
|
|
|
318
323
|
def request(method, path, options = {})
|
|
319
324
|
authenticate! unless @authenticated || path == '/auth'
|
|
320
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)
|
|
321
345
|
url = "#{config.base_url}#{path}"
|
|
322
346
|
|
|
323
347
|
request_options = {
|
|
@@ -342,9 +366,7 @@ module Keenetic
|
|
|
342
366
|
|
|
343
367
|
config.logger.debug { "Keenetic: #{method.upcase} #{url}" }
|
|
344
368
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
handle_response(response)
|
|
369
|
+
Typhoeus::Request.new(url, request_options).run
|
|
348
370
|
end
|
|
349
371
|
|
|
350
372
|
def build_headers
|
|
@@ -378,7 +400,7 @@ module Keenetic
|
|
|
378
400
|
end
|
|
379
401
|
end
|
|
380
402
|
|
|
381
|
-
def handle_response(response)
|
|
403
|
+
def handle_response(response, allow_401: false)
|
|
382
404
|
parse_cookies(response)
|
|
383
405
|
|
|
384
406
|
if response.timed_out?
|
|
@@ -389,7 +411,7 @@ module Keenetic
|
|
|
389
411
|
raise ConnectionError, "Connection failed: #{response.return_message}"
|
|
390
412
|
end
|
|
391
413
|
|
|
392
|
-
unless response.success? || response.code == 401
|
|
414
|
+
unless response.success? || (allow_401 && response.code == 401)
|
|
393
415
|
if response.code == 404
|
|
394
416
|
raise NotFoundError, "Resource not found"
|
|
395
417
|
end
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# DNS-based routes resource for managing FQDN domain groups and their routing rules.
|
|
4
|
+
#
|
|
5
|
+
# DNS-based routing lets you route traffic for a set of domain names through a
|
|
6
|
+
# specific interface. The router resolves each domain and automatically installs
|
|
7
|
+
# floating static routes for the resolved IPs.
|
|
8
|
+
#
|
|
9
|
+
# Two related concepts are managed here:
|
|
10
|
+
#
|
|
11
|
+
# 1. **FQDN Domain Groups** (`object-group fqdn`) — named lists of domains.
|
|
12
|
+
# 2. **DNS-Proxy Routes** (`dns-proxy route`) — maps a domain group to an interface.
|
|
13
|
+
#
|
|
14
|
+
# == API Endpoints Used
|
|
15
|
+
#
|
|
16
|
+
# === Reading FQDN Domain Groups
|
|
17
|
+
# POST /rci/ (batch)
|
|
18
|
+
# Body: [{"show":{"sc":{"object-group":{"fqdn":{}}}}}]
|
|
19
|
+
# Returns: Hash keyed by group name
|
|
20
|
+
#
|
|
21
|
+
# === Creating FQDN Domain Group
|
|
22
|
+
# POST /rci/ (batch)
|
|
23
|
+
# Body: [
|
|
24
|
+
# webhelp_event,
|
|
25
|
+
# {"object-group":{"fqdn":{"<name>":{"description":"...","include":[{"address":"..."}]}}}},
|
|
26
|
+
# save_config
|
|
27
|
+
# ]
|
|
28
|
+
#
|
|
29
|
+
# === Deleting FQDN Domain Group
|
|
30
|
+
# POST /rci/ (batch)
|
|
31
|
+
# Body: [
|
|
32
|
+
# webhelp_event,
|
|
33
|
+
# {"object-group":{"fqdn":{"<name>":{"no":true}}}},
|
|
34
|
+
# save_config
|
|
35
|
+
# ]
|
|
36
|
+
#
|
|
37
|
+
# === Reading DNS-Based Routes
|
|
38
|
+
# POST /rci/ (batch)
|
|
39
|
+
# Body: [{"show":{"sc":{"dns-proxy":{"route":{}}}}}]
|
|
40
|
+
# Returns: Array of route entries
|
|
41
|
+
#
|
|
42
|
+
# === Creating DNS-Based Route
|
|
43
|
+
# POST /rci/ (batch)
|
|
44
|
+
# Body: [
|
|
45
|
+
# webhelp_event,
|
|
46
|
+
# {"dns-proxy":{"route":{"group":"...","interface":"...","comment":"..."}}},
|
|
47
|
+
# save_config
|
|
48
|
+
# ]
|
|
49
|
+
#
|
|
50
|
+
# === Deleting DNS-Based Route
|
|
51
|
+
# POST /rci/ (batch)
|
|
52
|
+
# Body: [
|
|
53
|
+
# webhelp_event,
|
|
54
|
+
# {"dns-proxy":{"route":{"no":true,"index":"..."}}},
|
|
55
|
+
# save_config
|
|
56
|
+
# ]
|
|
57
|
+
#
|
|
58
|
+
class DnsRoutes < Base
|
|
59
|
+
# Get all FQDN domain groups.
|
|
60
|
+
#
|
|
61
|
+
# == Keenetic API Request
|
|
62
|
+
# POST /rci/ (batch)
|
|
63
|
+
# Body: [{"show":{"sc":{"object-group":{"fqdn":{}}}}}]
|
|
64
|
+
#
|
|
65
|
+
# == Response Structure from API
|
|
66
|
+
# {
|
|
67
|
+
# "domain-list0": {
|
|
68
|
+
# "description": "youtube.com",
|
|
69
|
+
# "include": [{"address": "googlevideo.com"}, {"address": "youtube.com"}]
|
|
70
|
+
# }
|
|
71
|
+
# }
|
|
72
|
+
#
|
|
73
|
+
# @return [Array<Hash>] List of domain groups, each with :name, :description, :domains
|
|
74
|
+
# @example
|
|
75
|
+
# groups = client.dns_routes.domain_groups
|
|
76
|
+
# # => [{ name: "domain-list0", description: "youtube.com", domains: ["googlevideo.com", "youtube.com"] }]
|
|
77
|
+
#
|
|
78
|
+
def domain_groups
|
|
79
|
+
response = client.batch([{ 'show' => { 'sc' => { 'object-group' => { 'fqdn' => {} } } } }])
|
|
80
|
+
fqdn_data = response.dig(0, 'show', 'sc', 'object-group', 'fqdn') || {}
|
|
81
|
+
normalize_domain_groups(fqdn_data)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Find a single FQDN domain group by name.
|
|
85
|
+
#
|
|
86
|
+
# @param name [String] Group name (e.g., "domain-list0")
|
|
87
|
+
# @return [Hash, nil] Domain group or nil if not found
|
|
88
|
+
# @example
|
|
89
|
+
# group = client.dns_routes.find_domain_group(name: "domain-list0")
|
|
90
|
+
# # => { name: "domain-list0", description: "youtube.com", domains: [...] }
|
|
91
|
+
#
|
|
92
|
+
def find_domain_group(name:)
|
|
93
|
+
domain_groups.find { |g| g[:name] == name }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Create an FQDN domain group.
|
|
97
|
+
#
|
|
98
|
+
# == Keenetic API Request
|
|
99
|
+
# POST /rci/ (batch)
|
|
100
|
+
# Body: [webhelp_event, {"object-group":{"fqdn":{"<name>":{"description":"...","include":[...]}}}}, save]
|
|
101
|
+
#
|
|
102
|
+
# @param name [String] Group identifier (e.g., "domain-list0")
|
|
103
|
+
# @param description [String] Human-readable label
|
|
104
|
+
# @param domains [Array<String>] List of domain names to include
|
|
105
|
+
# @return [Array<Hash>] API response
|
|
106
|
+
# @raise [ArgumentError] if name, description, or domains are missing/empty
|
|
107
|
+
# @example
|
|
108
|
+
# client.dns_routes.create_domain_group(
|
|
109
|
+
# name: "domain-list0",
|
|
110
|
+
# description: "YouTube",
|
|
111
|
+
# domains: ["youtube.com", "googlevideo.com"]
|
|
112
|
+
# )
|
|
113
|
+
#
|
|
114
|
+
def create_domain_group(name:, description:, domains:)
|
|
115
|
+
raise ArgumentError, 'Name is required' if name.nil? || name.to_s.strip.empty?
|
|
116
|
+
raise ArgumentError, 'Description is required' if description.nil? || description.to_s.strip.empty?
|
|
117
|
+
raise ArgumentError, 'Domains cannot be empty' if domains.nil? || domains.empty?
|
|
118
|
+
|
|
119
|
+
include_list = domains.map { |d| { 'address' => d.to_s } }
|
|
120
|
+
|
|
121
|
+
commands = [
|
|
122
|
+
webhelp_event,
|
|
123
|
+
{ 'object-group' => { 'fqdn' => { name.to_s => { 'description' => description.to_s, 'include' => include_list } } } },
|
|
124
|
+
save_config_command
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
client.batch(commands)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Delete an FQDN domain group.
|
|
131
|
+
#
|
|
132
|
+
# == Keenetic API Request
|
|
133
|
+
# POST /rci/ (batch)
|
|
134
|
+
# Body: [webhelp_event, {"object-group":{"fqdn":{"<name>":{"no":true}}}}, save]
|
|
135
|
+
#
|
|
136
|
+
# @param name [String] Group name to delete
|
|
137
|
+
# @return [Array<Hash>] API response
|
|
138
|
+
# @raise [ArgumentError] if name is missing
|
|
139
|
+
# @example
|
|
140
|
+
# client.dns_routes.delete_domain_group(name: "domain-list0")
|
|
141
|
+
#
|
|
142
|
+
def delete_domain_group(name:)
|
|
143
|
+
raise ArgumentError, 'Name is required' if name.nil? || name.to_s.strip.empty?
|
|
144
|
+
|
|
145
|
+
commands = [
|
|
146
|
+
webhelp_event,
|
|
147
|
+
{ 'object-group' => { 'fqdn' => { name.to_s => { 'no' => true } } } },
|
|
148
|
+
save_config_command
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
client.batch(commands)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Get all DNS-based routes.
|
|
155
|
+
#
|
|
156
|
+
# == Keenetic API Request
|
|
157
|
+
# POST /rci/ (batch)
|
|
158
|
+
# Body: [{"show":{"sc":{"dns-proxy":{"route":{}}}}}]
|
|
159
|
+
#
|
|
160
|
+
# == Response Structure from API
|
|
161
|
+
# [
|
|
162
|
+
# {
|
|
163
|
+
# "group": "domain-list0",
|
|
164
|
+
# "interface": "Wireguard2",
|
|
165
|
+
# "auto": true,
|
|
166
|
+
# "index": "c52bba355a2830fdf55ccb3748a879df",
|
|
167
|
+
# "comment": ""
|
|
168
|
+
# }
|
|
169
|
+
# ]
|
|
170
|
+
#
|
|
171
|
+
# @return [Array<Hash>] List of DNS-based routes with :group, :interface, :auto, :index, :comment
|
|
172
|
+
# @example
|
|
173
|
+
# routes = client.dns_routes.routes
|
|
174
|
+
# # => [{ group: "domain-list0", interface: "Wireguard2", auto: true, index: "c52b...", comment: "" }]
|
|
175
|
+
#
|
|
176
|
+
def routes
|
|
177
|
+
response = client.batch([{ 'show' => { 'sc' => { 'dns-proxy' => { 'route' => {} } } } }])
|
|
178
|
+
routes_data = response.dig(0, 'show', 'sc', 'dns-proxy', 'route') || []
|
|
179
|
+
normalize_routes(routes_data)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Find a DNS-based route by FQDN group name.
|
|
183
|
+
#
|
|
184
|
+
# @param group [String] FQDN group name
|
|
185
|
+
# @return [Hash, nil] Route or nil if not found
|
|
186
|
+
# @example
|
|
187
|
+
# route = client.dns_routes.find_route(group: "domain-list0")
|
|
188
|
+
# # => { group: "domain-list0", interface: "Wireguard2", index: "...", ... }
|
|
189
|
+
#
|
|
190
|
+
def find_route(group:)
|
|
191
|
+
routes.find { |r| r[:group] == group }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Create a DNS-based route mapping a domain group to an interface.
|
|
195
|
+
#
|
|
196
|
+
# == Keenetic API Request
|
|
197
|
+
# POST /rci/ (batch)
|
|
198
|
+
# Body: [webhelp_event, {"dns-proxy":{"route":{"group":"...","interface":"...","comment":"..."}}}, save]
|
|
199
|
+
#
|
|
200
|
+
# @param group [String] FQDN group name (e.g., "domain-list0")
|
|
201
|
+
# @param interface [String] Target interface (e.g., "Wireguard0")
|
|
202
|
+
# @param comment [String] Optional description (default: "")
|
|
203
|
+
# @return [Array<Hash>] API response
|
|
204
|
+
# @raise [ArgumentError] if group or interface is missing
|
|
205
|
+
# @example
|
|
206
|
+
# client.dns_routes.add_route(group: "domain-list0", interface: "Wireguard0")
|
|
207
|
+
#
|
|
208
|
+
def add_route(group:, interface:, comment: '')
|
|
209
|
+
raise ArgumentError, 'Group is required' if group.nil? || group.to_s.strip.empty?
|
|
210
|
+
raise ArgumentError, 'Interface is required' if interface.nil? || interface.to_s.strip.empty?
|
|
211
|
+
|
|
212
|
+
commands = [
|
|
213
|
+
webhelp_event,
|
|
214
|
+
{ 'dns-proxy' => { 'route' => { 'group' => group.to_s, 'interface' => interface.to_s, 'comment' => comment.to_s } } },
|
|
215
|
+
save_config_command
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
client.batch(commands)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Delete a DNS-based route by its index.
|
|
222
|
+
#
|
|
223
|
+
# == Keenetic API Request
|
|
224
|
+
# POST /rci/ (batch)
|
|
225
|
+
# Body: [webhelp_event, {"dns-proxy":{"route":{"no":true,"index":"..."}}}, save]
|
|
226
|
+
#
|
|
227
|
+
# @param index [String] Route index (MD5 hash from routes list)
|
|
228
|
+
# @return [Array<Hash>] API response
|
|
229
|
+
# @raise [ArgumentError] if index is missing
|
|
230
|
+
# @example
|
|
231
|
+
# client.dns_routes.delete_route(index: "c52bba355a2830fdf55ccb3748a879df")
|
|
232
|
+
#
|
|
233
|
+
def delete_route(index:)
|
|
234
|
+
raise ArgumentError, 'Index is required' if index.nil? || index.to_s.strip.empty?
|
|
235
|
+
|
|
236
|
+
commands = [
|
|
237
|
+
webhelp_event,
|
|
238
|
+
{ 'dns-proxy' => { 'route' => { 'no' => true, 'index' => index.to_s } } },
|
|
239
|
+
save_config_command
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
client.batch(commands)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private
|
|
246
|
+
|
|
247
|
+
def normalize_domain_groups(fqdn_data)
|
|
248
|
+
return [] unless fqdn_data.is_a?(Hash)
|
|
249
|
+
|
|
250
|
+
fqdn_data.map do |name, data|
|
|
251
|
+
next nil unless data.is_a?(Hash)
|
|
252
|
+
|
|
253
|
+
domains = Array(data['include']).map { |entry| entry['address'] }.compact
|
|
254
|
+
|
|
255
|
+
{
|
|
256
|
+
name: name,
|
|
257
|
+
description: data['description'],
|
|
258
|
+
domains: domains
|
|
259
|
+
}
|
|
260
|
+
end.compact
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def normalize_routes(routes_data)
|
|
264
|
+
return [] unless routes_data.is_a?(Array)
|
|
265
|
+
|
|
266
|
+
routes_data.map { |r| normalize_route(r) }.compact
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def normalize_route(data)
|
|
270
|
+
return nil unless data.is_a?(Hash)
|
|
271
|
+
|
|
272
|
+
{
|
|
273
|
+
group: data['group'],
|
|
274
|
+
interface: data['interface'],
|
|
275
|
+
auto: normalize_boolean(data['auto']),
|
|
276
|
+
index: data['index'],
|
|
277
|
+
comment: data['comment']
|
|
278
|
+
}
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def webhelp_event
|
|
282
|
+
{
|
|
283
|
+
'webhelp' => {
|
|
284
|
+
'event' => {
|
|
285
|
+
'push' => {
|
|
286
|
+
'data' => '{"type":"configuration_change","value":{"url":"/staticRoutes/dns"}}'
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def save_config_command
|
|
294
|
+
{ 'system' => { 'configuration' => { 'save' => {} } } }
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
data/lib/keenetic/version.rb
CHANGED
data/lib/keenetic.rb
CHANGED
|
@@ -29,6 +29,7 @@ require_relative 'keenetic/resources/users'
|
|
|
29
29
|
require_relative 'keenetic/resources/components'
|
|
30
30
|
require_relative 'keenetic/resources/qos'
|
|
31
31
|
require_relative 'keenetic/resources/ipv6'
|
|
32
|
+
require_relative 'keenetic/resources/dns_routes'
|
|
32
33
|
|
|
33
34
|
# Keenetic Router API Client
|
|
34
35
|
#
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keenetic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anton Zaytsev
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
10
|
+
date: 2026-03-27 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: logger
|
|
@@ -40,7 +39,6 @@ dependencies:
|
|
|
40
39
|
version: '1.4'
|
|
41
40
|
description: A Ruby client for interacting with Keenetic router REST API. Supports
|
|
42
41
|
authentication, device management, system monitoring, and network interfaces.
|
|
43
|
-
email:
|
|
44
42
|
executables: []
|
|
45
43
|
extensions: []
|
|
46
44
|
extra_rdoc_files: []
|
|
@@ -58,6 +56,7 @@ files:
|
|
|
58
56
|
- lib/keenetic/resources/dhcp.rb
|
|
59
57
|
- lib/keenetic/resources/diagnostics.rb
|
|
60
58
|
- lib/keenetic/resources/dns.rb
|
|
59
|
+
- lib/keenetic/resources/dns_routes.rb
|
|
61
60
|
- lib/keenetic/resources/dyndns.rb
|
|
62
61
|
- lib/keenetic/resources/firewall.rb
|
|
63
62
|
- lib/keenetic/resources/hotspot.rb
|
|
@@ -84,7 +83,6 @@ licenses:
|
|
|
84
83
|
- MIT
|
|
85
84
|
metadata:
|
|
86
85
|
rubygems_mfa_required: 'true'
|
|
87
|
-
post_install_message:
|
|
88
86
|
rdoc_options: []
|
|
89
87
|
require_paths:
|
|
90
88
|
- lib
|
|
@@ -99,8 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
99
97
|
- !ruby/object:Gem::Version
|
|
100
98
|
version: '0'
|
|
101
99
|
requirements: []
|
|
102
|
-
rubygems_version: 3.
|
|
103
|
-
signing_key:
|
|
100
|
+
rubygems_version: 3.6.2
|
|
104
101
|
specification_version: 4
|
|
105
102
|
summary: Ruby client for Keenetic router API
|
|
106
103
|
test_files: []
|