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 +98 -0
- package/main.mjs +300 -0
- package/package.json +28 -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,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))";
|