Skip to content

Commit

Permalink
add: session uid ratelimit
Browse files Browse the repository at this point in the history
  • Loading branch information
samparsky committed Apr 29, 2019
1 parent 58d4a6a commit a5fc577
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 18 deletions.
3 changes: 2 additions & 1 deletion cfg/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ module.exports = {
FETCH_TIMEOUT: 5000,
LIST_TIMEOUT: 5000,
VALIDATOR_TICK_TIMEOUT: 5000,
EVENTS_RATE_LIMIT: { type: 'ip', timeframe: 20000 },
IP_RATE_LIMIT: { type: 'ip', timeframe: 20000 },
SID_RATE_LIMIT: { type: 'sid', timeframe: 20000 },
ETHEREUM_CORE_ADDR: '0x333420fc6a897356e69b62417cd17ff012177d2b',
ETHEREUM_NETWORK: 'goerli'
}
2 changes: 1 addition & 1 deletion cfg/prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module.exports = {
FETCH_TIMEOUT: 5000,
LIST_TIMEOUT: 5000,
VALIDATOR_TICK_TIMEOUT: 5000,
EVENTS_RATE_LIMIT: { type: 'ip', timeframe: 20000 },
IP_RATE_LIMIT: { type: 'ip', timeframe: 20000 },
CREATORS_WHITELIST: [],
MINIMAL_DEPOSIT: 0,
MINIMAL_FEE: 0,
Expand Down
6 changes: 5 additions & 1 deletion db.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const dbName = process.env.DB_MONGO_NAME || 'adexValidator'
const redisUrl = process.env.REDIS_URL || 'redis://127.0.0.1:6379'

let mongoClient = null
let redisClient = null

function connect() {
return MongoClient.connect(url, { useNewUrlParser: true }).then(function(client) {
Expand All @@ -19,7 +20,10 @@ function getMongo() {
}

function getRedis() {
return redis.createClient(redisUrl)
if (!redisClient) {
redisClient = redis.createClient(redisUrl)
}
return redisClient
}

module.exports = { connect, getMongo, getRedis }
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
"test": "node test/index.js",
"test-eth-adapter": "node test/ethereum_adapter.js",
"test-integration": "./test/run-integration-tests.sh",
"test-integration-external": "./test/run-integration-tests.sh external",
"all-tests": "./test.sh"
"test-integration-external": "./test/run-integration-tests.sh external"
},
"bin": {
"adex-sentry": "./bin/sentry.js",
Expand Down
64 changes: 53 additions & 11 deletions services/sentry/lib/access.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ async function checkAccess(channel, session, events) {
const allowRules =
eventSubmission && Array.isArray(eventSubmission.allow)
? eventSubmission.allow
: [{ uids: [channel.creator] }, { uids: null, rateLimit: cfg.EVENTS_RATE_LIMIT }]
: [
{ uids: [channel.creator] },
{ uids: null, rateLimit: cfg.IP_RATE_LIMIT },
{ uids: null, rateLimit: cfg.SID_RATE_LIMIT }
]
// first, find an applicable access rule
const rule = allowRules.find(r => {
const rules = allowRules.filter(r => {
// uid === null means it applies to all UIDs
if (r.uids === null) return true
if (Array.isArray(r.uids)) {
Expand All @@ -42,17 +46,55 @@ async function checkAccess(channel, session, events) {
}
return false
})
if (rule && rule.rateLimit && rule.rateLimit.type === 'ip') {
if (events.length !== 1)
return { success: false, statusCode: 429, message: 'rateLimit: only allows 1 event' }
const key = `adexRateLimit:${channel.id}:${session.ip}`
if (await redisExists(key))
return { success: false, statusCode: 429, message: 'rateLimit: too many requests' }
const seconds = Math.ceil(rule.rateLimit.timeframe / 1000)
await redisSetex(key, seconds, '1')

let response = { success: true }

for (let i = 0; i < rules.length; i += 1) {
const rule = rules[i]
const type = rule.rateLimit && rule.rateLimit.type
const ourUid = session.uid || null
let key

// check if uid is allowed to submit whatever it likes
if (rule.uids && rule.uids.length > 0 && rule.uids.includes(ourUid)) break

// ip rateLimit
if (rule && rule.rateLimit && type === 'ip') {
if (events.length !== 1) {
response = { success: false, statusCode: 429, message: 'rateLimit: only allows 1 event' }
break
}
key = `adexRateLimit:${channel.id}:${session.ip}`
}

// session uid ratelimit
if (rule && rule.rateLimit && type === 'sid') {
// if unauthenticated reject request
if (!session.uid) {
response = {
success: false,
statusCode: 401,
message: 'rateLimit: unauthenticated request'
}
break
}
// if authenticated then use ratelimit
key = `adexRateLimit:${channel.id}:${session.uid}`
}

if (key) {
// eslint-disable-next-line no-await-in-loop
if (await redisExists(key)) {
response = { success: false, statusCode: 429, message: 'rateLimit: too many requests' }
break
}
const seconds = Math.ceil(rule.rateLimit.timeframe / 1000)
// eslint-disable-next-line no-await-in-loop
await redisSetex(key, seconds, '1')
}
}

return { success: true }
return response
}

module.exports = checkAccess
3 changes: 2 additions & 1 deletion test/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,5 +350,6 @@ module.exports = {
},
'channel validator fee is less than MINIMAL_FEE'
]
]
],
checkAccess: [[{}]]
}
54 changes: 53 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
#!/usr/bin/env node
const tape = require('tape')
const tape = require('tape-catch')

const BN = require('bn.js')
const { Joi } = require('celebrate')
const { isValidTransition, isHealthy } = require('../services/validatorWorker/lib/followerRules')
const { mergeAggrs } = require('../services/validatorWorker/lib/mergeAggrs')
const checkAccess = require('../services/sentry/lib/access')

const { getBalancesAfterFeesTree } = require('../services/validatorWorker/lib/fees')
const { getStateRootHash, toBNMap, toBNStringMap } = require('../services/validatorWorker/lib')
const schema = require('../routes/schemas')
const { Adapter } = require('../adapters/dummy')
const fixtures = require('./fixtures')
const dummyVals = require('./prep-db/mongo')
const { genEvents } = require('./lib')
const db = require('../db')

const dummyAdapter = new Adapter({ dummyIdentity: dummyVals.ids.leader }, {})
const dummyChannel = { depositAmount: new BN(100) }
Expand Down Expand Up @@ -299,3 +303,51 @@ tape('validator message schema', function(t) {

// @TODO: event aggregator
// @TODO: producer, possibly leader/follower; mergePayableIntoBalances

tape('check access: session uid rateLimit', async function(t) {
const channel = {
creator: 'creator',
spec: {
eventSubmission: {
allow: [{ uids: null, rateLimit: { type: 'sid', timeframe: 20000 } }]
}
}
}

const events = genEvents(2, 'working')
const response = await checkAccess(channel, { uid: 'response' }, events)
t.equal(response.success, true, 'should process request')

const tooManyRequest = await checkAccess(channel, { uid: 'response' }, events)

t.equal(tooManyRequest.success, false, 'should not process request')
t.equal(tooManyRequest.statusCode, 429, 'should have too many requests status code')

t.end()
})

tape('check access: ip rateLimit', async function(t) {
const channel = {
creator: 'creator',
spec: {
eventSubmission: {
allow: [{ uids: null, rateLimit: { type: 'ip', timeframe: 20000 } }]
}
}
}

const events = genEvents(2, 'working')
const allowOnlyOneEvent = await checkAccess(channel, {}, events)
t.equal(allowOnlyOneEvent.success, false, 'should not process request')
t.equal(allowOnlyOneEvent.statusCode, 429, 'shoudl ahbe err')
t.equal(allowOnlyOneEvent.message, 'rateLimit: only allows 1 event', 'invalid error message')

const response = await checkAccess(channel, {}, [events[0]])
t.equal(response.success, true, 'should process request')

t.end()
})

// redis connection preventing test from closing
// hence the quit
tape.onFinish(() => db.getRedis().quit())

0 comments on commit a5fc577

Please sign in to comment.