Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 150 additions & 109 deletions packages/realm-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,146 +278,187 @@ export class RealmServer {
}

private serveIndex = async (ctxt: Koa.Context, next: Koa.Next) => {
if (ctxt.header.accept?.includes('text/html')) {
// If this is a /connect iframe request, is the origin a valid published realm?
let acceptHeader = ctxt.header.accept ?? '';
let lowerAcceptHeader = acceptHeader.toLowerCase();
let includesVndMimeType = lowerAcceptHeader.includes('application/vnd.');
let includesHtmlMimeType = lowerAcceptHeader.includes('text/html');

let connectMatch = ctxt.request.path.match(/\/connect\/(.+)$/);
let requestURL = new URL(
`${ctxt.protocol}://${ctxt.host}${ctxt.originalUrl}`,
);

if (connectMatch) {
try {
let originParameter = new URL(decodeURIComponent(connectMatch[1]))
.href;
if (includesHtmlMimeType) {
if (includesVndMimeType) {
let isHostModeRequest = await this.isHostModeRequest(requestURL);

let publishedRealms = await query(this.dbAdapter, [
`SELECT published_realm_url FROM published_realms WHERE published_realm_url LIKE `,
param(`${originParameter}%`),
]);
if (isHostModeRequest) {
return next();
}
}
} else {
if (includesVndMimeType) {
return next();
}

if (publishedRealms.length === 0) {
ctxt.status = 404;
ctxt.body = `Not Found: No published realm found for origin ${originParameter}`;
let isHostModeRequest = await this.isHostModeRequest(requestURL);

this.log.debug(
`Ignoring /connect request for origin ${originParameter}: no matching published realm`,
);
if (!isHostModeRequest) {
return next();
}
}

return;
}
// If this is a /connect iframe request, is the origin a valid published realm?

ctxt.set(
'Content-Security-Policy',
`frame-ancestors ${originParameter}`,
);
} catch (error) {
ctxt.status = 400;
ctxt.body = 'Bad Request';
let connectMatch = ctxt.request.path.match(/\/connect\/(.+)$/);

if (connectMatch) {
try {
let originParameter = new URL(decodeURIComponent(connectMatch[1])).href;

let publishedRealms = await query(this.dbAdapter, [
`SELECT published_realm_url FROM published_realms WHERE published_realm_url LIKE `,
param(`${originParameter}%`),
]);

this.log.info(`Error processing /connect request: ${error}`);
if (publishedRealms.length === 0) {
ctxt.status = 404;
ctxt.body = `Not Found: No published realm found for origin ${originParameter}`;

this.log.debug(
`Ignoring /connect request for origin ${originParameter}: no matching published realm`,
);

return;
}
}

ctxt.type = 'html';

let requestURL = new URL(
`${ctxt.protocol}://${ctxt.host}${ctxt.originalUrl}`,
);
let cardURL = requestURL;
let isIndexRequest = requestURL.pathname.endsWith('/');
if (isIndexRequest) {
cardURL = new URL('index', requestURL);
}
ctxt.set(
'Content-Security-Policy',
`frame-ancestors ${originParameter}`,
);
} catch (error) {
ctxt.status = 400;
ctxt.body = 'Bad Request';

let indexHTML = await this.retrieveIndexHTML();
let hasPublicPermissions = await this.hasPublicPermissions(cardURL);
this.log.info(`Error processing /connect request: ${error}`);

if (!hasPublicPermissions) {
ctxt.body = indexHTML;
return;
}
}

this.headLog.debug(`Fetching head HTML for ${cardURL.href}`);
this.isolatedLog.debug(`Fetching isolated HTML for ${cardURL.href}`);
this.scopedCSSLog.debug(`Fetching scoped CSS for ${cardURL.href}`);
ctxt.type = 'html';

let [headHTML, isolatedHTML, scopedCSS] = await Promise.all([
this.retrieveHeadHTML(cardURL),
this.retrieveIsolatedHTML(cardURL),
retrieveScopedCSS({
cardURL,
dbAdapter: this.dbAdapter,
indexURLCandidates: (url) => this.indexURLCandidates(url),
indexCandidateExpressions: (candidates) =>
this.indexCandidateExpressions(candidates),
log: this.scopedCSSLog,
}),
]);
let cardURL = requestURL;
let isIndexRequest = requestURL.pathname.endsWith('/');
if (isIndexRequest) {
cardURL = new URL('index', requestURL);
}

if (headHTML != null) {
this.headLog.debug(
`Injecting head HTML for ${cardURL.href} (length ${headHTML.length})\n${this.truncateLogLines(
headHTML,
)}`,
);
} else {
this.headLog.debug(
`No head HTML found for ${cardURL.href}, serving base index.html`,
);
}
let indexHTML = await this.retrieveIndexHTML();
let hasPublicPermissions = await this.hasPublicPermissions(cardURL);

if (scopedCSS != null) {
this.scopedCSSLog.debug(
`Using scoped CSS for ${cardURL.href} (length ${scopedCSS.length})`,
);
} else {
this.scopedCSSLog.debug(
`No scoped CSS returned from database for ${cardURL.href}`,
);
}
if (!hasPublicPermissions) {
ctxt.body = indexHTML;
return;
}

let responseHTML = indexHTML;
let headFragments: string[] = [];
this.headLog.debug(`Fetching head HTML for ${cardURL.href}`);
this.isolatedLog.debug(`Fetching isolated HTML for ${cardURL.href}`);
this.scopedCSSLog.debug(`Fetching scoped CSS for ${cardURL.href}`);

if (headHTML != null) {
headFragments.push(headHTML);
}
let [headHTML, isolatedHTML, scopedCSS] = await Promise.all([
this.retrieveHeadHTML(cardURL),
this.retrieveIsolatedHTML(cardURL),
retrieveScopedCSS({
cardURL,
dbAdapter: this.dbAdapter,
indexURLCandidates: (url) => this.indexURLCandidates(url),
indexCandidateExpressions: (candidates) =>
this.indexCandidateExpressions(candidates),
log: this.scopedCSSLog,
}),
]);

if (scopedCSS != null) {
this.scopedCSSLog.debug(`Injecting scoped CSS for ${cardURL.href}`);
headFragments.push(
`<style data-boxel-scoped-css>\n${scopedCSS}\n</style>`,
);
}
if (headHTML != null) {
this.headLog.debug(
`Injecting head HTML for ${cardURL.href} (length ${headHTML.length})\n${this.truncateLogLines(
headHTML,
)}`,
);
} else {
this.headLog.debug(
`No head HTML found for ${cardURL.href}, serving base index.html`,
);
}

if (headFragments.length > 0) {
responseHTML = this.injectHeadHTML(
responseHTML,
headFragments.join('\n'),
);
}
if (scopedCSS != null) {
this.scopedCSSLog.debug(
`Using scoped CSS for ${cardURL.href} (length ${scopedCSS.length})`,
);
} else {
this.scopedCSSLog.debug(
`No scoped CSS returned from database for ${cardURL.href}`,
);
}

if (isolatedHTML != null) {
this.isolatedLog.debug(
`Injecting isolated HTML for ${cardURL.href} (length ${isolatedHTML.length})\n${this.truncateLogLines(
isolatedHTML,
)}`,
);
responseHTML = this.injectIsolatedHTML(responseHTML, isolatedHTML);
}
let responseHTML = indexHTML;
let headFragments: string[] = [];

ctxt.body = responseHTML;
return;
if (headHTML != null) {
headFragments.push(headHTML);
}
return next();

if (scopedCSS != null) {
this.scopedCSSLog.debug(`Injecting scoped CSS for ${cardURL.href}`);
headFragments.push(
`<style data-boxel-scoped-css>\n${scopedCSS}\n</style>`,
);
}

if (headFragments.length > 0) {
responseHTML = this.injectHeadHTML(
responseHTML,
headFragments.join('\n'),
);
}

if (isolatedHTML != null) {
this.isolatedLog.debug(
`Injecting isolated HTML for ${cardURL.href} (length ${isolatedHTML.length})\n${this.truncateLogLines(
isolatedHTML,
)}`,
);
responseHTML = this.injectIsolatedHTML(responseHTML, isolatedHTML);
}

ctxt.body = responseHTML;
return;
return;
};

private async hasPublicPermissions(cardURL: URL): Promise<boolean> {
let realm = this.realms.find((candidate) => {
private findRealmForRequestURL(requestURL: URL): Realm | undefined {
return this.realms.find((candidate) => {
let realmURL = new URL(candidate.url);
realmURL.protocol = cardURL.protocol;
return new RealmPaths(realmURL).inRealm(cardURL);
realmURL.protocol = requestURL.protocol;
return new RealmPaths(realmURL).inRealm(requestURL);
});
}

private async isHostModeRequest(requestURL: URL): Promise<boolean> {
let realm = this.findRealmForRequestURL(requestURL);
if (!realm) {
return false;
}

let rows = await query(this.dbAdapter, [
`SELECT published_realm_url FROM published_realms WHERE published_realm_url =`,
param(realm.url),
]);

return rows.length > 0;
}

private async hasPublicPermissions(cardURL: URL): Promise<boolean> {
let realm = this.findRealmForRequestURL(cardURL);

if (!realm) {
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { module, test } from 'qunit';
import { join, basename } from 'path';
import type { Test, SuperTest } from 'supertest';
import { systemInitiatedPriority } from '@cardstack/runtime-common';
import { setupServerEndpointsTest, testRealm2URL } from './helpers';
import { waitUntil } from '../helpers';
import { setupPermissionedRealmAtURL, waitUntil } from '../helpers';
import { ensureDirSync, writeFileSync, writeJSONSync } from 'fs-extra';
import '@cardstack/runtime-common/helpers/code-equality-assertion';

Expand Down Expand Up @@ -352,4 +353,49 @@ module(`server-endpoints/${basename(__filename)}`, function () {
});
},
);

module('Published realm index responses', function (hooks) {
let realmURL = new URL('http://127.0.0.1:4444/');
let request: SuperTest<Test>;

function onRealmSetup(args: {
request: SuperTest<Test>;
}) {
request = args.request;
}

setupPermissionedRealmAtURL(hooks, realmURL, {
permissions: {
'*': ['read'],
},
published: true,
onRealmSetup,
});

test('serves index HTML by default for published realm', async function (assert) {
let response = await request.get('/').set('Accept', 'application/json');

assert.strictEqual(response.status, 200, 'serves HTML response');
assert.ok(
response.headers['content-type']?.includes('text/html'),
'content type is text/html',
);
assert.ok(
response.text.includes('data-test-home-card'),
'index HTML is served',
);
});

test('skips index HTML when vendor mime type is requested', async function (assert) {
let response = await request
.get('/person-1')
.set('Accept', 'application/vnd.card+json');

assert.strictEqual(response.status, 200, 'serves JSON response');
assert.ok(
response.headers['content-type']?.includes('application/vnd.card+json'),
'content type is vendor JSON',
);
});
});
});
Loading