Fix runner api rate limit bypass

This commit is contained in:
Chocobozzz 2023-06-20 14:17:34 +02:00
parent 923e41fa4f
commit e915cde30e
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
26 changed files with 122 additions and 31 deletions

View File

@ -16,6 +16,7 @@ import {
abusesSortValidator, abusesSortValidator,
abuseUpdateValidator, abuseUpdateValidator,
addAbuseMessageValidator, addAbuseMessageValidator,
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
asyncRetryTransactionMiddleware, asyncRetryTransactionMiddleware,
authenticate, authenticate,
@ -32,6 +33,8 @@ import { AccountModel } from '../../models/account/account'
const abuseRouter = express.Router() const abuseRouter = express.Router()
abuseRouter.use(apiRateLimiter)
abuseRouter.get('/', abuseRouter.get('/',
openapiOperationDoc({ operationId: 'getAbuses' }), openapiOperationDoc({ operationId: 'getAbuses' }),
authenticate, authenticate,

View File

@ -9,6 +9,7 @@ import { getFormattedObjects } from '../../helpers/utils'
import { JobQueue } from '../../lib/job-queue' import { JobQueue } from '../../lib/job-queue'
import { Hooks } from '../../lib/plugins/hooks' import { Hooks } from '../../lib/plugins/hooks'
import { import {
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,
commonVideosFiltersValidator, commonVideosFiltersValidator,
@ -41,6 +42,8 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist'
const accountsRouter = express.Router() const accountsRouter = express.Router()
accountsRouter.use(apiRateLimiter)
accountsRouter.get('/', accountsRouter.get('/',
paginationValidator, paginationValidator,
accountsSortValidator, accountsSortValidator,

View File

@ -1,15 +1,17 @@
import express from 'express' import express from 'express'
import { handleToNameAndHost } from '@server/helpers/actors' import { handleToNameAndHost } from '@server/helpers/actors'
import { logger } from '@server/helpers/logger'
import { AccountBlocklistModel } from '@server/models/account/account-blocklist' import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { ServerBlocklistModel } from '@server/models/server/server-blocklist' import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
import { MActorAccountId, MUserAccountId } from '@server/types/models' import { MActorAccountId, MUserAccountId } from '@server/types/models'
import { BlockStatus } from '@shared/models' import { BlockStatus } from '@shared/models'
import { asyncMiddleware, blocklistStatusValidator, optionalAuthenticate } from '../../middlewares' import { apiRateLimiter, asyncMiddleware, blocklistStatusValidator, optionalAuthenticate } from '../../middlewares'
import { logger } from '@server/helpers/logger'
const blocklistRouter = express.Router() const blocklistRouter = express.Router()
blocklistRouter.use(apiRateLimiter)
blocklistRouter.get('/status', blocklistRouter.get('/status',
optionalAuthenticate, optionalAuthenticate,
blocklistStatusValidator, blocklistStatusValidator,

View File

@ -4,10 +4,12 @@ import { bulkRemoveCommentsOfValidator } from '@server/middlewares/validators/bu
import { VideoCommentModel } from '@server/models/video/video-comment' import { VideoCommentModel } from '@server/models/video/video-comment'
import { HttpStatusCode } from '@shared/models' import { HttpStatusCode } from '@shared/models'
import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model' import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model'
import { asyncMiddleware, authenticate } from '../../middlewares' import { apiRateLimiter, asyncMiddleware, authenticate } from '../../middlewares'
const bulkRouter = express.Router() const bulkRouter = express.Router()
bulkRouter.use(apiRateLimiter)
bulkRouter.post('/remove-comments-of', bulkRouter.post('/remove-comments-of',
authenticate, authenticate,
asyncMiddleware(bulkRemoveCommentsOfValidator), asyncMiddleware(bulkRemoveCommentsOfValidator),

View File

@ -8,11 +8,13 @@ import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '..
import { objectConverter } from '../../helpers/core-utils' import { objectConverter } from '../../helpers/core-utils'
import { CONFIG, reloadConfig } from '../../initializers/config' import { CONFIG, reloadConfig } from '../../initializers/config'
import { ClientHtml } from '../../lib/client-html' import { ClientHtml } from '../../lib/client-html'
import { asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares' import { apiRateLimiter, asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares'
import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config' import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config'
const configRouter = express.Router() const configRouter = express.Router()
configRouter.use(apiRateLimiter)
const auditLogger = auditLoggerFactory('config') const auditLogger = auditLoggerFactory('config')
configRouter.get('/', configRouter.get('/',

View File

@ -2,10 +2,12 @@ import express from 'express'
import { ServerConfigManager } from '@server/lib/server-config-manager' import { ServerConfigManager } from '@server/lib/server-config-manager'
import { ActorCustomPageModel } from '@server/models/account/actor-custom-page' import { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
import { HttpStatusCode, UserRight } from '@shared/models' import { HttpStatusCode, UserRight } from '@shared/models'
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' import { apiRateLimiter, asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
const customPageRouter = express.Router() const customPageRouter = express.Router()
customPageRouter.use(apiRateLimiter)
customPageRouter.get('/homepage/instance', customPageRouter.get('/homepage/instance',
asyncMiddleware(getInstanceHomepage) asyncMiddleware(getInstanceHomepage)
) )

View File

@ -1,9 +1,8 @@
import cors from 'cors' import cors from 'cors'
import express from 'express' import express from 'express'
import { buildRateLimiter } from '@server/middlewares'
import { HttpStatusCode } from '../../../shared/models' import { HttpStatusCode } from '../../../shared/models'
import { badRequest } from '../../helpers/express-utils' import { badRequest } from '../../helpers/express-utils'
import { CONFIG } from '../../initializers/config'
import { abuseRouter } from './abuse' import { abuseRouter } from './abuse'
import { accountsRouter } from './accounts' import { accountsRouter } from './accounts'
import { blocklistRouter } from './blocklist' import { blocklistRouter } from './blocklist'
@ -32,12 +31,6 @@ apiRouter.use(cors({
credentials: true credentials: true
})) }))
const apiRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
max: CONFIG.RATES_LIMIT.API.MAX
})
apiRouter.use(apiRateLimiter)
apiRouter.use('/server', serverRouter) apiRouter.use('/server', serverRouter)
apiRouter.use('/abuses', abuseRouter) apiRouter.use('/abuses', abuseRouter)
apiRouter.use('/bulk', bulkRouter) apiRouter.use('/bulk', bulkRouter)
@ -57,6 +50,8 @@ apiRouter.use('/plugins', pluginRouter)
apiRouter.use('/custom-pages', customPageRouter) apiRouter.use('/custom-pages', customPageRouter)
apiRouter.use('/blocklist', blocklistRouter) apiRouter.use('/blocklist', blocklistRouter)
apiRouter.use('/runners', runnersRouter) apiRouter.use('/runners', runnersRouter)
// apiRouter.use(apiRateLimiter)
apiRouter.use('/ping', pong) apiRouter.use('/ping', pong)
apiRouter.use('/*', badRequest) apiRouter.use('/*', badRequest)

View File

@ -4,6 +4,7 @@ import { HttpStatusCode, Job, JobState, JobType, ResultList, UserRight } from '@
import { isArray } from '../../helpers/custom-validators/misc' import { isArray } from '../../helpers/custom-validators/misc'
import { JobQueue } from '../../lib/job-queue' import { JobQueue } from '../../lib/job-queue'
import { import {
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,
ensureUserHasRight, ensureUserHasRight,
@ -17,6 +18,8 @@ import { listJobsValidator } from '../../middlewares/validators/jobs'
const jobsRouter = express.Router() const jobsRouter = express.Router()
jobsRouter.use(apiRateLimiter)
jobsRouter.post('/pause', jobsRouter.post('/pause',
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_JOBS), ensureUserHasRight(UserRight.MANAGE_JOBS),

View File

@ -1,11 +1,13 @@
import express from 'express' import express from 'express'
import { CONFIG } from '@server/initializers/config'
import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics' import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics'
import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models' import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models'
import { addPlaybackMetricValidator, asyncMiddleware } from '../../middlewares' import { addPlaybackMetricValidator, apiRateLimiter, asyncMiddleware } from '../../middlewares'
import { CONFIG } from '@server/initializers/config'
const metricsRouter = express.Router() const metricsRouter = express.Router()
metricsRouter.use(apiRateLimiter)
metricsRouter.post('/playback', metricsRouter.post('/playback',
asyncMiddleware(addPlaybackMetricValidator), asyncMiddleware(addPlaybackMetricValidator),
addPlaybackMetric addPlaybackMetric

View File

@ -4,10 +4,12 @@ import { OAuthClientModel } from '@server/models/oauth/oauth-client'
import { HttpStatusCode, OAuthClientLocal } from '@shared/models' import { HttpStatusCode, OAuthClientLocal } from '@shared/models'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config' import { CONFIG } from '../../initializers/config'
import { asyncMiddleware, openapiOperationDoc } from '../../middlewares' import { apiRateLimiter, asyncMiddleware, openapiOperationDoc } from '../../middlewares'
const oauthClientsRouter = express.Router() const oauthClientsRouter = express.Router()
oauthClientsRouter.use(apiRateLimiter)
oauthClientsRouter.get('/local', oauthClientsRouter.get('/local',
openapiOperationDoc({ operationId: 'getOAuthClient' }), openapiOperationDoc({ operationId: 'getOAuthClient' }),
asyncMiddleware(getLocalClient) asyncMiddleware(getLocalClient)

View File

@ -2,16 +2,18 @@ import express from 'express'
import memoizee from 'memoizee' import memoizee from 'memoizee'
import { logger } from '@server/helpers/logger' import { logger } from '@server/helpers/logger'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
import { getServerActor } from '@server/models/application/application'
import { VideoModel } from '@server/models/video/video' import { VideoModel } from '@server/models/video/video'
import { CategoryOverview, ChannelOverview, TagOverview, VideosOverview } from '../../../shared/models/overviews' import { CategoryOverview, ChannelOverview, TagOverview, VideosOverview } from '../../../shared/models/overviews'
import { buildNSFWFilter } from '../../helpers/express-utils' import { buildNSFWFilter } from '../../helpers/express-utils'
import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants' import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants'
import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares' import { apiRateLimiter, asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares'
import { TagModel } from '../../models/video/tag' import { TagModel } from '../../models/video/tag'
import { getServerActor } from '@server/models/application/application'
const overviewsRouter = express.Router() const overviewsRouter = express.Router()
overviewsRouter.use(apiRateLimiter)
overviewsRouter.get('/videos', overviewsRouter.get('/videos',
videosOverviewValidator, videosOverviewValidator,
optionalAuthenticate, optionalAuthenticate,

View File

@ -4,6 +4,7 @@ import { getFormattedObjects } from '@server/helpers/utils'
import { listAvailablePluginsFromIndex } from '@server/lib/plugins/plugin-index' import { listAvailablePluginsFromIndex } from '@server/lib/plugins/plugin-index'
import { PluginManager } from '@server/lib/plugins/plugin-manager' import { PluginManager } from '@server/lib/plugins/plugin-manager'
import { import {
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,
availablePluginsSortValidator, availablePluginsSortValidator,
@ -35,6 +36,8 @@ import {
const pluginRouter = express.Router() const pluginRouter = express.Router()
pluginRouter.use(apiRateLimiter)
pluginRouter.get('/available', pluginRouter.get('/available',
openapiOperationDoc({ operationId: 'getAvailablePlugins' }), openapiOperationDoc({ operationId: 'getAvailablePlugins' }),
authenticate, authenticate,

View File

@ -6,6 +6,8 @@ import { runnerRegistrationTokensRouter } from './registration-tokens'
const runnersRouter = express.Router() const runnersRouter = express.Router()
// No api route limiter here, they are defined in child routers
runnersRouter.use('/', manageRunnersRouter) runnersRouter.use('/', manageRunnersRouter)
runnersRouter.use('/', runnerJobsRouter) runnersRouter.use('/', runnerJobsRouter)
runnersRouter.use('/', runnerJobFilesRouter) runnersRouter.use('/', runnerJobFilesRouter)

View File

@ -3,7 +3,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage' import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage'
import { VideoPathManager } from '@server/lib/video-path-manager' import { VideoPathManager } from '@server/lib/video-path-manager'
import { getStudioTaskFilePath } from '@server/lib/video-studio' import { getStudioTaskFilePath } from '@server/lib/video-studio'
import { asyncMiddleware } from '@server/middlewares' import { apiRateLimiter, asyncMiddleware } from '@server/middlewares'
import { jobOfRunnerGetValidator } from '@server/middlewares/validators/runners' import { jobOfRunnerGetValidator } from '@server/middlewares/validators/runners'
import { import {
runnerJobGetVideoStudioTaskFileValidator, runnerJobGetVideoStudioTaskFileValidator,
@ -16,18 +16,21 @@ const lTags = loggerTagsFactory('api', 'runner')
const runnerJobFilesRouter = express.Router() const runnerJobFilesRouter = express.Router()
runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/max-quality', runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/max-quality',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidator), asyncMiddleware(jobOfRunnerGetValidator),
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator), asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
asyncMiddleware(getMaxQualityVideoFile) asyncMiddleware(getMaxQualityVideoFile)
) )
runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/previews/max-quality', runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/previews/max-quality',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidator), asyncMiddleware(jobOfRunnerGetValidator),
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator), asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
getMaxQualityVideoPreview getMaxQualityVideoPreview
) )
runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/studio/task-files/:filename', runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/studio/task-files/:filename',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidator), asyncMiddleware(jobOfRunnerGetValidator),
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator), asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
runnerJobGetVideoStudioTaskFileValidator, runnerJobGetVideoStudioTaskFileValidator,

View File

@ -7,6 +7,7 @@ import { MIMETYPES } from '@server/initializers/constants'
import { sequelizeTypescript } from '@server/initializers/database' import { sequelizeTypescript } from '@server/initializers/database'
import { getRunnerJobHandlerClass, updateLastRunnerContact } from '@server/lib/runners' import { getRunnerJobHandlerClass, updateLastRunnerContact } from '@server/lib/runners'
import { import {
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,
ensureUserHasRight, ensureUserHasRight,
@ -69,11 +70,13 @@ const runnerJobsRouter = express.Router()
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
runnerJobsRouter.post('/jobs/request', runnerJobsRouter.post('/jobs/request',
apiRateLimiter,
asyncMiddleware(getRunnerFromTokenValidator), asyncMiddleware(getRunnerFromTokenValidator),
asyncMiddleware(requestRunnerJob) asyncMiddleware(requestRunnerJob)
) )
runnerJobsRouter.post('/jobs/:jobUUID/accept', runnerJobsRouter.post('/jobs/:jobUUID/accept',
apiRateLimiter,
asyncMiddleware(runnerJobGetValidator), asyncMiddleware(runnerJobGetValidator),
acceptRunnerJobValidator, acceptRunnerJobValidator,
asyncMiddleware(getRunnerFromTokenValidator), asyncMiddleware(getRunnerFromTokenValidator),
@ -81,6 +84,7 @@ runnerJobsRouter.post('/jobs/:jobUUID/accept',
) )
runnerJobsRouter.post('/jobs/:jobUUID/abort', runnerJobsRouter.post('/jobs/:jobUUID/abort',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidator), asyncMiddleware(jobOfRunnerGetValidator),
abortRunnerJobValidator, abortRunnerJobValidator,
asyncMiddleware(abortRunnerJob) asyncMiddleware(abortRunnerJob)
@ -88,6 +92,7 @@ runnerJobsRouter.post('/jobs/:jobUUID/abort',
runnerJobsRouter.post('/jobs/:jobUUID/update', runnerJobsRouter.post('/jobs/:jobUUID/update',
runnerJobUpdateVideoFiles, runnerJobUpdateVideoFiles,
apiRateLimiter, // Has to be after multer middleware to parse runner token
asyncMiddleware(jobOfRunnerGetValidator), asyncMiddleware(jobOfRunnerGetValidator),
updateRunnerJobValidator, updateRunnerJobValidator,
asyncMiddleware(updateRunnerJobController) asyncMiddleware(updateRunnerJobController)
@ -101,6 +106,7 @@ runnerJobsRouter.post('/jobs/:jobUUID/error',
runnerJobsRouter.post('/jobs/:jobUUID/success', runnerJobsRouter.post('/jobs/:jobUUID/success',
postRunnerJobSuccessVideoFiles, postRunnerJobSuccessVideoFiles,
apiRateLimiter, // Has to be after multer middleware to parse runner token
asyncMiddleware(jobOfRunnerGetValidator), asyncMiddleware(jobOfRunnerGetValidator),
successRunnerJobValidator, successRunnerJobValidator,
asyncMiddleware(postRunnerJobSuccess) asyncMiddleware(postRunnerJobSuccess)

View File

@ -2,6 +2,7 @@ import express from 'express'
import { logger, loggerTagsFactory } from '@server/helpers/logger' import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { generateRunnerToken } from '@server/helpers/token-generator' import { generateRunnerToken } from '@server/helpers/token-generator'
import { import {
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,
ensureUserHasRight, ensureUserHasRight,
@ -19,15 +20,18 @@ const lTags = loggerTagsFactory('api', 'runner')
const manageRunnersRouter = express.Router() const manageRunnersRouter = express.Router()
manageRunnersRouter.post('/register', manageRunnersRouter.post('/register',
apiRateLimiter,
asyncMiddleware(registerRunnerValidator), asyncMiddleware(registerRunnerValidator),
asyncMiddleware(registerRunner) asyncMiddleware(registerRunner)
) )
manageRunnersRouter.post('/unregister', manageRunnersRouter.post('/unregister',
apiRateLimiter,
asyncMiddleware(getRunnerFromTokenValidator), asyncMiddleware(getRunnerFromTokenValidator),
asyncMiddleware(unregisterRunner) asyncMiddleware(unregisterRunner)
) )
manageRunnersRouter.delete('/:runnerId', manageRunnersRouter.delete('/:runnerId',
apiRateLimiter,
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS), ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(deleteRunnerValidator), asyncMiddleware(deleteRunnerValidator),
@ -35,6 +39,7 @@ manageRunnersRouter.delete('/:runnerId',
) )
manageRunnersRouter.get('/', manageRunnersRouter.get('/',
apiRateLimiter,
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS), ensureUserHasRight(UserRight.MANAGE_RUNNERS),
paginationValidator, paginationValidator,

View File

@ -1,6 +1,8 @@
import express from 'express' import express from 'express'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { generateRunnerRegistrationToken } from '@server/helpers/token-generator' import { generateRunnerRegistrationToken } from '@server/helpers/token-generator'
import { import {
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,
ensureUserHasRight, ensureUserHasRight,
@ -12,19 +14,20 @@ import {
import { deleteRegistrationTokenValidator } from '@server/middlewares/validators/runners' import { deleteRegistrationTokenValidator } from '@server/middlewares/validators/runners'
import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token' import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token'
import { HttpStatusCode, ListRunnerRegistrationTokensQuery, UserRight } from '@shared/models' import { HttpStatusCode, ListRunnerRegistrationTokensQuery, UserRight } from '@shared/models'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
const lTags = loggerTagsFactory('api', 'runner') const lTags = loggerTagsFactory('api', 'runner')
const runnerRegistrationTokensRouter = express.Router() const runnerRegistrationTokensRouter = express.Router()
runnerRegistrationTokensRouter.post('/registration-tokens/generate', runnerRegistrationTokensRouter.post('/registration-tokens/generate',
apiRateLimiter,
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS), ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(generateRegistrationToken) asyncMiddleware(generateRegistrationToken)
) )
runnerRegistrationTokensRouter.delete('/registration-tokens/:id', runnerRegistrationTokensRouter.delete('/registration-tokens/:id',
apiRateLimiter,
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS), ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(deleteRegistrationTokenValidator), asyncMiddleware(deleteRegistrationTokenValidator),
@ -32,6 +35,7 @@ runnerRegistrationTokensRouter.delete('/registration-tokens/:id',
) )
runnerRegistrationTokensRouter.get('/registration-tokens', runnerRegistrationTokensRouter.get('/registration-tokens',
apiRateLimiter,
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS), ensureUserHasRight(UserRight.MANAGE_RUNNERS),
paginationValidator, paginationValidator,

View File

@ -1,10 +1,13 @@
import express from 'express' import express from 'express'
import { apiRateLimiter } from '@server/middlewares'
import { searchChannelsRouter } from './search-video-channels' import { searchChannelsRouter } from './search-video-channels'
import { searchPlaylistsRouter } from './search-video-playlists' import { searchPlaylistsRouter } from './search-video-playlists'
import { searchVideosRouter } from './search-videos' import { searchVideosRouter } from './search-videos'
const searchRouter = express.Router() const searchRouter = express.Router()
searchRouter.use(apiRateLimiter)
searchRouter.use('/', searchVideosRouter) searchRouter.use('/', searchVideosRouter)
searchRouter.use('/', searchChannelsRouter) searchRouter.use('/', searchChannelsRouter)
searchRouter.use('/', searchPlaylistsRouter) searchRouter.use('/', searchPlaylistsRouter)

View File

@ -1,4 +1,5 @@
import express from 'express' import express from 'express'
import { apiRateLimiter } from '@server/middlewares'
import { contactRouter } from './contact' import { contactRouter } from './contact'
import { debugRouter } from './debug' import { debugRouter } from './debug'
import { serverFollowsRouter } from './follows' import { serverFollowsRouter } from './follows'
@ -9,6 +10,8 @@ import { statsRouter } from './stats'
const serverRouter = express.Router() const serverRouter = express.Router()
serverRouter.use(apiRateLimiter)
serverRouter.use('/', serverFollowsRouter) serverRouter.use('/', serverFollowsRouter)
serverRouter.use('/', serverRedundancyRouter) serverRouter.use('/', serverRedundancyRouter)
serverRouter.use('/', statsRouter) serverRouter.use('/', statsRouter)

View File

@ -15,6 +15,7 @@ import { Redis } from '../../../lib/redis'
import { buildUser, createUserAccountAndChannelAndPlaylist } from '../../../lib/user' import { buildUser, createUserAccountAndChannelAndPlaylist } from '../../../lib/user'
import { import {
adminUsersSortValidator, adminUsersSortValidator,
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
asyncRetryTransactionMiddleware, asyncRetryTransactionMiddleware,
authenticate, authenticate,
@ -50,6 +51,9 @@ import { twoFactorRouter } from './two-factor'
const auditLogger = auditLoggerFactory('users') const auditLogger = auditLoggerFactory('users')
const usersRouter = express.Router() const usersRouter = express.Router()
usersRouter.use(apiRateLimiter)
usersRouter.use('/', emailVerificationRouter) usersRouter.use('/', emailVerificationRouter)
usersRouter.use('/', registrationsRouter) usersRouter.use('/', registrationsRouter)
usersRouter.use('/', twoFactorRouter) usersRouter.use('/', twoFactorRouter)

View File

@ -2,6 +2,7 @@ import express from 'express'
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelSyncAuditView } from '@server/helpers/audit-logger' import { auditLoggerFactory, getAuditIdFromRes, VideoChannelSyncAuditView } from '@server/helpers/audit-logger'
import { logger } from '@server/helpers/logger' import { logger } from '@server/helpers/logger'
import { import {
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
asyncRetryTransactionMiddleware, asyncRetryTransactionMiddleware,
authenticate, authenticate,
@ -17,6 +18,8 @@ import { HttpStatusCode, VideoChannelSyncState } from '@shared/models'
const videoChannelSyncRouter = express.Router() const videoChannelSyncRouter = express.Router()
const auditLogger = auditLoggerFactory('channel-syncs') const auditLogger = auditLoggerFactory('channel-syncs')
videoChannelSyncRouter.use(apiRateLimiter)
videoChannelSyncRouter.post('/', videoChannelSyncRouter.post('/',
authenticate, authenticate,
ensureSyncIsEnabled, ensureSyncIsEnabled,

View File

@ -19,6 +19,7 @@ import { JobQueue } from '../../lib/job-queue'
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor' import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor'
import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
import { import {
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
asyncRetryTransactionMiddleware, asyncRetryTransactionMiddleware,
authenticate, authenticate,
@ -57,6 +58,8 @@ const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_
const videoChannelRouter = express.Router() const videoChannelRouter = express.Router()
videoChannelRouter.use(apiRateLimiter)
videoChannelRouter.get('/', videoChannelRouter.get('/',
paginationValidator, paginationValidator,
videoChannelsSortValidator, videoChannelsSortValidator,

View File

@ -25,6 +25,7 @@ import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlayli
import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url'
import { updatePlaylistMiniatureFromExisting } from '../../lib/thumbnail' import { updatePlaylistMiniatureFromExisting } from '../../lib/thumbnail'
import { import {
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
asyncRetryTransactionMiddleware, asyncRetryTransactionMiddleware,
authenticate, authenticate,
@ -52,6 +53,8 @@ const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIM
const videoPlaylistRouter = express.Router() const videoPlaylistRouter = express.Router()
videoPlaylistRouter.use(apiRateLimiter)
videoPlaylistRouter.get('/privacies', listVideoPlaylistPrivacies) videoPlaylistRouter.get('/privacies', listVideoPlaylistPrivacies)
videoPlaylistRouter.get('/', videoPlaylistRouter.get('/',

View File

@ -15,6 +15,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
import { JobQueue } from '../../../lib/job-queue' import { JobQueue } from '../../../lib/job-queue'
import { Hooks } from '../../../lib/plugins/hooks' import { Hooks } from '../../../lib/plugins/hooks'
import { import {
apiRateLimiter,
asyncMiddleware, asyncMiddleware,
asyncRetryTransactionMiddleware, asyncRetryTransactionMiddleware,
authenticate, authenticate,
@ -50,6 +51,8 @@ import { viewRouter } from './view'
const auditLogger = auditLoggerFactory('videos') const auditLogger = auditLoggerFactory('videos')
const videosRouter = express.Router() const videosRouter = express.Router()
videosRouter.use(apiRateLimiter)
videosRouter.use('/', blacklistRouter) videosRouter.use('/', blacklistRouter)
videosRouter.use('/', statsRouter) videosRouter.use('/', statsRouter)
videosRouter.use('/', rateVideoRouter) videosRouter.use('/', rateVideoRouter)

View File

@ -1,5 +1,6 @@
import express from 'express' import express from 'express'
import RateLimit, { Options as RateLimitHandlerOptions } from 'express-rate-limit' import RateLimit, { Options as RateLimitHandlerOptions } from 'express-rate-limit'
import { CONFIG } from '@server/initializers/config'
import { RunnerModel } from '@server/models/runner/runner' import { RunnerModel } from '@server/models/runner/runner'
import { UserRole } from '@shared/models' import { UserRole } from '@shared/models'
import { optionalAuthenticate } from './auth' import { optionalAuthenticate } from './auth'
@ -39,6 +40,11 @@ export function buildRateLimiter (options: {
}) })
} }
export const apiRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
max: CONFIG.RATES_LIMIT.API.MAX
})
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Private // Private
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -14,7 +14,6 @@ import {
import { import {
cleanupTests, cleanupTests,
createSingleServer, createSingleServer,
makePostBodyRequest,
PeerTubeServer, PeerTubeServer,
setAccessTokensToServers, setAccessTokensToServers,
setDefaultVideoChannel, setDefaultVideoChannel,
@ -641,24 +640,47 @@ describe('Test runner common actions', function () {
}) })
}) })
it('Should rate limit an unknown runner', async function () { it('Should rate limit an unknown runner, but not a registered one', async function () {
const path = '/api/v1/ping' this.timeout(60000)
const fields = { runnerToken: 'toto' }
await server.videos.quickUpload({ name: 'video' })
await waitJobs([ server ])
const { job } = await server.runnerJobs.autoAccept({ runnerToken })
for (let i = 0; i < 20; i++) { for (let i = 0; i < 20; i++) {
try { try {
await makePostBodyRequest({ url: server.url, path, fields, expectedStatus: HttpStatusCode.OK_200 }) await server.runnerJobs.request({ runnerToken })
await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
} catch {} } catch {}
} }
await makePostBodyRequest({ url: server.url, path, fields, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) // Invalid
}) {
await server.runnerJobs.request({ runnerToken: 'toto', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
await server.runnerJobs.update({
runnerToken: 'toto',
jobToken: job.jobToken,
jobUUID: job.uuid,
expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
})
}
it('Should not rate limit a registered runner', async function () { // Not provided
const path = '/api/v1/ping' {
await server.runnerJobs.request({ runnerToken: undefined, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
await server.runnerJobs.update({
runnerToken: undefined,
jobToken: job.jobToken,
jobUUID: job.uuid,
expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
})
}
for (let i = 0; i < 20; i++) { // Registered
await makePostBodyRequest({ url: server.url, path, fields: { runnerToken }, expectedStatus: HttpStatusCode.OK_200 }) {
await server.runnerJobs.request({ runnerToken })
await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
} }
}) })
}) })