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/comment-counts.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
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ const logging = require('@tryghost/logging');
|
|||
const tpl = require('@tryghost/tpl');
|
||||
const themeEngine = require('./frontend/services/theme-engine');
|
||||
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 settingsCache = require('./shared/settings-cache');
|
||||
const urlService = require('./server/services/url');
|
||||
|
@ -51,10 +51,6 @@ class Bridge {
|
|||
return themeEngine.getActive();
|
||||
}
|
||||
|
||||
ensureAdminAuthAssetsMiddleware() {
|
||||
return adminAuthAssets.serveMiddleware();
|
||||
}
|
||||
|
||||
async activateTheme(loadedTheme, checkedTheme) {
|
||||
let settings = {
|
||||
locale: settingsCache.get('locale')
|
||||
|
@ -69,10 +65,6 @@ class Bridge {
|
|||
const cardAssetConfig = this.getCardAssetConfig();
|
||||
debug('reload card assets config', cardAssetConfig);
|
||||
cardAssets.invalidate(cardAssetConfig);
|
||||
|
||||
// TODO: is this in the right place?
|
||||
// rebuild asset files
|
||||
adminAuthAssets.invalidate();
|
||||
} catch (err) {
|
||||
logging.error(new errors.InternalServerError({
|
||||
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 adminAuthAssets = new AdminAuthAssets();
|
||||
const cardAssets = new CardAssets();
|
||||
|
||||
module.exports = {
|
||||
adminAuthAssets,
|
||||
cardAssets
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ const shared = require('../shared');
|
|||
const errorHandler = require('@tryghost/mw-error-handler');
|
||||
const sentry = require('../../../shared/sentry');
|
||||
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.
|
||||
//
|
||||
// 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,
|
||||
// otherwise return a 204 to avoid JS and API requests being made unnecessarily
|
||||
try {
|
||||
|
@ -52,9 +52,7 @@ module.exports = function setupAdminApp() {
|
|||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}, serveStatic(
|
||||
path.join(config.getContentPath('public'), 'admin-auth')
|
||||
));
|
||||
}, createServeAuthFrameFileMw(config, urlUtils));
|
||||
|
||||
// Ember CLI's live-reload script
|
||||
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",
|
||||
"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: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",
|
||||
"test": "yarn test:unit",
|
||||
"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 sinon = require('sinon');
|
||||
|
||||
// Thing we are testing
|
||||
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 () {
|
||||
afterEach(function () {
|
||||
|
@ -60,5 +63,144 @@ describe('Admin App', function () {
|
|||
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