Skip to content

Commit

Permalink
Overridden functions should have the right prototype in an iframe wit…
Browse files Browse the repository at this point in the history
…hout src (fix #1824) (#1836)

* overridden functions should have the right prototype in an iframe without src (fix #1824)

* try to fix ie tests (close #1584)

* fix tests

* fix tests

* fix functional test

* refactoring

* fix unstable test

* remove comment
  • Loading branch information
LavrovArtem authored and miherlosev committed Nov 21, 2018
1 parent bc9eb86 commit c87aa9f
Show file tree
Hide file tree
Showing 16 changed files with 195 additions and 67 deletions.
1 change: 0 additions & 1 deletion src/client/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"no-restricted-globals": [2, "Promise"]
},
"globals": {
"isIframeWithoutSrc": true,
"initHammerheadClient": true
},
"env": {
Expand Down
13 changes: 2 additions & 11 deletions src/client/index.js.wrapper.mustache
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
(function () {
function initHammerheadClient(window, isIframeWithoutSrc) {
// NOTE: The following script will be executed in the current window context. However, when you call a script
// in an iframe, global variables are obtained from the top window. So, we need to override the global variables
// which can affect our script.
var document = window.document;
{{{source}}}
}

initHammerheadClient(window);
(function initHammerheadClient () {
{{{source}}}
})();
8 changes: 0 additions & 8 deletions src/client/sandbox/event/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,6 @@ export default class MessageSandbox extends SandboxBase {

args[0] = MessageSandbox._wrapMessage(MESSAGE_TYPE.user, args[0], targetUrl);

if (isIframeWithoutSrc) {
/*eslint-disable camelcase */
this.window.tc_cw_375fb9e7 = contentWindow;
this.window.tc_a_375fb9e7 = args;
/*eslint-disable camelcase */

return this.window.eval('this.window.tc_cw_375fb9e7.postMessage(this.window.tc_a_375fb9e7[0], this.window.tc_a_375fb9e7[1]); delete this.window.tc_cw_375fb9e7; delete this.window.tc_a_375fb9e7');
}

return fastApply(contentWindow, 'postMessage', args);
}
Expand Down
5 changes: 3 additions & 2 deletions src/client/sandbox/iframe.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import settings from '../settings';
import nativeMethods from '../sandbox/native-methods';
import { isJsProtocol } from '../../processing/dom';
import { isShadowUIElement, isIframeWithoutSrc } from '../utils/dom';
import { isFirefox, isWebKit } from '../utils/browser';
import { isFirefox, isWebKit, isIE } from '../utils/browser';
import * as JSON from '../json';

const IFRAME_WINDOW_INITED = 'hammerhead|iframe-window-inited';
Expand Down Expand Up @@ -103,7 +103,8 @@ export default class IframeSandbox extends SandboxBase {
static isIframeInitialized (iframe) {
const isFFIframeUninitialized = isFirefox && iframe.contentWindow.document.readyState === 'uninitialized';

return !isFFIframeUninitialized && !!iframe.contentDocument.documentElement;
return !isFFIframeUninitialized && !!iframe.contentDocument.documentElement ||
isIE && iframe.contentWindow[INTERNAL_PROPS.documentWasCleaned];
}

static isWindowInited (window) {
Expand Down
4 changes: 3 additions & 1 deletion src/client/sandbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ export default class Sandbox extends SandboxBase {
urlResolver.init(this.document);

// NOTE: Eval Hammerhead code script.
this.iframe.on(this.iframe.EVAL_HAMMERHEAD_SCRIPT_EVENT, e => initHammerheadClient(e.iframe.contentWindow, true));
this.iframe.on(this.iframe.EVAL_HAMMERHEAD_SCRIPT_EVENT, e => {
e.iframe.contentWindow.eval(`(${ initHammerheadClient.toString() })();//# sourceURL=hammerhead.js`);
});

// NOTE: We need to reattach a sandbox to the recreated iframe document.
this.node.mutation.on(this.node.mutation.DOCUMENT_CLEANED_EVENT, e => this.reattach(e.window, e.document));
Expand Down
32 changes: 30 additions & 2 deletions src/client/sandbox/node/document/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ export default class DocumentSandbox extends SandboxBase {
}

static _shouldEmitDocumentCleanedEvents (doc) {
if (isIE) {
if (doc.readyState !== 'loading')
return true;

const window = doc.defaultView;

if (window[INTERNAL_PROPS.documentWasCleaned])
return false;

const iframe = window && getFrameElement(window);

return iframe && isIframeWithoutSrc(iframe);
}

return doc.readyState !== 'loading' && doc.readyState !== 'uninitialized';
}

Expand Down Expand Up @@ -85,6 +99,18 @@ export default class DocumentSandbox extends SandboxBase {
nativeMethods.objectDefineProperty.call(window.Object, owner, prop, overriddenDescriptor);
}

iframeDocumentOpen (window, document, args) {
const iframe = window.frameElement;
const result = nativeMethods.documentOpen.apply(document, args);

nativeMethods.objectDefineProperty(window, INTERNAL_PROPS.documentWasCleaned, { value: true, configurable: true });

nativeMethods.restoreDocumentMeths(document);
this.nodeSandbox.iframeSandbox.onIframeBeganToRun(iframe);

return result;
}

attach (window, document) {
if (this._needToUpdateDocumentWriter(window, document)) {
this.documentWriter = new DocumentWriter(window, document);
Expand All @@ -104,6 +130,9 @@ export default class DocumentSandbox extends SandboxBase {
if (!isUninitializedIframe)
this._beforeDocumentCleaned();

if (isIE)
return parent[INTERNAL_PROPS.hammerhead].sandbox.node.doc.iframeDocumentOpen(window, document, args);

const result = nativeMethods.documentOpen.apply(document, args);

if (isIE)
Expand All @@ -115,8 +144,7 @@ export default class DocumentSandbox extends SandboxBase {
? window[INTERNAL_PROPS.hammerhead].nativeMethods.objectDefineProperty
: window.Object.defineProperty;

objectDefinePropertyFn
.call(window.Object, window, INTERNAL_PROPS.documentWasCleaned, { value: true, configurable: true });
objectDefinePropertyFn(window, INTERNAL_PROPS.documentWasCleaned, { value: true, configurable: true });

if (!isUninitializedIframe)
this.nodeSandbox.mutation.onDocumentCleaned({ window, document });
Expand Down
3 changes: 3 additions & 0 deletions src/client/sandbox/node/document/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ export default class DocumentWriter {
const nativeWriteMethod = ln ? nativeMethods.documentWriteLn : nativeMethods.documentWrite;
const result = nativeWriteMethod.call(this.document, htmlChunk);

if (isDocumentCleaned && isIE)
return result;

if (!this.isEndMarkerInDOM && !this.isAddContentToEl) {
let el = this.document.documentElement;

Expand Down
7 changes: 6 additions & 1 deletion src/client/transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ import { isWebKit, isFirefox } from './utils/browser';
import createUnresolvablePromise from './utils/create-unresolvable-promise';
import noop from './utils/noop';
import Promise from 'pinkie';
import { isIframeWithoutSrc, getFrameElement } from './utils/dom';

const SERVICE_MESSAGES_WAITING_INTERVAL = 50;

class Transport {
constructor () {
this.msgQueue = {};
this.activeServiceMessagesCounter = 0;

const frameElement = getFrameElement(window);

this.shouldAddRefferer = frameElement && isIframeWithoutSrc(frameElement);
}

static _storeMessage (msg) {
Expand Down Expand Up @@ -47,7 +52,7 @@ class Transport {
_performRequest (msg, callback) {
msg.sessionId = settings.get().sessionId;

if (isIframeWithoutSrc)
if (this.shouldAddRefferer)
msg.referer = settings.get().referer;

const sendMsg = forced => {
Expand Down
98 changes: 80 additions & 18 deletions src/client/utils/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,14 @@ const UNWRAP_DOCTYPE_RE = new RegExp(`<${ FAKE_DOCTYPE_TAG_NAME }>([\\S\\s]*
const FIND_SVG_RE = /<svg\s?[^>]*>/ig;
const FIND_NS_ATTRS_RE = /\s(?:NS[0-9]+:[^"']+('|")[\S\s]*?\1|[^:]+:NS[0-9]+=(?:""|''))/g;

const ATTRS_FOR_CLEANING = nativeMethods.arrayConcat.call(URL_ATTRS, ATTRS_WITH_SPECIAL_PROXYING_LOGIC);
const ATTRS_DATA_FOR_CLEANING = nativeMethods.arrayMap.call(ATTRS_FOR_CLEANING, attr => {
return {
attr,
storedAttr: DomProcessor.getStoredAttrName(attr)
};
});

const STORED_ATTRS_SELECTOR = (() => {
const storedAttrs = [];

for (const { storedAttr } of ATTRS_DATA_FOR_CLEANING)
storedAttrs.push(storedAttr);
for (const attr of URL_ATTRS)
storedAttrs.push(DomProcessor.getStoredAttrName(attr));

for (const attr of ATTRS_WITH_SPECIAL_PROXYING_LOGIC)
storedAttrs.push(DomProcessor.getStoredAttrName(attr));

return '[' + storedAttrs.join('],[') + ']';
})();
Expand Down Expand Up @@ -131,19 +126,86 @@ function processHtmlInternal (html, process) {
return processedHtml;
}

function cleanUpUrlAttr (el) {
const urlAttr = domProcessor.getUrlAttr(el);

if (!urlAttr || !nativeMethods.hasAttribute.call(el, urlAttr))
return;

const storedAttr = DomProcessor.getStoredAttrName(urlAttr);

if (nativeMethods.hasAttribute.call(el, storedAttr)) {
nativeMethods.setAttribute.call(el, urlAttr, nativeMethods.getAttribute.call(el, storedAttr));
nativeMethods.removeAttribute.call(el, storedAttr);
}
}

function cleanUpAutocompleteAttr (el) {
if (!nativeMethods.hasAttribute.call(el, 'autocomplete'))
return;

const storedAttr = DomProcessor.getStoredAttrName('autocomplete');

if (nativeMethods.hasAttribute.call(el, storedAttr)) {
const storedAttrValue = nativeMethods.getAttribute.call(el, storedAttr);

if (DomProcessor.isAddedAutocompleteAttr('autocomplete', storedAttrValue))
nativeMethods.removeAttribute.call(el, 'autocomplete');
else
nativeMethods.setAttribute.call(el, 'autocomplete', storedAttrValue);

nativeMethods.removeAttribute.call(el, storedAttr);
}
}

function cleanUpTargetAttr (el) {
const targetAttr = domProcessor.getTargetAttr(el);

if (!targetAttr || !nativeMethods.hasAttribute.call(el, targetAttr))
return;

const storedAttr = DomProcessor.getStoredAttrName(targetAttr);

if (nativeMethods.hasAttribute.call(el, storedAttr)) {
nativeMethods.setAttribute.call(el, targetAttr, nativeMethods.getAttribute.call(el, storedAttr));
nativeMethods.removeAttribute.call(el, storedAttr);
}
}

function cleanUpSandboxAttr (el) {
if (domProcessor.adapter.getTagName(el) !== 'iframe' || !nativeMethods.hasAttribute.call(el, 'sandbox'))
return;

const storedAttr = DomProcessor.getStoredAttrName('sandbox');

if (nativeMethods.hasAttribute.call(el, storedAttr)) {
nativeMethods.setAttribute.call(el, 'sandbox', nativeMethods.getAttribute.call(el, storedAttr));
nativeMethods.removeAttribute.call(el, storedAttr);
}
}

function cleanUpStyleAttr (el) {
if (!nativeMethods.hasAttribute.call(el, 'style'))
return;

const storedAttr = DomProcessor.getStoredAttrName('style');

if (nativeMethods.hasAttribute.call(el, storedAttr)) {
nativeMethods.setAttribute.call(el, 'style', nativeMethods.getAttribute.call(el, storedAttr));
nativeMethods.removeAttribute.call(el, storedAttr);
}
}

export function cleanUpHtml (html) {
return processHtmlInternal(html, container => {
let changed = false;

find(container, STORED_ATTRS_SELECTOR, el => {
for (const { attr, storedAttr } of ATTRS_DATA_FOR_CLEANING) {
if (el.hasAttribute(attr) && el.hasAttribute(storedAttr))
nativeMethods.setAttribute.call(el, attr, nativeMethods.getAttribute.call(el, storedAttr));
else if (attr === 'autocomplete')
nativeMethods.removeAttribute.call(el, attr);

nativeMethods.removeAttribute.call(el, storedAttr);
}
cleanUpUrlAttr(el);
cleanUpAutocompleteAttr(el);
cleanUpTargetAttr(el);
cleanUpSandboxAttr(el);
cleanUpStyleAttr(el);

changed = true;
});
Expand Down
14 changes: 12 additions & 2 deletions src/client/utils/url-resolver.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import nativeMethods from '../sandbox/native-methods';
import * as destLocation from '../utils/destination-location';
import { ensureTrailingSlash, parseUrl } from '../../utils/url';
import { isIframeWithoutSrc, getFrameElement } from '../utils/dom';

const DOCUMENT_URL_RESOLVER = 'hammerhead|document-url-resolver';

Expand Down Expand Up @@ -31,6 +32,15 @@ export default {
return doc[DOCUMENT_URL_RESOLVER];
},

_isNestedIframeWithoutSrc (win) {
if (!win || !win.parent || win.parent === win || win.parent.parent === win.parent)
return false;

const iframeElement = getFrameElement(window);

return !!iframeElement && isIframeWithoutSrc(iframeElement);
},

init (doc) {
this.updateBase(destLocation.get(), doc);
},
Expand All @@ -53,8 +63,8 @@ export default {
// NOTE: It looks like a Chrome bug: in a nested iframe without src (when an iframe is placed into another
// iframe) you cannot set a relative link href while the iframe loading is not completed. So, we'll do it with
// the parent's urlResolver Safari demonstrates similar behavior, but urlResolver.href has a relative URL value.
const needUseParentResolver = url && isIframeWithoutSrc && window.parent && window.parent.document &&
(!href || href.indexOf('/') === 0);
const needUseParentResolver = url && (!href || href.charAt(0) === '/') &&
this._isNestedIframeWithoutSrc(doc.defaultView);

if (needUseParentResolver)
return this.resolve(url, window.parent.document);
Expand Down
26 changes: 23 additions & 3 deletions src/client/utils/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ import settings from '../settings';
const HASH_RE = /#[\S\s]*$/;
const SUPPORTED_WEB_SOCKET_PROTOCOL_RE = /^wss?:/i;

// NOTE: The window.location equals 'about:blank' in iframes without src
// therefore we need to find a window with src to get the proxy settings
const DEFAULT_PROXY_SETTINGS = (function () {
/*eslint-disable no-restricted-properties*/
let locationWindow = window;
let proxyLocation = locationWindow.location;

while (!proxyLocation.hostname) {
locationWindow = locationWindow.parent;
proxyLocation = locationWindow.location;
}

return {
hostname: proxyLocation.hostname,
port: proxyLocation.port.toString(),
protocol: proxyLocation.protocol
};
/*eslint-enable no-restricted-properties*/
})();

export const REQUEST_DESCRIPTOR_VALUES_SEPARATOR = sharedUrlUtils.REQUEST_DESCRIPTOR_VALUES_SEPARATOR;

export function getProxyUrl (url, opts) {
Expand All @@ -23,9 +43,9 @@ export function getProxyUrl (url, opts) {
return url;

/*eslint-disable no-restricted-properties*/
const proxyHostname = opts && opts.proxyHostname || location.hostname;
const proxyPort = opts && opts.proxyPort || location.port.toString();
const proxyServerProtocol = opts && opts.proxyProtocol || location.protocol;
const proxyHostname = opts && opts.proxyHostname || DEFAULT_PROXY_SETTINGS.hostname;
const proxyPort = opts && opts.proxyPort || DEFAULT_PROXY_SETTINGS.port;
const proxyServerProtocol = opts && opts.proxyProtocol || DEFAULT_PROXY_SETTINGS.protocol;
/*eslint-enable no-restricted-properties*/

const proxyProtocol = parsedResourceType.isWebSocket
Expand Down
2 changes: 1 addition & 1 deletion src/processing/dom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const EXECUTABLE_SCRIPT_TYPES_REG_EX = /^\s*(application\/(x-)?(ecma|java)sc
const SVG_XLINK_HREF_TAGS = [
'animate', 'animateColor', 'animateMotion', 'animateTransform', 'mpath', 'set', //animation elements
'linearGradient', 'radialGradient', 'stop', //gradient elements
'a', 'altglyph', 'color-profile', 'cursor', 'feimage', 'filter', '<font-face-uri', 'glyphref', 'image',
'a', 'altglyph', 'color-profile', 'cursor', 'feimage', 'filter', 'font-face-uri', 'glyphref', 'image',
'mpath', 'pattern', 'script', 'textpath', 'use', 'tref'
];

Expand Down
3 changes: 2 additions & 1 deletion test/client/data/cross-domain/get-ancestor-origin.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
window.onmessage = function (evt) {
if (evt.data = 'get ancestorOrigin') {
var data = {
msg: JSON.stringify({ancestorOrigins: ancestorOrigins, ancestorOriginsLength: ancestorOrigins.length})
id: 'GH-1342',
msg: JSON.stringify({ ancestorOrigins: ancestorOrigins, ancestorOriginsLength: ancestorOrigins.length })
};

postMessage(window.parent, [data, '*']);
Expand Down
Loading

0 comments on commit c87aa9f

Please sign in to comment.