diff --git a/src/server/HeadHtmlSupport.js b/src/server/HeadHtmlSupport.js index f41d7c18..975d66a6 100644 --- a/src/server/HeadHtmlSupport.js +++ b/src/server/HeadHtmlSupport.js @@ -76,7 +76,7 @@ export default class HeadHtmlSupport { } constructor({ - proxyUrl, directory, allowInsecure, log, + proxyUrl, directory, allowInsecure, log, siteToken, }) { this.remoteHtml = ''; this.remoteDom = null; @@ -84,6 +84,7 @@ export default class HeadHtmlSupport { this.localHtml = ''; this.localStatus = 0; this.cookie = ''; + this.siteToken = siteToken; this.url = new URL(proxyUrl); this.url.pathname = '/head.html'; this.filePath = resolve(directory, 'head.html'); @@ -97,6 +98,10 @@ export default class HeadHtmlSupport { if (this.cookie) { headers.cookie = this.cookie; } + // hlx 5 site auth + if (this.siteToken) { + headers.authorization = `token ${this.siteToken}`; + } const resp = await getFetch(this.allowInsecure)(this.url, { cache: 'no-store', headers, diff --git a/src/server/HelixProject.js b/src/server/HelixProject.js index 413c898b..5818bf02 100644 --- a/src/server/HelixProject.js +++ b/src/server/HelixProject.js @@ -32,6 +32,11 @@ export class HelixProject extends BaseProject { return this; } + withSiteToken(value) { + this._server.withSiteToken(value); + return this; + } + withProxyUrl(value) { this._proxyUrl = value; return this; @@ -88,6 +93,7 @@ export class HelixProject extends BaseProject { log: this.log, proxyUrl: this.proxyUrl, allowInsecure: this.allowInsecure, + siteToken: this.siteToken, }); // register local head in live-reload diff --git a/src/server/HelixServer.js b/src/server/HelixServer.js index 7df9d300..4ef5ea29 100644 --- a/src/server/HelixServer.js +++ b/src/server/HelixServer.js @@ -34,6 +34,11 @@ export class HelixServer extends BaseServer { return this; } + withSiteToken(value) { + this._siteToken = value; + return this; + } + /** * Proxy Mode route handler * @param {Express.Request} req request @@ -98,12 +103,14 @@ export class HelixServer extends BaseServer { for (const [key, value] of proxyUrl.searchParams.entries()) { url.searchParams.append(key, value); } + await utils.proxyRequest(ctx, url.href, req, res, { injectLiveReload: this._project.liveReload, headHtml: this._project.headHtml, indexer: this._project.indexer, cacheDirectory: this._project.cacheDirectory, file404html: this._project.file404html, + siteToken: this._siteToken, }); } catch (err) { log.error(`${pfx}failed to proxy AEM request ${ctx.path}: ${err.message}`); diff --git a/src/server/utils.js b/src/server/utils.js index 157a6eb8..55b30f59 100644 --- a/src/server/utils.js +++ b/src/server/utils.js @@ -246,6 +246,12 @@ window.LiveReloadOptions = { ...req.headers, ...(opts.headers || {}), }; + + // hlx 5 site auth + if (opts.siteToken) { + headers.authorization = `token ${opts.siteToken}`; + } + // preserve hlx-auth-token cookie const cookies = cookie.parse(headers.cookie || ''); delete headers.cookie; @@ -255,6 +261,7 @@ window.LiveReloadOptions = { 'hlx-auth-token': hlxAuthToken, }).toString(); } + delete headers.connection; delete headers['proxy-connection']; delete headers.host; diff --git a/src/up.cmd.js b/src/up.cmd.js index 3fba9ff1..f5511035 100644 --- a/src/up.cmd.js +++ b/src/up.cmd.js @@ -40,6 +40,11 @@ export default class UpCommand extends AbstractServerCommand { return this; } + withSiteToken(value) { + this._siteToken = value; + return this; + } + async doStop() { await super.doStop(); if (this._watcher) { @@ -71,7 +76,9 @@ export default class UpCommand extends AbstractServerCommand { .withLogger(this._logger) .withKill(this._kill) .withPrintIndex(this._printIndex) - .withAllowInsecure(this._allowInsecure); + .withAllowInsecure(this._allowInsecure) + .withSiteToken(this._siteToken); + this.log.info(chalk`{yellow ___ ________ ___ __ __ v${pkgJson.version}}`); this.log.info(chalk`{yellow / | / ____/ |/ / _____(_)___ ___ __ __/ /___ _/ /_____ _____}`); this.log.info(chalk`{yellow / /| | / __/ / /|_/ / / ___/ / __ \`__ \\/ / / / / __ \`/ __/ __ \\/ ___/}`); diff --git a/src/up.js b/src/up.js index 6d8c070c..46b06ae4 100644 --- a/src/up.js +++ b/src/up.js @@ -51,6 +51,11 @@ export default function up() { type: 'int', default: 3000, }) + .option('site-token', { + alias: 'siteToken', + describe: 'Site token to be used by the cli to access the website', + type: 'string', + }) .option('addr', { describe: 'Bind development server on addr. use * to bind to any address and allow external connections.', type: 'string', @@ -116,6 +121,7 @@ export default function up() { .withOpen(path.basename(argv.$0) === 'aem' ? argv.open : false) .withTLS(argv.tlsKey, argv.tlsCert) .withLiveReload(argv.livereload) + .withSiteToken(argv.siteToken) .withUrl(argv.url) .withPrintIndex(argv.printIndex) .withAllowInsecure(argv.allowInsecure) diff --git a/test/fixtures/all.env b/test/fixtures/all.env index 30fa8ad1..7d00ca91 100644 --- a/test/fixtures/all.env +++ b/test/fixtures/all.env @@ -21,6 +21,7 @@ AEM_HOST=www.project-helix.io AEM_NO_OPEN=true AEM_LIVERELOAD=false AEM_PORT=1234 +AEM_SITE_TOKEN=secret-site-token AEM_ADDR=* AEM_LOCAL_REPO="., ../foo-content, ../bar-content" #AEM_SAVE_CONFIG diff --git a/test/head-html.test.js b/test/head-html.test.js index 6183af07..20a95bb9 100644 --- a/test/head-html.test.js +++ b/test/head-html.test.js @@ -244,6 +244,27 @@ describe('Head.html loading tests', () => { await hhs.update(); }); + it('update loads remote head.html with site token', async () => { + const siteToken = 'hlxtst_site-token'; + const directory = await setupProject(path.join(__rootdir, 'test', 'fixtures', 'project'), testRoot); + + nock('https://main--blog--adobe.hlx.page') + .get('/head.html') + .reply(function request() { + assert.strictEqual(this.req.headers.authorization, `token ${siteToken}`); + return [200, 'fooo\n']; + }); + + const hhs = new HeadHtmlSupport({ + log: console, + proxyUrl: 'https://main--blog--adobe.hlx.page', + directory, + siteToken, + }); + hhs.localStatus = 200; + await hhs.update(); + }); + it('update loads remote head.html can handle errors', async () => { const directory = await setupProject(path.join(__rootdir, 'test', 'fixtures', 'project'), testRoot); diff --git a/test/up-cli.test.js b/test/up-cli.test.js index 13f92f3a..cc1dca0c 100644 --- a/test/up-cli.test.js +++ b/test/up-cli.test.js @@ -45,6 +45,7 @@ describe('hlx up', () => { mockUp.withAllowInsecure.returnsThis(); mockUp.withKill.returnsThis(); mockUp.withCache.returnsThis(); + mockUp.withSiteToken.returnsThis(); mockUp.run.returnsThis(); cli = (await new CLI().initCommands()).withCommandExecutor('up', mockUp); }); @@ -72,6 +73,7 @@ describe('hlx up', () => { sinon.assert.calledWith(mockUp.withHttpPort, 1234); sinon.assert.calledWith(mockUp.withBindAddr, '*'); sinon.assert.calledWith(mockUp.withPrintIndex, true); + sinon.assert.calledWith(mockUp.withSiteToken, 'secret-site-token'); sinon.assert.calledOnce(mockUp.run); }); @@ -139,4 +141,10 @@ describe('hlx up', () => { sinon.assert.calledWith(mockUp.withKill, false); sinon.assert.calledOnce(mockUp.run); }); + + it('aem up can set site token', async () => { + await cli.run(['up', '--site-token', 'secret-site-token']); + sinon.assert.calledWith(mockUp.withSiteToken, 'secret-site-token'); + sinon.assert.calledOnce(mockUp.run); + }); }); diff --git a/test/up-cmd.test.js b/test/up-cmd.test.js index a36a8de4..c01a76f0 100644 --- a/test/up-cmd.test.js +++ b/test/up-cmd.test.js @@ -157,6 +157,86 @@ describe('Integration test for up command with helix pages', function suite() { assert.strictEqual(opened, `http://localhost:${port}/`); }); + it('up command opens browser and delivers correct response on helix 5 with auth.', async () => { + const TOKEN = 'secret-site-token'; + function checkTokenAndReply(req, response) { + const { authorization } = req.headers; + if (!authorization) { + return [401, 'Unauthorized']; + } + + if (authorization !== `token ${TOKEN}`) { + return [403, 'Forbidden']; + } + + return response; + } + + let opened; + const MockedCommand = await esmock('../src/up.cmd.js', { + '../src/abstract-server.cmd.js': await esmock('../src/abstract-server.cmd.js', { + open: (url) => { + opened = url; + }, + }), + }); + initGit(testDir, 'https://github.com/adobe/dummy-foo.git'); + const cmd = new MockedCommand() + .withLiveReload(false) + .withDirectory(testDir) + .withOpen('/') + .withHttpPort(0) + .withSiteToken(TOKEN); + + nock('https://main--dummy-foo--adobe.aem.page') + .get('/index.html') + .reply(function f() { + return checkTokenAndReply(this.req, [200, '## Welcome']); + }) + .get('/not-found.txt') + .reply(function f() { + return checkTokenAndReply(this.req, [404, 'Not Found']); + }); + + nock('https://admin.hlx.page:443') + .get('/sidekick/adobe/dummy-foo/main/config.json') + .reply(200, { + host: 'example.com', + liveHost: 'main--dummy-foo--adobe.aem.live', + previewHost: 'main--dummy-foo--adobe..aem.page', + project: 'Example Project on Helix 5', + testProperty: 'header', + }); + + let port; + await new Promise((resolve, reject) => { + let error = null; + cmd + .on('started', async () => { + try { + port = cmd.project.server.port; + let ret = await assertHttp(`http://127.0.0.1:${port}/index.html`, 200); + assert.strictEqual(ret.trim(), '## Welcome'); + ret = await assertHttp(`http://127.0.0.1:${port}/local.txt`, 200); + assert.strictEqual(ret.trim(), 'Hello, world.'); + await assertHttp(`http://127.0.0.1:${port}/not-found.txt`, 404); + } catch (e) { + error = e; + } + await cmd.stop(); + }) + .on('stopped', () => { + if (error) { + reject(error); + } + resolve(); + }) + .run() + .catch(reject); + }); + assert.strictEqual(opened, `http://localhost:${port}/`); + }); + it('up command correctly replaces variables in url', async () => { const MockedCommand = await esmock('../src/up.cmd.js', { '../src/abstract-server.cmd.js': await esmock('../src/abstract-server.cmd.js'),