diff --git a/README.md b/README.md index 4d4045e..8837c18 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,29 @@ git push -u origin master **5. Refresh content with a webhook (optional)** -At first start, a `git_secret` file will be generated, use it to create a new webhook as following : +Create a webhook on your git source (On GitHub, in the `Settings/Webhooks` part of the repository.) with the following parameters : * Payload URL : `https:///webhook` * Content type : `application/json` -* Secret : `` * Events : Just the push event -On GitHub, webhooks can be created in the `Settings/Webhooks` part of the repository. +**6. Securize your webhook (optional)** + +Here are the steps for Github, if you use another platform adapt it your way (header format on the config) : + +* Create a password or random secret +* Calculate it's SHA1 +* Edit your configuration to add webhook info +```json +{ +... +"webhook": { + "secret_value": "sha1=", + "secret_header": "X-Hub-Signature" + }, +... +} +``` +* Launch the server +* Update your webhook on github to include the secret +* Check if Github successfully reached the endpoint \ No newline at end of file diff --git a/src/app.js b/src/app.js index 05d62e1..1ce5022 100644 --- a/src/app.js +++ b/src/app.js @@ -14,16 +14,6 @@ const cons = { error: '\x1b[31m✘\x1b[0m %s', }; -const randStr = (length) => { - let result = ''; - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - return result; -}; - module.exports = (config) => { const fw = require('./file_walker')(config); const renderer = require('./renderer')(config); @@ -35,7 +25,6 @@ module.exports = (config) => { const articles = {}; let lastRSS = ''; - let webhookSecret; /** * Fetch articles from the data folder and send success as a response @@ -64,35 +53,6 @@ module.exports = (config) => { if (config['test']) app.reload = reload; - /** - * Fetch or create secret token for git webhook - * @param success - * @param error - */ - const checkSecret = (success, error) => { - if (!config['modules']['webhook']) - success(); - fs.readFile(config['webhook']['secret_file'], {encoding: 'UTF-8'}, (err, data) => { - if (err) { - webhookSecret = randStr(32); - fs.writeFile(config['webhook']['secret_file'], webhookSecret, {encoding: 'UTF-8'}, (err) => { - if (err) { - console.error(cons.error, 'error creating secret : ' + err); - return error ? error() : null; - } - console.log(cons.ok,'created git secret at '+config['webhook']['secret_file']); - success(); - }); - } else { - webhookSecret = data; - console.log(cons.ok,'loaded git secret from '+config['webhook']['secret_file']); - success(); - } - }); - }; - if (config['test']) - app.checkSecret = checkSecret; - /** * Render the page with the view engine and catch errors * @param res @@ -164,6 +124,20 @@ module.exports = (config) => { } }); + //webhook endpoint + app.post(config['webhook']['endpoint'], (req, res) => { + if (config['modules']['webhook']) { + if (config['webhook']['secret_header'] && req.get(config['webhook']['secret_header']) !== config['webhook']['secret_value']) { + res.sendStatus(403); + } else { + res.sendStatus(200); + //TODO reload + } + } else { + res.sendStatus(400); + } + }); + // catch all article urls and render them app.get('*', (req, res, next) => { if (/^\/\d{4}\/\d{2}\/\d{2}\/(\w*\/)?$/.test(req.path)) { @@ -222,10 +196,8 @@ module.exports = (config) => { // must be use in a server.js to start the server app.start = () => { reload(() => { - checkSecret(() => { - app.listen(config['node_port'], () => { - console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); - }); + app.listen(config['node_port'], () => { + console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); }); }); }; diff --git a/src/config.default.json b/src/config.default.json index 027b7d2..9e30297 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -30,7 +30,8 @@ }, "webhook": { "endpoint": "/webhook", - "secret_file": "git_secret" + "secret_value": "", + "secret_header": "" }, "showdown": { "parseImgDimensions": true, diff --git a/test/app.test.js b/test/app.test.js index 900da2a..3174ce5 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -19,6 +19,7 @@ config['article']['template'] = testTemplate; config['home']['hidden'].push('.test'); config['rss']['endpoint'] = '/rsstest'; config['rss']['length'] = 2; +config['webhook']['endpoint'] = '/webhooktest'; const app = require('../src/app')(config); @@ -74,52 +75,6 @@ describe('Test root path', () => { done(); }); }, fail); - - }); -}); - -describe('Test check secret', () => { - const secretFile = 'git_secret'; - const tmpSecretFile = 'tmp_git_secret'; - beforeEach(() => { - if (fs.existsSync(secretFile)) - fs.renameSync(secretFile, tmpSecretFile); - }); - - afterEach(() => { - if (fs.existsSync(tmpSecretFile)) { - fs.renameSync(tmpSecretFile, secretFile); - } else if (fs.existsSync(secretFile)) { - fs.unlinkSync(secretFile); //remove secret file if remaining - } - }); - - test('no check if not activated', (done, fail) => { - config['modules']['webhook'] = false; - app.checkSecret(() => { - config['modules']['webhook'] = true; - done(); - }, () => { - config['modules']['webhook'] = true; - fail(); - }); - }); - test('create if not exists', (done, fail) => { - if (fs.existsSync(secretFile)) - fs.unlinkSync(secretFile); - app.checkSecret(() => { - expect(fs.existsSync(secretFile)).toBe(true); - expect(fs.readFileSync(secretFile).length).toBeGreaterThan(0); - done(); - }, fail); - }); - test('read if exists', (done, fail) => { - fs.writeFileSync(secretFile,'secret value'); - app.checkSecret(() => { - expect(fs.existsSync(secretFile)).toBe(true); - expect(fs.readFileSync(secretFile, {encoding:'UTF-8'})).toBe('secret value'); - done(); - }, fail); }); }); @@ -180,6 +135,41 @@ describe('Test RSS feed', () => { }); }); +describe('Test webhook', () => { + test('400 webhook deactivated', (done) => { + config['modules']['webhook'] = false; + request(app).post('/webhooktest').then((response) => { + expect(response.statusCode).toBe(400); + config['modules']['webhook'] = true; + done(); + }); + }); + test('200 no secret', (done) => { + request(app).post('/webhooktest').then((response) => { + expect(response.statusCode).toBe(200); + //TODO test reload + done(); + }); + }); + test('403 wrong secret', (done) => { + config['webhook']['secret_header'] = 'testheader'; + config['webhook']['secret_value'] = 'testvalue'; + request(app).post('/webhooktest').set('testheader','testvalue2').then((response) => { + expect(response.statusCode).toBe(403); + done(); + }); + }); + test('200 valid secret', (done) => { + config['webhook']['secret_header'] = 'testheader'; + config['webhook']['secret_value'] = 'testvalue'; + request(app).post('/webhooktest').set('testheader','testvalue').then((response) => { + expect(response.statusCode).toBe(200); + //TODO test reload + done(); + }); + }); +}); + describe('Test articles rendering', () => { test('404 article not found', (done) => { request(app).get('/2019/05/06/untitled/').then((response) => {