2024-08-30 22:11:16 +02:00
|
|
|
const middleware = require("../../src/middleware/staticFileServingAndDirectoryListings.js");
|
|
|
|
const fs = require("fs");
|
|
|
|
const http = require("http");
|
|
|
|
const httpMocks = require("node-mocks-http");
|
|
|
|
|
|
|
|
jest.mock("fs");
|
|
|
|
|
|
|
|
describe("Static file serving and directory listings middleware", () => {
|
|
|
|
let req, res, logFacilities, config, next;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
req = httpMocks.createRequest({
|
|
|
|
method: "GET",
|
|
|
|
url: "/",
|
|
|
|
headers: {
|
|
|
|
host: "example.com",
|
|
|
|
"accept-encoding": "gzip, deflate, br",
|
2024-09-01 21:54:42 +02:00
|
|
|
"user-agent": "Mozilla/5.0"
|
2024-08-30 22:11:16 +02:00
|
|
|
},
|
|
|
|
socket: {
|
2024-09-01 21:54:42 +02:00
|
|
|
localAddress: "127.0.0.1"
|
|
|
|
}
|
2024-08-30 22:11:16 +02:00
|
|
|
});
|
|
|
|
req.parsedURL = {
|
2024-09-01 21:54:42 +02:00
|
|
|
pathname: "/"
|
2024-08-30 22:11:16 +02:00
|
|
|
};
|
|
|
|
req.originalParsedURL = {
|
2024-09-01 21:54:42 +02:00
|
|
|
pathname: "/"
|
2024-08-30 22:11:16 +02:00
|
|
|
};
|
|
|
|
res = httpMocks.createResponse({
|
2024-09-01 21:54:42 +02:00
|
|
|
eventEmitter: require("events").EventEmitter
|
2024-08-30 22:11:16 +02:00
|
|
|
});
|
|
|
|
res.error = (statusCode) => {
|
|
|
|
// Very simple replacement of res.error
|
|
|
|
res.writeHead(statusCode, { "Content-Type": "text/plain" });
|
|
|
|
res.end(statusCode + " " + http.STATUS_CODES[statusCode]);
|
|
|
|
};
|
2024-08-31 20:48:55 +02:00
|
|
|
res.head = "";
|
|
|
|
res.foot = "";
|
2024-08-30 22:11:16 +02:00
|
|
|
logFacilities = {
|
|
|
|
errmessage: jest.fn(),
|
2024-09-01 21:54:42 +02:00
|
|
|
resmessage: jest.fn()
|
2024-08-30 22:11:16 +02:00
|
|
|
};
|
|
|
|
config = {
|
|
|
|
enableDirectoryListing: true,
|
|
|
|
enableDirectoryListingVHost: [],
|
|
|
|
enableCompression: true,
|
|
|
|
dontCompress: [],
|
2024-09-01 21:54:42 +02:00
|
|
|
generateServerString: jest.fn().mockReturnValue("Server")
|
2024-08-30 22:11:16 +02:00
|
|
|
};
|
|
|
|
next = jest.fn();
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
jest.clearAllMocks();
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should return 404 if file does not exist", async () => {
|
|
|
|
fs.stat.mockImplementation((path, cb) => {
|
|
|
|
cb({ code: "ENOENT" });
|
|
|
|
});
|
|
|
|
|
|
|
|
await middleware(req, res, logFacilities, config, next);
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(404);
|
|
|
|
expect(logFacilities.errmessage).toHaveBeenCalledWith(
|
2024-09-01 21:54:42 +02:00
|
|
|
"Resource not found."
|
2024-08-30 22:11:16 +02:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should return 403 if directory listing is disabled", async () => {
|
|
|
|
config.enableDirectoryListing = false;
|
|
|
|
fs.stat.mockImplementation((path, cb) => {
|
|
|
|
cb(null, { isDirectory: () => true, isFile: () => false });
|
|
|
|
});
|
|
|
|
|
|
|
|
await middleware(req, res, logFacilities, config, next);
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
|
|
expect(logFacilities.errmessage).toHaveBeenCalledWith(
|
2024-09-01 21:54:42 +02:00
|
|
|
"Directory listing is disabled."
|
2024-08-30 22:11:16 +02:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should return 403 if access is denied", async () => {
|
|
|
|
fs.stat.mockImplementation((path, cb) => {
|
|
|
|
cb({ code: "EACCES" });
|
|
|
|
});
|
|
|
|
|
|
|
|
await middleware(req, res, logFacilities, config, next);
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
|
|
expect(logFacilities.errmessage).toHaveBeenCalledWith("Access denied.");
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should return 414 if the URI is too long", async () => {
|
|
|
|
fs.stat.mockImplementation((path, cb) => {
|
|
|
|
cb({ code: "ENAMETOOLONG" });
|
|
|
|
});
|
|
|
|
|
|
|
|
await middleware(req, res, logFacilities, config, next);
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(414);
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should return 503 if the server is unable to handle the request", async () => {
|
|
|
|
fs.stat.mockImplementation((path, cb) => {
|
|
|
|
cb({ code: "EMFILE" });
|
|
|
|
});
|
|
|
|
|
|
|
|
await middleware(req, res, logFacilities, config, next);
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(503);
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should return 508 if a loop is detected in symbolic links", async () => {
|
|
|
|
fs.stat.mockImplementation((path, cb) => {
|
|
|
|
cb({ code: "ELOOP" });
|
|
|
|
});
|
|
|
|
|
|
|
|
await middleware(req, res, logFacilities, config, next);
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(508);
|
|
|
|
expect(logFacilities.errmessage).toHaveBeenCalledWith(
|
2024-09-01 21:54:42 +02:00
|
|
|
"Symbolic link loop detected."
|
2024-08-30 22:11:16 +02:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should return 500 if an unknown error occurs", async () => {
|
|
|
|
fs.stat.mockImplementation((path, cb) => {
|
|
|
|
cb(new Error("Unknown error"));
|
|
|
|
});
|
|
|
|
|
|
|
|
await middleware(req, res, logFacilities, config, next);
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(500);
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should return 501 if the file is a block device, character device, FIFO, or socket", async () => {
|
|
|
|
fs.stat.mockImplementation((path, cb) => {
|
|
|
|
cb(null, {
|
|
|
|
isDirectory: () => false,
|
|
|
|
isFile: () => false,
|
2024-09-01 21:54:42 +02:00
|
|
|
isBlockDevice: () => true
|
2024-08-30 22:11:16 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
await middleware(req, res, logFacilities, config, next);
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(501);
|
|
|
|
expect(logFacilities.errmessage).toHaveBeenCalledWith(
|
2024-09-01 21:54:42 +02:00
|
|
|
expect.stringContaining("doesn't support block devices")
|
2024-08-30 22:11:16 +02:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test("should return a directory listing if the path is a directory and directory listing is enabled", async () => {
|
|
|
|
fs.readdir.mockImplementation((path, cb) => {
|
|
|
|
cb(null, ["file1.txt", "file2.txt"]);
|
|
|
|
});
|
|
|
|
fs.readFile.mockImplementation((path, cb) => {
|
|
|
|
if (path.match(/(?:^|\/)file[12]\.txt$/)) {
|
|
|
|
cb(null, Buffer.from("test"));
|
|
|
|
} else {
|
|
|
|
cb({ code: "ENOENT" });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
fs.stat.mockImplementation((path, cb) => {
|
|
|
|
if (!path.match(/(?:^|\/)file[12]\.txt$/)) {
|
|
|
|
cb(null, { isDirectory: () => true, isFile: () => false });
|
|
|
|
} else {
|
|
|
|
cb(null, {
|
|
|
|
isDirectory: () => false,
|
|
|
|
isFile: () => true,
|
|
|
|
size: 1024,
|
2024-09-01 21:54:42 +02:00
|
|
|
mtime: new Date()
|
2024-08-30 22:11:16 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
await middleware(req, res, logFacilities, config, next);
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
expect(res._getData()).toContain("Directory: /");
|
|
|
|
expect(res._getData()).toContain("file1.txt");
|
|
|
|
expect(res._getData()).toContain("file2.txt");
|
|
|
|
});
|
2024-08-31 20:48:55 +02:00
|
|
|
|
|
|
|
test("should serve static file if the path is a file", async () => {
|
|
|
|
req.headers["accept-encoding"] = undefined;
|
|
|
|
req.path = "/file.txt";
|
|
|
|
req.parsedURL.pathname = "/file.txt";
|
|
|
|
req.originalParsedURL.pathname = "/file.txt";
|
|
|
|
|
|
|
|
fs.stat.mockImplementation((path, cb) => {
|
|
|
|
if (!path.match(/(?:^|\/)file\.txt$/)) {
|
|
|
|
cb(null, { isDirectory: () => true, isFile: () => false });
|
|
|
|
} else {
|
|
|
|
cb(null, {
|
|
|
|
isDirectory: () => false,
|
|
|
|
isFile: () => true,
|
2024-09-01 21:54:42 +02:00
|
|
|
size: 9
|
2024-08-31 20:48:55 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let mockEndListener = () => {};
|
|
|
|
let mockDataSent = false;
|
|
|
|
const mockStream = {
|
|
|
|
on: (event, listener) => {
|
|
|
|
if (event == "open") {
|
|
|
|
listener();
|
|
|
|
} else if (event == "data") {
|
|
|
|
if (!mockDataSent) {
|
|
|
|
listener(Buffer.from("mock data"));
|
|
|
|
mockDataSent = true;
|
|
|
|
}
|
|
|
|
mockEndListener();
|
|
|
|
} else if (event == "end") {
|
|
|
|
mockEndListener = listener;
|
|
|
|
if (mockDataSent) mockEndListener();
|
|
|
|
}
|
|
|
|
return mockStream;
|
|
|
|
},
|
|
|
|
once: (event, listener) => {
|
|
|
|
mockStream.on(event, listener);
|
|
|
|
},
|
|
|
|
pipe: (destStream) => {
|
|
|
|
if (!mockDataSent) {
|
|
|
|
destStream.end("mock data");
|
|
|
|
}
|
|
|
|
return destStream;
|
2024-09-01 21:54:42 +02:00
|
|
|
}
|
2024-08-31 20:48:55 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
fs.createReadStream.mockImplementation(() => {
|
|
|
|
return mockStream;
|
|
|
|
});
|
|
|
|
|
|
|
|
await middleware(req, res, logFacilities, config, next);
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
expect(res._getData()).toBe("mock data");
|
2024-08-31 20:50:10 +02:00
|
|
|
});
|
2024-08-30 22:11:16 +02:00
|
|
|
});
|