@solid/community-server
Version:
Community Solid Server: an open and modular implementation of the Solid specifications
192 lines • 8.81 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StaticAssetHandler = exports.StaticAssetEntry = void 0;
const node_fs_1 = require("node:fs");
const escape_string_regexp_1 = __importDefault(require("escape-string-regexp"));
const mime = __importStar(require("mime-types"));
const global_logger_factory_1 = require("global-logger-factory");
const ContentTypes_1 = require("../../util/ContentTypes");
const InternalServerError_1 = require("../../util/errors/InternalServerError");
const NotFoundHttpError_1 = require("../../util/errors/NotFoundHttpError");
const NotImplementedHttpError_1 = require("../../util/errors/NotImplementedHttpError");
const PathUtil_1 = require("../../util/PathUtil");
const StreamUtil_1 = require("../../util/StreamUtil");
const HttpHandler_1 = require("../HttpHandler");
/**
* Used to link file paths with relative URLs.
* By using a separate class instead of a key/value map it is easier to replace values in Components.js.
*/
class StaticAssetEntry {
relativeUrl;
filePath;
constructor(relativeUrl, filePath) {
this.relativeUrl = relativeUrl;
this.filePath = filePath;
}
}
exports.StaticAssetEntry = StaticAssetEntry;
/**
* Handler that serves static resources on specific paths.
* Relative file paths are assumed to be relative to the current working directory.
* Relative file paths can be preceded by `@css:`, e.g. `@css:foo/bar`,
* in case they need to be relative to the module root.
* File paths ending in a slash assume the target is a folder and map all of its contents.
*/
class StaticAssetHandler extends HttpHandler_1.HttpHandler {
mappings;
pathMatcher;
expires;
logger = (0, global_logger_factory_1.getLoggerFor)(this);
/**
* Creates a handler for the provided static resources.
*
* @param assets - A list of {@link StaticAssetEntry}.
* @param baseUrl - The base URL of the server.
* @param options - Specific options.
* @param options.expires - Cache expiration time in seconds.
*/
constructor(assets, baseUrl, options = {}) {
super();
this.mappings = {};
const rootPath = (0, PathUtil_1.ensureTrailingSlash)(new URL(baseUrl).pathname);
for (const { relativeUrl, filePath } of assets) {
this.mappings[(0, PathUtil_1.trimLeadingSlashes)(relativeUrl)] = (0, PathUtil_1.resolveAssetPath)(filePath);
}
this.pathMatcher = this.createPathMatcher(rootPath);
this.expires = Number.isInteger(options.expires) ? Math.max(0, options.expires) : 0;
}
/**
* Creates a regular expression that matches the URL paths.
*/
createPathMatcher(rootPath) {
// Sort longest paths first to ensure the longest match has priority
const paths = Object.keys(this.mappings)
.sort((pathA, pathB) => pathB.length - pathA.length);
// Collect regular expressions for files and folders separately.
// The arrays need initial values to prevent matching everything, as they will if these are empty.
const files = ['.^'];
const folders = ['.^'];
for (const path of paths) {
const filePath = this.mappings[path];
if (filePath.endsWith('/') && !path.endsWith('/')) {
throw new InternalServerError_1.InternalServerError(`Server is misconfigured: StaticAssetHandler can not ` +
`have a file path ending on a slash if the URL does not, but received ${path} and ${filePath}`);
}
(filePath.endsWith('/') ? folders : files).push((0, escape_string_regexp_1.default)(path));
}
// Either match an exact document or a file within a folder (stripping the query string)
return new RegExp(`^${rootPath}(?:(${files.join('|')})|(${folders.join('|')})([^?]+))(?:\\?.*)?$`, 'u');
}
/**
* Obtains the file path corresponding to the asset URL
*/
getFilePath({ url }) {
// Verify if the URL matches any of the paths
const match = this.pathMatcher.exec(url ?? '');
if (!match || match[0].includes('/..')) {
throw new NotImplementedHttpError_1.NotImplementedHttpError(`No static resource configured at ${url}`);
}
// The mapping is either a known document, or a file within a folder
const [, document, folder, file] = match;
return typeof document === 'string' ?
this.mappings[document] :
(0, PathUtil_1.joinFilePath)(this.mappings[folder], decodeURIComponent(file));
}
async canHandle({ request }) {
if (request.method !== 'GET' && request.method !== 'HEAD') {
throw new NotImplementedHttpError_1.NotImplementedHttpError('Only GET and HEAD requests are supported');
}
this.getFilePath(request);
}
async handle({ request, response }) {
// Determine the asset to serve
const filePath = this.getFilePath(request);
this.logger.debug(`Serving ${request.url} via static asset ${filePath}`);
// Resolve when asset loading succeeds
const asset = (0, node_fs_1.createReadStream)(filePath);
return new Promise((resolve, reject) => {
// Write a 200 response when the asset becomes readable
asset.once('readable', () => {
const contentType = mime.lookup(filePath) || ContentTypes_1.APPLICATION_OCTET_STREAM;
response.writeHead(200, {
// eslint-disable-next-line ts/naming-convention
'content-type': contentType,
...this.getCacheHeaders(),
});
// With HEAD, only write the headers
if (request.method === 'HEAD') {
response.end();
asset.destroy();
// With GET, pipe the entire response
}
else {
(0, StreamUtil_1.pipeSafely)(asset, response);
}
resolve();
});
// Pass the error when something goes wrong
asset.once('error', (error) => {
const { code } = error;
// When the file if not found or a folder, signal a 404
if (code === 'ENOENT' || code === 'EISDIR') {
this.logger.debug(`Static asset ${filePath} not found`);
reject(new NotFoundHttpError_1.NotFoundHttpError(`Cannot find ${request.url}`));
// In other cases, we might already have started writing, so just hang up
}
else {
this.logger.warn(`Error reading asset ${filePath}: ${error.message}`);
response.end();
asset.destroy();
resolve();
}
});
});
}
getCacheHeaders() {
return this.expires <= 0 ?
{} :
{
// eslint-disable-next-line ts/naming-convention
'cache-control': `max-age=${this.expires}`,
expires: new Date(Date.now() + this.expires * 1000).toUTCString(),
};
}
}
exports.StaticAssetHandler = StaticAssetHandler;
//# sourceMappingURL=StaticAssetHandler.js.map