Skip to content

Commit f79f4c1

Browse files
authored
Merge pull request #19 from MethodGrab/feature/override-iframe-header
Add option to remove `X-Frame-Options` header
2 parents c61a19f + b2ad756 commit f79f4c1

File tree

6 files changed

+339
-97
lines changed

6 files changed

+339
-97
lines changed

.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ module.exports = {
1818
allowShortCircuit: true,
1919
allowTernary: true,
2020
}],
21+
'no-unused-vars': [ 'warn', {
22+
vars: 'all',
23+
args: 'none',
24+
argsIgnorePattern: '^_$',
25+
ignoreRestSiblings: true,
26+
}],
2127
'no-warning-comments': [ 'warn', {
2228
terms: [ 'fixme', 'xxx' ],
2329
location: 'start',

src/background.js

Lines changed: 146 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ const log = false;
55
// globals
66
const extensionNewTabPath = 'app.html';
77

8-
// TODO: this also exists in options.js and could be moved to a separate helper file
8+
// TODO: this also exists in options.js and could be moved to a separate helper file.
9+
const removeIframeHeadersPermissions = [
10+
'webRequest',
11+
'webRequestBlocking',
12+
];
13+
14+
// TODO: this also exists in options.js and could be moved to a separate helper file.
915
const forceOpenInTopFramePermissions = [
1016
'webRequest',
1117
'webRequestBlocking',
@@ -15,13 +21,14 @@ const forceOpenInTopFramePermissions = [
1521
// state
1622
let options = {
1723
customNewTabUrl: '',
24+
removeIframeHeaders: false,
1825
forceOpenInTopFrame: false,
1926
};
2027

2128

2229
// Firefox API helpers
2330

24-
// TODO: this also exists in options.js and could be moved to a separate helper file
31+
// TODO: this also exists in options.js and could be moved to a separate helper file.
2532
const toMatchPattern = urlStr => {
2633
try {
2734
// Match patterns without paths must have a trailing slash.
@@ -34,30 +41,53 @@ const toMatchPattern = urlStr => {
3441
}
3542
};
3643

44+
// TODO: this also exists in options.js and could be moved to a separate helper file.
45+
// Creates a matcher for any page on a given domain.
46+
// toWildcardDomainMatchPattern('https://example.com') // -> 'https://*.example.com/*'
47+
// toWildcardDomainMatchPattern('https://example.com:3000/foo/bar.html') // -> 'https://*.example.com:3000/*'
48+
const toWildcardDomainMatchPattern = urlStr => {
49+
try {
50+
const url = new URL( urlStr );
51+
const pattern = `${url.protocol}//*.${url.host}/*`;
52+
53+
log && console.debug( '#toWildcardDomainMatchPattern', { pattern } );
54+
return pattern;
55+
} catch ( err ) {
56+
console.error( '#toWildcardDomainMatchPattern', err );
57+
return false;
58+
}
59+
};
60+
3761

3862
const updateOptionsCache = opts => { options = Object.assign( {}, options, opts ); };
3963

40-
const refreshOptionsCache = async _ => await browser.storage.sync.get([ 'customNewTabUrl', 'forceOpenInTopFrame' ]).then( updateOptionsCache );
64+
const refreshOptionsCache = async _ =>
65+
await browser.storage.sync.get([
66+
'customNewTabUrl',
67+
'removeIframeHeaders',
68+
'forceOpenInTopFrame',
69+
]).then( updateOptionsCache );
4170

4271
const customNewTabUrlExists = _ => ( options.customNewTabUrl && options.customNewTabUrl.length !== 0 );
4372

44-
const applyFilter = details => {
45-
log && console.debug( '#applyFilter', options, details );
73+
74+
const forceOpenInTopFrameFilter = details => {
75+
log && console.debug( '#forceOpenInTopFrameFilter', options, details );
4676

4777
if ( !options.forceOpenInTopFrame ) {
4878
// Dont modify requests if the option is not enabled.
49-
log && console.debug( '#applyFilter // forceOpenInTopFrame option not enabled... skipping' );
79+
log && console.debug( '#forceOpenInTopFrameFilter // forceOpenInTopFrame option not enabled... skipping' );
5080
return false;
5181
}
5282

5383
if ( details.originUrl !== browser.extension.getURL( extensionNewTabPath ) ) {
5484
// Don't modify requests outside of the extension new tab page.
5585
// This is still needed because the `customNewTabUrl` scope used in the onBeforeRequest listener doesnt guarantee the request is coming from this extension.
56-
log && console.debug( '#applyFilter // outside of extension new tab page... skipping' );
86+
log && console.debug( '#forceOpenInTopFrameFilter // outside of extension new tab page... skipping' );
5787
return false;
5888
}
5989

60-
log && console.debug( '#applyFilter // modifying', { url: details.url, originUrl: details.originUrl } );
90+
log && console.debug( '#forceOpenInTopFrameFilter // modifying', { url: details.url, originUrl: details.originUrl } );
6191

6292
const decoder = new TextDecoder( 'utf-8' );
6393
const encoder = new TextEncoder();
@@ -77,23 +107,70 @@ const applyFilter = details => {
77107
return true;
78108
};
79109

110+
const removeIframeHeadersFilter = details => {
111+
log && console.debug( '#removeIframeHeadersFilter', options, details );
112+
113+
if ( !options.removeIframeHeaders ) {
114+
// Dont modify requests if the option is not enabled.
115+
log && console.debug( '#removeIframeHeadersFilter // removeIframeHeaders option not enabled... skipping' );
116+
return false;
117+
}
118+
119+
if ( details.originUrl !== browser.extension.getURL( extensionNewTabPath ) ) {
120+
// Don't modify requests outside of the extension new tab page.
121+
// This is still needed because the `customNewTabUrl` scope used in the onHeadersReceived listener doesnt guarantee the request is coming from this extension.
122+
log && console.debug( '#removeIframeHeadersFilter // outside of extension new tab page... skipping' );
123+
return false;
124+
}
125+
126+
log && console.debug( '#removeIframeHeadersFilter // modifying', { url: details.url, originUrl: details.originUrl } );
127+
128+
const responseHeaders = details.responseHeaders.filter(header => {
129+
return header.name.toLowerCase() !== 'x-frame-options';
130+
});
131+
132+
log && console.debug( '#removeIframeHeadersFilter // new response headers', responseHeaders );
133+
134+
// NOTE: changes made here will **not** show up in the browser DevTools network tab.
135+
return { responseHeaders };
136+
};
137+
138+
139+
const onBeforeRequestListener = async details => {
140+
try {
141+
return forceOpenInTopFrameFilter( details );
142+
} catch ( err ) {
143+
console.error( '#onBeforeRequestListener', err );
144+
return false;
145+
}
146+
};
147+
148+
149+
const onBeforeRequestListenerCleanup = _ => {
150+
const hasListener = browser.webRequest.onBeforeRequest.hasListener( onBeforeRequestListener );
151+
log && console.debug( '#onBeforeRequestListenerCleanup', { hasListener } );
80152

81-
const listener = async details => {
153+
if ( hasListener ) {
154+
browser.webRequest.onBeforeRequest.removeListener( onBeforeRequestListener );
155+
}
156+
};
157+
158+
const onHeadersReceivedListener = async details => {
82159
try {
83-
return applyFilter( details );
160+
return removeIframeHeadersFilter( details );
84161
} catch ( err ) {
85-
console.error( '#listener', err );
162+
console.error( '#onHeadersReceivedListener', err );
86163
return false;
87164
}
88165
};
89166

90167

91-
const removeRequestListener = _ => {
92-
const hasListener = browser.webRequest.onBeforeRequest.hasListener( listener );
93-
log && console.debug( '#removeRequestListener', { hasListener } );
168+
const onHeadersReceivedListenerCleanup = _ => {
169+
const hasListener = browser.webRequest.onHeadersReceived.hasListener( onHeadersReceivedListener );
170+
log && console.debug( '#onHeadersReceivedListenerCleanup', { hasListener } );
94171

95172
if ( hasListener ) {
96-
browser.webRequest.onBeforeRequest.removeListener( listener );
173+
browser.webRequest.onHeadersReceived.removeListener( onHeadersReceivedListener );
97174
}
98175
};
99176

@@ -104,37 +181,69 @@ const addRequestListener = _ => {
104181
return false;
105182
}
106183

107-
const customNewTabUrlMatchPattern = toMatchPattern( options.customNewTabUrl );
108-
log && console.debug( '#addRequestListener', { customNewTabUrlMatchPattern } );
109-
110-
if ( !customNewTabUrlMatchPattern ) {
111-
return false;
184+
// TODO: this logic is duplicated in options.js and should be centralized somewhere.
185+
const forceOpenInTopFrameCustomNewTabUrlMatchPattern = toMatchPattern( options.customNewTabUrl );
186+
log && console.debug( '#addRequestListener', { forceOpenInTopFrameCustomNewTabUrlMatchPattern } );
187+
188+
onBeforeRequestListenerCleanup();
189+
190+
if ( forceOpenInTopFrameCustomNewTabUrlMatchPattern ) {
191+
browser.webRequest.onBeforeRequest.addListener(
192+
onBeforeRequestListener,
193+
{
194+
urls: [
195+
browser.extension.getURL( extensionNewTabPath ),
196+
forceOpenInTopFrameCustomNewTabUrlMatchPattern,
197+
],
198+
types: [ 'sub_frame' ],
199+
},
200+
[ 'blocking' ],
201+
);
112202
}
113203

114-
// clean up old listeners
115-
removeRequestListener();
116-
117-
browser.webRequest.onBeforeRequest.addListener(
118-
listener,
119-
{
120-
urls: [
121-
browser.extension.getURL( extensionNewTabPath ),
122-
customNewTabUrlMatchPattern,
123-
],
124-
types: [ 'sub_frame' ],
125-
},
126-
[ 'blocking' ],
127-
);
204+
// TODO: this logic is duplicated in options.js and should be centralized somewhere.
205+
const removeIframeHeadersCustomNewTabUrlMatchPattern = toWildcardDomainMatchPattern( options.customNewTabUrl );
206+
log && console.debug( '#addRequestListener', { removeIframeHeadersCustomNewTabUrlMatchPattern } );
207+
208+
onHeadersReceivedListenerCleanup();
209+
210+
if ( removeIframeHeadersCustomNewTabUrlMatchPattern ) {
211+
browser.webRequest.onHeadersReceived.addListener(
212+
onHeadersReceivedListener,
213+
{
214+
urls: [
215+
browser.extension.getURL( extensionNewTabPath ),
216+
removeIframeHeadersCustomNewTabUrlMatchPattern,
217+
],
218+
types: [ 'sub_frame' ],
219+
},
220+
[ 'blocking', 'responseHeaders' ],
221+
);
222+
}
128223

129224
return true;
130225
};
131226

132227

228+
// TODO: this logic is duplicated in options.js and should be centralized somewhere.
133229
const hasPermissions = async _ => {
134230
try {
231+
let permissions = [];
232+
let origins = [];
233+
234+
if ( options.removeIframeHeaders ) {
235+
permissions = permissions.concat( removeIframeHeadersPermissions );
236+
origins = origins.concat( toWildcardDomainMatchPattern( options.customNewTabUrl ) );
237+
}
238+
239+
if ( options.forceOpenInTopFrame ) {
240+
permissions = permissions.concat( forceOpenInTopFramePermissions );
241+
origins = origins.concat( toMatchPattern( options.customNewTabUrl ) );
242+
}
243+
135244
const requiredPermissions = {
136-
permissions: forceOpenInTopFramePermissions,
137-
origins: [ toMatchPattern( options.customNewTabUrl ) ],
245+
permissions,
246+
origins,
138247
};
139248

140249
const hasPermissions_ = await browser.permissions.contains( requiredPermissions );
@@ -155,7 +264,7 @@ const init = async _ => {
155264
await refreshOptionsCache();
156265
log && console.debug( '#init // got options', options );
157266

158-
if ( options.forceOpenInTopFrame && customNewTabUrlExists() && await hasPermissions() ) {
267+
if ( (options.removeIframeHeaders || options.forceOpenInTopFrame) && customNewTabUrlExists() && await hasPermissions() ) {
159268
log && console.debug( '#init // has permissions, url exists, option set -- adding request listener' );
160269
addRequestListener();
161270
} else {

src/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
"storage"
2626
],
2727
"optional_permissions": [
28+
"<all_urls>",
2829
"webRequest",
29-
"webRequestBlocking",
30-
"<all_urls>"
30+
"webRequestBlocking"
3131
],
3232
"chrome_url_overrides" : {
3333
"newtab": "app.html"

src/options.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
/* GENERIC HELPER */
22
.is-hidden { display: none !important; }
33

4+
/* GENERIC OTHER */
5+
.code {
6+
padding: 0 2px;
7+
background-color: #e6e6e6;
8+
font-size: inherit;
9+
}
410

511
/* GENERIC FORM */
612
.form {

src/options.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<span class="form__hint">
1616
The URL to show when a new tab is opened. It should include the "https" or "http" part.
1717
<br>
18-
The site must allow iframe embedding or it won’t work.
18+
If the site does not allow iframe embedding enable the "Remove iframe headers" option below.
1919
</span>
2020

2121
<input class="form__input form__input--textlike" type="text" id="customNewTabUrl" placeholder="https://example.com">
@@ -47,6 +47,20 @@
4747

4848
</div>
4949

50+
<div class="form__field">
51+
<label class="form__label" for="removeIframeHeaders">Remove iframe headers?</label>
52+
53+
<input type="checkbox" id="removeIframeHeaders">
54+
55+
<span class="form__hint">
56+
If checked then this will remove any <code class="code">X-Frame-Options</code> headers from your new tab URL.
57+
<br>
58+
If you see an error like "Blocked by X-Frame-Options Policy" or "To protect your security, example.com will not allow Firefox to display the page if another site has embedded it. To see this page, you need to open it in a new window." then you can enable this option to get around the error.
59+
<br>
60+
This requires several extra permissions which you will be prompted to allow if you check this box.
61+
</span>
62+
</div>
63+
5064
<div class="form__field">
5165
<label class="form__label" for="forceOpenInTopFrame">Force links to open in the top frame (experimental)?</label>
5266

0 commit comments

Comments
 (0)