node-red-contrib-padavan 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.
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/main.mjs +300 -0
- package/nodes/config.html +71 -0
- package/nodes/config.js +19 -0
- package/nodes/devices.html +27 -0
- package/nodes/devices.js +27 -0
- package/nodes/history.html +27 -0
- package/nodes/history.js +32 -0
- package/nodes/locales/en-US/config.html +12 -0
- package/nodes/locales/en-US/devices.html +19 -0
- package/nodes/locales/en-US/history.html +25 -0
- package/nodes/locales/en-US/params.html +39 -0
- package/nodes/locales/en-US/speedtest.html +24 -0
- package/nodes/locales/en-US/system.html +79 -0
- package/nodes/locales/en-US/upgrade.html +33 -0
- package/nodes/locales/ru/config.html +12 -0
- package/nodes/locales/ru/devices.html +19 -0
- package/nodes/locales/ru/history.html +25 -0
- package/nodes/locales/ru/params.html +39 -0
- package/nodes/locales/ru/speedtest.html +24 -0
- package/nodes/locales/ru/system.html +79 -0
- package/nodes/locales/ru/upgrade.html +33 -0
- package/nodes/params.html +60 -0
- package/nodes/params.js +35 -0
- package/nodes/speedtest.html +28 -0
- package/nodes/speedtest.js +66 -0
- package/nodes/system.html +43 -0
- package/nodes/system.js +30 -0
- package/nodes/upgrade.html +43 -0
- package/nodes/upgrade.js +47 -0
- package/package.json +41 -0
- package/services/speedtest.sh +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Alex Smith
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
Router management with Padavan firmware
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## Install
|
|
5
|
+
|
|
6
|
+
``` shell
|
|
7
|
+
npm install node-red-contrib-padavan
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Nodes
|
|
12
|
+
|
|
13
|
+
- [Configuration](#configuration)
|
|
14
|
+
- [SpeedTest](#speedtest)
|
|
15
|
+
- [Devices](#devices)
|
|
16
|
+
- [History](#history)
|
|
17
|
+
- [Updates](#updates)
|
|
18
|
+
- [Status](#status)
|
|
19
|
+
- [Parameters](#parameters)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
### Input
|
|
26
|
+
|
|
27
|
+
| credentials | Description
|
|
28
|
+
| --- | ---
|
|
29
|
+
| *`repo`* | Repository where firmware builds are hosted
|
|
30
|
+
| *`branch`* | Branch for the device
|
|
31
|
+
| *`token`* | Access token for GitHub
|
|
32
|
+
| `host` | Router IP address
|
|
33
|
+
| `username` | Username for router access
|
|
34
|
+
| `password` | Password for router access
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## SpeedTest
|
|
38
|
+
|
|
39
|
+
Measure download and upload speeds
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Output
|
|
43
|
+
|
|
44
|
+
| outputs | Description
|
|
45
|
+
| --- | ---
|
|
46
|
+
| `result` | Result
|
|
47
|
+
| `history` | Result with date and timestamp
|
|
48
|
+
| `progress` | Test start and end
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
| result.msg | type | Value
|
|
52
|
+
| --- | --- | ---
|
|
53
|
+
| `topic` | string | NetworkControl
|
|
54
|
+
| `payload` | object |
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
| payload | Description
|
|
58
|
+
| --- | ---
|
|
59
|
+
| `networkDownloadSpeedMbps`| Download speed
|
|
60
|
+
| `networkUploadSpeedMbps` | Upload speed
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## Devices
|
|
64
|
+
|
|
65
|
+
Get the list of connected devices
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
### Output
|
|
69
|
+
|
|
70
|
+
| msg | type
|
|
71
|
+
| --- | ---
|
|
72
|
+
| `payload` | array
|
|
73
|
+
| `numConnectedDevices` | integer
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
| payload |
|
|
77
|
+
| --- |
|
|
78
|
+
| `hostname` |
|
|
79
|
+
| `ip` |
|
|
80
|
+
| `mac` |
|
|
81
|
+
| `rssi` |
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## History
|
|
85
|
+
|
|
86
|
+
Retrieve traffic history
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
### Output
|
|
90
|
+
|
|
91
|
+
| msg | type
|
|
92
|
+
| --- | ---
|
|
93
|
+
| `payload` | object
|
|
94
|
+
| `networkUsage` | string
|
|
95
|
+
| `networkUsageMB` | integer
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
| payload | type | Description
|
|
99
|
+
| --- | --- | ---
|
|
100
|
+
| `daily_history` | array | Daily
|
|
101
|
+
| `monthly_history` | array | Monthly
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
| \*\_history | Description
|
|
105
|
+
| --- | ---
|
|
106
|
+
| `0` | Year, month, day
|
|
107
|
+
| `1` | Download
|
|
108
|
+
| `2` | Upload
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
## Updates
|
|
112
|
+
|
|
113
|
+
Manage firmware updates
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
### Input
|
|
117
|
+
|
|
118
|
+
| msg | type
|
|
119
|
+
| --- | ---
|
|
120
|
+
| `topic` | string
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
| topic | Description
|
|
124
|
+
| --- | ---
|
|
125
|
+
| *`build`* | Build firmware
|
|
126
|
+
| *`changelog`* | List of changes
|
|
127
|
+
| *`upgrade`* | Install firmware
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
### Output
|
|
131
|
+
|
|
132
|
+
| msg | type | Value
|
|
133
|
+
| --- | --- | ---
|
|
134
|
+
| `topic` | string | changelog
|
|
135
|
+
| `error` | string |
|
|
136
|
+
| `payload` | object |
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
| payload | Description
|
|
140
|
+
| --- | ---
|
|
141
|
+
| `from_id` | Current firmware
|
|
142
|
+
| `to_id` | Built firmware
|
|
143
|
+
| `data` | Array of changes
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
## Status
|
|
147
|
+
|
|
148
|
+
Retrieve and manage router status
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
### Input
|
|
152
|
+
|
|
153
|
+
| msg | type
|
|
154
|
+
| --- | ---
|
|
155
|
+
| `topic` | string
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
| topic | Description
|
|
159
|
+
| --- | ---
|
|
160
|
+
| *`status`* | Get current status
|
|
161
|
+
| *`log`* | Get logs
|
|
162
|
+
| *`reboot`* | Reboot
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
### Output
|
|
166
|
+
|
|
167
|
+
| msg | type | Value
|
|
168
|
+
| --- | --- | ---
|
|
169
|
+
| `topic` | string | status
|
|
170
|
+
| `payload` | object |
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
| payload | Description
|
|
174
|
+
| --- | ---
|
|
175
|
+
| `lavg` | System load average over the last 1, 5, and 15 minutes
|
|
176
|
+
| `uptime` | Router uptime
|
|
177
|
+
| `ram` | RAM usage information
|
|
178
|
+
| `swap` | Swap memory status
|
|
179
|
+
| `cpu` | CPU load information
|
|
180
|
+
| `wifi2` | Wi-Fi status on the 2.4 GHz band
|
|
181
|
+
| `wifi5` | Wi-Fi status on the 5 GHz band
|
|
182
|
+
| `logmt` | Timestamp of the last log modification
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
| uptime |
|
|
186
|
+
| --- |
|
|
187
|
+
| `days` |
|
|
188
|
+
| `hours` |
|
|
189
|
+
| `minutes` |
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
| ram | Description
|
|
193
|
+
| --- | ---
|
|
194
|
+
| `total` | Total RAM (in KB)
|
|
195
|
+
| `used` | Used RAM
|
|
196
|
+
| `free` | Free RAM
|
|
197
|
+
| `buffers` | RAM used for buffers
|
|
198
|
+
| `cached` | RAM used for cache
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
| swap |
|
|
202
|
+
| --- |
|
|
203
|
+
| `total` |
|
|
204
|
+
| `used` |
|
|
205
|
+
| `free` |
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
| cpu | Description
|
|
209
|
+
| --- | ---
|
|
210
|
+
| `busy` | Total time the CPU has been busy
|
|
211
|
+
| `user` | Time spent on user processes
|
|
212
|
+
| `nice` | Time spent on processes with modified priority
|
|
213
|
+
| `system` | Time spent on system processes
|
|
214
|
+
| `idle` | Idle time when the CPU was not busy
|
|
215
|
+
| `iowait` | Time spent waiting for I/O operations
|
|
216
|
+
| `irq` | Time spent on hardware interrupt handling
|
|
217
|
+
| `sirq` | Time spent on software interrupt handling
|
|
218
|
+
| `total` | Total units of time since the system started
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
| wifi\* | Description
|
|
222
|
+
| --- | ---
|
|
223
|
+
| `state` | Network state
|
|
224
|
+
| `guest` | Guest network state
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
## Parameters
|
|
228
|
+
|
|
229
|
+
Retrieve and modify parameters
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
### Input
|
|
233
|
+
|
|
234
|
+
| msg | type
|
|
235
|
+
| --- | ---
|
|
236
|
+
| *`payload`* | object
|
|
237
|
+
| `topic` | string
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
| topic | Description
|
|
241
|
+
| --- | ---
|
|
242
|
+
| *`list`* | Retrieve all parameters
|
|
243
|
+
| *`get`* | Retrieve specific parameters
|
|
244
|
+
| *`set`* | Modify parameters
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
| payload |
|
|
248
|
+
| --- |
|
|
249
|
+
| *`sid_list`* |
|
|
250
|
+
| *`action_mode`* |
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
### Output
|
|
254
|
+
|
|
255
|
+
| msg | type
|
|
256
|
+
| --- | ---
|
|
257
|
+
| `topic` | string
|
|
258
|
+
| `payload` | object
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
### Details
|
|
262
|
+
|
|
263
|
+
If `sid_list` is specified, changes will be sent via the web panel.
|
|
264
|
+
|
|
265
|
+
If `action_mode` is not specified, it will be sent as ` Apply `.
|
package/main.mjs
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import ini from 'ini';
|
|
5
|
+
import ssh from 'ssh2';
|
|
6
|
+
import jszip from 'jszip';
|
|
7
|
+
|
|
8
|
+
export class Padavan {
|
|
9
|
+
constructor(credentials={}) {
|
|
10
|
+
this.credentials = credentials;
|
|
11
|
+
};
|
|
12
|
+
exec(SystemCmd) {
|
|
13
|
+
/* return this.router('apply.cgi', {
|
|
14
|
+
SystemCmd,
|
|
15
|
+
action_mode: ' SystemCmd '
|
|
16
|
+
}, 'POST').then(res => this.router('console_response.asp').then(res => res.text())); */
|
|
17
|
+
return new Promise((res, rej) => {
|
|
18
|
+
const stdout = [];
|
|
19
|
+
const stderr = [];
|
|
20
|
+
const conn = new ssh.Client();
|
|
21
|
+
conn.on('error', rej);
|
|
22
|
+
conn.on('ready', () => {
|
|
23
|
+
conn.exec(SystemCmd, (err, stream) => {
|
|
24
|
+
if (err)
|
|
25
|
+
return rej(err);
|
|
26
|
+
stream.on('close', (code, signal) => {
|
|
27
|
+
conn.end();
|
|
28
|
+
if (code)
|
|
29
|
+
return rej({
|
|
30
|
+
code,
|
|
31
|
+
message: stderr.join('')
|
|
32
|
+
});
|
|
33
|
+
return res(stdout.join('').slice(0, -1))
|
|
34
|
+
}).on('data', chunk => stdout.push(chunk)).stderr.on('data', chunk => stderr.push(chunk));
|
|
35
|
+
});
|
|
36
|
+
}).connect({
|
|
37
|
+
host: this.credentials.host,
|
|
38
|
+
username: this.credentials.username,
|
|
39
|
+
password: this.credentials.password
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
nvram(key, value) {
|
|
44
|
+
if (key) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return this.exec(`nvram get ${key}`);
|
|
47
|
+
else if (value === null)
|
|
48
|
+
return this.exec(`nvram set ${key}`);
|
|
49
|
+
else
|
|
50
|
+
return this.exec(`nvram set ${key}=${value}`);
|
|
51
|
+
}else
|
|
52
|
+
return this.exec('nvram showall').then(ini.parse);
|
|
53
|
+
};
|
|
54
|
+
async setParams(data) {
|
|
55
|
+
if (!data || !Object.keys(data).length)
|
|
56
|
+
throw new Error('Input data missing');
|
|
57
|
+
if (data.sid_list) {
|
|
58
|
+
if (!data.action_mode)
|
|
59
|
+
data.action_mode = ' Apply ';
|
|
60
|
+
return this.router('start_apply.htm', data, 'POST').then(res => res.text()).then(text => text.includes('<script>done_committing();</script>'));
|
|
61
|
+
}else
|
|
62
|
+
return Promise.all(Object.entries(data).map(([ key, value ]) => this.nvram(key, value))).then(() => {
|
|
63
|
+
return this.exec('nvram commit').then(() => true);
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
getParams(keys) {
|
|
67
|
+
return this.nvram().then(nvram => {
|
|
68
|
+
if (!keys)
|
|
69
|
+
return nvram;
|
|
70
|
+
return Object.fromEntries([].concat(keys).map(key => [ key, nvram[key] ]));
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
getLog() {
|
|
74
|
+
return this.router('log_content.asp').then(res => res.text());
|
|
75
|
+
};
|
|
76
|
+
getStatus() {
|
|
77
|
+
return this.router('system_status_data.asp').then(res => res.text()).then(text => {
|
|
78
|
+
return JSON.parse(
|
|
79
|
+
text.slice(11, -1)
|
|
80
|
+
.replace(new RegExp('(\\w+):\\s', 'g'), '"$1": ')
|
|
81
|
+
.replace(new RegExp('0x([0-9a-fA-F]+)', 'g'), (match, hex) => parseInt(hex, 16))
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
getHistory() {
|
|
86
|
+
// return this.exec('cat /var/spool/rstats-history.js').then(text => {
|
|
87
|
+
return this.router('Main_TrafficMonitor_daily.asp').then(res => res.text()).then(text => {
|
|
88
|
+
text = text.substring(text.indexOf('netdev'));
|
|
89
|
+
text = text.substring(0, text.indexOf('\n\n'));
|
|
90
|
+
text = text.trim()
|
|
91
|
+
.replace(new RegExp('\'', 'g'), '"')
|
|
92
|
+
.replace(new RegExp('^(.*) =', 'gm'), '"$1":')
|
|
93
|
+
.replace(new RegExp(';', 'g'), ',')
|
|
94
|
+
.replace(new RegExp(',$', 'g'), '')
|
|
95
|
+
.replace(new RegExp('0x([0-9a-fA-F]+)', 'g'), (match, hex) => parseInt(hex, 16));
|
|
96
|
+
const json = JSON.parse('{'+text+'}');
|
|
97
|
+
for (var k in json) {
|
|
98
|
+
if (k.endsWith('_history'))
|
|
99
|
+
json[k].forEach(stat => {
|
|
100
|
+
stat[0] = [(((stat[0] >> 16) & 0xFF) + 1900), ((stat[0] >>> 8) & 0xFF), (stat[0] & 0xFF)];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return json;
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
getDevices() {
|
|
107
|
+
return this.exec('arp -a').then(arp => {
|
|
108
|
+
return arp.split('\n').reduce((res, line) => {
|
|
109
|
+
const parts = line.match(/^(\S+)\s+\(([\d.]+)\)\s+at\s+([a-fA-F0-9:]+)\s+\[ether\](?:\s+(\S+))?\s+on\s+(\S+)/);
|
|
110
|
+
if (parts) {
|
|
111
|
+
if (parts[1] === '?')
|
|
112
|
+
parts[1] = null;
|
|
113
|
+
res[parts[3].toUpperCase()] = parts[1];
|
|
114
|
+
}
|
|
115
|
+
return res;
|
|
116
|
+
}, {});
|
|
117
|
+
}).then(arp => {
|
|
118
|
+
return this.router('device-map/clients.asp').then(res => res.text()).then(text => {
|
|
119
|
+
const ipmonitor = JSON.parse(text.match(new RegExp('^var ipmonitor = (.*?);$', 'm'))[1]);
|
|
120
|
+
const wireless = JSON.parse(text.match(new RegExp('^var wireless = (.*?);$', 'm'))[1]);
|
|
121
|
+
return ipmonitor.filter(device => device[5] !== '1').map(device => ({
|
|
122
|
+
hostname: arp[device[1]] || device[2],
|
|
123
|
+
ip: device[0],
|
|
124
|
+
mac: device[1],
|
|
125
|
+
rssi: wireless[device[1]]
|
|
126
|
+
}));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
router(path, body, method) {
|
|
131
|
+
return fetch(`http://${this.credentials.host}/${path}`, {
|
|
132
|
+
method,
|
|
133
|
+
headers: {
|
|
134
|
+
authorization: 'Basic '+btoa(this.credentials.username+':'+this.credentials.password)
|
|
135
|
+
},
|
|
136
|
+
body: (body?.constructor.name === 'Object') ? new URLSearchParams(body) : body
|
|
137
|
+
}).then(res => {
|
|
138
|
+
if (!res.ok)
|
|
139
|
+
return Promise.reject({
|
|
140
|
+
message: res.statusText,
|
|
141
|
+
code: res.status
|
|
142
|
+
});
|
|
143
|
+
return res;
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
github(path, body, method, headers={}) {
|
|
147
|
+
if (!path)
|
|
148
|
+
path = `https://api.github.com/repos/${this.credentials.repo}`;
|
|
149
|
+
else if (!path.startsWith('http'))
|
|
150
|
+
path = `https://api.github.com/repos/${this.credentials.repo}/${path}`;
|
|
151
|
+
if (body) {
|
|
152
|
+
if (method === 'POST')
|
|
153
|
+
body = JSON.stringify(body);
|
|
154
|
+
else{
|
|
155
|
+
path += '?'+new URLSearchParams(body);
|
|
156
|
+
body = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (this.credentials.token)
|
|
160
|
+
headers.authorization = `Bearer ${this.credentials.token}`;
|
|
161
|
+
return fetch(path, { method, headers, body }).then(res => {
|
|
162
|
+
if (!res.ok)
|
|
163
|
+
return Promise.reject({
|
|
164
|
+
message: res.statusText,
|
|
165
|
+
code: res.status
|
|
166
|
+
});
|
|
167
|
+
return res;
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
getCommits(repo, page=1, per_page=100) {
|
|
171
|
+
return fetch(`https://gitlab.com/api/v4/projects/${repo}/repository/commits?`+new URLSearchParams({ page, per_page })).then(res => res.json());
|
|
172
|
+
};
|
|
173
|
+
getWorkflowId() {
|
|
174
|
+
return this.github('actions/workflows').then(res => res.json()).then(json => {
|
|
175
|
+
return json.workflows?.find(workflow => workflow.path.endsWith('build.yml'))?.id;
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
async getRunId(id, code='success', per_page='1') {
|
|
179
|
+
if (!id)
|
|
180
|
+
id = await this.getWorkflowId();
|
|
181
|
+
return this.github(`actions/workflows/${id}/runs`, {
|
|
182
|
+
status: 'success',
|
|
183
|
+
branch: this.credentials.branch || 'main',
|
|
184
|
+
per_page: 1
|
|
185
|
+
}).then(res => res.json()).then(json => json.workflow_runs[0]?.id);
|
|
186
|
+
};
|
|
187
|
+
async getArtifact(id) {
|
|
188
|
+
if (!id)
|
|
189
|
+
id = await this.getRunId();
|
|
190
|
+
return this.github(`actions/runs/${id}/artifacts`).then(res => res.json()).then(json => {
|
|
191
|
+
const artifact = json.artifacts?.find(artifact => !artifact.expired);
|
|
192
|
+
if (artifact)
|
|
193
|
+
return artifact;
|
|
194
|
+
throw new Error('Firmware not found');
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
async getChangelog(from_id, to_id) {
|
|
198
|
+
if (!from_id)
|
|
199
|
+
from_id = (await this.nvram('firmver_sub')).split('_')[1];
|
|
200
|
+
if (!to_id)
|
|
201
|
+
to_id = (await this.getArtifact())?.name.split('-').slice(-1)[0];
|
|
202
|
+
if (to_id.length > 7)
|
|
203
|
+
to_id = to_id.slice(0, 7);
|
|
204
|
+
if (from_id != to_id)
|
|
205
|
+
return this.github('contents/variables', undefined, undefined, {
|
|
206
|
+
accept: 'application/vnd.github.raw+json'
|
|
207
|
+
}).then(res => res.text()).then(text => {
|
|
208
|
+
let [ padavan_repo ] = text.match(new RegExp('^PADAVAN_REPO="(.*?)"$', 'm'))?.slice(1) || [];
|
|
209
|
+
if (!padavan_repo)
|
|
210
|
+
throw new Error('PADAVAN_REPO not found in variables');
|
|
211
|
+
const firstSlashIndex = padavan_repo.indexOf('/', padavan_repo.indexOf('//') + 2);
|
|
212
|
+
const host = padavan_repo.substring(padavan_repo.indexOf('//') + 2, firstSlashIndex);
|
|
213
|
+
if (host !== 'gitlab.com')
|
|
214
|
+
throw new Error('Currently only gitlab.com is supported');
|
|
215
|
+
padavan_repo = encodeURIComponent(padavan_repo.substring(firstSlashIndex + 1, padavan_repo.length - 4));
|
|
216
|
+
const data = [];
|
|
217
|
+
return this.getCommits(padavan_repo).then(arr => {
|
|
218
|
+
for (const json of arr) {
|
|
219
|
+
if (json.id.startsWith(from_id))
|
|
220
|
+
break;
|
|
221
|
+
if (json.id.startsWith(to_id) || data.length)
|
|
222
|
+
data.push(json.title);
|
|
223
|
+
}
|
|
224
|
+
return { from_id, to_id, data };
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
startReboot() {
|
|
229
|
+
return this.exec('reboot');
|
|
230
|
+
};
|
|
231
|
+
startSpeedTest() {
|
|
232
|
+
const shell = fs.readFileSync(path.join(import.meta.dirname, 'services', 'speedtest.sh')).toString();
|
|
233
|
+
return this.exec(shell).then(ini.parse).then(res => ({
|
|
234
|
+
networkDownloadSpeedMbps: Number(parseFloat(res.download / 125000).toFixed(2)),
|
|
235
|
+
networkUploadSpeedMbps: Number(parseFloat(res.upload / 125000).toFixed(2))
|
|
236
|
+
}));
|
|
237
|
+
};
|
|
238
|
+
async startBuild(id) {
|
|
239
|
+
if (!id)
|
|
240
|
+
id = await this.getWorkflowId();
|
|
241
|
+
return this.github(`actions/workflows/${id}/dispatches`, {
|
|
242
|
+
ref: this.credentials.branch || 'main'
|
|
243
|
+
}, 'POST').then(() => id);
|
|
244
|
+
};
|
|
245
|
+
async startUpgrade(id) {
|
|
246
|
+
if (!id)
|
|
247
|
+
id = (await this.getArtifact())?.id;
|
|
248
|
+
return this.github(`actions/artifacts/${id}/zip`).then(res => res.arrayBuffer()).then(jszip.loadAsync).then(zip => {
|
|
249
|
+
const [ firmware, config ] = Object.values(zip.files);
|
|
250
|
+
return firmware.async('blob').then(blob => {
|
|
251
|
+
const data = new FormData();
|
|
252
|
+
data.append('file', blob, firmware.name);
|
|
253
|
+
return this.router('upgrade.cgi', data, 'POST').then(res => res.text()).then(text => text.includes('showUpgradeBar'));
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
async getForks(page=1, result=[]) {
|
|
258
|
+
const repo = await this.github().then(res => res.json());
|
|
259
|
+
const source = repo.source?.full_name || repo.full_name;
|
|
260
|
+
const res = await this.github(`https://api.github.com/repos/${source}/forks`, {
|
|
261
|
+
page,
|
|
262
|
+
per_page: 100
|
|
263
|
+
});
|
|
264
|
+
const forks = await res.json();
|
|
265
|
+
result.push(...forks);
|
|
266
|
+
if (res.headers.get('Link')?.includes('rel="next"'))
|
|
267
|
+
return await this.getForks(page + 1, result);
|
|
268
|
+
return result;
|
|
269
|
+
};
|
|
270
|
+
async find(id) {
|
|
271
|
+
if (!id)
|
|
272
|
+
id = await this.nvram('productid');
|
|
273
|
+
console.log('get forks list');
|
|
274
|
+
return this.getForks().then(async forks => {
|
|
275
|
+
const result = [];
|
|
276
|
+
for (const fork of forks) {
|
|
277
|
+
try {
|
|
278
|
+
console.log(`get artifacts: ${fork.full_name}`);
|
|
279
|
+
const res = await this.github(`https://api.github.com/repos/${fork.full_name}/actions/artifacts`);
|
|
280
|
+
const { artifacts } = await res.json();
|
|
281
|
+
result.push(...artifacts.filter((artifact, i) => {
|
|
282
|
+
return artifacts.findIndex(cur => artifact.workflow_run.head_branch === cur.workflow_run.head_branch) === i;
|
|
283
|
+
}).filter(artifact => artifact.name.includes(id)).map(artifact => {
|
|
284
|
+
return {
|
|
285
|
+
repo: fork.full_name,
|
|
286
|
+
branch: artifact.workflow_run.head_branch,
|
|
287
|
+
active: !artifact.expired,
|
|
288
|
+
created_at: artifact.created_at,
|
|
289
|
+
name: artifact.name
|
|
290
|
+
};
|
|
291
|
+
}));
|
|
292
|
+
} catch (e) {
|
|
293
|
+
console.error(e);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
export default Padavan;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="padavan-config">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-config-input-name"><i class="fa fa-pencil"></i> Name</label>
|
|
4
|
+
<input type="text" id="node-config-input-name" />
|
|
5
|
+
</div>
|
|
6
|
+
<fieldset>
|
|
7
|
+
<legend>Github</legend>
|
|
8
|
+
<div class="form-row">
|
|
9
|
+
<label for="node-config-input-repo"><i class="fa fa-user"></i> Repo</label>
|
|
10
|
+
<input type="text" id="node-config-input-repo">
|
|
11
|
+
</div>
|
|
12
|
+
<div class="form-row">
|
|
13
|
+
<label for="node-config-input-branch"><i class="fa fa-tag"></i> Branch</label>
|
|
14
|
+
<input type="text" id="node-config-input-branch">
|
|
15
|
+
</div>
|
|
16
|
+
<div class="form-row">
|
|
17
|
+
<label for="node-config-input-token"><i class="fa fa-key"></i> Token</label>
|
|
18
|
+
<input type="password" id="node-config-input-token">
|
|
19
|
+
</div>
|
|
20
|
+
</fieldset>
|
|
21
|
+
<fieldset>
|
|
22
|
+
<legend>Router</legend>
|
|
23
|
+
<div class="form-row">
|
|
24
|
+
<label for="node-config-input-host"><i class="fa fa-globe"></i> Host</label>
|
|
25
|
+
<input type="text" id="node-config-input-host">
|
|
26
|
+
</div>
|
|
27
|
+
<div class="form-row">
|
|
28
|
+
<label for="node-config-input-username"><i class="fa fa-user"></i> Username</label>
|
|
29
|
+
<input type="text" id="node-config-input-username">
|
|
30
|
+
</div>
|
|
31
|
+
<div class="form-row">
|
|
32
|
+
<label for="node-config-input-password"><i class="fa fa-key"></i> Password</label>
|
|
33
|
+
<input type="password" id="node-config-input-password">
|
|
34
|
+
</div>
|
|
35
|
+
</fieldset>
|
|
36
|
+
</script>
|
|
37
|
+
<script type="text/javascript">
|
|
38
|
+
RED.nodes.registerType('padavan-config', {
|
|
39
|
+
category: 'config',
|
|
40
|
+
defaults: {
|
|
41
|
+
name: { value: '' },
|
|
42
|
+
repo: { value: '' },
|
|
43
|
+
branch: { value: '' },
|
|
44
|
+
host: { value: '', required: true }
|
|
45
|
+
},
|
|
46
|
+
credentials: {
|
|
47
|
+
token: { type: 'password' },
|
|
48
|
+
username: {
|
|
49
|
+
type: 'text',
|
|
50
|
+
required: true
|
|
51
|
+
},
|
|
52
|
+
password: {
|
|
53
|
+
type: 'password',
|
|
54
|
+
required: true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
label: function() {
|
|
58
|
+
return this.name || this.branch || this.host || 'Padavan';
|
|
59
|
+
},
|
|
60
|
+
oneditprepare: function() {
|
|
61
|
+
$('#node-config-input-repo').on('change', ev => {
|
|
62
|
+
const regexp = /https:\/\/github\.com\/?([^\/]+\/[^\/]+)(?:\/tree\/(.+))?/;
|
|
63
|
+
const [ repo, branch ] = ev.target.value.match(regexp)?.slice(1) || [];
|
|
64
|
+
if (repo)
|
|
65
|
+
$('#node-config-input-repo').val(repo);
|
|
66
|
+
if (branch)
|
|
67
|
+
$('#node-config-input-branch').val(branch);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
</script>
|
package/nodes/config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module.exports = async function(RED) {
|
|
2
|
+
const { Padavan } = await import('../main.mjs');
|
|
3
|
+
function Config(config) {
|
|
4
|
+
RED.nodes.createNode(this, config);
|
|
5
|
+
this.getClient = async function() {
|
|
6
|
+
return new Padavan({
|
|
7
|
+
...config,
|
|
8
|
+
...this.credentials
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
RED.nodes.registerType('padavan-config', Config, {
|
|
13
|
+
credentials: {
|
|
14
|
+
token: { type: 'password' },
|
|
15
|
+
username: { type: 'text' },
|
|
16
|
+
password: { type: 'password' }
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="padavan-devices">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-settings"><i class="fa fa-cog"></i> Config</label>
|
|
4
|
+
<input id="node-input-settings" />
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-name"><i class="fa fa-pencil"></i> Name</label>
|
|
8
|
+
<input type="text" id="node-input-name" />
|
|
9
|
+
</div>
|
|
10
|
+
</script>
|
|
11
|
+
<script type="text/javascript">
|
|
12
|
+
RED.nodes.registerType('padavan-devices', {
|
|
13
|
+
category: 'Padavan',
|
|
14
|
+
defaults: {
|
|
15
|
+
settings: { value: null, required: true, type: 'padavan-config' },
|
|
16
|
+
name: { value: '' }
|
|
17
|
+
},
|
|
18
|
+
icon: 'font-awesome/fa-list',
|
|
19
|
+
inputs: 1,
|
|
20
|
+
outputs: 1,
|
|
21
|
+
color: '#49afcd',
|
|
22
|
+
paletteLabel: 'Devices',
|
|
23
|
+
label: function() {
|
|
24
|
+
return this.name || 'Devices';
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
</script>
|