hfs 3.1.0-beta5 → 3.1.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.
- package/admin/assets/{index-BWYlCMa-.js → index-14Q6fQhw.js} +1 -1
- package/admin/assets/{index-BfKB7Toy.js → index-VnuTY7OR.js} +98 -98
- package/admin/assets/{sha512-CUlBOCDV.js → sha512-DDIK-e99.js} +1 -1
- package/admin/index.html +1 -1
- package/frontend/assets/{index-legacy-FtSDGrqy.js → index-legacy-C8oPp-Hh.js} +1 -1
- package/frontend/assets/index-legacy-l9a4YlnQ.js +9 -0
- package/frontend/assets/{sha512-legacy-BATa4BG9.js → sha512-legacy-DOR1zo9J.js} +1 -1
- package/frontend/index.html +1 -1
- package/npm-shrinkwrap.json +16 -16
- package/package.json +1 -1
- package/src/cross-const.js +2 -1
- package/src/cross.js +1 -0
- package/src/index.js +6 -5
- package/src/log.js +12 -10
- package/src/makeQ.js +10 -4
- package/src/multipartUpload.js +7 -1
- package/src/serveGuiAndSharedFiles.js +10 -8
- package/src/serveGuiFiles.js +2 -1
- package/src/upload.js +4 -3
- package/src/vfs.js +5 -13
- package/src/webdav.js +84 -44
- package/frontend/assets/index-legacy-C91Lfwww.js +0 -9
package/src/webdav.js
CHANGED
|
@@ -3,7 +3,8 @@ 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.
|
|
6
|
+
exports.webdav = void 0;
|
|
7
|
+
exports.releaseWebdavLock = releaseWebdavLock;
|
|
7
8
|
const consumers_1 = require("node:stream/consumers");
|
|
8
9
|
const vfs_1 = require("./vfs");
|
|
9
10
|
const cross_1 = require("./cross");
|
|
@@ -33,13 +34,26 @@ const LOCK_MAX_SECONDS = cross_1.DAY / 1000;
|
|
|
33
34
|
const xmlParser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false, removeNSPrefix: true, trimValues: true });
|
|
34
35
|
const canOverwrite = new Set();
|
|
35
36
|
const locks = new Map();
|
|
36
|
-
function
|
|
37
|
+
function releaseWebdavLock(path) {
|
|
37
38
|
const lock = locks.get(path);
|
|
38
39
|
if (!lock)
|
|
39
40
|
return false;
|
|
41
|
+
clearTimeout(lock.timeout);
|
|
42
|
+
locks.delete(path);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
async function isLocked(path, ctx) {
|
|
46
|
+
const lock = locks.get(path);
|
|
47
|
+
if (!lock)
|
|
48
|
+
return false;
|
|
49
|
+
// if the resource is gone, keeping the lock only creates fake 423 responses
|
|
50
|
+
if (!await (0, vfs_1.urlToNode)(path, ctx)) {
|
|
51
|
+
releaseWebdavLock(path);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
40
54
|
const ifHeader = ctx.get('If');
|
|
41
55
|
const tokenHeader = ctx.get(TOKEN_HEADER);
|
|
42
|
-
if (hasToken(ifHeader, lock.token) || hasToken(tokenHeader, lock.token))
|
|
56
|
+
if (isSameLockPrincipal(lock, ctx) && (hasToken(ifHeader, lock.token) || hasToken(tokenHeader, lock.token)))
|
|
43
57
|
return false;
|
|
44
58
|
ctx.status = cross_1.HTTP_LOCKED;
|
|
45
59
|
return true;
|
|
@@ -49,7 +63,13 @@ function hasToken(header, token) {
|
|
|
49
63
|
return false;
|
|
50
64
|
return header.includes(`<${token}>`) || header.split(/[,;\s]+/).includes(token);
|
|
51
65
|
}
|
|
52
|
-
|
|
66
|
+
function getWebdavPrincipal(ctx) {
|
|
67
|
+
return (0, auth_1.getCurrentUsername)(ctx) || '';
|
|
68
|
+
}
|
|
69
|
+
function isSameLockPrincipal(lock, ctx) {
|
|
70
|
+
return lock.principal === getWebdavPrincipal(ctx);
|
|
71
|
+
}
|
|
72
|
+
const webdav = async (ctx, next) => {
|
|
53
73
|
let { path } = ctx;
|
|
54
74
|
path = path.replace(/^\/+/, '/'); // double-slash is causing empty listing in filezilla-pro
|
|
55
75
|
const ua = ctx.get('user-agent');
|
|
@@ -61,25 +81,26 @@ async function handledWebdav(ctx) {
|
|
|
61
81
|
if (isWebdavAuthRequest && ua && (0, auth_1.getCurrentUsername)(ctx))
|
|
62
82
|
webdavDetectedAgents.try(webdavAgentKey(ctx, ua), () => true);
|
|
63
83
|
if (ctx.method === 'OPTIONS') {
|
|
64
|
-
if (ctx.get('Access-Control-Request-Method'))
|
|
65
|
-
return;
|
|
84
|
+
if (ctx.get('Access-Control-Request-Method')) // it's a preflight cors request, not webdav
|
|
85
|
+
return next();
|
|
66
86
|
setWebdavHeaders();
|
|
67
87
|
ctx.body = '';
|
|
68
|
-
return
|
|
88
|
+
return;
|
|
69
89
|
}
|
|
70
90
|
if (isWebdavAuthRequest && shouldChallengeWebdav())
|
|
71
|
-
return
|
|
91
|
+
return;
|
|
72
92
|
if (ctx.method === 'PUT') {
|
|
73
|
-
if (isLocked(path, ctx))
|
|
74
|
-
return
|
|
93
|
+
if (await isLocked(path, ctx))
|
|
94
|
+
return;
|
|
95
|
+
const overwriteGraceKey = path + (0, cross_1.prefix)('|', (0, auth_1.getCurrentUsername)(ctx)); // bind temporary overwrite grace to the authenticated user so accounts cannot reuse each other's grace window
|
|
75
96
|
// Finder first creates an empty file (a test?) then wants to overwrite it, which requires deletion permission, but the user may not have it, causing a renamed upload. To solve, so we give it special permission for a few seconds.
|
|
76
97
|
const x = ctx.get('x-expected-entity-length'); // field used by Finder's webdav on actual upload, after
|
|
77
98
|
if (!x && !ctx.length) {
|
|
78
|
-
canOverwrite.add(
|
|
79
|
-
setTimeout(() => canOverwrite.delete(
|
|
99
|
+
canOverwrite.add(overwriteGraceKey);
|
|
100
|
+
setTimeout(() => canOverwrite.delete(overwriteGraceKey), 10_000); // grace period
|
|
80
101
|
}
|
|
81
|
-
else if (canOverwrite.has(
|
|
82
|
-
canOverwrite.delete(
|
|
102
|
+
else if (canOverwrite.has(overwriteGraceKey)) {
|
|
103
|
+
canOverwrite.delete(overwriteGraceKey);
|
|
83
104
|
const node = await (0, vfs_1.urlToNode)(path, ctx);
|
|
84
105
|
if (node?.source)
|
|
85
106
|
await (0, promises_1.rm)(node.source).catch(() => { });
|
|
@@ -88,25 +109,27 @@ async function handledWebdav(ctx) {
|
|
|
88
109
|
ctx.req.headers['content-length'] = x;
|
|
89
110
|
if (KNOWN_UA.test(ua) || webdavDetectedAgents.has(webdavAgentKey(ctx, ua)))
|
|
90
111
|
ctx.query.existing ??= 'overwrite'; // with webdav this is our default
|
|
91
|
-
return;
|
|
112
|
+
return next();
|
|
92
113
|
}
|
|
93
114
|
if (ctx.method === 'MKCOL') {
|
|
94
115
|
setWebdavHeaders();
|
|
95
|
-
if (isLocked(path, ctx))
|
|
96
|
-
return
|
|
116
|
+
if (await isLocked(path, ctx))
|
|
117
|
+
return;
|
|
97
118
|
const node = await (0, vfs_1.urlToNode)(path, ctx);
|
|
98
|
-
if (node)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
119
|
+
if (node) {
|
|
120
|
+
ctx.status = cross_1.HTTP_METHOD_NOT_ALLOWED;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const parentNode = await (0, vfs_1.urlToNode)((0, path_1.dirname)(path), ctx);
|
|
124
|
+
if (!parentNode) // this is a bit incoherent with the way we handle PUT, which doesn't stop in this case, but it's by RFC 4918 section 9.3
|
|
125
|
+
return ctx.status = cross_1.HTTP_CONFLICT;
|
|
126
|
+
const name = (0, cross_1.safeDecodeURIComponent)((0, path_1.basename)(path), '');
|
|
104
127
|
if (!(0, misc_1.isValidFileName)(name))
|
|
105
128
|
return ctx.status = cross_1.HTTP_BAD_REQUEST;
|
|
106
129
|
if ((0, vfs_1.statusCodeForMissingPerm)(parentNode, 'can_upload', ctx)) {
|
|
107
130
|
if (ctx.status === cross_1.HTTP_UNAUTHORIZED)
|
|
108
131
|
setWebdavHeaders(true);
|
|
109
|
-
return
|
|
132
|
+
return;
|
|
110
133
|
}
|
|
111
134
|
try {
|
|
112
135
|
await (0, promises_1.mkdir)((0, path_1.join)(parentNode.source, name));
|
|
@@ -118,21 +141,26 @@ async function handledWebdav(ctx) {
|
|
|
118
141
|
}
|
|
119
142
|
if (ctx.method === 'MOVE') {
|
|
120
143
|
setWebdavHeaders();
|
|
121
|
-
if (isLocked(path, ctx))
|
|
122
|
-
return
|
|
144
|
+
if (await isLocked(path, ctx))
|
|
145
|
+
return;
|
|
123
146
|
const node = await (0, vfs_1.urlToNode)(path, ctx);
|
|
124
147
|
if (!node)
|
|
125
|
-
return;
|
|
148
|
+
return next();
|
|
126
149
|
let dest = ctx.get('destination');
|
|
127
150
|
const i = dest.indexOf('//');
|
|
128
151
|
if (i >= 0)
|
|
129
152
|
dest = dest.slice(dest.indexOf('/', i + 2));
|
|
130
153
|
dest = (0, cross_1.join)(ctx.state.root || '', dest); // on Windows, we must use / as the delimiter to be able to compare with `path` below
|
|
131
|
-
if (isLocked(dest, ctx))
|
|
132
|
-
return
|
|
154
|
+
if (await isLocked(dest, ctx))
|
|
155
|
+
return;
|
|
133
156
|
if ((0, path_1.dirname)(path) === (0, path_1.dirname)(dest)) // rename case. `path` is is encoded, so we test before decoding `dest`
|
|
134
157
|
try {
|
|
135
|
-
|
|
158
|
+
// decode the single path segment so reserved chars like %2C become their real name on rename
|
|
159
|
+
const newName = (0, cross_1.safeDecodeURIComponent)((0, path_1.basename)(dest), '');
|
|
160
|
+
if (!newName)
|
|
161
|
+
return ctx.status = cross_1.HTTP_BAD_REQUEST;
|
|
162
|
+
await (0, frontEndApis_1.requestedRename)(node, newName, ctx);
|
|
163
|
+
releaseWebdavLock(path); // RFC 4918 says MOVE must not carry locks to destination, so clear source lock on success
|
|
136
164
|
return ctx.status = cross_1.HTTP_CREATED;
|
|
137
165
|
}
|
|
138
166
|
catch (e) {
|
|
@@ -142,13 +170,18 @@ async function handledWebdav(ctx) {
|
|
|
142
170
|
if (moveRes instanceof Error)
|
|
143
171
|
return ctx.status = moveRes.status || cross_1.HTTP_SERVER_ERROR;
|
|
144
172
|
const err = moveRes?.errors?.[0];
|
|
173
|
+
if (!err)
|
|
174
|
+
releaseWebdavLock(path); // successful MOVE leaves the old path invalid, therefore its lock must be dropped
|
|
145
175
|
return ctx.status = !err ? cross_1.HTTP_CREATED : typeof err === 'number' ? err : cross_1.HTTP_SERVER_ERROR;
|
|
146
176
|
}
|
|
147
177
|
if (ctx.method === 'DELETE') {
|
|
148
178
|
setWebdavHeaders();
|
|
149
|
-
if (isLocked(path, ctx))
|
|
150
|
-
return
|
|
151
|
-
|
|
179
|
+
if (await isLocked(path, ctx))
|
|
180
|
+
return;
|
|
181
|
+
await next();
|
|
182
|
+
if (ctx.status === cross_1.HTTP_OK)
|
|
183
|
+
releaseWebdavLock(path); // webdav clients may forget UNLOCK; successful delete must clear any lock
|
|
184
|
+
return;
|
|
152
185
|
}
|
|
153
186
|
if (ctx.method === 'UNLOCK') {
|
|
154
187
|
setWebdavHeaders();
|
|
@@ -156,8 +189,10 @@ async function handledWebdav(ctx) {
|
|
|
156
189
|
const lock = locks.get(path);
|
|
157
190
|
if (x !== lock?.token)
|
|
158
191
|
return ctx.status = cross_1.HTTP_BAD_REQUEST;
|
|
159
|
-
|
|
160
|
-
|
|
192
|
+
// with force_webdav_login disabled a client may silently fall back to anonymous; keep lock ownership on the original principal
|
|
193
|
+
if (!isSameLockPrincipal(lock, ctx))
|
|
194
|
+
return ctx.status = cross_1.HTTP_PRECONDITION_FAILED;
|
|
195
|
+
releaseWebdavLock(path);
|
|
161
196
|
ctx.set(TOKEN_HEADER, x);
|
|
162
197
|
if (const_1.IS_MAC)
|
|
163
198
|
(0, vfs_1.urlToNode)(path, ctx).then(x => x?.source && dotClean((0, path_1.dirname)(x.source)));
|
|
@@ -176,14 +211,17 @@ async function handledWebdav(ctx) {
|
|
|
176
211
|
const lock = locks.get(path);
|
|
177
212
|
if (token !== lock?.token)
|
|
178
213
|
return ctx.status = cross_1.HTTP_PRECONDITION_FAILED;
|
|
214
|
+
// same-token refresh from another principal would make abandoned locks effectively persistent
|
|
215
|
+
if (!isSameLockPrincipal(lock, ctx))
|
|
216
|
+
return ctx.status = cross_1.HTTP_PRECONDITION_FAILED;
|
|
179
217
|
// refresh lock – keep the same token on refresh so clients can continue using the lock they already hold
|
|
180
218
|
clearTimeout(lock.timeout);
|
|
181
|
-
lock.timeout = setTimeout(() =>
|
|
219
|
+
lock.timeout = setTimeout(() => releaseWebdavLock(path), seconds * 1000);
|
|
182
220
|
lock.seconds = seconds;
|
|
183
221
|
locks.set(path, lock);
|
|
184
222
|
ctx.set(TOKEN_HEADER, lock.token);
|
|
185
223
|
ctx.body = renderLockResponse(lock.token, lock.seconds);
|
|
186
|
-
return
|
|
224
|
+
return;
|
|
187
225
|
}
|
|
188
226
|
const lockinfo = (0, cross_1.try_)(() => xmlParser.parse(body).lockinfo);
|
|
189
227
|
const scope = lodash_1.default.keys(lockinfo?.lockscope)[0];
|
|
@@ -197,11 +235,11 @@ async function handledWebdav(ctx) {
|
|
|
197
235
|
if (locks.has(path))
|
|
198
236
|
return ctx.status = cross_1.HTTP_LOCKED;
|
|
199
237
|
const newToken = 'urn:uuid:' + (0, node_crypto_1.randomUUID)();
|
|
200
|
-
const timeout = setTimeout(() =>
|
|
201
|
-
locks.set(path, { token: newToken, timeout, seconds });
|
|
238
|
+
const timeout = setTimeout(() => releaseWebdavLock(path), seconds * 1000);
|
|
239
|
+
locks.set(path, { token: newToken, timeout, seconds, principal: getWebdavPrincipal(ctx) });
|
|
202
240
|
ctx.set(TOKEN_HEADER, newToken);
|
|
203
241
|
ctx.body = renderLockResponse(newToken, seconds);
|
|
204
|
-
return
|
|
242
|
+
return;
|
|
205
243
|
function getProvidedLockToken(ctx) {
|
|
206
244
|
const direct = ctx.get(TOKEN_HEADER).replace(/[<>]/g, '');
|
|
207
245
|
if (direct)
|
|
@@ -224,14 +262,14 @@ async function handledWebdav(ctx) {
|
|
|
224
262
|
setWebdavHeaders();
|
|
225
263
|
const node = await (0, vfs_1.urlToNode)(path, ctx);
|
|
226
264
|
if (!node)
|
|
227
|
-
return;
|
|
265
|
+
return next();
|
|
228
266
|
let depth = Number(ctx.get('depth'));
|
|
229
267
|
depth = isNaN(depth) ? Infinity : depth;
|
|
230
268
|
const isList = depth !== 0;
|
|
231
269
|
if ((0, vfs_1.statusCodeForMissingPerm)(node, isList ? 'can_list' : 'can_see', ctx)) {
|
|
232
270
|
if (ctx.status === cross_1.HTTP_UNAUTHORIZED)
|
|
233
271
|
setWebdavHeaders(true);
|
|
234
|
-
return
|
|
272
|
+
return;
|
|
235
273
|
}
|
|
236
274
|
ctx.type = 'xml';
|
|
237
275
|
ctx.status = 207;
|
|
@@ -246,7 +284,7 @@ async function handledWebdav(ctx) {
|
|
|
246
284
|
}
|
|
247
285
|
res.write(`</multistatus>`);
|
|
248
286
|
res.end();
|
|
249
|
-
return
|
|
287
|
+
return;
|
|
250
288
|
async function sendEntry(node, append = false) {
|
|
251
289
|
if ((0, vfs_1.nodeIsLink)(node))
|
|
252
290
|
return;
|
|
@@ -272,6 +310,7 @@ async function handledWebdav(ctx) {
|
|
|
272
310
|
setWebdavHeaders();
|
|
273
311
|
return ctx.status = cross_1.HTTP_METHOD_NOT_ALLOWED;
|
|
274
312
|
}
|
|
313
|
+
return next();
|
|
275
314
|
function setWebdavHeaders(authenticate = false) {
|
|
276
315
|
ctx.set('DAV', '1,2');
|
|
277
316
|
ctx.set('MS-Author-Via', 'DAV');
|
|
@@ -300,7 +339,8 @@ async function handledWebdav(ctx) {
|
|
|
300
339
|
return true;
|
|
301
340
|
}
|
|
302
341
|
}
|
|
303
|
-
}
|
|
342
|
+
};
|
|
343
|
+
exports.webdav = webdav;
|
|
304
344
|
function compileWebdavAgentRegex(v) {
|
|
305
345
|
return !v ? null : v === true ? /.*/ : new RegExp(v.trim(), 'i');
|
|
306
346
|
}
|