Skip to content

Commit

Permalink
fix Override WebSocket class (close #911) (#1341)
Browse files Browse the repository at this point in the history
* fix `Override WebSocket class` (close #911)

* fix origin header

* add server tests

* fix tests

* fix review's issues

* fix tests

* fix tests

* fix review issue

* add tests and override origin property

* add server tests

* add one more client test
  • Loading branch information
LavrovArtem authored and AlexanderMoskovkin committed Nov 15, 2017
1 parent bb90ef9 commit caaa7b1
Show file tree
Hide file tree
Showing 14 changed files with 585 additions and 40 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
"request": "~2.27.0",
"self-signed-https": "^1.0.5",
"tmp": "0.0.26",
"webmake": "0.3.42"
"webmake": "0.3.42",
"ws": "3.2.0"
},
"main": "lib/index",
"engines": {
Expand Down
33 changes: 25 additions & 8 deletions src/client/sandbox/native-methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,15 @@ class NativeMethods {
this.dateNow = win.Date.now;

// Object
this.objectToString = win.Object.prototype.toString;
this.objectAssign = win.Object.assign;
this.objectKeys = win.Object.keys;
this.objectDefineProperty = win.Object.defineProperty;
this.objectDefineProperties = win.Object.defineProperties;
this.objectCreate = win.Object.create;
this.objectIsExtensible = win.Object.isExtensible;
this.objectIsFrozen = win.Object.isFrozen;
this.objectToString = win.Object.prototype.toString;
this.objectAssign = win.Object.assign;
this.objectKeys = win.Object.keys;
this.objectDefineProperty = win.Object.defineProperty;
this.objectDefineProperties = win.Object.defineProperties;
this.objectCreate = win.Object.create;
this.objectIsExtensible = win.Object.isExtensible;
this.objectIsFrozen = win.Object.isFrozen;
this.objectGetOwnPropertyDescriptor = win.Object.getOwnPropertyDescriptor;

// DOMParser
if (win.DOMParser)
Expand All @@ -196,6 +197,21 @@ class NativeMethods {
if (textAreaValueDescriptor && typeof textAreaValueDescriptor.set === 'function')
this.textAreaValueSetter = textAreaValueDescriptor.set;

// Getters
if (win.WebSocket) {
const urlPropDescriptor = win.Object.getOwnPropertyDescriptor(window.WebSocket.prototype, 'url');

if (urlPropDescriptor && urlPropDescriptor.get && urlPropDescriptor.configurable)
this.webSocketUrlGetter = urlPropDescriptor.get;
}

if (win.MessageEvent) {
const originPropDescriptor = win.Object.getOwnPropertyDescriptor(window.MessageEvent.prototype, 'origin');

if (originPropDescriptor && originPropDescriptor.get && originPropDescriptor.configurable)
this.messageEventOriginGetter = originPropDescriptor.get;
}

// Stylesheets
if (win.CSSStyleDeclaration) {
this.CSSStyleDeclarationGetPropertyValue = win.CSSStyleDeclaration.prototype.getPropertyValue;
Expand Down Expand Up @@ -248,6 +264,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;
Expand Down
103 changes: 99 additions & 4 deletions src/client/sandbox/node/window.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ import { processScript } from '../../../processing/script';
import styleProcessor from '../../../processing/style';
import * as destLocation from '../../utils/destination-location';
import { processHtml } from '../../utils/html';
import { isSubDomain, parseUrl, getProxyUrl, parseProxyUrl, convertToProxyUrl, stringifyResourceType } from '../../utils/url';
import {
isSubDomain,
parseUrl,
getProxyUrl,
parseProxyUrl,
convertToProxyUrl,
stringifyResourceType
} from '../../utils/url';
import { isFirefox, isIE9, isIE } from '../../utils/browser';
import { isCrossDomainWindows, isImgElement, isBlob } from '../../utils/dom';
import { isCrossDomainWindows, isImgElement, isBlob, isWebSocket } from '../../utils/dom';
import { isPrimitiveType } from '../../utils/types';
import INTERNAL_ATTRS from '../../../processing/dom/internal-attributes';
import constructorIsCalledWithoutNewKeyword from '../../utils/constructor-is-called-without-new-keyword';
Expand All @@ -20,6 +27,8 @@ const nativeFunctionToString = nativeMethods.Function.toString();
// since they can be overriden by the client code. (GH-245)
const arrayConcat = Array.prototype.concat;

const HTTP_PROTOCOL_RE = /^http/i;

export default class WindowSandbox extends SandboxBase {
constructor (nodeSandbox, messageSandbox, listenersSandbox) {
super();
Expand Down Expand Up @@ -224,8 +233,9 @@ export default class WindowSandbox extends SandboxBase {
if (typeof scriptURL === 'string')
scriptURL = getProxyUrl(scriptURL, { resourceType: stringifyResourceType({ isScript: true }) });

return arguments.length ===
1 ? new nativeMethods.Worker(scriptURL) : new nativeMethods.Worker(scriptURL, options);
return arguments.length === 1
? new nativeMethods.Worker(scriptURL)
: new nativeMethods.Worker(scriptURL, options);
};
window.Worker.prototype = nativeMethods.Worker.prototype;
}
Expand Down Expand Up @@ -420,6 +430,91 @@ 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 }) });
const parsedUrl = parseUrl(url);
const origin = parsedUrl.protocol + '//' + parsedUrl.host;
let webSocket = null;

if (arguments.length === 1)
webSocket = new nativeMethods.WebSocket(proxyUrl);
else if (arguments.length === 2)
webSocket = new nativeMethods.WebSocket(proxyUrl, protocols);
else
webSocket = new nativeMethods.WebSocket(proxyUrl, protocols, arguments[2]);

// NOTE: We need to use deprecated methods instead of the defineProperty method
// in the following browsers:
// * Android 5.1 because prototype does not contain the url property
// and the defineProperty does not redefine descriptor of the WebSocket instance
// * Safari less than 10 versions because the configurable property of descriptor is false
// Try to drop this after moving to higher versions of browsers
if (!nativeMethods.webSocketUrlGetter) {
webSocket.__defineGetter__('url', () => url);
webSocket.__defineSetter__('url', value => value);
}

if (!nativeMethods.messageEventOriginGetter) {
webSocket.addEventListener('message', e => {
e.__defineGetter__('origin', () => origin);
e.__defineSetter__('origin', value => value);
});
}

return webSocket;
};

window.WebSocket.prototype = nativeMethods.WebSocket.prototype;
window.WebSocket.CONNECTING = nativeMethods.WebSocket.CONNECTING;
window.WebSocket.OPEN = nativeMethods.WebSocket.OPEN;
window.WebSocket.CLOSING = nativeMethods.WebSocket.CLOSING;
window.WebSocket.CLOSED = nativeMethods.WebSocket.CLOSED;

if (nativeMethods.webSocketUrlGetter) {
const urlPropDescriptor = nativeMethods.objectGetOwnPropertyDescriptor
.call(window.Object, window.WebSocket.prototype, 'url');

urlPropDescriptor.get = function () {
const url = nativeMethods.webSocketUrlGetter.call(this);
const parsedUrl = parseProxyUrl(url);

if (parsedUrl && parsedUrl.destUrl)
return parsedUrl.destUrl.replace(HTTP_PROTOCOL_RE, 'ws');

return url;
};

nativeMethods.objectDefineProperty
.call(window.Object, window.WebSocket.prototype, 'url', urlPropDescriptor);
}
}

if (nativeMethods.messageEventOriginGetter) {
const originPropDescriptor = nativeMethods.objectGetOwnPropertyDescriptor
.call(window.Object, window.MessageEvent.prototype, 'origin');

originPropDescriptor.get = function () {
const target = this.target;
const origin = nativeMethods.messageEventOriginGetter.call(this);

if (isWebSocket(target)) {
const parsedUrl = parseUrl(target.url);

if (parsedUrl)
return parsedUrl.protocol + '//' + parsedUrl.host;
}

return origin;
};

nativeMethods.objectDefineProperty
.call(window.Object, window.MessageEvent.prototype, 'origin', originPropDescriptor);
}

// NOTE: DOMParser supports an HTML parsing for IE10 and later
if (window.DOMParser && !isIE9) {
window.DOMParser.prototype.parseFromString = function (...args) {
Expand Down
4 changes: 4 additions & 0 deletions src/client/utils/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,10 @@ export function isTableDataCellElement (el) {
return instanceToString(el) === NATIVE_TABLE_CELL_STR;
}

export function isWebSocket (ws) {
return instanceToString(ws) === '[object WebSocket]';
}

export function matches (el, selector) {
if (!el)
return false;
Expand Down
36 changes: 28 additions & 8 deletions src/client/utils/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,30 @@ import * as destLocation from './destination-location';
import * as urlResolver from './url-resolver';
import settings from '../settings';

const HASH_RE = /#[\S\s]*$/;
const HASH_RE = /#[\S\s]*$/;
const SUPPORTED_WEB_SOCKET_PROTOCOL_RE = /^wss?:/i;

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 (!parsedResourceType.isWebSocket && !isSupportedProtocol(url) && !isSpecialPage(url))
return url;

// NOTE: Resolves relative URLs.
url = destLocation.resolveUrl(url);

if (!sharedUrlUtils.isValidUrl(url))
if (parsedResourceType.isWebSocket && !isValidWebSocketUrl(url) || !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;
let reqOrigin = opts && opts.reqOrigin;

const crossDomainPort = getCrossDomainProxyPort(proxyPort);

Expand All @@ -41,18 +46,19 @@ export function getProxyUrl (url, opts) {
const destUrl = sharedUrlUtils.formatUrl(parsedProxyUrl.destResourceInfo);

return getProxyUrl(destUrl, {
proxyProtocol,
proxyHostname,
proxyPort,
sessionId,
resourceType,
charset
charset,
reqOrigin
});
}

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.
Expand All @@ -67,12 +73,20 @@ export function getProxyUrl (url, opts) {
url = sharedUrlUtils.formatUrl(parsedUrl);
}

if (parsedResourceType.isWebSocket) {
parsedUrl.protocol = parsedUrl.protocol.replace('ws', 'http');
url = sharedUrlUtils.formatUrl(parsedUrl);
reqOrigin = reqOrigin || encodeURIComponent(destLocation.getOriginHeader());
}

return sharedUrlUtils.getProxyUrl(url, {
proxyProtocol,
proxyHostname,
proxyPort,
sessionId,
resourceType,
charset
charset,
reqOrigin
});
}

Expand Down Expand Up @@ -133,6 +147,12 @@ export function changeDestUrlPart (proxyUrl, prop, value, resourceType) {
return proxyUrl;
}

export function isValidWebSocketUrl (url) {
const resolvedUrl = resolveUrlAsDest(url);

return SUPPORTED_WEB_SOCKET_PROTOCOL_RE.test(resolvedUrl);
}

export function isSubDomain (domain, subDomain) {
return sharedUrlUtils.isSubDomain(domain, subDomain);
}
Expand Down
10 changes: 10 additions & 0 deletions src/proxy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions src/request-pipeline/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,7 +56,9 @@ export default class RequestPipelineContext {
isScript: parsedResourceType.isScript,
isEventSource: parsedResourceType.isEventSource,
isHtmlImport: parsedResourceType.isHtmlImport,
charset: parsed.charset
isWebSocket: parsedResourceType.isWebSocket,
charset: parsed.charset,
reqOrigin: parsed.reqOrigin
};

dest = this._omitDefaultPort(dest);
Expand Down Expand Up @@ -115,6 +118,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:';
Expand Down Expand Up @@ -244,7 +248,7 @@ export default class RequestPipelineContext {
closeWithError (statusCode, resBody) {
this.res.statusCode = statusCode;

if (resBody && !this.res.headersSent) {
if (resBody && !this.res.headersSent && this.res.setHeader) {
this.res.setHeader('content-type', 'text/html');
this.res.end(resBody);
}
Expand Down
8 changes: 8 additions & 0 deletions src/request-pipeline/destination-request/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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._onUpgrade(res, socket, head));
this.req.setTimeout(timeout, () => this._onTimeout());
this.req.write(this.opts.body);
this.req.end();
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit caaa7b1

Please sign in to comment.