padavan 2.1.0 → 2.2.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/README.md CHANGED
@@ -70,6 +70,10 @@ The constructor accepts a config object with the following properties:
70
70
  - **`exec(command)`**
71
71
  Executes a system command via the web console emulator (`SystemCmd`).
72
72
  *Returns:* Command output (stdout + stderr).
73
+ - **`sendAction(action, payload?)`**
74
+ Sends a low-level action to `apply.cgi`.
75
+ *Arguments:* `action` (string, e.g., `' Reboot '`), `payload` (data object).
76
+ *Returns:* Server response (command output or status).
73
77
  - **`startReboot()`**
74
78
  Reboots the router via HTTP command.
75
79
 
package/bin/cli.js CHANGED
@@ -4,8 +4,8 @@ import yargs from 'yargs';
4
4
  import { hideBin } from 'yargs/helpers';
5
5
  import Padavan from '../src/index.js';
6
6
  import { formatBytes, formatUptime } from '../src/utils/formatting.js';
7
- import { DEFAULT_HTTP_CONFIG, DEFAULT_FIRMWARE_REPO, ACTION_MODE, SERVICE_ID, GROUP_ID } from '../src/constants.js';
8
- /** @import { ActionMode, ServiceId, GroupId } from '../src/constants.js' */
7
+ import { DEFAULT_HTTP_CONFIG, DEFAULT_FIRMWARE_REPO, SYSTEM_ACTION, CONFIG_ACTION, SERVICE_ID, GROUP_ID, WIFI_BANDS } from '../src/constants.js';
8
+ /** @import { SystemAction, ConfigAction, ServiceId, GroupId, WifiBand } from '../src/constants.js' */
9
9
  /** @import { ArgumentsCamelCase } from 'yargs' */
10
10
  /** @import { Device, WifiNetwork, ChannelAnalysis } from '../src/index.js' */
11
11
 
@@ -28,6 +28,8 @@ import { DEFAULT_HTTP_CONFIG, DEFAULT_FIRMWARE_REPO, ACTION_MODE, SERVICE_ID, GR
28
28
  * @property {string} [model]
29
29
  */
30
30
 
31
+ const SYSTEM_ACTION_VALUES = Object.values(SYSTEM_ACTION);
32
+ const CONFIG_ACTION_VALUES = Object.values(CONFIG_ACTION);
31
33
  const logInfo = (/** @type {string} */ msg) => console.error(msg);
32
34
 
33
35
  /**
@@ -117,8 +119,8 @@ cli.command('traffic', 'Get traffic history', {}, async (/** @type {ArgumentsCam
117
119
  // --- WIFI DOCTOR ---
118
120
 
119
121
  cli.command('scan [band]', 'Scan Wi-Fi networks', (yargs) => {
120
- return yargs.positional('band', { type: 'string', choices: ['2.4', '5'], default: '2.4' });
121
- }, async (/** @type {ArgumentsCamelCase<CommonArgs & {band: '2.4'|'5'}>} */ argv) => {
122
+ return yargs.positional('band', { type: 'string', choices: WIFI_BANDS, default: WIFI_BANDS[0] });
123
+ }, async (/** @type {ArgumentsCamelCase<CommonArgs & {band: WifiBand}>} */ argv) => {
122
124
  const client = getClient(argv);
123
125
  logInfo(`Scanning ${argv.band}GHz networks...`);
124
126
  const networks = await client.startScan(argv.band);
@@ -131,8 +133,8 @@ cli.command('scan [band]', 'Scan Wi-Fi networks', (yargs) => {
131
133
  });
132
134
 
133
135
  cli.command('doctor [band]', 'Analyze Wi-Fi environment and recommend channel', (yargs) => {
134
- return yargs.positional('band', { type: 'string', choices: ['2.4', '5'], default: '2.4' });
135
- }, async (/** @type {ArgumentsCamelCase<CommonArgs & {band: '2.4'|'5'}>} */ argv) => {
136
+ return yargs.positional('band', { type: 'string', choices: WIFI_BANDS, default: WIFI_BANDS[0] });
137
+ }, async (/** @type {ArgumentsCamelCase<CommonArgs & {band: WifiBand}>} */ argv) => {
136
138
  const client = getClient(argv);
137
139
  logInfo(`Scanning and analyzing ${argv.band}GHz spectrum...`);
138
140
  const result = await client.getBestChannel(argv.band);
@@ -164,17 +166,17 @@ cli.command('set <pairs..>', 'Set parameters (key=value)', (yargs) => {
164
166
  .option('action', {
165
167
  type: 'string',
166
168
  describe: 'Action mode',
167
- default: 'Apply',
169
+ default: CONFIG_ACTION.APPLY.trim(),
170
+ choices: CONFIG_ACTION_VALUES.map(m => m.trim()),
168
171
  coerce: (arg) => {
169
- const clean = arg.trim();
170
- const match = ACTION_MODE.map(m => m.trim()).find(m => m.toLowerCase() === clean.toLowerCase());
172
+ const clean = arg.trim().toLowerCase();
173
+ const match = CONFIG_ACTION_VALUES.map(m => m.trim()).find(m => m.toLowerCase() === clean);
171
174
  return match || arg;
172
- },
173
- choices: ACTION_MODE.map(m => m.trim())
175
+ }
174
176
  })
175
177
  .example('$0 set rt_ssid=MyWifi --page "Advanced_WAdvanced_Content.asp"', 'Apply settings via Web UI')
176
178
  .example('$0 set "rt_ACLList=AA:BB:CC:DD:EE:FF" --action Add --group rt_ACLList', 'Add MAC to filter');
177
- }, async (/** @type {ArgumentsCamelCase<CommonArgs & {pairs: string[], sid?: string|ServiceId[], page?: string, action?: ActionMode, group?: GroupId, script?: string}>} */ argv) => {
179
+ }, async (/** @type {ArgumentsCamelCase<CommonArgs & {pairs: string[], sid?: string|ServiceId[], page?: string, action?: ConfigAction, group?: GroupId, script?: string}>} */ argv) => {
178
180
  const client = getClient(argv);
179
181
  const /** @type {Record<string, string>} */ params = {};
180
182
  argv.pairs.forEach(p => {
@@ -182,7 +184,7 @@ cli.command('set <pairs..>', 'Set parameters (key=value)', (yargs) => {
182
184
  if (k)
183
185
  params[k] = v.join('=');
184
186
  });
185
- const action_mode = ACTION_MODE.find(m => m.trim() === argv.action) || argv.action;
187
+ const action_mode = CONFIG_ACTION_VALUES.find(m => m.trim() === argv.action) || argv.action;
186
188
  await client.setParams(params, {
187
189
  action_mode,
188
190
  action_script: argv.script,
@@ -193,6 +195,40 @@ cli.command('set <pairs..>', 'Set parameters (key=value)', (yargs) => {
193
195
  logInfo('Settings applied successfully.');
194
196
  });
195
197
 
198
+ cli.command('call <action> [payload...]', 'Call a system action', (yargs) => {
199
+ return yargs
200
+ .positional('action', {
201
+ describe: 'System action to execute',
202
+ choices: SYSTEM_ACTION_VALUES.map(v => v.trim()),
203
+ coerce: (arg) => {
204
+ const clean = arg.trim().toLowerCase();
205
+ const match = SYSTEM_ACTION_VALUES.map(m => m.trim()).find(v => v.toLowerCase() === clean);
206
+ return match;
207
+ }
208
+ })
209
+ .example('$0 call SystemCmd ls -la /tmp', 'Execute a shell command')
210
+ .example('$0 call wg_action action=genkey', 'Pass payload for wg_action');
211
+ }, async (/** @type {ArgumentsCamelCase<CommonArgs & {action: SystemAction, payload?: string[]}>} */ argv) => {
212
+ const client = getClient(argv);
213
+ const action = SYSTEM_ACTION_VALUES.find(m => m.trim() === argv.action) || argv.action;
214
+ const payload = {};
215
+ if (argv.payload && argv.payload.length > 0) {
216
+ if (action === SYSTEM_ACTION.SYSTEM_CMD)
217
+ payload.SystemCmd = argv.payload.join(' ');
218
+ else
219
+ argv.payload.forEach(p => {
220
+ const [key, ...valParts] = p.split('=');
221
+ if (key)
222
+ payload[key] = valParts.join('=');
223
+ });
224
+ }
225
+ logInfo(`Executing action: ${action.trim()}`);
226
+ if (Object.keys(payload).length > 0)
227
+ logInfo(`With payload: ${JSON.stringify(payload)}`);
228
+ const result = await client.sendAction(action, payload);
229
+ printOutput(argv, result);
230
+ });
231
+
196
232
  // --- FIRMWARE ---
197
233
 
198
234
  cli.command('firmware <action>', 'Manage firmware', (yargs) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "padavan",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The core library for interacting with routers running Padavan firmware. Provides a programmatic API for local control via HTTP.",
package/src/constants.js CHANGED
@@ -32,17 +32,57 @@ export const NVRAM_CACHE_TTL = 3_000;
32
32
 
33
33
  /**
34
34
  * Режимы действия для apply.cgi.
35
- * Обратите внимание: большинство команд требуют пробелы по краям.
36
- * @typedef {(typeof ACTION_MODE)[number]} ActionMode
35
+ * @typedef {(typeof SYSTEM_ACTION)[keyof typeof SYSTEM_ACTION]} SystemAction
37
36
  */
38
- export const ACTION_MODE = /** @type {const} */ ([
39
- ' Apply ', ' Restart ', ' Reboot ', ' Shutdown ',
40
- ' Add ', ' Del ', ' ClearLog ', ' SystemCmd ',
41
- ' CommitFlash ', ' RestoreNVRAM ', ' RestoreStorage ', ' FreeMemory ',
42
- ' NTPSyncNow ', ' CreateCertHTTPS ', ' CheckCertHTTPS ',
43
- ' CreateCertOVPNS ', ' ExportConfOVPNC ', ' ExportWGConf ',
44
- ' wg_action ', 'Update'
45
- ]);
37
+ export const SYSTEM_ACTION = /** @type {const} */ ({
38
+ /** Полная перезагрузка роутера */
39
+ REBOOT: ' Reboot ',
40
+ /** Выключение роутера */
41
+ SHUTDOWN: ' Shutdown ',
42
+ /** Очистить системный лог */
43
+ CLEAR_LOG: ' ClearLog ',
44
+ /** Выполнить системную команду */
45
+ SYSTEM_CMD: ' SystemCmd ',
46
+ /** Сохранить NVRAM во флеш-память */
47
+ COMMIT_FLASH: ' CommitFlash ',
48
+ /** Сброс настроек к заводским */
49
+ RESTORE_NVRAM: ' RestoreNVRAM ',
50
+ /** Восстановить /etc/storage из сжатого файла */
51
+ RESTORE_STORAGE: ' RestoreStorage ',
52
+ /** Сбросить кэши памяти */
53
+ FREE_MEMORY: ' FreeMemory ',
54
+ /** Принудительная синхронизация времени */
55
+ NTP_SYNC_NOW: ' NTPSyncNow ',
56
+ /** Генерировать сертификат для HTTPS */
57
+ CREATE_CERT_HTTPS: ' CreateCertHTTPS ',
58
+ /** Проверка наличия SSL сертификата */
59
+ CHECK_CERT_HTTPS: ' CheckCertHTTPS ',
60
+ /** Генерировать ключи/сертификаты для OpenVPN сервера */
61
+ CREATE_CERT_OVPNS: ' CreateCertOVPNS ',
62
+ /** Экспорт конфига OpenVPN клиента */
63
+ EXPORT_CONF_OVPNC: ' ExportConfOVPNC ',
64
+ /** Экспорт конфига WireGuard */
65
+ EXPORT_WG_CONF: ' ExportWGConf ',
66
+ /** Действия WireGuard (требует доп. параметр action: genkey, pubkey, genpsk). */
67
+ WG_ACTION: ' wg_action '
68
+ });
69
+
70
+ /**
71
+ * Режимы действия для start_apply.htm.
72
+ * @typedef {(typeof CONFIG_ACTION)[keyof typeof CONFIG_ACTION]} ConfigAction
73
+ */
74
+ export const CONFIG_ACTION = /** @type {const} */ ({
75
+ /** Применить настройки (без перезагрузки всего роутера, если возможно) */
76
+ APPLY: ' Apply ',
77
+ /** Применить настройки с явным перезапуском связанных сервисов */
78
+ RESTART: ' Restart ',
79
+ /** Добавить запись в список (используется с group_id) */
80
+ ADD: ' Add ',
81
+ /** Удалить запись из списка (используется с group_id) */
82
+ DEL: ' Del ',
83
+ /** Используется в связке с action_script (например, для обновления DDNS или статуса принтера) */
84
+ UPDATE: 'Update'
85
+ });
46
86
 
47
87
  /**
48
88
  * Идентификаторы сервисов (Service ID).
@@ -66,6 +106,12 @@ export const GROUP_ID = /** @type {const} */ ([
66
106
  'LWFilterList', 'VPNSACLList'
67
107
  ]);
68
108
 
109
+ /**
110
+ * Диапазоны Wi-Fi.
111
+ * @typedef {(typeof WIFI_BANDS)[number]} WifiBand
112
+ */
113
+ export const WIFI_BANDS = /** @type {const} */ (['2.4', '5']);
114
+
69
115
  /** Системные команды роутера. */
70
116
  export const COMMANDS = {
71
117
  NVRAM_SHOW: 'nvram showall'
@@ -93,6 +139,8 @@ export const PAGES = {
93
139
  SYSLOG: 'Main_LogStatus_Content.asp',
94
140
  /** Страница обновления прошивки (POST) */
95
141
  UPGRADE: 'upgrade.cgi',
96
- /** Основная точка входа для применения настроек (POST) */
97
- APPLY: 'apply.cgi'
142
+ /** Обработчик мгновенных действий (Reboot, Shell, AJAX) без редиректа */
143
+ APPLY: 'apply.cgi',
144
+ /** Обработчик применения настроек с сохранением NVRAM и перезапуском сервисов */
145
+ START_APPLY: 'start_apply.htm'
98
146
  };
package/src/index.js CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  parsePageInputs, parseNvramOutput, parseLooseJson, normalizeTrafficHistory,
8
8
  extractJsVariable, extractTextareaValue, extractMacsFromTextarea, extractCurrentChannel
9
9
  } from './utils/parsers.js';
10
- import { LOG_LEVELS, DEFAULT_LOG_LEVEL, LIB_ID, NVRAM_CACHE_TTL, PAGES, COMMANDS } from './constants.js';
11
- /** @import { ActionMode, ServiceId, GroupId } from './constants.js' */
10
+ import { LOG_LEVELS, DEFAULT_LOG_LEVEL, LIB_ID, NVRAM_CACHE_TTL, SYSTEM_ACTION, PAGES, COMMANDS } from './constants.js';
11
+ /** @import { SystemAction, ConfigAction, ServiceId, GroupId, WifiBand } from './constants.js' */
12
12
  /** @import { Config as HttpConfig } from './transport/http.js' */
13
13
  /** @import { Config as GithubConfig } from './transport/github.js' */
14
14
  /** @typedef {HttpConfig & GithubConfig} Credentials */
@@ -52,7 +52,7 @@ import { LOG_LEVELS, DEFAULT_LOG_LEVEL, LIB_ID, NVRAM_CACHE_TTL, PAGES, COMMANDS
52
52
  * @property {string} [next_page] Страница перенаправления.
53
53
  * @property {string|ServiceId[]} [sid_list] Строка или массив сервисов для перезапуска.
54
54
  * @property {GroupId} [group_id] ID группы.
55
- * @property {ActionMode} [action_mode=' Apply '] Режим действия.
55
+ * @property {ConfigAction} [action_mode=' Apply '] Режим действия.
56
56
  * @property {string} [action_script] Имя скрипта.
57
57
  */
58
58
 
@@ -150,24 +150,7 @@ export default class Padavan {
150
150
  * @returns {Promise<string>} Вывод команды (stdout + stderr).
151
151
  */
152
152
  async exec(command) {
153
- const result = this.#commandQueue.then(async () => {
154
- this.log('debug', `Executing command: ${command}`);
155
- if (!this.#http)
156
- throw new Error('HTTP client not initialized');
157
- try {
158
- await this.#http.post(PAGES.APPLY, {
159
- action_mode: ' SystemCmd ',
160
- SystemCmd: command
161
- });
162
- const response = await this.#http.get(PAGES.CONSOLE_RESPONSE);
163
- return response.trim();
164
- } catch (e) {
165
- this.log('error', `Command execution failed: ${command}`, e);
166
- throw e;
167
- }
168
- });
169
- this.#commandQueue = result.catch(() => {});
170
- return result;
153
+ return this.sendAction(SYSTEM_ACTION.SYSTEM_CMD, { SystemCmd: command });
171
154
  };
172
155
 
173
156
  /**
@@ -224,19 +207,25 @@ export default class Padavan {
224
207
  const kvPairs = Object.entries(params);
225
208
  if (kvPairs.length === 0)
226
209
  return;
210
+
227
211
  const page = options.current_page || options.next_page;
212
+ let sid_list = options.sid_list;
213
+ let group_id = options.group_id || '';
228
214
 
229
- let sidList = options.sid_list;
230
- if (Array.isArray(sidList))
231
- sidList = sidList.join(';') + ';';
232
- else if (!sidList && page) {
215
+ if (Array.isArray(sid_list))
216
+ sid_list = sid_list.join(';') + ';';
217
+ else if (!sid_list && page) {
233
218
  this.log('debug', `sid_list not provided, searching on ${page}...`);
234
219
  try {
235
220
  const html = await this.#http.get(page);
236
221
  const pageInputs = parsePageInputs(html);
237
222
  if (pageInputs['sid_list']) {
238
- sidList = pageInputs['sid_list'];
239
- this.log('debug', `Found sid_list: ${sidList}`);
223
+ sid_list = pageInputs['sid_list'];
224
+ this.log('debug', `Found sid_list: ${sid_list}`);
225
+ if (!group_id && pageInputs['group_id']) {
226
+ group_id = pageInputs['group_id'];
227
+ this.log('debug', `Auto-detected group_id: ${group_id}`);
228
+ }
240
229
  } else
241
230
  this.log('warn', `Could not find sid_list on ${page}`);
242
231
  } catch (e) {
@@ -248,18 +237,18 @@ export default class Padavan {
248
237
  this.#nvramPromise = null;
249
238
  this.#nvramTimestamp = 0;
250
239
 
251
- if (sidList) {
240
+ if (sid_list) {
252
241
  const data = {
242
+ sid_list, group_id,
253
243
  action_mode: options.action_mode || ' Apply ',
254
244
  action_script: options.action_script || '',
255
- sid_list: sidList,
256
- group_id: options.group_id || '',
257
245
  current_page: page || 'index.asp',
258
246
  next_page: options.next_page || page || 'index.asp',
259
247
  ...params
260
248
  };
249
+ this.log('debug', `POST ${PAGES.START_APPLY} payload:`, data);
261
250
  try {
262
- await this.#http.post(PAGES.APPLY, data);
251
+ await this.#http.post(PAGES.START_APPLY, data);
263
252
  return true;
264
253
  } catch (e) {
265
254
  this.log('debug', 'setParams (UI) finished', e.message);
@@ -278,6 +267,34 @@ export default class Padavan {
278
267
  return true;
279
268
  };
280
269
 
270
+ /**
271
+ * Отправляет системную команду на apply.cgi и возвращает результат.
272
+ * @param {SystemAction} action Режим действия.
273
+ * @param {Object} [payload] Дополнительные данные.
274
+ * @returns {Promise<string>} Тело ответа от сервера.
275
+ */
276
+ async sendAction(action, payload = {}) {
277
+ const result = this.#commandQueue.then(async () => {
278
+ this.log('info', `Sending system action: ${action}`);
279
+ try {
280
+ const result = await this.#http.post(PAGES.APPLY, {
281
+ action_mode: action,
282
+ ...payload
283
+ });
284
+ if (action === SYSTEM_ACTION.SYSTEM_CMD) {
285
+ const response = await this.#http.get(PAGES.CONSOLE_RESPONSE);
286
+ return response.trim();
287
+ }
288
+ return result;
289
+ } catch (e) {
290
+ this.log('error', `Sending system action failed: ${action}`, e);
291
+ throw e;
292
+ }
293
+ });
294
+ this.#commandQueue = result.catch(() => {});
295
+ return result;
296
+ };
297
+
281
298
  /**
282
299
  * Получает текущий статус системы (CPU, RAM, Uptime, LoadAvg).
283
300
  * @returns {Promise<Object>}
@@ -376,7 +393,7 @@ export default class Padavan {
376
393
  /**
377
394
  * Сканирование эфира.
378
395
  * ВНИМАНИЕ: Если вы подключены по Wi-Fi к сканируемому диапазону, соединение разорвется.
379
- * @param {'2.4'|'5'} [band='2.4'] Частотный диапазон.
396
+ * @param {WifiBand} [band='2.4'] Частотный диапазон.
380
397
  * @returns {Promise<WifiNetwork[]>} Список найденных сетей, отсортированный по уровню сигнала.
381
398
  */
382
399
  async startScan(band = '2.4') {
@@ -409,7 +426,7 @@ export default class Padavan {
409
426
 
410
427
  /**
411
428
  * Анализирует эфир и предлагает лучший канал с учетом совместимости и региона.
412
- * @param {'2.4'|'5'} [band='2.4'] Частотный диапазон.
429
+ * @param {WifiBand} [band='2.4'] Частотный диапазон.
413
430
  * @param {WifiNetwork[]} [scanResults] Опционально: результаты сканирования.
414
431
  * @returns {Promise<ChannelAnalysis>} Результат анализа.
415
432
  */
@@ -561,7 +578,7 @@ export default class Padavan {
561
578
  this.log('warn', 'Rebooting router via HTTP...');
562
579
  try {
563
580
  await this.#http.post(PAGES.APPLY, {
564
- action_mode: ' Reboot '
581
+ action_mode: SYSTEM_ACTION.REBOOT
565
582
  });
566
583
  } catch (e) {
567
584
  this.log('debug', 'Reboot request sent (network error expected)', e.message);