-
Notifications
You must be signed in to change notification settings - Fork 172
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
load project assets from root (#5760)
- Loading branch information
Showing
5 changed files
with
245 additions
and
39 deletions.
There are no files selected for viewing
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,34 @@ | ||
import type { LoaderFunctionArgs } from '@remix-run/node' | ||
import { ALLOW } from '../handlers/validators' | ||
import { proxy } from '../util/proxy.server' | ||
import { handle } from '../util/api.server' | ||
import { getProxyAssetPath } from '../util/assets.server' | ||
|
||
export async function loader(args: LoaderFunctionArgs) { | ||
return handle(args, { | ||
GET: { | ||
validator: ALLOW, | ||
handler: handleLoadAssetFromRoot, | ||
}, | ||
}) | ||
} | ||
|
||
/** | ||
* Some editor routes can reference assets as if they are on the root path. | ||
* This function will look at the incoming request and if the requested resource | ||
* has an allowed extension it will try to derive the project ID | ||
* from the request's referer header. | ||
* If a match is found, and the project can be accessed, the request will be proxied to | ||
* /p/<id>/<resource>. | ||
*/ | ||
async function handleLoadAssetFromRoot(req: Request) { | ||
const proxyProjectAssetPath = await getProxyAssetPath(req) | ||
if (proxyProjectAssetPath == null) { | ||
return {} | ||
} | ||
|
||
return proxy(req, { | ||
path: proxyProjectAssetPath, | ||
rawOutput: true, | ||
}) | ||
} |
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,93 @@ | ||
import { newTestRequest } from '../test-util' | ||
import { getProjectIdFromReferer, getProxyAssetPath } from './assets.server' | ||
import * as validators from '../handlers/validators' | ||
|
||
describe('assets', () => { | ||
let validatorsMock: jest.SpyInstance | ||
|
||
afterEach(() => { | ||
validatorsMock.mockRestore() | ||
}) | ||
beforeEach(() => { | ||
validatorsMock = jest.spyOn(validators, 'canAccessProject') | ||
}) | ||
|
||
describe('getProjectIdFromReferer', () => { | ||
it("returns null if there's no referer", async () => { | ||
const got = getProjectIdFromReferer(newTestRequest()) | ||
expect(got).toBe(null) | ||
}) | ||
it('returns null if the origin does not match', async () => { | ||
const got = getProjectIdFromReferer( | ||
newTestRequest({ | ||
headers: { | ||
referer: 'http://foo.bar', | ||
}, | ||
}), | ||
) | ||
expect(got).toBe(null) | ||
}) | ||
it("returns null if the path is not a project's one", async () => { | ||
const got = getProjectIdFromReferer( | ||
newTestRequest({ | ||
headers: { | ||
referer: 'http://localhost:8000/wrong', | ||
}, | ||
}), | ||
) | ||
expect(got).toBe(null) | ||
}) | ||
it('returns null if the tokenization is not correct', async () => { | ||
const got = getProjectIdFromReferer( | ||
newTestRequest({ | ||
headers: { | ||
referer: 'http://localhost:8000/p/', | ||
}, | ||
}), | ||
) | ||
expect(got).toBe(null) | ||
}) | ||
it('returns the project id', async () => { | ||
const got = getProjectIdFromReferer( | ||
newTestRequest({ | ||
headers: { | ||
referer: 'http://localhost:8000/p/foo-1-2-3/bar/baz', | ||
}, | ||
}), | ||
) | ||
expect(got).toBe('foo') | ||
}) | ||
}) | ||
describe('getProxyAssetPath', () => { | ||
it('returns null if the extension is not allowed', async () => { | ||
const got = await getProxyAssetPath(newTestRequest({ path: '/foo.WRONG' })) | ||
expect(got).toBe(null) | ||
}) | ||
it('returns null if the project id cannot be derived', async () => { | ||
const got = await getProxyAssetPath( | ||
newTestRequest({ path: '/foo.png', headers: { referer: 'http://localhost:8000/p' } }), | ||
) | ||
expect(got).toBe(null) | ||
}) | ||
it('returns null if the project cannot be accessed', async () => { | ||
validatorsMock.mockResolvedValue({ ok: false }) | ||
const got = await getProxyAssetPath( | ||
newTestRequest({ | ||
path: '/foo.png', | ||
headers: { referer: 'http://localhost:8000/p/one' }, | ||
}), | ||
) | ||
expect(got).toBe(null) | ||
}) | ||
it('returns the proxied path', async () => { | ||
validatorsMock.mockResolvedValue({ ok: true }) | ||
const got = await getProxyAssetPath( | ||
newTestRequest({ | ||
path: '/foo.png', | ||
headers: { referer: 'http://localhost:8000/p/one' }, | ||
}), | ||
) | ||
expect(got).toBe(`/p/one/foo.png`) | ||
}) | ||
}) | ||
}) |
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,57 @@ | ||
import urlJoin from 'url-join' | ||
import { ServerEnvironment } from '../env.server' | ||
import { allowedAssetExtensions } from '../handlers/splatLoad' | ||
import { canAccessProject } from '../handlers/validators' | ||
import { UserProjectPermission } from '../types' | ||
|
||
export async function getProxyAssetPath(req: Request): Promise<string | null> { | ||
const url = new URL(req.url) | ||
|
||
// the extension must be allowed | ||
if (!allowedAssetExtensions.some((extension) => url.pathname.endsWith(extension))) { | ||
return null | ||
} | ||
|
||
// the request referer must contain the project id | ||
const maybeProjectId = getProjectIdFromReferer(req) | ||
if (maybeProjectId == null) { | ||
return null | ||
} | ||
|
||
// validate access to the project | ||
const { ok } = await canAccessProject({ | ||
projectId: maybeProjectId, | ||
permission: UserProjectPermission.CAN_VIEW_PROJECT, | ||
request: req, | ||
}) | ||
if (!ok) { | ||
return null | ||
} | ||
|
||
return '/' + urlJoin('p', maybeProjectId, url.pathname) | ||
} | ||
|
||
export function getProjectIdFromReferer(req: Request): string | null { | ||
const referer = req.headers.get('referer') | ||
if (referer == null) { | ||
return null | ||
} | ||
|
||
const refererURL = new URL(referer) | ||
const isMaybeProjectReferer = | ||
refererURL.origin === ServerEnvironment.CORS_ORIGIN && | ||
(refererURL.pathname.startsWith('/p/') || refererURL.pathname.startsWith('/project/')) | ||
if (!isMaybeProjectReferer) { | ||
return null | ||
} | ||
|
||
const maybeProjectId = refererURL.pathname | ||
.replace(/^\/p(roject)?\//, '') | ||
.split('/')[0] | ||
.split('-')[0] | ||
if (maybeProjectId.length === 0) { | ||
return null | ||
} | ||
|
||
return maybeProjectId | ||
} |