Skip to content

Commit

Permalink
feat(vite-plugin): add CORS Proxy (motion-canvas#338)
Browse files Browse the repository at this point in the history
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
WaldemarLehner and aarthificial committed Feb 19, 2023
1 parent 16752a3 commit 4ca5730
Show file tree
Hide file tree
Showing 8 changed files with 614 additions and 9 deletions.
44 changes: 37 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/2d/src/components/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
SignalValue,
SimpleSignal,
} from '@motion-canvas/core/lib/signals';
import {viaProxy} from '@motion-canvas/core/lib/utils';

export interface ImageProps extends RectProps {
src?: SignalValue<string>;
Expand Down Expand Up @@ -54,12 +55,13 @@ export class Image extends Rect {

@computed()
protected image(): HTMLImageElement {
const src = this.src();
const src = viaProxy(this.src());
if (Image.pool[src]) {
return Image.pool[src];
}

const image = document.createElement('img');
image.crossOrigin = 'anonymous';
image.src = src;
if (!image.complete) {
DependencyContext.collectPromise(
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './useThread';
export * from './useTime';
export * from './useContext';
export * from './useDuration';
export * from './proxyUtils';
104 changes: 104 additions & 0 deletions packages/core/src/utils/proxyUtils.test.ts
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));
});
});
});
146 changes: 146 additions & 0 deletions packages/core/src/utils/proxyUtils.ts
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];
}
Loading

0 comments on commit 4ca5730

Please sign in to comment.