From 6fbc9f46de8c081570575f484d075773f1bf9873 Mon Sep 17 00:00:00 2001 From: Nicholas Hairs Date: Mon, 9 Dec 2024 21:20:58 +1100 Subject: [PATCH] Refactor NameServer and other improvements (#7) Fixes: #6 ### Changes - Drop Python 3.7 support. - Add Python 3.10 support. - Move tooling to uv. - Add GitHub Actions test suite. - Refactor NameServer and Middleware - Add new CLI for running servers - Add contributing.md and security.md - Various updates to docs - Transport fixes (cherry-picked from 4a0c23c952b9c479aa3c3ef9215ac195d2fd3e77) ### Test Plan - unit tests - check docs using `./dev.sh docs` --- .dockerignore | 1 - .github/workflows/test-suite.yml | 41 ++ .gitignore | 1 + CODE_OF_CONDUCT.md | 1 + SECURITY.md | 1 + dev.sh | 200 +----- docker-compose.yml | 42 -- docs/architecture.md | 9 + ...erver-architecture-request-flow.drawio.png | Bin 0 -> 121463 bytes ...server-architecture-server-flow.drawio.png | Bin 0 -> 153347 bytes docs/blueprints.md | 54 -- docs/changelog.md | 45 +- docs/contributing.md | 107 +++ docs/error-handling.md | 8 +- docs/index.md | 11 +- docs/middleware.md | 43 +- docs/quickstart.md | 20 +- docs/security.md | 14 + docs/subserver-blueprint.md | 87 +++ lib/python/build.Dockerfile | 24 - lib/python/build.sh | 19 +- lib/python/common.Dockerfile | 26 - lib/python/install_pypy.sh | 39 -- lib/python/tox.Dockerfile | 50 -- mkdocs.yml | 15 +- pylintrc | 5 +- pyproject.toml | 35 +- src/nserver/__init__.py | 5 +- src/nserver/__main__.py | 18 + src/nserver/_version.py | 1 + src/nserver/application.py | 109 +++ src/nserver/cli.py | 134 ++++ src/nserver/exceptions.py | 8 +- src/nserver/middleware.py | 434 +++++------- src/nserver/models.py | 11 +- src/nserver/records.py | 13 +- src/nserver/rules.py | 53 +- src/nserver/server.py | 658 ++++++++---------- src/nserver/settings.py | 35 - src/nserver/transport.py | 100 ++- src/nserver/util.py | 3 + tests/test_blueprint.py | 139 +--- tests/test_server.py | 22 +- tests/test_subserver.py | 174 +++++ tox.ini | 26 +- 45 files changed, 1452 insertions(+), 1389 deletions(-) delete mode 100644 .dockerignore create mode 100644 .github/workflows/test-suite.yml create mode 120000 CODE_OF_CONDUCT.md create mode 120000 SECURITY.md delete mode 100644 docker-compose.yml create mode 100644 docs/architecture.md create mode 100644 docs/assets/images/nserver-architecture-request-flow.drawio.png create mode 100644 docs/assets/images/nserver-architecture-server-flow.drawio.png delete mode 100644 docs/blueprints.md create mode 100644 docs/contributing.md create mode 100644 docs/security.md create mode 100644 docs/subserver-blueprint.md delete mode 100644 lib/python/build.Dockerfile delete mode 100644 lib/python/common.Dockerfile delete mode 100755 lib/python/install_pypy.sh delete mode 100644 lib/python/tox.Dockerfile create mode 100644 src/nserver/__main__.py create mode 100644 src/nserver/application.py create mode 100644 src/nserver/cli.py delete mode 100644 src/nserver/settings.py create mode 100644 tests/test_subserver.py diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 2d2ecd6..0000000 --- a/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.git/ diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml new file mode 100644 index 0000000..64af757 --- /dev/null +++ b/.github/workflows/test-suite.yml @@ -0,0 +1,41 @@ +name: Test NServer + +on: + push: + branches: + - main + + pull_request: + branches: + - main + +jobs: + lint: + name: "Python Lint" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v3 + + - name: Lint with tox + run: uvx --with tox-uv tox -e lint + + test: + name: "Python Test ${{ matrix.os }}" + needs: [lint] + runs-on: "${{ matrix.os }}" + strategy: + fail-fast: false # allow tests to run on all platforms + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v3 + + - name: Test with tox + run: uvx --with tox-uv tox diff --git a/.gitignore b/.gitignore index 0f424ef..a8247d8 100644 --- a/.gitignore +++ b/.gitignore @@ -194,3 +194,4 @@ dmypy.json ### PROJECT ### ============================================================================ # Project specific stuff goes here +uv.lock diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 120000 index 0000000..d0fcfe9 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +docs/contributing.md \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 120000 index 0000000..6ac9ff1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1 @@ +docs/security.md \ No newline at end of file diff --git a/dev.sh b/dev.sh index 0eb753f..d95f317 100755 --- a/dev.sh +++ b/dev.sh @@ -15,13 +15,15 @@ set -e # Bail at the first sign of trouble # Notation Reference: https://unix.stackexchange.com/questions/122845/using-a-b-for-variable-assignment-in-scripts#comment685330_122848 : ${DEBUG:=0} : ${CI:=0} # Flag for if we are in CI - default to not. -: ${SKIP_BUILD:=0} # Allow some commands to forcibly skip compose-build -: ${PORT:=8000} # allows for some commands to change the port if ! command -v toml &> /dev/null; then pip install --user toml-cli fi +if ! command -v uv &> /dev/null; then + pip install --user uv +fi + ### CONTANTS ### ============================================================================ SOURCE_UID=$(id -u) @@ -45,8 +47,7 @@ PACKAGE_VERSION=$(toml get --toml-path pyproject.toml project.version) # You may want to customise these for your project # TODO: this potentially should be moved to manifest.env so that projects can easily # customise the main dev.sh -SOURCE_FILES="src tests" -PYTHON_MIN_VERSION="py37" +PYTHON_MIN_VERSION="py38" ## Build related ## ----------------------------------------------------------------------------- @@ -117,77 +118,6 @@ cp .tmp/env .env ### FUNCTIONS ### ============================================================================ -## Docker Functions -## ----------------------------------------------------------------------------- -function compose_build { - heading2 "🐋 Building $1" - if [[ "$CI" = 1 ]]; then - docker compose build --progress plain $1 - - elif [[ "$DEBUG" -gt 0 ]]; then - docker compose build --progress plain $1 - - else - docker compose build $1 - fi - echo -} - -function compose_run { - heading2 "🐋 running $@" - docker compose -f docker-compose.yml run --rm "$@" - echo -} - -function docker_clean { - heading2 "🐋 Removing $PACKAGE_NAME images" - IMAGES=$(docker images --filter "reference=${PACKAGE_NAME}-asdf*" | tail -n +2) - COUNT_IMAGES=$(echo -n "$IMAGES" | wc -l) - if [[ "$DEBUG" -gt 0 ]]; then - echo "IMAGES=$IMAGES" - echo "COUNT_IMAGES=$COUNT_IMAGES" - fi - - if [[ "$COUNT_IMAGES" -gt 0 ]]; then - docker images | grep "$PACKAGE_NAME" | awk '{OFS=":"} {print $1, $2}' | xargs -t docker rmi - fi -} - - -function docker_clean_unused { - docker images --filter "reference=${PACKAGE_NAME}-*" -a | \ - tail -n +2 | \ - grep -v "$GIT_COMMIT" | \ - awk '{OFS=":"} {print $1, $2}' | \ - xargs -t docker rmi -} - -function docker_autoclean { - if [[ "$CI" = 0 ]]; then - if [[ "$DEBUG" -gt 0 ]]; then - heading2 "🐋 determining if need to clean" - fi - - IMAGES=$( - docker images --filter "reference=${PACKAGE_NAME}-*" -a |\ - tail -n +2 |\ - grep -v "$GIT_COMMIT" ;\ - /bin/true - ) - COUNT_IMAGES=$(echo "$IMAGES" | wc -l) - - if [[ "$DEBUG" -gt 0 ]]; then - echo "IMAGES=${IMAGES}" - echo "COUNT_IMAGES=${COUNT_IMAGES}" - fi - - if [[ $COUNT_IMAGES -gt $AUTOCLEAN_LIMIT ]]; then - heading2 "Removing unused ${PACKAGE_NAME} images 🐋" - docker_clean_unused - fi - fi -} - ## Utility ## ----------------------------------------------------------------------------- function heading { @@ -228,32 +158,6 @@ function check_pyproject_toml { ## Command Functions ## ----------------------------------------------------------------------------- -function command_build { - if [[ -z "$1" || "$1" == "dist" ]]; then - BUILD_DIR="dist" - elif [[ "$1" == "tmp" ]]; then - BUILD_DIR=".tmp/dist" - else - return 1 - fi - - # TODO: unstashed changed guard - - if [[ ! -d "$BUILD_DIR" ]]; then - heading "setup 📜" - mkdir $BUILD_DIR - fi - - echo "BUILD_DIR=${BUILD_DIR}" >> .env - echo "BUILD_DIR=${BUILD_DIR}" >> .tmp/env - - heading "build 🐍" - # Note: we always run compose_build because we copy the package source code to - # the container so we can modify it without affecting local source code. - compose_build python-build - compose_run python-build -} - function display_usage { echo "dev.sh - development utility" @@ -306,70 +210,30 @@ case $1 in echo "ERROR! Do not run format in CI!" exit 250 fi - heading "black 🐍" - if [[ "$SKIP_BUILD" = 0 ]]; then - compose_build python-common - fi - - compose_run python-common \ - black --line-length 100 --target-version ${PYTHON_MIN_VERSION} $SOURCE_FILES + heading "tox 🐍 - format" + uvx tox -e format || true ;; "lint") - if [[ "$SKIP_BUILD" = 0 ]]; then - compose_build python-common - fi - - if [[ "$DEBUG" -gt 0 ]]; then - heading2 "🤔 Debugging" - compose_run python-common ls -lah - compose_run python-common pip list - fi - - heading "validate-pyproject 🐍" - compose_run python-common validate-pyproject pyproject.toml - - heading "black - check only 🐍" - compose_run python-common \ - black --line-length 100 --target-version ${PYTHON_MIN_VERSION} --check --diff $SOURCE_FILES - - heading "pylint 🐍" - compose_run python-common pylint -j 4 --output-format=colorized $SOURCE_FILES - - heading "mypy 🐍" - compose_run python-common mypy $SOURCE_FILES - + heading "tox 🐍 - lint" + uvx tox -e lint || true ;; "test") - command_build tmp - - heading "tox 🐍" - if [[ "$SKIP_BUILD" = 0 ]]; then - compose_build python-tox - fi - compose_run python-tox tox -e ${PYTHON_MIN_VERSION} || true - - rm -rf .tmp/dist/* + heading "tox 🐍 - single" + uvx tox -e py312 || true ;; "test-full") - command_build tmp - - heading "tox 🐍" - if [[ "$SKIP_BUILD" = 0 ]]; then - compose_build python-tox - fi - compose_run python-tox tox || true - - rm -rf .tmp/dist/* + heading "tox 🐍 - all" + uvx tox || true ;; "build") - command_build dist + source ./lib/python/build.sh ;; @@ -408,44 +272,20 @@ print('Your package is already imported 🎉\nPress ctrl+d to exit') EOF fi - if [[ "$SKIP_BUILD" = 0 ]]; then - compose_build python-common - fi - compose_run python-common bpython --config bpython.ini -i .tmp/repl.py - - ;; - - "run") - heading "Running 🐍" - if [[ "$SKIP_BUILD" = 0 ]]; then - compose_build python-common - fi - compose_run python-common "${@:2}" + uv run python -i .tmp/repl.py ;; "docs") heading "Preview Docs 🐍" - if [[ "$SKIP_BUILD" = 0 ]]; then - compose_build python-common - fi - compose_run -p 127.0.0.1:${PORT}:8080 python-common mkdocs serve -a 0.0.0.0:8080 -w docs + uv run --extra dev mkdocs serve -w docs ;; "build-docs") heading "Building Docs 🐍" - if [[ -z "$VIRTUAL_ENV" ]]; then - echo "This command should be run in a virtual environment to avoid poluting" - exit 1 - fi - - if [[ -z $(pip3 list | grep mike) ]]; then - pip install -e.[docs] - fi - - mike deploy "$PACKAGE_VERSION" "latest" \ + uv run --extra dev mike deploy "$PACKAGE_VERSION" "latest" \ --update-aliases \ --prop-set-string "git_branch=${GIT_BRANCH}" \ --prop-set-string "git_commit=${GIT_COMMIT}" \ @@ -462,7 +302,6 @@ EOF "clean") heading "Cleaning 📜" - docker_clean echo "🐍 pyclean" if ! command -v pyclean &> /dev/null; then @@ -471,6 +310,9 @@ EOF pyclean src pyclean tests + echo "🐍 clear .tox" + rm -rf .tox + echo "🐍 remove build artifacts" rm -rf build dist "src/${PACKAGE_PYTHON_NAME}.egg-info" @@ -523,5 +365,3 @@ EOF ;; esac - -docker_autoclean diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 86a3c9e..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -version: "3.1" -services: - python-common: &pythonBase - image: "${PACKAGE_NAME}-python-general:${GIT_COMMIT}" - build: - context: . - dockerfile: lib/python/common.Dockerfile - args: &pythonBaseBuildArgs - - "SOURCE_UID=${SOURCE_UID}" - - "SOURCE_GID=${SOURCE_GID}" - - "SOURCE_UID_GID=${SOURCE_UID_GID}" - user: devuser - working_dir: /code - env_file: - - .tmp/env - environment: - - "PATH=/home/devuser/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games" - volumes: - - .:/code - - python-build: - <<: *pythonBase - image: "${PACKAGE_NAME}-python-build:${GIT_COMMIT}" - build: - context: . - dockerfile: lib/python/build.Dockerfile - args: *pythonBaseBuildArgs - command: "/code/lib/python/build.sh" - volumes: - - ./${BUILD_DIR}:/code/dist - - python-tox: - <<: *pythonBase - image: "${PACKAGE_NAME}-python-tox:${GIT_COMMIT}" - build: - context: . - dockerfile: lib/python/tox.Dockerfile - args: *pythonBaseBuildArgs - volumes: - - ./${BUILD_DIR}:/code/dist - - ./tests:/code/tests - - ./tox.ini:/code/tox.ini diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..1299381 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,9 @@ +# Architecture + +## Request Flow + +![NServer Request Flow](assets/images/nserver-architecture-request-flow.drawio.png) + +## Server Middleware + +![NServer Server Middleware Flow](assets/images/nserver-architecture-server-flow.drawio.png) diff --git a/docs/assets/images/nserver-architecture-request-flow.drawio.png b/docs/assets/images/nserver-architecture-request-flow.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..358a06cc2a633c94a556b49ac9193c2799504dae GIT binary patch literal 121463 zcmeEP2|SeB`&Vg^R>Dn-60&69qU>ZB5+P(6>x{9CeXXP$i_v-%o+-}Xh?|I+%yytn&bH2~_d7d+Vsw%P@*VC>iAtBi) zFDI=|Lb6JagoIRR-5PL2=pif;{IkMdT~?ALwQOex2}!amO6CL#;cRLFhmkPyO3i*^ z^!_0 zTs(}t5?s9CFFtM#egOfU+4hEJFl+n`m65I%aJV5O&k=qOE^sU66WrX4yprIPl7+Q{ zGx*o}pn!n@9}D;-VPgY_X~B#XEx>b+@be0C@CrbeD9ULlD>3p&fzNOYD;W4k7G`XP zgf5XXw?iVpkt5tZ2RXQ)|G>dxhNgyg7DTsz-T{U(G@I?eiIA42!VyIUV;*}rue^() zjhrTE0JU+1+1Xnlt?{qP$HB+J%{zMr}F6{_slSM;m;xHUw{iyVMS5>j1Myv75q?PP2Eh zz>ob=V@WOvm7}s+DsTgbBU}gVG!80%dr!ih5zex9hBoHPND~+w+}^|)-!EPuHC*>i zT<}M@1o53Uvm-o+=)f@xSHdgd`p?5<=3rq$cyW9~6cPzXS=iu51AovMX>AQN#%lup zn4z5=(rIBcQzRV!diXZbxZ=Au+f(Sku|-1*9o4chL7C&*atjE}H}reUL|R z_5{L^&=~(F_U48rV2p92zDPK;14$T8J0ud^j!1UIrwNS^&QdTqG}?r5#6Jfp0XTS3NptXcE_;*< zUY`YQ9iaD@L>StcSy;29kTzh}gBDyiK(D}jCY%SK%%FBE$6$6~K7khMv)T+k9l`1I z?CrsbKu?%$On9aZ;lObl6GIeaaG*!b8yrGAaPeHzS;@|Klm`WK#`PbCfCIro*2c-) z0tGu}V`vP$q7#@D;208g4mZDerOho&OkiNvOWGkFtW9uc4$MsOPS6|&&A<)u8b(mB zd4b?$^nIIcg3P&vu^|ewsJNjdk`b79ID_hdfGb&;!r&Ix zKs1szFgpt{Ksa*=#~)UmKenVC&F94o%niyA+zGO_kl0P^kTx1XUBNQOD-B817zvgl z8+!{QoK8bW>|n+Yz~(!`)M56sW)3=vbU;BplA1GL&_Nrd9SR(=N7;dAKzD>0+M}F+ znFi;qVNNn8z(f&diy+a201t2kIyFB*z>0}m%m_1vpmhsd2ug$d{v9fK5U)#w#ekr1 zL^{aLg>SP!2MHH0A0>p~0&dREE8!x|BhnWeBoHKS2JuTW@=HQeA^xAAksm-JDf}5Q z(dW*x&z~Yb3|+kN4Ds37i@+@x>N(IVbEFy4+7M2x=}T%L3a87*ps54(`kT44T-EbC z=HmON=eu^3e^#(~r6s810-c?0L9DWZ1l?XfD$C8cNM$wb46W@UYqLmqe_73WGx`r( zO#plrXdpL1wHBx#z74Ss9wg}M^3g#a9Ebr>=a<=BKCAlDW)o*E{%*6m{4|#Tm&PtX z9Qt=?tRMsmAc(iX%+4NKU}f>mmXDF;#v>LYBdc!cqyjh}f};$*)-M+ca;JdX599~< zzGdPf@e;J|U*IDVs4yZuT!b@!`AE`?f2#|>wCTiIjK6HLpF^y_V>TX3hIaU164BC6 zWPc6e7G_XD3Jx=cRz`b3Vp~|7DdE0JgIJOz%-Tc(3aNlLMsTFD6*xq2jb}+u7!ff! zPc{+Jm6CXda<=)xi)WVx=oaAq;0yk(Y(TFG5x&1x5A0xYz!*C&eB4rE{Smu3&mgHn zR38|3JnhcEkUrw!!Lu-g2iYSX?2KXfi++q!pM4hLi9jTV5Y11xA)aak(DldrO}H1r z`J3J!{13hVecqqAlRtcWe*ED4|8CC<;MyX=C{OQXQQSZ9}vvCJyN zGWQ1XiAh9eme?CmbAZ{o{LKEqEC`!*5th^h!kqnm$Kg+^49EcF=EfU{zsZ+ahz5AS z^+lJtFY!(91@YhiX0T6KU67`x_Jpj{AML_F4dnTLtw%pgFzQblmv1l>A7QHh4u&E$ zBPjVl525%LuSd#;C}VTr38*{3L3R<&DfpQfso!0J2($FRw*vhm%MBmF86_y!Pqp0e z@&6sm%|E?BaO1PjiO?T3%fMti;$Ut3D?fI5=f}>a9Q+ID9>PjQq>0}%^?24AF#o?2 zWan}b{_^C6B^{<&oAGDp9*%{C&m3#{J78t`MSuAghti}BaYZ&b%mU@s<50`@G#RSm z0JR8k34A!s2#G==5le^9z7q}uK`%R(6Tr`f3IcFnw28k+L*b>lQ__&Z;0Fz7JGLl@ z_WO_pzc~IrWWg;ke?VFO9>~JZ&BwP$8}Zutr(w%Ky}0u&X3#+`i47prwD9;P|JJ0?%Te zSk(?`4D2if5ez}4KbN%1v46AH;&~8aAb>ZmKfBiQ5TF3TwhDX$4Srs&T}%}(&{Y{6 zO!%`F_GiJegzWhxRQP8{2j8kNAMZCR{DNG)>e;IJxmPdqjjc8&VwSl5=hG4%S zXulyDLpwN>yl)3XIoN@{Yi2hrGO`1oa9bNKz(2Dz$)7d(e+vK1K70|s{70-cq0;Ji zr^hn)BIkBZBA9i;ixK9+A2#cY!3=)F^AI8@|LKu`c>4X*jw^xo{Nod5ju8I`*PS1o zCCe77@Cyii8$}^7djCDG=KcML{=9mT?@2BFGr%nvs6g*1JBhg^Hy=Av^Oj$ z+7Q8&`^&brC2ZaEYiC|*Azq#YZwUXDDEn;AMc#h)8fVAXuB(qe~1d z*jyUg%ieHy@96pUYLO?k7>mpz65_w1B}{ga$y~%B5d=VFH0Rn7YB%V)rGl~xP( z6~Lx znKozYX1hWxCc=il#1rpZR{5V_$p|r^1%^(*5McwfANR8k4#0H+@Lkb1OZh6{t)k}BZov<+5bzX)>1fs&KFqb#&Pi)wn!+; z4h7DQE%8hs*f-+wo)gUSL4ivYzAoJO@LzxdXARYV0Rt{Wd2%m?^~(qZ{$WY{>p{#ee&(|Y!fC7XcEJ>a#P$v4pDO}YuF<}CDW*9eMngzjK7xBzO0)o7T z^E~r+GtZXfO$gwgIC{HuLtNQm^d(}fL^$^MWM<9cT0HU&ycjUq(%@*-xd%Y-%PA!hvDabc#L={A}9f|UM@q-w|FU9dW$H6HTx-e zTO7&q=gi6+>AuY6H=DM#fR0#*CG2o}5iJW6@Q;e+UkBh92b~wn49bL=&4B@s;$JI3 zf`ufunm-AEUmQ_hC_p3dd&@Rml@h=+hhqq+k`U=9 z67Z5~JOFugiwt~G^KuiBsr@^{~Jh>P|ipsz=ez2 z@>$6Kuvq?WNOFlP;QtYs{k-6Di2~LC5t;qWfN?3rzub7KZ{frb$Cl?X27y2P=QEvn z7RMt0l_`cez4|WzjO8fO#X*y06kg`}qmuZyVTvU-GF!MlE!P~wzgB=hFTmhgDi7)> z$Pf67XW&m9UM3_S6S-plY>weyJGJJ*U_`vq99AKYw-Cl*`M{DTep+P_twacw{WXFA zpD6A>JAuJ(ARF45fel)58zn6n<(;J({v7Hd#Q2Hzg>dX|j{6hT^S==1C(HmmK`202 zl?adgX#@Sdi}AhWe9+_@&uU>>%zvZk(hzpgbjg}L0po)(BU3O5XQ%lRrDMcpM1L~E z`7HrI_npX4E=j=i5Gq^oL!LCmQfMPm0*m%q?NdXu$uC zsT{Lf^ygNJpGJVchufDC0gs#9e?Tn%HfNb&cZrbtLTCA3D>DKTA-0r+a3-;(Bp{gO zVGa{;1?a+&JGx35f(1 zv$!&HOBf2}g5MX&&;f-6NRKEVo3INO;kR?N!Bt#fn?8_>z2qd2Y7JbAZQOYokd6A&9=rDkIwoT;4*N@lKvGp&R_o#M+}D<{(v3-VGj$+5jw*7 z{U_jemy&{kpZ)s1mI!BDv-$m`mK*>+{GMvRv;~wTARX+Cp*iv&wFRG!_`7cVY)t0+ z5`yFnw0YqU7|UH-@B{GAXv@4kfvgL`(##tP!mo7AnG>9CnKvgmn?x`*^Oya%Yzy9R z|6OJIw^|mwY5k=uKVVt#6u|H5${(edm%CkA%yE7HCx8~>mEYmH{%Aedf0x4IBlrLi ztN|;|5^lmA3MF?3uV>$d z@Nklq+bP6RuytYadlzV!84NsL3&NwnAy^h4|R z?neVhPvyjG83bn-n&SI?>${PYMzoDs-<5S<<={twGmrQ z%kH^(u3PmgF%@XVXEl!ct*)!jb1D30>t>^p{=FG?*rt;&a5kxsYO*On_1nxe}wn2*YsGzKnab3J9cnj(=Oq+4afq-K&c=dv+f=) z!>6PX_GLmn;8=-826cQ^eawr>4kAn)Pq(PNLZJ z1}m*2%q+tBEUrX?UO84stmU8TylxzL|LRfEhu-_j%0Hc_^L$&QixjCf2Rad zW4O+&cY~J2!~W+x*eT2#a~$l$?>Uc+wMF*;705W$_o#}Y5_LqW2cFXrUz27a7GV(| zextexNKhGZXQx~D{rDbZVS6UGnv?d*BLlL#T?(hPjNO1(W?)tPwI_{pdoU3bF1e09 z-N|YB>EUKkbyK=^Tc@V$tW&b8^v&h!Tq?G95vi9&u$NE>(6HPPMN^26Me1<(n8qFwZ z?8`;6EtDu6C|JMx7jKo_X_yf0nKLmnH8GNXdez#T89K4@ZQEnbFmH{>M~+5P?iBN! zEZA}wsDAHLKeK3$8X%puPS%LA)-b1V`my}X!~nLmN0npK0fO(jN+Y(LUVl;=nu4h> zj#Rt!xmb}i&x9@YfsssiieF-7&$*Q9Ag53Ja-!V7%2z2~G>dY14*U3Nzil5j_WUmA z52rC-o{|fe7Hv1XiS-<7r5QoOf*dfMtasSBcT?TJ6difE?WTQqh16z;g7;2M9*qKu z>*p48W}eeeFLJPJf;p^}+YukJb0%-(eFFzj7bXJ@3cbEZ1A*+|os@@EggY3re>le-5m^5A9D|ONV<|#V%iE@Xi`-6#WjvJRxT_}sU;7(qh zhv_NuCzW7eYHpU%-7DAt&tSr8T2_S|Q?bZ??`p-XR&hhb;$rHkqe^64?#O$$2&G3| zsk!j;yBxU?vIn#nE+040rHrNTO}?9c(PL_;cE(kDXXe`=OwjZ4PsiDErbh~9>XH(o zbZqa)!C!cB?oD+;KU7V6E!=gtcZ4GR)kejVu2*LP=&SOTFSj}7#%o=mxS>oKwQaG^ zft+^q@JN$a?rR_Gy_ZOjGh*XjpDjyg_NStP(S$gtYMMW6y~fMFhm8sr5;Am=<70(3 zCp)b}ywdi|?Hlu@PxGu)^W!*DQ4m_^+L@Ga@|yHhrR`@FIUW{flGL8iK)IZuj#dl4 zr+$QiV!z-!o3oT`ti@~EU8u4W;Du+%Im|a7{^G5BpiA=zg4b=N(MgYvHKneqzI9`M zI0u<=qfX*o|BIw;AD*0ToC(`}O~;aDVrrzRu^{O1SGoO%hhBd?PxqeTX67E6mPEM| zG7TcFP9oX$mdOt{L}NHqZXU!2Xtx$p9z})Agj!06QO^X6eX=6k0&`~AT{r`bwv$I; z`OK^RwZ~o=OM?t4e>6Y!_96KCSD{R>wiJlH(l^q z<6(sGJ`QbIPT9eh4KEI|ehqey*&#NBmNHV4snaTTDXOY7i$(yUU!QoGu32ny2a-X& z{dHEo9MpYo{3CYabIrzHm5+oPtE9rvgciPES(R;Q8 z2wJ=5zOR&_t$&_9_Ble3eYiz5!tzcMJT6qzj$8s3-CW074eUq*yelQ!F-Yzua~DOq zTuM&o9qX>}i$2w0z3ZL!a-X<1oiZI7cg#x-Tk455(mkW_l$AB-os_X=@QJGCtYZl( zy#+ci*^<%dBDYLEwV7+DgnBZo-t{$Wi_p30+$c%$-)~W<@K?dnUEp7Gyr@?lt^&<*R);P1mH&OKg3hu9~ zFQhVtolKWE4~yCx@}SY?*+#uE3VEzEn9okRop+Mol{50Sm^Cm=PYiane7?+4S|K2( z*NH~+`l{7nYRqyNwq%`CojD$Sp?0_+adN2EK*dCcb1!-z=xc`lsYK>A+sIkRZ>%WY zMHQfZM#VFfxwa(m0W+n2p4_cDNPHSi$^59jGQy5*Uu39SaE$sIbXH$SrGOMnhT$E$ zTRn+4^!Qs~CL7NP)xVEN1QatTGV^MVGo}~k%h1Rau9i6YDNzr>`Nl{?>Qs<`IPrgeaPm{V4*4JDJbB*1^KE!VEm?^3;$WCBgd&n6IL{FH?_-C(#iS z4u%1wFbixPXMdVTMYt?k=je86#MB$jO7pH5{hiDa2|`KE{+QFOfwBHaQJ!ks4T)A4 z;nO)=^mH;iZfc@Zk7SQpB&&z;U*x3MwZBf?UoV-6u*J%3tRIeppSPAhX?Tw6X5s*g z&I#Et=0X{j4$T~AU0d#0-S=f8BPYXJ{nC%yzveza(VJ|}cB4=Ur6129!Jv|vqFg>+ ze=MoAvajP})4C*kB$jc#Yd*><(v!(B32`M~rtKC3R}1~IY!cQgZJkV!!6F(1C1sSw zfmUm#yC%8T2SW~O1>4lbG4}KN^tXldXbf|Bx*5o!6?I7*o-4_i-A5UlTDXjNBt*cb zx6#$>B!VYgQ^?D#z|^+;O2HmFHaci!r|?l^)$8B6jwRnxs^$SZ+ZQ^LKXAcyP7z|L zNjoJ`WH(b4YUx#E$h8qJ7p+Cu_UEAXMUcwHaRybNBdHIhq0&=MwLgTVR}-t%9c>qQ zQggG-o-w<=!l6@`CKhdl3B8Za>d4tCB}1>+b_$I?poPLd9jrRq6tYpVBub$t(=lE# z|MF3dki9OzqTSFd<(#fRLk-Kx+<3I@oDSPKj{)MChMI38AKP}@s$u0&g>VkPGwS_! z*&~F{$59mU=!aLxNq0C2r)i8+tz@rD;qJT{EqyLMsa$Nvy~^ovhY;d?NFJ8PJ%KT& z@&V8L{Z(;qD>R=cO6!U`$mFN%vQk9l=y*kY4BO7s_^(3LY4|Fh(P^M?2s}!Y*KR~r z9l+6Uj!^mHHpdQZ0m`vePp8mo?=BDdw6b&Uhc7BHt1uFk5K7aWs+UAS85oi+fX z78x0A*c4QMa!po@hOTIl7RrL{7SDPt4R6=Pi_8yN@yd+#In+Z*B?5yUnrdpFWw*!F zTuc)Qt+!A{b<_wUVlQE&ujx~zIff2ZjB23o>RUN>jrX_jywa&~x81QYF;NUrbcuu7 z&p*}tbXrdFZ%343&02IDfckGgS{-!p^YK9s{ggUg045A`+@-?eq?=%IR{ceCj0{*R zF=NICg#vp;u__sMY-nGxzU3wbRkrt^$o*Il6UW{40UopNx`o+uryYo^-=9MQ`$)sj zt3XHhw}B1wpu$hQ@dAfLb&73{gQ`r!RrCRc!&PzieAp3bmSgB{U+WA0(d3oece1-X zd=Rr#M1Ict64<1l*d(KyM4w=3n6^3&L*I#!&>Kj(BK@h5rg3}*>+q%3+P(^bEbf*u z_>{aM+8TA~B6VrbwBB}7grQYWGilSSx*P54hjJvvbfmkQgw1GZl=>UUVbP=-F9LT( zlGURV;oc55478`dCZw}8z@VnS?h`cm{x>S_ds%E_bFebM zQE2JB?OG$k9+Netj2z86uGGv{e3@aZUVjf<=kAd+3Wyg4oGi4)lSgC_VQ-YZ>uHmw z8A;Iat3GE)rPQ~cu%?)}z~QH_$G~m*hPl=UgY}EB9zTW^;Jbe*-%Sav+SGF!?nm7k zd{50+TDO$maA#99i)g(ybM&3S{Bp$&=n^@gP*26RsmEkx=*8)^^C@&;nP`L`MGu9i znqLz|QndV*&et*HYCD2YT>cRH<+xvdkn)Uyu3GDUGG4L`k;YdLUsHVI@?H0gZ_08$ zqS8`D!(qi1^?X+~OWV~0(qZG+a1K}$nqDoj$A{voa5fd9Dx~R?m2E(=42|BWd`b>| zg@P|9I2D==o~LlLO=q%)uAFnMJ5P%@`f)_=!@5L_rZ~r$v7CpFHjtIM}rk077VbWb4jVoY8R>G8;dlX5{4R;sh9JbUY_hLU2+b78? z$jw-oc^&C5(po>TRnJwWEsWs;{jJy?j1E|N4eN)k)cY;j^cvE-uS%ob_i~=zr?%b0 zN#mWU-r3ZY0E$#rAW*Ltt1Tym{Iy6Z`HYmq?(372~diwqNN(?L9JB)YLj&dya-Sm&d-JK6sah z;X_Z3sBv|e-hMhNGFap7-$E-nsnZUg@?0@D9_uAzdZoi2$TqFsHWu?vhPHUxJUB&> zB=cYmh6lE5&{3~er)$mO$cI=b&rmjD`#9Y(-_3OIGqV)V$Tcy>4aj5J5!eiQ>98oZ z=lhGVw>PmjIwv++9crM`I{A?l?URtsQk>o)xe`X(oK1emQ{7Er`{+du5gM_z30{iQ zGTGdnO*+@DbYp-IB_3>EEyV6YmW4q(bX2w)a8wlS9M|XcV++s=KcXjQfYQ9=FZ<<|0VLf_wGZU{YBb?!Hc9#?%qv(A;9$i~{; zs_D;$ic}rOZVbtJqNXsf-c7r)SDw_+4qOH$mz4 zw7J%7AvTq*G!#V?lQ#y6$FH#o$0j1~v9^{;ux8xlqzY2#pwXxKl(CI>exAy#JV&87 zx^<-}X$YgbuX3CQYV7dQU304d;B{}y13s2Xxqkf_z_zXvJViQZ7`-3Q90lyPM&cnr zLg(1CtU5DC*`8zaZfDJ$;L6{*LYih^<(wlRzJ~#{z5M7jd(swhRNR^1BXisZ38^~0 zce?@^L+Ko*os~EM=Cz)!)E`D5{=fDxjK-XZ4ss1Z94=DxNlt27< z)#kSlQ86>s=hgGpI0#Ww-V!8c-E}u3FmIsjkaABb?N{JgH*n;38yR?x#bxlbLOvcM zqf`L%(RWAvYMPU(*l9pLHq`Y4a;grVQEF;-)7BS(sWTO@ZI(Xt_2s@Eh_xc4WP0ET zco+2uKnGBA-{zU01SF)L=Sa}WtFeqL>;UQ50G=29vPl$e1>8i;XtRF<#83e?DSemI zJKN#cyyb5J<7Ek4{R=(!U+iY;e-o!j(_nH{84lP=rQ;eFyhI+?Yg+0Uus5~ za0$RhunE8!pInn5b$*lMvyFle;MaJhCi)5rN~l@v9RP>eumkz%qo)%P@Y~0ySLtac zS=FX;D8?#Xm&P6yoBAXj%ws{pOkoK8$m~+FnMp^$t~GRLl=qHzRV4(8Obu2EjJ&gN zuq$CV^P22GPf16iNWl#lJEVXzLr#>}v zcP5kXWV*g-ZvELqavDRK$cAOSZ`16zzuv7cuMI=L+wSFFrqi~s7{d;3wJ)O5db?%^ z{1o7p?^B)vs5{)D^UbTzH|Qevt#b%mk;mA_Z8$=adDCSK#$;#DCs!jkc$(qG! zWGeT?oZV#CEY;BAIXz}JVh@WC2~g9N59dH+M>y77H1=6P>OT@zguV9gLA$zuQbCoY zIF;W+h*8aodmWx`Na4a-?90+pRUe*e{+^jUq5B$X<4*h6hRG?I8@qr@S(B{AsU>4{ zq%H-Tz!rC=08On@k=V3i)tU#R2%DpXTF;l=b(&zw!lnJaPDO5BKS(7g7Ot_4X2PS-a2GvK=xqVMoo`BGc81QF%b> z@g@8kneBazx!H|C6EQ7;hi>q2s$q*Z74MP%WUd(H6Lb+n$;uYrt#%R>2^h81_e#8_ zZ{GMyU*<@D3xW}P6EkDwUhn!)Nj~+%TlI=Bvm6pMDqN>)^JI170GL|&4XKGKeynK) z(>aJt>=ET~4(< zwLSeM2O?;jQ{-kgc42!*Fw=AGE!g#ngPm)>&aK0ckGozvh6}Bj-ORFM2#k+_-x}K* zw0>Ixu!%g`qhSv!wlh<~k~fJ>+U%#uY~?IQu_(r2LORqPJ0QMwrurd6qYpDylQn11 z-tQiW4Gb9ch9;^_q|3;7+)1mE4&$-QaZz>bJaAkCC3KE7UOKAyp5e2Ty6N)a{IHNr zy*^;0;jX6s>=+w*1+y9r&&hsS!QHT)5TUBoJ6J8VJtl^GXC^LkL^>REV?CzKA6oLo z159C+oeJGaQ;E!!2bW!U^F>p$7s^X98&Xi`gd~a-%q^DyqALv*Xy$)UR(uvmGGh0wmlGBmaYR-SsR8W3aD_erUVRuGL9~@WzVdTQzWfRKMP zEDF2H@OjYVSk6Z7oIF(^m12~p#Za}D)0F(A{7nlhG)G=VXixF= zN>l|DB-PFw6=bw*KwERZSxrZKwLUO9xH4|X9XcrVcBuEkGmnw56U_dMJE*lywwh%> zFn=^2Y0dNw1SQ`cY>{bufab}jf4I9Tm@oGVK1RworNx#&Z zoeaR9#@XvHI0um6-YtoTuyH--QzGNmuQ)8Q5E}+akYKBS^E3@1)(CI$fOT&|+wW|Z z$~m`SBJ5EO1}PM;<{b$f_WYGX@hgRbcyR-M$*-Ig;sN=UQ+Iq0f$WLnbt{7s zluuROmQUjM^ExIKt|K)n$R8ph5EAcocIT1B}#lQ%> zWre%9MJ2QtABWH#g&Kx^wb_ySFmo!{(QRYf<&d?P#MIa5!0bn36}(x*0*SxqlX~sR zxr1`udh=xK7V;tI`f0h`Jjo{;D&FF*UO~?mB0swCfYZm#lUu1z-@Ch4^oy)IaNTbg zzC<0DKhW@^YI=)OZXR6LU?v6E+hniJCWj;^gV~#Ir?dp!E=*4e5=(E5$aI(t4mp7t z^8c_|S}W8(W}Ql^-Gy_mhY?=48})CUT4N5!J((n!s+eJN>$Ti3Yp2)wi=U|3LN~gP z1!Tdg8=2(XVYt@QV&SzezEK|&n>JHZH7r}-Q(dq&eb?UUm)DS$HT|;cMs+nW=<2>I z*9%X}DI91Rm-&JlQdQFZT0(`AB2gHzO6xRLu2p;jxF>tRTo0p|URR(ym3v9D*ivuRab;1(~F5h;U^vj70cF2f0mu2X zN%!(5UbH@f?$w5`VP@ruiaUJ#apd{v0{x)lwQ0kkbk>F%*?OCMO5V1pqR>F&*Kc zMh8ON7eoL89j-O-s@uT(`V{0dM(QRB(c z{+fIbx2mSlL?yvS6b5@_M;)LhP64h2;eYB7o=6$F!Pbeq$P#Z}lUj4la(^j0-#Vv* zslg&4@?BFla2VgG2o10z`epSoGE-n~GepH*rSb-T+*J#;2{2sR=Qr)Dha*m+ES@BM zP>tEoPO%!7O$9l|*uMOUGSeFt@$p5*mBW>((YWD?>sKHJ39l)RAP^ju0{D=^QwK{P z11jX8Tl2tPgx57p7)FfMFE!s|q8=dP?i?ul0P)y+h05(mUyIp}Z&vfK;LIO=8YuMn zw+7%P>$$mHFm;_Z~{v@+gBp_~@(Oa?w0ZoLnO0Y`JlO>Ded+;^mTW)52x1xnkvr}%Dh4VVqy5rv7>cB|9W_McfjwR z_5cym-tALLUZW%RR-7YG-c}w~zjl1O_I#pGb>N|1bC4f+6LOQrL3#q5R>U>4W3#xs z=xdRYcS#d`?@!fFf7z9tp_<>RxQs1?77h= z)VCH0zIT3xOABB(V3l$QG|?g7i&Y?j@I^cb7JB)m+V%LS7|Gwio6m4$Yi6qj=UIqh)M%yg~eTf_5AnbBU;4<}lfx5hg% zyw2RB$}|CGtbtiSWROn>Xr2gJUF(N&R&C&=Y@&~si;UcC1dd#Hnvoj!LNE72@lKGH9||K$LliReR3+jt3^mjzZ6vaHsgtJw`t?1T0Q;IOIgL|n z{#-4Wc&DwN9FJprM^H>-pqPL=dI)bmyIItH*CEHuXPXYaX0IsHqzj=~Ns<1wkEi5- z@xCLMZ0go~I4gog2s9?pFPD0(VDhU!#DeyYK?G^@;VVIdPgp-r0&Yb3euALQ*D2QI zyUkIxir$rDmwh+uTJA%5jb7k@`3}j7a|&Ja69bKd73 z+!N$PoxH?4%(#xRVrcBvG~>k@122yeBL@9zy4FJbKQWY1mDj(Ye6>INL3zZ3zG~m@ z3i1_Hex4p{jBeW!_yE9eQW*+wA;kuxzvX}xH z9jWKpG?pUhp}1+L_M)hCR~^N6dkM~oK|u}94}DgUKLsR1Yj^jm^sBg6uc=>ay;syL<|ky(xYrF}S_5@0y;Y_Q;{U>Y6?V9|f6O zAet^}d8`mcURLc66no`1Ih`$*FTaldh#fijCjFDoxx{8H=I%@V zJfYf^0y=k(cbqYu1+t2)IiE79G*3JkAL?q`UA;DOk^#f&e<1w=jf?XOQL4#=S;rZqgM*ehC(kqMA-`MGBuOG7J$|JIYM72K{N zAo~d2v!`HcShwoEo8SI1@uw%AN^ReXsJ(h3I0jojC~%aMYw*0Bzv`6Qam!(!+dK8F{<`0iLrrp#uel{B3s{>oh=55 zx(Kb4w&y0YoAtZ~87k2Ne4ovu@j2beZ`G4xy=4<*Yiq6=PE*#s9!QwrAC z>5>VX*6g|)7Svuz{#7A5qd%od&C6AS&LiGR8t}$Zx)H~VQ5{IPIQ}94d3t@W>5s~K z?Mnd3+}WnUT4hd1T9rQAEcULt)>X}lb}H|3d*wKTPKIMgWl_rJtQOXV8ex-uMiu!w z>x9|_tIoaljU%_t@9w1bsyDjL`x`5^q1`+;%rjy2&5P9DI+GS3HCS^pD=Nz~xj5Nf zd2g{R>U>}`ua2MA1v=n7Jk>bE)3|8SMp5bTJ_0->nE|dlo0C%u4YAThIzLGHdm5ZC>`mA;Y@L!NnL9L##?yp#;Gbz53JHTwFt1SlaK<`mK& zoc4(h#xP{cspHdGP(1k#ppV`lGlm=;ON0CSK2jIuk7Mb05Nx5N*b*ua>-MIL++?g& z9`Wtx0-^$Fh8Pd=3y|?6q`Kedxn)Sz7tBoOQYunhwrxt@HFfKa;x-SJcE-T^%9E7Q zqxzm?5{S4r4HZxpacaTzc>1l&W{={=9v(|W#C_cS*{yXf3ibZ>$DrFD>IFQfF{813 z1Gf?Gqs*?yvM7rH%?^8kdRFF=*l84>xbCA^`ulMOihyBLuv(wOmL(88dXdH> zKa5guJoi?LubM}Sc2=%J7u8f`MHA~(k2=eg)0MGWYA*&0v4ETlsjMufA5HT1*3<=z zHk``aTHym;(MY?FJugKQi`FIQWPeO0Jsyc!3o>4OJrnL~1HoWs%q4EGe#E2g>Xw2~ zTu-t(i8^5Y2KIHj6nZ+|tgUxf-@b@3kw;z+2=tQ?2P*!Y#hyR^-Sn<@#(5+82L6#W}IZhShK$>cl8c&GK)M?HPZ)i!yYnW zpZ$#{KAhcbEzrL$%ymZ+yEm46*YOHE7e5(4D-o?;>Y@jigJ`QGw5zKX_m;}Qs*9w& z+yN>t^HLtK(SKml?^5sjNiCM*G~eVC_n88k!0NJ0zDc!|jfZEvNC8N+YnVb0RAN>3 zdTi?2c^@;9++G<6RSzHxgUV1A=cVJy>mCH@H0UQf+!(*Ir;1jUUTI`SJJJoL7CU&? z`0Ds4ng=BIc#rF9vsA??N}|VgS~?$`)u-Q=b0JZhw4MT$;t(SfLH*pdVK*m*sLlZ^ zqpnM#AV{`V=afvC9`hcD69!L}Rw_{Ht%%EJX)Qx_j8MQ_k4PSU5+hjMX>j_s$Fq8! z_~?#+>@(^Oml)n`y9-iE>XM3XyuWSLA162}%$Ib|qeC(|>ztOAyUw@=&a1lyAK9um z?tSHPXktyXBQTWqTA7v&x^bK3^sdLSTaV{azYxxd^{36$%L4OFhP*hiUCJPQCrrUX z1qn(iENX&2OSp?_v>b{y11{gg;Y_(pW;rHd@89@60qbD*=M49lu-!%=oZ9f9`-=X7 z#P*=zX_BPCx8^0$`j+iIQJ%XEeD37y-?)%H{ZjsM=9~Pq_R2P&G7rn-Xnpicr+SXN zh~2_%NhyKjtC-zT`if?289(ty^JNc)$GF5Ei1Z1Gs@?u{bm!Hc9F$bFmg__GiCS$z zkD7LS8Ewv?I9oQ#)JrFhZG zk?xYJ$f%c(rEwNkJRm`(ciR@YN(~^Dy>pt$JmpTo_VGhW1!@;kT>}CyJ+HnNWA$nq zd(P+U#+$OKSfbX~@VZ*DZ@s-5qmHGJ5PqFDpsB#m7Iy#}mrbvuVI^_5=afzb#JCIk z$!B(K%saygKA%p=*jI0`13{y1!(u*&(LZxko4xVQZy*yZn?u1#@w!EBD79try}db; zOwnMCI3FweC`1TLuZ=7Sfnmex^E_Cy^SE;S)E%-$YoF?RUg1jP3u0hmIE-!}&&pE^ za}FtSENqhs**>tt_N$J?iwGbLcdgF~_Ih4?D18+geP-&`&4`cH!-rv>pJ=A;zbT?* zt-pBdM0JFGXr)Y|Cy0kJ=xm#z5Du-cH9J#2ESZ%AzuByiS9?RQ#tmZkS$3T5M7U!C6F)%Rffb7yP&=Llr4Pf(4zt8Kyzo!&KSkE2>( zGBThQ&&aj8R3A;#&wE#-qQh&xQME%R6%)sHEH2DKxICH0emnxr-YeXeoP~B24#@>L z*un6E{@1P|5j0!LcAfKxY{vFGMKK>j-l~&aqxR#NQbFF_asM`s(Rj6=y)^c6_oap_ zI08q*TIN1EAaGa~5IEkvdh&juZ_;EcEbiYG+*N!jgi=pFKjl&J$F(D~l-gzQEWRMg zbV@AM9l^@%AQ2&I!QFJU17$pQQbjxm4Ie>zoxWPbI`pZig$y9Y5MtL}j=d}D)rnnlks0NGS*2e{gWBU+Rm4nE748M+>`}Das+va^c zqqhJ$oj#pma8w+1PSDKKXe$zKc}65#r5|aXXjQp&I)%(pn0?)f=GZ-fPha2IvPbmm zv%vP^AcqQ(ttg4y1k08$$m(@xtx0Cq^z6Qr%nceo-xMYY(HpzJDQEu~ z4)YNW#LnpyTEmKsADUts&)V`ciMzjfy8=bW2X%C059sJF)Y02vXC04LKmXWtXX~0q zlIe~0BEM-FgJ%Zp5+9KOxBhCs8ObiL)w5R*r2IpY5m| z=h~jAI|0uMj_=!$vtn{brW;r4bJ7bzw1>QV&w_q@YTJ!}_p>2CC<6NSGe6Q%i4kd6 zp&xm7oRC`0Ciw`t0P}fW;=cs8Tm|-z3Mt|6*?JG_flU~?tx)w~6EuTR=ijy!=L))_^V2uNNOaQ*x$=9p^r0HvvZU}Ewc=t z`96$}27GvLI8UDdWH13xT{oaT?;)}qYc?JJBJKuAYP}i+sN@!`vw82* zCS;f((B#s#YvkQ}w!0tT6&bm`ImuIibQf^!J~8rWS_IT+Lz#p)78EejxT1AX+3h<4 z)tg#w79EZ0Q0@YCie_Oa?fkNkO;HbtwyCHP2OC;!{M5b0mo+#Ya8Pxk6WzyZZZ>&N zlxm)|m2m^SG*tea5n-M4syBba8a%uUm!dht7S^EyN^E-|mWW=lJO#u$907G$kCmnC zk*<~BANk2AoU~@NzXd8M&IWvRQJ~21i3U(T4ON$CL+OY5UQ-UbD6C9lxxPy)m01hm zVtdCY+hc>k7>e3ABSFjy^WhA6&P;JW2yeUyK}>*>(34Dkpq|L7AgrC1+sKDfkHVwV z2{7Cno>09ddauirz$mN4NcAzL4SwBpsQVNs!Oj8EA(taE!zp|r9?ZVrG;f7pZo zffvkco{|SmihDYChNa7}`*DZ>I=TG8fId{sy9?lX6_or3IpB&GYdG}2tIJ}uWiY(m zeXSFe2wbzaLYIY3f&9eY0^ZkLXD7ljqGcc<*t*1MW_qdz1RosCUNF>~Wb+`$`~3Dt zwbx%`uCnhu8lK|P7Fj!WvhKcqo-2QKiM`6F)~3TRqzz_dNoII<>`@IT>3`DgP13*h z=}xege>g`1f7un&56Y(vIr9dul$-+9%9RFQQ>~bd3YU{qEI=6%`@P32p#-=Fkb!Io zGR>~DgMwFOv{h_?fL)I@2#3sAYSrC2zuWaCC`B*=RlK)Lbd+!Ky+YZ(03i-zkl&iM zKia*LES2%viKi%xl96)5I#>oqQeh`3n(t3V)UgC7^aKLM&7<_n;&2(uu4#e4neIo% zx<=Vf04U+ktd^h#AqAJ+vq+(stv$q0$F(CaQe2H%VXCvc8sh6Pqa8cnfKVZ_Ty)~D zymW_qSQMx^)VqOMVGY7Pr6)QkL!|>TB$igqg?^Nctvf9Ri&0RN1+n>!HV+v?}|+!u!k)O*fQhCrJ@cbOL&dcHVZpiG``{1or+@S~Ym?NUF7-HtYG3!kfN3QuD_Hce z?*gwTd41J|)Gd#;cyDyEE*J&3X}l&1%Rrr$3FFWMmB=%LFVrLzhs||w@u&zNW853B z6jEl&qDxJq4@yMH`A{5PAXtlm^7a5}B)Oq_0%&j}DE3h=KG6swiz@EQyrq0#TLdsU z+RAQPG{o{8&v_XOtfY!Y$LnilJ{>%pVHu!$qnBgm!RH%RN)1d7usk2MbTo#h7ce^< zEgnRSY3fsq*2o@>|(nNs`5ipesY}KT@a4>N`Ym5r)ghOj%H)@^rC8gGTUBpK1 za9%`^hN&_Q+?@OthQ{=G$XHeAi10DZ)Y0+MY<I!CHVFH>v-X8RzX~-X< z-C|hkdzY=tK~t%L$6?nrpj*c|`&+vNn_T&K9N6VXd1@j=kd;+n4}hL4BFir4fzH2A zypwvN{SK{RKkXMi_(Toc?$gaWfJ4}^&0%U1g!pXAw`er+@*t1XTCH9=B`H-uRd|5gyzh4xwEwBh%G~~*Ksm!uTZf3 z)CO4;f6b>;ZgR$B5m=^u;_eJBGCj}fA$W(x!Ml!EBPqVfyi;pxNPI7MSd#4Ll_rc) zXCTO&?>$*R?(JR_g3PONyM085RG}PIludOcd;2&vq&k?YO>^tLxduoruFSgU9dj{UV~bvALb}8G|O}W3=V7t$Rdi%UU-a zTmi7~io2}I^f`A(PZYlJSoJzvf9v))8Kt<9Ex+3dgO8@`AAWT(eUG@iiA1PC*EtAg z@81f+?D+jnBm*fluOD+2Zv8S8ffTvtd(^vU@5hOC$ptXY1DtS9)>8W9snD&(&BersEI zPsGQMWD#Wi1V4+LA!VPd^w?%!<-?D?%Df7b82I+>J8*XwuttvE9YXGqEfAHQwFU0b z1mRlsY>Twk*|HUwqx^|2+c7jNnxBEaXlO$qU2nlp4Fm30UC!&L4c9H$x@;xb3~YS` zi+02(ws`k07=p8B|Gp29A$;YUATT%Cy~UvfuvKMbxCygH`nSQ$9zZIb?N5wu?t47E zWr=0#a>mAde?*h3P{dKElr6Z~=zad^550;kZe~2D{jr$==AiJ}x1!ZCfo5;@0bu7; z(y&y8hA2=LKSYo{`!&f4ReQfC`L9X-Ym)z`m7Kl0t0pCaJ%6+r)cR!eZ=D21jPM$5 zv#hh5gkP$FU7J3fMSI~zkK zdH=`Wd&g7#zyIU*$U2CVA}gb8iBLkvmQguGc1reE*)t`Imh!n=5@bc_v?PSMseV^r*C~7s757_?pezI z25T-?X@w1LI3g3XCBd`H@2dCip;2pi_eE`6JS(OhfZmn89#vvn5f$l=I8z>d!#eUF z^ZU*bO!5RMjDNebI5X8o1m~+h06w#_+8sVYFbG6Yp=IKl0R%cA8B}5IVxJ#K0ee9Q z_Q~x%D$$&Y-r?r)3MGG$2~?)+Q6$ZCUV@xu*_}kC4u|BoZD6qH2)-~kK)`281iezF z{Et~J-k`TB=a1e$@+ugh`u*qqpbm7V-FYq>={$)8e*N!3xiH&ecf4+6#Th7<_a~Jp zX&4E}o2{?h`3!?(Wws^AJ{AGS_aX9xL7Wg81a+NXLl73T^nS6=ubuy5?{?<`TfoP) zsI1Iyru>~bm^!%TJ-lCv$cFpZPHz`!Z%gEvs?*YukBxG{RDj~N{hBB6Jyp}0E*VfQ z#LXM~9u;YBtbI2Wv4hWcpZ%E}9a5`&#|}dk7_>98WE*+d4T!xIDIZj;LoD{q8;c0s z#vdPd*c8SV0VmMMeDH@6qFiP`c~_Po-`9@Bf5bOBKFITrJ}wn+{!}w*rz@flkh`A2 z#~K(zgUi|jPWH2@upI=uxVs6whX&KEgU^e^?7EqfS_M9I&sIi2Z1gC2edTe9wvWzudvNR> zpt1VF$88dq64{URzR2$Dk*9VBjF^?1I4y4=;RV#A8F*;s=p8_cLzhlsGtwi%BRkF3 zF5R@3gK9gGs$JV09?8bSY+Yk7G8XRgr?@v_Jye&Ar6DI?j2xjYpi2g>;l4Ft4| zeI`&&4Q1=@ATajY>{b+u6iTb*|54DY+ z4gzF-aBci*Z#A%OmYN3PM^Dtlp-~6FqI>)5IC4Q05uF9rQAAqk_3;#rH;N@tGd&cm zny~MzPIJA^>Rpw8#90mqRo&n@E8ZapMOgDdu~lD6ZApKrrEyKt5pLZqq;7u z0o&jflBT|>I&Q&su7g|WxyC2py@-lmZoDa~`KF7Cx-)U)}7(n?7+qYi~R@L1xhX>e=hj zA;~|q{s)PZg{6^zeDvKC(InhEs6`&5y8QB}&Q_fQT#nv*QHMC>6SRtoj2mA2H2??K zAItJy2Dnqx5LA2n`1AE=c&qac73&7!pP_o&Yi4qZZEy8#U3KmHV!x!t{lb7eUWbSj z{$oK|#jit(D@V6f)Ea$xjEo{*UOQWz?Z#dMbl!)VD7KSq0Uh%2Cif$*6VKC$EV`8w z;-*4f`<`%n_ksp;bmiD*y&OG?;}rVW_Bi|)?PquvE9v^|?Rkm!IpWt^(o6l7lA_0* z8irlQF2CRTTGQh|4+bDyw8!2E7eP4F6NdZdK%~Re8MmQ*Jx4D`-C|;w)vh@3HaQ{W z5b1M15*JC8{xV3%LnnJT<^4;Ypqd|;6;dfnFiJl=&Yk+wVWQb)d2ZxuL5k#CsHZ>p z;k+chKvwb9jMrSES+Pem5z#kw>T;|)H{t8B(t z-jQJhSiUbW!%f&MktUg4ufy!AJiUUecS`$#;ZVvC4m(PZS>irhlbmZz zBNF)yh~%om_>L_T1?hEr^|?xxUqtfvo?zI~2Sy#ey0<0(2%J}{?b<>W2jEOJt)3un zjdps?U-%wBspE21FWZgp?Y@bs+k~D^umk)pu&FOz%d0yOpd76A1MvmqAj{*LDy;IE zjBkIY>fL$N8*8K50D~Wz6>)YS2@zBLhI{@cg%>(pCX`FmSuGk>8GA49+E`#cL(anS z=C9j@DnA`x)s~U_J_#oM<4<`|8FrxVzNn!i*DjI2aRL<5P{VW!lKN=CXjli$7rYF4DM$2ydpJUATFj<$0 zLeURi?bjOTV}{;*I@H&ef9-93f>sRcUlt)O5YB;bpLlTYu9hi!ffE7MqPFapLxLEt zNm?JZePS9@2^RHSePT0L2jdMM^UF@_(7V5N)1xQBQ%%j z0PnKq+DnVkp+R>0J;;9$^5r<7PJ#@X*DQuM9DKUrCe9ms@@C-(TFLc)SMo1%;OG^y ze%i?Zui>GUMX5XaiDdJ}Jcn);xuehB(D*M{`42`2^Ka_te@uD7t3ZHV)raGKfNR+R zq9L5`a2-e!Bqy?bq^qW}92wfK`rug;?656kdl(B5d2(p{_j4g03ej431;_(W3?)-; z{;%g1qKZGv&UPPWO#c6x3Hd`7J=6}Jpek0X7AGhk}R? zLNvo4x)ALiDzFFV$Uaso{}g?jXMI+E7ynV@t9k_{IBvRDzV9od7RtLAcRoYI>J<*>4pZ{WLU=r#IW}k+IO!e1aDpG>6_gyVDm zkw}1;ZW`YM&5OWyp0mw48Uah^oA-ggvDU9qIlil5F>K`~_i{zu`m^yo;7E@@NqR7q ziI|d*|7J@5ENC>t|0YWQE@e0rvfF_R)L69I;ktyFwLV9hq)%vA6`P9GOqVNYIwwk{ zVHY-4ZaM4>d|*3^((mU&LxUK*huUP;Jxd=O>KA6LHBAxKqZE9=atPkDwib`X5z+^L z(q#PRF^XuiC|jKV*W}(Wewp#`9yXg*)&Iki|Fg8H{#j>A4g??dcuK($5>Hr4OkezK zQv@??bP9qLfq>!!;woaxT;(F3c%fnCwtbf^wq`*&?sH-r%$DuRsN4I1rTs|D^Lw`b zkdDW9g-T8RA9fez^E7q2`9E5Ke{fuO%1lxymrn4%<)h`&Lc3DQQ5jTi{~B@dCwwv zf{Nu_vV41VT6@m%LLNbSz1V+t&=1T|s_K5Mf>G>1G&7GA_(W?Ry9G8p#kJt@TJ&=F zgBy0Jtm%m2rxcXJ?T1o{8SuFNnH~fd)Q`aV@k4h)2PkfB0ovsA_;jlI0q%GpE4+QT zVYXvQ954Un|J^r8jxn=~6`BSt<^dfTlhv_d*tUZva5dtZ{SK@^0=22tl9Du%&qi;&G@2q*ewy zSl5s3S5Up)D)MGQkQl;+AqcszonF-OdHNaj_#IU~6QjWjP-ZqYRA0~T6jPKp@ZOHn zh2?3QR3R4jy|K%tZ4eQbk;~6Ei+@svMl^VXc2q>-hdg9rSOs0cUZ)J$kG$ne{Xvinh012Ujq1n!_8Re#_< zK>5_!FIoOKk?}iVYiB^niC|$;KnS-4kRg22c=wt7V3Ede0Jk1H{mHjWHs7uLF$;4@ z%2rQ(c>D+%D9MA7Zq@w5S^yxgATUqhTr~+*M-(&Top*r zFZuW*8*BIZiJLn4> z`$r`zKG;3k8Bj1&IhII-(Ni|Q{bkpIAA4g3Nu_A6Q<;DcEw#!bSstbg z06E1;P2DCZPyiTgoveqdzz5EHb~dlY`_b-&-LKj?~r%zP0aSMeX+W|-h#>FwVZX@pvLo|=>%G(sMo)~I z*&W$DI8A6qQVDN$^F^ol`+Yqb9Y0+G95Xxg+0p*F&6rdni9u!{$&*7m`?oZWGbR(a zSsF*uFB zmRe|Wdq%o~2Em}kv7~tV(nkKwg_Uc#m!MtvP*8i+qWqpwldd1QTzU)MUPjeq#-d>& z^SAdN>v+AJ;cgY1UFqSR)8H^v3YfFrr0bq|7EvZHqHO_^)k-CTjX8yp9rrcPRK3l> zB?pIq-bJw_Z*yV|Ujxhpi4o|Aqhh=?u&?jo7ejDfPj9_C92XZ{BymXzNTZbfu2Au5 zP^NpXg9xR@&(=-t3{{TvqEasVoRau@mo(A0cuBw~jD zq0QYZN{~8w`_P^XzX#5TyrWXP6W33d zs&gJVkq?|I4$df-eY*K*&uP&5KRP1+{-bqs_o($X0{`LY-m7|&6Qz6E0@*x4M3hvW ztKL7Fq86w7u&`s;amSrfxlu`Dp?T3OqPDjsF7z3Q+!k%Td+7%8g1s;b*{B!%#X zQzKULCtkp$pTi{OV6y6)o+jO328Y4>wpv|w`x0c{&`W?od}thIY!U(C=|DM=muL`-~X;9dvhTrd;_6M9hbuyIm*VaOJB-&?sKX zj;f`)B&*rC@j!{qqwo5I9H`|uf0B^;$fVx%zOL(3J=pFfO<8aiq8iIsF8Xv6{E1eF z4{wve1L*+UF|d4B-D{e%c>9r~lSR{!YkSvKB&$?5*Stl{Dyqrq5BsAA)r zXBBgx8WZa9Iji@8>N3@B+?5s=sJnbGDszRf4(B?zvctQVDyuFc8FE8V?KwT^e5oqd z>r<+!Uxj1`cC^@UA;5J#g{kydUDpYj?xesx#%q&6hb}m*3$t1n-g7K5H=O3uTZVy| z3yO>?hlumzlRsHl6sX;16ptOh`l92)S&K}nC}P#+Wsdt#}xv=}_on z4YbX)waV&IVJ>V91u8K0k;O{ zO)2cMvsdnBUDyS??2akdF>@HCGHv1@b56dmrmtx;DR5F&Wz6-U@}W6mf%OH;t%JUw zfme_#8f0P8d^qg-R3+B+uZ?y&fbzUr7O#p15+Dg8^Pvl4qv&9Bn#w6X-?#H&L5AdK zQM)eJ^s4E*uQ|1cDt{E3;xv01f1!HO?GW8{9Y_nWt&5i#y0AL~$BqdNy|#LtEGX$Z z@r2WkhmyWX;^8@A!7_)*^`~&TmFLg0x6TQLmz{cx;yGem#Z|d8)!RS=f;E~9#4ML? zV@?}?U6{I}?qXR|TwwWd`P;%h_gcHljk)?{lv=g zIPtXQ*G!33;u_baf;2*3osbB$V!UPVut*gXHO=+ruqO!+G5-p&_h4;wn7`2O#W04c z^00Ltxl(nrzHD4rYryT{?B&Bg-M7cMj-ICKRY~PC-(EzCf=`3rqLRQNr)5%NoFXio zo$rl_2d&+2UNV~k<$evAPy!i}-m<6YPN`@A&^HaVu=Tke?S68l=h-Pfxh=n`5!JmU z1`|UXL#7yl$Klhfsxl|rW};f>*zuh4WVb?EzhQem>j4T%Quu#CL7Zp2KpWCJ&ymI% zfnE&n>WN195UPnlojvjN4{imKLj(6*g5J;trqE0L^83l{Za0dMHM?Jd7Y|X96xtv- z`JEpJ^xvyubSSx5K@zAO7A{SSuy0h$Yxbu}S_Q%S_hG-sU$X5NAPZBa#Qgnm^SXTZ>Lkk{qq0ca^)i6o1{Ye1~8JNW>WFRe+lRum3m+4Ziv~ zhZf5Uf3r59(M__FhUcNG4rt0AQT`kKO4A08y#^r;pM4CIL8zJI9DT!_1?;Pj3Tl6} zF25xbz#$M>-ol5Yky|(%eML3<0J+L$1{X=L((dj@5~4%)2W^5zM#jT@on%yhKl~RQ z3cC(OM)wEmpF4)pYER#!8)v0R&}%@0yH8R_^S41`dlCp~NU09=s^0|0|J@(o!`6_# zuvalKXu_g6xLul!E=5FDTzW4_ns{Ys`oB0HP(e^W|1}?E?c3}k*!eiO2U19{aiTqP z1pfCw7SJ!^13ny$-1=qPT^Lcr&{u~}(~vZG-!ggrYZ@_ha%Aw~$7{_~P%OL?P?fpQ zkz2oq#0Kb&aNi?-(LyK60QBruKJ3T^o_P9vKb9BDrRE#X|8|r!vVrA6p+C(wKRl_%m0wf< z2R#VIu5s7Z{U}VAzh=IWxwzl4IG9daWa>%!rCmX=T2}PoJu*ynzob!Xp)#Fw4(s|9 z*48UMdYSr#D@Ovj%S;bZWh5_*>1ke70bCk%AHH`6%@FXqz=j1kY;$zs&Ap{Fyd%AC z;kEFw{NOh`X=l1wV9iYa`;VXp8>YP!hRGGA`1NJv$|n_OJUe&(-W*3LZcOMVpH}Pp zX1Voek61fFhMmG!jkI#^G2O$j-#^IQ7x9Zm`ek?gBE0NVwdl7()62H@!c>#tq3FcX z+oAjS(R)=)4%MC7H397A>oevLHGa>szwyUkdKaqgp}*gA;_rL@H(!^YsRoh)GvLoi z1VhS-TzpnuLrIfobP~Ud>C}j7yFV z)dfMn6gPy|>rP~$RA!am%%p=JsBsIuzq~}*jd@{y5G~#|v9u&&@lT2`8*YY7XVI1&*H~P5MZv>3p3!>a5%cGmI4+IW zf{1YPvD|Gkcia`@GTvl-&uGr@Zn{KH`S}LN@#l))3N44~OV0ZpTC5GKuFCsb=UmeH z+hKOXiHutV;H**=AI#_M1O z6aBOk)WN4w)Jf8qqYO7HXBE}}znKKos%sJQ*}Y(CbZT?$%OTg{hOnZ-t~**gB3}aJ zaYI-fxg!~Tv~601L}GP_#p!Df2x>iY>gw+4`P`=h^-OKwS(m!mEf@M)_w<5D+wsuG z@*N;P@QYBRUrj#%^v8r<)No5w#{AxbWfMudGN^5M4+%17upTZ#OpY4`eLd?G$y?$^ zrhDDvv8VuAuqT0Xcp?%sxCs%?yt=sv?2=N6=>R95v8xBnS8Tx+D8~|fP`~c9)C~Dz zo+m^Xw23bmiJK}jxn5ccjE|QqE%=wf}f5ryHsEflUn3V z2y&8P7sq&@B<9pkb;?`klDPJs{;=2t@9l66xB-%bvt86(ruml3$XHrw1Z2%`%p3yv zBiV>1>KHq2@$$Dv5v5aUCWa;2*xAvK(Ci3|3wbW{q!*C-l*$O|7KS{l4W5s%;Hw&n zq0P$>XGe<&gNKJEMDclTw&%xWzpTAhN8WPW~3ezz7% zzK^U6yqAhAS>)5e^F7Sj>S;B6IKG3H7v}dda#5xaa&c`v zrv!A+M(GP;4=sn|(!F#muOJohvGN0Rq3!sWb7~ZAe3M(p=IZ=18l0?`Y#DG{CD)i6W)mQn-PawwfVcW_9+~{WdUXavt3nH=!vpVD&M)X< zmxui^Dh)rOwBf_7a9}QS6?)8a7Yu_%0E}O?6$b)@d|196@~&IP37aOGxY6Ntv@9%O~&%!Go?pvKwG;HX?psq`BHR3 z`nY)o9G`F(=42{^*a<_GpLfG}1grMAjs&+P#_kjDjiGme%y&mhwE0)F-l|hGUdrlm zl5cejS@sw)l&jFGeq9Cm3yxBqvwSf|tH%+@q0klU~$H zh4QRsIn$Oc}-Gvz@A&3&UR{~ZHN1`$_+&z&6sl7 z=GDr?OWOsC(wJ42ZBS{D<@)dvJw7ZQ+-@qA5<>DhEkX8EYcbI_OBEwX$Kzrjt)0K{ z-KlEa-sIZ$OV<_U7GM-Eu|0WoZWn9%JSS+{54GJgs0k*c`2&Hfxbxv!X) zBbK*vxZX3AvF%`AcFPeeUDzw(IFG1E!7tT!{vW@Ol2-3KJM0)3=S1CBOXGH?r= z)Ybh!!%eGXJz31|SX2)TkCaf&n6qh$Hf)liZ^hTbEH=^2%8#)7ltuaL%}pE_EY6E5 zI?&I+zty=s-f(y-Rs~+-wzqK3!8&Q&`#qvZ}!-J`e0$VX* zJUL&E^nCJohrGuWkJ-_Djsn${I;Yqon4fGnryvX&KXd)OZ5!-^&-i41b!5OUvJ+UQ zofV}uy>{{M;r={eb&jJeD%CEq*O=83W4lbCJmfA_wFf3RpNA-G7e%i!d>u}S8L(RO z?zbxuV&M%{^|4hRgnv&3D-3TU?sQ3+tl!hv5I%=RTzYVR%5*!1I0UBc>(F0HzZg2%Y+WQ&wT9{rhz(}dHWXs*taOreTlA051=9=L zWE2c1`_l>Q*ATu@vQny1%!k&OutgfC&u?aE+vc;Wc(#f3WQdPuF~rE}pX*~t>(IsG z&&}zm3>G)trCYZN;2&hF1}pj-%Twcg35)#uTyQOOX0e57{4xC zC{*PXWHb+~>$^2RGIiR13jSeF+e~gJntuBE4Yo^4E2-!R|IY_y-vxmK&Y8HZ*JBuE72XA8#h&3KDJaG23BgOGbj1 z!&pM*+?Oq*T@&_;QuXuC-=-Zrbl7?h-TiWb8I-pzB&w90Zf$1_+je&KIA84L9cpYw zLCbpaxO9XzTMXLJYB_F#;K`31vz)53To`{F$=Kdn^y9b60XKb zq?#$V@T)B=EGIR?lWaL>vnpj5Y1A5~)}51o2QFWQUzhJuh_+HML_4=UL0?~%Up#)i z=`Q1~KKpzjX6a52W^$tBiRv7|e4#4gA1ZcTD!B()G34lnE+G_W>c}-e%z_U81C<4m zaFuT){Te45p{h9Hyv6N8*)zuy*xs)1W`}`(R`eeOoh&F|L_y(VS?Z@D=xkE2@#Oa6 zYyNDRYX*`WI`eRPweBr^m-G7_oZFnS`ZiuTDqYpkLcN|M$5Vxq732HgS}9S<3BP%I zPhj`W(8)XJ?!$Ei5R~1*roeKJTw$2c>S(bFD@G)8WSf7W+MYyDsnNWzlu|`w854~W zwEdvs9J&6ae$28I-1VOF(1kKszebHJs%F<4cgOCR-!76rA{5Lm8=5}idCF%Y>KKQZ z$fNZ!-py@GhWkH~i7<_71O}c-Dk9LMXpV!&HGf4yiMc$TncVf3u1I>^-E$OqNu$?v zeSX?+4>X*<8&l4pGEBoAIs9;=$(C+AHw)89)a=miQKz9N;88hT*S}@xQ^QfIoGuv_`$h# z-11EU>3l-7O#l8o)acPy%W}-oC2;q?wFcdt_$hdG)YOXDuhq&`kvEhxW)vX6d00#E zj~0NBLI8e@c9F*x37pk@&8fWGZbpD!IX!cRniZdNVd<+YN;a;_enisRpr=xi)HpUO zI^s^~e$nQ3k6xY5TC=K^)4~kExa$=S&Jxr#IjGTDD~BP6$y)qU_k#}qjp6t-ca+>5 zQ0~rQkTL@hI~(5Hw8VuVe$W!8PJzyXkH2z`x_sG9Dv%c=892XpEeoQ759zgb zLT*sE?qjW#a4U>;c|`Bbytj0#zwB)$%NMMc+w_pp=j!0Y9I5p|(d)T=&MBc#?W1e7 z^gv&AVMMC>Py7ah0r4AsqUt5p(BXKalIv8q;j_it!D5wN2(!C4?;=MIGJ*|yxCh5U z+{Fxl1KF;M7|gZiqSinCi%TFjO!#sZE|>zgk*op;uDxrd%v~_xtvQhqj$!-P*00hr%UUvHu8{*ts8a z;>>1FKssdxb?eQSt2~|Mizn5QqdwCDslInX4kMlsg@~@;&!oQaqAMeqwv0$R- zV_VL+q{eN6qsKo&@@D4dhAkZU48E&;iv+LeKoiLVZYD0-mcrBJSj);YVbq)i)G(<~ z)K-iaU%*#|=?4t{xz9DkHj05_iZGUssQpS6it^f{6NhjF(bM@f{#jdprwmkH<>&;FIolKr$~d?h_$AG?3b zH!G@dtVTf}$I`w?#ugoTD^Pz?Lb^xNi<_rv$O?0CzJ-0vfUM;%hnOrz><#!ayFya( z>p@|cxiauQh&q{uSo0Bl%#u{j6w+Z|47E`nMxDY3vH3BQW%0zhA}o%r@5ya` z3?7r=xCDg&{)-uev4OpGyDxE+S=gdhfg(+iF+JhGi7N1kyCuQ1w@AVxLR3Q;lhKkKH%_ITu^7Gt?DG{sAt#(|kcf{(_#klb%c^&=E`LElEGk`;#-cX|v>Uu!i6T#)`d8ujSjY>W+jdU@{;am(Y4MytrZ1y$^S*9?Vb2P6 z?j)rg!TkMm|0>YasF$x?;Xg>Nh+5*e_rxAPn*N6VM9UHd!1FARa6i_Zhe9Cy=Um{ zd8Rh7m)OTWwo$dD$(@%z>)g)j%IipHZKUvsMY~5bclhHaI=QgW;LUyHdDr!$QKY}; zSET>oLb41u>tKbubv3kU8qo1dolR~ULK=wg)`zgP2^Tjv4>}i)hSptUIlQYX{Aa3| z7TnqBr(su>?tZ$ARLQ`fl8J}T2>T1+axZISJ5ZU)wDy&YV|>MbkGzlojC{^1Q}wLQ zQ~I+$_Y_3Sg7~s-7~}@OS2Da9BfuJ#NI$-GOEZS%3LBA zU+0spDZTsoG+pNR=S)*HvsAq&OZ^5ZjV=ldUki0=-0R_siMo%cS;6RR3em&<8RWi_CwhHjc8zi%1~o0%EDy181*s!_r0 z8Or%j1vj6ZpHkqVh{I8pvUbkayVRVrlteTBGws)m%bjHq=77*omfdLmwC4RCzXvA! z6BW3D-lW)Mc6|N$*5eCX!*b|ygX0Ar`u!O>Uh-JU5J>&$Bol3pj=X@mnv1TQ;CUFF znP|qdOH;LelT9zTX?F@ljz%|lhGyhZ2J5m^YEmo0=YcI{cc)5cj$2H zs^u7+oR6pV=yFxoY}0=|T`vVy;9qQB=` z0s{B$Z(EoBL$fZG_-zOpB?%Z=k;3UuLJ!-2QK`6wKVuM!Uerh>tToBjxftd{LyO>y z(y6VnbZ>>TheEb<2^igi^*S{Edz{S|TFl_dWp3lIAFYPsIUOR$LgsE#xI`_;b( zLE9(Qh7rZWsbtsG#TCiB)t}OaHqBl}pGv|;5ttDT-vhNlr~PZWq8Bj;ODi5t4<)kP-Mil?eR?0L7<-QU30xK@U*FJ^FW*uChfNX% z-_;}9KG1<(*1mPCb2?k*aeHIQH&4Yg^?D?EWMQE?u^UM^o{CWoLhJO-;T+QOV8`(H zBhwotlLLfL0D@OD3EgsQ$hieEH9(BDxlrQOq&S6k#gLP39) zP4MQ~e|@xwJ!##rvWk+ELR>b!V=*Y4g`Z9@3N}j#!>|xqG-qE9Q7nj3d=hGam1Sym zHSQ$o5K*OCWBvEU!e2h)fo6;Jn5wQ6E9(!g@KY?n!MT>e{A=(0W8D}@zTx6_`NwRP zXaDQfD0o1Vg)^mZ`!icDS7EeL0?9-?e^^%G-M>FNF~v8^N|iU~_&zdY`Ae#@G21AtiNP1am3R#7R^Kz+M<{NwL|8cu?l z_y^EW7WAceTvb(yhyX;}N`XUB$z$kCzk zGfDMeoa%WJE=^6pD}enqP~;(SW|EkvKPSsWz3jrm7js&sYgFi2M@4ezaM|`~C9n$o zf3OmivPA#?Q}O?+Q&H8zB6B3EXqmKXS`BfmF{)3f&{o9#*DdyEEB;^Id91NTY?nxT z^7v(m%Hi-u$bt6`Z7eFRqFU)_uG0im56cPZy5u<8o9=}d)Q&efy+(#=knYGTEbZ8X ze7bs3D-zVf2FZX8hV|v&pOWvwvA;)9%4Pn$eWGF;{|P8Ro&o;73>n5jHMs)P^{zt$ z3nH)&s7>n(#M9*@1!E;Lw>D6okwZ#<*LRlwozw6L<*p=_r^S~6FlzhKwtg{ zl|PTIa|iJPPDQOn9{VA2;+JL++dCpRI{-Cr*59LgLEW8eO5i&;P|pKRfaL*T1waRo z!hof(l4*j8`=?5$j)+iscQmDtqNS;jAmyNZc4<{%=h8Hy^_7EbiS6@0Sz!cBew0TBy*Lhd-~}X!4k_Wf5d5i56EP zmYd24FHF4;qun`<`+%(GSfHf*J3GdMWn~@f;q}q{8M?9Wss0Xden%k?y{0^a5E6w$ z9I`W}Re57TR_(#!qxpzXvoYkEGX;&8%E4l>7tQ8;u+`%bB!+?i8i0lz>-E`9oSFrZ zwgCdn)1lc-$|jJhy#&d8S3g(~%8isV@Q4Bn#Ubm@T-6GK>lqg@N9I_>EX`P(7n{I{ z>|Lxz!F^lhkC2^q0vQ0$tMs^DB~Y+`c;yb=l&WE6x!yeg*|W(RoEVM>y?8t-Y9aA4 z{BEYM9>O6D{~3w@^HeIyVd!VDjh~4!i&BCXDIaWiKp{w8fB411Feq>Fo;~df8hJON z2}m@1tVxBB39ZoG>`%d33qeh>_Q9h7R`vb}{Z z5*x|k0`_&I_yNLEB#h_>fr6{4rEjX5zM2o}X;n0X+7&@r{BU4Ho&{_j6~11+mwey& zdO8AA3pqP(IJe%+0+K@cQH?*46hhefAQVBc-Vsfhf*8sm?>$`MNBNuMiaK;LEnA3F zBSD3bMt=u=p@jV}(=b0GQ|8tVTdMW2p{Cjv*#&KDCnL8K_l(}f#YoZZa1m><D;^?a?LxJDzq)CQ;j*<&BB^PHdqlP~Hc`=8TJdk|IZ~XSfs-=?8Hv`Dr~U5x4!AcgY8pl26O^U4=kKBF*xB1i7Zn{7<(-VWh3Hn73V2r?WE(2C*= zTt&A6SiS=F=tq023~N1Id1i*nn(C-X_Rp|-M!nyFqdEEy;P_|Yc-Md|$GJ-b>uY^3 zVQJojpaUKPi2*&5zW3t{0bOmh{fSfmWIb~7sq9H<4~X~d1gP>fOoevENZsg(X+A>u z7BzG2d9`_75{@RS$uR@MOxs8wN;XTmJRipQ0LxzUC%f%Ji^J`G)pM_GRVAOTI+Ii$ zY`d7ZoDDf5*M<8k8XWlQ8qRd`j_N#zj_UUafwO)e(tbtCDGsmO4%8rci=yt_V7<$y zqRf&*@l|rtFb8(Rd|e#gi(V^5Iu-^VRY#~O__9}+_~^ZE3sLpezcHyTBzR>xF zX#4<!N>hXNHpf^w#*ONrg21r>}D8$A2{C#O57jm436hKNCg&4O_|g`h9rh zXSavqAz+c^XM}&f`aiMIU&!`T?H`a0#mpKE>WT5Hk)&W*0LPcEkv(D()P0fW3WVYA z0L1EIX*+pW3AQs7bUKnTs%L*e6`C6q64d1A5$=WF=L2kBgMlv?u2^8udy&Z*i}(3< zpA%M^_L-A^;aDgU>%jdbBvJJ$;bMDF#iBIXeqM|6IMX=qo7?^yuKD>kA7W) zPtj77h+$JK0!+Ru^DiI*_XZjqpBVWImQuE7?owT;w=B#2OA#bD$9SEj64-@sCYGP# zu{eb z>A*oCj)z2sX#1Qc4f+NAKL;IveLN>FmirwrCs$wm4iPs)MEI`!xei2&2?z<3hMScoXD*p;lW&cg7)`>nCo zdx*bmAy6lUfXMX-jB+N%`ATtOuO+~;(yH_wXm_; zJUQt6j!p?gH{V(8sG2BqA|CGyoKq9ALLC`bpB&n-MTRlRKNsSXiuaJ0*>{uAt}mbX z6VOV*u?GhG+#@_lZYq$tC;^`vV<1L6B=Qf@#Ni9jDA)8{9TFCb@QD2la5 z4caULD3IFw*zJQhw~F&V;L`jaSr%J4nsp;J5{fCeFL@XENj&~|)omdk$&MjL?T27) ze1nIG(L`z50eGQg5!g%ZS6roKx{5WFJwlgKxZ zA*f0mH5*ofe9{c$i7Sw>Ft!N!3&m=+lz~>WA0&aF)@YMaWE-m?T?r()D?C~`^2jQ@ zYEtae=m#ZBk3bvSk4$!u1{C?~f}yM=f+v=%NVtmJtc!ShQ6z@g1V-QdA7etnp@;!GQyIx~> zO9e@3`=AzYwnb~98~UOKz%yk@qvBaqmMjmEr_#s|zQ8VW zc7ukJj}ZRZr-2O&)rRJ$BCe4AEkikLNT_jQS=Im{^)p#e!=gAUavWuBDuP%kzQO!S zm({~`^pk>p&OS4ge~V-{AV4AHxdUD0;BM0gVybC;8=niRv)ggt9Kn^)NJH#YXHb-( z=_-SKY(Hc{58NDIS*qWhSqn2;2g|aP2f)0%kjMXnXX0-Bd%gqn?q&<|cRE4L*Gwq-e zmB7t=?vBm1!=m6hC8MSR}Q3*)yTQM%GFY2r}* z2{M&8hF1XpW%V}4@!-e#a_77Yz9Qg31W{L2DiIQeZG~*=xs6E(J~Bh&+b_KMZv=CC z0CrHhc~}I;->zQ&qhKzb|37)w|KVh{+rQPdTPwgeRxhC-pMXN=uZsqtYLM!ta1y=ZH zJ1ag;I=VzXa^`&Ro|u`lX6G9tcgmlu*doAWN0D!gp+)to{d+1wviF!HdlGzl`9A)F ziVOlSV+jg=6vZn|c-$rX0yY!tIVK7!Y>O>uuZcez57F(U+gq@uyxt>*wgvwwe7V_A zqaa+ZL2cLs_YSN9>|udb#0P;snF!wBqf$8xuC~h78a|DtF65DO&AKNve8T8AsxJhL zAidujx|^POe~0boFR)e2+-#7aeF;FlB*$Xj&g_vzru0Xc&86aUE zS~Omo`DV;eB6Bvap3U-wzz&aU1iQ#D?s>P76lx%%dqxz6ZBgo7TrwR`sJ{|^9O^LEYqFourb#GtMzFnY!Zg0znAldIW?+20nLq3a%pA`Ag3JHCW0jF77R>CpfM!k9dMH zK_$nIw|qEO{9TWB&84pcS(q?GSo9U_z8Hf_-hA!3$F<|(!-t>8@LUWW&V!shC1)|aK>hZ)_PAGGZ5@+CsFX$!|j5vL`_sW&*rM7p4DWI(*; z3ypF?QKQJ(4jh)aZv!r}X#Y9Ff@iv=pQo$(uGz<5ZY~06pvZqxqKa<=vll;vYU$JU z3Nl(f&IS)l>HS9SD6;+oM@VBH&d7d5-)%0|dm+lOO=kCU|Ih~hJBA|A$x2af7|NwR zEi&esl!=t6*y1a7P@rtW(rM`~b&KB9kxvJp$l|N8^Zs*W>`%=vavhJC)GzInC#slO zR~FwK&wJ;#ik+8v+R`$M%Ir%!BJaAduqn~`xv%mMX$J3e&)-~Fz06u zn8ICXQQ^wmKO+5-pNv4w+>ezxEiKt+eJ+V{Mm_M3WkIklHB<0ytGNx%wT4=Zh}pjI z_RvmsOWShVCA;UUF`zdYXR(;W9Q)Le=+qSzv71lS_zA`lB!)O%BDwlWwaWsW1;ml& z?Q|`ha8kN)U}gzWmVU+nXOej7qV{2HOwInSlmaX{tXLc;PK0=}DdfiWn2-p%R^_w3 zRWTVOaKsHwM~XY$HTSul)~T}JmSNWVMEWO`Xd-oEjQHbKpOGL^x%g#2 z9{ZdK!#5=d2`fz>X)Nv$?2b)gDD+$QSUi-bFVruUo(NOTyCEI@R9x67BXaa0W8iCM zwa8d;JcIO!Jl_m*;g-~ysO88V$+88~gd7I;&*n2t6_eO`F%TxFNMC8eaVfSiY@$A( z?8&JN{k3yv4@K^9nXGL|(h=CO38xP($C<7Y`FaSiLk%}evKX5f@&!vy{+g=lwDIem z_-@S=u}Q0fQnd`*K($jGzQ{Z7Yjrc~FwSgR6hCP=6Qk+~a7O)E3~dNPc3&DDe7pDn z**RitR4Y)F9L2Uhl2PT_6OoVISOcf#`8T~6j>EUCsW;a18EHLV5Nd)aBfO@*5+R}4 zYjs@l1$+Mf{hmw~=}tVS$*hItjN3#$^m3k9&(dBlY31r2YNBFSb?yk)Zl{YTqzP-XM(S0(HJyhngG ztj@4bT8Z8b?XCDNa6c^EM z)P^iZl#X~@1FL+$M7Q_37;(FcFhe(QVx0`(oA8<>rpoH8*`4rqFO^AHEYTZOxRVD z&LN)K;sg$BV0Pf+<-;HFdfbt>#deV1|Na8+P@fiW!t#RvU{TiCfJJ$x95dKUOYV%t z8F}R#rs(A~Q7k-?J1R_i76qpBQWQTU6O&fvx-Nc`qDWxtT+22NUt4FhJdl4%{5`xc zoiQVl64LuRl7G?ha&m7VLhC8std1$6ZD;RL{ah8QI){I1RtgYVh*Ee&6$tRF&;D`@ zb~r4vEofPtQFAdSeiJ77NJE7Hfo#u7dbz|ey#3RAkFB@n+|;H(aRc;Iapot_GI*=L zOzn@@1XDtQvE0RD%=0hbqPmNJ;#3GdYt9s^d>l zIU>=ufD$wSW88{s7q4gf%mN%oHJnR4)_;)M&Y&O0%}Zw>z4kO{q;5_9u zL>`^W`XOSrp=rsx7Jx#ul)L83;FSrHr7Sa)&scrbu_wP5x!6EfBLx}7_SA=LUY`L= z&bVl)#ArU~=^D>Y4OMZ8F96jhL}(MD#23P5)tWo-<-K`)SH^WcJVbY~I?6z9hEpV) zW8lnr(X&fu#bl2M-s)2^I`9Txp1b0DBM|8IA#dn*56$k2E-wgqBNWV=4(+x6< zGg-dVU}o_M)zkF=kD!-298><%R}I|m9ulN z7fdNmOi~U-(R9UgSJYn)3A8pBG<|jRpldI|HbL%ofJXKNP+pEoqnDkZT@80|U9mo6qevkLB{T%JveP7pku5+Dheb#4vmcQ++ zoF~9GgKvh*CB25aLI21GU;&31r0`@Lf>A6hl7NeB`^FNqitX2=LDg0YCR_D>qkrAS z9c6tU^=I;*4bKEgoCH*#Eu<6h%R(|rXxvd0aA`*&(X=Pp#&GsElvo%?p#2xZU85Vv z4z9XA@}&~O%6e=mM9`Y+A%HTyWDJ6S+}7kMfM07KLU0nw zDWF_;&6fNln6(|MveDu@9sS1qwO7z|9W;olK&Wil{^8I!Q0KnGe6Sf{DXY1XYloeu zFUDIzUhcp_skJm)&~3>-SOZdB^8~8a?*f-}#~nTr-0|m*OwI%@;=?VzyQqOy$U(M8 zTZ^m6`}pxGW@ETmtYV&6KIaAUipKVL?FFykAa^YKExR-tIJm9MbhMYaTrCDAFB@q3 z%|#$qx;q-%Zfi<9ewhU%031QMx%g^H5{&)^iKxL|7kzF2idi{p&R5lKfF4*Qowc4o zzYyWH+fZ+E{;sjbT2-OgD&x7?@g{MTxA)3&*UC3h76#GXq-3=P_VitsGID-9ZGjfB zA~@QnTGPY6)pto=%gRn|^3Zv(&b?x~#=Du;2k2xEXf#$NV&-*;EqX7$qQGRyTTaN> z!1U|X9`|_w8j9B5n?JrY8h3r0{Sy{B*N^u$r&+(;HEwpq$4Hz^k2uTz3cOdLz;&l{ zR?uDJaP!;;HVWoW&pE$u(7QGE`LHmFfZFZMbSk}4A!zsJdF?>QU&NPAH***d4+R9% zGr-v4`{>k|knVF#>+L(Lw|Zqx#O83^)o;l2dKq$U9d}1)-lM`ZmNJv=$|;ZbIDrQe zf(zq?;kH!T!@(8RH!1CYtdgt_1;om>FZy;?WObqn?ihesNDTZnC3_sn?jjR2uZk~@Z^>bedOuEho%NOlF zb2Z^Ovbu$k^;ezNAmgU{t%%e4@z#}z4Z^AIm!f?f;P~Xn4=Dv}KkpVJvaS=W^P&;o zi|DKuR(wwQhSz`45UdHSQK<*}8UA8$UoD(qsW>n%>f+sODz8G)WcuZS6Msj-!#IAO zMb>u26TahP&0Iu#Xy59w?h!p{&Cy5=6^-RPlpFj|%0$Cc?8*vkqJ(XBW+p|C6rdS^ zOC?{9&g(6_wqj@|M{k>a3Q2&aJWf|$cBY5v`#14zz!N+$xm!pv?lvr`O8Aw-!PQ~Y zP^Eaqik|?cYGtgvV&ftB(AO-@8Vd+J%Fh{Wbp0I;_CDsjA6&M}<)bVE+~p8P)QtJr zZU-4jR6-#aR(QUf$d^V#V0%E$tIGePViBH&sxGIgk46JS_uP%&8VeC}hKMkKV0->7 zaQszO=}RGr0#=0(l*8HU;Ft{BwA{?8{5?tLq0l!FGE^u0>`}b_xm=V8IH@Wbl`kcNOi6hP1^ZSv;5{~6Ulvr&P(nf>um1=)C2V_94_>bcPEGJHF5DgSg3iJ zYBL*=Ca&{RLpNy#a&g-gffvL3fI9CL!ZH2igwj)OY;11G6n$F%0~u;XHX;w2^1=(x zfWnlWluhepLTV0RN8Se5fu82CsKs$tH>z^~{p!sX$&-@r%ziR2f34tOl#qOw8N5g^ zW|*R%tl%wKJcfw-=pue;0HU66f9Kb2gzDxBk7_ij{bDftn3mlRz8h16u0kYFrtvH@IoL+VGQkQ^?ypb{>_Tw@`oyp5T1E5(!o}u}`tz!DN1@ z0;(Z2&gN{)ic7;&9eL-!4dX9w^7&ON2LZx==+&TB`|r5RYEy6w{}F}#CmF%q{oWtQ z2&n&{$@|`C4k~tx1WH2v)|5I~6%hgOoXP*B(5(=+8#_TN-p7qXeb50aR?5s9H=Z79 zl7N;pY*Oqd;%&?w)AE#uO;R(8p7sHPoe#-)``9Fi@D!j%LhcBY2ayt}Q*mj*=qAhA z0*ZNIkU&O~CL`7$4@)@$y>W;a3Q7>EZ6)J~tO!)6H@1*EDcu_~pym|^zeST2i*7Y8 zg&p{}f91;8cdSP@-U@VLav9CSU!v)wmyVA^0>0(sWdKmG@)n_~Da9ZYgT}>J9(52X z8j6=Ns|~+ob7;2H7)j4`qvRKc?__l4D`;_YvX8om%lrsq-KgfE2Uzdp_??eWvJ@?t zCSsIG#;=NmeK3YC;M?6$lC`Ch!=`cI*A^N)MnMN951@or5I8;$61#b*N}Lb#Oglky z#`>q4OdwR}!t8rJpmtoe20X|)o7Uu$Sm6=`R&7BDwbJPok#sHtUN{^|)uYgTnGHlv z$%(5_%PCrN?7gHs0%r-jS8aM#7`ophUMb(NnrqHAfPlt4H*L!V-JdQfo0~t0c8ok? z@{KIYGlp(;l5-=(#Y8K@nePwrz1;D$`$d~$y%Kvw%PKJyQdvMAiVt3LfG_L;$k`gz zd5>&8@&Vd1p^8qLiR#B_S}Llb_VrUqBcUK#adM#-nTJ7xMSU;D4&m}HWASg0@i>m5 zaY7XhpN};f&zu-K&U4$}vo*$ujN1Y|m9!*oeyK5F?j5}eF ziiO+KY@W2+;(;dOHDL7yDN%hhpv1Yhdfya|ACimb4-lgp*^MrytoJE-W>k&C3UzVa zkrF5#(WUiurt|u`e%}4UW)wOyjb3fl+sqyZ#>l@DT5zK;R*-&%deC#kW1jm?Z;gbP zUoGi^TtzGz3~Xzfvgl0#0zXT;7|xbMFD8_wZ}SxWP-g(u5+hai_v4a)mpr`pYW7W_ zLI+YcM)RC#^+QIGR5O@cGZn*muf}hn`*m~NPrO2Nj$RdLByR~^bSGJ2R2uaG@uLQt^g;n8Q1jKQ=0PYW2- zVR>hvv9ieK!Pqm@lgvV5k5~-nZwZ*etJrgrTzN*a8)7g16bJj4sHh(}qIni>XT|jv zstTSwcA`h?t9=yu)C6CMZocFraP2yXUV$u)In<;XKm{F!ZNv3r;9`RS5^FdIjzkpW z&PFq2YZUu@t{-6ggbMS6w)9P#(b${*882G8tntEFTC;R^?L{fG?#}%-ft_UPQn1}bQcEl z3NtJ*zkvEfG5v6W(xA3I8|lKc)lGJXk*eW<;hi*4vI!G_rcv8s4%Z`V2i{Os^}y!` z)q`JlRNtR)I!7)?No)avz?6=!sg*lX2{m5+t=P!TgzF<>Ti3Xp)nNou)CT;@sIkB{wv9t{t0DFVt`5<+{OwLJY3{0~^f1KlI)G z-bVI;=brSpSIVr_&q>4(8A^$Ca!%D@BJDwz3A#5$47dA{TzS`j>n9gL0aKI^^u9L` zPtu4J{@!H;;&N#gf>LdUg4RXHa1Y>`4zaczA+D4r(JrRpIP;dHQ3T5l1jrRwZpqTF zU+k|H|LH}Yo0rFkpbG4MUsl-eMY4Qs{q$J>y!$Yu=2ztcJ?noru=$nb{B?K?mFqur zp8dPmHcJM6SXCsfv_7F!N>#*xVX5wC=WyjH|zky|^gZ`fGV14u$Xcrb_z!)*`z6^N0l@uy86j(1r+#47}rKX@UyW~Ar7}5C!uyTKZuj8N?Lq3iE&4o z$5*@`=A(A>PwKTcX*6!AHSNo8E8|WdRUa8F_GF?7A%H%t^our9Wx1g0q}} z4+ijP`CjXOE1x3F(|S*f=x4+ew|Gb9WO_|hk-*G@h)rREI;bm~pkLk+0Z z({coVC}Wt=P@z(7X3si!f=Q0!Wfr7tG%w?Y70TC>!88=ew|W1fN~1LWufm%Df~^YB z*?*R`{XYh7EKa`q51FY&>xf0(Um+G@sT@>?v1onBi>FC#Uonk0Reyp#W4aL8hJ$gheQ`b z)}0FN8){L~{0h;V*SlvoC1Aty@LPed}?VU2Jz@~%s1*HF1On>NQ_#bDu|IZwi ziJxFKX4kQP98_L1>l}VPgeF6=L1gdW7aOnu272%}IMiAk4rH3J>+jI6Z$0q(E4J(B zZ%-b>ox+;X!}xgKXsTo9rTW;(lW@Akt^)-~G|7WoBpI1EwY;WMy{}g}wO_+ddy?sGpX!}#B90j2QFXKhOI;?W^ddENzNrDRI z3^dgPQXgX1Pe@RFJizPbTOj)})1Y{999=4#cV+a7ibksk*9-sB1|TeHS?_fDD{u)w z{(lFx`V9m8FG6LgB?VdkYPI+ol>C)>zP&{4cMm%xJ|thVNm{v3D~EdWU)`BS`gC4H z>ySF70->KQ9Y%5S=0~y)ion0%3oua_LkX;4e52~fQ}hhZzn`Jelw z1hKc<_ox&)Q7LGLpf2AmdG)(B5DJPAzA+1g>=N4@P?^s}z#GcH=jNdDEg3TgsE6hL z-iy~EQBp?j(sz~$#M`xBxvwr`m=2rjG(ecDpU~{V!BpFQJxNYb5H%@IsGW;z5eES83vKGo7}4s>f|fV##cA>+tc6s zdh(`#si22a0BCq0(r`Gk^&{o?^sC~k4tQw&xFHi_HPrZM7OKxXeJn~;<5Ty@pT2Vz znZn15h6w8Z>(t;{0Ye6q?y)J`kZ#D>?pOxk({{5Ja107y%;(!}Fxe)`7nWyjfj7ZLP{dYyX!_8L>@NIv;E&0}r##wrwqrFI^;7-y5)q=Xc{ zI$_Q1&`2u;J7Kf|c(<`p=tkjr#mA)30mNQAB+!g8PTI(A(A>Cu7@^e(o$4Q#4?|yh zbff$^;O2+iGDiI!@KsfLUG9mgB}2{tJJJYCTO;jHPE{))>+$o?sbb_Tw z+9r-bldUj``0GH;SEwt(EY@vyx2hVT?-m=?=3mM+AN`A{Zk)h8&9dLXBa$DOtC=`l z&~6W7^6XPpP%HOw<&4rr%Yrwy5vHM+QV#>IBx@;(8V^88KCLyC4r+G=yv*LV=hI7K z)=>q9e5z9D+*NNiRApD#x(WoOoGQ`?=-6#UGk#uE`0P;VLO3MSd6vd9TQo>qqQ}}8 z`Y|3DEpfQ#!(6YT^gBPMP@{S3)gOdJ+c+#hPw>+Dbj~IuM}q)AH&_ct=ny@w;efw3 z5K_74bTk; z0|Ypm?{@?^MU_BL;&+XbjgFl2s9hV)sU8DM{j6Wv{sPqbYF1oE67@Q&UJr<;@;ZRgpWjgDz1U6|h?NI3 zI@p~@f*=2x(r;#Oo3CH-eYjU{>xFG)%UPL+Y~#X7tIEZhJdod`;w{~ zj*T7A>%~lKUH7{E4XD)>TXhB;be+F=z4P@F9`p(>tq#$6DS;OimlJAs!Q#R_Tb>IL zom}sWr15@8?ys7?S(CH?b_hx=YV5D5UsrSJx zk(7n;Zn`5C{($Tz;#2ze@3_?C&M0(%EBcOm4|5m3Y%)8lOJbq01Ix%Yp(N=VC|zYnb2O2mLtXfh*+M? z(M?tFAA7tAyAdMhR|2hw{R>-TFP>S2H0K?m}B8RvFK-^|F+{Iyrpjt zxm^spE$X8_Bi%3vTz29w?nlDOn;GJmX}q?q1BK;ueSAQ#EZ6sRYa?)^DlQ7(gz7nS zjk)f5BfabInEAo6=jRwTHS_qHB%AS#K2r6X>P$)&;l6|+{ zCcQPb;`l>214d6&ai=&vTj(!!8>gp2v*mZ3vVTUqDSlmN;daoji0pZwDn|x`S^tzkf#NRLl19X$?!V_PCvy z3G1~B73=M)i@0fZtJGh%?DM_bFR1P`hAL>D*p4fLM4J&f zi0HbO>bOW1*(VGVbat=qs5*$>7B1a_5kgqKCa>KVI%rZxmF4R2DIgr>yct6S=_C$n zSj)51Em>vNMUk#}-AQqf@%zDmEuq|AqCFT$7Dlu44V1f1{z!}V zGXoO%l9;WX!mxVeJ7#@dDW=Agm0`DO?SZb;ADAm%*rW*D+YY*d(Val{Jjpd5`orlGFhy=LG1NO9MB)(MWe6FD7bIhvM6nn34 zcfZ?76AolA+;*G6FxG*BMQTD;>^B#F=(zcL3K1cTa$Sg4k)gq-n25+TqZh1c%u~}- zknJJY!e{qc(@j|ZS`6F53nI&AkH_cqa4d}6v3g*N=od^{qQXH%n0qo%z$ zO_9wS%ULS|&%XDUmB5}ddq&U-iok({=-oo7j2zglT#Ov{q>c#x;%xk4WBC*CC&BoV z&Urt@_!{DY@u^J$8lo6pdhsqG9c?04>HYAqd$!X4W%43 z@4x)Lc^^Jru-PKA=17ROXJ4zG_~Q9AMJmKjG^&!hzG|#8Vvgh z?&PIkM|0!RKcU^&lQlsNa_W@TUNbMz$IB~%q{K-;+5z79*CGu?Bcj%jA6g=*rg|kP z)$M$?I}LTthv9->Ux&i>G2!3W2R_of4{8Cnwvk^MkXk;FWd&>-9_Ff~!!W#5U9ImH!)`2j&Z{5I1&K0E7 z6I+@pL~SmCB`7~Je9SBtW8}`!;P;s9Gv11sWoJn(SP2u>=g{HFU!;li#w=2>kYL~* ziI}V>-RU&^ri|dpvxqDZjV$Y@TqjajTh~V7Uavjk7}^q0A2_m;(9JX!RW$hf+!@pb z=%VcK6DsPU1ot-ms!Lf2A&JtiXIVKu+`51a`;i=To`A#G{LV%<)8>zpN^DR;?_Azv<)1xUsWpIQPI#J)VDI` zYf4PZc=gNN8t+d4bO~%pw72omNbz05;Fxv}5`ZA(^(THb<~zI_b8;Bz1~cWq+$u=! z=gE=LNU_s<1!ET>*y4F=%qnS{eqCf}#g56Iq~f3=#Z2sIMQ9Xa<1(445NIjP^Cz)EcHCmf!T_$XtW<3xR9 z?7o6l=YmDRRkWp60YR3z8bsBmj}GW{?*O9%CzgDfnl~J+bg_5e>B}6Ar?9iB%cSNL zt?Hl{o~wk%^kE*a4y}Hr)ewwo=+vTwC6oHOFG$;hGPdm|$vg*I>iV$$lxfbU5EX9G z*Vv)him%C4#O0P`Dt?G+6hKc)tA8d4RTWXQ*)Jo32(mpD&p$lfz74yW{$pzb zsG4mjwxrf2HM+P8ZgfrrN6iPKNlZA=Qwr*hXNN+bx#JIcCWF#%B`D8QW*wjxNn&rM zo|^-CX>}z(sTGPKxCpr>*ub&T)c#J#gf5b+TOl}#ib}dxiAa1&2cO;`hoTw%jqCvm z>^9kTkD1DgX#F;66c^_r65uJ%FwAlkpyU-H-d)6@s!jbp8)e}nv8_s%Ng+(Rys&5; z4gS6U$@n)_T;y?VmvdG$mpDEIVNNBs^hslwf)<%h?GCKdP{dK_GLSbR0z+5HhX7kT zuMe0Jk_*Zn+)YG5g?t?x=)@PA@u@x*7uHj4J`-wlMr_SOH^&Dm#*t$8)~}lg-FT*6 z3f)->S_%GEuWgVaihsKGeMTNGu9b_kP2{k(zYGDE8w}x_GR_tIvB6h^!Hf5XRQ5!f z>_v9SSsbE64q_w%KIblWp!4_B^e~h7dEvs<#ot--B`Ro0Mf!~g#l3Y018k&fQ|uo4 z_m-<`;ei51ilDE1yB6T^J5BPzpg8^Vl*1teLy7_%eWt84kVFiU<-CKy80-NgB>%n2btF|osQ=Z zKQn(BBM7e^(mHVsZK;9%$G$3PpN+r@YaV)RH6&ur*}im>4tNy4@q)^*PZJYTBAW4JT-Uv$uP{hCd(9+ zEx;}ORdlv(Q`9jbp17Y}05GYadjx^rAV!Zb#Uh5OfvbEgAmj1i-PTLU(F)3Uh}ckr zhqk7k=XR(;%^yD7%UzY zSr^SH*vz2c2y=@Wi$MUfYQ?p9EFU0|o?6hMa}&Bm-$sf;1iAq&oo=9+sV1dRkcfyt897lR#cq46T+yP5>sHb%YmxfjG}@`fOpkO^j?%- z+E7vRt&(&c?VNahCT|D;xmasxi^5Pjvv8vuk#6UpD+tsmNu&Uy5cSVM)I3bm5pbVA zHyXU}ll<)zDh?qWb)SjfAqV=87$`NL&qGU_gs_nZG@6Zq@Lv|vbR-o?+j@kT`P!HJ z?J)CR3h=7F01DSA5Dpm&;Lpf}0R!zfwy{{ku)82Pp!v;0hRfu$<*a39q$9wZ-6lcD z%|MjrK5Da)fIrqNuxx*wW@Ei(@95 z!iG`*lzvkGwg#=hid6Y00)w*6M)d_?g3)pF&=qNOOCUcJK+=AjzUxlfhF+WX^?>$K zhI-G;9$r%73Et&CZ&VEMO*FwPk7t<`WR3t;9sGC&aHCdhR=I7ZNQOgwG;Sp^RJR&| zG5YZ%&}f+t-S<_b(rM0ZmM%a}$wAX=(8w5{FG@RYObYD)Z_NQ=oqj|N1lpR;1s0IS zm>UI>B3k5z?`3Q5OGr}{HR(Y&Sxr*-d9ngL6%lp36<~Pw9*w&`xkJ+$8ei%dztt7V z_As0V`5GHEwai1iX$Bza9zcOEApFN`6LBCd9tNkWd_B}!;}Me;IS8|zW-q1qiX7tTh-?w+}L_Br>oIz#@BPZgnZY0^gGb|4z!=-y$u zlm9Z}%+a0(Re&uMQ)nRM!d-HHQzT2N_ws3)>*2+r$ByNM#5-0ied&a(d3=U+EfPZz zk*|O&OuyN`)S$$<>*th`hC)Y57{^zzLbaUjCzw{vZufzb^&Zcc1FE|+opprP#RLS~5#uq$E2UZxxjhog%> z#vSq6f4e>vzBABIrm(w!^3o&>m+B(rd8o1fjH3(tYnJ?3qs`>5Su4O(=QCBI5wqi7A2YzRmljf6CMaHnLz5Edq z;ym>*k(7b(jb!KQN>N}w!+GdNzxS4W{&6&tcS=_>tCLq=Xb^p$-TMj!o~r%X;z1C3 z^8PLIv}eRu?=EUS7Qr!b)N=)2)jejg&?M586cQO z-)6}wX9FdnRgbY~iuokth_z?e6}WC4Kbao#KABk|zq^~tZ0ls`mea}mtPXy$d2k&s zblK;;f}W!Qe}4Y{$QQw96wVxdAAh`BwaHmvw~+jfDVh4zH9N=U<(LJx*g^N0De3W% z#Y0t1`@}E5pSuN#%vlKO9OYMd%t7%=+T5R^{#z#F8Nlq4iWx2EV80^E1LsJx}OAO|Uj>r+;C}wnl`*>$@oc6^b^ z%2KLSdOK!b_*@lt~5J z0~braX+EnK3d)qp5&AwmvcGJh&g5LX&CR!tH%H$)?ta#x-k~s8VC?I-^#^FG)Xilw zTQS+CSs4+D^q0(m%bi@K%-~Q`&&#atw$A^z_Ho6pY$DT?aG_g@BWlj zV<`y*juwx#Igdfk`?LB{jQf`Q9a|d>@{PD>!^)OTOP?L?Hxp_R4l-3HsuN3xe*J3& z)s88P;&^(yhMI^jk;mrTIpfLp1v3#ht&$iPEq>Nim^P6Ku8chMIl%Lj6Ro*UVENNf zOZ|f;LpY+6wdjm}Ii|{_W2`&#!_NHh;LAR5$r*>NPs089(i;bHuFjVuZ9>MD`oz>{ zv_z`Nfvt&Foms?z$m@NQ71UN68YVlVzSmmcsjK?bu(H>-Vt%D8jr%}w)l0`~V|OA{ z7Y&P)rV92XS&8Y(_iL_*Bvty_hJIqx*g<$ju&uD-N2B@x&N7j@(#Mv=f`G zJaDkO6MP)H=<{@hzT9k$)I{I(pI&0Ih!SCEqYk(36Ixa4NYlw_9_`Ax=5<3MG>(P@ z{Vl(j{{_l$!4us_?2pgNTcoLp3m(-A&cYEf3+=sdE%|avWr+L@b+*POr;kn&60DB3 zmZOY9wG63Gwh%pyIQ7}mP@;C%KK!eMuKO-2=PFwZ19S0LB4E*$e=XWdgrLg}{>8Eu z-z2IX+}J+7N=v`tVpL?QaZUg3x3GQU8FD_+R*V8tJUR{DngVTw_HP?Waf|!$#K-1Xy-md%>KU=8=cA@{HTnMFC8{a((sUVHW@^HjwAd@^^J*t5>W|$R=2I4t zytF%RwfEF#W>p?KoGyF$+$NdPa6y@NBasvt>()ZyJ10qSd~9*`iA-9ABD!gQo<-l= z3IiIO_;75(=ZV5Is5b>Vxy;f@;-i=VT67#hM$;t)nq zMZS{z%3&dEThOr;qsNuL7PsY`X=H2c>2E%Tr`g6R=~ezgsM(VRXUSFGKqfP=5AUA# z%H$NW+8y$i9lnM|eA=7PzS6NheiJ~FJYsMsm4qeztcs`3587QV167i0+*iu6H#~ZR zh?$l5!lhqZSgy`sML@Jd^I_>o+3PKLs~jlmGjg<^qD`y!j=MmJ52##s| zn<(pE4iee&D1xM#C?`+XZ`z1Ev=ftK7H5w((vU6#w{Mo02N8&?_kV zUUrRR&T51Gt7ezyfoIIx!qzB6SjsxBYW>4Wt>eR}q)XzWsoI^9y(DBeWr<0#eWN1e zl8@sdywaC?DGqj;O?WR@h0*=dMz>a02}}K(x=+rKIK*JiyNj=s?u=-Rw1rl~LL_Jnp1Q+-b8ED$=V#wELXv4!&vD`vnw^JswTK@m zt~W?=XFUB`k~}`XqA8m>>(?m|#;<=imhO+A-S!`>Ss-OFXa29v2p4Q8z%6o>&s}%- zr`UCOnCT(@!m~4Fw^J@fZ$9#oYKRSUH`_SkOh~48V=veGmoFDg=2cIt0k1Z4h>FRx z{5-Dp%CkyB1|llY@=WljHx#qw=T4#vnZ>fC`_w^cjlgMSzXSK0Bo9L7*b?LNJ;Gq2 zTI(y^`r7x}Aq$(1U0aA9wp*LE%Zs9-3&qZ6=ABbJa5ofS#pH?h^loY4lI0V?3@xPc z$!qtH;|GNHlk`(=yk6cR@vOVN!Kw>nz{ z-F#Ex`NpX4BmqP}4F6=g;CDYCB<>wROqNR& zrLf0!K?5e+NaVT$WNV4f+{Ub~Rarm2BcVdf;-KN&zw|zgqO&#SbPHtO5fU)6E_XO7 zDhWCq#nx5<>j(!$l%!n9qz{j;(icP1U=m`-q(eTrunL)y`{HC+S(><>Q804IWYo>} zrnq_c4?G8wYVKYMSHpaen(=lbFS2zX6Sro4D>U-cx-BB-JNSO z4BV$duI$VdpDXmFH3PwH#R0~h4siN_1-j~j0BZJdw((8JLrsol z?lY<962o46;Tpc{vHY2%B@aNSBwSwa3QY=ZbTNQ8!oiyPSskB|xw0 zpNAr%5SMPBYASNzF1X|!L{xq%1TA!$5!CI8=sHu<<}27mLl{W8mDI$u2RX&f=I`=+ zCXz-@cr2dQ1=d#!aMOa&Tm*;OeN|ujo&i6Lb9`^tlztVLn`6kG}DzA=%ZR1gw-(=6WI#&UJ$F5zd z`83wGSJWtfr>Qcr&EU6N=a$b)iGFACqJiztnTt&@6XU z^*#%K?qGwu%*wR&>v_W>?M+lWwh+3X-FiLVky0(JG9{T8-Cyw^ws&vU4dqOGnn*JT zK*v{nM1<&t&`oc%XnRO=E|ej?=&G~?T^d5uzMdIN=3I6SNkX%iCydm>SVx2-S_w~Y zyx>LGxQA~Z)ek)QRBMfRaKoUA`_`^5=R)xO^a+BNHkq80=l0!R*m31fFDub~k{>JO zCn(mxWB$>0B{DKE*?wGP?f56N8iVGGAH0aaKE4QnG?^Fo)Jd{Ui(s zBArU}a)I|zF=0YY)myMTeUmZ?R%_c@gZG?n_GK^#pX#R6St^JhH7?r)0O7JUbAUEw zyU+Qg)`Lt=8qcYSSlG+=&Xf8MPa3mu2O-}cUdfxlLzuf4q9I*fW-0u@kU9^Om-|=w zeHV1k#{UzP&KeKWwQil^qyUF8tMaTJr%R5moYif}~WrwuMJ$ zVuv7xQ=T%ae-`oV+SJ!X%u@W+pK=>cJ#u$SQsq+Ae}_Ee6Tx*4`D(eDK4}k`4hNy; zW#LMmI0&cSjE9r4H{K@5X}ry<6?WWn;YSO@pYI2Ao%&B2c^e^8csM~$d9|mk9Z~Jg z{%_^$u_}wOxFC4m<{(&+Y8~eFEBp~HiJF>c*_t*Z)e!n(J(i6i8^x4=3bK75iXtE+ z$yEo-EW)jBPKM7MSljlN#LF2Ax!P18fF>v*i7Ww;JFF8H^rC3Qi0SdY7~BDA4S2$z z0hHl+qUDvUKR+UtD*Z(`{Tdg(`Ro>)Gp(WY@>$}Io5U0QW-2wU%~n`iOk|R)eUQ-X z(d1^?CDq;KuJZy^|KPm+jM87@=9lh<5PH4Gg;SQbd95283M5gSE*nRLrC$)HE23AT z@{}Ecw@O$2aLyBbW@_B?HdS&M7C@Hl9%hCQ!1{bp-Z%v)CT2+5M>c%S^Yf;Gz1vXc zhvh`v+~xfqXG{qPU|MZrVoRR(4cAP5N6WgE7Z_TspiI`)bx)L_7 z(vS+Xx|8G9577EHWwA5Z18|wBRkqkReC3%7zH|y=yC~30Sa7Hy(z9Gx- zrMJmi7esW;Jkk^X4{KIk^V-g;#)1rc3u;Z~9Yiq%!$B(x`O!eVpKeI(Q~dWm8~sZR_g%F{6XCV#uSi;xAi|2J{&1| z#Zng}7pDS4-QLfY&LzT_Fwu?!5yO9-4-B)6J1LiT0L}U#MSsux*?b3(dH-s_a~@{vAAfUh(Et^!tLAX;rLsE)gESBPaGTww%)yP2y+?Ct|%r5JQ0#1 zydutC?ke`EqdzY^W7Ua{Ivr(^0iGg9XlWxd?A%85hJk{v25W6q1cosc7|fUod&0{q zpL^~?U1Z2zl1QCy>zuk~6!yE%zhX^75&Z&_RLp_dVbf7m$Pu&Y&UK~id0qtV3C()_ zi|x~iLT-E3B=cKLm8?MM;5Z_8OE`qu!A0%%enPh9LedC|mm&|lIErkVfwJdUX3-1v zY7jAYBqUh@9H=5{R{M6Y460zw4IZmg)*nXaYE8NaAtY-kfr7>uZT3QEm2~J?Z6jX? zh#tO1j)i}<^H}P6*ss=6P_{B=T?En?A&Lq?o%a6s_HI^E(xkn50UBQFJR&HEF z00H<~SikkXeb~;lYHZY4nXFE`OT9ko{dAH^v4o@8I1K7ICgk*zycy*?=pZhu1+#2k zZoDZSY+UD87cI3vDiVd?7S+Lff+919Tb>LW-!_Y{b?5{2!G*#{5p+^2=2AzD8yvdo zYI8edJy41kX5ehGdiQWy-h7o>2g*OM7MCuzJ^PC1>WR)Ctf1eI2i)G^ed5lg7au9? zj+#lNKSQy*1yt8)-w2OjLM z4~;ms)LH)#1!{?yA36l2G9oY<+bQ3TdD$GJGQT^45#- z(So8PW>0GsA|eizhXA~of9BQ$A4=~l{C-6M%soL+y&^=bosh~{dBnYP>i-WB(ieTzv zf#h#t#7=v5TE=+PnKOQ-Yf5i+L}sN$#+1`UN*4k05~60F?-%N?e(mFM39V5J8~Yv} zQ3TUGVowjuGtWvKDK22#-*YfW=3KAC%J-{fH89_{^5A`HKKpt~xNzI(i9 z%78X2>q5oooqJ|yY^rP_nXXo*9tV&i7sN5XvdBq^SbVKoEmD zkPzsLJ6C=bLE%*4MT%`HasFyy5iu0|dxd3auRwRt7Y5Z%{yykpaKw!(3OS#Y=DWaM zV?Z@Aa&Ap>99ObDAk?7rD#k*0S=7;|DR5QiAtQ&2S#=;sw{4qKi)lx9EKU-rlT-%67Ye0I( zc(lgI?rbk;VltG}H5n`$ZD}<4v5$JEi*1b~fsIyUOjq`W!rJ)=YAJ-)Kvz@LQsz>p zZRErk%#KmyM1U(oLF1Lp#L4HxPk;VAS~Kyffs^81q$(eeVZ);p&~0rf)^` z&o_GEUPhmLz1~j1b%>VeGwDUw0P)|0w9{qhy4N$N1(NWR=MC>2tB8rNQ)NcWNd=fo zIXfye!&(pQbafzFsek5vfrDLxoA8QR^=M26Tc!yLkRW)%@LLw)TRG7JE~>0%{I^}V zpE?K$MMq%Tq?ha%t~DYhns%k*7>c%8uwWtS-4<)>3pECdhHKwxr1Xpub{0}Wa(!E{ z*EgtcT>GV^e&LoqBt-Hkz?0unJr=+L5#Bq0EUY~JOvu>StR^Nai$OZP>(kmEO3t0W zvTU>(sBCfe?`&auQN--ox0rp#O70YeD>AHr0=&!Xc{Y1@vioNL%}pbZ`krSVJhR#W z(QjdyGyEu8V?J)cCp275pIFU~c3Ip#JbSOSSJN-^vH;+xmmkdR)fPX!0prrrei)be zb3h&B#`rlRL--U$`n`8zthyYwV)jH(0qA{8&P|BZihM-*L`TF^1npTa95L&ugJP&ch7 zJrk!Em_cJGUG$XsiVKyY^da6gu8rg(9M++bF5dF{St|b}5og+UYm-||d)W6#YD4MBiO=7%QLMRm{n}-rcw-SDREPCpsIXSYKp^c~B_?hY5o}vGAwKvcOT#M!~li^u@x|n<}czGZH zI2E*c6&tA3M$+}aoA{|ojz1jsB z6uXMb5&P?V26l=yW!JcMoRV5>j~8>>Cg-`Z!-uJ(75?|fX{NtnPVh4$di{|ww>NVd zF7LH#9@fR@fcJGZyDw#7YVZjPv+?_uJoHBAti!$`I3CbyM>K@E#d|Y(v$FK$eFrGJ zJb}FS@OaD^w0$=83^Umk?VW%pLYnJIxuAxa_%nS{Y45Nyn2L0Od1EG`X596TUq?&r zTzYyY1AAp4^Lv7*oE>v8HX;!e;7ZF~$ODiDaSg&LUP(p)RT1?qeaaqmR?|tL$R~ON5 zXTPbO@Vftq;*#++1^XsN99Ow3j_yXb99%uiCL0(eia?%Kj_L@CO3BQ>q@0|nfL!`e zRG>h9C&##XIt*G^I3>yrlmCtb$JR~42*o{RWiZe&5hk?ryM5l-|5i!~bQ0B8-9#^s zyN$c>xF*%8-E*^k@-AjP8=tzrWcc_shH85>RehnM?tsnnfzWIF>thxWYp(gMo=wP| zX1>(42Ob{qf7Fz9S2?C89ghmVs~mYBi)7q-hO;?udra;32r?Eb9?f1}f}w`8?^ALl z)B9npH}6^zm_`yq=?~up#{HuD#<75w@JAi$iTJ$f*G*fRiRL(vaR<9pH|vH65`Wk`8JOH^5z z`O@plwQcA@#aw1b6_czWVO0lxcQ@y@RD+PZPas?H;w?7$d4c!Q*u}01)Y8s_2ZkT2 z#nmN^z@c_~PxsqvNx*A$Evk1$qdR)ynDyW{3OgtHBJdnjIU2JI6L&oxUcL1lYE#xR zqYG(REy=n|^rTsUG$1$X=0&5|QazRmJZh@Q$DZ@|9Lc^A7iv;8s$}*JQP0LbK)CeF zg)D?Wy|``)*%Hi<@@7atg|IOY|2tT~SfP^x(g7JoeB;eJ>ZwhEV%<%0qMm23kJUy{ z=l1V^d<8%lHS;3tj@tK6=VuGb4zot+L$cX|j?J=K9oC{y;4U&vXx^t|dkY#MxaI4EL@X+L2SJz8t zV!3q#pK^bfIz%)ibttqq`Nwjl2M$6!4 ziOw|E+%InHr~SOmkL2PZ69vQPHq`Lf93~0=)DR0R;T*ktHo{={uCq}7I(vkDR;?F2 z-ePX+Db++&UM_1jez9=s%IVL1%yK!F==rhEAn11P(Z}|xB7#1D%2lR80sJbxKq`S2>U8~SSNSg^@kN}3LA4bD5XleuR@+xAkD3?T6N62grkr0T_P@RExaiWf~$1p4xij34dIzL_nWUT-($zD?Uka7Xni zpnYdJ#qVr`P<#hLw`Z7P(H30CKnuJpT{wc9Xc*3$1x6k7mMM{;~cxwco48vZt zb;f>el_a?c$djf#On=Ld3=e_lg3k4o?DR@aKVgTOr?!^*LuPvfftE-@PDoS&7kSz? zct`4Xf+T)#vU>64bNOut1}Bg0CnW^C-u7VvYSFHDk~0s(S#lYD#~cfZ&lBq36CX8M zSiMbFu0-aH=Q#PS?-EQk?Q#*R;(@bc;q%9{!;7UFALh>_R>+o4L)dMn# z(NL&nUu$7w-6?(U@(C(>BCLX@%>p+vM;a^%j_#qAMj4!t`hg%@8X{N~=JL<9vNiKe zCyiX-^v-q$9h627#`$}u`|s}jM2d><6B~bmDRv`{{8`3&W|f1e93 zamIa1s+kp z1_|GRjyPnv4{Lb(cH_?UwI1-S;_(cf5Rb2zg^eG};<=3B&Pu6iZ_00az*^7^#M{Q=AJk=|L@cDenkmm&zN_pKq>@X1AJwYJw*-F8844hQA@rkeV=)vx6K&uao=K zC6O;kgKbCDSrNj(o0(J?Cj9c7C&3i85rtt9NAd^#>&VbA3&uJGQK7>?uq&28x3a^J ztOr8!+^U$`36#P!Z8)xt6#gBy{h5{j!uDf`N8ky~f8e-~+k~PmuH_BWPva#ZB81%{ z3K04B07VaXiVVK@vmztPC>vFf|G6B5B)54Gm*_b1Gr%RfVXFh%PTsfojVTc`r4c(; z@V(wfNcferf~nj=QRC%v;K=4P4eCn|bF-tvKzK-+F!%N|YPx)70Fdq;j`)lf2`YZ` zs1HbyFEqUKFKj9pL=*9BPv|Yrlt6six!RnoUuau5%M#93OkDazxJ** z9O^8L&%_pqsV#&ks-dZIjbUUB<1#&B_2g2@nikn{>0)Q~sK#A)sdX7DWp-z!p&_}Y zSZlW|vs#&~#acyeh9a6&BKw{hm+V%L-EX^(`S}0vKl4BDdC&X%z3=Zm#~;kD&%CmT zEx&tRTs%f1Gw_%L`>I?JL@k>S9;%~-e)jX1+%s{KSo&ldv6O~K=8ra=hWVud^#Jd^ z0oBY#+h?TPQwZ&YI&5=s3;HaM>W_*OUP*G-Y%`Iv{+s@Vc`@D$_XH>s!d;}Fx2 zd~qm}1~tSCw~jA+SGFyqokaHK5^E zyri=SUqP4VG2nn=SNw(5+?l9e8NTK>QspaBwRaW&M@2H=C!QLh85% zMA_w?n^65Nik)x6lM;CxT?o&;6i|rGZp(UqwWUy9eXzy68|Ic&Ci=clLbQ z`UYiE4H9<(06Pnd)Z8VTr}^l(o>L>rumGthenRl%LYp~Bw)?K8sDLHt$e zd_+Suh_BK_#+_yXx6TfK+XN{GiNcdq(Vq}fXyNLDAbO@w>5nNGud)vOmTGWx&4tH9 zz0oFFI*D0>zP=3gaeXVu)>u3%WC$A&ONHy}dLAYZI&uy9lNXRw zF%N-nt_dsqiSB)%|HiQm>=8ByG=oox$VeXy!o^01Gcpg{z(r>hVvcPCfB4~D--|YM z_yYjfjjERBXsgkqxlCXXR~%p{D=&j?H&du-!F~5% zt@W=>o_pkroCh|~9(%Ceu9>5_rS+D$Mg7*Nwl26is2SA=nPA8)kcE*wHCLUB6DnQhU2!lEOyV4s~_$^WT8l zwg`Z#I^-F&4mwn-ne4dlnb$h{)MyYV+nVR774C<>(O3PDPF}S`4Whymq49W}xCFZX zqL}hFZmmlbXbh2@Mw=DBt3;z9Q5SXRC{^{tvRUyCrjb=s5&U$6=i-aA34X?`>Ycos zif*#1S3jgUdH%MdXE9I{-qcr}#-H+~g-@$4e>ueWwCtttl!@Aa}+2~>i zpfgwDEuhn|iydm>GuRi_f5f>`>~8&td69cKb|N~)0^pJKa3}?qky{Ppr3j&~Q1cK5 zqh!6_&c;nTKbiJ0tt$6QQq+ZwDlZ2ILa%384Qi#|Zi+RwD1+^yix+z5u?=h+nZxWQ z(j~IJtcrHe<1p{)R+|;-1guz-;Ih*L&4i&1BOn{15T|2XN8MBqINe$aC^4ZGq1?Q_ zB$y@QtHDdvtoiz=Vt>IvI=$m;ZNd!!`uvToMNYU9U2ZLlb5O?I0UqVOj3)QAIhZewRN_|ML}<{aD9eiCo)umCU396R^lFnmvbz z(xsVWMW?Z0K(gtT`o7ujp3p)92ibDz9UyM3&^2Q`DM6Vr-WWui@w|WUd1mzPfO}Q( zfqHwQ<)5$v>3a^kst+k)bxQP7F;6$<7Opjug|y=QWk*XYnNU&2`R>0O9DiGtCBRem zBIXXt>o?Ld*`OSs#Ldtx30vZ324mior-vzsJ;Y;7cMt1C$t_=lWjV@`Dlgq8Mad=4 zhfx=JsLxeEb^e7|l!asBwYc+$A9r@tml##mlPK-=1FqWyu-l}5u#EK?!W8RJz~Gn| zF*y$y+m;NOIV>KU-KF8Bbu=g)a##fzE)FTfljca@R?Mafgr(-gR{#wjXjg1a(a; zLd@iaRMqLc?bH1hh3gWIQQL3IQQI*rs(`wO2o%~&B}&Xecz6Jbt&`-Z%zQTF;Bstl zGt{O$eX=}CI&iCCS`#>9j<*va?54X2!8c#xV~^$KEND1|ozaWF8|$$f$B+_fO5sySveV5IWVtCNhrxk)3BTYf5S}7px_^IsJvMdWs8=%&@MvL`7=OAamAu0qrYmQLn6mpaj zvgJ@&thX%`VPyihz&4YOk&Tg=b@7R{gC%tFwh5_aV{6FF!L`^Em z7*tqH%HF|L(U<{16oXP*U6>74rZg-ZFwjt8V^I7lD{BWVRyi)%VA?Niu8S}-v0$)< zTY?b{Qk=51Ul^;n=iqfCOgdRbC<2TXkV0V*so*cM1*|S+4|)Vs!eU}vnU=U45K9AH zYm73%6!BG=;L?GYmy#|v;A=%$tfBS`^|v;M0l{K4iJh^DHB`}3S0B?vI{jnq-B(nza$sd{yd2Qjd%D2-00Q-aHYgi+Nd`nuK_ZMo2u%VY$`-GZ{TF^5T;7(!tt7CQi~(pH$8mT-hMxMO9F0A*lugz8#Z+W{2}o>@TcL=B9fin!6j z3BZFJ5TF2CfT^WxfN3BhFj3(W;83@8V<82n2*~&EfZ%B?maq_H#^G%l2ANM`)2zTC z?!}D*LX0V}Mw?-7EJNNhyja44AQwguyD$yAFh(lN|6`|N2j=Q81m!2|weYE^G z=H-=7mOov531qnv=Kxj4a3i>dE^Il{*F~W90+tmqLkCmqRbywP!t)#I;@hg{n|hOd zQPNn@;-I(!vx_N~LzWYV?TrIjX0|nuRYK@mSYfoy;*(|A{ZDId7_A8mpA`uFk<}*4 zf)TUI2>efN{6Q^A_j@D%V53w814*fe2%ZV`xFskW0GyTScEHl<( zS>_-q=-SBwJ_qM0uhO{xP4*jRvS-Di?qA>|;i#}>I9y|A{^ui!(EP0~{Q9bML1X-B zll_v#`W@BDw()c)D^}_MhuIB8JN+$~U55~5#)4tFV~NNf`eBk6@J5fCR5hFTa1VIn9XjUEiH zZw7AR2*pLx6S|C;{7N=0qbr564CP|-l`k(=4U+}PA6#HtwVVO}b8&!x!T@7zyRx|T z#QIzG;#UUg9ER!x{f?#G*;mp>EG$?S23L?3+ySze@h*?AfuM^Z(196)@ali{0?sn~aGztZM9PQQ>NX z>)Az8^mmAAp@Ja(i3^>4yO!A03!#wjrTmQ%leJ{^f8GE_$8Ge02|H%wV3fkYz#Cu% z?Z7nUKg(*vTUHC|;@hg{t4d$eYD-fd+sa8}$)){{H?Wirr*HrHP&4c4|1i7Zg3#+* zfqyb)apAk~V0Hs~1K5R0%hc?0Z$RD#ig5Um{eeXzY|%wn7YVpA`+djZ4?+g4?qg=g zDu}4NNJiIM-xzoT3N|pXx@f^EShn(HSwvuCGE(22fpDYrubYAXk?DpF z=ZxYs=npmBu(AIg)6G9UK`>)ipD(liFry3%HZdCu{r}d-ZtV53OG^&^1#}N?!diyJ zZ>f4LYYmwH{}#xuT%G-AuHRnQVOrD~e}wK~TuJyWv6kO4RyJPrmu+n*?TqfiCYuGD z1+%t(!D{&ylEG#VYp@pqCWH;A>A|h7;pXdy&%O~31N(3g4ywS<#cT*zxKLYuA%Y1n zEj<#!C=7OxaIs=*f@t42S+JAi_f3|C3Fdcz?N@T8$~b^JPjviN1_5$AMEfuZiU&6xOhhv4mP}e!^r+Ec!gUoY+5qVwK87VvxM35 z0;0j#-Fc_M)whsrZ(_X!Wbiiz#|s&NzrVs5bAdrI*t_tnFkUn`K_bwebyXQ|()saW zjI&lTT>@Im&b11}KWr?L1-pfHnOz_QwXy=+D8Y^=?4GLs5wxzi!~r`2eOF;GZi3se zdKfnlvG8TZt)#)};2#&hYjvo zS#YEq4yC`F;P|I41(vlu@i_!sALv<(iJ%L1@t+crRs1)j7R!SyHw3V%^+!i73(ge4 z=~j+arooSk+O<^i3apAQm!1p?_Zl@f)c9(G8W=sO9?8jP^g@ zYFNneSB^o&mG<>C+2!--!c*KSwksdt?tVQoRoP5lQI*wHLS0tOz*f}!9J4+wnYgaKsiBpbs4R?O?b0HgyZ5IZe~UZb z=fBf$aXYQQ?n%VT6X!8uFPE)QTk_+UpZ0)rv~X7M-?}AnT^OxHwSDt!rw+_FRuYkPFmz_|BeY_C=_?HL}XGZ>j z0$kT^`JWRYGc#7-{D2Y!bOwgeST+PD;BYgmf635V?=o<@Q_`4l3cqpN^3o>eWgn$l*x9_CFabO>lo>G1oFLaVJTn&uC#&vP zp81=r)7IrpaK`=e2=Dp{7sBIzY;wgdaawLG;qLuCt2dX(LM*+&fYVGkOYJXMv4-O( zaqK*nuEdr2BP{G$7ZJGaam&1`jc>y*eVlChRD=V;a$Igen{Vw@wEi(tIBoVr@U{yi z%O6uKOQib-m*3*jt`&5|N?QW4a9+`dAOU}`SpIbe{Mw-NN||9+%Pg*w0Vc)2R)9DS zxm;`hAO`%}i1JDS>Va=ETOt-eT=1`zA2ZGf`SApZ`)b58GvmJ{0W3>)49=>=Mf#Ts zc->_^8*oX$I$v?wc!uO@oVvqy_1l8Y$%->@WcTOOAQaOI@7 zaVE0gEtY?qCArQ{;r|ht{kX>CI-6MkM`ZRR8;t8&{HqAxe>}Fl+MM{!vE?Nj1IHi! z^DCWL*2W_Ll}ij4aP=1$7#mTfYl9{mD7?(_dnNI2vngMmHM9~g zRQBft{(qpj|Kb1!-$~X*7=c4v7Y7AY2^g>ueoc zzK!S)MmSd!@JrXr6y>@EJPU4z3l10myaUI&*4kgADL0(QyyQVI>+mH{YPqIan!+|v zhyNRwax9|gkIfW6i~zr9w{IW>!^XZPJyHRz*_8ezqYfq_*xZ{ zaE0r(_!j0{6JI~lvv7qm?J-Fgt)p+Qw#IHAUGy`+Yv7f2{VV1LfBm;OVhc9I@6hAF z*~7xD5fY>Mb_v)MsLnvar@_9L(%gO_+1E3t*6jc`?73_Ip#^M7fZHJSF=OOc(Slu$ z_+7VsF(&hE31Q?7(7f^ljE#;KYzO=^X!)v7FxmyDX}&56+?TwT)X9Qw`KnG9bP`V0 ze0}Y&(k)oO{kvfKw`vxwYW)u@zd^HLDS+={<@eId8(pug<+#56nV^;S%6E9K-@Bjd zucffqa6SOWtN}C5I&Q*}6}sF_z&TQXrJL}lo`Q)rT7VS5+$u_QN{I4pWx%FQl$*|q z2r1cX547*NTUkF}GLn^?1U=b2>C$QEIp8n(K;f$KK6y3CbO^CGwNsEmS-~*REgGrj zBWJyBp>gx?t)@Gt+7GbWT?xq<%Gd50`OJ4}GVksfZ|6s&4*nkJeP3pV3+Jz0*@U;9 z^5V)LOGN@|O6uH%o`@$$S3LrkvKRfX{J^{i3BW@_F0uS+7#tf2FeV z)b9?g78^zw=W`CMTlB>p4{7&SZ(a+Li<_u^rLut{yj#{F3N(bC^ZmL-pW2M3e#f<5 zr6_-;viJRe1!rA`>lb~jC|rd1r}Zl3@hg>8`W2i#Y`d{8#EZ5z1zhdH!g>X&{u>m_ z`~9CE6x+qzdm`MlDO^8C_B-C=ws~Yf^6vbhZ7yOfg}0s3{eB|#K|aSvEt%$IFH{~# z>>X^&QS>IJ$-QR%>cLsl+9=W60wOw(?d1b#;+S8Sn8rDD+fEs9gK`#BBx* z4oLhtAvwN@+cSZr8AY6(&PT(7ZTcHN)r8$fPduT%@pR&3*TlM<&s? zPbfb9MX2cUXm_QP45LwXI0O_WDq$@HJ9J~EW+2+)et(ZQ%$f9@>YaUQQ#-v~KfnE{ z9;qVdgdXq?iU+Scj#Y9-^9>pDo_|y1-q(_WWYj4bu=>np)%|+3t0M5EN=n$R6XneN zh(}JZ(zILs1#7%S4zL=&escLIEqp(_F%sf-t7<-Vl;KStUfri2xsx4EkSLKIG*Us* zovt&mOdo%;y$={ZPewN3;Iy2Fz)80tN0gHDS>n`SUJ*D%>n>M`Wx%a+y zB3Rk$O!L;;bdW^LGkr~I(N}gIAVy~}6@F2OkqX|^ty^N!dGv}%=6KqL_b-@pf2yd| zd;VN#IdXvpYosK4L?xfBp3z1e>oz~zIB2I|FJjkjqta9pok*>o@0gLOl6u&-tu|V` z{jkX1Rv^s}J8tj%`S=^Un~z&5+`dE-@_!aY8CBaljWit=w)0s2U*ns4%MD{I-bVzN5>w^BY4a7-rDvP7-;mU+=*le%NGqZLey#_5!0!bGTXi=*N`N_n<} zZNZ7k$$g1|+V*epn>97Mg4J3Y`OKeP*`27A@Cq`UUeMI0-Vbn{O?i@W#_HoMnuseS zgO0bZe|&4G@=m27_5Bw@qSz%k;;%iIEZt zuM^@V#LZ$AnUU#n6CdluQ}fX+?3GpSTYdtqFjp)3S*^mK<~jlW5ElLP)Mh^A8o3frcZU*sLrev zS?X$>$#*O1$P2{_doK_kMjjefQdRBp%isMn(U4vu{hH6-qhiO;T-jwo-@rdJz}igM z8Ez=#2cd0lO-@of|LU=W)ua=$5fz#Rmz%j-t?Jhr3p;=X6q#m>hyCZ ziTd0&eGeu(-8!GN^f&O;6BTfzWJ0yTjlwxFP?5(zG-$wWDxzRz!H`LNl`YLrBHZS3 zLfK5-RiWM#Cwf0V=?ZfQU?@p56gn432WcD5pY%%;Xq`>oSw7Tbl1Wi{m&>Y+f?-oS zWoaFZ)tn5)PGB(LHTj_|AWz3UBn(okirSsSSJ607tvoVOLwZ6v zw#l(8KG#o{%cQP+x??5+BJPkCC3^h|zWLtC+v%1_i}u_hOJw?}5__|=lS)F9wgi}0 zZbK{_TB`2xK++A$9tvpG2Tz)HUne;k3r2!Y*Iv-S&|fI1Zb2H=puw}ImBPXrd%8_} zs_t1u`q6$EAIO4+-Ovy;tqfOlvQ3r==T-9+n9H7-tzasoC%8Y5Gc!BcsdcB1{76V7 zeg@kLQ)`p^o1eE&`<2p>(U+$2T}o2hWI%GFK==qx6`eG!@DRaa-g=(BaW#nnV5)sD z9RJ{KK&~(70fxFX!$5K6kMG)(a?4W8lEr-w>lRM#fez$mw!L({GigD3&>?6_b(2ls z>k}%2x|Kor&3BTTS&$>|OY%CM=851!H)cOsG)}V3=yoPLC8>E59_~*=sj#g za4m_Y*cm1lA0Ik+)?jwRL#4!*f^h!;y+$TIy)?>9^$pbNLsKe;E0UOH{wmciJNCfI z{N(gfMInPov0z56wi0C%0}|ziq`u-gn9KFyIgu_zGMp22L9Jf>`4q~OquQ#M{8-C{ z%t`;7wRsLIyF-wJq6SsU35M0ao`R$-%~l5WddlpEjh{aR$UW_@*>|zUuoX;0_wybz zM2~OHtmWfz_E}|G*y9ON5FbcjU`mIIH6&eo-th?f*4wC%WSAoAsO*zYxB0p_gOZz?# zwRi-;XRf~EL|<3O5NUwnlK{_6%!6{$DId9B2p?!2Z|1n3IWO9lT6XCtq;gt}&0bDI z-ywcwaswwK(Ouq2*NRcjRWk>bnY$cX@0lBzTbY>y>n7#ki%`4sq@A0lJt(s`!1SLE~&}Wc?Oz)dZUUZB?rF3~ATm`~WSL+OgZZe!e@oB*d$;@`_ zJ7!&f42^!o0xi!bqB~Yeca#3&!)~n{eYgq=s&I>4qeXvnFne`a7X8noN?V${Yf5~` ztz6O#AuQpu`ceG>km{yzXfCF(LSDEaiY|&a-2Q3)ftr1Z&8)EMY)?xL z!os;}B*87?RrAG@6GX&Rw`w(h(G_^Ej)H1tK6rt0lQE#WmA|wum@XO<0DuL56;MY)+s15ezA3Pvy)K-@$$QmD*=4q@u!I&}vx+GS~?`VLJlj z)JSeY_NnE~qFbLO-h7uRP^(qKQH<_TIq`0U)IzNkn(RC^nxbAomqkB5!iGFG>iS+) zX>xpz_dxjGlLe2fQ7(48WNaG7#!k}BhAo-R8AeR^*i2q!D}JesD1GrWg%fe{1OL)+ zs1r|koSFcvEOxNop#Dt5z@Qz*0wj1vf9FwtMAD^Fj`>WeDS8_}@mK2fz(mKD=pddB;s}QIvD&Ljqgt5ZocG8eV8RvGZ>P_=ZzTv z?CPKOD>w#cOkUn$ws;TM@HebP9~m8BV23rGQn_VVNq2`A9(dj2@YJDIbJ!_-;bYV+ z+D-&PkQ=I~E+%Dmy~;d9Nyo)Z%eTUlHb}9w-GPIUfy|#P>R<;HXV8P~SMtNn)Mz~M z$vS`%tLBxm;q6h6@thSjm#P~uHvq=2HHDVcasA26S)HV2FkG{I2L|OcZCDE0K zdLBi;;FLqv6loj5NhnBPHIcQZWC-k~XiwJ2%H=F_2sQuNV)E+eJ}aQ|y?LEZG>YNVOrcR&NRsnY4}eMn~r%2O<}EXy+iy~~lVdBEwJutc8h zts|mOJNykh6({=Y30}^Z*PdK8IsLN3LrKEmXeNy=dZ*2BH#xByjSoI&hg9G|r40Hq zilH%`ZYR=q1or9B$WJB&`U9%fttk3k=`{SAnk5`tU3>1?X^M&g&r(zML&BY$Aiq+l zsesvsLpv@Gv}CF%Jnx=;!LGq?M7Fb4fh(^NpFZ60#id^e1og;>o2M$nIPISJ#WfQ% z66nFs2IKEQD>Dq|hYeV1C>z3i)4OMtIc87{B#-zW!0IdrT6j16<7<;TtL1@0d?vLI z_Fj0zXGO1iJzJ!TV%GaKd}J?iGv8y*-b{g3?Q@xrU(#KHby8J7Rwb!L54Jw*_-t6R zhk{=d_%WKh7R%=Y`)~lA8uHYlqCEDwUz!I`+$r#`f>(@!J#Ds3`7<+k{go~ z$^I{bY?=#B?G3{}Ns8B`a6dj2U!1f+Qw|KISct#sLwyKWEjxi>l6oX43deXl zg?V_YJCuxiRQ-$~bm6wiRJXLM>GEH`s7sQpK*h-#kYB3?eWC;UBz&exqx5A%hoPa6 zuQ`YL%s#dI#?|r7A`LJ(dSqrgX>);wvWXEyKitq=zO%q(%VZ&9eAg-kB8DeKLR@St za>!$)4YDu^TUO$Eh%`#S*s*FB0+)ilJBWxqJMOX^S=q&y?B9gqZ}(0Kxv_5*$iSt0 z%|1%%o%vi)k+oSCu@evxYq1`nUB!lAE+c6LC4P3aD!;l;mS&`YDmP@2U5CLGmb>V8 ze7yVi-c?2gCd)>8K?&5-)G3>BGlBQPM=7paJqU$21oXe;NqUI53&@Qx zc_de*HVa2(zy6>OdZkE)8nm?~KnDDbRrKN->|VMRaLSAemnl-94!KR2x~YYu?uXEE zU&G1nc?hWZU3ml*Mhe};psNR`j$wYF*0}x_GypQ?-M?_zNCUb+3npYmOeJ3vT z6ZS1*?^L?_G{GK6_NOvO;J>Q zFIJ@PrvYlNu}U&xK-#7W8W73XiTNqKu?L6etDwqPd+-+UmkUZtxz1oLdxUytXhKx< zQrqu>0*VjN?8nH}f)Vt2O*kC$qa)^p>p~Gw<>yxJm`h774E&2p*=Y9?<5w!x$}hFO zYU`zA)YDzTI@TrRcD>^t>m);1puZG-9(m7h@`{>4r{GKIIfXdcWIiYKaj77B`EWbH z$jIE|hWG-N%C3cRq3jwD18%c=tovn1G$4*30<+@**MN^EA1#(3=#DpZH><-@3Mj{h z;_LhCfs4MIoLdQypRaV>W@CVoH!Jg}$?Xc#7#~1Y_W|0C&@KXyk4RwFPj`pGpQh|j zI~pf@PvJT#!}v2of$Rdu79Vn+alk@gJjR0@FxA_$^V9vQS^@>reJ9&(8nqk&WmO+9 ze^e_6A&Q|uj>+DQuLx$!>v#S#QX+ZlNHO~X#3{V(TMGcf-c-xO93>=vCgZDGg3Kkw zi+-iz5(e&Pvn<*%oUU1D`yCGv;B2Y9ed?KETze#tNkO)F;>hu|b$u=uGg$GwH3TJI2sN{k1TOTV!)))aS%iQxH0pHvJ+-vtBk6 z)~rc#19DL9>KF(s<`I?91p>piruV5QL3!ZG;1@RzMBg~TnmYsN8+E|Yo&$_IBvI+a zJua&ko;0LGs=cEvW~rV|JL##X-(6=Ke{*?$wnpFmHsswQ9^03mLJhttT6wwE-IZZI z(|M-#bd~pb8X@FerMI5B*v`*R=8e%Sr8gi|m}WuY(_hA_+4W!CkT?@nI6tRBD|^>+ z9%V`?M^hVl#!D08=c$QSQwkJO)`e05N;LWQ=@B;pwvgavjf;WwYAhsxmUEfCXV-BH zLhdqJM(aMKC0GsAS?<<%tkjN=n*$u*6nI%rkL$F2Dt4S0XgnjAVT`m*jk?L4)U4_cB@^;IU2Wdz}EdUB2bCLROyMB2SWT8eQ3v7q+m#ozu_!1o9fb1{5L^EtCRh=Y9dcHaJr#*+x z0b==)O}}y=qZW6$U#Zp2iT*|}0&iZOVZc3C`yGGx3jj@&*I~y}hhx%rP7S;Vj9ZmU zd(dlk^A@%{1x&ps@wPt~ECN(f99dYFg%Z+8-MGK+o@6L}$j-pZXwF=8?K>!eCXqoU z4|U5!hkM%nbr2*eY&)4ZL_K`E>p{9f)jGgVF@;afFfZL((0i#Y%Qn$?E zIxBM?ktrL3v6)@=XQWgZFH~5c3`%}rH~qM0D9!f$!HI#^*+RG1#x24}U+igodDcp+ zFgn+5S8RVMVWj`kq>Y*6wmZ7}5qQ(br{%jQHK!-<*Kw}(q{)2T2dFMfovd;Sq-|Ov zIg&cPOC>=89$Qp|TMH0_h>!_kDTb6a5z`I(l_uUCVAVKmSot&#6@XvwyL7}TWus&Lxus&6%lU(oZbqL`spci!s+nP~ETATDI zUEbg1Y-==E3x?1)yNZw+h(gYO$dSg|6F2is#X&v2bAm@6o`Qk$Z=I8^+b#aDTmHScala44K`w(N?cr7}yVycSrbolQi4oeavo!lDcmFbK`#@)2 zuhlT0FFt})TBmPLH%cDm=$JW3V0vznACjAOW83(Fq`Z@z(N8G$yng8GWjveCAbIz= z{Ow6!UvYoG$;F7_N_Xr#Tktm0%3^z9kgv_YY)B*z>8m6X! z@{0n2qfmaA+`}&8@@mlQam`}o%8~k!HyCQu@4pCTU_vDxMOKaM@!q!QP_K_v1iwoY zJN=`y%E$=&cRTE6gBUb&t6}?$@@%Drk01rp`{Dqre-d!vLtk1l%<{U-GLH9*Qr3+jgKA4H?bYVSC=~dJWH3qN|ccom96NDEdE38`URiS9o%J z+09ZSPdR27?PrL{6Fb?MvOSBoHnngzk$In(X>ll&+xoK~U2tkr3R8ZpF#b+2GJjg? zmcqfSbR8OEC%cm&uQUSsh8#&U3~P^doeX^DDPq-K{i@9qHC|Q{kvlfeHRgK_a{T@l zp2E6M^3NW*)*ZCz|B`!+gz+%x4Z@6zwEn3YG_@fpquWQO+`^_?JH$g+q4KlpyX!yK zZfnUfiI~ZS@CWAPWpkvqrQ8f_6nAiI%{!4oMkTL#9l^I`CVj#OW>PxB8HrW{!5`I9 z-N&TDO>+Vuq-Cw^=}DF8<;#xT*7qvrIsrRu$DKI;(!`4c>dI#g>Um~oChZDa3b@g+ zYy=@ku85#*2yPlyhr84#sc+YB%qmbtIkn`2cu&&1vJPp(*+=;oc`G?CX9~U)K4BK& zf)2?Gnkb_SPSPt0g4^`gPz~H&h#~Q3@;+%vEu8Cnp%0N)4l$^pPinRapg#M~l>$&g zXw&q5HB>%#(xtvnZ=W%~Ot&mg88%Ti2?UYVhxvZ=+37USGw9jLErh!_Zxt5DNDM*@ z3;^Zg>p{B+yrofEC2>ko#%H_AZ_#Q>d|)tlugxxtX8(widt_3~K_eAPKcFusVGvn| zgj-)|lyq@2O;*j9-ZGP)I^fBg>t{URA)k^?D--%iw@00(uYAg^y;00(M#75p4tqLuD^7$X zS!2|*o8c~b*X*!_o6%hXRS%I1vgP$@Hcq)11#4!> z8YmZz0^xGS)-1T4kY~P21xIS8N?B{cnM{M1L1JnUKl7dDfuewZv$=^cr7UUy|Cml> zGS*#b(cIDsFC6UPHiR&v#h6;Xj>9`1r%0A@WY4|1OxWgP4nIN=pOjmQ7o9g;(ApC& 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 literal 0 HcmV?d00001 diff --git a/docs/blueprints.md b/docs/blueprints.md deleted file mode 100644 index 90462ec..0000000 --- a/docs/blueprints.md +++ /dev/null @@ -1,54 +0,0 @@ -# Blueprints - -[`Blueprint`][nserver.server.Blueprint]s provide a way for you to compose your application. They support most of the same functionality as a `NameServer`. - -Use cases: - -- Split up your application across different blueprints for maintainability / composability. -- Reuse a blueprint registered under different rules. -- Allow custom packages to define their own rules that you can add to your own server. - -Blueprints require `nserver>=2.0` - -## Using Blueprints - -```python -from nserver import Blueprint, NameServer, ZoneRule, ALL_CTYPES, A - -# First Blueprint -mysite = Blueprint("mysite") - -@mysite.rule("nicholashairs.com", ["A"]) -@mysite.rule("www.nicholashairs.com", ["A"]) -def nicholashairs_website(query: Query) -> A: - return A(query.name, "159.65.13.73") - -@mysite.rule(ZoneRule, "", ALL_CTYPES) -def nicholashairs_catchall(query: Query) -> None: - # Return empty response for all other queries - return None - -# Second Blueprint -en_blueprint = Blueprint("english-speaking-blueprint") - -@en_blueprint.rule("hello.{base_domain}", ["A"]) -def en_hello(query: Query) -> A: - return A(query.name, "1.1.1.1") - -# Register to NameServer -server = NameServer("server") -server.register_blueprint(mysite, ZoneRule, "nicholashairs.com", ALL_CTYPES) -server.register_blueprint(en_blueprint, ZoneRule, "au", ALL_CTYPES) -server.register_blueprint(en_blueprint, ZoneRule, "nz", ALL_CTYPES) -server.register_blueprint(en_blueprint, ZoneRule, "uk", ALL_CTYPES) -``` - -### Middleware, Hooks, and Error Handling - -Blueprints maintain their own `QueryMiddleware` stack which will run before any rule function is run. Included in this stack is the `HookMiddleware` and `ExceptionHandlerMiddleware`. - -## Key differences with `NameServer` - -- Does not use settings (`Setting`). -- Does not have a `Transport`. -- Does not have a `RawRecordMiddleware` stack. diff --git a/docs/changelog.md b/docs/changelog.md index 9f9f034..a374b55 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,47 @@ # Change Log +All notable changes to this project will be documented in this file. -## 2.0.0 +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [3.0.0](https://github.com/nhairs/nserver/compare/v2.0.0...dev) - UNRELEASED + +!!! tip + Version `3.0.0` represents a large incompatible refactor of `nserver` with version `2.0.0` considered a ["misfire"](https://github.com/nhairs/nserver/pull/4#issuecomment-2254354192). If you have been using functionality from `2.0.0` or the development branch you should expect a large number of breaking changes. + +### Added +- Add Python 3.13 support +- Generalised CLI interface for running applications; see `nserver --help`. + - Implemented in `nserver.cli`. +- `nserver.application` classes that focus on running a given server instance. + - This lays the ground work for different ways of running servers in the future; e.g. using threads. +- `nserver.server.RawNameServer` that handles `RawMiddleware` including exception handling. + +### Removed +- Drop Python 3.7 support +- `nserver.server.SubServer` has been removed. + - `NameServer` instances can now be registered to other `NameServer` instances. + +### Changed +- Refactored `nserver.server.NameServer` + - "Raw" functionality has been removed. This has been moved to the `nserver.server.RawNameServer`. + - "Transport" and other related "Application" functionality has been removed from `NameServer` instances. This has moved to the `nserver.application` classes. + - `NameServer` instances can now be registered to other instances. This replaces `SubServer` functionality that was in development. +- Refactoring of `nserver.server` and `nserver.middleware` classes. +- `NameServer` `name` argument / attribute is no longer used when creating the logger. + +### Fixed +- Uncaught errors from dropped connections in `nserver.transport.TCPv4Transport` [#6](https://github.com/nhairs/nserver/issues/6) + +### Development Changes +- Development tooling has moved to `uv`. + - The tooling remains wrapped in `dev.sh`. + - This remove the requirement for `docker` in local development. +- Test suite added to GitHub Actions. +- Added contributing guidelies. + +## [2.0.0](https://github.com/nhairs/nserver/compare/v1.0.0...v2.0.0) - 2023-12-20 - Implement [Middleware][middleware] - This includes adding error handling middleware that facilitates [error handling][error-handling]. @@ -10,6 +51,6 @@ - Add [Blueprints][blueprints] - Include refactoring `NameServer` into a new shared based `Scaffold` class. -## 1.0.0 +## [1.0.0](https://github.com/nhairs/nserver/commit/628db055848c6543641d514b4186f8d953b6af7d) - 2023-11-03 - Beta release diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..22c8de4 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,107 @@ +# Contributing + +Contributions are welcome! + +## Code of Conduct + +In general we follow the [Python Software Foundation Code of Conduct](https://policies.python.org/python.org/code-of-conduct/). Please note that we are not affiliated with the PSF. + +## Pull Request Process + +**0. Before you begin** + +If you're not familiar with contributing to open source software, [start by reading this guide](https://opensource.guide/how-to-contribute/). + +Be aware that anything you contribute will be licenced under [the project's licence](https://github.com/nhairs/nserver/blob/main/LICENSE). If you are making a change as a part of your job, be aware that your employer might own your work and you'll need their permission in order to licence the code. + +### 1. Find something to work on + +Where possible it's best to stick to established issues where discussion has already taken place. Contributions that haven't come from a discussed issue are less likely to be accepted. + +The following are things that can be worked on without an existing issue: + +- Updating documentation. This includes fixing in-code documentation / comments, and the overall docs. +- Small changes that don't change functionality such as refactoring or adding / updating tests. + +### 2. Fork the repository and make your changes + +We don't have styling documentation, so where possible try to match existing code. This includes the use of "headings" and "dividers" (this will make sense when you look at the code). + +Common devleopment tooling has been wrapped in `dev.sh` (which uses `uv` under the hood). + +Before creating your pull request you'll want to format your code and run the linters and tests: + +```shell +# Format +./dev.sh format + +# Lint +./dev.sh lint + +# Tests +./dev.sh test +``` + +If making changes to the documentation you can preview the changes locally using `./dev.sh docs`. Changes to the README can be previewed using [`grip`](https://github.com/joeyespo/grip) (not included in `dev` dependencies). + +!!! note + In general we will always squash merge pull requests so you do not need to worry about a "clean" commit history. + +### 3. Checklist + +Before pushing and creating your pull request, you should make sure you've done the following: + +- Updated any relevant tests. +- Formatted your code and run the linters and tests. +- Updated the version number in `pyproject.toml`. In general using a `.devN` suffix is acceptable. + This is not required for changes that do no affect the code such as documentation. +- Add details of the changes to the change log (`docs/changelog.md`), creating a new section if needed. +- Add notes for new / changed features in the relevant docstring. + +**4. Create your pull request** + +When creating your pull request be aware that the title and description will be used for the final commit so pay attention to them. + +Your pull request description should include the following: + +- Why the pull request is being made +- Summary of changes +- How the pull request was tested - especially if not covered by unit testing. + +Once you've submitted your pull request make sure that all CI jobs are passing. Pull requests with failing jobs will not be reviewed. + +### 5. Code review + +Your code will be reviewed by a maintainer. + +If you're not familiar with code review start by reading [this guide](https://google.github.io/eng-practices/review/). + +!!! tip "Remember you are not your work" + + You might be asked to explain or justify your choices. This is not a criticism of your value as a person! + + Often this is because there are multiple ways to solve the same problem and the reviewer would like to understand more about the way you solved. + +## Common Topics + +### Versioning and breaking compatability + +This project uses semantic versioning. + +In general backwards compatability is always preferred. + +Feature changes MUST be compatible with all [security supported versions of Python](https://endoflife.date/python) and SHOULD be compatible with all unsupported versions of Python where [recent downloads over the last 90 days exceeds 10% of all downloads](https://pypistats.org/packages/nserver). + +In general, only the latest `major.minor` version of NServer is supported. Bug fixes and feature backports requiring a version branch may be considered but must be discussed with the maintainers first. + +See also [Security Policy](security.md). + +### Spelling + +The original implementation of this project used Australian spelling so it will continue to use Australian spelling for all code. + +Documentation is more flexible and may use a variety of English spellings. + +### Contacting the Maintainers + +In general it is preferred to keep communication to GitHub, e.g. through comments on issues and pull requests. If you do need to contact the maintainers privately, please do so using the email addresses in the maintainers section of the `pyproject.toml`. diff --git a/docs/error-handling.md b/docs/error-handling.md index a27c2e5..a4c1d37 100644 --- a/docs/error-handling.md +++ b/docs/error-handling.md @@ -1,10 +1,8 @@ # Error Handling -Custom exception handling is handled through the [`ExceptionHandlerMiddleware`][nserver.middleware.ExceptionHandlerMiddleware] and [`RawRecordExceptionHandlerMiddleware`][nserver.middleware.RawRecordExceptionHandlerMiddleware] [Middleware][middleware]. These middleware will catch any `Exception`s raised by their respective middleware stacks. +Custom exception handling is handled through the [`QueryExceptionHandlerMiddleware`][nserver.middleware.QueryExceptionHandlerMiddleware] and [`RawExceptionHandlerMiddleware`][nserver.middleware.RawExceptionHandlerMiddleware] [Middleware][middleware]. These middleware will catch any `Exception`s raised by their respective middleware stacks. -Error handling requires `nserver>=2.0` - -In general you are probably able to use the `ExceptionHandlerMiddleware` as the `RawRecordExceptionHandlerMiddleware` is only needed to catch exceptions resulting from `RawRecordMiddleware` or broken exception handlers in the `ExceptionHandlerMiddleware`. If you only write `QueryMiddleware` and your `ExceptionHandlerMiddleware` handlers never raise exceptions then you'll be good to go with just the `ExceptionHandlerMiddleware`. +In general you are probably able to use the `QueryExceptionHandlerMiddleware` as the `RawExceptionHandlerMiddleware` is only needed to catch exceptions resulting from `RawMiddleware` or broken exception handlers in the `QueryExceptionHandlerMiddleware`. If you only write `QueryMiddleware` and your `QueryExceptionHandlerMiddleware` handlers never raise exceptions then you'll be good to go with just the `QueryExceptionHandlerMiddleware`. Both of these middleware have a default exception handler that will be used for anything not matching a registered handler. The default handler can be overwritten by registering a handler for the `Exception` class. @@ -15,6 +13,8 @@ Handlers are chosen by finding a handler for the most specific parent class of t ## Registering Exception Handlers +Exception handlers can be registered to `NameServer` and `RawNameSeerver` instances using either their `@exception_handler` decorators or their `register_exception_handler` methods. + ```python import dnslib from nserver import NameServer, Query, Response diff --git a/docs/index.md b/docs/index.md index 50c5ab4..ad02296 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,9 +14,6 @@ NServer has been built upon [dnslib](https://github.com/paulc/dnslib) however us NServer has been inspired by easy to use high level frameworks such as [Flask](https://github.com/pallets/flask) or [Requests](https://github.com/psf/requests). -!!! warning - NServer is currently Beta software and does not have complete documentation, testing, or implementation of certain features. - ## Features @@ -30,7 +27,7 @@ NServer has been inspired by easy to use high level frameworks such as [Flask](h Follow our [Quickstart Guide](quickstart.md). -```python title="TLDR" +```python title="tldr.py" from nserver import NameServer, Query, A server = NameServer("example") @@ -43,6 +40,9 @@ if __name__ == "__main__": server.run() ``` +```bash +nserver --server tldr.py:server +``` ## Bugs, Feature Requests etc Please [submit an issue on github](https://github.com/nhairs/nserver/issues). @@ -51,9 +51,6 @@ In the case of bug reports, please help us help you by following best practices In the case of feature requests, please provide background to the problem you are trying to solve so to help find a solution that makes the most sense for the library as well as your usecase. Before making a feature request consider looking at my (roughly written) [design notes](https://github.com/nhairs/nserver/blob/main/DESIGN_NOTES.md). -## Contributing -I am still working through open source licencing and contributing, so not taking PRs at this point in time. Instead raise and issue and I'll try get to it as soon a feasible. - ## Licence This project is licenced under the MIT Licence - see [`LICENCE`](https://github.com/nhairs/nserver/blob/main/LICENCE). diff --git a/docs/middleware.md b/docs/middleware.md index c54fc85..d5fb819 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -2,11 +2,9 @@ Middleware can be used to modify the behaviour of a server seperate to the individual rules that are registered to the server. Middleware is run on all requests and can modify both the input and response of a request. -Middleware requires `nserver>=2.0` - ## Middleware Stacks -Middleware operates in a stack with each middleware calling the middleware below it until one returns and the result is propagated back up the chain. NServer uses two stacks, the outmost stack deals with raw DNS records (`RawRecordMiddleware`), which will eventually convert the record to a `Query` which will then be passed to the main `QueryMiddleware` stack. +Middleware operates in a stack with each middleware calling the middleware below it until one returns and the result is propagated back up the chain. NServer uses two stacks, the outmost stack deals with raw DNS records (`RawMiddleware`), which will eventually convert the record to a `Query` which will then be passed to the main `QueryMiddleware` stack. Middleware can be added to the application until it is run. Once the server begins running the middleware cannot be modified. The ordering of middleware is kept in the order in which it is added to the server; that is the first middleware registered will be called before the second and so on. @@ -18,6 +16,8 @@ For most use cases you likely want to use [`QueryMiddleware`][nserver.middleware ### Registering `QueryMiddleware` +`QueryMiddleware` can be registered to `NameServer` instances using their `register_middleware` methods. + ```python from nserver import NameServer from nserver.middleware import QueryMiddleware @@ -38,7 +38,7 @@ from nserver import Query, Response class MyLoggingMiddleware(QueryMiddleware): def __init__(self, logging_name: str): super().__init__() - self.logger = logging.getLogger(f"my-awesome-app.{name}") + self.logger = logging.getLogger(f"my-awesome-app.{logging_name}") return def process_query( @@ -57,36 +57,39 @@ server.register_middleware(MyLoggingMiddleware("bar")) Once processed the `QueryMiddleware` stack will look as follows: -- [`ExceptionHandlerMiddleware`][nserver.middleware.ExceptionHandlerMiddleware] +- [`QueryExceptionHandlerMiddleware`][nserver.middleware.QueryExceptionHandlerMiddleware] - Customisable error handler for `Exception`s originating from within the stack. - `` - [`HookMiddleware`][nserver.middleware.HookMiddleware] - Runs hooks registered to the server. This can be considered a simplified version of middleware. -- [`RuleProcessor`][nserver.middleware.RuleProcessor] - - The entry point into our rule processing. -## `RawRecordMiddleware` +## `RawMiddleware` -[`RawRecordMiddleware`][nserver.middleware.RawRecordMiddleware] allows for modifying the raw `dnslib.DNSRecord`s that are recevied and sent by the server. +[`RawMiddleware`][nserver.middleware.RawMiddleware] allows for modifying the raw `dnslib.DNSRecord`s that are recevied and sent by the server. -### Registering `RawRecordMiddleware` +### Registering `RawMiddleware` + +`RawMiddleware` can be registered to `RawNameServer` instances using their `register_middleware` method. ```python # ... -from nserver.middleware import RawRecordMiddleware +from nserver import RawNameServer +from nserver.middleware import RawMiddleware + +raw_server = RawNameServer(server) -server.register_raw_middleware(RawRecordMiddleware()) +server.register_middleware(RawMiddleware()) ``` -### Creating your own `RawRecordMiddleware` +### Creating your own `RawMiddleware` -Using an unmodified `RawRecordMiddleware` isn't very interesting as it just passes the request onto the next middleware. To add your own middleware you should subclass `RawRecordMiddleware` and override the `process_record` method. +Using an unmodified `RawMiddleware` isn't very interesting as it just passes the request onto the next middleware. To add your own middleware you should subclass `RawMiddleware` and override the `process_record` method. ```python # ... -class SizeLimiterMiddleware(RawRecordMiddleware): +class SizeLimiterMiddleware(RawMiddleware): def __init__(self, max_size: int): super().__init__() self.max_size = max_size @@ -109,15 +112,13 @@ class SizeLimiterMiddleware(RawRecordMiddleware): return response -server.register_raw_middleware(SizeLimiterMiddleware(1400)) +server.register_middleware(SizeLimiterMiddleware(1400)) ``` -### Default `RawRecordMiddleware` stack +### Default `RawMiddleware` stack -Once processed the `RawRecordMiddleware` stack will look as follows: +Once processed the `RawMiddleware` stack will look as follows: -- [`RawRecordExceptionHandlerMiddleware`][nserver.middleware.RawRecordExceptionHandlerMiddleware] +- [`RawExceptionHandlerMiddleware`][nserver.middleware.RawExceptionHandlerMiddleware] - Customisable error handler for `Exception`s originating from within the stack. - `` -- [`QueryMiddlewareProcessor`][nserver.middleware.QueryMiddlewareProcessor] - - entry point into the `QueryMiddleware` stack. diff --git a/docs/quickstart.md b/docs/quickstart.md index 25d25fc..fa78f54 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -19,9 +19,6 @@ server = NameServer("example") @server.rule("example.com", ["A"]) def example_a_records(query: Query): return A(query.name, "1.2.3.4") - -if __name__ == "__main__": - server.run() ``` Here's what this code does: @@ -37,28 +34,25 @@ Here's what this code does: 4. When triggered our function will then return a single `A` record as a response. -5. Finally we add code so that we can run our server. - ### Running our server -With our server written we can now run it: +With our server written we can now run it using the `nserver` CLI: -```shell -python3 example_server.py +```bash +nserver --server path/to/minimal_server.py ``` - ```{.none .no-copy} -[INFO] Starting UDPv4Transport(address='localhost', port=9953) +[INFO] Starting UDPv4Transport(address='localhost', port=5300) ``` We can access it using `dig`. ```shell -dig -p 9953 @localhost A example.com +dig -p 5300 @localhost A example.com ``` ```{.none .no-copy} -; <<>> DiG 9.18.12-0ubuntu0.22.04.3-Ubuntu <<>> -p 9953 @localhost A example.com +; <<>> DiG 9.18.12-0ubuntu0.22.04.3-Ubuntu <<>> -p 5300 @localhost A example.com ; (1 server found) ;; global options: +cmd ;; Got answer: @@ -72,7 +66,7 @@ dig -p 9953 @localhost A example.com example.com. 300 IN A 1.2.3.4 ;; Query time: 324 msec -;; SERVER: 127.0.0.1#9953(localhost) (UDP) +;; SERVER: 127.0.0.1#5300(localhost) (UDP) ;; WHEN: Thu Nov 02 21:27:12 AEDT 2023 ;; MSG SIZE rcvd: 45 ``` diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..d2974bb --- /dev/null +++ b/docs/security.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + +Security support for Python JSON Logger is provided for all [security supported versions of Python](https://endoflife.date/python) and for unsupported versions of Python where [recent downloads over the last 90 days exceeds 10% of all downloads](https://pypistats.org/packages/nserver). + + +As of 2024-11-22 security support is provided for Python versions `3.8+`. + + +## Reporting a Vulnerability + +Please report vulnerabilties [using GitHub](https://github.com/nhairs/nserver/security/advisories/new). + diff --git a/docs/subserver-blueprint.md b/docs/subserver-blueprint.md new file mode 100644 index 0000000..2323222 --- /dev/null +++ b/docs/subserver-blueprint.md @@ -0,0 +1,87 @@ +# Sub-Servers and Blueprints + +## Sub-Servers + +To allow for composing an application into different parts, a [`NameServer`][nserver.server.NameServer] can be included in another `NameServer`. + +Use cases: + +- Split up your application across different servers for maintainability / composability. +- Reuse a server registered under different rules. +- Allow custom packages to define their own rules that you can add to your own server. + +### Using Sub-Servers + +```python +from nserver import NameServer, ZoneRule, ALL_CTYPES, A, TXT + +# First child NameServer +mysite = NameServer("mysite") + +@mysite.rule("nicholashairs.com", ["A"]) +@mysite.rule("www.nicholashairs.com", ["A"]) +def nicholashairs_website(query: Query) -> A: + return A(query.name, "159.65.13.73") + +@mysite.rule(ZoneRule, "", ALL_CTYPES) +def nicholashairs_catchall(query: Query) -> None: + # Return empty response for all other queries + return None + +# Second child NameServer +en_subserver = NameServer("english-speaking-blueprint") + +@en_subserver.rule("hello.{base_domain}", ["TXT"]) +def en_hello(query: Query) -> TXT: + return TXT(query.name, "Hello There!") + +# Register to main NameServer +server = NameServer("server") +server.register_subserver(mysite, ZoneRule, "nicholashairs.com", ALL_CTYPES) +server.register_subserver(en_subserver, ZoneRule, "au", ALL_CTYPES) +server.register_subserver(en_subserver, ZoneRule, "nz", ALL_CTYPES) +server.register_subserver(en_subserver, ZoneRule, "uk", ALL_CTYPES) +``` + +#### Middleware, Hooks, and Exception Handling + +Don't forget that each `NameServer` maintains it's own middleware stack, exception handlers, and hooks. + +In particular errors will not propagate up from a child server to it's parent as the child's exception handler will catch any exception and return a response. + +## Blueprints + +[`Blueprint`][nserver.server.Blueprint]s act as a container for rules. They are an efficient way to compose your application if you do not want or need to use functionality provided by a `QueryMiddleware` stack. + +### Using Blueprints + +```python +# ... +from nserver import Blueprint, MX + +no_email_blueprint = Blueprint("noemail") + +@no_email_blueprint.rule("{base_domain}", ["MX"]) +@no_email_blueprint.rule("**.{base_domain}", ["MX"]) +def no_email(query: Query) -> MX: + "Indicate that we do not have a mail exchange" + return MX(query.name, ".", 0) + + +## Add it to our sub-servers +en_subserver.register_rule(no_email_blueprint) + +# Problem! Because we have already registered the nicholashairs_catchall rule, +# it will prevent our blueprint from being called. So instead let's manually +# insert it as the first rule. +mysite.rules.insert(0, no_email_blueprint) +``` + +### Key differences with `NameServer` + +- Only provides the `@rule` decorator and `register_rule` method. + - It does not have a `QueryMiddleware` stack which means it does not support hooks or error-handling. +- Is used directly in `register_rule` (e.g. `some_server.register_rule(my_blueprint)`). +- If rule does not match an internal rule will continue to the next rule in the parent server. + + In comparison `NameServer` instances will return `NXDOMAIN` if a rule doesn't match their internal rules. diff --git a/lib/python/build.Dockerfile b/lib/python/build.Dockerfile deleted file mode 100644 index c25011a..0000000 --- a/lib/python/build.Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM python:3.7 - -ARG SOURCE_UID -ARG SOURCE_GID -ARG SOURCE_UID_GID - -RUN mkdir -p /code/src \ - && groupadd --gid ${SOURCE_GID} devuser \ - && useradd --uid ${SOURCE_GID} -g devuser --create-home --shel /bin/bash devuser \ - && chown -R ${SOURCE_UID_GID} /code \ - && su - devuser -c "pip install --user --upgrade pip" - -## ^^ copied from common.Dockerfile - try to keep in sync fo caching - -# Base stuff -ADD . /code - -RUN chown -R ${SOURCE_UID_GID} /code # needed twice because added files - -RUN ls -lah /code - -RUN su - devuser -c "cd /code && pip install --user build" - -CMD echo "docker-compose build python-build complete 🎉" diff --git a/lib/python/build.sh b/lib/python/build.sh index c4d7bcc..48c0405 100755 --- a/lib/python/build.sh +++ b/lib/python/build.sh @@ -47,22 +47,7 @@ replace_version_var BUILD_DATETIME "${BUILD_DATETIME}" 0 head -n 22 "src/${PACKAGE_PYTHON_NAME}/_version.py" | tail -n 7 -if [ "$PYTHON_PACKAGE_REPOSITORY" == "testpypi" ]; then - echo "MODIFYING PACKAGE_NAME" - # Replace name suitable for test.pypi.org - # https://packaging.python.org/tutorials/packaging-projects/#creating-setup-py - sed -i "s/^PACKAGE_NAME = .*/PACKAGE_NAME = \"${PACKAGE_NAME}-${TESTPYPI_USERNAME}\"/" setup.py - grep "^PACKAGE_NAME = " setup.py - - mv "src/${PACKAGE_PYTHON_NAME}" "src/${PACKAGE_PYTHON_NAME}_$(echo -n $TESTPYPI_USERNAME | tr '-' '_')" -fi - -if [[ "$GIT_BRANCH" != "master" && "$GIT_BRANCH" != "main" ]]; then - sed -i "s/^PACKAGE_VERSION = .*/PACKAGE_VERSION = \"${BUILD_VERSION}\"/" setup.py - grep "^PACKAGE_VERSION = " setup.py -fi - ## Build ## ----------------------------------------------------------------------------- -#python3 setup.py bdist_wheel -python3 -m build --wheel +uv build +git restore src/${PACKAGE_PYTHON_NAME}/_version.py diff --git a/lib/python/common.Dockerfile b/lib/python/common.Dockerfile deleted file mode 100644 index 668c098..0000000 --- a/lib/python/common.Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# syntax = docker/dockerfile:1.2 -FROM python:3.7 - - -ARG SOURCE_UID -ARG SOURCE_GID -ARG SOURCE_UID_GID - -RUN apt update && apt install -y \ - less - -RUN mkdir -p /code/src \ - && groupadd --gid ${SOURCE_GID} devuser \ - && useradd --uid ${SOURCE_GID} -g devuser --create-home --shell /bin/bash devuser \ - && chown -R ${SOURCE_UID_GID} /code \ - && su -l devuser -c "pip install --user --upgrade pip" - -ADD pyproject.toml /code -RUN chown -R ${SOURCE_UID_GID} /code # needed twice because added files - -RUN ls -lah /code /home /home/devuser /home/devuser/.cache /home/devuser/.cache/pip - -RUN --mount=type=cache,target=/home/devuser/.cache,uid=1000,gid=1000 \ - su -l devuser -c "cd /code && pip install --user -e .[dev,docs]" - -CMD echo "docker-compose build python-common complete 🎉" diff --git a/lib/python/install_pypy.sh b/lib/python/install_pypy.sh deleted file mode 100755 index c3547f0..0000000 --- a/lib/python/install_pypy.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -set -e - -PYPY_VERSION="7.3.9" -PYTHON_VERSIONS="3.7 3.8 3.9" - -# Note: pypy-7.3.9 is last version to support python3.7 - -if [ ! -d /tmp/pypy ]; then - mkdir /tmp/pypy -fi - -cd /tmp/pypy - -for PYTHON_VERSION in $PYTHON_VERSIONS; do - FULLNAME="pypy${PYTHON_VERSION}-v${PYPY_VERSION}-linux64" - FILENAME="${FULLNAME}.tar.bz2" - - if [ ! -f "${FILENAME}" ]; then - # not cached - fetch - echo "Fetching ${FILENAME}" - wget -q "https://downloads.python.org/pypy/${FILENAME}" - fi - - echo "Extracting ${FILENAME} to /opt/${FULLNAME}" - tar xf ${FILENAME} --directory=/opt - - echo "Removing temp file" - rm -f ${FILENAME} - - echo "sanity check" - ls /opt - - echo "Linking ${FULLNAME}/bin/pypy${PYTHON_VERSION} to /usr/bin" - ln -s "/opt/${FULLNAME}/bin/pypy${PYTHON_VERSION}" /usr/bin/ - - echo "" - -done diff --git a/lib/python/tox.Dockerfile b/lib/python/tox.Dockerfile deleted file mode 100644 index e8ad7c9..0000000 --- a/lib/python/tox.Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -FROM ubuntu:20.04 - -# We use deadsnakes ppa to install -# https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa -# -# As noted in the readme, 22.04 supports only 3.7+, so use 20.04 to support some older versions -# This also means we don't install 3.8 as it is already provided - -# TZ https://serverfault.com/a/1016972 -ARG DEBIAN_FRONTEND=noninteractive -ENV TZ=Etc/UTC - -RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ - --mount=target=/var/cache/apt,type=cache,sharing=locked \ - rm -f /etc/apt/apt.conf.d/docker-clean \ - && apt update \ - && apt upgrade --yes \ - && apt install --yes software-properties-common wget python3-pip\ - && add-apt-repository ppa:deadsnakes/ppa \ - && apt update --yes - -RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ - --mount=target=/var/cache/apt,type=cache,sharing=locked \ - apt install --yes \ - python3.6 python3.6-dev python3.6-distutils \ - python3.7 python3.7-dev python3.7-distutils \ - python3.9 python3.9-dev python3.9-distutils \ - python3.10 python3.10-dev python3.10-distutils \ - python3.11 python3.11-dev python3.11-distutils \ - python3.12 python3.12-dev python3.12-distutils - -## pypy -ADD lib/python/install_pypy.sh /tmp -RUN --mount=target=/tmp/pypy,type=cache,sharing=locked \ - /tmp/install_pypy.sh - - -ARG SOURCE_UID -ARG SOURCE_GID -ARG SOURCE_UID_GID - -RUN mkdir -p /code/dist /code/tests \ - && groupadd --gid ${SOURCE_GID} devuser \ - && useradd --uid ${SOURCE_GID} -g devuser --create-home --shell /bin/bash devuser \ - && chown -R ${SOURCE_UID_GID} /code \ - && su - devuser -c "pip install --user --upgrade pip" - -RUN su - devuser -c "pip install --user tox" - -CMD echo "docker-compose build python-tox complete 🎉" diff --git a/mkdocs.yml b/mkdocs.yml index fa88c1d..af68cf4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,12 +11,15 @@ watch: nav: - "Home": index.md - quickstart.md + - architecture.md - middleware.md - error-handling.md - - blueprints.md + - subserver-blueprint.md - production-deployment.md - - changelog.md - external-resources.md + - changelog.md + - security.md + - contributing.md - API Reference: - ... | reference/nserver/* @@ -84,10 +87,10 @@ plugins: python: paths: - src - #import: - # - https://docs.python.org/3/objects.inv - # - https://mkdocstrings.github.io/objects.inv - # - https://mkdocstrings.github.io/griffe/objects.inv + import: + - https://docs.python.org/3/objects.inv + - https://mkdocstrings.github.io/objects.inv + - https://mkdocstrings.github.io/griffe/objects.inv options: filters: - "!^_" diff --git a/pylintrc b/pylintrc index 776bfe6..2dd7180 100644 --- a/pylintrc +++ b/pylintrc @@ -456,7 +456,7 @@ preferred-modules= # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, - setUp, + setup, __post_init__ # List of member names, which should be excluded from the protected access @@ -479,6 +479,9 @@ valid-metaclass-classmethod-first-arg=cls # Maximum number of arguments for function / method. max-args=10 +# Max number of positional arguments for a function / method +max-positional-arguments=8 + # Maximum number of attributes for a class (see R0902). max-attributes=15 diff --git a/pyproject.toml b/pyproject.toml index 7c9a83b..6dd432d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,17 +4,19 @@ build-backend = "setuptools.build_meta" [project] name = "nserver" -version = "2.0.0" +version = "3.0.0.dev1" description = "DNS Name Server Framework" authors = [ {name = "Nicholas Hairs", email = "info+nserver@nicholashairs.com"}, ] # Dependency Information -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = [ "dnslib", + "pillar~=0.3", "tldextract", + "typing-extensions;python_version<'3.10'", ] # Extra information @@ -23,12 +25,12 @@ license = {text = "MIT"} classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: MIT License", "Typing :: Typed", "Topic :: Internet", @@ -36,17 +38,13 @@ classifiers = [ ] [project.urls] -homepage = "https://nhairs.github.io/nserver/latest/" -github = "https://github.com/nhairs/nserver" +HomePage = "https://nhairs.github.io/nserver" +GitHub = "https://github.com/nhairs/nserver" [project.optional-dependencies] -build = [ - "setuptools", - "wheel", -] - dev = [ - ### dev.sh dependencies + "tox", + "tox-uv", ## Formatting / Linting "validate-pyproject[all]", "black", @@ -54,11 +52,10 @@ dev = [ "mypy", ## Testing "pytest", - ## REPL - "bpython", -] - -docs = [ + ## Build + "setuptools", + "wheel", + ## Docs "black", "mkdocs", "mkdocs-material>=8.5", @@ -70,5 +67,11 @@ docs = [ "mike", ] +[project.scripts] +nserver = "nserver.__main__:main" + [tool.setuptools.package-data] nserver = ["py.typed"] + +[tool.black] +line-length = 100 diff --git a/src/nserver/__init__.py b/src/nserver/__init__.py index 54e8c75..dde0054 100644 --- a/src/nserver/__init__.py +++ b/src/nserver/__init__.py @@ -1,5 +1,6 @@ +### IMPORTS +### ============================================================================ from .models import Query, Response from .rules import ALL_QTYPES, StaticRule, ZoneRule, RegexRule, WildcardStringRule from .records import A, AAAA, NS, CNAME, PTR, SOA, MX, TXT, CAA -from .server import NameServer, Blueprint -from .settings import Settings +from .server import NameServer, RawNameServer, Blueprint diff --git a/src/nserver/__main__.py b/src/nserver/__main__.py new file mode 100644 index 0000000..4039844 --- /dev/null +++ b/src/nserver/__main__.py @@ -0,0 +1,18 @@ +### IMPORTS +### ============================================================================ +from .cli import CliApplication + + +### FUNCTIONS +### ============================================================================ +def main(): + "CLI Entrypoint" + app = CliApplication() + app.run() + return app + + +### MAIN +### ============================================================================ +if __name__ == "__main__": + main() diff --git a/src/nserver/_version.py b/src/nserver/_version.py index 24869af..722c41c 100644 --- a/src/nserver/_version.py +++ b/src/nserver/_version.py @@ -1,4 +1,5 @@ """Version information for this package.""" + ### IMPORTS ### ============================================================================ ## Standard Library diff --git a/src/nserver/application.py b/src/nserver/application.py new file mode 100644 index 0000000..195bc32 --- /dev/null +++ b/src/nserver/application.py @@ -0,0 +1,109 @@ +### IMPORTS +### ============================================================================ +## Future +from __future__ import annotations + +## Standard Library + +## Installed +from pillar.logging import LoggingMixin + +## Application +from .exceptions import InvalidMessageError +from .server import NameServer, RawNameServer +from .transport import TransportBase + + +### CLASSES +### ============================================================================ +class BaseApplication(LoggingMixin): + """Base class for all application classes. + + New in `3.0`. + """ + + def __init__(self, server: NameServer | RawNameServer) -> None: + if isinstance(server, NameServer): + server = RawNameServer(server) + self.server: RawNameServer = server + self.logger = self.get_logger() + return + + def run(self) -> int | None: + """Run this application. + + Child classes must override this method. + + Returns: + Integer status code to be returned. `None` will be treated as `0`. + """ + raise NotImplementedError() + + +class DirectApplication(BaseApplication): + """Application that directly runs the server. + + New in `3.0`. + """ + + MAX_ERRORS: int = 10 + + exit_code: int + + def __init__(self, server: NameServer | RawNameServer, transport: TransportBase) -> None: + super().__init__(server) + self.transport = transport + self.exit_code = 0 + self.shutdown_server = False + return + + def run(self) -> int: + """Start running the server + + Returns: + `exit_code`, `0` if exited normally + """ + # Start Server + # TODO: Do we want to recreate the transport instance or do we assume that + # transport.shutdown_server puts it back into a ready state? + # We could make this configurable? :thonking: + + self.info(f"Starting {self.transport}") + try: + self.transport.start_server() + except Exception as e: # pylint: disable=broad-except + self.critical(f"Failed to start server. {e}", exc_info=e) + self.exit_code = 1 + return self.exit_code + + # Process Requests + error_count = 0 + while True: + if self.shutdown_server: + break + + try: + message = self.transport.receive_message() + message.response = self.server.process_request(message.message) + self.transport.send_message_response(message) + + except InvalidMessageError as e: + self.warning(f"{e}") + + except Exception as e: # pylint: disable=broad-except + self.error(f"Uncaught error occured. {e}", exc_info=e) + error_count += 1 + if self.MAX_ERRORS and error_count >= self.MAX_ERRORS: + self.critical(f"Max errors hit ({error_count})") + self.shutdown_server = True + self.exit_code = 1 + + except KeyboardInterrupt: + self.info("KeyboardInterrupt received.") + self.shutdown_server = True + + # Stop Server + self.info("Shutting down server") + self.transport.stop_server() + + return self.exit_code diff --git a/src/nserver/cli.py b/src/nserver/cli.py new file mode 100644 index 0000000..88da49b --- /dev/null +++ b/src/nserver/cli.py @@ -0,0 +1,134 @@ +### IMPORTS +### ============================================================================ +## Future +from __future__ import annotations + +## Standard Library +import argparse +import importlib +import os +import pydoc + +## Installed +import pillar.application + +## Application +from . import transport +from . import _version + +from .application import BaseApplication, DirectApplication +from .server import NameServer, RawNameServer + + +### CLASSES +### ============================================================================ +class CliApplication(pillar.application.Application): + """NServer CLI tool for running servers""" + + application_name = "nserver" + name = "nserver" + version = _version.VERSION_INFO_FULL + epilog = "For full information including licence see https://github.com/nhairs/nserver" + + config_args_enabled = False + + def get_argument_parser(self) -> argparse.ArgumentParser: + parser = super().get_argument_parser() + + ## Server + ## --------------------------------------------------------------------- + parser.add_argument( + "--server", + action="store", + required=True, + help=( + "Import path of server / factory to run in the form of " + "package.module.path:attribute" + ), + ) + + ## Transport + ## --------------------------------------------------------------------- + parser.add_argument( + "--host", + action="store", + default="localhost", + help="Host (IP) to bind to. Defaults to localhost.", + ) + + parser.add_argument( + "--port", + action="store", + default=5300, + type=int, + help="Port to bind to. Defaults to 5300.", + ) + + transport_group = parser.add_mutually_exclusive_group() + transport_group.add_argument( + "--udp", + action="store_const", + const=transport.UDPv4Transport, + dest="transport", + help="Use UDPv4 socket for transport. (default)", + ) + transport_group.add_argument( + "--udp6", + action="store_const", + const=transport.UDPv6Transport, + dest="transport", + help="Use UDPv6 socket for transport.", + ) + transport_group.add_argument( + "--tcp", + action="store_const", + const=transport.TCPv4Transport, + dest="transport", + help="Use TCPv4 socket for transport.", + ) + + parser.set_defaults(transport=transport.UDPv4Transport) + return parser + + def setup(self, *args, **kwargs) -> None: + super().setup(*args, **kwargs) + + self.server = self.get_server() + self.application = self.get_application() + return + + def main(self) -> int | None: + return self.application.run() + + def get_server(self) -> NameServer | RawNameServer: + """Factory for getting the server to run based on current settings""" + module_path, attribute_path = self.args.server.split(":") + + obj: object + if os.path.isfile(module_path): + # Ref: https://stackoverflow.com/a/68361215/12281814 + obj = pydoc.importfile(module_path) + else: + obj = importlib.import_module(module_path) + + for attribute_name in attribute_path.split("."): + obj = getattr(obj, attribute_name) + + if isinstance(obj, (NameServer, RawNameServer)): + return obj + + # Assume callable (will throw error if not) + server = obj() # type: ignore[operator] + + if isinstance(server, (NameServer, RawNameServer)): + return server + + raise TypeError(f"Imported factory ({obj}) did not return a server ({server})") + + def get_application(self) -> BaseApplication: + """Factory for getting the application based on current settings""" + application = DirectApplication( + self.server, + self.args.transport(self.args.host, self.args.port), + ) + return application diff --git a/src/nserver/exceptions.py b/src/nserver/exceptions.py index c72946d..89cc2ca 100644 --- a/src/nserver/exceptions.py +++ b/src/nserver/exceptions.py @@ -1,11 +1,11 @@ ### IMPORTS ### ============================================================================ +## Future +from __future__ import annotations + ## Standard Library import base64 -# Note: Union can only be replaced with `X | Y` in 3.10+ -from typing import Tuple, Union - ## Installed ## Application @@ -17,7 +17,7 @@ class InvalidMessageError(ValueError): """An invalid DNS message""" def __init__( - self, error: Exception, raw_data: bytes, remote_address: Union[str, Tuple[str, int]] + self, error: Exception, raw_data: bytes, remote_address: str | tuple[str, int] ) -> None: """ Args: diff --git a/src/nserver/middleware.py b/src/nserver/middleware.py index 2662bd7..e27df48 100644 --- a/src/nserver/middleware.py +++ b/src/nserver/middleware.py @@ -1,163 +1,220 @@ ### IMPORTS ### ============================================================================ +## Future +from __future__ import annotations + ## Standard Library import inspect import threading -from typing import Callable, Dict, List, Type, Optional +from typing import TYPE_CHECKING, Callable, Generic, TypeVar +import sys ## Installed import dnslib +from pillar.logging import LoggingMixin ## Application from .models import Query, Response -from .records import RecordBase -from .rules import RuleBase, RuleResult +from .rules import coerce_to_response, RuleResult + +## Special +if sys.version_info < (3, 10): + from typing_extensions import TypeAlias +else: + from typing import TypeAlias ### CONSTANTS ### ============================================================================ +# pylint: disable=invalid-name +T_request = TypeVar("T_request") +T_response = TypeVar("T_response") +# pylint: enable=invalid-name + ## Query Middleware -QueryMiddlewareCallable = Callable[[Query], Response] +## ----------------------------------------------------------------------------- +QueryCallable: TypeAlias = Callable[[Query], Response] """Type alias for functions that can be used with `QueryMiddleware.next_function`""" -ExceptionHandler = Callable[[Query, Exception], Response] +QueryExceptionHandler: TypeAlias = Callable[[Query, Exception], Response] """Type alias for `ExceptionHandlerMiddleware` exception handler functions""" # Hooks -BeforeFirstQueryHook = Callable[[], None] +BeforeFirstQueryHook: TypeAlias = Callable[[], None] """Type alias for `HookMiddleware.before_first_query` functions.""" -BeforeQueryHook = Callable[[Query], RuleResult] +BeforeQueryHook: TypeAlias = Callable[[Query], RuleResult] """Type alias for `HookMiddleware.before_query` functions.""" -AfterQueryHook = Callable[[Response], Response] +AfterQueryHook: TypeAlias = Callable[[Response], Response] """Type alias for `HookMiddleware.after_query` functions.""" ## RawRecordMiddleware -RawRecordMiddlewareCallable = Callable[[dnslib.DNSRecord], dnslib.DNSRecord] -"""Type alias for functions that can be used with `RawRecordMiddleware.next_function`""" - -RawRecordExceptionHandler = Callable[[dnslib.DNSRecord, Exception], dnslib.DNSRecord] -"""Type alias for `RawRecordExceptionHandlerMiddleware` exception handler functions""" - - -### FUNCTIONS -### ============================================================================ -def coerce_to_response(result: RuleResult) -> Response: - """Convert some `RuleResult` to a `Response` - - New in `2.0`. - - Args: - result: the results to convert - - Raises: - TypeError: unsupported result type - """ - if isinstance(result, Response): - return result +## ----------------------------------------------------------------------------- +if TYPE_CHECKING: - if result is None: - return Response() + class RawRecord(dnslib.DNSRecord): + "Dummy class for type checking as dnslib is not typed" - if isinstance(result, RecordBase) and result.__class__ is not RecordBase: - return Response(answers=result) +else: + RawRecord: TypeAlias = dnslib.DNSRecord + """Type alias for raw records to allow easy changing of implementation details""" - if isinstance(result, list) and all(isinstance(item, RecordBase) for item in result): - return Response(answers=result) +RawMiddlewareCallable: TypeAlias = Callable[[RawRecord], RawRecord] +"""Type alias for functions that can be used with `RawRecordMiddleware.next_function`""" - raise TypeError(f"Cannot process result: {result!r}") +RawExceptionHandler: TypeAlias = Callable[[RawRecord, Exception], RawRecord] +"""Type alias for `RawRecordExceptionHandlerMiddleware` exception handler functions""" ### CLASSES ### ============================================================================ -## Request Middleware +## Generic Base Classes ## ----------------------------------------------------------------------------- -class QueryMiddleware: - """Middleware for interacting with `Query` objects +class MiddlewareBase(Generic[T_request, T_response], LoggingMixin): + """Generic base class for middleware classes. - New in `2.0`. + New in `3.0`. """ def __init__(self) -> None: - self.next_function: Optional[QueryMiddlewareCallable] = None + self.next_function: Callable[[T_request], T_response] | None = None + self.logger = self.get_logger() return - def __call__(self, query: Query) -> Response: + def __call__(self, request: T_request) -> T_response: + """Call this middleware + + Args: + request: request to process + + Raises: + RuntimeError: If `next_function` is not set. + """ + if self.next_function is None: - raise RuntimeError("next_function is not set") - return self.process_query(query, self.next_function) + raise RuntimeError("next_function is not set. Need to call register_next_function.") + return self.process_request(request, self.next_function) + + def set_next_function(self, next_function: Callable[[T_request], T_response]) -> None: + """Set the `next_function` of this middleware - def register_next_function(self, next_function: QueryMiddlewareCallable) -> None: - """Set the `next_function` of this middleware""" + Args: + next_function: Callable that this middleware should call next. + """ if self.next_function is not None: - raise RuntimeError("next_function is already set") + raise RuntimeError(f"next_function is already set to {self.next_function}") self.next_function = next_function return - def process_query(self, query: Query, call_next: QueryMiddlewareCallable) -> Response: - """Handle an incoming query. + def process_request( + self, request: T_request, call_next: Callable[[T_request], T_response] + ) -> T_response: + """Process a given request - Child classes should override this function (if they do not this middleware will - simply pass the query onto the next function). - - Args: - query: the incoming query - call_next: the next function in the chain + Child classes should override this method with their own logic. """ - return call_next(query) + return call_next(request) -class ExceptionHandlerMiddleware(QueryMiddleware): - """Middleware for handling exceptions originating from a `QueryMiddleware` stack. - - Allows registering handlers for individual `Exception` types. Only one handler can - exist for a given `Exception` type. - - When an exception is encountered, the middleware will search for the first handler that - matches the class or parent class of the exception in method resolution order. If no handler - is registered will use this classes `self.default_exception_handler`. - - New in `2.0`. +class ExceptionHandlerBase(MiddlewareBase[T_request, T_response]): + """Generic base class for middleware exception handlers Attributes: - exception_handlers: registered exception handlers + handlers: registered exception handlers + + New in `3.0`. """ def __init__( - self, exception_handlers: Optional[Dict[Type[Exception], ExceptionHandler]] = None + self, + handlers: dict[type[Exception], Callable[[T_request, Exception], T_response]] | None = None, ) -> None: - """ - Args: - exception_handlers: exception handlers to assign - """ super().__init__() - self.exception_handlers = exception_handlers if exception_handlers is not None else {} + self.handlers: dict[type[Exception], Callable[[T_request, Exception], T_response]] = ( + handlers if handlers is not None else {} + ) return - def process_query(self, query: Query, call_next: QueryMiddlewareCallable) -> Response: - """Call the next function catching any handling any errors""" + def process_request(self, request, call_next): + """Call the next function handling any exceptions that arise""" try: - response = call_next(query) + response = call_next(request) except Exception as e: # pylint: disable=broad-except - handler = self.get_exception_handler(e) - response = handler(query, e) + handler = self.get_handler(e) + response = handler(request, e) return response - def get_exception_handler(self, exception: Exception) -> ExceptionHandler: - """Get the exception handler for an `Exception`. + def set_handler( + self, + exception_class: type[Exception], + handler: Callable[[T_request, Exception], T_response], + *, + allow_overwrite: bool = False, + ) -> None: + """Add an exception handler for the given exception class + + Args: + exception_class: Exceptions to associate with this handler. + handler: The handler to add. + allow_overwrite: Allow overwriting existing handlers. + + Raises: + ValueError: If a handler already exists for the given exception and + `allow_overwrite` is `False`. + """ + if exception_class in self.handlers and not allow_overwrite: + raise ValueError( + f"Exception handler already exists for {exception_class} and allow_overwrite is False" + ) + self.handlers[exception_class] = handler + return + + def get_handler(self, exception: Exception) -> Callable[[T_request, Exception], T_response]: + """Get the exception handler for the given exception Args: exception: the exception we wish to handle """ for class_ in inspect.getmro(exception.__class__): - if class_ in self.exception_handlers: - return self.exception_handlers[class_] + if class_ in self.handlers: + return self.handlers[class_] # No exception handler found - use default handler - return self.default_exception_handler + return self.default_handler + + @staticmethod + def default_handler(request: T_request, exception: Exception) -> T_response: + """Default exception handler + + Child classes MUST override this method. + """ + raise NotImplementedError("Must overide this method") + + +## Request Middleware +## ----------------------------------------------------------------------------- +class QueryMiddleware(MiddlewareBase[Query, Response]): + """Middleware for interacting with `Query` objects + + New in `3.0`. + """ + + +class QueryExceptionHandlerMiddleware(ExceptionHandlerBase[Query, Response], QueryMiddleware): + """Middleware for handling exceptions originating from a `QueryMiddleware` stack. + + Allows registering handlers for individual `Exception` types. Only one handler can + exist for a given `Exception` type. + + When an exception is encountered, the middleware will search for the first handler that + matches the class or parent class of the exception in method resolution order. If no handler + is registered will use this classes `self.default_exception_handler`. + + New in `3.0`. + """ @staticmethod - def default_exception_handler(query: Query, exception: Exception) -> Response: + def default_handler(request: Query, exception: Exception) -> Response: """The default exception handler""" # pylint: disable=unused-argument return Response(error_code=dnslib.RCODE.SERVFAIL) @@ -182,21 +239,21 @@ class HookMiddleware(QueryMiddleware): hook or from the next function in the middleware chain. They take a `Response` input and must return a `Response`. - New in `2.0`. - Attributes: before_first_query: `before_first_query` hooks before_query: `before_query` hooks after_query: `after_query` hooks before_first_query_run: have we run the `before_first_query` hooks before_first_query_failed: did any `before_first_query` hooks fail + + New in `3.0`. """ def __init__( self, - before_first_query: Optional[List[BeforeFirstQueryHook]] = None, - before_query: Optional[List[BeforeQueryHook]] = None, - after_query: Optional[List[AfterQueryHook]] = None, + before_first_query: list[BeforeFirstQueryHook] | None = None, + before_query: list[BeforeQueryHook] | None = None, + after_query: list[AfterQueryHook] | None = None, ) -> None: """ Args: @@ -205,26 +262,24 @@ def __init__( after_query: initial `after_query` hooks to register """ super().__init__() - self.before_first_query: List[BeforeFirstQueryHook] = ( + self.before_first_query: list[BeforeFirstQueryHook] = ( before_first_query if before_first_query is not None else [] ) - self.before_query: List[BeforeQueryHook] = before_query if before_query is not None else [] - self.after_query: List[AfterQueryHook] = after_query if after_query is not None else [] + self.before_query: list[BeforeQueryHook] = before_query if before_query is not None else [] + self.after_query: list[AfterQueryHook] = after_query if after_query is not None else [] self.before_first_query_run: bool = False self.before_first_query_failed: bool = False self._before_first_query_lock = threading.Lock() return - def process_query(self, query: Query, call_next: QueryMiddlewareCallable) -> Response: - """Process a query running relevant hooks.""" + def process_request(self, request: Query, call_next: QueryCallable) -> Response: with self._before_first_query_lock: if not self.before_first_query_run: - # self._debug("Running before_first_query") self.before_first_query_run = True try: for before_first_query_hook in self.before_first_query: - # self._vdebug(f"Running before_first_query func: {hook}") + self.vdebug(f"Running before_first_query_hook: {before_first_query_hook}") before_first_query_hook() except Exception: self.before_first_query_failed = True @@ -233,92 +288,34 @@ def process_query(self, query: Query, call_next: QueryMiddlewareCallable) -> Res result: RuleResult for before_query_hook in self.before_query: - result = before_query_hook(query) + self.vdebug(f"Running before_query_hook: {before_query_hook}") + result = before_query_hook(request) if result is not None: - # self._debug(f"Got result from before_hook: {hook}") + self.debug(f"Got result from before_query_hook: {before_query_hook}") break else: # No before query hooks returned a response - keep going - result = call_next(query) + result = call_next(request) response = coerce_to_response(result) for after_query_hook in self.after_query: + self.vdebug(f"Running after_query_hook: {after_query_hook}") response = after_query_hook(response) return response -# Final callable -# .............................................................................. -# This is not a QueryMiddleware - it is however the end of the line for all QueryMiddleware -class RuleProcessor: - """Find and run a matching rule function. - - This class serves as the bottom of the `QueryMiddleware` stack. - - New in `2.0`. - """ - - def __init__(self, rules: List[RuleBase]) -> None: - """ - Args: - rules: rules to run against - """ - self.rules = rules - return - - def __call__(self, query: Query) -> Response: - for rule in self.rules: - rule_func = rule.get_func(query) - if rule_func is not None: - # self._info(f"Matched Rule: {rule}") - return coerce_to_response(rule_func(query)) - - # self._info("Did not match any rule") - return Response(error_code=dnslib.RCODE.NXDOMAIN) - - ## Raw Middleware ## ----------------------------------------------------------------------------- -class RawRecordMiddleware: +class RawMiddleware(MiddlewareBase[RawRecord, RawRecord]): """Middleware to be run against raw `dnslib.DNSRecord`s. - New in `2.0`. + New in `3.0`. """ - def __init__(self) -> None: - self.next_function: Optional[RawRecordMiddlewareCallable] = None - return - - def __call__(self, record: dnslib.DNSRecord) -> None: - if self.next_function is None: - raise RuntimeError("next_function is not set") - return self.process_record(record, self.next_function) - - def register_next_function(self, next_function: RawRecordMiddlewareCallable) -> None: - """Set the `next_function` of this middleware""" - if self.next_function is not None: - raise RuntimeError("next_function is already set") - self.next_function = next_function - return - - def process_record( - self, record: dnslib.DNSRecord, call_next: RawRecordMiddlewareCallable - ) -> dnslib.DNSRecord: - """Handle an incoming record. - - Child classes should override this function (if they do not this middleware will - simply pass the record onto the next function). - Args: - record: the incoming record - call_next: the next function in the chain - """ - return call_next(record) - - -class RawRecordExceptionHandlerMiddleware(RawRecordMiddleware): +class RawExceptionHandlerMiddleware(ExceptionHandlerBase[RawRecord, RawRecord]): """Middleware for handling exceptions originating from a `RawRecordMiddleware` stack. Allows registering handlers for individual `Exception` types. Only one handler can @@ -326,109 +323,36 @@ class RawRecordExceptionHandlerMiddleware(RawRecordMiddleware): When an exception is encountered, the middleware will search for the first handler that matches the class or parent class of the exception in method resolution order. If no handler - is registered will use this classes `self.default_exception_handler`. + is registered will use this classes `self.default_handler`. Danger: Important Exception handlers are expected to be robust - that is, they must always return correctly even if they internally encounter an `Exception`. - New in `2.0`. - Attributes: - exception_handlers: registered exception handlers - """ - - def __init__( - self, exception_handlers: Optional[Dict[Type[Exception], RawRecordExceptionHandler]] = None - ) -> None: - super().__init__() - self.exception_handlers: Dict[Type[Exception], RawRecordExceptionHandler] = ( - exception_handlers if exception_handlers is not None else {} - ) - return - - def process_record( - self, record: dnslib.DNSRecord, call_next: RawRecordMiddlewareCallable - ) -> dnslib.DNSRecord: - """Call the next function handling any exceptions that arise""" - try: - response = call_next(record) - except Exception as e: # pylint: disable=broad-except - handler = self.get_exception_handler(e) - response = handler(record, e) - return response - - def get_exception_handler(self, exception: Exception) -> RawRecordExceptionHandler: - """Get the exception handler for the given exception + handlers: registered exception handlers - Args: - exception: the exception we wish to handle - """ - for class_ in inspect.getmro(exception.__class__): - if class_ in self.exception_handlers: - return self.exception_handlers[class_] - # No exception handler found - use default handler - return self.default_exception_handler + New in `3.0`. + """ @staticmethod - def default_exception_handler( - record: dnslib.DNSRecord, exception: Exception - ) -> dnslib.DNSRecord: + def default_handler(request: RawRecord, exception: Exception) -> RawRecord: """Default exception handler""" # pylint: disable=unused-argument - response = record.reply() + response = request.reply() response.header.rcode = dnslib.RCODE.SERVFAIL return response -# Final Callable -# .............................................................................. -# This is not a RawRcordMiddleware - it is however the end of the line for all RawRecordMiddleware -class QueryMiddlewareProcessor: - """Convert an incoming DNS record and pass it to a `QueryMiddleware` stack. - - This class serves as the bottom of the `RawRcordMiddleware` stack. - - New in `2.0`. - """ - - def __init__(self, query_middleware: QueryMiddlewareCallable) -> None: - """ - Args: - query_middleware: the top of the middleware stack - """ - self.query_middleware = query_middleware - return - - def __call__(self, record: dnslib.DNSRecord) -> dnslib.DNSRecord: - response = record.reply() - - if record.header.opcode != dnslib.OPCODE.QUERY: - # self._info(f"Received non-query opcode: {record.header.opcode}") - # This server only response to DNS queries - response.header.rcode = dnslib.RCODE.NOTIMP - return response - - if len(record.questions) != 1: - # self._info(f"Received len(questions_ != 1 ({record.questions})") - # To simplify things we only respond if there is 1 question. - # This is apparently common amongst DNS server implementations. - # For more information see the responses to this SO question: - # https://stackoverflow.com/q/4082081 - response.header.rcode = dnslib.RCODE.REFUSED - return response - - try: - query = Query.from_dns_question(record.questions[0]) - except ValueError: - # self._warning(e) - response.header.rcode = dnslib.RCODE.FORMERR - return response - - result = self.query_middleware(query) - - response.add_answer(*result.get_answer_records()) - response.add_ar(*result.get_additional_records()) - response.add_auth(*result.get_authority_records()) - response.header.rcode = result.error_code - return response +### TYPE_CHECKING +### ============================================================================ +if TYPE_CHECKING and False: # pylint: disable=condition-evals-to-constant + # pylint: disable=undefined-variable + q1 = QueryExceptionHandlerMiddleware() + reveal_type(q1) + reveal_type(q1.handlers) + reveal_type(q1.default_handler) + r1 = RawExceptionHandlerMiddleware() + reveal_type(r1) + reveal_type(r1.handlers) + reveal_type(r1.default_handler) diff --git a/src/nserver/models.py b/src/nserver/models.py index a4626a3..dee7117 100644 --- a/src/nserver/models.py +++ b/src/nserver/models.py @@ -1,5 +1,8 @@ ### IMPORTS ### ============================================================================ +## Future +from __future__ import annotations + ## Standard Library from typing import Optional, Union, List @@ -37,7 +40,7 @@ def __init__(self, qtype: str, name: str) -> None: return @classmethod - def from_dns_question(cls, question: dnslib.DNSQuestion) -> "Query": + def from_dns_question(cls, question: dnslib.DNSQuestion) -> Query: """Create a new query from a `dnslib.DNSQuestion`""" if question.qtype not in dnslib.QTYPE.forward: raise ValueError(f"Invalid QTYPE: {question.qtype}") @@ -106,14 +109,14 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.__repr__() - def get_answer_records(self) -> List[dnslib.RD]: + def get_answer_records(self) -> list[dnslib.RD]: """Prepare resource records for answer section""" return [record.to_resource_record() for record in self.answers] - def get_additional_records(self) -> List[dnslib.RD]: + def get_additional_records(self) -> list[dnslib.RD]: """Prepare resource records for additional section""" return [record.to_resource_record() for record in self.additional] - def get_authority_records(self) -> List[dnslib.RD]: + def get_authority_records(self) -> list[dnslib.RD]: """Prepare resource records for authority section""" return [record.to_resource_record() for record in self.authority] diff --git a/src/nserver/records.py b/src/nserver/records.py index 9915f67..3510409 100644 --- a/src/nserver/records.py +++ b/src/nserver/records.py @@ -2,10 +2,13 @@ ### IMPORTS ### ============================================================================ +## Future +from __future__ import annotations + ## Standard Library from ipaddress import IPv4Address, IPv6Address import re -from typing import Any, Union, Dict +from typing import Any ## Installed import dnslib @@ -36,7 +39,7 @@ def __init__(self, resource_name: str, ttl: int) -> None: type_name = self.__class__.__name__ self._qtype = getattr(dnslib.QTYPE, type_name) self._class = getattr(dnslib, type_name) # class means python class not RR CLASS - self._record_kwargs: Dict[str, Any] + self._record_kwargs: dict[str, Any] is_unsigned_int_size(ttl, 32, throw_error=True, value_name="ttl") self.ttl = ttl self.resource_name = resource_name @@ -56,7 +59,7 @@ def to_resource_record(self) -> dnslib.RR: class A(RecordBase): # pylint: disable=invalid-name """Ipv4 Address (`A`) Record.""" - def __init__(self, resource_name: str, ip: Union[str, IPv4Address], ttl: int = 300) -> None: + def __init__(self, resource_name: str, ip: str | IPv4Address, ttl: int = 300) -> None: """ Args: resource_name: DNS resource name @@ -77,7 +80,7 @@ def __init__(self, resource_name: str, ip: Union[str, IPv4Address], ttl: int = 3 class AAAA(RecordBase): """Ipv6 Address (`AAAA`) Record.""" - def __init__(self, resource_name: str, ip: Union[str, IPv6Address], ttl: int = 300) -> None: + def __init__(self, resource_name: str, ip: str | IPv6Address, ttl: int = 300) -> None: """ Args: resource_name: DNS resource name @@ -222,7 +225,7 @@ class SOA(RecordBase): - https://en.wikipedia.org/wiki/SOA_record """ - def __init__( # pylint: disable=too-many-arguments + def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments self, zone_name: str, primary_name_server: str, diff --git a/src/nserver/rules.py b/src/nserver/rules.py index 3abf5bc..a3b111d 100644 --- a/src/nserver/rules.py +++ b/src/nserver/rules.py @@ -2,9 +2,12 @@ ### IMPORTS ### ============================================================================ +## Future +from __future__ import annotations + ## Standard Library import re -from typing import Callable, List, Optional, Pattern, Union, Type +from typing import Callable, Pattern, Union, Type, List ## Installed import dnslib @@ -16,7 +19,7 @@ ### CONSTANTS ### ============================================================================ -ALL_QTYPES: List[str] = list(dnslib.QTYPE.reverse.keys()) +ALL_QTYPES: list[str] = list(dnslib.QTYPE.reverse.keys()) """All supported Query Types New in `2.0`. @@ -27,7 +30,33 @@ ### FUNCTIONS ### ============================================================================ -def smart_make_rule(rule: "Union[Type[RuleBase], str, Pattern]", *args, **kwargs) -> "RuleBase": +def coerce_to_response(result: RuleResult) -> Response: + """Convert some `RuleResult` to a `Response` + + Args: + result: the results to convert + + Raises: + TypeError: unsupported result type + + New in `3.0`. + """ + if isinstance(result, Response): + return result + + if result is None: + return Response() + + if isinstance(result, RecordBase) and result.__class__ is not RecordBase: + return Response(answers=result) + + if isinstance(result, list) and all(isinstance(item, RecordBase) for item in result): + return Response(answers=result) + + raise TypeError(f"Cannot process result: {result!r}") + + +def smart_make_rule(rule: Union[Type[RuleBase], str, Pattern], *args, **kwargs) -> RuleBase: """Create a rule using shorthand notation. The exact type of rule returned depends on what is povided by `rule`. @@ -76,7 +105,7 @@ def smart_make_rule(rule: "Union[Type[RuleBase], str, Pattern]", *args, **kwargs class RuleBase: """Base class for all Rules to inherit from.""" - def get_func(self, query: Query) -> Optional[ResponseFunction]: + def get_func(self, query: Query) -> ResponseFunction | None: """From the given query return the function to run, if any. If no function should be run (i.e. because it does not match the rule), @@ -99,7 +128,7 @@ class StaticRule(RuleBase): def __init__( self, match_string: str, - allowed_qtypes: List[str], + allowed_qtypes: list[str], func: ResponseFunction, case_sensitive: bool = False, ) -> None: @@ -116,7 +145,7 @@ def __init__( self.case_sensitive = case_sensitive return - def get_func(self, query: Query) -> Optional[ResponseFunction]: + def get_func(self, query: Query) -> ResponseFunction | None: """Same as parent class""" if query.type not in self.allowed_qtypes: return None @@ -147,7 +176,7 @@ class ZoneRule(RuleBase): def __init__( self, zone: str, - allowed_qtypes: List[str], + allowed_qtypes: list[str], func: ResponseFunction, case_sensitive: bool = False, ) -> None: @@ -165,7 +194,7 @@ def __init__( self.case_sensitive = case_sensitive return - def get_func(self, query: Query) -> Optional[ResponseFunction]: + def get_func(self, query: Query) -> ResponseFunction | None: """Same as parent class""" if self.allowed_qtypes is not None and query.type not in self.allowed_qtypes: return None @@ -194,7 +223,7 @@ class RegexRule(RuleBase): def __init__( self, regex: Pattern, - allowed_qtypes: List[str], + allowed_qtypes: list[str], func: ResponseFunction, case_sensitive: bool = False, ) -> None: @@ -219,7 +248,7 @@ def __init__( self.case_sensitive = case_sensitive return - def get_func(self, query: Query) -> Optional[ResponseFunction]: + def get_func(self, query: Query) -> ResponseFunction | None: """Same as parent class""" if query.type not in self.allowed_qtypes: return None @@ -257,7 +286,7 @@ class WildcardStringRule(RuleBase): def __init__( self, wildcard_string: str, - allowed_qtypes: List, + allowed_qtypes: list, func: ResponseFunction, case_sensitive: bool = False, ) -> None: @@ -274,7 +303,7 @@ def __init__( self.case_sensitive = case_sensitive return - def get_func(self, query: Query) -> Optional[ResponseFunction]: + def get_func(self, query: Query) -> ResponseFunction | None: """Same as parent class""" if query.type not in self.allowed_qtypes: return None diff --git a/src/nserver/server.py b/src/nserver/server.py index 8c69b61..480d896 100644 --- a/src/nserver/server.py +++ b/src/nserver/server.py @@ -1,164 +1,158 @@ ### IMPORTS ### ============================================================================ -## Standard Library -import logging +## Future +from __future__ import annotations -# Note: Optional can only be replaced with `| None` in 3.10+ -from typing import List, Dict, Optional, Union, Type, Pattern +## Standard Library +from typing import TypeVar, Generic, Pattern ## Installed import dnslib +from pillar.logging import LoggingMixin ## Application -from .exceptions import InvalidMessageError from .models import Query, Response -from .rules import smart_make_rule, RuleBase, ResponseFunction -from .settings import Settings -from .transport import TransportBase, UDPv4Transport, UDPv6Transport, TCPv4Transport +from .rules import coerce_to_response, smart_make_rule, RuleBase, ResponseFunction -from . import middleware +from . import middleware as m ### CONSTANTS ### ============================================================================ -TRANSPORT_MAP: Dict[str, Type[TransportBase]] = { - "UDPv4": UDPv4Transport, - "UDPv6": UDPv6Transport, - "TCPv4": TCPv4Transport, -} +# pylint: disable=invalid-name +T_middleware = TypeVar("T_middleware", bound=m.MiddlewareBase) +T_exception_handler = TypeVar("T_exception_handler", bound=m.ExceptionHandlerBase) +# pylint: enable=invalid-name ### Classes ### ============================================================================ -class Scaffold: - """Base class for shared functionality between `NameServer` and `Blueprint` +class MiddlewareMixin(Generic[T_middleware, T_exception_handler]): + """Generic mixin for building a middleware stack in a server. - New in `2.0`. + Should not be used directly, instead use the servers that implement it: + `NameServer`, `RawNameServer`. - Attributes: - rules: registered rules - hook_middleware: hook middleware - exception_handler_middleware: Query exception handler middleware + New in `3.0`. """ - _logger: logging.Logger - - def __init__(self, name: str) -> None: - """ - Args: - name: The name of the server. This is used for internal logging. - """ - self.name = name - - self.rules: List[RuleBase] = [] - self.hook_middleware = middleware.HookMiddleware() - self.exception_handler_middleware = middleware.ExceptionHandlerMiddleware() + _exception_handler: T_exception_handler - self._user_query_middleware: List[middleware.QueryMiddleware] = [] - self._query_middleware_stack: List[ - Union[middleware.QueryMiddleware, middleware.QueryMiddlewareCallable] - ] = [] + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._middleware_stack_final: list[T_middleware] | None = None + self._middleware_stack_user: list[T_middleware] = [] return - ## Register Methods + ## Middleware ## ------------------------------------------------------------------------- - def register_rule(self, rule: RuleBase) -> None: - """Register the given rule + def middleware_is_prepared(self) -> bool: + """Check if the middleware has been prepared.""" + return self._middleware_stack_final is not None + + def append_middleware(self, middleware: T_middleware) -> None: + """Append this middleware to the middleware stack Args: - rule: the rule to register + middleware: middleware to append """ - self._debug(f"Registered rule: {rule!r}") - self.rules.append(rule) + if self.middleware_is_prepared(): + raise RuntimeError("Cannot append middleware once prepared") + self._middleware_stack_user.append(middleware) return - def register_blueprint( - self, blueprint: "Blueprint", rule_: Union[Type[RuleBase], str, Pattern], *args, **kwargs - ) -> None: - """Register a blueprint using [`smart_make_rule`][nserver.rules.smart_make_rule]. + def prepare_middleware(self) -> None: + """Prepare middleware for consumption - New in `2.0`. + Child classes should wrap this method to set the `next_function` on the + final middleware in the stack. + """ + if self.middleware_is_prepared(): + raise RuntimeError("Middleware is already prepared") - Args: - blueprint: the `Blueprint` to attach - rule_: rule as per `nserver.rules.smart_make_rule` - args: extra arguments to provide `smart_make_rule` - kwargs: extra keyword arguments to provide `smart_make_rule` + middleware_stack = self._prepare_middleware_stack() - Raises: - ValueError: if `func` is provided in `kwargs`. - """ + next_middleware: T_middleware | None = None - if "func" in kwargs: - raise ValueError("Must not provide `func` in kwargs") - self.register_rule(smart_make_rule(rule_, *args, func=blueprint.entrypoint, **kwargs)) + for middleware in middleware_stack[::-1]: + if next_middleware is not None: + middleware.set_next_function(next_middleware) + next_middleware = middleware + + self._middleware_stack_final = middleware_stack return - def register_before_first_query(self, func: middleware.BeforeFirstQueryHook) -> None: - """Register a function to be run before the first query. + def _prepare_middleware_stack(self) -> list[T_middleware]: + """Create final stack of middleware. - Args: - func: the function to register + Child classes may override this method to customise the final middleware stack. """ - self.hook_middleware.before_first_query.append(func) - return + return [self._exception_handler, *self._middleware_stack_user] # type: ignore[list-item] - def register_before_query(self, func: middleware.BeforeQueryHook) -> None: - """Register a function to be run before every query. + @property + def middleware(self) -> list[T_middleware]: + """Accssor for this servers middleware. - Args: - func: the function to register - If `func` returns anything other than `None` will stop processing the - incoming `Query` and continue to result processing with the return value. + If the server has been prepared then returns a copy of the prepared middleware. + Otherwise returns a mutable list of the registered middleware. """ - self.hook_middleware.before_query.append(func) + if self.middleware_is_prepared(): + return self._middleware_stack_final.copy() # type: ignore[union-attr] + return self._middleware_stack_user + + ## Exception Handler + ## ------------------------------------------------------------------------- + def register_exception_handler(self, *args, **kwargs) -> None: + """Shortcut for `self.exception_handler.set_handler`""" + self.exception_handler_middleware.set_handler(*args, **kwargs) return - def register_after_query(self, func: middleware.AfterQueryHook) -> None: - """Register a function to be run on the result of a query. + @property + def exception_handler_middleware(self) -> T_exception_handler: + """Read only accessor for this server's middleware exception handler""" + return self._exception_handler + + def exception_handler(self, exception_class: type[Exception]): + """Decorator for registering a function as an raw exception handler Args: - func: the function to register + exception_class: The `Exception` class to register this handler for """ - self.hook_middleware.after_query.append(func) - return - def register_middleware(self, query_middleware: middleware.QueryMiddleware) -> None: - """Add a `QueryMiddleware` to this server. + def decorator(func): + nonlocal exception_class + self.register_raw_exception_handler(exception_class, func) + return func - New in `2.0`. + return decorator - Args: - query_middleware: the middleware to add - """ - if self._query_middleware_stack: - # Note: we can use truthy expression as once processed there will always be at - # least one item in the stack - raise RuntimeError("Cannot register middleware after stack is created") - self._user_query_middleware.append(query_middleware) - return - def register_exception_handler( - self, exception_class: Type[Exception], handler: middleware.ExceptionHandler - ) -> None: - """Register an exception handler for the `QueryMiddleware` +## Mixins +## ----------------------------------------------------------------------------- +class RulesMixin(LoggingMixin): + """Base class for rules based functionality` + + Attributes: + rules: reistered rules - Only one handler can exist for a given exception type. + New in `3.0`. + """ - New in `2.0`. + def __init__(self) -> None: + super().__init__() + self.rules: list[RuleBase] = [] + return + + def register_rule(self, rule: RuleBase) -> None: + """Register the given rule Args: - exception_class: the type of exception to handle - handler: the function to call when handling an exception + rule: the rule to register """ - if exception_class in self.exception_handler_middleware.exception_handlers: - raise ValueError("Exception handler already exists for {exception_class}") - - self.exception_handler_middleware.exception_handlers[exception_class] = handler + self.vdebug(f"Registered rule: {rule!r}") + self.rules.append(rule) return - # Decorators - # .......................................................................... - def rule(self, rule_: Union[Type[RuleBase], str, Pattern], *args, **kwargs): + def rule(self, rule_: type[RuleBase] | str | Pattern, *args, **kwargs): """Decorator for registering a function using [`smart_make_rule`][nserver.rules.smart_make_rule]. Changed in `2.0`: This method now uses `smart_make_rule`. @@ -184,342 +178,241 @@ def decorator(func: ResponseFunction): return decorator - def before_first_query(self): - """Decorator for registering before_first_query hook. - - These functions are called when the server receives it's first query, but - before any further processesing. - """ - - def decorator(func: middleware.BeforeFirstQueryHook): - self.register_before_first_query(func) - return func - return decorator +## Servers +## ----------------------------------------------------------------------------- +class RawNameServer( + MiddlewareMixin[m.RawMiddleware, m.RawExceptionHandlerMiddleware], LoggingMixin +): + """Server that handles raw `dnslib.DNSRecord` queries. - def before_query(self): - """Decorator for registering before_query hook. + This allows interacting with the underlying DNS messages from our dns library. + As such this server is implementation dependent and may change from time to time. - These functions are called before processing each query. - """ + In general you should use `NameServer` as it is implementation independent. - def decorator(func: middleware.BeforeQueryHook): - self.register_before_query(func) - return func + New in `3.0`. + """ - return decorator + def __init__(self, nameserver: NameServer) -> None: + self._exception_handler = m.RawExceptionHandlerMiddleware() + super().__init__() + self.nameserver: NameServer = nameserver + self.logger = self.get_logger() + return - def after_query(self): - """Decorator for registering after_query hook. + def process_request(self, request: m.RawRecord) -> m.RawRecord: + """Process a request using this server. - These functions are after the rule function is run and may modify the - response. + This will pass the request through the middleware stack. """ + if not self.middleware_is_prepared(): + self.prepare_middleware() + return self.middleware[0](request) - def decorator(func: middleware.AfterQueryHook): - self.register_after_query(func) - return func + def send_request_to_nameserver(self, record: m.RawRecord) -> m.RawRecord: + """Send a request to the `NameServer` of this instance. - return decorator - - def exception_handler(self, exception_class: Type[Exception]): - """Decorator for registering a function as an exception handler - - New in `2.0`. - - Args: - exception_class: The `Exception` class to register this handler for + Although this is the final step after passing a request through all middleware, + it can be called directly to avoid using middleware such as when testing. """ + response = record.reply() + + if record.header.opcode != dnslib.OPCODE.QUERY: + self.debug(f"Received non-query opcode: {record.header.opcode}") + # This server only response to DNS queries + response.header.rcode = dnslib.RCODE.NOTIMP + return response + + if len(record.questions) != 1: + self.debug(f"Received len(questions_ != 1 ({record.questions})") + # To simplify things we only respond if there is 1 question. + # This is apparently common amongst DNS server implementations. + # For more information see the responses to this SO question: + # https://stackoverflow.com/q/4082081 + response.header.rcode = dnslib.RCODE.REFUSED + return response - def decorator(func: middleware.ExceptionHandler): - nonlocal exception_class - self.register_exception_handler(exception_class, func) - return func - - return decorator - - ## Internal Functions - ## ------------------------------------------------------------------------- - def _prepare_query_middleware_stack(self) -> None: - """Prepare the `QueryMiddleware` for this server.""" - if self._query_middleware_stack: - # Note: we can use truthy expression as once processed there will always be at - # least one item in the stack - raise RuntimeError("QueryMiddleware stack already exists") - - middleware_stack: List[middleware.QueryMiddleware] = [ - self.exception_handler_middleware, - *self._user_query_middleware, - self.hook_middleware, - ] - rule_processor = middleware.RuleProcessor(self.rules) - - next_middleware: Optional[middleware.QueryMiddleware] = None - for query_middleware in middleware_stack[::-1]: - if next_middleware is None: - query_middleware.register_next_function(rule_processor) - else: - query_middleware.register_next_function(next_middleware) - next_middleware = query_middleware - - self._query_middleware_stack.extend(middleware_stack) - self._query_middleware_stack.append(rule_processor) + try: + query = Query.from_dns_question(record.questions[0]) + except ValueError: + # TODO: should we embed raw DNS query? Maybe this should be configurable. + self.warning("Failed to parse Query from request", exc_info=True) + response.header.rcode = dnslib.RCODE.FORMERR + return response + + result = self.nameserver.process_request(query) + + response.add_answer(*result.get_answer_records()) + response.add_ar(*result.get_additional_records()) + response.add_auth(*result.get_authority_records()) + response.header.rcode = result.error_code + return response + + def prepare_middleware(self) -> None: + super().prepare_middleware() + self.middleware[-1].set_next_function(self.send_request_to_nameserver) return - ## Logging - ## ------------------------------------------------------------------------- - def _vvdebug(self, *args, **kwargs): - """Log very verbose debug message.""" - - return self._logger.log(6, *args, **kwargs) - - def _vdebug(self, *args, **kwargs): - """Log verbose debug message.""" - - return self._logger.log(8, *args, **kwargs) - - def _debug(self, *args, **kwargs): - """Log debug message.""" - return self._logger.debug(*args, **kwargs) +class NameServer( + MiddlewareMixin[m.QueryMiddleware, m.QueryExceptionHandlerMiddleware], RulesMixin, LoggingMixin +): + """High level DNS Name Server for responding to DNS queries. - def _info(self, *args, **kwargs): - """Log very verbose debug message.""" + *Changed in `3.0`*: - return self._logger.info(*args, **kwargs) + - "Raw" functionality removed and moved to `RawNameServer`. + - "Transport" and "Application" functionality removed. + """ - def _warning(self, *args, **kwargs): - """Log warning message.""" + def __init__(self, name: str) -> None: + """ + Args: + name: The name of the server. This is used for internal logging. + """ + self.name = name + self._exception_handler = m.QueryExceptionHandlerMiddleware() + super().__init__() + self.hooks = m.HookMiddleware() + self.logger = self.get_logger() + return - return self._logger.warning(*args, **kwargs) + def _prepare_middleware_stack(self) -> list[m.QueryMiddleware]: + stack = super()._prepare_middleware_stack() + stack.append(self.hooks) + return stack - def _error(self, *args, **kwargs): - """Log an error message.""" + ## Register Methods + ## ------------------------------------------------------------------------- + def register_subserver( + self, nameserver: NameServer, rule_: type[RuleBase] | str | Pattern, *args, **kwargs + ) -> None: + """Register a `NameServer` using [`smart_make_rule`][nserver.rules.smart_make_rule]. - return self._logger.error(*args, **kwargs) + This allows for composing larger applications. - def _critical(self, *args, **kwargs): - """Log a critical message.""" + Args: + subserver: the `SubServer` to attach + rule_: rule as per `nserver.rules.smart_make_rule` + args: extra arguments to provide `smart_make_rule` + kwargs: extra keyword arguments to provide `smart_make_rule` - return self._logger.critical(*args, **kwargs) + Raises: + ValueError: if `func` is provided in `kwargs`. + New in `3.0`. + """ -class NameServer(Scaffold): - """NameServer for responding to requests.""" + if "func" in kwargs: + raise ValueError("Must not provide `func` in kwargs") + self.register_rule(smart_make_rule(rule_, *args, func=nameserver.process_request, **kwargs)) + return - # pylint: disable=too-many-instance-attributes + def register_before_first_query(self, func: m.BeforeFirstQueryHook) -> None: + """Register a function to be run before the first query. - def __init__(self, name: str, settings: Optional[Settings] = None) -> None: - """ Args: - name: The name of the server. This is used for internal logging. - settings: settings to use with this `NameServer` instance + func: the function to register """ - super().__init__(name) - self._logger = logging.getLogger(f"nserver.i.{self.name}") - - self.raw_exception_handler_middleware = middleware.RawRecordExceptionHandlerMiddleware() - self._user_raw_record_middleware: List[middleware.RawRecordMiddleware] = [] - self._raw_record_middleware_stack: List[ - Union[middleware.RawRecordMiddleware, middleware.RawRecordMiddlewareCallable] - ] = [] - - self.settings = settings if settings is not None else Settings() - - transport = TRANSPORT_MAP.get(self.settings.server_transport) - if transport is None: - raise ValueError( - f"Invalid settings.server_transport {self.settings.server_transport!r}" - ) - self.transport = transport(self.settings) - - self.shutdown_server = False - self.exit_code = 0 + self.hooks.before_first_query.append(func) return - ## Register Methods - ## ------------------------------------------------------------------------- - def register_raw_middleware(self, raw_middleware: middleware.RawRecordMiddleware) -> None: - """Add a `RawRecordMiddleware` to this server. - - New in `2.0`. + def register_before_query(self, func: m.BeforeQueryHook) -> None: + """Register a function to be run before every query. Args: - raw_middleware: the middleware to add + func: the function to register + If `func` returns anything other than `None` will stop processing the + incoming `Query` and continue to result processing with the return value. """ - if self._raw_record_middleware_stack: - # Note: we can use truthy expression as once processed there will always be at - # least one item in the stack - raise RuntimeError("Cannot register middleware after stack is created") - self._user_raw_record_middleware.append(raw_middleware) + self.hooks.before_query.append(func) return - def register_raw_exception_handler( - self, exception_class: Type[Exception], handler: middleware.RawRecordExceptionHandler - ) -> None: - """Register a raw exception handler for the `RawRecordMiddleware`. - - Only one handler can exist for a given exception type. - - New in `2.0`. + def register_after_query(self, func: m.AfterQueryHook) -> None: + """Register a function to be run on the result of a query. Args: - exception_class: the type of exception to handle - handler: the function to call when handling an exception + func: the function to register """ - if exception_class in self.raw_exception_handler_middleware.exception_handlers: - raise ValueError("Exception handler already exists for {exception_class}") - - self.raw_exception_handler_middleware.exception_handlers[exception_class] = handler + self.hooks.after_query.append(func) return # Decorators # .......................................................................... - def raw_exception_handler(self, exception_class: Type[Exception]): - """Decorator for registering a function as an raw exception handler - - New in `2.0`. + def before_first_query(self): + """Decorator for registering before_first_query hook. - Args: - exception_class: The `Exception` class to register this handler for + These functions are called when the server receives it's first query, but + before any further processesing. """ - def decorator(func: middleware.RawRecordExceptionHandler): - nonlocal exception_class - self.register_raw_exception_handler(exception_class, func) + def decorator(func: m.BeforeFirstQueryHook): + self.register_before_first_query(func) return func return decorator - ## Public Methods - ## ------------------------------------------------------------------------- - def run(self) -> int: - """Start running the server + def before_query(self): + """Decorator for registering before_query hook. - Returns: - `exit_code`, `0` if exited normally + These functions are called before processing each query. """ - # Setup Logging - console_logger = logging.StreamHandler() - console_logger.setLevel(self.settings.console_log_level) - console_formatter = logging.Formatter( - "[{asctime}][{levelname}][{name}] {message}", style="{" - ) + def decorator(func: m.BeforeQueryHook): + self.register_before_query(func) + return func - console_logger.setFormatter(console_formatter) + return decorator - self._logger.addHandler(console_logger) - self._logger.setLevel(min(self.settings.console_log_level, self.settings.file_log_level)) + def after_query(self): + """Decorator for registering after_query hook. - # Start Server - # TODO: Do we want to recreate the transport instance or do we assume that - # transport.shutdown_server puts it back into a ready state? - # We could make this configurable? :thonking: + These functions are after the rule function is run and may modify the + response. + """ - self._info(f"Starting {self.transport}") - try: - self._prepare_middleware_stacks() - self.transport.start_server() - except Exception as e: # pylint: disable=broad-except - self._critical(e) - self.exit_code = 1 - return self.exit_code - - # Process Requests - error_count = 0 - while True: - if self.shutdown_server: - break - try: - message = self.transport.receive_message() - response = self._process_dns_record(message.message) - message.response = response - self.transport.send_message_response(message) - except InvalidMessageError as e: - self._warning(f"{e}") - except Exception as e: # pylint: disable=broad-except - self._error(f"Uncaught error occured. {e}", exc_info=True) - error_count += 1 - if error_count >= self.settings.max_errors: - self._critical(f"Max errors hit ({error_count})") - self.shutdown_server = True - self.exit_code = 1 - except KeyboardInterrupt: - self._info("KeyboardInterrupt received.") - self.shutdown_server = True - - # Stop Server - self._info("Shutting down server") - self.transport.stop_server() - - # Teardown Logging - self._logger.removeHandler(console_logger) - return self.exit_code + def decorator(func: m.AfterQueryHook): + self.register_after_query(func) + return func + + return decorator ## Internal Functions ## ------------------------------------------------------------------------- - def _process_dns_record(self, message: dnslib.DNSRecord) -> dnslib.DNSRecord: - """Process the given DNSRecord by sending it into the `RawRecordMiddleware` stack. + def process_request(self, query: Query) -> Response: + """Process a query passing it through all middleware.""" + if not self.middleware_is_prepared(): + self.prepare_middleware() + return self.middleware[0](query) + + def prepare_middleware(self) -> None: + super().prepare_middleware() + self.middleware[-1].set_next_function(self.send_query_to_rules) + return - Args: - message: the DNS query to process + def send_query_to_rules(self, query: Query) -> Response: + """Send a query to be processed by the rules of this instance. - Returns: - the DNS response + Although intended to be the final step after passing a query through all middleware, + this method can be used to bypass the middleware of this server such as for testing. """ - if self._raw_record_middleware_stack is None: - raise RuntimeError( - "RawRecordMiddleware stack does not exist. Have you called _prepare_middleware?" - ) - return self._raw_record_middleware_stack[0](message) - - def _prepare_middleware_stacks(self) -> None: - """Prepare all middleware for this server.""" - self._prepare_query_middleware_stack() - self._prepare_raw_record_middleware_stack() - return + for rule in self.rules: + rule_func = rule.get_func(query) + if rule_func is not None: + self.debug(f"Matched Rule: {rule}") + return coerce_to_response(rule_func(query)) - def _prepare_raw_record_middleware_stack(self) -> None: - """Prepare the `RawRecordMiddleware` for this server.""" - if not self._query_middleware_stack: - # Note: we can use truthy expression as once processed there will always be at - # least one item in the stack - raise RuntimeError("Must prepare QueryMiddleware stack first") - - if self._raw_record_middleware_stack: - # Note: we can use truthy expression as once processed there will always be at - # least one item in the stack - raise RuntimeError("RawRecordMiddleware stack already exists") - - middleware_stack: List[middleware.RawRecordMiddleware] = [ - self.raw_exception_handler_middleware, - *self._user_raw_record_middleware, - ] - - query_middleware_processor = middleware.QueryMiddlewareProcessor( - self._query_middleware_stack[0] - ) - - next_middleware: Optional[middleware.RawRecordMiddleware] = None - for raw_middleware in middleware_stack[::-1]: - if next_middleware is None: - raw_middleware.register_next_function(query_middleware_processor) - else: - raw_middleware.register_next_function(next_middleware) - next_middleware = raw_middleware - - self._raw_record_middleware_stack.extend(middleware_stack) - self._raw_record_middleware_stack.append(query_middleware_processor) - return + self.debug("Did not match any rule") + return Response(error_code=dnslib.RCODE.NXDOMAIN) -class Blueprint(Scaffold): - """Class that can replicate many of the functions of a `NameServer`. +class Blueprint(RulesMixin, RuleBase, LoggingMixin): + """A container for rules that can be registered onto a server - They can be used to construct or extend applications. + It can be registered as normal rule: `server.register_rule(blueprint_rule)` - New in `2.0`. + New in `3.0`. """ def __init__(self, name: str) -> None: @@ -527,15 +420,16 @@ def __init__(self, name: str) -> None: Args: name: The name of the server. This is used for internal logging. """ - super().__init__(name) - self._logger = logging.getLogger(f"nserver.b.{self.name}") + super().__init__() + self.name = name + self.logger = self.get_logger() return - def entrypoint(self, query: Query) -> Response: - """Entrypoint into this `Blueprint`. - - This method should be passed to rules as the function to run. - """ - if not self._query_middleware_stack: - self._prepare_query_middleware_stack() - return self._query_middleware_stack[0](query) + def get_func(self, query: Query) -> ResponseFunction | None: + for rule in self.rules: + func = rule.get_func(query) + if func is not None: + self.debug(f"matched {rule}") + return func + self.debug("did not match any rule") + return None diff --git a/src/nserver/settings.py b/src/nserver/settings.py deleted file mode 100644 index bd439cf..0000000 --- a/src/nserver/settings.py +++ /dev/null @@ -1,35 +0,0 @@ -### IMPORTS -### ============================================================================ -## Standard Library -from dataclasses import dataclass -import logging - -## Installed - -## Application - - -### CLASSES -### ============================================================================ -@dataclass -class Settings: - """Dataclass for NameServer settings - - Attributes: - server_transport: What `Transport` to use. See `nserver.server.TRANSPORT_MAP` for options. - server_address: What address `server_transport` will bind to. - server_port: what port `server_port` will bind to. - """ - - server_transport: str = "UDPv4" - server_address: str = "localhost" - server_port: int = 9953 - console_log_level: int = logging.INFO - file_log_level: int = logging.INFO - max_errors: int = 5 - - # Not implemented, ideas for useful things - # debug: bool = False # Put server into "debug mode" (e.g. hot reload) - # health_check: bool = False # provde route for health check - # stats: bool = False # provide route for retrieving operational stats - # remote_admin: bool = False # allow remote shutdown restart etc? diff --git a/src/nserver/transport.py b/src/nserver/transport.py index f431a36..010a6a5 100644 --- a/src/nserver/transport.py +++ b/src/nserver/transport.py @@ -1,5 +1,8 @@ ### IMPORTS ### ============================================================================ +## Future +from __future__ import annotations + ## Standard Library from collections import deque from dataclasses import dataclass @@ -8,16 +11,15 @@ import socket import struct import time +from typing import Deque, NewType, Any, cast -# Note: Union can only be replaced with `X | Y` in 3.10+ -from typing import Tuple, Optional, Dict, List, Deque, NewType, Any, Union, cast ## Installed import dnslib +from pillar.logging import LoggingMixin ## Application from .exceptions import InvalidMessageError -from .settings import Settings ### CONSTANTS @@ -48,7 +50,7 @@ class TcpState(enum.IntEnum): ### FUNCTIONS ### ============================================================================ -def get_tcp_info(connection: socket.socket) -> Tuple: +def get_tcp_info(connection: socket.socket) -> tuple: """Get `socket.TCP_INFO` from socket Args: @@ -111,9 +113,9 @@ class MessageContainer: # pylint: disable=too-few-public-methods def __init__( self, raw_data: bytes, - transport: "TransportBase", + transport: TransportBase, transport_data: Any, - remote_client: Union[str, Tuple[str, int]], + remote_client: str | tuple[str, int], ): """Create new message container @@ -148,7 +150,7 @@ def __init__( self.transport = transport self.transport_data = transport_data self.remote_client = remote_client - self.response: Optional[dnslib.DNSRecord] = None + self.response: dnslib.DNSRecord | None = None return def get_response_bytes(self): @@ -160,16 +162,11 @@ def get_response_bytes(self): ## Transport Classes ## ----------------------------------------------------------------------------- -class TransportBase: +class TransportBase(LoggingMixin): """Base class for all transports""" - def __init__(self, settings: Settings) -> None: - """ - Args: - settings: settings of the server this transport is attached to - """ - self.settings = settings - # TODO: setup logging + def __init__(self) -> None: + self.logger = self.get_logger() return def start_server(self, timeout: int = 60) -> None: @@ -199,7 +196,7 @@ class UDPMessageData: remote_address: UDP peername that this message was received from """ - remote_address: Tuple[str, int] + remote_address: tuple[str, int] class UDPv4Transport(TransportBase): @@ -207,10 +204,10 @@ class UDPv4Transport(TransportBase): _SOCKET_AF = socket.AF_INET - def __init__(self, settings: Settings): - super().__init__(settings) - self.address = self.settings.server_address - self.port = self.settings.server_port + def __init__(self, address: str, port: int): + super().__init__() + self.address = address + self.port = port self.socket = socket.socket(self._SOCKET_AF, socket.SOCK_DGRAM) return @@ -284,7 +281,7 @@ class CachedConnection: """ connection: socket.socket - remote_address: Tuple[str, int] + remote_address: tuple[str, int] last_data_time: float selector_key: selectors.SelectorKey cache_key: CacheKey @@ -306,17 +303,17 @@ class TCPv4Transport(TransportBase): CONNECTION_CACHE_TARGET = int(CONNECTION_CACHE_LIMIT * CONNECTION_CACHE_VACUUM_PERCENT) CONNECTION_CACHE_CLEAN_INTERVAL = 10 # seconds - def __init__(self, settings: Settings) -> None: - super().__init__(settings) - self.address = self.settings.server_address - self.port = self.settings.server_port + def __init__(self, address: str, port: int) -> None: + super().__init__() + self.address = address + self.port = port self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.setblocking(False) # Allow taking over of socket when in TIME_WAIT (i.e. previously released) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.selector = selectors.DefaultSelector() - self.cached_connections: Dict[CacheKey, CachedConnection] = {} + self.cached_connections: dict[CacheKey, CachedConnection] = {} self.last_cache_clean = 0.0 self.connection_queue: Deque[socket.socket] = deque() @@ -380,8 +377,41 @@ def stop_server(self) -> None: def __repr__(self): return f"{self.__class__.__name__}(address={self.address!r}, port={self.port!r})" - def _get_next_connection(self) -> Tuple[socket.socket, Tuple[str, int]]: - """Get the next connection that is ready to receive data on.""" + def _get_next_connection(self) -> tuple[socket.socket, tuple[str, int]]: + """Get the next connection that is ready to receive data on. + + Blocks until a good connection is found + """ + while True: + if not self.connection_queue: + self._populate_connection_queue() + + # There is something in the queue - attempt to get it + connection = self.connection_queue.popleft() + + if not self._connection_viable(connection): + self._remove_connection(connection) + continue + + # Connection is probably viable + try: + remote_address = connection.getpeername() + except OSError as e: + if e.errno == 107: # Transport endpoint is not connected + self._remove_connection(connection) + continue + + raise # Unknown OSError - raise it. + + break # we have a valid connection + + return connection, remote_address + + def _populate_connection_queue(self) -> None: + """Populate self.connection_queue + + Blocks until there is at least on connection + """ while not self.connection_queue: # loop until connection is ready for execution events = self.selector.select(self.SELECT_TIMEOUT) @@ -413,13 +443,7 @@ def _get_next_connection(self) -> Tuple[socket.socket, Tuple[str, int]]: # No connections ready, take advantage to do cleanup elif time.time() - self.last_cache_clean > self.CONNECTION_CACHE_CLEAN_INTERVAL: self._cleanup_cached_connections() - - # We have a connection in the queue - # print(f"connection_queue: {self.connection_queue}") - connection = self.connection_queue.popleft() - remote_address = connection.getpeername() - - return connection, remote_address + return def _accept_connection(self) -> socket.socket: """Accept a connection, cache it, and add it to the selector""" @@ -471,7 +495,7 @@ def _connection_viable(connection: socket.socket) -> bool: def _cleanup_cached_connections(self) -> None: "Cleanup cached connections" now = time.time() - cache_clear: List[CacheKey] = [] + cache_clear: list[CacheKey] = [] for cache_key, cache in self.cached_connections.items(): if now - cache.last_data_time > self.CONNECTION_KEEPALIVE_LIMIT: if cache.connection not in self.connection_queue: @@ -485,7 +509,7 @@ def _cleanup_cached_connections(self) -> None: for cache_key in cache_clear: self._remove_connection(cache_key=cache_key) - quiet_connections: List[CachedConnection] = [] + quiet_connections: list[CachedConnection] = [] cached_connections_len = len(self.cached_connections) cache_clear = [] @@ -516,7 +540,7 @@ def _cleanup_cached_connections(self) -> None: return def _remove_connection( - self, connection: Optional[socket.socket] = None, cache_key: Optional[CacheKey] = None + self, connection: socket.socket | None = None, cache_key: CacheKey | None = None ) -> None: """Remove a connection from the server (closing it in the process) diff --git a/src/nserver/util.py b/src/nserver/util.py index 261add3..191fad7 100644 --- a/src/nserver/util.py +++ b/src/nserver/util.py @@ -1,5 +1,8 @@ ### IMPORTS ### ============================================================================ +## Future +from __future__ import annotations + ## Standard Library ## Installed diff --git a/tests/test_blueprint.py b/tests/test_blueprint.py index e801dc8..30a6fa6 100644 --- a/tests/test_blueprint.py +++ b/tests/test_blueprint.py @@ -3,15 +3,11 @@ ### IMPORTS ### ============================================================================ ## Standard Library -from typing import no_type_check, List -import unittest.mock - ## Installed import dnslib import pytest -from nserver import NameServer, Blueprint, Query, Response, ALL_QTYPES, ZoneRule, A -from nserver.server import Scaffold +from nserver import NameServer, RawNameServer, Blueprint, Query, A ## Application @@ -22,6 +18,7 @@ blueprint_1 = Blueprint("blueprint_1") blueprint_2 = Blueprint("blueprint_2") blueprint_3 = Blueprint("blueprint_3") +raw_server = RawNameServer(server) ## Rules @@ -34,100 +31,11 @@ def dummy_rule(query: Query) -> A: return A(query.name, IP) -## Hooks -## ----------------------------------------------------------------------------- -def register_hooks(scaff: Scaffold) -> None: - scaff.register_before_first_query(unittest.mock.MagicMock(wraps=lambda: None)) - scaff.register_before_query(unittest.mock.MagicMock(wraps=lambda q: None)) - scaff.register_after_query(unittest.mock.MagicMock(wraps=lambda r: r)) - return - - -@no_type_check -def reset_hooks(scaff: Scaffold) -> None: - scaff.hook_middleware.before_first_query_run = False - scaff.hook_middleware.before_first_query[0].reset_mock() - scaff.hook_middleware.before_query[0].reset_mock() - scaff.hook_middleware.after_query[0].reset_mock() - return - - -def reset_all_hooks() -> None: - reset_hooks(server) - reset_hooks(blueprint_1) - reset_hooks(blueprint_2) - reset_hooks(blueprint_3) - return - - -@no_type_check -def check_hook_call_count(scaff: Scaffold, bfq_count: int, bq_count: int, aq_count: int) -> None: - assert scaff.hook_middleware.before_first_query[0].call_count == bfq_count - assert scaff.hook_middleware.before_query[0].call_count == bq_count - assert scaff.hook_middleware.after_query[0].call_count == aq_count - return - - -register_hooks(server) -register_hooks(blueprint_1) -register_hooks(blueprint_2) -register_hooks(blueprint_3) - - -## Exception handling -## ----------------------------------------------------------------------------- -class ErrorForTesting(Exception): - pass - - -@server.rule("throw-error.com", ["A"]) -def throw_error(query: Query) -> None: - raise ErrorForTesting() - - -def _query_error_handler(query: Query, exception: Exception) -> Response: - # pylint: disable=unused-argument - return Response(error_code=dnslib.RCODE.SERVFAIL) - - -query_error_handler = unittest.mock.MagicMock(wraps=_query_error_handler) -server.register_exception_handler(ErrorForTesting, query_error_handler) - - -class ThrowAnotherError(Exception): - pass - - -@server.rule("throw-another-error.com", ["A"]) -def throw_another_error(query: Query) -> None: - raise ThrowAnotherError() - - -def bad_error_handler(query: Query, exception: Exception) -> Response: - # pylint: disable=unused-argument - raise ErrorForTesting() - - -server.register_exception_handler(ThrowAnotherError, bad_error_handler) - - -def _raw_record_error_handler(record: dnslib.DNSRecord, exception: Exception) -> dnslib.DNSRecord: - # pylint: disable=unused-argument - response = record.reply() - response.header.rcode = dnslib.RCODE.SERVFAIL - return response - - -raw_record_error_handler = unittest.mock.MagicMock(wraps=_raw_record_error_handler) -server.register_raw_exception_handler(ErrorForTesting, raw_record_error_handler) - ## Get server ready ## ----------------------------------------------------------------------------- -server.register_blueprint(blueprint_1, ZoneRule, "b1.com", ALL_QTYPES) -server.register_blueprint(blueprint_2, ZoneRule, "b2.com", ALL_QTYPES) -blueprint_2.register_blueprint(blueprint_3, ZoneRule, "b3.b2.com", ALL_QTYPES) - -server._prepare_middleware_stacks() +server.register_rule(blueprint_1) +server.register_rule(blueprint_2) +blueprint_2.register_rule(blueprint_3) ### TESTS @@ -136,7 +44,7 @@ def _raw_record_error_handler(record: dnslib.DNSRecord, exception: Exception) -> ## ----------------------------------------------------------------------------- @pytest.mark.parametrize("question", ["s.com", "b1.com", "b2.com", "b3.b2.com"]) def test_response(question: str): - response = server._process_dns_record(dnslib.DNSRecord.question(question)) + response = raw_server.process_request(dnslib.DNSRecord.question(question)) assert len(response.rr) == 1 assert response.rr[0].rtype == 1 assert response.rr[0].rname == question @@ -145,40 +53,7 @@ def test_response(question: str): @pytest.mark.parametrize("question", ["miss.s.com", "miss.b1.com", "miss.b2.com", "miss.b3.b2.com"]) def test_nxdomain(question: str): - response = server._process_dns_record(dnslib.DNSRecord.question(question)) + response = raw_server.process_request(dnslib.DNSRecord.question(question)) assert len(response.rr) == 0 assert response.header.rcode == dnslib.RCODE.NXDOMAIN return - - -## Hooks -## ----------------------------------------------------------------------------- -@pytest.mark.parametrize( - "question,hook_counts", - [ - ("s.com", [1, 5, 5]), - ("b1.com", [1, 5, 5, 1, 5, 5]), - ("b2.com", [1, 5, 5, 0, 0, 0, 1, 5, 5]), - ("b3.b2.com", [1, 5, 5, 0, 0, 0, 1, 5, 5, 1, 5, 5]), - ], -) -def test_hooks(question: str, hook_counts: List[int]): - ## Setup - # fill unset hook_counts - hook_counts += [0] * (12 - len(hook_counts)) - assert len(hook_counts) == 12 - # reset hooks - reset_all_hooks() - - ## Test - for _ in range(5): - response = server._process_dns_record(dnslib.DNSRecord.question(question)) - assert len(response.rr) == 1 - assert response.rr[0].rtype == 1 - assert response.rr[0].rname == question - - check_hook_call_count(server, *hook_counts[:3]) - check_hook_call_count(blueprint_1, *hook_counts[3:6]) - check_hook_call_count(blueprint_2, *hook_counts[6:9]) - check_hook_call_count(blueprint_3, *hook_counts[9:]) - return diff --git a/tests/test_server.py b/tests/test_server.py index f8561f4..9a12db0 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -10,7 +10,7 @@ import dnslib import pytest -from nserver import NameServer, Query, Response, A +from nserver import NameServer, RawNameServer, Query, Response, A ## Application @@ -18,6 +18,7 @@ ### ============================================================================ IP = "127.0.0.1" server = NameServer("tests") +raw_server = RawNameServer(server) ## Rules @@ -106,11 +107,10 @@ def _raw_record_error_handler(record: dnslib.DNSRecord, exception: Exception) -> raw_record_error_handler = unittest.mock.MagicMock(wraps=_raw_record_error_handler) -server.register_raw_exception_handler(ErrorForTesting, raw_record_error_handler) +raw_server.register_exception_handler(ErrorForTesting, raw_record_error_handler) ## Get server ready ## ----------------------------------------------------------------------------- -server._prepare_middleware_stacks() ### TESTS @@ -118,13 +118,13 @@ def _raw_record_error_handler(record: dnslib.DNSRecord, exception: Exception) -> ## NameServer._process_dns_record ## ----------------------------------------------------------------------------- def test_none_response(): - response = server._process_dns_record(dnslib.DNSRecord.question("none-response.com")) + response = raw_server.process_request(dnslib.DNSRecord.question("none-response.com")) assert len(response.rr) == 0 return def test_response_response(): - response = server._process_dns_record(dnslib.DNSRecord.question("response-response.com")) + response = raw_server.process_request(dnslib.DNSRecord.question("response-response.com")) assert len(response.rr) == 1 assert response.rr[0].rtype == 1 assert response.rr[0].rname == "response-response.com." @@ -132,7 +132,7 @@ def test_response_response(): def test_record_response(): - response = server._process_dns_record(dnslib.DNSRecord.question("record-response.com")) + response = raw_server.process_request(dnslib.DNSRecord.question("record-response.com")) assert len(response.rr) == 1 assert response.rr[0].rtype == 1 assert response.rr[0].rname == "record-response.com." @@ -140,7 +140,7 @@ def test_record_response(): def test_multi_record_response(): - response = server._process_dns_record(dnslib.DNSRecord.question("multi-record-response.com")) + response = raw_server.process_request(dnslib.DNSRecord.question("multi-record-response.com")) assert len(response.rr) == 2 for record in response.rr: assert record.rtype == 1 @@ -160,12 +160,12 @@ def test_multi_record_response(): ) def test_hook_call_count(hook, call_count): # Setup - server.hook_middleware.before_first_query_run = False + server.hooks.before_first_query_run = False hook.reset_mock() # Test for _ in range(5): - response = server._process_dns_record(dnslib.DNSRecord.question("dummy.com")) + response = raw_server.process_request(dnslib.DNSRecord.question("dummy.com")) # Ensure respone returns and unchanged assert len(response.rr) == 1 assert response.rr[0].rtype == 1 @@ -183,7 +183,7 @@ def test_query_error_handler(): raw_record_error_handler.reset_mock() # Test - response = server._process_dns_record(dnslib.DNSRecord.question("throw-error.com")) + response = raw_server.process_request(dnslib.DNSRecord.question("throw-error.com")) assert len(response.rr) == 0 assert response.header.get_rcode() == dnslib.RCODE.SERVFAIL @@ -199,7 +199,7 @@ def test_raw_record_error_handler(): raw_record_error_handler.reset_mock() # Test - response = server._process_dns_record(dnslib.DNSRecord.question("throw-another-error.com")) + response = raw_server.process_request(dnslib.DNSRecord.question("throw-another-error.com")) assert len(response.rr) == 0 assert response.header.get_rcode() == dnslib.RCODE.SERVFAIL diff --git a/tests/test_subserver.py b/tests/test_subserver.py new file mode 100644 index 0000000..ee5c9b9 --- /dev/null +++ b/tests/test_subserver.py @@ -0,0 +1,174 @@ +# pylint: disable=missing-class-docstring,missing-function-docstring,protected-access + +### IMPORTS +### ============================================================================ +## Standard Library +from typing import no_type_check, List +import unittest.mock + +## Installed +import dnslib +import pytest + +from nserver import NameServer, RawNameServer, Query, Response, ALL_QTYPES, ZoneRule, A + +## Application + +### SETUP +### ============================================================================ +IP = "127.0.0.1" +nameserver = NameServer("test_subserver") +subserver_1 = NameServer("subserver_1") +subserver_2 = NameServer("subserver_2") +subserver_3 = NameServer("subserver_3") +raw_nameserver = RawNameServer(nameserver) + + +## Rules +## ----------------------------------------------------------------------------- +@nameserver.rule("s.com", ["A"]) +@subserver_1.rule("sub1.com", ["A"]) +@subserver_2.rule("sub2.com", ["A"]) +@subserver_3.rule("sub3.sub2.com", ["A"]) +def dummy_rule(query: Query) -> A: + return A(query.name, IP) + + +## Hooks +## ----------------------------------------------------------------------------- +def register_hooks(server: NameServer) -> None: + server.register_before_first_query(unittest.mock.MagicMock(wraps=lambda: None)) + server.register_before_query(unittest.mock.MagicMock(wraps=lambda q: None)) + server.register_after_query(unittest.mock.MagicMock(wraps=lambda r: r)) + return + + +@no_type_check +def reset_hooks(server: NameServer) -> None: + server.hooks.before_first_query_run = False + server.hooks.before_first_query[0].reset_mock() + server.hooks.before_query[0].reset_mock() + server.hooks.after_query[0].reset_mock() + return + + +def reset_all_hooks() -> None: + reset_hooks(nameserver) + reset_hooks(subserver_1) + reset_hooks(subserver_2) + reset_hooks(subserver_3) + return + + +@no_type_check +def check_hook_call_count(server: NameServer, bfq_count: int, bq_count: int, aq_count: int) -> None: + assert server.hooks.before_first_query[0].call_count == bfq_count + assert server.hooks.before_query[0].call_count == bq_count + assert server.hooks.after_query[0].call_count == aq_count + return + + +register_hooks(nameserver) +register_hooks(subserver_1) +register_hooks(subserver_2) +register_hooks(subserver_3) + + +## Exception handling +## ----------------------------------------------------------------------------- +class ErrorForTesting(Exception): + pass + + +@nameserver.rule("throw-error.com", ["A"]) +def throw_error(query: Query) -> None: + raise ErrorForTesting() + + +def _query_error_handler(query: Query, exception: Exception) -> Response: + # pylint: disable=unused-argument + return Response(error_code=dnslib.RCODE.SERVFAIL) + + +query_error_handler = unittest.mock.MagicMock(wraps=_query_error_handler) +nameserver.register_exception_handler(ErrorForTesting, query_error_handler) + + +class ThrowAnotherError(Exception): + pass + + +@nameserver.rule("throw-another-error.com", ["A"]) +def throw_another_error(query: Query) -> None: + raise ThrowAnotherError() + + +def bad_error_handler(query: Query, exception: Exception) -> Response: + # pylint: disable=unused-argument + raise ErrorForTesting() + + +nameserver.register_exception_handler(ThrowAnotherError, bad_error_handler) + + +## Get server ready +## ----------------------------------------------------------------------------- +nameserver.register_subserver(subserver_1, ZoneRule, "sub1.com", ALL_QTYPES) +nameserver.register_subserver(subserver_2, ZoneRule, "sub2.com", ALL_QTYPES) +subserver_2.register_subserver(subserver_3, ZoneRule, "sub3.sub2.com", ALL_QTYPES) + + +### TESTS +### ============================================================================ +## Responses +## ----------------------------------------------------------------------------- +@pytest.mark.parametrize("question", ["s.com", "sub1.com", "sub2.com", "sub3.sub2.com"]) +def test_response(question: str): + response = raw_nameserver.process_request(dnslib.DNSRecord.question(question)) + assert len(response.rr) == 1 + assert response.rr[0].rtype == 1 + assert response.rr[0].rname == question + return + + +@pytest.mark.parametrize( + "question", ["miss.s.com", "miss.sub1.com", "miss.sub2.com", "miss.sub3.sub2.com"] +) +def test_nxdomain(question: str): + response = raw_nameserver.process_request(dnslib.DNSRecord.question(question)) + assert len(response.rr) == 0 + assert response.header.rcode == dnslib.RCODE.NXDOMAIN + return + + +## Hooks +## ----------------------------------------------------------------------------- +@pytest.mark.parametrize( + "question,hook_counts", + [ + ("s.com", [1, 5, 5]), + ("sub1.com", [1, 5, 5, 1, 5, 5]), + ("sub2.com", [1, 5, 5, 0, 0, 0, 1, 5, 5]), + ("sub3.sub2.com", [1, 5, 5, 0, 0, 0, 1, 5, 5, 1, 5, 5]), + ], +) +def test_hooks(question: str, hook_counts: List[int]): + ## Setup + # fill unset hook_counts + hook_counts += [0] * (12 - len(hook_counts)) + assert len(hook_counts) == 12 + # reset hooks + reset_all_hooks() + + ## Test + for _ in range(5): + response = raw_nameserver.process_request(dnslib.DNSRecord.question(question)) + assert len(response.rr) == 1 + assert response.rr[0].rtype == 1 + assert response.rr[0].rname == question + + check_hook_call_count(nameserver, *hook_counts[:3]) + check_hook_call_count(subserver_1, *hook_counts[3:6]) + check_hook_call_count(subserver_2, *hook_counts[6:9]) + check_hook_call_count(subserver_3, *hook_counts[9:]) + return diff --git a/tox.ini b/tox.ini index 5e7f556..0aaa4fb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,24 @@ [tox] -envlist = py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39 +requires = tox>=3,tox-uv +envlist = pypy{38,39,310}, py{38,39,310,311,312,313} [testenv] -package = external -deps = pytest -commands = {posargs:pytest -ra tests} +description = run unit tests +extras = dev +commands = + pytest tests -[testenv:.pkg_external] -package_glob = /code/dist/* +[testenv:format] +description = run formatters +extras = dev +commands = + black src tests + +[testenv:lint] +description = run linters +extras = dev +commands = + validate-pyproject pyproject.toml + black --check --diff src tests + pylint src + mypy src tests