From ff2c1fe8133f9556f6aaa52058cd8b83c40085e6 Mon Sep 17 00:00:00 2001
From: Rigel Kent <sendmemail@rigelk.eu>
Date: Tue, 22 May 2018 19:43:13 +0200
Subject: [PATCH] feature: IP filtering on signup page

disable registration form on IP not in range
checking the CIDR list before filtering with it
placing the cidr filters as an attribute object in the config
---
 client/src/app/core/server/server.service.ts |  3 +-
 client/src/app/menu/menu.component.ts        |  3 +-
 config/default.yaml                          |  4 +++
 config/production.yaml.example               |  4 +++
 package.json                                 |  2 ++
 server/controllers/api/config.ts             |  6 ++--
 server/controllers/api/users.ts              |  2 ++
 server/helpers/utils.ts                      | 36 ++++++++++++++++++++
 server/initializers/checker.ts               |  4 ++-
 server/initializers/constants.ts             |  8 ++++-
 server/middlewares/validators/users.ts       | 19 +++++++++--
 shared/models/server/server-config.model.ts  |  3 +-
 yarn.lock                                    | 20 +++++++++++
 13 files changed, 105 insertions(+), 9 deletions(-)

diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index c5353023b1..ccae5a151f 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 4c35bb3a51..69216e2150 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 387acf43df..f43cbaf4b2 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 2f80beede8..a9d2c3b80a 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 4123c55ec7..bf69c4ce0e 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 12074a80e4..f678e3c4a2 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 0a591f11dd..8dff4b87c6 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 058c3211ef..e4556fa12d 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 5a9c603b50..6259c7b6c7 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 424947590a..a353067308 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -150,7 +150,13 @@ const CONFIG = {
   },
   SIGNUP: {
     get ENABLED () { return config.get<boolean>('signup.enabled') },
-    get LIMIT () { return config.get<number>('signup.limit') }
+    get LIMIT () { return config.get<number>('signup.limit') },
+    FILTERS: {
+      CIDR: {
+        get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') },
+        get BLACKLIST () { return config.get<string[]>('signup.filters.cidr.blacklist') }
+      }
+    }
   },
   USER: {
     get VIDEO_QUOTA () { return config.get<number>('user.video_quota') }
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 247b704c43..4ad0e33da7 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 d1f9561637..da0996dae7 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 5a66a665cb..49af4df030 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"
-- 
GitLab