chore: migrate http server to ts (#17677)

This commit is contained in:
Pavel Feldman 2022-09-28 16:01:13 -08:00 committed by GitHub
parent 6fc7d20e35
commit cadd4d1dd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 136 additions and 187 deletions

19
package-lock.json generated
View File

@ -26,6 +26,7 @@
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
"@types/resize-observer-browser": "^0.1.7",
"@types/ws": "^8.5.3",
"@types/xml2js": "^0.4.9",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
@ -1294,6 +1295,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/xml2js": {
"version": "0.4.9",
"dev": true,
@ -9205,6 +9215,15 @@
"version": "0.16.1",
"dev": true
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/xml2js": {
"version": "0.4.9",
"dev": true,

View File

@ -62,6 +62,7 @@
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
"@types/resize-observer-browser": "^0.1.7",
"@types/ws": "^8.5.3",
"@types/xml2js": "^0.4.9",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import type { ServerResponse } from '../../utils/testserver';
import type { ServerResponse } from 'http';
import { test as it, expect } from './pageTest';
it('Page.Events.Request @smoke', async ({ page, server }) => {

View File

@ -1,15 +1,26 @@
const { TestServer } = require('../../../utils/testserver/');
TestServer.create(__dirname, process.argv[2] || 3000).then(server => {
console.log('listening on port', server.PORT);
let ready = false;
setTimeout(() => ready = true, 750);
server.setRoute('/ready', (message, response) => {
const http = require('http');
const port = process.argv[2] || 3000;
let ready = false;
setTimeout(() => ready = true, 750);
const requestListener = function (req, res) {
if (req.url === '/ready') {
if (ready) {
response.statusCode = 200;
response.end('hello');
res.writeHead(200);
res.end('hello');
} else {
response.statusCode = 404;
response.end('not-ready');
res.writeHead(404);
res.end('not-ready');
}
});
} else {
res.writeHead(404);
res.end();
}
};
const server = http.createServer(requestListener);
server.listen(port, () => {
console.log('listening on port', port);
});

View File

@ -1,21 +1,36 @@
const { TestServer } = require('../../../utils/testserver/');
const http = require('http');
console.error('error from server');
const port = process.argv[2] || 3000;
const requestListener = function (req, res) {
if (req.url === '/hello') {
res.end('hello');
return;
}
if (req.url === '/env-FOO') {
res.end(process.env.FOO);
return;
}
if (req.url === '/port') {
res.end('' + port);
return;
}
if (req.url === '/redirect') {
res.writeHead(301, 'Moved');
res.end();
return;
}
res.writeHead(404);
res.end();
};
const server = http.createServer(requestListener);
// delay creating the server to test waiting for it
setTimeout(() => {
TestServer.create(__dirname, process.argv[2] || 3000).then(server => {
console.log('listening on port', server.PORT);
server.setRoute('/hello', (message, response) => {
response.end('hello');
});
server.setRoute('/env-FOO', (message, response) => {
response.end(process.env.FOO);
});
server.setRoute('/port', (_, response) => {
response.end('' + server.PORT);
});
server.setRoute('/redirect', (_, response) => {
response.writeHead(301, 'Moved');
response.end();
});
server.listen(port, () => {
console.log('listening on port', port);
});
}, process.argv[3] ? +process.argv[3] : 0);

View File

@ -1,44 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* 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.
*/
type ServerResponse = import('http').ServerResponse;
type IncomingMessage = import('http').IncomingMessage;
import WebSocket from 'ws';
export class TestServer {
static create(dirPath: string, port: number, loopback?: string): Promise<TestServer>;
static createHTTPS(dirPath: string, port: number, loopback?: string): Promise<TestServer>;
enableHTTPCache(pathPrefix: string);
setAuth(path: string, username: string, password: string);
enableGzip(path: string);
setCSP(path: string, csp: string);
setExtraHeaders(path: string, headers: { [key: string]: string });
stop(): Promise<void>;
setRoute(path: string, handler: (message: IncomingMessage & { postBody: Promise<Buffer> }, response: ServerResponse) => void);
setRedirect(from: string, to: string);
waitForRequest(path: string): Promise<IncomingMessage & { postBody: Promise<Buffer> }>;
waitForWebSocketConnectionRequest(): Promise<IncomingMessage>;
onceWebSocketConnection(handler: (ws: WebSocket, request: IncomingMessage) => void);
sendOnWebSocketConnection(data: string);
reset();
serveFile(request: IncomingMessage, response: ServerResponse);
serveFile(request: IncomingMessage, response: ServerResponse, filePath: string);
PORT: number;
PREFIX: string;
CROSS_PROCESS_PREFIX: string;
EMPTY_PAGE: string;
}

View File

@ -1,5 +1,6 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,41 +15,48 @@
* 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 zlib = require('zlib');
const util = require('util');
const mime = require('mime');
const WebSocketServer = require('ws').Server;
import fs from 'fs';
import http from 'http';
import https from 'https';
import mime from 'mime';
import type net from 'net';
import path from 'path';
import url from 'url';
import util from 'util';
import ws from 'ws';
import zlib, { gzip } from 'zlib';
const fulfillSymbol = Symbol('fullfil callback');
const rejectSymbol = Symbol('reject callback');
const gzipAsync = util.promisify(zlib.gzip.bind(zlib));
const gzipAsync = util.promisify(gzip.bind(zlib));
class TestServer {
/**
* @param {string} dirPath
* @param {number} port
* @param {string=} loopback
* @return {!Promise<TestServer>}
*/
static async create(dirPath, port, loopback) {
export class TestServer {
private _server: http.Server;
private _wsServer: ws.WebSocketServer;
private _dirPath: string;
readonly debugServer: any;
private _startTime: Date;
private _cachedPathPrefix: string | null;
private _sockets = new Set<net.Socket>();
private _routes = new Map<string, (arg0: http.IncomingMessage, arg1: http.ServerResponse) => any>();
private _auths = new Map<string, { username: string; password: string; }>();
private _csp = new Map<string, string>();
private _extraHeaders = new Map<string, object>();
private _gzipRoutes = new Set<string>();
private _requestSubscribers = new Map<string, Promise<any>>();
readonly PORT: number;
readonly PREFIX: string;
readonly CROSS_PROCESS_PREFIX: string;
readonly EMPTY_PAGE: string;
static async create(dirPath: string, port: number, loopback?: string): Promise<TestServer> {
const server = new TestServer(dirPath, port, loopback);
await new Promise(x => server._server.once('listening', x));
return server;
}
/**
* @param {string} dirPath
* @param {number} port
* @param {string=} loopback
* @return {!Promise<TestServer>}
*/
static async createHTTPS(dirPath, port, loopback) {
static async createHTTPS(dirPath: string, port: number, loopback?: string): Promise<TestServer> {
const server = new TestServer(dirPath, port, loopback, {
key: await fs.promises.readFile(path.join(__dirname, 'key.pem')),
cert: await fs.promises.readFile(path.join(__dirname, 'cert.pem')),
@ -58,21 +66,15 @@ class TestServer {
return server;
}
/**
* @param {string} dirPath
* @param {number} port
* @param {string=} loopback
* @param {!Object=} sslOptions
*/
constructor(dirPath, port, loopback, sslOptions) {
constructor(dirPath: string, port: number, loopback?: string, sslOptions?: object) {
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({ noServer: true });
this._wsServer = new ws.WebSocketServer({ noServer: true });
this._server.on('upgrade', async (request, socket, head) => {
const pathname = url.parse(request.url).pathname;
const pathname = url.parse(request.url!).path;
if (pathname === '/ws-slow')
await new Promise(f => setTimeout(f, 2000));
if (!['/ws', '/ws-slow'].includes(pathname)) {
@ -92,21 +94,6 @@ class TestServer {
this._startTime = new Date();
this._cachedPathPrefix = null;
/** @type {!Set<!NodeJS.Socket>} */
this._sockets = new Set();
/** @type {!Map<string, function(!http.IncomingMessage,http.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 {!Map<string, Object>} */
this._extraHeaders = new Map();
/** @type {!Set<string>} */
this._gzipRoutes = new Set();
/** @type {!Map<string, !Promise>} */
this._requestSubscribers = new Map();
const cross_origin = loopback || '127.0.0.1';
const same_origin = loopback || 'localhost';
const protocol = sslOptions ? 'https' : 'http';
@ -116,51 +103,35 @@ class TestServer {
this.EMPTY_PAGE = `${protocol}://${same_origin}:${port}/empty.html`;
}
_onSocket(socket) {
_onSocket(socket: net.Socket) {
this._sockets.add(socket);
// ECONNRESET and HPE_INVALID_EOF_STATE are legit errors given
// that tab closing aborts outgoing connections to the server.
socket.on('error', error => {
if (error.code !== 'ECONNRESET' && error.code !== 'HPE_INVALID_EOF_STATE')
if ((error as any).code !== 'ECONNRESET' && (error as any).code !== 'HPE_INVALID_EOF_STATE')
throw error;
});
socket.once('close', () => this._sockets.delete(socket));
}
/**
* @param {string} pathPrefix
*/
enableHTTPCache(pathPrefix) {
enableHTTPCache(pathPrefix: string) {
this._cachedPathPrefix = pathPrefix;
}
/**
* @param {string} path
* @param {string} username
* @param {string} password
*/
setAuth(path, username, password) {
setAuth(path: string, username: string, password: string) {
this.debugServer(`set auth for ${path} to ${username}:${password}`);
this._auths.set(path, {username, password});
this._auths.set(path, { username, password });
}
enableGzip(path) {
enableGzip(path: string) {
this._gzipRoutes.add(path);
}
/**
* @param {string} path
* @param {string} csp
*/
setCSP(path, csp) {
setCSP(path: string, csp: string) {
this._csp.set(path, csp);
}
/**
* @param {string} path
* @param {Object<string, string>} object
*/
setExtraHeaders(path, object) {
setExtraHeaders(path: string, object: Record<string, string>) {
this._extraHeaders.set(path, object);
}
@ -172,31 +143,19 @@ class TestServer {
await new Promise(x => this._server.close(x));
}
/**
* @param {string} path
* @param {function(!http.IncomingMessage,http.ServerResponse)} handler
*/
setRoute(path, handler) {
setRoute(path: string, handler: (arg0: http.IncomingMessage & { postBody: Promise<Buffer> }, arg1: http.ServerResponse) => any) {
this._routes.set(path, handler);
}
/**
* @param {string} from
* @param {string} to
*/
setRedirect(from, to) {
setRedirect(from: string, to: string) {
this.setRoute(from, (req, res) => {
let headers = this._extraHeaders.get(req.url) || {};
const headers = this._extraHeaders.get(req.url!) || {};
res.writeHead(302, { ...headers, location: to });
res.end();
});
}
/**
* @param {string} path
* @return {!Promise<!http.IncomingMessage>}
*/
waitForRequest(path) {
waitForRequest(path: string): Promise<http.IncomingMessage & { postBody: Promise<Buffer> }> {
let promise = this._requestSubscribers.get(path);
if (promise)
return promise;
@ -223,28 +182,24 @@ class TestServer {
this._requestSubscribers.clear();
}
/**
* @param {http.IncomingMessage} request
* @param {http.ServerResponse} response
*/
_onRequest(request, response) {
_onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
request.on('error', error => {
if (error.code === 'ECONNRESET')
if ((error as any).code === 'ECONNRESET')
response.end();
else
throw error;
});
request.postBody = new Promise(resolve => {
const chunks = [];
(request as any).postBody = new Promise(resolve => {
const chunks: Buffer[] = [];
request.on('data', chunk => {
chunks.push(chunk);
});
request.on('end', () => resolve(Buffer.concat(chunks)));
});
const path = url.parse(request.url).path;
const path = url.parse(request.url!).path;
this.debugServer(`request ${request.method} ${path}`);
if (this._auths.has(path)) {
const auth = this._auths.get(path);
const auth = this._auths.get(path)!;
const credentials = Buffer.from((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString();
this.debugServer(`request credentials ${credentials}`);
this.debugServer(`actual credentials ${auth.username}:${auth.password}`);
@ -257,24 +212,18 @@ class TestServer {
}
// Notify request subscriber.
if (this._requestSubscribers.has(path)) {
this._requestSubscribers.get(path)[fulfillSymbol].call(null, request);
this._requestSubscribers.get(path)![fulfillSymbol].call(null, request);
this._requestSubscribers.delete(path);
}
const handler = this._routes.get(path);
if (handler) {
if (handler)
handler.call(null, request, response);
} else {
else
this.serveFile(request, response);
}
}
/**
* @param {!http.IncomingMessage} request
* @param {!http.ServerResponse} response
* @param {string|undefined} filePath
*/
async serveFile(request, response, filePath) {
let pathName = url.parse(request.url).path;
async serveFile(request: http.IncomingMessage, response: http.ServerResponse, filePath?: string) {
let pathName = url.parse(request.url!).path;
if (!filePath) {
if (pathName === '/')
pathName = '/index.html';
@ -293,7 +242,7 @@ class TestServer {
response.setHeader('Cache-Control', 'no-cache, no-store');
}
if (this._csp.has(pathName))
response.setHeader('Content-Security-Policy', this._csp.get(pathName));
response.setHeader('Content-Security-Policy', this._csp.get(pathName)!);
if (this._extraHeaders.has(pathName)) {
const object = this._extraHeaders.get(pathName);
@ -301,7 +250,7 @@ class TestServer {
response.setHeader(key, object[key]);
}
const {err, data} = await fs.promises.readFile(filePath).then(data => ({data})).catch(err => ({err}));
const { err, data } = await fs.promises.readFile(filePath).then(data => ({ data, err: undefined })).catch(err => ({ data: undefined, err }));
// The HTTP transaction might be already terminated after async hop here - do nothing in this case.
if (response.writableEnded)
return;
@ -332,7 +281,7 @@ class TestServer {
}
waitForWebSocketConnectionRequest() {
return new Promise(fullfil => {
return new Promise<http.IncomingMessage & { headers: http.IncomingHttpHeaders }>(fullfil => {
this._wsServer.once('connection', (ws, req) => fullfil(req));
});
}
@ -341,5 +290,3 @@ class TestServer {
this.onceWebSocketConnection(ws => ws.send(data));
}
}
module.exports = {TestServer};