fix(scanner): normalize Epson collection separators
All checks were successful
Build and Release FEDEO / verify (push) Successful in 55s
Build and Release FEDEO / build-and-release (push) Successful in 6m0s

This commit is contained in:
2026-09-30 19:10:59 +02:00
parent b585b972f2
commit 2143ce0933
3 changed files with 25 additions and 2 deletions

View File

@@ -26,7 +26,9 @@ export default async function scannerInboxGateway(server: FastifyInstance, optio
const handle = async (req: FastifyRequest, reply: any) => {
const identity = identities.get(req)!
const filename = (req.params as { "*"?: string })["*"] || ""
// Epson appends its own separator even when the configured inbox URL ends in '/'.
// Remove only extra separators at the collection boundary, not within filenames.
const filename = ((req.params as { "*"?: string })["*"] || "").replace(/^\/+/, "")
if (filename && (filename.length > 200 || filename.startsWith(".") || /[\\/\u0000-\u001f\u007f]/.test(filename))) {
throw scannerError(400, "Unterordner und ungültige Dateinamen werden nicht unterstützt.")
}

View File

@@ -101,6 +101,23 @@ test("Epson discovery works with both slash forms, XML bodies and a backend prox
assert.equal((await app.inject({ method: "PROPFIND", url: "/scanner-inbox/", headers: { ...basic(), depth: "infinity" } })).statusCode, 403)
})
test("Epson's extra separator does not turn filename checks into false collisions", async t => {
const { app, calls } = await protocolFixture()
t.after(() => app.close())
for (const separator of ["//", "///"]) {
const url = `/scanner-inbox${separator}scan.pdf`
assert.equal((await app.inject({ method: "HEAD", url, headers: basic() })).statusCode, 404)
const upload = await app.inject({ method: "PUT", url, headers: basic(), payload: samplePdf })
assert.equal(upload.statusCode, 201)
assert.equal(calls.at(-1)[1], "scan.pdf")
assert.deepEqual(calls.at(-1)[2], samplePdf)
for (const name of ["sub/scan.pdf", "sub%2Fscan.pdf", "..%5Cscan.pdf", ".hidden.pdf", "%2e%2e%2Fscan.pdf"]) {
assert.equal((await app.inject({ method: "PUT", url: `/scanner-inbox${separator}${name}`, headers: basic(), payload: samplePdf })).statusCode, 400)
}
}
assert.equal(calls.length, 2)
})
test("raw PDFs reach storage unchanged and scanner credentials cannot download or delete", async t => {
const { app, calls } = await protocolFixture()
t.after(() => app.close())
@@ -211,6 +228,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)
assert.equal((await app.inject({ method: "HEAD", url: "/scanner-inbox//scan.pdf", headers: basic(first.username, first.password) })).statusCode, 200)
assert.equal((await app.inject({ method: "PUT", url: "/scanner-inbox//scan.pdf", headers: basic(first.username, first.password), payload: pdf })).statusCode, 204)
assert.equal((await service.list(1, 50, 0)).total, 1)
assert.equal((await app.inject({ method: "GET", url: "/scanner-inbox//scan.pdf", headers: basic(first.username, first.password) })).statusCode, 405)
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"))

View File

@@ -22,7 +22,7 @@ Beim ES-580W nach einem Neustart eine gegebenenfalls angezeigte Freigabe der Net
Die vollständige Adresse wird aus der konfigurierten Backend-Adresse gebildet, z. B. `https://fedeo.example/backend/scanner-inbox/`. Ein vorhandener `/backend`-Präfix muss erhalten bleiben. Es ist kein lokaler Rechner oder Geräte-Agent erforderlich.
Grundlage sind die Epson-Handbücher für [ES-580W](https://download.epson.de/pub/download/6371/epson637123eu.pdf) und [ES-C380W](https://download4.epson.biz/sec_pubs/es-c380w/useg/en/GUID-55C5993A-63B2-45C9-B3C9-9C6D6801CDF0.htm). Am physischen ES-580W wurde eine anfängliche `GET`-Prüfung des Empfangsordners beobachtet: Auf die `401`-Anmeldeaufforderung folgt ein authentifiziertes `GET`. Dieses muss erfolgreich beantwortet werden. Einen vollständigen Scan bis zum Eintrag in der Dokumentenablage bei der Inbetriebnahme prüfen.
Grundlage sind die Epson-Handbücher für [ES-580W](https://download.epson.de/pub/download/6371/epson637123eu.pdf) und [ES-C380W](https://download4.epson.biz/sec_pubs/es-c380w/useg/en/GUID-55C5993A-63B2-45C9-B3C9-9C6D6801CDF0.htm). Am physischen ES-580W wurde eine anfängliche `GET`-Prüfung des Empfangsordners beobachtet: Auf die `401`-Anmeldeaufforderung folgt ein authentifiziertes `GET`. Dieses muss erfolgreich beantwortet werden. Danach prüft Epson den Dateinamen per `HEAD`. Bei einer Zieladresse mit abschließendem Schrägstrich hängt das Gerät einen weiteren Trenner an, beispielsweise `/scanner-inbox//scan.pdf`. Der Endpunkt behandelt diese Adresse wie `/scanner-inbox/scan.pdf`; echte Unterordner und ungültige Dateinamen bleiben gesperrt. Ein fälschlich mit `400` beantworteter HEAD-Test führte am Gerät zur Meldung „Dateiname bereits verwendet“. Einen vollständigen Scan bis zum Eintrag in der Dokumentenablage bei der Inbetriebnahme prüfen.
## Schnittstelle und Betrieb