| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  | # SPDX-License-Identifier: AGPL-3.0-or-later | 
					
						
							|  |  |  | # lint: pylint | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  | """Bot protection / IP rate limitation.  The intention of rate limitation is to
 | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  | limit suspicious requests from an IP.  The motivation behind this is the fact | 
					
						
							|  |  |  | that SearXNG passes through requests from bots and is thus classified as a bot | 
					
						
							|  |  |  | itself.  As a result, the SearXNG engine then receives a CAPTCHA or is blocked | 
					
						
							|  |  |  | by the search engine (the origin) in some other way. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | To avoid blocking, the requests from bots to SearXNG must also be blocked, this | 
					
						
							|  |  |  | is the task of the limiter.  To perform this task, the limiter uses the methods | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  | from the :ref:`botdetection`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Analysis of the HTTP header in the request / :ref:`botdetection probe headers` | 
					
						
							|  |  |  |   can be easily bypassed. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Block and pass lists in which IPs are listed / :ref:`botdetection ip_lists` | 
					
						
							|  |  |  |   are hard to maintain, since the IPs of bots are not all known and change over | 
					
						
							|  |  |  |   the time. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - Detection & dynamically :ref:`botdetection rate limit` of bots based on the | 
					
						
							|  |  |  |   behavior of the requests.  For dynamically changeable IP lists a Redis | 
					
						
							|  |  |  |   database is needed. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The prerequisite for IP based methods is the correct determination of the IP of | 
					
						
							|  |  |  | the client. The IP of the client is determined via the X-Forwarded-For_ HTTP | 
					
						
							|  |  |  | header. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. attention:: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |    A correct setup of the HTTP request headers ``X-Forwarded-For`` and | 
					
						
							|  |  |  |    ``X-Real-IP`` is essential to be able to assign a request to an IP correctly: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |    - `NGINX RequestHeader`_ | 
					
						
							|  |  |  |    - `Apache RequestHeader`_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. _X-Forwarded-For: | 
					
						
							|  |  |  |     https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For | 
					
						
							|  |  |  | .. _NGINX RequestHeader: | 
					
						
							|  |  |  |     https://docs.searxng.org/admin/installation-nginx.html#nginx-s-searxng-site | 
					
						
							|  |  |  | .. _Apache RequestHeader: | 
					
						
							|  |  |  |     https://docs.searxng.org/admin/installation-apache.html#apache-s-searxng-site | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Enable Limiter | 
					
						
							|  |  |  | ============== | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | To enable the limiter activate: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code:: yaml | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |    server: | 
					
						
							|  |  |  |      ... | 
					
						
							|  |  |  |      limiter: true  # rate limit the number of request on the instance, block some bots | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | and set the redis-url connection. Check the value, it depends on your redis DB | 
					
						
							|  |  |  | (see :ref:`settings redis`), by example: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code:: yaml | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |    redis: | 
					
						
							|  |  |  |      url: unix:///usr/local/searxng-redis/run/redis.sock?db=0 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | Configure Limiter | 
					
						
							|  |  |  | ================= | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The methods of :ref:`botdetection` the limiter uses are configured in a local | 
					
						
							|  |  |  | file ``/etc/searxng/limiter.toml``.  The defaults are shown in limiter.toml_ / | 
					
						
							|  |  |  | Don't copy all values to your local configuration, just enable what you need by | 
					
						
							|  |  |  | overwriting the defaults.  For instance to activate the ``link_token`` method in | 
					
						
							|  |  |  | the :ref:`botdetection.ip_limit` you only need to set this option to ``true``: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. code:: toml | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |    [botdetection.ip_limit] | 
					
						
							|  |  |  |    link_token = true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. _limiter.toml: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ``limiter.toml`` | 
					
						
							|  |  |  | ================ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | In this file the limiter finds the configuration of the :ref:`botdetection`: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - :ref:`botdetection ip_lists` | 
					
						
							|  |  |  | - :ref:`botdetection rate limit` | 
					
						
							|  |  |  | - :ref:`botdetection probe headers` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. kernel-include:: $SOURCEDIR/limiter.toml | 
					
						
							|  |  |  |    :code: toml | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Implementation | 
					
						
							|  |  |  | ============== | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  | from __future__ import annotations | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  | import sys | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-26 17:24:43 +02:00
										 |  |  | from pathlib import Path | 
					
						
							| 
									
										
										
										
											2023-06-03 13:43:34 +02:00
										 |  |  | from ipaddress import ip_address | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  | import flask | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  | import werkzeug | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  | from searx import ( | 
					
						
							|  |  |  |     logger, | 
					
						
							|  |  |  |     redisdb, | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | from searx import botdetection | 
					
						
							|  |  |  | from searx.botdetection import ( | 
					
						
							|  |  |  |     config, | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  |     http_accept, | 
					
						
							|  |  |  |     http_accept_encoding, | 
					
						
							|  |  |  |     http_accept_language, | 
					
						
							|  |  |  |     http_user_agent, | 
					
						
							|  |  |  |     ip_limit, | 
					
						
							| 
									
										
										
										
											2023-06-03 13:43:34 +02:00
										 |  |  |     ip_lists, | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  |     get_network, | 
					
						
							|  |  |  |     get_real_ip, | 
					
						
							|  |  |  |     dump_request, | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  | # the configuration are limiter.toml and "limiter" in settings.yml so, for | 
					
						
							|  |  |  | # coherency, the logger is "limiter" | 
					
						
							|  |  |  | logger = logger.getChild('limiter') | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | CFG: config.Config = None  # type: ignore | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  | _INSTALLED = False | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-26 17:24:43 +02:00
										 |  |  | LIMITER_CFG_SCHEMA = Path(__file__).parent / "limiter.toml" | 
					
						
							|  |  |  | """Base configuration (schema) of the botdetection.""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | LIMITER_CFG = Path('/etc/searxng/limiter.toml') | 
					
						
							| 
									
										
										
										
											2023-09-15 09:53:03 +02:00
										 |  |  | """Local Limiter configuration.""" | 
					
						
							| 
									
										
										
										
											2023-05-26 17:24:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | CFG_DEPRECATED = { | 
					
						
							|  |  |  |     # "dummy.old.foo": "config 'dummy.old.foo' exists only for tests.  Don't use it in your real project config." | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-28 18:58:31 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | def get_cfg() -> config.Config: | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  |     global CFG  # pylint: disable=global-statement | 
					
						
							| 
									
										
										
										
											2023-05-28 18:58:31 +02:00
										 |  |  |     if CFG is None: | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  |         CFG = config.Config.from_toml(LIMITER_CFG_SCHEMA, LIMITER_CFG, CFG_DEPRECATED) | 
					
						
							| 
									
										
										
										
											2023-05-28 18:58:31 +02:00
										 |  |  |     return CFG | 
					
						
							| 
									
										
										
										
											2023-05-26 17:24:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  | def filter_request(request: flask.Request) -> werkzeug.Response | None: | 
					
						
							| 
									
										
										
										
											2023-06-03 13:43:34 +02:00
										 |  |  |     # pylint: disable=too-many-return-statements | 
					
						
							| 
									
										
										
										
											2023-05-26 17:24:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  |     cfg = get_cfg() | 
					
						
							| 
									
										
										
										
											2023-06-03 13:43:34 +02:00
										 |  |  |     real_ip = ip_address(get_real_ip(request)) | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  |     network = get_network(real_ip, cfg) | 
					
						
							| 
									
										
										
										
											2023-06-03 13:43:34 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if request.path == '/healthz': | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # link-local | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  |     if network.is_link_local: | 
					
						
							|  |  |  |         return None | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-03 13:43:34 +02:00
										 |  |  |     # block- & pass- lists | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # 1. The IP of the request is first checked against the pass-list; if the IP | 
					
						
							|  |  |  |     #    matches an entry in the list, the request is not blocked. | 
					
						
							|  |  |  |     # 2. If no matching entry is found in the pass-list, then a check is made against | 
					
						
							|  |  |  |     #    the block list; if the IP matches an entry in the list, the request is | 
					
						
							|  |  |  |     #    blocked. | 
					
						
							|  |  |  |     # 3. If the IP is not in either list, the request is not blocked. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     match, msg = ip_lists.pass_ip(real_ip, cfg) | 
					
						
							|  |  |  |     if match: | 
					
						
							|  |  |  |         logger.warning("PASS %s: matched PASSLIST - %s", network.compressed, msg) | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-03 13:43:34 +02:00
										 |  |  |     match, msg = ip_lists.block_ip(real_ip, cfg) | 
					
						
							|  |  |  |     if match: | 
					
						
							|  |  |  |         logger.error("BLOCK %s: matched BLOCKLIST - %s", network.compressed, msg) | 
					
						
							|  |  |  |         return flask.make_response(('IP is on BLOCKLIST - %s' % msg, 429)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # methods applied on / | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  |     for func in [ | 
					
						
							|  |  |  |         http_user_agent, | 
					
						
							|  |  |  |     ]: | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  |         val = func.filter_request(network, request, cfg) | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  |         if val is not None: | 
					
						
							|  |  |  |             return val | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-03 13:43:34 +02:00
										 |  |  |     # methods applied on /search | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  |     if request.path == '/search': | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for func in [ | 
					
						
							|  |  |  |             http_accept, | 
					
						
							|  |  |  |             http_accept_encoding, | 
					
						
							|  |  |  |             http_accept_language, | 
					
						
							|  |  |  |             http_user_agent, | 
					
						
							|  |  |  |             ip_limit, | 
					
						
							|  |  |  |         ]: | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  |             val = func.filter_request(network, request, cfg) | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  |             if val is not None: | 
					
						
							|  |  |  |                 return val | 
					
						
							| 
									
										
										
										
											2023-06-01 15:41:48 +02:00
										 |  |  |     logger.debug(f"OK {network}: %s", dump_request(flask.request)) | 
					
						
							| 
									
										
										
										
											2023-05-23 18:16:37 +02:00
										 |  |  |     return None | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def pre_request(): | 
					
						
							|  |  |  |     """See :py:obj:`flask.Flask.before_request`""" | 
					
						
							|  |  |  |     return filter_request(flask.request) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def is_installed(): | 
					
						
							| 
									
										
										
										
											2023-10-02 18:29:58 +02:00
										 |  |  |     """Returns ``True`` if limiter is active and a redis DB is available.""" | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  |     return _INSTALLED | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def initialize(app: flask.Flask, settings): | 
					
						
							| 
									
										
										
										
											2023-10-02 18:29:58 +02:00
										 |  |  |     """Install the limiter""" | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  |     global _INSTALLED  # pylint: disable=global-statement | 
					
						
							| 
									
										
										
										
											2023-10-02 18:29:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if not (settings['server']['limiter'] or settings['server']['public_instance']): | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  |         return | 
					
						
							| 
									
										
										
										
											2023-10-02 18:29:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  |     redis_client = redisdb.client() | 
					
						
							|  |  |  |     if not redis_client: | 
					
						
							|  |  |  |         logger.error( | 
					
						
							|  |  |  |             "The limiter requires Redis, please consult the documentation: " | 
					
						
							| 
									
										
										
										
											2023-10-02 18:29:58 +02:00
										 |  |  |             "https://docs.searxng.org/admin/searx.limiter.html" | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  |         ) | 
					
						
							|  |  |  |         if settings['server']['public_instance']: | 
					
						
							|  |  |  |             sys.exit(1) | 
					
						
							|  |  |  |         return | 
					
						
							| 
									
										
										
										
											2023-10-02 18:29:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-02 16:36:07 +02:00
										 |  |  |     _INSTALLED = True | 
					
						
							| 
									
										
										
										
											2023-10-02 18:29:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     cfg = get_cfg() | 
					
						
							|  |  |  |     if settings['server']['public_instance']: | 
					
						
							|  |  |  |         # overwrite limiter.toml setting | 
					
						
							|  |  |  |         cfg.set('botdetection.ip_limit.link_token', True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     botdetection.init(cfg, redis_client) | 
					
						
							|  |  |  |     app.before_request(pre_request) |