fix(scanner): accept Epson inbox GET preflight
This commit is contained in:
@@ -30,8 +30,12 @@ export default async function scannerInboxGateway(server: FastifyInstance, optio
|
||||
if (filename && (filename.length > 200 || filename.startsWith(".") || /[\\/\u0000-\u001f\u007f]/.test(filename))) {
|
||||
throw scannerError(400, "Unterordner und ungültige Dateinamen werden nicht unterstützt.")
|
||||
}
|
||||
if (req.method === "OPTIONS") return reply.header("DAV", "1").header("Allow", methods).code(200).send()
|
||||
if (req.method === "MKCOL") return reply.header("Allow", methods).code(filename ? 403 : 405).send()
|
||||
const allowedMethods = filename ? methods : `${methods}, GET`
|
||||
if (req.method === "OPTIONS") return reply.header("DAV", "1").header("Allow", allowedMethods).code(200).send()
|
||||
if (req.method === "MKCOL") return reply.header("Allow", allowedMethods).code(filename ? 403 : 405).send()
|
||||
// ES-580W checks authentication with GET before WebDAV discovery or uploading.
|
||||
// Confirm only the collection itself; never return a listing or document content.
|
||||
if (req.method === "GET" && !filename) return reply.header("DAV", "1").code(200).send()
|
||||
if (req.method === "PUT") {
|
||||
if (!filename) throw scannerError(400, "Ein Dateiname ist erforderlich.")
|
||||
if (req.headers["content-range"]) throw scannerError(400, "Bitte die vollständige Datei übertragen.")
|
||||
@@ -62,7 +66,7 @@ export default async function scannerInboxGateway(server: FastifyInstance, optio
|
||||
return reply.header("DAV", "1").type("application/xml; charset=utf-8").code(207).send(
|
||||
`<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:"><d:response><d:href>${escapeXml(href)}</d:href><d:propstat><d:prop><d:displayname>${escapeXml(filename || "FEDEO Dokumentenablage")}</d:displayname>${props}</d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response></d:multistatus>`)
|
||||
}
|
||||
return reply.header("Allow", methods).code(405).send()
|
||||
return reply.header("Allow", allowedMethods).code(405).send()
|
||||
}
|
||||
|
||||
// Both slash forms work without redirects (some scanners discard authentication on redirects).
|
||||
|
||||
@@ -53,7 +53,7 @@ const protocolFixture = async () => {
|
||||
test("WebDAV authentication happens before body parsing, including discovery requests", async t => {
|
||||
const { app } = await protocolFixture()
|
||||
t.after(() => app.close())
|
||||
for (const method of ["OPTIONS", "PROPFIND", "PUT", "HEAD"] as const) {
|
||||
for (const method of ["OPTIONS", "PROPFIND", "PUT", "HEAD", "GET"] as const) {
|
||||
const res = await app.inject({ method, url: "/scanner-inbox/scan.pdf" })
|
||||
assert.equal(res.statusCode, 401)
|
||||
assert.match(res.headers["www-authenticate"] as string, /^Basic /)
|
||||
@@ -62,6 +62,27 @@ test("WebDAV authentication happens before body parsing, including discovery req
|
||||
assert.equal(res.statusCode, 401)
|
||||
})
|
||||
|
||||
test("Epson can authenticate with a GET probe of the inbox without listing documents", async t => {
|
||||
const { app, calls } = await protocolFixture()
|
||||
t.after(() => app.close())
|
||||
for (const url of ["/scanner-inbox", "/scanner-inbox/"]) {
|
||||
const challenge = await app.inject({ method: "GET", url })
|
||||
assert.equal(challenge.statusCode, 401)
|
||||
assert.match(challenge.headers["www-authenticate"] as string, /^Basic /)
|
||||
assert.equal((await app.inject({ method: "GET", url, headers: basic("scanner", "wrong") })).statusCode, 401)
|
||||
const probe = await app.inject({ method: "GET", url, headers: basic() })
|
||||
assert.equal(probe.statusCode, 200)
|
||||
assert.equal(probe.body, "")
|
||||
assert.equal(probe.headers.dav, "1")
|
||||
assert.equal(probe.headers["cache-control"], "no-store")
|
||||
assert.equal(probe.headers.location, undefined)
|
||||
const options = await app.inject({ method: "OPTIONS", url, headers: basic() })
|
||||
assert.ok((options.headers.allow as string).split(/,\s*/).includes("GET"))
|
||||
}
|
||||
assert.equal(calls.length, 0)
|
||||
assert.equal((await app.inject({ method: "GET", url: "/scanner-inbox/missing.pdf", headers: basic() })).statusCode, 404)
|
||||
})
|
||||
|
||||
test("Epson discovery works with both slash forms, XML bodies and a backend proxy prefix", async t => {
|
||||
const { app } = await protocolFixture()
|
||||
t.after(() => app.close())
|
||||
@@ -90,7 +111,7 @@ test("raw PDFs reach storage unchanged and scanner credentials cannot download o
|
||||
assert.equal(calls.at(-1)[1], "Rechnung & Beleg.pdf")
|
||||
assert.deepEqual(calls.at(-1)[2], samplePdf)
|
||||
}
|
||||
for (const method of ["GET", "DELETE", "POST", "PATCH"] as const) {
|
||||
for (const method of ["DELETE", "POST", "PATCH"] as const) {
|
||||
assert.equal((await app.inject({ method, url: "/scanner-inbox/", headers: basic() })).statusCode, 405)
|
||||
}
|
||||
for (const name of ["sub/scan.pdf", "sub%2Fscan.pdf", "..%5Cscan.pdf"]) {
|
||||
@@ -190,6 +211,10 @@ test("scanner inbox integration: migration, persistence, isolation, retries and
|
||||
assert.equal(listing.json().total, 1)
|
||||
assert.equal(listing.json().documents[0].name, "scan.pdf")
|
||||
assert.equal((await app.inject({ method: "HEAD", url: "/scanner-inbox/scan.pdf", headers: basic(first.username, first.password) })).statusCode, 200)
|
||||
const download = await app.inject({ method: "GET", url: "/scanner-inbox/scan.pdf", headers: basic(first.username, first.password) })
|
||||
assert.equal(download.statusCode, 405)
|
||||
assert.ok(!(download.headers.allow as string).split(/,\s*/).includes("GET"))
|
||||
assert.notDeepEqual(download.rawPayload, pdf)
|
||||
})
|
||||
await t.test("serialises simultaneous retries and preserves existing content on filename conflicts", async () => {
|
||||
const retry = await Promise.all([send("scan.pdf"), send("scan.pdf")])
|
||||
|
||||
Reference in New Issue
Block a user