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 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,98 @@
1
+ Router management with Padavan firmware
2
+
3
+
4
+ ## Install
5
+
6
+ ``` shell
7
+ npm install padavan
8
+ ```
9
+
10
+
11
+ ## Usage
12
+
13
+ ```javascript
14
+ import Padavan from 'padavan';
15
+ const client = new Padavan({
16
+ repo, branch, token, // GITHUB
17
+ host, username, password // Router
18
+ });
19
+ ```
20
+
21
+
22
+ ## API
23
+
24
+ ### Get current status
25
+ ```javascript
26
+ await client.getStatus();
27
+ // { lavg, uptime, ram, swap, cpu, wifi2, wifi5 }
28
+ ```
29
+
30
+ ### Get traffic history
31
+ ```javascript
32
+ await client.getHistory();
33
+ // { daily_history, monthly_history }
34
+ ```
35
+
36
+ ### Get logs
37
+ ```javascript
38
+ await client.getLog();
39
+ ```
40
+
41
+ ### Get device list
42
+ ```javascript
43
+ await client.getDevices();
44
+ // [{ hostname, ip, mac, rssi }]
45
+ ```
46
+
47
+ ### Get parameters
48
+ ```javascript
49
+ await client.getParams(); // Object with all parameters
50
+ await client.getParams('firmver_sub'); // Only { firmver_sub }
51
+ await client.getParams([ 'firmver_sub', 'ip6_service' ]); // { firmver_sub, ip6_service }
52
+ ```
53
+
54
+ ### Set parameters
55
+ ```javascript
56
+ await client.setParams({
57
+ sid_list: 'IP6Connection;',
58
+ ip6_service: '6in4'
59
+ }); // Enable ipv6
60
+ await client.setParams({
61
+ ip6_service: ''
62
+ }); // Disable ipv6
63
+ ```
64
+
65
+ ### Start SpeedTest on the device
66
+ ```javascript
67
+ await client.startSpeedTest();
68
+ // { networkDownloadSpeedMbps, networkUploadSpeedMbps }
69
+ ```
70
+
71
+ ### Start firmware build in the repository
72
+ ```javascript
73
+ await client.startBuild();
74
+ ```
75
+
76
+ ### Get the firmware changelog
77
+ ```javascript
78
+ await client.getChangelog();
79
+ await client.getChangelog(from_id, to_id);
80
+ // { from_id, to_id, data: [] }
81
+ ```
82
+
83
+ ### Start firmware upgrade
84
+ ```javascript
85
+ await client.startUpgrade();
86
+ ```
87
+
88
+ ### Reboot
89
+ ```javascript
90
+ await client.startReboot();
91
+ ```
92
+
93
+ ### Find repositories with firmware for the device
94
+ ```javascript
95
+ await client.find();
96
+ await client.find(productid);
97
+ // [{ repo, branch, active, created_at, name }]
98
+ ```
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;
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "padavan",
3
+ "version": "1.0.0",
4
+ "description": "Router management with Padavan firmware",
5
+ "main": "main.mjs",
6
+ "files": [
7
+ "services/",
8
+ "main.mjs"
9
+ ],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/alex2844/node-padavan.git"
13
+ },
14
+ "keywords": [
15
+ "nodejs",
16
+ "iot",
17
+ "smart home",
18
+ "router",
19
+ "padavan"
20
+ ],
21
+ "author": "Alex Smith",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "ini": "^4.1.3",
25
+ "jszip": "^3.10.1",
26
+ "ssh2": "^1.15.0"
27
+ }
28
+ }
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+
3
+ size=25000000;
4
+ iterations=5;
5
+ json=$(curl -s https://www.speedtest.net/api/js/servers?limit=1);
6
+ url_upload=$(echo "${json}" | grep -o '"url":"[^"]*"' | sed 's/"url":"\([^"]*\)".*/\1/' | sed 's/\\//g');
7
+ url_download=$(echo "${url_upload}" | sed 's@\(/\([^/]*\)/\([^/]*\)/\).*@\1download?size=@');
8
+ speed_download=0;
9
+ speed_upload=0;
10
+ speed_upload_total=0;
11
+ speed_download_total=0;
12
+ available_space=$(df /tmp/ | awk 'NR==2 {print $4}');
13
+ available_space=$((available_space * 1024 - 1048576));
14
+ if [ -n "$available_space" ] && [ "$available_space" -lt "$size" ]; then
15
+ size=$available_space
16
+ fi
17
+ for i in $(seq 1 $iterations); do
18
+ speed_download=$(curl --connect-timeout 8 "${url_download}${size}" -w "%{speed_download}" -o /tmp/speedtest.bin -s);
19
+ speed_upload=$(curl --connect-timeout 8 -F "file=@/tmp/speedtest.bin" "${url_upload}" -w "%{speed_upload}" -o /dev/null -s);
20
+ speed_upload_total=$((speed_upload_total + speed_upload));
21
+ speed_download_total=$((speed_download_total + speed_download));
22
+ rm -f /tmp/speedtest.bin;
23
+ done
24
+ echo "download=$((speed_download_total / iterations))";
25
+ echo "upload=$((speed_upload_total / iterations))";