Skip to content

Commit

Permalink
feat(vite-plugin): add CORS Proxy (#357)
Browse files Browse the repository at this point in the history
Closes: #338
  • Loading branch information
WaldemarLehner authored Feb 23, 2023
1 parent fbeb392 commit a3c5822
Show file tree
Hide file tree
Showing 8 changed files with 617 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', () => {
delete import.meta.env['VITE_MC_PROXY_ALLOW_LIST'];
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));
});
});
});
149 changes: 149 additions & 0 deletions packages/core/src/utils/proxyUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* Utility to redirect remote sources via Proxy
*
* This utility is used to rewrite a request to be routed through
* the Proxy instead.
*/

import {useLogger} from './useProject';

/**
* Route the given url 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)}`;
}

/**
* Check the provided host is allowed to be routed
* to the Proxy.
*/
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;
}

/**
* Check if the proxy is enabled via the plugin by checking
* for `import.meta.env.VITE_MC_PROXY_ENABLED`
*
* @remarks The value can either be 'true' of 'false'
* (as strings) if present, or be undefined if not run
* from a vite context or run without the MC Plugin.
*/
export function isProxyEnabled() {
if (import.meta.env) {
return import.meta.env.VITE_MC_PROXY_ENABLED === 'true';
}
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;
/**
* Return 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];
}
}

// Inline function gets immediately invoked
// and the result stored in getAllowListCache.
// The cached value is used on subsequent requests.
const result = (function () {
if (!isProxyEnabled() || !import.meta.env) {
return [];
}

// 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)) {
useLogger().error(
'Parsed Allow List expected to be an Array, but is ' +
typeof parsedJson,
);
}
const validatedEntries = [];
for (const entry of parsedJson) {
if (typeof entry !== 'string') {
useLogger().warn(
'Unexpected Value in Allow List: ' +
entry +
'. Expected a String. Skipping.',
);
continue;
}
validatedEntries.push(entry);
}
return validatedEntries;
})();
getAllowListCache = result;
return [...getAllowListCache];
}
Loading

0 comments on commit a3c5822

Please sign in to comment.