superstatic
Version:
A static file server for fancy apps
146 lines (145 loc) • 4.93 kB
JavaScript
"use strict";
/**
* Copyright (c) 2022 Google LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const isUrl = require("is-url");
const _ = require("lodash");
const patterns = require("../utils/patterns");
const pathToRegexp = require("path-to-regexp");
const slasher = require("glob-slasher");
function formatExternalUrl(u) {
const cleaned = u
.replace("/http:/", "http://")
.replace("/https:/", "https://");
return isUrl(cleaned) ? cleaned : u;
}
function addQuery(url, qs) {
if (url.indexOf("?") >= 0) {
return url + "&" + qs;
}
else if (qs?.length) {
return url + "?" + qs;
}
return url;
}
const Redirect = function (glob, regex, destination, type) {
this.type = type || 301;
this.glob = slasher(glob);
this.regex = regex;
this.destination = destination;
if (this.destination.match(/(?:^|\/):/)) {
this.captureKeys = [];
if (this.glob) {
this.engine = "glob";
this.capture = pathToRegexp(this.glob, this.captureKeys);
}
if (this.regex) {
this.engine = "pattern";
this.capture = patterns.createRaw(this.regex);
}
this.compileDestination = pathToRegexp.compile(this.destination);
}
};
Redirect.prototype.test = function (url) {
let qs = "";
if (url.indexOf("?") >= 0) {
const parts = url.split("?");
url = parts[0];
qs = parts[1];
}
let match = undefined;
if (this.capture) {
match = this.capture.exec(url);
}
if (match) {
let params = {};
if (this.engine === "glob") {
for (let i = 0; i < this.captureKeys.length; i++) {
let m = match[i + 1];
if (m && m.indexOf("/") >= 0) {
m = m.split("/");
}
params[this.captureKeys[i].name] = m;
}
}
else {
for (let j = 0; j < match.length; j++) {
params[j.toString()] = match[j];
}
if (match.groups) {
params = Object.assign(params, match.groups);
}
}
try {
const dest = decodeURIComponent(this.compileDestination(params));
return {
type: this.type,
destination: encodeURI(addQuery(dest, qs)),
};
}
catch (e) {
return undefined;
}
}
else if (patterns.configMatcher(url, { glob: this.glob, regex: this.regex })) {
return {
type: this.type,
destination: encodeURI(addQuery(this.destination, qs)),
};
}
return undefined;
};
module.exports = function () {
return function (req, res, next) {
const config = _.get(req, "superstatic.redirects");
if (!config) {
return next();
}
const redirects = [];
if (_.isArray(config)) {
config.forEach((redir) => {
const glob = redir.glob || redir.source;
redirects.push(new Redirect(glob, redir.regex, redir.destination, redir.type));
});
}
else {
throw new Error("redirects provided in an unrecognized format");
}
const matcher = function (url) {
for (const redirect of redirects) {
const result = redirect.test(url);
if (result) {
return result;
}
}
return undefined;
};
const match = matcher(decodeURI(req.url));
if (!match) {
return next();
}
// Remove leading slash of a url
const redirectUrl = formatExternalUrl(match.destination);
return res.superstatic.handle({
redirect: redirectUrl,
status: match.type,
});
};
};