Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnMcLear committed Feb 27, 2021
2 parents 55f76c5 + 37769cc commit ad84c8a
Show file tree
Hide file tree
Showing 17 changed files with 239 additions and 246 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# 1.8.11

### Notable fixes

* Fix server crash issue within PadMessageHandler due to SocketIO handling
* Fix editor issue with drop downs not being visible
* Ensure correct version is passed when loading front end resources
* Ensure underscore and jquery are available in original location for plugin comptability

### Notable enhancements

* Improved page load speeds

# 1.8.10

### Security Patches
Expand Down
33 changes: 15 additions & 18 deletions src/node/handler/PadMessageHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -1409,16 +1409,14 @@ const composePadChangesets = async (padId, startNum, endNum) => {
};

const _getRoomSockets = (padID) => {
const roomSockets = [];
const room = socketio.sockets.adapter.rooms[padID];

if (room) {
for (const id of Object.keys(room.sockets)) {
roomSockets.push(socketio.sockets.sockets[id]);
}
}

return roomSockets;
const ns = socketio.sockets; // Default namespace.
const adapter = ns.adapter;
// We could call adapter.clients(), but that method is unnecessarily asynchronous. Replicate what
// it does here, but synchronously to avoid a race condition. This code will have to change when
// we update to socket.io v3.
const room = adapter.rooms[padID];
if (!room) return [];
return Object.keys(room.sockets).map((id) => ns.connected[id]).filter((s) => s);
};

/**
Expand All @@ -1438,14 +1436,13 @@ exports.padUsers = async (padID) => {
await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => {
const s = sessioninfos[roomSocket.id];
if (s) {
return authorManager.getAuthor(s.author).then((author) => {
// Fixes: https://github.com/ether/etherpad-lite/issues/4120
// On restart author might not be populated?
if (author) {
author.id = s.author;
padUsers.push(author);
}
});
const author = await authorManager.getAuthor(s.author);
// Fixes: https://github.com/ether/etherpad-lite/issues/4120
// On restart author might not be populated?
if (author) {
author.id = s.author;
padUsers.push(author);
}
}
}));

Expand Down
2 changes: 1 addition & 1 deletion src/node/hooks/express/static.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const getTar = async () => {
exports.expressCreateServer = async (hookName, args) => {
// Cache both minified and static.
const assetCache = new CachingMiddleware();
args.app.all(/\/javascripts\/(.*)/, assetCache.handle);
args.app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache));

// Minify will serve static files compressed (minify enabled). It also has
// file-specific hacks for ace/require-kernel/etc.
Expand Down
237 changes: 109 additions & 128 deletions src/node/utils/caching_middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
* limitations under the License.
*/

const async = require('async');
const Buffer = require('buffer').Buffer;
const fs = require('fs');
const fsp = fs.promises;
const path = require('path');
const zlib = require('zlib');
const settings = require('./Settings');
const existsSync = require('./path_exists');
const util = require('util');

/*
* The crypto module can be absent on reduced node installations.
Expand Down Expand Up @@ -77,146 +78,126 @@ if (_crypto) {
should replace this.
*/

function CachingMiddleware() {
}
CachingMiddleware.prototype = new function () {
const handle = (req, res, next) => {
module.exports = class CachingMiddleware {
handle(req, res, next) {
this._handle(req, res, next).catch((err) => next(err || new Error(err)));
}

async _handle(req, res, next) {
if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) {
return next(undefined, req, res);
}

const old_req = {};
const old_res = {};
const oldReq = {};
const oldRes = {};

const supportsGzip =
(req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;

const path = require('url').parse(req.url).path;
const cacheKey = generateCacheKey(path);
const url = new URL(req.url, 'http://localhost');
const cacheKey = generateCacheKey(url.pathname + url.search);

fs.stat(`${CACHE_DIR}minified_${cacheKey}`, (error, stats) => {
const modifiedSince = (req.headers['if-modified-since'] &&
new Date(req.headers['if-modified-since']));
const lastModifiedCache = !error && stats.mtime;
if (lastModifiedCache && responseCache[cacheKey]) {
req.headers['if-modified-since'] = lastModifiedCache.toUTCString();
} else {
delete req.headers['if-modified-since'];
const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {});
const modifiedSince =
req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']);
if (stats != null && stats.mtime && responseCache[cacheKey]) {
req.headers['if-modified-since'] = stats.mtime.toUTCString();
} else {
delete req.headers['if-modified-since'];
}

// Always issue get to downstream.
oldReq.method = req.method;
req.method = 'GET';

// This handles read/write synchronization as well as its predecessor,
// which is to say, not at all.
// TODO: Implement locking on write or ditch caching of gzip and use
// existing middlewares.
const respond = () => {
req.method = oldReq.method || req.method;
res.write = oldRes.write || res.write;
res.end = oldRes.end || res.end;

const headers = {};
Object.assign(headers, (responseCache[cacheKey].headers || {}));
const statusCode = responseCache[cacheKey].statusCode;

let pathStr = `${CACHE_DIR}minified_${cacheKey}`;
if (supportsGzip && /application\/javascript/.test(headers['content-type'])) {
pathStr += '.gz';
headers['content-encoding'] = 'gzip';
}

// Always issue get to downstream.
old_req.method = req.method;
req.method = 'GET';
const lastModified = headers['last-modified'] && new Date(headers['last-modified']);

const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires);
if (expirationDate > new Date()) {
// Our cached version is still valid.
return respond();
if (statusCode === 200 && lastModified <= modifiedSince) {
res.writeHead(304, headers);
res.end();
} else if (req.method === 'GET') {
const readStream = fs.createReadStream(pathStr);
res.writeHead(statusCode, headers);
readStream.pipe(res);
} else {
res.writeHead(statusCode, headers);
res.end();
}
};

const _headers = {};
old_res.setHeader = res.setHeader;
res.setHeader = (key, value) => {
// Don't set cookies, see issue #707
if (key.toLowerCase() === 'set-cookie') return;

_headers[key.toLowerCase()] = value;
old_res.setHeader.call(res, key, value);
};

old_res.writeHead = res.writeHead;
res.writeHead = function (status, headers) {
res.writeHead = old_res.writeHead;
if (status === 200) {
// Update cache
let buffer = '';

Object.keys(headers || {}).forEach((key) => {
res.setHeader(key, headers[key]);
});
headers = _headers;

old_res.write = res.write;
old_res.end = res.end;
res.write = function (data, encoding) {
buffer += data.toString(encoding);
};
res.end = function (data, encoding) {
async.parallel([
function (callback) {
const path = `${CACHE_DIR}minified_${cacheKey}`;
fs.writeFile(path, buffer, (error, stats) => {
callback();
});
},
function (callback) {
const path = `${CACHE_DIR}minified_${cacheKey}.gz`;
zlib.gzip(buffer, (error, content) => {
if (error) {
callback();
} else {
fs.writeFile(path, content, (error, stats) => {
callback();
});
}
});
},
], () => {
responseCache[cacheKey] = {statusCode: status, headers};
respond();
});
};
} else if (status === 304) {
// Nothing new changed from the cached version.
old_res.write = res.write;
old_res.end = res.end;
res.write = function (data, encoding) {};
res.end = function (data, encoding) { respond(); };
} else {
res.writeHead(status, headers);
}
};

next(undefined, req, res);

// This handles read/write synchronization as well as its predecessor,
// which is to say, not at all.
// TODO: Implement locking on write or ditch caching of gzip and use
// existing middlewares.
function respond() {
req.method = old_req.method || req.method;
res.write = old_res.write || res.write;
res.end = old_res.end || res.end;

const headers = {};
Object.assign(headers, (responseCache[cacheKey].headers || {}));
const statusCode = responseCache[cacheKey].statusCode;

let pathStr = `${CACHE_DIR}minified_${cacheKey}`;
if (supportsGzip && /application\/javascript/.test(headers['content-type'])) {
pathStr += '.gz';
headers['content-encoding'] = 'gzip';
}

const lastModified = (headers['last-modified'] &&
new Date(headers['last-modified']));

if (statusCode === 200 && lastModified <= modifiedSince) {
res.writeHead(304, headers);
res.end();
} else if (req.method === 'GET') {
const readStream = fs.createReadStream(pathStr);
res.writeHead(statusCode, headers);
readStream.pipe(res);
} else {
res.writeHead(statusCode, headers);
res.end();
}
}
});
};
const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires);
if (expirationDate > new Date()) {
// Our cached version is still valid.
return respond();
}

this.handle = handle;
}();
const _headers = {};
oldRes.setHeader = res.setHeader;
res.setHeader = (key, value) => {
// Don't set cookies, see issue #707
if (key.toLowerCase() === 'set-cookie') return;

_headers[key.toLowerCase()] = value;
oldRes.setHeader.call(res, key, value);
};

oldRes.writeHead = res.writeHead;
res.writeHead = (status, headers) => {
res.writeHead = oldRes.writeHead;
if (status === 200) {
// Update cache
let buffer = '';

Object.keys(headers || {}).forEach((key) => {
res.setHeader(key, headers[key]);
});
headers = _headers;

oldRes.write = res.write;
oldRes.end = res.end;
res.write = (data, encoding) => {
buffer += data.toString(encoding);
};
res.end = async (data, encoding) => {
await Promise.all([
fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}),
util.promisify(zlib.gzip)(buffer)
.then((content) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content))
.catch(() => {}),
]);
responseCache[cacheKey] = {statusCode: status, headers};
respond();
};
} else if (status === 304) {
// Nothing new changed from the cached version.
oldRes.write = res.write;
oldRes.end = res.end;
res.write = (data, encoding) => {};
res.end = (data, encoding) => { respond(); };
} else {
res.writeHead(status, headers);
}
};

module.exports = CachingMiddleware;
next(undefined, req, res);
}
};
Loading

0 comments on commit ad84c8a

Please sign in to comment.