summaryrefslogtreecommitdiff
path: root/searx/botdetection/ip_lists.py
blob: 456ef436554d19df9ad2b9d10a372f410115fb8c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# SPDX-License-Identifier: AGPL-3.0-or-later
# lint: pylint
""".. _botdetection.ip_lists:

Method ``ip_lists``
-------------------

The ``ip_lists`` method implements IP :py:obj:`block- <block_ip>` and
:py:obj:`pass-lists <pass_ip>`.

.. code:: toml

   [botdetection.ip_lists]

   pass_ip = [
    '140.238.172.132', # IPv4 of check.searx.space
    '192.168.0.0/16',  # IPv4 private network
    'fe80::/10'        # IPv6 linklocal
   ]
   block_ip = [
      '93.184.216.34', # IPv4 of example.org
      '257.1.1.1',     # invalid IP --> will be ignored, logged in ERROR class
   ]

"""
# pylint: disable=unused-argument

from __future__ import annotations
from typing import Tuple
from ipaddress import (
    ip_network,
    IPv4Address,
    IPv6Address,
)

from searx.tools import config
from ._helpers import logger

logger = logger.getChild('ip_limit')

SEARXNG_ORG = [
    # https://github.com/searxng/searxng/pull/2484#issuecomment-1576639195
    '140.238.172.132',  # IPv4 check.searx.space
    '2603:c022:0:4900::/56',  # IPv6 check.searx.space
]
"""Passlist of IPs from the SearXNG organization, e.g. `check.searx.space`."""


def pass_ip(real_ip: IPv4Address | IPv6Address, cfg: config.Config) -> Tuple[bool, str]:
    """Checks if the IP on the subnet is in one of the members of the
    ``botdetection.ip_lists.pass_ip`` list.
    """

    if cfg.get('botdetection.ip_lists.pass_searxng_org', default=True):
        for net in SEARXNG_ORG:
            net = ip_network(net, strict=False)
            if real_ip.version == net.version and real_ip in net:
                return True, f"IP matches {net.compressed} in SEARXNG_ORG list."
    return ip_is_subnet_of_member_in_list(real_ip, 'botdetection.ip_lists.pass_ip', cfg)


def block_ip(real_ip: IPv4Address | IPv6Address, cfg: config.Config) -> Tuple[bool, str]:
    """Checks if the IP on the subnet is in one of the members of the
    ``botdetection.ip_lists.block_ip`` list.
    """

    block, msg = ip_is_subnet_of_member_in_list(real_ip, 'botdetection.ip_lists.block_ip', cfg)
    if block:
        msg += " To remove IP from list, please contact the maintainer of the service."
    return block, msg


def ip_is_subnet_of_member_in_list(
    real_ip: IPv4Address | IPv6Address, list_name: str, cfg: config.Config
) -> Tuple[bool, str]:

    for net in cfg.get(list_name, default=[]):
        try:
            net = ip_network(net, strict=False)
        except ValueError:
            logger.error("invalid IP %s in %s", net, list_name)
            continue
        if real_ip.version == net.version and real_ip in net:
            return True, f"IP matches {net.compressed} in {list_name}."
    return False, f"IP is not a member of an item in the f{list_name} list"