link_token.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. # lint: pylint
  3. """
  4. Method ``link_token``
  5. ---------------------
  6. The ``link_token`` method evaluates a request as :py:obj:`suspicious
  7. <is_suspicious>` if the URL ``/client<token>.css`` is not requested by the
  8. client. By adding a random component (the token) in the URL a bot can not send
  9. a ping by request a static URL.
  10. .. note::
  11. This method requires a redis DB and needs a HTTP X-Forwarded-For_ header.
  12. To get in use of this method a flask URL route needs to be added:
  13. .. code:: python
  14. @app.route('/client<token>.css', methods=['GET', 'POST'])
  15. def client_token(token=None):
  16. link_token.ping(request, token)
  17. return Response('', mimetype='text/css')
  18. And in the HTML template from flask a stylesheet link is needed (the value of
  19. ``link_token`` comes from :py:obj:`get_token`):
  20. .. code:: html
  21. <link rel="stylesheet"
  22. href="{{ url_for('client_token', token=link_token) }}"
  23. type="text/css" />
  24. .. _X-Forwarded-For:
  25. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
  26. """
  27. import string
  28. import random
  29. import flask
  30. from searx import logger
  31. from searx import redisdb
  32. from searx.redislib import secret_hash
  33. TOKEN_LIVE_TIME = 600
  34. """Livetime (sec) of limiter's CSS token."""
  35. PING_KEY = 'SearXNG_limiter.ping'
  36. TOKEN_KEY = 'SearXNG_limiter.token'
  37. logger = logger.getChild('botdetection.link_token')
  38. def is_suspicious(request: flask.Request):
  39. """Checks if there is a valid ping for this request, if not this request is
  40. rated as *suspicious*"""
  41. redis_client = redisdb.client()
  42. if not redis_client:
  43. return False
  44. ping_key = get_ping_key(request)
  45. if not redis_client.get(ping_key):
  46. logger.warning(
  47. "missing ping (IP: %s) / request: %s",
  48. request.headers.get('X-Forwarded-For', ''),
  49. ping_key,
  50. )
  51. return True
  52. logger.debug("found ping for this request: %s", ping_key)
  53. return False
  54. def ping(request: flask.Request, token: str):
  55. """This function is called by a request to URL ``/client<token>.css``"""
  56. redis_client = redisdb.client()
  57. if not redis_client:
  58. return
  59. if not token_is_valid(token):
  60. return
  61. ping_key = get_ping_key(request)
  62. logger.debug("store ping for: %s", ping_key)
  63. redis_client.set(ping_key, 1, ex=TOKEN_LIVE_TIME)
  64. def get_ping_key(request: flask.Request):
  65. """Generates a hashed key that fits (more or less) to a request. At least
  66. X-Forwarded-For_ is needed to be able to assign the request to an IP.
  67. """
  68. return secret_hash(
  69. PING_KEY
  70. + request.headers.get('X-Forwarded-For', '')
  71. + request.headers.get('Accept-Language', '')
  72. + request.headers.get('User-Agent', '')
  73. )
  74. def token_is_valid(token) -> bool:
  75. valid = token == get_token()
  76. logger.debug("token is valid --> %s", valid)
  77. return valid
  78. def get_token() -> str:
  79. """Returns current token. If there is no currently active token a new token
  80. is generated randomly and stored in the redis DB.
  81. - :py:obj:`TOKEN_LIVE_TIME`
  82. - :py:obj:`TOKEN_KEY`
  83. """
  84. redis_client = redisdb.client()
  85. if not redis_client:
  86. # This function is also called when limiter is inactive / no redis DB
  87. # (see render function in webapp.py)
  88. return '12345678'
  89. token = redis_client.get(TOKEN_KEY)
  90. if token:
  91. token = token.decode('UTF-8')
  92. else:
  93. token = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
  94. redis_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME)
  95. return token