| 1 | // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt |
| 2 | |
| 3 | import { defineConfig } from './config' |
| 4 | import { disconnect, getConnections, normalizeIp } from './connections' |
| 5 | import { makeNetMatcher, MINUTE, netMatches, onlyTruthy } from './misc' |
| 6 | import { isIP, Socket } from 'net' |
| 7 | import _ from 'lodash' |
| 8 | |
| 9 | export interface BlockingRule { ip: string, comment?: string, expire?: Date, disabled?: boolean } |
| 10 | |
| 11 | export const block = defineConfig('block', [] as BlockingRule[], rules => { |
| 12 | const now = new Date() |
| 13 | const ret = !Array.isArray(rules) ? [] |
| 14 | : onlyTruthy(rules.map(rule => { |
| 15 | rule.expire &&= new Date(rule.expire) |
| 16 | return !rule.disabled && (rule.expire || now) >= now && makeNetMatcher(rule.ip) |
| 17 | })) |
| 18 | setTimeout(() => { // wait until defineConfig has stored the newly compiled rules, because applyBlock uses isBlocked, that uses block.compiled |
| 19 | for (const { socket, ip } of getConnections()) // reapply new block to existing connections |
| 20 | applyBlock(socket, ip) |
| 21 | }) |
| 22 | return ret |
| 23 | }) |
| 24 | |
| 25 | export function applyBlock(socket: Socket, ip=normalizeIp(socket.remoteAddress||'')) { |
| 26 | if (ip && isBlocked(ip)) |
| 27 | return disconnect(socket, 'block-ip') |
| 28 | } |
| 29 | |
| 30 | export function isBlocked(ip: string) { |
| 31 | return block.compiled().find(rule => rule(ip)) |
| 32 | } |
| 33 | |
| 34 | setInterval(() => { // twice a minute, check if any block has expired |
| 35 | const now = new Date() |
| 36 | const next = block.get().filter(x => !x.expire || x.expire > now) |
| 37 | const n = block.get().length - next.length |
| 38 | if (!n) return |
| 39 | console.log("Blocking rules:", n, "expired") |
| 40 | block.set(next) |
| 41 | }, MINUTE/2) |
| 42 | |
| 43 | export function addBlock(rule: BlockingRule, merge?: Partial<BlockingRule>) { |
| 44 | if (isIP(rule.ip) && isBlocked(rule.ip)) return // already |
| 45 | block.set(was => { |
| 46 | const match = merge && _.matches(merge) |
| 47 | const foundIdx = match ? _.findIndex(was, v => match(v) && !v.disabled) : -1 |
| 48 | // in case the rule is disabled, and isBlocked returned false |
| 49 | return foundIdx < 0 ? [...was, { ...merge, ...rule }] // add as new rule |
| 50 | : was.map((x, i) => i === foundIdx ? { ...x, ...rule, ip: `${x.ip}|${rule.ip}` } : x) |
| 51 | }) |
| 52 | } |