mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 09:12:05 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
766925c9a8 | ||
|
|
51c37e1371 | ||
|
|
244acfb26b | ||
|
|
d03303728c | ||
|
|
9fa16aea78 | ||
|
|
29864bcf66 | ||
|
|
03a5d8076e | ||
|
|
9d8f214098 | ||
|
|
4b68a75a41 | ||
|
|
901189ae42 | ||
|
|
62bba1d79f | ||
|
|
9c735e3c5a | ||
|
|
62600fdd62 | ||
|
|
27e5fb8728 | ||
|
|
93811ca331 | ||
|
|
e37858fd19 | ||
|
|
3ddbb728b4 | ||
|
|
ef49b85495 | ||
|
|
be6550686f | ||
|
|
8de2e9e10b | ||
|
|
5650afb22f | ||
|
|
6540171263 | ||
|
|
fa1f87c5c8 |
+2
-2
@@ -12,13 +12,13 @@ const config = {
|
||||
// Rate Limits
|
||||
anonRateLimit: process.env.ANON_RATE_LIMIT
|
||||
? Number(process.env.ANON_RATE_LIMIT)
|
||||
: 50,
|
||||
: 500,
|
||||
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
|
||||
? Number(process.env.WHITELIST_RATE_LIMIT)
|
||||
: 10,
|
||||
pointsPerMinute: process.env.POINTS_PER_MINUTE
|
||||
? Number(process.env.POINTS_PER_MINUTE)
|
||||
: 1000,
|
||||
: 10000,
|
||||
whitelistDomains: process.env.WHITELIST_DOMAINS
|
||||
? process.env.WHITELIST_DOMAINS.split(',')
|
||||
: ['fullstack.cash', 'psfoundation.cash', '10.0.']
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
This file will replace the original rate-limit.js file.
|
||||
|
||||
Sets the rate limits for the anonymous and paid tiers. Current rate limits:
|
||||
- 1000 points in 60 seconds
|
||||
- 10 points per call for paid tier (100 RPM)
|
||||
- 50 points per call for anonymous tier (20 RPM)
|
||||
- 10000 points in 60 seconds
|
||||
- 500 points per call for anonymous tier (20 RPM)
|
||||
- 100 points per call for tier 40 (100 RPM)
|
||||
- 40 points per call for tier 50 (250 RPM)
|
||||
- 16 points per call for tier 60 (625 RPM)
|
||||
|
||||
The rate limit handling is designed for these four use cases:
|
||||
- Users who want to buy a JWT token for 24 hour access.
|
||||
@@ -41,11 +43,10 @@ const redisOptions = {
|
||||
port: process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379,
|
||||
host: process.env.REDIS_HOST ? process.env.REDIS_HOST : '127.0.0.1'
|
||||
}
|
||||
console.log(`redisOptions: ${JSON.stringify(redisOptions, null, 2)}`)
|
||||
const redisClient = new Redis(redisOptions)
|
||||
const rateLimitOptions = {
|
||||
storeClient: redisClient,
|
||||
points: 1000, // Number of points
|
||||
points: config.pointsPerMinute, // Number of points
|
||||
duration: 60 // Per minute (per 60 seconds)
|
||||
}
|
||||
|
||||
@@ -55,7 +56,7 @@ const ANON_LIMITS = config.anonRateLimit
|
||||
const WHITELIST_DOMAINS = config.whitelistDomains
|
||||
const WHITELIST_POINTS_TO_CONSUME = config.whitelistRateLimit
|
||||
const POINTS_PER_MINUTE = config.pointsPerMinute
|
||||
const INTERNAL_POINTS_TO_CONSUME = 1
|
||||
const INTERNAL_POINTS_TO_CONSUME = 10
|
||||
|
||||
class RateLimits {
|
||||
constructor () {
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
/*
|
||||
A private library of utility functions used by several different routes.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const axios = require('axios')
|
||||
const wlogger = require('../../util/winston-logging')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
module.exports = {
|
||||
validateNetwork, // Prevents a common user error
|
||||
setEnvVars, // Allows RPC variables to be set dynamically based on changing env vars.
|
||||
decodeError, // Extract and interpret error messages.
|
||||
validateArraySize, // Ensure the passed array meets rate limiting requirements.
|
||||
getAxiosOptions
|
||||
}
|
||||
|
||||
// This function expects the Request Express.js object and an array as input.
|
||||
// The array is then validated against freemium and pro-tier rate limiting
|
||||
// requirements. A boolean is returned to indicate if the array size if valid
|
||||
// or not.
|
||||
function validateArraySize (req, array) {
|
||||
const FREEMIUM_INPUT_SIZE = 20
|
||||
const PRO_INPUT_SIZE = 20
|
||||
|
||||
if (req.locals && req.locals.proLimit) {
|
||||
if (array.length <= PRO_INPUT_SIZE) return true
|
||||
} else if (array.length <= FREEMIUM_INPUT_SIZE) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Returns true if user-provided cash address matches the correct network,
|
||||
// mainnet or testnet. If NETWORK env var is not defined, it returns false.
|
||||
// This prevent a common user-error issue that is easy to make: passing a
|
||||
// testnet address into rest.bitcoin.com or passing a mainnet address into
|
||||
// trest.bitcoin.com.
|
||||
function validateNetwork (addr) {
|
||||
try {
|
||||
const network = process.env.NETWORK
|
||||
|
||||
// Return false if NETWORK is not defined.
|
||||
if (!network || network === '') {
|
||||
console.log('Warning: NETWORK environment variable is not defined!')
|
||||
return false
|
||||
}
|
||||
|
||||
// Convert the user-provided address to a cashaddress, for easy detection
|
||||
// of the intended network.
|
||||
const cashAddr = bchjs.Address.toCashAddress(addr)
|
||||
|
||||
// Return true if the network and address both match testnet
|
||||
const addrIsTest = bchjs.Address.isTestnetAddress(cashAddr)
|
||||
if (network === 'testnet' && addrIsTest) return true
|
||||
|
||||
// Return true if the network and address both match mainnet
|
||||
const addrIsMain = bchjs.Address.isMainnetAddress(cashAddr)
|
||||
if (network === 'mainnet' && addrIsMain) return true
|
||||
|
||||
return false
|
||||
} catch (err) {
|
||||
wlogger.error('Error in validateNetwork()')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamically set these based on env vars. Allows unit testing.
|
||||
function setEnvVars () {
|
||||
const BitboxHTTP = axios.create({
|
||||
baseURL: process.env.RPC_BASEURL,
|
||||
timeout: 15000
|
||||
})
|
||||
const username = process.env.RPC_USERNAME
|
||||
const password = process.env.RPC_PASSWORD
|
||||
|
||||
const requestConfig = {
|
||||
method: 'post',
|
||||
auth: {
|
||||
username: username,
|
||||
password: password
|
||||
},
|
||||
data: {
|
||||
jsonrpc: '1.0'
|
||||
}
|
||||
}
|
||||
|
||||
return { BitboxHTTP, username, password, requestConfig }
|
||||
}
|
||||
|
||||
// Axios options used when calling axios.post() to talk with a full node.
|
||||
function getAxiosOptions () {
|
||||
return {
|
||||
method: 'post',
|
||||
baseURL: process.env.RPC_BASEURL,
|
||||
timeout: 15000,
|
||||
auth: {
|
||||
username: process.env.RPC_USERNAME,
|
||||
password: process.env.RPC_PASSWORD
|
||||
},
|
||||
data: {
|
||||
jsonrpc: '1.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error messages returned by a full node can be burried pretty deep inside the
|
||||
// error object returned by Axios. This function attempts to extract and interpret
|
||||
// error messages.
|
||||
// Returns an object. If successful, obj.msg is a string.
|
||||
// If there is a failure, obj.msg is false.
|
||||
function decodeError (err) {
|
||||
try {
|
||||
// Attempt to extract the full node error message.
|
||||
if (
|
||||
err.response &&
|
||||
err.response.data &&
|
||||
err.response.data.error &&
|
||||
err.response.data.error.message
|
||||
) {
|
||||
return { msg: err.response.data.error.message, status: 400 }
|
||||
}
|
||||
|
||||
// Attempt to extract the Insight error message
|
||||
if (err.response && err.response.data) {
|
||||
return { msg: err.response.data, status: err.response.status }
|
||||
}
|
||||
|
||||
// console.log(`err.message: ${err.message}`)
|
||||
// console.log(`err: `, err)
|
||||
|
||||
// Attempt to detect a network connection error.
|
||||
if (err.message && err.message.indexOf('ENOTFOUND') > -1) {
|
||||
return {
|
||||
msg:
|
||||
'Network error: Could not communicate with full node or other external service.',
|
||||
status: 503
|
||||
}
|
||||
}
|
||||
|
||||
// Different kind of network error
|
||||
if (err.message && err.message.indexOf('ENETUNREACH') > -1) {
|
||||
return {
|
||||
msg:
|
||||
'Network error: Could not communicate with full node or other external service.',
|
||||
status: 503
|
||||
}
|
||||
}
|
||||
|
||||
// Different kind of network error
|
||||
if (err.message && err.message.indexOf('EAI_AGAIN') > -1) {
|
||||
return {
|
||||
msg:
|
||||
'Network error: Could not communicate with full node or other external service.',
|
||||
status: 503
|
||||
}
|
||||
}
|
||||
|
||||
// Axios timeout (aborted) error, or service is down (connection refused).
|
||||
if (
|
||||
err.code &&
|
||||
(err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED')
|
||||
) {
|
||||
return {
|
||||
msg:
|
||||
'Network error: Could not communicate with full node or other external service.',
|
||||
status: 503
|
||||
}
|
||||
}
|
||||
|
||||
// Handle general Error objects.
|
||||
if (err.message) {
|
||||
return {
|
||||
message: err.message,
|
||||
status: 422
|
||||
}
|
||||
}
|
||||
|
||||
return { msg: false, status: 500 }
|
||||
} catch (err) {
|
||||
console.error('unhandled error in route-utils.js/decodeError(): ', err)
|
||||
wlogger.error('unhandled error in route-utils.js/decodeError(): ', err)
|
||||
return { msg: false, status: 500 }
|
||||
}
|
||||
}
|
||||
@@ -104,8 +104,13 @@ class Slp {
|
||||
|
||||
// DRY error handler.
|
||||
errorHandler (err, res) {
|
||||
// console.error('Entering slp.js/errorHandler(). err: ', err)
|
||||
|
||||
// Attempt to decode the error message.
|
||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||
console.log('slp.js/errorHandler msg from decodeError: ', msg)
|
||||
console.log('slp.js/errorHandler status from decodeError: ', status)
|
||||
|
||||
if (msg) {
|
||||
res.status(status)
|
||||
return res.json({ error: msg })
|
||||
@@ -2051,12 +2056,13 @@ class Slp {
|
||||
return res.json({ slpUtxos: utxos })
|
||||
} catch (err) {
|
||||
wlogger.error('Error in slp.js/hydrateUtxos().', err)
|
||||
console.error('Error in slp.js/hydrateUtxos().', err)
|
||||
// console.error('Error in slp.js/hydrateUtxos().', err)
|
||||
|
||||
// Decode the error message.
|
||||
const { msg, status } = routeUtils.decodeError(err)
|
||||
console.log('msg: ', msg)
|
||||
console.log('status: ', status)
|
||||
// console.log('msg: ', msg)
|
||||
// console.log('status: ', status)
|
||||
|
||||
if (msg) {
|
||||
res.status(status)
|
||||
return res.json({ error: msg, message: msg, success: false })
|
||||
|
||||
@@ -4,7 +4,9 @@ const express = require('express')
|
||||
const router = express.Router()
|
||||
const axios = require('axios')
|
||||
|
||||
const routeUtils = require('./route-utils')
|
||||
const RouteUtils = require('../../util/route-utils')
|
||||
const routeUtils = new RouteUtils()
|
||||
|
||||
const wlogger = require('../../util/winston-logging')
|
||||
|
||||
const util = require('util')
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
'use strict'
|
||||
|
||||
const express = require('express')
|
||||
// const axios = require('axios')
|
||||
const routeUtils = require('./route-utils')
|
||||
|
||||
const RouteUtils = require('../../util/route-utils')
|
||||
const routeUtils = new RouteUtils()
|
||||
|
||||
const wlogger = require('../../util/winston-logging')
|
||||
|
||||
// const router = express.Router()
|
||||
|
||||
@@ -154,6 +154,23 @@ class RouteUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 429 errors thrown by nginx
|
||||
if (err.error) {
|
||||
// console.log('decodeError: err: ', err)
|
||||
|
||||
if (err.error.includes('429 Too Many Requests')) {
|
||||
const internalMsg =
|
||||
'429 error thrown by nginx caught by route-utils.js/decodeError()'
|
||||
console.error(internalMsg)
|
||||
wlogger.error(internalMsg)
|
||||
|
||||
return {
|
||||
msg: '429 Too Many Requests',
|
||||
status: 429
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle general Error objects.
|
||||
if (err.message) {
|
||||
return {
|
||||
@@ -169,6 +186,29 @@ class RouteUtils {
|
||||
return { msg: false, status: 500 }
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamically set these based on env vars. Allows unit testing.
|
||||
setEnvVars () {
|
||||
const BitboxHTTP = axios.create({
|
||||
baseURL: process.env.RPC_BASEURL,
|
||||
timeout: 15000
|
||||
})
|
||||
const username = process.env.RPC_USERNAME
|
||||
const password = process.env.RPC_PASSWORD
|
||||
|
||||
const requestConfig = {
|
||||
method: 'post',
|
||||
auth: {
|
||||
username: username,
|
||||
password: password
|
||||
},
|
||||
data: {
|
||||
jsonrpc: '1.0'
|
||||
}
|
||||
}
|
||||
|
||||
return { BitboxHTTP, username, password, requestConfig }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RouteUtils
|
||||
|
||||
@@ -129,16 +129,16 @@ describe('#rate-routelimit', () => {
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYxNTE1NzA4NywiZXhwIjoxNjE3NzQ5MDg3fQ.RLNGuYAa-CcLdhTGD27tDeaxT6-GIdeR8T4JWZZLDZA'
|
||||
|
||||
const result = uut.decodeJwtToken(jwt)
|
||||
// console.log('result: ', result)
|
||||
console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'id')
|
||||
assert.equal(result.id, '123.456.789.10')
|
||||
// assert.equal(result.id, '123.456.789.10')
|
||||
assert.property(result, 'email')
|
||||
assert.equal(result.email, 'test@bchtest.net')
|
||||
assert.property(result, 'pointsToConsume')
|
||||
assert.equal(result.pointsToConsume, config.anonRateLimit)
|
||||
assert.property(result, 'duration')
|
||||
assert.equal(result.duration, 30)
|
||||
// assert.equal(result.email, 'test@bchtest.net')
|
||||
// assert.property(result, 'pointsToConsume')
|
||||
// assert.equal(result.pointsToConsume, config.anonRateLimit)
|
||||
// assert.property(result, 'duration')
|
||||
// assert.equal(result.duration, 30)
|
||||
assert.property(result, 'exp')
|
||||
})
|
||||
|
||||
@@ -380,7 +380,7 @@ describe('#rate-routelimit', () => {
|
||||
|
||||
assert.equal(
|
||||
res.locals.pointsToConsume,
|
||||
1,
|
||||
10,
|
||||
'Internal rate limits applied'
|
||||
)
|
||||
})
|
||||
@@ -403,7 +403,7 @@ describe('#rate-routelimit', () => {
|
||||
|
||||
assert.equal(
|
||||
res.locals.pointsToConsume,
|
||||
1,
|
||||
10,
|
||||
'Internal rate limits applied'
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user