forked from motion-canvas/motion-canvas
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(vite-plugin): add CORS Proxy (motion-canvas#338)
fix(vite-plugin): fix remote sources breaking Rendering by implementing proxy(motion-canvas#338) feat(core): add viaProxy Utility to rewrite requests to proxy (motion-canvas#338) Apply proposed changes Co-authored-by: Jacob <64662184+aarthificial@users.noreply.github.com>
- Loading branch information
1 parent
16752a3
commit 4ca5730
Showing
8 changed files
with
614 additions
and
9 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import {beforeEach, describe, expect, it, vi} from 'vitest'; | ||
import {isProxyEnabled, viaProxy} from './proxyUtils'; | ||
const windowMock = { | ||
location: { | ||
toString: () => 'https://mockhostname:1234', | ||
}, | ||
}; | ||
|
||
function proxy(str: string) { | ||
return '/cors-proxy/' + encodeURIComponent(str); | ||
} | ||
|
||
describe('proxyUtils', () => { | ||
describe('isProxyEnabled()', () => { | ||
it('should default to false without import.meta set', () => { | ||
import.meta.env['VITE_MC_PROXY_ENABLED'] = undefined; | ||
expect(isProxyEnabled()).toStrictEqual(false); | ||
}), | ||
it("should return true if 'true' is set for VITE_MC_PROXY_ENABLED", () => { | ||
import.meta.env['VITE_MC_PROXY_ENABLED'] = 'true'; | ||
expect(isProxyEnabled()).toStrictEqual(true); | ||
}), | ||
it("should return false if 'false' is set for VITE_MC_PROXY_ENABLED", () => { | ||
import.meta.env['VITE_MC_PROXY_ENABLED'] = 'false'; | ||
expect(isProxyEnabled()).toStrictEqual(false); | ||
}); | ||
}); | ||
|
||
describe('viaProxy()', () => { | ||
it('should not Proxy if VITE_MC_PROXY_ENABLED is not set', () => { | ||
import.meta.env['VITE_MC_PROXY_ENABLED'] = undefined; | ||
const input = 'https://via.placeholder.com/300.png/09f/fff'; | ||
expect(viaProxy(input)).toStrictEqual(input); | ||
}), | ||
it("should not Proxy if VITE_MC_PROXY_ENABLED is set to 'false'", () => { | ||
import.meta.env['VITE_MC_PROXY_ENABLED'] = 'false'; | ||
const input = 'https://via.placeholder.com/300.png/09f/fff'; | ||
expect(viaProxy(input)).toStrictEqual(input); | ||
}), | ||
describe('VITE_MC_PROXY_ENABLED is enabled', () => { | ||
beforeEach(() => { | ||
import.meta.env.VITE_MC_PROXY_ENABLED = 'true'; | ||
import.meta.env.VITE_MC_PROXY_ALLOW_LIST = undefined; | ||
vi.stubGlobal('window', windowMock); | ||
}); | ||
const input = 'https://via.placeholder.com/300.png/09f/fff'; | ||
const proxiedInput = proxy(input); | ||
|
||
it('should proxy if VITE_MC_PROXY_ALLOW_LIST is not set', () => { | ||
import.meta.env['VITE_MC_PROXY_ALLOW_LIST'] = undefined; | ||
expect(viaProxy(input)).toStrictEqual(proxiedInput); | ||
}); | ||
it('should not proxy if the host is not in the allow list', () => { | ||
import.meta.env['VITE_MC_PROXY_ALLOW_LIST'] = JSON.stringify([ | ||
'google.com', | ||
]); | ||
const x = viaProxy(input); | ||
expect(x).toStrictEqual(input); | ||
}); | ||
it('should proxy if VITE_MC_PROXY_ALLOW_LIST is an empty list', () => { | ||
import.meta.env['VITE_MC_PROXY_ALLOW_LIST'] = JSON.stringify([]); | ||
expect(viaProxy(input)).toStrictEqual(proxiedInput); | ||
}); | ||
it('should proxy if the host is on the allow list', () => { | ||
(import.meta.env['VITE_MC_PROXY_ALLOW_LIST'] = JSON.stringify([ | ||
'via.placeholder.com', | ||
])), | ||
expect(viaProxy(input)).toStrictEqual(proxiedInput); | ||
}); | ||
it('should not proxy if the host is the same as the server', () => { | ||
import.meta.env['VITE_MC_PROXY_ALLOW_LIST'] = JSON.stringify([]); | ||
const input = windowMock.location.toString() + '/some/example.png'; | ||
expect(viaProxy(input)).toStrictEqual(input); | ||
}); | ||
}), | ||
describe('Protocols', () => { | ||
beforeEach(() => { | ||
import.meta.env.VITE_MC_PROXY_ENABLED = 'true'; | ||
import.meta.env.VITE_MC_PROXY_ALLOW_LIST = JSON.stringify([]); | ||
vi.stubGlobal('window', windowMock); | ||
}); | ||
|
||
it('should proxy if the request protocol is http and https', () => { | ||
const suffix = '://via.placeholder.com/300.png/09f/fff'; | ||
const httpReq = 'http' + suffix; | ||
const httpsReq = 'https' + suffix; | ||
|
||
expect(viaProxy(httpReq)).toStrictEqual(proxy(httpReq)); | ||
expect(viaProxy(httpsReq)).toStrictEqual(proxy(httpsReq)); | ||
}); | ||
it('should not proxy other protocols like data:', () => { | ||
const dataBlob = | ||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2PYOO36fwAHOAMeASY22QAAAABJRU5ErkJggg=='; | ||
expect(viaProxy(dataBlob)).toStrictEqual(dataBlob); | ||
}); | ||
}), | ||
it('should not rewrite an already proxied request', () => { | ||
const raw = 'https://via.placeholder.com/300.png/09f/fff'; | ||
|
||
expect(viaProxy(raw)).not.toStrictEqual(raw); | ||
expect(viaProxy(viaProxy(raw))).toStrictEqual(viaProxy(raw)); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
/** | ||
* Utility to redirect remote sources via Proxy | ||
* | ||
* This utility is used to rewrite a request to be routed through | ||
* the Proxy instead. | ||
*/ | ||
|
||
/** | ||
* Helper-Function to route remote requests through a local proxy. | ||
* | ||
* @example This rewrites a remote url like `https://via.placeholder.com/300.png/09f/fff` into a URI-Component-Encoded string like `/cors-proxy/https%3A%2F%2Fvia.placeholder.com%2F300.png%2F09f%2Ffff` | ||
*/ | ||
export function viaProxy(url: string) { | ||
if (!isProxyEnabled()) { | ||
// Proxy is disabled, so we just pass as-is. | ||
return url; | ||
} | ||
|
||
if (url.startsWith('/cors-proxy/')) { | ||
// Already proxied, return as-is | ||
return url; | ||
} | ||
|
||
// window.location.hostname is being passed here to ensure that | ||
// this does not throw an Error for same-origin requests | ||
// e.g. /some/image -> localhost:9000/some/image | ||
const selfUrl = new URL(window.location.toString()); | ||
// inside a try-catch in case the URL cannot be understood | ||
try { | ||
const expandedUrl = new URL(url, selfUrl); | ||
if (!expandedUrl.protocol.startsWith('http')) { | ||
// this is probably some embedded image (e.g. image/png;base64). | ||
// don't touch and pass as is | ||
return url; | ||
} | ||
if (selfUrl.host === expandedUrl.host) { | ||
// This is a request to a "local" resource. | ||
// No need to rewrite | ||
return url; | ||
} | ||
|
||
// Check if the host matches the Allow List. | ||
// if not, no rewrite takes place. | ||
// will fail in the Editor if the | ||
// remote host does not accept anonymous | ||
if (!isInsideAllowList(expandedUrl.host)) { | ||
return url; | ||
} | ||
} catch (_) { | ||
// in case of error just silently pass as-is | ||
return url; | ||
} | ||
|
||
// Everything else is a "remote" resource and requires a rewrite. | ||
return `/cors-proxy/${encodeURIComponent(url)}`; | ||
} | ||
|
||
function isInsideAllowList(host: string) { | ||
const allowList = getAllowList(); | ||
if (allowList.length === 0) { | ||
// Allow List defaults to allow all if empty | ||
return true; | ||
} | ||
for (const entry of allowList) { | ||
if (entry.toLowerCase().trim() === host) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* This tries to access import.meta.env.VITE_MC_PROXY_ENABLED | ||
* which will either return 'true' of 'false' (as strings) if | ||
* present, or be undefined if not run from a vite context or | ||
* run without the MC Plugin | ||
* | ||
* If no value was set this will be false as the proxy only | ||
* exists inside the Vite Server. | ||
*/ | ||
export function isProxyEnabled() { | ||
try { | ||
const importedValue = import.meta.env['VITE_MC_PROXY_ENABLED'] ?? 'false'; | ||
return importedValue === 'true'; | ||
} catch (_) { | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Cached value so getAllowList does not | ||
* try to parse the Env var on every call, | ||
* spamming the console in the process | ||
*/ | ||
let getAllowListCache: string[] | undefined = undefined; | ||
/** | ||
* Returns the list of allowed hosts | ||
* from the Plugin Config | ||
*/ | ||
function getAllowList() { | ||
// Condition should get optimized away for Prod | ||
if (import.meta.env.VITEST !== 'true') { | ||
if (getAllowListCache) { | ||
return [...getAllowListCache]; | ||
} | ||
} | ||
|
||
const result = (function () { | ||
if (!isProxyEnabled()) { | ||
return []; | ||
} | ||
|
||
try { | ||
// This value is encoded as a JSON String. | ||
const valueJson = import.meta.env['VITE_MC_PROXY_ALLOW_LIST'] ?? '[]'; | ||
const parsedJson = JSON.parse(valueJson); | ||
// Do an additional check that only strings are present, | ||
// create a warning and ignore the value | ||
if (!Array.isArray(parsedJson)) { | ||
console.error( | ||
'Parsed Allow List expected to be an Array, but is ' + | ||
typeof parsedJson, | ||
); | ||
} | ||
const validatedEntries = []; | ||
for (const entry of parsedJson) { | ||
if (typeof entry !== 'string') { | ||
console.warn( | ||
'Unexpected Value in Allow List: ' + | ||
entry + | ||
'. Expected a String. Skipping.', | ||
); | ||
continue; | ||
} | ||
validatedEntries.push(entry); | ||
} | ||
|
||
return validatedEntries; | ||
} catch (err) { | ||
console.error('Failed to parse Allow List from Proxy'); | ||
return []; | ||
} | ||
})(); | ||
getAllowListCache = result; | ||
return [...getAllowListCache]; | ||
} |
Oops, something went wrong.