serverest 3.2.0 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app.js +5 -0
- package/src/middlewares/rate-limiter.js +26 -9
- package/src/server.js +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverest",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.2",
|
|
4
4
|
"description": "Servidor REST local de forma rápida e simples para estudo de testes de API",
|
|
5
5
|
"author": "Paulo Gonçalves <author@serverest.dev> (https://www.linkedin.com/in/paulo-goncalves/)",
|
|
6
6
|
"license": "GPL-3.0",
|
package/src/app.js
CHANGED
|
@@ -50,6 +50,11 @@ if (process.env.BLOCKED_IPS) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
app.set('json spaces', 4)
|
|
53
|
+
// Confia em exatamente um proxy, que é o do Cloud Run. Sem isso req.ip resolve para o
|
|
54
|
+
// endereço do proxy e todos os clientes viram a mesma chave no rate limiter. Com 'true'
|
|
55
|
+
// o Express passaria a ler o primeiro endereço do X-Forwarded-For, que o próprio cliente
|
|
56
|
+
// pode forjar, então o número de saltos precisa ser explícito.
|
|
57
|
+
app.set('trust proxy', 1)
|
|
53
58
|
app.use(express.json())
|
|
54
59
|
app.use(express.urlencoded({ extended: false }))
|
|
55
60
|
app.use(queryParser())
|
|
@@ -6,9 +6,20 @@ const {
|
|
|
6
6
|
} = require('../utils/ambiente')
|
|
7
7
|
const { LOAD_TEST_DETECTED } = require('../utils/constants')
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// Limite por cliente. Depende do 'trust proxy' definido no app.js: sem ele req.ip resolve
|
|
10
|
+
// para o endereço do proxy do Cloud Run e este limite passaria a valer para todos somados.
|
|
11
|
+
const limitePorIp = new RateLimiterMemory({
|
|
12
|
+
points: 300, // requests
|
|
13
|
+
duration: 30 // segundos, por IP
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// Limite agregado do container. Existe porque o limite por IP, sozinho, não impõe teto ao
|
|
17
|
+
// total: bastariam vários clientes simultâneos para saturar a instância, que roda com
|
|
18
|
+
// 256 MiB e max-instances 1. Fica acima do limite por IP para que um único cliente abusivo
|
|
19
|
+
// seja barrado pelo próprio limite antes de consumir a cota que protege os demais.
|
|
20
|
+
const limiteGlobal = new RateLimiterMemory({
|
|
21
|
+
points: 900, // requests
|
|
22
|
+
duration: 30 // segundos, somando todos os clientes
|
|
12
23
|
})
|
|
13
24
|
|
|
14
25
|
module.exports = async (req, res, next) => {
|
|
@@ -16,11 +27,17 @@ module.exports = async (req, res, next) => {
|
|
|
16
27
|
return next()
|
|
17
28
|
}
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
try {
|
|
31
|
+
// O global vem primeiro de propósito. Avaliar os dois em paralelo faria a requisição
|
|
32
|
+
// consumir a cota individual mesmo quando o bloqueio viesse do limite agregado, o que
|
|
33
|
+
// penalizaria clientes inocentes durante congestionamento e, pior, criaria uma chave
|
|
34
|
+
// por endereço no mapa mesmo para requisições já rejeitadas.
|
|
35
|
+
await limiteGlobal.consume('global')
|
|
36
|
+
await limitePorIp.consume(req.ip)
|
|
37
|
+
return next()
|
|
38
|
+
} catch (_) {
|
|
39
|
+
return res.status(429).send({
|
|
40
|
+
message: LOAD_TEST_DETECTED
|
|
25
41
|
})
|
|
42
|
+
}
|
|
26
43
|
}
|
package/src/server.js
CHANGED
|
@@ -7,16 +7,57 @@ require('dotenv').config({ path: path.resolve(__dirname, '../.env'), quiet: true
|
|
|
7
7
|
|
|
8
8
|
const colors = require('colors')
|
|
9
9
|
const debug = require('debug')('nodestr:server')
|
|
10
|
+
const fs = require('fs')
|
|
10
11
|
const http = require('http')
|
|
12
|
+
const util = require('util')
|
|
11
13
|
const open = require('open').default
|
|
12
14
|
|
|
13
15
|
const { version } = require('../package.json')
|
|
14
16
|
const { formaDeExecucao, urlDocumentacao, aplicacaoExecutandoLocalmente } = require('./utils/ambiente')
|
|
15
17
|
const { conf } = require('./utils/conf')
|
|
16
18
|
const getRandomFinancialContributor = require('./utils/getRandomFinancialContributor')
|
|
19
|
+
const { log } = require('./utils/logger')
|
|
17
20
|
|
|
18
21
|
const DEFAULT_PORT = 3000
|
|
19
22
|
|
|
23
|
+
// Erros fora do ciclo de uma requisição encerram o processo sem deixar registro da causa,
|
|
24
|
+
// o que no Cloud Run aparece apenas como um container reiniciando sem explicação.
|
|
25
|
+
/* istanbul ignore next */
|
|
26
|
+
process.on('uncaughtException', valor => {
|
|
27
|
+
// Node permite lançar qualquer valor, não só Error. Sem esta normalização, um throw de
|
|
28
|
+
// string ou null faria o próprio handler lançar ao acessar .code, perdendo o registro
|
|
29
|
+
// da causa, que é justamente o que estes handlers existem para garantir.
|
|
30
|
+
const erro = valor instanceof Error ? valor : new Error(util.inspect(valor))
|
|
31
|
+
|
|
32
|
+
// Responder a uma requisição já encerrada é inofensivo: a resposta chegou ao cliente e o
|
|
33
|
+
// processo segue íntegro. Derrubar o servidor por isso custaria uma indisponibilidade
|
|
34
|
+
// inteira para corrigir nada.
|
|
35
|
+
if (erro.code === 'ERR_HTTP_HEADERS_SENT') {
|
|
36
|
+
log({ level: 'error', message: `Tentativa de responder requisição já encerrada: ${erro.message}` })
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
encerrar(`Exceção não capturada: ${erro.message}. Stack: ${erro.stack}`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/* istanbul ignore next */
|
|
44
|
+
process.on('unhandledRejection', motivo => {
|
|
45
|
+
encerrar(`Promise rejeitada sem tratamento: ${motivo instanceof Error ? motivo.stack : util.inspect(motivo)}`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Sai de imediato em vez de drenar as conexões abertas. Com max-instances 1 no Cloud Run
|
|
49
|
+
// não existe outra instância para atender quem chegar durante a drenagem, então esperar
|
|
50
|
+
// só adiaria a subida do container substituto e prolongaria a indisponibilidade.
|
|
51
|
+
/* istanbul ignore next */
|
|
52
|
+
function encerrar (mensagem) {
|
|
53
|
+
// console.log é assíncrono quando stdout é um pipe, caso do Cloud Run, e process.exit
|
|
54
|
+
// não drena writes pendentes. A escrita síncrona no stderr garante que a causa fique
|
|
55
|
+
// registrada mesmo que o log estruturado seja truncado pelo encerramento.
|
|
56
|
+
fs.writeSync(2, `${mensagem}\n`)
|
|
57
|
+
log({ level: 'error', message: mensagem })
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
20
61
|
const yargs = require('yargs/yargs')
|
|
21
62
|
const { hideBin } = require('yargs/helpers')
|
|
22
63
|
|