keenetic 0.2.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.
@@ -0,0 +1,103 @@
1
+ module Keenetic
2
+ module Resources
3
+ # Firewall resource for managing firewall policies and access lists.
4
+ #
5
+ # == API Endpoints Used
6
+ #
7
+ # === Reading Firewall Policies
8
+ # GET /rci/show/ip/policy
9
+ # Returns: Firewall policy configuration
10
+ #
11
+ # === Reading Access Lists
12
+ # GET /rci/show/access-list
13
+ # Returns: Access control lists
14
+ #
15
+ # === Adding Firewall Rule
16
+ # POST /rci/ip/policy
17
+ # Body: Rule configuration
18
+ #
19
+ class Firewall < Base
20
+ # Get firewall policies.
21
+ #
22
+ # == Keenetic API Request
23
+ # GET /rci/show/ip/policy
24
+ #
25
+ # @return [Hash] Firewall policy configuration
26
+ # @example
27
+ # policies = client.firewall.policies
28
+ #
29
+ def policies
30
+ response = get('/rci/show/ip/policy')
31
+ normalize_response(response)
32
+ end
33
+
34
+ # Get access control lists.
35
+ #
36
+ # == Keenetic API Request
37
+ # GET /rci/show/access-list
38
+ #
39
+ # @return [Hash] Access lists configuration
40
+ # @example
41
+ # lists = client.firewall.access_lists
42
+ #
43
+ def access_lists
44
+ response = get('/rci/show/access-list')
45
+ normalize_response(response)
46
+ end
47
+
48
+ # Add a firewall rule.
49
+ #
50
+ # == Keenetic API Request
51
+ # POST /rci/ip/policy
52
+ #
53
+ # @param params [Hash] Rule configuration
54
+ # @return [Hash, nil] API response
55
+ #
56
+ # @example Add a basic rule
57
+ # client.firewall.add_rule(
58
+ # action: 'permit',
59
+ # protocol: 'tcp',
60
+ # src: '192.168.1.0/24',
61
+ # dst_port: 80
62
+ # )
63
+ #
64
+ def add_rule(**params)
65
+ post('/rci/ip/policy', normalize_params(params))
66
+ end
67
+
68
+ # Delete a firewall rule.
69
+ #
70
+ # == Keenetic API Request
71
+ # POST /rci/ip/policy
72
+ # Body: { index, no: true }
73
+ #
74
+ # @param index [Integer] Rule index to delete
75
+ # @return [Hash, nil] API response
76
+ #
77
+ # @example
78
+ # client.firewall.delete_rule(index: 1)
79
+ #
80
+ def delete_rule(index:)
81
+ post('/rci/ip/policy', { 'index' => index, 'no' => true })
82
+ end
83
+
84
+ private
85
+
86
+ def normalize_response(response)
87
+ return {} unless response.is_a?(Hash)
88
+
89
+ deep_normalize_keys(response)
90
+ end
91
+
92
+ def normalize_params(params)
93
+ result = {}
94
+ params.each do |key, value|
95
+ # Convert snake_case to kebab-case for API
96
+ api_key = key.to_s.tr('_', '-')
97
+ result[api_key] = value
98
+ end
99
+ result
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,74 @@
1
+ module Keenetic
2
+ module Resources
3
+ # IPv6 resource for IPv6 network information.
4
+ #
5
+ # == API Endpoints Used
6
+ #
7
+ # === IPv6 Interfaces
8
+ # GET /rci/show/ipv6/interface
9
+ #
10
+ # === IPv6 Routes
11
+ # GET /rci/show/ipv6/route
12
+ #
13
+ # === IPv6 Neighbors
14
+ # GET /rci/show/ipv6/neighbor
15
+ #
16
+ class Ipv6 < Base
17
+ # Get IPv6 interfaces.
18
+ #
19
+ # @return [Array<Hash>] List of IPv6 interfaces
20
+ # @example
21
+ # interfaces = client.ipv6.interfaces
22
+ #
23
+ def interfaces
24
+ response = get('/rci/show/ipv6/interface')
25
+ normalize_list(response, 'interface')
26
+ end
27
+
28
+ # Get IPv6 routing table.
29
+ #
30
+ # @return [Array<Hash>] List of IPv6 routes
31
+ # @example
32
+ # routes = client.ipv6.routes
33
+ #
34
+ def routes
35
+ response = get('/rci/show/ipv6/route')
36
+ normalize_list(response, 'route')
37
+ end
38
+
39
+ # Get IPv6 neighbor table (NDP cache).
40
+ #
41
+ # @return [Array<Hash>] List of IPv6 neighbors
42
+ # @example
43
+ # neighbors = client.ipv6.neighbors
44
+ #
45
+ def neighbors
46
+ response = get('/rci/show/ipv6/neighbor')
47
+ normalize_list(response, 'neighbor')
48
+ end
49
+
50
+ private
51
+
52
+ def normalize_list(response, key)
53
+ data = case response
54
+ when Array
55
+ response
56
+ when Hash
57
+ response[key] || response["#{key}s"] || []
58
+ else
59
+ []
60
+ end
61
+
62
+ return [] unless data.is_a?(Array)
63
+
64
+ data.map { |item| normalize_item(item) }.compact
65
+ end
66
+
67
+ def normalize_item(data)
68
+ return nil unless data.is_a?(Hash)
69
+
70
+ deep_normalize_keys(data)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,84 @@
1
+ module Keenetic
2
+ module Resources
3
+ # Mesh resource for monitoring mesh Wi-Fi system status.
4
+ #
5
+ # == API Endpoints Used
6
+ #
7
+ # === Reading Mesh Status
8
+ # GET /rci/show/mws
9
+ # Returns: Mesh Wi-Fi system status and configuration
10
+ #
11
+ # === Reading Mesh Members
12
+ # GET /rci/show/mws/member
13
+ # Returns: Connected mesh nodes/extenders
14
+ #
15
+ class Mesh < Base
16
+ # Get mesh Wi-Fi system status.
17
+ #
18
+ # == Keenetic API Request
19
+ # GET /rci/show/mws
20
+ #
21
+ # @return [Hash] Mesh system status and configuration
22
+ # @example
23
+ # status = client.mesh.status
24
+ # # => { enabled: true, role: "controller", members_count: 2, ... }
25
+ #
26
+ def status
27
+ response = get('/rci/show/mws')
28
+ normalize_status(response)
29
+ end
30
+
31
+ # Get connected mesh members (nodes/extenders).
32
+ #
33
+ # == Keenetic API Request
34
+ # GET /rci/show/mws/member
35
+ #
36
+ # @return [Array<Hash>] List of mesh members
37
+ # @example
38
+ # members = client.mesh.members
39
+ # # => [
40
+ # # { mac: "AA:BB:CC:DD:EE:FF", name: "Living Room", mode: "extender", ... },
41
+ # # ...
42
+ # # ]
43
+ #
44
+ def members
45
+ response = get('/rci/show/mws/member')
46
+ normalize_members(response)
47
+ end
48
+
49
+ private
50
+
51
+ def normalize_status(response)
52
+ return {} unless response.is_a?(Hash)
53
+
54
+ result = deep_normalize_keys(response)
55
+ normalize_booleans(result, %i[enabled active])
56
+ result
57
+ end
58
+
59
+ def normalize_members(response)
60
+ # Response might be array directly or wrapped in an object
61
+ members_data = case response
62
+ when Array
63
+ response
64
+ when Hash
65
+ response['member'] || response['members'] || []
66
+ else
67
+ []
68
+ end
69
+
70
+ return [] unless members_data.is_a?(Array)
71
+
72
+ members_data.map { |member| normalize_member(member) }.compact
73
+ end
74
+
75
+ def normalize_member(data)
76
+ return nil unless data.is_a?(Hash)
77
+
78
+ result = deep_normalize_keys(data)
79
+ normalize_booleans(result, %i[online active connected])
80
+ result
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,202 @@
1
+ module Keenetic
2
+ module Resources
3
+ # NAT resource for managing port forwarding rules.
4
+ #
5
+ # == API Endpoints Used
6
+ #
7
+ # === Reading NAT Rules
8
+ # GET /rci/show/ip/nat
9
+ # Returns: Array of port forwarding rules
10
+ #
11
+ # === Adding Port Forward
12
+ # POST /rci/ip/nat
13
+ # Body: { index, description, protocol, interface, port, to-host, to-port, enabled }
14
+ #
15
+ # === Deleting Port Forward
16
+ # POST /rci/ip/nat
17
+ # Body: { index, no: true }
18
+ #
19
+ # === Reading UPnP Mappings
20
+ # GET /rci/show/upnp/redirect
21
+ # Returns: Array of automatic UPnP port mappings
22
+ #
23
+ class Nat < Base
24
+ # Get all NAT/port forwarding rules.
25
+ #
26
+ # == Keenetic API Request
27
+ # GET /rci/show/ip/nat
28
+ #
29
+ # == Response Fields
30
+ # - index: Rule index/priority
31
+ # - description: Rule description
32
+ # - protocol: "tcp", "udp", or "any"
33
+ # - interface: WAN interface
34
+ # - port: External port
35
+ # - end_port: End of port range (optional)
36
+ # - to_host: Internal host IP
37
+ # - to_port: Internal port
38
+ # - enabled: Rule is active
39
+ #
40
+ # @return [Array<Hash>] List of normalized NAT rules
41
+ # @example
42
+ # rules = client.nat.rules
43
+ # # => [{ index: 1, description: "Web Server", protocol: "tcp", port: 8080, ... }]
44
+ #
45
+ def rules
46
+ response = get('/rci/show/ip/nat')
47
+ normalize_rules(response)
48
+ end
49
+
50
+ # Find a NAT rule by index.
51
+ #
52
+ # @param index [Integer] Rule index
53
+ # @return [Hash, nil] Rule data or nil if not found
54
+ # @example
55
+ # rule = client.nat.find_rule(1)
56
+ # # => { index: 1, description: "Web Server", ... }
57
+ #
58
+ def find_rule(index)
59
+ rules.find { |r| r[:index] == index }
60
+ end
61
+
62
+ # Add a port forwarding rule.
63
+ #
64
+ # == Keenetic API Request
65
+ # POST /rci/ip/nat
66
+ # Body: { index, description, protocol, interface, port, to-host, to-port, enabled }
67
+ #
68
+ # @param index [Integer] Rule index/priority
69
+ # @param protocol [String] Protocol: "tcp", "udp", or "any"
70
+ # @param port [Integer] External port
71
+ # @param to_host [String] Internal host IP address
72
+ # @param to_port [Integer] Internal port
73
+ # @param interface [String] WAN interface name (default: "ISP")
74
+ # @param description [String, nil] Optional rule description
75
+ # @param end_port [Integer, nil] End of port range (optional)
76
+ # @param enabled [Boolean] Whether rule is active (default: true)
77
+ # @return [Hash, Array, nil] API response
78
+ #
79
+ # @example Add simple port forward
80
+ # client.nat.add_forward(
81
+ # index: 1,
82
+ # protocol: 'tcp',
83
+ # port: 8080,
84
+ # to_host: '192.168.1.100',
85
+ # to_port: 80
86
+ # )
87
+ #
88
+ # @example Add port range forward
89
+ # client.nat.add_forward(
90
+ # index: 2,
91
+ # protocol: 'udp',
92
+ # port: 27015,
93
+ # end_port: 27030,
94
+ # to_host: '192.168.1.50',
95
+ # to_port: 27015,
96
+ # description: 'Game Server'
97
+ # )
98
+ #
99
+ def add_forward(index:, protocol:, port:, to_host:, to_port:, interface: 'ISP',
100
+ description: nil, end_port: nil, enabled: true)
101
+ params = {
102
+ 'index' => index,
103
+ 'protocol' => protocol,
104
+ 'interface' => interface,
105
+ 'port' => port,
106
+ 'to-host' => to_host,
107
+ 'to-port' => to_port,
108
+ 'enabled' => enabled
109
+ }
110
+ params['description'] = description if description
111
+ params['end-port'] = end_port if end_port
112
+
113
+ post('/rci/ip/nat', params)
114
+ end
115
+
116
+ # Delete a port forwarding rule.
117
+ #
118
+ # == Keenetic API Request
119
+ # POST /rci/ip/nat
120
+ # Body: { index, no: true }
121
+ #
122
+ # @param index [Integer] Rule index to delete
123
+ # @return [Hash, Array, nil] API response
124
+ #
125
+ # @example
126
+ # client.nat.delete_forward(index: 1)
127
+ #
128
+ def delete_forward(index:)
129
+ post('/rci/ip/nat', { 'index' => index, 'no' => true })
130
+ end
131
+
132
+ # Get all UPnP port mappings.
133
+ #
134
+ # UPnP mappings are automatically created by devices on the network.
135
+ # These are read-only and managed by the devices themselves.
136
+ #
137
+ # == Keenetic API Request
138
+ # GET /rci/show/upnp/redirect
139
+ #
140
+ # == Response Fields
141
+ # - protocol: "tcp" or "udp"
142
+ # - interface: WAN interface
143
+ # - port: External port
144
+ # - to_host: Internal host IP
145
+ # - to_port: Internal port
146
+ # - description: Mapping description (from device)
147
+ #
148
+ # @return [Array<Hash>] List of normalized UPnP mappings
149
+ # @example
150
+ # mappings = client.nat.upnp_mappings
151
+ # # => [{ protocol: "tcp", port: 51234, to_host: "192.168.1.50", ... }]
152
+ #
153
+ def upnp_mappings
154
+ response = get('/rci/show/upnp/redirect')
155
+ normalize_upnp_mappings(response)
156
+ end
157
+
158
+ private
159
+
160
+ def normalize_upnp_mappings(response)
161
+ return [] unless response.is_a?(Array)
162
+
163
+ response.map { |mapping| normalize_upnp_mapping(mapping) }.compact
164
+ end
165
+
166
+ def normalize_upnp_mapping(data)
167
+ return nil unless data.is_a?(Hash)
168
+
169
+ {
170
+ protocol: data['protocol'],
171
+ interface: data['interface'],
172
+ port: data['port'],
173
+ to_host: data['to-host'],
174
+ to_port: data['to-port'],
175
+ description: data['description']
176
+ }
177
+ end
178
+
179
+ def normalize_rules(response)
180
+ return [] unless response.is_a?(Array)
181
+
182
+ response.map { |rule| normalize_rule(rule) }.compact
183
+ end
184
+
185
+ def normalize_rule(data)
186
+ return nil unless data.is_a?(Hash)
187
+
188
+ {
189
+ index: data['index'],
190
+ description: data['description'],
191
+ protocol: data['protocol'],
192
+ interface: data['interface'],
193
+ port: data['port'],
194
+ end_port: data['end-port'],
195
+ to_host: data['to-host'],
196
+ to_port: data['to-port'],
197
+ enabled: normalize_boolean(data['enabled'])
198
+ }
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,89 @@
1
+ module Keenetic
2
+ module Resources
3
+ # QoS resource for Quality of Service and traffic control.
4
+ #
5
+ # == API Endpoints Used
6
+ #
7
+ # === Traffic Shaper Status
8
+ # GET /rci/show/ip/traffic-control
9
+ #
10
+ # === IntelliQoS Settings
11
+ # GET /rci/show/ip/qos
12
+ #
13
+ # === Traffic Statistics by Host
14
+ # GET /rci/show/ip/hotspot/summary
15
+ #
16
+ class Qos < Base
17
+ # Get traffic shaper status.
18
+ #
19
+ # @return [Hash] Traffic shaper configuration and status
20
+ # @example
21
+ # shaper = client.qos.traffic_shaper
22
+ #
23
+ def traffic_shaper
24
+ response = get('/rci/show/ip/traffic-control')
25
+ normalize_response(response)
26
+ end
27
+
28
+ # Alias for traffic_shaper
29
+ alias shaper traffic_shaper
30
+
31
+ # Get IntelliQoS settings.
32
+ #
33
+ # @return [Hash] IntelliQoS configuration
34
+ # @example
35
+ # qos = client.qos.intelliqos
36
+ #
37
+ def intelliqos
38
+ response = get('/rci/show/ip/qos')
39
+ normalize_response(response)
40
+ end
41
+
42
+ # Alias for intelliqos
43
+ alias settings intelliqos
44
+
45
+ # Get traffic statistics by host.
46
+ #
47
+ # @return [Array<Hash>] Traffic statistics per host
48
+ # @example
49
+ # stats = client.qos.traffic_stats
50
+ #
51
+ def traffic_stats
52
+ response = get('/rci/show/ip/hotspot/summary')
53
+ normalize_stats(response)
54
+ end
55
+
56
+ # Alias for traffic_stats
57
+ alias host_stats traffic_stats
58
+
59
+ private
60
+
61
+ def normalize_response(response)
62
+ return {} unless response.is_a?(Hash)
63
+
64
+ deep_normalize_keys(response)
65
+ end
66
+
67
+ def normalize_stats(response)
68
+ stats_data = case response
69
+ when Array
70
+ response
71
+ when Hash
72
+ response['host'] || response['hosts'] || response['stat'] || []
73
+ else
74
+ []
75
+ end
76
+
77
+ return [] unless stats_data.is_a?(Array)
78
+
79
+ stats_data.map { |stat| normalize_stat(stat) }.compact
80
+ end
81
+
82
+ def normalize_stat(data)
83
+ return nil unless data.is_a?(Hash)
84
+
85
+ deep_normalize_keys(data)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,87 @@
1
+ module Keenetic
2
+ module Resources
3
+ # Schedule resource for managing access control schedules.
4
+ #
5
+ # == API Endpoints Used
6
+ #
7
+ # === List Schedules
8
+ # GET /rci/show/schedule
9
+ #
10
+ # === Create Schedule
11
+ # POST /rci/schedule
12
+ #
13
+ # === Delete Schedule
14
+ # POST /rci/schedule with { "name": "...", "no": true }
15
+ #
16
+ class Schedule < Base
17
+ # List all schedules.
18
+ #
19
+ # @return [Array<Hash>] List of schedules
20
+ # @example
21
+ # schedules = client.schedule.all
22
+ #
23
+ def all
24
+ response = get('/rci/show/schedule')
25
+ normalize_schedules(response)
26
+ end
27
+
28
+ # Find a schedule by name.
29
+ #
30
+ # @param name [String] Schedule name
31
+ # @return [Hash, nil] Schedule data or nil
32
+ #
33
+ def find(name)
34
+ all.find { |s| s[:name] == name }
35
+ end
36
+
37
+ # Create a new schedule.
38
+ #
39
+ # @param name [String] Schedule name
40
+ # @param entries [Array<Hash>] Schedule entries with days, start, end, action
41
+ # @return [Hash, nil] API response
42
+ # @example
43
+ # client.schedule.create(
44
+ # name: 'kids_bedtime',
45
+ # entries: [
46
+ # { days: 'mon,tue,wed,thu,fri', start: '22:00', end: '07:00', action: 'deny' }
47
+ # ]
48
+ # )
49
+ #
50
+ def create(name:, entries:)
51
+ post('/rci/schedule', { 'name' => name, 'entries' => entries })
52
+ end
53
+
54
+ # Delete a schedule.
55
+ #
56
+ # @param name [String] Schedule name
57
+ # @return [Hash, nil] API response
58
+ #
59
+ def delete(name:)
60
+ post('/rci/schedule', { 'name' => name, 'no' => true })
61
+ end
62
+
63
+ private
64
+
65
+ def normalize_schedules(response)
66
+ schedules_data = case response
67
+ when Array
68
+ response
69
+ when Hash
70
+ response['schedule'] || response['schedules'] || []
71
+ else
72
+ []
73
+ end
74
+
75
+ return [] unless schedules_data.is_a?(Array)
76
+
77
+ schedules_data.map { |schedule| normalize_schedule(schedule) }.compact
78
+ end
79
+
80
+ def normalize_schedule(data)
81
+ return nil unless data.is_a?(Hash)
82
+
83
+ deep_normalize_keys(data)
84
+ end
85
+ end
86
+ end
87
+ end