Imgur Proxy

Intercepts Imgur image requests on any site and re-routes them through proxies to bypass geoblocks.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==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();
    }
})();