use worker to prevent dos

This commit is contained in:
Tony Brix 2019-02-13 12:16:39 -06:00
parent 3fe557209a
commit acf9c82388
3 changed files with 213 additions and 56 deletions

View File

@ -62,7 +62,7 @@ header h1 {
flex-grow: 1; flex-grow: 1;
} }
#options.badParse { .error {
border-color: red; border-color: red;
background-color: #FEE background-color: #FEE
} }

View File

@ -7,12 +7,17 @@ if (!window.fetch) {
window.fetch = unfetch; window.fetch = unfetch;
} }
onunhandledrejection = function (e) {
throw e.reason;
};
var $markdownElem = document.querySelector('#markdown'); var $markdownElem = document.querySelector('#markdown');
var $markedVerElem = document.querySelector('#markedVersion'); var $markedVerElem = document.querySelector('#markedVersion');
var $markedVer = document.querySelector('#markedCdn'); var $markedVer = document.querySelector('#markedCdn');
var $optionsElem = document.querySelector('#options'); var $optionsElem = document.querySelector('#options');
var $outputTypeElem = document.querySelector('#outputType'); var $outputTypeElem = document.querySelector('#outputType');
var $inputTypeElem = document.querySelector('#inputType'); var $inputTypeElem = document.querySelector('#inputType');
var $previewElem = document.querySelector('#preview');
var $previewIframe = document.querySelector('#preview iframe'); var $previewIframe = document.querySelector('#preview iframe');
var $permalinkElem = document.querySelector('#permalink'); var $permalinkElem = document.querySelector('#permalink');
var $clearElem = document.querySelector('#clear'); var $clearElem = document.querySelector('#clear');
@ -20,6 +25,7 @@ var $htmlElem = document.querySelector('#html');
var $lexerElem = document.querySelector('#lexer'); var $lexerElem = document.querySelector('#lexer');
var $panes = document.querySelectorAll('.pane'); var $panes = document.querySelectorAll('.pane');
var $inputPanes = document.querySelectorAll('.inputPane'); var $inputPanes = document.querySelectorAll('.inputPane');
var lastInput = '';
var inputDirty = true; var inputDirty = true;
var $activeOutputElem = null; var $activeOutputElem = null;
var search = searchToObject(); var search = searchToObject();
@ -74,14 +80,7 @@ fetch('https://data.jsdelivr.com/v1/package/npm/marked')
if ('options' in search && search.options) { if ('options' in search && search.options) {
$optionsElem.value = search.options; $optionsElem.value = search.options;
} else { } else {
$optionsElem.value = JSON.stringify( setDefaultOptions();
marked.getDefaults(),
function (key, value) {
if (value && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) {
return undefined;
}
return value;
}, ' ');
} }
}); });
}); });
@ -141,17 +140,31 @@ $optionsElem.addEventListener('keydown', handleInput, false);
$clearElem.addEventListener('click', function () { $clearElem.addEventListener('click', function () {
$markdownElem.value = ''; $markdownElem.value = '';
$markedVerElem.value = 'master'; $markedVerElem.value = 'master';
updateVersion().then(function () { updateVersion().then(setDefaultOptions);
}, false);
function setDefaultOptions() {
if (window.Worker) {
messageWorker({
task: 'defaults',
version: markedVersions[$markedVerElem.value]}
);
} else {
var defaults = marked.getDefaults();
setOptions(defaults);
}
}
function setOptions(opts) {
$optionsElem.value = JSON.stringify( $optionsElem.value = JSON.stringify(
marked.getDefaults(), opts,
function (key, value) { function (key, value) {
if (value && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) { if (value && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) {
return undefined; return undefined;
} }
return value; return value;
}, ' '); }, ' ');
}); }
}, false);
function searchToObject() { function searchToObject() {
// modified from https://stackoverflow.com/a/7090123/806777 // modified from https://stackoverflow.com/a/7090123/806777
@ -213,6 +226,10 @@ function updateLink() {
} }
function updateVersion() { function updateVersion() {
if (window.Worker) {
handleInput();
return Promise.resolve();
}
var promise; var promise;
if ($markedVerElem.value in markedVersionCache) { if ($markedVerElem.value in markedVersionCache) {
promise = Promise.resolve(markedVersionCache[$markedVerElem.value]); promise = Promise.resolve(markedVersionCache[$markedVerElem.value]);
@ -234,30 +251,40 @@ function updateVersion() {
} }
var delayTime = 1; var delayTime = 1;
var options = {}; var checkChangeTimeout = null;
function checkForChanges() { function checkForChanges() {
if (inputDirty && typeof marked !== 'undefined') { if (inputDirty && (typeof marked !== 'undefined' || window.Worker)) {
inputDirty = false; inputDirty = false;
updateLink(); updateLink();
var startTime = new Date(); var options = {};
var scrollPercent = getScrollPercent();
try {
var optionsString = $optionsElem.value || '{}'; var optionsString = $optionsElem.value || '{}';
try {
var newOptions = JSON.parse(optionsString); var newOptions = JSON.parse(optionsString);
options = newOptions; options = newOptions;
$optionsElem.classList.remove('badParse'); $optionsElem.classList.remove('error');
} catch (err) { } catch (err) {
$optionsElem.classList.add('badParse'); $optionsElem.classList.add('error');
} }
var lexed = marked.lexer($markdownElem.value, options); var version = markedVersions[$markedVerElem.value];
var markdown = $markdownElem.value;
var hash = version + markdown + optionsString;
if (lastInput !== hash) {
lastInput = hash;
if (window.Worker) {
delayTime = 100;
messageWorker({
task: 'parse',
version: version,
markdown: markdown,
options: options
});
} else {
var startTime = new Date();
var lexed = marked.lexer(markdown, options);
var lexedList = []; var lexedList = [];
for (var i = 0; i < lexed.length; i++) { for (var i = 0; i < lexed.length; i++) {
var lexedLine = []; var lexedLine = [];
for (var j in lexed[i]) { for (var j in lexed[i]) {
@ -265,17 +292,10 @@ function checkForChanges() {
} }
lexedList.push('{' + lexedLine.join(', ') + '}'); lexedList.push('{' + lexedLine.join(', ') + '}');
} }
var parsed = marked.parser(lexed, options); var parsed = marked.parser(lexed, options);
var scrollPercent = getScrollPercent();
if (iframeLoaded) { setParsed(parsed, lexedList.join('\n'));
$previewIframe.contentDocument.body.innerHTML = (parsed);
}
$htmlElem.value = (parsed);
$lexerElem.value = (lexedList.join('\n'));
setScrollPercent(scrollPercent); setScrollPercent(scrollPercent);
var endTime = new Date(); var endTime = new Date();
delayTime = endTime - startTime; delayTime = endTime - startTime;
if (delayTime < 50) { if (delayTime < 50) {
@ -284,7 +304,69 @@ function checkForChanges() {
delayTime = 1000; delayTime = 1000;
} }
} }
window.setTimeout(checkForChanges, delayTime); }
}
checkChangeTimeout = window.setTimeout(checkForChanges, delayTime);
}; };
function setParsed(parsed, lexed) {
if (iframeLoaded) {
$previewIframe.contentDocument.body.innerHTML = parsed;
}
$htmlElem.value = parsed;
$lexerElem.value = lexed;
}
var markedWorker;
function messageWorker(message) {
if (!markedWorker || markedWorker.working) {
if (markedWorker) {
clearTimeout(markedWorker.timeout);
markedWorker.terminate();
}
markedWorker = new Worker('worker.js');
markedWorker.onmessage = function (e) {
clearTimeout(markedWorker.timeout);
markedWorker.working = false;
switch (e.data.task) {
case 'defaults':
setOptions(e.data.defaults);
break;
case 'parse':
$previewElem.classList.remove('error');
$htmlElem.classList.remove('error');
$lexerElem.classList.remove('error');
var scrollPercent = getScrollPercent();
setParsed(e.data.parsed, e.data.lexed);
setScrollPercent(scrollPercent);
break;
}
clearTimeout(checkChangeTimeout);
delayTime = 10;
checkForChanges();
};
markedWorker.onerror = markedWorker.onmessageerror = function (err) {
clearTimeout(markedWorker.timeout);
var error = 'There was an error in the Worker';
if (err) {
if (err.message) {
error = err.message;
} else {
error = err;
}
}
$previewElem.classList.add('error');
$htmlElem.classList.add('error');
$lexerElem.classList.add('error');
setParsed(error, error);
setScrollPercent(0);
};
}
markedWorker.working = true;
markedWorker.timeout = setTimeout(function () {
markedWorker.onerror('Marked is taking a while...');
}, 1000);
markedWorker.postMessage(message);
}
checkForChanges(); checkForChanges();
setScrollPercent(0); setScrollPercent(0);

75
docs/demo/worker.js Normal file
View File

@ -0,0 +1,75 @@
/* global marked */
var versionCache = {};
var currentVersion;
onmessage = function (e) {
if (e.data.version === currentVersion) {
parse(e);
} else {
getVersion(e.data.version).then(function (text) {
// eslint-disable-next-line no-new-func
Function(text)();
currentVersion = e.data.version;
parse(e);
});
}
};
onunhandledrejection = function (e) {
throw e.reason;
};
function parse(e) {
switch (e.data.task) {
case 'defaults':
var defaults = marked.getDefaults();
defaults.renderer = null;
postMessage({
task: e.data.task,
defaults: defaults
});
break;
case 'parse':
var lexed = marked.lexer(e.data.markdown, e.data.options);
var lexedList = [];
for (var i = 0; i < lexed.length; i++) {
var lexedLine = [];
for (var j in lexed[i]) {
lexedLine.push(j + ':' + jsonString(lexed[i][j]));
}
lexedList.push('{' + lexedLine.join(', ') + '}');
}
var parsed = marked.parser(lexed, e.data.options);
postMessage({
task: e.data.task,
lexed: lexedList.join('\n'),
parsed: parsed
});
break;
}
}
function jsonString(input) {
var output = (input + '')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(/\f/g, '\\f')
.replace(/[\\"']/g, '\\$&')
.replace(/\u0000/g, '\\0');
return '"' + output + '"';
};
function getVersion(ver) {
if (ver in versionCache) {
return Promise.resolve(versionCache[ver]);
}
return fetch(ver)
.then(function (res) { return res.text(); })
.then(function (text) {
versionCache[ver] = text;
return text;
});
}