aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-07-04 16:42:40 +0200
committerChocobozzz <me@florianbigard.com>2019-07-04 16:42:40 +0200
commitc1340a6ac35f924161e6ec2a1d728e20c89e55c8 (patch)
tree8f0a6b72b36be586422002039720d3a08309cbea
parentfd0bfc3ac43eb0c0c2ac0b21bc2e0670f546384f (diff)
downloadPeerTube-c1340a6ac35f924161e6ec2a1d728e20c89e55c8.tar.gz
PeerTube-c1340a6ac35f924161e6ec2a1d728e20c89e55c8.tar.zst
PeerTube-c1340a6ac35f924161e6ec2a1d728e20c89e55c8.zip
Add rate limit to registration and API endpoints
-rw-r--r--config/default.yaml8
-rw-r--r--config/production.yaml.example8
-rw-r--r--config/test.yaml8
-rw-r--r--server.ts4
-rw-r--r--server/controllers/api/index.ts10
-rw-r--r--server/controllers/api/users/index.ts18
-rw-r--r--server/initializers/config.ts8
-rw-r--r--server/initializers/constants.ts14
-rw-r--r--server/tests/api/server/reverse-proxy.ts57
9 files changed, 112 insertions, 23 deletions
diff --git a/config/default.yaml b/config/default.yaml
index a213d5b0a..be5c8993c 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -10,10 +10,18 @@ webserver:
10 port: 9000 10 port: 9000
11 11
12rates_limit: 12rates_limit:
13 api:
14 # 50 attempts in 10 seconds
15 window: 10 seconds
16 max: 50
13 login: 17 login:
14 # 15 attempts in 5 min 18 # 15 attempts in 5 min
15 window: 5 minutes 19 window: 5 minutes
16 max: 15 20 max: 15
21 signup:
22 # 2 attempts in 5 min (only succeeded attempts are taken into account)
23 window: 5 minutes
24 max: 2
17 ask_send_email: 25 ask_send_email:
18 # 3 attempts in 5 min 26 # 3 attempts in 5 min
19 window: 5 minutes 27 window: 5 minutes
diff --git a/config/production.yaml.example b/config/production.yaml.example
index cdf6136d8..f55f5c096 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -9,10 +9,18 @@ webserver:
9 port: 443 9 port: 443
10 10
11rates_limit: 11rates_limit:
12 api:
13 # 50 attempts in 10 seconds
14 window: 10 seconds
15 max: 50
12 login: 16 login:
13 # 15 attempts in 5 min 17 # 15 attempts in 5 min
14 window: 5 minutes 18 window: 5 minutes
15 max: 15 19 max: 15
20 signup:
21 # 2 attempts in 5 min (only succeeded attempts are taken into account)
22 window: 5 minutes
23 max: 2
16 ask_send_email: 24 ask_send_email:
17 # 3 attempts in 5 min 25 # 3 attempts in 5 min
18 window: 5 minutes 26 window: 5 minutes
diff --git a/config/test.yaml b/config/test.yaml
index 8d3921614..0a5df75be 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -5,6 +5,14 @@ listen:
5webserver: 5webserver:
6 https: false 6 https: false
7 7
8rates_limit:
9 signup:
10 window: 10 minutes
11 max: 50
12 login:
13 window: 5 minutes
14 max: 20
15
8database: 16database:
9 hostname: 'localhost' 17 hostname: 'localhost'
10 port: 5432 18 port: 5432
diff --git a/server.ts b/server.ts
index aa4382ee7..9f0b123e0 100644
--- a/server.ts
+++ b/server.ts
@@ -27,9 +27,9 @@ const app = express()
27import { checkMissedConfig, checkFFmpeg } from './server/initializers/checker-before-init' 27import { checkMissedConfig, checkFFmpeg } from './server/initializers/checker-before-init'
28 28
29// Do not use barrels because we don't want to load all modules here (we need to initialize database first) 29// Do not use barrels because we don't want to load all modules here (we need to initialize database first)
30import { logger } from './server/helpers/logger'
31import { API_VERSION, FILES_CACHE, WEBSERVER, loadLanguages } from './server/initializers/constants'
32import { CONFIG } from './server/initializers/config' 30import { CONFIG } from './server/initializers/config'
31import { API_VERSION, FILES_CACHE, WEBSERVER, loadLanguages } from './server/initializers/constants'
32import { logger } from './server/helpers/logger'
33 33
34const missed = checkMissedConfig() 34const missed = checkMissedConfig()
35if (missed.length !== 0) { 35if (missed.length !== 0) {
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 60a84036e..ea2615e28 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -1,4 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as RateLimit from 'express-rate-limit'
2import { configRouter } from './config' 3import { configRouter } from './config'
3import { jobsRouter } from './jobs' 4import { jobsRouter } from './jobs'
4import { oauthClientsRouter } from './oauth-clients' 5import { oauthClientsRouter } from './oauth-clients'
@@ -12,6 +13,7 @@ import * as cors from 'cors'
12import { searchRouter } from './search' 13import { searchRouter } from './search'
13import { overviewsRouter } from './overviews' 14import { overviewsRouter } from './overviews'
14import { videoPlaylistRouter } from './video-playlist' 15import { videoPlaylistRouter } from './video-playlist'
16import { CONFIG } from '../../initializers/config'
15 17
16const apiRouter = express.Router() 18const apiRouter = express.Router()
17 19
@@ -21,6 +23,14 @@ apiRouter.use(cors({
21 credentials: true 23 credentials: true
22})) 24}))
23 25
26// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138
27// @ts-ignore
28const apiRateLimiter = RateLimit({
29 windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
30 max: CONFIG.RATES_LIMIT.API.MAX
31})
32apiRouter.use(apiRateLimiter)
33
24apiRouter.use('/server', serverRouter) 34apiRouter.use('/server', serverRouter)
25apiRouter.use('/oauth-clients', oauthClientsRouter) 35apiRouter.use('/oauth-clients', oauthClientsRouter)
26apiRouter.use('/config', configRouter) 36apiRouter.use('/config', configRouter)
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index c1d72087c..63747a0a9 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -3,7 +3,7 @@ import * as RateLimit from 'express-rate-limit'
3import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' 3import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { getFormattedObjects } from '../../../helpers/utils' 5import { getFormattedObjects } from '../../../helpers/utils'
6import { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants' 6import { WEBSERVER } from '../../../initializers/constants'
7import { Emailer } from '../../../lib/emailer' 7import { Emailer } from '../../../lib/emailer'
8import { Redis } from '../../../lib/redis' 8import { Redis } from '../../../lib/redis'
9import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' 9import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
@@ -53,14 +53,21 @@ const auditLogger = auditLoggerFactory('users')
53// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138 53// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138
54// @ts-ignore 54// @ts-ignore
55const loginRateLimiter = RateLimit({ 55const loginRateLimiter = RateLimit({
56 windowMs: RATES_LIMIT.LOGIN.WINDOW_MS, 56 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
57 max: RATES_LIMIT.LOGIN.MAX 57 max: CONFIG.RATES_LIMIT.LOGIN.MAX
58})
59
60// @ts-ignore
61const signupRateLimiter = RateLimit({
62 windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
63 max: CONFIG.RATES_LIMIT.SIGNUP.MAX,
64 skipFailedRequests: true
58}) 65})
59 66
60// @ts-ignore 67// @ts-ignore
61const askSendEmailLimiter = new RateLimit({ 68const askSendEmailLimiter = new RateLimit({
62 windowMs: RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, 69 windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
63 max: RATES_LIMIT.ASK_SEND_EMAIL.MAX 70 max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
64}) 71})
65 72
66const usersRouter = express.Router() 73const usersRouter = express.Router()
@@ -114,6 +121,7 @@ usersRouter.post('/',
114) 121)
115 122
116usersRouter.post('/register', 123usersRouter.post('/register',
124 signupRateLimiter,
117 asyncMiddleware(ensureUserRegistrationAllowed), 125 asyncMiddleware(ensureUserRegistrationAllowed),
118 ensureUserRegistrationAllowedForIP, 126 ensureUserRegistrationAllowedForIP,
119 asyncMiddleware(usersRegisterValidator), 127 asyncMiddleware(usersRegisterValidator),
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index bb278ba43..eefb45fb9 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -72,6 +72,14 @@ const CONFIG = {
72 PORT: config.get<number>('webserver.port') 72 PORT: config.get<number>('webserver.port')
73 }, 73 },
74 RATES_LIMIT: { 74 RATES_LIMIT: {
75 API: {
76 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')),
77 MAX: config.get<number>('rates_limit.api.max')
78 },
79 SIGNUP: {
80 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.signup.window')),
81 MAX: config.get<number>('rates_limit.signup.max')
82 },
75 LOGIN: { 83 LOGIN: {
76 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')), 84 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')),
77 MAX: config.get<number>('rates_limit.login.max') 85 MAX: config.get<number>('rates_limit.login.max')
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 500f8770a..abd9c2003 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -280,17 +280,6 @@ let CONSTRAINTS_FIELDS = {
280 } 280 }
281} 281}
282 282
283const RATES_LIMIT = {
284 LOGIN: {
285 WINDOW_MS: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
286 MAX: CONFIG.RATES_LIMIT.LOGIN.MAX
287 },
288 ASK_SEND_EMAIL: {
289 WINDOW_MS: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
290 MAX: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
291 }
292}
293
294let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour 283let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
295let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour 284let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
296 285
@@ -624,8 +613,6 @@ if (isTestInstance() === true) {
624 FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 613 FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
625 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 614 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1
626 ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' 615 ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms'
627
628 RATES_LIMIT.LOGIN.MAX = 20
629} 616}
630 617
631updateWebserverUrls() 618updateWebserverUrls()
@@ -696,7 +683,6 @@ export {
696 SCHEDULER_INTERVALS_MS, 683 SCHEDULER_INTERVALS_MS,
697 REPEAT_JOBS, 684 REPEAT_JOBS,
698 STATIC_DOWNLOAD_PATHS, 685 STATIC_DOWNLOAD_PATHS,
699 RATES_LIMIT,
700 MIMETYPES, 686 MIMETYPES,
701 CRAWL_REQUEST_CONCURRENCY, 687 CRAWL_REQUEST_CONCURRENCY,
702 DEFAULT_AUDIO_RESOLUTION, 688 DEFAULT_AUDIO_RESOLUTION,
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts
index 987538237..00d9fca23 100644
--- a/server/tests/api/server/reverse-proxy.ts
+++ b/server/tests/api/server/reverse-proxy.ts
@@ -2,7 +2,7 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { cleanupTests, getVideo, uploadVideo, userLogin, viewVideo, wait } from '../../../../shared/extra-utils' 5import { cleanupTests, getVideo, registerUser, uploadVideo, userLogin, viewVideo, wait } from '../../../../shared/extra-utils'
6import { flushAndRunServer, setAccessTokensToServers } from '../../../../shared/extra-utils/index' 6import { flushAndRunServer, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
7 7
8const expect = chai.expect 8const expect = chai.expect
@@ -13,7 +13,27 @@ describe('Test application behind a reverse proxy', function () {
13 13
14 before(async function () { 14 before(async function () {
15 this.timeout(30000) 15 this.timeout(30000)
16 server = await flushAndRunServer(1) 16
17 const config = {
18 rates_limit: {
19 api: {
20 max: 50,
21 window: 5000
22 },
23 signup: {
24 max: 3,
25 window: 5000
26 },
27 login: {
28 max: 20
29 }
30 },
31 signup: {
32 limit: 20
33 }
34 }
35
36 server = await flushAndRunServer(1, config)
17 await setAccessTokensToServers([ server ]) 37 await setAccessTokensToServers([ server ])
18 38
19 const { body } = await uploadVideo(server.url, server.accessToken, {}) 39 const { body } = await uploadVideo(server.url, server.accessToken, {})
@@ -82,6 +102,39 @@ describe('Test application behind a reverse proxy', function () {
82 await userLogin(server, user, 429) 102 await userLogin(server, user, 429)
83 }) 103 })
84 104
105 it('Should rate limit signup', async function () {
106 for (let i = 0; i < 3; i++) {
107 await registerUser(server.url, 'test' + i, 'password')
108 }
109
110 await registerUser(server.url, 'test42', 'password', 429)
111 })
112
113 it('Should not rate limit failed signup', async function () {
114 this.timeout(30000)
115
116 await wait(7000)
117
118 for (let i = 0; i < 3; i++) {
119 await registerUser(server.url, 'test' + i, 'password', 409)
120 }
121
122 await registerUser(server.url, 'test43', 'password', 204)
123
124 })
125
126 it('Should rate limit API calls', async function () {
127 this.timeout(30000)
128
129 await wait(7000)
130
131 for (let i = 0; i < 50; i++) {
132 await getVideo(server.url, videoId)
133 }
134
135 await getVideo(server.url, videoId, 429)
136 })
137
85 after(async function () { 138 after(async function () {
86 await cleanupTests([ server ]) 139 await cleanupTests([ server ])
87 }) 140 })