mikser-io 11.5.0 → 11.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/routes.js +42 -4
- package/src/server.js +47 -1
package/package.json
CHANGED
package/src/routes.js
CHANGED
|
@@ -50,7 +50,12 @@ const DEFAULT_AUTH_LABEL = {
|
|
|
50
50
|
// an origin is known — public --url / config.url wins (clickable,
|
|
51
51
|
// shareable), localhost:port is the dev fallback — bare path when the
|
|
52
52
|
// engine doesn't own a listener (external-app embedding, no url set).
|
|
53
|
-
export function routeLocation(displayPath) {
|
|
53
|
+
export function routeLocation(displayPath, host) {
|
|
54
|
+
// A host-scoped route HAS an origin, and it is not this server's. Printing
|
|
55
|
+
// `http://localhost:3001//drive.example.com/` names a place nothing
|
|
56
|
+
// answers. Protocol-relative because the scheme belongs to whatever
|
|
57
|
+
// terminates TLS in front, which this process does not know.
|
|
58
|
+
if (host) return `//${host}${displayPath === '/' ? '/' : displayPath}`
|
|
54
59
|
const origin = runtime.options.url
|
|
55
60
|
?? (runtime.options.port ? `http://localhost:${runtime.options.port}` : null)
|
|
56
61
|
return origin ? `${origin}${displayPath}` : displayPath
|
|
@@ -62,12 +67,21 @@ export function routeLocation(displayPath) {
|
|
|
62
67
|
// '/drive') answers for its own requests rather than its parent's. Exported
|
|
63
68
|
// because the CORS middleware needs the same answer Express will reach, and a
|
|
64
69
|
// second implementation of "which mount is this" would drift from this one.
|
|
65
|
-
export function routeFor(requestPath) {
|
|
70
|
+
export function routeFor(requestPath, host) {
|
|
66
71
|
if (!requestPath) return null
|
|
67
72
|
let best = null
|
|
68
73
|
for (const route of runtime.routes ?? []) {
|
|
74
|
+
// A host-scoped route answers for its own domain and no other.
|
|
75
|
+
if (route.host && route.host !== host) continue
|
|
69
76
|
const base = route.path
|
|
70
|
-
|
|
77
|
+
// A host-scoped route mounted at `/` owns every path on that host —
|
|
78
|
+
// it IS the server there. Prefix matching cannot express that, since
|
|
79
|
+
// nothing starts with `//`. An unscoped route at `/` is left alone:
|
|
80
|
+
// it would otherwise swallow the whole site.
|
|
81
|
+
const owns = base === '/' && route.host
|
|
82
|
+
? true
|
|
83
|
+
: requestPath === base || requestPath.startsWith(`${base}/`)
|
|
84
|
+
if (!owns) continue
|
|
71
85
|
if (!best || base.length > best.path.length) best = route
|
|
72
86
|
}
|
|
73
87
|
return best
|
|
@@ -87,6 +101,22 @@ export function routeFor(requestPath) {
|
|
|
87
101
|
// facade must disable buffering for it (Caddy
|
|
88
102
|
// flush_interval -1, nginx proxy_buffering off).
|
|
89
103
|
// Default false.
|
|
104
|
+
// host when set, the mount answers only for this Host. Used
|
|
105
|
+
// by a route that takes a domain of its own — a WebDAV
|
|
106
|
+
// share at `/` on `drive.example.com`, say, which would
|
|
107
|
+
// otherwise shadow every page on the site.
|
|
108
|
+
// cors `false` to emit no CORS headers for this mount. The
|
|
109
|
+
// same word as the global `--no-cors` /
|
|
110
|
+
// `config.server.cors: false`, meaning the same thing,
|
|
111
|
+
// scoped to one route. Default undefined — inherit the
|
|
112
|
+
// global setting.
|
|
113
|
+
//
|
|
114
|
+
// For a mount whose clients are not browsers: an OS
|
|
115
|
+
// WebDAV redirector, a CLI tool and curl never consult
|
|
116
|
+
// CORS, so a header naming who may read the endpoint
|
|
117
|
+
// from a web page is a claim with no beneficiary — and
|
|
118
|
+
// on an authenticated endpoint it is better made
|
|
119
|
+
// deliberately than inherited.
|
|
90
120
|
// methods the HTTP verbs this mount actually serves. Two
|
|
91
121
|
// consequences, and the second one is load-bearing:
|
|
92
122
|
// CORS advertises these for the route instead of a
|
|
@@ -125,6 +155,8 @@ export function registerRoute({
|
|
|
125
155
|
reachability = 'public',
|
|
126
156
|
streaming = false,
|
|
127
157
|
methods = null,
|
|
158
|
+
cors,
|
|
159
|
+
host,
|
|
128
160
|
label,
|
|
129
161
|
detail,
|
|
130
162
|
displayPath,
|
|
@@ -142,8 +174,14 @@ export function registerRoute({
|
|
|
142
174
|
throw new Error('registerRoute: `methods` must be an array of verb strings')
|
|
143
175
|
}
|
|
144
176
|
|
|
177
|
+
if (cors !== undefined && cors !== false) {
|
|
178
|
+
throw new Error('registerRoute: `cors` accepts only false — omit it to inherit the global setting')
|
|
179
|
+
}
|
|
180
|
+
|
|
145
181
|
const descriptor = { path, plugin, reachability, streaming }
|
|
146
182
|
if (methods) descriptor.methods = methods.map(m => m.toUpperCase())
|
|
183
|
+
if (cors === false) descriptor.cors = false
|
|
184
|
+
if (host) descriptor.host = host
|
|
147
185
|
|
|
148
186
|
// Dedup by path — a re-register (same path) replaces rather than
|
|
149
187
|
// duplicates. Mounts happen once per process, but this keeps the
|
|
@@ -154,7 +192,7 @@ export function registerRoute({
|
|
|
154
192
|
|
|
155
193
|
const logger = useLogger()
|
|
156
194
|
if (logger) {
|
|
157
|
-
const location = routeLocation(displayPath ?? path)
|
|
195
|
+
const location = routeLocation(displayPath ?? path, host)
|
|
158
196
|
const bracket = authLabel ?? DEFAULT_AUTH_LABEL[reachability]
|
|
159
197
|
logger.info('%s mounted: %s [%s]%s',
|
|
160
198
|
label ?? plugin, location, bracket, detail ? ` ${detail}` : '')
|
package/src/server.js
CHANGED
|
@@ -223,7 +223,7 @@ export function setupServer() {
|
|
|
223
223
|
// because routes are registered at onLoaded and this middleware
|
|
224
224
|
// at onLoad — it is mounted before any of them exist, and every
|
|
225
225
|
// request arrives long after they all do.
|
|
226
|
-
const route = routeFor(req.path ?? req.url)
|
|
226
|
+
const route = routeFor(req.path ?? req.url, req.hostname)
|
|
227
227
|
|
|
228
228
|
// Does that mount answer OPTIONS itself?
|
|
229
229
|
//
|
|
@@ -249,6 +249,52 @@ export function setupServer() {
|
|
|
249
249
|
// PROPFIND, LOCK and the rest.
|
|
250
250
|
const ownsPreflight = Boolean(route?.methods?.includes('OPTIONS'))
|
|
251
251
|
|
|
252
|
+
// A bare OPTIONS is not a preflight, and must not be answered as
|
|
253
|
+
// one.
|
|
254
|
+
//
|
|
255
|
+
// A CORS preflight is defined by its headers: the browser sends
|
|
256
|
+
// `Access-Control-Request-Method`, and without it the request is
|
|
257
|
+
// something else entirely. This middleware used to answer ALL of
|
|
258
|
+
// them — so every path on the site, routed or not, replied 204 to
|
|
259
|
+
// any OPTIONS with a list of five REST verbs and no `DAV:` header.
|
|
260
|
+
//
|
|
261
|
+
// That is a manufactured "I exist and I am not WebDAV", and it is
|
|
262
|
+
// a stronger negative than the 404 a static path would otherwise
|
|
263
|
+
// give. The Microsoft WebDAV redirector establishes a session by
|
|
264
|
+
// walking up from the path it was given, and on a mikser site
|
|
265
|
+
// every level of that walk answered no — including `/` and
|
|
266
|
+
// `/drive`, neither of which is anything. Deployments that DO map
|
|
267
|
+
// on Windows (Nextcloud at /remote.php/dav, SharePoint) sit behind
|
|
268
|
+
// hosts that simply have nothing to say at those paths.
|
|
269
|
+
//
|
|
270
|
+
// Falling through costs nothing a browser wanted: a real preflight
|
|
271
|
+
// still carries the header and is still answered below, and a
|
|
272
|
+
// non-preflight OPTIONS now reaches whatever owns the path — the
|
|
273
|
+
// mount's own discovery response, or the static handler's 404.
|
|
274
|
+
//
|
|
275
|
+
// `origin: false` is how the package steps aside: with a falsy
|
|
276
|
+
// origin it never builds an originCallback, so it calls next()
|
|
277
|
+
// without touching the response (cors/lib/index.js:210-228).
|
|
278
|
+
if (req.method === 'OPTIONS' && !req.headers['access-control-request-method']) {
|
|
279
|
+
return callback(null, { origin: false })
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// A route that wants no CORS headers at all.
|
|
283
|
+
//
|
|
284
|
+
// `origin: false` is the cors package's way of emitting none — the
|
|
285
|
+
// same thing --no-cors asks for globally, scoped to one mount.
|
|
286
|
+
//
|
|
287
|
+
// It needs no preflightContinue, and passing one here was dead
|
|
288
|
+
// code: with a falsy origin the package never builds an
|
|
289
|
+
// originCallback, so it calls next() and does not touch the
|
|
290
|
+
// response at all (cors/lib/index.js:210-228). The mount's own
|
|
291
|
+
// OPTIONS is therefore reached whatever preflightContinue says —
|
|
292
|
+
// which is why removing it changed no test, and why the comment
|
|
293
|
+
// that used to claim otherwise was wrong.
|
|
294
|
+
if (route?.cors === false) {
|
|
295
|
+
return callback(null, { origin: false })
|
|
296
|
+
}
|
|
297
|
+
|
|
252
298
|
callback(null, {
|
|
253
299
|
origin: configured === true ? '*' : String(configured),
|
|
254
300
|
// The verbs the mount actually serves, when it said. A fixed
|