hfs 0.48.0-alpha2 → 0.48.0-beta6

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.
@@ -1,4 +1,4 @@
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:{}};/*
1
+ import{g as OF,c as UF}from"./index-ad985b09.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
@@ -1,19 +1,9 @@
1
1
  @font-face {
2
2
  font-family: 'fontello';
3
- src: url('fontello.woff2?44288867') format('woff2');
3
+ src: url('fontello.woff2?13611077') format('woff2');
4
4
  font-weight: normal;
5
5
  font-style: normal;
6
6
  }
7
- /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
8
- /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
9
- /*
10
- @media screen and (-webkit-min-device-pixel-ratio:0) {
11
- @font-face {
12
- font-family: 'fontello';
13
- src: url('../font/fontello.svg?44288867#fontello') format('svg');
14
- }
15
- }
16
- */
17
7
  [class^="fa-"]:before, [class*=" fa-"]:before {
18
8
  font-family: "fontello";
19
9
  font-style: normal;
@@ -64,6 +54,7 @@
64
54
  .fa-key:before { content: '\e80c'; } /* '' */
65
55
  .fa-trash:before { content: '\e80d'; } /* '' */
66
56
  .fa-to_start:before { content: '\e80e'; } /* '' */
57
+ .fa-pause:before { content: '\e80f'; } /* '' */
67
58
  .fa-to_end:before { content: '\e810'; } /* '' */
68
59
  .fa-image:before { content: '\e811'; } /* '' */
69
60
  .fa-camera:before { content: '\e812'; } /* '' */
Binary file
@@ -5,7 +5,7 @@
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-12411ff6.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-ad985b09.js"></script>
9
9
  <link rel="stylesheet" href="/assets/index-05dfd82f.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.48.0-alpha2",
3
+ "version": "0.48.0-beta6",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -32,8 +32,8 @@
32
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",
33
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",
34
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",
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",
35
+ "dist-win": "npm run dist-modules && cd dist && pkg . --public -C gzip -t node18-win-x64",
36
+ "dist-mac": "npm run dist-modules && cd dist && pkg . --public -C gzip -t node18-macos-arm64",
37
37
  "dist-node": "npm run dist-modules && cd dist && zip hfs-node.zip -r * -x *.zip *.exe hfs-* *.log logs"
38
38
  },
39
39
  "engines": {
@@ -68,6 +68,7 @@
68
68
  "dependencies": {
69
69
  "@koa/router": "^10.1.1",
70
70
  "@node-rs/crc32": "^1.6.0",
71
+ "acme-client": "^5.0.0",
71
72
  "basic-auth": "^2.0.1",
72
73
  "buffer-crc32": "^0.2.13",
73
74
  "fast-glob": "^3.2.7",
@@ -14,6 +14,7 @@ exports.config = {
14
14
  "IP only": 'ip',
15
15
  "Username only": 'user',
16
16
  "IP + username": 'ip+user',
17
+ "Icon + text on mouse over": 'tooltip',
17
18
  },
18
19
  defaultValue: 'ip+user',
19
20
  frontend: true,
@@ -15,8 +15,9 @@
15
15
  : display === 'ip' || !x.username ? x.ip
16
16
  : x.ip + ' (' + x.username + ')'
17
17
  }, [data])
18
- return text && HFS.h('span', { className: 'uploader', title: HFS.t`Uploader` },
19
- HFS.hIcon('upload'), ' ', text, ' ')
18
+ const iconOnly = display === 'tooltip'
19
+ return text && HFS.h('span', { className: 'uploader', title: HFS.t`Uploader` + (iconOnly ? ' ' + text : '') },
20
+ HFS.hIcon('upload'), ' ', !iconOnly && text, ' – ')
20
21
  }
21
22
 
22
23
  function getDetails(batched) {
@@ -162,6 +162,7 @@ class QuickZipStream extends stream_1.Readable {
162
162
  data.on('error', (err) => console.error(err));
163
163
  data.on('end', () => {
164
164
  this.workingFile = undefined;
165
+ entry.crc = crc;
165
166
  if (sourcePath)
166
167
  crcCache[sourcePath] = { ts, crc };
167
168
  this.entries.push(entry);
@@ -34,7 +34,8 @@ const get_file_list = async ({ uri, offset, limit, search, c }, ctx) => {
34
34
  const walker = (0, vfs_1.walkNode)(node, ctx, search ? Infinity : 0);
35
35
  const onDirEntryHandlers = (0, plugins_1.mapPlugins)(plug => plug.onDirEntry);
36
36
  const can_upload = (0, vfs_1.hasPermission)(node, 'can_upload', ctx);
37
- const can_delete = (0, vfs_1.hasPermission)(node, 'can_delete', ctx);
37
+ const fakeChild = (0, vfs_1.applyParentToChild)({}, node); // we want to know if we want to delete children
38
+ const can_delete = (0, vfs_1.hasPermission)(fakeChild, 'can_delete', ctx);
38
39
  const props = { can_upload, can_delete, accept: node.accept };
39
40
  if (!list)
40
41
  return { ...props, list: await (0, misc_1.asyncGeneratorToArray)(produceEntries()) };
@@ -84,41 +85,41 @@ const get_file_list = async ({ uri, offset, limit, search, c }, ctx) => {
84
85
  break;
85
86
  }
86
87
  }
88
+ async function nodeToDirEntry(ctx, node) {
89
+ let { source, default: def } = node;
90
+ const name = (0, vfs_1.getNodeName)(node);
91
+ if (!source)
92
+ return name ? { n: name + '/' } : null;
93
+ if (def)
94
+ return { n: name };
95
+ try {
96
+ const st = await (0, promises_1.stat)(source);
97
+ const folder = st.isDirectory();
98
+ const { ctime, mtime } = st;
99
+ const pl = node.can_list === vfs_1.WHO_NO_ONE ? 'l'
100
+ : !(0, vfs_1.hasPermission)(node, 'can_list', ctx) ? 'L'
101
+ : '';
102
+ // no download here, but maybe inside?
103
+ const pr = node.can_read === vfs_1.WHO_NO_ONE && !(folder && filesInsideCould()) ? 'r'
104
+ : !(0, vfs_1.hasPermission)(node, 'can_read', ctx) ? 'R'
105
+ : '';
106
+ const pd = !can_delete && (0, vfs_1.hasPermission)(node, 'can_delete', ctx) ? 'd' : '';
107
+ return {
108
+ n: name + (folder ? '/' : ''),
109
+ c: ctime,
110
+ m: Math.abs(+mtime - +ctime) < 1000 ? undefined : mtime,
111
+ s: folder ? undefined : st.size,
112
+ p: (pr + pl + pd) || undefined
113
+ };
114
+ }
115
+ catch (_a) {
116
+ return null;
117
+ }
118
+ function filesInsideCould(n = node) {
119
+ var _a;
120
+ return (0, vfs_1.masksCouldGivePermission)(n.masks, 'can_read')
121
+ || ((_a = n.children) === null || _a === void 0 ? void 0 : _a.some(c => c.can_read || filesInsideCould(c))); // we count on the boolean-compliant nature of the permission type here
122
+ }
123
+ }
87
124
  };
88
125
  exports.get_file_list = get_file_list;
89
- async function nodeToDirEntry(ctx, node) {
90
- let { source, default: def } = node;
91
- const name = (0, vfs_1.getNodeName)(node);
92
- if (!source)
93
- return name ? { n: name + '/' } : null;
94
- if (def)
95
- return { n: name };
96
- try {
97
- const st = await (0, promises_1.stat)(source);
98
- const folder = st.isDirectory();
99
- const { ctime, mtime } = st;
100
- const pl = node.can_list === vfs_1.WHO_NO_ONE ? 'l'
101
- : !(0, vfs_1.hasPermission)(node, 'can_list', ctx) ? 'L'
102
- : '';
103
- // no download here, but maybe inside?
104
- const pr = node.can_read === vfs_1.WHO_NO_ONE && !(folder && filesInsideCould()) ? 'r'
105
- : !(0, vfs_1.hasPermission)(node, 'can_read', ctx) ? 'R'
106
- : '';
107
- const pd = (0, vfs_1.hasPermission)(node, 'can_delete', ctx) ? 'd' : '';
108
- return {
109
- n: name + (folder ? '/' : ''),
110
- c: ctime,
111
- m: Math.abs(+mtime - +ctime) < 1000 ? undefined : mtime,
112
- s: folder ? undefined : st.size,
113
- p: (pr + pl + pd) || undefined
114
- };
115
- }
116
- catch (_a) {
117
- return null;
118
- }
119
- function filesInsideCould(n = node) {
120
- var _a;
121
- return (0, vfs_1.masksCouldGivePermission)(n.masks, 'can_read')
122
- || ((_a = n.children) === null || _a === void 0 ? void 0 : _a.some(c => c.can_read || filesInsideCould(c))); // we count on the boolean-compliant nature of the permission type here
123
- }
124
- }
package/src/api.net.js CHANGED
@@ -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.externalIp = void 0;
7
+ exports.acmeMiddleware = exports.externalIp = void 0;
8
8
  const apiMiddleware_1 = require("./apiMiddleware");
9
9
  const nat_upnp_1 = require("nat-upnp");
10
10
  const const_1 = require("./const");
@@ -16,17 +16,15 @@ const github_1 = require("./github");
16
16
  const util_http_1 = require("./util-http");
17
17
  const child_process_1 = require("child_process");
18
18
  const misc_1 = require("./misc");
19
- const client = new nat_upnp_1.Client({ timeout: 5000 });
20
- const original = client.getGateway;
19
+ const acme_client_1 = __importDefault(require("acme-client"));
20
+ const promises_1 = __importDefault(require("fs/promises"));
21
+ const http_1 = require("http");
22
+ const promises_2 = require("dns/promises");
23
+ const client = new nat_upnp_1.Client({ timeout: 4000 });
24
+ const originalMethod = client.getGateway;
21
25
  // 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();
26
+ client.getGateway = (0, misc_1.debounceAsync)(() => originalMethod.apply(client), 0, { retain: misc_1.HOUR, retainFailure: 30000 });
27
+ client.getGateway().catch(() => { });
30
28
  exports.externalIp = Promise.resolve(''); // poll external ip
31
29
  (0, misc_1.repeat)(10 * misc_1.MINUTE, () => {
32
30
  const was = exports.externalIp;
@@ -42,8 +40,7 @@ const getNatInfo = (0, misc_1.debounceAsync)(async () => {
42
40
  const gatewayIp = res ? new URL(res.gateway.description).hostname : await findGateway().catch(() => null);
43
41
  const localIp = (res === null || res === void 0 ? void 0 : res.address) || (await (0, listen_1.getIps)())[0];
44
42
  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');
43
+ const mapped = lodash_1.default.find(mappings, x => x.private.host === localIp && x.private.port === internalPort);
47
44
  return {
48
45
  upnp: Boolean(res),
49
46
  localIp,
@@ -80,57 +77,164 @@ function findGateway() {
80
77
  resolve((_a = re.exec(out)) === null || _a === void 0 ? void 0 : _a[1]);
81
78
  }));
82
79
  }
80
+ let acmeMiddlewareEnabled = false;
81
+ const acmeTokens = {};
82
+ const acmeListener = (req, res) => {
83
+ var _a;
84
+ const BASE = '/.well-known/acme-challenge/';
85
+ if (!((_a = req.url) === null || _a === void 0 ? void 0 : _a.startsWith(BASE)))
86
+ return;
87
+ const token = req.url.slice(BASE.length);
88
+ console.debug("got http challenge", token);
89
+ res.statusCode = const_1.HTTP_OK;
90
+ res.end(acmeTokens[token]);
91
+ return true;
92
+ };
93
+ const acmeMiddleware = (ctx, next) => {
94
+ if (!acmeMiddlewareEnabled || !Boolean(acmeListener(ctx.req, ctx.res)))
95
+ return next();
96
+ };
97
+ exports.acmeMiddleware = acmeMiddleware;
98
+ async function checkDomain(domain) {
99
+ const resolver = new promises_2.Resolver();
100
+ const prjInfo = await (0, github_1.getProjectInfo)();
101
+ resolver.setServers(prjInfo.dnsServers);
102
+ const settled = await Promise.allSettled([
103
+ resolver.resolve(domain, 'A'),
104
+ resolver.resolve(domain, 'AAAA'),
105
+ (0, promises_2.lookup)(domain).then(x => [x.address]),
106
+ ]);
107
+ // merge all results
108
+ const ips = lodash_1.default.uniq((0, misc_1.onlyTruthy)(settled.map(x => x.status === 'fulfilled' && x.value)).flat());
109
+ if (!ips.length)
110
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "domain not working");
111
+ const { publicIp } = await getNatInfo(); // do this before stopping the server
112
+ if (!ips.includes(publicIp))
113
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, `please configure your domain to point to ${publicIp} (currently on ${ips[0]}) --- a change can take hours to be effective`);
114
+ }
115
+ async function generateSSLCert(domain, email) {
116
+ await checkDomain(domain);
117
+ // will answer challenge through our koa app (if on port 80) or must we spawn a dedicated server?
118
+ const { http } = await (0, listen_1.getServerStatus)();
119
+ const tempSrv = http.listening && http.port === 80 ? undefined : (0, http_1.createServer)(acmeListener);
120
+ if (tempSrv)
121
+ await new Promise((resolve, reject) => tempSrv.listen(80, resolve).on('error', (e) => reject(e.code || e)));
122
+ else
123
+ acmeMiddlewareEnabled = true;
124
+ console.debug('acme challenge server ready');
125
+ try {
126
+ const { upnp, externalPort } = await getNatInfo(); // do this before stopping the server
127
+ let check = await checkPort(domain, 80); // some check services may not consider the domain, but we already verified that
128
+ if (check && !check.success && upnp && externalPort !== 80) { // consider a short-lived mapping
129
+ // @ts-ignore
130
+ await client.createMapping({ private: 80, public: { host: '', port: 80 }, description: 'hfs challenge', ttl: 0 }).catch(() => { });
131
+ check = await checkPort(domain, 80); // repeat test
132
+ }
133
+ if (!check)
134
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "couldn't test port 80");
135
+ if (!check.success)
136
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "port 80 is not working on the specified domain");
137
+ const client = new acme_client_1.default.Client({
138
+ accountKey: await acme_client_1.default.crypto.createPrivateKey(),
139
+ directoryUrl: acme_client_1.default.directory.letsencrypt.production
140
+ });
141
+ const [key, csr] = await acme_client_1.default.crypto.createCsr({ commonName: domain });
142
+ const cert = await client.auto({
143
+ csr,
144
+ email,
145
+ challengePriority: ['http-01'],
146
+ skipChallengeVerification: true,
147
+ termsOfServiceAgreed: true,
148
+ async challengeCreateFn(_, c, ka) {
149
+ console.debug("producing challenge");
150
+ acmeTokens[c.token] = ka;
151
+ },
152
+ async challengeRemoveFn(_, c) {
153
+ delete acmeTokens[c.token];
154
+ },
155
+ });
156
+ return { key, cert };
157
+ }
158
+ finally {
159
+ acmeMiddlewareEnabled = false;
160
+ if (tempSrv)
161
+ await new Promise(res => tempSrv.close(res));
162
+ console.debug('acme terminated');
163
+ }
164
+ }
165
+ async function checkPort(ip, port) {
166
+ const prjInfo = await (0, github_1.getProjectInfo)();
167
+ for (const services of lodash_1.default.chunk(lodash_1.default.shuffle(prjInfo.checkServerServices), 2)) {
168
+ try {
169
+ return Promise.any(services.map(async (svc) => {
170
+ var _a, _b;
171
+ const service = new URL(svc.url).hostname;
172
+ console.log('trying service', service);
173
+ const api = axios_1.default[svc.method];
174
+ const body = ((_a = svc.body) === null || _a === void 0 ? void 0 : _a.replace('$IP', ip).replace('$PORT', String(port))) || '';
175
+ const res = await api(svc.url, body, { headers: svc.headers });
176
+ const parsed = (_b = (0, node_html_parser_1.parse)(res.data).querySelector(svc.selector)) === null || _b === void 0 ? void 0 : _b.innerText;
177
+ if (!parsed)
178
+ throw console.debug('empty:' + service);
179
+ const success = new RegExp(svc.regexpSuccess).test(parsed);
180
+ const failure = new RegExp(svc.regexpFailure).test(parsed);
181
+ if (success === failure)
182
+ throw console.debug('inconsistent:' + service); // this result cannot be trusted
183
+ console.debug(service, 'responded', success);
184
+ return { success, service };
185
+ }));
186
+ }
187
+ catch (_a) { }
188
+ }
189
+ }
83
190
  const apis = {
84
191
  get_nat: getNatInfo,
85
- async map_port({ external }) {
86
- const { gatewayIp, mapped, internalPort } = await getNatInfo();
87
- if (!gatewayIp)
192
+ check_domain({ domain }) {
193
+ (0, misc_1.apiAssertTypes)({ string: domain });
194
+ return checkDomain(domain);
195
+ },
196
+ async map_port({ external, internal }) {
197
+ const { upnp, externalPort, internalPort } = await getNatInfo();
198
+ if (!upnp)
88
199
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, 'upnp failed');
89
200
  if (!internalPort)
90
201
  return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, 'no internal port');
91
- if (mapped)
202
+ if (externalPort)
92
203
  try {
93
- await client.removeMapping({ public: { host: '', port: mapped.public.port } });
204
+ await client.removeMapping({ public: { host: '', port: externalPort } });
94
205
  }
95
206
  catch (e) {
96
207
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, 'removeMapping failed: ' + String(e));
97
208
  }
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 });
209
+ if (external) // must use the object form of 'public' to work around a bug of the library
210
+ await client.createMapping({ private: internal || internalPort, public: { host: '', port: external }, description: 'hfs', ttl: 0 });
100
211
  return {};
101
212
  },
102
- async check_server() {
213
+ async check_server({ port }) {
103
214
  const { publicIp, internalPort, externalPort } = await getNatInfo();
104
215
  if (!publicIp)
105
216
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, 'cannot detect public ip');
106
217
  if (!internalPort)
107
218
  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;
219
+ port || (port = externalPort || internalPort);
110
220
  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');
221
+ return await checkPort(publicIp, port)
222
+ || new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE);
223
+ },
224
+ async make_cert({ domain, email }) {
225
+ if (!domain)
226
+ return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad params');
227
+ const res = await generateSSLCert(domain, email);
228
+ const CERT_FILE = 'acme.cert';
229
+ const KEY_FILE = 'acme.key';
230
+ await promises_1.default.writeFile(CERT_FILE, res.cert);
231
+ await promises_1.default.writeFile(KEY_FILE, res.key);
232
+ listen_1.cert.set(CERT_FILE); // update config
233
+ listen_1.privateKey.set(KEY_FILE);
234
+ return {};
134
235
  },
236
+ get_cert() {
237
+ return (0, misc_1.objSameKeys)(lodash_1.default.pick((0, listen_1.getCertObject)(), ['subject', 'issuer', 'validFrom', 'validTo']), v => v);
238
+ }
135
239
  };
136
240
  exports.default = apis;
@@ -24,13 +24,6 @@ const apis = {
24
24
  },
25
25
  pluginUninstalled: id => list.remove({ id }),
26
26
  });
27
- function serialize(p) {
28
- const o = 'getData' in p ? Object.assign(lodash_1.default.pick(p, ['id', 'started']), p.getData())
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;
32
- return lodash_1.default.defaults(o, { started: null, badApi: null }); // nulls should be used to be sure to overwrite previous values,
33
- }
34
27
  },
35
28
  async get_plugin_updates() {
36
29
  return new apiMiddleware_1.SendListReadable({
@@ -44,8 +37,13 @@ const apis = {
44
37
  if (!(online === null || online === void 0 ? void 0 : online.apiRequired) || online.badApi)
45
38
  return;
46
39
  const disk = (0, plugins_1.getPluginInfo)(folder);
47
- if (online.version > disk.version)
40
+ if (!disk)
41
+ return; // plugin removed in the meantime?
42
+ if (online.version > disk.version) { // it IS newer
43
+ online.id = disk.id; // id is installation-dependant, and online cannot know
44
+ online.repo = serialize(disk).repo; // show the user the current repo we are getting this update from, not a possibly-changed future one
48
45
  list.add(online);
46
+ }
49
47
  }
50
48
  catch (err) {
51
49
  if (err.message === '404') // the plugin is declaring a wrong repo
@@ -97,8 +95,8 @@ const apis = {
97
95
  const undo = [];
98
96
  ctx.req.once('close', () => undo.forEach(x => x()));
99
97
  const folder2repo = (0, github_1.getFolder2repo)();
100
- for await (const pl of (0, github_1.searchPlugins)(text)) {
101
- const repo = pl.id;
98
+ for await (const pl of await (0, github_1.searchPlugins)(text)) {
99
+ const repo = pl.repo || pl.id; // .repo property can be more trustworthy in case github user renamed and left the previous link in 'repo'
102
100
  if (lodash_1.default.includes(folder2repo, repo))
103
101
  continue; // don't include installed plugins
104
102
  list.add(pl);
@@ -125,24 +123,24 @@ const apis = {
125
123
  }
126
124
  });
127
125
  },
128
- async download_plugin(pl) {
129
- await checkDependencies(pl.id, pl.branch);
130
- const res = await (0, github_1.downloadPlugin)(pl.id, pl.branch);
126
+ async download_plugin({ id, branch }) {
127
+ await checkDependencies(id, branch);
128
+ const res = await (0, github_1.downloadPlugin)(id, { branch });
131
129
  if (typeof res !== 'string')
132
130
  return res;
133
131
  return (await (0, misc_1.waitFor)(() => (0, plugins_1.getPluginInfo)(res), { timeout: 5000 }))
134
132
  || new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR);
135
133
  },
136
- async update_plugin(pl) {
137
- await checkDependencies(pl.id, pl.branch);
138
- const found = (0, plugins_1.findPluginByRepo)(pl.id); // github id !== local id
134
+ async update_plugin({ id }) {
135
+ const found = (0, plugins_1.getPluginInfo)(id);
139
136
  if (!found)
140
137
  return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND);
141
- const enabled = (0, plugins_1.isPluginEnabled)(found.id);
142
- await (0, plugins_1.stopPlugin)(found.id);
143
- await (0, github_1.downloadPlugin)(pl.id, pl.branch, true);
138
+ await checkDependencies(found.repo);
139
+ const enabled = (0, plugins_1.isPluginEnabled)(id);
140
+ await (0, plugins_1.stopPlugin)(id);
141
+ await (0, github_1.downloadPlugin)(found.repo, { overwrite: true });
144
142
  if (enabled)
145
- (0, plugins_1.startPlugin)(found.id).then(); // don't wait, in case it fails to start
143
+ (0, plugins_1.startPlugin)(id).then(); // don't wait, in case it fails to start
146
144
  return {};
147
145
  },
148
146
  async uninstall_plugin({ id }) {
@@ -152,6 +150,13 @@ const apis = {
152
150
  }
153
151
  };
154
152
  exports.default = apis;
153
+ function serialize(p) {
154
+ const o = 'getData' in p ? Object.assign(lodash_1.default.pick(p, ['id', 'started']), p.getData())
155
+ : { ...p }; // _.defaults mutates object, and we don't want that
156
+ if (typeof o.repo === 'object') // custom repo
157
+ o.repo = o.repo.web;
158
+ return lodash_1.default.defaults(o, { started: null, badApi: null }); // nulls should be used to be sure to overwrite previous values,
159
+ }
155
160
  async function checkDependencies(repo, branch) {
156
161
  const rec = await (0, github_1.readOnlinePlugin)(repo, branch);
157
162
  if (!rec)
package/src/config.js CHANGED
@@ -178,11 +178,10 @@ const saveDebounced = (0, misc_1.debounceAsync)(async () => {
178
178
  await (0, misc_1.wait)(100);
179
179
  // keep backup
180
180
  const bak = filePath + '.bak';
181
- const aWeekAgo = Date.now() - const_1.DAY * 7;
181
+ const aWeekAgo = Date.now() - misc_1.DAY * 7;
182
182
  if (await (0, promises_1.stat)(bak).then(x => aWeekAgo > Number(x.mtime || x.ctime), () => true))
183
183
  await (0, promises_1.copyFile)(filePath, bak).catch(() => { }); // ignore errors
184
- const txt = yaml_1.default.stringify({ ...state, version: const_1.VERSION }, { lineWidth: 1000 });
185
- save(filePath, txt)
184
+ await save(yaml_1.default.stringify({ ...state, version: const_1.VERSION }, { lineWidth: 1000 }))
186
185
  .catch(err => console.error('Failed at saving config file, please ensure it is writable.', String(err)));
187
186
  });
188
187
  const saveConfigAsap = () => void (saveDebounced());
package/src/const.js CHANGED
@@ -27,7 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  return (mod && mod.__esModule) ? mod : { "default": mod };
28
28
  };
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.APP_PATH = exports.IS_BINARY = exports.IS_MAC = exports.IS_WINDOWS = exports.HTTP_SERVICE_UNAVAILABLE = exports.HTTP_SERVER_ERROR = exports.HTTP_FAILED_DEPENDENCY = exports.HTTP_FOOL = exports.HTTP_RANGE_NOT_SATISFIABLE = exports.HTTP_PAYLOAD_TOO_LARGE = exports.HTTP_CONFLICT = exports.HTTP_NOT_ACCEPTABLE = exports.HTTP_METHOD_NOT_ALLOWED = exports.HTTP_NOT_FOUND = exports.HTTP_FORBIDDEN = exports.HTTP_UNAUTHORIZED = exports.HTTP_BAD_REQUEST = exports.HTTP_NOT_MODIFIED = exports.HTTP_TEMPORARY_REDIRECT = exports.HTTP_PARTIAL_CONTENT = exports.HTTP_NO_CONTENT = exports.HTTP_OK = exports.PLUGINS_PUB_URI = exports.API_URI = exports.ADMIN_URI = exports.FRONTEND_URI = exports.SPECIAL_URI = exports.HFS_REPO = exports.COMPATIBLE_API_VERSION = exports.API_VERSION = exports.DAY = exports.RUNNING_BETA = exports.VERSION = exports.BUILD_TIMESTAMP = exports.HFS_STARTED = exports.ORIGINAL_CWD = exports.DEV = exports.argv = void 0;
30
+ exports.APP_PATH = exports.IS_BINARY = exports.IS_MAC = exports.IS_WINDOWS = exports.HTTP_SERVICE_UNAVAILABLE = exports.HTTP_SERVER_ERROR = exports.HTTP_FAILED_DEPENDENCY = exports.HTTP_FOOL = exports.HTTP_RANGE_NOT_SATISFIABLE = exports.HTTP_PAYLOAD_TOO_LARGE = exports.HTTP_CONFLICT = exports.HTTP_NOT_ACCEPTABLE = exports.HTTP_METHOD_NOT_ALLOWED = exports.HTTP_NOT_FOUND = exports.HTTP_FORBIDDEN = exports.HTTP_UNAUTHORIZED = exports.HTTP_BAD_REQUEST = exports.HTTP_NOT_MODIFIED = exports.HTTP_TEMPORARY_REDIRECT = exports.HTTP_PARTIAL_CONTENT = exports.HTTP_NO_CONTENT = exports.HTTP_OK = exports.PLUGINS_PUB_URI = exports.API_URI = exports.ADMIN_URI = exports.FRONTEND_URI = exports.SPECIAL_URI = exports.HFS_REPO_BRANCH = exports.HFS_REPO = exports.COMPATIBLE_API_VERSION = exports.API_VERSION = exports.RUNNING_BETA = exports.VERSION = exports.BUILD_TIMESTAMP = exports.HFS_STARTED = exports.ORIGINAL_CWD = exports.DEV = exports.argv = void 0;
31
31
  const minimist_1 = __importDefault(require("minimist"));
32
32
  const fs = __importStar(require("fs"));
33
33
  const os_1 = require("os");
@@ -38,14 +38,14 @@ exports.DEV = process.env.DEV || exports.argv.dev ? 'DEV' : '';
38
38
  exports.ORIGINAL_CWD = process.cwd();
39
39
  exports.HFS_STARTED = new Date();
40
40
  const PKG_PATH = (0, path_1.join)(__dirname, '..', 'package.json');
41
- exports.BUILD_TIMESTAMP = "2023-09-07T17:51:10.938Z";
41
+ exports.BUILD_TIMESTAMP = "2023-09-17T18:48:53.971Z";
42
42
  const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
43
43
  exports.VERSION = pkg.version;
44
44
  exports.RUNNING_BETA = exports.VERSION.includes('-');
45
- exports.DAY = 86400000;
46
45
  exports.API_VERSION = 8.3;
47
46
  exports.COMPATIBLE_API_VERSION = 1; // while changes in the api are not breaking, this number stays the same, otherwise it is made equal to API_VERSION
48
47
  exports.HFS_REPO = 'rejetto/hfs';
48
+ exports.HFS_REPO_BRANCH = exports.RUNNING_BETA ? 'next' : 'main';
49
49
  exports.SPECIAL_URI = '/~/';
50
50
  exports.FRONTEND_URI = exports.SPECIAL_URI + 'frontend/';
51
51
  exports.ADMIN_URI = exports.SPECIAL_URI + 'admin/';
@@ -99,3 +99,4 @@ else if (!winExe) { // still considering whether to use this behavior with Windo
99
99
  process.chdir(dir);
100
100
  }
101
101
  console.log('cwd', process.cwd());
102
+ console.log('node', process.version);
package/src/cross.js CHANGED
@@ -3,13 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.repeat = exports.asyncGeneratorToArray = exports.filterMapGenerator = exports.throw_ = exports.hasProp = exports.typedEntries = exports.typedKeys = exports.objRenameKey = exports.randomId = exports.getOrSet = exports.waitFor = exports.newObj = exports.removeStarting = exports.findDefined = exports.isOrderedEqual = exports.swap = exports.tryJson = exports.basename = exports.pendingPromise = exports._log = exports.wantArray = exports.formatPerc = exports.with_ = exports.try_ = exports.setHidden = exports.onlyTruthy = exports.truthy = exports.enforceFinal = exports.objSameKeys = exports.wait = exports.prefix = exports.formatBytes = exports.MINUTE = exports.WIKI_URL = exports.REPO_URL = void 0;
6
+ exports.xlate = exports.isEqualLax = exports.isWindowsDrive = exports.isIP = exports.isPrimitive = exports.formatTimestamp = exports.repeat = exports.asyncGeneratorToArray = exports.filterMapGenerator = exports.throw_ = exports.hasProp = exports.typedEntries = exports.typedKeys = exports.objRenameKey = exports.randomId = exports.getOrSet = exports.waitFor = exports.newObj = exports.removeStarting = exports.findDefined = exports.isOrderedEqual = exports.swap = exports.tryJson = exports.basename = exports.pendingPromise = exports._log = exports.wantArray = exports.formatPerc = exports.with_ = exports.try_ = exports.setHidden = exports.onlyTruthy = exports.truthy = exports.enforceFinal = exports.objSameKeys = exports.wait = exports.prefix = exports.formatBytes = exports.DAY = exports.HOUR = exports.MINUTE = exports.WIKI_URL = exports.REPO_URL = void 0;
7
7
  // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
8
8
  // all content here is shared between client and server
9
9
  const lodash_1 = __importDefault(require("lodash"));
10
10
  exports.REPO_URL = 'https://github.com/rejetto/hfs/';
11
11
  exports.WIKI_URL = exports.REPO_URL + 'wiki/';
12
12
  exports.MINUTE = 60000;
13
+ exports.HOUR = 60 * exports.MINUTE;
14
+ exports.DAY = 24 * exports.HOUR;
13
15
  const MULTIPLIERS = ['', 'K', 'M', 'G', 'T'];
14
16
  function formatBytes(n, { post = 'B', k = 1024, digits = NaN } = {}) {
15
17
  if (isNaN(Number(n)) || n < 0)
@@ -219,3 +221,30 @@ function repeat(every, cb) {
219
221
  Promise.allSettled([cb()]).then(() => setTimeout(() => repeat(every, cb), every));
220
222
  }
221
223
  exports.repeat = repeat;
224
+ function formatTimestamp(x) {
225
+ return x ? new Date(x).toLocaleString() : '-';
226
+ }
227
+ exports.formatTimestamp = formatTimestamp;
228
+ function isPrimitive(x) {
229
+ return !x || Object(x) !== x;
230
+ }
231
+ exports.isPrimitive = isPrimitive;
232
+ function isIP(address) {
233
+ return /^([.:\da-f]+)$/i.test(address);
234
+ }
235
+ exports.isIP = isIP;
236
+ function isWindowsDrive(s) {
237
+ return s && /^[a-zA-Z]:$/.test(s);
238
+ }
239
+ exports.isWindowsDrive = isWindowsDrive;
240
+ function isEqualLax(a, b) {
241
+ return a == b //eslint-disable-line
242
+ || (a && b && typeof a === 'object' && typeof b === 'object'
243
+ && Object.entries(a).every(([k, v]) => isEqualLax(v, b[k])));
244
+ }
245
+ exports.isEqualLax = isEqualLax;
246
+ function xlate(input, table) {
247
+ var _a;
248
+ return (_a = table[input]) !== null && _a !== void 0 ? _a : input;
249
+ }
250
+ exports.xlate = xlate;