feature: IP filtering on signup page
authorRigel Kent <sendmemail@rigelk.eu>
Tue, 22 May 2018 17:43:13 +0000 (19:43 +0200)
committerRigel Kent <par@rigelk.eu>
Tue, 22 May 2018 17:44:34 +0000 (19:44 +0200)
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

13 files changed:
client/src/app/core/server/server.service.ts
client/src/app/menu/menu.component.ts
config/default.yaml
config/production.yaml.example
package.json
server/controllers/api/config.ts
server/controllers/api/users.ts
server/helpers/utils.ts
server/initializers/checker.ts
server/initializers/constants.ts
server/middlewares/validators/users.ts
shared/models/server/server-config.model.ts
yarn.lock

index c5353023b10572338dd44801e48a1546888ec6db..ccae5a151fbc97bf4b929981431421da361d6e0c 100644 (file)
@@ -34,7 +34,8 @@ export class ServerService {
     },
     serverVersion: 'Unknown',
     signup: {
-      allowed: false
+      allowed: false,
+      allowedForCurrentIP: false
     },
     transcoding: {
       enabledResolutions: []
index 4c35bb3a51ebed4f9d9fdb7a2adc8ab2d6c5ae19..69216e2150c37dfcea3e57490d05f6bb281581ce 100644 (file)
@@ -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 () {
index 387acf43df728abc1ff170329c1e45385d199630..f43cbaf4b25a310718e7414eb4a1476e40e41c96 100644 (file)
@@ -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).
index 2f80beede82d6be1013d3505e654d5f0db889cb3..a9d2c3b80afde1bfce435a4a180352bbae2bba1c 100644 (file)
@@ -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).
index 4123c55ec7d891d1270893ca93ba7a3ca9e69be3..bf69c4ce0e3605337e84fa5f0721d934ed5e0b9b 100644 (file)
@@ -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",
index 12074a80e408b63e857976c09c5c0c188b4b00c3..f678e3c4a2b5042b5b94bcb41480565cd1ff6abf 100644 (file)
@@ -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
index 0a591f11dd121866f373df7a18a8cf89d1994718..8dff4b87c63a00b0aceed357f34dc265ac66d3bf 100644 (file)
@@ -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)
 )
index 058c3211efd39b810c8dd73ea0c094aa23af876e..e4556fa12d4a875b4665170785e3b940403d9dc1 100644 (file)
@@ -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,
index 5a9c603b50c73d3534eeededf8101bc40a454b37..6259c7b6c74960f435d5849950841c9ab1076b00 100644 (file)
@@ -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'
index 424947590a0f3e02cc1abcc14f8ce9514c82c80c..a353067308c5498e2227536ce7d31c2855f9805b 100644 (file)
@@ -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') }
index 247b704c4331d8ff27c397220ec41de20d90303f..4ad0e33da7d203d2d9eedad64d512370b917e81c 100644 (file)
@@ -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,
index d1f9561637b637818f73c13f5d2fd794e70eecd7..da0996dae76bc81dfd53fafd37480ee80e9c1ca7 100644 (file)
@@ -15,7 +15,8 @@ export interface ServerConfig {
   }
 
   signup: {
-    allowed: boolean
+    allowed: boolean,
+    allowedForCurrentIP: boolean
   }
 
   transcoding: {
index 5a66a665cb44621f440a7e2ea7211b05eca84ecb..49af4df030d42e5d21f4a662c45a8e2d86c0c2df 100644 (file)
--- 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"