mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
serve auth-frame from public
This commit is contained in:
parent
024b644662
commit
61bd938280
10 changed files with 184 additions and 86 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -124,6 +124,8 @@ test/functional/*.png
|
||||||
/ghost/core/core/frontend/public/ghost.min.css
|
/ghost/core/core/frontend/public/ghost.min.css
|
||||||
/ghost/core/core/frontend/public/comment-counts.min.js
|
/ghost/core/core/frontend/public/comment-counts.min.js
|
||||||
/ghost/core/core/frontend/public/member-attribution.min.js
|
/ghost/core/core/frontend/public/member-attribution.min.js
|
||||||
|
/ghost/core/core/frontend/public/admin-auth/admin-auth.min.js
|
||||||
|
|
||||||
# Caddyfile - for local development with ssl + caddy
|
# Caddyfile - for local development with ssl + caddy
|
||||||
Caddyfile
|
Caddyfile
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ const logging = require('@tryghost/logging');
|
||||||
const tpl = require('@tryghost/tpl');
|
const tpl = require('@tryghost/tpl');
|
||||||
const themeEngine = require('./frontend/services/theme-engine');
|
const themeEngine = require('./frontend/services/theme-engine');
|
||||||
const appService = require('./frontend/services/apps');
|
const appService = require('./frontend/services/apps');
|
||||||
const {adminAuthAssets, cardAssets} = require('./frontend/services/assets-minification');
|
const {cardAssets} = require('./frontend/services/assets-minification');
|
||||||
const routerManager = require('./frontend/services/routing').routerManager;
|
const routerManager = require('./frontend/services/routing').routerManager;
|
||||||
const settingsCache = require('./shared/settings-cache');
|
const settingsCache = require('./shared/settings-cache');
|
||||||
const urlService = require('./server/services/url');
|
const urlService = require('./server/services/url');
|
||||||
|
@ -51,10 +51,6 @@ class Bridge {
|
||||||
return themeEngine.getActive();
|
return themeEngine.getActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureAdminAuthAssetsMiddleware() {
|
|
||||||
return adminAuthAssets.serveMiddleware();
|
|
||||||
}
|
|
||||||
|
|
||||||
async activateTheme(loadedTheme, checkedTheme) {
|
async activateTheme(loadedTheme, checkedTheme) {
|
||||||
let settings = {
|
let settings = {
|
||||||
locale: settingsCache.get('locale')
|
locale: settingsCache.get('locale')
|
||||||
|
@ -69,10 +65,6 @@ class Bridge {
|
||||||
const cardAssetConfig = this.getCardAssetConfig();
|
const cardAssetConfig = this.getCardAssetConfig();
|
||||||
debug('reload card assets config', cardAssetConfig);
|
debug('reload card assets config', cardAssetConfig);
|
||||||
cardAssets.invalidate(cardAssetConfig);
|
cardAssets.invalidate(cardAssetConfig);
|
||||||
|
|
||||||
// TODO: is this in the right place?
|
|
||||||
// rebuild asset files
|
|
||||||
adminAuthAssets.invalidate();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logging.error(new errors.InternalServerError({
|
logging.error(new errors.InternalServerError({
|
||||||
message: tpl(messages.activateFailed, {theme: loadedTheme.name}),
|
message: tpl(messages.activateFailed, {theme: loadedTheme.name}),
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
// const debug = require('@tryghost/debug')('comments-counts-assets');
|
|
||||||
const Minifier = require('@tryghost/minifier');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const logging = require('@tryghost/logging');
|
|
||||||
const config = require('../../../shared/config');
|
|
||||||
const urlUtils = require('../../../shared/url-utils');
|
|
||||||
const AssetsMinificationBase = require('./AssetsMinificationBase');
|
|
||||||
|
|
||||||
module.exports = class AdminAuthAssets extends AssetsMinificationBase {
|
|
||||||
constructor(options = {}) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
this.src = options.src || path.join(config.get('paths').assetSrc, 'admin-auth');
|
|
||||||
/** @private */
|
|
||||||
this.dest = options.dest || path.join(config.getContentPath('public'), 'admin-auth');
|
|
||||||
|
|
||||||
this.minifier = new Minifier({src: this.src, dest: this.dest});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// TODO: don't do this synchronously
|
|
||||||
fs.mkdirSync(this.dest, {recursive: true});
|
|
||||||
fs.copyFileSync(path.join(this.src, 'index.html'), path.join(this.dest, 'index.html'));
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'EACCES') {
|
|
||||||
logging.error('Ghost was not able to write admin-auth asset files due to permissions.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
generateGlobs() {
|
|
||||||
return {
|
|
||||||
'admin-auth.min.js': '*.js'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
generateReplacements() {
|
|
||||||
// Clean the URL, only keep schema, host and port (without trailing slashes or subdirectory)
|
|
||||||
const url = new URL(urlUtils.getSiteUrl());
|
|
||||||
const origin = url.origin;
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Properly encode the origin
|
|
||||||
'\'{{SITE_ORIGIN}}\'': JSON.stringify(origin)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minify, move into the destination directory, and clear existing asset files.
|
|
||||||
*
|
|
||||||
* @override
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async load() {
|
|
||||||
const globs = this.generateGlobs();
|
|
||||||
const replacements = this.generateReplacements();
|
|
||||||
await this.minify(globs, {replacements});
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,10 +1,7 @@
|
||||||
const AdminAuthAssets = require('./AdminAuthAssets');
|
|
||||||
const CardAssets = require('./CardAssets');
|
const CardAssets = require('./CardAssets');
|
||||||
|
|
||||||
const adminAuthAssets = new AdminAuthAssets();
|
|
||||||
const cardAssets = new CardAssets();
|
const cardAssets = new CardAssets();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
adminAuthAssets,
|
|
||||||
cardAssets
|
cardAssets
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,7 @@ const shared = require('../shared');
|
||||||
const errorHandler = require('@tryghost/mw-error-handler');
|
const errorHandler = require('@tryghost/mw-error-handler');
|
||||||
const sentry = require('../../../shared/sentry');
|
const sentry = require('../../../shared/sentry');
|
||||||
const redirectAdminUrls = require('./middleware/redirect-admin-urls');
|
const redirectAdminUrls = require('./middleware/redirect-admin-urls');
|
||||||
const bridge = require('../../../bridge');
|
const createServeAuthFrameFileMw = require('./middleware/serve-auth-frame-file');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -39,7 +39,7 @@ module.exports = function setupAdminApp() {
|
||||||
// request to the Admin API /users/me/ endpoint to check if the user is logged in.
|
// request to the Admin API /users/me/ endpoint to check if the user is logged in.
|
||||||
//
|
//
|
||||||
// Used by comments-ui to add moderation options to front-end comments when logged in.
|
// Used by comments-ui to add moderation options to front-end comments when logged in.
|
||||||
adminApp.use('/auth-frame', bridge.ensureAdminAuthAssetsMiddleware(), function authFrameMw(req, res, next) {
|
adminApp.use('/auth-frame', function authFrameMw(req, res, next) {
|
||||||
// only render content when we have an Admin session cookie,
|
// only render content when we have an Admin session cookie,
|
||||||
// otherwise return a 204 to avoid JS and API requests being made unnecessarily
|
// otherwise return a 204 to avoid JS and API requests being made unnecessarily
|
||||||
try {
|
try {
|
||||||
|
@ -52,9 +52,7 @@ module.exports = function setupAdminApp() {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
}, serveStatic(
|
}, createServeAuthFrameFileMw(config, urlUtils));
|
||||||
path.join(config.getContentPath('public'), 'admin-auth')
|
|
||||||
));
|
|
||||||
|
|
||||||
// Ember CLI's live-reload script
|
// Ember CLI's live-reload script
|
||||||
if (config.get('env') === 'development') {
|
if (config.get('env') === 'development') {
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('node:fs/promises');
|
||||||
|
|
||||||
|
function createServeAuthFrameFileMw(config, urlUtils) {
|
||||||
|
const placeholders = {
|
||||||
|
'{{SITE_ORIGIN}}': new URL(urlUtils.getSiteUrl()).origin
|
||||||
|
};
|
||||||
|
|
||||||
|
return function serveAuthFrameFileMw(req, res, next) {
|
||||||
|
const filename = path.parse(req.url).base;
|
||||||
|
|
||||||
|
let basePath = path.join(config.get('paths').publicFilePath, 'admin-auth');
|
||||||
|
let filePath;
|
||||||
|
|
||||||
|
if (filename === '') {
|
||||||
|
filePath = path.join(basePath, 'index.html');
|
||||||
|
} else {
|
||||||
|
filePath = path.join(basePath, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.readFile(filePath).then((data) => {
|
||||||
|
let dataString = data.toString();
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(placeholders)) {
|
||||||
|
dataString = dataString.replace(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end(dataString);
|
||||||
|
}).catch(() => {
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createServeAuthFrameFileMw;
|
|
@ -23,7 +23,7 @@
|
||||||
"archive": "npm pack",
|
"archive": "npm pack",
|
||||||
"dev": "node --watch index.js",
|
"dev": "node --watch index.js",
|
||||||
"build:assets:css": "postcss core/frontend/public/ghost.css --no-map --use cssnano -o core/frontend/public/ghost.min.css",
|
"build:assets:css": "postcss core/frontend/public/ghost.css --no-map --use cssnano -o core/frontend/public/ghost.min.css",
|
||||||
"build:assets:js": "../minifier/bin/minify core/frontend/public/comment-counts.js core/frontend/public/comment-counts.min.js && ../minifier/bin/minify core/frontend/public/member-attribution.js core/frontend/public/member-attribution.min.js",
|
"build:assets:js": "../minifier/bin/minify core/frontend/public/comment-counts.js core/frontend/public/comment-counts.min.js && ../minifier/bin/minify core/frontend/public/member-attribution.js core/frontend/public/member-attribution.min.js && ../minifier/bin/minify core/frontend/public/admin-auth/message-handler.js core/frontend/public/admin-auth/admin-auth.min.js",
|
||||||
"build:assets": "yarn build:assets:css && yarn build:assets:js",
|
"build:assets": "yarn build:assets:css && yarn build:assets:js",
|
||||||
"test": "yarn test:unit",
|
"test": "yarn test:unit",
|
||||||
"test:base": "mocha --reporter dot --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js",
|
"test:base": "mocha --reporter dot --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js",
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
const fs = require('node:fs/promises');
|
||||||
const should = require('should');
|
const should = require('should');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
// Thing we are testing
|
// Thing we are testing
|
||||||
const redirectAdminUrls = require('../../../../../core/server/web/admin/middleware/redirect-admin-urls');
|
const redirectAdminUrls = require('../../../../../core/server/web/admin/middleware/redirect-admin-urls');
|
||||||
|
const createServeAuthFrameFileMw = require('../../../../../core/server/web/admin/middleware/serve-auth-frame-file');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
describe('Admin App', function () {
|
describe('Admin App', function () {
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
@ -60,5 +63,144 @@ describe('Admin App', function () {
|
||||||
res.redirect.called.should.be.false();
|
res.redirect.called.should.be.false();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('serveAuthFrameFile', function () {
|
||||||
|
let config;
|
||||||
|
let urlUtils;
|
||||||
|
let readFile;
|
||||||
|
|
||||||
|
const siteUrl = 'https://foo.bar';
|
||||||
|
const publicFilePath = 'foo/bar/public';
|
||||||
|
|
||||||
|
const indexContent = '<html><body><h1>Hello, world!</h1></body></html>';
|
||||||
|
const fooJsContent = '(function() { console.log("Hello, world!"); })();';
|
||||||
|
const fooJsContentWithSiteOrigin = '(function() { console.log("{{SITE_ORIGIN}}"); })();';
|
||||||
|
|
||||||
|
function createMiddleware() {
|
||||||
|
return createServeAuthFrameFileMw(config, urlUtils);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config = {
|
||||||
|
get: sinon.stub()
|
||||||
|
};
|
||||||
|
|
||||||
|
config.get.withArgs('paths').returns({
|
||||||
|
publicFilePath
|
||||||
|
});
|
||||||
|
|
||||||
|
urlUtils = {
|
||||||
|
getSiteUrl: sinon.stub().returns(siteUrl)
|
||||||
|
};
|
||||||
|
readFile = sinon.stub(fs, 'readFile');
|
||||||
|
|
||||||
|
const adminAuthFilePath = filename => path.join(publicFilePath, 'admin-auth', filename);
|
||||||
|
|
||||||
|
readFile.withArgs(adminAuthFilePath('index.html'))
|
||||||
|
.resolves(Buffer.from(indexContent));
|
||||||
|
readFile.withArgs(adminAuthFilePath('foo.js'))
|
||||||
|
.resolves(Buffer.from(fooJsContent));
|
||||||
|
readFile.withArgs(adminAuthFilePath('foo-2.js'))
|
||||||
|
.resolves(Buffer.from(fooJsContentWithSiteOrigin));
|
||||||
|
readFile.withArgs(adminAuthFilePath('bar.js'))
|
||||||
|
.rejects(new Error('File not found'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
readFile.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should serve index.html if the url is /', async function () {
|
||||||
|
const middleware = createMiddleware();
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
url: '/'
|
||||||
|
};
|
||||||
|
const res = {
|
||||||
|
end: sinon.stub()
|
||||||
|
};
|
||||||
|
const next = sinon.stub();
|
||||||
|
|
||||||
|
await middleware(req, res, next);
|
||||||
|
|
||||||
|
res.end.calledWith(indexContent).should.be.true();
|
||||||
|
next.called.should.be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should serve the correct file corresponding to the url', async function () {
|
||||||
|
const middleware = createMiddleware();
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
url: '/foo.js'
|
||||||
|
};
|
||||||
|
const res = {
|
||||||
|
end: sinon.stub()
|
||||||
|
};
|
||||||
|
const next = sinon.stub();
|
||||||
|
|
||||||
|
await middleware(req, res, next);
|
||||||
|
|
||||||
|
res.end.calledWith(fooJsContent).should.be.true();
|
||||||
|
next.called.should.be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace {{SITE_ORIGIN}} with the site url', async function () {
|
||||||
|
const middleware = createMiddleware();
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
url: '/foo-2.js'
|
||||||
|
};
|
||||||
|
const res = {
|
||||||
|
end: sinon.stub()
|
||||||
|
};
|
||||||
|
const next = sinon.stub();
|
||||||
|
|
||||||
|
await middleware(req, res, next);
|
||||||
|
|
||||||
|
res.end.calledOnce.should.be.true();
|
||||||
|
|
||||||
|
const args = res.end.getCall(0).args;
|
||||||
|
args[0].toString().includes(siteUrl).should.be.true();
|
||||||
|
args[0].toString().includes(`{{SITE_ORIGIN}}`).should.be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow path traversal', async function () {
|
||||||
|
const middleware = createMiddleware();
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
url: '/foo/../../foo.js'
|
||||||
|
};
|
||||||
|
const res = {
|
||||||
|
end: sinon.stub()
|
||||||
|
};
|
||||||
|
const next = sinon.stub();
|
||||||
|
|
||||||
|
await middleware(req, res, next);
|
||||||
|
|
||||||
|
res.end.calledOnce.should.be.true();
|
||||||
|
|
||||||
|
// Because we use base name when resolving the file, foo.js should be served
|
||||||
|
res.end.calledWith(fooJsContent).should.be.true();
|
||||||
|
|
||||||
|
next.calledOnce.should.be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call next if the file is not found', async function () {
|
||||||
|
const middleware = createMiddleware();
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
url: '/bar.js'
|
||||||
|
};
|
||||||
|
const res = {
|
||||||
|
end: sinon.stub()
|
||||||
|
};
|
||||||
|
const next = sinon.stub();
|
||||||
|
|
||||||
|
await middleware(req, res, next);
|
||||||
|
|
||||||
|
res.end.calledOnce.should.be.false();
|
||||||
|
next.calledOnce.should.be.true();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue