| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Copyright 2017 Google Inc. All rights reserved. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Licensed under the Apache License, Version 2.0 (the "License"); | 
					
						
							|  |  |  |  * you may not use this file except in compliance with the License. | 
					
						
							|  |  |  |  * You may obtain a copy of the License at | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  *     http://www.apache.org/licenses/LICENSE-2.0
 | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  |  * distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  |  * See the License for the specific language governing permissions and | 
					
						
							|  |  |  |  * limitations under the License. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const http = require('http'); | 
					
						
							|  |  |  | const https = require('https'); | 
					
						
							|  |  |  | const url = require('url'); | 
					
						
							|  |  |  | const fs = require('fs'); | 
					
						
							|  |  |  | const path = require('path'); | 
					
						
							| 
									
										
										
										
											2020-12-10 09:47:06 -08:00
										 |  |  | const zlib = require('zlib'); | 
					
						
							|  |  |  | const util = require('util'); | 
					
						
							| 
									
										
										
										
											2021-08-25 11:18:35 -07:00
										 |  |  | const mime = require('mime'); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  | const WebSocketServer = require('ws').Server; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const fulfillSymbol = Symbol('fullfil callback'); | 
					
						
							|  |  |  | const rejectSymbol = Symbol('reject callback'); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-10 09:47:06 -08:00
										 |  |  | const gzipAsync = util.promisify(zlib.gzip.bind(zlib)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  | class TestServer { | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} dirPath | 
					
						
							|  |  |  |    * @param {number} port | 
					
						
							| 
									
										
										
										
											2021-04-09 07:59:09 -07:00
										 |  |  |    * @param {string=} loopback | 
					
						
							| 
									
										
										
										
											2020-02-10 13:20:13 -08:00
										 |  |  |    * @return {!Promise<TestServer>} | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-04-09 07:59:09 -07:00
										 |  |  |   static async create(dirPath, port, loopback) { | 
					
						
							|  |  |  |     const server = new TestServer(dirPath, port, loopback); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     await new Promise(x => server._server.once('listening', x)); | 
					
						
							|  |  |  |     return server; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} dirPath | 
					
						
							|  |  |  |    * @param {number} port | 
					
						
							| 
									
										
										
										
											2021-04-09 07:59:09 -07:00
										 |  |  |    * @param {string=} loopback | 
					
						
							| 
									
										
										
										
											2020-02-10 13:20:13 -08:00
										 |  |  |    * @return {!Promise<TestServer>} | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-04-09 07:59:09 -07:00
										 |  |  |   static async createHTTPS(dirPath, port, loopback) { | 
					
						
							|  |  |  |     const server = new TestServer(dirPath, port, loopback, { | 
					
						
							| 
									
										
										
										
											2021-06-03 09:55:33 -07:00
										 |  |  |       key: await fs.promises.readFile(path.join(__dirname, 'key.pem')), | 
					
						
							|  |  |  |       cert: await fs.promises.readFile(path.join(__dirname, 'cert.pem')), | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |       passphrase: 'aaaa', | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     await new Promise(x => server._server.once('listening', x)); | 
					
						
							|  |  |  |     return server; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} dirPath | 
					
						
							|  |  |  |    * @param {number} port | 
					
						
							| 
									
										
										
										
											2021-04-09 07:59:09 -07:00
										 |  |  |    * @param {string=} loopback | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |    * @param {!Object=} sslOptions | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-04-09 07:59:09 -07:00
										 |  |  |   constructor(dirPath, port, loopback, sslOptions) { | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     if (sslOptions) | 
					
						
							|  |  |  |       this._server = https.createServer(sslOptions, this._onRequest.bind(this)); | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       this._server = http.createServer(this._onRequest.bind(this)); | 
					
						
							|  |  |  |     this._server.on('connection', socket => this._onSocket(socket)); | 
					
						
							| 
									
										
										
										
											2021-09-22 17:08:49 -07:00
										 |  |  |     this._wsServer = new WebSocketServer({ noServer: true }); | 
					
						
							|  |  |  |     this._server.on('upgrade', async (request, socket, head) => { | 
					
						
							| 
									
										
										
										
											2021-06-28 21:23:29 +02:00
										 |  |  |       const pathname = url.parse(request.url).pathname; | 
					
						
							| 
									
										
										
										
											2021-09-22 17:08:49 -07:00
										 |  |  |       if (pathname === '/ws-slow') | 
					
						
							|  |  |  |         await new Promise(f => setTimeout(f, 2000)); | 
					
						
							| 
									
										
										
										
											2021-12-22 11:17:34 -08:00
										 |  |  |       if (!['/ws', '/ws-slow'].includes(pathname)) { | 
					
						
							| 
									
										
										
										
											2021-09-22 17:08:49 -07:00
										 |  |  |         socket.write('HTTP/1.1 400 Bad Request\r\n\r\n'); | 
					
						
							|  |  |  |         socket.destroy(); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       this._wsServer.handleUpgrade(request, socket, head, ws => { | 
					
						
							|  |  |  |         // Next emit is only for our internal 'connection' listeners.
 | 
					
						
							|  |  |  |         this._wsServer.emit('connection', ws, request); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2021-05-26 15:18:52 -07:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     this._server.listen(port); | 
					
						
							|  |  |  |     this._dirPath = dirPath; | 
					
						
							| 
									
										
										
										
											2020-03-24 09:11:56 -07:00
										 |  |  |     this.debugServer = require('debug')('pw:server'); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     this._startTime = new Date(); | 
					
						
							|  |  |  |     this._cachedPathPrefix = null; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-10 13:20:13 -08:00
										 |  |  |     /** @type {!Set<!NodeJS.Socket>} */ | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     this._sockets = new Set(); | 
					
						
							| 
									
										
										
										
											2020-02-10 18:35:47 -08:00
										 |  |  |     /** @type {!Map<string, function(!http.IncomingMessage,http.ServerResponse)>} */ | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     this._routes = new Map(); | 
					
						
							|  |  |  |     /** @type {!Map<string, !{username:string, password:string}>} */ | 
					
						
							|  |  |  |     this._auths = new Map(); | 
					
						
							|  |  |  |     /** @type {!Map<string, string>} */ | 
					
						
							|  |  |  |     this._csp = new Map(); | 
					
						
							| 
									
										
										
										
											2021-08-30 09:58:44 -07:00
										 |  |  |     /** @type {!Map<string, Object>} */ | 
					
						
							|  |  |  |     this._extraHeaders = new Map(); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     /** @type {!Set<string>} */ | 
					
						
							|  |  |  |     this._gzipRoutes = new Set(); | 
					
						
							|  |  |  |     /** @type {!Map<string, !Promise>} */ | 
					
						
							|  |  |  |     this._requestSubscribers = new Map(); | 
					
						
							| 
									
										
										
										
											2020-08-10 16:48:34 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-09 07:59:09 -07:00
										 |  |  |     const cross_origin = loopback || '127.0.0.1'; | 
					
						
							|  |  |  |     const same_origin = loopback || 'localhost'; | 
					
						
							| 
									
										
										
										
											2020-08-10 16:48:34 -07:00
										 |  |  |     const protocol = sslOptions ? 'https' : 'http'; | 
					
						
							|  |  |  |     this.PORT = port; | 
					
						
							| 
									
										
										
										
											2021-04-09 07:59:09 -07:00
										 |  |  |     this.PREFIX = `${protocol}://${same_origin}:${port}`; | 
					
						
							|  |  |  |     this.CROSS_PROCESS_PREFIX = `${protocol}://${cross_origin}:${port}`; | 
					
						
							|  |  |  |     this.EMPTY_PAGE = `${protocol}://${same_origin}:${port}/empty.html`; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   _onSocket(socket) { | 
					
						
							|  |  |  |     this._sockets.add(socket); | 
					
						
							| 
									
										
										
										
											2020-11-04 10:24:01 -08:00
										 |  |  |     // ECONNRESET and HPE_INVALID_EOF_STATE are legit errors given
 | 
					
						
							|  |  |  |     // that tab closing aborts outgoing connections to the server.
 | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     socket.on('error', error => { | 
					
						
							| 
									
										
										
										
											2020-11-04 10:24:01 -08:00
										 |  |  |       if (error.code !== 'ECONNRESET' && error.code !== 'HPE_INVALID_EOF_STATE') | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |         throw error; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     socket.once('close', () => this._sockets.delete(socket)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} pathPrefix | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   enableHTTPCache(pathPrefix) { | 
					
						
							|  |  |  |     this._cachedPathPrefix = pathPrefix; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} path | 
					
						
							|  |  |  |    * @param {string} username | 
					
						
							|  |  |  |    * @param {string} password | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   setAuth(path, username, password) { | 
					
						
							| 
									
										
										
										
											2020-03-24 09:11:56 -07:00
										 |  |  |     this.debugServer(`set auth for ${path} to ${username}:${password}`); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     this._auths.set(path, {username, password}); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   enableGzip(path) { | 
					
						
							|  |  |  |     this._gzipRoutes.add(path); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} path | 
					
						
							|  |  |  |    * @param {string} csp | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   setCSP(path, csp) { | 
					
						
							|  |  |  |     this._csp.set(path, csp); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-30 09:58:44 -07:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} path | 
					
						
							|  |  |  |    * @param {Object<string, string>} object | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   setExtraHeaders(path, object) { | 
					
						
							|  |  |  |     this._extraHeaders.set(path, object); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   async stop() { | 
					
						
							|  |  |  |     this.reset(); | 
					
						
							|  |  |  |     for (const socket of this._sockets) | 
					
						
							|  |  |  |       socket.destroy(); | 
					
						
							|  |  |  |     this._sockets.clear(); | 
					
						
							|  |  |  |     await new Promise(x => this._server.close(x)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} path | 
					
						
							| 
									
										
										
										
											2020-02-10 18:35:47 -08:00
										 |  |  |    * @param {function(!http.IncomingMessage,http.ServerResponse)} handler | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |    */ | 
					
						
							|  |  |  |   setRoute(path, handler) { | 
					
						
							|  |  |  |     this._routes.set(path, handler); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} from | 
					
						
							|  |  |  |    * @param {string} to | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   setRedirect(from, to) { | 
					
						
							|  |  |  |     this.setRoute(from, (req, res) => { | 
					
						
							| 
									
										
										
										
											2021-08-30 09:58:44 -07:00
										 |  |  |       let headers = this._extraHeaders.get(req.url) || {}; | 
					
						
							|  |  |  |       res.writeHead(302, { ...headers, location: to }); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |       res.end(); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} path | 
					
						
							| 
									
										
										
										
											2020-02-10 18:35:47 -08:00
										 |  |  |    * @return {!Promise<!http.IncomingMessage>} | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |    */ | 
					
						
							|  |  |  |   waitForRequest(path) { | 
					
						
							|  |  |  |     let promise = this._requestSubscribers.get(path); | 
					
						
							|  |  |  |     if (promise) | 
					
						
							|  |  |  |       return promise; | 
					
						
							|  |  |  |     let fulfill, reject; | 
					
						
							|  |  |  |     promise = new Promise((f, r) => { | 
					
						
							|  |  |  |       fulfill = f; | 
					
						
							|  |  |  |       reject = r; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     promise[fulfillSymbol] = fulfill; | 
					
						
							|  |  |  |     promise[rejectSymbol] = reject; | 
					
						
							|  |  |  |     this._requestSubscribers.set(path, promise); | 
					
						
							|  |  |  |     return promise; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   reset() { | 
					
						
							|  |  |  |     this._routes.clear(); | 
					
						
							|  |  |  |     this._auths.clear(); | 
					
						
							|  |  |  |     this._csp.clear(); | 
					
						
							| 
									
										
										
										
											2021-08-30 09:58:44 -07:00
										 |  |  |     this._extraHeaders.clear(); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     this._gzipRoutes.clear(); | 
					
						
							|  |  |  |     const error = new Error('Static Server has been reset'); | 
					
						
							|  |  |  |     for (const subscriber of this._requestSubscribers.values()) | 
					
						
							|  |  |  |       subscriber[rejectSymbol].call(null, error); | 
					
						
							|  |  |  |     this._requestSubscribers.clear(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-10 18:35:47 -08:00
										 |  |  |   /** | 
					
						
							| 
									
										
										
										
											2020-07-28 18:47:25 -07:00
										 |  |  |    * @param {http.IncomingMessage} request | 
					
						
							|  |  |  |    * @param {http.ServerResponse} response | 
					
						
							| 
									
										
										
										
											2020-02-10 18:35:47 -08:00
										 |  |  |    */ | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   _onRequest(request, response) { | 
					
						
							|  |  |  |     request.on('error', error => { | 
					
						
							|  |  |  |       if (error.code === 'ECONNRESET') | 
					
						
							|  |  |  |         response.end(); | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         throw error; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     request.postBody = new Promise(resolve => { | 
					
						
							| 
									
										
										
										
											2020-07-24 12:16:45 -07:00
										 |  |  |       let body = Buffer.from([]); | 
					
						
							|  |  |  |       request.on('data', chunk => body = Buffer.concat([body, chunk])); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |       request.on('end', () => resolve(body)); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2021-09-13 15:38:27 -07:00
										 |  |  |     const path = url.parse(request.url).path; | 
					
						
							|  |  |  |     this.debugServer(`request ${request.method} ${path}`); | 
					
						
							|  |  |  |     if (this._auths.has(path)) { | 
					
						
							|  |  |  |       const auth = this._auths.get(path); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |       const credentials = Buffer.from((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString(); | 
					
						
							| 
									
										
										
										
											2020-03-24 09:11:56 -07:00
										 |  |  |       this.debugServer(`request credentials ${credentials}`); | 
					
						
							|  |  |  |       this.debugServer(`actual credentials ${auth.username}:${auth.password}`); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |       if (credentials !== `${auth.username}:${auth.password}`) { | 
					
						
							| 
									
										
										
										
											2020-03-24 09:11:56 -07:00
										 |  |  |         this.debugServer(`request write www-auth`); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |         response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Secure Area"' }); | 
					
						
							|  |  |  |         response.end('HTTP Error 401 Unauthorized: Access is denied'); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     // Notify request subscriber.
 | 
					
						
							| 
									
										
										
										
											2021-09-13 15:38:27 -07:00
										 |  |  |     if (this._requestSubscribers.has(path)) { | 
					
						
							|  |  |  |       this._requestSubscribers.get(path)[fulfillSymbol].call(null, request); | 
					
						
							|  |  |  |       this._requestSubscribers.delete(path); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-09-13 15:38:27 -07:00
										 |  |  |     const handler = this._routes.get(path); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     if (handler) { | 
					
						
							|  |  |  |       handler.call(null, request, response); | 
					
						
							|  |  |  |     } else { | 
					
						
							| 
									
										
										
										
											2020-08-31 08:43:14 -07:00
										 |  |  |       this.serveFile(request, response); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							| 
									
										
										
										
											2020-02-10 18:35:47 -08:00
										 |  |  |    * @param {!http.IncomingMessage} request | 
					
						
							|  |  |  |    * @param {!http.ServerResponse} response | 
					
						
							| 
									
										
										
										
											2020-08-31 08:43:14 -07:00
										 |  |  |    * @param {string|undefined} filePath | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |    */ | 
					
						
							| 
									
										
										
										
											2020-12-10 09:47:06 -08:00
										 |  |  |   async serveFile(request, response, filePath) { | 
					
						
							| 
									
										
										
										
											2020-08-31 08:43:14 -07:00
										 |  |  |     let pathName = url.parse(request.url).path; | 
					
						
							|  |  |  |     if (!filePath) { | 
					
						
							|  |  |  |       if (pathName === '/') | 
					
						
							|  |  |  |         pathName = '/index.html'; | 
					
						
							|  |  |  |       filePath = path.join(this._dirPath, pathName.substring(1)); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (this._cachedPathPrefix !== null && filePath.startsWith(this._cachedPathPrefix)) { | 
					
						
							|  |  |  |       if (request.headers['if-modified-since']) { | 
					
						
							|  |  |  |         response.statusCode = 304; // not modified
 | 
					
						
							|  |  |  |         response.end(); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2020-01-03 15:34:51 -08:00
										 |  |  |       response.setHeader('Cache-Control', 'public, max-age=31536000, no-cache'); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |       response.setHeader('Last-Modified', this._startTime.toISOString()); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       response.setHeader('Cache-Control', 'no-cache, no-store'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (this._csp.has(pathName)) | 
					
						
							|  |  |  |       response.setHeader('Content-Security-Policy', this._csp.get(pathName)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-30 09:58:44 -07:00
										 |  |  |     if (this._extraHeaders.has(pathName)) { | 
					
						
							|  |  |  |       const object = this._extraHeaders.get(pathName); | 
					
						
							|  |  |  |       for (const key in object) | 
					
						
							|  |  |  |         response.setHeader(key, object[key]); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-03 09:55:33 -07:00
										 |  |  |     const {err, data} = await fs.promises.readFile(filePath).then(data => ({data})).catch(err => ({err})); | 
					
						
							| 
									
										
										
										
											2020-12-10 09:47:06 -08:00
										 |  |  |     // The HTTP transaction might be already terminated after async hop here - do nothing in this case.
 | 
					
						
							|  |  |  |     if (response.writableEnded) | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     if (err) { | 
					
						
							|  |  |  |       response.statusCode = 404; | 
					
						
							|  |  |  |       response.end(`File not found: ${filePath}`); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const extension = filePath.substring(filePath.lastIndexOf('.') + 1); | 
					
						
							| 
									
										
										
										
											2021-08-25 11:18:35 -07:00
										 |  |  |     const mimeType = mime.getType(extension) || 'application/octet-stream'; | 
					
						
							| 
									
										
										
										
											2020-12-10 09:47:06 -08:00
										 |  |  |     const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(mimeType); | 
					
						
							|  |  |  |     const contentType = isTextEncoding ? `${mimeType}; charset=utf-8` : mimeType; | 
					
						
							|  |  |  |     response.setHeader('Content-Type', contentType); | 
					
						
							|  |  |  |     if (this._gzipRoutes.has(pathName)) { | 
					
						
							|  |  |  |       response.setHeader('Content-Encoding', 'gzip'); | 
					
						
							|  |  |  |       const result = await gzipAsync(data); | 
					
						
							|  |  |  |       // The HTTP transaction might be already terminated after async hop here.
 | 
					
						
							|  |  |  |       if (!response.writableEnded) | 
					
						
							|  |  |  |         response.end(result); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       response.end(data); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-03 17:52:22 -07:00
										 |  |  |   onceWebSocketConnection(handler) { | 
					
						
							|  |  |  |     this._wsServer.once('connection', handler); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-23 14:52:27 -07:00
										 |  |  |   waitForWebSocketConnectionRequest() { | 
					
						
							|  |  |  |     return new Promise(fullfil => { | 
					
						
							|  |  |  |       this._wsServer.once('connection', (ws, req) => fullfil(req)); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-26 15:18:52 -07:00
										 |  |  |   sendOnWebSocketConnection(data) { | 
					
						
							| 
									
										
										
										
											2021-12-22 11:17:34 -08:00
										 |  |  |     this.onceWebSocketConnection(ws => ws.send(data)); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports = {TestServer}; |