diff --git a/config/default.yaml b/config/default.yaml index 10d3f79e7c2..53d8c45de8e 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -36,6 +36,26 @@ rates_limit: # 10 attempts in 10 min window: 10 minutes max: 10 + plugins: + # 500 attempts in 10 seconds (we also serve plugin static files) + window: 10 seconds + max: 500 + well_known: + # 200 attempts in 10 seconds + window: 10 seconds + max: 200 + feeds: + # 50 attempts in 10 seconds + window: 10 seconds + max: 50 + activity_pub: + # 500 attempts in 10 seconds (we can have many AP requests) + window: 10 seconds + max: 500 + client: # HTML files generated by PeerTube + # 500 attempts in 10 seconds (to not break crawlers) + window: 10 seconds + max: 500 oauth2: token_lifetime: diff --git a/config/production.yaml.example b/config/production.yaml.example index a829b46f92b..87ef2b67604 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -34,6 +34,26 @@ rates_limit: # 10 attempts in 10 min window: 10 minutes max: 10 + plugins: + # 500 attempts in 10 seconds (we also serve plugin static files) + window: 10 seconds + max: 500 + well_known: + # 200 attempts in 10 seconds + window: 10 seconds + max: 200 + feeds: + # 50 attempts in 10 seconds + window: 10 seconds + max: 50 + activity_pub: + # 500 attempts in 10 seconds (we can have many AP requests) + window: 10 seconds + max: 500 + client: # HTML files generated by PeerTube + # 500 attempts in 10 seconds (to not break crawlers) + window: 10 seconds + max: 500 oauth2: token_lifetime: diff --git a/server.ts b/server.ts index e25322b66e3..8e358716e4e 100644 --- a/server.ts +++ b/server.ts @@ -115,7 +115,7 @@ import { pluginsRouter, trackerRouter, createWebsocketTrackerServer, - botsRouter, + sitemapRouter, downloadRouter } from './server/controllers' import { advertiseDoNotTrack } from './server/middlewares/dnt' @@ -222,9 +222,7 @@ OpenTelemetryMetrics.Instance.init(app) // ----------- Views, routes and static files ----------- -// API -const apiRoute = '/api/' + API_VERSION -app.use(apiRoute, apiRouter) +app.use('/api/' + API_VERSION, apiRouter) // Services (oembed...) app.use('/services', servicesRouter) @@ -235,7 +233,7 @@ app.use('/', pluginsRouter) app.use('/', activityPubRouter) app.use('/', feedsRouter) app.use('/', trackerRouter) -app.use('/', botsRouter) +app.use('/', sitemapRouter) // Static files app.use('/', staticRouter) diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index c47c61f52a0..be52f1662e3 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -19,6 +19,7 @@ import { getLocalVideoSharesActivityPubUrl } from '../../lib/activitypub/url' import { + activityPubRateLimiter, asyncMiddleware, ensureIsLocalChannel, executeIfActivityPub, @@ -47,32 +48,38 @@ activityPubClientRouter.use(cors()) activityPubClientRouter.get( [ '/accounts?/:name', '/accounts?/:name/video-channels', '/a/:name', '/a/:name/video-channels' ], executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(localAccountValidator), asyncMiddleware(accountController) ) activityPubClientRouter.get('/accounts?/:name/followers', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(localAccountValidator), asyncMiddleware(accountFollowersController) ) activityPubClientRouter.get('/accounts?/:name/following', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(localAccountValidator), asyncMiddleware(accountFollowingController) ) activityPubClientRouter.get('/accounts?/:name/playlists', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(localAccountValidator), asyncMiddleware(accountPlaylistsController) ) activityPubClientRouter.get('/accounts?/:name/likes/:videoId', executeIfActivityPub, + activityPubRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS), asyncMiddleware(getAccountVideoRateValidatorFactory('like')), asyncMiddleware(getAccountVideoRateFactory('like')) ) activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', executeIfActivityPub, + activityPubRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS), asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')), asyncMiddleware(getAccountVideoRateFactory('dislike')) @@ -81,47 +88,56 @@ activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', activityPubClientRouter.get( [ '/videos/watch/:id', '/w/:id' ], executeIfActivityPub, + activityPubRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS), asyncMiddleware(videosCustomGetValidator('all')), asyncMiddleware(videoController) ) activityPubClientRouter.get('/videos/watch/:id/activity', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videosCustomGetValidator('all')), asyncMiddleware(videoController) ) activityPubClientRouter.get('/videos/watch/:id/announces', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), asyncMiddleware(videoAnnouncesController) ) activityPubClientRouter.get('/videos/watch/:id/announces/:actorId', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videosShareValidator), asyncMiddleware(videoAnnounceController) ) activityPubClientRouter.get('/videos/watch/:id/likes', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), asyncMiddleware(videoLikesController) ) activityPubClientRouter.get('/videos/watch/:id/dislikes', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), asyncMiddleware(videoDislikesController) ) activityPubClientRouter.get('/videos/watch/:id/comments', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), asyncMiddleware(videoCommentsController) ) activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videoCommentGetValidator), asyncMiddleware(videoCommentController) ) activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videoCommentGetValidator), asyncMiddleware(videoCommentController) ) @@ -129,24 +145,28 @@ activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity activityPubClientRouter.get( [ '/video-channels/:nameWithHost', '/video-channels/:nameWithHost/videos', '/c/:nameWithHost', '/c/:nameWithHost/videos' ], executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videoChannelsNameWithHostValidator), ensureIsLocalChannel, asyncMiddleware(videoChannelController) ) activityPubClientRouter.get('/video-channels/:nameWithHost/followers', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videoChannelsNameWithHostValidator), ensureIsLocalChannel, asyncMiddleware(videoChannelFollowersController) ) activityPubClientRouter.get('/video-channels/:nameWithHost/following', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videoChannelsNameWithHostValidator), ensureIsLocalChannel, asyncMiddleware(videoChannelFollowingController) ) activityPubClientRouter.get('/video-channels/:nameWithHost/playlists', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videoChannelsNameWithHostValidator), ensureIsLocalChannel, asyncMiddleware(videoChannelPlaylistsController) @@ -154,11 +174,13 @@ activityPubClientRouter.get('/video-channels/:nameWithHost/playlists', activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videoFileRedundancyGetValidator), asyncMiddleware(videoRedundancyController) ) activityPubClientRouter.get('/redundancy/streaming-playlists/:streamingPlaylistType/:videoId', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videoPlaylistRedundancyGetValidator), asyncMiddleware(videoRedundancyController) ) @@ -166,17 +188,20 @@ activityPubClientRouter.get('/redundancy/streaming-playlists/:streamingPlaylistT activityPubClientRouter.get( [ '/video-playlists/:playlistId', '/videos/watch/playlist/:playlistId', '/w/p/:playlistId' ], executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videoPlaylistsGetValidator('all')), asyncMiddleware(videoPlaylistController) ) activityPubClientRouter.get('/video-playlists/:playlistId/videos/:playlistElementId', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(videoPlaylistElementAPGetValidator), asyncMiddleware(videoPlaylistElementController) ) activityPubClientRouter.get('/videos/local-viewer/:localViewerId', executeIfActivityPub, + activityPubRateLimiter, asyncMiddleware(getVideoLocalViewerValidator), asyncMiddleware(getVideoLocalViewerController) ) diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts index 66a38e05569..862c7baf121 100644 --- a/server/controllers/activitypub/inbox.ts +++ b/server/controllers/activitypub/inbox.ts @@ -5,6 +5,7 @@ import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' import { logger } from '../../helpers/logger' import { + activityPubRateLimiter, asyncMiddleware, checkSignature, ensureIsLocalChannel, @@ -17,6 +18,7 @@ import { activityPubValidator } from '../../middlewares/validators/activitypub/a const inboxRouter = express.Router() inboxRouter.post('/inbox', + activityPubRateLimiter, signatureValidator, asyncMiddleware(checkSignature), asyncMiddleware(activityPubValidator), @@ -24,13 +26,16 @@ inboxRouter.post('/inbox', ) inboxRouter.post('/accounts/:name/inbox', + activityPubRateLimiter, signatureValidator, asyncMiddleware(checkSignature), asyncMiddleware(localAccountValidator), asyncMiddleware(activityPubValidator), inboxController ) + inboxRouter.post('/video-channels/:nameWithHost/inbox', + activityPubRateLimiter, signatureValidator, asyncMiddleware(checkSignature), asyncMiddleware(videoChannelsNameWithHostValidator), diff --git a/server/controllers/activitypub/index.ts b/server/controllers/activitypub/index.ts index 8c681820adc..c14d95108ee 100644 --- a/server/controllers/activitypub/index.ts +++ b/server/controllers/activitypub/index.ts @@ -1,4 +1,5 @@ import express from 'express' + import { activityPubClientRouter } from './client' import { inboxRouter } from './inbox' import { outboxRouter } from './outbox' diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts index 4175cf2765f..8c88b6971e8 100644 --- a/server/controllers/activitypub/outbox.ts +++ b/server/controllers/activitypub/outbox.ts @@ -7,7 +7,13 @@ import { VideoPrivacy } from '../../../shared/models/videos' import { logger } from '../../helpers/logger' import { buildAudience } from '../../lib/activitypub/audience' import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send' -import { asyncMiddleware, ensureIsLocalChannel, localAccountValidator, videoChannelsNameWithHostValidator } from '../../middlewares' +import { + activityPubRateLimiter, + asyncMiddleware, + ensureIsLocalChannel, + localAccountValidator, + videoChannelsNameWithHostValidator +} from '../../middlewares' import { apPaginationValidator } from '../../middlewares/validators/activitypub' import { VideoModel } from '../../models/video/video' import { activityPubResponse } from './utils' @@ -15,12 +21,14 @@ import { activityPubResponse } from './utils' const outboxRouter = express.Router() outboxRouter.get('/accounts/:name/outbox', + activityPubRateLimiter, apPaginationValidator, localAccountValidator, asyncMiddleware(outboxController) ) outboxRouter.get('/video-channels/:nameWithHost/outbox', + activityPubRateLimiter, apPaginationValidator, asyncMiddleware(videoChannelsNameWithHostValidator), ensureIsLocalChannel, diff --git a/server/controllers/client.ts b/server/controllers/client.ts index a85c1072019..2d0c499042d 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts @@ -5,27 +5,53 @@ import { join } from 'path' import { logger } from '@server/helpers/logger' import { CONFIG } from '@server/initializers/config' import { Hooks } from '@server/lib/plugins/hooks' +import { root } from '@shared/core-utils' import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n' import { HttpStatusCode } from '@shared/models' -import { root } from '@shared/core-utils' import { STATIC_MAX_AGE } from '../initializers/constants' import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html' -import { asyncMiddleware, embedCSP } from '../middlewares' +import { asyncMiddleware, buildRateLimiter, embedCSP } from '../middlewares' const clientsRouter = express.Router() +const clientsRateLimiter = buildRateLimiter({ + windowMs: CONFIG.RATES_LIMIT.CLIENT.WINDOW_MS, + max: CONFIG.RATES_LIMIT.CLIENT.MAX +}) + const distPath = join(root(), 'client', 'dist') const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html') // Special route that add OpenGraph and oEmbed tags // Do not use a template engine for a so little thing -clientsRouter.use([ '/w/p/:id', '/videos/watch/playlist/:id' ], asyncMiddleware(generateWatchPlaylistHtmlPage)) -clientsRouter.use([ '/w/:id', '/videos/watch/:id' ], asyncMiddleware(generateWatchHtmlPage)) -clientsRouter.use([ '/accounts/:nameWithHost', '/a/:nameWithHost' ], asyncMiddleware(generateAccountHtmlPage)) -clientsRouter.use([ '/video-channels/:nameWithHost', '/c/:nameWithHost' ], asyncMiddleware(generateVideoChannelHtmlPage)) -clientsRouter.use('/@:nameWithHost', asyncMiddleware(generateActorHtmlPage)) +clientsRouter.use([ '/w/p/:id', '/videos/watch/playlist/:id' ], + clientsRateLimiter, + asyncMiddleware(generateWatchPlaylistHtmlPage) +) + +clientsRouter.use([ '/w/:id', '/videos/watch/:id' ], + clientsRateLimiter, + asyncMiddleware(generateWatchHtmlPage) +) + +clientsRouter.use([ '/accounts/:nameWithHost', '/a/:nameWithHost' ], + clientsRateLimiter, + asyncMiddleware(generateAccountHtmlPage) +) + +clientsRouter.use([ '/video-channels/:nameWithHost', '/c/:nameWithHost' ], + clientsRateLimiter, + asyncMiddleware(generateVideoChannelHtmlPage) +) + +clientsRouter.use('/@:nameWithHost', + clientsRateLimiter, + asyncMiddleware(generateActorHtmlPage) +) const embedMiddlewares = [ + clientsRateLimiter, + CONFIG.CSP.ENABLED ? embedCSP : (req: express.Request, res: express.Response, next: express.NextFunction) => next(), @@ -48,11 +74,11 @@ clientsRouter.use('/video-playlists/embed', ...embedMiddlewares) const testEmbedController = (req: express.Request, res: express.Response) => res.sendFile(testEmbedPath) -clientsRouter.use('/videos/test-embed', testEmbedController) -clientsRouter.use('/video-playlists/test-embed', testEmbedController) +clientsRouter.use('/videos/test-embed', clientsRateLimiter, testEmbedController) +clientsRouter.use('/video-playlists/test-embed', clientsRateLimiter, testEmbedController) // Dynamic PWA manifest -clientsRouter.get('/manifest.webmanifest', asyncMiddleware(generateManifest)) +clientsRouter.get('/manifest.webmanifest', clientsRateLimiter, asyncMiddleware(generateManifest)) // Static client overrides // Must be consistent with static client overrides redirections in /support/nginx/peertube @@ -88,7 +114,10 @@ clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => // Always serve index client page (the client is a single page application, let it handle routing) // Try to provide the right language index.html -clientsRouter.use('/(:language)?', asyncMiddleware(serveIndexHTML)) +clientsRouter.use('/(:language)?', + clientsRateLimiter, + asyncMiddleware(serveIndexHTML) +) // --------------------------------------------------------------------------- diff --git a/server/controllers/feeds/comment-feeds.ts b/server/controllers/feeds/comment-feeds.ts index bdc53b51f51..68dc9ef905b 100644 --- a/server/controllers/feeds/comment-feeds.ts +++ b/server/controllers/feeds/comment-feeds.ts @@ -23,7 +23,7 @@ const { middleware: cacheRouteMiddleware } = cacheRouteFactory({ // --------------------------------------------------------------------------- -commentFeedsRouter.get('/feeds/video-comments.:format', +commentFeedsRouter.get('/video-comments.:format', feedsFormatValidator, setFeedFormatContentType, cacheRouteMiddleware(ROUTE_CACHE_LIFETIME.FEEDS), diff --git a/server/controllers/feeds/index.ts b/server/controllers/feeds/index.ts index e344a1448c4..19352318d65 100644 --- a/server/controllers/feeds/index.ts +++ b/server/controllers/feeds/index.ts @@ -1,13 +1,22 @@ import express from 'express' +import { CONFIG } from '@server/initializers/config' +import { buildRateLimiter } from '@server/middlewares' import { commentFeedsRouter } from './comment-feeds' import { videoFeedsRouter } from './video-feeds' import { videoPodcastFeedsRouter } from './video-podcast-feeds' const feedsRouter = express.Router() -feedsRouter.use('/', commentFeedsRouter) -feedsRouter.use('/', videoFeedsRouter) -feedsRouter.use('/', videoPodcastFeedsRouter) +const feedsRateLimiter = buildRateLimiter({ + windowMs: CONFIG.RATES_LIMIT.FEEDS.WINDOW_MS, + max: CONFIG.RATES_LIMIT.FEEDS.MAX +}) + +feedsRouter.use('/feeds', feedsRateLimiter) + +feedsRouter.use('/feeds', commentFeedsRouter) +feedsRouter.use('/feeds', videoFeedsRouter) +feedsRouter.use('/feeds', videoPodcastFeedsRouter) // --------------------------------------------------------------------------- diff --git a/server/controllers/feeds/video-feeds.ts b/server/controllers/feeds/video-feeds.ts index b6e0663eb3c..97ac594ecdd 100644 --- a/server/controllers/feeds/video-feeds.ts +++ b/server/controllers/feeds/video-feeds.ts @@ -26,7 +26,7 @@ const { middleware: cacheRouteMiddleware } = cacheRouteFactory({ // --------------------------------------------------------------------------- -videoFeedsRouter.get('/feeds/videos.:format', +videoFeedsRouter.get('/videos.:format', videosSortValidator, setDefaultVideosSort, feedsFormatValidator, @@ -37,7 +37,7 @@ videoFeedsRouter.get('/feeds/videos.:format', asyncMiddleware(generateVideoFeed) ) -videoFeedsRouter.get('/feeds/subscriptions.:format', +videoFeedsRouter.get('/subscriptions.:format', videosSortValidator, setDefaultVideosSort, feedsFormatValidator, diff --git a/server/controllers/feeds/video-podcast-feeds.ts b/server/controllers/feeds/video-podcast-feeds.ts index bd399580eba..fca82ba6871 100644 --- a/server/controllers/feeds/video-podcast-feeds.ts +++ b/server/controllers/feeds/video-podcast-feeds.ts @@ -40,7 +40,7 @@ for (const event of ([ 'channel-updated', 'channel-deleted' ] as const)) { // --------------------------------------------------------------------------- -videoPodcastFeedsRouter.get('/feeds/podcast/videos.xml', +videoPodcastFeedsRouter.get('/podcast/videos.xml', setFeedPodcastContentType, videoFeedsPodcastSetCacheKey, podcastCacheRouteMiddleware(ROUTE_CACHE_LIFETIME.FEEDS), diff --git a/server/controllers/index.ts b/server/controllers/index.ts index 09d657aca4b..8a647aff16b 100644 --- a/server/controllers/index.ts +++ b/server/controllers/index.ts @@ -1,6 +1,6 @@ export * from './activitypub' export * from './api' -export * from './bots' +export * from './sitemap' export * from './client' export * from './download' export * from './feeds' diff --git a/server/controllers/misc.ts b/server/controllers/misc.ts index 163352ac50f..a7dfc7867ea 100644 --- a/server/controllers/misc.ts +++ b/server/controllers/misc.ts @@ -7,7 +7,7 @@ import { HttpStatusCode } from '@shared/models' import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo/nodeinfo.model' import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION, ROUTE_CACHE_LIFETIME } from '../initializers/constants' import { getThemeOrDefault } from '../lib/plugins/theme-utils' -import { asyncMiddleware } from '../middlewares' +import { apiRateLimiter, asyncMiddleware } from '../middlewares' import { cacheRoute } from '../middlewares/cache/cache' import { UserModel } from '../models/user/user' import { VideoModel } from '../models/video/video' @@ -18,12 +18,14 @@ const miscRouter = express.Router() miscRouter.use(cors()) miscRouter.use('/nodeinfo/:version.json', + apiRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.NODEINFO), asyncMiddleware(generateNodeinfo) ) // robots.txt service miscRouter.get('/robots.txt', + apiRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.ROBOTS), (_, res: express.Response) => { res.type('text/plain') @@ -33,12 +35,14 @@ miscRouter.get('/robots.txt', ) miscRouter.all('/teapot', + apiRateLimiter, getCup, asyncMiddleware(serveIndexHTML) ) // security.txt service miscRouter.get('/security.txt', + apiRateLimiter, (_, res: express.Response) => { return res.redirect(HttpStatusCode.MOVED_PERMANENTLY_301, '/.well-known/security.txt') } diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts index 51db1ad89f4..f0491b16ab2 100644 --- a/server/controllers/plugins.ts +++ b/server/controllers/plugins.ts @@ -1,6 +1,8 @@ import express from 'express' import { join } from 'path' import { logger } from '@server/helpers/logger' +import { CONFIG } from '@server/initializers/config' +import { buildRateLimiter } from '@server/middlewares' import { optionalAuthenticate } from '@server/middlewares/auth' import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' import { HttpStatusCode } from '../../shared/models/http/http-error-codes' @@ -18,57 +20,72 @@ const sendFileOptions = { const pluginsRouter = express.Router() +const pluginsRateLimiter = buildRateLimiter({ + windowMs: CONFIG.RATES_LIMIT.PLUGINS.WINDOW_MS, + max: CONFIG.RATES_LIMIT.PLUGINS.MAX +}) + pluginsRouter.get('/plugins/global.css', + pluginsRateLimiter, servePluginGlobalCSS ) pluginsRouter.get('/plugins/translations/:locale.json', + pluginsRateLimiter, getPluginTranslations ) pluginsRouter.get('/plugins/:pluginName/:pluginVersion/auth/:authName', + pluginsRateLimiter, getPluginValidator(PluginType.PLUGIN), getExternalAuthValidator, handleAuthInPlugin ) pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)', + pluginsRateLimiter, getPluginValidator(PluginType.PLUGIN), pluginStaticDirectoryValidator, servePluginStaticDirectory ) pluginsRouter.get('/plugins/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)', + pluginsRateLimiter, getPluginValidator(PluginType.PLUGIN), pluginStaticDirectoryValidator, servePluginClientScripts ) pluginsRouter.use('/plugins/:pluginName/router', + pluginsRateLimiter, getPluginValidator(PluginType.PLUGIN, false), optionalAuthenticate, servePluginCustomRoutes ) pluginsRouter.use('/plugins/:pluginName/:pluginVersion/router', + pluginsRateLimiter, getPluginValidator(PluginType.PLUGIN), optionalAuthenticate, servePluginCustomRoutes ) pluginsRouter.get('/themes/:pluginName/:pluginVersion/static/:staticEndpoint(*)', + pluginsRateLimiter, getPluginValidator(PluginType.THEME), pluginStaticDirectoryValidator, servePluginStaticDirectory ) pluginsRouter.get('/themes/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)', + pluginsRateLimiter, getPluginValidator(PluginType.THEME), pluginStaticDirectoryValidator, servePluginClientScripts ) pluginsRouter.get('/themes/:themeName/:themeVersion/css/:staticEndpoint(*)', + pluginsRateLimiter, serveThemeCSSValidator, serveThemeCSSDirectory ) diff --git a/server/controllers/services.ts b/server/controllers/services.ts index 7c7ca1ff366..0fd63a30f6c 100644 --- a/server/controllers/services.ts +++ b/server/controllers/services.ts @@ -2,17 +2,19 @@ import express from 'express' import { MChannelSummary } from '@server/types/models' import { escapeHTML } from '@shared/core-utils/renderer' import { EMBED_SIZE, PREVIEWS_SIZE, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants' -import { asyncMiddleware, oembedValidator } from '../middlewares' +import { apiRateLimiter, asyncMiddleware, oembedValidator } from '../middlewares' import { accountNameWithHostGetValidator } from '../middlewares/validators' import { forceNumber } from '@shared/core-utils' const servicesRouter = express.Router() servicesRouter.use('/oembed', + apiRateLimiter, asyncMiddleware(oembedValidator), generateOEmbed ) servicesRouter.use('/redirect/accounts/:accountName', + apiRateLimiter, asyncMiddleware(accountNameWithHostGetValidator), redirectToAccountUrl ) diff --git a/server/controllers/bots.ts b/server/controllers/sitemap.ts similarity index 93% rename from server/controllers/bots.ts rename to server/controllers/sitemap.ts index 2b825a73093..07f4c554e30 100644 --- a/server/controllers/bots.ts +++ b/server/controllers/sitemap.ts @@ -5,17 +5,16 @@ import { logger } from '@server/helpers/logger' import { getServerActor } from '@server/models/application/application' import { buildNSFWFilter } from '../helpers/express-utils' import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' -import { asyncMiddleware } from '../middlewares' +import { apiRateLimiter, asyncMiddleware } from '../middlewares' import { cacheRoute } from '../middlewares/cache/cache' import { AccountModel } from '../models/account/account' import { VideoModel } from '../models/video/video' import { VideoChannelModel } from '../models/video/video-channel' -const botsRouter = express.Router() +const sitemapRouter = express.Router() -// Special route that add OpenGraph and oEmbed tags -// Do not use a template engine for a so little thing -botsRouter.use('/sitemap.xml', +sitemapRouter.use('/sitemap.xml', + apiRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP), asyncMiddleware(getSitemap) ) @@ -23,7 +22,7 @@ botsRouter.use('/sitemap.xml', // --------------------------------------------------------------------------- export { - botsRouter + sitemapRouter } // --------------------------------------------------------------------------- diff --git a/server/controllers/well-known.ts b/server/controllers/well-known.ts index bb9acfb372b..322cf6ea204 100644 --- a/server/controllers/well-known.ts +++ b/server/controllers/well-known.ts @@ -1,7 +1,7 @@ import cors from 'cors' import express from 'express' import { join } from 'path' -import { asyncMiddleware, handleStaticError, webfingerValidator } from '@server/middlewares' +import { asyncMiddleware, buildRateLimiter, handleStaticError, webfingerValidator } from '@server/middlewares' import { root } from '@shared/core-utils' import { CONFIG } from '../initializers/config' import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' @@ -9,14 +9,21 @@ import { cacheRoute } from '../middlewares/cache/cache' const wellKnownRouter = express.Router() +const wellKnownRateLimiter = buildRateLimiter({ + windowMs: CONFIG.RATES_LIMIT.WELL_KNOWN.WINDOW_MS, + max: CONFIG.RATES_LIMIT.WELL_KNOWN.MAX +}) + wellKnownRouter.use(cors()) wellKnownRouter.get('/.well-known/webfinger', + wellKnownRateLimiter, asyncMiddleware(webfingerValidator), webfingerController ) wellKnownRouter.get('/.well-known/security.txt', + wellKnownRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.SECURITYTXT), (_, res: express.Response) => { res.type('text/plain') @@ -26,6 +33,7 @@ wellKnownRouter.get('/.well-known/security.txt', // nodeinfo service wellKnownRouter.use('/.well-known/nodeinfo', + wellKnownRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.NODEINFO), (_, res: express.Response) => { return res.json({ @@ -41,6 +49,7 @@ wellKnownRouter.use('/.well-known/nodeinfo', // dnt-policy.txt service (see https://www.eff.org/dnt-policy) wellKnownRouter.use('/.well-known/dnt-policy.txt', + wellKnownRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.DNT_POLICY), (_, res: express.Response) => { res.type('text/plain') @@ -51,18 +60,21 @@ wellKnownRouter.use('/.well-known/dnt-policy.txt', // dnt service (see https://www.w3.org/TR/tracking-dnt/#status-resource) wellKnownRouter.use('/.well-known/dnt/', + wellKnownRateLimiter, (_, res: express.Response) => { res.json({ tracking: 'N' }) } ) wellKnownRouter.use('/.well-known/change-password', + wellKnownRateLimiter, (_, res: express.Response) => { res.redirect('/my-account/settings') } ) wellKnownRouter.use('/.well-known/host-meta', + wellKnownRateLimiter, (_, res: express.Response) => { res.type('application/xml') @@ -76,6 +88,7 @@ wellKnownRouter.use('/.well-known/host-meta', ) wellKnownRouter.use('/.well-known/', + wellKnownRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.WELL_KNOWN), express.static(CONFIG.STORAGE.WELL_KNOWN_DIR, { fallthrough: false }), handleStaticError diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index f77b0defb71..0139ded4f0c 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts @@ -56,7 +56,11 @@ function checkMissedConfig () { 'followers.instance.enabled', 'followers.instance.manual_approval', 'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces', 'history.videos.max_age', 'views.videos.remote.max_age', 'views.videos.local_buffer_update_interval', 'views.videos.ip_view_expiration', - 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max', + 'rates_limit.api.window', 'rates_limit.api.max', 'rates_limit.login.window', 'rates_limit.login.max', + 'rates_limit.signup.window', 'rates_limit.signup.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max', + 'rates_limit.receive_client_log.window', 'rates_limit.receive_client_log.max', 'rates_limit.plugins.window', 'rates_limit.plugins.max', + 'rates_limit.well_known.window', 'rates_limit.well_known.max', 'rates_limit.feeds.window', 'rates_limit.feeds.max', + 'rates_limit.activity_pub.window', 'rates_limit.activity_pub.max', 'rates_limit.client.window', 'rates_limit.client.max', 'static_files.private_files_require_auth', 'object_storage.enabled', 'object_storage.endpoint', 'object_storage.region', 'object_storage.upload_acl.public', 'object_storage.upload_acl.private', 'object_storage.proxy.proxify_private_files', 'object_storage.credentials.access_key_id', diff --git a/server/initializers/config.ts b/server/initializers/config.ts index f12d9b85a54..2724990c1ae 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -183,6 +183,26 @@ const CONFIG = { ASK_SEND_EMAIL: { WINDOW_MS: parseDurationToMs(config.get('rates_limit.ask_send_email.window')), MAX: config.get('rates_limit.ask_send_email.max') + }, + PLUGINS: { + WINDOW_MS: parseDurationToMs(config.get('rates_limit.plugins.window')), + MAX: config.get('rates_limit.plugins.max') + }, + WELL_KNOWN: { + WINDOW_MS: parseDurationToMs(config.get('rates_limit.well_known.window')), + MAX: config.get('rates_limit.well_known.max') + }, + FEEDS: { + WINDOW_MS: parseDurationToMs(config.get('rates_limit.feeds.window')), + MAX: config.get('rates_limit.feeds.max') + }, + ACTIVITY_PUB: { + WINDOW_MS: parseDurationToMs(config.get('rates_limit.activity_pub.window')), + MAX: config.get('rates_limit.activity_pub.max') + }, + CLIENT: { + WINDOW_MS: parseDurationToMs(config.get('rates_limit.client.window')), + MAX: config.get('rates_limit.client.max') } }, TRUST_PROXY: config.get('trust_proxy'), diff --git a/server/middlewares/rate-limiter.ts b/server/middlewares/rate-limiter.ts index 8257965dd35..143d4363251 100644 --- a/server/middlewares/rate-limiter.ts +++ b/server/middlewares/rate-limiter.ts @@ -45,6 +45,11 @@ export const apiRateLimiter = buildRateLimiter({ max: CONFIG.RATES_LIMIT.API.MAX }) +export const activityPubRateLimiter = buildRateLimiter({ + windowMs: CONFIG.RATES_LIMIT.ACTIVITY_PUB.WINDOW_MS, + max: CONFIG.RATES_LIMIT.ACTIVITY_PUB.MAX +}) + // --------------------------------------------------------------------------- // Private // --------------------------------------------------------------------------- diff --git a/server/tests/api/check-params/video-source.ts b/server/tests/api/check-params/video-source.ts index 3c641ccd3ec..767590d5ec1 100644 --- a/server/tests/api/check-params/video-source.ts +++ b/server/tests/api/check-params/video-source.ts @@ -114,7 +114,7 @@ describe('Test video sources API validator', function () { await server.videos.replaceSourceFile({ fixture: 'video_short_fake.webm', videoId, - expectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 + completedExpectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 }) await server.videos.replaceSourceFile({ diff --git a/server/tests/api/search/search-activitypub-video-playlists.ts b/server/tests/api/search/search-activitypub-video-playlists.ts index 25b16207488..2bb5d869ac6 100644 --- a/server/tests/api/search/search-activitypub-video-playlists.ts +++ b/server/tests/api/search/search-activitypub-video-playlists.ts @@ -23,7 +23,7 @@ describe('Test ActivityPub playlists search', function () { let command: SearchCommand before(async function () { - this.timeout(120000) + this.timeout(240000) servers = await createMultipleServers(2)