Skip to content

Commit

Permalink
fix Override WebSocket class (close DevExpress#911)
Browse files Browse the repository at this point in the history
  • Loading branch information
LavrovArtem committed Nov 3, 2017
1 parent 98fc917 commit 11e5a1e
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 15 deletions.
1 change: 1 addition & 0 deletions src/client/sandbox/native-methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/client/sandbox/node/window.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 15 additions & 6 deletions src/client/utils/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 ?
Expand All @@ -42,6 +45,7 @@ export function getProxyUrl (url, opts) {
const destUrl = sharedUrlUtils.formatUrl(parsedProxyUrl.destResourceInfo);

return getProxyUrl(destUrl, {
proxyProtocol,
proxyHostname,
proxyPort,
sessionId,
Expand All @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
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
3 changes: 3 additions & 0 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,6 +56,7 @@ export default class RequestPipelineContext {
isScript: parsedResourceType.isScript,
isEventSource: parsedResourceType.isEventSource,
isHtmlImport: parsedResourceType.isHtmlImport,
isWebSocket: parsedResourceType.isWebSocket,
charset: parsed.charset
};

Expand Down Expand Up @@ -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:';
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._onResponse(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
38 changes: 37 additions & 1 deletion src/request-pipeline/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 17 additions & 8 deletions src/utils/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -25,7 +26,8 @@ export function parseResourceType (resourceType) {
isForm: false,
isScript: false,
isEventSource: false,
isHtmlImport: false
isHtmlImport: false,
isWebSocket: false
};
}

Expand All @@ -34,23 +36,25 @@ 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 [
resourceType.isIframe ? 'i' : '',
resourceType.isForm ? 'f' : '',
resourceType.isScript ? 's' : '',
resourceType.isEventSource ? 'e' : '',
resourceType.isHtmlImport ? 'h' : ''
resourceType.isHtmlImport ? 'h' : '',
resourceType.isWebSocket ? 'w' : ''
].join('');
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down

0 comments on commit 11e5a1e

Please sign in to comment.