/ index.js
index.js
  1  /*
  2  * Bot that receives a POST request (from a GitHub issue comment webhook),
  3  * finds the address of the bounty contract and funds it automatically
  4  * with either ETH or any ERC20/223 token.
  5  * REVIEW hardcoded string length.
  6  * Depends on openbounty version as of 2018-03-20.
  7  */
  8  
  9  const config = require('./config')
 10  const bot = require('./bot')
 11  const crypto = require('crypto')
 12  const lru = require('lru-cache')
 13  const previouslyFundedContracts = lru(10)
 14  
 15  const express = require('express')
 16  const cors = require('cors')
 17  const helmet = require('helmet')
 18  const app = express()
 19  const bodyParser = require('body-parser')
 20  const jsonParser = bodyParser.json()
 21  
 22  app.use(cors())
 23  app.use(helmet())
 24  
 25  // Receive a POST request at the url specified by an env. var.
 26  app.post(`${config.urlEndpoint}`, jsonParser, function (req, res, next) {
 27    bot.info(`Handling ${req.body.issue.url}`)
 28  
 29    if (!req.body || !req.body.action) {
 30      return res.sendStatus(400)
 31    } else if (!bot.needsFunding(req)) {
 32      return res.sendStatus(204)
 33    }
 34  
 35    const validation = validateRequest(req)
 36  
 37    if (validation.correct) {
 38      setTimeout(async () => {
 39        try {
 40          if (await processRequest(req)) {
 41            bot.info(`Issue well funded: ${req.body.issue.url}`)
 42          }
 43        } catch (err) {
 44          bot.error(`Error processing request: ${req.body.issue.url}`)
 45          bot.error(err)
 46          bot.error(`Dump: ${JSON.stringify(req.body)}`)
 47        }
 48      }, config.delayInMiliSeconds)
 49    } else {
 50      bot.error(`Error validating issue: ${req.body.issue.url}`)
 51      bot.error(`Error: ${validation.error}`)
 52    }
 53    return res.sendStatus(200)
 54  })
 55  
 56  function validateRequest (req) {
 57    const validation = { correct: false, error: '' }
 58    const webhookSecret = process.env.WEBHOOK_SECRET
 59  
 60    if (!webhookSecret) {
 61      validation.error = 'Github Webhook Secret key not found. ' +
 62                         'Please set env variable WEBHOOK_SECRET to github\'s webhook secret value'
 63    } else {
 64      const blob = JSON.stringify(req.body)
 65      const hmac = crypto.createHmac('sha1', webhookSecret)
 66      const ourSignature = `sha1=${hmac.update(blob).digest('hex')}`
 67  
 68      const theirSignature = req.get('X-Hub-Signature')
 69  
 70      const bufferA = Buffer.from(ourSignature, 'utf8')
 71      const bufferB = Buffer.from(theirSignature, 'utf8')
 72  
 73      const safe = crypto.timingSafeEqual(bufferA, bufferB)
 74  
 75      if (safe) {
 76        validation.correct = true
 77      } else {
 78        validation.error = 'Invalid signature. Check that WEBHOOK_SECRET ' +
 79                           'env variable matches github\'s webhook secret value'
 80      }
 81    }
 82  
 83    return validation
 84  }
 85  
 86  async function processRequest (req) {
 87    const to = bot.getAddress(req)
 88  
 89    const previousHash = previouslyFundedContracts.get(to)
 90    if (previousHash) {
 91      bot.info(`Issue has been funded before (tx hash=${previousHash}), ignoring`)
 92      return null
 93    }
 94  
 95    const amount = await bot.getAmount(req)
 96    const gasPrice = await bot.getGasPrice()
 97    const transaction = await bot.sendTransaction(to, amount, gasPrice)
 98  
 99    previouslyFundedContracts.set(to, transaction.hash)
100  
101    bot.logTransaction(transaction)
102  
103    return transaction
104  }
105  
106  const port = process.env.PORT || 8181
107  app.listen(port, function () {
108    bot.info(`Autobounty listening on port ${port}`)
109  })