lupine.api 1.1.56 → 1.1.57

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.
@@ -9,6 +9,7 @@ import {
9
9
  formatBytes,
10
10
  downloadStream,
11
11
  ActionSheetSelectPromise,
12
+ encodeHtml,
12
13
  } from 'lupine.components';
13
14
 
14
15
  interface ReleaseListProps {
@@ -290,7 +291,7 @@ export const AdminReleasePage = () => {
290
291
  NotificationMessage.sendMessage(dataResponse.message || 'Failed to refresh cache', NotificationColor.Error);
291
292
  return;
292
293
  }
293
- domLog.value = <pre>{JSON.stringify(dataResponse, null, 2)}</pre>;
294
+ domLog.value = <pre>{encodeHtml(JSON.stringify(dataResponse, null, 2))}</pre>;
294
295
  NotificationMessage.sendMessage('Cache refreshed successfully', NotificationColor.Success);
295
296
  };
296
297
 
@@ -308,9 +309,25 @@ export const AdminReleasePage = () => {
308
309
  return;
309
310
  }
310
311
  }
312
+ };
313
+
314
+ const onShellLocal = async () => {
315
+ return onShell(true);
316
+ };
317
+ const onShellRemote = async () => {
318
+ return onShell(false);
319
+ };
320
+ const onShell = async (isLocal?: boolean) => {
321
+ const data = getDomData();
322
+ if (!isLocal) {
323
+ if (!data.targetUrl || !data.accessToken) {
324
+ NotificationMessage.sendMessage('Please fill in all fields', NotificationColor.Error);
325
+ return;
326
+ }
327
+ }
311
328
 
312
329
  const index = await ActionSheetSelectPromise({
313
- title: 'Restart App (users may get disconnected errors) ?',
330
+ title: 'Run Cmd ?',
314
331
  options: ['OK'],
315
332
  cancelButtonText: 'Cancel',
316
333
  });
@@ -318,17 +335,18 @@ export const AdminReleasePage = () => {
318
335
  return;
319
336
  }
320
337
 
321
- const response = await getRenderPageProps().renderPageFunctions.fetchData('/api/admin/release/restart-app', {
338
+ const response = await getRenderPageProps().renderPageFunctions.fetchData('/api/admin/release/shell', {
322
339
  ...data,
323
340
  isLocal,
341
+ cmd: ref.$('.release-cmd').value,
324
342
  });
325
343
  const dataResponse = await response.json;
326
- console.log('restart-app', dataResponse);
344
+ console.log('shell', dataResponse);
327
345
  if (!dataResponse || dataResponse.status !== 'ok') {
328
- NotificationMessage.sendMessage(dataResponse.message || 'Failed to Restart App', NotificationColor.Error);
346
+ NotificationMessage.sendMessage(dataResponse.message || 'Failed to run cmd', NotificationColor.Error);
329
347
  return;
330
348
  }
331
- domLog.value = <pre>{JSON.stringify(dataResponse, null, 2)}</pre>;
349
+ domLog.value = <pre>{encodeHtml(dataResponse.message) + '\r\n<br>' + encodeHtml(dataResponse.result)}</pre>;
332
350
  NotificationMessage.sendMessage('Restart App successfully', NotificationColor.Success);
333
351
  };
334
352
 
@@ -341,19 +359,19 @@ export const AdminReleasePage = () => {
341
359
  };
342
360
  return (
343
361
  <div ref={ref} css={css} class='admin-release-top'>
344
- <div class='row-box mt1 mb1'>
362
+ <div class='row-box mt-m'>
345
363
  <label class='label mr-m release-label'>Target Url:</label>
346
364
  <div class='w-50p'>
347
365
  <input type='text' class='input-base w-100p target-url' placeholder='Target Url' />
348
366
  </div>
349
367
  </div>
350
- <div class='row-box mt1 mb1'>
368
+ <div class='row-box mt-m'>
351
369
  <label class='label mr-m release-label'>Access token:</label>
352
370
  <div class='w-50p'>
353
371
  <input type='text' class='input-base w-100p access-token' placeholder='Access token' />
354
372
  </div>
355
373
  </div>
356
- <div class='row-box mt1 mb1'>
374
+ <div class='row-box mt-m'>
357
375
  <button onClick={onCheck} class='button-base mr-m'>
358
376
  Check
359
377
  </button>
@@ -370,6 +388,15 @@ export const AdminReleasePage = () => {
370
388
  Restart App (Local)
371
389
  </button>
372
390
  </div>
391
+ <div class='row-box mt-m mb-m'>
392
+ <input type='text' class='input-base w-50p release-cmd' placeholder='Command' />
393
+ <button onClick={onShellRemote} class='button-base color-red'>
394
+ Run Cmd (Remote)
395
+ </button>
396
+ <button onClick={onShellLocal} class='button-base'>
397
+ Run Cmd (Local)
398
+ </button>
399
+ </div>
373
400
  {domUpdate.node}
374
401
  {domLog.node}
375
402
  </div>
@@ -6,7 +6,7 @@ import {
6
6
  getDefaultPageLimit,
7
7
  HtmlVar,
8
8
  PagingLink,
9
- escapeHtml,
9
+ encodeHtml,
10
10
  } from 'lupine.components';
11
11
 
12
12
  const loadData = async (
@@ -34,7 +34,7 @@ const loadData = async (
34
34
  <div class='table'>
35
35
  <div class='fields bg-gray'>
36
36
  {Object.keys(data.json.result[0]).map((field: any, index: number) => {
37
- return <div class='p1 item'>{escapeHtml(field)}</div>;
37
+ return <div class='p1 item'>{encodeHtml(field)}</div>;
38
38
  })}
39
39
  {update && update.onDelete && update.onEdit && <div class='p1 item'>Action</div>}
40
40
  </div>
@@ -43,7 +43,7 @@ const loadData = async (
43
43
  return (
44
44
  <div class='values'>
45
45
  {Object.keys(item).map((field: any, index: number) => {
46
- return <div class='p1 item'>{escapeHtml(item[field])}</div>;
46
+ return <div class='p1 item'>{encodeHtml(item[field])}</div>;
47
47
  })}
48
48
  {update && update.onDelete && update.onEdit && (
49
49
  <div class='p1 item'>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.api",
3
- "version": "1.1.56",
3
+ "version": "1.1.57",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -12,6 +12,7 @@ import {
12
12
  processRefreshCache,
13
13
  apiStorage,
14
14
  processRestartApp,
15
+ processShell,
15
16
  } from 'lupine.api';
16
17
  import path from 'path';
17
18
  import { needDevAdminSession } from './admin-auth';
@@ -39,12 +40,16 @@ export class AdminRelease implements IApiBase {
39
40
  this.router.use('/refresh-cache', needDevAdminSession, this.refreshCache.bind(this));
40
41
  this.router.use('/restart-app', needDevAdminSession, this.restartApp.bind(this));
41
42
 
43
+ this.router.use('/shell', needDevAdminSession, this.shell.bind(this));
44
+
42
45
  // ...ByClient will verify credentials from post, so it doesn't need AdminSession
43
46
  this.router.use('/byClientCheck', this.byClientCheck.bind(this));
44
47
  this.router.use('/byClientUpdate', this.byClientUpdate.bind(this));
45
48
  this.router.use('/byClientRefreshCache', this.byClientRefreshCache.bind(this));
46
49
  this.router.use('/byClientRestartApp', this.byClientRestartApp.bind(this));
47
50
  this.router.use('/byClientViewLog', this.byClientViewLog.bind(this));
51
+
52
+ this.router.use('/byClientShell', this.byClientShell.bind(this));
48
53
  }
49
54
 
50
55
  async viewLog(req: ServerRequest, res: ServerResponse) {
@@ -170,6 +175,48 @@ export class AdminRelease implements IApiBase {
170
175
  return true;
171
176
  }
172
177
 
178
+ async shell(req: ServerRequest, res: ServerResponse) {
179
+ // check whether it's from online admin
180
+ const json = await adminApiHelper.getDevAdminFromCookie(req, res, false);
181
+ const jsonData = req.locals.json();
182
+ if (json && jsonData && !Array.isArray(jsonData) && jsonData.isLocal) {
183
+ const result = await processShell(req);
184
+ const response = {
185
+ status: 'ok',
186
+ message: 'Shell executed successfully.',
187
+ result,
188
+ };
189
+ ApiHelper.sendJson(req, res, response);
190
+ return true;
191
+ }
192
+
193
+ const data = await this.chkData(jsonData, req, res, true);
194
+ if (!data) return true;
195
+
196
+ let targetUrl = data.targetUrl as string;
197
+ if (targetUrl.endsWith('/')) {
198
+ targetUrl = targetUrl.slice(0, -1);
199
+ }
200
+ const remoteData = await fetch(targetUrl + '/api/admin/release/byClientShell', {
201
+ method: 'POST',
202
+ body: JSON.stringify(data),
203
+ });
204
+ const resultText = await remoteData.text();
205
+ let remoteResult: any;
206
+ try {
207
+ remoteResult = JSON.parse(resultText);
208
+ } catch (e: any) {
209
+ remoteResult = { status: 'error', message: resultText };
210
+ }
211
+ const response = {
212
+ status: 'ok',
213
+ message: 'check.',
214
+ ...remoteResult,
215
+ };
216
+ ApiHelper.sendJson(req, res, response);
217
+ return true;
218
+ }
219
+
173
220
  public async chkData(data: any, req: ServerRequest, res: ServerResponse, chkCredential: boolean) {
174
221
  // add access token
175
222
  if (!data || Array.isArray(data) || typeof data !== 'object' || !data.accessToken || !data.targetUrl) {
@@ -486,14 +533,12 @@ export class AdminRelease implements IApiBase {
486
533
  body: JSON.stringify({ ...data, chkOption, index: cnt, size: fileContent.length }) + '\n\n' + chunk,
487
534
  };
488
535
  this.logger.info(
489
- `updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${
490
- fileContent.length
536
+ `updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${fileContent.length
491
537
  }), f: ${sendFile}`
492
538
  );
493
539
  apiStorage.set(
494
540
  releaseProgress,
495
- `updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${
496
- fileContent.length
541
+ `updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${fileContent.length
497
542
  }), f: ${sendFile}`
498
543
  );
499
544
  i > 0 && (await new Promise((resolve) => setTimeout(resolve, 1000)));
@@ -637,4 +682,19 @@ export class AdminRelease implements IApiBase {
637
682
  ApiHelper.sendJson(req, res, response);
638
683
  return true;
639
684
  }
685
+
686
+ async byClientShell(req: ServerRequest, res: ServerResponse) {
687
+ const jsonData = req.locals.json();
688
+ const data = await this.chkData(jsonData, req, res, true);
689
+ if (!data) return true;
690
+
691
+ const result = await processShell(req);
692
+ const response = {
693
+ status: 'ok',
694
+ message: 'Shell executed successfully.',
695
+ result,
696
+ };
697
+ ApiHelper.sendJson(req, res, response);
698
+ return true;
699
+ }
640
700
  }
@@ -1,4 +1,4 @@
1
- import { spawn, ChildProcessWithoutNullStreams } from 'child_process';
1
+ import { spawn, exec, ChildProcessWithoutNullStreams } from 'child_process';
2
2
  import os from 'os';
3
3
  import { MiniWebSocket } from './mini-web-socket';
4
4
  import { Duplex } from 'stream';
@@ -13,7 +13,7 @@ export class ShellService {
13
13
  this._socket = socket;
14
14
  this._miniWebSocket = miniWebSocket;
15
15
  try {
16
- const shellCmd: string = this.getDefaultShell();
16
+ const shellCmd: string = ShellService.getDefaultShell();
17
17
  this._shell = spawn(shellCmd, [], {
18
18
  stdio: ['pipe', 'pipe', 'pipe'],
19
19
  });
@@ -35,7 +35,7 @@ export class ShellService {
35
35
  });
36
36
  }
37
37
 
38
- getDefaultShell() {
38
+ static getDefaultShell() {
39
39
  const platform = os.platform();
40
40
  if (platform === 'win32') {
41
41
  return process.env.COMSPEC || 'cmd.exe';
@@ -63,4 +63,12 @@ export class ShellService {
63
63
  this._miniWebSocket.sendMessage(this._socket!, 'Shell is not available.');
64
64
  }
65
65
  }
66
+
67
+ public static async directCmd(cmd: string): Promise<string> {
68
+ return new Promise((resolve) => {
69
+ exec(cmd, { shell: this.getDefaultShell() }, (error, stdout, stderr) => {
70
+ resolve(stdout + stderr);
71
+ });
72
+ });
73
+ }
66
74
  }
@@ -6,6 +6,7 @@ import { appHelper } from './app-helper';
6
6
  import { DebugService } from '../api/debug-service';
7
7
  import { AppCacheGlobal, AppCacheKeys, getAppCache, ServerRequest } from '../models';
8
8
  import { cleanupAndExit } from './cleanup-exit';
9
+ import { ShellService } from '../api/shell-service';
9
10
  const logger = new Logger('process-dev-requests');
10
11
 
11
12
  function deleteRequireCache(moduleName: string) {
@@ -76,6 +77,16 @@ export async function processRestartApp(req: ServerRequest) {
76
77
  }
77
78
  }
78
79
 
80
+ export async function processShell(req: ServerRequest) {
81
+ const data = req.locals.json();
82
+ if (!data || Array.isArray(data) || !data.cmd) {
83
+ return 'Wrong data.';
84
+ }
85
+ const cmd = data.cmd as string;
86
+ const shell = await ShellService.directCmd(cmd);
87
+ return shell;
88
+ }
89
+
79
90
  // this is called from a request in debug mode
80
91
  export async function processDevRequests(req: ServerRequest, res: ServerResponse, rootUrl?: string) {
81
92
  res.end();