forked from svrjs/svrjs
Update to SVR.JS 3.13.0
This commit is contained in:
parent
4928ac1d2c
commit
26214c9eb8
6 changed files with 425 additions and 552 deletions
12
index.html
12
index.html
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>SVR.JS 3.12.3</title>
|
<title>SVR.JS 3.13.0</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<style>
|
<style>
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Welcome to SVR.JS 3.12.3</h1>
|
<h1>Welcome to SVR.JS 3.13.0</h1>
|
||||||
<br/>
|
<br/>
|
||||||
<img src="/logo.png" style="width: 256px;" />
|
<img src="/logo.png" style="width: 256px;" />
|
||||||
<br/>
|
<br/>
|
||||||
|
@ -134,8 +134,12 @@
|
||||||
</div>
|
</div>
|
||||||
<p>Changes:</p>
|
<p>Changes:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Removed all remnants of "DorianTech".</li>
|
<li>Added support for skipping URL rewriting, when the URL refers to a file or a directory.</li>
|
||||||
<li>Fixed bug with wildcard in domain name selectors.</li>
|
<li>Dropped support for svrmodpack.</li>
|
||||||
|
<li>Added support for 307 and 308 redirects (both in config.json and in redirect() SVR.JS API method).</li>
|
||||||
|
<li>Mitigated log file injection vulnerability for HTTP authentication.</li>
|
||||||
|
<li>Mitigated log file injection vulnerability for SVR.JS mod file names.</li>
|
||||||
|
<li>SVR.JS no longer crashes, when access to a log file is denied.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="/tests.html">Tests</a><br/>
|
<a href="/tests.html">Tests</a><br/>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>SVR.JS 3.12.3 Licenses</title>
|
<title>SVR.JS 3.13.0 Licenses</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<style>
|
<style>
|
||||||
|
@ -12,8 +12,8 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>SVR.JS 3.12.3 Licenses</h1>
|
<h1>SVR.JS 3.13.0 Licenses</h1>
|
||||||
<h2>SVR.JS 3.12.3</h2>
|
<h2>SVR.JS 3.13.0</h2>
|
||||||
<div style="display: inline-block; text-align: left; border-width: 2px; border-style: solid; border-color: gray; padding: 8px;">
|
<div style="display: inline-block; text-align: left; border-width: 2px; border-style: solid; border-color: gray; padding: 8px;">
|
||||||
MIT License<br/>
|
MIT License<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE<br/>
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE<br/>
|
||||||
SOFTWARE.<br/>
|
SOFTWARE.<br/>
|
||||||
</div>
|
</div>
|
||||||
<h2>Packages used by SVR.JS 3.12.3</h2>
|
<h2>Packages used by SVR.JS 3.13.0</h2>
|
||||||
<div style="width: 100%; background-color: #ccc; border: 1px solid green; text-align: left; margin: 10px 0;">
|
<div style="width: 100%; background-color: #ccc; border: 1px solid green; text-align: left; margin: 10px 0;">
|
||||||
<div style="float: right;">License: MIT</div>
|
<div style="float: right;">License: MIT</div>
|
||||||
<div style="font-size: 20px;">
|
<div style="font-size: 20px;">
|
||||||
|
|
88
node_modules/svrmodpack/index.js
generated
vendored
88
node_modules/svrmodpack/index.js
generated
vendored
|
@ -1,88 +0,0 @@
|
||||||
var fs = require("fs");
|
|
||||||
var zlib = require("zlib");
|
|
||||||
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
function mkDirByPathSync(targetDir, { isRelativeToScript = false } = {}) {
|
|
||||||
const sep = path.sep;
|
|
||||||
const initDir = path.isAbsolute(targetDir) ? sep : '';
|
|
||||||
const baseDir = isRelativeToScript ? __dirname : '.';
|
|
||||||
|
|
||||||
return targetDir.split(sep).reduce((parentDir, childDir) => {
|
|
||||||
const curDir = path.resolve(baseDir, parentDir, childDir);
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(curDir);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code === 'EEXIST') { // curDir already exists!
|
|
||||||
return curDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows.
|
|
||||||
if (err.code === 'ENOENT') { // Throw the original parentDir error on curDir `ENOENT` failure.
|
|
||||||
throw new Error(`EACCES: permission denied, mkdir '${parentDir}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const caughtErr = ['EACCES', 'EPERM', 'EISDIR'].indexOf(err.code) > -1;
|
|
||||||
if (!caughtErr || caughtErr && curDir === path.resolve(targetDir)) {
|
|
||||||
throw err; // Throw if it's just the last created dir.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return curDir;
|
|
||||||
}, initDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pack(ins, out, modinfof) {
|
|
||||||
if (typeof modinfof === 'undefined') {
|
|
||||||
modinfof = 'mod.info';
|
|
||||||
}
|
|
||||||
var modinfo = JSON.parse(fs.readFileSync(modinfof));
|
|
||||||
var file = "SVR\0";
|
|
||||||
var modinfo2 = "";
|
|
||||||
modinfo2 += modinfo.name;
|
|
||||||
modinfo2 += "\0";
|
|
||||||
modinfo2 += modinfo.version;
|
|
||||||
modinfo2 += "\0";
|
|
||||||
file += modinfo2
|
|
||||||
for (var i = 0; i < ins.length; i++) {
|
|
||||||
var script = fs.readFileSync(ins[i]);
|
|
||||||
file += ins[i];
|
|
||||||
file += "\0";
|
|
||||||
file += script.toString();
|
|
||||||
file += "\0";
|
|
||||||
}
|
|
||||||
fs.writeFileSync(out, zlib.gzipSync(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
function unpack(inputf, outf, modinfof) {
|
|
||||||
if (typeof outf === 'undefined') {
|
|
||||||
outf = '';
|
|
||||||
}
|
|
||||||
if (typeof modinfof === 'undefined') {
|
|
||||||
modinfof = 'mod.info';
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
mkDirByPathSync(outf);
|
|
||||||
} catch (ex) {}
|
|
||||||
var script = "";
|
|
||||||
var modinfo = {};
|
|
||||||
var file = zlib.gunzipSync(fs.readFileSync(inputf)).toString();
|
|
||||||
var tokens = file.split("\0");
|
|
||||||
var files = [];
|
|
||||||
if (tokens[0] != "SVR") throw new Error("wrong signature");
|
|
||||||
modinfo.name = tokens[1];
|
|
||||||
modinfo.version = tokens[2];
|
|
||||||
for (var i = 3; i < tokens.length - 1; i += 2) {
|
|
||||||
files.push({
|
|
||||||
name: tokens[i],
|
|
||||||
content: tokens[i + 1]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fs.writeFileSync((outf + "/" + modinfof).replace(/\/\//g, "/"), JSON.stringify(modinfo, null, 2));
|
|
||||||
for (var i = 0; i < files.length; i++) {
|
|
||||||
fs.writeFileSync((outf + "/" + files[i].name).replace(/\/\//g, "/"), files[i].content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {pack: pack, unpack: unpack}
|
|
48
node_modules/svrmodpack/package.json
generated
vendored
48
node_modules/svrmodpack/package.json
generated
vendored
|
@ -1,48 +0,0 @@
|
||||||
{
|
|
||||||
"_from": "svrmodpack",
|
|
||||||
"_id": "svrmodpack@1.0.0",
|
|
||||||
"_inBundle": false,
|
|
||||||
"_integrity": "sha512-17SjkfDtZL3KOGjzLTT/nEZzCuvD4rz07y4UFVXfo/Xo0qTMwgICnDSIEOonaAs1GdzZe0hiJxkPYvdTKQifwg==",
|
|
||||||
"_location": "/svrmodpack",
|
|
||||||
"_phantomChildren": {},
|
|
||||||
"_requested": {
|
|
||||||
"type": "tag",
|
|
||||||
"registry": true,
|
|
||||||
"raw": "svrmodpack",
|
|
||||||
"name": "svrmodpack",
|
|
||||||
"escapedName": "svrmodpack",
|
|
||||||
"rawSpec": "",
|
|
||||||
"saveSpec": null,
|
|
||||||
"fetchSpec": "latest"
|
|
||||||
},
|
|
||||||
"_requiredBy": [
|
|
||||||
"#USER",
|
|
||||||
"/"
|
|
||||||
],
|
|
||||||
"_resolved": "https://registry.npmjs.org/svrmodpack/-/svrmodpack-1.0.0.tgz",
|
|
||||||
"_shasum": "412def1c6ff93a7a15849519411a30b3281ee822",
|
|
||||||
"_spec": "svrmodpack",
|
|
||||||
"_where": "/media/serveradmin/Server/developement",
|
|
||||||
"author": {
|
|
||||||
"name": "Dorian Niemiec",
|
|
||||||
"email": "niemiecdorian2008@gmail.com"
|
|
||||||
},
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/dorian-tech/svrmodpack/issues"
|
|
||||||
},
|
|
||||||
"bundleDependencies": false,
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "DorianTech SVR.JS Mods packer/unpacker library ",
|
|
||||||
"homepage": "https://github.com/dorian-tech/svrmodpack#readme",
|
|
||||||
"license": "GPL-3.0-or-later",
|
|
||||||
"main": "index.js",
|
|
||||||
"name": "svrmodpack",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/dorian-tech/svrmodpack.git"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
219
svr.js
219
svr.js
|
@ -1,15 +1,4 @@
|
||||||
/////////////////////////////////////////
|
// SVR.JS - a web server running on Node.JS
|
||||||
// //
|
|
||||||
// S V R . J S //
|
|
||||||
// S O U R C E C O D E //
|
|
||||||
// //
|
|
||||||
/////////////////////////////////////////
|
|
||||||
/////////////////////////////////////////
|
|
||||||
///// SVR.JS /////
|
|
||||||
/////a web server running on Node.JS/////
|
|
||||||
/////////////////////////////////////////
|
|
||||||
/////////////////////////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MIT License
|
* MIT License
|
||||||
|
@ -33,11 +22,11 @@ if (typeof require === "undefined") {
|
||||||
} else {
|
} else {
|
||||||
if (typeof alert !== "undefined" && typeof document !== "undefined") {
|
if (typeof alert !== "undefined" && typeof document !== "undefined") {
|
||||||
// If it runs on web browser, display an alert.
|
// If it runs on web browser, display an alert.
|
||||||
alert("SVR.JS doesn't work on web browser. SVR.JS requires use of Node.JS (or compatible JS runtime).");
|
alert("SVR.JS doesn't work on a web browser. SVR.JS requires use of Node.JS (or compatible JS runtime).");
|
||||||
}
|
}
|
||||||
// If it's not, throw an error.
|
// If it's not, throw an error.
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
throw new Error("SVR.JS doesn't work on web browser. SVR.JS requires use of Node.JS (or compatible JS runtime).");
|
throw new Error("SVR.JS doesn't work on a web browser. SVR.JS requires use of Node.JS (or compatible JS runtime).");
|
||||||
} else {
|
} else {
|
||||||
throw new Error("SVR.JS doesn't work on Deno/QuickJS. SVR.JS requires use of Node.JS (or compatible JS runtime).");
|
throw new Error("SVR.JS doesn't work on Deno/QuickJS. SVR.JS requires use of Node.JS (or compatible JS runtime).");
|
||||||
}
|
}
|
||||||
|
@ -80,7 +69,7 @@ function deleteFolderRecursive(path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var os = require("os");
|
var os = require("os");
|
||||||
var version = "3.12.3";
|
var version = "3.13.0";
|
||||||
var singlethreaded = false;
|
var singlethreaded = false;
|
||||||
|
|
||||||
if (process.versions) process.versions.svrjs = version; // Inject SVR.JS into process.versions
|
if (process.versions) process.versions.svrjs = version; // Inject SVR.JS into process.versions
|
||||||
|
@ -91,8 +80,8 @@ for (var i = (process.argv[0].indexOf("node") > -1 || process.argv[0].indexOf("b
|
||||||
console.log("SVR.JS usage:");
|
console.log("SVR.JS usage:");
|
||||||
console.log("node svr.js [-h] [--help] [-?] [/h] [/?] [--secure] [--reset] [--clean] [--disable-mods] [--single-threaded] [-v] [--version]");
|
console.log("node svr.js [-h] [--help] [-?] [/h] [/?] [--secure] [--reset] [--clean] [--disable-mods] [--single-threaded] [-v] [--version]");
|
||||||
console.log("-h -? /h /? --help -- Displays help");
|
console.log("-h -? /h /? --help -- Displays help");
|
||||||
console.log("--clean -- Cleans files, that SVR.JS created");
|
console.log("--clean -- Cleans up files created by SVR.JS");
|
||||||
console.log("--reset -- Resets SVR.JS to factory settings (WARNING: DANGEROUS)");
|
console.log("--reset -- Resets SVR.JS to default settings (WARNING: DANGEROUS)");
|
||||||
console.log("--secure -- Runs HTTPS server");
|
console.log("--secure -- Runs HTTPS server");
|
||||||
console.log("--disable-mods -- Disables mods (safe mode)");
|
console.log("--disable-mods -- Disables mods (safe mode)");
|
||||||
console.log("--single-threaded -- Run single-threaded");
|
console.log("--single-threaded -- Run single-threaded");
|
||||||
|
@ -123,8 +112,8 @@ for (var i = (process.argv[0].indexOf("node") > -1 || process.argv[0].indexOf("b
|
||||||
console.log("SVR.JS usage:");
|
console.log("SVR.JS usage:");
|
||||||
console.log("node svr.js [-h] [--help] [-?] [/h] [/?] [--secure] [--reset] [--clean] [--disable-mods] [--single-threaded] [-v] [--version]");
|
console.log("node svr.js [-h] [--help] [-?] [/h] [/?] [--secure] [--reset] [--clean] [--disable-mods] [--single-threaded] [-v] [--version]");
|
||||||
console.log("-h -? /h /? --help -- Displays help");
|
console.log("-h -? /h /? --help -- Displays help");
|
||||||
console.log("--clean -- Cleans files, that SVR.JS created");
|
console.log("--clean -- Cleans up files created by SVR.JS");
|
||||||
console.log("--reset -- Resets SVR.JS to factory settings (WARNING: DANGEROUS)");
|
console.log("--reset -- Resets SVR.JS to default settings (WARNING: DANGEROUS)");
|
||||||
console.log("--secure -- Runs HTTPS server");
|
console.log("--secure -- Runs HTTPS server");
|
||||||
console.log("--disable-mods -- Disables mods (safe mode)");
|
console.log("--disable-mods -- Disables mods (safe mode)");
|
||||||
console.log("--single-threaded -- Run single-threaded");
|
console.log("--single-threaded -- Run single-threaded");
|
||||||
|
@ -320,10 +309,10 @@ function generateETag(filePath, stat) {
|
||||||
// Brute force-related
|
// Brute force-related
|
||||||
var bruteForceDb = {};
|
var bruteForceDb = {};
|
||||||
|
|
||||||
// PBKDF2 cache
|
// PBKDF2/scrypt cache
|
||||||
var pbkdf2Cache = [];
|
var pbkdf2Cache = [];
|
||||||
var scryptCache = [];
|
var scryptCache = [];
|
||||||
var pbkdf2CacheIntervalId = -1;
|
var passwordHashCacheIntervalId = -1;
|
||||||
|
|
||||||
// SVR.JS worker spawn-related
|
// SVR.JS worker spawn-related
|
||||||
var SVRJSInitialized = false;
|
var SVRJSInitialized = false;
|
||||||
|
@ -400,14 +389,6 @@ try {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Don't use hexstrbase64
|
// Don't use hexstrbase64
|
||||||
}
|
}
|
||||||
var svrmodpack = undefined;
|
|
||||||
try {
|
|
||||||
svrmodpack = require("svrmodpack");
|
|
||||||
} catch (err) {
|
|
||||||
svrmodpack = {
|
|
||||||
_errored: err
|
|
||||||
};
|
|
||||||
}
|
|
||||||
var zlib = require("zlib");
|
var zlib = require("zlib");
|
||||||
var tar = undefined;
|
var tar = undefined;
|
||||||
try {
|
try {
|
||||||
|
@ -1321,6 +1302,9 @@ function LOG(s) {
|
||||||
flags: "a",
|
flags: "a",
|
||||||
autoClose: false
|
autoClose: false
|
||||||
});
|
});
|
||||||
|
logFile.on("error", function(err) {
|
||||||
|
if (!s.match(/^SERVER WARNING MESSAGE(?: \[Request Id: [0-9a-f]{6}\])?: There was a problem while saving logs! Logs will not be kept in log file\. Reason: /) && !reallyExiting) serverconsole.locwarnmessage("There was a problem while saving logs! Logs will not be kept in log file. Reason: " + err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (logFile.writable) {
|
if (logFile.writable) {
|
||||||
logFile.write("[" + new Date().toISOString() + "] " + s + "\r\n");
|
logFile.write("[" + new Date().toISOString() + "] " + s + "\r\n");
|
||||||
|
@ -1452,7 +1436,6 @@ process.exit = function (code) {
|
||||||
|
|
||||||
var modLoadingErrors = [];
|
var modLoadingErrors = [];
|
||||||
var SSJSError = undefined;
|
var SSJSError = undefined;
|
||||||
var svrmodpackUsed = false;
|
|
||||||
|
|
||||||
// Load mods if the `disableMods` flag is not set
|
// Load mods if the `disableMods` flag is not set
|
||||||
if (!disableMods) {
|
if (!disableMods) {
|
||||||
|
@ -1514,10 +1497,8 @@ if (!disableMods) {
|
||||||
C: __dirname + "/temp/" + modloaderFolderName + "/" + modFileRaw
|
C: __dirname + "/temp/" + modloaderFolderName + "/" + modFileRaw
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If it's not a ".tar.gz" file, unpack it using `svrmodpack`
|
// If it's not a ".tar.gz" file, throw an error about `svrmodpack` support being dropped
|
||||||
if (svrmodpack._errored) throw svrmodpack._errored;
|
throw new Error("This version of SVR.JS no longer supports \"svrmodpack\" library for SVR.JS mods. Please consider using newer mods with .tar.gz format.");
|
||||||
svrmodpack.unpack(modFile, __dirname + "/temp/" + modloaderFolderName + "/" + modFileRaw);
|
|
||||||
svrmodpackUsed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize variables for mod loading
|
// Initialize variables for mod loading
|
||||||
|
@ -2119,7 +2100,9 @@ if (!cluster.isPrimary) {
|
||||||
var snMatches = sniCredentialsSingle.name.match(/^([^:[]*|\[[^]]*\]?)((?::.*)?)$/);
|
var snMatches = sniCredentialsSingle.name.match(/^([^:[]*|\[[^]]*\]?)((?::.*)?)$/);
|
||||||
if(!snMatches[1][0].match(/^\.+$/)) snMatches[1][0] = snMatches[1][0].replace(/\.+$/,"");
|
if(!snMatches[1][0].match(/^\.+$/)) snMatches[1][0] = snMatches[1][0].replace(/\.+$/,"");
|
||||||
server._contexts[server._contexts.length-1][0] = new RegExp("^" + snMatches[1].replace(/([.^$+?\-\\[\]{}])/g, "\\$1").replace(/\*/g, "[^.:]*") + ((snMatches[1][0] == "[" || snMatches[1].match(/^(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/)) ? "" : "\.?") + snMatches[2].replace(/([.^$+?\-\\[\]{}])/g, "\\$1").replace(/\*/g, "[^.]*") + "$", "i");
|
server._contexts[server._contexts.length-1][0] = new RegExp("^" + snMatches[1].replace(/([.^$+?\-\\[\]{}])/g, "\\$1").replace(/\*/g, "[^.:]*") + ((snMatches[1][0] == "[" || snMatches[1].match(/^(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/)) ? "" : "\.?") + snMatches[2].replace(/([.^$+?\-\\[\]{}])/g, "\\$1").replace(/\*/g, "[^.]*") + "$", "i");
|
||||||
} catch(ex) {}
|
} catch(ex) {
|
||||||
|
// Can't replace regex, ignoring...
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
server.on("request", reqhandler);
|
server.on("request", reqhandler);
|
||||||
|
@ -3147,15 +3130,21 @@ if (!cluster.isPrimary) {
|
||||||
|
|
||||||
|
|
||||||
// Function to perform HTTP redirection to a specified destination URL
|
// Function to perform HTTP redirection to a specified destination URL
|
||||||
function redirect(destination, isTemporary, customHeaders) {
|
function redirect(destination, isTemporary, keepMethod, customHeaders) {
|
||||||
|
// If keepMethod is a object, then save it to customHeaders
|
||||||
|
if (typeof keepMethod == "object") customHeaders = keepMethod;
|
||||||
|
|
||||||
|
// If isTemporary is a object, then save it to customHeaders
|
||||||
|
if (typeof isTemporary == "object") customHeaders = isTemporary;
|
||||||
|
|
||||||
// If customHeaders are not provided, get the default custom headers
|
// If customHeaders are not provided, get the default custom headers
|
||||||
if (customHeaders === undefined) customHeaders = getCustomHeaders();
|
if (customHeaders === undefined) customHeaders = getCustomHeaders();
|
||||||
|
|
||||||
// Set the "Location" header to the destination URL
|
// Set the "Location" header to the destination URL
|
||||||
customHeaders["Location"] = destination;
|
customHeaders["Location"] = destination;
|
||||||
|
|
||||||
// Determine the status code for redirection based on the isTemporary flag
|
// Determine the status code for redirection based on the isTemporary and keepMethod flags
|
||||||
var statusCode = isTemporary ? 302 : 301;
|
var statusCode = keepMethod ? (isTemporary ? 307 : 308) : (isTemporary ? 302 : 301);
|
||||||
|
|
||||||
// Write the response header with the appropriate status code and message
|
// Write the response header with the appropriate status code and message
|
||||||
res.writeHead(statusCode, http.STATUS_CODES[statusCode], customHeaders);
|
res.writeHead(statusCode, http.STATUS_CODES[statusCode], customHeaders);
|
||||||
|
@ -3276,7 +3265,7 @@ if (!cluster.isPrimary) {
|
||||||
try {
|
try {
|
||||||
decodedHref = decodeURIComponent(href);
|
decodedHref = decodeURIComponent(href);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Return 400 error
|
// Return an 400 error
|
||||||
callServerError(400);
|
callServerError(400);
|
||||||
serverconsole.errmessage("Bad request!");
|
serverconsole.errmessage("Bad request!");
|
||||||
return;
|
return;
|
||||||
|
@ -3287,7 +3276,7 @@ if (!cluster.isPrimary) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MOD EXCECUTION FUNCTION
|
// Mod execution function
|
||||||
function modExecute(mods, ffinals) {
|
function modExecute(mods, ffinals) {
|
||||||
// Prepare modFunction
|
// Prepare modFunction
|
||||||
var modFunction = ffinals;
|
var modFunction = ffinals;
|
||||||
|
@ -3305,7 +3294,7 @@ if (!cluster.isPrimary) {
|
||||||
modFunction = modO.callback(req, res, serverconsole, responseEnd, href, ext, uobject, search, "index.html", users, page404, head, foot, "", modFunction, configJSON, callServerError, getCustomHeaders, origHref, redirect, parsePostData);
|
modFunction = modO.callback(req, res, serverconsole, responseEnd, href, ext, uobject, search, "index.html", users, page404, head, foot, "", modFunction, configJSON, callServerError, getCustomHeaders, origHref, redirect, parsePostData);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Execute modfunction
|
// Execute modFunction
|
||||||
modFunction();
|
modFunction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4177,24 +4166,74 @@ if (!cluster.isPrimary) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle URL rewriting
|
// Handle URL rewriting
|
||||||
function rewriteURL(address, map) {
|
function rewriteURL(address, map, callback, _fileState, _mapBegIndex) {
|
||||||
var rewrittenAddress = address;
|
var rewrittenURL = address;
|
||||||
map.every(function (mapEntry) {
|
|
||||||
if (matchHostname(mapEntry.host) && createRegex(mapEntry.definingRegex).test(address)) {
|
|
||||||
mapEntry.replacements.forEach(function (replacement) {
|
|
||||||
rewrittenAddress = rewrittenAddress.replace(createRegex(replacement.regex), replacement.replacement);
|
|
||||||
});
|
|
||||||
if (mapEntry.append) rewrittenAddress += mapEntry.append;
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return rewrittenAddress;
|
|
||||||
}
|
|
||||||
var origHref = href;
|
|
||||||
if (!isProxy) {
|
if (!isProxy) {
|
||||||
var rewrittenURL = rewriteURL(req.url, rewriteMap);
|
var doCallback = true;
|
||||||
|
for(var i=(_mapBegIndex ? _mapBegIndex : 0);i<map.length;i++) {
|
||||||
|
var mapEntry = map[i];
|
||||||
|
if(href != "/" && (mapEntry.isNotDirectory || mapEntry.isNotFile) && !_fileState) {
|
||||||
|
fs.stat("." + decodeURIComponent(href), function(err, stats) {
|
||||||
|
var _fileState = 3;
|
||||||
|
if(err) {
|
||||||
|
_fileState = 3;
|
||||||
|
} else if(stats.isDirectory()) {
|
||||||
|
_fileState = 2;
|
||||||
|
} else if(stats.isFile()) {
|
||||||
|
_fileState = 1;
|
||||||
|
} else {
|
||||||
|
_fileState = 3;
|
||||||
|
}
|
||||||
|
rewriteURL(address, map, callback, _fileState, i);
|
||||||
|
});
|
||||||
|
doCallback = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (matchHostname(mapEntry.host) && createRegex(mapEntry.definingRegex).test(address) && !(mapEntry.isNotDirectory && _fileState == 2) && !(mapEntry.isNotFile && _fileState == 1)) {
|
||||||
|
mapEntry.replacements.forEach(function (replacement) {
|
||||||
|
rewrittenURL = rewrittenURL.replace(createRegex(replacement.regex), replacement.replacement);
|
||||||
|
});
|
||||||
|
if (mapEntry.append) rewrittenURL += mapEntry.append;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(doCallback) callback(rewrittenURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailing slash redirection
|
||||||
|
function redirectTrailingSlashes(callback) {
|
||||||
|
if (!disableTrailingSlashRedirects && href[href.length - 1] != "/" && origHref[origHref.length - 1] != "/") {
|
||||||
|
fs.stat("." + decodeURIComponent(href), function (err, stats) {
|
||||||
|
if (err || !stats.isDirectory()) {
|
||||||
|
try {
|
||||||
|
callback();
|
||||||
|
} catch (err) {
|
||||||
|
callServerError(500, undefined, err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var destinationURL = uobject;
|
||||||
|
destinationURL.path = null;
|
||||||
|
destinationURL.href = null;
|
||||||
|
destinationURL.pathname = origHref + "/";
|
||||||
|
destinationURL.hostname = null;
|
||||||
|
destinationURL.host = null;
|
||||||
|
destinationURL.port = null;
|
||||||
|
destinationURL.protocol = null;
|
||||||
|
destinationURL.slashes = null;
|
||||||
|
destinationURL = url.format(destinationURL);
|
||||||
|
redirect(destinationURL);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var origHref = href;
|
||||||
|
|
||||||
|
// Rewrite URLs
|
||||||
|
rewriteURL(req.url, rewriteMap, function(rewrittenURL) {
|
||||||
if (rewrittenURL != req.url) {
|
if (rewrittenURL != req.url) {
|
||||||
serverconsole.resmessage("URL rewritten: " + req.url + " => " + rewrittenURL);
|
serverconsole.resmessage("URL rewritten: " + req.url + " => " + rewrittenURL);
|
||||||
req.url = rewrittenURL;
|
req.url = rewrittenURL;
|
||||||
|
@ -4248,8 +4287,6 @@ if (!cluster.isPrimary) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Set response headers
|
// Set response headers
|
||||||
if (!isProxy) {
|
if (!isProxy) {
|
||||||
var hkh = getCustomHeaders();
|
var hkh = getCustomHeaders();
|
||||||
|
@ -4325,7 +4362,7 @@ if (!cluster.isPrimary) {
|
||||||
// Handle non-standard codes
|
// Handle non-standard codes
|
||||||
if (nonscodeIndex > -1) {
|
if (nonscodeIndex > -1) {
|
||||||
var nonscode = nonStandardCodes[nonscodeIndex];
|
var nonscode = nonStandardCodes[nonscodeIndex];
|
||||||
if (nonscode.scode == 301 || nonscode.scode == 302) {
|
if (nonscode.scode == 301 || nonscode.scode == 302 || nonscode.scode == 307 || nonscode.scode == 308) {
|
||||||
var location = "";
|
var location = "";
|
||||||
if (regexI[nonscodeIndex]) {
|
if (regexI[nonscodeIndex]) {
|
||||||
location = req.url.replace(regexI[nonscodeIndex], nonscode.location);
|
location = req.url.replace(regexI[nonscodeIndex], nonscode.location);
|
||||||
|
@ -4334,7 +4371,7 @@ if (!cluster.isPrimary) {
|
||||||
} else {
|
} else {
|
||||||
location = nonscode.location + "?" + req.url.split("?")[1];
|
location = nonscode.location + "?" + req.url.split("?")[1];
|
||||||
}
|
}
|
||||||
redirect(location, nonscode.scode == 302);
|
redirect(location, nonscode.scode == 302 || nonscode.scode == 307, nonscode.scode == 307 || nonscode.scode == 308);
|
||||||
return;
|
return;
|
||||||
} else if (nonscode.scode == 403) {
|
} else if (nonscode.scode == 403) {
|
||||||
callServerError(403);
|
callServerError(403);
|
||||||
|
@ -4355,35 +4392,6 @@ if (!cluster.isPrimary) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trailing slash redirection
|
|
||||||
function redirectTrailingSlashes(callback) {
|
|
||||||
if (!disableTrailingSlashRedirects && href[href.length - 1] != "/" && origHref[origHref.length - 1] != "/") {
|
|
||||||
fs.stat("." + decodeURIComponent(href), function (err, stats) {
|
|
||||||
if (err || !stats.isDirectory()) {
|
|
||||||
try {
|
|
||||||
callback();
|
|
||||||
} catch (err) {
|
|
||||||
callServerError(500, undefined, err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var destinationURL = uobject;
|
|
||||||
destinationURL.path = null;
|
|
||||||
destinationURL.href = null;
|
|
||||||
destinationURL.pathname = origHref + "/";
|
|
||||||
destinationURL.hostname = null;
|
|
||||||
destinationURL.host = null;
|
|
||||||
destinationURL.port = null;
|
|
||||||
destinationURL.protocol = null;
|
|
||||||
destinationURL.slashes = null;
|
|
||||||
destinationURL = url.format(destinationURL);
|
|
||||||
redirect(destinationURL);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle HTTP authentication
|
// Handle HTTP authentication
|
||||||
if (authIndex > -1) {
|
if (authIndex > -1) {
|
||||||
var authcode = nonStandardCodes[authIndex];
|
var authcode = nonStandardCodes[authIndex];
|
||||||
|
@ -4402,12 +4410,13 @@ if (!cluster.isPrimary) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var hashedPassword = sha256(password + list[_i].salt);
|
var hashedPassword = sha256(password + list[_i].salt);
|
||||||
|
var cacheEntry = null;
|
||||||
if (list[_i].scrypt) {
|
if (list[_i].scrypt) {
|
||||||
if (!crypto.scrypt) {
|
if (!crypto.scrypt) {
|
||||||
callServerError(500, undefined, new Error("SVR.JS doesn't support scrypt-hashed passwords on Node.JS versions without scrypt hash support."));
|
callServerError(500, undefined, new Error("SVR.JS doesn't support scrypt-hashed passwords on Node.JS versions without scrypt hash support."));
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
var cacheEntry = scryptCache.find(function (entry) {
|
cacheEntry = scryptCache.find(function (entry) {
|
||||||
return (entry.password == hashedPassword && entry.salt == list[_i].salt);
|
return (entry.password == hashedPassword && entry.salt == list[_i].salt);
|
||||||
});
|
});
|
||||||
if (cacheEntry) {
|
if (cacheEntry) {
|
||||||
|
@ -4434,7 +4443,7 @@ if (!cluster.isPrimary) {
|
||||||
callServerError(500, undefined, new Error("SVR.JS doesn't support PBKDF2-hashed passwords on Node.JS versions without crypto support."));
|
callServerError(500, undefined, new Error("SVR.JS doesn't support PBKDF2-hashed passwords on Node.JS versions without crypto support."));
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
var cacheEntry = pbkdf2Cache.find(function (entry) {
|
cacheEntry = pbkdf2Cache.find(function (entry) {
|
||||||
return (entry.password == hashedPassword && entry.salt == list[_i].salt);
|
return (entry.password == hashedPassword && entry.salt == list[_i].salt);
|
||||||
});
|
});
|
||||||
if (cacheEntry) {
|
if (cacheEntry) {
|
||||||
|
@ -4516,7 +4525,7 @@ if (!cluster.isPrimary) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callServerError(401, undefined, undefined, ha);
|
callServerError(401, undefined, undefined, ha);
|
||||||
serverconsole.errmessage("User \"" + username + "\" failed to log in.");
|
serverconsole.errmessage("User \"" + String(username).replace(/[\r\n]/g, "") + "\" failed to log in.");
|
||||||
} else {
|
} else {
|
||||||
if (bruteProtection) {
|
if (bruteProtection) {
|
||||||
if (process.send) {
|
if (process.send) {
|
||||||
|
@ -4527,7 +4536,7 @@ if (!cluster.isPrimary) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serverconsole.reqmessage("Client is logged in as \"" + username + "\"");
|
serverconsole.reqmessage("Client is logged in as \"" + String(username).replace(/[\r\n]/g, "") + "\".");
|
||||||
redirectTrailingSlashes(function () {
|
redirectTrailingSlashes(function () {
|
||||||
modExecute(mods, vres(req, res, serverconsole, responseEnd, href, ext, uobject, search, "index.html", users, page404, head, foot, "", callServerError, getCustomHeaders, origHref, redirect, parsePostData));
|
modExecute(mods, vres(req, res, serverconsole, responseEnd, href, ext, uobject, search, "index.html", users, page404, head, foot, "", callServerError, getCustomHeaders, origHref, redirect, parsePostData));
|
||||||
});
|
});
|
||||||
|
@ -4582,6 +4591,8 @@ if (!cluster.isPrimary) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
callServerError(500, undefined, generateErrorStack(err));
|
callServerError(500, undefined, generateErrorStack(err));
|
||||||
}
|
}
|
||||||
|
@ -4842,9 +4853,9 @@ function start(init) {
|
||||||
for (i = 0; i < logo.length; i++) console.log(logo[i]); // Print logo
|
for (i = 0; i < logo.length; i++) console.log(logo[i]); // Print logo
|
||||||
console.log();
|
console.log();
|
||||||
console.log("Welcome to SVR.JS - a web server running on Node.JS");
|
console.log("Welcome to SVR.JS - a web server running on Node.JS");
|
||||||
|
|
||||||
// Print warnings
|
// Print warnings
|
||||||
if (version.indexOf("Nightly-") === 0) serverconsole.locwarnmessage("This version is only for test purposes and may be unstable.");
|
if (version.indexOf("Nightly-") === 0) serverconsole.locwarnmessage("This version is only for test purposes and may be unstable.");
|
||||||
if (svrmodpackUsed) serverconsole.locwarnmessage("The \"svrmodpack\" library is deprecated. Mods using svrmodpack format may not work in future SVR.JS versions.");
|
|
||||||
if (configJSON.enableHTTP2 && !secure) serverconsole.locwarnmessage("HTTP/2 without HTTPS may not work in web browsers. Web browsers only support HTTP/2 with HTTPS!");
|
if (configJSON.enableHTTP2 && !secure) serverconsole.locwarnmessage("HTTP/2 without HTTPS may not work in web browsers. Web browsers only support HTTP/2 with HTTPS!");
|
||||||
if (process.isBun) {
|
if (process.isBun) {
|
||||||
serverconsole.locwarnmessage("Bun support is experimental. Some features of SVR.JS, SVR.JS mods and SVR.JS server-side JavaScript may not work as expected.");
|
serverconsole.locwarnmessage("Bun support is experimental. Some features of SVR.JS, SVR.JS mods and SVR.JS server-side JavaScript may not work as expected.");
|
||||||
|
@ -4873,7 +4884,7 @@ function start(init) {
|
||||||
// Display mod and server-side JavaScript errors
|
// Display mod and server-side JavaScript errors
|
||||||
if (process.isPrimary || process.isPrimary === undefined) {
|
if (process.isPrimary || process.isPrimary === undefined) {
|
||||||
modLoadingErrors.forEach(function (modLoadingError) {
|
modLoadingErrors.forEach(function (modLoadingError) {
|
||||||
serverconsole.locwarnmessage("There was a problem while loading a \"" + modLoadingError.modName + "\" mod.");
|
serverconsole.locwarnmessage("There was a problem while loading a \"" + String(modLoadingError.modName).replace(/[\r\n]/g, "") + "\" mod.");
|
||||||
serverconsole.locwarnmessage("Stack:");
|
serverconsole.locwarnmessage("Stack:");
|
||||||
serverconsole.locwarnmessage(generateErrorStack(modLoadingError.error));
|
serverconsole.locwarnmessage(generateErrorStack(modLoadingError.error));
|
||||||
});
|
});
|
||||||
|
@ -4885,7 +4896,7 @@ function start(init) {
|
||||||
if (SSJSError || modLoadingErrors.length > 0) console.log();
|
if (SSJSError || modLoadingErrors.length > 0) console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print info
|
// Print server information
|
||||||
serverconsole.locmessage("Server version: " + version);
|
serverconsole.locmessage("Server version: " + version);
|
||||||
if (process.isBun) serverconsole.locmessage("Bun version: v" + process.versions.bun);
|
if (process.isBun) serverconsole.locmessage("Bun version: v" + process.versions.bun);
|
||||||
else serverconsole.locmessage("Node.JS version: " + process.version);
|
else serverconsole.locmessage("Node.JS version: " + process.version);
|
||||||
|
@ -4909,7 +4920,7 @@ function start(init) {
|
||||||
if (sniReDos) throw new Error("Refusing to start, because the current SNI configuration would make the server vulnerable to ReDoS.");
|
if (sniReDos) throw new Error("Refusing to start, because the current SNI configuration would make the server vulnerable to ReDoS.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Information about starting the server
|
// Print server startup information
|
||||||
if (!(secure && disableNonEncryptedServer)) serverconsole.locmessage("Starting HTTP server at " + (typeof port == "number" ? (listenAddress ? ((listenAddress.indexOf(":") > -1 ? "[" + listenAddress + "]" : listenAddress)) + ":" : "port ") : "") + port.toString() + "...");
|
if (!(secure && disableNonEncryptedServer)) serverconsole.locmessage("Starting HTTP server at " + (typeof port == "number" ? (listenAddress ? ((listenAddress.indexOf(":") > -1 ? "[" + listenAddress + "]" : listenAddress)) + ":" : "port ") : "") + port.toString() + "...");
|
||||||
if (secure) serverconsole.locmessage("Starting HTTPS server at " + (typeof sport == "number" ? (sListenAddress ? ((sListenAddress.indexOf(":") > -1 ? "[" + sListenAddress + "]" : sListenAddress)) + ":" : "port ") : "") + sport.toString() + "...");
|
if (secure) serverconsole.locmessage("Starting HTTPS server at " + (typeof sport == "number" ? (sListenAddress ? ((sListenAddress.indexOf(":") > -1 ? "[" + sListenAddress + "]" : sListenAddress)) + ":" : "port ") : "") + sport.toString() + "...");
|
||||||
}
|
}
|
||||||
|
@ -4997,7 +5008,7 @@ function start(init) {
|
||||||
},
|
},
|
||||||
stop: function (retcode) {
|
stop: function (retcode) {
|
||||||
reallyExiting = true;
|
reallyExiting = true;
|
||||||
clearInterval(pbkdf2CacheIntervalId);
|
clearInterval(passwordHashCacheIntervalId);
|
||||||
if ((!cluster.isPrimary && cluster.isPrimary !== undefined) && server.listening) {
|
if ((!cluster.isPrimary && cluster.isPrimary !== undefined) && server.listening) {
|
||||||
try {
|
try {
|
||||||
server.close(function () {
|
server.close(function () {
|
||||||
|
@ -5155,7 +5166,7 @@ function start(init) {
|
||||||
}, 300000);
|
}, 300000);
|
||||||
}
|
}
|
||||||
if (!cluster.isPrimary) {
|
if (!cluster.isPrimary) {
|
||||||
pbkdf2CacheIntervalId = setInterval(function () {
|
passwordHashCacheIntervalId = setInterval(function () {
|
||||||
pbkdf2Cache = pbkdf2Cache.filter(function (entry) {
|
pbkdf2Cache = pbkdf2Cache.filter(function (entry) {
|
||||||
return entry.addDate > (new Date() - 3600000);
|
return entry.addDate > (new Date() - 3600000);
|
||||||
});
|
});
|
||||||
|
@ -5718,9 +5729,3 @@ try {
|
||||||
process.exit(err.errno ? err.errno : 1);
|
process.exit(err.errno ? err.errno : 1);
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////
|
|
||||||
//// THE END! ////
|
|
||||||
//// WARNING: THE CODE HAS ////
|
|
||||||
//// 5000+ LINES! ////
|
|
||||||
//////////////////////////////////
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>SVR.JS 3.12.3 Tests</title>
|
<title>SVR.JS 3.13.0 Tests</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<style>
|
<style>
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>SVR.JS 3.12.3 Tests</h1>
|
<h1>SVR.JS 3.13.0 Tests</h1>
|
||||||
<h2>Directory (without trailing slash)</h2>
|
<h2>Directory (without trailing slash)</h2>
|
||||||
<iframe src="/testdir" width="50%" height="300px"></iframe>
|
<iframe src="/testdir" width="50%" height="300px"></iframe>
|
||||||
<h2>Directory (with query)</h2>
|
<h2>Directory (with query)</h2>
|
||||||
|
|
Reference in a new issue