From d25d2ef9930335ad650e27c722e3ac585587b00a Mon Sep 17 00:00:00 2001 From: Arthur Bols Date: Sun, 21 Mar 2021 23:01:09 +0100 Subject: [PATCH] update --- client/command.py | 15 +- client/httpclient.py | 98 +---------- ...ResponseHandler.py => response_handler.py} | 98 +---------- httplib/exceptions.py | 41 +++++ httplib/httpsocket.py | 82 +++++++++ httplib/parser.py | 160 ++++++++++++++++++ client/Retriever.py => httplib/retriever.py | 11 +- public/index.html | 53 ++++++ public/ulyssis.png | Bin 0 -> 21458 bytes server.py | 111 ++++++++---- server/RequestHandler.py | 57 +++++++ server/httpserver.py | 94 ++++++++++ server/worker.py | 83 +++++++++ server_flow.md | 4 + 14 files changed, 681 insertions(+), 226 deletions(-) rename client/{ResponseHandler.py => response_handler.py} (68%) create mode 100644 httplib/exceptions.py create mode 100644 httplib/httpsocket.py create mode 100644 httplib/parser.py rename client/Retriever.py => httplib/retriever.py (91%) create mode 100644 public/index.html create mode 100644 public/ulyssis.png create mode 100644 server/RequestHandler.py create mode 100644 server/httpserver.py create mode 100644 server/worker.py create mode 100644 server_flow.md diff --git a/client/command.py b/client/command.py index e3d1c9c..19b8ae6 100644 --- a/client/command.py +++ b/client/command.py @@ -2,9 +2,10 @@ import logging from abc import ABC, abstractmethod from urllib.parse import urlparse -from client.ResponseHandler import ResponseHandler -from client.httpclient import FORMAT, HTTPClient, InvalidResponse, InvalidStatusLine, UnsupportedEncoding - +from client.response_handler import ResponseHandler +from client.httpclient import FORMAT, HTTPClient +from httplib import parser +from httplib.exceptions import InvalidResponse, InvalidStatusLine, UnsupportedEncoding class AbstractCommand(ABC): @@ -34,7 +35,7 @@ class AbstractCommand(ABC): (host, path) = self.parse_uri() client = HTTPClient(host) - client.connect((host, int(self.port))) + client.conn.connect((host, int(self.port))) message = f"{self.command} {path} HTTP/1.1\r\n" message += f"Host: {host}\r\n" @@ -44,7 +45,7 @@ class AbstractCommand(ABC): logging.info("---request begin---\r\n%s---request end---", encoded_msg.decode(FORMAT)) logging.debug("Sending HTTP message: %r", encoded_msg) - client.sendall(encoded_msg) + client.conn.sendall(encoded_msg) logging.info("HTTP request sent, awaiting response...") @@ -118,9 +119,9 @@ class GetCommand(AbstractCommand): return "GET" def _await_response(self, client): - (version, status, msg) = ResponseHandler.get_status_line(client) + (version, status, msg) = parser.get_status_line(client) logging.debug("Parsed status-line: version: %s, status: %s", version, status) - headers = ResponseHandler.get_headers(client) + headers = parser.get_headers(client) logging.debug("Parsed headers: %r", headers) handler = ResponseHandler.create(client, headers, status, self.url) diff --git a/client/httpclient.py b/client/httpclient.py index c3e4376..e0f23bc 100644 --- a/client/httpclient.py +++ b/client/httpclient.py @@ -1,6 +1,6 @@ -import logging import socket -from io import BufferedReader + +from httplib.httpsocket import HTTPSocket BUFSIZE = 4096 TIMEOUT = 3 @@ -8,98 +8,8 @@ FORMAT = "UTF-8" MAXLINE = 4096 -class HTTPClient(socket.socket): +class HTTPClient(HTTPSocket): host: str - file: BufferedReader def __init__(self, host: str): - - super().__init__(socket.AF_INET, socket.SOCK_STREAM) - self.settimeout(TIMEOUT) - self.host = host - self.setblocking(True) - self.settimeout(3.0) - self.file = self.makefile("rb") - - def close(self): - self.file.close() - super().close() - - def reset_request(self): - self.file.close() - self.file = self.makefile("rb") - - def __do_receive(self): - if self.fileno() == -1: - raise Exception("Connection closed") - - result = self.recv(BUFSIZE) - return result - - def receive(self): - """Receive data from the client up to BUFSIZE - """ - count = 0 - while True: - count += 1 - try: - return self.__do_receive() - except socket.timeout: - logging.debug("Socket receive timed out after %s seconds", TIMEOUT) - if count == 3: - break - logging.debug("Retrying %s", count) - - logging.debug("Timed out after waiting %s seconds for response", TIMEOUT * count) - raise TimeoutError("Request timed out") - - def read(self, size=BUFSIZE, blocking=True) -> bytes: - if blocking: - return self.file.read(size) - - return self.file.read1(size) - - def read_line(self): - return str(self.read_bytes_line(), FORMAT) - - def read_bytes_line(self): - """ - - :rtype: bytes - """ - line = self.file.readline(MAXLINE + 1) - if len(line) > MAXLINE: - raise InvalidResponse("Line too long") - - return line - - -class HTTPException(Exception): - """ Base class for HTTP exceptions """ - - -class InvalidResponse(HTTPException): - """ Response message cannot be parsed """ - - def __init(self, message): - self.message = message - - -class InvalidStatusLine(HTTPException): - """ Response status line is invalid """ - - def __init(self, line): - self.line = line - - -class UnsupportedEncoding(HTTPException): - """ Reponse Encoding not support """ - - def __init(self, enc_type, encoding): - self.enc_type = enc_type - self.encoding = encoding - - -class IncompleteResponse(HTTPException): - def __init(self, cause): - self.cause = cause + super().__init__(socket.socket(socket.AF_INET, socket.SOCK_STREAM), host) diff --git a/client/ResponseHandler.py b/client/response_handler.py similarity index 68% rename from client/ResponseHandler.py rename to client/response_handler.py index 6b71282..824f383 100644 --- a/client/ResponseHandler.py +++ b/client/response_handler.py @@ -1,14 +1,15 @@ import logging import os -import re from abc import ABC, abstractmethod from typing import Dict from urllib.parse import urlparse from bs4 import BeautifulSoup -from client.Retriever import Retriever -from client.httpclient import HTTPClient, UnsupportedEncoding, FORMAT, InvalidResponse, InvalidStatusLine +from client.httpclient import HTTPClient, FORMAT +from httplib.retriever import Retriever +from httplib import parser +from httplib.exceptions import InvalidResponse class ResponseHandler(ABC): @@ -31,17 +32,6 @@ class ResponseHandler(ABC): @staticmethod def create(client: HTTPClient, headers, status_code, url): - # only chunked transfer-encoding is supported - transfer_encoding = headers.get("transfer-encoding") - if transfer_encoding and transfer_encoding != "chunked": - raise UnsupportedEncoding("transfer-encoding", transfer_encoding) - chunked = transfer_encoding - - # content-encoding is not supported - content_encoding = headers.get("content-encoding") - if content_encoding: - raise UnsupportedEncoding("content-encoding", content_encoding) - retriever = Retriever.create(client, headers) content_type = headers.get("content-type") @@ -49,78 +39,6 @@ class ResponseHandler(ABC): return HTMLDownloadHandler(retriever, client, headers, url) return RawDownloadHandler(retriever, client, headers, url) - @staticmethod - def get_status_line(client: HTTPClient): - line = client.read_line() - - split = list(filter(None, line.split(" "))) - if len(split) < 3: - raise InvalidStatusLine(line) - - # Check HTTP version - http_version = split.pop(0) - if len(http_version) < 8 or http_version[4] != "/": - raise InvalidStatusLine(line) - - (name, version) = http_version[:4], http_version[5:] - if name != "HTTP" or not re.match(r"1\.[0|1]", version): - raise InvalidStatusLine(line) - - status = split.pop(0) - if not re.match(r"\d{3}", status): - raise InvalidStatusLine(line) - status = int(status) - if status < 100 or status > 999: - raise InvalidStatusLine(line) - - reason = split.pop(0) - return version, status, reason - - @staticmethod - def get_headers(client: HTTPClient): - headers = [] - # first header after the status-line may not contain a space - while True: - line = client.read_line() - if line[0].isspace(): - continue - else: - break - - while True: - if line in ("\r\n", "\n", " "): - break - - if line[0].isspace(): - headers[-1] = headers[-1].rstrip("\r\n") - - headers.append(line.lstrip()) - line = client.read_line() - - result = {} - header_str = "".join(headers) - for line in header_str.splitlines(): - pos = line.find(":") - - if pos <= 0 or pos >= len(line) - 1: - continue - - (header, value) = map(str.strip, line.split(":", 1)) - ResponseHandler.check_next_header(result, header, value) - result[header.lower()] = value.lower() - - return result - - @staticmethod - def check_next_header(headers, next_header: str, next_value: str): - if next_header == "content-length": - if "content-length" in headers: - logging.error("Multiple content-length headers specified") - raise InvalidResponse() - if not next_value.isnumeric() or int(next_value) <= 0: - logging.error("Invalid content-length value: %r", next_value) - raise InvalidResponse() - @staticmethod def parse_uri(uri: str): parsed = urlparse(uri) @@ -196,9 +114,9 @@ class DownloadHandler(ResponseHandler, ABC): def _handle_sub_request(self, client, url): - (version, status, _) = self.get_status_line(client) + (version, status, _) = parser.get_status_line(client) logging.debug("Parsed status-line: version: %s, status: %s", version, status) - headers = self.get_headers(client) + headers = parser.get_headers(client) logging.debug("Parsed headers: %r", headers) if status != 200: @@ -297,8 +215,8 @@ class HTMLDownloadHandler(DownloadHandler): client.reset_request() else: client = HTTPClient(img_src) - client.connect((img_host, 80)) - client.sendall(message) + client.conn.connect((img_host, 80)) + client.conn.sendall(message) filename = self._handle_sub_request(client, img_host + img_path) if not same_host: diff --git a/httplib/exceptions.py b/httplib/exceptions.py new file mode 100644 index 0000000..930ae7a --- /dev/null +++ b/httplib/exceptions.py @@ -0,0 +1,41 @@ +class HTTPException(Exception): + """ Base class for HTTP exceptions """ + + +class InvalidResponse(HTTPException): + """ Response message cannot be parsed """ + + def __init(self, message): + self.message = message + + +class InvalidStatusLine(HTTPException): + """ Response status line is invalid """ + + def __init(self, line): + self.line = line + + +class UnsupportedEncoding(HTTPException): + """ Reponse Encoding not support """ + + def __init(self, enc_type, encoding): + self.enc_type = enc_type + self.encoding = encoding + + +class IncompleteResponse(HTTPException): + def __init(self, cause): + self.cause = cause + +class HTTPServerException(Exception): + """ Base class for HTTP Server exceptions """ + + +class BadRequest(HTTPServerException): + """ Malformed HTTP request""" + +class MethodNotAllowed(HTTPServerException): + """ Method is not allowed """ + def __init(self, allowed_methods): + self.allowed_methods = allowed_methods \ No newline at end of file diff --git a/httplib/httpsocket.py b/httplib/httpsocket.py new file mode 100644 index 0000000..a894d09 --- /dev/null +++ b/httplib/httpsocket.py @@ -0,0 +1,82 @@ +import logging +import socket +from io import BufferedReader + +BUFSIZE = 4096 +TIMEOUT = 3 +FORMAT = "UTF-8" +MAXLINE = 4096 + + +class HTTPSocket: + host: str + conn: socket.socket + file: BufferedReader + + def __init__(self, conn: socket.socket, host: str): + + self.host = host + self.conn = conn + self.conn.settimeout(TIMEOUT) + self.conn.setblocking(True) + self.conn.settimeout(3.0) + self.file = self.conn.makefile("rb") + + def close(self): + self.file.close() + self.conn.close() + + def reset_request(self): + self.file.close() + self.file = self.conn.makefile("rb") + + def __do_receive(self): + if self.conn.fileno() == -1: + raise Exception("Connection closed") + + result = self.conn.recv(BUFSIZE) + return result + + def receive(self): + """Receive data from the client up to BUFSIZE + """ + count = 0 + while True: + count += 1 + try: + return self.__do_receive() + except socket.timeout: + logging.debug("Socket receive timed out after %s seconds", TIMEOUT) + if count == 3: + break + logging.debug("Retrying %s", count) + + logging.debug("Timed out after waiting %s seconds for response", TIMEOUT * count) + raise TimeoutError("Request timed out") + + def read(self, size=BUFSIZE, blocking=True) -> bytes: + if blocking: + return self.file.read(size) + + return self.file.read1(size) + + def read_line(self): + return str(self.read_bytes_line(), FORMAT) + + def read_bytes_line(self) -> bytes: + line = self.file.readline(MAXLINE + 1) + if len(line) > MAXLINE: + raise InvalidResponse("Line too long") + + return line + + +class HTTPException(Exception): + """ Base class for HTTP exceptions """ + + +class InvalidResponse(HTTPException): + """ Response message cannot be parsed """ + + def __init(self, message): + self.message = message diff --git a/httplib/parser.py b/httplib/parser.py new file mode 100644 index 0000000..771b7e1 --- /dev/null +++ b/httplib/parser.py @@ -0,0 +1,160 @@ +import logging +import re +from urllib.parse import urlparse + +from httplib.exceptions import InvalidStatusLine, InvalidResponse, BadRequest +from httplib.httpsocket import HTTPSocket + + +def _get_start_line(client: HTTPSocket): + line = client.read_line() + split = list(filter(None, line.split(" "))) + if len(split) < 3: + raise InvalidStatusLine(line) # TODO fix exception + + return line, split + + +def _is_valid_http_version(http_version: str): + if len(http_version) < 8 or http_version[4] != "/": + return False + + (name, version) = http_version[:4], http_version[5:] + if name != "HTTP" or not re.match(r"1\.[0|1]", version): + return False + + +def get_status_line(client: HTTPSocket): + line, (http_version, status, reason) = _get_start_line(client) + + if not _is_valid_http_version(http_version): + raise InvalidStatusLine(line) + version = http_version[:4] + + if not re.match(r"\d{3}", status): + raise InvalidStatusLine(line) + status = int(status) + if status < 100 or status > 999: + raise InvalidStatusLine(line) + + return version, status, reason + + +def parse_request_line(client: HTTPSocket): + line, (method, target, version) = _get_start_line(client) + + if method not in ("CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE"): + raise BadRequest() + + if not _is_valid_http_version(version): + raise BadRequest() + + if len(target) == "": + raise BadRequest() + parsed_target = urlparse(target) + + return method, parsed_target, version + + +def retrieve_headers(client: HTTPSocket): + raw_headers = [] + # first header after the status-line may not contain a space + while True: + line = client.read_line() + if line[0].isspace(): + continue + else: + break + + while True: + if line in ("\r\n", "\n", " "): + break + + if line[0].isspace(): + raw_headers[-1] = raw_headers[-1].rstrip("\r\n") + + raw_headers.append(line.lstrip()) + line = client.read_line() + + result = [] + header_str = "".join(raw_headers) + for line in header_str.splitlines(): + pos = line.find(":") + + if pos <= 0 or pos >= len(line) - 1: + continue + + (header, value) = line.split(":", 1) + result.append((header.lower(), value.lower())) + + return result + + +def parse_request_headers(client: HTTPSocket): + raw_headers = retrieve_headers(client) + headers = {} + + key: str + for (key, value) in raw_headers: + if any((c.isspace()) for c in key): + raise BadRequest() + + if key == "content-length": + if key in headers: + logging.error("Multiple content-length headers specified") + raise BadRequest() + if not value.isnumeric() or int(value) <= 0: + logging.error("Invalid content-length value: %r", value) + raise BadRequest() + elif key == "host": + if value != client.host or key in headers: + raise BadRequest() + + headers[key] = value + + return headers + + +def get_headers(client: HTTPSocket): + headers = [] + # first header after the status-line may not contain a space + while True: + line = client.read_line() + if line[0].isspace(): + continue + else: + break + + while True: + if line in ("\r\n", "\n", " "): + break + + if line[0].isspace(): + headers[-1] = headers[-1].rstrip("\r\n") + + headers.append(line.lstrip()) + line = client.read_line() + + result = {} + header_str = "".join(headers) + for line in header_str.splitlines(): + pos = line.find(":") + + if pos <= 0 or pos >= len(line) - 1: + continue + + (header, value) = map(str.strip, line.split(":", 1)) + check_next_header(result, header, value) + result[header.lower()] = value.lower() + + return result + + +def check_next_header(headers, next_header: str, next_value: str): + if next_header == "content-length": + if "content-length" in headers: + logging.error("Multiple content-length headers specified") + raise InvalidResponse() + if not next_value.isnumeric() or int(next_value) <= 0: + logging.error("Invalid content-length value: %r", next_value) + raise InvalidResponse() diff --git a/client/Retriever.py b/httplib/retriever.py similarity index 91% rename from client/Retriever.py rename to httplib/retriever.py index 5a66cf9..280a3d6 100644 --- a/client/Retriever.py +++ b/httplib/retriever.py @@ -2,13 +2,14 @@ import logging from abc import ABC, abstractmethod from typing import Dict -from client.httpclient import HTTPClient, BUFSIZE, IncompleteResponse, InvalidResponse, UnsupportedEncoding +from httplib.exceptions import IncompleteResponse, InvalidResponse, UnsupportedEncoding +from httplib.httpsocket import HTTPSocket, BUFSIZE class Retriever(ABC): - client: HTTPClient + client: HTTPSocket - def __init__(self, client: HTTPClient): + def __init__(self, client: HTTPSocket): self.client = client @abstractmethod @@ -16,7 +17,7 @@ class Retriever(ABC): pass @staticmethod - def create(client: HTTPClient, headers: Dict[str, str]): + def create(client: HTTPSocket, headers: Dict[str, str]): # only chunked transfer-encoding is supported transfer_encoding = headers.get("transfer-encoding") @@ -44,7 +45,7 @@ class Retriever(ABC): class ContentLengthRetriever(Retriever): length: int - def __init__(self, client: HTTPClient, length: int): + def __init__(self, client: HTTPSocket, length: int): super().__init__(client) self.length = length diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..225dbac --- /dev/null +++ b/public/index.html @@ -0,0 +1,53 @@ + + + + Computer Networks example + + + + + + + + +
+

Example Domain

+

This domain is for use in illustrative examples in documents. You may use this + domain in literature without prior coordination or asking for permission.

+
+
+

Remote image

+ +
+
+

Local image

+ + + + diff --git a/public/ulyssis.png b/public/ulyssis.png new file mode 100644 index 0000000000000000000000000000000000000000..14819f53d630371eba1739557fffa1bdea8255fd GIT binary patch literal 21458 zcmZU*2RNH=7e8*+rmCpj(i*K%wS#IYHHsRwV%4ZxA&5OYXsfhJQL}1~*piq@ZAG+3 ztRO|}OY9Xhevj|_zW@L4x_-}fS&!s7_c`~u&;2={bMCx-Z1j+Yk(ZHzf`Ua)_kk$| z1&tB#`!@Xr;JaB^bQ17`#$Qv0RCce_sjEwhAzJ5AbrOFnsFbdNs&B;Q3WE z9~XCD;461mMhtij>-lT$!vdcB_)$E8ctU)FAn&g+Zv+1k3^lh3c;M>q?C0(q;O+yV zpa{+~FcBr@e z{r&s{1BWKRoDQtF<$mZ{f>MWVUF>?(-^#CZL`1G)6$SYmE#OOddQxv3s?cA1cgX}PpCArk8TKQUC3Td2N&qekmqe;ezpZ+JsoLF_ntPPdBv=hbchh|8gO%sA7!Nd09uT~5v z=0i+J%<{8Fj{geO^bw1C(A$A|$2oKc8ajL!Gi0e2<)IU;1r^XS5T8vAx$Dc{0ly~l z-!UYerk}9w71Jr7#pdjMph7cz)VZmMflr%`S`W)W91j{?{%f72)A_{(Zsiu(#Oe9q=_^cq z{%6aATs@4_wnnEAuXy%&*zlFY$$ZJS2PmUt$u<*DhW|<|sB%Bhz@V=`2GMa9`1^hh zSdpg+&)&=y%|~gwDwS1FeEIiD^!IT&mYTVRJK>4UHnrG9ujQNLCZW<6| zFmM$1BM&CjISLuG|5IWo}ySH3n{SjLeGhEsa?v?2eC)8nV za?lRc2T4o(`ItG8KX(5el)jD7SsZDc=8-MzBY0J^WmQKpLxWJXOz|XVd~en_`#}OJFF zy@!B|f}K$)pcafrO1aGjc*i!sr)E!|K1QZD_r;D4VTE!iB8 zK7k1?cQt!c95ULci-pe79p67JT6x5jZp17@Ih*D0#pS#BacM81e#=hA8pnvr{-TIU zV76gTzq4PbR75B$9u$fXiHN-WKCCL#cXduXzxF|F9_ZFEiwBYY^Xa`7yHVTGrJLts zi{z0bE_Z#ySe8@l(q&<;<9ZiMofs;2uKT#?^|Fh#{gS$-`do9G{)~PoXdb7;GwOoR z5Dy_McB)K!&^j{VpHC-rJ1yiyRT)GsIdB?8$N93Avd!xl+Ogq(?wASK4u4pw&{Zmd z^W8ko3!W}%EE)Q`^S!yUFPlnfo;SqHuuZ2GmhKei`yc5q>-S#&O#qE2QV;L~JHN89H>w8Cjwr=Cv|Lipz zn7e5=-S<|%?;2L{jB0~wHhOxvTJPC!LB*kV!-rNb8JW+%P_-}yvkmgV0{DCxeQ!d_ zu@gnqKD4tj#6RAjCQ)5tLsVETBA>2$<#%<8U4Ln^3H!@nKC*Y5C8Z!+iiLO&Mecx# zA|2^QcdJ&;u_}h>mncPp>ztb$y-Y}harWo;xKU4~ zhCQwfM*f6bo6qN=Y`(XrRK!!LeLJcvHazW5oFzY4kixX_88jb)x6}32UWJW#tjJ5c zBjWq2GSg@tE%_RQ@(YmnR=ZJ&SuH1RdF_p23n2X+Y((U1|6;F8)ndN<>oIVKA?a)L zCWIi&adh@C|HsKC*fQ+jN%rI8%fiz15l?yby<4@sPwx$rqYvmDv!54kR5tjw#^Ojw zB`!i+VZ#*!Zph5nv)sxdtp!D;BxBn7qj7P3h)7kmBMit#DP);$n{DqL`IM>K@)J}@ z;tB>PFW)WYM{4TCoa7XL7fT=gIdK!|kC zQ8mY1%K-(XAHDh*fr+^#6MTJn>pWMGcqT=B4f^iFLM|#VM>sWHfTqJpYk6rhCqO4g zfU4-pSl#9l?Cjmr8Da_l;p4tjqie;x=;$`5ZWxqruv$@J#mXu5KwAA5?tNr__{>_IsLvgUz8XaGu1S8}5RtKh8@ zE>;Y@-|4hzswkc9gd(_`f^`3f8DL`;ZO@fLLvK58^>Mcvbh@?LBP4H8p?5WUE;rHs zhsW%jQ-zfTmU%AAg8FaloY01Q|9c3rmo|q zea^bcd-ZEjS_l%=;@wfmKW05PnO+l4eE0wpaA6N*+@=$Qq}5$`lCvX2 ziD3Sy!`w`j=6u#vy4iCHe~jPJi=uh%RaQ6n27#OWCL^+v_{GG))4;oxC&>kz8+-b? zZ$7ox^|R|K|Mc<9D*-3dsWLD)z(7AI5#1)pc^eK^U(ULCJNte`BA>~pw5rFdv8Nr$ z-Br|C8uf26H3cdT4!=K4L+9>PsgjEjSu0-t`G&B4!wH8WNit_+g~cc?ldrFS?_1m) zXaNcMM}HIR^l@v{iLga2Be{WT<$bd-;2@8|^Hp`ZjOn<~IRQqj>Da4An~Ux#x6M8W z-qKyLrW~es*CGr}pzUv#b!ig32q7tFsXkG7X$CP0&yKH6Q4_Za;u1FFJRnWPY#wG)G z%!sSVYILAAKW}3N%na<~mR`RnbN%tAWPQF-1fpl(ekj4A^9O53o1qKT^xRuKgvspI zZWkUA*>VCRIRT-Oi+w}lkn3(why;hrq&+~_r#EcHzU+Ax2ty>lv&^0GuWR)}QLKZJ z4j4(w{Tp^=bu({@#wJsAwA57mtcl+cvk6yFaLX?bTMb{MS=M-o%mHuqB7E9g5x_Q@ z;oDjO=Avdx)>xHY;^UNDR1ofGC(#oTBr#jaf!$w_TG8TZpJY8(< z_TU@e_pws}gOl~~Xr}zX&w>Gwzv@AmKjfA!5kiTNXucw51*nmG44jt?3iDpv<;V3M zwJH6!2Rr238=~xo)!9N54cXGMB00{mpsa94lIYBk6QP50IGvgQw+fjB|NOFnal4`3 zuvY1A6VJg%n5bU~R$oJ6bh4pam{!U8jBNUOs%dv8$~$5rG5K?J!b{iUjN@n(+?RhY z5{+U+18A)NC#g=oM>8<1;Lf~@G5&>1h>^%6a0EkjB(_GZdhSQWErJgQ(S7)_{{1}S zpU44_NW{6E(ot)5pV)ka^r_?sE^bYEvaEeB7PlKaC6ALCw&W{zaJf_62ovodpj>aj zzS0%44g2rf%@uzA%Nu zo%E@uMe8Cs+w@^x?35vgZ}ziPQ}i5|;t7tGQDV>Y#>^k0@)!L>`24$q+w+U}9A$>K zlA~3r3+m*mD2K1$STT(Sas{@-P!ZpHnCKAz7t|!itRn==jlAu)CC71NP~n&9 z43A_VoiPx&I(GXXPhXVyn^`8ONp7!sSt|&=^CGIgPYbgZGaQ<>G$B+o9UH8QNBrZ- zTPU+^^Ma4+6a~}iHUXYmI%cizZvj{%aANAE@E^Q zA0ROICRwj@gZ?g!5{>eZ8-1P5cqrzUS`S<&)9WR%-g;U*LT~yl>i3p}Nj7Kh=t@P7 zN?b8Q`fkK@6UfNrB97F`K$<$@-ScSQ`vfT~U+7etr`=3Y?ZUN&II|N^x zqc{tBr%adr^dq8RBb0E3_~?B;=+@oRvB@XVEO0s5zYxbtnCOMCz|$MOBhnL- z-=bjrrnk9Cn-Y7~>z8#So?2C-|3#3c+lFH?p*mUj@QKOe^9~#kC;*t$KE7!#(W4#bYRNj6^D-}N3}kqoMTDB zUcJk<>h=*LAlP9yZjI@1K07@?<%(eRoolvHQB^OGQ-eKbc#L})Fxt2;R~`aT1t-=8 z@;rSsduwlL$KHeJLA*PQxcjK*Ew4wA_1y932B{mxopaeXj;O$?XTs>2b+08c896Vc z8NvJ%=b1s@GVOnI9Us^O3*{uMDSSW0aH>Pq4TN-rm{PjUZ3QNuuP$|?d_E$-d%J6> zJO-6EvL3tv+}$Kw>LE-~3OBUaY8rKSs05ytkI2yy5HrO!y_#Njx)KkSVU;ig+^o2= z6vQQBWI=m(RV7l^H}Gq{J&jj}|Ees37&9#4DL^P!D7GHcB$yIpoS{6v#w^QN+DhK| zEz#dPBuK*a-tR9cDYzNWnrcNCO2P{k^!Km?n_3LYP97 z_KMRShW;+kqgOG>;S!{-UroJ|_c!mKgv}ToKQ<7n0KT&y99Jok_;Nr5uqU z(&d~KT(0w-exQ*Ypu@mr%@QoT>5bQXj~n+*Kk(WtH_cx0QY)*2+Pys8>vNjvb!7pv z1HlH^2ZKxEwp^u>rqi@O9sBlY!smOh;=Z?VeA)x)a$QaiU$egLl?@VAVck2i1QSB_ zy=9l8g}cY_^ps~Dcx#KhIEo|YBkp4HG>ppc`1IvuEv~t$h&>HMYjB|7d_HA4 z4ovDo0NbbO*vrN7IR{#OXBwrGJ2C=u5c!USEHQW&hLv&zG@XoF2K0B<#^W7{@eSD}oET+hS&w6`$RAGC06+wYZ3x9BT^e0owh<5)<&lDm$P^T)N$xdZs&M{+d@D7|<;_#Ol`Zs5bxvX?qco**m}4 z>&nTHS29=uFh5M{?kBe~hq>>gT9eKI;^~<-LD1NyiaFhaM`(Mo5F30&Y1i`I8q3Mc z1Ja@fhEd_O`$Ygoj_iR{XX(i%aj@d_9;)8o>d5j(8& z?N+?im%aJM)*6FzEqx`)p)Dp~1H$K=UQPTx;m+BaGeEIkjRyFqCeHNBx8cGZ(V6Op zrMI{qpY0YXD$`hMIOCIFqt?XD@~A@!a2Po26bt#h(g}m zT3B1mbQE{@fC0qxt0!l|>g4#F<^{rdCK+%2=p6NboLa&m~PQ&#u0jw?jWh9~$C z!*qI2s<+C|;qoDZ1un9m2smre`Bb8$06?i3!bxsA8MwE6QYDrUe=FzsLyrHP?u7UE z2)2GU)2O)RAvHk3Ho?}EHKiBRoE=Ei*3~VacHA)_aXQZckVtZJ_)vH7e6f}l0O$g| zGq}0S;v?7Q{m&CC!|xhV=tTO)YA0snfAOTzxm`_n;gbebg*!Klc`ctPZ;*3#5~*aW zeAgqxM<|K*KYi8lzLx#tO`b8Iow@6^0%;*%r_`>a;w*7-vV(IPq zRb9yLz;ZES_HKd~5#7<|CiibUNh--p3AiW;8t66l0V9h?D#ccc0b=70$WNsXhYhi0?3Yp z^B!T)k;8p4!zu&G-Qq8!ocDQ$8dC|e$-ui=$*UOm&nEIyqJj8J=r=EppKT1KG>Zwz z{&2Pi0QpZK*tiLK5&e8l)Bq37L(H=DA@{Nko>eB6G({9J|J9h7-Jk|m#HY(#LxWBh z=g#t%&oA8KtGqoy&pG8g&eWQcD|u-m+7PvFa0 zV2U1}Uy1wySTkP=rugCRhr?2MP@EpvvghrA7gfdcbaFl>J9arO4{_~|+T45vY_*K~ zkE)Ty_c_^ZZ+w00W6}BCvV%h)MmOBquL{v_mesRzAn~iRqG~JOGXuLpScbh@L4A)$|kEl%K*s5vU7@v=|DI5UM>G{Y@ojdhP5|e`{iHrPU{3KCS+62*HG zJx9wK#tjNA%>c5N`Yu5wk_s~MT=!u}VgzNT(H}eJ;mhEB)U~1391Kg0y=@reLNRDs z-4IwN79DG~sV`I|$fn;Q|9Ps+2=ormgqqI0RxmxP2gD)ZG$ZlzRU*azWTg3&VQ(_G zEf{KC)5!{_uo79C@WNx2uC1}CDJ)H>W7q(*;}Uh2-(|yn5;2D2^&l{w(`FJTbgM%mfDIlF>y_=2Gw~ z(U0rl18L&zQCzb`BO;P{pHnuTbWZ3rPy#WBVc21D`EUaHR|Ut3b4Y9%U3*|#oI2W%6g7Eg?pEjy!#y0zIvm9nrk*=*pL4{V znZq2-YL$sh1B?5&Z%R!UJ+q>tvXjUB$Y%)5(;T#F26>wBWGm3IFw41k0Wn)w8}oE( zVUQw{slq#(T`yy$qP~j}r66W@n=jop`z;iBG?%u3mg%~&aNf(-jK)MR$Gi~H|ZMU}}jO5bF_rD7igE8VA451p7?JkO;ww3FvCxlHuk zM&}&~Ks-Jcnk?)dPg0;Llt@uB3oW{#fH|a0LuOye=MIhiXV4xcGp-REX6QM-j~bn5 zh@lmmPC;(mX3!gOg7RVg5-Ez()?7-ppXWPT7rytc+7U_Xm&AZ!iq=>_2&<(8w0+?O zm(>k({hI=X-?ap%**vii>v zWH*0bRk6X{U-Zc*857#+-C1vr(T&(QQ@YI~yxW*jmxE5FX(6%$X%j#qkN7E9oIBMt zswp5g{9!KwO^Ylt{DEk|-eUu%u$HcGLz$sLdG)G@QFy*#n?cs=@(qH7v-ixZI@7Ia?Vyt#U=}FO{ zs=g10&~+$2D{}|M8&}7y-Cr^k)S2`wpg^g-fDCOjkP#R4IN0r*R>ej$$mqhzE8>k$ zJ;-}r*`8UCFlzIm$H8V$dYxJ;`e6>FbLpoXu_EBUxRabLdmYi$OiRD6kDf-2}b z3eskVddX~GEn8&}6)DL`%*yg#nfGrFWqw@ehMgzc4V$p6lDxpQy;kGXNMo^V^#vc= zzt(P=1urIZoIMF`_sfpF^?sRW_hFzpbBB^$U@^7@-R-$Jw0@}fu}2fh(WaXbMZAN` zryl%YFTlu-YP{9apyU3}&|AJD5PcjR(;jTSDmgYu25@G_(U%s>A45wX#M4qZT|puT zkI#)&hhTs$0>IYq1JHF*it+`WaP!}M&^xrsR1@+`2Q!$roFf}w-YsG-)3*v{dBuSd`tV=a)(*fnAiKNO%n4-S+Bbblggd4-T>JP?g;?ZBJ$I!Gd@GXf!4@)6MX1xVMEK&Quc3hFrE9nur{tk z!B;~cQl9CmK0TW%zM_^x5y{B^KB^wdSBh1XYqE8d^XVYpb=fNYm&<7V^6LN*sl3lE z8-JylM>i_j_w)F6WEDqi%Gm0A0Puce5UPFft|A7VBg7$|Jr=VTO$}|H4(@=~3(GW`GOHN3otg5l&OM+^`p@ZR#DNi9H)n1kJeo-_DzKCtQ{Is9cI0 zI*2iRxwgbgc&PBVu&gKg1K3UTg*Cg9{E?1rT)4YVqla#C(b=2>s zLYuG8;Vd!m0MD5Yd40JeNPQovx^-^jyed%H-Dc-fV-EoH(Wf-Q(I=r~x8@4kJ?M7- zfnY~Ci{~X@_5G@s?>3>Fo90swVTjgW-JoXG{c(l(bYf%|z`@VzR>FPaPPO~KF_-~y zHuoIx1LpYDhl=QhPj`vm7CW~|trWTGF1NA(GM!lN>yvIc9UFFITQ%-4MfsE~{F7F7 zl$3er+t(ZTEQ+AuJ02#AXZEGF_6K{i>HdBA0u-t>!$&nc`* z$hn#g%w>P>y7MW3BlXou`Cj)Gh0OY+j}Da~Y3XbxY8mr%Doa-sQ{Vp~h7&STq@R;%L7f5pKE2Ga_*1I#;c2@DR4FH z?Z<_HHCl~}D&`x{lWujo>97cEpjwqW%;mNb52$LN9HSMZAscMhDs*u5Fq;d>3dKHk zW{1So%u$KWbMc0;LQ)>hIxRmDAoLl7b)E>mI`#Y}3{lf*&glc3S_lC4N={KMbyjkI z*3PD2pQv=iUexDaM>|xo=An72F3|xXRri@?yX3nQPrQv#D~{fG z_OYxG@}G$zs8+!o{i@cUcKQz2Yr7YGv?E*&9c00q^WBxZ;wJ7Zw)$LuAJ}E+I zbnX_5ZGE#iR72K!?O*i#2El1{<1?TT<*FvYL^A zFiD*;oOeP9iSc~qPXLk`jdy%1u&Sj4AG(3#0K6tbrUT}iMP*BmPbA_Qe(nF2n^1~5 zTTmIVHjBzwY+UrQhCh$QYi$KC7y``O`g8s4Q?`9z7%U=bW9I=aKNUen(lBOd>vxP! ztq%hJT6fN9CjU#?$op9rHn$l=Pmd+Ybfuh70j?*+LnKwIr#7Bfn~rjWs*U-C5efd; zLb;>Rs~Ms}%5q~FnrrAYCq&gm%>aRD!CeTbt3F(T1qxRt&mMsis6GJdD*zGt;Vy#g zNcEb(i)A5)|C~%^0OH+ERg2L-RTIc~$&A{sMe_YRmyH`#zAZKunM5!xG!=O!Qt(lP zQCCr8Ws;CfpVfk9k=Fk0u4vgDTKtPsVzIE-0G(ofHn1*vO2;P$YtTB@C&0Qi1K1So zn9-n9l6c)(`MA|2>;^o|B&I`vsVEc}%RmNO%@wT`W zpj<8Ytzj>M1E@Z@_1rAjh4CRss(X!OxoUt3J7<)J4LLT^toZkQ!=RFzO!x@Le}>I1 zpZ&A`ly*rOvxC%#|3v(oPQm{IJTZQx@We!&>k~T691MilTc(wDLKyOq3lt!mi*&tP z?bz$#q)6w~;SAv;U#r})YP$n>hUK}9l&M%9XFcYyg`$Yd^EU1?&met`M(F?Mi!*9^N(FoQEw!AX~HR1=o(Z7(*OJEz?JO{O_Tug zYW_~0E9%&sqwI8I#CoHmh<6(Hp6P5N zA}y(2{}BE&RyqAw9zY-5-_oj^{Lejq6=(n$R`(=u31G|peaeH$V@%jw?wxKjZa7UG zyHtL3GE@HotruZeoTA2HNr#RDn=5_x266WH=&6vZJpn|;0Tej*YS5zAwg5Be4Q%gO z^i#NE(Z^pUx$@v|hyk>KON`}xYF+OQ0(H(U-aviGK*3{VJm z<@Q9XE~@qK9LL8zadK^_7O^1!>jC<9 zrgS`r%Q_zsIV|7x-S5t1qd0wUv~kurOpERvCY!~uRGcFke4KL9N8C;2$gpU6EL|Yq z{DwVCxaz&5jttsx&V#Q@L4lo8SI1QwdXcwR2QkiLbvWzz=<=R#L7Cr6F@cyzc+DQZ zMD6_z1lFv8+4wWQTuX|c-?593^+R=7 zRsr#Mjd&>B?(+%V_#Ra2MeZx&0Wm`WSYxt4J+IK*%`D_Ef7jVH`;}sZz1<%~j#E$H zAW*ozIEVZOd#?HLp1yZJKu2cuAR$J*dqvVuQHdM?@yb<8_N&G-|GkFS45`{WF~R|j zsARfJuq^qxd<=_olA9}Dya3+am0Ewy_x%fsmfT=rXJhWRGk84#q{?E|qs;aN40Q9JB?(Mi`G220jCt>ywf z7%sJ~B)O>sZmIk|7RC0f^l^`#I^u%G9jw+En`sSP7nJ>uje-PVbele8M}HIL2*a;g z2Rb#{U|C;q^bX5>dyTx({t5P$FPlI}WhYBaDx^6~Qu+$%%(8uKBRkAV)$z5BzvT?q zL=^q!YIC;*v!1sF_q=CB8h!x{(6m}N+GY|)a^sFKCe=#m6`Ps(INf=(7h?wn|A9mk zDZVh{>F_VXK8u?8_F^ER_FUk)9;SRR2SW3g5G!I?BL|5n7=e1`?jCnvk2N8L9dN&m zP$6r3DHuKs|4VsxC4S&0W+%pSp6gnU*T<^+g}b&A&{Aw{ej%TZVdb*AKc^Rti66VFiAj1vuy1hVs|ohF_j~P+ z>}sg0(NIBYStB&_9@)U`5mj3>Uj+1CADV{}Q5*~xnPZiG`A$~dhmCNITJ}1lY-8lt z%+lV66xbA3>1640vQY_jv?^u0W^w15>v+6VADi6X9gr}@y+!2M^z=~yO-r4#sI@Z5 z!nQVse+(aGJiP#sUNPykMwMd|leL{?-?QW2WFo0Gx($z8q9g}%DRPBW_Xn;G<*LBj z=g3H>DPaO`N`(fA8>(KrQFQt4D}BH_Pc7QfkrvtO9fswcJUYbbBkc*c%)a@Yve!wb zMi{ca4?B<%a~eJ^ky8Y+826&F=>E50QX9u@G?BZYfqsGx8LL;ZeNyYhQ*;Oacy#(! z(XHVj&zh(xULm2k){gEi<4z6_&C2Uk0YG1ix(Z+)Rr~me2-Yc_86WgpDQNE?s(X3p zJ4=Z1Bn^HaED_9%>Hu+kDv*PM3Eh|dI`t4XVL5M6N(j5?s`sa=^<~6cgS3%%JLlZ% z-*)=)ZfXDxA;rsJ3w_(-JdkRRjF8{N1~XjCZR2o13Mb)?h$+rme~Z*^{o*66H?61z2Xja+0%%;cwSp-@uS zUyTq9?geqEKLgl*wy{Zohm!`@=^ib|Q`yVT;-DHsK z)+qos|9*J~?#M4$$G_!E4Zd9|=WB?Em=seQryLwh;@HwjY}^HV3wNV{R&ARbPo{^} z+5_GgQLOXcUbCdpRibUCh=;m1F$U*(2f7R>RYz6*n5RwZ2Z@hO%9moL_c&Io?sE+# z+TzkjaU-K;2#8(u?DG*NztSU-eNrQXj0*s2K(6PYMt`S;B`Q0FIaug+mw#gMLeXGK z=}Q^bbkZr)a8uU~^7yylZSK?o(!qg0#xD!xI5kcCX!-~JCR?i1cxjQ!rvbTh;ZEZ~ z0rb_1SChG0TGORt&yQUf{*#V2*^J=eOS=ha!}WQJQ=Fvl9T^B=!S^~&eZfBB%4A;r ze&>!ldM5~vlv7I6-EhCL^F-&DdFvNc_KTxN3Z%`k4$6C@lJEA~#gTwJQM`m8?%Tok zX1fy@9fYI>{N!99pLb&Z9N&`iAI zvHCuVRQ69HvvYV=gKI&pF1+BW>dD@p1InSG=id)?cq)>=z9%vbt3HqRS7$)_9g`#k z@5}Xu#1Ba7mMcx&&hP_mQuL>-si%JVbXLc?zwsm=^~^&>(2_e?(F)H z)>DAiW+DE>hEmuC8xJ1>(0fOLt;OEcD}AqQO^9mSGKU%O&W<9IU=O zwvbHeXH48s#5AsV!YBQdH%*5&5=Tpi;mhlu_m-?#3AjDC=3W?nf=p0dcpLV18;an?6VOlXC2tTR($&Q74j7Rg*0x|9B{c`K&ykXjhoXW4xP+}mvAVgUs zfM)D6uf5Z(7yfaxoPSea?AptAstE06ZBUQZ;Vn&6bllfpPG?}e-u092moqaG0`qL5 zPmjmYPTciCr3(;dx5B%U&b<0TL#v zuU|?+yLX7*sB93)Io8Bmyb3{dYD~F4&Kp;;=CR6$unPaTn7Uu45QI1 z7m+s#Mu}1w9PSGy5>Eeb>yDkifY?i*YdFyY0VAqBY&)^txx&XON}x8_da*VZ=pAN;dlPZ5CEgXO z``S!#Bgght+~flI z@ljmPPL_~#e}vt>e(t&tjVBg@rkTs? z`j0vG$FIc;0L&{5kX1d0fgMur#k`vrxWlh>ehtH+o%X{a7*Rf}6mmolmJ*}WRVMSK{q{Vy?E>fV9>~!!!l%#jZF=5Ys zzgyaX+1|I=5G2^L1!FnFAUkPl?jPoL3go|!KsS(i_8k!F9Iz)t({Jrpt;SCl)c=2T zfH}?~R|2p~UrdF{)wjWmckkDTw`s|RJ|KRCo7=*AZ3e6!?Xf{&&9ukF)UJA9N^W;Y z9cak-g=&v%+1qTL42dg8_1e+f+kV6`+!(6!_#TqTn!Xe{Yxj{nlQhEiR2cGo_DYqj z8ztYUWr{@@bys#VtUDjGVq%bG3`6jkyB$w+9D62pRSBmx+yrd$&jYZ=wU`$ym(r4O z5LqrnXGEQMgv^hYD++x-3Pubsq zCG>gEWv7JF)Qo>A1=SRXalt60E{HzFYCYMwy6!n;i&xU%Al`i3|DK}9`D|AHZku4; zPg)>07lDW*(bXz#u3Uf@DFe2CTq(ld73Nf)&J=!G=|P(TqQ`rrh2l8u@#XlQ?dj{P zPorjP51h}3Q(Sw}8V2s%4cthcJvh*c4v-etT_8}nM}uKAX;}9gLvr~9wJO$>IW=v8 zd-(-3zcpSSS)kEW$iRd^MR6ZS$bBs$sW9?sLw4D}`%yfn;Z}v5JwU!Z0l*O4-}}^> z;j-5RaE_KL{qjj!fvZM_LZQdy1N)R)?Blm;&=BkAx7K(67!FOyIA!<@>D4I-79D9? zqCJc-`FiqKVg<$1Wf-GB=01AH8>A9?Ps-UF53$B==$*Fv zsg95MZ5Mz%fw{?=dm5!)I{{QT%9UAws0XWs8yE73TnUG&Zfc<7mJky($`?=oXjgr% zCYyRzbV##5F3q%d{WGGYHy76c=?p+8C;vSt0ZkD&8Y^u0gk7>M2#6kf5O z<jV~rRh4q=Br)fY#?^NIV`vS z*1iAcZeH4|SV^t+Dk5{M%NetbIqQJ&BgS+vJ-$qHV27PMCOxCbNMOa~fxv_1)d1}j zuJCUgSq^K%p5UZX8j`~D6Aq95Y0NEsP)%aIqtJ5Vi6o3j9%pdM$4c=TF6 zXG0}WW<8?x{WmEZA&HQjo9iXl5k`=yl;nF zYV(Kq=n$1^r%>0aPJYJ*Mt9&CH;L_n^3XGs`!$xb|I6Nbl-Pd6x9|$&v@cW1^0j;#PF6Y(&Z{(ylD!m!-rS>dOsML*j$U z@CzrW#g>mYSn>5;JhW)iQ^KPQxn0O~5J%M)F|k?7}V7Xse} ztm~nY>|R!M$Z?Zm?f$SF2>*a#PI!i5Y|6Vd1VCbx}h-@IQ6Ks9fNrYMOUq82wB}U{w zbRNJiHiN(}0BN~xft!PF*hxGF2aV$B{I7L;pVF_!O^au+|Jv`XoTP^s$P$UA&jYiJ z)WED?ip!WH&-|k6u|;^z37IZ$?%bf98}j}%R===!iPNrYXD1hr1HV1Duzg94ZukZ& zzan2j^h)A(j!LLi296`r3p?zryaGL>UNSkId=F8 z?iVk+lo^eOI7ZJ!uy3+ClCl=2=l>p(X{^5c=WIA>+;P5g%*~I?DXU}K)L&53t;4hN zKWLa~ECafU?pu#f-Z4FLy84-9(*Wa1(F5m>E}cnrOZky3@fHTct*Hc^8`)mjNzp)y zQFOiL0Nux@0Cdi?0cR@_Hhey0&iV^hib!OFn>ni~yUP1DsQA?#GbF6&4qh!CjA5J= z{uBZq7ckB|lJ@t@hv}!kpXKj9adUl)gCQhgS&|0cb{AZ6xMNrr#R*feI>bOYX+RTi z#H~?-)pe8r2vq;V3Fx*ho~j}~?&Me@_8^lT|4;lk_A96S1~yD%pZ8So7Lg$pa;QjeMG5xDRnw9>*TUU_FPc4Xr<$0Bml0%%)g>8;;Ws_HZmNSn$=t`p z^5=n)*VpI4QI#K&5w{ACn;Dl`d5bN2iwo7a(ul=@*3|f^n*cvA^6qbEHTZqp|K`V9 z0$!^0foz~Ss^@Jd`~3P0PVDx?eDu4(ZbKBjd}>8z!ogX)Zoy{0sY-LUP2MGgBRcG6 zfvWDw|0&|ilpQVwsc)Z6lKzRl45i<>9n0;(@rzogb zrYp>+OI<+jHji(KD$m}S$oDhqY4iJSdN_RoUV?G!ImF0Sa1YL9Qlk*uGtq=%3-fB- zb?@Gd(a7F`^vrNsb_Z26hwLdX3i`gV=$yVEyu-Ah=d|fk4$$Rl1&57l(Pkl#u=--x zaNCH{eEdoKsDo|zb-mMaxhS4~-P7umt-IQIFpSH@M+phf+oE*$_g$1GwC1nLn}mci zP6VoSzs&x&)V|l5Mc%lZxto@U$FMoH>m=XDOT?gTL1uEifU{S=+_5|JUZv0u{|J?&H-y96c$2#6qiHQdC6(9~ zOrm8?r;URG?4yN6`HKkrgrsZfXogfMgC|hH4M2`xFYU=tVW+!iwekW8os5wW`RXJ6 zqy{**MYB#a!okaMpKu00?`e&Cwu80)&vT!+k~h(7wN9V#0%+dUt3i!Cd*bVC6vuKo zcJ+{OjEa9tFszCbJ#9ht%Wg&TMPAN{LXZ_-c$z>49fejq%hX^vlyyyMPa%LLA8Z{f zEiT0?jF3ooh9P@8JHi06_>TA1R{io90w0#Nd#ewt5lNuhB!{o%nD^}vTG}c%;>q%J zlb1@{*T12%h}b&ob^?a zByOYE?%?&ln}K$kOSS*hOo=~NDeN7Vf8k(5`fsYrBh4SJFMJs}5h}2a023V!4`fDt zFzT03E~Wm=r<53a>V?_-LGygAobZ~4p2E69suE!gHDZL)IdBa@lzZ^3htlea;>(`* zbDW^-I<)u1*H_hxX5dB%!U~hd%QzPcc@s>YF6OMG$+~&XV><0^W7MHdLm$-L1x^IY zv;8~Sc1fLH=fB!i$2cK@D@a4(MUS`ci!0}=zqSGAuLNk#HcfU)cK{=%G=Ekqm$(wc zRH5Lh8Hj`eSL5Zil>9Moy$zgga_ra-MmGnIn-epa;1l8J2zm1`Hy*RG53|3@= zHu35M{WJzK)7packV4l0M%G>^f+LAddzvvOL-GB>$%z>k3z+a9cAGjweP{+QlRXU9 z;pwXBu#JMZu55nSMqb=msqWr5x7tD!az_6#Z3U1cB>r{TeR1rRhuDJXYac<6dwW+ zy`@PNUU#4ej5sR=xla~e!6htx$D>P7ahwF@Y%HiS^Ap4>mE|eK+zz&Of+k1Y@{-WC z`?IdBj%vCD?gct*g08Ly&>k);7|-8lqeO&EXmM%8ttwuZPfZ>4lAf?nL}a?1VAh2^ z;tl$B5kz@69YkW^X+e21!YQ`j9_OdH2kkWL8O|xYKC*al&1-u#X+Ed%=9D+>F>!)e zb5r@_DRUO{#TnEC*qXy%%4Y0u0N#sC(m-sZ2w|e{z|pCnexU+{Z{9j^(yJzj5F8aH zRXcHU)58$F>^Im`?D>eLhe-=|1kZ}i<+u>u6fbzI-8XX|Y;XJE^CkDr-$Tq=P&WhD zhk7k;c<|0Yq{vrPdoS_opmT6NFq{%;TdQ7rW!{%?W|#Ahb#bZik}tyZN&D@T7vA4& z3b6;`=SwDL%gQyN@N>K43EmA!&Q7|W0obnQdsq403cI~VH!T)?JfvyZzZX88-owuA z*%KY4=l*hUj1)s>&#@COr(1Q)AT4Q!6Owlm3URfA@~f(|mIUtp=ONe&QjUXJwCp)Bs7M){mcG*C#Lj*u>RFVcJ~frYw?J~V24`)vyo(! zK5d3*?LAJd^IP4QqF{v_IP4>4qt&w(-~ARs8qogrdyE@@t)JUU+*y909^RHHx#V{l zt&=`dC zy%@^Id^Rcy1KRuGuf+yP+ODa7Tm}ZBv~WtcL4&)!EetkdD2;%ZUJ@Sj5@5!>Pcp#C z`UP{~mzv(|9C`|@PWZx*X_Z5Q>L_$W<-B?+G{ca{P7EWgeDoxihWU&Q^?C&GE^rM# zNI?Ubqzis84!+V%sZkH*e+_Cn4)h?G;+?E?K9Ats3b~^(ojmDX1jz1cQ4C73RqF*a3nX{Kaa(| zweayL*VC;=oNbN9vR5Jaaq=e;xq(>H*ZhRVRD~u{quXuT(UsBrAp%+rm8KQ_@hkKpGx_Q zC+U0S8I2LBd*ZqzgSxU~vR+OMk6=3vZE-!_B@)OC5>U*=tR>{08 z-qU6?isW?7l!$i6Y@QE(gi;`(_c=PO3RWb9sf->4yy1|80q!|}DvdKaX$~%H8`rG(WvKht=x3*!^rYs%NT+|TQt}52O|IQ| zFG~K>e~Fxe(GgoL7CXJON3%Ylt24HXLyWr;qkD~)U*iAJ#EXLtDSbC?#Z97EC-z0 zy2P3zUiBsx^O?YB96?n#l!-7l`eWIQIa!`b&N_;$n=mWaYT7ALQ<H5x?t{OvG4ZP5}sI3fEZlWy8+#&XbP%k49e zWg*MUnxt7bt#1*)E?gUwe#E>t@S6k>wv`HSMcrIrj<&^S;X;JaGYDWtO8k+g(~W(y zUsqFvfnFB1Oax2OlRsJ6;157@+_vJ6X1QDZTnkw4*`7*B{oV~V%Wr+n6)af-FMhr* zikd>LYJ5SAb6lT1@{{eqk8ogM*gMyUL+Wg>2tCl&)Z-$dQFdhzHQfieDX%xLo^r6+zw8&jsLlHOrDk%*|y*I?BWjR{Kz>} z#Z=7Pl%ZXrg%CZs6vjok_E{6clK0t+Gvi8N`wpe@Tl%63TG4vnLvzRHG14W*2fM3C zO9J;EC67cw>qA3>Y}AP|X{Tu7+*qLEW>>3u-a;aG8}l%pXiDtR(X(zt<$ToiCR=B5 zztpb}5Ma$N%QUbVm;DJw+9J$9`&<%8kL@%VUL2@&OqTb$b%!IIN3pq4U)rS$+<5ZF zdHi72KUwDnv=r=8qr>Ek-w-4;L^RH*cNno|T1HFhw1QNBxMh={nmFhN`~4<0?Se57 z