1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-30 05:19:57 +00:00

Network performance optimizations for node.js (#5436)

This commit is contained in:
FlashSystems 2021-05-08 17:05:39 +02:00 committed by GitHub
parent e699cf1fe8
commit f4d7b2c7f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 83 additions and 39 deletions

View File

@ -30,6 +30,7 @@ All parameters are optional with safe defaults, and can be specified in any orde
* ''tls-key'' - pathname of TLS key file (relative to wiki folder) * ''tls-key'' - pathname of TLS key file (relative to wiki folder)
* ''debug-level'' - optional debug level; set to "debug" to view request details (defaults to "none") * ''debug-level'' - optional debug level; set to "debug" to view request details (defaults to "none")
* ''gzip'' - set to "yes" to enable gzip compression for some http endpoints (defaults to "no") * ''gzip'' - set to "yes" to enable gzip compression for some http endpoints (defaults to "no")
* ''use-browser-cache'' - set to "yes" to allow the browser to cache responses to save bandwith (defaults to "no")
For information on opening up your instance to the entire local network, and possible security concerns, see the WebServer tiddler at TiddlyWiki.com. For information on opening up your instance to the entire local network, and possible security concerns, see the WebServer tiddler at TiddlyWiki.com.

View File

@ -17,9 +17,8 @@ exports.method = "GET";
exports.path = /^\/favicon.ico$/; exports.path = /^\/favicon.ico$/;
exports.handler = function(request,response,state) { exports.handler = function(request,response,state) {
response.writeHead(200, {"Content-Type": "image/x-icon"});
var buffer = state.wiki.getTiddlerText("$:/favicon.ico",""); var buffer = state.wiki.getTiddlerText("$:/favicon.ico","");
response.end(buffer,"base64"); state.sendResponse(200,{"Content-Type": "image/x-icon"},buffer,"base64");
}; };
}()); }());

View File

@ -34,10 +34,7 @@ exports.handler = function(request,response,state) {
content = content; content = content;
type = ($tw.config.fileExtensionInfo[extension] ? $tw.config.fileExtensionInfo[extension].type : "application/octet-stream"); type = ($tw.config.fileExtensionInfo[extension] ? $tw.config.fileExtensionInfo[extension].type : "application/octet-stream");
} }
response.writeHead(status,{ state.sendResponse(status,{"Content-Type": type},content);
"Content-Type": type
});
response.end(content);
}); });
}; };

View File

@ -12,38 +12,16 @@ GET /
/*global $tw: false */ /*global $tw: false */
"use strict"; "use strict";
var zlib = require("zlib");
exports.method = "GET"; exports.method = "GET";
exports.path = /^\/$/; exports.path = /^\/$/;
exports.handler = function(request,response,state) { exports.handler = function(request,response,state) {
var acceptEncoding = request.headers["accept-encoding"];
if(!acceptEncoding) {
acceptEncoding = "";
}
var text = state.wiki.renderTiddler(state.server.get("root-render-type"),state.server.get("root-tiddler")), var text = state.wiki.renderTiddler(state.server.get("root-render-type"),state.server.get("root-tiddler")),
responseHeaders = { responseHeaders = {
"Content-Type": state.server.get("root-serve-type") "Content-Type": state.server.get("root-serve-type")
}; };
/* state.sendResponse(200,responseHeaders,text);
If the gzip=yes flag for `listen` is set, check if the user agent permits
compression. If so, compress our response. Note that we use the synchronous
functions from zlib to stay in the imperative style. The current `Server`
doesn't depend on this, and we may just as well use the async versions.
*/
if(state.server.enableGzip) {
if (/\bdeflate\b/.test(acceptEncoding)) {
responseHeaders["Content-Encoding"] = "deflate";
text = zlib.deflateSync(text);
} else if (/\bgzip\b/.test(acceptEncoding)) {
responseHeaders["Content-Encoding"] = "gzip";
text = zlib.gzipSync(text);
}
}
response.writeHead(200,responseHeaders);
response.end(text);
}; };
}()); }());

View File

@ -17,7 +17,6 @@ exports.method = "GET";
exports.path = /^\/status$/; exports.path = /^\/status$/;
exports.handler = function(request,response,state) { exports.handler = function(request,response,state) {
response.writeHead(200, {"Content-Type": "application/json"});
var text = JSON.stringify({ var text = JSON.stringify({
username: state.authenticatedUsername || state.server.get("anon-username") || "", username: state.authenticatedUsername || state.server.get("anon-username") || "",
anonymous: !state.authenticatedUsername, anonymous: !state.authenticatedUsername,
@ -28,7 +27,7 @@ exports.handler = function(request,response,state) {
}, },
tiddlywiki_version: $tw.version tiddlywiki_version: $tw.version
}); });
response.end(text,"utf8"); state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8");
}; };
}()); }());

View File

@ -32,9 +32,9 @@ exports.handler = function(request,response,state) {
renderTemplate = renderTemplate || state.server.get("tiddler-render-template"); renderTemplate = renderTemplate || state.server.get("tiddler-render-template");
} }
var text = state.wiki.renderTiddler(renderType,renderTemplate,{parseAsInline: true, variables: {currentTiddler: title}}); var text = state.wiki.renderTiddler(renderType,renderTemplate,{parseAsInline: true, variables: {currentTiddler: title}});
// Naughty not to set a content-type, but it's the easiest way to ensure the browser will see HTML pages as HTML, and accept plain text tiddlers as CSS or JS // Naughty not to set a content-type, but it's the easiest way to ensure the browser will see HTML pages as HTML, and accept plain text tiddlers as CSS or JS
response.writeHead(200); state.sendResponse(200,{},text,"utf8");
response.end(text,"utf8");
} else { } else {
response.writeHead(404); response.writeHead(404);
response.end(); response.end();

View File

@ -36,8 +36,7 @@ exports.handler = function(request,response,state) {
tiddlerFields.revision = state.wiki.getChangeCount(title); tiddlerFields.revision = state.wiki.getChangeCount(title);
tiddlerFields.bag = "default"; tiddlerFields.bag = "default";
tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki";
response.writeHead(200, {"Content-Type": "application/json"}); state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8");
response.end(JSON.stringify(tiddlerFields),"utf8");
} else { } else {
response.writeHead(404); response.writeHead(404);
response.end(); response.end();

View File

@ -33,7 +33,6 @@ exports.handler = function(request,response,state) {
} }
var excludeFields = (state.queryParameters.exclude || "text").split(","), var excludeFields = (state.queryParameters.exclude || "text").split(","),
titles = state.wiki.filterTiddlers(filter); titles = state.wiki.filterTiddlers(filter);
response.writeHead(200, {"Content-Type": "application/json"});
var tiddlers = []; var tiddlers = [];
$tw.utils.each(titles,function(title) { $tw.utils.each(titles,function(title) {
var tiddler = state.wiki.getTiddler(title); var tiddler = state.wiki.getTiddler(title);
@ -45,7 +44,7 @@ exports.handler = function(request,response,state) {
} }
}); });
var text = JSON.stringify(tiddlers); var text = JSON.stringify(tiddlers);
response.end(text,"utf8"); state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8");
}; };
}()); }());

View File

@ -17,7 +17,9 @@ if($tw.node) {
fs = require("fs"), fs = require("fs"),
url = require("url"), url = require("url"),
path = require("path"), path = require("path"),
querystring = require("querystring"); querystring = require("querystring"),
crypto = require("crypto"),
zlib = require("zlib");
} }
/* /*
@ -47,6 +49,8 @@ function Server(options) {
this.csrfDisable = this.get("csrf-disable") === "yes"; this.csrfDisable = this.get("csrf-disable") === "yes";
// Initialize Gzip compression // Initialize Gzip compression
this.enableGzip = this.get("gzip") === "yes"; this.enableGzip = this.get("gzip") === "yes";
// Initialize browser-caching
this.enableBrowserCache = this.get("use-browser-cache") === "yes";
// Initialise authorization // Initialise authorization
var authorizedUserName = (this.get("username") && this.get("password")) ? this.get("username") : "(anon)"; var authorizedUserName = (this.get("username") && this.get("password")) ? this.get("username") : "(anon)";
this.authorizationPrincipals = { this.authorizationPrincipals = {
@ -78,6 +82,71 @@ function Server(options) {
this.transport = require(this.protocol); this.transport = require(this.protocol);
} }
/*
Send a response to the client. This method checks if the response must be sent
or if the client alrady has the data cached. If that's the case only a 304
response will be transmitted and the browser will use the cached data.
Only requests with status code 200 are considdered for caching.
request: request instance passed to the handler
response: response instance passed to the handler
statusCode: stauts code to send to the browser
headers: response headers (they will be augmented with an `Etag` header)
data: the data to send (passed to the end method of the response instance)
encoding: the encoding of the data to send (passed to the end method of the response instance)
*/
function sendResponse(request,response,statusCode,headers,data,encoding) {
if(this.enableBrowserCache && (statusCode == 200)) {
var hash = crypto.createHash('md5');
// Put everything into the hash that could change and invalidate the data that
// the browser already stored. The headers the data and the encoding.
hash.update(data);
hash.update(JSON.stringify(headers));
if(encoding) {
hash.update(encoding);
}
var contentDigest = hash.digest("hex");
// RFC 7232 section 2.3 mandates for the etag to be enclosed in quotes
headers["Etag"] = '"' + contentDigest + '"';
headers["Cache-Control"] = "max-age=0, must-revalidate";
// Check if any of the hashes contained within the if-none-match header
// matches the current hash.
// If one matches, do not send the data but tell the browser to use the
// cached data.
// We do not implement "*" as it makes no sense here.
var ifNoneMatch = request.headers["if-none-match"];
if(ifNoneMatch) {
var matchParts = ifNoneMatch.split(",").map(function(etag) {
return etag.replace(/^[ "]+|[ "]+$/g, "");
});
if(matchParts.indexOf(contentDigest) != -1) {
response.writeHead(304,headers);
response.end();
return;
}
}
}
/*
If the gzip=yes is set, check if the user agent permits compression. If so,
compress our response if the raw data is bigger than 2k. Compressing less
data is inefficient. Note that we use the synchronous functions from zlib
to stay in the imperative style. The current `Server` doesn't depend on
this, and we may just as well use the async versions.
*/
if(this.enableGzip && (data.length > 2048)) {
var acceptEncoding = request.headers["accept-encoding"] || "";
if(/\bdeflate\b/.test(acceptEncoding)) {
headers["Content-Encoding"] = "deflate";
data = zlib.deflateSync(data);
} else if(/\bgzip\b/.test(acceptEncoding)) {
headers["Content-Encoding"] = "gzip";
data = zlib.gzipSync(data);
}
}
response.writeHead(statusCode,headers);
response.end(data,encoding);
}
Server.prototype.defaultVariables = { Server.prototype.defaultVariables = {
port: "8080", port: "8080",
host: "127.0.0.1", host: "127.0.0.1",
@ -89,7 +158,8 @@ Server.prototype.defaultVariables = {
"system-tiddler-render-type": "text/plain", "system-tiddler-render-type": "text/plain",
"system-tiddler-render-template": "$:/core/templates/wikified-tiddler", "system-tiddler-render-template": "$:/core/templates/wikified-tiddler",
"debug-level": "none", "debug-level": "none",
"gzip": "no" "gzip": "no",
"use-browser-cache": "no"
}; };
Server.prototype.get = function(name) { Server.prototype.get = function(name) {
@ -167,6 +237,7 @@ Server.prototype.requestHandler = function(request,response,options) {
state.urlInfo = url.parse(request.url); state.urlInfo = url.parse(request.url);
state.queryParameters = querystring.parse(state.urlInfo.query); state.queryParameters = querystring.parse(state.urlInfo.query);
state.pathPrefix = options.pathPrefix || this.get("path-prefix") || ""; state.pathPrefix = options.pathPrefix || this.get("path-prefix") || "";
state.sendResponse = sendResponse.bind(self,request,response);
// Get the principals authorized to access this resource // Get the principals authorized to access this resource
var authorizationType = this.methodMappings[request.method] || "readers"; var authorizationType = this.methodMappings[request.method] || "readers";
// Check for the CSRF header if this is a write // Check for the CSRF header if this is a write

View File

@ -31,5 +31,6 @@ Mögliche Parameter:
* ''tls-key'' - Pfad zur "TLS key" Datei (relativ zum Wiki Verzeichnis) * ''tls-key'' - Pfad zur "TLS key" Datei (relativ zum Wiki Verzeichnis)
* ''debug-level'' - "debug" bewikt eine detailierte Anzeige der HTTP Anfrage-Parameter. (Standard: "none") * ''debug-level'' - "debug" bewikt eine detailierte Anzeige der HTTP Anfrage-Parameter. (Standard: "none")
* ''gzip'' - Wenn auf "yes" gesetzt, dann wird gzip Kompression aktiviert. (Standard: "no") * ''gzip'' - Wenn auf "yes" gesetzt, dann wird gzip Kompression aktiviert. (Standard: "no")
* ''use-browser-cache'' - Ist dieser Parameter auf "yes" gesetzt kann der Browser Inhalte zwischenspeichern um Übertragungsbandbreite zu sparen. (Standard: "no")
Für weitere Sicherheitshinweise und Informationen für die Verwendung in lokalen Netzwerken siehe: WebServer auf TiddlyWiki.com Für weitere Sicherheitshinweise und Informationen für die Verwendung in lokalen Netzwerken siehe: WebServer auf TiddlyWiki.com