From 548f69cd12359b241764d3b34cf99ba034c6ca8f Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Wed, 27 Nov 2019 19:16:15 -0500 Subject: [PATCH] Refactor proxy into own package, implement middleware pattern (#5136) * renames * Refactor proxy into own package, implement middleware pattern don't need these mocha opts anymore fix test no more zunder READMEs fix test * pass request by reference * fix cors path * Move replace_stream to proxy, concat-stream util in network * Pin dependency versions * Revert addDefaultPort behavior * Add READMEs for proxy, network * Update README.md * eslint --fix * set to null not undefined * use delete and bump node types * import cors from package now * parse-domain@2.3.4 * proxy package needs common-tags * move pumpify dep * load through where it's needed, remove unused passthru_stream * remove unneeded getbuffer call Co-authored-by: Gleb Bahmutov --- circle.yml | 2 +- package.json | 3 +- packages/network/README.md | 44 ++ packages/network/lib/blacklist.ts | 18 + packages/network/lib/concat-stream.ts | 29 ++ .../lib/util/cors.js => network/lib/cors.ts} | 56 ++- packages/network/lib/index.ts | 12 +- .../lib/util/uri.js => network/lib/uri.ts} | 46 +- packages/network/package.json | 3 + packages/network/test/mocha.opts | 2 +- .../test/unit/blacklist_spec.ts} | 7 +- .../test/unit/cors_spec.ts} | 7 +- packages/proxy/README.md | 37 ++ packages/proxy/index.js | 5 + packages/proxy/lib/http/error-middleware.ts | 48 ++ packages/proxy/lib/http/index.ts | 232 ++++++++++ packages/proxy/lib/http/request-middleware.ts | 156 +++++++ .../proxy/lib/http/response-middleware.ts | 332 ++++++++++++++ packages/proxy/lib/http/util/buffers.ts | 65 +++ packages/proxy/lib/http/util/inject.ts | 25 ++ .../lib/http}/util/replace_stream.ts | 6 +- .../lib/http/util/rewriter.ts} | 30 +- .../lib/http/util/security.ts} | 12 +- packages/proxy/lib/index.ts | 7 + packages/proxy/lib/network-proxy.ts | 26 ++ packages/proxy/package.json | 34 ++ packages/proxy/test/.eslintrc | 5 + packages/proxy/test/mocha.opts | 5 + packages/proxy/test/pretest.ts | 2 + .../test/unit/http/error-middleware.spec.ts | 75 ++++ packages/proxy/test/unit/http/helpers.ts | 18 + packages/proxy/test/unit/http/index.spec.ts | 160 +++++++ .../test/unit/http/request-middleware.spec.ts | 18 + .../unit/http/response-middleware.spec.ts | 25 ++ .../proxy/test/unit/http/util/buffers.spec.ts | 62 +++ .../unit/http/util}/replace_stream_spec.js | 11 +- .../test/unit/http/util/security.spec.ts} | 42 +- packages/proxy/tsconfig.json | 11 + .../server/lib/browsers/cdp_automation.ts | 5 +- packages/server/lib/browsers/electron.coffee | 1 + packages/server/lib/controllers/proxy.coffee | 411 ------------------ packages/server/lib/routes.coffee | 5 +- packages/server/lib/server.coffee | 37 +- packages/server/lib/util/blacklist.js | 22 - packages/server/lib/util/buffers.js | 58 --- .../server/lib/util/conditional_stream.js | 11 - packages/server/lib/util/inject.js | 27 -- packages/server/lib/util/passthru_stream.js | 9 - packages/server/package.json | 11 +- .../integration/http_requests_spec.coffee | 3 +- .../test/integration/server_spec.coffee | 25 +- packages/server/test/unit/buffers_spec.js | 65 --- packages/server/test/unit/server_spec.coffee | 8 +- .../server/test/unit/stream_buffer_spec.js | 2 +- packages/ts/index.d.ts | 6 + 55 files changed, 1591 insertions(+), 793 deletions(-) create mode 100644 packages/network/README.md create mode 100644 packages/network/lib/blacklist.ts create mode 100644 packages/network/lib/concat-stream.ts rename packages/{server/lib/util/cors.js => network/lib/cors.ts} (58%) rename packages/{server/lib/util/uri.js => network/lib/uri.ts} (68%) rename packages/{server/test/unit/blacklist_spec.js => network/test/unit/blacklist_spec.ts} (92%) rename packages/{server/test/unit/cors_spec.js => network/test/unit/cors_spec.ts} (98%) create mode 100644 packages/proxy/README.md create mode 100644 packages/proxy/index.js create mode 100644 packages/proxy/lib/http/error-middleware.ts create mode 100644 packages/proxy/lib/http/index.ts create mode 100644 packages/proxy/lib/http/request-middleware.ts create mode 100644 packages/proxy/lib/http/response-middleware.ts create mode 100644 packages/proxy/lib/http/util/buffers.ts create mode 100644 packages/proxy/lib/http/util/inject.ts rename packages/{server/lib => proxy/lib/http}/util/replace_stream.ts (92%) rename packages/{server/lib/util/rewriter.js => proxy/lib/http/util/rewriter.ts} (58%) rename packages/{server/lib/util/security.js => proxy/lib/http/util/security.ts} (87%) create mode 100644 packages/proxy/lib/index.ts create mode 100644 packages/proxy/lib/network-proxy.ts create mode 100644 packages/proxy/package.json create mode 100644 packages/proxy/test/.eslintrc create mode 100644 packages/proxy/test/mocha.opts create mode 100644 packages/proxy/test/pretest.ts create mode 100644 packages/proxy/test/unit/http/error-middleware.spec.ts create mode 100644 packages/proxy/test/unit/http/helpers.ts create mode 100644 packages/proxy/test/unit/http/index.spec.ts create mode 100644 packages/proxy/test/unit/http/request-middleware.spec.ts create mode 100644 packages/proxy/test/unit/http/response-middleware.spec.ts create mode 100644 packages/proxy/test/unit/http/util/buffers.spec.ts rename packages/{server/test/unit => proxy/test/unit/http/util}/replace_stream_spec.js (98%) rename packages/{server/test/unit/security_spec.js => proxy/test/unit/http/util/security.spec.ts} (91%) create mode 100644 packages/proxy/tsconfig.json delete mode 100644 packages/server/lib/controllers/proxy.coffee delete mode 100644 packages/server/lib/util/blacklist.js delete mode 100644 packages/server/lib/util/buffers.js delete mode 100644 packages/server/lib/util/conditional_stream.js delete mode 100644 packages/server/lib/util/inject.js delete mode 100644 packages/server/lib/util/passthru_stream.js delete mode 100644 packages/server/test/unit/buffers_spec.js diff --git a/circle.yml b/circle.yml index d6c70c70b6b2..951cfab046bd 100644 --- a/circle.yml +++ b/circle.yml @@ -297,7 +297,7 @@ jobs: - run: npm run all test -- --package https-proxy - run: npm run all test -- --package launcher - run: npm run all test -- --package network - # how to pass Mocha reporter through zunder? + - run: npm run all test -- --package proxy - run: npm run all test -- --package reporter - run: npm run all test -- --package runner - run: npm run all test -- --package socket diff --git a/package.json b/package.json index 5ce4da5cbb20..ba4937581039 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@types/markdown-it": "0.0.9", "@types/mini-css-extract-plugin": "0.8.0", "@types/mocha": "5.2.7", - "@types/node": "11.12.0", + "@types/node": "12.12.14", "@types/ramda": "0.25.47", "@types/react-dom": "16.9.4", "@types/request-promise": "4.1.42", @@ -163,6 +163,7 @@ "stop-only": "3.0.1", "strip-ansi": "4.0.0", "terminal-banner": "1.1.0", + "through": "2.3.8", "ts-node": "8.3.0", "typescript": "3.5.3", "vinyl-paths": "2.1.0" diff --git a/packages/network/README.md b/packages/network/README.md new file mode 100644 index 000000000000..d64753feaf40 --- /dev/null +++ b/packages/network/README.md @@ -0,0 +1,44 @@ +# network + +This package contains networking-related classes and utilities. + +## Exports + +You can see a list of the modules exported from this package in [./lib/index.ts](./lib/index.ts). Here is a brief description of what's available: + +* `agent` is a HTTP/HTTPS [agent][1] with support for HTTP/HTTPS proxies and keepalive whenever possible +* `allowDestroy` can be used to wrap a `net.Server` to add a `.destroy()` method +* `blacklist` is a utility for matching glob blacklists +* `concatStream` is a wrapper around [`concat-stream@1.6.2`][2] that makes it always yield a `Buffer` +* `connect` contains utilities for making network connections, including `createRetryingSocket` +* `cors` contains utilities for Cross-Origin Resource Sharing +* `uri` contains utilities for URL parsing and formatting + +See the individual class files in [`./lib`](./lib) for more information. + +## Installing Dependencies + +```shell +npm i +``` + +## Building + +Note: you should not ever need to build the .js files manually. `@packages/ts` provides require-time transpilation when in development. + +```shell +npm run build-js +``` + +## Testing + +Tests are located in [`./test`](./test) + +To run tests: + +```shell +npm run test +``` + +[1]: https://devdocs.io/node/http#http_class_http_agent +[2]: https://github.com/maxogden/concat-stream/tree/v1.6.2 diff --git a/packages/network/lib/blacklist.ts b/packages/network/lib/blacklist.ts new file mode 100644 index 000000000000..1a6b7021ecba --- /dev/null +++ b/packages/network/lib/blacklist.ts @@ -0,0 +1,18 @@ +import _ from 'lodash' +import minimatch from 'minimatch' +import { stripProtocolAndDefaultPorts } from './uri' + +export function matches (urlToCheck, blacklistHosts) { + // normalize into flat array + blacklistHosts = [].concat(blacklistHosts) + + urlToCheck = stripProtocolAndDefaultPorts(urlToCheck) + + // use minimatch against the url + // to see if any match + const matchUrl = (hostMatcher) => { + return minimatch(urlToCheck, hostMatcher) + } + + return _.find(blacklistHosts, matchUrl) +} diff --git a/packages/network/lib/concat-stream.ts b/packages/network/lib/concat-stream.ts new file mode 100644 index 000000000000..d26757026e17 --- /dev/null +++ b/packages/network/lib/concat-stream.ts @@ -0,0 +1,29 @@ +import _ from 'lodash' +import _concatStream from 'concat-stream' + +type Callback = (buf: Buffer) => void +type ConcatOpts = { + encoding?: string +} + +/** + * Wrapper for `concat-stream` to handle empty streams. + */ +export const concatStream: typeof _concatStream = function (opts: Callback | ConcatOpts, cb?: Callback) { + let _cb: Callback = cb! + + if (!_cb) { + _cb = opts as Callback + opts = {} + } + + return _concatStream(opts as ConcatOpts, function (buf: Buffer) { + if (!_.get(buf, 'length')) { + // concat-stream can give an empty array if the stream has + // no data - just call the callback with an empty buffer + return _cb(Buffer.from('')) + } + + return _cb(buf) + }) +} diff --git a/packages/server/lib/util/cors.js b/packages/network/lib/cors.ts similarity index 58% rename from packages/server/lib/util/cors.js rename to packages/network/lib/cors.ts index 69e8f76d80ae..f8ad7d0dc6e1 100644 --- a/packages/server/lib/util/cors.js +++ b/packages/network/lib/cors.ts @@ -1,32 +1,43 @@ -const _ = require('lodash') -const url = require('url') -const uri = require('./uri') -const debug = require('debug')('cypress:server:cors') -const parseDomain = require('parse-domain') +import _ from 'lodash' +import * as uri from './uri' +import debugModule from 'debug' +import _parseDomain, { ParsedDomain } from 'parse-domain' + +const debug = debugModule('cypress:network:cors') const ipAddressRe = /^[\d\.]+$/ -function getSuperDomain (url) { +type ParsedHost = { + port?: string + tld?: string + domain?: string +} + +export function getSuperDomain (url) { const parsed = parseUrlIntoDomainTldPort(url) return _.compact([parsed.domain, parsed.tld]).join('.') } -function _parseDomain (domain, options = {}) { - return parseDomain(domain, _.defaults(options, { +export function parseDomain (domain: string, options = {}) { + return _parseDomain(domain, _.defaults(options, { privateTlds: true, customTlds: ipAddressRe, })) } -function parseUrlIntoDomainTldPort (str) { - let { hostname, port, protocol } = url.parse(str) +export function parseUrlIntoDomainTldPort (str) { + let { hostname, port, protocol } = uri.parse(str) - if (port == null) { + if (!hostname) { + hostname = '' + } + + if (!port) { port = protocol === 'https:' ? '443' : '80' } - let parsed = _parseDomain(hostname) + let parsed: Partial | null = parseDomain(hostname) // if we couldn't get a parsed domain if (!parsed) { @@ -43,46 +54,33 @@ function parseUrlIntoDomainTldPort (str) { } } - const obj = {} + const obj: ParsedHost = {} obj.port = port obj.tld = parsed.tld obj.domain = parsed.domain - // obj.protocol = protocol debug('Parsed URL %o', obj) return obj } -function urlMatchesOriginPolicyProps (urlStr, props) { +export function urlMatchesOriginPolicyProps (urlStr, props) { // take a shortcut here in the case // where remoteHostAndPort is null if (!props) { return false } - const parsedUrl = this.parseUrlIntoDomainTldPort(urlStr) + const parsedUrl = parseUrlIntoDomainTldPort(urlStr) // does the parsedUrl match the parsedHost? return _.isEqual(parsedUrl, props) } -function urlMatchesOriginProtectionSpace (urlStr, origin) { +export function urlMatchesOriginProtectionSpace (urlStr, origin) { const normalizedUrl = uri.addDefaultPort(urlStr).format() const normalizedOrigin = uri.addDefaultPort(origin).format() return _.startsWith(normalizedUrl, normalizedOrigin) } - -module.exports = { - parseUrlIntoDomainTldPort, - - parseDomain: _parseDomain, - - getSuperDomain, - - urlMatchesOriginPolicyProps, - - urlMatchesOriginProtectionSpace, -} diff --git a/packages/network/lib/index.ts b/packages/network/lib/index.ts index d2b58d7f42e8..f40984659a19 100644 --- a/packages/network/lib/index.ts +++ b/packages/network/lib/index.ts @@ -1,9 +1,17 @@ import agent from './agent' +import * as blacklist from './blacklist' import * as connect from './connect' -import { allowDestroy } from './allow-destroy' +import * as cors from './cors' +import * as uri from './uri' export { agent, - allowDestroy, + blacklist, connect, + cors, + uri, } + +export { allowDestroy } from './allow-destroy' + +export { concatStream } from './concat-stream' diff --git a/packages/server/lib/util/uri.js b/packages/network/lib/uri.ts similarity index 68% rename from packages/server/lib/util/uri.js rename to packages/network/lib/uri.ts index c5e59bff7b11..2cab246d593c 100644 --- a/packages/server/lib/util/uri.js +++ b/packages/network/lib/uri.ts @@ -5,8 +5,8 @@ // node's url formatting algorithm (which acts pretty unexpectedly) // - https://nodejs.org/api/url.html#url_url_format_urlobject -const _ = require('lodash') -const url = require('url') +import _ from 'lodash' +import url from 'url' // yup, protocol contains a: ':' colon // at the end of it (-______________-) @@ -25,9 +25,9 @@ const parseClone = (urlObject) => { return url.parse(_.clone(urlObject)) } -const parse = url.parse +export const parse = url.parse -const stripProtocolAndDefaultPorts = function (urlToCheck) { +export function stripProtocolAndDefaultPorts (urlToCheck) { // grab host which is 'hostname:port' only const { host, hostname, port } = url.parse(urlToCheck) @@ -41,20 +41,18 @@ const stripProtocolAndDefaultPorts = function (urlToCheck) { return host } -const removePort = (urlObject) => { +export function removePort (urlObject) { const parsed = parseClone(urlObject) - // set host to null else - // url.format(...) will ignore - // the port property + // set host to undefined else url.format(...) will ignore the port property // https://nodejs.org/api/url.html#url_url_format_urlobject - parsed.host = null - parsed.port = null + delete parsed.host + delete parsed.port return parsed } -const removeDefaultPort = function (urlToCheck) { +export function removeDefaultPort (urlToCheck) { let parsed = parseClone(urlToCheck) if (portIsDefault(parsed.port)) { @@ -64,33 +62,23 @@ const removeDefaultPort = function (urlToCheck) { return parsed } -const addDefaultPort = function (urlToCheck) { +export function addDefaultPort (urlToCheck) { const parsed = parseClone(urlToCheck) if (!parsed.port) { // unset host... // see above for reasoning - parsed.host = null - parsed.port = DEFAULT_PROTOCOL_PORTS[parsed.protocol] + delete parsed.host + if (parsed.protocol) { + parsed.port = DEFAULT_PROTOCOL_PORTS[parsed.protocol] + } else { + delete parsed.port + } } return parsed } -const getPath = (urlToCheck) => { +export function getPath (urlToCheck) { return url.parse(urlToCheck).path } - -module.exports = { - parse, - - getPath, - - removePort, - - addDefaultPort, - - removeDefaultPort, - - stripProtocolAndDefaultPorts, -} diff --git a/packages/network/package.json b/packages/network/package.json index a98d0bd3545a..bd4865a5dc69 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -10,12 +10,15 @@ }, "dependencies": { "bluebird": "3.5.3", + "concat-stream": "1.6.2", "debug": "4.1.1", "lodash": "4.17.15", + "parse-domain": "2.3.4", "proxy-from-env": "1.0.0" }, "devDependencies": { "@cypress/debugging-proxy": "2.0.1", + "@types/concat-stream": "1.6.0", "bin-up": "1.2.2", "express": "4.16.4", "request": "2.88.0", diff --git a/packages/network/test/mocha.opts b/packages/network/test/mocha.opts index 7ef9f2476490..184d5278afb5 100644 --- a/packages/network/test/mocha.opts +++ b/packages/network/test/mocha.opts @@ -1,5 +1,5 @@ test/unit test/integration ---compilers ts:@packages/ts/register +--compilers ts:@packages/ts/register,coffee:@packages/coffee/register --timeout 10000 --recursive diff --git a/packages/server/test/unit/blacklist_spec.js b/packages/network/test/unit/blacklist_spec.ts similarity index 92% rename from packages/server/test/unit/blacklist_spec.js rename to packages/network/test/unit/blacklist_spec.ts index 5c40b2dddd97..81e223b4f915 100644 --- a/packages/server/test/unit/blacklist_spec.js +++ b/packages/network/test/unit/blacklist_spec.ts @@ -1,6 +1,5 @@ -require('../spec_helper') - -const blacklist = require(`${root}lib/util/blacklist`) +import { blacklist } from '../..' +import { expect } from 'chai' const hosts = [ '*.google.com', @@ -26,7 +25,7 @@ const matchesHost = (url, host) => { expect(blacklist.matches(url, hosts)).to.eq(host) } -describe('lib/util/blacklist', () => { +describe('lib/blacklist', () => { it('handles hosts, ports, wildcards', () => { matchesArray('https://mail.google.com/foo', true) matchesArray('https://shop.apple.com/bar', true) diff --git a/packages/server/test/unit/cors_spec.js b/packages/network/test/unit/cors_spec.ts similarity index 98% rename from packages/server/test/unit/cors_spec.js rename to packages/network/test/unit/cors_spec.ts index 56b219251020..5031badf4c9a 100644 --- a/packages/server/test/unit/cors_spec.js +++ b/packages/network/test/unit/cors_spec.ts @@ -1,8 +1,7 @@ -require('../spec_helper') +import { cors } from '../../lib' +import { expect } from 'chai' -const cors = require(`${root}lib/util/cors`) - -describe('lib/util/cors', () => { +describe('lib/cors', () => { context('.parseUrlIntoDomainTldPort', () => { beforeEach(function () { this.isEq = (url, obj) => { diff --git a/packages/proxy/README.md b/packages/proxy/README.md new file mode 100644 index 000000000000..663a5384df96 --- /dev/null +++ b/packages/proxy/README.md @@ -0,0 +1,37 @@ +# proxy + +This package contains the code for Cypress's HTTP interception proxy. + +## HTTP interception + +[`./lib/http`](./lib/http) contains the code that intercepts HTTP requests. The bulk of the proxy's behavior is in three files: + +* [`request-middleware.ts`](./lib/http/request-middleware.ts) contains code that manipulates HTTP requests from the browser +* [`response-middleware.ts`](./lib/http/responseest-middleware.ts) contains code that manipulates HTTP responses to the browser +* [`error-middleware.ts`](./lib/http/responseest-middleware.ts) handles errors that occur in the request/response cycle + +## Installing Dependencies + +```shell +npm i +``` + +## Building + +Note: you should not ever need to build the .js files manually. `@packages/ts` provides require-time transpilation when in development. + +```shell +npm run build-js +``` + +## Testing + +Tests are located in [`./test`](./test) + +To run tests: + +```shell +npm run test +``` + +Additionally, the `server` package contains tests that use the `proxy`. diff --git a/packages/proxy/index.js b/packages/proxy/index.js new file mode 100644 index 000000000000..99166f024f49 --- /dev/null +++ b/packages/proxy/index.js @@ -0,0 +1,5 @@ +if (process.env.CYPRESS_ENV !== 'production') { + require('@packages/ts/register') +} + +module.exports = require('./lib') diff --git a/packages/proxy/lib/http/error-middleware.ts b/packages/proxy/lib/http/error-middleware.ts new file mode 100644 index 000000000000..59f5bc4ecf34 --- /dev/null +++ b/packages/proxy/lib/http/error-middleware.ts @@ -0,0 +1,48 @@ +import _ from 'lodash' +import debugModule from 'debug' +import { HttpMiddleware } from '.' +import { Readable } from 'stream' +import { Request } from 'request' + +const debug = debugModule('cypress:proxy:http:error-middleware') + +export type ErrorMiddleware = HttpMiddleware<{ + error: Error + incomingResStream?: Readable + outgoingReq?: Request +}> + +const LogError: ErrorMiddleware = function () { + debug('error proxying request %o', _.pick(this, 'error', 'req', 'res', 'incomingRes', 'outgoingReq', 'incomingResStream')) + this.next() +} + +export const AbortRequest: ErrorMiddleware = function () { + if (this.outgoingReq) { + debug('aborting outgoingReq') + this.outgoingReq.abort() + } + + this.next() +} + +export const UnpipeResponse: ErrorMiddleware = function () { + if (this.incomingResStream) { + debug('unpiping resStream from response') + this.incomingResStream.unpipe() + } + + this.next() +} + +export const DestroyResponse: ErrorMiddleware = function () { + this.res.destroy() + this.end() +} + +export default { + LogError, + AbortRequest, + UnpipeResponse, + DestroyResponse, +} diff --git a/packages/proxy/lib/http/index.ts b/packages/proxy/lib/http/index.ts new file mode 100644 index 000000000000..bee731351d28 --- /dev/null +++ b/packages/proxy/lib/http/index.ts @@ -0,0 +1,232 @@ +import _ from 'lodash' +import debugModule from 'debug' +import ErrorMiddleware from './error-middleware' +import { HttpBuffers } from './util/buffers' +import { IncomingMessage } from 'http' +import Promise from 'bluebird' +import { Readable } from 'stream' +import { Request, Response } from 'express' +import RequestMiddleware from './request-middleware' +import ResponseMiddleware from './response-middleware' + +const debug = debugModule('cypress:proxy:http') + +export enum HttpStages { + IncomingRequest, + IncomingResponse, + Error +} + +export type HttpMiddleware = (this: HttpMiddlewareThis) => void + +export type CypressRequest = Request & { + // TODO: what's this difference from req.url? is it only for non-proxied requests? + proxiedUrl: string + abort: () => void +} + +type MiddlewareStacks = { + [stage in HttpStages]: { + [name: string]: HttpMiddleware + } +} + +export type CypressResponse = Response & { + isInitial: null | boolean + wantsInjection: 'full' | 'partial' | false + wantsSecurityRemoved: null | boolean +} + +type HttpMiddlewareCtx = { + req: CypressRequest + res: CypressResponse + + middleware: MiddlewareStacks +} & T + +const READONLY_MIDDLEWARE_KEYS: (keyof HttpMiddlewareThis<{}>)[] = [ + 'buffers', + 'config', + 'getRemoteState', + 'request', + 'next', + 'end', + 'onResponse', + 'onError', + 'skipMiddleware', +] + +type HttpMiddlewareThis = HttpMiddlewareCtx & Readonly<{ + buffers: HttpBuffers + config: any + getRemoteState: () => any + request: any + + next: () => void + /** + * Call to completely end the stage, bypassing any remaining middleware. + */ + end: () => void + onResponse: (incomingRes: Response, resStream: Readable) => void + onError: (error: Error) => void + skipMiddleware: (name: string) => void +}> + +export function _runStage (type: HttpStages, ctx: any) { + const stage = HttpStages[type] + + debug('Entering stage %o', { stage }) + + const runMiddlewareStack = () => { + const middlewares = ctx.middleware[type] + + // pop the first pair off the middleware + const middlewareName = _.keys(middlewares)[0] + + if (!middlewareName) { + return Promise.resolve() + } + + const middleware = middlewares[middlewareName] + + ctx.middleware[type] = _.omit(middlewares, middlewareName) + + return new Promise((resolve) => { + let ended = false + + function copyChangedCtx () { + _.chain(fullCtx) + .omit(READONLY_MIDDLEWARE_KEYS) + .forEach((value, key) => { + if (ctx[key] !== value) { + debug(`copying %o`, { [key]: value }) + ctx[key] = value + } + }) + .value() + } + + function _end (retval?) { + if (ended) { + return + } + + ended = true + + copyChangedCtx() + + resolve(retval) + } + + if (!middleware) { + return resolve() + } + + debug('Running middleware %o', { stage, middlewareName }) + + const fullCtx = { + next: () => { + copyChangedCtx() + + _end(runMiddlewareStack()) + }, + end: () => _end(), + onResponse: (incomingRes: IncomingMessage, resStream: Readable) => { + ctx.incomingRes = incomingRes + ctx.incomingResStream = resStream + + _end() + }, + onError: (error: Error) => { + debug('Error in middleware %o', { stage, middlewareName, error, ctx }) + + if (type === HttpStages.Error) { + return + } + + ctx.error = error + + _end(_runStage(HttpStages.Error, ctx)) + }, + + skipMiddleware: (name) => { + ctx.middleware[type] = _.omit(ctx.middleware[type], name) + }, + + ...ctx, + } + + try { + middleware.call(fullCtx) + } catch (err) { + fullCtx.onError(err) + } + }) + } + + return runMiddlewareStack() + .then(() => { + debug('Leaving stage %o', { stage }) + }) +} + +export class Http { + buffers: HttpBuffers + config: any + getRemoteState: () => any + middleware: MiddlewareStacks + request: any + + constructor (opts: { + config: any + getRemoteState: () => any + middleware?: MiddlewareStacks + request: any + }) { + this.buffers = new HttpBuffers() + + this.config = opts.config + this.getRemoteState = opts.getRemoteState + this.request = opts.request + + if (typeof opts.middleware === 'undefined') { + this.middleware = { + [HttpStages.IncomingRequest]: RequestMiddleware, + [HttpStages.IncomingResponse]: ResponseMiddleware, + [HttpStages.Error]: ErrorMiddleware, + } + } else { + this.middleware = opts.middleware + } + } + + handle (req, res) { + const ctx: HttpMiddlewareCtx = { + req, + res, + + buffers: this.buffers, + config: this.config, + getRemoteState: this.getRemoteState, + request: this.request, + middleware: _.cloneDeep(this.middleware), + } + + return _runStage(HttpStages.IncomingRequest, ctx) + .then(() => { + if (ctx.incomingRes) { + return _runStage(HttpStages.IncomingResponse, ctx) + } + + return debug('warning: Request was not fulfilled with a response.') + }) + } + + reset () { + this.buffers.reset() + } + + setBuffer (buffer) { + return this.buffers.set(buffer) + } +} diff --git a/packages/proxy/lib/http/request-middleware.ts b/packages/proxy/lib/http/request-middleware.ts new file mode 100644 index 000000000000..e460f9675316 --- /dev/null +++ b/packages/proxy/lib/http/request-middleware.ts @@ -0,0 +1,156 @@ +import _ from 'lodash' +import { blacklist, cors } from '@packages/network' +import debugModule from 'debug' +import { HttpMiddleware } from '.' + +export type RequestMiddleware = HttpMiddleware<{ + outgoingReq: any +}> + +const debug = debugModule('cypress:proxy:http:request-middleware') + +const LogRequest: RequestMiddleware = function () { + debug('proxying request %o', { + req: _.pick(this.req, 'method', 'proxiedUrl', 'headers'), + }) + + this.next() +} + +const RedirectToClientRouteIfUnloaded: RequestMiddleware = function () { + // if we have an unload header it means our parent app has been navigated away + // directly and we need to automatically redirect to the clientRoute + if (this.req.cookies['__cypress.unload']) { + this.res.redirect(this.config.clientRoute) + + return this.end() + } + + this.next() +} + +// TODO: is this necessary? it seems to be for requesting Cypress w/o the proxy, +// which isn't currently supported +const RedirectToClientRouteIfNotProxied: RequestMiddleware = function () { + // when you access cypress from a browser which has not had its proxy setup then + // req.url will match req.proxiedUrl and we'll know to instantly redirect them + // to the correct client route + if (this.req.url === this.req.proxiedUrl && !this.getRemoteState().visiting) { + // if we dont have a remoteState.origin that means we're initially requesting + // the cypress app and we need to redirect to the root path that serves the app + this.res.redirect(this.config.clientRoute) + + return this.end() + } + + this.next() +} + +const EndRequestsToBlacklistedHosts: RequestMiddleware = function () { + const { blacklistHosts } = this.config + + if (blacklistHosts) { + const matches = blacklist.matches(this.req.proxiedUrl, blacklistHosts) + + if (matches) { + this.res.set('x-cypress-matched-blacklisted-host', matches) + debug('blacklisting request %o', { + url: this.req.proxiedUrl, + matches, + }) + + this.res.status(503).end() + + return this.end() + } + } + + this.next() +} + +const MaybeEndRequestWithBufferedResponse: RequestMiddleware = function () { + const buffer = this.buffers.take(this.req.proxiedUrl) + + if (buffer) { + debug('got a buffer %o', buffer) + this.res.wantsInjection = 'full' + + return this.onResponse(buffer.response, buffer.stream) + } + + this.next() +} + +const StripUnsupportedAcceptEncoding: RequestMiddleware = function () { + // Cypress can only support plaintext or gzip, so make sure we don't request anything else + const acceptEncoding = this.req.headers['accept-encoding'] + + if (acceptEncoding) { + if (acceptEncoding.includes('gzip')) { + this.req.headers['accept-encoding'] = 'gzip' + } else { + delete this.req.headers['accept-encoding'] + } + } + + this.next() +} + +function reqNeedsBasicAuthHeaders (req, { auth, origin }) { + //if we have auth headers, this request matches our origin, protection space, and the user has not supplied auth headers + return auth && !req.headers['authorization'] && cors.urlMatchesOriginProtectionSpace(req.proxiedUrl, origin) +} + +const MaybeSetBasicAuthHeaders: RequestMiddleware = function () { + const remoteState = this.getRemoteState() + + if (reqNeedsBasicAuthHeaders(this.req, remoteState)) { + const { auth } = remoteState + const base64 = Buffer.from(`${auth.username}:${auth.password}`).toString('base64') + + this.req.headers['authorization'] = `Basic ${base64}` + } + + this.next() +} + +const SendRequestOutgoing: RequestMiddleware = function () { + const requestOptions = { + timeout: this.config.responseTimeout, + strictSSL: false, + followRedirect: false, + retryIntervals: [0, 100, 200, 200], + url: this.req.proxiedUrl, + } + + const remoteState = this.getRemoteState() + + if (remoteState.strategy === 'file' && requestOptions.url.startsWith(remoteState.origin)) { + requestOptions.url = requestOptions.url.replace(remoteState.origin, remoteState.fileServer) + } + + const req = this.request.create(requestOptions) + + req.on('error', this.onError) + req.on('response', (incomingRes) => this.onResponse(incomingRes, req)) + this.req.on('aborted', () => { + debug('request aborted') + req.abort() + }) + + // pipe incoming request body, headers to new request + this.req.pipe(req) + + this.outgoingReq = req +} + +export default { + LogRequest, + RedirectToClientRouteIfUnloaded, + RedirectToClientRouteIfNotProxied, + EndRequestsToBlacklistedHosts, + MaybeEndRequestWithBufferedResponse, + StripUnsupportedAcceptEncoding, + MaybeSetBasicAuthHeaders, + SendRequestOutgoing, +} diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts new file mode 100644 index 000000000000..88aa42b1ba52 --- /dev/null +++ b/packages/proxy/lib/http/response-middleware.ts @@ -0,0 +1,332 @@ +import _ from 'lodash' +import charset from 'charset' +import { CookieOptions } from 'express' +import { cors, concatStream } from '@packages/network' +import { CypressRequest, CypressResponse, HttpMiddleware } from '.' +import debugModule from 'debug' +import iconv from 'iconv-lite' +import { IncomingMessage, IncomingHttpHeaders } from 'http' +import { PassThrough, Readable } from 'stream' +import * as rewriter from './util/rewriter' +import zlib from 'zlib' + +export type ResponseMiddleware = HttpMiddleware<{ + incomingRes: IncomingMessage + incomingResStream: Readable +}> + +const debug = debugModule('cypress:proxy:http:response-middleware') + +// https://github.com/cypress-io/cypress/issues/1756 +const zlibOptions = { + flush: zlib.Z_SYNC_FLUSH, + finishFlush: zlib.Z_SYNC_FLUSH, +} + +// https://github.com/cypress-io/cypress/issues/1543 +function getNodeCharsetFromResponse (headers: IncomingHttpHeaders, body: Buffer) { + const httpCharset = (charset(headers, body, 1024) || '').toLowerCase() + + debug('inferred charset from response %o', { httpCharset }) + if (iconv.encodingExists(httpCharset)) { + return httpCharset + } + + // browsers default to latin1 + return 'latin1' +} + +function reqMatchesOriginPolicy (req: CypressRequest, remoteState) { + if (remoteState.strategy === 'http') { + return cors.urlMatchesOriginPolicyProps(req.proxiedUrl, remoteState.props) + } + + if (remoteState.strategy === 'file') { + return req.proxiedUrl.startsWith(remoteState.origin) + } + + return false +} + +function reqWillRenderHtml (req: CypressRequest) { + // will this request be rendered in the browser, necessitating injection? + // https://github.com/cypress-io/cypress/issues/288 + + // don't inject if this is an XHR from jquery + if (req.headers['x-requested-with']) { + return + } + + // don't inject if we didn't find both text/html and application/xhtml+xml, + const accept = req.headers['accept'] + + return accept && accept.includes('text/html') && accept.includes('application/xhtml+xml') +} + +function resContentTypeIs (res: IncomingMessage, contentType: string) { + return (res.headers['content-type'] || '').includes(contentType) +} + +function resContentTypeIsJavaScript (res: IncomingMessage) { + return _.some( + ['application/javascript', 'application/x-javascript', 'text/javascript'] + .map(_.partial(resContentTypeIs, res)) + ) +} + +function resIsGzipped (res: IncomingMessage) { + return (res.headers['content-encoding'] || '').includes('gzip') +} + +// https://github.com/cypress-io/cypress/issues/4298 +// https://tools.ietf.org/html/rfc7230#section-3.3.3 +// HEAD, 1xx, 204, and 304 responses should never contain anything after headers +const NO_BODY_STATUS_CODES = [204, 304] + +function responseMustHaveEmptyBody (req: CypressRequest, res: IncomingMessage) { + return _.some([_.includes(NO_BODY_STATUS_CODES, res.statusCode), _.invoke(req.method, 'toLowerCase') === 'head']) +} + +function setCookie (res: CypressResponse, k: string, v: string, domain: string) { + let opts: CookieOptions = { domain } + + if (!v) { + v = '' + + opts.expires = new Date(0) + } + + return res.cookie(k, v, opts) +} + +function setInitialCookie (res: CypressResponse, remoteState: any, value) { + // dont modify any cookies if we're trying to clear the initial cookie and we're not injecting anything + // dont set the cookies if we're not on the initial request + if ((!value && !res.wantsInjection) || !res.isInitial) { + return + } + + return setCookie(res, '__cypress.initial', value, remoteState.domainName) +} + +const LogResponse: ResponseMiddleware = function () { + debug('received response %o', { + req: _.pick(this.req, 'method', 'proxiedUrl', 'headers'), + incomingRes: _.pick(this.incomingRes, 'headers', 'statusCode'), + }) + + this.next() +} + +const PatchExpressSetHeader: ResponseMiddleware = function () { + const originalSetHeader = this.res.setHeader + + // express.Response.setHeader does all kinds of silly/nasty stuff to the content-type... + // but we don't want to change it at all! + this.res.setHeader = (k, v) => { + if (k === 'content-type') { + v = this.incomingRes.headers['content-type'] || v + } + + return originalSetHeader.call(this.res, k, v) + } + + this.next() +} + +const SetInjectionLevel: ResponseMiddleware = function () { + this.res.isInitial = this.req.cookies['__cypress.initial'] === 'true' + + const getInjectionLevel = () => { + if (this.incomingRes.headers['x-cypress-file-server-error'] && !this.res.isInitial) { + return 'partial' + } + + if (!resContentTypeIs(this.incomingRes, 'text/html') || !reqMatchesOriginPolicy(this.req, this.getRemoteState())) { + return false + } + + if (this.res.isInitial) { + return 'full' + } + + if (!reqWillRenderHtml(this.req)) { + return false + } + + return 'partial' + } + + if (!this.res.wantsInjection) { + this.res.wantsInjection = getInjectionLevel() + } + + this.res.wantsSecurityRemoved = this.config.modifyObstructiveCode && ( + (this.res.wantsInjection === 'full') + || resContentTypeIsJavaScript(this.incomingRes) + ) + + debug('injection levels: %o', _.pick(this.res, 'isInitial', 'wantsInjection', 'wantsSecurityRemoved')) + + this.next() +} + +const OmitProblematicHeaders: ResponseMiddleware = function () { + const headers = _.omit(this.incomingRes.headers, [ + 'set-cookie', + 'x-frame-options', + 'content-length', + 'content-security-policy', + 'connection', + ]) + + this.res.set(headers) + + this.next() +} + +const MaybePreventCaching: ResponseMiddleware = function () { + // do not cache injected responses + // TODO: consider implementing etag system so even injected content can be cached + if (this.res.wantsInjection) { + this.res.setHeader('cache-control', 'no-cache, no-store, must-revalidate') + } + + this.next() +} + +const CopyCookiesFromIncomingRes: ResponseMiddleware = function () { + const cookies: string | string[] | undefined = this.incomingRes.headers['set-cookie'] + + if (cookies) { + ([] as string[]).concat(cookies).forEach((cookie) => { + try { + this.res.append('Set-Cookie', cookie) + } catch (err) { + debug('failed to Set-Cookie, continuing %o', { err, cookie }) + } + }) + } + + this.next() +} + +const REDIRECT_STATUS_CODES: any[] = [301, 302, 303, 307, 308] + +// TODO: this shouldn't really even be necessary? +const MaybeSendRedirectToClient: ResponseMiddleware = function () { + const { statusCode, headers } = this.incomingRes + const newUrl = headers['location'] + + if (!REDIRECT_STATUS_CODES.includes(statusCode) || !newUrl) { + return this.next() + } + + setInitialCookie(this.res, this.getRemoteState(), true) + + debug('redirecting to new url %o', { statusCode, newUrl }) + this.res.redirect(Number(statusCode), newUrl) + + return this.end() +} + +const CopyResponseStatusCode: ResponseMiddleware = function () { + this.res.status(Number(this.incomingRes.statusCode)) + this.next() +} + +const ClearCyInitialCookie: ResponseMiddleware = function () { + setInitialCookie(this.res, this.getRemoteState(), false) + this.next() +} + +const MaybeEndWithEmptyBody: ResponseMiddleware = function () { + if (responseMustHaveEmptyBody(this.req, this.incomingRes)) { + this.res.end() + + return this.end() + } + + this.next() +} + +const MaybeGunzipBody: ResponseMiddleware = function () { + if (resIsGzipped(this.incomingRes) && (this.res.wantsInjection || this.res.wantsSecurityRemoved)) { + debug('ungzipping response body') + + const gunzip = zlib.createGunzip(zlibOptions) + + this.incomingResStream = this.incomingResStream.pipe(gunzip).on('error', this.onError) + } else { + this.skipMiddleware('GzipBody') // not needed anymore + } + + this.next() +} + +const MaybeInjectHtml: ResponseMiddleware = function () { + if (!this.res.wantsInjection) { + return this.next() + } + + this.skipMiddleware('MaybeRemoveSecurity') // we only want to do one or the other + + debug('injecting into HTML') + + this.incomingResStream.pipe(concatStream((body) => { + const nodeCharset = getNodeCharsetFromResponse(this.incomingRes.headers, body) + const decodedBody = iconv.decode(body, nodeCharset) + const injectedBody = rewriter.html(decodedBody, this.getRemoteState().domainName, this.res.wantsInjection, this.res.wantsSecurityRemoved) + const encodedBody = iconv.encode(injectedBody, nodeCharset) + + const pt = new PassThrough + + pt.write(encodedBody) + pt.end() + + this.incomingResStream = pt + this.next() + })).on('error', this.onError) +} + +const MaybeRemoveSecurity: ResponseMiddleware = function () { + if (!this.res.wantsSecurityRemoved) { + return this.next() + } + + debug('removing JS framebusting code') + + this.incomingResStream.setEncoding('utf8') + this.incomingResStream = this.incomingResStream.pipe(rewriter.security()).on('error', this.onError) + this.next() +} + +const GzipBody: ResponseMiddleware = function () { + debug('regzipping response body') + this.incomingResStream = this.incomingResStream.pipe(zlib.createGzip(zlibOptions)).on('error', this.onError) + + this.next() +} + +const SendResponseBodyToClient: ResponseMiddleware = function () { + this.incomingResStream.pipe(this.res).on('error', this.onError) + this.res.on('end', () => this.end()) +} + +export default { + LogResponse, + PatchExpressSetHeader, + SetInjectionLevel, + OmitProblematicHeaders, + MaybePreventCaching, + CopyCookiesFromIncomingRes, + MaybeSendRedirectToClient, + CopyResponseStatusCode, + ClearCyInitialCookie, + MaybeEndWithEmptyBody, + MaybeGunzipBody, + MaybeInjectHtml, + MaybeRemoveSecurity, + GzipBody, + SendResponseBodyToClient, +} diff --git a/packages/proxy/lib/http/util/buffers.ts b/packages/proxy/lib/http/util/buffers.ts new file mode 100644 index 000000000000..9c22b6d4e73b --- /dev/null +++ b/packages/proxy/lib/http/util/buffers.ts @@ -0,0 +1,65 @@ +import _ from 'lodash' +import debugModule from 'debug' +import { uri } from '@packages/network' +import { Readable } from 'stream' +import { Response } from 'express' + +const debug = debugModule('cypress:proxy:http:util:buffers') + +export type HttpBuffer = { + details: object + originalUrl: string + response: Response + stream: Readable + url: string +} + +const stripPort = (url) => { + try { + return uri.removeDefaultPort(url).format() + } catch (e) { + return url + } +} + +export class HttpBuffers { + buffer: Optional = undefined + + reset (): void { + debug('resetting buffers') + + delete this.buffer + } + + set (obj) { + obj = _.cloneDeep(obj) + obj.url = stripPort(obj.url) + obj.originalUrl = stripPort(obj.originalUrl) + + if (this.buffer) { + debug('warning: overwriting existing buffer...', { buffer: _.pick(this.buffer, 'url') }) + } + + debug('setting buffer %o', _.pick(obj, 'url')) + + this.buffer = obj + } + + get (str): Optional { + if (this.buffer && this.buffer.url === stripPort(str)) { + return this.buffer + } + } + + take (str): Optional { + const foundBuffer = this.get(str) + + if (foundBuffer) { + delete this.buffer + + debug('found request buffer %o', { buffer: _.pick(foundBuffer, 'url') }) + + return foundBuffer + } + } +} diff --git a/packages/proxy/lib/http/util/inject.ts b/packages/proxy/lib/http/util/inject.ts new file mode 100644 index 000000000000..19b99bb0bea2 --- /dev/null +++ b/packages/proxy/lib/http/util/inject.ts @@ -0,0 +1,25 @@ +import { oneLine } from 'common-tags' + +export function partial (domain) { + return oneLine` + + ` +} + +export function full (domain) { + return oneLine` + + ` +} diff --git a/packages/server/lib/util/replace_stream.ts b/packages/proxy/lib/http/util/replace_stream.ts similarity index 92% rename from packages/server/lib/util/replace_stream.ts rename to packages/proxy/lib/http/util/replace_stream.ts index ee423ff6c8f3..ace4a8f6294b 100644 --- a/packages/server/lib/util/replace_stream.ts +++ b/packages/proxy/lib/http/util/replace_stream.ts @@ -12,7 +12,7 @@ const splitter: IGraphemeSplitter = new GraphemeSplitter() * UTF-8 grapheme aware stream replacer * https://github.com/cypress-io/cypress/pull/4984 */ -function replaceStream (patterns: RegExp | RegExp[], replacements: string | string[], options = { maxTailLength: 100 }) { +export function replaceStream (patterns: RegExp | RegExp[], replacements: string | string[], options = { maxTailLength: 100 }) { if (!Array.isArray(patterns)) { patterns = [patterns] } @@ -78,7 +78,3 @@ function replaceStream (patterns: RegExp | RegExp[], replacements: string | stri this.queue(null) }) } - -module.exports = { - replaceStream, -} diff --git a/packages/server/lib/util/rewriter.js b/packages/proxy/lib/http/util/rewriter.ts similarity index 58% rename from packages/server/lib/util/rewriter.js rename to packages/proxy/lib/http/util/rewriter.ts index ec675036f743..4829017d0a45 100644 --- a/packages/server/lib/util/rewriter.js +++ b/packages/proxy/lib/http/util/rewriter.ts @@ -1,22 +1,12 @@ -/* eslint-disable - default-case, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const inject = require('./inject') -const security = require('./security') +import * as inject from './inject' +import { strip, stripStream } from './security' const doctypeRe = /(<\!doctype.*?>)/i const headRe = /()/i const bodyRe = /()/i const htmlRe = /()/i -const rewriteHtml = function (html, domainName, wantsInjection, wantsSecurityRemoved) { +export function html (html: string, domainName: string, wantsInjection, wantsSecurityRemoved) { const replace = (re, str) => { return html.replace(re, str) } @@ -27,13 +17,15 @@ const rewriteHtml = function (html, domainName, wantsInjection, wantsSecurityRem return inject.full(domainName) case 'partial': return inject.partial(domainName) + default: + return } })() - //# strip clickjacking and framebusting - //# from the HTML if we've been told to + // strip clickjacking and framebusting + // from the HTML if we've been told to if (wantsSecurityRemoved) { - html = security.strip(html) + html = strip(html) } switch (false) { @@ -55,8 +47,4 @@ const rewriteHtml = function (html, domainName, wantsInjection, wantsSecurityRem } } -module.exports = { - html: rewriteHtml, - - security: security.stripStream, -} +export const security = stripStream diff --git a/packages/server/lib/util/security.js b/packages/proxy/lib/http/util/security.ts similarity index 87% rename from packages/server/lib/util/security.js rename to packages/proxy/lib/http/util/security.ts index 6e37c11bf516..9906b3aae273 100644 --- a/packages/server/lib/util/security.js +++ b/packages/proxy/lib/http/util/security.ts @@ -1,5 +1,3 @@ -// Tests located in packages/server/test/unit/security_spec - const pumpify = require('pumpify') const { replaceStream } = require('./replace_stream') const utf8Stream = require('utf8-stream') @@ -9,7 +7,7 @@ const topOrParentEqualityAfterRe = /(top|parent)((?:["']\])?\s*[!=]==?\s*(?:wind const topOrParentLocationOrFramesRe = /([^\da-zA-Z\(\)])?(top|parent)([.])(location|frames)/g const jiraTopWindowGetterRe = /(!function\s*\((\w{1})\)\s*{\s*return\s*\w{1}\s*(?:={2,})\s*\w{1}\.parent)(\s*}\(\w{1}\))/g -const strip = (html) => { +export function strip (html: string) { return html .replace(topOrParentEqualityBeforeRe, '$1self') .replace(topOrParentEqualityAfterRe, 'self$2') @@ -17,7 +15,7 @@ const strip = (html) => { .replace(jiraTopWindowGetterRe, '$1 || $2.parent.__Cypress__$3') } -const stripStream = () => { +export function stripStream () { return pumpify( utf8Stream(), replaceStream( @@ -36,9 +34,3 @@ const stripStream = () => { ) ) } - -module.exports = { - strip, - - stripStream, -} diff --git a/packages/proxy/lib/index.ts b/packages/proxy/lib/index.ts new file mode 100644 index 000000000000..4c603b280632 --- /dev/null +++ b/packages/proxy/lib/index.ts @@ -0,0 +1,7 @@ +export { NetworkProxy } from './network-proxy' + +export { ErrorMiddleware } from './http/error-middleware' + +export { RequestMiddleware } from './http/request-middleware' + +export { ResponseMiddleware } from './http/response-middleware' diff --git a/packages/proxy/lib/network-proxy.ts b/packages/proxy/lib/network-proxy.ts new file mode 100644 index 000000000000..5dca62ad96fc --- /dev/null +++ b/packages/proxy/lib/network-proxy.ts @@ -0,0 +1,26 @@ +import { Http } from './http' + +export class NetworkProxy { + http: Http + + constructor (opts: { + config: any + getRemoteState: () => any + middleware?: any + request: any + }) { + this.http = new Http(opts) + } + + handleHttpRequest (req, res) { + this.http.handle(req, res) + } + + setHttpBuffer (buffer) { + this.http.setBuffer(buffer) + } + + reset () { + this.http.reset() + } +} diff --git a/packages/proxy/package.json b/packages/proxy/package.json new file mode 100644 index 000000000000..bc6353d244fa --- /dev/null +++ b/packages/proxy/package.json @@ -0,0 +1,34 @@ +{ + "name": "@packages/proxy", + "version": "0.0.0", + "private": true, + "main": "index.js", + "scripts": { + "build-js": "bin-up tsc --project .", + "clean-deps": "rm -rf node_modules", + "test": "bin-up mocha --reporter mocha-multi-reporters --reporter-options configFile=../../mocha-reporter-config.json" + }, + "dependencies": { + "bluebird": "3.5.3", + "charset": "1.0.1", + "common-tags": "1.8.0", + "debug": "4.1.1", + "grapheme-splitter": "1.0.4", + "iconv-lite": "0.5.0", + "lodash": "4.17.15", + "pumpify": "1.5.1", + "through": "2.3.8", + "utf8-stream": "0.0.0" + }, + "devDependencies": { + "@cypress/sinon-chai": "^2.9.0", + "@types/express": "^4.17.1", + "bin-up": "1.2.0", + "request": "^2.88.0", + "request-promise": "^4.2.4" + }, + "files": [ + "lib" + ], + "types": "./lib/index.ts" +} diff --git a/packages/proxy/test/.eslintrc b/packages/proxy/test/.eslintrc new file mode 100644 index 000000000000..b5ed5206d083 --- /dev/null +++ b/packages/proxy/test/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": [ + "plugin:@cypress/dev/tests" + ] +} diff --git a/packages/proxy/test/mocha.opts b/packages/proxy/test/mocha.opts new file mode 100644 index 000000000000..7a20898f8d83 --- /dev/null +++ b/packages/proxy/test/mocha.opts @@ -0,0 +1,5 @@ +test/unit +--compilers ts:@packages/ts/register,coffee:@packages/coffee/register +--timeout 10000 +--recursive +--require test/pretest.ts diff --git a/packages/proxy/test/pretest.ts b/packages/proxy/test/pretest.ts new file mode 100644 index 000000000000..471760d6c14d --- /dev/null +++ b/packages/proxy/test/pretest.ts @@ -0,0 +1,2 @@ +require('chai') +.use(require('@cypress/sinon-chai')) diff --git a/packages/proxy/test/unit/http/error-middleware.spec.ts b/packages/proxy/test/unit/http/error-middleware.spec.ts new file mode 100644 index 000000000000..e2baeca38c0e --- /dev/null +++ b/packages/proxy/test/unit/http/error-middleware.spec.ts @@ -0,0 +1,75 @@ +import _ from 'lodash' +import ErrorMiddleware, { + AbortRequest, + UnpipeResponse, + DestroyResponse, +} from '../../../lib/http/error-middleware' +import { expect } from 'chai' +import sinon from 'sinon' +import { + testMiddleware, +} from './helpers' + +describe('http/error-middleware', function () { + it('exports the members in the correct order', function () { + expect(_.keys(ErrorMiddleware)).to.have.ordered.members([ + 'LogError', + 'AbortRequest', + 'UnpipeResponse', + 'DestroyResponse', + ]) + }) + + context('AbortRequest', function () { + it('destroys outgoingReq if it exists', function () { + const ctx = { + outgoingReq: { + abort: sinon.stub(), + }, + } + + return testMiddleware([AbortRequest], ctx) + .then(() => { + expect(ctx.outgoingReq.abort).to.be.calledOnce + }) + }) + + it('does not destroy outgoingReq if it does not exist', function () { + return testMiddleware([AbortRequest], {}) + }) + }) + + context('UnpipeResponse', function () { + it('unpipes incomingRes if it exists', function () { + const ctx = { + incomingResStream: { + unpipe: sinon.stub(), + }, + } + + return testMiddleware([UnpipeResponse], ctx) + .then(() => { + expect(ctx.incomingResStream.unpipe).to.be.calledOnce + }) + }) + + it('does not unpipe incomingRes if it does not exist', function () { + return testMiddleware([UnpipeResponse], {}) + }) + }) + + context('DestroyResponse', function () { + it('destroys the response', function () { + const ctx = { + res: { + destroy: sinon.stub(), + }, + } + + return testMiddleware([DestroyResponse], ctx) + .then(() => { + expect(ctx.res.destroy).to.be.calledOnce + }) + }) + }) +}) diff --git a/packages/proxy/test/unit/http/helpers.ts b/packages/proxy/test/unit/http/helpers.ts new file mode 100644 index 000000000000..70b1720fdbf5 --- /dev/null +++ b/packages/proxy/test/unit/http/helpers.ts @@ -0,0 +1,18 @@ +import { HttpMiddleware, _runStage } from '../../../lib/http' + +export function testMiddleware (middleware: HttpMiddleware[], ctx = {}) { + const fullCtx = { + req: {}, + res: {}, + config: {}, + getRemoteState: () => {}, + + middleware: { + 0: middleware, + }, + + ...ctx, + } + + return _runStage(0, fullCtx) +} diff --git a/packages/proxy/test/unit/http/index.spec.ts b/packages/proxy/test/unit/http/index.spec.ts new file mode 100644 index 000000000000..e8e29d3988d2 --- /dev/null +++ b/packages/proxy/test/unit/http/index.spec.ts @@ -0,0 +1,160 @@ +import { Http, HttpStages } from '../../../lib/http' +import { expect } from 'chai' +import sinon from 'sinon' + +describe('http', function () { + context('Http.handle', function () { + let config + let getRemoteState + let middleware + let incomingRequest + let incomingResponse + let error + + beforeEach(function () { + config = {} + getRemoteState = sinon.stub().returns({}) + + incomingRequest = sinon.stub() + incomingResponse = sinon.stub() + error = sinon.stub() + + middleware = { + [HttpStages.IncomingRequest]: [incomingRequest], + [HttpStages.IncomingResponse]: [incomingResponse], + [HttpStages.Error]: [error], + } + }) + + it('calls IncomingRequest stack, then IncomingResponse stack', function () { + incomingRequest.callsFake(function () { + expect(incomingResponse).to.not.be.called + expect(error).to.not.be.called + + this.incomingRes = {} + + this.end() + }) + + incomingResponse.callsFake(function () { + expect(incomingRequest).to.be.calledOnce + expect(error).to.not.be.called + + this.end() + }) + + return new Http({ config, getRemoteState, middleware }) + .handle({}, {}) + .then(function () { + expect(incomingRequest).to.be.calledOnce + expect(incomingResponse).to.be.calledOnce + expect(error).to.not.be.called + }) + }) + + it('moves to Error stack if err in IncomingRequest', function () { + incomingRequest.throws(new Error('oops')) + + error.callsFake(function () { + expect(this.error.message).to.eq('oops') + this.end() + }) + + return new Http({ config, getRemoteState, middleware }) + .handle({}, {}) + .then(function () { + expect(incomingRequest).to.be.calledOnce + expect(incomingResponse).to.not.be.called + expect(error).to.be.calledOnce + }) + }) + + it('moves to Error stack if err in IncomingResponse', function () { + incomingRequest.callsFake(function () { + this.incomingRes = {} + this.end() + }) + + incomingResponse.throws(new Error('oops')) + + error.callsFake(function () { + expect(this.error.message).to.eq('oops') + this.end() + }) + + return new Http({ config, getRemoteState, middleware }) + .handle({}, {}) + .then(function () { + expect(incomingRequest).to.be.calledOnce + expect(incomingResponse).to.be.calledOnce + expect(error).to.be.calledOnce + }) + }) + + it('self can be modified by middleware and passed on', function () { + const reqAdded = {} + const resAdded = {} + const errorAdded = {} + + let expectedKeys = ['req', 'res', 'config', 'getRemoteState', 'middleware'] + + incomingRequest.callsFake(function () { + expect(this).to.include.keys(expectedKeys) + this.reqAdded = reqAdded + expectedKeys.push('reqAdded') + this.next() + }) + + const incomingRequest2 = sinon.stub().callsFake(function () { + expect(this).to.include.keys(expectedKeys) + expect(this.reqAdded).to.equal(reqAdded) + this.incomingRes = {} + expectedKeys.push('incomingRes') + this.end() + }) + + incomingResponse.callsFake(function () { + expect(this).to.include.keys(expectedKeys) + this.resAdded = resAdded + expectedKeys.push('resAdded') + this.next() + }) + + const incomingResponse2 = sinon.stub().callsFake(function () { + expect(this).to.include.keys(expectedKeys) + expect(this.resAdded).to.equal(resAdded) + expectedKeys.push('error') + throw new Error('goto error stack') + }) + + error.callsFake(function () { + expect(this.error.message).to.eq('goto error stack') + expect(this).to.include.keys(expectedKeys) + this.errorAdded = errorAdded + this.next() + }) + + const error2 = sinon.stub().callsFake(function () { + expect(this).to.include.keys(expectedKeys) + expect(this.errorAdded).to.equal(errorAdded) + this.end() + }) + + middleware[HttpStages.IncomingRequest].push(incomingRequest2) + middleware[HttpStages.IncomingResponse].push(incomingResponse2) + middleware[HttpStages.Error].push(error2) + + return new Http({ config, getRemoteState, middleware }) + .handle({}, {}) + .then(function () { + [ + incomingRequest, incomingRequest2, + incomingResponse, incomingResponse2, + error, error2, + ].forEach(function (fn) { + expect(fn).to.be.calledOnce + }) + }) + }) + }) +}) diff --git a/packages/proxy/test/unit/http/request-middleware.spec.ts b/packages/proxy/test/unit/http/request-middleware.spec.ts new file mode 100644 index 000000000000..5fb61b6839e9 --- /dev/null +++ b/packages/proxy/test/unit/http/request-middleware.spec.ts @@ -0,0 +1,18 @@ +import _ from 'lodash' +import RequestMiddleware from '../../../lib/http/request-middleware' +import { expect } from 'chai' + +describe('http/request-middleware', function () { + it('exports the members in the correct order', function () { + expect(_.keys(RequestMiddleware)).to.have.ordered.members([ + 'LogRequest', + 'RedirectToClientRouteIfUnloaded', + 'RedirectToClientRouteIfNotProxied', + 'EndRequestsToBlacklistedHosts', + 'MaybeEndRequestWithBufferedResponse', + 'StripUnsupportedAcceptEncoding', + 'MaybeSetBasicAuthHeaders', + 'SendRequestOutgoing', + ]) + }) +}) diff --git a/packages/proxy/test/unit/http/response-middleware.spec.ts b/packages/proxy/test/unit/http/response-middleware.spec.ts new file mode 100644 index 000000000000..8933879254f4 --- /dev/null +++ b/packages/proxy/test/unit/http/response-middleware.spec.ts @@ -0,0 +1,25 @@ +import _ from 'lodash' +import ResponseMiddleware from '../../../lib/http/response-middleware' +import { expect } from 'chai' + +describe('http/response-middleware', function () { + it('exports the members in the correct order', function () { + expect(_.keys(ResponseMiddleware)).to.have.ordered.members([ + 'LogResponse', + 'PatchExpressSetHeader', + 'SetInjectionLevel', + 'OmitProblematicHeaders', + 'MaybePreventCaching', + 'CopyCookiesFromIncomingRes', + 'MaybeSendRedirectToClient', + 'CopyResponseStatusCode', + 'ClearCyInitialCookie', + 'MaybeEndWithEmptyBody', + 'MaybeGunzipBody', + 'MaybeInjectHtml', + 'MaybeRemoveSecurity', + 'GzipBody', + 'SendResponseBodyToClient', + ]) + }) +}) diff --git a/packages/proxy/test/unit/http/util/buffers.spec.ts b/packages/proxy/test/unit/http/util/buffers.spec.ts new file mode 100644 index 000000000000..9eb132c15d57 --- /dev/null +++ b/packages/proxy/test/unit/http/util/buffers.spec.ts @@ -0,0 +1,62 @@ +import { expect } from 'chai' +import { HttpBuffers, HttpBuffer } from '../../../../lib/http/util/buffers' + +describe('http/util/buffers', () => { + let buffers: HttpBuffers + + beforeEach(() => { + buffers = new HttpBuffers() + }) + + context('#get', () => { + it('returns buffer by url', () => { + const obj = { url: 'foo' } as HttpBuffer + + buffers.set(obj) + + const buffer = buffers.get('foo') as HttpBuffer + + expect(buffer.url).to.eq(obj.url) + }) + + it('falls back to setting the port when buffer could not be found', () => { + const obj = { url: 'https://www.google.com/' } as HttpBuffer + + buffers.set(obj) + + const buffer = buffers.get('https://www.google.com:443/') as HttpBuffer + + expect(buffer.url).to.eq(obj.url) + }) + }) + + context('#take', () => { + it('removes the found buffer', () => { + const obj = { url: 'https://www.google.com/' } as HttpBuffer + + buffers.set(obj) + + expect(buffers.buffer).to.exist + + const buffer = buffers.take('https://www.google.com:443/') as HttpBuffer + + expect(buffer.url).to.eq(obj.url) + + expect(buffers.buffer).to.be.undefined + }) + + it('does not remove anything when not found', () => { + const obj = { url: 'https://www.google.com/' } as HttpBuffer + + buffers.set(obj) + + expect(buffers.buffer).to.exist + + const buffer = buffers.take('asdf') + + expect(buffer).to.be.undefined + + expect(buffers.buffer).to.exist + }) + }) +}) diff --git a/packages/server/test/unit/replace_stream_spec.js b/packages/proxy/test/unit/http/util/replace_stream_spec.js similarity index 98% rename from packages/server/test/unit/replace_stream_spec.js rename to packages/proxy/test/unit/http/util/replace_stream_spec.js index b0f7e41887bd..89f7c8cf1e2f 100644 --- a/packages/server/test/unit/replace_stream_spec.js +++ b/packages/proxy/test/unit/http/util/replace_stream_spec.js @@ -29,11 +29,10 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -require('../spec_helper') - -const concatStream = require('concat-stream') -const { passthruStream } = require(`${root}lib/util/passthru_stream`) -const { replaceStream } = require(`${root}lib/util/replace_stream`) +const { concatStream } = require('@packages/network') +const { expect } = require('chai') +const { PassThrough } = require('stream') +const { replaceStream } = require(`../../../../lib/http/util/replace_stream`) const script = [ ' - ` - }, - - full (domain) { - return oneLine` - - ` - }, -} diff --git a/packages/server/lib/util/passthru_stream.js b/packages/server/lib/util/passthru_stream.js deleted file mode 100644 index dcfca425a8a5..000000000000 --- a/packages/server/lib/util/passthru_stream.js +++ /dev/null @@ -1,9 +0,0 @@ -const through = require('through') - -module.exports = { - passthruStream () { - return through(function (chunk) { - this.queue(chunk) - }) - }, -} diff --git a/packages/server/package.json b/packages/server/package.json index e62665383bfb..01c3320fbfda 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -51,7 +51,6 @@ "browserify": "16.3.0", "chai": "1.10.0", "chalk": "2.4.2", - "charset": "1.0.1", "check-more-types": "2.24.0", "chokidar": "3.2.2", "chrome-remote-interface": "0.28.0", @@ -60,7 +59,6 @@ "color-string": "1.5.3", "common-tags": "1.8.0", "compression": "1.7.4", - "concat-stream": "1.6.2", "content-type": "1.0.4", "cookie-parser": "1.4.4", "data-uri-to-buffer": "2.0.1", @@ -79,13 +77,10 @@ "get-port": "5.0.0", "getos": "3.1.1", "glob": "7.1.3", - "graceful-fs": "4.2.3", - "grapheme-splitter": "1.0.4", - "http-accept": "0.1.6", + "graceful-fs": "4.2.0", "http-proxy": "1.17.0", "http-status-codes": "1.3.2", "human-interval": "0.1.6", - "iconv-lite": "0.5.0", "image-size": "0.7.4", "is-fork-pr": "2.5.0", "is-html": "2.0.0", @@ -110,9 +105,7 @@ "opn": "cypress-io/opn#2f4e9a216ca7bdb95dfae9d46d99ddf004b3cbb5", "ospath": "1.2.2", "p-queue": "6.1.0", - "parse-domain": "2.3.4", "pluralize": "8.0.0", - "pumpify": "1.5.1", "ramda": "0.24.1", "randomstring": "1.1.5", "request": "cypress-io/request#47cdc67085c9fddc8d39d3172538f3f86c96bb8b", @@ -128,13 +121,11 @@ "strip-ansi": "3.0.1", "syntax-error": "1.4.0", "term-size": "2.1.0", - "through": "2.3.8", "tough-cookie": "3.0.1", "trash": "5.2.0", "underscore": "1.9.1", "underscore.string": "3.3.5", "url-parse": "1.4.7", - "utf8-stream": "0.0.0", "uuid": "3.3.2", "which": "1.3.1", "widest-line": "3.1.0", diff --git a/packages/server/test/integration/http_requests_spec.coffee b/packages/server/test/integration/http_requests_spec.coffee index f5510b6a770f..b7b3d5e96414 100644 --- a/packages/server/test/integration/http_requests_spec.coffee +++ b/packages/server/test/integration/http_requests_spec.coffee @@ -2453,8 +2453,7 @@ describe "Routes", -> "Cookie": "__cypress.initial=false" } - if type? - headers["Accept"] = type + headers["Accept"] = type @rp({ url: "http://www.google.com/iframe" diff --git a/packages/server/test/integration/server_spec.coffee b/packages/server/test/integration/server_spec.coffee index efd2950c8eb9..f9e6940074b7 100644 --- a/packages/server/test/integration/server_spec.coffee +++ b/packages/server/test/integration/server_spec.coffee @@ -6,7 +6,6 @@ rp = require("request-promise") Promise = require("bluebird") evilDns = require("evil-dns") httpsServer = require("#{root}../https-proxy/test/helpers/https_server") -buffers = require("#{root}lib/util/buffers") config = require("#{root}lib/config") Server = require("#{root}lib/server") Fixtures = require("#{root}test/support/helpers/fixtures") @@ -85,6 +84,8 @@ describe "Server", -> @proxy = "http://localhost:" + port + @buffers = @server._networkProxy.http.buffers + @fileServer = @server._fileServer.address() ]) @@ -170,7 +171,7 @@ describe "Server", -> cookies: [] }) - expect(buffers.getAny()).to.include({ url: "http://localhost:2000/index.html" }) + expect(@buffers.buffer).to.include({ url: "http://localhost:2000/index.html" }) .then => @server._onResolveUrl("/index.html", {}, @automationRequest) .then (obj = {}) => @@ -195,7 +196,7 @@ describe "Server", -> expect(res.body).to.include("document.domain") expect(res.body).to.include("localhost") expect(res.body).to.include("Cypress") - expect(buffers.getAny()).to.be.null + expect(@buffers.buffer).to.be.undefined it "can follow static file redirects", -> @server._onResolveUrl("/sub", {}, @automationRequest) @@ -266,13 +267,13 @@ describe "Server", -> cookies: [] }) - expect(buffers.getAny()).to.include({ url: "http://localhost:2000/index.html" }) + expect(@buffers.buffer).to.include({ url: "http://localhost:2000/index.html" }) .then => @rp("http://localhost:2000/index.html") - .then (res) -> + .then (res) => expect(res.statusCode).to.eq(200) - expect(buffers.getAny()).to.be.null + expect(@buffers.buffer).to.be.undefined describe "http", -> beforeEach -> @@ -530,7 +531,7 @@ describe "Server", -> ] }) - expect(buffers.getAny()).to.include({ url: "http://espn.go.com/" }) + expect(@buffers.buffer).to.include({ url: "http://espn.go.com/" }) .then => @server._onResolveUrl("http://espn.com/", {}, @automationRequest) .then (obj = {}) => @@ -557,7 +558,7 @@ describe "Server", -> expect(res.body).to.include("document.domain") expect(res.body).to.include("go.com") expect(res.body).to.include("Cypress.action('app:window:before:load', window); espn") - expect(buffers.getAny()).to.be.null + expect(@buffers.buffer).to.be.undefined it "does not buffer 'bad' responses", -> sinon.spy(@server._request, "sendStream") @@ -655,7 +656,7 @@ describe "Server", -> }) @server._onResolveUrl("http://getbootstrap.com/#/foo", {}, @automationRequest) - .then (obj = {}) -> + .then (obj = {}) => expectToEqDetails(obj, { isOkStatusCode: true isHtml: true @@ -668,13 +669,13 @@ describe "Server", -> cookies: [] }) - expect(buffers.getAny()).to.include({ url: "http://getbootstrap.com/" }) + expect(@buffers.buffer).to.include({ url: "http://getbootstrap.com/" }) .then => @rp("http://getbootstrap.com/") - .then (res) -> + .then (res) => expect(res.statusCode).to.eq(200) - expect(buffers.getAny()).to.be.null + expect(@buffers.buffer).to.be.undefined it "can serve non 2xx status code requests when option set", -> nock("http://google.com") diff --git a/packages/server/test/unit/buffers_spec.js b/packages/server/test/unit/buffers_spec.js deleted file mode 100644 index ae0f87a067f1..000000000000 --- a/packages/server/test/unit/buffers_spec.js +++ /dev/null @@ -1,65 +0,0 @@ -require('../spec_helper') - -const buffers = require(`${root}lib/util/buffers`) - -describe('lib/util/buffers', () => { - beforeEach(() => { - buffers.reset() - }) - - afterEach(() => { - buffers.reset() - }) - - context('#get', () => { - it('returns buffer by url', () => { - const obj = { url: 'foo' } - - buffers.set(obj) - - const buffer = buffers.get('foo') - - expect(buffer.url).to.eq(obj.url) - }) - - it('falls back to setting the port when buffer could not be found', () => { - const obj = { url: 'https://www.google.com/' } - - buffers.set(obj) - - const buffer = buffers.get('https://www.google.com:443/') - - expect(buffer.url).to.eq(obj.url) - }) - }) - - context('#take', () => { - it('removes the found buffer', () => { - const obj = { url: 'https://www.google.com/' } - - buffers.set(obj) - - expect(buffers.getAny()).to.exist - - const buffer = buffers.take('https://www.google.com:443/') - - expect(buffer.url).to.eq(obj.url) - - expect(buffers.getAny()).to.be.null - }) - - it('does not remove anything when not found', () => { - const obj = { url: 'https://www.google.com/' } - - buffers.set(obj) - - expect(buffers.getAny()).to.exist - - const buffer = buffers.take('asdf') - - expect(buffer).to.be.undefined - - expect(buffers.getAny()).to.exist - }) - }) -}) diff --git a/packages/server/test/unit/server_spec.coffee b/packages/server/test/unit/server_spec.coffee index 5bad0ce26ece..6edc2731943f 100644 --- a/packages/server/test/unit/server_spec.coffee +++ b/packages/server/test/unit/server_spec.coffee @@ -13,7 +13,6 @@ Server = require("#{root}lib/server") Socket = require("#{root}lib/socket") fileServer = require("#{root}lib/file_server") ensureUrl = require("#{root}lib/util/ensure-url") -buffers = require("#{root}lib/util/buffers") morganFn = -> mockery.registerMock("morgan", -> morganFn) @@ -193,11 +192,14 @@ describe "lib/server", -> context "#reset", -> beforeEach -> - sinon.stub(buffers, "reset") + @server.open(@config) + .then => + @buffers = @server._networkProxy.http + sinon.stub(@buffers, "reset") it "resets the buffers", -> @server.reset() - expect(buffers.reset).to.be.called + expect(@buffers.reset).to.be.called it "sets the domain to the previous base url if set", -> @server._baseUrl = "http://localhost:3000" diff --git a/packages/server/test/unit/stream_buffer_spec.js b/packages/server/test/unit/stream_buffer_spec.js index e369193b1b74..bfb5db3da574 100644 --- a/packages/server/test/unit/stream_buffer_spec.js +++ b/packages/server/test/unit/stream_buffer_spec.js @@ -4,7 +4,7 @@ const _ = require('lodash') const fs = require('fs') const stream = require('stream') const Promise = require('bluebird') -const concatStream = require('concat-stream') +const { concatStream } = require('@packages/network') const { streamBuffer } = require('../../lib/util/stream_buffer') function drain (stream) { diff --git a/packages/ts/index.d.ts b/packages/ts/index.d.ts index b1f619bcec07..47f70d38414a 100644 --- a/packages/ts/index.d.ts +++ b/packages/ts/index.d.ts @@ -90,6 +90,12 @@ declare interface SymbolConstructor { for(str: string): SymbolConstructor } +declare module 'url' { + interface UrlWithStringQuery { + format(): string + } +} + declare interface InternalStream { queue(str: string | null): void }