diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index c5353023b..ccae5a151 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -34,7 +34,8 @@ export class ServerService { }, serverVersion: 'Unknown', signup: { - allowed: false + allowed: false, + allowedForCurrentIP: false }, transcoding: { enabledResolutions: [] diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 4c35bb3a5..69216e215 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts @@ -52,7 +52,8 @@ export class MenuComponent implements OnInit { } isRegistrationAllowed () { - return this.serverService.getConfig().signup.allowed + return this.serverService.getConfig().signup.allowed && + this.serverService.getConfig().signup.allowedForCurrentIP } getFirstAdminRightAvailable () { diff --git a/config/default.yaml b/config/default.yaml index 387acf43d..f43cbaf4b 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -60,6 +60,10 @@ admin: signup: enabled: false limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited + filters: + cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist + whitelist: [] + blacklist: [] user: # Default value of maximum video BYTES the user can upload (does not take into account transcoded files). diff --git a/config/production.yaml.example b/config/production.yaml.example index 2f80beede..a9d2c3b80 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -76,6 +76,10 @@ admin: signup: enabled: false limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited + filters: + cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist + whitelist: [] + blacklist: [] user: # Default value of maximum video BYTES the user can upload (does not take into account transcoded files). diff --git a/package.json b/package.json index 4123c55ec..bf69c4ce0 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,8 @@ "express-rate-limit": "^2.11.0", "express-validator": "^5.0.0", "fluent-ffmpeg": "^2.1.0", + "ipaddr.js": "https://github.com/whitequark/ipaddr.js.git#8e69afeb4053ee32447a101845f860848280eca5", + "is-cidr": "^2.0.5", "iso-639-3": "^1.0.1", "js-yaml": "^3.5.4", "jsonld": "^1.0.1", diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 12074a80e..f678e3c4a 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -4,7 +4,7 @@ import { ServerConfig, UserRight } from '../../../shared' import { About } from '../../../shared/models/server/about.model' import { CustomConfig } from '../../../shared/models/server/custom-config.model' import { unlinkPromise, writeFilePromise } from '../../helpers/core-utils' -import { isSignupAllowed } from '../../helpers/utils' +import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/utils' import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers' import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' import { customConfigUpdateValidator } from '../../middlewares/validators/config' @@ -36,6 +36,7 @@ configRouter.delete('/custom', async function getConfig (req: express.Request, res: express.Response, next: express.NextFunction) { const allowed = await isSignupAllowed() + const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip) const enabledResolutions = Object.keys(CONFIG.TRANSCODING.RESOLUTIONS) .filter(key => CONFIG.TRANSCODING.RESOLUTIONS[key] === true) @@ -54,7 +55,8 @@ async function getConfig (req: express.Request, res: express.Response, next: exp }, serverVersion: packageJSON.version, signup: { - allowed + allowed, + allowedForCurrentIP }, transcoding: { enabledResolutions diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 0a591f11d..8dff4b87c 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -19,6 +19,7 @@ import { authenticate, ensureUserHasRight, ensureUserRegistrationAllowed, + ensureUserRegistrationAllowedForIP, paginationValidator, setDefaultPagination, setDefaultSort, @@ -106,6 +107,7 @@ usersRouter.post('/', usersRouter.post('/register', asyncMiddleware(ensureUserRegistrationAllowed), + ensureUserRegistrationAllowedForIP, asyncMiddleware(usersRegisterValidator), asyncMiddleware(registerUserRetryWrapper) ) diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 058c3211e..e4556fa12 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts @@ -1,4 +1,6 @@ import { Model } from 'sequelize-typescript' +import * as ipaddr from 'ipaddr.js' +const isCidr = require('is-cidr') import { ResultList } from '../../shared' import { VideoResolution } from '../../shared/models/videos' import { CONFIG } from '../initializers' @@ -48,6 +50,39 @@ async function isSignupAllowed () { return totalUsers < CONFIG.SIGNUP.LIMIT } +function isSignupAllowedForCurrentIP (ip: string) { + const addr = ipaddr.parse(ip) + let excludeList = [ 'blacklist' ] + let matched: string + + // if there is a valid, non-empty whitelist, we exclude all unknown adresses too + if (CONFIG.SIGNUP.FILTERS.CIDR.WHITELIST.filter(cidr => isCidr(cidr)).length > 0) { + excludeList.push('unknown') + } + + if (addr.kind() === 'ipv4') { + const addrV4 = ipaddr.IPv4.parse(ip) + const rangeList = { + whitelist: CONFIG.SIGNUP.FILTERS.CIDR.WHITELIST.filter(cidr => isCidr.v4(cidr)) + .map(cidr => ipaddr.IPv4.parseCIDR(cidr)), + blacklist: CONFIG.SIGNUP.FILTERS.CIDR.BLACKLIST.filter(cidr => isCidr.v4(cidr)) + .map(cidr => ipaddr.IPv4.parseCIDR(cidr)) + } + matched = ipaddr.subnetMatch(addrV4, rangeList, 'unknown') + } else if (addr.kind() === 'ipv6') { + const addrV6 = ipaddr.IPv6.parse(ip) + const rangeList = { + whitelist: CONFIG.SIGNUP.FILTERS.CIDR.WHITELIST.filter(cidr => isCidr.v6(cidr)) + .map(cidr => ipaddr.IPv6.parseCIDR(cidr)), + blacklist: CONFIG.SIGNUP.FILTERS.CIDR.BLACKLIST.filter(cidr => isCidr.v6(cidr)) + .map(cidr => ipaddr.IPv6.parseCIDR(cidr)) + } + matched = ipaddr.subnetMatch(addrV6, rangeList, 'unknown') + } + + return !excludeList.includes(matched) +} + function computeResolutionsToTranscode (videoFileHeight: number) { const resolutionsEnabled: number[] = [] const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS @@ -99,6 +134,7 @@ export { generateRandomString, getFormattedObjects, isSignupAllowed, + isSignupAllowedForCurrentIP, computeResolutionsToTranscode, resetSequelizeInstance, getServerActor, diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts index 5a9c603b5..6259c7b6c 100644 --- a/server/initializers/checker.ts +++ b/server/initializers/checker.ts @@ -27,7 +27,9 @@ function checkMissedConfig () { 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', 'log.level', 'user.video_quota', - 'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads', + 'cache.previews.size', 'admin.email', + 'signup.enabled', 'signup.limit', 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', + 'transcoding.enabled', 'transcoding.threads', 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', 'instance.default_nsfw_policy', 'instance.robots', 'services.twitter.username', 'services.twitter.whitelisted' diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 424947590..a35306730 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -150,7 +150,13 @@ const CONFIG = { }, SIGNUP: { get ENABLED () { return config.get('signup.enabled') }, - get LIMIT () { return config.get('signup.limit') } + get LIMIT () { return config.get('signup.limit') }, + FILTERS: { + CIDR: { + get WHITELIST () { return config.get('signup.filters.cidr.whitelist') }, + get BLACKLIST () { return config.get('signup.filters.cidr.blacklist') } + } + } }, USER: { get VIDEO_QUOTA () { return config.get('user.video_quota') } diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 247b704c4..4ad0e33da 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -16,8 +16,8 @@ import { } from '../../helpers/custom-validators/users' import { isVideoExist } from '../../helpers/custom-validators/videos' import { logger } from '../../helpers/logger' -import { isSignupAllowed } from '../../helpers/utils' -import { CONSTRAINTS_FIELDS } from '../../initializers' +import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/utils' +import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' import { Redis } from '../../lib/redis' import { UserModel } from '../../models/account/user' import { areValidationErrors } from './utils' @@ -177,6 +177,20 @@ const ensureUserRegistrationAllowed = [ } ] +const ensureUserRegistrationAllowedForIP = [ + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + const allowed = isSignupAllowedForCurrentIP(req.ip) + + if (allowed === false) { + return res.status(403) + .send({ error: 'You are not on a network authorized for registration.' }) + .end() + } + + return next() + } +] + const usersAskResetPasswordValidator = [ body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'), @@ -230,6 +244,7 @@ export { usersUpdateMeValidator, usersVideoRatingValidator, ensureUserRegistrationAllowed, + ensureUserRegistrationAllowedForIP, usersGetValidator, usersUpdateMyAvatarValidator, usersAskResetPasswordValidator, diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index d1f956163..da0996dae 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts @@ -15,7 +15,8 @@ export interface ServerConfig { } signup: { - allowed: boolean + allowed: boolean, + allowedForCurrentIP: boolean } transcoding: { diff --git a/yarn.lock b/yarn.lock index 5a66a665c..49af4df03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1294,6 +1294,12 @@ ci-info@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.3.tgz#710193264bb05c77b8c90d02f5aaf22216a667b2" +cidr-regex@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-2.0.8.tgz#c79bae6223d241c0860d93bfde1fb1c1c4fdcab6" + dependencies: + ip-regex "^2.1.0" + circular-json@^0.3.1: version "0.3.3" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" @@ -3671,6 +3677,10 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + ip-set@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ip-set/-/ip-set-1.0.1.tgz#633b66d0bd6c8d0de968d053263c9120d3b6727e" @@ -3693,6 +3703,10 @@ ipaddr.js@1.6.0: version "1.7.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.7.0.tgz#2206ed334afc32e01fed3ee838b6b2521068b9d2" +"ipaddr.js@https://github.com/whitequark/ipaddr.js.git#8e69afeb4053ee32447a101845f860848280eca5": + version "1.7.0" + resolved "https://github.com/whitequark/ipaddr.js.git#8e69afeb4053ee32447a101845f860848280eca5" + ipv6-normalize@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ipv6-normalize/-/ipv6-normalize-1.0.1.tgz#1b3258290d365fa83239e89907dde4592e7620a8" @@ -3747,6 +3761,12 @@ is-ci@^1.0.10, is-ci@^1.1.0: dependencies: ci-info "^1.0.0" +is-cidr@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-2.0.5.tgz#13227927d71865d1177fe0e5b60e6ddd3dee0034" + dependencies: + cidr-regex "^2.0.8" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"