Intercepts Imgur image requests on any site and re-routes them through proxies to bypass geoblocks.
// ==UserScript==
// @name Imgur Proxy
// @namespace imgur-proxy
// @version 0.7
// @description Intercepts Imgur image requests on any site and re-routes them through proxies to bypass geoblocks.
// @author Sharknado
// @match *://*/*
// @grant GM_xmlhttpRequest
// @license MIT
// @connect i.imgur.com
// @connect imgur.com
// @connect images.weserv.nl
// @connect wsrv.nl
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// -------------------------------------------------------------------------
// Proxy fetch (userscript world — has GM_xmlhttpRequest)
// -------------------------------------------------------------------------
const IMGUR_RE = /^https?:\/\/(?:i\.)?imgur\.com\//;
// Set to true to see per-request logs in the browser console.
const DEBUG = true;
const log = (...a) => DEBUG && console.debug('[ImgurProxy]', ...a);
// Tried in order; first success wins. Direct is cheapest but may be geoblocked.
const PROXY_STRATEGIES = [
// { name: 'direct', url: (u) => u },
{ name: 'weserv.nl', url: (u) => `https://images.weserv.nl/?url=${encodeURIComponent(u)}` },
{ name: 'wsrv.nl', url: (u) => `https://wsrv.nl/?url=${encodeURIComponent(u)}` },
];
function fetchViaStrategy(strategy, originalUrl) {
const proxyUrl = strategy.url(originalUrl);
log(`[${strategy.name}] → ${proxyUrl}`);
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: proxyUrl,
responseType: 'blob',
timeout: 10000,
onload: (res) => {
log(`[${strategy.name}] ← HTTP ${res.status} size=${res.response?.size ?? '?'}B for ${originalUrl}`);
if (res.status === 200 && res.response && res.response.size > 0) {
resolve(URL.createObjectURL(res.response));
} else {
reject(new Error(`HTTP ${res.status}`));
}
},
onerror: (err) => {
log(`[${strategy.name}] ← network error for ${originalUrl}`, err);
reject(new Error('network error'));
},
ontimeout: () => {
log(`[${strategy.name}] ← timeout for ${originalUrl}`);
reject(new Error('timeout'));
},
});
});
}
// Maps original imgur URL → Promise<blobUrl> so the same image always
// resolves to the same blob URL and is never fetched more than once per session.
const blobCache = new Map();
function fetchAsBlob(originalUrl) {
if (blobCache.has(originalUrl)) {
log(`cache hit for ${originalUrl}`);
return blobCache.get(originalUrl);
}
log(`starting proxy chain for ${originalUrl}`);
const promise = (async () => {
let lastErr;
for (const strategy of PROXY_STRATEGIES) {
try {
const blobUrl = await fetchViaStrategy(strategy, originalUrl);
log(`[${strategy.name}] SUCCESS for ${originalUrl} → ${blobUrl}`);
return blobUrl;
} catch (e) {
lastErr = e;
log(`[${strategy.name}] FAILED for ${originalUrl}:`, e.message);
}
}
log(`all strategies exhausted for ${originalUrl}`);
throw lastErr;
})();
blobCache.set(originalUrl, promise);
promise.catch(() => blobCache.delete(originalUrl)); // allow retry on total failure
return promise;
}
// -------------------------------------------------------------------------
// Page-world prototype override via unsafeWindow
// Directly overrides HTMLImageElement.prototype on the page's window so
// img.src assignments made by page scripts are intercepted here without
// injecting a <script> tag — which would fail on pages enforcing Trusted Types.
// Functions defined in the userscript isolated world retain GM API access
// even when placed on the page prototype chain.
// -------------------------------------------------------------------------
{
const _srcDesc = Object.getOwnPropertyDescriptor(
unsafeWindow.HTMLImageElement.prototype, 'src'
);
Object.defineProperty(unsafeWindow.HTMLImageElement.prototype, 'src', {
get() { return _srcDesc.get.call(this); },
set(value) {
if (IMGUR_RE.test(value)) {
const el = this;
fetchAsBlob(value).then(blobUrl => {
_srcDesc.set.call(el, blobUrl);
}).catch(() => {
log('prototype setter: all strategies failed for', value);
});
// Don't call original setter — prevents the browser ever requesting imgur directly
} else {
_srcDesc.set.call(this, value);
}
},
configurable: true,
});
log('HTMLImageElement.prototype.src overridden via unsafeWindow');
}
// -------------------------------------------------------------------------
// MutationObserver — secondary coverage for setAttribute('src') and srcset
// (setAttribute bypasses the prototype setter, but fires a DOM mutation)
// -------------------------------------------------------------------------
async function patchSrcset(srcset) {
const parts = srcset.split(',').map(s => s.trim()).filter(Boolean);
const patched = await Promise.all(parts.map(async (part) => {
const spaceIdx = part.search(/\s/);
const url = spaceIdx === -1 ? part : part.slice(0, spaceIdx);
const descriptor = spaceIdx === -1 ? '' : part.slice(spaceIdx);
if (!IMGUR_RE.test(url)) return part;
try { return (await fetchAsBlob(url)) + descriptor; } catch { return part; }
}));
return patched.join(', ');
}
async function patchImg(img) {
const src = img.getAttribute('src') || '';
if (IMGUR_RE.test(src) && img.dataset.igProxied !== src) {
img.dataset.igProxied = src;
try { img.src = await fetchAsBlob(src); }
catch (e) { console.warn('[ImgurProxy] srcset all strategies failed', e.message); }
}
const srcset = img.getAttribute('srcset') || '';
if (srcset && img.dataset.igProxiedSrcset !== srcset && IMGUR_RE.test(srcset)) {
img.dataset.igProxiedSrcset = srcset;
try { img.srcset = await patchSrcset(srcset); }
catch (e) { console.warn('[ImgurProxy] srcset failed:', e.message); }
}
}
async function patchSource(source) {
const srcset = source.getAttribute('srcset') || '';
if (srcset && source.dataset.igProxied !== srcset && IMGUR_RE.test(srcset)) {
source.dataset.igProxied = srcset;
try { source.srcset = await patchSrcset(srcset); }
catch (e) { console.warn('[ImgurProxy] source srcset failed:', e.message); }
}
}
function scan(root) {
root.querySelectorAll('img').forEach(patchImg);
root.querySelectorAll('source').forEach(patchSource);
}
function onNode(node) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.tagName === 'IMG') patchImg(node);
else if (node.tagName === 'SOURCE') patchSource(node);
else scan(node);
}
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'childList') m.addedNodes.forEach(onNode);
else if (m.type === 'attributes') {
const el = m.target;
if (el.tagName === 'IMG') patchImg(el);
else if (el.tagName === 'SOURCE') patchSource(el);
}
}
});
function start() {
observer.observe(document.documentElement, {
childList: true, subtree: true,
attributes: true, attributeFilter: ['src', 'srcset'],
});
scan(document);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
})();