Skip to content

Commit b5e243b

Browse files
committed
WIP: handle sites that pull globals from an iframe
1 parent dfef03b commit b5e243b

File tree

3 files changed

+141
-55
lines changed

3 files changed

+141
-55
lines changed

lib/client-scripts.js

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ module.exports = function ({ prefix }) {
2121
maxAge: "10m",
2222
};
2323

24-
const INJECTION_SNIPET = `
25-
<script>var unblocker=${JSON.stringify({ prefix })}</script>
26-
<script src="${clientScriptPathWeb}"></script>`;
27-
2824
function server(req, res, next) {
2925
if (req.url === clientScriptPathWeb) {
3026
send(req, clientScriptPathFs, sendOpts).pipe(res);
@@ -33,30 +29,33 @@ module.exports = function ({ prefix }) {
3329
next();
3430
}
3531

36-
function createStream() {
37-
return new Transform({
38-
decodeStrings: false,
39-
transform: function (chunk, encoding, next) {
40-
// todo: only inject once (maybe make an "injects into head" helper)
41-
var updated = chunk
42-
.toString()
43-
.replace(/(<head[^>]*>)/i, "$1" + INJECTION_SNIPET + "\n");
44-
this.push(updated, "utf8");
45-
next();
46-
},
47-
});
48-
}
49-
5032
function injector(data) {
5133
if (contentTypes.html.includes(data.contentType)) {
52-
data.stream = data.stream.pipe(createStream());
34+
data.stream = data.stream.pipe(
35+
new Transform({
36+
decodeStrings: false,
37+
transform: function (chunk, encoding, next) {
38+
// todo: only inject once (maybe make an "injects into head" helper)
39+
var updated = chunk.toString().replace(
40+
/(<head[^>]*>)/i,
41+
`$1
42+
<script src="${clientScriptPathWeb}"></script>
43+
<script>unblockerInit(${JSON.stringify({
44+
prefix,
45+
url: data.url,
46+
})}, window);</script>
47+
`
48+
);
49+
this.push(updated, "utf8");
50+
next();
51+
},
52+
})
53+
);
5354
}
5455
}
5556

56-
injector.createStream = createStream;
5757
return {
5858
server,
5959
injector,
60-
createStream, // for testing
6160
};
6261
};

lib/client/unblocker-client.js

Lines changed: 113 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,32 @@
1-
(function (exports) {
1+
(function (global) {
22
"use strict";
3-
/*global unblocker*/
43

54
// todo:
6-
// - fetch
7-
// - history API
85
// - postMessage
96
// - open
107
// - split each part into separate files (?)
118
// - wrap other JS and provide proxies to fix writes to window.location and document.cookie
129
// - will require updating contentTypes.html.includes(data.contentType) to include js
1310
// - that, in turn will require decompressing js....
1411

15-
console.log("begin unblocker client scripts", unblocker);
16-
17-
function fixUrl(target, prefix, location) {
18-
var currentRemoteHref =
19-
location.pathname.substr(prefix.length) + location.search + location.hash;
20-
var url = new URL(target, currentRemoteHref);
12+
function fixUrl(urlStr, config, location) {
13+
var currentRemoteHref;
14+
if (location.pathname.substr(0, config.prefix.length) === config.prefix) {
15+
currentRemoteHref =
16+
location.pathname.substr(config.prefix.length) +
17+
location.search +
18+
location.hash;
19+
} else {
20+
// in case sites like youtube bypass our history wrapper somehow
21+
currentRemoteHref = config.url;
22+
}
23+
var url = new URL(urlStr, currentRemoteHref);
2124

2225
//todo: handle already proxied urls (will be important for checking current dom)
2326

2427
// don't break data: urls
2528
if (url.protocol === "data:") {
26-
return target;
29+
return urlStr;
2730
}
2831

2932
// sometimes websites are tricky
@@ -36,37 +39,56 @@
3639
url.protocol = currentRemoteUrl.protocol;
3740
// todo: handle websocket protocols
3841
}
39-
return prefix + url.href;
42+
return config.prefix + url.href;
4043
}
4144

42-
function initXMLHttpRequest(config) {
43-
var _XMLHttpRequest = XMLHttpRequest;
45+
function initXMLHttpRequest(config, window) {
46+
if (typeof window.XMLHttpRequest === "undefined") return;
47+
var _XMLHttpRequest = window.XMLHttpRequest;
4448

4549
window.XMLHttpRequest = function (opts) {
4650
var xhr = new _XMLHttpRequest(opts);
4751
var _open = xhr.open;
4852
xhr.open = function () {
4953
var args = Array.prototype.slice.call(arguments);
50-
args[1] = fixUrl(args[1], config.prefix, location);
54+
args[1] = fixUrl(args[1], config, location);
5155
return _open.apply(xhr, args);
5256
};
5357
return xhr;
5458
};
5559
}
5660

57-
function initCreateElement(config) {
58-
var prefix = config.prefix;
59-
var _createElement = document.createElement;
61+
function initFetch(config, window) {
62+
if (typeof window.fetch === "undefined") return;
63+
var _fetch = window.fetch;
6064

61-
document.createElement = function (tagName, options) {
62-
var element = _createElement.call(document, tagName, options);
65+
window.fetch = function (resource, init) {
66+
if (resource.url) {
67+
resource.url = fixUrl(resource.url, config, location);
68+
} else {
69+
resource = fixUrl(resource.toString(), config, location);
70+
}
71+
return _fetch(resource, init);
72+
};
73+
}
74+
75+
function initCreateElement(config, window) {
76+
if (
77+
typeof window.document === "undefined" ||
78+
typeof window.document.createElement === "undefined"
79+
)
80+
return;
81+
var _createElement = window.document.createElement;
82+
83+
window.document.createElement = function (tagName, options) {
84+
var element = _createElement.call(window.document, tagName, options);
6385
// todo: whitelist elements with href or src attributes and only check those
6486
setTimeout(function () {
6587
if (element.src) {
66-
element.src = fixUrl(element.src, prefix, location);
88+
element.src = fixUrl(element.src, config, location);
6789
}
6890
if (element.href) {
69-
element.href = fixUrl(element.href, prefix, location);
91+
element.href = fixUrl(element.href, config, location);
7092
}
7193
// todo: support srcset and ..?
7294
}, 0);
@@ -75,8 +97,22 @@
7597
};
7698
}
7799

78-
function initWebsockets(config) {
79-
var _WebSocket = WebSocket;
100+
// js on some sites, such as youtube, uses an iframe to grab native APIs such as history, so we need to fix those also.
101+
// the
102+
// function initBodyAppendiFrame(config, window) {
103+
// if (
104+
// typeof window.document === "undefined" ||
105+
// typeof window.document.body === "undefined"
106+
// )
107+
// if (tagName.toLowerCase() === 'iframe' && element.contentWindow) {
108+
// // todo: check if we need to wait for onLoad or whatever
109+
// initForWindow(config, element.contentWindow);
110+
// }
111+
// }
112+
113+
function initWebsockets(config, window) {
114+
if (typeof window.WebSocket === "undefined") return;
115+
var _WebSocket = window.WebSocket;
80116
var prefix = config.prefix;
81117
var proxyHost = location.host;
82118
var isSecure = location.protocol === "https";
@@ -118,13 +154,59 @@
118154
};
119155
}
120156

121-
initXMLHttpRequest(unblocker);
122-
initCreateElement(unblocker);
123-
initWebsockets(unblocker);
157+
// todo: figure out how youtube bypasses this
158+
// notes: look at bindHistoryStateFunctions_ - it looks like it checks the contentWindow.history of an iframe *fitst*, then it's __proto__, then the global history api
159+
// - so, we need to inject this into iframes also
160+
function initPushState(config, window) {
161+
if (
162+
typeof window.history === "undefined" ||
163+
typeof window.history.pushState === "undefined"
164+
)
165+
return;
166+
167+
var _pushState = window.history.pushState;
168+
window.history.pushState = function (state, title, url) {
169+
if (url) {
170+
url = fixUrl(url, config, location);
171+
config.url = new URL(url, config.url);
172+
return _pushState.call(history, state, title, url);
173+
}
174+
};
175+
176+
if (typeof window.history.replaceState === "undefined") return;
177+
var _replaceState = window.history.replaceState;
178+
window.history.replaceState = function (state, title, url) {
179+
if (url) {
180+
url = fixUrl(url, config, location);
181+
config.url = new URL(url, config.url);
182+
return _replaceState.call(history, state, title, url);
183+
}
184+
};
185+
}
124186

125-
console.log("unblocker client scripts initialized");
187+
function initForWindow(unblocker, window) {
188+
console.log("begin unblocker client scripts", unblocker, window);
189+
initXMLHttpRequest(unblocker, window);
190+
initFetch(unblocker, window);
191+
initCreateElement(unblocker, window);
192+
initWebsockets(unblocker, window);
193+
initPushState(unblocker, window);
194+
if (window === global) {
195+
// leave no trace
196+
delete global.unblockerInit;
197+
}
198+
console.log("unblocker client scripts initialized");
199+
}
126200

127-
// export things for testing if loaded via commonjs
128-
exports.fixUrl = fixUrl;
201+
// either export things for testing or put the init method into the global scope to be called
202+
// with config by the next script tag in a browser
129203
/*globals module*/
130-
})((typeof module === "object" && module.exports) || {});
204+
if (typeof module === "undefined") {
205+
global.unblockerInit = initForWindow;
206+
} else {
207+
module.exports = {
208+
initForWindow: initForWindow,
209+
fixUrl: fixUrl,
210+
};
211+
}
212+
})(this); // window in a browser, global in node.js

test/client/fix-url-spec.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"use strict";
22
const { test } = require("tap");
3-
const { fixUrl } = require("../../lib/client/unblocker-client.js");
4-
const proxy = "http://localhost";
53
const prefix = "/proxy/";
4+
const proxy = "http://localhost";
65
const target = "http://example.com/page.html?query#hash";
76
const location = new URL(proxy + prefix + target);
87

8+
const config = (global.unblocker = { prefix, url: target });
9+
const { fixUrl } = require("../../lib/client/unblocker-client.js");
10+
911
// 1x1 transparent gif
1012
const pixel =
1113
" data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
@@ -36,16 +38,19 @@ const testCases = [
3638
{ url: "http://localhost/path", expected: "/proxy/http://example.com/path" },
3739
// don't break data: urls
3840
{ url: pixel, expected: pixel },
41+
// don't break protocol-relative urls
42+
{ url: "//example.com/foo", expected: "/proxy/http://example.com/foo" },
3943
// todo: port numbers
4044
// todo: more https tests
4145
// todo: websockets(?)
4246
// todo: already proxied input url
47+
// todo: about:blank
4348
];
4449

4550
testCases.forEach((tc) => {
4651
test(JSON.stringify(tc), (t) => {
4752
// todo: replace || with ??
48-
const actual = fixUrl(tc.url, tc.prefix || prefix, tc.location || location);
53+
const actual = fixUrl(tc.url, tc.config || config, tc.location || location);
4954
t.equal(actual, tc.expected);
5055
t.end();
5156
});

0 commit comments

Comments
 (0)