diff --git a/lib/cwmp.ts b/lib/cwmp.ts index a2b6250..2305cef 100644 --- a/lib/cwmp.ts +++ b/lib/cwmp.ts @@ -48,7 +48,7 @@ import { CpeFault, } from "./types"; import { IncomingMessage, ServerResponse } from "http"; -import { Readable } from "stream"; +import { pipeline, Readable } from "stream"; import { promisify } from "util"; import { decode, encodingExists } from "iconv-lite"; import { parseXmlDeclaration } from "./xml-parser"; @@ -1451,10 +1451,14 @@ async function listenerAsync( if (httpRequest.headers["content-encoding"]) { switch (httpRequest.headers["content-encoding"]) { case "gzip": - stream = httpRequest.pipe(zlib.createGunzip()); + stream = pipeline(stream, zlib.createGunzip(), () => { + // Errors are also raised by the async iterator + }); break; case "deflate": - stream = httpRequest.pipe(zlib.createInflate()); + stream = pipeline(stream, zlib.createInflate(), () => { + // Errors are also raised by the async iterator + }); break; default: httpResponse.writeHead(415, { Connection: "close" }); @@ -1463,34 +1467,17 @@ async function listenerAsync( } } - const body = await new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - let bytes = 0; + const chunks: Buffer[] = []; + try { + for await (const chunk of stream) chunks.push(chunk); + // In Node versions prior to 15, the stream will not emit an error if the + // connection is closed before the stream is finished. + if (!stream.readableEnded) throw new Error("Connection closed"); + } catch (err) { + return; + } - stream.on("data", (chunk) => { - chunks.push(chunk); - bytes += chunk.length; - }); - - stream.on("end", () => { - const _body = Buffer.allocUnsafe(bytes); - let offset = 0; - for (const chunk of chunks) { - chunk.copy(_body, offset, 0, chunk.length); - offset += chunk.length; - } - resolve(_body); - }); - - stream.on("error", reject); - - httpRequest.on("aborted", () => { - resolve(null); - }); - }); - - // Request aborted - if (!body) return; + const body = Buffer.concat(chunks); let sessionContext = currentSessions.get(httpRequest.socket); diff --git a/lib/fs.ts b/lib/fs.ts index f1bf1fb..96bf4da 100644 --- a/lib/fs.ts +++ b/lib/fs.ts @@ -19,6 +19,7 @@ import * as url from "url"; import { IncomingMessage, ServerResponse } from "http"; +import { pipeline, Readable } from "stream"; import { Collection, GridFSBucket } from "mongodb"; import { onConnect } from "./db"; import * as logger from "./logger"; @@ -34,21 +35,19 @@ onConnect(async (db) => { }); const getFile = memoize( - (uploadDate: number, size: number, filename: string): Promise => { - return new Promise((resolve, reject) => { - const buffer = Buffer.allocUnsafe(size); - let i = 0; - const downloadStream = filesBucket.openDownloadStreamByName(filename); - downloadStream.on("error", reject); - downloadStream.on("data", (data: Buffer) => { - data.copy(buffer, i); - i += data.length; - }); - downloadStream.on("end", () => { - if (i !== size) reject(new Error("File size mismatch")); - else resolve(buffer); - }); - }); + async ( + uploadDate: number, + size: number, + filename: string + ): Promise> => { + const chunks: Buffer[] = []; + const downloadStream = filesBucket.openDownloadStreamByName(filename); + for await (const chunk of downloadStream) chunks.push(chunk); + // Node 12-14 don't throw error when stream is closed prematurely. + // However, we don't need to check for that since we're checking file size. + if (size !== chunks.reduce((a, b) => a + b.length, 0)) + throw new Error("File size mismatch"); + return chunks; } ); @@ -81,7 +80,7 @@ export async function listener( return; } - const buffer = await getFile( + const chunks = await getFile( file["uploadDate"].getTime(), file.length, filename @@ -92,6 +91,9 @@ export async function listener( "Content-Length": file.length, }); - response.end(buffer); + pipeline(Readable.from(chunks), response, () => { + // Ignore errors resulting from client disconnecting + }); + logger.accessInfo(log); } diff --git a/lib/ui/db.ts b/lib/ui/db.ts index bd1ffee..b9f6b38 100644 --- a/lib/ui/db.ts +++ b/lib/ui/db.ts @@ -23,7 +23,7 @@ import { onConnect, optimizeProjection } from "../db"; import * as mongodbFunctions from "../mongodb-functions"; import * as expression from "../common/expression"; import { QueryOptions, Expression } from "../types"; -import { Readable, Writable } from "stream"; +import { Readable } from "stream"; import { minimize } from "../common/boolean-expression"; const RESOURCE_COLLECTION = { @@ -342,10 +342,19 @@ export function putFile( metadata: metadata, } ); + + contentStream.on("close", () => { + if (!contentStream.readableEnded) + uploadStream.destroy(new Error("Stream closed prematurely")); + }); + + contentStream.on("error", (err) => { + uploadStream.destroy(err); + }); + uploadStream.on("error", reject); - contentStream.on("error", reject); uploadStream.on("finish", resolve); - contentStream.pipe(uploadStream as Writable); + contentStream.pipe(uploadStream); }); }