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