Skip to content

Commit

Permalink
Creator Node Content Blacklist (#885)
Browse files Browse the repository at this point in the history
* refactor dp reselect logic (WIP)

* update dp selection logic

* Add comments and console logs

* add unit test + update retry count to 5

* update console log and recursive return value

* update log message and test description

* fix styling

* update comment

* wIP content blacklist

* more wip content blacklist

* update content blacklist

* fix ci

* remove logs

* move ContentBlacklist db logic to BlacklistManager

* add contentBlacklist.test.js + refactor contentBlacklist.js

* refactor content blacklist + add tests + add update-content-blacklist.sh

* update ContentBlacklist tests

* seprating content blacklist logic to controller and components

* add /track_content blacklist test

* update delegate keys

* update README and content blacklist script

* remove commonEnv logic

* removing commonEnv#.sh files

* fix closed connection error + clear Blacklist after test

* fix README

* add test for bad private key + refactor
  • Loading branch information
vicky-g committed Oct 8, 2020
1 parent 875e2c9 commit d516122
Show file tree
Hide file tree
Showing 21 changed files with 1,273 additions and 106 deletions.
8 changes: 8 additions & 0 deletions creator-node/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
An Audius Creator Node maintains the availability of creators' content on IPFS. The information stored includes audius user metadata, images, and audio content. The content is backed by either aws S3 or a local directory.

To blacklist content on an already running node:
1. export the keys `delegatePrivateKey` and `creatorNodeEndpoint` in your terminal
```
$ export delegatePrivateKey='your_private_key'
$ export creatorNodeEndpoint='http://the_creator_node_endpoint'
```
2. Run the script `scripts/update-content-blacklist.sh`. Refer to the script for usage!

For detailed instructions on how to develop on the service, please see [The Wiki](https://github.com/AudiusProject/audius-protocol/wiki/Creator-Node-%E2%80%90-How-to-run).
129 changes: 129 additions & 0 deletions creator-node/scripts/updateContentBlacklist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const axios = require('axios')

const models = require('../src/models')
const { generateTimestampAndSignature } = require('../src/apiHelpers')

const PRIVATE_KEY = process.env.delegatePrivateKey
const CREATOR_NODE_ENDPOINT = process.env.creatorNodeEndpoint

// Available action types
const ACTION_ARR = ['ADD', 'DELETE']
const ACTION_SET = new Set(ACTION_ARR)
const TYPES_ARR = models.ContentBlacklist.Types
const TYPES_SET = new Set(TYPES_ARR)

// Script usage:
// node updateContentBlacklist.js <action> <type> <id>
// node updateContentBlacklist.js add user 1
// node updateContentBlacklist.js delete track 4

/**
* Process command line args and either add or delete an entry in/to ContentBlacklist table
*/
async function run () {
let args
try {
args = parseEnvVarsAndArgs()
} catch (e) {
console.error(`\nIncorrect script usage: ${e.message}`)
console.error(`action: [${ACTION_ARR.toString()}]\ntype: [${TYPES_ARR.toString()}]\nid: [integer of 0 or greater]`)
console.error(`Script usage: node addToContentBlacklist.js <action> <type> <id>`)
return
}

const { action, type, id } = args
// Add or remove type and id entry to ContentBlacklist
try {
switch (action) {
case 'ADD': {
await addToContentBlacklist(type, id)
break
}
case 'DELETE': {
await removeFromContentBlacklist(type, id)
break
}
}

console.log(`Successfully performed [${action}] for type [${type}] and id [${id}]`)
} catch (e) {
console.error(e)
}
}

/**
* Parses the environment variables and command line args
*/
function parseEnvVarsAndArgs () {
if (!CREATOR_NODE_ENDPOINT || !PRIVATE_KEY) {
let errorMsg = `Creator node endpoint [${CREATOR_NODE_ENDPOINT}] or private key [${PRIVATE_KEY}] have not been exported. `
errorMsg += "Please export environment variables 'delegatePrivateKey' and 'creatorNodeEndpoint'."
throw new Error(errorMsg)
}

const args = process.argv.slice(2)
if (args.length !== 3) {
throw new Error('Incorrect number of args provided.')
}

const action = args[0].toUpperCase()
const type = args[1].toUpperCase()
let id = args[2]
if (!ACTION_SET.has(action) || !TYPES_SET.has(type) || isNaN(id)) {
throw new Error(`Improper action (${action}), type (${type}), or id (${id}).`)
}

id = parseInt(id)

return { action, id, type }
}

/**
* 1. Signs the object {type, id, timestamp} when sorted and stringified
* 2. Sends axios request to add entry to content blacklist of type and id
* @param {string} type
* @param {int} id
*/
async function addToContentBlacklist (type, id) {
const { timestamp, signature } = generateTimestampAndSignature({ type, id }, PRIVATE_KEY)

let resp
try {
resp = await axios({
url: `${CREATOR_NODE_ENDPOINT}/blacklist/add`,
method: 'post',
params: { type, id, timestamp, signature },
responseType: 'json'
})
} catch (e) {
throw new Error(`Error with adding type [${type}] and id [${id}] to ContentBlacklist: ${e}`)
}

return resp.data
}

/**
* 1. Signs the data with PRIVATE_KEY specified in this script
* 2. Sends axios request to remove entry from content blacklist of type and id
* @param {string} type
* @param {int} id
*/
async function removeFromContentBlacklist (type, id) {
const { timestamp, signature } = generateTimestampAndSignature({ type, id }, PRIVATE_KEY)

let resp
try {
resp = await axios({
url: `${CREATOR_NODE_ENDPOINT}/blacklist/delete`,
method: 'post',
params: { type, id, timestamp, signature },
responseType: 'json'
})
} catch (e) {
throw new Error(`Error with removing type [${type}] and id [${id}] from ContentBlacklist: ${e}`)
}

return resp.data
}

run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict'

module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('ContentBlacklists', {
id: {
allowNull: false,
type: Sequelize.INTEGER,
primaryKey: true
},
type: {
allowNull: false,
type: Sequelize.ENUM('USER', 'TRACK'),
primaryKey: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
})
},

down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('ContentBlacklists')
}
}
41 changes: 24 additions & 17 deletions creator-node/src/apiHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,39 +61,45 @@ const isValidResponse = module.exports.isValidResponse = (resp) => {
}

module.exports.successResponse = (obj = {}) => {
// generate timestamp
const timestamp = new Date().toISOString()

// format data to sign
let toSign = {
const toSignData = {
data: {
...obj
},
// TODO: remove duplication of obj -- kept for backwards compatibility
...obj,
signer: config.get('delegateOwnerWallet'),
...versionInfo,
timestamp
...versionInfo
}

const toSignStr = JSON.stringify(sortKeys(toSign))

// hash data
const toSignHash = web3.utils.keccak256(toSignStr)

// generate signature with hashed data and private key
const signedResponse = web3.eth.accounts.sign(toSignHash, config.get('delegatePrivateKey'))

const responseWithSignature = { ...toSign, signature: signedResponse.signature }
const { timestamp, signature } = generateTimestampAndSignature(toSignData, config.get('delegatePrivateKey'))

return {
statusCode: 200,
object: {
...responseWithSignature
...toSignData,
timestamp,
signature
}
}
}

/**
* Generate the timestamp and signature for api signing
* @param {object} data
* @param {string} privateKey
*/
const generateTimestampAndSignature = (data, privateKey) => {
const timestamp = new Date().toISOString()
const toSignObj = { ...data, timestamp }
// JSON stringify automatically removes white space given 1 param
const toSignStr = JSON.stringify(sortKeys(toSignObj))
const toSignHash = web3.utils.keccak256(toSignStr)
const signedResponse = web3.eth.accounts.sign(toSignHash, privateKey)

return { timestamp, signature: signedResponse.signature }
}
module.exports.generateTimestampAndSignature = generateTimestampAndSignature

/**
* Recover the public wallet address
* @param {*} data obj with structure {...data, timestamp}
Expand All @@ -107,6 +113,7 @@ const recoverWallet = (data, signature) => {

return recoveredWallet
}
module.exports.recoverWallet = recoverWallet

const sortKeys = x => {
if (typeof x !== 'object' || !x) { return x }
Expand Down
2 changes: 2 additions & 0 deletions creator-node/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { readOnlyMiddleware } = require('./middlewares/readOnly/readOnlyMiddlewar
const { userReqLimiter, trackReqLimiter, audiusUserReqLimiter, metadataReqLimiter, imageReqLimiter } = require('./reqLimiter')
const config = require('./config')
const healthCheckRoutes = require('./components/healthCheck/healthCheckController')
const contentBlacklistRoutes = require('./components/contentBlacklist/contentBlacklistController')

const app = express()
// middleware functions will be run in order they are added to the app below
Expand All @@ -29,6 +30,7 @@ app.use('/image_upload', imageReqLimiter)
// import routes
require('./routes')(app)
app.use('/', healthCheckRoutes)
app.use('/', contentBlacklistRoutes)

function errorHandler (err, req, res, next) {
req.logger.error('Internal server error')
Expand Down

0 comments on commit d516122

Please sign in to comment.