py.lib

Yohn Y. 2022-08-27 Parent:4186c3b229fa Child:d9a3784f681b

41:483727ff89c4 Go to Latest

py.lib/webapp/bottle_urils.py

+ Возможность удалить cookie при формировании штатного ответа.

History
awgur@31 1 # coding: utf-8
awgur@31 2 """\
awgur@31 3 Набор инструментов, для более удобного взаимодействия с фреймворком ``bottle``
awgur@31 4 """
awgur@31 5
awgur@31 6 import bottle
awgur@31 7 import json
awgur@31 8 from typing import Union, List, Optional, Tuple, Any
awgur@31 9 from datetime import datetime
awgur@31 10 from hashlib import sha512
awgur@31 11 from dataclasses import is_dataclass, asdict
awgur@31 12
awgur@31 13
awgur@31 14 VARIABLE_PREFIX = 'ru.a0fs.app' # Префикс переменных и иных структур, которые сохраняются во внутренних
awgur@31 15 # структурах ``bottle``
awgur@31 16 IP_HEADER = 'X-Real-IP' # Заголовок, в котором реверс-прокси хранит реальный IP клиента.
awgur@31 17 ID_HEADER = 'X-Req-ID' # Заголовок запроса, в котором может храниться уникальный ID запроса,
awgur@31 18 # выставленного реверс-прокси.
awgur@31 19 CONN_ID_HEADER = 'X-Conn-ID' # Идентификатор текущего соединения.
awgur@31 20
awgur@31 21
awgur@31 22 class Cookie(object):
awgur@31 23 """\
awgur@31 24 Класс хранящий ``cookie`` и способный их устанавливать в объекты http-ответов ``bottle``
awgur@31 25 """
awgur@31 26
awgur@31 27 def __init__(self, name: str, value: str,
awgur@31 28 max_age: int = None, expires: Union[int, datetime] = None, path: str = None,
awgur@31 29 secure: bool = True, httponly: bool = True,
awgur@31 30 samesite: bool = False, domain: str = None):
awgur@31 31 """\
awgur@31 32 :param name: Имя ``cookie``
awgur@31 33 :param value: Значение ``cookie``
awgur@31 34 :param max_age: Время жизни ``cookie`` в секундах.
awgur@31 35 :param expires: Значение времени, когда cookie будет удалена, задаётся в виде ``unix timestamp (int)`` или
awgur@31 36 ``datetime``
awgur@31 37 :param path: Префикс пути поиска ресурса на данном сайте, для которого следует отправлять данное ``cookie``
awgur@31 38 :param secure: Отправлять ``cookie`` только по шифрованным каналам связи
awgur@31 39 :param httponly: Сделать ``cookie`` не доступной для ``JavaScript``
awgur@31 40 :param samesite: Не отправлять данную cookie, если запрос пришёл не с того же сайта (анализируется заголовок
awgur@31 41 referer)
awgur@31 42 :param domain: Имя домена в рамках которого выставляется cookie. В современных браузерах может и глючить
awgur@31 43 """
awgur@31 44
awgur@31 45 self.name = name
awgur@31 46 self.value = value
awgur@31 47 self.max_age = max_age
awgur@31 48 self.expires = expires
awgur@31 49 self.path = path
awgur@31 50 self.secure = secure
awgur@31 51 self.httponly = httponly
awgur@31 52 self.samesite = samesite
awgur@31 53 self.domain = domain
awgur@31 54
awgur@31 55 def response_add(self, resp: bottle.BaseResponse):
awgur@31 56 res = {
awgur@31 57 'name': self.name,
awgur@31 58 'value': self.value,
awgur@31 59 'secure': self.secure,
awgur@31 60 'httponly': self.httponly,
awgur@31 61 }
awgur@31 62
awgur@31 63 if self.samesite:
awgur@31 64 res['samesite'] = 'strict'
awgur@31 65
awgur@31 66 for k in ('max_age', 'expires', 'path', 'domain'):
awgur@31 67 if getattr(self, k) is not None:
awgur@31 68 res[k] = getattr(self, k)
awgur@31 69
awgur@31 70 resp.set_cookie(**res)
awgur@31 71
awgur@31 72
awgur@31 73 def get_client_ip() -> str:
awgur@31 74 """\
awgur@31 75 Получение реального адреса клиента
awgur@31 76 """
awgur@31 77
awgur@31 78 ip = bottle.request.get_header(IP_HEADER)
awgur@31 79 if ip:
awgur@31 80 return ip
awgur@31 81
awgur@31 82 else:
awgur@31 83 return bottle.request.remote_addr
awgur@31 84
awgur@31 85
awgur@31 86 def get_session_fingerprint(*add_params) -> str:
awgur@31 87 """\
awgur@31 88 Конструируем некий отпечаток пользовательской сессии.
awgur@31 89
awgur@31 90 В качестве параметра принимается всё, что должно участвовать в создании отпечатка.
awgur@31 91 Это всё превращается в строки и подмешивается в хэш
awgur@31 92 """
awgur@31 93
awgur@31 94 ua = bottle.request.get_header('user-agent')
awgur@31 95 add_params_mixin = ':'.join(map(str, add_params))
awgur@31 96
awgur@31 97 return sha512(f'{ua}:{get_client_ip()}:{add_params_mixin}'.encode('utf-8')).hexdigest().lower()
awgur@31 98
awgur@31 99
awgur@31 100 def variable_prep(value, new_value_type: type = str, postprocess: bool = True) -> Any:
awgur@31 101 """\
awgur@31 102 Более интеллектуальная процедура преобразования базовых типов
awgur@31 103
awgur@31 104 :param value: Значение, которое необходимо преобразовать
awgur@31 105 :param new_value_type: Тип в который необходимо преобразовать значение
awgur@31 106 :param postprocess: Применять ли последующую обработку полученного результата
awgur@31 107 :returns: Значение ``value`` преобразованное к типу ``value_type``
awgur@31 108 """
awgur@31 109
awgur@31 110 if value is None:
awgur@31 111 return value
awgur@31 112
awgur@31 113 if issubclass(new_value_type, bool):
awgur@31 114 if isinstance(value, str):
awgur@31 115 if value.lower() in ('on', 'yes', '1', 'true',):
awgur@31 116 return True
awgur@31 117 elif value.lower() in ('off', 'no', '0', 'false',):
awgur@31 118 return False
awgur@31 119 else:
awgur@31 120 raise ValueError(f'Unknown method translate "{value}" to "{new_value_type.__name__}"')
awgur@31 121
awgur@31 122 res = new_value_type(value)
awgur@31 123
awgur@31 124 if isinstance(res, str) and postprocess:
awgur@31 125 res = res.strip()
awgur@31 126
awgur@31 127 return res
awgur@31 128
awgur@31 129
awgur@31 130 def get_variable(name: str, default=None):
awgur@31 131 """\
awgur@31 132 Возвращает значения из хранилища контекста запроса bottle
awgur@31 133 """
awgur@31 134
awgur@31 135 _name = f'{VARIABLE_PREFIX}.{name}'
awgur@31 136 if _name in bottle.request.environ:
awgur@31 137 return bottle.request.environ[_name]
awgur@31 138
awgur@31 139 else:
awgur@31 140 return default
awgur@31 141
awgur@31 142
awgur@31 143 def set_variable(name: str, value):
awgur@31 144 """\
awgur@31 145 Устанавливает значения в хранилище контекста запроса bottle
awgur@31 146 """
awgur@31 147
awgur@31 148 bottle.request.environ[f'{VARIABLE_PREFIX}.{name}'] = value
awgur@31 149
awgur@31 150
awgur@31 151 def make_response(
awgur@41 152 data=None, status=200, cookies: Optional[List[Cookie]] = None,
awgur@41 153 remove_cookies: Optional[List[str]] = None
awgur@31 154 ) -> bottle.HTTPResponse:
awgur@31 155 """\
awgur@31 156 Формирует ``API`` ответ.
awgur@31 157 """
awgur@31 158
awgur@31 159 if data is None:
awgur@31 160 data = {}
awgur@31 161
awgur@31 162 res = bottle.HTTPResponse(body=json.dumps(json_type_sanitizer(data)), status=status)
awgur@31 163 res.content_type = 'application/json'
awgur@31 164
awgur@31 165 if cookies is not None:
awgur@31 166 for cookie in cookies:
awgur@31 167 cookie.response_add(res)
awgur@31 168
awgur@41 169 if remove_cookies is not None and remove_cookies:
awgur@41 170 for cookie in remove_cookies:
awgur@41 171 res.delete_cookie(cookie)
awgur@41 172
awgur@31 173 return res
awgur@31 174
awgur@31 175
awgur@31 176 def get_request_json() -> Optional[dict]:
awgur@31 177 """\
awgur@31 178 Если в запросе, который мы обрабатываем в текущий момент, использовалась посылка данных JSON, получить их.
awgur@31 179 """
awgur@31 180
awgur@31 181 res = bottle.request.json
awgur@31 182 if res is None:
awgur@31 183 raise ValueError('Не обнаружено JSON в запросе')
awgur@31 184
awgur@31 185 return res
awgur@31 186
awgur@31 187
awgur@31 188 def get_cookie(name: str) -> Optional[str]:
awgur@31 189 """\
awgur@31 190 Получить значение ``cookie`` с заданным именем из текущего запроса.
awgur@31 191 """
awgur@31 192
awgur@31 193 return bottle.request.get_cookie(name)
awgur@31 194
awgur@31 195
awgur@31 196 def get_param(name: str, param_type: Union[type, str] = str,
awgur@31 197 default=None, postprocess: bool = True, param_source: str = None):
awgur@31 198 """\
awgur@31 199 Получить из обрабатываемого на данный момент запроса значения установленного параметра. Обрабатываются как
awgur@31 200 ``GET``, так и ``POST`` параметры, если иное не указано конкретно.
awgur@31 201
awgur@31 202 :param name: Имя параметра
awgur@31 203 :param param_type: Тип, в который нужно преобразовать полученный параметр
awgur@31 204 :param default: Значение, отдаваемое, если параметр не установлен в запросе
awgur@31 205 :param postprocess: Производить ли постобработку параметра
awgur@31 206 :param param_source: Источник параметра, [``get``, ``post``]
awgur@31 207 :returns Полученное из запроса значение
awgur@31 208 """
awgur@31 209
awgur@31 210 if param_source is None:
awgur@31 211 res = bottle.request.params.getunicode(name)
awgur@31 212 elif param_source.lower() == 'get':
awgur@31 213 res = bottle.request.GET.getunicode(name)
awgur@31 214 elif param_source.lower() == 'post':
awgur@31 215 res = bottle.request.POST.getunicode(name)
awgur@31 216 else:
awgur@31 217 raise ValueError(f'Unknown parameter source "{param_source}" for "{name}"')
awgur@31 218
awgur@31 219 if res is None:
awgur@31 220 res = default
awgur@31 221
awgur@31 222 if isinstance(param_type, str):
awgur@31 223 if param_type.lower() == 'json':
awgur@31 224 if not res:
awgur@31 225 return None
awgur@31 226
awgur@31 227 else:
awgur@31 228 try:
awgur@31 229 return json.loads(res)
awgur@31 230
awgur@31 231 except json.JSONDecodeError as e:
awgur@31 232 raise ValueError(f'Unable to get JSON from from parameter "{name}": param="{res}" error="{e}"')
awgur@31 233 else:
awgur@31 234 try:
awgur@31 235 return variable_prep(res, new_value_type=param_type, postprocess=postprocess)
awgur@31 236
awgur@31 237 except (TypeError, ValueError) as e:
awgur@31 238 raise ValueError(f'Error parsing parameter "{name}": {e}')
awgur@31 239
awgur@31 240
awgur@31 241 def dict_filter(data: dict, need_keys: Union[List[str], Tuple[str]]) -> dict:
awgur@31 242 """\
awgur@31 243 Создать новый словарь на основе существующего, добавив в него только нужные ключи
awgur@31 244 """
awgur@31 245
awgur@31 246 return dict((key, val) for key, val in data.items() if key in need_keys)
awgur@31 247
awgur@31 248
awgur@31 249 def json_type_sanitizer(val):
awgur@31 250 """\
awgur@31 251 Преобразует значение ``val`` в пригодное для преобразования в json значение.
awgur@31 252 """
awgur@31 253
awgur@31 254 val_t = type(val)
awgur@31 255
awgur@31 256 if is_dataclass(val):
awgur@31 257 return json_type_sanitizer(asdict(val))
awgur@31 258
awgur@31 259 elif val_t in (int, float, str, bool) or val is None:
awgur@31 260 return val
awgur@31 261
awgur@31 262 elif val_t in (list, tuple):
awgur@31 263 return list(map(json_type_sanitizer, val))
awgur@31 264
awgur@31 265 elif val_t == dict:
awgur@31 266 return dict((key, json_type_sanitizer(d_val)) for key, d_val in val.items())
awgur@31 267
awgur@31 268 else:
awgur@31 269 return str(val)
awgur@31 270
awgur@31 271
awgur@31 272 def make_log_ident(user: Optional[str] = None) -> str:
awgur@31 273 """\
awgur@31 274 Создаём строку, идентифицирующую данный запрос для журналирования операций.
awgur@31 275
awgur@31 276 :param user: Если известен пользователь, задаём его здесь
awgur@31 277 """
awgur@31 278
awgur@31 279 ip = get_client_ip()
awgur@31 280 url_path = bottle.request.fullpath
awgur@31 281 conn_id = bottle.request.get_header(CONN_ID_HEADER)
awgur@31 282
awgur@31 283 if conn_id is not None:
awgur@31 284 ip = f'{ip} | {conn_id}'
awgur@31 285
awgur@31 286 if user is not None:
awgur@31 287 ip = f'_NOID_[{ip}]'
awgur@31 288
awgur@31 289 else:
awgur@31 290 ip = f'{user}[{ip}]'
awgur@31 291
awgur@31 292 return f'{ip} - {url_path}'
awgur@31 293
awgur@31 294
awgur@31 295 def make_error_response(
awgur@31 296 code: int, err: Union[Exception, str],
awgur@31 297 msg: str = None,
awgur@31 298 remove_cookies: Optional[List[str]] = None,
awgur@31 299 cookies: Optional[List[Cookie]] = None
awgur@31 300 ) -> bottle.HTTPResponse:
awgur@31 301 """\
awgur@31 302 Создание сообщения об ошибке для JSON REST API сервисов
awgur@31 303
awgur@31 304 :param code: HTTP-код ответа
awgur@31 305 :param err: Либо объект исключения, либо название ошибки
awgur@31 306 :param msg: Если не ``None`` - сообщение об ошибке. В противном случае, если ``err`` является исключением,
awgur@31 307 сообщение берётся из ``str(err)`` если нет, становится пустой строкой.
awgur@31 308 :param remove_cookies: Список cookie, которые должны быть удалены с клиента.
awgur@31 309 :param cookies: Набор cookies вставляемый в конструктор ответа без изменений. Читать в соответствующем
awgur@31 310 параметре ``make_response``
awgur@31 311 """
awgur@31 312
awgur@31 313 if isinstance(err, Exception):
awgur@31 314 err_type = type(err).__name__
awgur@31 315 err_msg = str(err) if msg is None else msg
awgur@31 316
awgur@31 317 else:
awgur@31 318 err_type = err
awgur@31 319 err_msg = msg if msg is not None else ''
awgur@31 320
awgur@31 321 res = make_response({
awgur@31 322 'error': err_type,
awgur@31 323 'msg': err_msg,
awgur@31 324 }, code, cookies)
awgur@31 325
awgur@31 326 if remove_cookies is not None:
awgur@31 327 for cookie_name in remove_cookies:
awgur@31 328 res.delete_cookie(cookie_name)
awgur@31 329
awgur@31 330 return res