.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
proxy.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2"""Implementations for a favicon proxy"""
3
4from __future__ import annotations
5
6from typing import Callable
7
8import importlib
9import base64
10import pathlib
11import urllib.parse
12
13import flask
14from httpx import HTTPError
15import msgspec
16
17from searx import get_setting
18
19from searx.webutils import new_hmac, is_hmac_of
20from searx.exceptions import SearxEngineResponseException
21
22from .resolvers import DEFAULT_RESOLVER_MAP
23from . import cache
24
25DEFAULT_FAVICON_URL = {}
26CFG: FaviconProxyConfig = None # type: ignore
27
28
29def init(cfg: FaviconProxyConfig):
30 global CFG # pylint: disable=global-statement
31 CFG = cfg
32
33
35 d = {}
36 name: str = get_setting("search.favicon_resolver", None) # type: ignore
37 if name:
38 func = DEFAULT_RESOLVER_MAP.get(name)
39 if func:
40 d = {name: f"searx.favicons.resolvers.{func.__name__}"}
41 return d
42
43
44class FaviconProxyConfig(msgspec.Struct):
45 """Configuration of the favicon proxy."""
46
47 max_age: int = 60 * 60 * 24 * 7 # seven days
48 """HTTP header Cache-Control_ ``max-age``
49
50 .. _Cache-Control: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
51 """
52
53 secret_key: str = get_setting("server.secret_key") # type: ignore
54 """By default, the value from :ref:`server.secret_key <settings server>`
55 setting is used."""
56
57 resolver_timeout: int = get_setting("outgoing.request_timeout") # type: ignore
58 """Timeout which the resolvers should not exceed, is usually passed to the
59 outgoing request of the resolver. By default, the value from
60 :ref:`outgoing.request_timeout <settings outgoing>` setting is used."""
61
62 resolver_map: dict[str, str] = msgspec.field(default_factory=_initial_resolver_map)
63 """The resolver_map is a key / value dictionary where the key is the name of
64 the resolver and the value is the fully qualifying name (fqn) of resolver's
65 function (the callable). The resolvers from the python module
66 :py:obj:`searx.favicons.resolver` are available by default."""
67
68 def get_resolver(self, name: str) -> Callable | None:
69 """Returns the callable object (function) of the resolver with the
70 ``name``. If no resolver is registered for the ``name``, ``None`` is
71 returned.
72 """
73 fqn = self.resolver_map.get(name)
74 if fqn is None:
75 return None
76 mod_name, _, func_name = fqn.rpartition('.')
77 mod = importlib.import_module(mod_name)
78 func = getattr(mod, func_name)
79 if func is None:
80 raise ValueError(f"resolver {fqn} is not implemented")
81 return func
82
83 favicon_path: str = get_setting("ui.static_path") + "/themes/{theme}/img/empty_favicon.svg" # type: ignore
84 favicon_mime_type: str = "image/svg+xml"
85
86 def favicon(self, **replacements):
87 """Returns pathname and mimetype of the default favicon."""
88 return (
89 pathlib.Path(self.favicon_path.format(**replacements)),
91 )
92
93 def favicon_data_url(self, **replacements):
94 """Returns data image URL of the default favicon."""
95
96 cache_key = ", ".join(f"{x}:{replacements[x]}" for x in sorted(list(replacements.keys()), key=str))
97 data_url = DEFAULT_FAVICON_URL.get(cache_key)
98 if data_url is not None:
99 return data_url
100
101 fav, mimetype = CFG.favicon(**replacements)
102 # hint: encoding utf-8 limits favicons to be a SVG image
103 with fav.open("r", encoding="utf-8") as f:
104 data_url = f.read()
105
106 data_url = urllib.parse.quote(data_url)
107 data_url = f"data:{mimetype};utf8,{data_url}"
108 DEFAULT_FAVICON_URL[cache_key] = data_url
109 return data_url
110
111
113 """REST API of SearXNG's favicon proxy service
114
115 ::
116
117 /favicon_proxy?authority=<...>&h=<...>
118
119 ``authority``:
120 Domain name :rfc:`3986` / see :py:obj:`favicon_url`
121
122 ``h``:
123 HMAC :rfc:`2104`, build up from the :ref:`server.secret_key <settings
124 server>` setting.
125
126 """
127 authority = flask.request.args.get('authority')
128
129 # malformed request or RFC 3986 authority
130 if not authority or "/" in authority:
131 return '', 400
132
133 # malformed request / does not have authorisation
134 if not is_hmac_of(
135 CFG.secret_key,
136 authority.encode(),
137 flask.request.args.get('h', ''),
138 ):
139 return '', 400
140
141 resolver = flask.request.preferences.get_value('favicon_resolver') # type: ignore
142 # if resolver is empty or not valid, just return HTTP 400.
143 if not resolver or resolver not in CFG.resolver_map.keys():
144 return "", 400
145
146 data, mime = search_favicon(resolver, authority)
147
148 if data is not None and mime is not None:
149 resp = flask.Response(data, mimetype=mime) # type: ignore
150 resp.headers['Cache-Control'] = f"max-age={CFG.max_age}"
151 return resp
152
153 # return default favicon from static path
154 theme = flask.request.preferences.get_value("theme") # type: ignore
155 fav, mimetype = CFG.favicon(theme=theme)
156 return flask.send_from_directory(fav.parent, fav.name, mimetype=mimetype)
157
158
159def search_favicon(resolver: str, authority: str) -> tuple[None | bytes, None | str]:
160 """Sends the request to the favicon resolver and returns a tuple for the
161 favicon. The tuple consists of ``(data, mime)``, if the resolver has not
162 determined a favicon, both values are ``None``.
163
164 ``data``:
165 Binary data of the favicon.
166
167 ``mime``:
168 Mime type of the favicon.
169
170 """
171
172 data, mime = (None, None)
173
174 func = CFG.get_resolver(resolver)
175 if func is None:
176 return data, mime
177
178 # to avoid superfluous requests to the resolver, first look in the cache
179 data_mime = cache.CACHE(resolver, authority)
180 if data_mime is not None:
181 return data_mime
182
183 try:
184 data, mime = func(authority, timeout=CFG.resolver_timeout)
185 if data is None or mime is None:
186 data, mime = (None, None)
187
188 except (HTTPError, SearxEngineResponseException):
189 pass
190
191 cache.CACHE.set(resolver, authority, mime, data)
192 return data, mime
193
194
195def favicon_url(authority: str) -> str:
196 """Function to generate the image URL used for favicons in SearXNG's result
197 lists. The ``authority`` argument (aka netloc / :rfc:`3986`) is usually a
198 (sub-) domain name. This function is used in the HTML (jinja) templates.
199
200 .. code:: html
201
202 <div class="favicon">
203 <img src="{{ favicon_url(result.parsed_url.netloc) }}">
204 </div>
205
206 The returned URL is a route to :py:obj:`favicon_proxy` REST API.
207
208 If the favicon is already in the cache, the returned URL is a `data URL`_
209 (something like ``data:image/png;base64,...``). By generating a data url from
210 the :py:obj:`.cache.FaviconCache`, additional HTTP roundtripps via the
211 :py:obj:`favicon_proxy` are saved. However, it must also be borne in mind
212 that data urls are not cached in the client (web browser).
213
214 .. _data URL: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs
215
216 """
217
218 resolver = flask.request.preferences.get_value('favicon_resolver') # type: ignore
219 # if resolver is empty or not valid, just return nothing.
220 if not resolver or resolver not in CFG.resolver_map.keys():
221 return ""
222
223 data_mime = cache.CACHE(resolver, authority)
224
225 if data_mime == (None, None):
226 # we have already checked, the resolver does not have a favicon
227 theme = flask.request.preferences.get_value("theme") # type: ignore
228 return CFG.favicon_data_url(theme=theme)
229
230 if data_mime is not None:
231 data, mime = data_mime
232 return f"data:{mime};base64,{str(base64.b64encode(data), 'utf-8')}" # type: ignore
233
234 h = new_hmac(CFG.secret_key, authority.encode())
235 proxy_url = flask.url_for('favicon_proxy')
236 query = urllib.parse.urlencode({"authority": authority, "h": h})
237 return f"{proxy_url}?{query}"
Callable|None get_resolver(self, str name)
Definition proxy.py:68
favicon(self, **replacements)
Definition proxy.py:86
favicon_data_url(self, **replacements)
Definition proxy.py:93
str favicon_url(str authority)
Definition proxy.py:195
tuple[None|bytes, None|str] search_favicon(str resolver, str authority)
Definition proxy.py:159