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 ENABLE_SERVICE_WORKER = process.env.ENABLE_SERVICE_WORKER || false;
|
16 |
|
17 |
|
18 | const BROWSER_FORCE_RESTART_PERIOD = process.env.BROWSER_FORCE_RESTART_PERIOD || 3600000;
|
19 |
|
20 |
|
21 | const BROWSER_TRY_RESTART_PERIOD = process.env.BROWSER_TRY_RESTART_PERIOD || 600000;
|
22 |
|
23 | const server = exports = module.exports = {};
|
24 |
|
25 |
|
26 |
|
27 | server.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 |
|
45 | server.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 |
|
67 | server.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 |
|
90 | server.spawnBrowser = function() {
|
91 |
|
92 | util.log(`Starting ${this.browser.name}`);
|
93 | return this.browser.spawn(this.options);
|
94 | };
|
95 |
|
96 |
|
97 |
|
98 | server.killBrowser = function() {
|
99 | util.log(`Stopping ${this.browser.name}`);
|
100 | this.isBrowserClosing = true;
|
101 | this.browser.kill();
|
102 | };
|
103 |
|
104 |
|
105 |
|
106 | server.restartBrowser = function() {
|
107 | this.isBrowserConnected = false;
|
108 | util.log(`Restarting ${this.browser.name}`);
|
109 | this.browser.kill();
|
110 | };
|
111 |
|
112 |
|
113 |
|
114 | server.connectToBrowser = function() {
|
115 | return this.browser.connect();
|
116 | };
|
117 |
|
118 |
|
119 |
|
120 | server.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 |
|
145 | server.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 |
|
167 | server.disconnectBrowserIfBrowserShouldBeRestarted = function() {
|
168 |
|
169 |
|
170 |
|
171 | if(this.browserRequestsInFlight > 0 && new Date().getTime() - this.lastRestart > BROWSER_FORCE_RESTART_PERIOD) {
|
172 | this.isBrowserConnected = false;
|
173 | }
|
174 | };
|
175 |
|
176 |
|
177 |
|
178 | server.use = function(plugin) {
|
179 | this.plugins.push(plugin);
|
180 | if (typeof plugin.init === 'function') plugin.init(this);
|
181 | };
|
182 |
|
183 |
|
184 |
|
185 | server.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 |
|
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 |
|
275 | server.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 |
|
302 | server.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 |
|
350 | server._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 |
|
388 | res.removeHeader('Transfer-Encoding');
|
389 |
|
390 | res.removeHeader('Connection');
|
391 |
|
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 |