| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 | # SPDX-License-Identifier: AGPL-3.0-or-later"""Implementation of the default settings."""import typingimport numbersimport errnoimport osimport loggingfrom base64 import b64decodefrom os.path import dirname, abspathfrom .sxng_locales import sxng_localessearx_dir = abspath(dirname(__file__))logger = logging.getLogger('searx')OUTPUT_FORMATS = ['html', 'csv', 'json', 'rss']SXNG_LOCALE_TAGS = ['all', 'auto'] + list(l[0] for l in sxng_locales)SIMPLE_STYLE = ('auto', 'light', 'dark', 'black')CATEGORIES_AS_TABS = {    'general': {},    'images': {},    'videos': {},    'news': {},    'map': {},    'music': {},    'it': {},    'science': {},    'files': {},    'social media': {},}STR_TO_BOOL = {    '0': False,    'false': False,    'off': False,    '1': True,    'true': True,    'on': True,}_UNDEFINED = object()class SettingsValue:    """Check and update a setting value"""    def __init__(        self,        type_definition: typing.Union[None, typing.Any, typing.Tuple[typing.Any]] = None,        default: typing.Any = None,        environ_name: str = None,    ):        self.type_definition = (            type_definition if type_definition is None or isinstance(type_definition, tuple) else (type_definition,)        )        self.default = default        self.environ_name = environ_name    @property    def type_definition_repr(self):        types_str = [t.__name__ if isinstance(t, type) else repr(t) for t in self.type_definition]        return ', '.join(types_str)    def check_type_definition(self, value: typing.Any) -> None:        if value in self.type_definition:            return        type_list = tuple(t for t in self.type_definition if isinstance(t, type))        if not isinstance(value, type_list):            raise ValueError('The value has to be one of these types/values: {}'.format(self.type_definition_repr))    def __call__(self, value: typing.Any) -> typing.Any:        if value == _UNDEFINED:            value = self.default        # override existing value with environ        if self.environ_name and self.environ_name in os.environ:            value = os.environ[self.environ_name]            if self.type_definition == (bool,):                value = STR_TO_BOOL[value.lower()]        self.check_type_definition(value)        return valueclass SettingSublistValue(SettingsValue):    """Check the value is a sublist of type definition."""    def check_type_definition(self, value: typing.Any) -> typing.Any:        if not isinstance(value, list):            raise ValueError('The value has to a list')        for item in value:            if not item in self.type_definition[0]:                raise ValueError('{} not in {}'.format(item, self.type_definition))class SettingsDirectoryValue(SettingsValue):    """Check and update a setting value that is a directory path"""    def check_type_definition(self, value: typing.Any) -> typing.Any:        super().check_type_definition(value)        if not os.path.isdir(value):            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), value)    def __call__(self, value: typing.Any) -> typing.Any:        if value == '':            value = self.default        return super().__call__(value)class SettingsBytesValue(SettingsValue):    """str are base64 decoded"""    def __call__(self, value: typing.Any) -> typing.Any:        if isinstance(value, str):            value = b64decode(value)        return super().__call__(value)def apply_schema(settings, schema, path_list):    error = False    for key, value in schema.items():        if isinstance(value, SettingsValue):            try:                settings[key] = value(settings.get(key, _UNDEFINED))            except Exception as e:  # pylint: disable=broad-except                # don't stop now: check other values                logger.error('%s: %s', '.'.join([*path_list, key]), e)                error = True        elif isinstance(value, dict):            error = error or apply_schema(settings.setdefault(key, {}), schema[key], [*path_list, key])        else:            settings.setdefault(key, value)    if len(path_list) == 0 and error:        raise ValueError('Invalid settings.yml')    return errorSCHEMA = {    'general': {        'debug': SettingsValue(bool, False, 'SEARXNG_DEBUG'),        'instance_name': SettingsValue(str, 'SearXNG'),        'privacypolicy_url': SettingsValue((None, False, str), None),        'contact_url': SettingsValue((None, False, str), None),        'donation_url': SettingsValue((bool, str), "https://docs.searxng.org/donate.html"),        'enable_metrics': SettingsValue(bool, True),        'open_metrics': SettingsValue(str, ''),    },    'brand': {        'issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues'),        'new_issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues/new'),        'docs_url': SettingsValue(str, 'https://docs.searxng.org'),        'public_instances': SettingsValue((False, str), 'https://searx.space'),        'wiki_url': SettingsValue(str, 'https://github.com/searxng/searxng/wiki'),        'custom': SettingsValue(dict, {'links': {}}),    },    'search': {        'safe_search': SettingsValue((0, 1, 2), 0),        'autocomplete': SettingsValue(str, ''),        'autocomplete_min': SettingsValue(int, 4),        'favicon_resolver': SettingsValue(str, ''),        'default_lang': SettingsValue(tuple(SXNG_LOCALE_TAGS + ['']), ''),        'languages': SettingSublistValue(SXNG_LOCALE_TAGS, SXNG_LOCALE_TAGS),        'ban_time_on_fail': SettingsValue(numbers.Real, 5),        'max_ban_time_on_fail': SettingsValue(numbers.Real, 120),        'suspended_times': {            'SearxEngineAccessDenied': SettingsValue(numbers.Real, 86400),            'SearxEngineCaptcha': SettingsValue(numbers.Real, 86400),            'SearxEngineTooManyRequests': SettingsValue(numbers.Real, 3600),            'cf_SearxEngineCaptcha': SettingsValue(numbers.Real, 1296000),            'cf_SearxEngineAccessDenied': SettingsValue(numbers.Real, 86400),            'recaptcha_SearxEngineCaptcha': SettingsValue(numbers.Real, 604800),        },        'formats': SettingsValue(list, OUTPUT_FORMATS),        'max_page': SettingsValue(int, 0),    },    'server': {        'port': SettingsValue((int, str), 8888, 'SEARXNG_PORT'),        'bind_address': SettingsValue(str, '127.0.0.1', 'SEARXNG_BIND_ADDRESS'),        'limiter': SettingsValue(bool, False, 'SEARXNG_LIMITER'),        'public_instance': SettingsValue(bool, False, 'SEARXNG_PUBLIC_INSTANCE'),        'secret_key': SettingsValue(str, environ_name='SEARXNG_SECRET'),        'base_url': SettingsValue((False, str), False, 'SEARXNG_BASE_URL'),        'image_proxy': SettingsValue(bool, False, 'SEARXNG_IMAGE_PROXY'),        'http_protocol_version': SettingsValue(('1.0', '1.1'), '1.0'),        'method': SettingsValue(('POST', 'GET'), 'POST'),        'default_http_headers': SettingsValue(dict, {}),    },    'redis': {        'url': SettingsValue((None, False, str), False, 'SEARXNG_REDIS_URL'),    },    'ui': {        'static_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'static')),        'static_use_hash': SettingsValue(bool, False, 'SEARXNG_STATIC_USE_HASH'),        'templates_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'templates')),        'default_theme': SettingsValue(str, 'simple'),        'default_locale': SettingsValue(str, ''),        'theme_args': {            'simple_style': SettingsValue(SIMPLE_STYLE, 'auto'),        },        'center_alignment': SettingsValue(bool, False),        'results_on_new_tab': SettingsValue(bool, False),        'advanced_search': SettingsValue(bool, False),        'query_in_title': SettingsValue(bool, False),        'infinite_scroll': SettingsValue(bool, False),        'cache_url': SettingsValue(str, 'https://web.archive.org/web/'),        'search_on_category_select': SettingsValue(bool, True),        'hotkeys': SettingsValue(('default', 'vim'), 'default'),        'url_formatting': SettingsValue(('pretty', 'full', 'host'), 'pretty'),    },    'preferences': {        'lock': SettingsValue(list, []),    },    'outgoing': {        'useragent_suffix': SettingsValue(str, ''),        'request_timeout': SettingsValue(numbers.Real, 3.0),        'enable_http2': SettingsValue(bool, True),        'verify': SettingsValue((bool, str), True),        'max_request_timeout': SettingsValue((None, numbers.Real), None),        'pool_connections': SettingsValue(int, 100),        'pool_maxsize': SettingsValue(int, 10),        'keepalive_expiry': SettingsValue(numbers.Real, 5.0),        # default maximum redirect        # from https://github.com/psf/requests/blob/8c211a96cdbe9fe320d63d9e1ae15c5c07e179f8/requests/models.py#L55        'max_redirects': SettingsValue(int, 30),        'retries': SettingsValue(int, 0),        'proxies': SettingsValue((None, str, dict), None),        'source_ips': SettingsValue((None, str, list), None),        # Tor configuration        'using_tor_proxy': SettingsValue(bool, False),        'extra_proxy_timeout': SettingsValue(int, 0),        'networks': {},    },    'result_proxy': {        'url': SettingsValue((None, str), None),        'key': SettingsBytesValue((None, bytes), None),        'proxify_results': SettingsValue(bool, False),    },    'plugins': SettingsValue(list, []),    'enabled_plugins': SettingsValue((None, list), None),    'checker': {        'off_when_debug': SettingsValue(bool, True, None),        'scheduling': SettingsValue((None, dict), None, None),    },    'categories_as_tabs': SettingsValue(dict, CATEGORIES_AS_TABS),    'engines': SettingsValue(list, []),    'doi_resolvers': {},}
 |