hfs 0.47.3 → 0.48.0-alpha2

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.
Files changed (41) hide show
  1. package/admin/assets/index-1c2b890b.css +1 -0
  2. package/admin/assets/index-344723f8.js +520 -0
  3. package/{frontend/assets/sha512-108e3b0a.js → admin/assets/sha512-136b5a7c.js} +1 -1
  4. package/admin/index.html +2 -2
  5. package/frontend/assets/index-05dfd82f.css +1 -0
  6. package/frontend/assets/{index-7eec0199.js → index-12411ff6.js} +15 -15
  7. package/{admin/assets/sha512-0935647a.js → frontend/assets/sha512-549e6c3a.js} +1 -1
  8. package/frontend/index.html +2 -2
  9. package/package.json +20 -13
  10. package/plugins/vhosting/plugin.js +15 -15
  11. package/src/adminApis.js +7 -4
  12. package/src/api.auth.js +0 -3
  13. package/src/api.file_list.js +8 -7
  14. package/src/api.lang.js +1 -1
  15. package/src/api.net.js +136 -0
  16. package/src/api.plugins.js +31 -36
  17. package/src/api.vfs.js +1 -1
  18. package/src/apiMiddleware.js +22 -20
  19. package/src/config.js +2 -16
  20. package/src/const.js +4 -2
  21. package/src/cross.js +221 -0
  22. package/src/customHtml.js +7 -22
  23. package/src/debounceAsync.js +12 -9
  24. package/src/frontEndApis.js +2 -4
  25. package/src/github.js +86 -40
  26. package/src/langs/embedded.js +2 -1
  27. package/src/langs/hfs-lang-de.json +131 -0
  28. package/src/langs/hfs-lang-it.json +1 -1
  29. package/src/listen.js +44 -37
  30. package/src/log.js +8 -5
  31. package/src/middlewares.js +1 -3
  32. package/src/misc.js +33 -137
  33. package/src/plugins.js +46 -26
  34. package/src/serveFile.js +1 -0
  35. package/src/update.js +1 -1
  36. package/src/upload.js +2 -1
  37. package/src/util-http.js +11 -8
  38. package/admin/assets/index-62247236.css +0 -1
  39. package/admin/assets/index-ca5ac85e.js +0 -518
  40. package/frontend/assets/index-0ea37f5f.css +0 -1
  41. package/src/util-generators.js +0 -31
@@ -1,4 +1,4 @@
1
- import{g as OF,c as UF}from"./index-ca5ac85e.js";function gF(sF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in sF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(sF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(sF,Symbol.toStringTag,{value:"Module"}))}var dF={exports:{}};/*
1
+ import{g as OF,c as UF}from"./index-12411ff6.js";function gF(sF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in sF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(sF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(sF,Symbol.toStringTag,{value:"Module"}))}var dF={exports:{}};/*
2
2
  * [js-sha512]{@link https://github.com/emn178/js-sha512}
3
3
  *
4
4
  * @version 0.8.0
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
6
6
  <link href="/fontello.css" rel="stylesheet" />
7
7
 
8
- <script type="module" crossorigin src="/assets/index-7eec0199.js"></script>
9
- <link rel="stylesheet" href="/assets/index-0ea37f5f.css">
8
+ <script type="module" crossorigin src="/assets/index-12411ff6.js"></script>
9
+ <link rel="stylesheet" href="/assets/index-05dfd82f.css">
10
10
  </head>
11
11
  <body>
12
12
  <noscript>You need to enable JavaScript to run this app.</noscript>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.47.3",
3
+ "version": "0.48.0-alpha2",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -9,7 +9,12 @@
9
9
  "homepage": "https://rejetto.com/hfs",
10
10
  "license": "GPL-3.0",
11
11
  "author": "Massimo Melina <a@rejetto.com>",
12
- "workspaces": [ "admin", "frontend", "shared", "mui-grid-form" ],
12
+ "workspaces": [
13
+ "admin",
14
+ "frontend",
15
+ "shared",
16
+ "mui-grid-form"
17
+ ],
13
18
  "scripts": {
14
19
  "watch-server": "cross-env DEV=1 nodemon --ignore tests/ --watch src -e ts,tsx --exec ts-node src",
15
20
  "watch-server-proxied": "cross-env FRONTEND_PROXY=3005 ADMIN_PROXY=3006 npm run watch-server",
@@ -25,10 +30,10 @@
25
30
  "pub": "cd dist && npm publish",
26
31
  "dist": "npm run build-all && npm run dist-bin",
27
32
  "dist-bin": "npm run dist-modules && cd dist && pkg . --public -C gzip && mv -f hfs-win-x64.exe hfs.exe && zip hfs-windows.zip hfs.exe -r plugins && cp -f hfs-linux-x64 hfs && zip hfs-linux.zip hfs -r plugins && cp -f hfs-macos-x64 hfs && zip hfs-mac.zip hfs -r plugins && cp -f hfs-macos-arm64 hfs && zip hfs-mac-arm.zip hfs -r plugins && rm hfs",
28
- "dist-modules": "cp package*.json dist && cd dist && npm ci --omit=dev && npm run dist-crclib && rm package-lock.json && cd .. && node prune_modules",
33
+ "dist-modules": "cp package*.json central.json dist && cd dist && npm ci --omit=dev && npm run dist-crclib && rm package-lock.json && cd .. && node prune_modules",
29
34
  "dist-crclib": "npm i -f --no-save --omit=dev @node-rs/crc32-win32-x64-msvc @node-rs/crc32-darwin-arm64 @node-rs/crc32-darwin-x64 @node-rs/crc32-linux-x64-gnu",
30
- "dist-win": "cp package*.json dist && cd dist && npm ci --omit=dev && npm i -f --no-save --omit=dev @node-rs/crc32-win32 && pkg . --public -C gzip -t node16-win-x64",
31
- "dist-mac": "cp package*.json dist && cd dist && npm ci --omit=dev && pkg . --public -C gzip -t node16-macos-arm64",
35
+ "dist-win": "npm run dist-modules && cd dist && pkg . --public -C gzip -t node16-win-x64",
36
+ "dist-mac": "npm run dist-modules && cd dist && pkg . --public -C gzip -t node16-macos-arm64",
32
37
  "dist-node": "npm run dist-modules && cd dist && zip hfs-node.zip -r * -x *.zip *.exe hfs-* *.log logs"
33
38
  },
34
39
  "engines": {
@@ -49,14 +54,15 @@
49
54
  },
50
55
  "pkg": {
51
56
  "assets": [
57
+ "central.json",
52
58
  "admin/**/*",
53
59
  "frontend/**/*"
54
60
  ],
55
61
  "targets": [
56
- "node16-win-x64",
57
- "node16-mac-x64",
58
- "node16-mac-arm64",
59
- "node16-linux-x64"
62
+ "node18-win-x64",
63
+ "node18-mac-x64",
64
+ "node18-mac-arm64",
65
+ "node18-linux-x64"
60
66
  ]
61
67
  },
62
68
  "dependencies": {
@@ -64,10 +70,9 @@
64
70
  "@node-rs/crc32": "^1.6.0",
65
71
  "basic-auth": "^2.0.1",
66
72
  "buffer-crc32": "^0.2.13",
67
- "cidr-tools": "^4.3.0",
68
73
  "fast-glob": "^3.2.7",
69
74
  "find-process": "^1.4.7",
70
- "formidable": "^2.1.1",
75
+ "formidable": "^3.5.1",
71
76
  "fs-x-attributes": "^1.0.2",
72
77
  "koa": "^2.13.4",
73
78
  "koa-compress": "^5.1.0",
@@ -76,6 +81,8 @@
76
81
  "limiter": "^2.1.0",
77
82
  "lodash": "^4.17.21",
78
83
  "minimist": "^1.2.6",
84
+ "nat-upnp": "github:kaden-sharpin/node-nat-upnp",
85
+ "node-html-parser": "^6.1.5",
79
86
  "open": "^8.4.0",
80
87
  "tssrp6a": "^3.0.0",
81
88
  "unzip-stream": "^0.3.1",
@@ -85,7 +92,7 @@
85
92
  "devDependencies": {
86
93
  "@types/archiver": "^5.1.1",
87
94
  "@types/basic-auth": "^1.1.3",
88
- "@types/formidable": "^2.0.5",
95
+ "@types/formidable": "^3.4.1",
89
96
  "@types/koa": "^2.13.4",
90
97
  "@types/koa__router": "^8.0.11",
91
98
  "@types/koa-compress": "^4.0.3",
@@ -96,7 +103,7 @@
96
103
  "@types/mime-types": "^2.1.1",
97
104
  "@types/minimist": "^1.2.2",
98
105
  "@types/mocha": "^9.0.0",
99
- "@types/node": "^16.11.12",
106
+ "@types/node": "^18.17.14",
100
107
  "@types/tough-cookie": "^4.0.2",
101
108
  "@types/unzipper": "^0.10.5",
102
109
  "axios": "^0.24.0",
@@ -1,5 +1,5 @@
1
1
  exports.description = "If you want to have different home folders, based on domain"
2
- exports.version = 3.12
2
+ exports.version = 3.2
3
3
  exports.apiRequired = 2 // 2 is for the config 'array'
4
4
 
5
5
  exports.config = {
@@ -27,12 +27,11 @@ exports.init = api => {
27
27
  middleware(ctx) {
28
28
  let params // undefined if we are not going to work on api parameters
29
29
  if (ctx.path.startsWith(api.const.SPECIAL_URI)) { // special uris should be excluded...
30
- // ...unless it's a frontend api with a path param
31
- if (!ctx.path.startsWith(api.const.API_URI)) return
30
+ if (!ctx.path.startsWith(api.const.API_URI)) return // ...unless it's an api
32
31
  let { referer } = ctx.headers
33
32
  referer &&= new URL(referer).pathname
34
33
  if (referer?.startsWith(ctx.state.revProxyPath + api.const.ADMIN_URI)) return // exclude apis for admin-panel
35
- params = ctx.params
34
+ params = ctx.params || ctx.query // for api we'll translate params
36
35
  }
37
36
 
38
37
  const hosts = api.getConfig('hosts')
@@ -46,17 +45,18 @@ exports.init = api => {
46
45
  return
47
46
  }
48
47
  let { root='' } = row
49
- if (root.endsWith('/'))
50
- root = root.slice(0, -1)
51
- if (root && root[0] !== '/') // normalize
52
- root = '/' + root
53
- if (!root) return
54
- if (!params)
55
- ctx.path = root + ctx.path
56
- else
57
- for (const [k,v] of Object.entries(params))
58
- if (k.startsWith('uri'))
59
- params[k] = Array.isArray(v) ? v.map(x => root + x) : root + v
48
+ if (!root || root === '/') return
49
+ if (params === undefined) {
50
+ ctx.path = join(root, ctx.path)
51
+ return
52
+ }
53
+ for (const [k,v] of Object.entries(params))
54
+ if (k.startsWith('uri'))
55
+ params[k] = Array.isArray(v) ? v.map(x => join(root, x)) : join(root, v)
60
56
  }
61
57
  }
62
58
  }
59
+
60
+ function join(a, b) {
61
+ return a + (b && b[0] !== '/' ? '/' : '') + b
62
+ }
package/src/adminApis.js CHANGED
@@ -37,6 +37,7 @@ const api_accounts_1 = __importDefault(require("./api.accounts"));
37
37
  const api_plugins_1 = __importDefault(require("./api.plugins"));
38
38
  const api_monitor_1 = __importDefault(require("./api.monitor"));
39
39
  const api_lang_1 = __importDefault(require("./api.lang"));
40
+ const api_net_1 = __importDefault(require("./api.net"));
40
41
  const connections_1 = require("./connections");
41
42
  const misc_1 = require("./misc");
42
43
  const events_1 = __importDefault(require("./events"));
@@ -58,6 +59,7 @@ exports.adminApis = {
58
59
  ...api_plugins_1.default,
59
60
  ...api_monitor_1.default,
60
61
  ...api_lang_1.default,
62
+ ...api_net_1.default,
61
63
  async set_config({ values: v }) {
62
64
  var _a;
63
65
  if (v) {
@@ -104,7 +106,7 @@ exports.adminApis = {
104
106
  apiVersion: const_1.API_VERSION,
105
107
  compatibleApiVersion: const_1.COMPATIBLE_API_VERSION,
106
108
  ...await (0, listen_1.getServerStatus)(),
107
- urls: (0, listen_1.getUrls)(),
109
+ urls: await (0, listen_1.getUrls)(),
108
110
  baseUrl: middlewares_1.baseUrl.get(),
109
111
  updatePossible: !(0, update_1.updateSupported)() ? false : await (0, update_1.localUpdateAvailable)() ? 'local' : true,
110
112
  proxyDetected: (0, middlewares_1.getProxyDetected)(),
@@ -163,10 +165,10 @@ exports.adminApis = {
163
165
  }
164
166
  });
165
167
  function parse(line) {
166
- const m = /^(.+?) (.+?) (.+?) \[(.{11}):(.{14})] "(\w+) ([^"]+) HTTP\/\d.\d" (\d+) (-|\d+)/.exec(line);
168
+ const m = /^(.+?) (.+?) (.+?) \[(.{11}):(.{14})] "(\w+) ([^"]+) HTTP\/\d.\d" (\d+) (-|\d+) ?(.*)/.exec(line);
167
169
  if (!m)
168
170
  return;
169
- const [, ip, , user, date, time, method, uri, status, length] = m;
171
+ const [, ip, , user, date, time, method, uri, status, length, extra] = m;
170
172
  return {
171
173
  ip,
172
174
  user: user === '-' ? undefined : user,
@@ -175,6 +177,7 @@ exports.adminApis = {
175
177
  uri,
176
178
  status: Number(status),
177
179
  length: length === '-' ? undefined : Number(length),
180
+ extra: (0, misc_1.tryJson)((0, misc_1.tryJson)(extra)) || undefined,
178
181
  };
179
182
  }
180
183
  },
@@ -185,7 +188,7 @@ for (const [k, was] of Object.entries(exports.adminApis))
185
188
  return new apiMiddleware_1.ApiError(const_1.HTTP_FORBIDDEN);
186
189
  if (ctxAdminAccess(ctx))
187
190
  return was(params, ctx);
188
- const props = { any: anyAccountCanLoginAdmin() };
191
+ const props = { possible: anyAccountCanLoginAdmin() };
189
192
  return ctx.headers.accept === 'text/event-stream'
190
193
  ? new apiMiddleware_1.SendListReadable({ doAtStart: x => x.error(const_1.HTTP_UNAUTHORIZED, true, props) })
191
194
  : new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, props);
package/src/api.auth.js CHANGED
@@ -7,7 +7,6 @@ const crypt_1 = require("./crypt");
7
7
  const apiMiddleware_1 = require("./apiMiddleware");
8
8
  const tssrp6a_1 = require("tssrp6a");
9
9
  const const_1 = require("./const");
10
- const misc_1 = require("./misc");
11
10
  const api_helpers_1 = require("./api.helpers");
12
11
  const adminApis_1 = require("./adminApis");
13
12
  const middlewares_1 = require("./middlewares");
@@ -22,12 +21,10 @@ async function loggedIn(ctx, username) {
22
21
  return ctx.throw(const_1.HTTP_SERVER_ERROR, 'session');
23
22
  if (username === false) {
24
23
  delete s.username;
25
- ctx.cookies.set('csrf', '');
26
24
  return;
27
25
  }
28
26
  s.username = (0, perm_1.normalizeUsername)(username);
29
27
  await (0, middlewares_1.prepareState)(ctx, async () => { }); // updating the state is necessary to send complete session data so that frontend shows admin button
30
- ctx.cookies.set('csrf', (0, misc_1.randomId)(), { signed: false, httpOnly: false });
31
28
  }
32
29
  function makeExp() {
33
30
  return !keepSessionAlive.get() ? undefined
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.file_list = void 0;
7
+ exports.get_file_list = void 0;
8
8
  const vfs_1 = require("./vfs");
9
9
  const apiMiddleware_1 = require("./apiMiddleware");
10
10
  const promises_1 = require("fs/promises");
@@ -12,9 +12,10 @@ const plugins_1 = require("./plugins");
12
12
  const misc_1 = require("./misc");
13
13
  const lodash_1 = __importDefault(require("lodash"));
14
14
  const const_1 = require("./const");
15
- const file_list = async ({ uri, offset, limit, search, c, sse }, ctx) => {
15
+ const get_file_list = async ({ uri, offset, limit, search, c }, ctx) => {
16
+ var _a;
16
17
  const node = await (0, vfs_1.urlToNode)(uri || '/', ctx);
17
- const list = new apiMiddleware_1.SendListReadable();
18
+ const list = ctx.get('accept') === 'text/event-stream' ? new apiMiddleware_1.SendListReadable() : undefined;
18
19
  if (!node)
19
20
  return fail(const_1.HTTP_NOT_FOUND);
20
21
  if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_list', ctx))
@@ -22,7 +23,7 @@ const file_list = async ({ uri, offset, limit, search, c, sse }, ctx) => {
22
23
  if ((0, misc_1.dirTraversal)(search))
23
24
  return fail(const_1.HTTP_FOOL);
24
25
  if (node.default)
25
- return (sse ? list.custom : lodash_1.default.identity)({
26
+ return ((_a = list === null || list === void 0 ? void 0 : list.custom) !== null && _a !== void 0 ? _a : lodash_1.default.identity)({
26
27
  redirect: uri // tell the browser to access the folder (instead of using this api), so it will get the default file
27
28
  });
28
29
  if (!await (0, vfs_1.nodeIsDirectory)(node))
@@ -35,7 +36,7 @@ const file_list = async ({ uri, offset, limit, search, c, sse }, ctx) => {
35
36
  const can_upload = (0, vfs_1.hasPermission)(node, 'can_upload', ctx);
36
37
  const can_delete = (0, vfs_1.hasPermission)(node, 'can_delete', ctx);
37
38
  const props = { can_upload, can_delete, accept: node.accept };
38
- if (!sse)
39
+ if (!list)
39
40
  return { ...props, list: await (0, misc_1.asyncGeneratorToArray)(produceEntries()) };
40
41
  setTimeout(async () => {
41
42
  if (can_upload || can_delete)
@@ -46,7 +47,7 @@ const file_list = async ({ uri, offset, limit, search, c, sse }, ctx) => {
46
47
  });
47
48
  return list;
48
49
  function fail(code = ctx.status) {
49
- if (!sse)
50
+ if (!list)
50
51
  return new apiMiddleware_1.ApiError(code);
51
52
  list.error(code, true);
52
53
  return list;
@@ -84,7 +85,7 @@ const file_list = async ({ uri, offset, limit, search, c, sse }, ctx) => {
84
85
  }
85
86
  }
86
87
  };
87
- exports.file_list = file_list;
88
+ exports.get_file_list = get_file_list;
88
89
  async function nodeToDirEntry(ctx, node) {
89
90
  let { source, default: def } = node;
90
91
  const name = (0, vfs_1.getNodeName)(node);
package/src/api.lang.js CHANGED
@@ -13,7 +13,7 @@ const misc_1 = require("./misc");
13
13
  const lang_1 = require("./lang");
14
14
  const embedded_1 = __importDefault(require("./langs/embedded"));
15
15
  const apis = {
16
- list_langs() {
16
+ get_langs() {
17
17
  return new apiMiddleware_1.SendListReadable({
18
18
  doAtStart: async (list) => {
19
19
  for await (let name of fast_glob_1.default.stream((0, lang_1.code2file)('*'))) {
package/src/api.net.js ADDED
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.externalIp = void 0;
8
+ const apiMiddleware_1 = require("./apiMiddleware");
9
+ const nat_upnp_1 = require("nat-upnp");
10
+ const const_1 = require("./const");
11
+ const axios_1 = __importDefault(require("axios"));
12
+ const node_html_parser_1 = require("node-html-parser");
13
+ const lodash_1 = __importDefault(require("lodash"));
14
+ const listen_1 = require("./listen");
15
+ const github_1 = require("./github");
16
+ const util_http_1 = require("./util-http");
17
+ const child_process_1 = require("child_process");
18
+ const misc_1 = require("./misc");
19
+ const client = new nat_upnp_1.Client({ timeout: 5000 });
20
+ const original = client.getGateway;
21
+ // other client methods call getGateway too, so this will ensure they reuse this same result
22
+ client.getGateway = function getGatewayCaching() {
23
+ const promise = original.apply(client);
24
+ client.getGateway = () => promise; // multiple callings = same job
25
+ promise.then(() => console.debug('caching gateway'), // store in cache only if successful.
26
+ () => client.getGateway = getGatewayCaching); // failed, try again
27
+ return promise;
28
+ };
29
+ client.getGateway();
30
+ exports.externalIp = Promise.resolve(''); // poll external ip
31
+ (0, misc_1.repeat)(10 * misc_1.MINUTE, () => {
32
+ const was = exports.externalIp;
33
+ exports.externalIp = client.getPublicIp().catch(() => was); //fallback to previous value
34
+ });
35
+ const getNatInfo = (0, misc_1.debounceAsync)(async () => {
36
+ var _a, _b;
37
+ const gettingIp = getPublicIp(); // don't wait, do it in parallel
38
+ const res = await client.getGateway().catch(() => null);
39
+ const status = await (0, listen_1.getServerStatus)();
40
+ const mappings = res && await client.getMappings().catch(() => null);
41
+ console.debug('mappings found', mappings);
42
+ const gatewayIp = res ? new URL(res.gateway.description).hostname : await findGateway().catch(() => null);
43
+ const localIp = (res === null || res === void 0 ? void 0 : res.address) || (await (0, listen_1.getIps)())[0];
44
+ const internalPort = ((_a = status === null || status === void 0 ? void 0 : status.https) === null || _a === void 0 ? void 0 : _a.listening) && status.https.port || ((_b = status === null || status === void 0 ? void 0 : status.http) === null || _b === void 0 ? void 0 : _b.listening) && status.http.port;
45
+ const mapped = lodash_1.default.find(mappings, x => x.private.host === localIp && x.private.port === internalPort || x.description === 'hfs');
46
+ console.debug('responding');
47
+ return {
48
+ upnp: Boolean(res),
49
+ localIp,
50
+ gatewayIp,
51
+ publicIp: await gettingIp || await exports.externalIp,
52
+ externalIp: await exports.externalIp,
53
+ mapped,
54
+ internalPort,
55
+ externalPort: mapped === null || mapped === void 0 ? void 0 : mapped.public.port,
56
+ };
57
+ });
58
+ async function getPublicIp() {
59
+ const prjInfo = await (0, github_1.getProjectInfo)();
60
+ for (const urls of lodash_1.default.chunk(lodash_1.default.shuffle(prjInfo.publicIpServices), 2)) // small parallelization
61
+ try {
62
+ return await Promise.any(urls.map(url => (0, util_http_1.httpString)(url).then(res => {
63
+ var _a;
64
+ const ip = (_a = res.body) === null || _a === void 0 ? void 0 : _a.trim();
65
+ if (!/[.:0-9a-fA-F]/.test(ip))
66
+ throw Error("bad result: " + ip);
67
+ return ip;
68
+ })));
69
+ }
70
+ catch (e) {
71
+ console.debug(String(e));
72
+ }
73
+ }
74
+ function findGateway() {
75
+ return new Promise((resolve, reject) => (0, child_process_1.exec)(const_1.IS_WINDOWS || const_1.IS_MAC ? 'netstat -rn' : 'route -n', (err, out) => {
76
+ var _a;
77
+ if (err)
78
+ return reject(err);
79
+ const re = const_1.IS_WINDOWS ? /(?:0\.0\.0\.0 +){2}([\d.]+)/ : const_1.IS_MAC ? /default +([\d.]+)/ : /^0\.0\.0\.0 +([\d.]+)/;
80
+ resolve((_a = re.exec(out)) === null || _a === void 0 ? void 0 : _a[1]);
81
+ }));
82
+ }
83
+ const apis = {
84
+ get_nat: getNatInfo,
85
+ async map_port({ external }) {
86
+ const { gatewayIp, mapped, internalPort } = await getNatInfo();
87
+ if (!gatewayIp)
88
+ return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, 'upnp failed');
89
+ if (!internalPort)
90
+ return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, 'no internal port');
91
+ if (mapped)
92
+ try {
93
+ await client.removeMapping({ public: { host: '', port: mapped.public.port } });
94
+ }
95
+ catch (e) {
96
+ return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, 'removeMapping failed: ' + String(e));
97
+ }
98
+ if (external) // must use the object form of 'public' to workaround a bug of the library
99
+ await client.createMapping({ private: internalPort, public: { host: '', port: external }, description: 'hfs', ttl: 0 });
100
+ return {};
101
+ },
102
+ async check_server() {
103
+ const { publicIp, internalPort, externalPort } = await getNatInfo();
104
+ if (!publicIp)
105
+ return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, 'cannot detect public ip');
106
+ if (!internalPort)
107
+ return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, 'no internal port');
108
+ const prjInfo = await (0, github_1.getProjectInfo)();
109
+ const port = externalPort || internalPort;
110
+ console.log(`checking server ${publicIp}:${port}`);
111
+ for (const services of lodash_1.default.chunk(lodash_1.default.shuffle(prjInfo.checkServerServices), 2)) {
112
+ try {
113
+ return Promise.any(services.map(async (svc) => {
114
+ var _a, _b;
115
+ const service = new URL(svc.url).hostname;
116
+ console.log('trying service', service);
117
+ const api = axios_1.default[svc.method];
118
+ const body = ((_a = svc.body) === null || _a === void 0 ? void 0 : _a.replace('$IP', publicIp).replace('$PORT', String(port))) || '';
119
+ const res = await api(svc.url, body, { headers: svc.headers });
120
+ const parsed = (_b = (0, node_html_parser_1.parse)(res.data).querySelector(svc.selector)) === null || _b === void 0 ? void 0 : _b.innerText;
121
+ if (!parsed)
122
+ throw console.debug('empty:' + service);
123
+ const success = new RegExp(svc.regexpSuccess).test(parsed);
124
+ const failure = new RegExp(svc.regexpFailure).test(parsed);
125
+ if (success === failure)
126
+ throw console.debug('inconsistent:' + service); // this result cannot be trusted
127
+ console.debug(service, 'responded', success);
128
+ return { success, service };
129
+ }));
130
+ }
131
+ catch (_a) { }
132
+ }
133
+ return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, 'no service available to detect upnp mapping');
134
+ },
135
+ };
136
+ exports.default = apis;
@@ -15,7 +15,7 @@ const github_1 = require("./github");
15
15
  const const_1 = require("./const");
16
16
  const apis = {
17
17
  get_plugins({}, ctx) {
18
- const list = new apiMiddleware_1.SendListReadable({ addAtStart: [...(0, plugins_1.mapPlugins)(serialize), ...(0, plugins_1.getAvailablePlugins)()] });
18
+ const list = new apiMiddleware_1.SendListReadable({ addAtStart: [...(0, plugins_1.mapPlugins)(serialize), ...(0, plugins_1.getAvailablePlugins)().map(serialize)] });
19
19
  return list.events(ctx, {
20
20
  pluginInstalled: p => list.add(serialize(p)),
21
21
  'pluginStarted pluginStopped pluginUpdated': p => {
@@ -27,35 +27,37 @@ const apis = {
27
27
  function serialize(p) {
28
28
  const o = 'getData' in p ? Object.assign(lodash_1.default.pick(p, ['id', 'started']), p.getData())
29
29
  : { ...p }; // _.defaults mutates object, and we don't want that
30
+ if (typeof o.repo === 'object') // custom repo
31
+ o.repo = o.repo.web;
30
32
  return lodash_1.default.defaults(o, { started: null, badApi: null }); // nulls should be used to be sure to overwrite previous values,
31
33
  }
32
34
  },
33
35
  async get_plugin_updates() {
34
- const list = new apiMiddleware_1.SendListReadable();
35
- setTimeout(async () => {
36
- const errs = await Promise.all(lodash_1.default.map((0, github_1.getFolder2repo)(), async (repo, folder) => {
37
- try {
38
- if (!repo)
39
- return;
40
- //TODO shouldn't we consider other branches here?
41
- const online = await (0, github_1.readOnlinePlugin)(repo);
42
- if (!online.apiRequired || online.badApi)
43
- return;
44
- const disk = (0, plugins_1.getPluginInfo)(folder);
45
- if (online.version > disk.version)
46
- list.add(online);
47
- }
48
- catch (err) {
49
- if (err.message === '404') // the plugin is declaring a wrong repo
50
- return;
51
- return err.code || err.message;
52
- }
53
- }));
54
- for (const x of lodash_1.default.uniq((0, misc_1.onlyTruthy)(errs)))
55
- list.error(x);
56
- list.close();
36
+ return new apiMiddleware_1.SendListReadable({
37
+ async doAtStart(list) {
38
+ const errs = await Promise.all(lodash_1.default.map((0, github_1.getFolder2repo)(), async (repo, folder) => {
39
+ try {
40
+ if (!repo)
41
+ return;
42
+ //TODO shouldn't we consider other branches here?
43
+ const online = await (0, github_1.readOnlinePlugin)(repo);
44
+ if (!(online === null || online === void 0 ? void 0 : online.apiRequired) || online.badApi)
45
+ return;
46
+ const disk = (0, plugins_1.getPluginInfo)(folder);
47
+ if (online.version > disk.version)
48
+ list.add(online);
49
+ }
50
+ catch (err) {
51
+ if (err.message === '404') // the plugin is declaring a wrong repo
52
+ return;
53
+ return err.code || err.message;
54
+ }
55
+ }));
56
+ for (const x of lodash_1.default.uniq((0, misc_1.onlyTruthy)(errs)))
57
+ list.error(x);
58
+ list.close();
59
+ }
57
60
  });
58
- return list;
59
61
  },
60
62
  async start_plugin({ id }) {
61
63
  if ((0, plugins_1.isPluginRunning)(id))
@@ -87,7 +89,7 @@ const apis = {
87
89
  }
88
90
  };
89
91
  },
90
- search_online_plugins({ text }, ctx) {
92
+ get_online_plugins({ text }, ctx) {
91
93
  return new apiMiddleware_1.SendListReadable({
92
94
  async doAtStart(list) {
93
95
  try {
@@ -98,12 +100,7 @@ const apis = {
98
100
  for await (const pl of (0, github_1.searchPlugins)(text)) {
99
101
  const repo = pl.id;
100
102
  if (lodash_1.default.includes(folder2repo, repo))
101
- continue;
102
- const folder = lodash_1.default.findKey(folder2repo, x => x === repo);
103
- const installed = folder && (0, plugins_1.getPluginInfo)(folder);
104
- Object.assign(pl, {
105
- update: installed && installed.version < pl.version,
106
- });
103
+ continue; // don't include installed plugins
107
104
  list.add(pl);
108
105
  // watch for events about this plugin, until this request is closed
109
106
  undo.push((0, misc_1.onOff)(events_1.default, {
@@ -115,10 +112,6 @@ const apis = {
115
112
  if (repo === (0, github_1.getFolder2repo)()[folder])
116
113
  list.update({ id: repo }, { installed: false });
117
114
  },
118
- pluginUpdated: p => {
119
- if (p.repo === repo)
120
- list.update({ id: repo }, { update: p.version < pl.version });
121
- },
122
115
  ['pluginDownload_' + repo](status) {
123
116
  list.update({ id: repo }, { downloading: status !== null && status !== void 0 ? status : null });
124
117
  }
@@ -161,6 +154,8 @@ const apis = {
161
154
  exports.default = apis;
162
155
  async function checkDependencies(repo, branch) {
163
156
  const rec = await (0, github_1.readOnlinePlugin)(repo, branch);
157
+ if (!rec)
158
+ return;
164
159
  const miss = rec.depend && rec.depend.map((dep) => {
165
160
  const res = (0, plugins_1.findPluginByRepo)(dep.repo);
166
161
  const error = !res ? 'missing'
package/src/api.vfs.js CHANGED
@@ -151,7 +151,7 @@ const apis = {
151
151
  path = (0, path_1.dirname)(path);
152
152
  return { path };
153
153
  },
154
- ls({ path, files = true, fileMask }, ctx) {
154
+ get_ls({ path, files = true, fileMask }, ctx) {
155
155
  return new apiMiddleware_1.SendListReadable({
156
156
  async doAtStart(list) {
157
157
  if (!path && const_1.IS_WINDOWS) {
@@ -24,14 +24,17 @@ function apiMiddleware(apis) {
24
24
  return async (ctx) => {
25
25
  if (!logApi.get())
26
26
  ctx.state.dont_log = true;
27
- const { params } = ctx;
28
- console.debug('API', ctx.method, ctx.path, { ...params });
29
- const apiFun = apis.hasOwnProperty(ctx.path) && apis[ctx.path];
30
- if (!apiFun) {
31
- ctx.body = 'invalid api';
32
- return ctx.status = const_1.HTTP_NOT_FOUND;
33
- }
34
- const csrf = ctx.cookies.get('csrf');
27
+ const isPost = ctx.params;
28
+ const params = isPost ? ctx.params || {} : ctx.query;
29
+ const apiName = ctx.path;
30
+ console.debug('API', ctx.method, apiName, { ...params });
31
+ const safe = isPost && ctx.get('x-hfs-anti-csrf') // POST is safe because browser will enforce SameSite cookie
32
+ || apiName.startsWith('get_'); // "get_" apis are safe because they make no change
33
+ if (!safe)
34
+ return send(const_1.HTTP_FOOL);
35
+ const apiFun = apis.hasOwnProperty(apiName) && apis[apiName];
36
+ if (!apiFun)
37
+ return send(const_1.HTTP_NOT_FOUND, 'invalid api');
35
38
  // we don't rely on SameSite cookie option because it's https-only
36
39
  let res;
37
40
  try {
@@ -42,10 +45,9 @@ function apiMiddleware(apis) {
42
45
  fixUri(params, k);
43
46
  else if (typeof (v === null || v === void 0 ? void 0 : v[0]) === 'string')
44
47
  v.forEach((x, i) => fixUri(v, i));
45
- res = csrf && csrf !== params.csrf ? new ApiError(const_1.HTTP_UNAUTHORIZED, 'csrf')
46
- : await apiFun(params || {}, ctx);
47
- function fixUri(o, k) {
48
- o[k] = (0, misc_1.removeStarting)(ctx.state.revProxyPath, o[k]);
48
+ res = await apiFun(params, ctx);
49
+ function fixUri(obj, k) {
50
+ obj[k] = (0, misc_1.removeStarting)(ctx.state.revProxyPath, obj[k]);
49
51
  }
50
52
  }
51
53
  catch (e) {
@@ -60,15 +62,15 @@ function apiMiddleware(apis) {
60
62
  resAsReadable.destroy());
61
63
  return;
62
64
  }
63
- if (res instanceof ApiError) {
64
- ctx.body = res.message;
65
- return ctx.status = res.status;
66
- }
67
- if (res instanceof Error) { // generic exception
68
- ctx.body = res.message || String(res);
69
- return ctx.status = const_1.HTTP_BAD_REQUEST;
70
- }
65
+ if (res instanceof ApiError)
66
+ return send(res.status, res.message);
67
+ if (res instanceof Error) // generic error/exception
68
+ return send(const_1.HTTP_BAD_REQUEST, res.message || String(res));
71
69
  ctx.body = res;
70
+ function send(status, body) {
71
+ ctx.body = body;
72
+ ctx.status = status;
73
+ }
72
74
  };
73
75
  }
74
76
  exports.apiMiddleware = apiMiddleware;