server/routes/api.js
import crypto from 'crypto'
import url from 'url'
import qs from 'qs'
import nconf from '../nconf'
import { githubService } from '../service/GithubService'
import { requireAuth } from './auth'
import { hookHandler } from '../handler/HookHandler'
import { checkRunner } from '../checks/CheckRunner'
import { checkHandler } from '../handler/CheckHandler'
import { repositoryHandler } from '../handler/RepositoryHandler'
import ZapprConfiguration from '../zapprfile/Configuration'
import { getCheckByType } from '../checks'
import { logger } from '../../common/debug'
const error = logger('api', 'error')
const warn = logger('api', 'warn')
const info = logger('api', 'info')
const NODE_ENV = nconf.get('NODE_ENV')
const PROD_ENV = 'production'
const GITHUB_HOOK_SECRET = nconf.get('GITHUB_HOOK_SECRET')
const GITHUB_SIGNATURE_HEADER = 'x-hub-signature'
const GITHUB_EVENT_HEADER = 'x-github-event'
function validateIsCalledFromGithub(ctx, next) {
const {header, body} = ctx.request
const actualSignature = header[GITHUB_SIGNATURE_HEADER]
// not require signature header for backwards compatibility
if (!actualSignature) {
warn(`Request from host ${header.host} is missing X-Hub-Signature header!`)
return next()
}
const sha1 = crypto.createHmac('sha1', GITHUB_HOOK_SECRET)
// use buffer otherwise unicde (emojis! 💩) break the hash
const hmac = sha1.update(new Buffer(JSON.stringify(body))).digest('hex')
const expectedSignature = `sha1=${hmac}`
if (actualSignature !== expectedSignature) {
error(`Hook for ${body.repository.full_name} called with invalid signature "${actualSignature}"`
+ `(expected: "${expectedSignature}") from ${header.host}!`)
ctx.throw(400)
}
return next()
}
/**
* Environment variables endpoint.
*/
export function env(router) {
return router.get('/api/env', requireAuth, ctx => {
ctx.body = {
'NODE_ENV': nconf.get('NODE_ENV')
}
})
}
/**
* Repository collection.
*/
export function repos(router) {
return router.get('/api/repos', requireAuth, async(ctx) => {
try {
const user = ctx.req.user
const all = ctx.request.query.all == 'true'
const repos = await repositoryHandler.onGetAll(user, all)
ctx.body = repos.map(repo => repo.toJSON())
} catch (e) {
ctx.throw(503, e)
}
})
}
/**
* Single repository.
*/
export function repo(router) {
return router
.get('/api/repos/:id', requireAuth, async(ctx) => {
try {
const query = qs.parse(url.parse(ctx.req.url).query)
const user = ctx.req.user
const id = parseInt(ctx.params.id)
const autoSync = query.autoSync === 'true'
const repo = await repositoryHandler.onGetOne(id, user, false, autoSync)
ctx.body = repo
} catch (e) {
ctx.throw(404, e)
}
})
.post('/api/repos/:id/refreshTokens', requireAuth, async(ctx) => {
try{
const user = ctx.req.user;
const id = parseInt(ctx.params.id);
const updatedTokens = await checkHandler.onRefreshTokens(id, user);
info('updated Checks with new access token for repository: ', id);
ctx.response.status = 200;
ctx.body = {
message: `${updatedTokens[0]} checks were updated`,
};
} catch(e) {
ctx.throw(404, e)
}
})
.get('/api/repos/:id/zapprfile', requireAuth, async(ctx) => {
const user = ctx.req.user
const id = parseInt(ctx.params.id, 10)
let repo
try {
repo = await repositoryHandler.onGetOne(id, user, true)
} catch (e) {
ctx.throw(404, e)
}
const zapprFileContent = await githubService.readZapprFile(repo.json.owner.login, repo.json.name, user.accessToken)
const config = new ZapprConfiguration(zapprFileContent, repo)
const message = zapprFileContent === '' ?
'No Zapprfile found, using default config' :
(config.isValid() ?
'' :
config.getParseError())
ctx.body = {
config: config.getConfiguration(),
message,
valid: config.isValid()
}
ctx.response.status = 200
})
.put('/api/repos/:id/:type', requireAuth, async(ctx) => {
try {
const user = ctx.req.user
const id = parseInt(ctx.params.id)
const type = ctx.params.type
const repo = await repositoryHandler.onGetOne(id, user, true)
const token = user.accessToken
const owner = repo.json.owner.login
const name = repo.json.name
const defaultBranch = repo.json.default_branch
const zapprFile = await githubService.readZapprFile(owner, name, token)
if (!repo.welcomed) {
try {
if (zapprFile.length === 0) {
await githubService.proposeZapprfile(owner, name, defaultBranch, token)
info(`${owner}/${name}: Welcome to Zappr.`)
} else {
info(`${owner}/${name}: Welcome to Zappr (no PR needed).`)
}
await repositoryHandler.onWelcome(id)
} catch (e) {
error(`${owner}/${name}: Could not welcome. ${e.message}`)
}
}
const check = await checkHandler.onEnableCheck(user, repo, type)
const checkContext = getCheckByType(type).CONTEXT
if (checkContext) {
// autobranch doesn't have a context
const config = new ZapprConfiguration(zapprFile, repo)
if (NODE_ENV !== PROD_ENV) {
// blocking in dev and tests
await githubService.protectBranch(owner, name, defaultBranch, checkContext, token)
await checkRunner.handleExistingPullRequests(repo, getCheckByType(type).TYPE, {config: config.getConfiguration(), token})
} else {
// not blocking in production
githubService.protectBranch(owner, name, defaultBranch, checkContext, token)
checkRunner.handleExistingPullRequests(repo, getCheckByType(type).TYPE, {config: config.getConfiguration(), token})
}
}
ctx.response.status = 201
ctx.body = check.toJSON()
} catch (e) {
ctx.throw(503, e)
}
})
.delete('/api/repos/:id/:type', requireAuth, async(ctx) => {
try {
const user = ctx.req.user
const id = parseInt(ctx.params.id)
const repo = await repositoryHandler.onGetOne(id, user)
const type = ctx.params.type
const checkContext = getCheckByType(type).CONTEXT
// check first if user has permissions
if (checkContext) {
if (NODE_ENV !== PROD_ENV) {
try {
// block when not in prod
// => so we can test the API calls
await checkRunner.release(repo, type, user.accessToken)
await githubService.removeRequiredStatusCheck(repo.json.owner.login, repo.json.name, repo.json.default_branch, checkContext, user.accessToken)
} catch (e) {
ctx.throw(503, e)
error(`${repo.json.full_name}: Could not remove status check. ${e.detail}`)
}
} else {
// BugFix machinery/zappr-deploy/issues/14 (Zalando internal reference)
// https://github.com/zalando/zappr/pull/560/
// not block when in prod
try {
checkRunner.release(repo, type, user.accessToken)
.catch(e => error(`${repo.json.full_name} [${type}]: Could not release pull requests. ${e.message}`))
await githubService.removeRequiredStatusCheck(repo.json.owner.login, repo.json.name, repo.json.default_branch, checkContext, user.accessToken)
} catch (e) {
ctx.throw(503, e)
error(`${repo.json.full_name}: Could not remove status check. ${e.detail}`)
}
}
}
await checkHandler.onDisableCheck(user, repo, type)
ctx.response.status = 204
ctx.body = null
} catch (e) {
ctx.throw(503, e)
}
})
.post('/api/hook', validateIsCalledFromGithub, async(ctx) => {
const {header, body} = ctx.request
const event = header[GITHUB_EVENT_HEADER]
const hookResult = await hookHandler.onHandleHook(event, body)
ctx.response.status = 200
ctx.body = hookResult
})
}