UNPKG

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