├── .gitignore ├── README.md ├── icon16.png ├── icon48.png ├── icon128.png ├── icon48-warning.png ├── manifest.json ├── LICENSE ├── background.js ├── popup.js └── popup.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # url-throttler 2 | Chrome extension to throttle specific URLs 3 | -------------------------------------------------------------------------------- /icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/severest/url-throttler/HEAD/icon16.png -------------------------------------------------------------------------------- /icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/severest/url-throttler/HEAD/icon48.png -------------------------------------------------------------------------------- /icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/severest/url-throttler/HEAD/icon128.png -------------------------------------------------------------------------------- /icon48-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/severest/url-throttler/HEAD/icon48-warning.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "URL Throttler", 4 | "description": "An extension that lets you delay the response from specific URLs", 5 | "version": "2.0.0", 6 | "icons": { 7 | "16": "icon16.png", 8 | "48": "icon48.png", 9 | "128": "icon128.png" 10 | }, 11 | "permissions": [ 12 | "webRequest", 13 | "webRequestBlocking", 14 | "storage", 15 | "https://*/*", 16 | "http://*/*" 17 | ], 18 | "browser_action": { 19 | "default_title": "URL Throttler", 20 | "default_popup": "popup.html" 21 | }, 22 | "background": { 23 | "scripts": ["background.js"] 24 | }, 25 | "content_security_policy": "script-src 'self' 'unsafe-eval' https://cdnjs.cloudflare.com; object-src 'self'" 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sean Everest 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | const delay = (ms) => { 2 | const startPoint = new Date().getTime() 3 | while (new Date().getTime() - startPoint <= ms) {/* wait */} 4 | } 5 | 6 | function matchRuleShort(str, rule) { 7 | return new RegExp(str.replace(/\*/g, "[^ ]*")).test(rule); 8 | } 9 | 10 | let handler; 11 | chrome.storage.onChanged.addListener(function(changes, namespace) { 12 | if (changes.hasOwnProperty('requestThrottler')) { 13 | const throttlerConfig = changes.requestThrottler.newValue; 14 | if (handler) { 15 | chrome.webRequest.onBeforeRequest.removeListener(handler); 16 | handler = null; 17 | } 18 | 19 | const urls = throttlerConfig.urls.filter((u) => !u.error && u.url !== '' && u.checked).map((u) => u.url.trim()); 20 | 21 | if (throttlerConfig.enabled && urls.length > 0) { 22 | chrome.browserAction.setIcon({path: 'icon48-warning.png'}); 23 | handler = (info) => { 24 | //ex: {checked: true, delay: '10000', error: false, url: 'https://stackoverflow.com/tags'} 25 | const thisUrlConfig = throttlerConfig.urls.filter((item) => item.checked && matchRuleShort(item.url, info.url))[0]; 26 | 27 | if(thisUrlConfig && thisUrlConfig.checked) { 28 | const thisUrlDelay = thisUrlConfig.delay || throttlerConfig.defaultDelay; 29 | console.log(`URL Throttler: Intercepted ${info.url}, going to wait ${thisUrlDelay} ms...`); 30 | delay(thisUrlDelay); 31 | console.log('URL Throttler: Done'); 32 | } 33 | return; 34 | }; 35 | console.log('Blocking urls', urls); 36 | chrome.webRequest.onBeforeRequest.addListener( 37 | handler, 38 | // filters 39 | { 40 | urls: urls, 41 | }, 42 | // extraInfoSpec 43 | ["blocking"] 44 | ); 45 | } else { 46 | chrome.browserAction.setIcon({path: 'icon48.png'}); 47 | } 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | const defaultURL = { 2 | url: '', 3 | error: false, 4 | }; 5 | const urlRegex = /^(https?|\*):\/\/.*\/.*$/; 6 | var lastChangeTarget; 7 | let fadeOut = (target) => setInterval(function () { 8 | if (target.style.opacity > 0) { 9 | target.style.opacity -= 0.1; 10 | } else { 11 | clearInterval(fadeEffect); 12 | target.innerHTML = ""; 13 | } 14 | }, 250); 15 | 16 | chrome.storage.local.get(['requestThrottler'], (result) => { 17 | const data = Object.assign( 18 | {}, { 19 | enabled: false, 20 | urls: [{ ...defaultURL }], 21 | defaultDelay: 2000, 22 | }, 23 | result.requestThrottler 24 | ); 25 | var app = new Vue({ 26 | el: '#app', 27 | data: data, 28 | methods: { 29 | applyConfig: function() { 30 | let newConfig = prompt('Please input new config (it should appear in JSON format as in example below), or leave as {} to reset.\n\n{"defaultDelay":"5000","enabled":true,"urls":[{"checked":true,"error":false,"url":"*://*/api/foo"},{"checked":true,"delay":"2000","error":false,"url":"https://joecoyle.net/api/bar"}]}\n', "{}"); 31 | if(newConfig) { 32 | try { 33 | chrome.storage.local.set({'requestThrottler': JSON.parse(newConfig)}, function() { 34 | location.reload(); 35 | }); 36 | } catch { 37 | alert("Error applying config. Is your JSON formatting correct?"); 38 | } 39 | } 40 | }, 41 | copyCurrentConfig: function() { 42 | chrome.storage.local.get((config) => { 43 | navigator.clipboard.writeText(JSON.stringify(config["requestThrottler"])); 44 | lastChangeTarget = document.getElementById("messageDisplay"); 45 | lastChangeTarget.innerHTML = "Configuration copied to clipboard"; 46 | lastChangeTarget.style.opacity = 1; 47 | fadeOut(lastChangeTarget); 48 | }); 49 | }, 50 | addUrlInput: function() { 51 | this.urls = this.urls.concat({ ...defaultURL }); 52 | }, 53 | removeUrlInput: function(index) { 54 | this.urls.splice(index, 1); 55 | }, 56 | updateStorage: function(newStorage) { 57 | chrome.storage.local.set(newStorage); 58 | } 59 | }, 60 | updated: _.debounce(function() { 61 | chrome.storage.local.get((currStorage) => { 62 | var newStorage = {requestThrottler: { 63 | defaultDelay: this.defaultDelay, 64 | enabled: this.enabled, 65 | urls: this.urls, 66 | }}; 67 | 68 | if(JSON.stringify(currStorage) != JSON.stringify(newStorage)) { 69 | this.updateStorage(newStorage); 70 | 71 | lastChangeTarget = document.getElementById("messageDisplay"); 72 | lastChangeTarget.innerHTML = "Changes saved @ " + (new Date).toLocaleTimeString(); 73 | lastChangeTarget.style.opacity = 1; 74 | fadeOut(lastChangeTarget); 75 | } 76 | }); 77 | }, 700) 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 69 | 70 | 71 |
72 |
73 |
74 | 75 |
76 | 77 |
78 |
79 |
80 |
81 |
Delay (ms):
82 | 83 |
84 |
85 |
URLs:
86 |
87 | URLs must use the Match pattern syntax. 88 |
89 | Examples: 90 |
91 | http://127.0.0.1/* 92 |
93 | *://*/api/foo 94 |
95 | If no delay is specified below, delay will default to the value specified above. 96 |
97 |
98 | 99 | 100 | 101 | 102 |
103 |
104 |
105 | 106 |
107 |
108 |
109 | 110 | 111 |
112 |
113 | 114 | 115 | 116 |
117 |
118 | 119 | 120 | --------------------------------------------------------------------------------