summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/utils/testserver/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/utils/testserver/index.js')
-rw-r--r--remote/test/puppeteer/utils/testserver/index.js284
1 files changed, 284 insertions, 0 deletions
diff --git a/remote/test/puppeteer/utils/testserver/index.js b/remote/test/puppeteer/utils/testserver/index.js
new file mode 100644
index 0000000000..7d21009c94
--- /dev/null
+++ b/remote/test/puppeteer/utils/testserver/index.js
@@ -0,0 +1,284 @@
+// @ts-nocheck
+
+/**
+ * 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 mime = require('mime');
+const WebSocketServer = require('ws').Server;
+
+const fulfillSymbol = Symbol('fullfil callback');
+const rejectSymbol = Symbol('reject callback');
+
+class TestServer {
+ PORT = undefined;
+ PREFIX = undefined;
+ CROSS_PROCESS_PREFIX = undefined;
+ EMPTY_PAGE = undefined;
+
+ /**
+ * @param {string} dirPath
+ * @param {number} port
+ * @returns {!TestServer}
+ */
+ 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
+ * @returns {!TestServer}
+ */
+ 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));
+ this._wsServer = new WebSocketServer({ server: this._server });
+ this._wsServer.on('connection', this._onWebSocketConnection.bind(this));
+ this._server.listen(port);
+ this._dirPath = dirPath;
+
+ this._startTime = new Date();
+ this._cachedPathPrefix = null;
+
+ /** @type {!Set<!net.Socket>} */
+ this._sockets = new Set();
+
+ /** @type {!Map<string, function(!IncomingMessage, !ServerResponse)>} */
+ 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
+ * @param {function(!IncomingMessage, !ServerResponse)} handler
+ */
+ 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
+ * @returns {!Promise<!IncomingMessage>}
+ */
+ 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();
+ }
+
+ _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);
+ }
+ }
+
+ /**
+ * @param {!IncomingMessage} request
+ * @param {!ServerResponse} response
+ * @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;
+ }
+ response.setHeader('Cache-Control', 'public, max-age=31536000');
+ 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;
+ }
+ const mimeType = mime.getType(filePath);
+ 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);
+ }
+ });
+ }
+
+ _onWebSocketConnection(connection) {
+ connection.send('opened');
+ }
+}
+
+module.exports = { TestServer };