1 | const util = require('./util.js');
|
2 | const zlib = require('zlib');
|
3 | const validUrl = require('valid-url');
|
4 |
|
5 | const WAIT_AFTER_LAST_REQUEST = process.env.WAIT_AFTER_LAST_REQUEST || 500;
|
6 |
|
7 | const PAGE_DONE_CHECK_INTERVAL = process.env.PAGE_DONE_CHECK_INTERVAL || 500;
|
8 |
|
9 | const PAGE_LOAD_TIMEOUT = process.env.PAGE_LOAD_TIMEOUT || 20 * 1000;
|
10 |
|
11 | const FOLLOW_REDIRECTS = process.env.FOLLOW_REDIRECTS || false;
|
12 |
|
13 | const LOG_REQUESTS = process.env.LOG_REQUESTS || false;
|
14 |
|
15 | const CAPTURE_CONSOLE_LOG = process.env.CAPTURE_CONSOLE_LOG || false;
|
16 |
|
17 | const ENABLE_SERVICE_WORKER = process.env.ENABLE_SERVICE_WORKER || false;
|
18 |
|
19 |
|
20 | const BROWSER_FORCE_RESTART_PERIOD = process.env.BROWSER_FORCE_RESTART_PERIOD || 3600000;
|
21 |
|
22 |
|
23 | const BROWSER_TRY_RESTART_PERIOD = process.env.BROWSER_TRY_RESTART_PERIOD || 600000;
|
24 |
|
25 | const BROWSER_DEBUGGING_PORT = process.env.BROWSER_DEBUGGING_PORT || 9222;
|
26 |
|
27 | const server = exports = module.exports = {};
|
28 |
|
29 |
|
30 |
|
31 | server.init = function(options) {
|
32 | this.plugins = this.plugins || [];
|
33 | this.options = options || {};
|
34 |
|
35 | this.options.waitAfterLastRequest = this.options.waitAfterLastRequest || WAIT_AFTER_LAST_REQUEST;
|
36 | this.options.pageDoneCheckInterval = this.options.pageDoneCheckInterval || PAGE_DONE_CHECK_INTERVAL;
|
37 | this.options.pageLoadTimeout = this.options.pageLoadTimeout || PAGE_LOAD_TIMEOUT;
|
38 | this.options.followRedirects = this.options.followRedirects || FOLLOW_REDIRECTS;
|
39 | this.options.logRequests = this.options.logRequests || LOG_REQUESTS;
|
40 | this.options.captureConsoleLog = this.options.captureConsoleLog || CAPTURE_CONSOLE_LOG;
|
41 | this.options.enableServiceWorker = this.options.enableServiceWorker || ENABLE_SERVICE_WORKER;
|
42 | this.options.pdfOptions = this.options.pdfOptions || {
|
43 | printBackground: true
|
44 | };
|
45 | this.options.browserDebuggingPort = this.options.browserDebuggingPort || BROWSER_DEBUGGING_PORT;
|
46 |
|
47 | this.browser = require('./browsers/chrome');
|
48 |
|
49 | return this;
|
50 | };
|
51 |
|
52 |
|
53 |
|
54 | server.start = function() {
|
55 | util.log('Starting Prerender');
|
56 | this.isBrowserConnected = false;
|
57 | this.startPrerender().then(() => {
|
58 |
|
59 | process.on('SIGINT', () => {
|
60 | this.killBrowser();
|
61 | setTimeout(() => {
|
62 | util.log('Stopping Prerender');
|
63 | process.exit();
|
64 | }, 500);
|
65 | });
|
66 |
|
67 | }).catch(() => {
|
68 | if(process.exit) {
|
69 | process.exit();
|
70 | }
|
71 | });
|
72 | };
|
73 |
|
74 |
|
75 |
|
76 | server.startPrerender = function() {
|
77 | return new Promise((resolve, reject) => {
|
78 | this.spawnBrowser().then(() => {
|
79 |
|
80 | this.listenForBrowserClose();
|
81 | return this.connectToBrowser();
|
82 | }).then(() => {
|
83 | this.browserRequestsInFlight = 0;
|
84 | this.lastRestart = new Date().getTime();
|
85 | this.isBrowserConnected = true;
|
86 | util.log(`Started ${this.browser.name}: ${this.browser.version}`)
|
87 | resolve();
|
88 | }).catch((err) => {
|
89 | util.log(err);
|
90 | util.log(`Failed to start and/or connect to ${this.browser.name}. Please make sure ${this.browser.name} is running`);
|
91 | this.killBrowser();
|
92 | reject();
|
93 | });
|
94 | });
|
95 | };
|
96 |
|
97 |
|
98 |
|
99 | server.spawnBrowser = function() {
|
100 |
|
101 | util.log(`Starting ${this.browser.name}`);
|
102 | return this.browser.spawn(this.options);
|
103 | };
|
104 |
|
105 |
|
106 |
|
107 | server.killBrowser = function() {
|
108 | util.log(`Stopping ${this.browser.name}`);
|
109 | this.isBrowserClosing = true;
|
110 | this.browser.kill();
|
111 | };
|
112 |
|
113 |
|
114 |
|
115 | server.restartBrowser = function() {
|
116 | this.isBrowserConnected = false;
|
117 | util.log(`Restarting ${this.browser.name}`);
|
118 | this.browser.kill();
|
119 | };
|
120 |
|
121 |
|
122 |
|
123 | server.connectToBrowser = function() {
|
124 | return this.browser.connect();
|
125 | };
|
126 |
|
127 |
|
128 |
|
129 | server.listenForBrowserClose = function() {
|
130 | let start = new Date().getTime();
|
131 |
|
132 | this.isBrowserClosing = false;
|
133 |
|
134 | this.browser.onClose(() => {
|
135 | this.isBrowserConnected = false;
|
136 | if(this.isBrowserClosing) {
|
137 | util.log(`Stopped ${this.browser.name}`);
|
138 | return;
|
139 | }
|
140 |
|
141 | util.log(`${this.browser.name} connection closed... restarting ${this.browser.name}`);
|
142 |
|
143 | if (new Date().getTime() - start < 1000) {
|
144 | util.log(`${this.browser.name} died immediately after restart... stopping Prerender`);
|
145 | return process.exit();
|
146 | }
|
147 |
|
148 | this.startPrerender();
|
149 | });
|
150 | };
|
151 |
|
152 |
|
153 |
|
154 | server.waitForBrowserToConnect = function() {
|
155 | return new Promise((resolve, reject) => {
|
156 | var checks = 0;
|
157 |
|
158 | let check = () => {
|
159 | if(++checks > 100) {
|
160 | return reject(`Timed out waiting for ${this.browser.name} connection`);
|
161 | }
|
162 |
|
163 | if (!this.isBrowserConnected) {
|
164 | return setTimeout(check, 200);
|
165 | }
|
166 |
|
167 | resolve();
|
168 | }
|
169 |
|
170 | check();
|
171 | });
|
172 | };
|
173 |
|
174 |
|
175 |
|
176 | server.disconnectBrowserIfBrowserShouldBeRestarted = function() {
|
177 |
|
178 |
|
179 |
|
180 | if(this.browserRequestsInFlight > 0 && new Date().getTime() - this.lastRestart > BROWSER_FORCE_RESTART_PERIOD) {
|
181 | this.isBrowserConnected = false;
|
182 | }
|
183 | };
|
184 |
|
185 |
|
186 |
|
187 | server.use = function(plugin) {
|
188 | this.plugins.push(plugin);
|
189 | if (typeof plugin.init === 'function') plugin.init(this);
|
190 | };
|
191 |
|
192 |
|
193 |
|
194 | server.onRequest = function(req, res) {
|
195 |
|
196 | req.prerender = util.getOptions(req);
|
197 | req.prerender.start = new Date();
|
198 | req.prerender.responseSent = false;
|
199 |
|
200 | util.log('getting', req.prerender.url);
|
201 |
|
202 | this.firePluginEvent('requestReceived', req, res)
|
203 | .then(() => {
|
204 |
|
205 | if (!validUrl.isWebUri(encodeURI(req.prerender.url))) {
|
206 | util.log('invalid URL:', req.prerender.url);
|
207 | req.prerender.statusCode = 400;
|
208 | return Promise.reject();
|
209 | }
|
210 |
|
211 | req.prerender.startConnectingToBrowser = new Date();
|
212 |
|
213 | this.disconnectBrowserIfBrowserShouldBeRestarted();
|
214 |
|
215 | return this.waitForBrowserToConnect();
|
216 |
|
217 | }).then(() => {
|
218 | req.prerender.startOpeningTab = new Date();
|
219 |
|
220 | this.browserRequestsInFlight++;
|
221 | req.prerender.browserRequestIsInFlight = true;
|
222 |
|
223 |
|
224 | setTimeout(() => {
|
225 | if (!req.prerender.responseSent) {
|
226 | util.log('response not sent for', req.prerender.url);
|
227 | req.prerender.browserRequestIsInFlight = false;
|
228 | this.browserRequestsInFlight--;
|
229 | }
|
230 | }, 60000);
|
231 |
|
232 | return this.browser.openTab(req.prerender);
|
233 |
|
234 | }).then((tab) => {
|
235 | req.prerender.endOpeningTab = new Date();
|
236 | req.prerender.tab = tab;
|
237 |
|
238 | return this.firePluginEvent('tabCreated', req, res);
|
239 | }).then(() => {
|
240 | req.prerender.startLoadingUrl = new Date();
|
241 |
|
242 | return this.browser.loadUrlThenWaitForPageLoadEvent(req.prerender.tab, req.prerender.url);
|
243 | }).then(() => {
|
244 | req.prerender.endLoadingUrl = new Date();
|
245 |
|
246 | if (req.prerender.javascript) {
|
247 | return this.browser.executeJavascript(req.prerender.tab, req.prerender.javascript);
|
248 | } else {
|
249 | return Promise.resolve();
|
250 | }
|
251 | }).then(() => {
|
252 | return this.firePluginEvent('beforeParse', req, res);
|
253 | }).then(() => {
|
254 | req.prerender.startParse = new Date();
|
255 |
|
256 | if (req.prerender.renderType == 'png') {
|
257 |
|
258 | return this.browser.captureScreenshot(req.prerender.tab, 'png', req.prerender.fullpage);
|
259 |
|
260 | } else if (req.prerender.renderType == 'jpeg') {
|
261 |
|
262 | return this.browser.captureScreenshot(req.prerender.tab, 'jpeg', req.prerender.fullpage);
|
263 |
|
264 | } else if (req.prerender.renderType == 'pdf') {
|
265 |
|
266 | return this.browser.printToPDF(req.prerender.tab, this.options.pdfOptions);
|
267 |
|
268 | } else if (req.prerender.renderType == 'har') {
|
269 |
|
270 | return this.browser.getHarFile(req.prerender.tab);
|
271 | } else {
|
272 |
|
273 | return this.browser.parseHtmlFromPage(req.prerender.tab);
|
274 | }
|
275 | }).then(() => {
|
276 | req.prerender.endParse = new Date();
|
277 |
|
278 | req.prerender.statusCode = req.prerender.tab.prerender.statusCode;
|
279 | req.prerender.prerenderData = req.prerender.tab.prerender.prerenderData;
|
280 | req.prerender.content = req.prerender.tab.prerender.content;
|
281 | req.prerender.headers = req.prerender.tab.prerender.headers;
|
282 |
|
283 | return this.firePluginEvent('pageLoaded', req, res);
|
284 | }).then(() => {
|
285 | this.finish(req, res);
|
286 | }).catch((err) => {
|
287 | if (err) util.log(err);
|
288 | req.prerender.startCatchError = new Date();
|
289 | this.finish(req, res);
|
290 | });
|
291 | };
|
292 |
|
293 |
|
294 |
|
295 | server.finish = function(req, res) {
|
296 | req.prerender.startFinish = new Date();
|
297 |
|
298 | if (req.prerender.tab) {
|
299 | this.browser.closeTab(req.prerender.tab).catch((err) => {
|
300 | util.log('error closing Chrome tab', err);
|
301 | });
|
302 | }
|
303 |
|
304 | if (req.prerender.browserRequestIsInFlight) {
|
305 | req.prerender.responseSent = true;
|
306 | this.browserRequestsInFlight--;
|
307 | }
|
308 |
|
309 | if (this.browserRequestsInFlight <= 0 && new Date().getTime() - this.lastRestart > BROWSER_TRY_RESTART_PERIOD) {
|
310 | this.lastRestart = new Date().getTime();
|
311 | this.restartBrowser();
|
312 | }
|
313 |
|
314 | req.prerender.timeSpentConnectingToBrowser = ((req.prerender.startOpeningTab || req.prerender.startFinish) - req.prerender.startConnectingToBrowser) || 0;
|
315 | req.prerender.timeSpentOpeningTab = ((req.prerender.endOpeningTab || req.prerender.startFinish) - req.prerender.startOpeningTab) || 0;
|
316 | req.prerender.timeSpentLoadingUrl = ((req.prerender.endLoadingUrl || req.prerender.startFinish) - req.prerender.startLoadingUrl) || 0;
|
317 | req.prerender.timeSpentParsingPage = ((req.prerender.endParse || req.prerender.startFinish) - req.prerender.startParse) || 0;
|
318 | req.prerender.timeUntilError = 0;
|
319 |
|
320 | if (req.prerender.startCatchError) {
|
321 | req.prerender.timeUntilError = req.prerender.startCatchError - req.prerender.start;
|
322 | }
|
323 |
|
324 | this.firePluginEvent('beforeSend', req, res)
|
325 | .then(() => {
|
326 | this._send(req, res);
|
327 | }).catch(() => {
|
328 | this._send(req, res);
|
329 | });
|
330 | };
|
331 |
|
332 |
|
333 |
|
334 | server.firePluginEvent = function(methodName, req, res) {
|
335 | return new Promise((resolve, reject) => {
|
336 | let index = 0;
|
337 | let done = false;
|
338 | let next = null;
|
339 | var newRes = {};
|
340 | var args = [req, newRes];
|
341 |
|
342 | newRes.send = function(statusCode, content) {
|
343 | if (statusCode) req.prerender.statusCode = statusCode;
|
344 | if (content) req.prerender.content = content;
|
345 | done = true;
|
346 | reject();
|
347 | };
|
348 |
|
349 | newRes.setHeader = function(key, value) {
|
350 | res.setHeader(key, value);
|
351 | }
|
352 |
|
353 | next = () => {
|
354 | if (done) return;
|
355 |
|
356 | let layer = this.plugins[index++];
|
357 | if (!layer) {
|
358 | return resolve();
|
359 | }
|
360 |
|
361 | let method = layer[methodName];
|
362 |
|
363 | if (method) {
|
364 | try {
|
365 | method.apply(layer, args);
|
366 | } catch (e) {
|
367 | util.log(e);
|
368 | next();
|
369 | }
|
370 | } else {
|
371 | next();
|
372 | }
|
373 | };
|
374 |
|
375 | args.push(next);
|
376 | next();
|
377 | });
|
378 | };
|
379 |
|
380 |
|
381 |
|
382 | server._send = function(req, res) {
|
383 |
|
384 | req.prerender.statusCode = parseInt(req.prerender.statusCode) || 504;
|
385 | let contentTypes = {
|
386 | 'jpeg': 'image/jpeg',
|
387 | 'png': 'image/png',
|
388 | 'pdf': 'application/pdf',
|
389 | 'har': 'application/json'
|
390 | }
|
391 |
|
392 | if (req.prerender.renderType == 'html') {
|
393 | Object.keys(req.prerender.headers || {}).forEach(function(header) {
|
394 | try {
|
395 | res.setHeader(header, req.prerender.headers[header].split('\n'));
|
396 | } catch (e) {
|
397 | util.log('warning: unable to set header:', header);
|
398 | }
|
399 | });
|
400 | }
|
401 |
|
402 | if (req.prerender.prerenderData) {
|
403 | res.setHeader('Content-Type', 'application/json');
|
404 | } else {
|
405 | res.setHeader('Content-Type', contentTypes[req.prerender.renderType] || 'text/html;charset=UTF-8');
|
406 | }
|
407 |
|
408 | if (!req.prerender.prerenderData) {
|
409 |
|
410 | if (req.prerender.content) {
|
411 | if (Buffer.isBuffer(req.prerender.content)) {
|
412 | res.setHeader('Content-Length', req.prerender.content.length);
|
413 | } else if (typeof req.prerender.content === 'string'){
|
414 | res.setHeader('Content-Length', Buffer.byteLength(req.prerender.content, 'utf8'));
|
415 | }
|
416 | }
|
417 | }
|
418 |
|
419 |
|
420 | res.removeHeader('Transfer-Encoding');
|
421 |
|
422 | res.removeHeader('Connection');
|
423 |
|
424 | res.removeHeader('Content-Encoding');
|
425 |
|
426 | res.status(req.prerender.statusCode);
|
427 |
|
428 | if (req.prerender.prerenderData) {
|
429 | res.json({
|
430 | prerenderData: req.prerender.prerenderData,
|
431 | content: req.prerender.content
|
432 | });
|
433 | }
|
434 |
|
435 | if (!req.prerender.prerenderData && req.prerender.content) {
|
436 | res.send(req.prerender.content);
|
437 | }
|
438 |
|
439 | if (!req.prerender.content) {
|
440 | res.end();
|
441 | }
|
442 |
|
443 | var ms = new Date().getTime() - req.prerender.start.getTime();
|
444 | util.log('got', req.prerender.statusCode, 'in', ms + 'ms', 'for', req.prerender.url);
|
445 | }; |
\ | No newline at end of file |