|
3 | 3 | var originalPageLoad = Date.now(); |
4 | 4 | console.log("originalPageLoad: ", originalPageLoad); |
5 | 5 |
|
| 6 | +// Transparently route browser fetches to allowlisted hosts through the |
| 7 | +// server-side proxy at /load-shareurl, but only when the direct path doesn't |
| 8 | +// work. |
| 9 | +// |
| 10 | +// Strategy: the FIRST fetch to an allowlisted host fires direct + proxied in |
| 11 | +// parallel. We decide shouldProxy for the rest of the page-load from direct's |
| 12 | +// response *headers*: |
| 13 | +// - direct returned 2xx with content-type text/plain -> shouldProxy=false: |
| 14 | +// serve direct's response, abort the in-flight proxy fetch. |
| 15 | +// - direct failed, hung past timeout, or returned anything else |
| 16 | +// -> shouldProxy=true: |
| 17 | +// serve proxy's response. |
| 18 | +// A key idea is that network-blocky things sometimes return 200 with a |
| 19 | +// message page about blocking (or an error, but that counts as a fail). We |
| 20 | +// don't want to accidentally think that's a success. |
| 21 | +// shouldProxy state is in-memory and per-host — never persisted, since |
| 22 | +// reachability changes between networks and a stale value would silently |
| 23 | +// break loads. |
| 24 | +// |
| 25 | +// Installed on the global fetch as early as possible so it catches every fetch |
| 26 | +// caller; some of them are in the pyret-lang runtime and would be otherwise |
| 27 | +// difficult to configure. |
| 28 | +const SHAREURL_PROXY_HOSTS = new Set(['raw.githubusercontent.com']); |
| 29 | +const SHAREURL_DIRECT_TIMEOUT_MS = 5000; |
| 30 | +const _origFetch = window.fetch.bind(window); |
| 31 | + |
| 32 | +const _shareurlShouldProxy = new Map(); // host -> boolean |
| 33 | +const _shareurlShouldProxyInflight = new Map(); // host -> Promise<boolean> |
| 34 | + |
| 35 | +function _shareurlProxyUrl(fetchInput) { |
| 36 | + return '/load-shareurl?url=' + encodeURIComponent(_shareurlInputToUrl(fetchInput)); |
| 37 | +} |
| 38 | + |
| 39 | +function _shareurlInputToUrl(fetchInput) { |
| 40 | + return (typeof fetchInput === 'string') ? fetchInput |
| 41 | + : (typeof Request !== 'undefined' && fetchInput instanceof Request) ? fetchInput.url |
| 42 | + : String(fetchInput); |
| 43 | +} |
| 44 | + |
| 45 | +function _shareurlVerifyDirect(r) { |
| 46 | + if (!r.ok) return false; |
| 47 | + const ct = (r.headers.get('content-type') || '').toLowerCase(); |
| 48 | + // Source files served from raw.githubusercontent.com come back as |
| 49 | + // text/plain (.arr, .json, .csv, .md all do). Anything else — HTML block |
| 50 | + // pages, captive portals, surprise content types — we don't trust as a |
| 51 | + // real upstream response. |
| 52 | + return ct.startsWith('text/plain'); |
| 53 | +} |
| 54 | + |
| 55 | +function _shareurlFetch(shouldProxy, fetchInput, fetchInit) { |
| 56 | + const maybeProxyInput = shouldProxy ? _shareurlProxyUrl(fetchInput) : fetchInput; |
| 57 | + return _origFetch(maybeProxyInput, fetchInit); |
| 58 | +} |
| 59 | + |
| 60 | +function _shareurlRace(fetchInput, fetchInit) { |
| 61 | + const proxyCtrl = new AbortController(); |
| 62 | + // NOTE(joe): The signal overwrite is technically not the right fetch() |
| 63 | + // polyfill. If the caller elsewhere in the codebase provided a different |
| 64 | + // signal (which in the fetch API is only for aborting as of April '26), that |
| 65 | + // caller aborting through that signal won't cancel the proxy fetch. |
| 66 | + // I'm OK letting that case slip through here in exchange for not having a |
| 67 | + // bunch of extra event handler forwarding |
| 68 | + const proxyP = _origFetch(_shareurlProxyUrl(fetchInput), |
| 69 | + Object.assign({}, fetchInit, { signal: proxyCtrl.signal })); |
| 70 | + const directP = _origFetch(fetchInput, fetchInit).then(r => { |
| 71 | + if (!_shareurlVerifyDirect(r)) throw new Error('direct request failed'); |
| 72 | + return r; |
| 73 | + }); |
| 74 | + |
| 75 | + // shouldProxy: false iff direct verified before the timeout, else true. |
| 76 | + // Whether to proxy is decided solely on whether direct succeeds or not |
| 77 | + const shouldProxyPromise = Promise.race([ |
| 78 | + directP.then(() => false, () => true), |
| 79 | + new Promise(resolve => setTimeout(() => resolve(true), SHAREURL_DIRECT_TIMEOUT_MS)), |
| 80 | + ]); |
| 81 | + |
| 82 | + // Settlement-order check: if direct verifies before proxy returns, abort |
| 83 | + // the in-flight proxy to stop wasting server bandwidth. We must NOT |
| 84 | + // abort once proxy has already returned, since by then the caller is |
| 85 | + // reading proxy's body and aborting would error its stream mid-read. |
| 86 | + const directFinishedSuccessfullyAndFirstP = Promise.race([ |
| 87 | + directP.then(() => true, () => false), |
| 88 | + proxyP.then(() => false, () => false), |
| 89 | + ]); |
| 90 | + directFinishedSuccessfullyAndFirstP.then(directFirst => { |
| 91 | + if (directFirst) proxyCtrl.abort(); |
| 92 | + }); |
| 93 | + |
| 94 | + // Caller's response: whichever of direct-verified or proxy fulfills |
| 95 | + // first. If both fail, surface proxy's error (the more authoritative |
| 96 | + // upstream — direct's may just be 'direct-not-verified'). |
| 97 | + const responsePromise = Promise.any([directP, proxyP]).catch( |
| 98 | + aggErr => Promise.reject(aggErr.errors[1] || aggErr.errors[0]) |
| 99 | + ); |
| 100 | + |
| 101 | + return { responsePromise, shouldProxyPromise }; |
| 102 | +} |
| 103 | + |
| 104 | +window.fetch = function(fetchInput, fetchInit) { |
| 105 | + let host; |
| 106 | + try { host = new URL(_shareurlInputToUrl(fetchInput), window.location.href).hostname; } |
| 107 | + catch (_) { return _origFetch(fetchInput, fetchInit); } |
| 108 | + if (!SHAREURL_PROXY_HOSTS.has(host)) return _origFetch(fetchInput, fetchInit); |
| 109 | + |
| 110 | + const shouldProxy = _shareurlShouldProxy.get(host); |
| 111 | + const inflight = _shareurlShouldProxyInflight.get(host); |
| 112 | + if (shouldProxy !== undefined) { |
| 113 | + return _shareurlFetch(shouldProxy, fetchInput, fetchInit); |
| 114 | + } else if (inflight) { |
| 115 | + // shouldProxy pending: queue this fetch on it and issue a single fresh |
| 116 | + // request once shouldProxy is decided. |
| 117 | + return inflight.then(sp => _shareurlFetch(sp, fetchInput, fetchInit)); |
| 118 | + } else { |
| 119 | + // First fetch to this host this page-load: run the race. |
| 120 | + const { responsePromise, shouldProxyPromise } = _shareurlRace(fetchInput, fetchInit); |
| 121 | + _shareurlShouldProxyInflight.set(host, shouldProxyPromise); |
| 122 | + shouldProxyPromise.then(sp => { |
| 123 | + _shareurlShouldProxy.set(host, sp); |
| 124 | + _shareurlShouldProxyInflight.delete(host); |
| 125 | + }); |
| 126 | + return responsePromise; |
| 127 | + } |
| 128 | +}; |
| 129 | + |
6 | 130 | const isEmbedded = window.parent !== window; |
7 | 131 |
|
8 | 132 | var shareAPI = makeShareAPI(process.env.CURRENT_PYRET_RELEASE); |
|
0 commit comments