From ad9f0a6ecbbb3fa26b60083742379a58907bea3c Mon Sep 17 00:00:00 2001 From: jaketothepast Date: Sun, 17 Feb 2019 13:38:37 -0500 Subject: [PATCH] Finished post on proxies --- content/post/node-http-proxy.md | 120 ++++++++++++++++++++++++++++++++ static/img/post/proxy.png | Bin 0 -> 11809 bytes 2 files changed, 120 insertions(+) create mode 100644 content/post/node-http-proxy.md create mode 100644 static/img/post/proxy.png diff --git a/content/post/node-http-proxy.md b/content/post/node-http-proxy.md new file mode 100644 index 0000000..dfe798d --- /dev/null +++ b/content/post/node-http-proxy.md @@ -0,0 +1,120 @@ +--- +title: "HTTP Proxies: AKA School Firewall Evaders" +date: 2019-02-17T12:57:51-05:00 +draft: false +tags: ["node", "side project", "proxy", "networking"] +--- + +As a fun little side project, and because I work with proxies a lot in my current job at Verodin, I decided to build an HTTP proxy. I fiddled with several language choices, but eventually landed on Node.js as it's event-driven default nature lends itself well to networking applications. I also wanted to take advantage of cool language features like closures, which only exist in languages like Javascript and Common Lisp for reasons unbeknownst to me. Before I talk about my implementation, and stumbles along the way, first I'll give a quick explanation on HTTP proxies. + +# HTTP Proxies + +![Image](/img/post/proxy.png) + + +A proxy, like the one shown above, is often used as a gateway between two servers or networks of servers. In short, a proxy will be handed requests from servers on one side of the proxy (often an internal network), and the proxy will send the request off to wherever the server wanted it to go. The server's intended recipient will reply directly to the proxy, which will then forward the reply back to the server who sent the request. + +HTTP proxies work on an application level, and will only mediate the sending and receiving of HTTP requests/responses. + +Proxies are useful tools for several reasons, many pertaining to security. At a base level, a proxy can log any and all traffic that goes through it, making monitoring of traffic to and from a network a breeze. In addition, proxies can have extra capabilities. One of the earliest uses of proxy servers were as caching clients. Any frequently used resources were cached by the proxy, saving money on bandwidth and processing costs. + +# My Node.js implementation. + +In all it's glory, can be seen on my Github (link on the side). There are some key features of Node.js, particularly the http library, that allowed me to make the proxy in a simple way. In the end, I went with a combination of the http Node.js library, the `winston` logging library from NPM, sqlite3 and the sqlite3 Javascript library for making queries, and closures. + +## Starting out + +Starting the code was simple enough, I had to create an http server that I could begin passing requests to. That function looked something like this: + +```javascript +/** + * Start the server. + */ +function startServer() { + // Start the server, set up data and end handlers. + var server = http.createServer(); + + // Start the server + server.listen(8124, () => { + console.log("Server bound."); + }); + + return server; +} +``` + +Once I had the server running, I had to figure out how requests to HTTP proxies should be structured. To do some testing, I wrote code with the `py.test` library and `requests`. + +```python +http_proxy = "http://127.0.0.1:8124" + +def test_proxy_basic(): + resp = requests.get("http://httpbin.org", proxies={"http": http_proxy}) + assert resp.status_code == 200 +``` + +With the test code in place, I modified my proxy with a console.log to show the request object, and used py.test to fire sample requests at the proxy. What I found was surprising. + +```json + trailers: {}, + rawTrailers: [], + aborted: false, + upgrade: false, + url: 'http://httpbin.org/', + method: 'GET', + statusCode: null, + statusMessage: null, +``` + +The URL field of the `IncomingRequest` object was the full URI, versus being just a path off of the host. With this information in mind, I realized that utilizing the URL field this way allowed for HTTP proxies to easily parse the URL and forward the request without incident, so I set to work building a parsing function. My parsing function, affectionately known as `processRequest` was designed with closures in mind. + +```javascript +/** + * Connection handler for proxy, forward request and then pipe response + * back to the client. + * + * @param {IncomingMessage} request The request from the client + * @param {OutgoingMessage} response The response to write back to the client + */ +const processRequest = (request, response) => { + let body = []; + // Use closure for request. + let req = request; + let res = response; + + // Set up a data handler for the socket connection. + request.on("data", (chunk) => { + body.push(chunk); + }); + + // Set up an end handler for the socket connection. + request.on("end", () => { + console.log(req) + body = Buffer.concat(body).toString(); + logger.info({message: "Requesting: " + req.url}); + + const hostUrl = new url.URL(req.url); + + // Do the work of actually updating our db with the visit. + // Val in the callback will be false if blocked. + myDb.visitHost(hostUrl.hostname, (val) => { + if (val === false) { + // blocked. + console.log("Blocked you fool.") + sendBlockPage(res); + } else { + // Not blocked + req.pipe(http.request(req.url, (resp) => { + resp.pipe(res); + })) + } + }); + }); +} +``` + +It took a few iterations to get here but I'll just explain the end product. I first use let bindings to bind `req`, and `res`, needing both of them in a closure to properly forward the request and pipe the response back to the client. I then set up two socket event handlers, `end` and `data`. On `end`, meaning that the proxy received all of the data from the client, my proxy asks the DB to check out the host. As configured currently, the `myDb.visitHost` function will query `sqlite` to see if the host should be blocked. If the host should be blocked, `visitHost` calls the callback with `false`. If the host is allowed, using a closure, I pipe the request to a new http request using the url field. Then, in the reply callback for the piped request, I pipe the reply from the outer host back to the client who sent the request to the proxy. + +My proxy logs all traffic that goes through it, and allows users to configure hosts that they want to block in a file `blocked.json` which gets saved to the database each time the proxy is started. + +I had a lot of fun building this project, and look forward to my next one soon! \ No newline at end of file diff --git a/static/img/post/proxy.png b/static/img/post/proxy.png new file mode 100644 index 0000000000000000000000000000000000000000..4ba157e2a79dc60d56081ce6b2a6b353a876645e GIT binary patch literal 11809 zcmZ{KWmr^g7w*tQNq4=364H%Q4vn-!cMC{&H%JQ7Ae}>ZN(s`UbVy3KAV^4@hwuD4 zKhE~L5XZe|Keg5!YelK4$l+p9VnHAfTm^Y)4G08@9QAwgThU$972 zHi^i1>6*JrhY|)I;_ZX1TpkG2m>#?4{`u6lICF2`D?V~jDmeAJJJw%~B$@Sf(ss5ai$(@rBFU=Q!p9c9sbRjD! zwWN}?K?abi=h-`Wd8j>NkeV6w+>utKD~uq*hbL7x$gP+`9KN0vrADHnqIzp=LAsEB z$O>|Tj)wtsjXFqmWF=g4e*zMLEYJ2&3Brz?hiAe+js9y&F}Hy&`;rF|T7RaW5 zYUzNiW8k48(lWA1k=nUh`G3DK=xetaA#zX!T(JIdK2LFw6h$5>uiQnv_fO~2zIa;q zLE4?Y+HAnq+2T2(vf-7$;K&xGR-!&xr1=v9aDCP_S{Ett%g+u71()K@mem)m_JCNfeE+YRkE#&ksC z=Q#+(H(!tm@>?+IWc1z$J1_6B2_2V@d*)kja@t&xs-^d_8Wn!^@8 z&*|^qjmTFRnWcJHnoP9+yK+)7itW(6+#+*xjj0fDTN-k5FVDUR+!Hb1zwGZfg!!Ti zHnU&2FTQkK`JCX`b!}Cs^z6XQ*7j!ZY-?1=VWHl#cdArXE=IbbQowGOUD$2+M_GsG z31<`D$?TCnthG_ z4y9j>mS*rcT5OU*AP`5K2Cb0zjUR|N%)&2U_AM<@M@2;qjf{j87BW&(Qzs-Q2LHP} zdHj5Qe4K@yz5iR55It!Ok4X=NB}GwMPA(KbB;fAmm9a4$4-XFuE9<&t^M4tEx2G7@ zh8-pKb#)5qV2<@9&$U==GF5cC1MZS;maN+axn&_%Sm9sx6pLLT^kF>w{BUw|^7=3* z_^VgJItVx~PTvqqbbBS7O%?eHbARSZj?dX%X>K$wF7B#PhX+S?W@bC9nAq~f%nXVB zV&g=Ut?0K;Jv}{>x3_-L8?83M!NJV?da7yY1N#5rUtyz$2$~PXuCE06R!+{$C|f!> z7}YVng1(ZKeL+LxpVH=c&ER@}>z!$-`;^{-ZqRuZGBllk&w(W&A(3dn*)6X=Z6_`+ z-WV075V{GbZP4z9x$`|&PEU_=Qsa#}Tt{c)nqRqZWRdcys%rh$FJvw*F1P(@M85Gx zO3;QdSf!1z3_f`k6MshC5JE>JwsP$ff;$hYwkqTejc;In2W<+y>bG~54t!Qo0uLh@!Xo`&Ff8;h{?YCS zv+F~Nj6-8%(j6X0CXSAH&!0ahr=ptE6XN2+Yin!s`n`sPg^fJ{juUuvqv5-FXnuaa z>;=eJs%=i|U*2RWZMRvDrhZ-=w|7C2nmT`xDBtM!?{9WA0i!Gn6@%2XBz67fccdq) z-Kt>KZBi5kaBZ2na@ek}uPM}so4jVqHC-tp_6$~Ucr$P+v}*?b+W#~uFOB?QV8GpM zr|l*rRMW!gQcRfkMg!mFk`qQqG=VUed$-U|vM~@>(NZ}ZrdSM@z-U_p83kpd-Th!& z)bvZ3&x|kFz=*C>bW|6SC*I!qrQzXP=Rci~C}-tl*8A`z`h!C;j9D3zl9EE{r6T@& z`I73T=W%}!5=!Hwio85jt4c3iXvGV~)YO!cDwPn1PfYB_ARyo#)!iNF;=TjrpVU}> zJ6L~McDa)EY^q4j*Vp&>>eS?V8j)nr%g5KJn|g#NSw1YN#}rJRtf>)743wcFkz2{;9_divim!(qX!Z%XnS8;QH`Z=Sz! z+s$igex{s>Dkmogwh<%O?+ml+;r?Hmb~#EI{^rikhtS1v3tRUc{w=Lw+A#cfxeqyP z%GeHb)il?8rHoi&Vx64)%7GO&*BpskqiG#Ah8=MA#VKQcQMyWYD0myya5-16hom2A~R6c?v{K_9pb(>c@)3C`n*ESuyO&?mF zYbSs2OWFIa9mX#B-p#HC-{)GYNI&s5)MWbNQ2ckVi`H(wy*Vv8u@Bo?=2U` z84~5UwztRs9xrFr*VZznrlx*sYZIXl11GYW7z=H3YHF{)Uz&}c-YDbKr^-?d#M@v= z{BQM#JP>*{U*Z;WWMpJN_jE+CAFInW;}B%I*dGNRGjG)NqUWAaOZKF4 zq)P+_B(3*|nLTpr9BZ}b(peEj%vx6XW(SB-@@wxJ;g#*kQh_YY5P*7~nOzS@7h zyc$k_4=H-D|Ll~3#lS>E?v*Llpn$A-+V52>KRNzAHdll@Uw)+?eqR1`J} z4U5-*M@Xnu6k)bLFW$Ca*#GTz-aubkTEeHM#?jW+&fb0e1%Qf$g+*Tele82OKo}Sp z7$)axj2*SXC?7&?7!r5KGx$mc8dS7U0EzfD{z>Hd-By}$jOr(73NGcQI@bm=gdq{i zC>yJWj_$zT86c+BZ2eu_2AnPoWZ~!dPzK2PRP-6;m+#+I8%_Hns?g9beyS7Pj&kif zT9@*Hr#Cv>rDUz1-QOH=W$)!FhlGZj+Sye+{!dO$j)^YmWAr9k+u30!%EPN(dM4dV zvk*szGOK(FGN`W8+_!ahI~)SJ9>HI?sW8PKfk#&@>UYI_U1#1+e7QeUAt9r#ofp40 z%f77kT2YZ2z=2^yL&JfA90LAp+Od0`gQnxu)BY!EHV3=YFX`--jz$(34EB3F%W)qb zDQLfF(i4P4*)hP$$*BVl+Q9xuW;@=El)5@Wi{}aL>8PrG!1>WnZ4zo~jdqWtxyLvy zMh^bX8}zq*wt@|(PgH=HccihtzT7oC5|56dCJ$15QC7&Y^s}jN=DD_8Rm4Ezutlru z&J&-@<0?DNwf+6*-QBlwLmDi^!anD_>EL~fJr?Eg^6b@s4h624FFRy>;OfNTcoLtg zp}1n2w&vzXZ1~V>B&X>nqm4AwCjXw8~RxQzgp2jsvk2PHt|}kxyuhC_k2>k z%L)yytFI4%H7g5$y$%+CueP9O%WxX+O|ORC`@j zRaTl-luVPTWr5VQ&! zYHFkRtob%5<`9#3HI{Rbd%+ei#&BXL-6|7y@p%LsPaLPJC2 z23Z(ujd+?>FhFX`v5Or^jn}LnN}@bf-sZZ)s~M<^jfLfOd2G1U^Y&dy+T*lTNR zkl?&MkB;|W&2;qj`xY152fUsGdUkodECquhdz`Gq0SysazycJ8c9=2?8{3!uC?aQ& zp3@>s-(osZM>4DCzz?Pohz>c3Ntfh*R{#;GJXfwAdQm+Xs zOUQ8`i{|AQd3IR~MJ$8GroM+b8tgaDQ)9zvwd|bFm>G9(+zm#Ddg?7l*VfmAdqXki zY@m|{>IitN_n&7Thx5UA|1QmUf8;+NAh3Z1IqU{HF0FwzDt(-vjSxek|sVmr#-zOKUu{a$soqPc> zg7wvm2k^9oivyt{B_*YtfAM0zFD6b;Pdi6^k=wUHyktk6UP&G^zI%(_&iH8n9eu_be~Exg0-jk^Qerle%&Jpwf%&y{1d_U)|N(=pnYnSjk!V0I#V8tVuebWyXDtt60v}X zyGvceb~nVOZ2R@`ar(*elQxa0#sUWEqM|iLpBFcJ`cgyOlyeu4Fr@Oj4+{@(J{6bj z$lrb=%#=^ofv9&F?o1v868P5h#UzW`%7e7|6H>-wX zPLB@_uq3xy%M=JGKIs%vev_2^EgqBT*)-zy*>BU2iur2%G0aj3cs;+ekS(D)JAPh6 z2E_|ImMeAT)+R^1mcQS|Q{)f0aZvR2_2(BCo3Gvn05P|`EQVODM<23dCX6az=lF>W zeIhmQigN*6bRsTFE1BDRlvtznI$gKe1by7j)Pvy%dbuu(EsozyJ=Sdqi!@lF^srz_ z6V$b~;(ebBlQ7siC(u}tV7jbc8wEK_H@{*me}PQIrkk1_Z}ky10N4pnezrJXejD*I zl^^RXbbjG+2xVq2{Q<;Uu@*c?cU zG5Dd0p~Ls`nDVu@293A3Hw!0cvnBZewdPsU%uEkXQ;TPUu8z*E4Sf8ynilKt zmu8tImrm4|tU*>H=-0nuxE8B)8?3(WZsr?9_+0T*f|$NJ%`~&V=?L z#cy~>bs|nVGuH)7$p;iU(^^>B*{9zJpYG2drJQTpDBfHgME>}pRy8#HR}dS}es;t5 zbKmF!qu&%26~Xja;h$%`_h!*jEqTlbA|n73msOcM*G#nxh1^y$k{C-G z8$Wav7jL;ge_qj*nP6RAv&yvh+Q>3CE^htsu+mO|vcJLlyAw!J6n}QUuQq^@@5Cw@ z;*RxAPfu$*xM(BbX2T!9qQml`qpAM%2UCM72MZ06(FH8T#Kan&$4gd^L5l#sAB<8{ zzcH+70y6|2R$I$0>VHEVL&`DP^aA%8BO?lcK$LG|W0gona8593%-1R^MoFoudKT7m z>nVG~P*h~U+tY%{58|1YURxupHw=mI0gFfghR5^mZ&}PI^L30u4xh-V2ng~&2{|6S zr6{^12YKAIUGxkN4tkBIa_|8=#+ZO1uewOwH^Nmx;fha$qQu81eSb5oS5FB?utAm1 z>+Y8C@4!GvK8p@H>p(Q3r8e(xL5sI>kaKlOHTIN+T1*!#B~EwT-g7H{|U{Du+2@2 zWVRG1yH#4CYzM;}me)+o3TArb86tWs#r@UF;SB40Z(@&^C=}4phEj$fZ5MNM^G5rh zT6RZvS%7GSMMj#hcKOeD2TJhr@^V`Y;j`=1Lf(|B$jnt6_P#YG+8YJt)4*^NbDhQT zN7+(N9$wxrzt{V^$$|fbjreb$2cz)ncD}B#yX$g=-!fO+--wm5u%<@!{^Y?!0enZr zYDpYVi^H;yuB|N_4M5=eRvtAjfXJNe?0Gs`$LJ>1LDFSJO?yl-^fKx;5fKp?z^NeN zYXG#%>*_bX!)jMtYpd{5hbNw5F(F`vJw3rFJT9B^2A{nMsNC^SbC7Fq;)H&TP7*So zEca1sYs-J>Gqtb?JNnte$jXX|91H>Wr<{um?;|$@ z-_G>x8H_FEOMgEqaB0)U{a>Kt(c%CnE|ykIk1^@w8yYYO5)u;fAtB|dGY6##|KVo~ z@bJ(E02b927x(QsAG}=2!GTjVo7vUrcMU9R6T7p1x@j57Eo3oK9nJmim>UEE;YAWf zOVIzzW?Q5fp*DCS5dLDKlLk@&NWChrkWgUZQr6ti5H$Oxr!3$A6F{80?M-gA8IR}X z{RfWt;0z}fU8{G)a4O=D1@9+zebO3gYiR_Wfs^wIpjhued>HJD#G_9d?%~D(G~h?R z6vq6*LhtM>De$A-0)G9~*PMleL&nsU9zf#n^S!C))@}fk@~Wye&`?&=nDyP=_h5PY zzJ5gm_McBLBqBWAbg9)Dyf+OaXcE1tsWd9+jpxbABlZK}JgjgaE1R>DE7mwQdiCnn zVJyXz{S#WzGOv_v^AUtiR{vD#LZdv>Nb=~GZ#k<2Mi1$JWH-umTf`Lgjr@Q>KQCXKVGI z;zQTAx6y@!TF6EK9ZyV6jYv)=epDay^hhAV4}JS46VT;zLes~s#+HH)MHSfYnw^`A zRC*Sw!(J^c$4&~eTH~*oE`Q(F_VzD;D>OGZQ?_*lEm8mu>`s}eNn$^kF=6rJ1O^;L zwx?UCRX_<-0{KjTc}A%!`+>G^mNph~;d*`clqF^4J!Le=xOK|;qrbk!7@nP-q3>*N zzTHIHwQ`7e*q(_nuqRDVPw(mPFB%YT<3IwOANX?0bGH2N7efMsm}tjP;x{Ux^nBF@ z*Dw9H6pdd!+#g4{?u<_WgQcvjc>AR-&fcDlo10ssPO*jnwlO(Oo>NA>{DwU3Z0H|W z%5mvjGBrjbso_s|nXdv`yDLHsqEw;s(^s2CO=x!xZ>2jJAQC{3(jle0Pz!OT;kOWPQkSAQj`9=0FMH)ISU&Iu*<^VURijR+fF$0J;26AxTgh(RfKM;EK^z@^^ z5^7VRdew~$p$6N5k(Wb8ehc2fv$wa$n5<~28owb8>~&MmW1Nq+c~w?&9v-AkAU6hU zAeL}xswg$({6hjz%F75;qCITx7AquSL5GwecWf~4^qwI;pDJfrm=kSgbz#J7?$?l@I7^~hd; zH#4H3EZLw71sXVT8Z0ln1DS6#Re}q+(EF<^FT;wEtE($p<}oUm_m7kEPBO`V2=mC# ze-{sutEvnu^zDBMKLIF0F!*&DdC-|?;%dt(ZXzLX6)044F)N(${sA+OT=DNI*{>D9 zTt3_u+6W*)2ANd@j$IcUtXs1)q8yo3rWJkLt$y`K345P0rE(eHFP%~eIhg#}{-$T+ z3cj1WhF^gzX&9e^A_^omJLv%XSKUpvCgc%yq*2n!f_tOR)Iesi=12tQz*JRL?Oery zyEXmsX>LqG0fR{V#y~!WOasdFt8Q~Mvyj8X!~GOghsl>GCga0NEG!)1h?Q=eIvJcl zOV;*;9uMMUN=n@C=4+qX)F3j1of(?nErbAF0^ck^o<K!l~G zrO9rV78U)+!ot!sH+LD--TmPDTwzg=n@vip?^)JTTy!+yyXAJ>leJ#|F>PaD^bE#Z z3S{UdZwRAeJTT~W2hcF4C}P9eW`07d?JALQC&AJI^a)?xpSe1|aj!i8T_!9(){E3f zP0e3!><++g-u-|y^)(Zv?(*As9131k$IqweUH^_dc6``D-kQ|;P#_4Uhhkxcw6q9S z^7AJe>Hl!7atO&|vDNxOD@F~ZQkKQppNbkS?5W8~+ULa4uH6p-&EHie?&Phlt+B}? zB7pm7Go)MrXFL5aF_)1WaHDX0y%Q5&s*+9Ez}`{Hp1I8UEih)F;oSmnBnWO}5J4=3 zK8Gq+w+&j1wR@Dz%*?>pH~YQTOG4DG zEaT+F4U>{0AS4XAS)yt(efKUF?9UHis&>`lss8Lsv#+jpSn2KUw)J0sm`1$$@?GLV zDHop?+Cu{Q3)y}zUBC(|VNoSEW@csuwe_~&n;acwfQzC*EYmwQ^d1P4R7?64Mb>Z_ zQihT&2^ksUV^WfWhexwKsFN`yvdHACQKQ#V1$mfJ*+QeFC$W2u!m-J|C|ZvvNBWUYT93a?eK&gpxxi>pIJ8rqdvy_aK)MK^LX3CYvyUE@p zmLg(!rQY(nkjJ4;l|}f&>0n>M!~LZl!Ju>GD6)8yUS!veB=i)mHb4KOu&|KF<;uxz z5Ycr88VOn+djW*_`tF@K8cCu1o1MNoe)pp2PbkBttiLxewx#Vqmc7t%DNz9}J6vqS z2EOg2;>(GN37y8b1n=za2>@?(Hcw4Ye~*Ik1QH}ADcR$SRLXyZ@g{{Ky1>JJ;f&*H z+Nb~<$iZ2eV!kCSnF7X9x~UgCaiiUjIc{}#4&TW4NI?4jf-Gl}UzScJ#F&tO{Kp_U7mNT2a{w>4!M5?djyO~9d&?I;3KXWkolADTeKyf)b-ChqJxI22DaQ4(CI zbG)-f&%{IUKt_d~uM_)Ftv^@LCu1A(>zA3|{q@cxX$1vJp1^xQFCQO4;c01LJF4W0 zKW-tY2KEjPVmuajJWQTvkM$-`<(j6)0H59j$nK&tM|KLp>0-#Zu=>6Pn2+a(J%{{; zBj7hE^JtPKBysxO0RaI%pcIiv=YOtl?2n6f5FZ`=CGENPFBc0YRQ@=m=H^-PY<%3#V*6Jeo>G#OvyuRq1DAFSVm%rA{3(DJd!CM2!GG8CfJ(_bp+m zYA!lGs)K_=ZA(ieP>byB?6!yNDXFOp&!1z9h={cMUUL50*f2FQ3BnV3y^MaKzSy_ zZmyc1jqM{V2Ztpp@++p%dJH6LSw1DQ$xm+&sg;y1EG^eJhm(W@??p9e#bBUVS?}VK zS7}PtoZlr5GLebBJ#Lz~|Jv48I3Phy`<4dac*aTUi{=lBkztrX4u`G(M~tNjZj5#rHPB-N zfS;u{eBa|Lt^cA9jV~h&C^1h*M{LIACi^5@WGY2Hqvg%~1 z?awwS|2$g0h82BNBhF4vK`kwp)_|$c%Kvc8u>!v8m>GGA4luu2qmgCQO~}y!lazW} zHqN)v{Gu^Kq=6OK4zj>jZNMbcCky;j;&ScSq>l~4gBJkdqz(ih1~t`94U32%#Kw-1 z9eo!Ef-szki3xl@JT}IJ92^A7KHwiwA3iXH!(CJ7a4q0~pceN_2Noq;UHiX;lio%R zP>o3fqb;0}JkT`!#v1*_I@e!o-PGsq#vRao3|KzC|IPWUca<-2r@w?_JKtV8LwW!p z*E+4kZZGDHom^ay2NBurnDo1o#n^;I6`iVCVvgN+f}BR5p%5r0CQ=_aNoS|T!z~ff zGaxWL7K>Y3b*kZP2ec5q(FC@@nur&Cy>B3Vqw$&nNRp~$msG6(>n}pw3*H{g=x2Y< z+}EB{sQ__lomx}FrEOraRA$VaY)F?`q5$Nyf|Jv4S4+S(m=udY;=|s@#9%Kp+f%V} zay|p{g|XB3@>v*uff~#E(o*JyI&+NE^K&Mw@DP015Ws;UFcBzym_e6cMl8+CX7%>{ zkHFLThHf7Y0vChizRnV8?8WcFR{z6ivLq=y}12623qZP0c5Ipa!**I4 zGB=kes!yneCBcoudsC&)Ba|USqB zBpL#dh)Eyz?q_qH(kJ0MqYFF6r%xZx=-X$lH4$aDCcCyH<>FWDIVbi`DdQEN3AK_w zFYdSKfw+1n9N)3{erLNFy@VHNaV8d)vR|)QPkM>T$cXUp@Lc{L>hc1{8EyOq2GwnK zq8ITvB&QJf^D>`)N9yJ6O#t!$Z@{e=3`+la8o^>c&3Ng_NiS_5q`oI!#1f5P&=tQG zQCL*8Dbi;!4tgErjRNo70VRTI)fnArfC&VG{jO_iVFsQ;5dgH!n(q5@?Ym6CTK@y* z0`PnO3j8olTqSo)EJ#Xdicx4ghM8JS<)Zi;yxaC=RXkXC=LiZAqTjE> zL$D=CV7=%;^HLZ-Y&bbTml!oAF>` zVKIc2`CCMa7&7j387rGy()%(c^eY*B-Cr)BL?i^I z(YD;UDO)R2nn9O2QC=Q2#$`KXmPBj_pY>wU!N*1$AL~6($l2}$BUs_flT~JPbaax= zB;W!jCNj!>tWSPHg2%hhjqxU+D?A+S`sSd4)`rl7!(XU9$50d)6>MD8Lb!OD|{j%loUlY zBdeH%VPL^11YWIUyaJ9+S7hX1QZ}%zsHpvz+9?IXpe=h~5rtj+F z(nTficU2AA!=SF<0tVN{$==>|b#bw!sfUNh1Tcv{#8V2`hSk-b#vLBMdnP7!chU`- zTbe-C*|@%;fdh2YI2yOR4gZ-ee!U8el^4L6XEy2%@XskN-4VsWz-XwftRx2GOB8uD z5WZ3mE+>s{8FyW8X9XIxI;}gv;qcGZC%XS@c%;oKmuLEDb$~td$=#RY*1?uGTRjb< z^r@Huhr(P*h!FQ<+i0k9)}{@qJQGOO*U<%KmXKsfDzdPS5jq4^Ly|vyP*sKavPSP? ztPv{yCTy^ z=CFh=e&v@0u<a@{1!O z>dPWxv6pZyib`feUf8Q-OBBYBoju^v)XU+{-q8DFJooj%% z%`cwYJN!nEC|y|F*%{PyO*d?FvD`|a6oS$>TwGk>j{0D`72kYE%c*{qBN5aF^eD8) z>$8P~5iYOo