| 
									
										
										
										
											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'); | 
					
						
							|  |  |  | const WebSocketServer = require('ws').Server; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const fulfillSymbol = Symbol('fullfil callback'); | 
					
						
							|  |  |  | const rejectSymbol = Symbol('reject callback'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TestServer { | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} dirPath | 
					
						
							|  |  |  |    * @param {number} port | 
					
						
							| 
									
										
										
										
											2020-02-10 13:20:13 -08:00
										 |  |  |    * @return {!Promise<TestServer>} | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |    */ | 
					
						
							|  |  |  |   static async create(dirPath, port) { | 
					
						
							|  |  |  |     const server = new TestServer(dirPath, port); | 
					
						
							|  |  |  |     await new Promise(x => server._server.once('listening', x)); | 
					
						
							|  |  |  |     return server; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} dirPath | 
					
						
							|  |  |  |    * @param {number} port | 
					
						
							| 
									
										
										
										
											2020-02-10 13:20:13 -08:00
										 |  |  |    * @return {!Promise<TestServer>} | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |    */ | 
					
						
							|  |  |  |   static async createHTTPS(dirPath, port) { | 
					
						
							|  |  |  |     const server = new TestServer(dirPath, port, { | 
					
						
							|  |  |  |       key: fs.readFileSync(path.join(__dirname, 'key.pem')), | 
					
						
							|  |  |  |       cert: fs.readFileSync(path.join(__dirname, 'cert.pem')), | 
					
						
							|  |  |  |       passphrase: 'aaaa', | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     await new Promise(x => server._server.once('listening', x)); | 
					
						
							|  |  |  |     return server; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} dirPath | 
					
						
							|  |  |  |    * @param {number} port | 
					
						
							|  |  |  |    * @param {!Object=} sslOptions | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   constructor(dirPath, port, sslOptions) { | 
					
						
							|  |  |  |     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)); | 
					
						
							| 
									
										
										
										
											2020-01-21 11:48:48 -08:00
										 |  |  |     this._wsServer = new WebSocketServer({server: this._server, path: '/ws'}); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     this._wsServer.on('connection', this._onWebSocketConnection.bind(this)); | 
					
						
							|  |  |  |     this._server.listen(port); | 
					
						
							|  |  |  |     this._dirPath = dirPath; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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(); | 
					
						
							|  |  |  |     /** @type {!Set<string>} */ | 
					
						
							|  |  |  |     this._gzipRoutes = new Set(); | 
					
						
							|  |  |  |     /** @type {!Map<string, !Promise>} */ | 
					
						
							|  |  |  |     this._requestSubscribers = new Map(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   _onSocket(socket) { | 
					
						
							|  |  |  |     this._sockets.add(socket); | 
					
						
							|  |  |  |     // ECONNRESET is a legit error given
 | 
					
						
							|  |  |  |     // that tab closing simply kills process.
 | 
					
						
							|  |  |  |     socket.on('error', error => { | 
					
						
							|  |  |  |       if (error.code !== 'ECONNRESET') | 
					
						
							|  |  |  |         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) { | 
					
						
							|  |  |  |     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); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   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) => { | 
					
						
							|  |  |  |       res.writeHead(302, { location: to }); | 
					
						
							|  |  |  |       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(); | 
					
						
							|  |  |  |     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
										 |  |  |   /** | 
					
						
							|  |  |  |    * @param {http.IncomingMessage} request  | 
					
						
							|  |  |  |    * @param {http.ServerResponse} response  | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											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 => { | 
					
						
							|  |  |  |       let body = ''; | 
					
						
							|  |  |  |       request.on('data', chunk => body += chunk); | 
					
						
							|  |  |  |       request.on('end', () => resolve(body)); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     const pathName = url.parse(request.url).path; | 
					
						
							|  |  |  |     if (this._auths.has(pathName)) { | 
					
						
							|  |  |  |       const auth = this._auths.get(pathName); | 
					
						
							|  |  |  |       const credentials = Buffer.from((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString(); | 
					
						
							|  |  |  |       if (credentials !== `${auth.username}:${auth.password}`) { | 
					
						
							|  |  |  |         response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Secure Area"' }); | 
					
						
							|  |  |  |         response.end('HTTP Error 401 Unauthorized: Access is denied'); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     // Notify request subscriber.
 | 
					
						
							|  |  |  |     if (this._requestSubscribers.has(pathName)) { | 
					
						
							|  |  |  |       this._requestSubscribers.get(pathName)[fulfillSymbol].call(null, request); | 
					
						
							|  |  |  |       this._requestSubscribers.delete(pathName); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const handler = this._routes.get(pathName); | 
					
						
							|  |  |  |     if (handler) { | 
					
						
							|  |  |  |       handler.call(null, request, response); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       const pathName = url.parse(request.url).path; | 
					
						
							|  |  |  |       this.serveFile(request, response, pathName); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							| 
									
										
										
										
											2020-02-10 18:35:47 -08:00
										 |  |  |    * @param {!http.IncomingMessage} request | 
					
						
							|  |  |  |    * @param {!http.ServerResponse} response | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |    * @param {string} pathName | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   serveFile(request, response, pathName) { | 
					
						
							|  |  |  |     if (pathName === '/') | 
					
						
							|  |  |  |       pathName = '/index.html'; | 
					
						
							|  |  |  |     const filePath = path.join(this._dirPath, pathName.substring(1)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     fs.readFile(filePath, (err, data) => { | 
					
						
							|  |  |  |       if (err) { | 
					
						
							|  |  |  |         response.statusCode = 404; | 
					
						
							|  |  |  |         response.end(`File not found: ${filePath}`); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2020-02-10 11:18:03 -08:00
										 |  |  |       const extension = filePath.substring(filePath.lastIndexOf('.') + 1); | 
					
						
							|  |  |  |       const mimeType = extensionToMime[extension] || 'application/octet-stream'; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -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 zlib = require('zlib'); | 
					
						
							|  |  |  |         zlib.gzip(data, (_, result) => { | 
					
						
							|  |  |  |           response.end(result); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         response.end(data); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-21 11:48:48 -08:00
										 |  |  |   _onWebSocketConnection(ws) { | 
					
						
							|  |  |  |     ws.send('incoming'); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-10 11:18:03 -08:00
										 |  |  | const extensionToMime = { | 
					
						
							|  |  |  |   'ai': 'application/postscript', | 
					
						
							|  |  |  |   'apng': 'image/apng', | 
					
						
							|  |  |  |   'appcache': 'text/cache-manifest', | 
					
						
							|  |  |  |   'au': 'audio/basic', | 
					
						
							|  |  |  |   'bmp': 'image/bmp', | 
					
						
							|  |  |  |   'cer': 'application/pkix-cert', | 
					
						
							|  |  |  |   'cgm': 'image/cgm', | 
					
						
							|  |  |  |   'coffee': 'text/coffeescript', | 
					
						
							|  |  |  |   'conf': 'text/plain', | 
					
						
							|  |  |  |   'crl': 'application/pkix-crl', | 
					
						
							|  |  |  |   'css': 'text/css', | 
					
						
							|  |  |  |   'csv': 'text/csv', | 
					
						
							|  |  |  |   'def': 'text/plain', | 
					
						
							|  |  |  |   'doc': 'application/msword', | 
					
						
							|  |  |  |   'dot': 'application/msword', | 
					
						
							|  |  |  |   'drle': 'image/dicom-rle', | 
					
						
							|  |  |  |   'dtd': 'application/xml-dtd', | 
					
						
							|  |  |  |   'ear': 'application/java-archive', | 
					
						
							|  |  |  |   'emf': 'image/emf', | 
					
						
							|  |  |  |   'eps': 'application/postscript', | 
					
						
							|  |  |  |   'exr': 'image/aces', | 
					
						
							|  |  |  |   'fits': 'image/fits', | 
					
						
							|  |  |  |   'g3': 'image/g3fax', | 
					
						
							|  |  |  |   'gbr': 'application/rpki-ghostbusters', | 
					
						
							|  |  |  |   'gif': 'image/gif', | 
					
						
							|  |  |  |   'glb': 'model/gltf-binary', | 
					
						
							|  |  |  |   'gltf': 'model/gltf+json', | 
					
						
							|  |  |  |   'gz': 'application/gzip', | 
					
						
							|  |  |  |   'h261': 'video/h261', | 
					
						
							|  |  |  |   'h263': 'video/h263', | 
					
						
							|  |  |  |   'h264': 'video/h264', | 
					
						
							|  |  |  |   'heic': 'image/heic', | 
					
						
							|  |  |  |   'heics': 'image/heic-sequence', | 
					
						
							|  |  |  |   'heif': 'image/heif', | 
					
						
							|  |  |  |   'heifs': 'image/heif-sequence', | 
					
						
							|  |  |  |   'htm': 'text/html', | 
					
						
							|  |  |  |   'html': 'text/html', | 
					
						
							|  |  |  |   'ics': 'text/calendar', | 
					
						
							|  |  |  |   'ief': 'image/ief', | 
					
						
							|  |  |  |   'ifb': 'text/calendar', | 
					
						
							|  |  |  |   'iges': 'model/iges', | 
					
						
							|  |  |  |   'igs': 'model/iges', | 
					
						
							|  |  |  |   'in': 'text/plain', | 
					
						
							|  |  |  |   'ini': 'text/plain', | 
					
						
							|  |  |  |   'jade': 'text/jade', | 
					
						
							|  |  |  |   'jar': 'application/java-archive', | 
					
						
							|  |  |  |   'jls': 'image/jls', | 
					
						
							|  |  |  |   'jp2': 'image/jp2', | 
					
						
							|  |  |  |   'jpe': 'image/jpeg', | 
					
						
							|  |  |  |   'jpeg': 'image/jpeg', | 
					
						
							|  |  |  |   'jpf': 'image/jpx', | 
					
						
							|  |  |  |   'jpg': 'image/jpeg', | 
					
						
							|  |  |  |   'jpg2': 'image/jp2', | 
					
						
							|  |  |  |   'jpgm': 'video/jpm', | 
					
						
							|  |  |  |   'jpgv': 'video/jpeg', | 
					
						
							|  |  |  |   'jpm': 'image/jpm', | 
					
						
							|  |  |  |   'jpx': 'image/jpx', | 
					
						
							|  |  |  |   'js': 'application/javascript', | 
					
						
							|  |  |  |   'json': 'application/json', | 
					
						
							|  |  |  |   'json5': 'application/json5', | 
					
						
							|  |  |  |   'jsx': 'text/jsx', | 
					
						
							|  |  |  |   'jxr': 'image/jxr', | 
					
						
							|  |  |  |   'kar': 'audio/midi', | 
					
						
							|  |  |  |   'ktx': 'image/ktx', | 
					
						
							|  |  |  |   'less': 'text/less', | 
					
						
							|  |  |  |   'list': 'text/plain', | 
					
						
							|  |  |  |   'litcoffee': 'text/coffeescript', | 
					
						
							|  |  |  |   'log': 'text/plain', | 
					
						
							|  |  |  |   'm1v': 'video/mpeg', | 
					
						
							|  |  |  |   'm21': 'application/mp21', | 
					
						
							|  |  |  |   'm2a': 'audio/mpeg', | 
					
						
							|  |  |  |   'm2v': 'video/mpeg', | 
					
						
							|  |  |  |   'm3a': 'audio/mpeg', | 
					
						
							|  |  |  |   'm4a': 'audio/mp4', | 
					
						
							|  |  |  |   'm4p': 'application/mp4', | 
					
						
							|  |  |  |   'man': 'text/troff', | 
					
						
							|  |  |  |   'manifest': 'text/cache-manifest', | 
					
						
							|  |  |  |   'markdown': 'text/markdown', | 
					
						
							|  |  |  |   'mathml': 'application/mathml+xml', | 
					
						
							|  |  |  |   'md': 'text/markdown', | 
					
						
							|  |  |  |   'mdx': 'text/mdx', | 
					
						
							|  |  |  |   'me': 'text/troff', | 
					
						
							|  |  |  |   'mesh': 'model/mesh', | 
					
						
							|  |  |  |   'mft': 'application/rpki-manifest', | 
					
						
							|  |  |  |   'mid': 'audio/midi', | 
					
						
							|  |  |  |   'midi': 'audio/midi', | 
					
						
							|  |  |  |   'mj2': 'video/mj2', | 
					
						
							|  |  |  |   'mjp2': 'video/mj2', | 
					
						
							|  |  |  |   'mjs': 'application/javascript', | 
					
						
							|  |  |  |   'mml': 'text/mathml', | 
					
						
							|  |  |  |   'mov': 'video/quicktime', | 
					
						
							|  |  |  |   'mp2': 'audio/mpeg', | 
					
						
							|  |  |  |   'mp21': 'application/mp21', | 
					
						
							|  |  |  |   'mp2a': 'audio/mpeg', | 
					
						
							|  |  |  |   'mp3': 'audio/mpeg', | 
					
						
							|  |  |  |   'mp4': 'video/mp4', | 
					
						
							|  |  |  |   'mp4a': 'audio/mp4', | 
					
						
							|  |  |  |   'mp4s': 'application/mp4', | 
					
						
							|  |  |  |   'mp4v': 'video/mp4', | 
					
						
							|  |  |  |   'mpe': 'video/mpeg', | 
					
						
							|  |  |  |   'mpeg': 'video/mpeg', | 
					
						
							|  |  |  |   'mpg': 'video/mpeg', | 
					
						
							|  |  |  |   'mpg4': 'video/mp4', | 
					
						
							|  |  |  |   'mpga': 'audio/mpeg', | 
					
						
							|  |  |  |   'mrc': 'application/marc', | 
					
						
							|  |  |  |   'ms': 'text/troff', | 
					
						
							|  |  |  |   'msh': 'model/mesh', | 
					
						
							|  |  |  |   'n3': 'text/n3', | 
					
						
							|  |  |  |   'oga': 'audio/ogg', | 
					
						
							|  |  |  |   'ogg': 'audio/ogg', | 
					
						
							|  |  |  |   'ogv': 'video/ogg', | 
					
						
							|  |  |  |   'ogx': 'application/ogg', | 
					
						
							|  |  |  |   'otf': 'font/otf', | 
					
						
							|  |  |  |   'p10': 'application/pkcs10', | 
					
						
							|  |  |  |   'p7c': 'application/pkcs7-mime', | 
					
						
							|  |  |  |   'p7m': 'application/pkcs7-mime', | 
					
						
							|  |  |  |   'p7s': 'application/pkcs7-signature', | 
					
						
							|  |  |  |   'p8': 'application/pkcs8', | 
					
						
							|  |  |  |   'pdf': 'application/pdf', | 
					
						
							|  |  |  |   'pki': 'application/pkixcmp', | 
					
						
							|  |  |  |   'pkipath': 'application/pkix-pkipath', | 
					
						
							|  |  |  |   'png': 'image/png', | 
					
						
							|  |  |  |   'ps': 'application/postscript', | 
					
						
							|  |  |  |   'pskcxml': 'application/pskc+xml', | 
					
						
							|  |  |  |   'qt': 'video/quicktime', | 
					
						
							|  |  |  |   'rmi': 'audio/midi', | 
					
						
							|  |  |  |   'rng': 'application/xml', | 
					
						
							|  |  |  |   'roa': 'application/rpki-roa', | 
					
						
							|  |  |  |   'roff': 'text/troff', | 
					
						
							|  |  |  |   'rsd': 'application/rsd+xml', | 
					
						
							|  |  |  |   'rss': 'application/rss+xml', | 
					
						
							|  |  |  |   'rtf': 'application/rtf', | 
					
						
							|  |  |  |   'rtx': 'text/richtext', | 
					
						
							|  |  |  |   's3m': 'audio/s3m', | 
					
						
							|  |  |  |   'sgi': 'image/sgi', | 
					
						
							|  |  |  |   'sgm': 'text/sgml', | 
					
						
							|  |  |  |   'sgml': 'text/sgml', | 
					
						
							|  |  |  |   'shex': 'text/shex', | 
					
						
							|  |  |  |   'shtml': 'text/html', | 
					
						
							|  |  |  |   'sil': 'audio/silk', | 
					
						
							|  |  |  |   'silo': 'model/mesh', | 
					
						
							|  |  |  |   'slim': 'text/slim', | 
					
						
							|  |  |  |   'slm': 'text/slim', | 
					
						
							|  |  |  |   'snd': 'audio/basic', | 
					
						
							|  |  |  |   'spx': 'audio/ogg', | 
					
						
							|  |  |  |   'stl': 'model/stl', | 
					
						
							|  |  |  |   'styl': 'text/stylus', | 
					
						
							|  |  |  |   'stylus': 'text/stylus', | 
					
						
							|  |  |  |   'svg': 'image/svg+xml', | 
					
						
							|  |  |  |   'svgz': 'image/svg+xml', | 
					
						
							|  |  |  |   't': 'text/troff', | 
					
						
							|  |  |  |   't38': 'image/t38', | 
					
						
							|  |  |  |   'text': 'text/plain', | 
					
						
							|  |  |  |   'tfx': 'image/tiff-fx', | 
					
						
							|  |  |  |   'tif': 'image/tiff', | 
					
						
							|  |  |  |   'tiff': 'image/tiff', | 
					
						
							|  |  |  |   'tr': 'text/troff', | 
					
						
							|  |  |  |   'ts': 'video/mp2t', | 
					
						
							|  |  |  |   'tsv': 'text/tab-separated-values', | 
					
						
							|  |  |  |   'ttc': 'font/collection', | 
					
						
							|  |  |  |   'ttf': 'font/ttf', | 
					
						
							|  |  |  |   'ttl': 'text/turtle', | 
					
						
							|  |  |  |   'txt': 'text/plain', | 
					
						
							|  |  |  |   'uri': 'text/uri-list', | 
					
						
							|  |  |  |   'uris': 'text/uri-list', | 
					
						
							|  |  |  |   'urls': 'text/uri-list', | 
					
						
							|  |  |  |   'vcard': 'text/vcard', | 
					
						
							|  |  |  |   'vrml': 'model/vrml', | 
					
						
							|  |  |  |   'vtt': 'text/vtt', | 
					
						
							|  |  |  |   'war': 'application/java-archive', | 
					
						
							|  |  |  |   'wasm': 'application/wasm', | 
					
						
							|  |  |  |   'wav': 'audio/wav', | 
					
						
							|  |  |  |   'weba': 'audio/webm', | 
					
						
							|  |  |  |   'webm': 'video/webm', | 
					
						
							|  |  |  |   'webmanifest': 'application/manifest+json', | 
					
						
							|  |  |  |   'webp': 'image/webp', | 
					
						
							|  |  |  |   'wmf': 'image/wmf', | 
					
						
							|  |  |  |   'woff': 'font/woff', | 
					
						
							|  |  |  |   'woff2': 'font/woff2', | 
					
						
							|  |  |  |   'wrl': 'model/vrml', | 
					
						
							|  |  |  |   'x3d': 'model/x3d+xml', | 
					
						
							|  |  |  |   'x3db': 'model/x3d+fastinfoset', | 
					
						
							|  |  |  |   'x3dbz': 'model/x3d+binary', | 
					
						
							|  |  |  |   'x3dv': 'model/x3d-vrml', | 
					
						
							|  |  |  |   'x3dvz': 'model/x3d+vrml', | 
					
						
							|  |  |  |   'x3dz': 'model/x3d+xml', | 
					
						
							|  |  |  |   'xaml': 'application/xaml+xml', | 
					
						
							|  |  |  |   'xht': 'application/xhtml+xml', | 
					
						
							|  |  |  |   'xhtml': 'application/xhtml+xml', | 
					
						
							|  |  |  |   'xm': 'audio/xm', | 
					
						
							|  |  |  |   'xml': 'text/xml', | 
					
						
							|  |  |  |   'xsd': 'application/xml', | 
					
						
							|  |  |  |   'xsl': 'application/xml', | 
					
						
							|  |  |  |   'xslt': 'application/xslt+xml', | 
					
						
							|  |  |  |   'yaml': 'text/yaml', | 
					
						
							|  |  |  |   'yml': 'text/yaml', | 
					
						
							|  |  |  |   'zip': 'application/zip' | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  | module.exports = {TestServer}; |