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.

941 lines
32 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
  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 µMatrix</title>',
  75. '</head>',
  76. '<body>',
  77. '<div id="p">',
  78. '<div id="t"><b>{{hostname}}</b> blocked by µ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 µMatrix</title>',
  136. '</head>',
  137. '<body title="&ldquo;{{hostname}}&rdquo; frame\nblocked by µ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. // Is the target page still blacklisted?
  158. var pageURL = decodeURIComponent(matches[1]);
  159. var hostname = decodeURIComponent(matches[2]);
  160. if ( µm.mustBlock(µm.scopeFromURL(pageURL), hostname, '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 stats store
  181. else {
  182. µm.bindTabToPageStats(tabId, details.url);
  183. }
  184. var uri = µm.URI.set(details.url);
  185. if ( uri.scheme !== 'http' && uri.scheme !== 'https' ) {
  186. return;
  187. }
  188. var requestURL = uri.normalizedURI();
  189. var requestHostname = uri.hostname;
  190. var pageStats = µm.pageStatsFromTabId(tabId);
  191. var pageURL = µm.pageUrlFromPageStats(pageStats);
  192. var block = µm.mustBlock(pageStats.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(pageStats);
  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. pageStats.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. // Process a request.
  223. //
  224. // This can be called from the context of onBeforeSendRequest() or
  225. // onBeforeSendHeaders().
  226. var processRequest = function(µm, details) {
  227. var µmuri = µm.URI;
  228. var requestType = requestTypeNormalizer[details.type];
  229. var requestURL = µmuri.set(details.url).normalizedURI();
  230. var requestHostname = µmuri.hostname;
  231. var requestPath = µmuri.path;
  232. // Re-classify orphan HTTP requests as behind-the-scene requests. There is
  233. // not much else which can be done, because there are URLs
  234. // which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
  235. // as this would lead to complications with no obvious solution, like how
  236. // to scope on unknown scheme? Etc.
  237. // https://github.com/gorhill/httpswitchboard/issues/191
  238. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  239. var pageStats = µm.pageStatsFromTabId(details.tabId);
  240. if ( !pageStats ) {
  241. pageStats = µm.pageStatsFromTabId(µm.behindTheSceneTabId);
  242. }
  243. var pageURL = µm.pageUrlFromPageStats(pageStats);
  244. // rhill 2013-12-15:
  245. // Try to transpose generic `other` category into something more
  246. // meaningful.
  247. if ( requestType === 'other' ) {
  248. requestType = µm.transposeType(requestType, requestPath);
  249. }
  250. // Block request?
  251. var block = µm.mustBlock(pageStats.pageHostname, requestHostname, requestType);
  252. // Record request.
  253. // https://github.com/gorhill/httpswitchboard/issues/342
  254. // The way requests are handled now, it may happen at this point some
  255. // processing has already been performed, and that a synthetic URL has
  256. // been constructed for logging purpose. Use this synthetic URL if
  257. // it is available.
  258. pageStats.recordRequest(requestType, details.µmRequestURL || requestURL, block);
  259. // whitelisted?
  260. if ( !block ) {
  261. // console.debug('onBeforeRequestHandler()> ALLOW "%s": %o', details.url, details);
  262. return;
  263. }
  264. // blacklisted
  265. // console.debug('onBeforeRequestHandler()> BLOCK "%s": %o', details.url, details);
  266. // If it's a blacklisted frame, redirect to frame.html
  267. // rhill 2013-11-05: The root frame contains a link to noop.css, this
  268. // allows to later check whether the root frame has been unblocked by the
  269. // user, in which case we are able to force a reload using a redirect.
  270. if ( requestType === 'frame' ) {
  271. var html = subFrameReplacement
  272. .replace(/{{hostname}}/g, requestHostname)
  273. .replace('{{frameSrc}}', requestURL)
  274. .replace(/{{subframeColor}}/g, µm.userSettings.subframeColor)
  275. .replace('{{subframeOpacity}}', (µm.userSettings.subframeOpacity / 100).toFixed(1));
  276. return { 'redirectUrl': 'data:text/html,' + encodeURIComponent(html) };
  277. }
  278. return { 'cancel': true };
  279. };
  280. /******************************************************************************/
  281. // Intercept and filter web requests according to white and black lists.
  282. var onBeforeRequestHandler = function(details) {
  283. var µm = µMatrix;
  284. var µmuri = µm.URI;
  285. var requestURL = details.url;
  286. var requestScheme = µmuri.schemeFromURI(requestURL);
  287. // rhill 2014-02-17: Ignore 'filesystem:': this can happen when listening
  288. // to 'chrome-extension://'.
  289. if ( requestScheme === 'filesystem' ) {
  290. return;
  291. }
  292. // console.debug('onBeforeRequestHandler()> "%s": %o', details.url, details);
  293. var requestType = requestTypeNormalizer[details.type];
  294. // https://github.com/gorhill/httpswitchboard/issues/303
  295. // Wherever the main doc comes from, create a receiver page URL: synthetize
  296. // one if needed.
  297. if ( requestType === 'doc' && details.parentFrameId < 0 ) {
  298. return onBeforeRootFrameRequestHandler(details);
  299. }
  300. // Is it µMatrix's noop css file?
  301. if ( requestType === 'css' && requestURL.slice(0, µm.noopCSSURL.length) === µm.noopCSSURL ) {
  302. return onBeforeChromeExtensionRequestHandler(details);
  303. }
  304. // Ignore non-http schemes
  305. if ( requestScheme.indexOf('http') !== 0 ) {
  306. return;
  307. }
  308. // Do not block myself from updating assets
  309. // https://github.com/gorhill/httpswitchboard/issues/202
  310. if ( requestType === 'xhr' && requestURL.slice(0, µm.projectServerRoot.length) === µm.projectServerRoot ) {
  311. return;
  312. }
  313. // https://github.com/gorhill/httpswitchboard/issues/342
  314. // If the request cannot be bound to a specific tab, delay request
  315. // handling to onBeforeSendHeaders(), maybe there the referrer
  316. // information will allow us to properly bind the request to the tab
  317. // from which it originates.
  318. // Important: since it is not possible to redirect requests at
  319. // onBeforeSendHeaders() point, we can't delay when the request type is
  320. // `sub_frame`.
  321. if ( requestType !== 'frame' && details.tabId < 0 ) {
  322. return;
  323. }
  324. return processRequest(µm, details);
  325. };
  326. /******************************************************************************/
  327. // This is where tabless requests are processed, as here there may be a chance
  328. // we can bind a request to a specific tab, as headers may contain useful
  329. // information to accomplish this.
  330. //
  331. // Also we sanitize outgoing headers as per user settings.
  332. var onBeforeSendHeadersHandler = function(details) {
  333. var µm = µMatrix;
  334. var requestURL = details.url;
  335. // console.debug('onBeforeSendHeadersHandler()> "%s": %o', details.url, details);
  336. // Do not block myself from updating assets
  337. // https://github.com/gorhill/httpswitchboard/issues/202
  338. var requestType = requestTypeNormalizer[details.type];
  339. if ( requestType === 'xhr' && requestURL.slice(0, µm.projectServerRoot.length) === µm.projectServerRoot ) {
  340. return;
  341. }
  342. // https://github.com/gorhill/httpswitchboard/issues/342
  343. // Is this hyperlink auditing?
  344. // If yes, create a synthetic URL for reporting hyperlink auditing
  345. // record requests. This way the user is better informed of what went
  346. // on.
  347. var linkAuditor = hyperlinkAuditorFromHeaders(details.requestHeaders);
  348. if ( linkAuditor ) {
  349. details.µmRequestURL = requestURL + '{Ping-To:' + linkAuditor + '}';
  350. }
  351. // If we are dealing with a behind-the-scene request, make a last attempt
  352. // to bind the request to a specific tab by using the referrer or the
  353. // `Ping-From` header if any of these exists.
  354. var r;
  355. if ( details.tabId < 0 ) {
  356. details.tabId = tabIdFromHeaders(µm, details.requestHeaders) || -1;
  357. // Do not process `main_frame`/`sub_frame` requests, these were handled
  358. // unconditionally at onBeforeRequest() time (because of potential
  359. // need to redirect).
  360. if ( requestType !== 'doc' && requestType !== 'frame' ) {
  361. r = processRequest(µm, details);
  362. }
  363. }
  364. // If the request was not cancelled above, check whether hyperlink auditing
  365. // is globally forbidden.
  366. if ( !r ) {
  367. if ( linkAuditor && µm.userSettings.processHyperlinkAuditing ) {
  368. r = { 'cancel': true };
  369. }
  370. }
  371. // Block request?
  372. if ( r ) {
  373. // Count number of hyperlink auditing foiled, even attempts blocked
  374. // through the matrix.
  375. if ( linkAuditor ) {
  376. µm.hyperlinkAuditingFoiledCounter += 1;
  377. }
  378. return r;
  379. }
  380. // If we reach this point, request is not blocked, so what is left to do
  381. // is to sanitize headers.
  382. // Re-classify orphan HTTP requests as behind-the-scene requests. There is
  383. // not much else which can be done, because there are URLs
  384. // which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
  385. // as this would lead to complications with no obvious solution, like how
  386. // to scope on unknown scheme? Etc.
  387. // https://github.com/gorhill/httpswitchboard/issues/191
  388. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  389. var tabId = details.tabId;
  390. var pageStats = µm.pageStatsFromTabId(tabId);
  391. if ( !pageStats ) {
  392. tabId = µm.behindTheSceneTabId;
  393. pageStats = µm.pageStatsFromTabId(tabId);
  394. }
  395. var pageURL = µm.pageUrlFromPageStats(pageStats);
  396. var reqHostname = µm.hostnameFromURL(requestURL);
  397. var changed = false;
  398. if ( µm.mustBlock(pageStats.pageHostname, reqHostname, 'cookie') ) {
  399. changed = foilCookieHeaders(µm, details) || changed;
  400. }
  401. // TODO: use cookie cell to determine whether the referrer info must be
  402. // foiled.
  403. if ( µm.userSettings.processReferer && µm.mustBlock(pageStats.pageHostname, reqHostname, '*') ) {
  404. changed = foilRefererHeaders(µm, reqHostname, details) || changed;
  405. }
  406. if ( µm.userSettings.spoofUserAgent ) {
  407. changed = foilUserAgent(µm, details) || changed;
  408. // https://github.com/gorhill/httpswitchboard/issues/252
  409. // To avoid potential mismatch between the user agent from HTTP headers
  410. // and the user agent from subrequests and the window.navigator object,
  411. // I could always store here the effective user agent, but I am really
  412. // not convinced it is worth the added overhead given the low
  413. // probability and the benign consequence if it ever happen. Can always
  414. // be revised if ever I become aware a mismatch is a terrible thing
  415. }
  416. if ( changed ) {
  417. // console.debug('onBeforeSendHeadersHandler()> CHANGED "%s": %o', requestURL, details);
  418. return { requestHeaders: details.requestHeaders };
  419. }
  420. };
  421. /******************************************************************************/
  422. // http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#hyperlink-auditing
  423. //
  424. // Target URL = the href of the link
  425. // Doc URL = URL of the document containing the target URL
  426. // Ping URLs = servers which will be told that user clicked target URL
  427. //
  428. // `Content-Type` = `text/ping` (always present)
  429. // `Ping-To` = target URL (always present)
  430. // `Ping-From` = doc URL
  431. // `Referer` = doc URL
  432. // request URL = URL which will receive the information
  433. //
  434. // With hyperlink-auditing, removing header(s) is pointless, the whole
  435. // request must be cancelled.
  436. var hyperlinkAuditorFromHeaders = function(headers) {
  437. var i = headers.length;
  438. while ( i-- ) {
  439. if ( headers[i].name.toLowerCase() === 'ping-to' ) {
  440. return headers[i].value;
  441. }
  442. }
  443. return;
  444. };
  445. /******************************************************************************/
  446. var tabIdFromHeaders = function(µm, headers) {
  447. var header;
  448. var i = headers.length;
  449. while ( i-- ) {
  450. header = headers[i];
  451. if ( header.name.toLowerCase() === 'referer' ) {
  452. return µm.tabIdFromPageUrl(header.value);
  453. }
  454. if ( header.name.toLowerCase() === 'ping-from' ) {
  455. return µm.tabIdFromPageUrl(header.value);
  456. }
  457. }
  458. return -1;
  459. };
  460. /******************************************************************************/
  461. var foilCookieHeaders = function(µm, details) {
  462. var changed = false;
  463. var headers = details.requestHeaders;
  464. var header;
  465. var i = headers.length;
  466. while ( i-- ) {
  467. header = headers[i];
  468. if ( header.name.toLowerCase() !== 'cookie' ) {
  469. continue;
  470. }
  471. // console.debug('foilCookieHeaders()> foiled browser attempt to send cookie(s) to "%s"', details.url);
  472. headers.splice(i, 1);
  473. µm.cookieHeaderFoiledCounter++;
  474. changed = true;
  475. }
  476. return changed;
  477. };
  478. /******************************************************************************/
  479. var foilRefererHeaders = function(µm, toHostname, details) {
  480. var headers = details.requestHeaders;
  481. var header;
  482. var fromDomain, toDomain;
  483. var i = headers.length;
  484. while ( i-- ) {
  485. header = headers[i];
  486. if ( header.name.toLowerCase() !== 'referer' ) {
  487. continue;
  488. }
  489. fromDomain = µm.URI.domainFromURI(header.value);
  490. if ( !toDomain ) {
  491. toDomain = µm.URI.domainFromHostname(toHostname);
  492. }
  493. if ( toDomain === fromDomain ) {
  494. continue;
  495. }
  496. // console.debug('foilRefererHeaders()> foiled referer "%s" for "%s"', fromDomain, toDomain);
  497. // https://github.com/gorhill/httpswitchboard/issues/222#issuecomment-44828402
  498. // Splicing instead of blanking: not sure how much it helps
  499. headers.splice(i, 1);
  500. µm.refererHeaderFoiledCounter++;
  501. return true;
  502. }
  503. return false;
  504. };
  505. /******************************************************************************/
  506. var foilUserAgent = function(µm, details) {
  507. var headers = details.requestHeaders;
  508. var header;
  509. var i = 0;
  510. while ( header = headers[i] ) {
  511. if ( header.name.toLowerCase() === 'user-agent' ) {
  512. header.value = µm.userAgentReplaceStr;
  513. return true; // Assuming only one `user-agent` entry
  514. }
  515. i += 1;
  516. }
  517. return false;
  518. };
  519. /******************************************************************************/
  520. // To prevent inline javascript from being executed.
  521. // Prevent inline scripting using `Content-Security-Policy`:
  522. // https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html
  523. // This fixes:
  524. // https://github.com/gorhill/httpswitchboard/issues/35
  525. var onHeadersReceived = function(details) {
  526. // console.debug('onHeadersReceived()> "%s": %o', details.url, details);
  527. // Ignore schemes other than 'http...'
  528. if ( details.url.indexOf('http') !== 0 ) {
  529. return;
  530. }
  531. var requestType = requestTypeNormalizer[details.type];
  532. if ( requestType === 'frame' ) {
  533. return onSubDocHeadersReceived(details);
  534. }
  535. if ( requestType === 'doc' ) {
  536. return onMainDocHeadersReceived(details);
  537. }
  538. };
  539. /******************************************************************************/
  540. var onMainDocHeadersReceived = function(details) {
  541. // console.debug('onMainDocHeadersReceived()> "%s": %o', details.url, details);
  542. var µm = µMatrix;
  543. // Do not ignore traffic outside tabs.
  544. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  545. var tabId = details.tabId;
  546. if ( tabId < 0 ) {
  547. tabId = µm.behindTheSceneTabId;
  548. }
  549. var µmuri = µm.URI.set(details.url);
  550. var requestURL = µmuri.normalizedURI();
  551. var requestScheme = µmuri.scheme;
  552. var requestHostname = µmuri.hostname;
  553. // rhill 2013-12-07:
  554. // Apparently in Opera, onBeforeRequest() is triggered while the
  555. // URL is not yet bound to a tab (-1), which caused the code here
  556. // to not be able to lookup the pageStats. So let the code here bind
  557. // the page to a tab if not done yet.
  558. // https://github.com/gorhill/httpswitchboard/issues/75
  559. µm.bindTabToPageStats(tabId, requestURL);
  560. // Re-classify orphan HTTP requests as behind-the-scene requests. There is
  561. // not much else which can be done, because there are URLs
  562. // which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
  563. // as this would lead to complications with no obvious solution, like how
  564. // to scope on unknown scheme? Etc.
  565. // https://github.com/gorhill/httpswitchboard/issues/191
  566. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  567. var pageStats = µm.pageStatsFromTabId(tabId);
  568. if ( !pageStats ) {
  569. tabId = µm.behindTheSceneTabId;
  570. pageStats = µm.pageStatsFromTabId(tabId);
  571. }
  572. var headers = details.responseHeaders;
  573. // Simplify code paths by splitting func in two different handlers, one
  574. // for main docs, one for sub docs.
  575. // rhill 2014-01-15: Report redirects.
  576. // https://github.com/gorhill/httpswitchboard/issues/112
  577. // rhill 2014-02-10: Handle all redirects.
  578. // https://github.com/gorhill/httpswitchboard/issues/188
  579. if ( /\s+30[12378]\s+/.test(details.statusLine) ) {
  580. var i = headerIndexFromName('location', headers);
  581. if ( i >= 0 ) {
  582. // rhill 2014-01-20: Be ready to handle relative URLs.
  583. // https://github.com/gorhill/httpswitchboard/issues/162
  584. var locationURL = µmuri.set(headers[i].value.trim()).normalizedURI();
  585. if ( µmuri.authority === '' ) {
  586. locationURL = requestScheme + '://' + requestHostname + µmuri.path;
  587. }
  588. µm.redirectRequests[locationURL] = requestURL;
  589. }
  590. // console.debug('onMainDocHeadersReceived()> redirect "%s" to "%s"', requestURL, headers[i].value);
  591. }
  592. // rhill 2014-01-15: Report redirects if any.
  593. // https://github.com/gorhill/httpswitchboard/issues/112
  594. if ( details.statusLine.indexOf(' 200') > 0 ) {
  595. var mainFrameStack = [requestURL];
  596. var destinationURL = requestURL;
  597. var sourceURL;
  598. while ( sourceURL = µm.redirectRequests[destinationURL] ) {
  599. mainFrameStack.push(sourceURL);
  600. delete µm.redirectRequests[destinationURL];
  601. destinationURL = sourceURL;
  602. }
  603. while ( destinationURL = mainFrameStack.pop() ) {
  604. pageStats.recordRequest('doc', destinationURL, false);
  605. }
  606. }
  607. // Evaluate
  608. if ( µm.mustAllow(pageStats.pageHostname, requestHostname, 'script') ) {
  609. // https://github.com/gorhill/httpswitchboard/issues/181
  610. pageStats.pageScriptBlocked = false;
  611. return;
  612. }
  613. // https://github.com/gorhill/httpswitchboard/issues/181
  614. pageStats.pageScriptBlocked = true;
  615. // If javascript not allowed, say so through a `Content-Security-Policy`
  616. // directive.
  617. // console.debug('onMainDocHeadersReceived()> PAGE CSP "%s": %o', details.url, details);
  618. headers.push({
  619. 'name': 'Content-Security-Policy',
  620. 'value': "script-src 'none'"
  621. });
  622. return { responseHeaders: headers };
  623. };
  624. /******************************************************************************/
  625. var onSubDocHeadersReceived = function(details) {
  626. // console.debug('onSubDocHeadersReceived()> "%s": %o', details.url, details);
  627. var µm = µMatrix;
  628. // Do not ignore traffic outside tabs.
  629. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  630. var tabId = details.tabId;
  631. if ( tabId < 0 ) {
  632. tabId = µm.behindTheSceneTabId;
  633. }
  634. // Re-classify orphan HTTP requests as behind-the-scene requests. There is
  635. // not much else which can be done, because there are URLs
  636. // which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
  637. // as this would lead to complications with no obvious solution, like how
  638. // to scope on unknown scheme? Etc.
  639. // https://github.com/gorhill/httpswitchboard/issues/191
  640. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
  641. var pageStats = µm.pageStatsFromTabId(tabId);
  642. if ( !pageStats ) {
  643. tabId = µm.behindTheSceneTabId;
  644. pageStats = µm.pageStatsFromTabId(tabId);
  645. }
  646. // Evaluate
  647. if ( µm.mustAllow(pageStats.pageHostname, µm.hostnameFromURL(details.url), 'script') ) {
  648. return;
  649. }
  650. // If javascript not allowed, say so through a `Content-Security-Policy`
  651. // directive.
  652. // For inline javascript within iframes, we need to sandbox.
  653. // https://github.com/gorhill/httpswitchboard/issues/73
  654. // Now because sandbox cancels all permissions, this means
  655. // not just javascript is disabled. To avoid negative side
  656. // effects, I allow some other permissions, but...
  657. // https://github.com/gorhill/uMatrix/issues/27
  658. // Need to add `allow-popups` to prevent completely breaking links on
  659. // some sites old style sites.
  660. // TODO: Reuse CSP `sandbox` directive if it's already in the
  661. // headers (strip out `allow-scripts` if present),
  662. // and find out if the `sandbox` in the header interfere with a
  663. // `sandbox` attribute which might be present on the iframe.
  664. // console.debug('onSubDocHeadersReceived()> FRAME CSP "%s": %o, scope="%s"', details.url, details, pageURL);
  665. details.responseHeaders.push({
  666. 'name': 'Content-Security-Policy',
  667. 'value': 'sandbox allow-forms allow-same-origin allow-popups allow-top-navigation'
  668. });
  669. return { responseHeaders: details.responseHeaders };
  670. };
  671. /******************************************************************************/
  672. // As per Chrome API doc, webRequest.onErrorOccurred event is the last
  673. // one called in the sequence of webRequest events.
  674. // http://developer.chrome.com/extensions/webRequest.html
  675. var onErrorOccurredHandler = function(details) {
  676. // console.debug('onErrorOccurred()> "%s": %o', details.url, details);
  677. var requestType = requestTypeNormalizer[details.type];
  678. // Ignore all that is not a main document
  679. if ( requestType !== 'doc'|| details.parentFrameId >= 0 ) {
  680. return;
  681. }
  682. var µm = µMatrix;
  683. var pageStats = µm.pageStatsFromPageUrl(details.url);
  684. if ( !pageStats ) {
  685. return;
  686. }
  687. // rhill 2014-01-28: Unwind the stack of redirects if any. Chromium will
  688. // emit an error when a web page redirects apparently endlessly, so
  689. // we need to unravel and report all these redirects upon error.
  690. // https://github.com/gorhill/httpswitchboard/issues/171
  691. var requestURL = µm.URI.set(details.url).normalizedURI();
  692. var mainFrameStack = [requestURL];
  693. var destinationURL = requestURL;
  694. var sourceURL;
  695. while ( sourceURL = µm.redirectRequests[destinationURL] ) {
  696. mainFrameStack.push(sourceURL);
  697. delete µm.redirectRequests[destinationURL];
  698. destinationURL = sourceURL;
  699. }
  700. while ( destinationURL = mainFrameStack.pop() ) {
  701. pageStats.recordRequest('doc', destinationURL, false);
  702. }
  703. };
  704. /******************************************************************************/
  705. // Caller must ensure headerName is normalized to lower case.
  706. var headerIndexFromName = function(headerName, headers) {
  707. var i = headers.length;
  708. while ( i-- ) {
  709. if ( headers[i].name.toLowerCase() === headerName ) {
  710. return i;
  711. }
  712. }
  713. return -1;
  714. };
  715. /******************************************************************************/
  716. var requestTypeNormalizer = {
  717. 'main_frame' : 'doc',
  718. 'sub_frame' : 'frame',
  719. 'stylesheet' : 'css',
  720. 'script' : 'script',
  721. 'image' : 'image',
  722. 'object' : 'plugin',
  723. 'xmlhttprequest': 'xhr',
  724. 'other' : 'other'
  725. };
  726. /******************************************************************************/
  727. var start = function() {
  728. chrome.webRequest.onBeforeRequest.addListener(
  729. //function(details) {
  730. // quickProfiler.start('onBeforeRequest');
  731. // var r = onBeforeRequestHandler(details);
  732. // quickProfiler.stop();
  733. // return r;
  734. //},
  735. onBeforeRequestHandler,
  736. {
  737. "urls": [
  738. "http://*/*",
  739. "https://*/*",
  740. "chrome-extension://*/*"
  741. ],
  742. "types": [
  743. "main_frame",
  744. "sub_frame",
  745. 'stylesheet',
  746. "script",
  747. "image",
  748. "object",
  749. "xmlhttprequest",
  750. "other"
  751. ]
  752. },
  753. [ "blocking" ]
  754. );
  755. //console.log('µMatrix > Beginning to intercept net requests at %s', (new Date()).toISOString());
  756. chrome.webRequest.onBeforeSendHeaders.addListener(
  757. onBeforeSendHeadersHandler,
  758. {
  759. 'urls': [
  760. "http://*/*",
  761. "https://*/*"
  762. ]
  763. },
  764. ['blocking', 'requestHeaders']
  765. );
  766. chrome.webRequest.onHeadersReceived.addListener(
  767. onHeadersReceived,
  768. {
  769. 'urls': [
  770. "http://*/*",
  771. "https://*/*"
  772. ]
  773. },
  774. ['blocking', 'responseHeaders']
  775. );
  776. chrome.webRequest.onErrorOccurred.addListener(
  777. onErrorOccurredHandler,
  778. {
  779. 'urls': [
  780. "http://*/*",
  781. "https://*/*"
  782. ]
  783. }
  784. );
  785. };
  786. /******************************************************************************/
  787. return {
  788. blockedRootFramePrefix: 'data:text/html;base64,' + btoa(rootFrameReplacement).slice(0, 80),
  789. start: start
  790. };
  791. /******************************************************************************/
  792. })();
  793. /******************************************************************************/