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 +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +28 -26
- data/lib/keenetic/client.rb +131 -1
- data/lib/keenetic/resources/base.rb +4 -0
- data/lib/keenetic/resources/components.rb +88 -0
- data/lib/keenetic/resources/config.rb +89 -0
- data/lib/keenetic/resources/diagnostics.rb +102 -0
- data/lib/keenetic/resources/dns.rb +95 -0
- data/lib/keenetic/resources/dyndns.rb +71 -0
- data/lib/keenetic/resources/firewall.rb +103 -0
- data/lib/keenetic/resources/hotspot.rb +282 -0
- data/lib/keenetic/resources/ipv6.rb +74 -0
- data/lib/keenetic/resources/mesh.rb +84 -0
- data/lib/keenetic/resources/nat.rb +202 -0
- data/lib/keenetic/resources/qos.rb +89 -0
- data/lib/keenetic/resources/routes.rb +432 -0
- data/lib/keenetic/resources/schedule.rb +87 -0
- data/lib/keenetic/resources/system.rb +134 -0
- data/lib/keenetic/resources/usb.rb +135 -0
- data/lib/keenetic/resources/users.rb +92 -0
- data/lib/keenetic/resources/vpn.rb +153 -0
- data/lib/keenetic/version.rb +1 -2
- data/lib/keenetic.rb +33 -0
- metadata +20 -3
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# USB resource for managing USB devices and storage.
|
|
4
|
+
#
|
|
5
|
+
# == API Endpoints Used
|
|
6
|
+
#
|
|
7
|
+
# === Reading USB Devices
|
|
8
|
+
# GET /rci/show/usb
|
|
9
|
+
# Returns: Connected USB devices
|
|
10
|
+
#
|
|
11
|
+
# === Reading Storage/Media
|
|
12
|
+
# GET /rci/show/media
|
|
13
|
+
# Returns: Mounted storage partitions
|
|
14
|
+
#
|
|
15
|
+
# === Safely Eject USB
|
|
16
|
+
# POST /rci/usb/eject
|
|
17
|
+
# Body: { port }
|
|
18
|
+
#
|
|
19
|
+
class Usb < Base
|
|
20
|
+
# Get connected USB devices.
|
|
21
|
+
#
|
|
22
|
+
# == Keenetic API Request
|
|
23
|
+
# GET /rci/show/usb
|
|
24
|
+
#
|
|
25
|
+
# == Response Fields (per device)
|
|
26
|
+
# - port: USB port number
|
|
27
|
+
# - manufacturer: Device manufacturer
|
|
28
|
+
# - product: Product name
|
|
29
|
+
# - serial: Serial number
|
|
30
|
+
# - class: USB device class
|
|
31
|
+
# - speed: USB speed
|
|
32
|
+
# - connected: Currently connected
|
|
33
|
+
#
|
|
34
|
+
# @return [Array<Hash>] List of USB devices
|
|
35
|
+
# @example
|
|
36
|
+
# devices = client.usb.devices
|
|
37
|
+
# # => [{ port: 1, manufacturer: "SanDisk", product: "USB Flash", ... }]
|
|
38
|
+
#
|
|
39
|
+
def devices
|
|
40
|
+
response = get('/rci/show/usb')
|
|
41
|
+
normalize_devices(response)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get mounted storage partitions.
|
|
45
|
+
#
|
|
46
|
+
# == Keenetic API Request
|
|
47
|
+
# GET /rci/show/media
|
|
48
|
+
#
|
|
49
|
+
# == Response Fields (per partition)
|
|
50
|
+
# - name: Device name
|
|
51
|
+
# - label: Volume label
|
|
52
|
+
# - uuid: Volume UUID
|
|
53
|
+
# - fs: Filesystem type
|
|
54
|
+
# - mountpoint: Mount path
|
|
55
|
+
# - total: Total bytes
|
|
56
|
+
# - used: Used bytes
|
|
57
|
+
# - free: Free bytes
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<Hash>] List of storage partitions
|
|
60
|
+
# @example
|
|
61
|
+
# media = client.usb.media
|
|
62
|
+
# # => [{ name: "sda1", label: "USB_DRIVE", fs: "ext4", total: 32000000000, ... }]
|
|
63
|
+
#
|
|
64
|
+
def media
|
|
65
|
+
response = get('/rci/show/media')
|
|
66
|
+
normalize_media(response)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Alias for media
|
|
70
|
+
alias storage media
|
|
71
|
+
|
|
72
|
+
# Safely eject a USB device.
|
|
73
|
+
#
|
|
74
|
+
# == Keenetic API Request
|
|
75
|
+
# POST /rci/usb/eject
|
|
76
|
+
# Body: { "port": 1 }
|
|
77
|
+
#
|
|
78
|
+
# @param port [Integer] USB port number to eject
|
|
79
|
+
# @return [Hash, nil] API response
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# client.usb.eject(port: 1)
|
|
83
|
+
#
|
|
84
|
+
def eject(port:)
|
|
85
|
+
post('/rci/usb/eject', { 'port' => port })
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def normalize_devices(response)
|
|
91
|
+
devices_data = case response
|
|
92
|
+
when Array
|
|
93
|
+
response
|
|
94
|
+
when Hash
|
|
95
|
+
response['device'] || response['devices'] || []
|
|
96
|
+
else
|
|
97
|
+
[]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
return [] unless devices_data.is_a?(Array)
|
|
101
|
+
|
|
102
|
+
devices_data.map { |device| normalize_device(device) }.compact
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def normalize_device(data)
|
|
106
|
+
return nil unless data.is_a?(Hash)
|
|
107
|
+
|
|
108
|
+
result = deep_normalize_keys(data)
|
|
109
|
+
normalize_booleans(result, %i[connected])
|
|
110
|
+
result
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def normalize_media(response)
|
|
114
|
+
media_data = case response
|
|
115
|
+
when Array
|
|
116
|
+
response
|
|
117
|
+
when Hash
|
|
118
|
+
response['media'] || response['partition'] || []
|
|
119
|
+
else
|
|
120
|
+
[]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return [] unless media_data.is_a?(Array)
|
|
124
|
+
|
|
125
|
+
media_data.map { |partition| normalize_partition(partition) }.compact
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def normalize_partition(data)
|
|
129
|
+
return nil unless data.is_a?(Hash)
|
|
130
|
+
|
|
131
|
+
deep_normalize_keys(data)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# Users resource for managing router user accounts.
|
|
4
|
+
#
|
|
5
|
+
# == API Endpoints Used
|
|
6
|
+
#
|
|
7
|
+
# === List Users
|
|
8
|
+
# GET /rci/show/user
|
|
9
|
+
#
|
|
10
|
+
# === Create User
|
|
11
|
+
# POST /rci/user
|
|
12
|
+
#
|
|
13
|
+
# === Delete User
|
|
14
|
+
# POST /rci/user with { "name": "...", "no": true }
|
|
15
|
+
#
|
|
16
|
+
# == User Tags (permissions)
|
|
17
|
+
# - http: Web interface access
|
|
18
|
+
# - cli: CLI/Telnet access
|
|
19
|
+
# - cifs: File sharing access
|
|
20
|
+
# - ftp: FTP access
|
|
21
|
+
# - vpn: VPN access
|
|
22
|
+
#
|
|
23
|
+
class Users < Base
|
|
24
|
+
# List all users.
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<Hash>] List of users
|
|
27
|
+
# @example
|
|
28
|
+
# users = client.users.all
|
|
29
|
+
#
|
|
30
|
+
def all
|
|
31
|
+
response = get('/rci/show/user')
|
|
32
|
+
normalize_users(response)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Find a user by name.
|
|
36
|
+
#
|
|
37
|
+
# @param name [String] User name
|
|
38
|
+
# @return [Hash, nil] User data or nil
|
|
39
|
+
#
|
|
40
|
+
def find(name)
|
|
41
|
+
all.find { |u| u[:name] == name }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Create a new user.
|
|
45
|
+
#
|
|
46
|
+
# @param name [String] User name
|
|
47
|
+
# @param password [String] User password
|
|
48
|
+
# @param tag [Array<String>] Permission tags (http, cli, cifs, ftp, vpn)
|
|
49
|
+
# @return [Hash, nil] API response
|
|
50
|
+
# @example
|
|
51
|
+
# client.users.create(name: 'guest', password: 'guestpass', tag: ['http', 'cifs'])
|
|
52
|
+
#
|
|
53
|
+
def create(name:, password:, tag: [])
|
|
54
|
+
params = { 'name' => name, 'password' => password }
|
|
55
|
+
params['tag'] = tag unless tag.empty?
|
|
56
|
+
post('/rci/user', params)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Delete a user.
|
|
60
|
+
#
|
|
61
|
+
# @param name [String] User name
|
|
62
|
+
# @return [Hash, nil] API response
|
|
63
|
+
#
|
|
64
|
+
def delete(name:)
|
|
65
|
+
post('/rci/user', { 'name' => name, 'no' => true })
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def normalize_users(response)
|
|
71
|
+
users_data = case response
|
|
72
|
+
when Array
|
|
73
|
+
response
|
|
74
|
+
when Hash
|
|
75
|
+
response['user'] || response['users'] || []
|
|
76
|
+
else
|
|
77
|
+
[]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
return [] unless users_data.is_a?(Array)
|
|
81
|
+
|
|
82
|
+
users_data.map { |user| normalize_user(user) }.compact
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def normalize_user(data)
|
|
86
|
+
return nil unless data.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
deep_normalize_keys(data)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# VPN resource for managing VPN server and monitoring connections.
|
|
4
|
+
#
|
|
5
|
+
# == API Endpoints Used
|
|
6
|
+
#
|
|
7
|
+
# === Reading VPN Server Status
|
|
8
|
+
# GET /rci/show/vpn-server
|
|
9
|
+
# Returns: VPN server configuration and status
|
|
10
|
+
#
|
|
11
|
+
# === Reading VPN Clients
|
|
12
|
+
# GET /rci/show/vpn-server/clients
|
|
13
|
+
# Returns: Array of connected VPN clients
|
|
14
|
+
#
|
|
15
|
+
# === Reading IPsec Status
|
|
16
|
+
# GET /rci/show/crypto/ipsec/sa
|
|
17
|
+
# Returns: IPsec security associations
|
|
18
|
+
#
|
|
19
|
+
# === Configuring VPN Server
|
|
20
|
+
# POST /rci/vpn-server
|
|
21
|
+
# Body: { type, enabled, pool-start, pool-end, ... }
|
|
22
|
+
#
|
|
23
|
+
class Vpn < Base
|
|
24
|
+
# Get VPN server status and configuration.
|
|
25
|
+
#
|
|
26
|
+
# == Keenetic API Request
|
|
27
|
+
# GET /rci/show/vpn-server
|
|
28
|
+
#
|
|
29
|
+
# == Response Fields
|
|
30
|
+
# - type: VPN server type (pptp, l2tp, sstp, etc.)
|
|
31
|
+
# - enabled: Whether server is enabled
|
|
32
|
+
# - running: Whether server is currently running
|
|
33
|
+
# - pool_start: Start of IP pool for clients
|
|
34
|
+
# - pool_end: End of IP pool for clients
|
|
35
|
+
# - interface: VPN interface name
|
|
36
|
+
#
|
|
37
|
+
# @return [Hash] VPN server status
|
|
38
|
+
# @example
|
|
39
|
+
# status = client.vpn.status
|
|
40
|
+
# # => { type: "l2tp", enabled: true, running: true, ... }
|
|
41
|
+
#
|
|
42
|
+
def status
|
|
43
|
+
response = get('/rci/show/vpn-server')
|
|
44
|
+
normalize_status(response)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get connected VPN clients.
|
|
48
|
+
#
|
|
49
|
+
# == Keenetic API Request
|
|
50
|
+
# GET /rci/show/vpn-server/clients
|
|
51
|
+
#
|
|
52
|
+
# == Response Fields (per client)
|
|
53
|
+
# - name: Client username
|
|
54
|
+
# - ip: Assigned IP address
|
|
55
|
+
# - uptime: Connection duration in seconds
|
|
56
|
+
# - rxbytes: Bytes received
|
|
57
|
+
# - txbytes: Bytes transmitted
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<Hash>] List of connected VPN clients
|
|
60
|
+
# @example
|
|
61
|
+
# clients = client.vpn.clients
|
|
62
|
+
# # => [{ name: "user1", ip: "192.168.1.200", uptime: 3600, ... }]
|
|
63
|
+
#
|
|
64
|
+
def clients
|
|
65
|
+
response = get('/rci/show/vpn-server/clients')
|
|
66
|
+
normalize_clients(response)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get IPsec security associations status.
|
|
70
|
+
#
|
|
71
|
+
# == Keenetic API Request
|
|
72
|
+
# GET /rci/show/crypto/ipsec/sa
|
|
73
|
+
#
|
|
74
|
+
# == Response Fields
|
|
75
|
+
# - established: Number of established SAs
|
|
76
|
+
# - sa: Array of security associations
|
|
77
|
+
#
|
|
78
|
+
# @return [Hash] IPsec status with security associations
|
|
79
|
+
# @example
|
|
80
|
+
# ipsec = client.vpn.ipsec_status
|
|
81
|
+
# # => { established: 2, sa: [...] }
|
|
82
|
+
#
|
|
83
|
+
def ipsec_status
|
|
84
|
+
response = get('/rci/show/crypto/ipsec/sa')
|
|
85
|
+
normalize_ipsec(response)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Configure VPN server.
|
|
89
|
+
#
|
|
90
|
+
# == Keenetic API Request
|
|
91
|
+
# POST /rci/vpn-server
|
|
92
|
+
# Body: { type, enabled, pool-start, pool-end, ... }
|
|
93
|
+
#
|
|
94
|
+
# @param type [String] VPN type: "pptp", "l2tp", "sstp"
|
|
95
|
+
# @param enabled [Boolean] Enable or disable the server
|
|
96
|
+
# @param pool_start [String, nil] Start of client IP pool
|
|
97
|
+
# @param pool_end [String, nil] End of client IP pool
|
|
98
|
+
# @param mppe [String, nil] MPPE encryption: "require", "prefer", "none"
|
|
99
|
+
# @return [Hash, Array, nil] API response
|
|
100
|
+
#
|
|
101
|
+
# @example Enable L2TP server
|
|
102
|
+
# client.vpn.configure(
|
|
103
|
+
# type: 'l2tp',
|
|
104
|
+
# enabled: true,
|
|
105
|
+
# pool_start: '192.168.1.200',
|
|
106
|
+
# pool_end: '192.168.1.210'
|
|
107
|
+
# )
|
|
108
|
+
#
|
|
109
|
+
# @example Disable VPN server
|
|
110
|
+
# client.vpn.configure(type: 'pptp', enabled: false)
|
|
111
|
+
#
|
|
112
|
+
def configure(type:, enabled:, pool_start: nil, pool_end: nil, mppe: nil)
|
|
113
|
+
params = {
|
|
114
|
+
'type' => type,
|
|
115
|
+
'enabled' => enabled
|
|
116
|
+
}
|
|
117
|
+
params['pool-start'] = pool_start if pool_start
|
|
118
|
+
params['pool-end'] = pool_end if pool_end
|
|
119
|
+
params['mppe'] = mppe if mppe
|
|
120
|
+
|
|
121
|
+
post('/rci/vpn-server', params)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def normalize_status(response)
|
|
127
|
+
return {} unless response.is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
result = deep_normalize_keys(response)
|
|
130
|
+
normalize_booleans(result, %i[enabled running])
|
|
131
|
+
result
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def normalize_clients(response)
|
|
135
|
+
return [] unless response.is_a?(Array)
|
|
136
|
+
|
|
137
|
+
response.map { |client_data| normalize_client(client_data) }.compact
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def normalize_client(data)
|
|
141
|
+
return nil unless data.is_a?(Hash)
|
|
142
|
+
|
|
143
|
+
deep_normalize_keys(data)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def normalize_ipsec(response)
|
|
147
|
+
return {} unless response.is_a?(Hash)
|
|
148
|
+
|
|
149
|
+
deep_normalize_keys(response)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/keenetic/version.rb
CHANGED
data/lib/keenetic.rb
CHANGED
|
@@ -13,6 +13,22 @@ require_relative 'keenetic/resources/policies'
|
|
|
13
13
|
require_relative 'keenetic/resources/dhcp'
|
|
14
14
|
require_relative 'keenetic/resources/routing'
|
|
15
15
|
require_relative 'keenetic/resources/logs'
|
|
16
|
+
require_relative 'keenetic/resources/routes'
|
|
17
|
+
require_relative 'keenetic/resources/hotspot'
|
|
18
|
+
require_relative 'keenetic/resources/config'
|
|
19
|
+
require_relative 'keenetic/resources/nat'
|
|
20
|
+
require_relative 'keenetic/resources/vpn'
|
|
21
|
+
require_relative 'keenetic/resources/diagnostics'
|
|
22
|
+
require_relative 'keenetic/resources/firewall'
|
|
23
|
+
require_relative 'keenetic/resources/mesh'
|
|
24
|
+
require_relative 'keenetic/resources/usb'
|
|
25
|
+
require_relative 'keenetic/resources/dns'
|
|
26
|
+
require_relative 'keenetic/resources/dyndns'
|
|
27
|
+
require_relative 'keenetic/resources/schedule'
|
|
28
|
+
require_relative 'keenetic/resources/users'
|
|
29
|
+
require_relative 'keenetic/resources/components'
|
|
30
|
+
require_relative 'keenetic/resources/qos'
|
|
31
|
+
require_relative 'keenetic/resources/ipv6'
|
|
16
32
|
|
|
17
33
|
# Keenetic Router API Client
|
|
18
34
|
#
|
|
@@ -57,6 +73,23 @@ require_relative 'keenetic/resources/logs'
|
|
|
57
73
|
# # Ports
|
|
58
74
|
# client.ports.all # Physical port statuses
|
|
59
75
|
#
|
|
76
|
+
# # Static Routes
|
|
77
|
+
# client.routes.all # All static routes
|
|
78
|
+
# client.routes.add(...) # Add static route
|
|
79
|
+
# client.routes.delete(...) # Delete static route
|
|
80
|
+
#
|
|
81
|
+
# # Hotspot / Policies
|
|
82
|
+
# client.hotspot.policies # All IP policies
|
|
83
|
+
# client.hotspot.hosts # All hosts with policies
|
|
84
|
+
# client.hotspot.set_host_policy(mac: '...', policy: '...')
|
|
85
|
+
#
|
|
86
|
+
# # Configuration
|
|
87
|
+
# client.system_config.save # Save configuration
|
|
88
|
+
# client.system_config.download # Download startup config
|
|
89
|
+
#
|
|
90
|
+
# # Raw RCI Access
|
|
91
|
+
# client.rci({ ... }) # Execute arbitrary RCI commands
|
|
92
|
+
#
|
|
60
93
|
# == Error Handling
|
|
61
94
|
#
|
|
62
95
|
# begin
|
metadata
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keenetic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
- Anton
|
|
7
|
+
- Anton Zaytsev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
@@ -45,24 +45,41 @@ executables: []
|
|
|
45
45
|
extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
|
47
47
|
files:
|
|
48
|
+
- LICENSE.txt
|
|
48
49
|
- README.md
|
|
49
50
|
- lib/keenetic.rb
|
|
50
51
|
- lib/keenetic/client.rb
|
|
51
52
|
- lib/keenetic/configuration.rb
|
|
52
53
|
- lib/keenetic/errors.rb
|
|
53
54
|
- lib/keenetic/resources/base.rb
|
|
55
|
+
- lib/keenetic/resources/components.rb
|
|
56
|
+
- lib/keenetic/resources/config.rb
|
|
54
57
|
- lib/keenetic/resources/devices.rb
|
|
55
58
|
- lib/keenetic/resources/dhcp.rb
|
|
59
|
+
- lib/keenetic/resources/diagnostics.rb
|
|
60
|
+
- lib/keenetic/resources/dns.rb
|
|
61
|
+
- lib/keenetic/resources/dyndns.rb
|
|
62
|
+
- lib/keenetic/resources/firewall.rb
|
|
63
|
+
- lib/keenetic/resources/hotspot.rb
|
|
56
64
|
- lib/keenetic/resources/internet.rb
|
|
65
|
+
- lib/keenetic/resources/ipv6.rb
|
|
57
66
|
- lib/keenetic/resources/logs.rb
|
|
67
|
+
- lib/keenetic/resources/mesh.rb
|
|
68
|
+
- lib/keenetic/resources/nat.rb
|
|
58
69
|
- lib/keenetic/resources/network.rb
|
|
59
70
|
- lib/keenetic/resources/policies.rb
|
|
60
71
|
- lib/keenetic/resources/ports.rb
|
|
72
|
+
- lib/keenetic/resources/qos.rb
|
|
73
|
+
- lib/keenetic/resources/routes.rb
|
|
61
74
|
- lib/keenetic/resources/routing.rb
|
|
75
|
+
- lib/keenetic/resources/schedule.rb
|
|
62
76
|
- lib/keenetic/resources/system.rb
|
|
77
|
+
- lib/keenetic/resources/usb.rb
|
|
78
|
+
- lib/keenetic/resources/users.rb
|
|
79
|
+
- lib/keenetic/resources/vpn.rb
|
|
63
80
|
- lib/keenetic/resources/wifi.rb
|
|
64
81
|
- lib/keenetic/version.rb
|
|
65
|
-
homepage: https://github.com/
|
|
82
|
+
homepage: https://github.com/antonzaytsev/keenetic-ruby
|
|
66
83
|
licenses:
|
|
67
84
|
- MIT
|
|
68
85
|
metadata:
|