Skip to content

Commit

Permalink
got proxies working for location
Browse files Browse the repository at this point in the history
instagram now loads (when logged out), at least partially fixes #149
fixed a few warnings
fixed script wrapper to handle opening script tags that are split across chunks
  • Loading branch information
nfriedly committed Mar 31, 2021
1 parent 805dfe9 commit 66801b5
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 38 deletions.
22 changes: 21 additions & 1 deletion lib/client-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,23 @@ module.exports = function ({ prefix }) {
const WRAPPER_END = '\n}(unblocker.window, unblocker.location, unblocker.document));';

const reHeadTag = /(<head[^>]*>)/i;
// catches a partial tag at the end of a chunk; it will be cached until the next chunk
const rePartialTag = /<[^>]*$/;

function injector(data) {
if (contentTypes.html.includes(data.contentType)) {
var open = [];
var partialTag = "";
data.stream = data.stream.pipe(
new Transform({
decodeStrings: false,
transform: function (chunk, encoding, next) {

// include any leftovers from the previous chunk
var updated = partialTag + chunk.toString();
partialTag = "";

// wrap inline script tags from the site with our custom globals
var updated = chunk.toString();
updated = updated.replace(/<script([^>]*)>/ig, function(match, attrs) {
debug('found script tag', match, attrs)
if(attrs.includes('src=') || attrs.includes('json')) {
Expand Down Expand Up @@ -77,9 +84,22 @@ module.exports = function ({ prefix }) {
})}, window);</script>
`
);

// don't loose tags that are split across chunks
updated = updated.replace(rePartialTag, function(match) {
partialTag = match;
return "";
});

this.push(updated, "utf8");
next();
},
flush: function() {
if (partialTag) {
this.push(partialTag, "utf8");
}
this.push(null);
}
})
);
} else if (contentTypes.javascript.includes(data.contentType)) {
Expand Down
166 changes: 131 additions & 35 deletions lib/client/unblocker-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,24 @@
// - poster (on <video> elements)
// - perhaps some/all of this could be shared by the server-side url-rewriter
// - split each part into separate files (?)
// - wrap other JS and provide proxies to fix writes to window.location and document.cookie
// - will require updating contentTypes.html.includes(data.contentType) to include js
// - that, in turn will require decompressing js....
// call() and apply() on `this || original_thing`
// prevent a failure in one initializer from stopping subsequent initializers
// - fixup writes to document.cookie
// - run call() and apply() on `this || original_thing`
// - prevent a failure in one initializer from stopping subsequent initializers
// - add a getter for parentNode on all direct children of document so things that walk up the tree until they get to document work properly

function fixUrl(urlStr, config, location) {
var currentRemoteHref;
function getRemoteHref(config, location) {
if (location.pathname.substr(0, config.prefix.length) === config.prefix) {
currentRemoteHref =
location.pathname.substr(config.prefix.length) +
return location.pathname.substr(config.prefix.length) +
location.search +
location.hash;
} else {
// in case sites (such as youtube) manage to bypass our history wrapper
currentRemoteHref = config.url;
// in case sites (such as youtube) manage to bypass our history wrapper or break the URL some other way
return config.url;
}
}

function fixUrl(urlStr, config, location) {
var currentRemoteHref = getRemoteHref(config, location);

// check if it's already proxied (root-relative)
if (urlStr.substr(0, config.prefix.length) === config.prefix) {
Expand Down Expand Up @@ -230,46 +231,143 @@
function initGlobalProxies(config, window) {
if (unblocker.window) return;

// a lot of native functions really don't like being called on a proxy (e.g. setTimeout)
// so we'll identify them eagerly (to avoid functions later defined by JS in the site)
var nativeWindowMethods = [];

// props that trigger a warning when we call typeof on them
// none are functions, so we don't care
var skipProps = [
// deprecation warning in chrome
'webkitStorageInfo',
// deprecation warning in firefox
'onmozfullscreenchange',
'onmozfullscreenerror',
// warning due to forced layout
'scrollMaxX',
'scrollMaxY'
];

for (var prop in window) {
if (skipProps.indexOf(prop) !== -1) {
continue;
}
if (typeof window[prop] === 'function') {
nativeWindowMethods.push(prop);
}
}
// and then bind them lazily, but cache the result
var boundWindowMethods = {};

unblocker.window = new Proxy(window, {
get: function(obj, prop) {
get: function(obj, prop, receiver) {
// return our "special" things
// this is the only part here that we actually want to do,
// the rest is just necessary to allow this to work
if (prop === 'location') {
return unblocker.location;
}
if (prop === 'document') {
return unblocker.document;
}

// handle native methods
// check the cache first
if (boundWindowMethods[prop]) {
return boundWindowMethods[prop];
}
// check the list of functions to be bound second, bind and cache if this one is on the list
if (nativeWindowMethods.includes(prop)) {
return boundWindowMethods[prop] = window[prop].bind(window);
}

// finally just return the property
return obj[prop];
},
set: function(obj, prop, value) {
if (prop === 'location') {
// todo: handle this
unblocker.location.href = value;
return true;
}

// The default behavior to store the value
obj[prop] = value;

// Indicate success
return true;
return obj[prop] = value;
}
});


unblocker.location = new Proxy(window.location, {
get: function(obj, prop) {
var targetUrl = new URL(config.url)
return targetUrl[prop] || obj[prop];
// proxy on {} because some methods on window.location are non-writeable and non-configurable,
// and proxies force you to return the original method in that case
unblocker.location = new Proxy({}, {
get: function(obj, prop/*, receiver*/) {
// return wrappers for assign and replace methods
if (prop === 'assign' || prop === 'replace') {
return function(href) {
return location[prop](fixUrl(href, config, location));
}
}
var targetUrl = new URL(config.url);
if (prop in targetUrl) {
return targetUrl[prop];
}
return location[prop];
// try {
// return Reflect.get(obj, prop, receiver);
// } catch (ex) {
// console.log(`Unblocker: error reading location.${prop} with Reflect.get(), trying directly.\n`, ex);
// return window.location[prop];
// }
},
set: function(obj, prop, value) {

// The default behavior to store the value
obj[prop] = value;

// Indicate success
return true;
var targetUrl = new URL(getRemoteHref(config, location));
if (prop in targetUrl) {
targetUrl[prop] = value;
return window.location = fixUrl(targetUrl.href, config, location);
}
return location[prop] = value;
}
});
// todo: proxy document.cookie writes
unblocker.document = window.document;


// Similar to window, a lot of native functions really don't like being called on a proxy (e.g. addEventListener)
// so we'll identify them eagerly (to avoid functions later defined by JS in the site)
var nativeDocumentMethods = [];
for (var prop in window.document) {
if (skipProps.indexOf(prop) !== -1) {
continue;
}
if (typeof window.document[prop] === 'function') {
nativeDocumentMethods.push(prop);
}
}
// and then bind them lazily, but cache the result
var boundWindowMethods = {};

unblocker.document = new Proxy(window.document, {
get: function(obj, prop, receiver) {
// handle things we actually care about
if (prop === 'location') {
return unblocker.location;
}

// handle native methods
// check the cache first
if (nativeDocumentMethods[prop]) {
return nativeDocumentMethods[prop];
}
// check the list of functions to be bound second, bind and cache if this one is on the list
if (nativeDocumentMethods.includes(prop)) {
return nativeDocumentMethods[prop] = window.document[prop].bind(window.document);
}

return obj[prop];
},
set: function(obj, prop, value) {
if (prop === 'location') {
unblocker.location.href = value;
return true;
}
// todo: fixup domain and path on cookies
return obj[prop] = value;
}
});
}

var config;
Expand All @@ -290,9 +388,7 @@
// with config by the next script tag in a browser
/*globals module*/
if (typeof module === "undefined") {
global.unblocker = {
init: initForWindow,
}
unblocker.init = initForWindow
} else {
module.exports = {
initForWindow: initForWindow,
Expand Down
6 changes: 5 additions & 1 deletion lib/cookies.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ function cookies(config) {
targetUri.host +
(cookie.path || "/");
delete cookie.domain;
delete cookie.secure; // todo: maybe leave this if we know the proxy is being accessed over https?
if (cookie.secure) {
// todo: maybe leave this if we know the proxy is being accessed over https?
delete cookie.secure;
delete cookie.sameSite; // the 'None' option requires 'Secure'
}
cookie.encode = noChange;
return libCookie.serialize(cookie.name, cookie.value, cookie);
});
Expand Down
6 changes: 5 additions & 1 deletion lib/host.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ var URL = require("url");

module.exports = function (/*config*/) {
return function hostHeader(data) {
data.headers.host = URL.parse(data.url).host;
var url = URL.parse(data.url);
data.headers.host = url.host;
if (data.headers.origin) {
data.headers.origin = url.protocol + url.host;
}
};
};

0 comments on commit 66801b5

Please sign in to comment.