mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-21 16:52:04 -07:00
Synced with master
This commit is contained in:
@@ -9,6 +9,10 @@ sudo: required
|
||||
|
||||
services:
|
||||
- docker
|
||||
- redis-server
|
||||
|
||||
#before_install:
|
||||
# - ./install-redis.sh
|
||||
|
||||
script: "npm run test"
|
||||
|
||||
|
||||
@@ -10,5 +10,6 @@ export DATA="{\"ref\":\"$DEPLOY_SECRET\"}"
|
||||
#echo $DATA
|
||||
|
||||
curl -X POST http://fullstack.cash:9000/hooks/bch-api-mainnet -H "Content-Type: application/json" -d $DATA
|
||||
curl -X POST http://fullstack.cash:9000/hooks/bch-api-testnet -H "Content-Type: application/json" -d $DATA
|
||||
|
||||
echo "...Finished deploying to production."
|
||||
|
||||
@@ -11,6 +11,9 @@ USER safeuser
|
||||
# Prep 'sudo' commands.
|
||||
#RUN echo 'abcd8765' | sudo -S pwd
|
||||
|
||||
# Install Redis. TODO: Make this a separate container.
|
||||
|
||||
|
||||
# Clone the repository
|
||||
WORKDIR /home/safeuser
|
||||
RUN git clone https://github.com/christroutner/bch-api
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
bch-api:
|
||||
bch-api-main:
|
||||
build: .
|
||||
dockerfile: Dockerfile
|
||||
#image: bitcore
|
||||
container_name: bch-api-main
|
||||
#links:
|
||||
# - mongodb
|
||||
links:
|
||||
- redis-main
|
||||
ports:
|
||||
- "12400:3000"
|
||||
restart: always
|
||||
|
||||
redis-main:
|
||||
image: redis:latest
|
||||
container_name: redis-main
|
||||
restart: always
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
@@ -17,4 +17,8 @@ export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
|
||||
export JWT_AUTH_SERVER=http://localhost:5001/
|
||||
|
||||
# Redis DB
|
||||
export REDIS_PORT=6379
|
||||
export REDIS_HOST=172.17.0.1
|
||||
|
||||
npm start
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
bch-api:
|
||||
bch-api-test:
|
||||
build: .
|
||||
dockerfile: Dockerfile
|
||||
#image: bitcore
|
||||
container_name: bch-api
|
||||
#links:
|
||||
# - mongodb
|
||||
container_name: bch-api-test
|
||||
links:
|
||||
- redis-test
|
||||
ports:
|
||||
- "13400:3000"
|
||||
restart: always
|
||||
|
||||
redis-test:
|
||||
image: redis:latest
|
||||
container_name: redis-test
|
||||
restart: always
|
||||
ports:
|
||||
- "6380:6379"
|
||||
|
||||
@@ -17,4 +17,8 @@ export BLOCKBOOK_URL=https://172.17.0.1:19131/
|
||||
# Allow node.js to make network calls to https using self-signed certificate.
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
|
||||
# Redis DB
|
||||
export REDIS_PORT=6380
|
||||
export REDIS_HOST=172.17.0.1
|
||||
|
||||
npm start
|
||||
|
||||
Generated
+185
-4
@@ -731,11 +731,27 @@
|
||||
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/bn.js": {
|
||||
"version": "4.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
|
||||
"integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/color-name": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
|
||||
},
|
||||
"@types/elliptic": {
|
||||
"version": "6.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.12.tgz",
|
||||
"integrity": "sha512-gP1KsqoouLJGH6IJa28x7PXb3cRqh83X8HCLezd2dF+XcAIMKYv53KV+9Zn6QA561E120uOqZBQ+Jy/cl+fviw==",
|
||||
"requires": {
|
||||
"@types/bn.js": "*"
|
||||
}
|
||||
},
|
||||
"@types/events": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
||||
@@ -1158,6 +1174,17 @@
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"asn1.js": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.3.0.tgz",
|
||||
"integrity": "sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA==",
|
||||
"requires": {
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"assert": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz",
|
||||
@@ -1870,6 +1897,11 @@
|
||||
"ieee754": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
|
||||
},
|
||||
"buffer-equals": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz",
|
||||
@@ -2256,6 +2288,11 @@
|
||||
"wrap-ansi": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"cluster-key-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -3041,6 +3078,11 @@
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"dev": true
|
||||
},
|
||||
"denque": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
|
||||
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
@@ -3231,6 +3273,14 @@
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"ecurve": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/ecurve/-/ecurve-1.0.6.tgz",
|
||||
@@ -5690,6 +5740,22 @@
|
||||
"p-is-promise": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"ioredis": {
|
||||
"version": "4.14.1",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.14.1.tgz",
|
||||
"integrity": "sha512-94W+X//GHM+1GJvDk6JPc+8qlM7Dul+9K+lg3/aHixPN7ZGkW6qlvX0DG6At9hWtH2v3B32myfZqWoANUJYGJA==",
|
||||
"requires": {
|
||||
"cluster-key-slot": "^1.1.0",
|
||||
"debug": "^4.1.1",
|
||||
"denque": "^1.1.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"redis-commands": "1.5.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0",
|
||||
"standard-as-callback": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
|
||||
@@ -6244,6 +6310,30 @@
|
||||
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
|
||||
"dev": true
|
||||
},
|
||||
"jsonwebtoken": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
|
||||
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
|
||||
"requires": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^5.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
@@ -6271,6 +6361,25 @@
|
||||
"integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==",
|
||||
"dev": true
|
||||
},
|
||||
"jwa": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||
"requires": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"requires": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"keccak": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz",
|
||||
@@ -6282,6 +6391,17 @@
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"key-encoder": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/key-encoder/-/key-encoder-2.0.3.tgz",
|
||||
"integrity": "sha512-fgBtpAGIr/Fy5/+ZLQZIPPhsZEcbSlYu/Wu96tNDFNSjSACw5lEIOFeaVdQ/iwrb8oxjlWi6wmWdH76hV6GZjg==",
|
||||
"requires": {
|
||||
"@types/elliptic": "^6.4.9",
|
||||
"asn1.js": "^5.0.1",
|
||||
"bn.js": "^4.11.8",
|
||||
"elliptic": "^6.4.1"
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
@@ -6473,12 +6593,22 @@
|
||||
"integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
|
||||
},
|
||||
"lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||
"integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
|
||||
},
|
||||
"lodash.flattendeep": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
|
||||
@@ -6490,23 +6620,46 @@
|
||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
|
||||
},
|
||||
"lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
||||
},
|
||||
"lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
||||
},
|
||||
"lodash.ismatch": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
|
||||
"integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
|
||||
},
|
||||
"lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
|
||||
"dev": true
|
||||
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
|
||||
},
|
||||
"lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=",
|
||||
"dev": true
|
||||
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
|
||||
},
|
||||
"lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
@@ -12165,6 +12318,11 @@
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||
},
|
||||
"rate-limiter-flexible": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.0.0.tgz",
|
||||
"integrity": "sha512-DjLeci3BuHWNr9LVVm+YPJ+Lrki/J9iDb9cEJFZELpa/ZBhXmfnfR+eCI5jzmsPJtpYGYhw/vyRfjjQjTzVtRg=="
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
|
||||
@@ -12332,6 +12490,24 @@
|
||||
"esprima": "~4.0.0"
|
||||
}
|
||||
},
|
||||
"redis-commands": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz",
|
||||
"integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg=="
|
||||
},
|
||||
"redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
|
||||
},
|
||||
"redis-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
|
||||
"requires": {
|
||||
"redis-errors": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"referrer-policy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz",
|
||||
@@ -13628,6 +13804,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"standard-as-callback": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz",
|
||||
"integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg=="
|
||||
},
|
||||
"standard-engine": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-8.0.1.tgz",
|
||||
|
||||
+6
-2
@@ -15,15 +15,15 @@
|
||||
"test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/blockchain.js",
|
||||
"test:integration": "mocha test/v3/integration",
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"coverage:report": "export NETWORK=testnet && nyc --reporter=html mocha test/v2/",
|
||||
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v3/",
|
||||
"docs": "./node_modules/.bin/apidoc -i src/routes/v3 -o docs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"apidoc": "^0.20.0",
|
||||
"@chris.troutner/bch-js": "^2.0.0",
|
||||
"apidoc": "^0.20.0",
|
||||
"axios": "^0.19.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"cookie-parser": "~1.4.3",
|
||||
@@ -34,6 +34,9 @@
|
||||
"express-basic-auth": "^1.1.3",
|
||||
"express-rate-limit": "^5.0.0",
|
||||
"helmet": "^3.21.2",
|
||||
"ioredis": "^4.14.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"key-encoder": "^2.0.3",
|
||||
"level": "^6.0.0",
|
||||
"mkdirp": "^1.0.0",
|
||||
"mocha": "^7.0.1",
|
||||
@@ -44,6 +47,7 @@
|
||||
"passport-http": "^0.3.0",
|
||||
"pg": "^7.11.0",
|
||||
"pg-hstore": "^2.3.2",
|
||||
"rate-limiter-flexible": "^2.0.0",
|
||||
"strftime": "^0.10.0",
|
||||
"winston": "^3.2.1",
|
||||
"winston-daily-rotate-file": "^4.0.0"
|
||||
|
||||
+12
-3
@@ -3,7 +3,9 @@
|
||||
const express = require("express")
|
||||
|
||||
// Middleware
|
||||
const { routeRateLimit } = require("./middleware/route-ratelimit")
|
||||
// const { routeRateLimit } = require("./middleware/route-ratelimit")
|
||||
const RateLimits = require("./middleware/route-ratelimit")
|
||||
const rateLimits = new RateLimits()
|
||||
|
||||
const path = require("path")
|
||||
const logger = require("morgan")
|
||||
@@ -22,7 +24,7 @@ const jwtAuth = require("./middleware/jwt-auth")
|
||||
|
||||
// v3
|
||||
const healthCheckV3 = require("./routes/v3/health-check")
|
||||
const blockchainV3 = require("./routes/v3/full-node/blockchain")
|
||||
const BlockchainV3 = require("./routes/v3/full-node/blockchain")
|
||||
const controlV3 = require("./routes/v3/full-node/control")
|
||||
const miningV3 = require("./routes/v3/full-node/mining")
|
||||
const networkV3 = require("./routes/v3/full-node/network")
|
||||
@@ -35,6 +37,9 @@ const Ninsight = require("./routes/v3/ninsight")
|
||||
|
||||
require("dotenv").config()
|
||||
|
||||
// Instantiate route libraries.
|
||||
const blockchainV3 = new BlockchainV3()
|
||||
|
||||
const app = express()
|
||||
|
||||
app.locals.env = process.env
|
||||
@@ -78,11 +83,15 @@ const v3prefix = "v3"
|
||||
app.use(`/${v3prefix}/`, jwtAuth.getTokenFromHeaders)
|
||||
|
||||
// Instantiate the authorization middleware, used to implement pro-tier rate limiting.
|
||||
// Handles Anonymous and Basic Authorization schemes used by passport.js
|
||||
const auth = new AuthMW()
|
||||
app.use(`/${v3prefix}/`, auth.mw())
|
||||
|
||||
// Rate limit on all v3 routes
|
||||
app.use(`/${v3prefix}/`, routeRateLimit) // Establish and enforce rate limits.
|
||||
// Establish and enforce rate limits.
|
||||
// app.use(`/${v3prefix}/`, rateLimits.routeRateLimit)
|
||||
app.use(`/${v3prefix}/`, rateLimits.rateLimitByResource)
|
||||
|
||||
app.use(`/${v3prefix}/` + `health-check`, healthCheckV3)
|
||||
app.use(`/${v3prefix}/` + `blockchain`, blockchainV3.router)
|
||||
app.use(`/${v3prefix}/` + `control`, controlV3.router)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/*
|
||||
CT 2/4/20 Note: This library handles anonymous and Basic auth. This library
|
||||
can be phased out with the chage to JWT tokens and the new rate-limit library.
|
||||
|
||||
Handle authorization for bypassing rate limits.
|
||||
|
||||
1) Default is 'Anonymous Authentication', which unlocks the freemimum tier by
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/*
|
||||
This is a middleware library for handling and processing JWT tokens.
|
||||
|
||||
This middleware inspects the request header for a JWT token.
|
||||
If found, will populate req.locals.jwtToken with the JWT token.
|
||||
*/
|
||||
|
||||
+454
-162
@@ -1,26 +1,39 @@
|
||||
/*
|
||||
This file controls the request-per-minute (RPM) rate limits.
|
||||
|
||||
It is assumed that this middleware is run AFTER the jwt-auth.js and auth.js
|
||||
middleware.
|
||||
|
||||
Current rate limiting rules in requests-per-minute:
|
||||
- anonymous access: 3
|
||||
- free access: 10, apiLevel = 0
|
||||
- any paid tier: 100, apiLevel > 0
|
||||
|
||||
If a person signs up for full node access but not indexer access, then the
|
||||
apiLevel will be 10. If they call an endpoint that uses an indexer, the apiLevel
|
||||
will be downgraded to 0 on-the-fly. Indexer endpoints will effectively be
|
||||
downgraded to the anonymous access tier.
|
||||
*/
|
||||
|
||||
"use strict"
|
||||
|
||||
const express = require("express")
|
||||
const RateLimit = require("express-rate-limit")
|
||||
const axios = require("axios")
|
||||
|
||||
const wlogger = require("../util/winston-logging")
|
||||
|
||||
const jwt = require("jsonwebtoken")
|
||||
const KeyEncoder = require("key-encoder").default
|
||||
const keyEncoder = new KeyEncoder("secp256k1")
|
||||
|
||||
// Redis
|
||||
const redisOptions = {
|
||||
enableOfflineQueue: false,
|
||||
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 Redis = require("ioredis")
|
||||
const redisClient = new Redis(redisOptions)
|
||||
|
||||
// Rate limiter middleware lib.
|
||||
const { RateLimiterRedis } = require("rate-limiter-flexible")
|
||||
const rateLimitOptions = {
|
||||
storeClient: redisClient,
|
||||
points: 100, // Number of points
|
||||
duration: 1 // Per second
|
||||
}
|
||||
|
||||
// This hard-coded value is temporary. It will be swapped out with an environment
|
||||
// variable when moved to production.
|
||||
const publicKey =
|
||||
"03e6c358092a459f7da9420de770eef3e16cf3c9c54a3d3d14ac2d7f0b82af4d7d"
|
||||
|
||||
// Set max requests per minute
|
||||
const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS
|
||||
? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS)
|
||||
@@ -32,169 +45,448 @@ const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS
|
||||
// Unique route mapped to its rate limit
|
||||
const uniqueRateLimits = {}
|
||||
|
||||
const routeRateLimit = async function(req, res, next) {
|
||||
// Disable rate limiting if 0 passed from RATE_LIMIT_MAX_REQUESTS
|
||||
if (maxRequests === 0) return next()
|
||||
let _this
|
||||
|
||||
// Create a res.locals object if not passed in.
|
||||
if (!req.locals) {
|
||||
req.locals = {
|
||||
// default values
|
||||
jwtToken: "",
|
||||
proLimit: false,
|
||||
apiLevel: 0
|
||||
}
|
||||
class RateLimits {
|
||||
constructor() {
|
||||
_this = this
|
||||
|
||||
this.jwt = jwt
|
||||
this.rateLimiter = new RateLimiterRedis(rateLimitOptions)
|
||||
}
|
||||
|
||||
// Warn if JWT_AUTH_SERVER env var is not set.
|
||||
const authServer = process.env.JWT_AUTH_SERVER
|
||||
if (!authServer || authServer === "") {
|
||||
console.warn(
|
||||
"JWT_AUTH_SERVER env var is not set. JWT tokens not being evaluated."
|
||||
)
|
||||
} else {
|
||||
// If a JWT token is passed in, validate it and enable pro-tier rate limits
|
||||
// if it's valid.
|
||||
if (req.locals.jwtToken) {
|
||||
// console.log(`req.locals.jwtToken: ${req.locals.jwtToken}`)
|
||||
// Used to disconnect from the Redis DB.
|
||||
// Called by unit tests so that node.js thread doesn't live forever.
|
||||
closeRedis() {
|
||||
redisClient.disconnect()
|
||||
}
|
||||
|
||||
// URL for the auth server.
|
||||
const path = `${authServer}apitoken/isvalid/${req.locals.jwtToken}`
|
||||
// CT 2/7/20: Older rate-limiting code that does not scale well.
|
||||
/*
|
||||
This function controls the tierd request-per-minute (RPM) rate limits.
|
||||
This is an older implementation that is currently not used.
|
||||
|
||||
// Ask Auth server if the JWT token is valid.
|
||||
// Get the API level for this user.
|
||||
let jwtInfo = await axios.get(path)
|
||||
jwtInfo = jwtInfo.data
|
||||
It is assumed that this middleware is run AFTER the jwt-auth.js and auth.js
|
||||
middleware.
|
||||
|
||||
Current rate limiting rules in requests-per-minute:
|
||||
- anonymous access: 3
|
||||
- free access: 10, apiLevel = 0
|
||||
- any paid tier: 100, apiLevel > 0
|
||||
|
||||
If a person signs up for full node access but not indexer access, then the
|
||||
apiLevel will be 10. If they call an endpoint that uses an indexer, the apiLevel
|
||||
will be downgraded to 0 on-the-fly. Indexer endpoints will effectively be
|
||||
downgraded to the anonymous access tier.
|
||||
*/
|
||||
async routeRateLimit(req, res, next) {
|
||||
// Disable rate limiting if 0 passed from RATE_LIMIT_MAX_REQUESTS
|
||||
if (maxRequests === 0) return next()
|
||||
|
||||
// Create a res.locals object if not passed in.
|
||||
if (!req.locals) {
|
||||
req.locals = {
|
||||
// default values
|
||||
jwtToken: "",
|
||||
proLimit: false,
|
||||
apiLevel: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Warn if JWT_AUTH_SERVER env var is not set.
|
||||
const authServer = process.env.JWT_AUTH_SERVER
|
||||
if (!authServer || authServer === "") {
|
||||
console.warn(
|
||||
"JWT_AUTH_SERVER env var is not set. JWT tokens not being evaluated."
|
||||
)
|
||||
} else {
|
||||
// If a JWT token is passed in, validate it and enable pro-tier rate limits
|
||||
// if it's valid.
|
||||
if (req.locals.jwtToken) {
|
||||
// console.log(`req.locals.jwtToken: ${req.locals.jwtToken}`)
|
||||
|
||||
// URL for the auth server.
|
||||
const path = `${authServer}apitoken/isvalid/${req.locals.jwtToken}`
|
||||
|
||||
// Ask Auth server if the JWT token is valid.
|
||||
// Get the API level for this user.
|
||||
let jwtInfo = await axios.get(path)
|
||||
jwtInfo = jwtInfo.data
|
||||
// console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`)
|
||||
|
||||
// If JWT if valid, evaluate the API level for the user.
|
||||
if (jwtInfo.isValid) {
|
||||
// Set fine-grain permissions for each user based on the JWT token.
|
||||
const userPermissions = _this.evalUserPermissioins(req, jwtInfo)
|
||||
// console.log(
|
||||
// `userPermissions: ${JSON.stringify(userPermissions, null, 2)}`
|
||||
// )
|
||||
|
||||
req.locals.proLimit = userPermissions.proLimit
|
||||
req.locals.apiLevel = userPermissions.apiLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Current route
|
||||
const rateLimitTier = req.locals.proLimit ? "PRO" : "BASIC"
|
||||
const path = req.baseUrl + req.path
|
||||
|
||||
// Create a unique string as a route identifier.
|
||||
const route =
|
||||
rateLimitTier +
|
||||
req.method +
|
||||
req.locals.apiLevel + // Generates new rate limit when user upgrades JWT token.
|
||||
path
|
||||
.split("/")
|
||||
.slice(0, 4)
|
||||
.join("/")
|
||||
//console.log(`route identifier: ${JSON.stringify(route, null, 2)}`)
|
||||
|
||||
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
|
||||
|
||||
// This boolean value is passed from the auth.js middleware.
|
||||
const proRateLimits = req.locals.proLimit
|
||||
|
||||
// console.log(`proRateLimits: ${proRateLimits}`)
|
||||
|
||||
// Pro level rate limits
|
||||
if (proRateLimits || proRateLimits === 0) {
|
||||
// TODO: replace the console.logs with calls to our logging system.
|
||||
// console.log(`applying pro-rate limits`)
|
||||
|
||||
let PRO_RPM = 10 // Default value for free tier
|
||||
if (req.locals.apiLevel > 0) PRO_RPM = 100 // RPM for paid tiers.
|
||||
|
||||
// console.log(`PRO_RPM: ${PRO_RPM}, apiLevel: ${req.locals.apiLevel}`)
|
||||
|
||||
// Create new RateLimit if none exists for this route
|
||||
if (!uniqueRateLimits[route]) {
|
||||
uniqueRateLimits[route] = new RateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute window
|
||||
delayMs: 0, // disable delaying - full speed until the max limit is reached
|
||||
max: PRO_RPM, // start blocking after this many requests per minute
|
||||
handler: function(req, res) {
|
||||
//console.log(`pro-tier rate-handler triggered.`)
|
||||
|
||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||
return res.json({
|
||||
error: `Too many requests. Limits are ${PRO_RPM} requests per minute. Increase rate limits at https://account.bchjs.cash`
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Freemium level rate limits
|
||||
} else {
|
||||
// TODO: replace the console.logs with calls to our logging system.
|
||||
// console.log(`applying freemium limits`)
|
||||
|
||||
// Create new RateLimit if none exists for this route
|
||||
if (!uniqueRateLimits[route]) {
|
||||
uniqueRateLimits[route] = new RateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute window
|
||||
delayMs: 0, // disable delaying - full speed until the max limit is reached
|
||||
max: maxRequests, // start blocking after maxRequests
|
||||
handler: function(req, res) {
|
||||
//console.log(`freemium rate-handler triggered.`)
|
||||
|
||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||
return res.json({
|
||||
error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://account.bchjs.cash`
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//console.log(`calling uniqueRateLimits() on this route: ${route}`)
|
||||
|
||||
// Call rate limit for this route
|
||||
uniqueRateLimits[route](req, res, next)
|
||||
}
|
||||
|
||||
// CT 2/7/20: I believe this is older code that is only used by routeRateLimit.
|
||||
// It will probably be removed in the future.
|
||||
// This function returns an object with proLimit and apiLevel properties.
|
||||
// It does fine-grane analysis on the data coming from the auth servers and
|
||||
// uses its output to adjust rate limits on-the-fly based on the users
|
||||
// permission level.
|
||||
evalUserPermissioins(req, authData) {
|
||||
// console.log(`authData: ${JSON.stringify(authData, null, 2)}`)
|
||||
|
||||
// Return object with default values
|
||||
const retObj = {
|
||||
proLimit: authData.isValid,
|
||||
apiLevel: authData.apiLevel
|
||||
}
|
||||
|
||||
// if apiLevel = 0 (free tier), then return the default values.
|
||||
if (retObj.apiLevel === 0) return retObj
|
||||
|
||||
const level20Routes = ["insight", "bitcore", "blockbook"]
|
||||
|
||||
const locals = req.locals
|
||||
// console.log(`locals: ${JSON.stringify(locals, null, 2)}`)
|
||||
const url = req.url
|
||||
// console.log(`url: ${JSON.stringify(url, null, 2)}`)
|
||||
|
||||
if (authData.apiLevel < 20) {
|
||||
// Loop through the routes that are not accessible to this tier.
|
||||
for (let i = 0; i < level20Routes.length; i++) {
|
||||
// If the requested route is for a higher tier,
|
||||
// revert to anonymous level permissions.
|
||||
if (url.indexOf(level20Routes[i]) > -1) {
|
||||
retObj.proLimit = false
|
||||
retObj.apiLevel = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retObj
|
||||
}
|
||||
|
||||
// This is the new rate limit function that uses the rate-limiter-flexible npm
|
||||
// library. It uses fine-grain rate limiting based on the resources being
|
||||
// consumed.
|
||||
async rateLimitByResource(req, res, next) {
|
||||
try {
|
||||
let userId
|
||||
let decoded = {}
|
||||
|
||||
// Create a res.locals object if not passed in.
|
||||
if (!req.locals) {
|
||||
req.locals = {
|
||||
// default values
|
||||
jwtToken: "",
|
||||
proLimit: false,
|
||||
apiLevel: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the JWT token if one exists.
|
||||
if (req.locals.jwtToken) {
|
||||
const jwtOptions = {
|
||||
algorithms: ["ES256"]
|
||||
}
|
||||
|
||||
const pemPublicKey = keyEncoder.encodePublic(publicKey, "raw", "pem")
|
||||
|
||||
// Validate the JWT token.
|
||||
decoded = _this.jwt.verify(
|
||||
req.locals.jwtToken,
|
||||
pemPublicKey,
|
||||
jwtOptions
|
||||
)
|
||||
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
|
||||
|
||||
userId = decoded.id
|
||||
} else {
|
||||
wlogger.debug(`No JWT token found!`)
|
||||
}
|
||||
|
||||
// Code here for the rate limiter is adapted from this example:
|
||||
// https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users
|
||||
try {
|
||||
// The resource being consumed: full node, indexer, SLPDB, etc.
|
||||
const resource = _this.getResource(req.url)
|
||||
wlogger.debug(`resource: ${resource}`)
|
||||
|
||||
let key = userId ? userId : req.ip
|
||||
|
||||
// const pointsToConsume = userId ? 1 : 30
|
||||
decoded.resource = resource
|
||||
const pointsToConsume = _this.calcPoints(decoded)
|
||||
|
||||
wlogger.info(
|
||||
`User ${key} consuming ${pointsToConsume} point for resource ${resource}.`
|
||||
)
|
||||
|
||||
// Update the key so that rate limits track both the user and the resource.
|
||||
key = `${key}-${resource}`
|
||||
|
||||
await _this.rateLimiter.consume(key, pointsToConsume)
|
||||
} catch (err) {
|
||||
// console.log(`err: `, err)
|
||||
|
||||
// Rate limited was triggered
|
||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||
return res.json({
|
||||
error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://account.bchjs.cash`
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
wlogger.error(`Error in route-ratelimit.js/newRateLimit(): `, err)
|
||||
// throw err
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// Calculates the points consumed, based on the jwt information and the route
|
||||
// requested.
|
||||
calcPoints(jwtInfo) {
|
||||
let retVal = 30 // By default, use anonymous tier.
|
||||
|
||||
try {
|
||||
// console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`)
|
||||
|
||||
// If JWT if valid, evaluate the API level for the user.
|
||||
if (jwtInfo.isValid) {
|
||||
// Set fine-grain permissions for each user based on the JWT token.
|
||||
const userPermissions = evalUserPermissioins(req, jwtInfo)
|
||||
// console.log(
|
||||
// `userPermissions: ${JSON.stringify(userPermissions, null, 2)}`
|
||||
// )
|
||||
const apiLevel = jwtInfo.apiLevel
|
||||
const resource = jwtInfo.resource
|
||||
|
||||
req.locals.proLimit = userPermissions.proLimit
|
||||
req.locals.apiLevel = userPermissions.apiLevel
|
||||
const level30Routes = ["insight", "bitcore", "blockbook"]
|
||||
const level40Routes = ["slp"]
|
||||
|
||||
wlogger.debug(`apiLevel: ${apiLevel}`)
|
||||
|
||||
// Only evaluate if user is using a JWT token.
|
||||
if (jwtInfo.id) {
|
||||
// SLP indexer routes
|
||||
if (level40Routes.includes(resource)) {
|
||||
if (apiLevel >= 40) retVal = 1
|
||||
// else if (apiLevel >= 10) retVal = 10
|
||||
else retVal = 10
|
||||
}
|
||||
|
||||
// Normal indexer routes
|
||||
else if (level30Routes.includes(resource)) {
|
||||
if (apiLevel >= 30) retVal = 1
|
||||
else retVal = 10
|
||||
}
|
||||
|
||||
// Full node tier
|
||||
else if (apiLevel >= 20) {
|
||||
retVal = 1
|
||||
}
|
||||
|
||||
// Free tier, full node only.
|
||||
else {
|
||||
retVal = 10
|
||||
}
|
||||
}
|
||||
|
||||
return retVal
|
||||
} catch (err) {
|
||||
wlogger.error(`Error in route-ratelimit.js/calcPoints()`)
|
||||
// throw err
|
||||
retVal = 30
|
||||
}
|
||||
|
||||
return retVal
|
||||
}
|
||||
|
||||
// This function parses the req.url property to identify what resource
|
||||
// the user is requesting.
|
||||
// This was created as a function so that it can be unit tested. Not sure
|
||||
// what kind of variations will be seen in production.
|
||||
getResource(url) {
|
||||
try {
|
||||
wlogger.debug(`url: ${JSON.stringify(url, null, 2)}`)
|
||||
|
||||
const splitUrl = url.split("/")
|
||||
const resource = splitUrl[1]
|
||||
|
||||
return resource
|
||||
} catch (err) {
|
||||
wlogger.error(`Error in getResource().`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Current route
|
||||
const rateLimitTier = req.locals.proLimit ? "PRO" : "BASIC"
|
||||
const path = req.baseUrl + req.path
|
||||
// This is a variation of rateLimitByResource() function. This version will
|
||||
// potentially be used by Bitcoin.com.
|
||||
// Rather than using apiLevel, the rateLimit is explicitly recorded in the
|
||||
// JWT token.
|
||||
async rateLimitSimple(req, res, next) {
|
||||
try {
|
||||
let userId
|
||||
let decoded = {}
|
||||
|
||||
// Create a unique string as a route identifier.
|
||||
const route =
|
||||
rateLimitTier +
|
||||
req.method +
|
||||
req.locals.apiLevel + // Generates new rate limit when user upgrades JWT token.
|
||||
path
|
||||
.split("/")
|
||||
.slice(0, 4)
|
||||
.join("/")
|
||||
//console.log(`route identifier: ${JSON.stringify(route, null, 2)}`)
|
||||
|
||||
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
|
||||
|
||||
// This boolean value is passed from the auth.js middleware.
|
||||
const proRateLimits = req.locals.proLimit
|
||||
|
||||
// console.log(`proRateLimits: ${proRateLimits}`)
|
||||
|
||||
// Pro level rate limits
|
||||
if (proRateLimits || proRateLimits === 0) {
|
||||
// TODO: replace the console.logs with calls to our logging system.
|
||||
// console.log(`applying pro-rate limits`)
|
||||
|
||||
let PRO_RPM = 10 // Default value for free tier
|
||||
if (req.locals.apiLevel > 0) PRO_RPM = 100 // RPM for paid tiers.
|
||||
|
||||
// console.log(`PRO_RPM: ${PRO_RPM}, apiLevel: ${req.locals.apiLevel}`)
|
||||
|
||||
// Create new RateLimit if none exists for this route
|
||||
if (!uniqueRateLimits[route]) {
|
||||
uniqueRateLimits[route] = new RateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute window
|
||||
delayMs: 0, // disable delaying - full speed until the max limit is reached
|
||||
max: PRO_RPM, // start blocking after this many requests per minute
|
||||
handler: function(req, res) {
|
||||
//console.log(`pro-tier rate-handler triggered.`)
|
||||
|
||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||
return res.json({
|
||||
error: `Too many requests. Limits are ${PRO_RPM} requests per minute. Increase rate limits at https://account.bchjs.cash`
|
||||
})
|
||||
// Create a res.locals object if not passed in.
|
||||
if (!req.locals) {
|
||||
req.locals = {
|
||||
// default values
|
||||
jwtToken: "",
|
||||
proLimit: false,
|
||||
rateLimit: 3
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Decode the JWT token if one exists.
|
||||
if (req.locals.jwtToken) {
|
||||
const jwtOptions = {
|
||||
algorithms: ["ES256"]
|
||||
}
|
||||
|
||||
const pemPublicKey = keyEncoder.encodePublic(publicKey, "raw", "pem")
|
||||
|
||||
// Validate the JWT token.
|
||||
decoded = _this.jwt.verify(
|
||||
req.locals.jwtToken,
|
||||
pemPublicKey,
|
||||
jwtOptions
|
||||
)
|
||||
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
|
||||
|
||||
userId = decoded.id
|
||||
} else {
|
||||
wlogger.debug(`No JWT token found!`)
|
||||
}
|
||||
|
||||
// Code here for the rate limiter is adapted from this example:
|
||||
// https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users
|
||||
try {
|
||||
// Key for Redis key/value pair.
|
||||
const key = userId ? userId : req.ip
|
||||
|
||||
const pointsToConsume = _this.calcPoints2(decoded)
|
||||
|
||||
wlogger.debug(`User ${key} consuming ${pointsToConsume}.`)
|
||||
|
||||
await _this.rateLimiter.consume(key, pointsToConsume)
|
||||
} catch (err) {
|
||||
// console.log(`err: `, err)
|
||||
|
||||
// Rate limited was triggered
|
||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||
return res.json({
|
||||
error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://account.bchjs.cash`
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
wlogger.error(`Error in route-ratelimit.js/rateLimitSimple(): `, err)
|
||||
// throw err
|
||||
}
|
||||
|
||||
// Freemium level rate limits
|
||||
} else {
|
||||
// TODO: replace the console.logs with calls to our logging system.
|
||||
// console.log(`applying freemium limits`)
|
||||
|
||||
// Create new RateLimit if none exists for this route
|
||||
if (!uniqueRateLimits[route]) {
|
||||
uniqueRateLimits[route] = new RateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute window
|
||||
delayMs: 0, // disable delaying - full speed until the max limit is reached
|
||||
max: maxRequests, // start blocking after maxRequests
|
||||
handler: function(req, res) {
|
||||
//console.log(`freemium rate-handler triggered.`)
|
||||
|
||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||
return res.json({
|
||||
error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://account.bchjs.cash`
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
//console.log(`calling uniqueRateLimits() on this route: ${route}`)
|
||||
// Calculates the points consumed, based on the explicit rateLimit defined
|
||||
// in the JWT token.
|
||||
calcPoints2(jwtInfo) {
|
||||
let retVal = 30 // By default, use anonymous tier.
|
||||
|
||||
// Call rate limit for this route
|
||||
uniqueRateLimits[route](req, res, next)
|
||||
try {
|
||||
// console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`)
|
||||
|
||||
const MAX_RATE_LIMIT = 100
|
||||
|
||||
const rateLimit = jwtInfo.rateLimit
|
||||
|
||||
// Only evaluate if user is using a JWT token.
|
||||
if (jwtInfo.id) {
|
||||
const points = Math.floor(MAX_RATE_LIMIT / rateLimit)
|
||||
|
||||
retVal = points
|
||||
}
|
||||
} catch (err) {
|
||||
wlogger.error(`Error in route-ratelimit.js/calcPoints2()`)
|
||||
// throw err
|
||||
retVal = 30
|
||||
}
|
||||
|
||||
return retVal
|
||||
}
|
||||
}
|
||||
|
||||
// This function returns the an object with proLimit and apiLevel properties.
|
||||
// It does fine-grane analysis on the data coming from the auth servers and
|
||||
// uses its output to adjust rate limits on-the-fly based on the users
|
||||
// permission level.
|
||||
function evalUserPermissioins(req, authData) {
|
||||
// console.log(`authData: ${JSON.stringify(authData, null, 2)}`)
|
||||
|
||||
// Return object with default values
|
||||
const retObj = {
|
||||
proLimit: authData.isValid,
|
||||
apiLevel: authData.apiLevel
|
||||
}
|
||||
|
||||
// if apiLevel = 0 (free tier), then return the default values.
|
||||
if (retObj.apiLevel === 0) return retObj
|
||||
|
||||
const level20Routes = ["insight", "bitcore", "blockbook"]
|
||||
|
||||
const locals = req.locals
|
||||
// console.log(`locals: ${JSON.stringify(locals, null, 2)}`)
|
||||
const url = req.url
|
||||
// console.log(`url: ${JSON.stringify(url, null, 2)}`)
|
||||
|
||||
if (authData.apiLevel < 20) {
|
||||
// Loop through the routes that are not accessible to this tier.
|
||||
for (let i = 0; i < level20Routes.length; i++) {
|
||||
// If the requested route is for a higher tier,
|
||||
// revert to anonymous level permissions.
|
||||
if (url.indexOf(level20Routes[i]) > -1) {
|
||||
retObj.proLimit = false
|
||||
retObj.apiLevel = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retObj
|
||||
}
|
||||
|
||||
module.exports = { routeRateLimit }
|
||||
module.exports = RateLimits
|
||||
|
||||
+827
-1094
File diff suppressed because it is too large
Load Diff
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
A library for interacting with the Full Node
|
||||
*/
|
||||
|
||||
"use strict"
|
||||
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
|
||||
const axios = require("axios")
|
||||
const wlogger = require("../../../util/winston-logging")
|
||||
|
||||
const RouteUtils = require("../route-utils2")
|
||||
const routeUtils = new RouteUtils()
|
||||
|
||||
// Used to convert error messages to strings, to safely pass to users.
|
||||
const util = require("util")
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const BCHJS = require("@chris.troutner/bch-js")
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
let _this
|
||||
|
||||
class Blockchain {
|
||||
constructor() {
|
||||
_this = this
|
||||
|
||||
this.bchjs = bchjs
|
||||
this.axios = axios
|
||||
this.routeUtils = routeUtils
|
||||
|
||||
this.router = router
|
||||
this.router.get("/", this.root)
|
||||
this.router.get("/getBestBlockHash", this.getBestBlockHash)
|
||||
this.router.get("/getBlockchainInfo", this.getBlockchainInfo)
|
||||
}
|
||||
|
||||
root(req, res, next) {
|
||||
return res.json({ status: "blockchain" })
|
||||
}
|
||||
|
||||
// DRY error handler.
|
||||
errorHandler(err, res) {
|
||||
// Attempt to decode the error message.
|
||||
const { msg, status } = this.routeUtils.decodeError(err)
|
||||
if (msg) {
|
||||
res.status(status)
|
||||
return res.json({ error: msg })
|
||||
}
|
||||
|
||||
res.status(500)
|
||||
return res.json({ error: util.inspect(err) })
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /blockchain/getBestBlockHash Get best block hash
|
||||
* @apiName GetBestBlockHash
|
||||
* @apiGroup Blockchain
|
||||
* @apiDescription Returns the hash of the best (tip) block in the longest
|
||||
* block chain.
|
||||
*
|
||||
* @apiExample curl Example:
|
||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getBestBlockHash" -H "accept: application/json"
|
||||
*
|
||||
* @apiSuccess {String} bestBlockHash 000000000000000002bc884334336d99c9a9c616670a9244c6a8c1fc35aa91a1
|
||||
*/
|
||||
async getBestBlockHash(req, res, next) {
|
||||
try {
|
||||
// Axios options
|
||||
const options = this.routeUtils.getAxiosOptions()
|
||||
options.data.id = "getbestblockhash"
|
||||
options.data.method = "getbestblockhash"
|
||||
options.data.params = []
|
||||
|
||||
const response = await this.axios.request(options)
|
||||
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
|
||||
|
||||
return res.json(response.data.result)
|
||||
} catch (err) {
|
||||
// Write out error to error log.
|
||||
wlogger.error(`Error in blockchain.ts/getBestBlockHash().`, err)
|
||||
|
||||
return this.errorHandler(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /blockchain/getBlockchainInfo Get blockchain info
|
||||
* @apiName GetBlockchainInfo
|
||||
* @apiGroup Blockchain
|
||||
* @apiDescription Returns an object containing various state info regarding blockchain processing.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getBlockchainInfo" -H "accept: application/json"
|
||||
*
|
||||
* @apiSuccess {Object} object Object containing data
|
||||
* @apiSuccess {String} object.chain "main"
|
||||
* @apiSuccess {Number} object.blocks 561838
|
||||
* @apiSuccess {Number} object.headers 561838
|
||||
* @apiSuccess {String} object.bestblockhash "000000000000000002307dd38cd01c7308b8febfcdf5772cf087b5bb023d55bc"
|
||||
* @apiSuccess {Number} object.difficulty 246585566638.1496
|
||||
* @apiSuccess {String} object.mediantime 1545402693
|
||||
* @apiSuccess {Number} object.verificationprogress 0.999998831622689
|
||||
* @apiSuccess {Boolean} object.chainwork "000000000000000000000000000000000000000000d8c09a8ab7262080266b3e"
|
||||
* @apiSuccess {Number} object.pruned false
|
||||
* @apiSuccess {Array} object.softforks Array of objects
|
||||
* @apiSuccess {String} object.softforks.id "bip34"
|
||||
* @apiSuccess {String} object.softforks.version 2
|
||||
* @apiSuccess {Object} object.softforks.reject
|
||||
* @apiSuccess {String} object.softforks.reject.status true
|
||||
*/
|
||||
async getBlockchainInfo(req, res, next) {
|
||||
try {
|
||||
// Axios options
|
||||
const options = this.routeUtils.getAxiosOptions()
|
||||
options.data.id = "getblockchaininfo"
|
||||
options.data.method = "getblockchaininfo"
|
||||
options.data.params = []
|
||||
|
||||
const response = await this.axios.request(options)
|
||||
|
||||
return res.json(response.data.result)
|
||||
} catch (err) {
|
||||
// Write out error to error log.
|
||||
wlogger.error(`Error in blockchain.ts/getBlockchainInfo().`, err)
|
||||
|
||||
return this.errorHandler(err, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Blockchain
|
||||
+757
-273
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ util.inspect.defaultOptions = { depth: 1 }
|
||||
const SERVER = `http://localhost:3000/v3/`
|
||||
|
||||
const TEST_JWT =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYTc5ZDk4OTYyMjRjNjM2MmQwYzkwMiIsImlhdCI6MTU3MTUzOTU1MSwiZXhwIjoxNTc0MTMxNTUxfQ.PfPW_Z2NYT1O2zUHXopcz2aLGHSGudaKOIGnt7SuAi4"
|
||||
"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlM2EwNDE1ZWIyOWE5NjJkYTI3MDhiNCIsImFwaUxldmVsIjowLCJyYXRlTGltaXQiOjEwLCJpYXQiOjE1ODA4NjA0NjcsImV4cCI6MTU4MzQ1MjQ2N30.fuY5S-YrF0J11h5uyMjPe7wiVkYRnIyXi4dL9-V-C6pLJm33p0dSq_pSheVVWw78n5kAvL_9kFHngbnmQiOJYQ"
|
||||
|
||||
describe("#rate limits", () => {
|
||||
it("should get control/getNetworkInfo() with no auth", async () => {
|
||||
|
||||
+575
-88
@@ -3,6 +3,7 @@
|
||||
const chai = require("chai")
|
||||
const assert = chai.assert
|
||||
const nock = require("nock") // HTTP mocking
|
||||
const sinon = require("sinon")
|
||||
|
||||
// Used for debugging.
|
||||
const util = require("util")
|
||||
@@ -12,7 +13,10 @@ util.inspect.defaultOptions = { depth: 1 }
|
||||
const { mockReq, mockRes, mockNext } = require("./mocks/express-mocks")
|
||||
|
||||
// Libraries under test
|
||||
let rateLimitMiddleware = require("../../src/middleware/route-ratelimit")
|
||||
const RateLimits = require("../../src/middleware/route-ratelimit")
|
||||
let rateLimits = new RateLimits()
|
||||
let rateLimitMiddleware = rateLimits.routeRateLimit
|
||||
|
||||
const controlRoute = require("../../src/routes/v3/full-node/control")
|
||||
const jwtAuth = require("../../src/middleware/jwt-auth")
|
||||
|
||||
@@ -23,6 +27,8 @@ let originalEnvVars // Used during transition from integration to unit tests.
|
||||
const jwt = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYWRlM2Y1NzM5ZTZjMGZmMDM0YjlhMSIsImlhdCI6MTU3MTY3NzQ1MCwiZXhwIjoxNTc0MjY5NDUwfQ.SSz7F7ETyBB3eoNG2VKCzPOhddtB-vrtmEoj7PxicrQ`
|
||||
|
||||
describe("#route-ratelimits & jwt-auth", () => {
|
||||
let sandbox
|
||||
|
||||
before(() => {
|
||||
// Save existing environment variables.
|
||||
originalEnvVars = {
|
||||
@@ -47,6 +53,17 @@ describe("#route-ratelimits & jwt-auth", () => {
|
||||
req.params = {}
|
||||
req.body = {}
|
||||
req.query = {}
|
||||
req.locals = {}
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
after(() => {
|
||||
rateLimits.closeRedis()
|
||||
})
|
||||
|
||||
describe("#jwt-auth.js", () => {
|
||||
@@ -71,83 +88,10 @@ describe("#route-ratelimits & jwt-auth", () => {
|
||||
assert.equal(req.locals.jwtToken, jwt)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: This code has been refactored and these unit tests no longer apply.
|
||||
// I think I forgot to create new unit tests to reflect the change in code though.
|
||||
/*
|
||||
describe("#routeAccess", () => {
|
||||
it("should do nothing if req.locals.jwtToken is undefined", () => {
|
||||
// Initialize req.locals
|
||||
req.locals = {
|
||||
proLimit: false,
|
||||
apiLevel: 0
|
||||
}
|
||||
req.url = "/insight/address/details"
|
||||
|
||||
// Reset the history of the stub and assert it has not been called.
|
||||
res.status.resetHistory()
|
||||
assert.equal(res.status.called, false, "stub history reset")
|
||||
|
||||
jwtAuth.routeAccess(req, res, next)
|
||||
|
||||
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
|
||||
assert.equal(
|
||||
res.status.called,
|
||||
false,
|
||||
"stub should NOT have been called."
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error if full-node tier tries to access indexer", () => {
|
||||
// Initialize req.locals
|
||||
req.locals = {
|
||||
proLimit: true,
|
||||
apiLevel: 10,
|
||||
jwtToken: jwt
|
||||
}
|
||||
req.url = "/insight/address/details"
|
||||
|
||||
try {
|
||||
res.status.resetHistory()
|
||||
assert.equal(res.status.called, false, "stub history reset")
|
||||
|
||||
jwtAuth.routeAccess(req, res, next)
|
||||
|
||||
assert.equal(res.status.called, true, "stub should have been called.")
|
||||
} catch (err) {
|
||||
console.log(`caught error: `, err)
|
||||
}
|
||||
})
|
||||
|
||||
it("should allow indexer tier tries access indexer endpoints", () => {
|
||||
// Initialize req.locals
|
||||
req.locals = {
|
||||
proLimit: true,
|
||||
apiLevel: 20,
|
||||
jwtToken: jwt
|
||||
}
|
||||
req.url = "/insight/address/details"
|
||||
|
||||
try {
|
||||
res.status.resetHistory()
|
||||
assert.equal(res.status.called, false, "stub history reset")
|
||||
|
||||
jwtAuth.routeAccess(req, res, next)
|
||||
|
||||
assert.equal(
|
||||
res.status.called,
|
||||
false,
|
||||
"stub should NOT have been called."
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(`caught error: `, err)
|
||||
}
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
||||
|
||||
describe("#routeRateLimit", () => {
|
||||
rateLimitMiddleware = new RateLimits()
|
||||
let routeRateLimit = rateLimitMiddleware.routeRateLimit
|
||||
const getInfo = controlRoute.testableComponents.getInfo
|
||||
|
||||
@@ -186,10 +130,11 @@ describe("#route-ratelimits & jwt-auth", () => {
|
||||
|
||||
it("should NOT trigger rate-limit for free-tier at 5 RPM", async () => {
|
||||
// Clear the require cache before running this test.
|
||||
delete require.cache[
|
||||
require.resolve("../../src/middleware/route-ratelimit")
|
||||
]
|
||||
rateLimitMiddleware = require("../../src/middleware/route-ratelimit")
|
||||
// delete require.cache[
|
||||
// require.resolve("../../src/middleware/route-ratelimit")
|
||||
// ]
|
||||
// rateLimitMiddleware = require("../../src/middleware/route-ratelimit")
|
||||
rateLimitMiddleware = new RateLimits()
|
||||
routeRateLimit = rateLimitMiddleware.routeRateLimit
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
@@ -216,7 +161,7 @@ describe("#route-ratelimits & jwt-auth", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("should trigger rate-limit for free tier 10 RPM", async () => {
|
||||
it("should trigger rate-limit for free tier after 10 RPM", async () => {
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.method = "GET"
|
||||
@@ -241,10 +186,11 @@ describe("#route-ratelimits & jwt-auth", () => {
|
||||
|
||||
it("should NOT trigger rate-limit handler for pro-tier at 25 RPM", async () => {
|
||||
// Clear the require cache before running this test.
|
||||
delete require.cache[
|
||||
require.resolve("../../src/middleware/route-ratelimit")
|
||||
]
|
||||
rateLimitMiddleware = require("../../src/middleware/route-ratelimit")
|
||||
// delete require.cache[
|
||||
// require.resolve("../../src/middleware/route-ratelimit")
|
||||
// ]
|
||||
// rateLimitMiddleware = require("../../src/middleware/route-ratelimit")
|
||||
rateLimitMiddleware = new RateLimits()
|
||||
routeRateLimit = rateLimitMiddleware.routeRateLimit
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
@@ -273,10 +219,11 @@ describe("#route-ratelimits & jwt-auth", () => {
|
||||
|
||||
it("rate-limiting should still kick in at a higher RPM for pro-tier", async () => {
|
||||
// Clear the require cache before running this test.
|
||||
delete require.cache[
|
||||
require.resolve("../../src/middleware/route-ratelimit")
|
||||
]
|
||||
rateLimitMiddleware = require("../../src/middleware/route-ratelimit")
|
||||
// delete require.cache[
|
||||
// require.resolve("../../src/middleware/route-ratelimit")
|
||||
// ]
|
||||
// rateLimitMiddleware = require("../../src/middleware/route-ratelimit")
|
||||
rateLimitMiddleware = new RateLimits()
|
||||
routeRateLimit = rateLimitMiddleware.routeRateLimit
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
@@ -303,6 +250,546 @@ describe("#route-ratelimits & jwt-auth", () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#getResource", () => {
|
||||
it("should decode a blockchain request", () => {
|
||||
const url =
|
||||
"/blockchain/getTxOut/62a3ea958a463a372bc0caf2c374a7f60be9c624be63a0db8db78f05809df6d8/0?include_mempool=true"
|
||||
|
||||
const result = rateLimits.getResource(url)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
assert.equal(result, "blockchain")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#calcPoints", () => {
|
||||
it("should return 30 points for anonymous user", () => {
|
||||
const result = rateLimits.calcPoints()
|
||||
// console.log(`result: ${result}`)
|
||||
|
||||
assert.equal(result, 30)
|
||||
})
|
||||
|
||||
it("should return 10 points for free tier requesting full node access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 10,
|
||||
resource: "blockchain",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 10)
|
||||
})
|
||||
|
||||
it("should return 10 points for free tier requesting indexer access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 10,
|
||||
resource: "blockbook",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 10)
|
||||
})
|
||||
|
||||
it("should return 10 points for free tier requesting SLPDB access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 10,
|
||||
resource: "slp",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 10)
|
||||
})
|
||||
|
||||
it("should return 1 point for full node tier requesting full node access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 20,
|
||||
resource: "blockchain",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 1)
|
||||
})
|
||||
|
||||
it("should return 10 points for full-node tier requesting indexer access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 20,
|
||||
resource: "blockbook",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 10)
|
||||
})
|
||||
|
||||
it("should return 10 points for full node tier requesting SLPDB access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 20,
|
||||
resource: "slp",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 10)
|
||||
})
|
||||
|
||||
it("should return 1 point for indexer tier requesting full node access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 30,
|
||||
resource: "blockchain",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 1)
|
||||
})
|
||||
|
||||
it("should return 1 points for indexer tier requesting indexer access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 30,
|
||||
resource: "blockbook",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 1)
|
||||
})
|
||||
|
||||
it("should return 10 points for indexer tier requesting SLPDB access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 30,
|
||||
resource: "slp",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 10)
|
||||
})
|
||||
|
||||
it("should return 1 point for SLP tier requesting full node access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 40,
|
||||
resource: "blockchain",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 1)
|
||||
})
|
||||
|
||||
it("should return 1 points for SLP tier requesting indexer access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 40,
|
||||
resource: "blockbook",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 1)
|
||||
})
|
||||
|
||||
it("should return 1 points for SLP tier requesting SLPDB access", () => {
|
||||
const jwtInfo = {
|
||||
apiLevel: 40,
|
||||
resource: "slp",
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints(jwtInfo)
|
||||
assert.equal(result, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#calcPoints2", () => {
|
||||
it("should return 30 points for anonymous user", () => {
|
||||
const result = rateLimits.calcPoints2({})
|
||||
// console.log(`result: ${result}`)
|
||||
|
||||
assert.equal(result, 30)
|
||||
})
|
||||
|
||||
it("should return 10 point for free tier", () => {
|
||||
const jwtInfo = {
|
||||
rateLimit: 10,
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints2(jwtInfo)
|
||||
assert.equal(result, 10)
|
||||
})
|
||||
|
||||
it("should return 1 point for pro tier", () => {
|
||||
const jwtInfo = {
|
||||
rateLimit: 100,
|
||||
id: "5e3a0415eb29a962da2708b4"
|
||||
}
|
||||
|
||||
const result = rateLimits.calcPoints2(jwtInfo)
|
||||
assert.equal(result, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#rateLimitByResource", () => {
|
||||
it("should pass through rate-limit middleware", async () => {
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
// Call the route twice to trigger the rate handling.
|
||||
await rateLimits.rateLimitByResource(req, res, next)
|
||||
await rateLimits.rateLimitByResource(req, res, next)
|
||||
|
||||
// next() will be called if rate-limit is not triggered
|
||||
assert.equal(next.called, true)
|
||||
})
|
||||
|
||||
it("should trigger rate-limit handler if rate limits exceeds 5 request per minute", async () => {
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitByResource(req, res, next)
|
||||
//console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
false,
|
||||
`next should not be called if rate limit was triggered.`
|
||||
)
|
||||
})
|
||||
|
||||
it("should NOT trigger rate-limit for free-tier at 5 RPM", async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
req.locals.jwtToken = "some-token"
|
||||
|
||||
const jwtInfo = {
|
||||
apiLevel: 10,
|
||||
id: "5e3a0415eb29a962da2708b1"
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitByResource(req, res, next)
|
||||
//console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
//console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
true,
|
||||
`next should be called if rate limit was not triggered.`
|
||||
)
|
||||
})
|
||||
|
||||
it("should trigger rate-limit for free tier after 10 RPM", async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
req.locals.jwtToken = "some-token"
|
||||
|
||||
const jwtInfo = {
|
||||
apiLevel: 10,
|
||||
id: "5e3a0415eb29a962da2708b2"
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitByResource(req, res, next)
|
||||
//console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
false,
|
||||
`next should not be called if rate limit was triggered.`
|
||||
)
|
||||
})
|
||||
|
||||
it("should NOT trigger rate-limit handler for indexer-tier at 25 RPM", async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
req.locals.jwtToken = "some-token"
|
||||
|
||||
const jwtInfo = {
|
||||
apiLevel: 20,
|
||||
id: "5e3a0415eb29a962da2708b3"
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 25; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitByResource(req, res, next)
|
||||
//console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
//console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
true,
|
||||
`next should be called if rate limit was not triggered.`
|
||||
)
|
||||
})
|
||||
|
||||
it("should still rate-limit at a higher RPM for pro-tier", async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
req.locals.jwtToken = "some-token"
|
||||
|
||||
const jwtInfo = {
|
||||
apiLevel: 20,
|
||||
id: "5e3a0415eb29a962da2708b5"
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 150; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitByResource(req, res, next)
|
||||
//console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
//console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
false,
|
||||
`next should NOT be called if rate limit was triggered.`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#rateLimitSimple", () => {
|
||||
it("should pass through rate-limit middleware", async () => {
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
// Call the route twice to trigger the rate handling.
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
|
||||
// next() will be called if rate-limit is not triggered
|
||||
assert.equal(next.called, true)
|
||||
})
|
||||
|
||||
it("should trigger rate-limit handler if rate limits exceeds 5 request per minute", async () => {
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
//console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
false,
|
||||
`next should not be called if rate limit was triggered.`
|
||||
)
|
||||
})
|
||||
|
||||
it("should NOT trigger rate-limit for free-tier at 5 RPM", async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
req.locals.jwtToken = "some-token"
|
||||
|
||||
const jwtInfo = {
|
||||
rateLimit: 10,
|
||||
id: "5e3a0415eb29a962da2708c1"
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
//console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
//console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
true,
|
||||
`next should be called if rate limit was not triggered.`
|
||||
)
|
||||
})
|
||||
|
||||
it("should trigger rate-limit for free tier after 10 RPM", async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
req.locals.jwtToken = "some-token"
|
||||
|
||||
const jwtInfo = {
|
||||
rateLimit: 10,
|
||||
id: "5e3a0415eb29a962da2708c2"
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
//console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
false,
|
||||
`next should not be called if rate limit was triggered.`
|
||||
)
|
||||
})
|
||||
|
||||
it("should NOT trigger rate-limit handler for pro-tier at 25 RPM", async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
req.locals.jwtToken = "some-token"
|
||||
|
||||
const jwtInfo = {
|
||||
rateLimit: 100,
|
||||
id: "5e3a0415eb29a962da2708c3"
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 25; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
//console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
//console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
true,
|
||||
`next should be called if rate limit was not triggered.`
|
||||
)
|
||||
})
|
||||
|
||||
it("should still rate-limit at a higher RPM for pro-tier", async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = "/v3"
|
||||
req.path = "/control/getNetworkInfo"
|
||||
req.url = req.path
|
||||
req.method = "GET"
|
||||
|
||||
req.locals.jwtToken = "some-token"
|
||||
|
||||
const jwtInfo = {
|
||||
rateLimit: 100,
|
||||
id: "5e3a0415eb29a962da2708c4"
|
||||
}
|
||||
|
||||
// Mock the call to the jwt library.
|
||||
sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo)
|
||||
|
||||
for (let i = 0; i < 150; i++) {
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitSimple(req, res, next)
|
||||
//console.log(`next() called: ${next.called}`)
|
||||
}
|
||||
|
||||
//console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
||||
|
||||
// Note: next() will be called unless the rate-limit kicks in.
|
||||
assert.equal(
|
||||
next.called,
|
||||
false,
|
||||
`next should NOT be called if rate limit was triggered.`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Generates a Basic authorization header.
|
||||
|
||||
Reference in New Issue
Block a user