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.

263 lines
9.6 KiB

  1. /*******************************************************************************
  2. uMatrix - a browser extension to block requests.
  3. Copyright (C) 2017-present 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. // For background page
  17. 'use strict';
  18. /******************************************************************************/
  19. (( ) => {
  20. // https://github.com/uBlockOrigin/uBlock-issues/issues/407
  21. if ( vAPI.webextFlavor.soup.has('firefox') === false ) { return; }
  22. // https://github.com/gorhill/uBlock/issues/2950
  23. // Firefox 56 does not normalize URLs to ASCII, uBO must do this itself.
  24. // https://bugzilla.mozilla.org/show_bug.cgi?id=945240
  25. const evalMustPunycode = ( ) => {
  26. return vAPI.webextFlavor.soup.has('firefox') &&
  27. vAPI.webextFlavor.major < 57;
  28. };
  29. let mustPunycode = evalMustPunycode();
  30. // The real actual webextFlavor value may not be set in stone, so listen
  31. // for possible future changes.
  32. window.addEventListener('webextFlavor', ( ) => {
  33. mustPunycode = evalMustPunycode();
  34. }, { once: true });
  35. const punycode = self.punycode;
  36. const reAsciiHostname = /^https?:\/\/[0-9a-z_.:@-]+[/?#]/;
  37. const parsedURL = new URL('about:blank');
  38. // Related issues:
  39. // - https://github.com/gorhill/uBlock/issues/1327
  40. // - https://github.com/uBlockOrigin/uBlock-issues/issues/128
  41. // - https://bugzilla.mozilla.org/show_bug.cgi?id=1503721
  42. // Extend base class to normalize as per platform.
  43. vAPI.Net = class extends vAPI.Net {
  44. constructor() {
  45. super();
  46. this.pendingRequests = [];
  47. this.cnames = new Map([ [ '', '' ] ]);
  48. this.cnameIgnoreList = null;
  49. this.cnameIgnore1stParty = true;
  50. this.cnameIgnoreExceptions = true;
  51. this.cnameIgnoreRootDocument = true;
  52. this.cnameMaxTTL = 60;
  53. this.cnameReplayFullURL = false;
  54. this.cnameTimer = undefined;
  55. this.canRevealCNAME = browser.dns instanceof Object;
  56. }
  57. setOptions(options) {
  58. super.setOptions(options);
  59. this.cnameIgnoreList = this.regexFromStrList(options.cnameIgnoreList);
  60. this.cnameIgnore1stParty = options.cnameIgnore1stParty !== false;
  61. this.cnameIgnoreExceptions = options.cnameIgnoreExceptions !== false;
  62. this.cnameIgnoreRootDocument = options.cnameIgnoreRootDocument !== false;
  63. this.cnameMaxTTL = options.cnameMaxTTL || 120;
  64. this.cnameReplayFullURL = options.cnameReplayFullURL === true;
  65. this.cnames.clear(); this.cnames.set('', '');
  66. }
  67. normalizeDetails(details) {
  68. if ( mustPunycode && !reAsciiHostname.test(details.url) ) {
  69. parsedURL.href = details.url;
  70. details.url = details.url.replace(
  71. parsedURL.hostname,
  72. punycode.toASCII(parsedURL.hostname)
  73. );
  74. }
  75. const type = details.type;
  76. if ( type === 'imageset' ) {
  77. details.type = 'image';
  78. return;
  79. }
  80. // https://github.com/uBlockOrigin/uBlock-issues/issues/345
  81. // Re-categorize an embedded object as a `sub_frame` if its
  82. // content type is that of a HTML document.
  83. if ( type === 'object' && Array.isArray(details.responseHeaders) ) {
  84. for ( const header of details.responseHeaders ) {
  85. if ( header.name.toLowerCase() === 'content-type' ) {
  86. if ( header.value.startsWith('text/html') ) {
  87. details.type = 'sub_frame';
  88. }
  89. break;
  90. }
  91. }
  92. }
  93. }
  94. denormalizeTypes(types) {
  95. if ( types.length === 0 ) {
  96. return Array.from(this.validTypes);
  97. }
  98. const out = new Set();
  99. for ( const type of types ) {
  100. if ( this.validTypes.has(type) ) {
  101. out.add(type);
  102. }
  103. if ( type === 'image' && this.validTypes.has('imageset') ) {
  104. out.add('imageset');
  105. }
  106. if ( type === 'sub_frame' ) {
  107. out.add('object');
  108. }
  109. }
  110. return Array.from(out);
  111. }
  112. processCanonicalName(hn, cn, details) {
  113. const hnBeg = details.url.indexOf(hn);
  114. if ( hnBeg === -1 ) { return; }
  115. const oldURL = details.url;
  116. let newURL = oldURL.slice(0, hnBeg) + cn;
  117. const hnEnd = hnBeg + hn.length;
  118. if ( this.cnameReplayFullURL ) {
  119. newURL += oldURL.slice(hnEnd);
  120. } else {
  121. const pathBeg = oldURL.indexOf('/', hnEnd);
  122. if ( pathBeg !== -1 ) {
  123. newURL += oldURL.slice(hnEnd, pathBeg + 1);
  124. }
  125. }
  126. details.url = newURL;
  127. details.aliasURL = oldURL;
  128. return super.onBeforeSuspendableRequest(details);
  129. }
  130. recordCanonicalName(hn, record) {
  131. let cname =
  132. typeof record.canonicalName === 'string' &&
  133. record.canonicalName !== hn
  134. ? record.canonicalName
  135. : '';
  136. if (
  137. cname !== '' &&
  138. this.cnameIgnore1stParty &&
  139. vAPI.domainFromHostname(cname) === vAPI.domainFromHostname(hn)
  140. ) {
  141. cname = '';
  142. }
  143. if (
  144. cname !== '' &&
  145. this.cnameIgnoreList !== null &&
  146. this.cnameIgnoreList.test(cname)
  147. ) {
  148. cname = '';
  149. }
  150. this.cnames.set(hn, cname);
  151. if ( this.cnameTimer === undefined ) {
  152. this.cnameTimer = self.setTimeout(
  153. ( ) => {
  154. this.cnameTimer = undefined;
  155. this.cnames.clear(); this.cnames.set('', '');
  156. },
  157. this.cnameMaxTTL * 60000
  158. );
  159. }
  160. return cname;
  161. }
  162. regexFromStrList(list) {
  163. if (
  164. typeof list !== 'string' ||
  165. list.length === 0 ||
  166. list === 'unset' ||
  167. browser.dns instanceof Object === false
  168. ) {
  169. return null;
  170. }
  171. if ( list === '*' ) {
  172. return /^./;
  173. }
  174. return new RegExp(
  175. '(?:^|\.)(?:' +
  176. list.trim()
  177. .split(/\s+/)
  178. .map(a => a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
  179. .join('|') +
  180. ')$'
  181. );
  182. }
  183. onBeforeSuspendableRequest(details) {
  184. const r = super.onBeforeSuspendableRequest(details);
  185. if ( this.canRevealCNAME === false ) { return r; }
  186. if ( r !== undefined ) {
  187. if ( r.cancel === false ) { return; }
  188. if (
  189. r.cancel === true ||
  190. r.redirectUrl !== undefined ||
  191. this.cnameIgnoreExceptions
  192. ) {
  193. return r;
  194. }
  195. }
  196. if (
  197. details.type === 'main_frame' &&
  198. this.cnameIgnoreRootDocument
  199. ) {
  200. return;
  201. }
  202. const hn = vAPI.hostnameFromNetworkURL(details.url);
  203. const cname = this.cnames.get(hn);
  204. if ( cname === '' ) { return; }
  205. if ( cname !== undefined ) {
  206. return this.processCanonicalName(hn, cname, details);
  207. }
  208. return browser.dns.resolve(hn, [ 'canonical_name' ]).then(
  209. rec => {
  210. const cname = this.recordCanonicalName(hn, rec);
  211. if ( cname === '' ) { return; }
  212. return this.processCanonicalName(hn, cname, details);
  213. },
  214. ( ) => {
  215. this.cnames.set(hn, '');
  216. }
  217. );
  218. }
  219. suspendOneRequest(details) {
  220. const pending = {
  221. details: Object.assign({}, details),
  222. resolve: undefined,
  223. promise: undefined
  224. };
  225. pending.promise = new Promise(resolve => {
  226. pending.resolve = resolve;
  227. });
  228. this.pendingRequests.push(pending);
  229. return pending.promise;
  230. }
  231. unsuspendAllRequests() {
  232. const pendingRequests = this.pendingRequests;
  233. this.pendingRequests = [];
  234. for ( const entry of pendingRequests ) {
  235. entry.resolve(this.onBeforeSuspendableRequest(entry.details));
  236. }
  237. }
  238. canSuspend() {
  239. return true;
  240. }
  241. };
  242. })();
  243. /******************************************************************************/