You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

885 lines
30 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. /*******************************************************************************
  2. µMatrix - a Chromium browser extension to black/white list requests.
  3. Copyright (C) 2014 Raymond Hill
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see {http://www.gnu.org/licenses/}.
  14. Home: https://github.com/gorhill/uMatrix
  15. */
  16. /* global chrome, µMatrix */
  17. /******************************************************************************/
  18. // Start isolation from global scope
  19. µMatrix.webRequest = (function() {
  20. /******************************************************************************/
  21. // The `id='uMatrix'` is important, it allows µMatrix to detect whether a
  22. // specific data URI originates from itself.
  23. var rootFrameReplacement = [
  24. '<!DOCTYPE html><html id="uMatrix">',
  25. '<head>',
  26. '<meta charset="utf-8" />',
  27. '<style>',
  28. '@font-face {',
  29. 'font-family:httpsb;',
  30. 'font-style:normal;',
  31. 'font-weight:400;',
  32. 'src: local("httpsb"),url("',
  33. µMatrix.fontCSSURL,
  34. '") format("truetype");',
  35. '}',
  36. 'body {',
  37. 'margin:0;',
  38. 'border:0;',
  39. 'padding:0;',
  40. 'font:15px httpsb,sans-serif;',
  41. 'width:100%;',
  42. 'height:100%;',
  43. 'background-color:transparent;',
  44. 'background-size:10px 10px;',
  45. 'background-image:',
  46. 'repeating-linear-gradient(',
  47. '-45deg,',
  48. 'rgba(204,0,0,0.5),rgba(204,0,0,0.5) 24%,',
  49. 'transparent 26%,transparent 49%,',
  50. 'rgba(204,0,0,0.5) 51%,rgba(204,0,0,0.5) 74%,',
  51. 'transparent 76%,transparent',
  52. ');',
  53. 'text-align: center;',
  54. '}',
  55. '#p {',
  56. 'margin:8px;',
  57. 'padding:4px;',
  58. 'display:inline-block;',
  59. 'background-color:white;',
  60. '}',
  61. '#t {',
  62. 'margin:2px;',
  63. 'border:0;',
  64. 'padding:0 2px;',
  65. 'display:inline-block;',
  66. '}',
  67. '#t b {',
  68. 'padding:0 4px;',
  69. 'background-color:#eee;',
  70. 'font-weight:normal;',
  71. '}',
  72. '</style>',
  73. '<link href="{{cssURL}}?url={{originalURL}}&hostname={{hostname}}&t={{now}}" rel="stylesheet" type="text/css">',
  74. '<title>Blocked by &mu;Matrix</title>',
  75. '</head>',
  76. '<body>',
  77. '<div id="p">',
  78. '<div id="t"><b>{{hostname}}</b> blocked by &mu;Matrix</div>',
  79. '</div>',
  80. '</body>',
  81. '</html>'
  82. ].join('');
  83. var subFrameReplacement = [
  84. '<!DOCTYPE html>',
  85. '<html>',
  86. '<head>',
  87. '<meta charset="utf-8" />',
  88. '<style>',
  89. '@font-face{',
  90. 'font-family:httpsb;',
  91. 'font-style:normal;',
  92. 'font-weight:400;',
  93. 'src:local("httpsb"),url("',
  94. µMatrix.fontCSSURL,
  95. '") format("truetype");',
  96. '}',
  97. 'body{',
  98. 'margin:0;',
  99. 'border:0;',
  100. 'padding:0;',
  101. 'font:13px httpsb,sans-serif;',
  102. '}',
  103. '#bg{',
  104. 'border:1px dotted {{subframeColor}};',
  105. 'position:absolute;',
  106. 'top:0;',
  107. 'right:0;',
  108. 'bottom:0;',
  109. 'left:0;',
  110. 'background-color:transparent;',
  111. 'background-size:10px 10px;',
  112. 'background-image:',
  113. 'repeating-linear-gradient(',
  114. '-45deg,',
  115. '{{subframeColor}},{{subframeColor}} 24%,',
  116. 'transparent 25%,transparent 49%,',
  117. '{{subframeColor}} 50%,{{subframeColor}} 74%,',
  118. 'transparent 75%,transparent',
  119. ');',
  120. 'opacity:{{subframeOpacity}};',
  121. 'text-align:center;',
  122. '}',
  123. '#bg > div{',
  124. 'display:inline-block;',
  125. 'background-color:rgba(255,255,255,1);',
  126. '}',
  127. '#bg > div > a {',
  128. 'padding:0 2px;',
  129. 'display:inline-block;',
  130. 'color:white;',
  131. 'background-color:{{subframeColor}};',
  132. 'text-decoration:none;',
  133. '}',
  134. '</style>',
  135. '<title>Blocked by &mu;Matrix</title>',
  136. '</head>',
  137. '<body title="&ldquo;{{hostname}}&rdquo; frame\nblocked by &mu;Matrix">',
  138. '<div id="bg"><div><a href="{{frameSrc}}" target="_blank">{{hostname}}</a></div></div>',
  139. '</body>',
  140. '</html>'
  141. ].join('');
  142. /******************************************************************************/
  143. // If it is HTTP Switchboard's root frame replacement URL, verify that
  144. // the page that was blacklisted is still blacklisted, and if not,
  145. // redirect to the previously blacklisted page.
  146. var onBeforeChromeExtensionRequestHandler = function(details) {
  147. var requestURL = details.url;
  148. // console.debug('onBeforeChromeExtensionRequestHandler()> "%s": %o', details.url, details);
  149. // rhill 2013-12-10: Avoid regex whenever a faster indexOf() can be used:
  150. // here we can use fast indexOf() as a first filter -- which is executed
  151. // for every single request (so speed matters).
  152. var matches = requestURL.match(/url=([^&]+)&hostname=([^&]+)/);
  153. if ( !matches ) {
  154. return;
  155. }
  156. var µm = µMatrix;
  157. var pageURL = decodeURIComponent(matches[1]);
  158. var pageHostname = decodeURIComponent(matches[2]);
  159. // Blacklisted as per matrix?
  160. if ( µm.mustBlock(µm.scopeFromURL(pageURL), pageHostname, 'doc') ) {
  161. return;
  162. }
  163. µMatrix.asyncJobs.add(
  164. 'gotoURL-' + details.tabId,
  165. { tabId: details.tabId, url: pageURL },
  166. µm.utils.gotoURL,
  167. 200,
  168. false
  169. );
  170. };
  171. /******************************************************************************/
  172. // Intercept and filter web requests according to white and black lists.
  173. var onBeforeRootFrameRequestHandler = function(details) {
  174. var µm = µMatrix;
  175. // Do not ignore traffic outside tabs
  176. var tabId = details.tabId;
  177. if ( tabId < 0 ) {
  178. tabId = µm.behindTheSceneTabId;
  179. }
  180. // It's a root frame, bind to a new page store
  181. else {
  182. µm.bindTabToPageStats(tabId, details.url);
  183. }
  184. var uri = µm.URI.set(details.url);
  185. if ( uri.scheme.indexOf('http') === -1 ) {
  186. return;
  187. }
  188. var requestURL = uri.normalizedURI();
  189. var requestHostname = uri.hostname;
  190. var pageStore = µm.pageStatsFromTabId(tabId);
  191. // Disallow request as per matrix?
  192. var block = µm.mustBlock(pageStore.pageHostname, requestHostname, 'doc');
  193. // console.debug('onBeforeRequestHandler()> block=%s "%s": %o', block, details.url, details);
  194. // whitelisted?
  195. if ( !block ) {
  196. // rhill 2013-11-07: Senseless to do this for behind-the-scene requests.
  197. // rhill 2013-12-03: Do this here only for root frames.
  198. if ( tabId !== µm.behindTheSceneTabId ) {
  199. µm.cookieHunter.recordPageCookies(pageStore);
  200. }
  201. return;
  202. }
  203. // blacklisted
  204. // rhill 2014-01-15: Delay logging of non-blocked top `main_frame`
  205. // requests, in order to ensure any potential redirects is reported
  206. // in proper chronological order.
  207. // https://github.com/gorhill/httpswitchboard/issues/112
  208. pageStore.recordRequest('doc', requestURL, block);
  209. // If it's a blacklisted frame, redirect to frame.html
  210. // rhill 2013-11-05: The root frame contains a link to noop.css, this
  211. // allows to later check whether the root frame has been unblocked by the
  212. // user, in which case we are able to force a reload using a redirect.
  213. var html = rootFrameReplacement;
  214. html = html.replace('{{cssURL}}', µm.noopCSSURL);
  215. html = html.replace(/{{hostname}}/g, encodeURIComponent(requestHostname));
  216. html = html.replace('{{originalURL}}', encodeURIComponent(requestURL));
  217. html = html.replace('{{now}}', String(Date.now()));
  218. var dataURI = 'data:text/html;base64,' + btoa(html);
  219. return { 'redirectUrl': dataURI };
  220. };
  221. /******************************************************************************/
  222. // Intercept and filter web requests according to white and black lists.
  223. var onBeforeRequestHandler = function(details) {
  224. var µm = µMatrix;
  225. var µmuri = µm.URI.set(details.url);
  226. var requestScheme = µmuri.scheme;
  227. // rhill 2014-02-17: Ignore 'filesystem:': this can happen when listening
  228. // to 'chrome-extension://'.
  229. if ( requestScheme === 'filesystem' ) {
  230. return;
  231. }
  232. // console.debug('onBeforeRequestHandler()> "%s": %o', details.url, details);
  233. var requestType = requestTypeNormalizer[details.type];
  234. // https://github.com/gorhill/httpswitchboard/issues/303
  235. // Wherever the main doc comes from, create a receiver page URL: synthetize
  236. // one if needed.
  237. if ( requestType === 'doc' && details.parentFrameId < 0 ) {
  238. return onBeforeRootFrameRequestHandler(details);
  239. }
  240. var requestURL = details.url;
  241. // Is it µMatrix's noop css file?
  242. if ( requestType === 'css' && requestURL.slice(0, µm.noopCSSURL.length) === µm.noopCSSURL ) {
  243. return onBeforeChromeExtensionRequestHandler(details);
  244. }
  245. // Ignore non-http schemes
  246. if ( requestScheme.indexOf('http') !== 0 ) {
  247. return;
  248. }
  249. // Do not block myself from updating assets
  250. // https://github.com/gorhill/httpswitchboard/issues/202
  251. if ( requestType === 'xhr' && requestURL.slice(0, µm.projectServerRoot.length) === µm.projectServerRoot ) {
  252. return;
  253. }
  254. var requestHostname = µmuri.hostname;
  255. // rhill 2013-12-15:
  256. // Try to transpose generic `other` category into something more
  257. // meaningful.
  258. if ( requestType === 'other' ) {
  259. requestType = µm.transposeType(requestType, µmuri.path);
  260. }
  261. // Re-classify orphan HTTP requests as behind-the-scene requests. There is
  262. // not much else which can be done, because there are URLs
  263. // which cannot be handled by µMatrix, i.e. `opera://startpage`,
  264. // as this would lead to complications with no obvious solution, like how
  265. // to scope on unknown scheme? Etc.
  266. // https://github.com/gorhill/httpswitchboard/issues/191
  267. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  268. var pageStore = µm.pageStatsFromTabId(details.tabId);
  269. if ( !pageStore ) {
  270. pageStore = µm.pageStatsFromTabId(µm.behindTheSceneTabId);
  271. }
  272. // Disallow request as per temporary matrix?
  273. var block = µm.mustBlock(pageStore.pageHostname, requestHostname, requestType);
  274. // Record request.
  275. // https://github.com/gorhill/httpswitchboard/issues/342
  276. // The way requests are handled now, it may happen at this point some
  277. // processing has already been performed, and that a synthetic URL has
  278. // been constructed for logging purpose. Use this synthetic URL if
  279. // it is available.
  280. pageStore.recordRequest(requestType, requestURL, block);
  281. // whitelisted?
  282. if ( !block ) {
  283. // console.debug('onBeforeRequestHandler()> ALLOW "%s": %o', details.url, details);
  284. return;
  285. }
  286. // blacklisted
  287. // console.debug('onBeforeRequestHandler()> BLOCK "%s": %o', details.url, details);
  288. // If it's a blacklisted frame, redirect to frame.html
  289. // rhill 2013-11-05: The root frame contains a link to noop.css, this
  290. // allows to later check whether the root frame has been unblocked by the
  291. // user, in which case we are able to force a reload using a redirect.
  292. if ( requestType === 'frame' ) {
  293. var html = subFrameReplacement
  294. .replace(/{{hostname}}/g, requestHostname)
  295. .replace('{{frameSrc}}', requestURL)
  296. .replace(/{{subframeColor}}/g, µm.userSettings.subframeColor)
  297. .replace('{{subframeOpacity}}', (µm.userSettings.subframeOpacity / 100).toFixed(1));
  298. return { 'redirectUrl': 'data:text/html,' + encodeURIComponent(html) };
  299. }
  300. return { 'cancel': true };
  301. };
  302. /******************************************************************************/
  303. // This is where tabless requests are processed, as here there may be a chance
  304. // we can bind a request to a specific tab, as headers may contain useful
  305. // information to accomplish this.
  306. //
  307. // Also we sanitize outgoing headers as per user settings.
  308. var onBeforeSendHeadersHandler = function(details) {
  309. var µm = µMatrix;
  310. // console.debug('onBeforeSendHeadersHandler()> "%s": %o', details.url, details);
  311. // Re-classify orphan HTTP requests as behind-the-scene requests. There is
  312. // not much else which can be done, because there are URLs
  313. // which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
  314. // as this would lead to complications with no obvious solution, like how
  315. // to scope on unknown scheme? Etc.
  316. // https://github.com/gorhill/httpswitchboard/issues/191
  317. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  318. var tabId = details.tabId;
  319. var pageStore = µm.pageStatsFromTabId(tabId);
  320. if ( !pageStore ) {
  321. tabId = µm.behindTheSceneTabId;
  322. pageStore = µm.pageStatsFromTabId(tabId);
  323. }
  324. // https://github.com/gorhill/httpswitchboard/issues/342
  325. // Is this hyperlink auditing?
  326. // If yes, create a synthetic URL for reporting hyperlink auditing
  327. // in request log. This way the user is better informed of what went
  328. // on.
  329. var requestURL = details.url;
  330. var requestType = requestTypeNormalizer[details.type];
  331. if ( requestType === 'other' ) {
  332. var linkAuditor = hyperlinkAuditorFromHeaders(details.requestHeaders);
  333. if ( linkAuditor ) {
  334. var block = µm.userSettings.processHyperlinkAuditing;
  335. pageStore.recordRequest('other', requestURL + '{Ping-To:' + linkAuditor + '}', block);
  336. if ( block ) {
  337. µm.hyperlinkAuditingFoiledCounter += 1;
  338. return { 'cancel': true };
  339. }
  340. }
  341. }
  342. // If we reach this point, request is not blocked, so what is left to do
  343. // is to sanitize headers.
  344. var reqHostname = µm.hostnameFromURL(requestURL);
  345. var changed = false;
  346. if ( µm.mustBlock(pageStore.pageHostname, reqHostname, 'cookie') ) {
  347. changed = foilCookieHeaders(µm, details) || changed;
  348. }
  349. if ( µm.tMatrix.evaluateSwitchZ('referrer-spoof', pageStore.pageHostname) ) {
  350. changed = foilRefererHeaders(µm, reqHostname, details) || changed;
  351. }
  352. if ( µm.tMatrix.evaluateSwitchZ('ua-spoof', pageStore.pageHostname) ) {
  353. changed = foilUserAgent(µm, details) || changed;
  354. // https://github.com/gorhill/httpswitchboard/issues/252
  355. // To avoid potential mismatch between the user agent from HTTP headers
  356. // and the user agent from subrequests and the window.navigator object,
  357. // I could always store here the effective user agent, but I am really
  358. // not convinced it is worth the added overhead given the low
  359. // probability and the benign consequence if it ever happen. Can always
  360. // be revised if ever I become aware a mismatch is a terrible thing
  361. }
  362. if ( changed ) {
  363. // console.debug('onBeforeSendHeadersHandler()> CHANGED "%s": %o', requestURL, details);
  364. return { requestHeaders: details.requestHeaders };
  365. }
  366. };
  367. /******************************************************************************/
  368. // http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#hyperlink-auditing
  369. //
  370. // Target URL = the href of the link
  371. // Doc URL = URL of the document containing the target URL
  372. // Ping URLs = servers which will be told that user clicked target URL
  373. //
  374. // `Content-Type` = `text/ping` (always present)
  375. // `Ping-To` = target URL (always present)
  376. // `Ping-From` = doc URL
  377. // `Referer` = doc URL
  378. // request URL = URL which will receive the information
  379. //
  380. // With hyperlink-auditing, removing header(s) is pointless, the whole
  381. // request must be cancelled.
  382. var hyperlinkAuditorFromHeaders = function(headers) {
  383. var i = headers.length;
  384. while ( i-- ) {
  385. if ( headers[i].name.toLowerCase() === 'ping-to' ) {
  386. return headers[i].value;
  387. }
  388. }
  389. return;
  390. };
  391. /******************************************************************************/
  392. var tabIdFromHeaders = function(µm, headers) {
  393. var header;
  394. var i = headers.length;
  395. while ( i-- ) {
  396. header = headers[i];
  397. if ( header.name.toLowerCase() === 'referer' ) {
  398. return µm.tabIdFromPageUrl(header.value);
  399. }
  400. if ( header.name.toLowerCase() === 'ping-from' ) {
  401. return µm.tabIdFromPageUrl(header.value);
  402. }
  403. }
  404. return -1;
  405. };
  406. /******************************************************************************/
  407. var foilCookieHeaders = function(µm, details) {
  408. var changed = false;
  409. var headers = details.requestHeaders;
  410. var header;
  411. var i = headers.length;
  412. while ( i-- ) {
  413. header = headers[i];
  414. if ( header.name.toLowerCase() !== 'cookie' ) {
  415. continue;
  416. }
  417. // console.debug('foilCookieHeaders()> foiled browser attempt to send cookie(s) to "%s"', details.url);
  418. headers.splice(i, 1);
  419. µm.cookieHeaderFoiledCounter++;
  420. changed = true;
  421. }
  422. return changed;
  423. };
  424. /******************************************************************************/
  425. var foilRefererHeaders = function(µm, toHostname, details) {
  426. var headers = details.requestHeaders;
  427. var i = headers.length, header;
  428. while ( i-- ) {
  429. header = headers[i];
  430. if ( header.name.toLowerCase() === 'referer' ) {
  431. break;
  432. }
  433. }
  434. if ( i === -1 ) {
  435. return false;
  436. }
  437. var µmuri = µm.URI;
  438. var fromDomain = µmuri.domainFromURI(header.value);
  439. var toDomain = µmuri.domainFromHostname(toHostname);
  440. if ( toDomain === fromDomain ) {
  441. return false;
  442. }
  443. //console.debug('foilRefererHeaders()> foiled referer for "%s"', details.url);
  444. //console.debug('\treferrer "%s"', header.value);
  445. // https://github.com/gorhill/httpswitchboard/issues/222#issuecomment-44828402
  446. header.value = µmuri.schemeFromURI(details.url) + '://' + toHostname + '/';
  447. //console.debug('\treplaced with "%s"', header.value);
  448. µm.refererHeaderFoiledCounter++;
  449. return true;
  450. };
  451. /******************************************************************************/
  452. var foilUserAgent = function(µm, details) {
  453. var headers = details.requestHeaders;
  454. var header;
  455. var i = 0;
  456. while ( header = headers[i] ) {
  457. if ( header.name.toLowerCase() === 'user-agent' ) {
  458. header.value = µm.userAgentReplaceStr;
  459. return true; // Assuming only one `user-agent` entry
  460. }
  461. i += 1;
  462. }
  463. return false;
  464. };
  465. /******************************************************************************/
  466. // To prevent inline javascript from being executed.
  467. // Prevent inline scripting using `Content-Security-Policy`:
  468. // https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html
  469. // This fixes:
  470. // https://github.com/gorhill/httpswitchboard/issues/35
  471. var onHeadersReceived = function(details) {
  472. // console.debug('onHeadersReceived()> "%s": %o', details.url, details);
  473. // Ignore schemes other than 'http...'
  474. if ( details.url.slice(0, 4) !== 'http' ) {
  475. return;
  476. }
  477. var requestType = requestTypeNormalizer[details.type];
  478. if ( requestType === 'frame' ) {
  479. return onSubDocHeadersReceived(details);
  480. }
  481. if ( requestType === 'doc' ) {
  482. return onMainDocHeadersReceived(details);
  483. }
  484. };
  485. /******************************************************************************/
  486. var onMainDocHeadersReceived = function(details) {
  487. // console.debug('onMainDocHeadersReceived()> "%s": %o', details.url, details);
  488. var µm = µMatrix;
  489. // Do not ignore traffic outside tabs.
  490. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  491. var tabId = details.tabId;
  492. if ( tabId < 0 ) {
  493. tabId = µm.behindTheSceneTabId;
  494. }
  495. var µmuri = µm.URI.set(details.url);
  496. var requestURL = µmuri.normalizedURI();
  497. var requestScheme = µmuri.scheme;
  498. var requestHostname = µmuri.hostname;
  499. // rhill 2013-12-07:
  500. // Apparently in Opera, onBeforeRequest() is triggered while the
  501. // URL is not yet bound to a tab (-1), which caused the code here
  502. // to not be able to lookup the pageStats. So let the code here bind
  503. // the page to a tab if not done yet.
  504. // https://github.com/gorhill/httpswitchboard/issues/75
  505. µm.bindTabToPageStats(tabId, requestURL);
  506. // Re-classify orphan HTTP requests as behind-the-scene requests. There is
  507. // not much else which can be done, because there are URLs
  508. // which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
  509. // as this would lead to complications with no obvious solution, like how
  510. // to scope on unknown scheme? Etc.
  511. // https://github.com/gorhill/httpswitchboard/issues/191
  512. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  513. var pageStats = µm.pageStatsFromTabId(tabId);
  514. if ( !pageStats ) {
  515. tabId = µm.behindTheSceneTabId;
  516. pageStats = µm.pageStatsFromTabId(tabId);
  517. }
  518. var headers = details.responseHeaders;
  519. // Simplify code paths by splitting func in two different handlers, one
  520. // for main docs, one for sub docs.
  521. // rhill 2014-01-15: Report redirects.
  522. // https://github.com/gorhill/httpswitchboard/issues/112
  523. // rhill 2014-02-10: Handle all redirects.
  524. // https://github.com/gorhill/httpswitchboard/issues/188
  525. if ( /\s+30[12378]\s+/.test(details.statusLine) ) {
  526. var i = headerIndexFromName('location', headers);
  527. if ( i >= 0 ) {
  528. // rhill 2014-01-20: Be ready to handle relative URLs.
  529. // https://github.com/gorhill/httpswitchboard/issues/162
  530. var locationURL = µmuri.set(headers[i].value.trim()).normalizedURI();
  531. if ( µmuri.authority === '' ) {
  532. locationURL = requestScheme + '://' + requestHostname + µmuri.path;
  533. }
  534. µm.redirectRequests[locationURL] = requestURL;
  535. }
  536. // console.debug('onMainDocHeadersReceived()> redirect "%s" to "%s"', requestURL, headers[i].value);
  537. }
  538. // rhill 2014-01-15: Report redirects if any.
  539. // https://github.com/gorhill/httpswitchboard/issues/112
  540. if ( details.statusLine.indexOf(' 200') > 0 ) {
  541. var mainFrameStack = [requestURL];
  542. var destinationURL = requestURL;
  543. var sourceURL;
  544. while ( sourceURL = µm.redirectRequests[destinationURL] ) {
  545. mainFrameStack.push(sourceURL);
  546. delete µm.redirectRequests[destinationURL];
  547. destinationURL = sourceURL;
  548. }
  549. while ( destinationURL = mainFrameStack.pop() ) {
  550. pageStats.recordRequest('doc', destinationURL, false);
  551. }
  552. }
  553. // Maybe modify inbound headers
  554. var csp = '';
  555. // Enforce strict HTTPS?
  556. if ( requestScheme === 'https' && µm.tMatrix.evaluateSwitchZ('https-strict', pageStats.pageHostname) ) {
  557. csp += "default-src chrome-search: data: https: wss: 'unsafe-eval' 'unsafe-inline';";
  558. }
  559. // https://github.com/gorhill/httpswitchboard/issues/181
  560. pageStats.pageScriptBlocked = µm.mustBlock(pageStats.pageHostname, requestHostname, 'script');
  561. if ( pageStats.pageScriptBlocked ) {
  562. // If javascript not allowed, say so through a `Content-Security-Policy` directive.
  563. // console.debug('onMainDocHeadersReceived()> PAGE CSP "%s": %o', details.url, details);
  564. csp += " script-src 'none'";
  565. }
  566. // https://github.com/gorhill/httpswitchboard/issues/181
  567. if ( csp !== '' ) {
  568. headers.push({
  569. 'name': 'Content-Security-Policy',
  570. 'value': csp.trim()
  571. });
  572. return { responseHeaders: headers };
  573. }
  574. };
  575. /******************************************************************************/
  576. var onSubDocHeadersReceived = function(details) {
  577. // console.debug('onSubDocHeadersReceived()> "%s": %o', details.url, details);
  578. var µm = µMatrix;
  579. // Do not ignore traffic outside tabs.
  580. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  581. var tabId = details.tabId;
  582. if ( tabId < 0 ) {
  583. tabId = µm.behindTheSceneTabId;
  584. }
  585. // Re-classify orphan HTTP requests as behind-the-scene requests. There is
  586. // not much else which can be done, because there are URLs
  587. // which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
  588. // as this would lead to complications with no obvious solution, like how
  589. // to scope on unknown scheme? Etc.
  590. // https://github.com/gorhill/httpswitchboard/issues/191
  591. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  592. var pageStats = µm.pageStatsFromTabId(tabId);
  593. if ( !pageStats ) {
  594. tabId = µm.behindTheSceneTabId;
  595. pageStats = µm.pageStatsFromTabId(tabId);
  596. }
  597. // Evaluate
  598. if ( µm.mustAllow(pageStats.pageHostname, µm.hostnameFromURL(details.url), 'script') ) {
  599. return;
  600. }
  601. // If javascript not allowed, say so through a `Content-Security-Policy`
  602. // directive.
  603. // For inline javascript within iframes, we need to sandbox.
  604. // https://github.com/gorhill/httpswitchboard/issues/73
  605. // Now because sandbox cancels all permissions, this means
  606. // not just javascript is disabled. To avoid negative side
  607. // effects, I allow some other permissions, but...
  608. // https://github.com/gorhill/uMatrix/issues/27
  609. // Need to add `allow-popups` to prevent completely breaking links on
  610. // some sites old style sites.
  611. // TODO: Reuse CSP `sandbox` directive if it's already in the
  612. // headers (strip out `allow-scripts` if present),
  613. // and find out if the `sandbox` in the header interfere with a
  614. // `sandbox` attribute which might be present on the iframe.
  615. // console.debug('onSubDocHeadersReceived()> FRAME CSP "%s": %o, scope="%s"', details.url, details, pageURL);
  616. details.responseHeaders.push({
  617. 'name': 'Content-Security-Policy',
  618. 'value': 'sandbox allow-forms allow-same-origin allow-popups allow-top-navigation'
  619. });
  620. return { responseHeaders: details.responseHeaders };
  621. };
  622. /******************************************************************************/
  623. // As per Chrome API doc, webRequest.onErrorOccurred event is the last
  624. // one called in the sequence of webRequest events.
  625. // http://developer.chrome.com/extensions/webRequest.html
  626. var onErrorOccurredHandler = function(details) {
  627. // console.debug('onErrorOccurred()> "%s": %o', details.url, details);
  628. var requestType = requestTypeNormalizer[details.type];
  629. // Ignore all that is not a main document
  630. if ( requestType !== 'doc'|| details.parentFrameId >= 0 ) {
  631. return;
  632. }
  633. var µm = µMatrix;
  634. var pageStats = µm.pageStatsFromPageUrl(details.url);
  635. if ( !pageStats ) {
  636. return;
  637. }
  638. // rhill 2014-01-28: Unwind the stack of redirects if any. Chromium will
  639. // emit an error when a web page redirects apparently endlessly, so
  640. // we need to unravel and report all these redirects upon error.
  641. // https://github.com/gorhill/httpswitchboard/issues/171
  642. var requestURL = µm.URI.set(details.url).normalizedURI();
  643. var mainFrameStack = [requestURL];
  644. var destinationURL = requestURL;
  645. var sourceURL;
  646. while ( sourceURL = µm.redirectRequests[destinationURL] ) {
  647. mainFrameStack.push(sourceURL);
  648. delete µm.redirectRequests[destinationURL];
  649. destinationURL = sourceURL;
  650. }
  651. while ( destinationURL = mainFrameStack.pop() ) {
  652. pageStats.recordRequest('doc', destinationURL, false);
  653. }
  654. };
  655. /******************************************************************************/
  656. // Caller must ensure headerName is normalized to lower case.
  657. var headerIndexFromName = function(headerName, headers) {
  658. var i = headers.length;
  659. while ( i-- ) {
  660. if ( headers[i].name.toLowerCase() === headerName ) {
  661. return i;
  662. }
  663. }
  664. return -1;
  665. };
  666. /******************************************************************************/
  667. var requestTypeNormalizer = {
  668. 'main_frame' : 'doc',
  669. 'sub_frame' : 'frame',
  670. 'stylesheet' : 'css',
  671. 'script' : 'script',
  672. 'image' : 'image',
  673. 'object' : 'plugin',
  674. 'xmlhttprequest': 'xhr',
  675. 'other' : 'other'
  676. };
  677. /******************************************************************************/
  678. var start = function() {
  679. chrome.webRequest.onBeforeRequest.addListener(
  680. //function(details) {
  681. // quickProfiler.start('onBeforeRequest');
  682. // var r = onBeforeRequestHandler(details);
  683. // quickProfiler.stop();
  684. // return r;
  685. //},
  686. onBeforeRequestHandler,
  687. {
  688. "urls": [
  689. "http://*/*",
  690. "https://*/*",
  691. "chrome-extension://*/*"
  692. ],
  693. "types": [
  694. "main_frame",
  695. "sub_frame",
  696. 'stylesheet',
  697. "script",
  698. "image",
  699. "object",
  700. "xmlhttprequest",
  701. "other"
  702. ]
  703. },
  704. [ "blocking" ]
  705. );
  706. //console.log('µMatrix > Beginning to intercept net requests at %s', (new Date()).toISOString());
  707. chrome.webRequest.onBeforeSendHeaders.addListener(
  708. onBeforeSendHeadersHandler,
  709. {
  710. 'urls': [
  711. "http://*/*",
  712. "https://*/*"
  713. ]
  714. },
  715. ['blocking', 'requestHeaders']
  716. );
  717. chrome.webRequest.onHeadersReceived.addListener(
  718. onHeadersReceived,
  719. {
  720. 'urls': [
  721. "http://*/*",
  722. "https://*/*"
  723. ]
  724. },
  725. ['blocking', 'responseHeaders']
  726. );
  727. chrome.webRequest.onErrorOccurred.addListener(
  728. onErrorOccurredHandler,
  729. {
  730. 'urls': [
  731. "http://*/*",
  732. "https://*/*"
  733. ]
  734. }
  735. );
  736. };
  737. /******************************************************************************/
  738. return {
  739. blockedRootFramePrefix: 'data:text/html;base64,' + btoa(rootFrameReplacement).slice(0, 80),
  740. start: start
  741. };
  742. /******************************************************************************/
  743. })();
  744. /******************************************************************************/