UNPKG

11.9 kBJavaScriptView Raw
1const util = require('./util.js');
2const zlib = require('zlib');
3const validUrl = require('valid-url');
4
5const WAIT_AFTER_LAST_REQUEST = process.env.WAIT_AFTER_LAST_REQUEST || 500;
6
7const PAGE_DONE_CHECK_INTERVAL = process.env.PAGE_DONE_CHECK_INTERVAL || 500;
8
9const PAGE_LOAD_TIMEOUT = process.env.PAGE_LOAD_TIMEOUT || 20 * 1000;
10
11const FOLLOW_REDIRECTS = process.env.FOLLOW_REDIRECTS || false;
12
13const LOG_REQUESTS = process.env.LOG_REQUESTS || false;
14
15const CAPTURE_CONSOLE_LOG = process.env.CAPTURE_CONSOLE_LOG || false;
16
17const ENABLE_SERVICE_WORKER = process.env.ENABLE_SERVICE_WORKER || false;
18
19//delay incoming requests and force browser to restart before proceeding with requests
20const BROWSER_FORCE_RESTART_PERIOD = process.env.BROWSER_FORCE_RESTART_PERIOD || 3600000;
21
22//try to restart the browser only if there are zero requests in flight
23const BROWSER_TRY_RESTART_PERIOD = process.env.BROWSER_TRY_RESTART_PERIOD || 600000;
24
25const BROWSER_DEBUGGING_PORT = process.env.BROWSER_DEBUGGING_PORT || 9222;
26
27const server = exports = module.exports = {};
28
29
30
31server.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
54server.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
76server.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
99server.spawnBrowser = function() {
100
101 util.log(`Starting ${this.browser.name}`);
102 return this.browser.spawn(this.options);
103};
104
105
106
107server.killBrowser = function() {
108 util.log(`Stopping ${this.browser.name}`);
109 this.isBrowserClosing = true;
110 this.browser.kill();
111};
112
113
114
115server.restartBrowser = function() {
116 this.isBrowserConnected = false;
117 util.log(`Restarting ${this.browser.name}`);
118 this.browser.kill();
119};
120
121
122
123server.connectToBrowser = function() {
124 return this.browser.connect();
125};
126
127
128
129server.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
154server.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
176server.disconnectBrowserIfBrowserShouldBeRestarted = function() {
177 //force a browser restart every hour
178 //this lets any current browser requests finish while preventing new tabs from being created
179 //this causes new requests to wait for the browser to restart before opening a new tab
180 if(this.browserRequestsInFlight > 0 && new Date().getTime() - this.lastRestart > BROWSER_FORCE_RESTART_PERIOD) {
181 this.isBrowserConnected = false;
182 }
183};
184
185
186
187server.use = function(plugin) {
188 this.plugins.push(plugin);
189 if (typeof plugin.init === 'function') plugin.init(this);
190};
191
192
193
194server.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 //if there is a case where a page hangs, this will at least let us restart chrome
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
295server.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
334server.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
382server._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 //if the original server had a chunked encoding, we should remove it since we aren't sending a chunked response
420 res.removeHeader('Transfer-Encoding');
421 //if the original server wanted to keep the connection alive, let's close it
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