From 11e5a1eea1d2414fcdf2c70e1e3b73792a5fc21d Mon Sep 17 00:00:00 2001 From: LavrovArtem Date: Tue, 10 Oct 2017 17:51:02 +0300 Subject: [PATCH] fix `Override WebSocket class` (close #911) --- src/client/sandbox/native-methods.js | 1 + src/client/sandbox/node/window.js | 16 ++++++++ src/client/utils/url.js | 21 +++++++--- src/proxy/index.js | 10 +++++ src/request-pipeline/context.js | 3 ++ .../destination-request/index.js | 8 ++++ src/request-pipeline/index.js | 38 ++++++++++++++++++- src/utils/url.js | 25 ++++++++---- 8 files changed, 107 insertions(+), 15 deletions(-) diff --git a/src/client/sandbox/native-methods.js b/src/client/sandbox/native-methods.js index cb51398c9..72080944f 100644 --- a/src/client/sandbox/native-methods.js +++ b/src/client/sandbox/native-methods.js @@ -248,6 +248,7 @@ class NativeMethods { this.StorageEvent = win.StorageEvent; this.MutationObserver = win.MutationObserver; this.EventSource = win.EventSource; + this.WebSocket = win.WebSocket; if (win.DataTransfer) this.DataTransfer = win.DataTransfer; diff --git a/src/client/sandbox/node/window.js b/src/client/sandbox/node/window.js index cfd1c3715..db648c5d8 100644 --- a/src/client/sandbox/node/window.js +++ b/src/client/sandbox/node/window.js @@ -420,6 +420,22 @@ export default class WindowSandbox extends SandboxBase { }; } + if (window.WebSocket) { + window.WebSocket = function (url, protocols) { + if (arguments.length === 0) + return new nativeMethods.WebSocket(); + + const proxyUrl = getProxyUrl(url, { resourceType: stringifyResourceType({ isWebSocket: true }) }); + + if (arguments.length === 1) + return new nativeMethods.WebSocket(proxyUrl); + else if (arguments.length === 2) + return new nativeMethods.WebSocket(proxyUrl, protocols); + + return new nativeMethods.WebSocket(proxyUrl, protocols, arguments[2]); + }; + } + // NOTE: DOMParser supports an HTML parsing for IE10 and later if (window.DOMParser && !isIE9) { window.DOMParser.prototype.parseFromString = function (...args) { diff --git a/src/client/utils/url.js b/src/client/utils/url.js index 93bef0a7b..2b3d75b76 100644 --- a/src/client/utils/url.js +++ b/src/client/utils/url.js @@ -9,7 +9,10 @@ const HASH_RE = /#[\S\s]*$/; export const REQUEST_DESCRIPTOR_VALUES_SEPARATOR = sharedUrlUtils.REQUEST_DESCRIPTOR_VALUES_SEPARATOR; export function getProxyUrl (url, opts) { - if (!isSupportedProtocol(url) && !isSpecialPage(url)) + const resourceType = opts && opts.resourceType; + const parsedResourceType = sharedUrlUtils.parseResourceType(resourceType); + + if (!isSupportedProtocol(url, parsedResourceType.isWebSocket) && !isSpecialPage(url)) return url; // NOTE: Resolves relative URLs. @@ -18,10 +21,10 @@ export function getProxyUrl (url, opts) { if (!sharedUrlUtils.isValidUrl(url)) return url; + const proxyProtocol = parsedResourceType.isWebSocket ? 'ws:' : void 0; const proxyHostname = opts && opts.proxyHostname || location.hostname; const proxyPort = opts && opts.proxyPort || location.port.toString(); const sessionId = opts && opts.sessionId || settings.get().sessionId; - const resourceType = opts && opts.resourceType; let charset = opts && opts.charset; const crossDomainPort = settings.get().crossDomainProxyPort === proxyPort ? @@ -42,6 +45,7 @@ export function getProxyUrl (url, opts) { const destUrl = sharedUrlUtils.formatUrl(parsedProxyUrl.destResourceInfo); return getProxyUrl(destUrl, { + proxyProtocol, proxyHostname, proxyPort, sessionId, @@ -51,9 +55,8 @@ export function getProxyUrl (url, opts) { } const parsedUrl = sharedUrlUtils.parseUrl(url); - const isScript = sharedUrlUtils.parseResourceType(resourceType).isScript; - charset = charset || isScript && document[INTERNAL_PROPS.documentCharset]; + charset = charset || parsedResourceType.isScript && document[INTERNAL_PROPS.documentCharset]; // NOTE: It seems that the relative URL had the leading slash or dots, so that the proxy info path part was // removed by the resolver and we have an origin URL with the incorrect host and protocol. @@ -68,7 +71,13 @@ export function getProxyUrl (url, opts) { url = sharedUrlUtils.formatUrl(parsedUrl); } + if (parsedResourceType.isWebSocket) { + parsedUrl.protocol = parsedUrl.protocol.replace('ws', 'http'); + url = sharedUrlUtils.formatUrl(parsedUrl); + } + return sharedUrlUtils.getProxyUrl(url, { + proxyProtocol, proxyHostname, proxyPort, sessionId, @@ -132,8 +141,8 @@ export function isSubDomain (domain, subDomain) { return sharedUrlUtils.isSubDomain(domain, subDomain); } -export function isSupportedProtocol (url) { - return sharedUrlUtils.isSupportedProtocol(url); +export function isSupportedProtocol (url, resourceType) { + return sharedUrlUtils.isSupportedProtocol(url, resourceType); } export function isSpecialPage (url) { diff --git a/src/proxy/index.js b/src/proxy/index.js index 67be1adf3..8ca6793ff 100644 --- a/src/proxy/index.js +++ b/src/proxy/index.js @@ -44,6 +44,9 @@ export default class Proxy extends Router { this.server1 = http.createServer((req, res) => this._onRequest(req, res, this.server1Info)); this.server2 = http.createServer((req, res) => this._onRequest(req, res, this.server2Info)); + this.server1.on('upgrade', (req, socket, head) => this._onUpgradeRequest(req, socket, head, this.server1Info)); + this.server2.on('upgrade', (req, socket, head) => this._onUpgradeRequest(req, socket, head, this.server2Info)); + this.server1.listen(port1); this.server2.listen(port2); @@ -139,6 +142,13 @@ export default class Proxy extends Router { runRequestPipeline(req, res, serverInfo, this.openSessions); } + _onUpgradeRequest (req, socket, head, serverInfo) { + if (head && head.length) + socket.unshift(head); + + this._onRequest(req, socket, serverInfo); + } + _processStaticContent (handler) { if (handler.isShadowUIStylesheet) handler.content = prepareShadowUIStylesheet(handler.content); diff --git a/src/request-pipeline/context.js b/src/request-pipeline/context.js index db443e433..e5eb1ca01 100644 --- a/src/request-pipeline/context.js +++ b/src/request-pipeline/context.js @@ -25,6 +25,7 @@ export default class RequestPipelineContext { this.isFetch = false; this.isPage = false; this.isHtmlImport = false; + this.isWebSocket = false; this.isIframe = false; this.isSpecialPage = false; this.contentInfo = null; @@ -55,6 +56,7 @@ export default class RequestPipelineContext { isScript: parsedResourceType.isScript, isEventSource: parsedResourceType.isEventSource, isHtmlImport: parsedResourceType.isHtmlImport, + isWebSocket: parsedResourceType.isWebSocket, charset: parsed.charset }; @@ -115,6 +117,7 @@ export default class RequestPipelineContext { this.isPage = !this.isXhr && acceptHeader && contentTypeUtils.isPage(acceptHeader) || this.dest.isHtmlImport; this.isHtmlImport = this.dest.isHtmlImport; + this.isWebSocket = this.dest.isWebSocket; this.isIframe = this.dest.isIframe; this.isSpecialPage = urlUtils.isSpecialPage(this.dest.url); this.isFileProtocol = this.dest.protocol === 'file:'; diff --git a/src/request-pipeline/destination-request/index.js b/src/request-pipeline/destination-request/index.js index dd683d9c4..ae8477724 100644 --- a/src/request-pipeline/destination-request/index.js +++ b/src/request-pipeline/destination-request/index.js @@ -51,6 +51,7 @@ export default class DestinationRequest extends EventEmitter { this.req.on('response', res => this._onResponse(res)); this.req.on('error', err => this._onError(err)); + this.req.on('upgrade', (res, socket, head) => this._onResponse(res, socket, head)); this.req.setTimeout(timeout, () => this._onTimeout()); this.req.write(this.opts.body); this.req.end(); @@ -82,6 +83,13 @@ export default class DestinationRequest extends EventEmitter { } } + _onUpgrade (res, socket, head) { + if (head && head.length) + socket.unshift(head); + + this._onResponse(res); + } + async _resendWithCredentials (res) { addCredentials(this.opts.credentials, this.opts, res, this.protocolInterface); this.credentialsSent = true; diff --git a/src/request-pipeline/index.js b/src/request-pipeline/index.js index 74edac16c..670718e08 100644 --- a/src/request-pipeline/index.js +++ b/src/request-pipeline/index.js @@ -61,8 +61,13 @@ const stages = { if (ctx.contentInfo.requireProcessing && ctx.destRes.statusCode === 204) ctx.destRes.statusCode = 200; + if (ctx.isWebSocket) { + performWebSocketConnection(ctx); + + return; + } // NOTE: Just pipe the content body to the browser if we don't need to process it. - if (!ctx.contentInfo.requireProcessing) { + else if (!ctx.contentInfo.requireProcessing) { sendResponseHeaders(ctx); if (!ctx.isSpecialPage) { @@ -168,6 +173,37 @@ function sendResponseHeaders (ctx) { ctx.res.addTrailers(ctx.destRes.trailers); } +function performWebSocketConnection (ctx) { + const headers = headerTransforms.forResponse(ctx); + + writeWebSocketHead(ctx.res, ctx.destRes, headers); + + ctx.destRes.socket.pipe(ctx.res); + ctx.res.pipe(ctx.destRes.socket); +} + +function writeWebSocketHead (socket, destRes, headers) { + const { httpVersion, statusCode, statusMessage } = destRes; + + const resRaw = [`HTTP/${httpVersion} ${statusCode} ${statusMessage}`]; + const headersNames = Object.keys(headers); + + for (const headerName of headersNames) { + const headerValue = headers[headerName]; + + if (Array.isArray(headerValue)) { + for (const value of headerValue) + resRaw.push(headerName + ': ' + value); + } + else + resRaw.push(headerName + ': ' + headerValue); + } + + resRaw.push('', ''); + + socket.write(resRaw.join('\r\n')); +} + function error (ctx, err) { if (ctx.isPage && !ctx.isIframe) ctx.session.handlePageError(ctx, err); diff --git a/src/utils/url.js b/src/utils/url.js index 79d7cbed9..4d4a8a594 100644 --- a/src/utils/url.js +++ b/src/utils/url.js @@ -14,6 +14,7 @@ const QUERY_AND_HASH_RE = /(\?.+|#[^#]*)$/; const PATH_AFTER_HOST_RE = /^\/([^\/]+?)\/([\S\s]+)$/; export const SUPPORTED_PROTOCOL_RE = /^(https?|file):/i; +export const SUPPORTED_WEB_SOCKET_PROTOCOL_RE = /^(https?|wss?):/i; export const HASH_RE = /^#/; export const REQUEST_DESCRIPTOR_VALUES_SEPARATOR = '!'; export const SPECIAL_PAGES = ['about:blank', 'about:error']; @@ -25,7 +26,8 @@ export function parseResourceType (resourceType) { isForm: false, isScript: false, isEventSource: false, - isHtmlImport: false + isHtmlImport: false, + isWebSocket: false }; } @@ -34,15 +36,16 @@ export function parseResourceType (resourceType) { isForm: /f/.test(resourceType), isScript: /s/.test(resourceType), isEventSource: /e/.test(resourceType), - isHtmlImport: /h/.test(resourceType) + isHtmlImport: /h/.test(resourceType), + isWebSocket: /w/.test(resourceType) }; } export function getResourceTypeString (resourceType) { resourceType = resourceType || {}; - if (!resourceType.isIframe && !resourceType.isForm && !resourceType.isScript && - !resourceType.isEventSource && !resourceType.isHtmlImport) + if (!resourceType.isIframe && !resourceType.isForm && !resourceType.isScript && !resourceType.isEventSource && + !resourceType.isHtmlImport && !resourceType.isWebSocket) return null; return [ @@ -50,7 +53,8 @@ export function getResourceTypeString (resourceType) { resourceType.isForm ? 'f' : '', resourceType.isScript ? 's' : '', resourceType.isEventSource ? 'e' : '', - resourceType.isHtmlImport ? 'h' : '' + resourceType.isHtmlImport ? 'h' : '', + resourceType.isWebSocket ? 'w' : '' ].join(''); } @@ -117,7 +121,10 @@ export function getProxyUrl (url, opts) { params = params.join(REQUEST_DESCRIPTOR_VALUES_SEPARATOR); - return 'http://' + opts.proxyHostname + ':' + opts.proxyPort + '/' + params + '/' + convertHostToLowerCase(url); + const proxyProtocol = opts.proxyProtocol || 'http:'; + + return proxyProtocol + '//' + opts.proxyHostname + ':' + opts.proxyPort + '/' + params + '/' + + convertHostToLowerCase(url); } export function getDomain (parsed) { @@ -223,7 +230,7 @@ export function parseUrl (url) { return parsed; } -export function isSupportedProtocol (url) { +export function isSupportedProtocol (url, isWebSocket) { url = trim(url || ''); const isHash = HASH_RE.test(url); @@ -236,7 +243,9 @@ export function isSupportedProtocol (url) { if (!protocol) return true; - return SUPPORTED_PROTOCOL_RE.test(protocol[0]); + const supportedProtocolRe = isWebSocket ? SUPPORTED_WEB_SOCKET_PROTOCOL_RE : SUPPORTED_PROTOCOL_RE; + + return supportedProtocolRe.test(protocol[0]); } export function resolveUrlAsDest (url, getProxyUrlMeth) {