From 01bc0a0a0adaccdc636191f7b6e4ec74d319b5bd Mon Sep 17 00:00:00 2001 From: Sheddy Date: Thu, 11 Sep 2025 08:44:04 +0100 Subject: [PATCH 01/12] Add New Secure Section to the Documentation --- .../assets/img/secure/oidc-auth-flow.png | Bin 0 -> 174395 bytes .../secure/secure-api-access-with-jwt.md | 204 ++++++++++++++++++ .../secure/secure-api-access-with-oidc.md | 110 ++++++++++ .../secure/secure-api-access-with-waf.md | 190 ++++++++++++++++ docs/mkdocs.yml | 4 + 5 files changed, 508 insertions(+) create mode 100644 docs/content/assets/img/secure/oidc-auth-flow.png create mode 100644 docs/content/secure/secure-api-access-with-jwt.md create mode 100644 docs/content/secure/secure-api-access-with-oidc.md create mode 100644 docs/content/secure/secure-api-access-with-waf.md diff --git a/docs/content/assets/img/secure/oidc-auth-flow.png b/docs/content/assets/img/secure/oidc-auth-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..c41c325583f54e66d61108383685883e965447c8 GIT binary patch literal 174395 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%0Avi&PDcie*K>D(-)sgOc$1&oO~>?Wkf zO-DV2G({~5VrMNeU(zfDyjkfElYXFi!R=LGnk8DYdg7^_jzqMGMQS2Ti2p@7KOb6_P|zk?2Duh z?@RdXhm-jgTcJdt9XM{p?jqG&hdM!S#8thnAOti^4;*!2M>KyY9>to@+hzwneCf3=X5x2;Y)YCdFE!?<*GJ|V0UZ9rv8 zbO8Py>7ieXpHNur%n8(d3`0A}oGs&0FSwxr3Ttqeu@Zmv!U*&*^3aV^`$}hBrAKF3 z9Bf&+Sh6u}-?>>zT_YDKJP7HJwiqZ)zX*iH0_b#t0U+Vl{@$OJDNE>LQ40O9hM+gz z!jT~$%?v_!3lm~LFE48-wV(EkU$Yhxs!DKql|$euSliEbAjYsy2JKooBGl-$nGQzy zN?ZbzCRKOW-2+1+Aq(DRX9;B->q7I3N-ad6GN1dO-*XzG;~o=!wC)*|Q-MNhuLm6$hc>%V zFbkFr!d(9eHxg88^MaFl0CAoDtblj9xX!)9fP>1v{||ohXpz~{ zS*goV2z=?2tJfC(ru-Ic6?lJ}l7?`;i+JC3#mAD;TKLkT4KCsji{yXOW6OAR&T9Xu0E(|9v3hT{#ZSJRg zckFDD*(zY>3($&TZ2~Y(|DBry#0C|R>0xrmxT6@N-Wt#sCI@KJ0)FS)_OPn8(m}|+ z^GyIbt94Ad{IzKglYR!*C^}~^4Rx?q`mVx}MZYV~@lP%ZuEZxVwN}qI0*TzvN$&4E zu98cYjn`syol9iSb>5tgu=M9B`LbAo>4<|!tK{wD_5D z-!sdgI_{pWe)3dN_3=9HItxDfjjIP!|49=L=U*El98w1chzPtx7tInaZsJt@=fH;L z+^Dm-fx?Ze$9ryN=2l>{N>*FgF_57?DODjEjey&11?OHGac$@Ae>f&BCI)RA7F*c! zi^ul&2##Cx=@{tbXT|8T9vr|RG8B{~%ZQu?NP*_O`A3*>0;vGc;C)WHDyUaAXQER>Wz8~-rcA<&@3kJ%~gj)f9 zF;=$ce7xh3DLPr|Os!X)51Au$avO@@-!YCBa>_$lkB!1fat8ydYB3`@P&yuhlDCAW zLlxakvS)|FVO3-F-qJ6C(n3LdyU}GPB>v=%-@&l~1)e~P7q3+P^I}Qh9kuw(x&PJ} zbTKI69q};C6b4`VMTp{D%9D@AyK+ggM)qZ&g6~>Xs;d4t@P^ zh@f`N;Fr0Qoqm6ThFh>P$2-#J|Bdof1gJpmr|$*d>GC}vku~_In+|zz5uMad&s61u zANFy;4j$Qku=w8&#={QQs6CUFdz7@O56uMi$^Q*R{7MO2(c-q;!6Fw=FoAI;k+Tw4 zJn<7O5o+lWDtk3^wvFahBkR9mz|{5;(G;!S`<4g4`wS{0`lDxB*niR_x>*yBcvt2+ zi|UPAixU4`@Eo8=(Fq-CnFl{qW}@^Nlp7fu99OPTk*x;r{=fg{04el!Q}rpH@fpO< zQ$ShE|9zDJm-e=ad2{fI_*wYjqj6ide_vmhfCKI9$mQ%{(xQU0du8WIC}z1zmYLfK zj}S}U9pTuK#PbabNB{dZI52#5vGP3!a#WXvhwYmy=l=I??>o3tfP)fQ_7iT6`xcz2 zhq@C!4I6{ocXs`!k0O|n(~W&RtE{ZHv!th?oO`0qW#0Nlu}5>WlSM!{hK2PHRmFYBQtxe z6Zj^_$^MNnln9TIzB}*zR~AG^5vhAQ@Sm*Sga0k78Bsc*>oZ96{$4^ah$@vPRrueq zu0Y4^f3v;{--Pkce)v!N8t@*~rxTXJ0s1zyT$1SYNnWn{?`hA$SrmRK_}bsq_3r=r z^k8J#!au*qtqxsjD2Z&Jy-j{4; zpF`b1=K9|5H-WW+dG{KMF#gqhBru9|R}@J9UWW%nZs22k@1H2HgVV@ot2L_sn%qd^ z`s6DGB0i%Yy83SiywZiPYjhadhv{FOAA%ctC@56&s@=)S{0Gv~ExSiouK|M2oNGZ>?SzXH)!BMzFf z&F;*<(f|LSCz{E@aABs?2iAZg6{LSVSvG#WSOVy`8iaeIG-tn^5i2Iz&QK}G63Tp- zfS(k8T=@6bBO6eWap?ENH_KcU+Go0!geA^731!;T$XPL$l*Qdl)0U>ddT;`VDXSEK zGE;pdLXk^K&;xkqU{0RN!I@{14C2H^sn%tIJEbJ^;y{xqg}mYe-+Q8TURm zbd1qm7cBey+_K3JveG;Fd@oC4r!_!e5hZ(DIDVISdKprP{3%p|!M7mqwH0Dn&_MIv z076Rn7GNC74(;wPdyNr|W9T>YG=#ubZUBYK_^d~TalMJ@9&3ZQyyoj3RH~q+uQ^_*gs@O6_7y4Y+qM$`+1GI)R0@u~ah)Q}xP(27v@w}JQk`&Tdj|;p%@H;ze zou$N1UU9BiRvcv2*yU=}?+O#3n)UijFe~@wS_uHI0i(g`puTw>seB>*)fBYRE+pFYgc^c~hCu*t4%p2BS!*B=3|O0nmxAw^{Jg?A zZc5)dw^3TV+s-T-^^#at%LB~ajUca`Z_wt-q;jFqpX-jR#ro#LV9_F+=;>Tue$*EyoNhNh!K!{zq5Ui*8a24KSmX*b5XuRejc;>+&w57XWI)W2WCU+hp6 z6IuCAABBE;*7>!26$DMapDlcdZW4^e)H0@m?sT#0-lh|@ zb&e791ix@kR62ek?#Dc|KQ9K?ao7#FHv$e*E_A1x{{}=lDlaaeJoWF@bn%G&sXMc? zd;WAwPE%HP9(HWVwu!!4>mFyk8{g&VWseJiL890@?tr=c*;;~*vYvv*7w5N#YkYS% zeJWi(-YTndD1$cx@A7l@nyZel9l{GrcTnu@b$61;&Q}wSY^a1_1_o=i*goVpt;9=R z#2QODxPd8a2}YW%B*(1Av5}yy$du0a=_;X`jvP$tm+WYV?RCrokk{4V58nh{8cT_9bF}Aji;e%D{-ZL2HW`YJMm#% z6}^Oxi;#&Y=&XQDel_)-N`Bq!#C=}D=U3zS{O%20aMt?4xpH^Z{J&)h7E?G`u z>+WRAKMAi-uUeh>1zAqPqVB-2FM;*9{Z-a{J}cnF%58o>D<;X0zbpF~?bKkqd%Ru>%&-OK-tL7(Rxn7_+Lzt5|0|)4E?G zdc?YBGi{lPEbc@#H7Twr59N#B4D@W4XxdwHs+AejnOIAs)X~eR*-7YJ0|jqd3Z6uA zr&&h4gjsG~HkYw4$C%)-%WUkt8>c%nBFJ<|-=q1yUiH$qxwHheL?Jq&-W1=2HPmX| z2FTXI(VkEO@69XsTNK;~{ulU2+1S45?j`j~C8uqQDb_~obeY@pz-qhlgyh&|8}XKa zTQcS1x6O;7faQh#8oEvFn3G&N6>^&6btW}+ITEaX zbU-4uRpIq}dBS9Pj-N3~@{B@RwmqW#iyux0IyCC3`UhF9CvwdK8LHDY$46gl7EV>m z5CS!NPK2yvNTSSTp`eLRP^B&aE>?EJwYYkJZ(DTd`OQ>9GUi5v=49)sItv-Z$2d9< z2A__5brzzCAZ?~C&AKAtVUvmVrF(Tb_0q9iAr{djN@VVl+KFS-CT9L_W53u4DIs#J zc=GA2h3veFmSuZ$|1={x4dfmpJz3Gv`I*d9UApLiYa83=r5i_p)57bON7pwp*J~nj zsd_^Dd%5w{@Y~3K^85$f*^b5(Pagcvz9MEXwD9Gd`}VK2_JGNa{E@lUwg-y0fAX#y z9<#Ld8Fa@bxXHvT?hZfy8D~yGsHkzTJ8S9{)n$3q4Y5*or4+4UncKepaJ z9Vrn_rOP%o^0Jp> zmsEOa&DHf~gR&5O$G{wfX~8?#g;+nHqgd z%Y4I&z(PG?bN1X~g@HvzUag!ot@pW_(=4nzVKzG zM!1l^=c4Y2P{rHZ{IUW$tcLNBp(<%j)(V}?$H3^ENv{sHhe=XZl&?J7;F+bB>zBo$ zDburTRnb+l(QaI{v-i(t-r35s`jz`A{dTw?o!WeQ#i^dN+aY#4(~@r6nNbKYQ>><% ziMnpC$mJ3f5s8s{H`}GbXMv(eI7Z$k?zBnatJ7|y9cn6S=`LNmM8BHMxcft1CMPrY z&REY-hhUS()vsQAywqi)Qc<2&I<=kwEdrD8m=*x={nEtfH7qYgZ_GAz4^cMmL(vvs zu@Ub?+p&!|q*MN0eecis>)oM&iM5zb(fk9KyM^VeQsZ<{DlwKs)ZrOt4yWDD3-Y{W z$PP@A%qMCt;9t!&e7{dr_gy4cFSPR``=ER#>tnl~vMpVy!?MTU6>rQD0k8gv-ipvc zzTCB}1WAoD&l%UW!4ChW9@lKE-fW!^1>6fWj-QkZX1je}``crl9`d5U*XzL31xc!&sdM{Q=!5i>r1<+}Y-Qs!bM%?ivNMqDX}5;E8tXkCmog9zSz< z75esolrspSteO6VKHIS4Muu~eTy@fb#j|^{n=b5kk$obU(od_4 zsl?yU4a%-C(K6Y6`YumVmK&21?LtHel!!E*q+!jI|5TURGkAYgD0f(ylkiEiMe2&; ztIUsg9yt>s@KC8MIhB9F+%~R$MWM2zlma0exnu&bUv8RC-%wyw*4?L%7V25{)y<#J z6#21Q-s!*7KC78)o1OWb*Dn(t(swCoNb}GX?vuIejLD!Ln}Mu%bU8gh#x*hTsaUp3 z?;3s?%E~mmQ`(mK@!NJe_QGhV`>mI3Q|{RR$KHE~Q~m${cgoXU77-mN&^e(Cp5B4?3Ln*c*Z+qKs4k6ltx9!|ee&t2xW4iyf0M-?Z%uHXP9%)2@hRoJm(Gm>O%0)4~L0P9N&! z2|q4rcirO131=iVb4C!3?SCdru3H!xQgohmWmo*qj7in0ABxvdbd~SfZa632Q!b6- z&^e4dOEE$#+2WL_HR-gN116ls4ft`_9o3)HS}GJ8@MyJbKTWGu4UEW9{TccDpVOfX zKT|ml98pu3^;s-q&lwkATRo8z5H{`GPWOrBxecy6i(v0uui84%9WSI~7+G}oLP(>E zUk`P z-JOK4iG)*iZRbep=(iL903hwVa4mICiqZ({=sSi*1MWrh0*qs6TEKlCb6j^tAQzX~ z_vNI8YVO%;4he3NP|k}EA7mF0b9v(($IoA$0Z+sSTDT(*9WAF!CGayA-{20VfapQf z>Si%d<1k|8FF3Mn-+yDo%uj-&3At>e?4Sq)1aq3)T_q!Onf=B++ocjQ^>?0cTofhQ42SVjsco2ZHDQjH?_(zfy=Og{*_g5pnE=g9c7qI7Zit}1KCjybwz%&Vx>rL$r&DyKDBJuYThRrS0 z56K@DA0{%tt}=~d5y>~&X=^FOu}J)`gCUsd+G|nnysUq!pT`|En82XM=N^E${ViZ# zu+`*k#Nis-xxy*WR^g&6jESisC%gDjroxeNvRQ%)vOy8*oO2?42I~GJdi_ZJFg~Lluu#ZX z(ft3o6QBI)1Qs8B4+@miek5iY$1a5=TB=URh^ZnWmt!1Hx>Dd#!f>!*c-te~I%Sc< zm40|U!Ay)zbotsR5Yt|W=>J=h*`GWw+V8Py?=}){vso z(GT?5%um^cF-^GvzmPy1*rTdNm+)!AZb;}2=`TA0=XvrY8OmHUF{3D${+4?uMmyG2 zQVU(aHgsC{4_eh$wFl&GYkKC=ohrF~mj(yYgi{aoh$b(T(F@*$d_848kC@(e2&*-- zl6NIfH-&@WX1S$N?^w0z402>B?C}C0#Mt(^N=ieOGk?Te)6t7y%C%r6yBbzFZc^*d zWT$;&ggxiH19uvd`69qSTgp?&TN_6b00P%@fHWJ%kvCqw1l2DaB~EM*cB-c{6EAV^ zhg`qqd~C*anFxt^x8Mg7nHuZeG^cjtE>R6T|F!3-mS?&Vy3{g5I5m5q2QFmmO7_Yj zC7o}@#jdroQjF4|7&aoWYiOGs ze;xgO!lS(?4Z2o;g!+58r;|3Z?SfOYRX614hTAW`O_ezWqTve`isV?CW_D6h0LS<7 z{`$ptt)X%Xty*2v~5k&l-<&=6zLVII~j{)#*vM(lGkgxtO`5j zi+tOU)`SKeL~@pvpWVSyyH}4QkD7v&ARe|ttOTB}pfOs;UB~&o&HRr3B9x46I@Gi_MJn%^7oVG^1DtEf~Iq{+fCy<)C7s_(5rijm|R<#t3 znM)V6ihrv+gVpHhidSJHsrU0ak`ux?mP2LKrXkmAFw#anA1EP&5h^w-r2+}_6|9x2I zNxWs5sg93vj4z?x`W-G4zIzXO?x=EqK;ix9`n5x#jr#Iu6I|ks>!mqUURkP960O<+ zR49AqLSLDkf=ae?^H&~w$XW7$I zu~y2Jm=%MToo~^S z8PnzcG*v`i`K;sR(lL}uEPymKW%SzWe4ztRX*svlM$$eA$rWACV)jGQs<7u+$d($4 z8|!ELK+gZgYwsB-u+-{39|WJ$tBIEP$33*|$}20V<$c0Vc8Nj5t7~SC&Gq4mTBv-! z&_>>i{^xYeZltJk`%zhlzs+Vm7jagPN1EjV`in_2%2-dA9T!r~r^cDns`4`|0@b!> z-#cEL;A0M9wug+_6uzcpLw6|%tHt1E5FxhD-YrJM8Tgx7DU!E_+(XQ6L5EN)pc=zTh^JK9|Sz$1)w>`P6we33|yptzGaS?C4ra9m6 zhwdWF56kYE606xl#+w$+wDm(@5{A|M!#QU7feY+cv>3t)6 zXpZ&Y2$MiXON@Ewbfg|wUwMOeZ1!FiM30SnWz=tmRvz%i&Tu$VZ@;++1g~p~ce4m9$AA=H2(y5gA=kDd`(q1xmEQI))?{NRAw&J2 zl|R2PEliY1nnc0>fBA-?`S!lt5EJKbr#wd69*_$CiF zhyaI8`VG!SnGsms()W<1ZddFpo^Lapgd~TMbbTIQOfQl`jK(c|$I`5BP6DQV3cg46 zYR6XLt`kXQsnt+#C$cJ#MAoNW)fzaYVsPWmZ7)a`b_sm?EgvC)wH*`>cgY(GM|T17 z{uW#_0}-V^(2Rbw&Xjc-N?npWH{5%#|NByx+HbKb@aQ>&Z(Y7b1&WIK;4-#Ac|FIw zZuG>~;rP$()eeN!pP!=^+km*jG$xR;bDf7xt-*mBLV`z>=_;gI@;86!w zo~2?PYkcMR&Ed6>;0q{=!u%Tr&LkN)c@yMAwokSef2*jh$&kk#n(kwgR4xJPM-?6r zc&)(CHPa2S>Exb8&4aFTj*Q>Ue7dpyO{qBy2=6DwO3zE0K zJdOP3D-JS37(4cup;5!TWkX66E6?f_{f)yb^iWmf&9u0l~U@swRCbZ z$?%(6Z$3migd z>-7aTTz6#6{uACLN>%9Hso_dvSsu4=sAdo6e+}vv+>B1+*PlYnP~S^T69`<6HNqaQ z1Mbf~q1Z49-oBDa=`U78J99dREetW7!|Pryj;sjW|L>FxMN%Ko)gp{P#kTH*D9^nv zn?$i$%wRrq-2`K)-EB6Lt=jtzDg^TtcKj0*^N;ctelFe1Ww*Z4PMSF*4sb?2uOr(h zbL)1&^0OScu%1?J|NBv1npVCO%K2>uOW`TDtqTD~Y~*bUc1=Bxg(CI~$M+dPT)pQ8 z+00p6$4E0L@EfvKQJp6;9WHKC01)t1mRaAe2pJ3F-8tY{7_9Dp5vtn8DTm(Jr{^pM zkT9MRxb>I;^_vG;&FMEMsK0|EN&b<&+a|XOD%Nk>2KnpXsA8kONNk>p$rE%T@A#jBGx&l_=JmK zsoiTg(hK|brNKaZq}A7V76fsm^{Bg{LF4rd?>6>l0*a_m1`lH07`wKvT&W$=%Pt8F z!vB7#7@&Ff0VD1O@5-4F6W3(>yjI;SAqv|A)gZw!o$t=i-Ee_p&Qk%*JDAaO(8%8m z6~Ov!`yd>Ik-q65*XDgFR7`cPMBvMJUHUKD*xpqV=_cJ_bwg_$#K|=Be#~;Y{ zJ>u|m9(ZqGZ(rPgT$x_(C_T6M{}LM0L^W99pVMyY*=&Lvw}&Dct))F%#|`OX1iWR} zi*27LcRqkdRyZl3Yi<4qFLyXpvzi0{Y|h~t@|FYOVH2_im@0(7LXzPpT-lxTmCnJ# z&I?b^+HEymKR3!Ro&+vjvBIzO7dD?-ShA<^L@0_mZ=!GHEa0op4%X;wzY9*tlb%0a zrQ-`-IvM|Nc)4nM!gD#*ul zSE{zBfsl2yA0uT|cpbDAVI$7h8BuTTqAfyaa9YhU5<4BrA98MZ1fJ|#<#LX#E1>cJ z5l#gVB8?CGwl@9$O!@!p^8Y#5|Nq?OV}(>)?!UcoP=iN3oH7EH*mn@EwjwW#A-o14 zAB6di=ZhI;l=}Thdi7Thz}oK_qSN!}c)8cLZX^elEOem>RcBIf0_SH|_9LEWv?lB} z6l6Z7XO+=8H7TFYyZ2!BE(u_&DBQ{ePgDMzS)W0h$EKP;KQbL?|DbeUJfbe}qnBQw z;4=}#c{kAOfUTN4nIw%oH}>F&w82X!m;C*r1+;TrSX-Tr@HjP=BYW@jwYK=l9iwfX zdl)Beql$uv_0JnAi==h?H>cGXqVwW|4XW70EUK8@0LEjt_IG(r9e_P{P*u;p^mvcH z?|x1?Vo$;S!B5oy0h+R3S)9Hbfhpo!)ZX8y8K>9@u#HjKRpX)ue`Mq4I@_b$`Ud`9 z-$2L>RlXvC@7W23k-jdS(G_69F@aru@<6n9+i~{>yGx&t+I@)Dl1xBDdq;TVy2V@e zc^NQi&@93jOO$n38m0jC?9^fIE9R2<&Z!2n7QTDqX~YrF0YR|23l)52H4J(?z70id zGxfw3Lz?266TgccKs^W*>}VwCrK??dMrqgrLzfpIy>N4NZIKkctgD#`p0Fv%NUHw= z@6{K{35H-HrMc=fCWD!DD{OE}ZZN<3e;EBd+A1;UR$xq~99k!aa z5pmK-s2Mlzwd!B(TXZieAAPUM)8n@=+1(AY=(m-Q+`pVAq_cAx!WSz9E+yhOXKLmG z+3Ix25UKX1CUW|*@@M}U{MP+w=dKw{8^oq#kAKx1YSXvkY<2_x06SvH;>I~@<-KO# zLVAS&KqlKsYuK+7m3RP5bRurTl&8T4j1ArxehA{B5p^!lhTEyrU;-ij*zS}^5Zym9 zbArez8@eT~90*_+OEkSI2)qn6sXv#yAxVlZz!#>jg^>P!MPgY=IK`BwNj*ve7!ENrNFHSQt+9_c&n;ba6 zY`Ab6>VD9_SO;;6uJh!P{?4eZ&t8u{>>pVI9=F|idF2m7VJsiu@F05Bj+k$f-Z$6nd2{C2xs&ftuFD$zZb z&p_<>4B|FILH7bKr7aZqn@H-kXpgkPt;`AIznDKCo4b_0^r6s9H`)sCX&xbl4qm6K z07Sl(J?SvkG-DazQ$b^8$g|*e#~L&#MUG36Q7a?{7OPo6z4j{R6vmux+t<4M%{=Id*d5 zsoXr>5S?WnDk&knl{p4Ux^i(Q=z^+v$2-q87g^Q#gyj~44+xmB4XcJt^Z1zFXc2x~ zXQ!{CGlEG&;I+$|))FINKo-b>kPWHxk6D|IUSGm$W787a7z~NnhNt5GO-<}^DTxV^ zI>%yEch(~CJ-VY0jQz8k2@vy+h=r@6KSBS_>IDEttzuoo#6}?uevL&cnUdHEZCT@# ziT6xe^Q#ayq=QVbhiGQo_k9t4&EX+kqE#2K53=`hNxSCS;Pm{Ne|6_)nl|f@%`Ll` zD{UtF5pDsh!)%u!fKqVxbgG~@`&hqMmiVaM6$8j(87FWbV9`6yG15lGSvYJDO#lc=~MMt;}?1* z%|FD;tE7)d*f(Rv4&Fq+OsOB@b@Wd;H)m44D1{^3Ns+Kt)#ioH5lbNPPYC7es)RLC_VQkY)3yd-18ED^Eg~{A$9Fud&{A zDC?B^>^MCxBhM+hObw?yzq|@?;=hzaq#j83NK7XO)tg#Gn$vHpr%T_t2C#*sMz*8! z76a~U5yf}K9KFky06EpL{;MrX-=5utxH6IITWx{c$72ag>TjlNIEUXs=|1KcEaSJb zk1UcRhMzA4fs?ls?Mm~O*-+uzzre7Fs=gwY@0S!!4)mx?H5C;jwe7555aEvefFA4L z*LyZRzyqkaYs7lg+xs$g%WG-0?W0I^`riW?;CLV$|5+JjcgS%(E9tAUKNV^}y#L1x zO-q3sUA9Dt{hH|2akjy(ne@lrjH(JLGc3QxHM_5{ z84vA3j%kxG(Zgye6R^!CUmGr&*M?cW8{Z`!#a^ZNH2w3XI5L!*m)rD8e;{DUyE3*d zykNBJ-IEfTE)AoNzc77h!1X1QXGbFLb{)2gGza01xl57Wck^%n5BC_lEy@JF z3oc?Og(;K|t4dQ7g4B&gffm3w z8gNcAqQ1K#UkWB)eElm^Z(x^b{FI=_ANqA8&#w%Y*nla?UQY=xrw3Ewl_A6A6%Q_x2%5(%;W1vOXy8A?l9kzOL zHo6{{+PV9^X=H;~o(;|#w?DkwfjXy1y{ujl=Q0GK-v;w$HD z=W^urd;M0jpG=kizWa>IAU*IXin>phGOkWx+mlhUU~o5h(#ktP&rK=d$oTZ&?=VK$ zW|7C8?ooy-`iCaI^FVHh3DWc1>LJM6WDS6}?v@jO+^kw=X>B8b_uV9PTUd{&^w&e`s*8!Gm z!6#mJJ|2e=wXdSq;1ib+8up-_3dHVIt)k{#b7%V-f9iVZONUa0D*p+ULQNG961~jd zj{?VjXZA`E#>u{?F&~0GrwQGd+2VngQfl@!qeZ`kpw%nxRsQEy3iA}TFe!Zx^j*^l z#ik>xgrvXjD7IrOvqo939eAiId;&qrg&OtK4@=Ef*q6f#U!m8GT9xJvh|e#8FCKEHpbeET0}^hfRxk-8KDCizYEpXT4DKDW7;#OUQ89 zND#f$4UpY_Cfi1RVEi^cZb0$CYWDjK%oC$3Jg*0dBW%QP4IY~tkY!h`@+V&J1(%ID zDPX5u`06FaWBVuZfow6;g_DP(*rtp*k9^uMJ=YL)9h)}s zp{Tg=AyF!i^wKRoezwI5^CW-$S279vsq06XjA=bjz=bkXQX#2qRCDp}2nxg%*85o* z`(_`SzMC3`a%th(+I_FwX^viW(RvTBnrE7fmbu?xvV+C1vb*cEmvQ5Y9jmA!Il*E1 zISsoiyvte#cx*f+g6?~IT$9Feji&|X=0+nQ_PI3ns=h0hYvM}$=1#bP`>E)#n%#P| zS4BcEnw}!1*nwnS;Kl++Crm^+0{7Ne1-1W<-enj{SzimX!%ug zfgtvSr=qQ=N;eRGf7CaRo+%n9YH;+1c>JB(Fyj+T2Y3{hRHw!*<#Uj_xP$>S<^@xa>mg#C%=Ym&P*Q6;24Gfoti#f!GO_kP* zfJH<3$lde&P)$YhV-EYV%s8P7?Fl(lDzt9+m(Cw@lZXR!Ca5;l1iYExS`QMqPiI*D9T zP-$AkLDaC~Lc)WG#N-^A(m%zcwF@;e%_{OP5es)gu7lPnGA zkV3x8Z}IX8BLt==XR@k#%lP=c(0$e7>JL%5@fg7QLS#Ze`{-=_OtMtMy6@bAvS z6!m9S%brKw+14I59{Y`#MHAS7Q?qg|!zj=+Yaqc*mpQCC^ z?mnw04Kh61Cc5a%M5cXzU-5155KqrqUuLDuJR)Yr)+cxfDAqH!?~^?=hY$KK(pEdd zlrBGkXEXVYg#yhHD;DOAx9K>4fiLus7dr-GIvk}x__m4<$6j6ma030cvWEw1PW|0u z?LPwLCm)IS%P}6iVFC$m*KJx-sH7<8dsIt1#zHzt(1Yx+2R@M9n3>`TJm#fE`J|*Q z4GSsYGONY_K5Vds`$;*SYfn(_c;m}PIx#laD}jD8jH3m4baBO$*%~vH8V>NcJI*-L z^N5K(N7;pyrcqIctwZEu(;^-lEpMItNXfi_>Wcqwf1i>w#Sj-vWNK#8N$Dv0JNscU z#>S>xc-2_q&qmceay>1#F3pkhv0v5~WBr%+8SZi4) zE?8f-RxvENJK#<$XMuWXLNl37-SyKkc(TOW*z@lo*DA{T^1TsO@6OXlU3fyHE@45e@E=4T#cLg|ti##OIx2*@ zwGif_rr&&d2-SoRa(a&?sNziR5D1j>hcFj){aNxW)s>TZR2($ zw~-J>LURjqvw8JCOR;NulmxkRhW$@)bk1dYJ?x)i3+8TL^zIZg;D6zu)xnPP*VQT! z3m0z@?jD3#WLjNwEFUyBL|2;Rc+X;lIK0E|5s!+csLwaj4BO2&5QQ8>uSci+AjGF5 z7ZUaGog#N?MyI?E?!MeK{*_1J#6yssSRE`Im31|uv7G+2OzZhVQQtSc&desV~~e7+O2Z;0{oIt1;7U*NnE2=*_}M zi^H)?=xFOWbI1#(~tVjJ(fzJd}ZdT7>Bcht&6Wwis@v|vcV8Xg?L zZKb5I7J+ZwH2o-H@^q-ZZP~l?u!+HkLkA8dIeNkPY8jfHCqk;sB@rmHV427+@82GC z!fDef^Ry!Qx4b(qU}@T0!-F8Ig{w#n zximTIqzLi`yg#?2d?B(YJ99&CpDTDkPw$(1mB`eC0}g<;d9uAB+yG2{JyBi_~brti>OmRom> zNmQ*+J@p_a1?I`5d5rk}kT9sPLEDNFdvb7l&Oh$_@v6RF-&kbUpwV?hO|B9VjZpES zY{lr0J|Eoatv~f-ORXxz7|IU6z8LL*V7`*I#sflpk0>R|tyK`eT$luD_*sfzX8G0j z5sfmzxE#DZa$F>J8WP0T&L_4q#I1=^k_Z>Dm1+H;l0hbOQLcE<@RnKE)Hr6B?DuL1 zz<1-&+-z6g*wdsCHD^fl}7)xqiEqilgcaAw7L^d*#%n`G~X?o%HAMritqI-^` zQBdFPljqhMVLkWW^m3(?VrD;X(b&|dZQVG_>{6Tpa}9B|Yq$7>yP@Zc00zU19Xg=0 zFq?+Gl=t;d%b&t3{wqSMn%PD52z~UFEc(IZSc2dK>vTiTbqH{j`m0u?)J%hTLj7l# z10DN)j@>YAdzSO|cxbEk(oB1OYYqS3wAT8LG1i2XwtAPSh{8TX0K>WrUbBdMsFkL> zh98dOVC1Ah*zhbncqx@zs$>7|$vWJQb*s{xlaN5_p=Ckn5?`U(v|PX@cD7h|H8uCp%{_W!}w?yDb8KwVCxY5fx}p}q4?ZhtPRkpDVQ)L~}d z1D)YFbRs5V(6%42BOk-&_mdB!i+)+ZOw#ZSlwSBf{N%Hqg_Vy`;erhb)!V)D`Vlb* zfC60SI+y!NI{9M~B&|fX`JHzNw^^L4&)dPKO5_*LiXJz{K*CO&N4%H3k~T3LvaHWA z|4im6xIM%ps=}P}(HA@vCqniL&=&YG%ARq3E@(KcBtBm{?YZt|+6LgbU;WPqlMhuL zsF|>OvvE=`j`vvZRmg)O$CpRv{hXb@b9XuRWtW=aA!rnSB^!M6Ql7`Uea^3FKR z^NC4}ka1C?eV6G-KwlwOA^41Ah$4i*;e~&_Q5D$n!b9h&yR3aba%)`rw`Qqlgwe1v zOnK-_XA-3X;+^UAM?C-j8ByE^n6@UP&q0^~di@&JdYkEz#dn3+{k7?_)|jBzg&L&@ z#Trv})ewTUNksetf!LC!%ZPK!m=q;Pp4Vvz{1*b)oBc#wZMrF1phHyrS{pDhk2rEC z%l7%x<-)_*8A6sfSSQxC-p5?K=7=^7U%f(-MfFPA9$emKSqt)~_Lb!jtU!t3s|WE6M=mrNJ+hfn zVSxPQbjJtfetpvLp01}VN*z_H87~E{wKoY%v{C;i3hFS5r?f!>O?ynZ(fV=&?R_bo zjH$PfI5~ymm%eX0_S>^3gsaAXYJR#<@NEaQYqEw4|J-7xlTZB>5HUbt-)rxjw4_&8 zjiU7+PzX8mcbT6uoY03_JW16|Y590t$q*z2R2RKhUwan{Mu*Z{Kuk-2f|%sKDo+{o zMtcTYECkB^DN`kjCWiOFvQT6w@%6ceTpdPoHW>T6B-y%A+5S~sSuT*T zQeSs8!A*MKc$1iMm~uVw>_Y$Py+@s6*26;er)%J?l!^ORVB=FuolGdBJG(?ciHa_H z-kqC%?O_Qm1CUo1`biYxzQdJtFt>{RlqMZ7qO` zz;}b3x&}EBdDBaz{);D%o{rtIJBN01sm%^mYKNY1wBK!OdQ49!HJI;Q56I@IlJ-3$ z|6P8h<>eKj+v2M(p}4aWfyb5mGA;&oC90Ad^nOev@GQjoFYsk)j{S!BN1V=*<&n{Y z#w#NU8FQrxWrEcE=^T5t->$I@tb8p%QAaPgl&XsT+I10sr;B;O9?W{e@0-~D5ma=> z3fP8(9de`>cQKAUVzudp)k{j(8eckusUIIpf7O!M)+j-t9DUiF)#6n-}~^88{VW97^V zn4bbxmQR~@m`{!fymMU4d8@kD>jiDNn1-8+!BhCG!dF!ziqk0DxH5^nZ~m5!iii|Xld z`RTaOpuo7yWb{oCK!ph}8~jCEsz0G{YCWzwGClhjbG>Ww+B-Tt{^Z2jjTPb2wL+Z6 zvzcQi9AoN<99|QPXFv7ad81n6FSoeUX~k^uglRqS#EJ;s3iIVZN+085CK7}Ysh}&V ztpkoYkKRgs&-LPD0_6`h?*1eDjoFe+&tzVmFIdG=lXCRF%=&U)KBnv5>f~Kda&?>7 zoPGl(Vf9?QZi<=iFnO)Leh+ng*IYak{wrMq%LlN7^1j5N7{QS{^ns4}csfUH)bG1K z9wTG@FO-hDd|8)F5~RWNn~oZ_(+&Sdpo4b+%~G3CCbQ)y%l+^KmtZ~TN9U&7_8k0V z-N;D#7cPaKYr9eI?ON>hO`W0m;GO-nXUq^JBK%qjvU9Ds?qTX`(Jrtt5PK36W zynmHmdX?MSaWRmN?%T+qzBNY`_TAL3`AnkGLGN{=_A^&Zht5bwQ;_BF$uAP@nX4uC zu6V0n^xQLK8y(@@>FJeFD0d-}L&odiOG1eB9|xHw5TfUcb+2<9;)sJnnFxZbN2K?pQ4B2<@bE_=f%* z1fVXkCnKtFLwC_y9%^oFZPxf#3u&_g`{e+QHX=SNHb;fszye(jM}i;M$c1g-5F)Eq zaBJQZG_5F$_gnK`nh_?7p3uSbF9QFo_Q!8lDjh`6`%2WMO{CI6fYL{VW+cRJSKc<^ zL0EV8QajFvm^_p}C|SGtxWYG}cBWfhu&O&k^05H%MuI{IsLALYYcLX$beXwhTJGNV zGtE0eV+&=TqARyY~f>K4`BvkfmkA92T_K4+ffPeI+sd+j7jTy@GY`{50cjwtLzHQLkmMHON5d6UKB8I?x37J zfDaCJ{SgHfN=S>RQ301H-56-e)W(RgJL=XK8bo1@ULyL<7EEl1mn)(x>}zt)o&yN5 zWMvx{8fK0fBKRhR(mfnQ_fKxf@E|Y92RL}A;uCF7Zy-41cOwAb;-S|n&x?!{6dp%Y zxq$b1F% zHK-Oxd@)I^-B`BagG&IDwgYaLpz@#fMbC9B@t%7M%2n@~wv9}ii{acl(=?1p| zzt5|n)K2sz!<7U7?RLn)nd{lRxf4NZKg3DP{v}HTei!L~^_3Ju)z1}mr;;f`$lM!I z908QdT;w+p`Wr`7BS+k$#s6!K5Y9mRJN{3`8|ufFUR02p>ZIztr|jK%tQ09*ASdW5 z0O41+>gi21?w~S9m?o}{oJ#|}7bnQ9-E7Y=wnqdv5Gei7_csV*bOBuZJJNRne3=7+ z1UzK7Ut2g5kV4ak<$LFrpG5#Ap^*yxD>S*xH7K@9pX!7EyD2KlHSZ5Ra`C*CIY%m0`gu*u&Kv#oV;llXLL8e+RAfy6Tj{{Hbzs@OK&Yh)Q3i^{L*8E@K^9WyhAaQ5 z>U;vnurtfNylM4(bR?+r5TJ~CXKG{{Bz;Vy9sU`%w}e_Oz>IJVrFKT~m`Et4K4cmx z%39G`&i2m9;_UvtMogjNR#(C0;FCGiJP7c*u;MxqotZ16(HEWVOvUA zFi3cuH~sl(=ZQH<`;8O9Tk;t!izPIowEp-5BH!%*%dG51ga@)xsgz9Y2_BtQL`J*Q{S=KO*b z&ERa-2DsZ>TmQcFtAgXM9b!q46GSj8eJ!XYruP^T6LkXE_sIUA(*NGdDY%v73>7~N zQ>k6!6J{p87|7`bwce0Z5#so;!DRkl&kyj?D=?q_R7B#V9yBQvfWeLwSIll#y!h^7 zG{OO#PZEMC3oCZnex_(z^1y}~KfHU%pC^?6ssEej2P%QGJUmKg4WMtPZP52IBt5ii zb8OdmkQhC*?QtyuktzexP#ZV6pp%H&)`O>twM2ji_%Gnu&Sn(6=hEjenf;giZ~{rE z^GQc}O2R(o4^$qc{X%P5ezJV7;wNwxDmPpm6eIbNzlv5O}f&@33y=sN}!w2v-DzGTO(VW{(h>?&}_)g8cMP2ks3V zd6~e!Pd+j&#J+>6xyIYjlhj!da#-R)5>nZ_uRiGf?`8-foAI7QrHcmi_Z@EBl%e{% zJ7+nlUU=&`zm(dkSaT|~-bQ%CqPtLINE9c~iK%{0tOPvI9dcTZ+JED{9MkZF3!B%0 z9!?i2wR6GoN&CSr{Fj!goF;sH<)X;zq+?wI@w7=$xXVKfX6N~$ z2jjPpN1W54lDqBW?QfQW!>Zfs3HKBDFJr^b_9sI9N53|ngol%V=kY6BvyuA`&BB;- zRDdwZg(s$3X6eT+4q1;+wV|kQ5`^hnK$#(=duGL^lh#(+h`7~kqX_wb&wv4f!}@A+ z_HGmimXcM*^T5d}=l;rqyt=+ro$2O5l zF6JG~m0b9c9u4~9=9oyGC$L=yoFli1XZrAQPIlH-4~1+fMFhVMDcc%J%zotX#2ixT z0m#_F$2W|sVe z4JpA*A$++QW*FCh1BfSCfdlF~)mzr3C}bGQVa4)4fbF{QaoV}9M;08U-8mM+@%Og2 zIB6GrDlzuuptzSA;Y$p2dp7*5YQ-bl)A|F`7<9QWx_Q9kk^RRXx7gl)X>gv~9H&{^ zfPh%mb0L)mzU=t#3rJ%U;XUo3vH*{Q|EwT>TWy&WoU67=r-KV&r0ip^Ej5eX`_2wE}`yU*HEo({fT&UY+moi{io*8rXK_ z-o4UR6Vi@tPXISOQ{fZJAIK4JaA}8dH=Q#x9=e9^K)S-6-TB2%_ngs_&7Za3?zlxr zaZWz6(Qp135G{{A3bprEPW#Qol*6*;8WvA`%#vQ@_}Ma=KJcJJW6!&}{yk zbOuJF^XJCJ?XOprgW>LebMq(nG0U_2f+pw=$9;o`jE#cVqU{sqn&<`Kqq>3W^DiLq=YfCul@+w7>K!0Gn|67gB@^07 zd*;#}I@JNS4-5hY&s>KWJ|Bc=$`(B#^NVV}E~1}lQeyY&3hT$ZD9MmsUvQ%95bdW3 zXgg6WHCy8ml@3@s-Aucxqtj~*czu}wo9q}mN0x&824Jwjug(yELy zZzgd4`BzR~DnuilsK$3t9FR|owAo29;J0i|Abkri5aE)6HRuY+@g;PCy9 z8af|6PjAg|7XgVkt4ZdC#W3`<{K3BItY5N9$5G5Kl_B+sKZc)t<=A2B?EArI**F-s z@;ihL57mfLZ6CB48$q#d7K}$R#suPsK9wT}{SK9Ty zztBp!{t*N;I*~*jB#Nn16>C^8U-nr){V3FeZ+lVHVNu*ha<^_9G|Yelf@*VbeV_ld z(?BD^UleYm=n>V~*tT1`WhutRPCBEfe-^{mcLGMtP)yhjgwwh}6ax)#+K@@`nQd-2 zsVN)lC0Q$XoeaKteA2_vqw=)nXz1HNF`I zIzzPh@=k(I1X}=H>36l~tIyfoZgUA@l>M8f%sc*~2}%C<>lNIIx_A*zawl*q{t8^{ zZNO+flZ{8jxkwY?b@FfTfS6+ne_qQC*w6?T=3yby?2rMn}uw z+0lR4kB zM>zQ-K?K6$TA8T_wG zkC)d-|7cw>{_c?ek)O~}7M;m4EpQBymLgx%&Y$hc|FDRFRmd5(kei+@U&~Z!Zv@7c|g*TcktE)PY8n6+!7j=nj}hneNMvD z)sY-GxbZeMe1;3pAm#FabNguvhqtP2w8(y=!9t{ZRaNol2*!k51N^{V4_<&D>mq@c`Z2WE0U*5Iw4a{CGAw`))h zcRQRaR0=#C;`sF&$|p|ZAHMxm6|xaKfFi8n9nFF}w7l^klhoyF@izChwk{WoE8H8{ z=Y?WULv5zwd(dwP_@PlFU*kgWs7QF~9@o!So6%Li{YHb_nUjbEDJl=UTS*@{gA*ei zo@2>vwVrLJ5`H>^ET`v%e;?dCtn<`7{zm9q9!uyh!7aOZupJ6a33o-hP0ec#wsomW z!OLb@9qg3eQRocz1;=ZOR}7AE2Yf+EWpE1%$9=jojUGjR_TlpL2zuLtf)^77k!fdc zzF3aeZ7EK2Dk-)ldZXnN-*z9&SI0?8F{Hz^0T~}Y3|qP8r^|zw7j^YA}gxlY@uv> zvKHT#+mXceN5s-^_y{q?!M|Soi>aytxDTRQ-{m&F0WakFv(6qnv+Z8k!z zliNd13JEKz?$^vQJ%#Vz6|RiS^C+E;yysodVRd+d->37-vqLx+Hrr(qLm??@Y_`2; zslBlG zj&4(gT)~rHjcQuK%1byLZ!&$z9jxU7w1w)*$vgf)zj$1Uk6M_2?)xU(W2z^kyX9}P zboznQ6+91pFhgG~hMwFV^2lW>3!6c@yyVnSRTdMz= zsXnUi!S>4~fVUi4R^N)%)#fACJk=J?l)(BzCE=(tfVVMKp@t1cFjXEJJ~pZ(bE} zPYx;CwP)YeLmw_(wm6gn=E0SB%JgK_30I@1KXS>PGP^JFaz}<4CEYd++-;X|?v2rz z1crQt*3mbQszOkZOpdO{H_vx^%VCS9o1ZJ2WCFYHRd?RlmGBeU-5E!Fumi z95l|KyPBzSSGcC?K+eI}0|QQCiSs9C%OO89G;8skSmXZFkdeP^ z9qWG_iYUZOp-)V)`$~FV!1hiC&9|SoNIJjUJY)2-^56{@+8%dhha1Z$uqw(wU0yD= zJNE`mRpL1oWS?u`5c%a+{HX4LsXXf43A~tkRi?zi_et0J_VT5#Q-Yq$er&a}e}9ua zgCrs8M_FzI;2a!#lbR(>ngAyZ$qe15~*TtsW4f^Gmu8trvNr1~8=1s$X z6AZh*nCTqrwqfrASFi1zOG*CI(DcQ_wq(5y`Q z;hv;^zVJe_wj}1HZSJbThhy)vpLJe$N#;yH6Rk?{St&!y2y=WeCJ3WX$97dZ3AUuS zXQ5m1q5-CuKPFT<^EM9?Z~O?oi0Y-!);q5z>92zU{%Ifvo?cc@?>V~TD7}(S#1`lA zXj3$cmtU`>xPN`#d1K`GQ+?&~nj!hwg!}vFuZ?_r<5ZX8dEcD9Ep^5++sQlyw|2}q z&*vn)jykc$KsZ&VVm(Qf?fr$57t0;VWgPiqia$>1b6&<)o;YxJnXCUW=TM5|n@+3d z+3~vf{zged8Z{&z)Zi`DUtS)8dkV2xijN_-ceFm+y(CZhgkpqKIa_m*#|w#)yehb1 zH6sh(CvJvtJ85zm;PdW#nWMS~a^|HZRlCH$*g2}%F&)ES{vfH>aDm71biKAdEy)96=n%PM%lx0FH5Ugu=!1NHZUrVgX-EAD?@ z7KAP5?b5{Z?Y6&KxI1Oa*8@rPy^;)R4taZ7YuU)t`$w(LU6keU(@rF%zo;~G!IW-N zxr60RS*Tx_UzShyGkv@TEBc{g+FevgXshZlJ$D$(^PACms~6aA8@H?d5Q=`CgTI0P znkFyc=P!A-vwA?NKDPIF&lz>grobbjr<8~7_ZdZVZ(0OY zo`~K-amUfZs;^(|_#$;shK5CIwrYq|8L6j;v`_~lpb8#z|E$9F#)AR|z}gKyCqzGX zCGWgN=(9YrlX9sBHF{$2&Kp;2CTcXkhuhs+KxEDr<)4~QO0=qJQW~WvE9r|Z$}_s; za`?Zb-3h69t-8lYM10U{#D`d63eJ^@snSdRKW;y97e-M;Ns$maoiK!{)6Vyi^@{_F z_V7d?X(UDbX;VRsfKp8g*R5dn%c-)UkQ7#*Sg7YbBA*;onB)_x(_uQPh*Znyv8~Y} zsh{Y)jCu2PYb<3>hfU%~Q4ygG=+hCQI#=)eUpkHw(1IrN&a!tUe4XIz`Vm03oVD0K zCokDW7bk&6iEOTjI)QMWJnA?LjuWch=={MAN8I`dtUyKBS=WDK`fm7~z_93wVG%a0 z&uRXEQ-CG;vIkibpN5iyAz^$pq2Q0pVpU@AA}WP~ZYSoLb;Tm=QV5}(0RBo*|LO1a zAw~UlMHt`J5zvJ^Ijh@cHHq3c>TLOLC&&3S%xPVPP~s|&{}lV8JmXGlqN9S2(~r+y zHdOpNniUbY&-J5N&MhZI#X(T?-MVo2+dTm?Ft%D|EGypL7bS}owrk;x zwbkHwhKr+_ats#ZKm3-u{{&f||BJmh52vzy+lH&#eHSW45-JoCA!NxclFTyC zidbdL92ruEka->!4d$6;9=pptXUv>A!y;3r@E#Xd@9%lG@7vz@`}5nj=f76Ab*=L{ zhvPhkecz8AaLSx$kjR#OYy=6pcH?R<%F`(qxCFjiGW2|@I&5l|c52cU9JaCU&Gpe2 zbo0${$_fiLZ+-GeLQYk5>SNji8zq7`pSLt@$niD`h5Vgt-gbydzc4E~nU~)y(yZ0t z)UkxX$Wvv)B)7l6y!GHV6oP5~Q4Qvt4^+PWkT3%fLAM=2iRLQqbCbXvXZy)3X^~t; zSlsT(wa1l}P-)P;U`WiLzM@;uDL-v$eEiY<{_bSVD^gkGbcIqVZ93@^CO`8uWx9Gj z)iIw;>5;Y=(J_@BUq%mSRTuB9Pt5l@J;GaL=pI`XU!H9_=@8?mk;`vX@KKR2YbUaBdfSv*Ss=P_wUqu(Cuy=6YYca-+7gbhB}C(Ki+uYbT1-t%wHR=8|Qp z%Gwb3Cjx(4?t#E_E&H@HO`KQH8x8I=tuD^M;O^`v<~QK>Z_Wqw+w;?q4KVMIKDGJT zUR?VhFWap4rp{z}5{(_IVWSu`C+9oO;h%Z6v{D-#I&EMwWqA@$p4gvEs+W*bI(p5U zP4w_SZcl>0oB33+QO8SGaFP-j-+x%UI)WSOb@|EKI*=Y>8)FhqRVb&_edW7*dI<*8 zQ}M!H>v52H9*WCq;(iPeaLu551QL2h6TdcQi%myVIqi@#?|}GIwyw;QGbxqjWZqK* zAtghvrbJ`=!P_^NYmFqct&9zMm}&>;OaW>xqqn?$26B%#=j0swkme6^a*1m*z_vp}48L86`KHE49n)aHdd?uM8k&%@54B0G@dP z+~`e6`vGgtZm1OGn|3GZ0Dn4~W}T}|V0^P=EP(Bw3!dP1q1eCs z7#avv&5RH+0yD_pzeY9PznU_J!Ku zMU^#O$C<>fvq2zAQ-`Q9Sfit&$vj*tkXpxJ)?+>KT-dbvGuDm-D=(04mQUIdZ$0L4oI#Xo>5FdAaBu1dUw_1uZ zTNmQAikkXkL*;cyEUF#AnO|E;DOrFZtAs`}a-e%*#02x??UD`XbVU*;n=RBGMPyN{ zEjHKt`n$HC9qVaJ?eY8O?G>lD{_%)jYlHKM>h)OY`}9Ff`w2LaF6%L}F6$D)al_Jf zx&D{|o|bPHP8_BsbqSre{%!j;)LW%l*VnJLSX0(z!ZSmG4@=HG;W{?r!aXFk{VdwT z^{b%~Hi|#r1Y6nu4BFr}_tXi{r%@oc(gLl>vR|u5%o?XeUMGgb80s_W5gyE3AT;gx zXwEJ^DJ}3NAE>dFX9WL_g1lV|E{Tg`p-Z_A>V%l!use5EG@H}7jOAZ7;U2F#hioX1 zbfX!(QxF9UX$I$o{(|&~N5NIWK{fOZ*Cd}1-YMGdr1%=ia7E{WA^VZ>H3GNU&jK<0 z#9h*N8rqTMhCj4|(1KMT?{gkus2z7;=%e2nJT1X)r$XB`pvgf^wfBW)_aRVl3u&_$ z=%i)Y<_q-L&Fk_@bUsyXXj``!f~er~2cFjA$dZ)sNR>MJzJl`$#p9>+G>E|Ke343W)UFC zB*&&{&F#U7Mo~}=tVEdFkEY-NlJRT#7S>r3y`#Rb^Yr(5b=s9ek3}CD%K6Xc0}#_j z7T!aU8rGvhEw~h(%-6JTzLT-(nddQMqDsgeSaXZ>#K9eS6Yju&Vs4faP)e%(0@(%X8>9@dqP#l29)#I|q* zyghbcbbZ0-=1%zz zkg3*$RenKoAztt6Y(~}|{&H6+YEK?LK_78MA5~)AR==OkdxwKaeZXH3L%xJSy)HglOn4TS<6W;RZ3ufrNj0L+b}X(}cD zu0M=dGA5YLV8aq5b;OZ8riZr-B>`+`o^9x~_}d|$vWRr^L5C*V(s=sW`xPNF!Z&x| z<-K9#vV);q5BIlO%3m-rzDR=7gK7tqPjrw_XKgiKavH9@SAr#eQrG-w_AFj(EfM~z ziHO%Ii$bff^OkNs%SYr}kt7D9Q!{Z)e**9Ahl+zodmcq}7|m+J#{p)ibNG@0E!6w4 zgpm`ZUT-2<&tB+jy807R_uDTL7<;dC5fc6}~pZ0Y`+|dN{4mq}r5wMzM$yX}CE6VvL%kejq2^hIHf60wj(! zvxE`;nZc4rIAWR|&|L>^RUr({tSP<+p8`&Cg$de<|ERfshC|C_jL>8;Seh*tNjc6x zAfXW&gpQpb^O}^-;8*4`?1XCuag+@EmbVL8N9UH~75jk^QZLzsH<+`K>o1i6CqK^Z znmIx`DdK9q{jvLMIY6(FUQ(G^UPOPQ0bTh0DU8;=e-5GrX_(t8Yq2)`-&07C@kM$$ z&i<`m7-#g+dutiIg>Qf@j<>!GmMsVAbjoKt`xr8AZ$a9c0X>YEwS7FvddX3|aj`yo z0R%5-+5!Hgv-;@_R{Z0YtA{s>KFkQYn4~y5V2%DL{gQ`a6(a_w4r6yz+m>-ozMoVTSmF$LSe)J~KD^k(Re$@co=@wt=Vd6|XpP1{2FJEA zJ~@lE)GR+dab2Ep;!v?q|Hcop7D@dMmZYn;oyB%viTUmbMK^P#uQZkvT)WqT-~-tq zy`rJ_vXX-h4v}kt&a;VGhLzAH>n&@=YDZMWCK!(Wx`{~0+4^fnU(Q$)YF3-lRVy>}ytufDG|8UCPt91)qcxNQE%;%yK>;cfoI z@w1<)7mIh!#Qa_esMKC$C{wR%#rbBvev5ypPzl(_of0;eSfdCanxNbghLTyR|93D%Ltk(>N9x=e%vobQVCiGw?$)K6ec=xRGX%* z){`8}FcV@hp*oAV479v3wGB~o`Vu~otl{T2eXa#s)S0d?44Jw8eLe?reyo|5(rhKw zNcjwu*yYMD@6oyqRJrbLFbw~>Fx&|siLJ7la_)gxnNftMlek^;d0D^17jtVwt^m)g z1&4((kQ9NRY~e4O^gqd=#m3t>NsnlrB3-y6*LD%+3UW+@S$;w7OM3D_6w=ccIkctW zn^SMD`scuuqgFWM@n+}==dqet4r=!d-LTsRg$?JAemO|Bcp{0bH%~u=;HNjA4)zT7 z=j_g;vsha`4!^-ns0Y;{&73mCesm=IBD8m;;0qeV?&1$7?EeK#6K^0AMf5>y4QzO) zI7Vy$+Rw%Qr)@X`1voAb(+eE>sKD=s@umVlEL(qW5(D0jQiZ^jb4+JOSKMn>4KWv; zivt|F8T8lVm8b$?mjRG!F(ooV#F$M+Dk~xi`nl8F`Z@qzw7BxPtd1N1m}p-S6i1A7 zy=TzX|9eY+t#|<`RDwuHWQYiempT0y1A%rV-SY%On3gCtp3_1b4+kJEv@$h%?LQ6& zn-MW#-%Wiug#Z3u9zufN9WWsig6xIoZu#EGdou*J)Wmai?D&VCk`d7_RHvi%^o~45 z|NPK&b@{{G*kwE={9{e%VcD7;(v7mgnk2;hEM-ta+E%D4wU=OlPGprn_x>1uMyOtY zB4#iD4hf_R@#b(*-|~Z{{y%Owa&Zww71u^tq^JY^5#J4tK~v*{VbapU-Xx5A;z_uF zTx^jGTdngB8h@}-#9(pxVnb02r22(^jI@L;Bhcm$1=eR9Dk;8~7mg9%c! zYii3Jx>2RE&TE7P21pzlQT7{$VXd;mWqJyD| zc*8zpIChmEKe9vTlj7n{{X*I+oXXK~E;{a}k{q53cxjVE_xyie8rdsU|FhEnzuw*= zSRY7?IQJk?86pkJ?3a-`M-84flJ!IsDWL$5ZmFdb5AvfC9!7v*F@W|q?lCtKL(n%! zKQpMr8gsSWQkudgZ3&q5-{J=jf!TYH^Lb$2%8d;6LD~TO#UeiVvF$CDWHX*MBco?U^Ik76gu4xr_7(vrSkeO@!?Jw|j3dJVMy7I7qVXYF>9u z-9HIW*je&lD^5;>`FvK^7wSX$y9nHiJ%?&`iV45S5%K_H6~ z`{U)`fd9zL;kKo#HdT_ha6X--Q8Nz_g9$YI{J66{vS;r2uWD=K7I?g-FhX;>vj|3F zzZ))OR$jB_)nA!Z)=`au9DM(2z8|#g^7LNmQC5CBW{A+;FTkVK0YaJ-QQlAM^Fw8C zgO`xAyR*IDN*FNpsT!_bN&T%d5IM#p4Cmd|WMRW$EyUmxFZt}HLGV2;nE{~W6phI*=02pIt5%)= zr5zGtXl^4h0PV7NO8cQVFXCPQf(vVV8#FfyaXDX3yJ#n4>@*vky9R$87Xrb3FC@ay zXhL9XyT>mD*Fb1jAI;cX)ANg6tzzxjB@QhTtO(7%*`|F8R#!5{vi48vC>8!Akv3)B*02wG#UTQ$BG&SmBmp2upM^y_Ruv)Rb%ayw%awJ}IMEErlQK8K1k zp8fl`E`bT7p;jU|pkJ~J$m9jS?gFxnV#v>`iQM=*Ah-%3OV7qS0Emc5dy2U_ zHkOPW(f$ggf$idRxia-hlK!)0&_iqh91t`Tk^nx1HB;O<;K?O~4YBKJQp2c^I%OeA z12@g+6|RGu=G_%|WAcCRGvF`WTO8ndkja}ziHS9b6Z-2)u!bn-A?ez-KIsC=gdtwSa{Pgesq#@u-F&`pRaN&-wMS+-ETVA3vO9E zg2O@ZjgtC>gI1Go)sAf=%5T;mN}kTGwz=+h689m6HhNEDhH6tf(eAQ<$jr-H#)r9( zi7Svr68D_Lk5c4&6^uK>9f`+et~h1!xzT%TV0c;RQn}dBF=nVa(4riP;#q0tdeQ2! zFan(8UohkSfTW8#e{2J?^gOtSN=&L4+Y9oblp8!^mUQWTh2vVD%g)xOweq-wRYh+} zI^V!g=(2(Z)ZFLzFR4S$e7ziT`JD=*_6U4#3pKWGUNU*!)gS%M!x6GyTbC?}rpwxd zwl`MjxOblkvl$hHJXoHG)3t0iMZ~cU3H%KXpoAq0W&8l~$Kfj{mk~yhp7UB5dL8tL zw{!BSI+5lONHsAvqXV)hGt=U|=X`GIk(zL@hbvX)n&@&@(HXYpwnjNYl!vsf{3!t6 z;OIR4c_XVoRVle2=poSZy)>1=KR{slO$DTKT-5UH#KNhlG_AAGl=Ho|f-vr8sM^7wmSqde;R?&G`1DO-Z)B7M-7sLRw^Yic$tp^R&iGr)~9uK(7{TYiA`# zRlslAy2?(srd^g4#89VU2I<0pslYuLYJmzqBxqfx0da?xv1uGp9Ec! zW`_AI-8OA$Zcq*LH#Nobdv?cK<@hN%etX+rxm(nt0w(4KoqD(*GHQ}zP=Xm*b^ch= zGx*dzv@5))P?3!zeee8_G8Nnezi)Z$C9EZCYf(nv>G5THdw<)=>F0Y8{80!CVboZ% z9936G((cK=>AQ#F-8!`&8u-aT@~$2$F@g_7QxLo}D-(|@HKYupvy*<9DOP7BM75HH zO%N%HToDmf}ge1pv3|I{zkp zI;YaLWG92(a$^@UMdwHUa3z}17;PfLV~Wz&sVlzSULo>AeZ)#(#IM9Z-yiz{J9kYx zzg79(bhMFFbi3|vm18Qg^lV}DgbZGx{U)megGO;2+Yl>nFOyD3Gj^wPtS$@uY~8uh!M>^;oW<^M4Z=rZ zYIPOlxwH4Q{*}48|!|2-DC^U&&$-6z+ zNy4>?Nt#xWTvp$~^%s~hS0k^!;JSLI(Y-gvTD8ovTa6*}0pat+pW0Fwa25PgrM>I1>KPac)Mmxbi-Hp5T3s>8V z8PJ%oyJBbVm6%-^TKQx*_ROmDxfg=$a(vsilXj`wzXtuK^|Gfnf!MIt8~alJ)F^br zTk8!Sk;LA&^RNt(fZ}7>i*##}7l*P;Mv`i?D~+4Yi|l;S9NoEZ zc|01Qx~vI~+D+Y)JF?}tb`e3cR)R~smQBe$wRfR5v$8H^U^8lL(&gs&-uTMnw=AUO zLM7y^B(_@HR}y}YmQ6zkO<9%1r5~j|*eQ{6o0f)_dP!5^)6R9X(NnEEx9MwS;=hy| z%a+kJY`j=FemQmPi}g%(UR6@%lgu~rzqfCw(2o1{R87?r?mX#Ad7!aZjHwU`*!q~T zcstCJRN!MFlT}CmNauQt;*Sw1%}?#UNGSN=7+b!+IcSdSvL5-6$5-|9OA(ji-nM>! z0M~4JkGk_@v_{y;pmN1a=En^R2cGukSt@#h1>T_zd>v`~Ud}m&G>24d>0E;5LsEh! z;989LIq`bSdB6K9=9YglmxN6@-9lcJMX!V8Wc6npkxWw(&86a;4BgMh{-J|e_u|!l zk4aupv#qW%4b(MHMhm37Rsqv>`*(DDPXetG!yZFB0*2Jf-umD`oElnLtP@b_4+^I4 zW2wEg1Ft`I`sKu7y)}whZmFQkuTMsWtA{=gRS2PPiTKRt419$ZLlaAOy^>F&ZB|A^ z`uP{=C>uQ&BjBPpI3?VC!qBRs^wMXSxLPxvqGpbqn z>w|+N))nw1uSLsv{voZXKmsTIRl$1}hEPibi|6L`!;B1}xuO?XOmoQ+rwGzX7%z%; z8;=nUsa(S?QGjC8_xRpniLZ{4~1Hyf2oAYRHpAfWYFotTy2BmM)-7r33m|?P%@Uv1bN2ADm zNCsWQEMOu){zYDa5b z{}`dB=mK=fnKf5%k0a3+swiBuS6s-Ad933?>u_)Kk@n-eaji)zZ_F^AGj`{bTS7h) zvSd6H2+bWRK=YCGNea?TaStc;yxdx>#=5$UtxGx3h*GBOFjh26%2#D!oTT~Zu%VT= zN6q=>)X~gyH>RIc2|Rz+PYLRrpUj_u6DC6RZS{f`r#F z0T;j?)BtLZ3*lXCaIx9mLl-7s$<_ikHiPfRJ${pr&?<^ZjaA$VsrQNhLRl@zU6au& zW6OVSXEP|+3s`6=!T~)v0&a@)g*7+3gef|1*=C`Wem36-ly8<=Eq&IYdri9IjhFy= zlcb>(IX3u5FqSXd8syf!c}zx4M`=+j-Ws`NVi;ClAYvtVlkL4*@zROZ4;c(mZV9J< z%n(jrL-YK*+j$L}%H+o-J);2o827M4IfP#~8uC}{?E4%%59lV-2NuQerfWUYPt!=d zoYF_da?IJeMiXOAmI{=O^{T(%bW@8@B`$ zqVcwYZjxK8Cx0A2AvNnuyg1*h{rmXkrQIhwrI@^3#S(p0;d2k-^9$E7V|GUdfC_cN z;@aFcEy9xW?;?@qti^!4w3>b2n+Hgw*CS> z>6?Po8o6gsPliraj6}5Z(7PqLbDj?Qs1QzsnYrsvayE)mlV`-a=&#lY!bj#}7$WPm zGZCY2H5j#iPdB&6)46hF8;iYHAm3F*r+D7X;EPZ&n~D7#D4OzB%9FFFFM8>m=*=?B zr(LTPlC$oH0x_dp?_MI$4q5v5V>Q+%f&`sbPo!_S7>t{R2sU_jtJV1L#<8coip7dStp} zuIIEr_mQ1^qk++5{by;#SL_8u@NEIh!;pQTlOFE1s+o^_u@J>K-X|{i)9k!sYgZ7& z4a5u!b8WxAkW&y1k969XC{`}|{TwH0d3yC)wdvJXD(C&YjN3h^dYG+JChYw&crj3? z=y#{icBwWUZ_)U&e*3znwA+iyW)6MH;JUL=nQ1t)|{>Blsof`d!y++`6W1QhoEIgc1h0r zR1-&QNEB|*LBd$I-!@F;lJQk8auy?%Ru8*hYsCtQPrC`^K+;}v)-ma)%+SUj&zS`I;~ z-jlkF*^a>{$7i}ym7FU^*}Z#yUVTN|#Az?&S@7MTU=el{Wcrx-O~2h7`GxdQPiIbKUnqZ|*Mpghy7qG|w(KT$SNEH3 zR^!_1<9=L~{HOu$k+bV+*W2=^Wcin|f_uT^OG6*oWC|DSpwv$m%{}l zNH#{yxlM8^8%l#N0^-~goE`S6JB0L;VElIH zK`oV|=yg}T?FRVwQ3GaAA>YQycvBV~J5`pACBe9JGV*6Y5qjz_MmWA5cX|z-Du*vOy-B^0t=|@@vWuXHxr}T*GY0M$BiVJ~Dn;kst0~5)oDkXYeDv+rGeMAHYucdH+JyZy(~_C z@5g-78ivZc2464E4r*Zn(zza}X+CAp%igsTHosX^HaKRRU_SLc#G%%-J}LJ{hFs=2 zhe7Q(;Fs#!=qcYbT)W6t`r||8$WW>6iit+ss}4v{2WbX?iHozi>Jn#KX!72|5wS#@ zUl)!Op&;$L&D*7JqnmPp4XCe+OXRXUI|lcpxx$pJFOR7v;${VOVGf!h)5g2-CZ+LAMq*)CQH~Ys|e*X zQi+J;#b{-f^t#$#E8%)V62faiboeT^g37U24lR}PQyRSbg>=c8x%`-O94t$-cJYo6 z-cfD?@b>#&t}~P=I0=hH*T3A5zHwhp@`la)x37C*r32(f=E;dAXE2q1fohye9de?g zlEHSP9u%m_5h#0z6~<<>y-lP@G4G9};E}$O>mDIUDgy$uDEp*LNdo2)YcgkD$vit; ze;*`{Ow1|ba8-M|x?`y__k*u}yyv$(cI)@~YTWLvqLp5$v)HsY4}We7m)^ms6y0JQ zC~;XoLw}F1R=?lpjxJ7CTe9b`EqAvpv)g$-NEx7L$9ntyHpDPLDXGK*Pq zRdQ_h_+P;e(e{$UEoD7Y0+Jol3+@Fe1uTycJ+tRWk-lKbdGax+mH?G8)x|J+@9wOr z1d!)VEe{T0BgoX-i>}f~QnYzgMRAmpkJcm=Wd?m`eAOj$W{#gEZ62eeOUf-9`rv|b zry+VAlGB%E=${;l))d_UNoO7kPVKKZom7A`QAH8XwrZ7ToXMJL=$yU~&+oId9=Pn& zh5+@=BP#TIOM6PJ(R+eP+{ugBKxPIbm3SBR4-OsnrCTAaVJt>NSsK=54HdB}6i_`fCkM{w53c)rf0D{5 za6StNd31LmsT_m#T(pF`sW#cwhMCWXTSHlW%jZh7IfI2jdM=ehnKZ~F*pA0oE!|G& zA8)l3nd0oiRg&MqRKZW_p7v(k^(S~FAbF8FenR4Xf{sn_i~b<0>@tNC({+ZQ+9_Hg z&zWuC_RX@aX{N?DUMeG2nk$jE>b~;SMf*NlSfZpyGbWY!M654RhTuhe+=438Z;dZHRac!QhI=ipM(IKj;rc#A{J`qQmL z1%~^yFqOv`F?9QA50MzCa}{o$Yd-`Y9wC^G1y`n(5d?>U9`x4_Neh5nu9AP+I9!S_ z1n?oCWPAwtI0pqP*1K=E@s1EZ25>7gEbk^a&SF`ko+UQI*#2Xv1f&3debzzAx$nH@ z^TJSKe$2=A^PUIcZHGr#Z27I0(P%0F#5c-3NRJaVU?=Z-8K=T7+ zwH2&e2v={7|2Yn=8`qar-oj7oGo>DIj%Wj!jv-Ppeit}CBaQHaYXxn7C=38?Uy51m z4_NuXi`4`Jc`#;vvkm1~ZHi*nPXDiBH}9?!3V>6Ow9yO`EJW(pbY2$~?Hq{yfHs3+ z2n=Ldc#D6;?KX@#_Mb%ngf;P{@VfQ*XyC#%pj2)=Y(e!01Y~vsge0i<6~@nrXU=|t zq6r#wJS5U*-`&KAEqj2?-k~8L#=|_1xS7IU=}V#CrqW0Ct&Cjz$gb8bRX;1kTOkDg zs9A?S)gJ`FF9N#}XM4nU5~DI`%-3eSo6JEBJl27|Y@QQQm$v>7h-(e(@bhgxv46HG z;1E$!D{&TGZ~B~Uiq*)ECHnLg2cEhl+WZe``Qe*K$9#fBj1ed^v)`@?Y==_GxwFgD*1K5c{{K4H2lG~{~wpuO;jHh5APxN zpYIXz(yr?C#oIaGk9YxQ4yia&6cZ`kh>Br9by?usY2JxICynCcho@iUEG&Vz$Q5Gz zB6(i}#M|mB`jUV?isd|;YKRCMht}J`Q#=1<-SEJ$2XkU{)hnAnFzc-e6UyLM5Q@Cbjh?0!}q7MZ({D*1; zzKeVBb-7W4od=)AU_y@bZMlg*xZe_Z;|9#O_Em}IsViUB($2o|?Q2V}eB|}pc`vO% zyavB(Zn3*TR`;6>SIc7r*GTg?SHIADx)p&u@UUMv1mB-YISa?bPo(oCegaPc9Dg83 zNv**Ds!_UUxQ{jRe1{jddFIwFV^9n0HO-v?ADRWlvWXw!6NO^{^|WclWCDcaXW3)G z?HF!D&GYvO3fqyjpMHO}_0Dd5gz!G z2Sha$IVFoYf;2ZfM~WcLeg5~aBKm=5ow7WvjxPb9P_;JK+lHWx%MI-Y%y{^}zx-?Y zKM=Y9T?(uL*`L2aKuS;_MNOWi5+(u=WEFjg3CC#eeoV_Ng zB}IiYjWh$J{-4-sab)_Adf&jm89l&5QC~abZGY}7x~a>;1C2Wpfggo{L`7D}is2f< zA^aJ*?I$nIe{%avmfvt1QUm3i#ed9@1B`Og^5Z%Dd^P~BM7C9%f?u)PG&iAMjRT(Q z^FHCq;Zr{lmV+dx%lbT&a3AWqu4T&Zm0()9(Ij(4JywNVROy=2tr)TvuYV8Wv}p)` z-|TbOA2;PA>xR&|P%q{F z&;f3p1B)dlt0v3m8<|OHMCA?Nu6_FRGJY`sOMkN7aK>)PL(#d@5utQv!{ zkV{B;9ITLAHF5x42#*|KcKZ?9^r?59^!S-Rg)qMqI#ohI2g&4B)IH?emQ;9(jmSGv z_#F1`{-wjGb-<349v44c-XjFTuxDe+dD(9PKA%o>FdOkrYJwE$4jxat1mE`7J9cE> zyL3Y#M2NqB-1@WZVgQOpb=h<$U||xlyy80`m;(C z@bzmJt;FDuX}vkK&H^EemSuu6-pdBJ&~z-~$dUb3dv^*(K5&Fj7VjOjE5O)JaqDo; z+|Vz)7WtGOzD=F{?~8*i1~pZ1G7&wo!>jRsmK;AZ|Fh)(52yV8IX94#Ye)KLZ$s|N z6gbg22sc_)*I^Q6aPz-of?KwN(nk-`xucW_G`A#AWpWFonKD7*9-&jICGI{?EU7j< zkbZ5uL~{~ z8h4xe+I0mW@MX@)akyd35D`KIY>eDt1x#w0#ItRH19=D@^vZ`EgF63in)~?9fJ~%XCdr!O64Hnrac+y&}(6QXJqu8Z>~e(ss? zg885elvO^ZU+8@e`@j?A=0c-B#3OS*>-!1#uEs>2X=gH#m~6-<>vHux0MwqSXu-0e$0Z(h!+iUJk7m}Z0{{Zd9_woo(d$;uJ^kf?kz4Z4>jFBS#c`s$bK#TLs|@-W*cvxKJa3u3)Y&XSoQ+@^Rk zdE*<;#T@UJA!j;0yLNxq+|8-I*EDm0l%35sG}uIRGypLCu<;|;E71G2Mf5?Oc7OJs z;u(AhnCDmVj0Niepxv^zt`XammZS>li7NK-EjE^Iz11m`YV5MNW8bz6*w`G%f;Ggy z&sfVUX_e7WTchm(Wsaq`8W6va;||T8?T+38+_3?ipLCi2#S+zBycETf${|j#& zoV`ycIhvU3f%()z|9LtM@hvWKoI?DoCsM~iAu;*7@?x|})=XR5Ti@p9qk7N`3SnV7 z9PNHDc6={QfcZt^_2h4+*p?~Z-#d%0>!Q{`+qMJ9+l$fjt=THN-Dl;4!w?)JqIhyW zD9t5Bh6CLUGP!GZwUp@U^wmX%S4dP(Je?y1`E`UQywzSdYann5Fz=8BSl?VR?VbW0 zvBh{@KmdAa5zzA{fJR)(Q${y4cddYKJUZTWw}irt)D}r&3S)UL|Dzdedme|V-7cDB zkiMig%MZE)M?v><+R@yvQKhci10%oFXXAvNOHToTn?9QzdW?^dcuxagH>$rWv`pyB z-X4QLpYU@;aaNyqk_!0rx@IgH`ESuo;Zk6sD)yW4?f_KZs9z(^j$XWG6#lmW8nySW zyahy2vhYqr(*r_NOSQP1d#7)IKY|cwoiLlb9eq_}3=J&dq7kMPK=J%5k`krmCKHk&<;0Z+ks#g zi|gkmpVH`+JoKOxA;^(txaWd56v6kvgUY95*Dqh7@j}Go+-9nF&&xV3Y3-qBBdU7x z&8YSR0=HU*pAg@O%jt`oM;K4E68XhGYvQ|5^tyf8l9SWw3aizz8(W}xa;0b-aD`Sp zDy`mpu4cZhg>MG4zSK9Q)#LK~0RArRIz-r}tcyVU^f|W1Y4$D*K{Ymr%pfSlC}`l{ zxfQ;sh{l*XzX5M5-XKbZ;u}Qf7PK|n+O#$ac=Zlt4#t-|pfn^bXvsVc{#sR2Ig5F;)_8y`J*Q@j7M<93fZA<+LyYhSHUg@sc z)N;-D+(JU?Qp-UOG>d)l@uLX>vkALL_kg#Zg=S(iugj}ibbpfeb}M!h4R|`Ccrx!) zry47tfF1vV1wUQaU6VaD;N+ZiqU~G4Y8Tv}r@0ha_v#IRff67Ydd8pr5&EO_>$yVA zspK$}rRa)#_xiLCqJ>kB)AvL4+UjvOe$bB#S5G*j3=QT!Q(2 zTiMZE1=%Km=Mi{%k&5ClEv#_bP5zqf%HD`ehn8@tQY`1ej(`LHZicYljev%8Ct`2s zg={BD?9nOucY3%GS_LIA?9pRM9LR2w75@%}eQ$aZhW=GFRPy~`+yz}181l4FjHN)T zDrgyWm!b(G7G^3G&-k;^TL`367^Uq2i&9=EBC?aaq3wBaan~An>gC$c**DXdkz`Ks zxv(onD)&FD*b2b3r1E`^p)~?_E*0CR+M1T4iay&LZAqc-al7KA$uK8vsCEkodz&ZH z8Wlm=GrxTF^?>EvoR4qcGxl<+VPiS^ezA&C-6<24Tq9YnOYf!i#N7b7GuTp^yUu5@ z#*Fo0w3SUtGUOhiF7CtVrceOOw72{s3x-KRJcfbTbVKSdUUqU7a{J zCYT{H+08c$sR*r!Q>3@wXi)Tj>wQsuR>FN&F&k+bUzJ=!e;BzpLfFXaMP-PO)9xc$ zz`;gk#kr)7#cyJ^7}6J`WZA=C^(5XbrrIx zj7z??X-x}haWN$tNDr)z5>TGXcHK!w$W(Ge#P8o?6bc2`2f1`JT^9*bc%$apj8vG$ zn?%Uj z(KknUBgdLOoVkm*y5h;xxp=G@yhXgf0YI~(%cEqiLwcE|UZ7~@D^e)VHBi-)=2?JWvxN39J(G_D*Khz>U~_X>VUc0Vh@P%&p2qZml&F-t(9chQJvHb$j>%*R zqIB0g6T-*Ui(@tmssd3qs|Ou65ju8~EP6K>$%)n=fy^+*y-01E<-uZs^@{EFYBs(|H%N*&b7M8*zP~yU5VdYon9#o>>IW6Rg=_c=tBVd zh>mprwmCXi&v*3Vc#iqnxOp3Fe+o@t0RFx`*8BW0d%=Xr zUR;oxz{{1;Hh@zs8MkxMfZg1HwS-oI7qEa?7p0B&y=FHiEB=3+Z}(hRO1euonEAQ+ ze26HM0mhF?UQTt3&-QeBu;?Y0=8SpBMQyBhsYNVY8;~foW$O zZ&Q0C>gQ#q`%fO6;Xb z@QE{xwV;0M@1C!a8R{Z;mmBVLmQK3=HHx_fZQ3% zQrF8S7y=R2XjR!<{a;WL_+|T;-xsCSTpIYVkOj)&huzPu7v>RWX*73s5pOlsWK*NUgt|ah3paY6@$TR8t!8WMWBNTFpVoRI z7LHUN{+V=+;{|M>ZAZ?!=XICtH|7>%I~C0O^i^M}8(2@zB+dF%UH1rS^vI=gsD2>s ztr6A-j)^mk_Wh^MG7R>Y)2p(Ea_C@!;Fxh+(S6vtST`-RW+Ll_=_Kt~$mp%ds8QF{ z6cZC-`pL5}gHl^(UtKVcust)m>j|{Ma$EK{eSp6HDtt9~A$V*l!A@Bie=W1Y7Acuv z`cu)=8_0)e2Nc9 z!x~|3abqIVRw2UeKg~b?kZ-$NlG{4C(PlWc)z6mB8yBu+o(n|dq3Kg-+hH&byUiM| zUw6VBYBRFZGl&gMxr~}~ppwuTdlFTEOGunK+5T)7opN=hd#8;HXr6&!yx=O{6wW_D5)x^6%3w)kpth%~<#0(WrI)m)MCgZN_XnWu@F! zKPab~0!TP2UWrk^#p6z4Vz_4GJ^zcTrii$WHGdF7tzJeqwnEfr2Yyi0_MKPpy&J!u z-Z1ydwp|<^inS>~l%#A_a|bBr+E`DckEZJu`S#rM;;U14ng;c>ZBT)88XEgB?L}~4 zIGDVSpqsEZ`&OtUg z>-Xpi0b-s|iM%GvzfaBUQmYt85z+mAwz7GBz$ve4z}>`5yTp*o^gb=Yk^ZjEeGT>- zK)^G073|dm3U_76&+63W;P+k^t=>@7keZoKXZTTZH&WP_8R5VcS+i25Kq^5zzPL7nlfr-7A-DF%(?#dp$QDNy9Zg2g} z>yjPC8zwETm`=4RDZ0Y1?ro&mI3Z|l9TZu}NH>gtvm$bn~RY>K76| zxOrHQ6oGD(Mkb!oo_MDiikqNXh$&{aqvp^OFIL7NcoBYzEPOIaK?v;O<~-A3mYxYD z`UImvzM&_kzf ze}|;q`7BlDQTl~G&rE0h!Jvj?B9Xh&G2O)l;oR? zeu3cG5W3{aZ`2>EN+A&;qPzIz-{Xhps)1nZ7jCYJfEQ%%RNVhVOA;pkzvSdp$sz?D z@}MRBO|&3{usJ{#8$u90u@0OT&2T&LCYl-P3jJA90a((B7T&7|OWH_!LLb#d1!e71 z|BJLY52tzy-^R;Pp)^P;N@PeR88c*v2D5}Mwkc({2_bWe29Y9jhB6PEOxsKekuhWD zBO!C5uIo3v*Za>o*AAb(K5IQ|t!KEO`|cRbc-ARQ{g=*chi5PtCU+R? z2Gt)(7u8@HtfwU$@FEtBz`zEAErvqCFZb3dt2ZC+Yn=BHOtr%ci@&Q4Kr*3$y$=Vr zfo7?ccYQh^cn4&6l5-mhDL-E#{hm||8(69WyIk3$P^DF>Wy!@U@>?%c`UaOz(a@B$ z{Qgc^!NhAxLsmUTM&5bk>g$I$45a?~O84QB)Q#j1DCx%n1sAu9*-8JNhO>SkeXQ zH%#U*h-xi6XU>f@<^dEc;@WqVg%kpfP@j=sA3>L=Q1C0k+~@>RUe;3w#MOpV#LV{n z(SGEiP~U4jIQeFG4msr8qlp4-52c(yk!|+;WO0ZAA=0JnGEQUvaNl}v_jLlfKA;^VBg!?`s4x=dnbmDMbGaBJa7qGjY%H&d-ru+!e zb58zI!*cy)4p7W=L!!Mz5t51%(3ejzYf0#QeVtKj=F)zmh#l$snV_mp@m6p^8f)D< zKv?&2vn-+BOmwpH6}t=yeShTe!mogWot)_V=T?sm2+u`Xc=GsQ51^!FQQKS82}1iF zXj!6;H2x9og3?IJgSY>X23ksXid);zu5a?*L&5e@liCF=!Hw5cWLqy#kiz$rq{M=j zZ>W;e)yqU-KT|uHVIhwlUj?EK2>Ymt#tsWZxIGDx+&OFw(1cC^LMN{#@J#ocvuadE zsC9C@zpM-;XuaLyPbrHG(RM?QSix~lZ2LN zhCsfrHaTZW;!)?~)Yg-nO-McG$$Oa5j!6mb*l|ok?#g9#;Kw@^O}J%I`x1al!RNI8 z2x30X1xF(xLkv5bK)bk~K`5>H_9F4AQVbw`NU#|}EaM;(tw^4-E8E7)a@70Q9fG=7 zKT%L}hi3z$ueF_NYBSWNdEV>lqD=A2M_fhSsrzAd{QC)j#NxB{E}0M zL!ZKh?uL^7e+m7l`~)0F?e?H8wAu-A&K(A;{M)(<_lQcX6hE;b@YuhhFurZUYA`O5 zHSZ$nZJYKSKExD?iz+n@7Gn;@1xHnnx*>%$ROe`G74^DF;qaB_I=_}@X)Y;c+9cUp5w}y~eGiwCDiSiho#p;il z!7uzbg>sMW+FpKBYxs-BDGzObWSC82@c5F!1`VQZ#Mr|ZU=uT($CN3JUE~YvGqUdu zNTV=q68ea8o#uu$T5rK=_6LRvROcG!J`;B<3{D;1_OF?lqRAWdDh`cnTp@?>62IQQGu;=pkOt_*@s$jA-S z!E!K(9gI@x>7DI47PtoE=1p%Usj;rWBGs$t8@JKG!&sBgAcbRdPO8(y?~;f4dvk-9 zs1z*&pZ4)4Z%fnfB)0__-H2K%e5xwwY0lW#a>MlICfV#KPJ%jUwc-;#1ELM>8hRGc z_dKR)PLk+c2j$V>M4oJO%1@S`f; zgRB75YU_>i+S@oDbrvoQ7tq1Dv|EMu5LYJ!rVrCHaEv&8$P9hCa!Ie`4vRk55~7vs z1?r@7V{+W^65&3Nh$i^zCX9oP79eM60;_MpCmF>Ql1b};nCfsAcHEp6bf8_|>L4Xz zZu7T~iQ{U6N2iY7+xKT2o>lOMLRCT9L~CP`gy~rstxeOG1{GS?g*ecc{U0Z?ESgLj zAH(f&+(o+d%GutMYGh5nt)&byzk@w=fL_kQxq%i)w{&B`0%`w6)Ko zaY(BYmuVk-;;)F~Bn=u_-ua^3^lOLe+AF`*&g#7k>GU zPBKYscNUC1BD%CLg$?v)eJVF{YvOEx_ z)&x=NaS)}}hE=rqPn0_8|7aU~I_xzT6BV@teR?6Iue56bN5QpeXyzhxF0ec7HI!Wa z=Klv%0QiCHXQShcj2V;{-4dZa|qn5BSSl(F|PcyOz;WV(^I!D9qyA z7PF%f8(aGIDU+@oGbG;{RMHL)v_f&|(>bHPKku64j$~*bwh+(AZ|GZBK3D2r?8Y8i zkM(b6Bv~ao9xcALXZRZs7S=utX~SV^t6N3AeJ3={lTFgp^`Q$Jz&$InMb@r^3@Gu~ z3E0g^M3%Gz!y?{s>kjpFM&CZ>aKX?vib`v!*iXg&&b>d5H^8uHeDObJjUYHDfkh%d zFQ{f(H&^#)7{o}h(jK!HbYnlv6)zZHD1sE;cYueFP0vhpW={eHDF>QDTXMD>%hn?C z`9FLLVpZnWgDmdjM}ksQFP0sy?N!lRJXALvf5vPN(b5IvVGpTh;juiQEXNmhYx_h% z?%LtxMWQ?2LX{(PVGS_~(pGWaxG9V{WHcuIw+D{2elFFuziF(et;0-K`|fvWTl78O z!q+wrV*~iscSs+oerV3A>R)MaoIo5YD&pym7p<}QMtvC4+<5(Th3!&ch#Psf)2LjR zC-LO=1A%kg8_V%_@O}#2I=QDhN@7J?tl3GL$Cd1l%e{FjsqW9hGf`QTppYj@D{mzbD#jMmvw4<=Fyel1>ncpH$m`{)u33 zzSS6)ruiZS=IXbb`2<3GZFD?(6R|fmzQ8);Y+FrlD0Qde06VmB7PnCFpdu~7#~naK z6J=W)8KdDrU)Ji=;`V;SZV2|n%WZAb)XDWmSlZ?o1Rc+kXnaMfIq`5I(7t(}#B2${EX zjCW+{ueVJvKs=PGspnKF{)C!gxKVW`sSCWSPJ}X88`CTsTS7W8NN#Uz5?QZU7%gY| z@5_@-)w<%?6wSt%Rn75CI%|3(PL-e*Uh&5e*5Pbkmz(%0x*_m7=JqXHcbYDd?>x1!#x#UYLUM&vZWReLMb%dz<-gPIs zC6#kr2TJ&7o3txe2dTBjHon8k7T@YxVrx&1HWTlAFi3r|UEG4xvgBgnNP1M+4~run zQw>hKB~Mc>x}Tv_nP$9xZd_vFDUWuDn3Y-SfrUMc;ZL+1lUC)XaUd(ZF4F3Fc_v{x z0I_D4@ywr0a&+5R=-@fu91u-ujLquK2TsX97wxaha8oc=SiM#_R7a4#J~S!>hhA$$ z&aQ1&V)&rxQ-ehiV=b%?7pzS$7^Y{CaD5Q+n~E9aEAMne#p)8pFJLj;O243@#^^C= zq0iO&?c2B8l53rf6R<+_Zd7hLDn?NFFwox;pu-$Yx~JypRo(&woi4&jgH*oiDg_3Z z+usmd+Ykho+K8kwEo-6?s#@P3zifUCxPy#H+plN6dy)C{-ffPPQ}9ltN)X_?>G}2% z!>)Jn5oBdZj>hqefdkYF<_#`{kH$RWE~r%U9EZZZHDc>~DuOUw_0f|3_Zt9NhWnE= z4+@bs-E~oax%{XiPV@M#afUl^T^xtZkd=UFjF(+zwydrD*O?K+H1|^;u*tZK)=A2Fkq|t)gwJjx+Y9 z>P*Zb(z?&a-uI~rXr;zai1SmuFYKayVs>R2cG6H|blUv6l$S{U@v}wS%u~trPl-C1 z-$^bTprF0(1SD7DCDHRbRZ0W+c74oo@s_T6wdw0-7g7)EF41QsM|ZV4Lgg2u+pEnY7aB!ikvsspZnTB$&VE71oi*(??SjB` zh=6@$pzLdMV~hK#6y;pCotuC=7@Q_VapAt4u=7>X!Z(`N9&rEtI6f-XwjaycFVFDF zl52r(i4+8sfX00{!I#I$`ZQ#bWBO_y5-geh561EB?ymHFUov?$}XVfI-K{S6>n z?MP=$JO0*7^=0H{ay>J-Je0dezmym(`}<1EMtL#uQ?ghG9@Ja4doQv7^4)Wo+)3wz z`}G&7I;Ledj$9N!T$p!I&*h}EP`PO{D|wf@g(OLS~En)q}+Z1;<7(bsPU!IG3 ze493V$^IB~v7iLioJ8sm82iz^{Vj#$lvZAsXEA}bRpG6)rHnPsN3)Z zD!Ds~uGi{sHaG5#z2#O`{{C2X62209Y-7ym^P8MfvEA8)Rs}BV?~XmJG$7=cE{piK z&pWpq<0NleFceM`Ei^CsT4BPnI~%B~XPjv%LL{zJ1>N^ErCD2tq@1RPx6;U29rYXW z7t*@uuB|vn#$VPN6+tYWAw|GHA$syA1>LnXE))zh3cHL88!u?sC1z3C=1Ri1=0QEW zd+Xk1k9{|a&swS(q)D1LjcDf`tNSXwvouPkB)0ItQ0 zt#Z6oe@43VoX=R&@Sr`Emou&4cxhhuy%5Jod``U6=GRErIwxy6J3g0 z_m`68KAUFUs`Gequd1W8wKYk5j^FoTHfSESB#yKSL<0%Rp99u3ly1v7UU^ z@bfQmH4ih<-e#ro0du#GgPbc1^TnoP>Is4GmT)H;G;U9n-=B-51x9ITN|@(BQ>D zW#QRsmPw@6dkTe8-l{J)_l=dDojRjrbCN6v3( zh^e+*8;>rHnC~#(kjRy6AIDX9`CrNUIGE(F=AnCDgHLLx6V+SWb>0KY|{hLjq`Pw`L< z;ddW+*140sRc%X+?`r<$_2Qu3S8l3S1X%RF?{q`WfkwW&-#L=XpcV(*M1`j1KY6l9Z^QSdxpV6{V{elx+ts9tu?w{*!q<4CXt_Ylx#@aYvAc|c0tvcqPObmq&zks(grD;=^T zxnswUt*tY^eWOUM!V{5u^3^MQpKeicJbF*zb8_PSwOCV2j_)phhR><5-pH(=GzSm)b`iS56_l7&qU7@DmHIc-m)NAb?Sm$2k(l`}`Uz(Z{YqXo5Y6x3xc>B1| z(WHuooI*Y9V~QOWgy_30O3{O-wL&aSlL$4sT!S2rTYXG4J6$XVumyY?JfR4>gywqxx+G?xAzdJB}{HLf7l>*tR; zIs$D5>H&ot@(XHJ%c09aYVO)qve?CSJY;nkI%(!RfZA%X^;ZRglMObCjGQ=?!F!`0 zsylCT?yvra(kpwrzK2`?qQ1w5Ag@LHxo8J)X3YYGt`x)Gk*JRk(W8IAJ`Q(&Fx2AP zu{<--jS;8Y@)U}~tA(f&x&K;Q;sw{`d-{F<9Jutas0C-f! z*4x!_MD%R-IrItd7F?SF;dNz_6CXaib^)-juqO8)ugjuke+U##PhYuO+7QP7C9xYC zFjz9CAw^Z14*wCGfgpKh3Bn24z#L$;4lHx{xO304#cjpdu6|a#*N0BIFTcx}gHTf` z7>VMsA*K5(8G0o&o+<{1?6=tYH>dq(EzUE#{{9pevTjmesq5G)=)^s>ar2x%FhH|p zBJNTlvxcW4$4xfb7{X@RHA|bSgqPm=wDR+QScUE}ns5sM)oGlrbBw37lW3T`@aDLn zG7?V67>A}E^{l&1bC>V@Mgc|RU}BUrrxPC2uNWoJvb82=_G4aql5CRLNg}m_>6$+lB4NHF=6IQHbMrJl(*$+s^><}Z)y#bGQubIzSm0q@ykjYJaYMfCCb+7DsAzDlU-ti;wrC$%Z1=f?4d(0U%grIBT3O zSCVh&F#EHnR9Wt0-ETyTZQ-##cxodaI(iSL1$3)ko}u>#rl&aF{VTu|KqyUpsK~ok z&Emg23n`8OOsNEivb{c#xNmA7o8!nl4wBagDUE|fAm6BOOwb2VQg%pNfKKw|M@pw} z8o7dl_7 zC1E0V2`XnY8A?CC3O}z_S5=}fKfvoa)l(6@Ie(@Z$Zwsv zsJ%_o4qajk0Jli}Qiw5^u514)+omc8`d^8^Kz+QkwT zN3!L+^Q`stH(_>T{c8@A=T!Jg0~8jZ?y{Eo15ci#2J?{DX-q@)$4%Z2bpHlhWxpe6 z3^thJUj|vpfEnk&)ak;gv&MppG+Npkp`4d<->Xg1=;L3hKclzv__X1}W&F6Vd0D5MK_9u`Cd8X6a=J6EFqcN_@?@JyW;JbP7)}IS7Pl_0^!)d7y zXv+utw>BNM0r9se3DPVz%dT@gVHXCK(p_z)WsW4<@@eKRPWSic@z)JXM?LNQs_q(E zd%4cMG`>#;H7;d1K0WDhV>Wz0^kOhd(EsfsKZ2HXM?qCe==DiKGV&DJe?tn&I=>7i zuF;3wvf7EwSBv0mSJwC+=lOBTP4j4vOP?L_<0+GPToGpByOJGQLyu7DFs7>WrUJJS z%I)Tww|%XG464undvi*m0f2 zD@6zm6U1`?MPBn(_5(E4G|=hZS4H|Jfkycn*~33$bFF&2Agy}_B@5~)FWHfm1AbSq z=-2xf69{rW2~#`F`m8`BvZmN7Hx2QVAQ2S1)YgERFzdHo{CT|}>VccE^eyO#hIbQM zI|eX=oiqyPZX*|KIx(=AfFd1sD}`Gv{(sCQ= z_z&e&!>pmtD^YSm@~NU#t$`icmu#Ezn(JcB79jVMZ~Yj1@>i#rs)no6n}sa@eB&&PEV~d1nQ*?TkO+_+2=udk?I*~*dw8al1~ zs&-OKkx^%-UhLa+KmXI|m`1{wt^UIq$teYaVa!xFz-Dle5sVSpX$>Wve1uz-%v&g? zZloPbt-LVQqO~~aXqPtm2%k3ob zqEO7YEN7Ch<9u8Z#+pK&@rZR2;%IBDtM3p*K;ry*z;ZDV0oLII=4P+X1V~O<%JD?m z%8Aw}4u#yZ<+7G}t)$oc^)in&n;UB=+Zd}CTOyk{Qi9_7_U2pU?eP}*fU}bT#5c1N zOa{&HLd=mlsSCLiQYEk=;uNXXj{H>}KOsYvS>%FpUZtG+wN z%OwQ(!559ulN|t`os2JFEov^*2-4$Bc3)6&vo+}m$8L;vCco8j>sWxh>p{&jAPw7V zCEWB(-+31wI|+tusza9EPWUg!?P1fPJrx4+@sQc_W&9Zb~k_O zAHUerV=*=zI&#zj^FIy^6UZGPnLGh)3QJ^5(;izcdK`x;6JLvTb6Jt$ofLT5X&Jt!SE7Z+@mc^Y&k@XGN^G%K@IH9LzIgm>TC#N5}Tr zsSX}2b>R-2vV2aHb3(H#QTM=&}zSGT;W^WJyQ&At{~-OG!O z{pWi2Uxo;zZP#|AL-8sc&Bs$#%{UAR?o|O%;P{|ZSDVW78cnumLELLrJnJ)zi&?yZ z$Ro^NbB)NmFEN9|R5T{qVTnf!%&!+bUR{RV@bWBMi*(ahDLiKolOnbGi)Qb(sN@d5 zel9#lHD!{NOL3*^F;4H=!o%LZT4URPpq@JD>}x!a!`;1=4P~w?oHnQEW}$4=e=tL2 zvSDgL|3{Nuq8^zcLGN0CJMqn#Zkz>>q5NxnukE4iTrmg1Zd(vU+<%gqsFaviKOwoHW%7IGodTb@6BPe<<;8=h(lBOnxzE=@CefWr z{S>iEVqf&xpF&uFx#&aA~{ zZ5%3(Xnse|P2PCXMAi!gkfZ1-j*}%lRf+`|9)!v{j<#Q~u)JL?tGgT4m`^HhTa6y< z9kH{t-1|*xxDz6SYHGGFVOKMU>yTC~x9 zXWvP|L&-c}pCCb@kw&i5q1W3xis{A$PgIzm0zTSDbKd)qa4Dj%7+}dDxj9lX127B* ztLiz2W4sYZ4^e(a>^oNP-~4T`Nj6*rA%Ywrw%ccdZ%JE>aC6R!6UXhJ z3zJNc>BbTa!Ctarmm}zVU*O|=BM%ScygVyI9Mdbgm|MJD%!uMxzXy8tUum5qHHcBm zp`O`H=x`J;=`XS$H)gOd&Qyw)=+$!`Wm|(okfnu($P;VPmZI^~{`7wn0~~~P?Q_5A zIxbdN+VoUwkim*l{;np{FFllE^^~r(JecLI(PXYwVack!{iiWIDNjJ?2l~CsY2uQQ zXiL+~&_hy7UFfil5O*OUmQ>K)Zmqrq8KNAJ;l8HFLYjx26s`R5HRAqOHf}O?bYJ{* z*kH|rcld7u)xi?3&^+TpO1~WY5P{S7VEmM&wlMpTU0~vR0+DU`Zfg3w0eOgt6DbMi zOwIu0b5uW;&%A8{a;7K#m#OVQQ!8G4)qX!OmR5)_2=)e+N_>{o3~&%phSjs*mC-`1 zQQMSZruPxVH3$#^I4xe;a%GT7M3i%P>2Vg9fF`gTyth!_T33F<3JTPv}2&|Yy=!^ z@9d#Dq+Q#B8?mxY0D>S3AjHP?v3n7~a}rv!L|X2Vw98>i8DL6F{7gqTF4qM&yrQ9k zMpN#C$7)DRU9G^RR;T=63&#L0F>-ZPlwF24K-ugY_(p(u%oVajkLZE#zQ_4buYCvJ zSr@p}CSX=Gg>YTpp;OSR#AS(al}@Vydfkp8Qbd-yvp35%CCN$e#ndDV6YVF^HM8my z>i6Zjx)B-!i=q#S+T`P5*(2Hg_3KS9C59=g|DNK;WS}~T`s({LUiLz6#B~(0GNbOp z%?(I>gyvQuHkFq^bCT9kKsqO2nqR{-GhWZlkzfY8rt4FuVVYk&yhcYFjx7y*o319# z8rPjW%VnHvE~MNP=D40Iy#H+TpYN6`*~aj~ySG^8Bk@fvJmkdtK$RQcVfjHjxTT)$i5nRIuP@=d1sUE7E%E7z)KTvVnfQC(ZUr)%1%zhX%uCnC*=^ zxpTNPD<&j2XAYFfFG3KJ2L)U~+!37dCgj9=K=X5^&h#&Se}`uwaNgm^2e?!0o^`(e z@H&|mj?A0CULPeLnH*#`6_(XUF^x|&D?uXr(;Ct46}1;O(avj}8TCf1=DZ z914dxuGBqQhbS>&^!g(@UWO#%$#C!jY8^yhCZdX1pONBdVD$#<8ry`De;@5deRefr zPhR|Ycr!vX#kh^S!Z>iq2)O?|{|GFJ!u0OnZsRkaKpYXM54 zyZ{@^!qEhDlfBW|zl&tYJ3O^fb9bd`s%6BX?4Tiq(L-Pqhi*J0nSTr`q5uuxJcD{H zp}itzKjkF|48Zl_Fr2%bpFG(>WllSM2vKiS=A#Td-I41i4CRq|DdBfl4o%oeHI(grD zHZZDS-&bTLH>MhW!H)9l+nY!#jKUYNXr2RK%d(gNI-&bMDo_|DMYN z&lP`ZZssOk7hL7va}^ekZl~X03ca5&HTBP*64$^pfT9jy8BY$JvVz8dy0fMLSi+%J zqdzxyvc%ucpdFshKzD!-d3{b_64>+Z$jR}-(%pG<`Z#H$Wuniqr-@{2FvX`_e|sX* z9=Ltf*ES=f@okqT(=0lSa4&FMAGNaBUb7LWe+StD5~TKz)z*%N%{#;5h}P?>6w6-H zvqj+9c0UG)txcfincyl~>%MOFGFSIQkiBk!+tsm{8>Hig=|(O5dG00WATu#T0-w%V zLF5tc7uNZ^{2Z`@U?-7UauIKX-12`NKK~%c=Ofe6?^i1SZf-)=5#v|oq5r7CuV@eRl)1OC$3?&AOr>1D2h3xu{6@w5*>ms0ul zzm^7#c!q?Xy9+*^KW)61{Msy{e^fA$r6gHe92iQ%+`osn9~VOp@7fllNTkez&C3U) z<@VPz>UtT9-=`^G56p5A|LhMT*%b%!e>p7g5=Z|AHPq4kx6VyF;Ca*+s&_dY;>9vu z2nazmccL=##J(sgCe*(dxhIHz9pLLi3 zEL9dF4TtmA8kne58s?Y!J<7zP7V7Zx_4U=2P<|V49(Ec^;Kk{%M9fgY6`+w%n5El- z;Ql*8j-m7_^d4pud#h>Jqt#hI*i3At1Wr6;tG7Q2-nwK+*u4ke9^o5`UpN%EL43nt z#OrHY^%He7p<0F>0YJUZ+=;hy;;zyFob%`eDBpbeXTR`A#Kc;RyYwsOv1PdzrNd|H zi?^U`XPW?Ca_uA_=yHfrP8dH<D>Qz)y^R^+z^os zFE-CYr~6{62R}nXP$|mw<3Qk=04~cJ9#o(~8XGPkE{q50P0g}x@d~hDjaw*n6M^ayWl zWZ*xUN#4PT2dJTpGD*_~Z(zurCVf_lXr5S-vF3I#x7+!{lhxbJ%Y?@eaoy(ZUQ(xB6vT-Jd!vT$_QTyC~J1XM2I z{aJzt^ZN~0bN9Vn_U&39+c4>n*0;X9iJ+}-u8-K9 z&#vRsdxkF@OEEKpcB~;y7kB8~gGfWbqc3SSZZUp0uQPywU7ZYBGZXl4@#ht8z=m); zB?(-IYrWI%BL(N7FSO?UtgZ@s-2;dh`oAMeno0FY0A8h`6evh%tmV83a09P`Fb&fO zaxM`425k%+@c01n^963)at!~oN)5Nx#d0VSKi(MP@u(>Z%Ium4);@1TOQ=7~55D8q z(T5<~1)%M&VlKc-Z~*jd+ws0ZQ}B%f_`7Xy13tDMOMH^Qq1HcLG%c|7jC}lC-cn#)Eh2`19l16qXy)P0f7Ume3hgZ5An zE|!<;-6i++2hk=VElpXoVIff(zJ(Z9M&e!MeW80!#v{WlJ+VE5%wu!KMAEudIahTb zOx(bMayk+UJGeAWp%t-FV69eYiDmlvQAu7ybMx^MC*T+jnr!J1Wjr91Cz z-rPb4spPhbFaU(um!e6Z( zCJk@R0MQ!kuXe?_Gufohr$E?ze4sVQFb`bPH9LciT}xb6Tf6t4Fh%%=>R{J4LM>=I z=f(Oufw;)7Q%MQv00?@D+XsNPV#L z*%iI41<-wxbKh@0fA!%7>zwzPR3E{9#Qt$u<+-ij^VK+B)n_!>0gLnp>JeabngGkO znq#x_gl}zr^bQ3y%@Xk%$ad{?|RA~9S0k=zuvYW z@|Yh##*DHTZGAf<6_Y{^d4Iu~Pv1|iH|AImtLIvAk~kD%Bq$ZP210&%=-0iSQE+p} zy5?-(WAla0dH&by2XKVBy@-4`6P*Vc7B8$)_&q+30Sp~G0uJzuhZ&#*%}=+9<-dsiWN1mn%#V_sDOt&NfQ3#FGNdZ)PI&+W0;>e@u6tqI zetl`fD=yyofvIF~u_zYgL3$I78VmVtDbGLe7pwas<`k~TuuxT#?fO z+uOr2l)yig^WEHv10T4^Y-W2t@?JS5<(W_7VrPyA4*tT}u(woAMz8)Tz4Y(6ZL0Ie z2~8b}a1TyRoK3mpm9_h~^`7U>dbx(n(A&+_?3?oK@;bH!34ySXW(kX_)CR=Crscq) zPuc6E>(PsES8C9$XUUWA_#$?Dtk#a}(}qc2R;LHu8el%!@-8pQno}&9OO(D-%vG5X z=nyOo-{WI`R?aAa|tgz{IWRrp?6n=SfdKOE~+T`fvU&=d-y1n~^e>T4%8x#n!=Df|AUHZQm zq1F2V_yXyJzE*pC*=vC5XS(*)vml+oP&@2ym(jOj!u<<%^D7bxtf>-tz1BBRaQauo zU$IS@P%9>ns=6dAHWXrsIZpF3c>w&aC9}Ebov%h9cQ!u?Qoz{JRo4IXDbo=% z1hELAnAdEIE;Gm`K7>`x{V_fb-0{N|~i7@POv2TWu|A_8SX?@OA zhPN0K$y`ZKHM7a%YuAcI>3qrS{N@arxXv-JSKi#lbN0Yks;?YXl04~YKb+L?tF<$p zs>}A)$OBQEw{z=Vko^C2|5^$mss0j=KrXd1=Ni9CZX2HPOo>`Fq)krJ?sTnn0ZK!L zW8Ewe+Ceh5?Ri$~&){ii?H|t$bZNIgzgM=9)s;5qY|oh5uxqk8{zcjiw?$Mb&A@8oEk%^!|r2Ju+$MfUvFVo%`N&- zH62kRzN#xTkI;)8q$lvTFIILrXk)d_f}OSLKNz#U@R*lplg@YEMQCn;$nM6jWclR|269 zqiZ3NRwW69CoH``(y3a4_~oI10F+ghe5!G}5r;PiaJ1!3iQytfkj7z@*BP_KM5={x z|58+}NwNHd`!v4ml~-6-Q5C(#a<(2DxtQbBgi+e;m|`>H3F{FmA=L5JUZESoywC>7 z*t$p>kaRt`OMkUcqnQ00`{iJTz7!9uR4osoOcGPBA9jgmqhdxz|Xs}5X!B}r{1?^ez_rSgcL z14Cziar9rCm0_A}EBky&N6ZM_xVzfD)Rsv^<~SmUsx9v6CNCYcn2VGDpRdr1z+5?I zeF-hxgFDLpLWnDiTzel5J^kWkzwEp{ouT_}RY1y!JZ;3jNWP5%ke35^;8^H2ge6 z>+A3wwjL^MzaCC0b<$Z)rgS{nY+)0+D;JH-PUPaOD3s&YmRq4^L*2!`9lt#4tG_Ex z8jkzMx*KDdsxB%`d$S3C8G!OAN3G_FW?xi>(=15iIo?+zogjhP>hUr2=*#FEzceV! z+<~bY&;x9wS~zWdwx^&;?m|vr(en?J_mZ$CSH?>S+RD`4zp0I`^`;K=3;=dfG6>b=HCNR#%Os%heHM^1>*TdGgqs z^W>_|9TFW^YaV_0&wkT@(`t19s40+`{NtSA%)Po|`G6ojfWG%<36^@a1*x3WKZD@{;|W_-VHan`UM@3Vkw>8aiaiC-rDUitdE_lF0xh3{Za ztN8YO{lRV)d)ntvw$&~>b=BvtCUu*@g!-~>1QWa}1ig&OHQB*m{1}dg3GwrRzAO~~ zdmtLq$*X@b9Dn85;ri>|OsZ=O;~^9yrBM*INqCI)@OW8sWFO>Zpijw85PTVCD6BJ* zTL`yh+mI5DbfC8)EsaS(t#~Yi{sOnZTK_2(|IY&-e7w^i`HcN(vcsBdBJ0O0eZ*4R zI^IqfiWM~{HqyFG`D&ah16&=e3zHU$nH_Ymu@06;)E#-iEU}=$r5GfsRtLRFPxWre zw|hh7Uw*tHS4lBKi2KT#rko-Ekp=~qY;SueWldHq(`7p8TX;Z-Cx_DWb*BfVCYx2< z#2vzVyLPD6D!V}#*S!`LU{%y}H>a;?4(ql;j*ix0MJl6wF@rmhBE3 zAsknJ(YW|2QgW>OP}fn6wK-dMXyWd?pez-JinLkToAQ^6@L_6!&cOk*$>`+%g6o(V z7lmT+%@?wzjqJvP?erJ(G_3h>!wmIT@p^6FCm{6@7Gmd)Ii+4YY;TO31g7Toue zf+LB;7{66J`QELCWqwYm5e~fVuzwPNSaR6wjpS2%$O&B75I!4zX-U1lVqTJA zETDk=QslPLO8WH$T^rP}D4B<=-Q{PeN?H4JN9};lG zZ(15ea;Fv~#$;>saw=K0B=}8&Y5<{Go@*srT_dXMHw0cwqw}e+HbrJeM_x3rw&y2% z&^zY!=JHH~Q$x7(QlzSfuCKJ@9ZlrP)ND_gZu_XT2FX5%U8QM?;L9fh6wc2?@7Z_E zKv{=9W5|X7!EXb*RBb#q&x(DjFrF4NRZ5Zz31qmH{MOY$bkM=8+*ua zY+)Fh(j{AAsbQlQ23gp!J)|Sx4bqYBx$=UC2Z&$Wa0tE>j_v%#riB1Zw}N`?Qg?9o zFEr5%g{-dts!VCt0#rl=@>DifBJ2R(_2V5Qt@k(cn!P=Mszi?P1=j(C8Eup-wwjLj)Qx^eR{T-U7#q6+U z;pB&%4UtNHseAGMUO3}{DH#5w=AjjI>&v zeJFkYIcH{S)9}W<2Tf76 zg4;~M^lwR!8y)6sFXiV5l)T-=>kL^?aFW?C7D=q8sK0?9zkhHRfJLUe$!;rH;49h- zK6QP?cHceB)P!no*k;qFzYqoO({L3loCq4mK_S{{<>Fnap*%U^AW8(&Cvm`!ug=^Q zCTD8eM~)&39z$r6kQs-RQzrpy^(6g<(MgoxpZT|0kd)f}iqI`|zV;dv077|^EhPFl z6z^yAK4^RIduk)_S^T&6ct8}KAQNTD*BL3PO7<5xxJ|ij{+Dt)<~6DdndJ;F9N}f~ zKb-s(rGBo3(voCEA0oNL&6)LMpg{V_ZLPN>_(Q+tfVbFpDy}pG$d^Xp0Op7CP0~m_ z2C;m4@*(>>&QbZdGUW)ml}VET{cH${R`3ecQ}@2R+LZ>^~e@PbwVEzS;8j@0@52YZXPtE#(8iwsj zo}q!dOX$rxi8amElb(={>;yqd0S10!#cyt6KoEz6aG6I`@;K=c%RC_W$4?sfPKuvU zwXLsDJd|6-~z;(WBTi`WpMQDk|In%q4 ztG&kPw)&SU^91~Z@EdwAKp$pS6OX{7lPNjm+{F8b06iD#{)0lh!ZNm=X7w-9LfAIr{D6Ty3Y?`)2*cFNy62xu?`Fk+l*a87t&}e&oy)10?WhjpQ zw>t4{lWvotdID+$$aMYyd>);UAG(crXEq=qE;1+k@8Vb+mkIrO;-b)df_5syYV=tI z1KRf=V37%F7=|mbd;vpqnvY1HhSwld$tCVgdbU@xtd@|K^>14Rt@ALfVLb>Imxo(x4(u+J#I_ z9I#S;e9bd!&qMtheco)>4rG0Z=_dgSPgFI4oc)Q^ygeuGJw$G(L$AFiY?^+*cHk~)F!(k_Ae zs5P{*U}4++CuB;kz|?;{J6T#t>VIn7!Xar2JJ|HR--#EYxS!vw6G~Z(BPcJm5m(nd z^dqDdEkV0d#O2=l%$IIp(*2dk*AVvG<7jTV1F%lDr)Z=K*T@l>ijyqJIx8&SyrO3M zb7GPFKdJu@YheEk`%kNsn-1#JR!CtQ@TE?f&M85;kM|sS85DelXg1DgKswisvhyR^ ze@I63qyKk{wjo29P$Y+DS&|R2I1X%^2kS&jf>#)kU9PJ-l=Uw9KR713KMn%qj-fvO z^_@f6ewSA3TOBJb`8^Z0iTAkQ;RkSaPeM85tO5@tbgfsJNtv+U`eblr{N56hudAYd z)|nqC`xm7fizLm)s!gUZ0zEWCx;ii$6Qm8!(h(EQJIJSs=ah?}qOCAy2DvOnllH%; zjBT+*_rLm>F`KZB$31-aPC_FGTw**3x^>sdn1vJjblX za48i)iV1e+e<3?>p27$&oj6Nuz1!iriU^2YO;=DlZ3I9)C7GJ0_gwZ?meTT%D#4An>Hb$2qGdz1W^%? zC>caSvcys-1SO{eiAoj~B#21PK?#zPR0IhIK*=DnD1u6kwj?Frxqzbo?|a5Q=e%Rw zPxo8Xg_~V_uQ1n~&-^_*$o}Utmdgu%zZRD21}5K`^mjk@t=IFl*Q|toDMJ*yvw^4W znD^q=Ab2FM0pUm8;{XZLnK(rc?pE+*592$}a5n1W1Ro^Th&>@d+F8BMT^{(XGd1gP z^v8Eg_35K1eTnoW4PdioW02jqt;SMBugmtX+~^Tq{LY8^Y#WZv7XLanZ!o+0w6>Ni zHKbos(Z8@QfBYZn(va!6!8Mtyq#4+D0YZzI1LowtpqB&Xa}`o&ffAbZxjuZmhDJ}; zt%)WJA7gwj4Uu-#@7j+0-oVqa6MV~T3#~3>!nsEfko))* zYtrX0H)!n)Bs=(vNaT1SH+XyhFN0{VqfRhvr2|q zlLlG}9bC_mzGenVO+-V34-m!TSu$7ff2+9*M`mngeo>&}Oy` zslDcLcvT?1C&ks`_Hg-0^CGfM$xaGVZzX+EVJ8jbc@*DsoYg3Ih57M0Fi;mRSG`k` zi*)z8aQD?xl3SawH~~!~&%npq(+Ih337+VpF7W_u%1I4hG%xb^Gq#Xe052GdNx%CA zsh?sfZ*Bb?f{qS{HEOpp_S$gm2?M6O&VvBczpbKzFz(^;(juTJo*;_CHtv(ynbZR6rdVso^uAl^PF} z4bU+6rl~qmWDfmzi53OUV8yTjaB6+dvkLujka`evtv(Vu15JW&0X5Ly)B-%(UUpI%;DHW(v)+;dyRolT8{J=#nS;7jlMvRKA&_z=zPlDxq4a+tH7jbBMINjA z$1Uqc7YAvgxC~@39d`800@YZ~*HEDr2r{N1s?9;B522c-7g|L?qOixeWc?W>gCWxJ zP#)v?g4%Z;N4*|NnEnj9p)*|TP>j|?eg1J302x#HFcj8Z{+N~NZ+P17zydL(cqFMp z8dJvtVaA+!CSvsFP@cfA`#GD2$+8h}hD_ZFG!;6?47N@}i;NtrEaMDt5~DJ6s*joc zvJzW?P2}8Fq+R*k@b$;n6A*K02lqmqyd&X;|B=pe2?pCO)qN)PKdilJUKx^Iha`w* zXVx8G_GwbTE*|7Z-JoyNvK3%s8-QrU1mr6YL52;Wioyd(fq3Cfv@Qi(zEaRcl@9?D zqaPIczwbo0?vuz=WC-ccI+FxZ6F3st1TCe}D^F@&;M2(}zXL`w+7E|iX|m9PHF>W6 z05qLeOac3#033~4+;XHO7EpCW=aP5H`c1(xCI*04{tO#5eRU?-0mc3R*>7iBMoo_E zE(GXhpM1{UXlyN_b3Jvc?w06AcULf)v;Wa?(!3YOvft%ek9m;09_3I8bBE`HM~6K~hnZ3bP#bD;UI9YjJJ^3$YKtXUWB9FJpFrk|(PIY}3XXvmgm3w? z*xL7@en`@4ruRzY5exFo9Z(1^fF6mE&8MK9@-+&b#XzxjN+$|4N6RpNr``hLM}bAw z1s0X&)XxL4)Hs5^jzCpL^&m)~3Q*ezaQ#&$sY0T&yNXrJ%lRogRuhc$_AaUT#>NG9 zF6CY`ZMMmW)}K^mbq9_X_U~D=Lx?!28KeB$E%gTtXOw67K^P-XP?+BJsPztMJne)1 zf$gY9uMw<-t5k)h2cXlfFT5G7&)=Jaan&4g-c-FE7>;+2Fj4P7b#9%j>UFyKOP7x& zQjv3J=GN2Z`)GbuW}T(VQ;_kX&C~q#5X|zJXpH5$5wU5wF=pac4twc8A8X7URV_H9 zFb}z1&f8}lj>-zkTY(gzVX3G%S%N^xK4Q_3yYM1%q_%>uB zUK8F}JL4Upc@P>KWsD~$c@awJWd+IHOBmn6Idj>DrXa?kjW0DKCWPr2x8d`!V$>qs zKmIVmk5Bd-gP3cn%sydgu)#w|&!T!>NeZ_a)G%V@>E@+WB#nC0Rv?Yoj845+m-+Tp zji}TymDz8@Uw5N%r(^y zMqMcq>bq8@G$ysxerltwBVg)Hr;XR_Y>duY=GL66;hI0!AFUjB3ZO5D;_nhyP*7Nn zte4mJchuvw3nF3d(3ziAnm5TpXKoX_BTHwZ!qo-3OEgv+SI9h;=4?qzOs+Cu3o-0@(-$#o7$lpKm`MC_moI3SzKZT+@*3 zUzokFXZPjIvo1#8OFvpuwX9lQTC(m4aeT9L#kEB@m!;#4Pcl=JubVfEBrac=*7_Ba zymFB#ZdxIxFoRo<*A*!6uPiNPtsMHHVvK0GE^xqITkBl-ShC(O+PmzSJhuN{jh5dS zB&Aj~-j1(B+a-;LW%)?fZPS(f$G4g)p=-zAbw5%E>1gAZ#boQpi>)D<5>#d-uZ_eCQrudP9qp$>dUp3=h}Ce4OKsc0 z4mK1`eLSyT=;uWkpGoF}V62V@4qTdy?%+@1)WUcFn>fliHlCAh96k7pnmUjT+5yv$ zZts1a8qW8Uz)SjkmublG#L>sBT;e>Z@f%iZCWH7&|H_0l{_OZAFfC-{aGe*(zskz1 zzQFnUc!Dv<5u1>Z>@2XlP0D#`zRz0XwVIKO?5MP;jP9sWK|5yFNLKdZd9QW^Izo<2 zy#TP$E;pV~^(X~{7EFEet)0~66YG3u!}bJb690Jju8sP^&3cK}4b1-;0} zikm~Parc=Ud8F=@vO~Iy=hqqfboEGt`{^$c%vW{b={qBRz$7;1C$;fKjxXBe3Nh-a zz3}xw3v-Gq>gRtej+ar5cen^ZK2qN;xB_gbl5-9i)thFSG+r3>JrL-yFVKs+*QYg_ zN@plUtpZNxE4B5&+xZ0Dkb^^d>6pw?b6O$XSh^G6yf4l8r;uCPV^!~}HOQ1qcnvS! zlo0OM`bAmyht}vtDtpX;6UuelPn3toW88QOs~9b)uKn`T!?auwg6iu0rl%SCntwS zErI$q-OXt3xq?epn!Jb(gfj=%lm2Qr=yBSx>UNV*FU9wW1UCkFQ$^==dVbL_qWCv{ zAFQ(KkK}2$V?30BRXC_)aO~&W=hz+YY1H9DBb}9d^xXatYb}pZZ7<{s0xPlI^9q2MC=J@JXZ7$^%}M1S9yeTr2c z8HmnM;HUrYb~uiefAQNae?Y{2)}PcCv>G34+@E3(h1U;PzPdTks4UNaC|D^%@e6ab z{I6e%5`Xd+Yil3$l803yYK4+cRh2h`P8OCe^1cMmcg$X=-Q!U~x#TR`E`o)t=DC@Q zri=WAT%51$FJ#jwlF#2X*wC8h6Z)R6m4w(S1e${ql04+ZW12#8mIE}$Sgay=OjLeA zdS`E#iyLfZ5;}!6j5U7{UiNcwh;wYg(g5ZXOEmw?FC--8$_!Gd!M{`gME+pSe`3^@d6q&fvzTZMOKZZKYAkiuL0?U6 zA(`Hk=~R_@dH$DTtr1LBy%aecZf^e6u{uP;U7b~`=lv{k- zIm8^WNE7=&ap9Z|Qm~5Ne3<1)5qUGx2nngYWS@=%Jsj)b#Ysm~7})v7PKR1Lc?jvL z-A%z$$s~E*ZCQp!Zuxgb_f)bdDMUK0#71U)`opvOnALT+^;FDiTkFi;QHIX{qJ}*G z=+X5UY{1BRGn>FSjHsg(jnB)|HpFhnnjiQY%9gcu+}Lm?U$a&ix6gQ0gPYY$j6@7*c0V_q)zGfcf8Qy|c zT+GA(bIu$Ar!ZllDSf=M1+1!$z0NH!X^$x1?_E;=4S#x}lc|`?rx~ z7V~Kpu@@KAT+hHlI)<@-!>fP`xPMruch5cxgQRRrnA~eN#jd+JiRrhppJFLThl;Rc zqZ5H&R#2ZodhKNA+$mR?HUCCdj@8yT2Y)}-H#4rl`o(sr6&mCt9SHkwat3BY-rdSJ zEt$ddL()8ko&a=SHj8A@O?4gB2oUJ!njMN{jWAIa8>wYUW%-rrnkcI>ICG6%#s4e262tW;l zc&^5cZcp(%n)~S;=8$wxo{}#xNW)jOJA`)o*aCbE4}smEOt}R7(RO^iqHTE4%e4$? zkUH*sCU)%~*qvUqU%}nW$h=S>l7dwpDm-_9xF>xFt~GT^uz>ju)*}>?Omozgco#>P z#;rOV67tjWd`~9TJTbtN?aC*7^ z6qxi07FBVe?6z2QZggISthwclxHuIJ+)s3G1Vob`g+#HA0Wjmuq!;OZ*eO#&#Qnfd-DKo3q_o z4%Q&HBeect@vy96cUx2Oj0b0jLzUo&8dm*TypdPXPgnW&KK`psY0g;set^g0L8}NN z$@1&p3y;~q?zQPGOPmUyk8WKo{~p^K+5&s5U@K-IV@b`@@QdC0L_|E>b34Vi(~>b$ z$620o6&Sp0Ola7zxVDU&o`2rV*RZe3+#th9ZhTf|Xte+6DmDREB4CNRz88*P1xQ9$ z;H`QJM%_kIsXz)Oy=J(|i~ZV_=0o(V*HA;_ZYoj8F0utaYMc`WqLRFu(gWhBC7ZLo z_PN>_($3@Z4dz#kZjSqpRC#HHT)6P6q_$H`ZVA6&7;+UHn4^`3YH0U? z5H2ZROi0zpR+cOEKw(+FSQB5{Atu@5oar{&z4qC$6-i?1^WW)_9{u=o9Z#nNufF%Lx9&xOC9%483Dd(C?ENmOrnWdj*6P z>Tzp58igK(2(S>A&B0mJ{&=lNE{63|2VtjH?p23AVEfFv_Tsy=YfISdS2Z#sm%m4M z)VmhzU0EeWUDVE1F6Zo_xaX`ETdP3n;X`P%F}fyT5mJf+x9f6kziaq-CkMzSa2MVw z340OfUo4RYr)N=@zhDo%&+4V1qyJ^Uh0}eGPj(hIFTX{YsEB<&l=ofkMC#(4w)Gtw z?G-`$N#pdak~BBGbXF_?Dz)#@E%mTT){kqexXo1ssCh>crt6!&M^Kl)40I52_C8Zk zZ|zO^#~}E0F89PRYOQpag)AT@^70T#7os!)kyU}{V_IXkHOYcRFpNWk!ofl1*iK}o zEf}Rl^c$C#^?1u`#ox-U({RQGX7n?z0@~EZ(DqP#_c3nsU1>H~<~6XB)s%$VeT%bq zntE!xyxID9$CyRYdxB#_wnwJhCrq{Sl)4xhvECZKRc|dnuYcoS$`LM8A_nKgAkk&d zW6C;EpDAJIncY2x(eRvHTT!Yq5z;I)<@>%fTWkb_^%-C3alTp_A_l*Bxm?ll%7t^$ za>p9~p(GwiztnIrMm_EKcgORmffl*@jKV_@k2d_ef5dD&BBu5YHwUlOcm;mPgY?VS ztR%}kK|b3Qj!u{^{XJnj1F^zJ3#@jm0{|M8NylPMd==Uvs;BGd)*@-jYJ`jNT#CJY z{QAxIPY{1;3ExkDfPAb~Es@sMb;Zlp}z$Ja~dWKJ(4|!-j+&3%EN0if*81fN3-38)u)$|*Dqk|(@v8g4PkI)(io79KHXqDY-0Ej&IZmA)IAYrq%8{>2|D|z)s0L_ zLUKhLGxHY~drkZF7to~RmVInMllO`-=H11UNRsNme2m$!z2d&B6;*x2YAv(jlQw%S z`xO%@fy#`1cIl2T>u(fex)Wct?d>)Cad<=^&@3x4G-6TomS`dG+iV@96hqJV5-#2h z;Mx7X&z4oUHLdn=9_+;6JbcwcF_Nc7>_OFs%y!UIwyfa6&LZUfMF9%spc**YcR>=qtc@(Usx*C{c zfgR8%SoM1abSH(BrCuE<1Z(DV`PMhn>8`z8u_kIP3u{)TQ!y*|I>flPCu1ze%Oqes zO_SI0TBg0O53Pi)Z=+bJrU~IHaecrp%K0==6dmU^nysXq|%hc`xY=kxM|Mv}nxtl5WC?{TslDXk_pnOkDL(vH2{X4Gu3f+ z{69-)BZeX{QHKLjqb8a0r!3B$yb;3tLNDVf{*|J{6`SEtVMWoeOhhciRBk45mYy0b3rk_g}zHR}s z=|EG(9h0Pxm57&upRHOX=?WkX*GCtVeTn7vY)8^#uBLGH=*HSMg1<$bstu8icvEb3 zut}<&kZy_v$X#&#=dt}YZT!F;{koWK)fjH~xFwD^)#g0!fh8;JuhEH@3)2X6Z}8{& zy(gQ}+XNy6E!FkLF_$&R7k$4Af6;}L`(pgSXaCWzWx0SpF{LHjtwl=g-yq}qt(U~D32ujfw-%QwDzY^7+p%&5*{C<| z61l;n!|NaOjqkSynVVbsq?QeH+ll!|);OBnVf|RK^zO`}eC9B%uc&|7Ca}BW!DG&j z#oV;QE^b3!o#?0YHrgW>Cd~T~VzbF6=2x1NOp}3!MC`kyXwG!k3FrH}=H;3k3#r?i zVzhqSg@<`WbF%hL*RjRsH70iyH(ONYx`mo)o34$;DmO>GhjtgxNXF*{_2S1|wZ|5b z#*3**dx8GXMmod?$$EX>rfw_fBAH;+@(P&K;&?W6hQi2*+Gn2?40#)xB5*Tr$kdwo z$eS;zQl0sAE|;%zOx-Qz!ANx^ap5bI9+L($ACnW)ZswyB4@IG7EYV_-ok=Ll@uHy-pflj+)Ci}om39@ImH#M$( zIBi%zm+lpVIW`HEo&}X8^>hp#L`KUCJC!qXH};xx5dPak4{;>+jwuqD>Tv`x5}>2o zDX1qPrJk0_wwFFm`!Z|H@XMXaPjf#)>UnxXsdyhXXW$Bwdgv-!6*HwJMhzC|$CsPO zQyDc5A_Z+9zM^C6{gsTICGcC-=@rCD-ozAFsGo9R=ytYAzx^tCP z+&j3{G_~K)t96lq6)pz4xSX+EZXQOmXLK(9zR+;(LyIOXX*a9 zGNW8L-e?k}fWzy(EyA6%3IjoNYkysMzG z3QdLI%vU1|Jga$U3~y9m5UNrAv9{BED&(b-V@-@2eN==t>-Qc^jjMqgY~?Fyo%K8s zaqYVcSJQMPLmb%V^Ab}qSa#@=lDQY=c<1NFvD45YBCa4(fia6JB1l83r-?d5H^$7I z$${xUbMc&N=6%q|KfI8xo)$P~^>``4;f(-OOCUW{ClfB$d!>HPO7wFCw^gn3vBF|s znWrW%jDoT>$1cfag}8+5*HMTt{8=GXb!4Ka3Nu>%wgA_8(ai1#dtYF3$lf1k0#Y2H z-8jv{uOjxKY$uS|Ga72H+{!hm54%Z5z!jLoI&RxxB#qClJCwcDyYNXN_8wtap=ZQG zpc~`IHCOtTTkW_1psiZ-O7pCGuIQYO&hD>t)^PCJu`?3s?2B(aS|Vu&j1k_1&E;uY zzi6fTe8d%J$hmvQk?lbGCZ*P$`6mio^E2;{?-R`QYOC!l!Y*|1JU!C&hkgsGT$lk! zWg8#DkuK**l3GdzcDBgle8;x)`Lt;7+xt!`4FKF<#M@Q559Q7CM9v{ChxovuX1)Z$ z8*0R7rmV`De_9u_HIv>!MT!8HhIv`eX2P276P&tu$Bfi14ILF!g8wGpWP2eUx%h@8 zHcaY;WIf-^=oQueLwOB5V@)@{U;L$=qRW%qZuoK7!KMpOx7(NBDY5-!^kh6*C5Brr z1(9y&BUI0B9YZ+yNRX>MJ7#GXgvo7Xkp3G9%mLheR; zK7GQ)*YW?2)`$cXxo4BaNr{y4=kU}Ucjxc#1r*+2jFN|u0t2e7q_fb=I9N2r&Z=zb z!(^HN0rp3CIHw-!sNe&84^Rwdu{Gn%*z)SF@h#O8MC3gPcnhi2_Lrx&LUX7QQo{G? z-ETk;e7vD7@{t3*LD=CjQq1A?Uw`(Xc?dv@NuU$sq4FhcISg^R=K9(y4r*9UQQ(12 zdKmG2@GK@=o>KfgrHj9BI0(%^y7=pl^hv7dcyjo=G)yFYZmq;ODDdIISFi9}t|HW@ ziSVBkjW>3a9>-}Vuq;MHU}%hDLl2gOpf_Z90}gL}75CfldCp~$)KQ7bT~$$_3WAl0 z$1{oNM3FrTq73-`5Fx!SDr3a5@;o+uJ4Uj;`s4N2*3{?K>%nch-qKgCkx%&u=@$zS zCXYC9V`B3~4jn22YV~B+yXlL?_rQ=m7Ru%J%wgC5`uEEpfiG`n)pn6+2qdRP6~~?K zx>XrZGh7dYwVa<y0o|KB5m#4ZyWn3D6s z6{sfCK0xwF4%+CT7c`icl8*F}01b-dI1^uv^mA02|C;=wu|d2{dr7Iz_@4tGnq@D| zZZ%RUiIPG6z~2S35COZg0Q6xG!dYw-A8rz~&HiI~Yp3ooKIq8M2yj4>eW4#-XBo2( zk#7@t#*r{E_C{e*Ip84&q_U{@0+e1^f=|sCY0F9C8gY0h^?z@^Lve?_rGU4$hd*e< zHVKPW4(J9W^(Vkj1&y9U@a0D23{x-|f1qCuUBd*`n zA>d0E-oUJXt>C|CEP={ZC%iQdgP)P4rWzFQpu&wqlplx=mwBLt36K2wEXw9FKw2f{ z4hNNnRBoK2HvX-<6bOZIi7hWWZ&jKLD<)n%>u#>g_u?gGgLwUJ>Qa)1?KGK}#_(U8 zBPCvV=K+C1DpZlmJA%9@Zo|wd+o-OY!~ZKpr~&Xs)dPXOLx(f`@sgWSynp@2TRef@ z;`jK_F6K&e?;SPT{OP3|y~RGEk&~uuRM!WUf)0oQ^#6v*dlJ!10^<_;<|aCu4nXNRMXr*h z`T}4g($o=Ov6bj;s48=AlK}&E%&q61TgB9{t}o+u`fYo#0}7Zap4%`CYasw!mX5xQxYb@IcJLllUyO zW;s6e9Y4N-dAsN#9vn_Nv`c_N`9tX z=Oe_W*?#qJq`i9peabXp_aAS;5xs@a%50f8ffn~C;N4zC#5Bzb%!>epf0V4(Z_?v| z*1Nwq|J!|r>JK#3;o$AmYjz>d4eEVFMge?AjmJT`v1s0wqRa=08Gz{8>z5-dQCmK? zfBQvv5DYxQ@8Q}Sqv0$!qhtg*V56?hr~Vfn=A%cjYm>VD-~ofOmSPervtKm7mCiPT z?#vFM1)ve!#W1kA_#@cs=mN$clf|S((vJe*6tSDbK544F+2FO=aPEZT`73tvZYY>M z^WUDtfUetaFjJBugZR}CiTGV%_yo!UFvVpGcwy&@kMASxZa~XJjYPf)7y=*A^6J9A zp4|%_{AYcbwhuYGS>F2)cE|0o-P~(y74Jgf$NsAfONiPu$jK~GFJS>_ofECM;fXtT znj%IB=0=wFlvF+D1sk&wq-z?;JdH_9pbwH99Xmm4#L*xZhKjU(hlY_sS?t8e-3;b; z-^AEQBGZ84Kr70BH~Ie~lvKQ4O8Lwd#Z*bq9Xa`^XVb7KKEKoT*Pi2dQL_>rN33qV z_HSE+l0RCF`O}i-$vLsd#H{I1L-0(QCrK?Gvghst^o9U7CsUwPs@L4G zLPl1gc0c<>%#QuXi2Pp`UKlL&_f!s(w&Pfc-Mu#hE~=CR?vP1n6NMu(qAa=mZg1kt zX1iNwhwZ@k*t<+XL}+*HDjR7Gi^ogBpfb~sPwSGeG^f3%+U^EZKH^`o`DRP$g-xXR z>6dZugKruU{QDbn@D%vmEY`xNIQPT51D z4@+KuYz?XM(5Ul6q?H`jhyz>yH0>t*C70p0Ed}c%aU3YwIyxSzC&0c57{V7ka4DYS z&ZNN|zaL(#!6WG{c#1n2JR*Q;T9XFhF(fko?kQ~f>=43@$M?Xm51o^BAU&7=SSZF( z0zXcGm$X|dVRx?im(A{QN|Rn+xOU@u6`8%-B<0#dAEvX!zUsw^HuXuDJ5H8npS3%D z%B~!y@Y(f_@yO%}2C~ADPtXYr6$U{$?j^PO>!SMd8AMi@Y>6V(~p%& zBCH`!A0C|Hw{N>h!^Hsc>;Ar;w)OQ6qa}gqaxN*3ZnlNnSGY2?YtQp=HZQOJLuiU! zQ6Ar3L^$h!D!Lh-LC6gNdXw!|gC(RT&R$HDvOI)1q73wKDSG~ge{Drc;`H!N>vjZO z+}gh+VS7J9{(O@3pm+VX&*nIC{4sqRncNZ%@W zQwEv}GEnr~l$hX42E6~W-Q3rTbda-yw*g~?cLlfc0iQ9xrDKRIyVY4si02@^&NkHD%ho7MT!3C-{7O_V z=u1@blRQ;(}T4s19hJpAVF$5)E+H-fNG zyL%)kG1G!c9pu}GZ%6|wVnq~n7S;@ zc^|p3vnTo{7@SHQj9u6pc5)Ic+}yAL)trJVMh-HRsOvUq zO^(oS1KG%}vt^m`SE%xei~ArZu!rET+nXhJ^sJA~$ff=m(6KcF-rX4MwmQ$<(P#1| z#t?q15CEfke1Jd=yHgM6jyOEL;$?T~e+=K~R%XUixy) zy&dS$ZxK$_wk@oBVJYocJsj$?V&idi@%7vu>~> zyolCb1g(K@f}6`ld@L*h6AlZb+snr$x_D^G6q-kLX4|P?>E)~B;R1V$_^afD)OkM9 ztHk~_!xKXG`EXp-1R%rLouDx1bpj2Fe1IPe=mjph5%+Tn)OV@P&q_W2XQT^BU-6aH za)0oz?GSujw239oS+~q_J{RS&!MrKN_((;jIZzH)1Y(i=vl*j5-x!9mg*$LrJ<-QL7kv*gw8s1dPKXX={XTER#!9zI|i6Z8?6%Pr;zTv!Hht)}Eh*mK~8A z(B|np_;#8Vl;}>$HZi>YjvIQz}x^FXYX8v{M~gPRhTvnJjCZvs7kp)R$Ms=a#;QDz9OThSnb;;TFCP_AgH?!BQxSW z9@*H^1GSdb;`lYG*a}yGeFn`f!Fj_)z#!ydn|%$n&Ka4HGJ&aU%`c`Ado`I22O{+8 zJlPVq{ALO51)06FOlA~@NyzgZX6==A;gzJ(rZd+(|L|QadnF&pZP@+>#LOQyP?+c4 zMF6ob1b8QNfd5|d3D`N9^ZDcibI8&goTYl0e*7O}3N0BC&gLRsMJw-fxGAUc)Hh3c zR_$&{iL*=sc<5Hizv~PUxI+Z+>egE2`i-3&*ZSAjI*}K_WZhapWkMq+Bp;)1_Uyok zuZy4+CgKzFP3*BuGsMByzfLSQJ_%=QtAE4=1rn4yX0f>5OVpL znH#3{=>w~AaTReYLmeiYM~j(5{DUvAsla^F8V7Z?ypc`IlM5`otMhW~#Y`@%(D*Ua z8C|acmSh))o|@K@_XN{CURwg!hw#Rz3GW~-(d-~D_ddWwgCu`GeUsp zrlN>Xc7=LSu&9kka7X8TspcJ4lSGTK`C!3CDxs_fl7LyTss?8iC^4{=SD zRF^KTNYHBe?gHAH)tI^|Ok)?#aR(W%#0CX3wSsQ#WRg^g!j+5C?=RlIqJHgkD4Yt9P|%^x zxoxNWX~R2hy0VOO2pn9CtDD$W1J$jOs#L*?V`KO|3p0{jT#-F|MWRRx`#MZ7?wyx< zT&HAxRbSX!wNpJu@D~adETvA$+@kw1`g5!}5WqL49lr6#hK0O2mMnV($eHPC#ym28 zrTh#Pil@9>`e4)bJ=d6Wjl<_2Q_BVNq1Vr_Z8oqeEjd9ejPBAmMqL-7zz@ZNOc6_C zuXSt;Osquyu?;94bBeTVPY?r!x!Y1nVt!WfRD5ga<8Xf;jb7VBnW8^|4LCZhcwCHa zRZMWtsTDEMI&>>^Ca_#h)PckgedmC-^z(EM3*X)%V(Mwbp!`>-j;Ca$ge!a$%K9$x zGs3#j;T68EqA2GM#15{dy1_Azl@9)_f`>C`Cxuykf3<$D_}WI!*u5L&=X#4@8EsEJ z6#3)oud&TP&U{Whr&OGZHDDb`j&k zWnElmIvXq(4|<;~3q*_;o@>V!bW35zKy0~y9rS$8ez867MmneoaXENxyF&@C6oaD? zfu?Ri6rUw7R^!;5fHt={%lgT_1k{u+h=XoiE9S}etcibMKZ;OSLQ9YN;~UyLDq0e` zwX0r*gZgmSKM)1bW+tDP>aJDCmXw^oYr`m*5DPM~TFXK$U=>u=w<5O3=i@fd#adOd z+!K?xGu@3#pz}_FD7NQCixW6mFLxs%pk+hGRMNaAQP>Qt(~#Zxrro}|`y+;U(rm&o zXYUwn$G@@1ZUEA=4EdeJDP*t(+SyMe>a4xxs8*b*`iQAUEsCNpOHb2Q7K^*TIzl`> zqo>gCOKN)-0szHd4Z8qCG7ako-VSe zv;M+g<4&YIpYfPKK5dd!ux6K)hnG7jGjY{ObmYgWnO~ddZk!y*6^Wr@nWTfK((=x( z+M-d8h2y&E|TV zN52?`-`nj3K=l>om`@izFa!>2_m{h@%Ec5oK+cK?Zafx(zBBPp4$SYN6LkxYoz{^S1vOp{LlGYyl zJMDT`&`D^A%7(v7skMz70`VkPua$t8X^db~udgmR#KOV*UZS*>DmQk}sVUg0ake1I zqROv9k{IRdh=*OL7d%AWpZ7spezmyy`?G`J%fi_V)xSvAE}y!evVXIvXNn?wz7Y>? zQpQ6`2qNMFZaUu9AkBUKNgNGV;eki##nLmwAvA6$yzv%pLs)W+E0R+1?vyp;f^H3x z8J=gyPBfzV-bDiVRhD|UmGY@D@n=LMEwRd(4AA<>pSd(>F71uHAP)qNnxF=33KsW}OoKABI^axw3E137V09 z=TMnV=3L}GNZr`UoK~zyd+S;c#=8V*hWuHhy~i0()d@Dj4%iX5JdVGtP%K^A2BGmc z=Efsf^A?HWYTjtI`jV8}I@$Vn?JtIfvw130m0rb2E(fz2GS@K4+eu|Le)CI)qi?H! z9gcF6gRb?Nv54auIF$b>(NeQo!cgTs0885?%|I4_h!@^AB9p^=tffvum+X%bXIQiq z$5vV<)uiiDp0-@Nhdxgx(Zt^AyB$^DkKW8n%{!UBC68@{`iRA z&}kTJxSFXEjh}gwiFxS-yW4I;!G-UPHkz6#o9PV>$L&V_%>+nk2lje>$I^b@yX#=` z!k>zUq47~ND_zW!Ag>^KE24k$*%QneOlJKMyFULV_YpY}(05vwDQOqR;Fj>$2hK&A z{~%gpiAxjIPfVAAwJAG7aT5$~>0*~YC+cDXA`w8szu6Y}sBMQTF=l5C+oe>b0J`FN zDq-m-;HyXdiEEwxPXBz^)7sf9kiUkiyWo_2PtupF6!lu@Q_%4g4!DuT-CbI9$sXK>>e;?p1#mkZhEX^^o4}6#TN*>HM{NzK zAbttvwK~NU3TN)mJ$#?arw}3#X?dQ{-j0dShk^uld10ynM?V#z5W!W2I?ehq?g8EOjj8}5Z#$4U=W>^`|pUH>^GsJ8&^iocE87e-q?{Vvv9X74DQ=)up-m=T87 zMQWh*eZh_08q{@P+pA*MBg^S$DM6!7|jW5G=}>{8DydL zH{}#rG$&TfM)D>pOK8jh|A)xeV^*vV>7ypaCkvFE#CV#zw~%Aa4_z!c(Xq^nNd8YwfcwIgbtjfIe4#h8z(%LXX@3`Pb-B5;7F0r2)&pUF~Q2iU=PxGLp-lg?4n}dTs zZK2npu>2r(^rFfc&h%UvdYLrYB3bEmsK=BO;KIVh+!(8 zm6(dQY2<7_pikvKNp{+`_n75RY+2Fq$QHvm!&o|IDjePdOlMj zqi%Nb@G%B1eQD2dN=jLu;;CpH6qpxUs=hmBeU@-hB2H`Rs6NeSvdl_!P&&o@^jf}k zR%y$`kK?zpyMw4Y9{MWyRH_lCdkqVIR{UwDi?eJQ%QP!IP=yKMu2^DgNhvpr31(#; zI?<{96+*6IVH2#?Y8$U)ncQ>}Ppf=!?)?)RIp*;V6bj1=i|x<~31!n>Wkq=cp@m!B zuT{Cye4w8B?8f=Ty_Tj3AOzMvgxW%FF*f<7p@Yl1lT7{Jt$hhguf$0sxE-xr?3S9O zIzapy%D7uLUhLHalnJ@28wLE0Bhp1EVuKm*1lht>xy8JRMt(CqQ4%+BaunqbB)$YZM&{lI3ismm<8{9gb1KbaiepkIsv zfh{h=AT$Tr>>#LzZW`0JX(ZL<%1gfgw;B8{5ItJZY29~sf!yvclesBk=K>tEu=WH4 zieYAi@z{L4NVzice|J&qphCz^8Fig_(4)+=tA+sH9GRomhqr1t;_-{{{Gu!y={Ywp zo-YF17qRItk?qg%D3|5r+Wq>07XcsNcO7>UG%})^__i~!DNHXwk|(e2NAUKrkU^Kz ze@%aG<8o~XbfciKCx&zsj^B+gcVDHu2viPyv5%xI)*xF`h?oWK*HHBq2iXvFI}x?5 zFAC~5s1or__i$~pa8mBTFzb$Ca3mEK#c`<9y;`_Ug#*G}9tO&`FhmN8qlq$;21Cm9 z%%UGYd|*3D0JttF!bIyr+I8IEq=3wA9cJ%uH)RaN_{Q}W8x3x(6-p<`P?8bSA&o%C z4KI?YG}l$r->x>vz?X=!{V}_rfir5)fkIi9%Xi=J0ECFG`L5Nad2Y#8duT!e=pySx z>l=SKQrg+W0pRm+XB8wNs$U3>r$qlQ-mZ$-Kqi~-_X_uB&!Dl0JE(C*?|2WIQ(yHB zS2#PcO+BUZf_wC<`q~DrY6>E&A@nPIPC4HdntXfBdOH&nkprIbbQMX} zM@HoZ1vkHuul2V;8YsL>du$o$3n1BKVLoM3&-qLQAQ<*wfr657p^Y|hj7DlDWO#dT zNppclvMmd=PJ}QAlAdwL&i$Nqj9X(Y|AlyTI3@2ebw22pytZ5_p;Sf%g<6KDqzJ%h zMma37@+`QmFZU;N-y2W?$0P;n1>m{Ag=ix=uLp953vkltLXGO>p{v&ofH&AVSyxJ0 zBtRRd(Xk~I$;rGQ91rNOI6gi^jZoCkTCeoz~; z2tjCM6FRVad)XjOYS3$371yU(iI!j_79bs-2btu-ON9tI1C-R@Sun?>w% zGr)=7-!HZ$Lup(zm2zl}SqH8@*-ZdA4-Vqi1qV1f2%=Mf%i$@2n(Ee#y~c|oVWCam z$o5xnkh}Yp!>x->kHulfESq4N-IOOx_xAT+Hr{^xVKNVh-u4lc#B}*yej~lWzxGzb z+^n`S8BPbVX6))dzg0-26aqkFBx4nVnH)$Dx5Cm)Hvu5aCL%y0h+us&1=+7J8pvln z4}1YxM_epORp@4WqRYo8(8$OnIVbb|Laua zUvMVUrxUX7yz7Rb&KMcULt;PU?#-;CANw{pqqEzaQ3GuC#OT?^l+8!!yc3|SjQZE$ zk^S%fg1uzj!q5JGQ2=vb3ST?E;Q!%;>Abd!6shsB!yp`JWIMaDvAB;@9R)XF%H#aa zOol<{D$P|?2Df({4YYvS4cqv`VjM;JKvU)$;>WD3c1D`ci9Llg0=|KN;CW`MXYTgJ z`QhSEZeIShdGSaTQq-8dQYC%fhFnxA6{P%ZcX49R(zK-XK7?3&3YeA(qT_hfkFk*FYFJRH@sA@Cq=P0bXfg1u=Nd~QFIoaqRok99_&ZMrK* z&!uew_9kk7M_4uITkxQ+meKNjPP!K!U<;eiHj13x6xQK^?1mFs$lsHNCTFl^Kdsf~+)x##xD5DFfJ!Hq#0Pbi3(GEqVU1!J6TzG7G-v4a52rD_jzFNzj zyo+?J<4F2bK9Wsc1}4RiFWL<=|I=I8PYm)eH-r=Fj3=Jnl5oQfs^y{+efab?yN&PAn!`E%fR8{8YE@u0ocj44b*w zM#IPg)j-{CYV{5>0k~P4*Wb6MbG$ChBid_nRof3~FM6T!`t;9$8h2JcO%}xAQO{|L zNh^^wV%|m5lCY9Ppi^O^lNsL*N;YN3hH|Iqz$Mi__|k?r`~G3l5IfBRmsjK=O)51c zMb|Y>J-QjD?r`4?vNYUF?@I%hK`8W8M!g1Npdc&8ZRTAK+Vl-S{pymFSC$SPIeC+coNtg7hk3EI!T|Jlk{1e>up>yiYFhUioy|5 z;peOX8)#ns*8!;fONFb2 zz|i2MXR+SC`)K%CfYo|S@KuXMZtQ_=TOi*@;+v@2`cP4Ju!WvHuYtrR-rvq|2e*oF z`DkO(tW7xzA6GT2Y=-La1S#P_MLbQTQGE}TQhS5a0^#AF8@Rp=y$poy>zt9~7C~LS z81BAKGSHDUEsWLR@=`H`ub%-M8FPYJ4h9Wol%x%v7T#>;QO%2_s4D&{ zT#rGYv5tTLh9Xg&3ofT0{V(*=8h#$k__C#vKaaYQiltQ!lEM)`Kt&Q$xUow`;0~OZ zpzjT@iurNEQPSuCUr+kxQQZZQC*Wbx{)~WV=pYFC4=p^{oZJSgx9~^QTdgQEuZFfb zBu~Cb*HJmY|CDHt1=vn)Wa8lW<+ZXf+{r2zoFJyhfyo9QG9?9=cN%IHNKNiEne*8k z;O8CLIu*}zLE;(X0z5&4WWNov53Xs03S?}G1C25svK0i#WrdWBfZr-eGoKiRmfs#? z0n@>mS#(fE!0btAE6LX`b0i4Q*6Sft&6={B&sl@AHiy;vbsUCh#pu4T#++phJENpb zw%FmqR@%?|M|13kB+Kjikh6w$`}6-wy9Ng2OdUijP^a?g&26!6q1pn+C9xZGn>^KH z{^AKf1lZgjeqVwrDn2|Pgv7nm73;2M{pDpY5M1HGy08mKb7mk!GW0DCDZdZ-$!=61 zzj55^>HN?BimwgorE5@X>4O6Eq~veV-YEZCNNaiTL8omD1_w`KAd!mr82UWLPz`qc zC@|}^xAOPvC2BD42^^W@PLD{*|+!Z$zqAMH0SO7LN&8y&R)DpagWW= z&2Z4EBJ9mjh3O(3vL``Jk^`EMc7`NV>HrDwOSWsXn|!FS9SfzyNbcoW2L4mapn4AsR;0gj^2 z5@V}UvVzRiCeTU2McJtZ<&i4eGpF9^j>`o8 zhR%^(l;}GkIC^^h(<3H>FRFs>DxJouL$W_j=H-U5zpZ}$w*Ia3a|Fpk*1wHHaPvH~ z{Mf1&uIL2^qv6EEnTu^8ZS#1qfRSxYAtfM3}Z?>K%|)*uyWVsn>%nWHs&%AnC{0 zO|S!h{k{87=KChWiA&3%l?p8n8%+DlmIAKy>iig#W< z5l`?YAQmX-1t?k_f*?qW0+Bn*=P&CI1TGrVUY&!=Z{%w~aJ4=_gu}$DKoo&ix^2|_ zcFdi0wjdbit`$|do`^JRtWQcCJvbn& zPj}og_~6FP8*9zA){N(QCZLeCfdg)p^n$2yQMA@_f}GvZ8wI$e5~$y9GdqG*;hWd= z<_TxJBbKWAW@Po##EEsgBB6n4px}*VWo?Rp7cH!6`Wia6Ujy$zd3J58Y&KA2*?kd=V2(kEtTe8&?aR{-iCbc%0m-a z^wlG)qCcyC2J_3>Jp|}iAy6l3ruxlNg4~5L>oxQ-aUAA%{2d_GBDb2PX;COB?!?&cUmrA(A7Ov7gWzQDr=4E0_<+6iZa1h znC?o!v#TSNJXYxaz#bCJqyuwwaU{(&X6+9|(w2abda<9|Ml`9#)J|BsXkR&+GO-%p zhIU*p7ks(8Sx|nSp`mOwURiY+0^y0?z>*n_j%`aghBo-6YEX;bJfUwr0J0-%YBFc9 zkZ?k44jL(CP35k|FV!g~#ksme!R}|SS_!lxXPTUe$u-RetixtGqOR+x#`@xn2(F>J{*Fu{qICH*$Ee{fs{D>|eMm8hiY~z`1fxLg zp;bFQ>cv}F`ufw(^L_0BI)2ZPa*)T5tMn$6h#oy4Ru?7$ zlD{oC_(Bq}p+#uleKdYH;)c35!FvJM)nC0~>*b2NFBT~ue>|>NlGP4p;>ZR5dA8B2 zy^c44;?TfS)mfXU9v=c#J%lM3OKjDc;VbKB<|pXK!XEZmt)nM)z~q+x85CY=W?7f} z-cQQ?w@}CGhmzm?!zIW-m=Cz}M%Qd1s>H;}KU`ytU$eo27#1U9%}(YJZO_-@x46|L zLn4D$1OZyysH;YVWU(|0<1WCWiXkb{F7#?I%{q+sHKHrwv6zb;S2A%y%AR#57xM>d zB*sH$UF-xVFBS3Wt9Q&oevF%4rK939F^04@W6pIn4scR4%&&g-F(kC8My7s~56yM; zV0e)!WG$Y*A;yKEQR$Qz`<=jt*mmkBPMa=||C&p`b}xZqOX(hHSIZhIOD)moO^BKb zzg+Rb(5~Uq$A0fcw`_I|^p^KXjlQ%{arvsfEW_a5GW|hOiXW3i87w(&-fTJe3kG0l z>b!?J#hgIu%IoJO@&l(aISV62Q@8HQO=+iudXy`N+HHLb=!xCRc4?c+*)!vJ#UwDx z77d0T8-_`AA5Gby31o$3ysFvoqsRl)C02x@+uRs(uVHd1XU$MAQZcEm1&hP)w_918 z^F~G)Gwd6ondtXbHx`@WDc9DnZG<4@~97?QdGlI$=G>Kwle!< ze`;zeLlt8Uy@C$T?L>B5Sp=x~ul6aW9X*tr=qX+KPdgBFZk5Y9g6f1$JW>J~wt0i>gM__H4nW zpHJ_|2d8a+r{256Ngt~0nm=A+)!4uSn7p`{mbv7y9SkDv8VTQt^<0(JA=GYcSaBFB zqi6Zt7UitW__O4XS#Zj(-Rlqr;*(K!V|ONK6dQ3*jVi2(?4Fgf&wD37^g@P#{;_hj z5xwz_0P*>ls*F9x>M%3|J>D*vE!J%eKpmj2?q;2!Vfk4+X46vMIOCgT%aCT)UPVCT z+JqxB4gKy;CFp^?klAY&|F$&;ZU2;N*JxOEuwz;0j7|C84>*YOOkg`ViYKM_8a>D= z~-Oqv8AhrtGgUh#q4kOg)XW$kwq`wh4$XEs8n{lzvCCBa%e*v(*Gj+$0Z!W}g z;ji`e+hjT<@up?0X-iRQ1%u*j568RDHYoa?KwT@KVbAPkY3!A{jc~@d>B9Gc0ZRZS;f2gYSmlm(G?5qp6rw-dNRPpnPwpDR9xK3V7O`r8EoO zr}KV>aMcl{Z>3CK`-+Kh5Rl5wiUXP{m+BOy?s!V|Vo796+0_l|+>;HM`S4X#rd<*% z3)Zdo(cyiW35SSEZ502Pmlq$kzkGbHNn?oYJm8g9BHr(9&7t0JwM#gK_Vx6gMV&H^ z>a?t6#RRXtx!NQ9XTH}|%qcWPUAw)!ODoi&RUU!Hx6V;#cb1OzB-_+hj;=k57zlM> z=J^^V<5=eFrk}x|!dKE*_LT68;LiJr{t&UA#2CON? z`Ujr#AnZ?d6ZyrsrSKZ#PtzFnnRv+NeY>h}wB6v{Nv2R+1$-q74P`3+XUe1jGxkXp zXNEQ004pRah6=po+qQ3zHIUy$kGyOr>{ahrBTRj$8l7VkB=IC zv3Lm;gi0iuh&o?Vz5J@s-sG>;++PdDrdPfGXtBUEUMQF&+PuDy7~etqhYnXDiCJ<4b?ngGs+i)l7#` z+aGXas6Wn3^Q(AUIUY;JPHSI+C1fFs6WiSr1oaxer{OFJ;SAf1@F=@Ug=CQR?hGB~ z|5Uw^$Oo@Hdx0Y?)m^{kBD9h4Xt>FnN@I}58b|xF_=V9BB`&YH1!KZp;UvPU?-%sv zWvFIltfg{jq(0y6kX)9w&TbXClj+_e$n@y%Oj7h4s()7ARVFmoAz&T$fL!_Rg3j{w zrks*n%;Cig*~vd?G$RNxxjgT_zBG=#o4RPuWFe00lV|z7OKnppRD9zZtY)4jQwKMb z2^Gc+bVR*I^_Poy&m@@$lXk1)i+A{a`>BL4laeRD1bp4P!|~>@fe3GZ?foBGA-~G| z%-x3Nc=~I4)22;uT^mfg^O&Ec%pmZTUa?~PK9f6E0Tj!%$Z9p`x5QL_CDQhu-7yl> zjCkE%S+94tef#T&i(=j`I>u%pVLx7;F!(k=qfJWOd3sHE$qO^I-ZGYaW7XXi zwINphrKir;QfYIn#;PxV3!p6;k+BRjWvU_!hCG4SaJPT)MlqXomyiUwT!+fW^%je{ zTJUZtgruW{AzO&W<$sha4T0(b59?K%!(<|(XkvR!uJ?;7l1NqQ&}9^F0ssl~RGmZQ z%V&E_K7?ZNg<%xW=s#wv9@oMPu{C7X*X7IMlBB`)rlJg8=hbq~uMmxQ-zfT;i(OP3 zDbvLGSw&XgW7kn1#j=TCMuT6JeIu43i_`hC+|3#KLi|vguq|5r3{JT(>**p-A|XUZ z<@}Zue3)V9uNmi;ZLRs-T@cs|J5NL%oSg<==$Qs+PG}g%4}`=by~YZtaT3aKR?2wB zup?5Vu*=5FTcXboUX|(b*DXP-rP@8n_PuLOPfq-rxL2KhPD;BWn*vu|O1LtiSfbd` zKB?Y0NFV{tJc%M{acFukp7=z- zrJ^6F@%pu&GkI{)QbaX+ZUkq8gIoHZtaAVH?<+Lqiv{< zKOM~)Q3+}m$>O^!ZR!r+W(<46YA>~9s*`KQ_$LQTB$k?S*l_$e7A0VdJ~|%84FDU zy-!%vE-OKb>teTl<@VEAtfDrQ%r50H#+Y(ZnE3q;l_v5 z{=P`1eEa)ZaRUi@Eks3)Uneg=3&**XCV2zZf3;=urbH8Cqw+JQWcIrfFI;IUXw}5d zP%Bx#us{8!^6hY%ULERFUXkJ2L`lu#BpMg;%f>%)W#e)(Rb?T$qc&YN0H!6f_%MJj zPB+VT5kh^&Sru4hPw0ZsY7a%i8OAu9Huklo(mt058=QTp`^vw3nz#B#la_T-Oq?jW zfxxtq(786ghLJ69t6Nuw{ki2sGS6dJd1g)X2}>?t5W-SiHJ2oyn|;*h!>`l2hL&l{ z_Rw0Il?iz$;#6lHbA{ScjNf*?c=z3GfSuygHO<}>j+`S^=r^Th?Cpkvv-?&a@{-zk69H?IBgQkTvyh+LQJZokk$RM^n&uGcfI^X(9;WM zF{ak#`L3T$AF!ho#~oK+5*p-8AxK*RK>l*Y@h;Qom%rs+LE1>LU&R!-^xppB#;p9X zRt496_bqL-LhBdxem)^}2KRN-f?FtcY}`BUe;y}jcw|}c`q7F1I9=%4!aJ4v=xd(a zaG+cD%T~9z!CdRU6e%HZoc(+nSfejF?d+{HD0^QS_SNmazr2;l{Stki>xq>E)&iIC zb&*7W>~y)^&e|%0_R`z;mS(KdHTl&F9l`58j1KW}j{t8%#hXNNBNa|_ZN6WO51aRS zMN|J;%c$;W6_PK^ALj3KKdYdxf?~pOF3OnNzZRXWi9pnbx7OxN5s75pLNj?+^!C{B zD0F$rYesolFUmZzA54i3NNzIgh;uVvl(Dz}{LOW3c4Db~fvXwCU=8e%m~z&RzfezS zp2Tu9diz1W0U(7;tdujmH^@a;`NXCnssVc>B%ewfd##tyDOZgiu!?M9Joy=GY+Fg;o9nsth!c>9eO&$>Zxkb)`dV1N~I(pw=ij~gz!U5G%( zNX?Dt*bt6g+js4d@OVC*7u(AHXl$xPhNraqk%SsnLTReH-7T^bnHaPDMvELxb}`x4Yb%9-W=A&+&8 zll0vX`gHlUKtit+u{~BZ5g?1I>2}07q6S$83QiZc4X9E80DG`A_1^;3>Xl>fpE@uO2a|4Y>&&*|!&Mi&o-Rn@FK%<&jZ50UKZf2;x>vicZTZ>HxY}oRX*_(Mk zpaFCpf4mPZzG&zIeU;E*%T?Bu`dm@70o&~_3z!z1cKuH-t#%-FwIRp{mzDb@!zawTk0p;#!D6130(;YLB$w)XLEP>*N?M zeBED@UFF@MQktz>OcsjU0#Ij*$vI@lTI%r{XKp@uTdm~hACh`70>+Ug;4p~Df z4t*&*6#^AIDIuPayNuz>ewq=Ni5u*8eSbh-7jEJPa{_UK?6HhZ05kETglL}AR*3l< zO(j1_H84%YlQs>Am>r1`U>rhOvei8b_Jm}6^ zmx|yVA_)PMySV-1J^?Koc4|vMe)Sh@MD)C+Ne_McAc=B#dK4F>_cFDWuCjDhbEza` zJ&INvK}YTL(q)m!+0F^}ZtJcjQwmIv-rQ{ymD<-sbb|Kk1Vfvo>Mn2nLg@3a?_Hf4 z&8Yy$B>g3o!lv$5sq^?nCcgb|JEFJo*$0PKM(I-SHI?8xksT_$aUsNCF$LD=2go8a z+O|xPa@~eJ)iQnn0TED`bywAM;+e?g^rx9Z+4MW27DJ7nIoQ)&TO7t6S0;duvlEKe0e6Xy-jc32$IG_;W#W0Ddz0;u{hjX zXkx(GGvt|%OuS(p#7$g1{Yru+P;DPl2XPxkR5rt=_VrKm_RKX-uU#kOzPfywOrt*I z9;TpGd-pwkA!~mO7sdzG!86ND36hsQMiEP_h9%2^=GOqm!gCgf^&b)g(R9keS3c67 zGs9|z2B1EoK>9p>95)suYF+)ZaE4Rgm0j@ZNe&n=)Y1-+2`=Xp&>mu~J86w#ak*Tw zs}|?411;|dE@+$D^j@xRoKaS$k@aqlbrLzd zd5n!UZ{A80CT>`wD5aPk)q61 z=l-JN5%BP&NoOwPzMA<~tZ&=YSdo9uFh2}To+E+DbPdPNzn?SB;i01AukxMtBEojq zTY%;*7T;K_KM37Yp)OPkYYC!&`XnM-2I33vH|_PAxO-r|d`)1wNt$D3Y%MYj07nnO ztd=XxCggq6NS8ywcSx^RtH@}$J4a_jo!?)i&tXIG?0*Re1jjG&>3}EgDcp@C*%ZA0 z!+HA)oz~I|Q!Cqiqi#M?G0^RxhL#1~u2*f6MIEEF@@lg=1sz#T^QFLe6H&K-!HWs5$5=G}lLiF#8aCKr0@ z9LWO^6A+fO`uQfc`oEyt4m6h9w+b$@Ywz3NcfJmgzTxopqe@&e%yG{SVY|q|EZSos zIx0dj@A?dGzN@E3N<)KCiMVg2w0r~!1L_MRkhINkbNd)T((XyWcmX4E7Jp3e2+Z&m zEQ3fUaOW@#8plQ&PVxghJdC)$kr!Hp3_x7!AKN_kS?0i&j$xA_a!_ zc=Jx==FO^{iU&8R_Cnwuz>TvWHJoaRvj(TH7<$jXsS3ZsX(H=`j<5plqD)<2)*~>0 zEe=F|aBXpTkkFC5&YBgPqou~ZdT<@3b2;!L??s$se zjylq<$RgAHu%&bx7fga2XZN`SQQ7Rf4~EKY({cJjDja4EPg6LAc$5#>ed`ZrP~oVV z2ULPJgyM;LpnAyzCE$cdagIHOz_=JJ8GY)u4dupNZd4L4)Qc6F|J%dV`1QAKQki@) zDho!mQW3Q^FYH%p2-u9wg6uF-<3eJJa^2CRZy;l1{&01V=>>SNPagG%ZA8`F5doJh<6j6*u^_@$!uV40RvK(q)7 z{ei&aG*7D`D~HbxV2yuGl?x#Sd{JOO@%OVA!NhI=q}h zRQq}z!3WTZi5vzNASDBvE-9_u&>MO*yLkEg&89(9-b$BDLhDE z)*IX!Fx%p}-Q>I3=Uc8iY{PA9xwRB+7&<=hq`hP_>dwO$wd<}&2DuA?VzXUYVL?}T zI}j0Dx2>6sFAx6<|3L+>?e|4~>*|-yBSyAD&_+l_*6ouB_5UQsHbMIfNXmWSERkxP z$D#!`$HV?M27n-tPS*s=!bQ;SwFW^(ZG%v9M=z}6_Er1%qkZF;m+LiXT1!qdo9zvV z_O*KTTE7r(HOPgk$T?e|164Kh6=BvxOkxlS>HMSY2`3Wa1XA$#Am>$IKG+ekMh8Vs zQy2rjEMG2ur+Q(lN8ba*kI;%>qc3^;Vh)sojiGCUrB&rW1`LoPWE0y+i{JWTF=9wfyFsKT*a@1A7H5Y2 z6NwQ{>_N1lY(;qjPa?Yj37Fpr+^abV!j-#=>(G@jBZDI){f)&bgiwrV%RUA^Za<>< zko@&FdyXtL3d@ap2cU68K6n6*^Z69B3}|AK{}YiEu0n06>#txR0+qoRI_qV{9-~n2 z1LgzwIsvv#^~tCk?wsQj?Q=NBa$+#XTq@1N2Y`NmY(%1^^cqWg!}`MDAgIzsPf{aNRPCpyX--|;42dg?4kvQ#E>^$rfo@frYj4{b3z46z6 zRo{FB#DysWc+=GC?tjN;6nW%Hfvno=kBK7BtpL?W#~#CV%_J<;BHkU^y1bc-CS@=z zF-d9DYk7KL&yGSwnK8+{XTZ zcZm!i*G*)*khg9)A=VTb82l^^&e=%mV7sGTXB?pC&Eh_DhsPSoxbE+5@0QeNsWK|1 z0jK&c1+s|&T9l?yf$rcqfPJXV3^v6eo|HVwi-sj@=dzL{!6>M`ULW1p>lyt2N&MG` zlCp|KSO6k@hq(QJu}^UL8qUr479@<8Jb{4Jx3cx0gdkPhP59IQjq~mDQ$fU;Vso+o^ zC?i^(OjJIctV9H3ALD+U&HeA7nO%Yb;`t<>ay0H;`0#ZKIrWGNe{>#!S@b4^59_Q% zcmBad?!Yii{E$49M)fiT`wU1D|JN}g8p$7%nhNZr@ryP$AD3dm*OA)k;XKm8p<%sz z^5d_AB_juaJ&4U8bXcjOQoILWcjVGkn}s2ePBfoHL=~#<|N4iqUxxEH^5u2Rp&|PN zp@7+k$mL&fBLUwxiCx<<0(H}=FT-cx>*~iC)ef&84Jh-H2I6C14mct_2p!?lJlEmC z0?iiex~JV(Eekj~j9&Cf3(7}Y0O@b4;ZgSuKXD$}S|coe?W@6EhaBA-$dV$_8b4An z|KB;l0|+-j?rrD6Du0ZL$Z;VK?C9Ki!QR7h#%QZYv+Lb`gx4 zq1o$0(DMIp1pluY!T&cWzH9mysQVZ|01&VXH3!5EMW}g5wzdR>F|4tuYKb`kXg_Ka{`+uap*mEy&D-AsI)Kni$6Hv? z?m~`~;xMn;g?PKo3Q8(YY`*;f*KPnY(FBvnN5zHR?Tymw!MCYV0yANVjufyg&%43V zieDs^#H}beS?xtvblm3|+S*gf_;0Ti{$O?Rg4OA@Ll`~tNH*OF?AP*o+ zIUu;luFdy%PHh62bm7x!%3>r3&W<$NaIp3|N%R&$VC#R&Y^ub&gkc&1sx1bAT9@XS zO@@GnDc}Z`2-H4wyXG9-N5sA-;c>x;k(C2O)^U{nw;MSVbi^PSkX~{%^U>M@Y0l;B}a6Pyo=!G_@zY?MS{&-x~@*3QcEwL0z^1QG;c5qFo_mzXK_c!Crk2 zYXrW$RMaTrpM87)3n6s8vWpQ3o*iyh;9#c#6VDNDFOxCU+-~B~J!``LU89O6uEWr3z`qTOitwY3_yvqrG)fR{>`DD8H zg(EJSq7iI5+&tGXZ4?_qW;8ge5g)r4JW^{v@Qv(mZf-NES;y2KO|h9Q)}=ZHnf;Bg z<5Hbu_921Ui#WLeuw)?aeq%??G3-q2^=1HJMXVuW3;NFZ`3&^7DhNMcMSd|25ugeW zv&e$5jCq0@PlaWRdkvISw?AZm^W)Uln^@)3st!xriw_H%;aRH@ciEbQ#KD8Dt&_w$ z-wX1JJER%t%~a&kX|=%=AU4@CW&0Ylw+ERF)F!|txE&$?k*bplDw9d+vGo9eoNE9z zA$JN0`fM8G3g)(FsZ~+yY(&IKG0OTnck`-+?8fWE0|?g_GDoH53EYU_?{yj*s>1_5 z$m&8T#J%`EgwGle@(Ijl+4~AG33WXY)?x^@D!FtqJPF&IpUDE)Du-6~jEo`4%mC zUa6&m_tur2E-QHv4s`KdSG)=Y-^}mt?vC&JarfTh`%jEy;pKTn^4B$(fw}$jmPwCR zldN&3ql%D81}n%eGe7h++X#_kjJ?_MRy~zTDWa4KB$|YQ2hY!c2n@~iKO;c=b-F-? z+9U&M>E=eDb8P|CU-zQ1ZJeYTyH#0yHJy8`owz~7HP_o#b!XOxpSZVlsii!KnYfa~ zI40nGpx$~b?eCf~U=vx{Ds-rAbltb(d)m&rdKTpb0(|dXAF;J7Y zdqX4o86=n3O27-<9^E%Fj`*m)j}Uu21f(bf;-&&mU1wcx_Q0#1b>@iS% zcKgR%X+OncOQfS{UjhYra}G~jz1-+oZPFY$`nR2$7`eAC`Q5x9`2sYu6p@j3Fw#_5 zZmyn`)mF|?nAPSgmwey`3_YVI@fBKKv4PL`2Z<~#4e9N7w*qYv^6W!yK|Uu2)VrD+ z#?3D6l?UK#Ol|YcSOe+oi7V63GE7G@8?lAYF>TV`|D;tRCJaY@GhI_`P>NS%jZ>`| zBhkb&*TiXaIO-D+jxg0lM$6FS)yyN!Hcw^*gsx46S1<=u)~GbIXtnh~#=(QmqBmdH zo`;V8J)-rty;-B9oR#kX>;WQh(kxzAV#XhZu7qZQvK|PX(_ZN#bE7ybHhTpw{xYos8szEdo|1Y;;t*)VvwiDwh*!Jorm^e8$ki54zF zJ5E_{Wy*+?`DAK}lS_v(bg=PraBG~l^W&XhmB_|m&}!j};8W_cJ>zT?!BX5+ugO6p zG8=lqz*)IwXAiV6p3<)cuP&&+2fXg`2ILZWO2rFbEdwOV=V|SlZ7}cT4cB-vRR62; z$#DlJO6(mk4()|~39>mgaab(5{Uy z<1}ADp70t4;!sgG+`!HCl}aliDl(WGMLY$bktar@8L1wS%hrrfD4Dwa%H2?%U;`4q z8%jlt8Ipr=u8rRN&PaVLx;XD0e?iZ^=6s-diiE zCVr!p?aR3&-_eo#gYxg~^(%|jRIT$>o5=iok`~H_T^iGI`jIB25MP)XA`NS|tTQpp zPF3FM=6e}VOOW$*wwWe9%1>bs&kRH=RVp)I7un26T*625O}y$tYt(HE@%=e!CJ31r zEtv;nf6Qn0tI&7MwJdW|?qD~HKFGuLNP~)x9RslvT4}WX9_2#8`)OH^VR5#tUiqsf z_53zNWr6Qpliag6V71uTwpugIEg|$i(hZn%5|3f2AFGZ?V_ROos461VSjjEe+n z9~SR)nPK+ICm(=MEvwJkRcAQGt83d6=Dy+qS<=nD>^w6&?c3XPaK1XP+Rd+k4>hM{ zbfH0NwCk_>B)$wSear6L{92}4za{^umeNyYYl8nm3yPsg)i}x^2`2{O)*nzRn)Yv5 z{mgH7C&#BUEJ^2bv3Kx+d?eab;;;;uNp~F58&WYPp#;7GXtjQbUq(bGsy|~-=QPASf=M}RV|R*?Bz>f&$s30v|8>=_jFMbO<<9U@g#^KL-+IK$ zihaD=yQDmZiBz4&IN41;Wt_Ov#0z$Qc)ASb$>pNWTU-^ zudc7bsxxw=;KQ=N{t{n;hPE;jK6}g~b&HLw_(d50TdV(aWdl^gH3tgMARZ!9)J}sE zY2sKlB?lt3^Z5TyOIB)*8eAad9|N5<23pAeN z)b?qobO`g2M0nLc(tit4=6R+43gVX{q}k%5PdS_}7`6Ns$%rQxu(*9Stru<{XDrX5 znW6u|q&Zw@bk9T1BdZ0(-fMKa3Zr}~((ND6TB$vfl=^BFDwr+bY~A5<0agF>CfEMh z4LyR~Fj388IbB7j3aooDs#Af?C62XA5D^-% zR{cp%AYZ$u?IM#XV`3S^c+Et>r(zqVHk?%?ULK_vGdUn)i}30dvG?ugL#Im2QUAB3 z!2ooaDp{EFyc+(}v*lPseW>EO*WTq8>fsqQvB|Q}ht9ar(qGQIku+FtKlQjgo$|6& z?$aqgQ;iM6lQ~5yDN`91iXu%Fc~OIZZj#YKF>jDN{;HWOCrbga0e0@d#~ z-MRNoJYg!vV?ss03JSQ@r{4A6D~{J`RTFF=vm2C;8Qki>XF8QF(@X1jc_NovQ`%#> zySegz(8Eb*1`V-qqK655;iKM6C`8t_AM|Zp6&_tvtOi$bjo*3oK$Qtn&9g{)^`*T4 z<{3IKqu(N=iW-r*AU#r_@c|#j(u`yJ4EBI0jLiAOP8#ehAjtRaP>>JK>2b&hH@9Yx zE`fBJZO`D7CPeJ+SoC53hYL~IIw8*}_DmQb?k)b2nhCoR(LDhLkwe6Yr?423j~Xv7 zu}}o@!5=A)8Q_~~iYe!gHh)C7Z>9amRjA8*wum9ZRJ$$}{Zp+xd())Xum_OnZ=tXA zpYkk5;Q=&4U2i598dUhH%(Eg})b~ursWdolu=i=*jDa{T8Ycg!#-DK2pP7%3dZl^w zz)+OaLUZC1g?{ksTrl4voTdK*YE6a!;0gvWk*0oB2YjGb+ub-K( z#vqzG4Rjxfd;-vUFEkS`9{MV^kN~#M-8AO<`&p3^nF@A=RS?7dJ7*wHNdlmhIPf#q z;SGtckj$DB1X74_h8i0yDH(JwF8MuKJb4dr-J1-_&kz5u0x2KAXGr<-_jB7ZxbAs2 zzjH@D_q^0Ek#HFMBH{z$y+ufB0;-qhSL&3xDk@muFXv`*T3fCIje2C7`_R#dgb}iI zahfLo`8btNFmPowqSD*UTVDD#bAE^E2lnUqCuxou^u#{x zUdug0-DE;+jPVmecT0Z1j4gcZ?+^3^Mt_|;`1dhv9ff0`bLZX|-Nie0{NM+Lhvs;A z1|h2R1|o7J(~z}If%2t_p&{@Q8Y7rGqq!}o!K5-EebXPCqgh0kr&HcBN#fKb1=r%; z&7RYoxQA|*3+bjiWuSiDOXJ_AuN^ZWN;q-&@v*>hoT7ZwV0@9IXYo()#L1`rp!Fy; zxa{G#$l0=jf`TA6raDl6k%k69scmhFWc*H67Yx+5kS;D+248%7hQ}&#afQ*`Nh)iC zX&=1%$#LHe3{f>%(EK`sMRn}&4;405)0q9izmH+%)5GwrHL+9Ucpm%vBmDbPU!~iv zAaEsWL%;!6xqqJoI#=G$p>nt%# zDoDxK=U-D6TZh_oDZUBD2VOp{Ip)?BW0OU%#lmk3dE_=T8oI5GTIeAE&49?8+LF{w zM8=lw$>Ji>Da=Q&;@N;}oNB5pt?Uz>rk_ac;^IPNSKC{Zzdb4=!p|`N@FWGD{(``r zJImb{f({>wir;Ilcg6Xhj9d#%`Dr&-*N`EPxWdabHt8%Z$`@PE1Y7@}mhHCJuV0TV zp8QT8)-)T-t|TkEMPL%cZ7Gr9zU(*r)VA#oykB%(l+PPK4BPf@KJ7U1_ZR#9B(-jA zgh$4AG%$yE|M&6ZRj*{6BDolzG$b#pS9;ro8s1rMTsB4as}?#>k=au^@^>Ll(WdVC zcb2^@DYC>)&km;V7*3g|u%QlJoY3V0iEQ;-XyT`rc?^3_)*9j*8X6m}S7uQIp&NGu z*0T?p(hF2!O=miftR3%|*9Y92_uqPgOL_KvkUw>k(`=WYNfX+jRR0PWG`=XB{GDcC zVyJ+|@aWZmc@<}}TgshV`ZvXjMnnlm^9d&1g?1FM55zgLJ;fdDD(JHk&1D{{D+U*b zG;tUg;A-CQjv*WjgM1y){?6u$&+o) zb&>@C$Owcfv4gMhibC5qCp0@SdNYu?&0sg_`YwSdTaNxHy64^JGeu+}@CD_S$!3i1 zhktYJi7cp7ht`uw0wc(LYMO-L-<5A+n>0s@2M{qcz7sf4#`RcINSp_oq6}GwQ4eaV zda(T&o3u7wY#eS4uql899D6|P@xvul77yR-<*f7l_l2!%k9W5mVc)sA0HRlI5+_fe z?%dg2$$~CgFBVS_lLf#Z7;Szd>=8wCzaVGFJsjh<%V0VccBcc6{~Z)WE!SkYrX5Xp zv{r@lu?>xk%t3*s*m|%;XK>t!`sU5R5%(22i;h$&8@b0r-pF!l%W_R1@Ybw!!&&z_ zFnG`3h~31uK4AZQDbL@yW}mHueTmg_E*bCO8l3v~EFI z8ij|A;SC$yW@gYw$)x$mJ+Wr2&$&rMCyfpb`Xj(hn#exOu^o)*M`Pq6nv&2L{yq0= zoVyYd9ig-`h)FnXE>B!RZEkHTf%#2CjU|nUS!?|Es}qYNa2B}z(bOpNX3Jr6{z!7P z7{N;`N`HKv?)cv&@_-E<k$GR*cP-KG;pgd=_~!b- z-p+?S52#RxktkH72N>Z(PONmN5C8J3=&{dMg%9cy4+g+S9v&dZ;}{$2$m+ew3Cf7# zB#2vRvYasFC&kc}*VtEXd9qnPrJS?z!8383A|Sy&yqFJiO8jD}IW(+ul5nx(ai_xm zEqfhl_>GJ$xgi@)k@<4AkOBN49p2N{k1dTwGweKhU4vwAgHZY78F?H20c51J;Ud&HO;orvf&)v5jmHC==$OwoKhqqU}h)y&eJ{rx9f!?Ha*5u z^8g0$2c11Bu0fB^UX9h^+7JeZZ?`snq#6$ zPoMO8qN0fVh*7CA=FS~~Wvf?V|DKI15g#V`^+%?oWf_bt%fed8f8VG@yd3mX`}A(O zk5q5ac9k7_gdt0Y^1%6;z9Br9$G@zy-c=E5wGh89E*JT1do=FHp&3UJ5#f^)hZD_m z9j@~*lO6BKzCA?{cOvs=tQn8DCjDo}x_qO8)b@#bLTMRHQ1IIMUv!UzSeMs-)382L zkl~tNo9kr=Q@N6n@les#;#3-)Y1>-h9M5#4>aUb{mv^m#4Nf2JO~)(48 zBJWwUA8R1st!b+0SR|O{WL1+zBQ4da$homPrd3G0TQAg?AxYD5+m+`x*}c(p<29Mw z1@&xv$!j_uMC3;oI)-x}Hh4?96Gvy_NAUZbDttI4{$nmIh`FrA72SNvHh1%#O~<1% z0U1;Bbhcz;xm{nMdW{h342*_m_u-S=jg|WlX)%*6x^|3Fv%)O7AdU0Ge=hSs?j3f> zhz9FyoDfBKR|c$FK5!jyo z7TkK~TZnZk5L?^WO8MZY9;K&v8+(k`sobCcae1Nd`ShEiDB3Z8Uv6}z zKg=(jBxX%0{9|YpBdU;-o|)U!h?O?$+Bl`g+Lp-p+)K*qEylrB2RoF%uqWl2d+}0} zTXR|CKbO=JUFXC}y7O4OiM+J)w^4;mCF9;){;xpt^!&{qa%z^RD-7=k9_=Y7>F^wbJ;r}wrH6DJ z-+4fC*6++YJmTqiVOV!xe{Z~Ndsn5J{;Xhe;e{70gGu_3_(-Y!JyM0IpQ;G2WVgO# zo7})MnYO}`vds#A%>5#}=X~nN2X|zhc^<9)T_h$nc(#0gp}arECEpyo`zX5o6Z#S> zjeXBFYY|OCc5SGWMkvdtJ6&l(LrdFW>vQd0LWVAMbUZdpAJK z)IIj!hTHx**kQhxVAJF+W~KaA4PAOI=w48B3d@8x@gY}MCyn=qkl=<~o7xY;AK9?; zb!5}f-^^$CEJ;!fZr~6;6V`Dc*!=TEaU%Noi!!pPgFT{*(<}9l1B(J$GAZI&{WtP} z`r*x@2kYhZbs0*cvK-y-)4Uhc&lspXv$R)>yi2ZC{*s5jdRsq8GBI#fHlK1z1VjFE znv(41flvRK3Cos!#^Hx7K}7zQdG(cAfhtng#PiGGMR39_~%dRA|?rplT(w}9Gv-` z5i7q{iwxc_N+V{xa?w;I;}He(Tq9v^|3b06`4x3q;iwW4)Z9L!iji2W= zaf%jy+HS;W5tx*JxcdO7gP~%B{&>-8uQB?^d#VHvjYZMSPZd65nO>6o!J!~k2q!=@ z=`M0IdXnPmREA53K1@|TiBn{<$~w8~fkk9cB7)be+@rz6k5jb2acN}Y`Ny;sh8>~B z+<|v#jHhgCEPki)mytCn73ql8v$h3TbkzmXP%esHIyf|l;Vhb?ZX=N(Mvm0a=h?8Z zg*{>O{95->7dcgl6^!DMjraQ#*nKEk2?b`uU>iqmom9X4>|=w_PX+<*F!v`fL>kz7 zdm@`{Y^Vz^$OSeK_~nce-rYOCrL`6^tT%sfu9qPJ$Kp@Rrf{6kHw90^Rb3%wR%ea( zq-ddo4ZPpGT?iQ}G;&a^Y!Pu5!6Dugd3ybvG!-{(m^?+0M$hS}b^84W35IcgUFZm{ zqIVHF;|gitwuy2CNUj{7MO^Tp+yqY?5hrE%(>L^W9Fpg7L8Cvo>O{!7k<5Ab;@>yQ zM|Sd@so_T>@Ws}t^jO=Qz25@v#AQwrX)*YI_z=JvRCwyXjOi8Cj+pE&#t|I7X1agQ zI^wAh`N4Iamw6`7U~Rz*sE_V>IR+y~uIaDr4R+(k;l$ZKN za*A^Omv7G-RWI~>9U4mxyFq&`?gL67%jr6*FZC=NG6N7gCfED8TyJmC&_J} z6UXuDBj5#e+P`}@Aa3#-F?I@M79U@O+pu^CCHW)&ZTR)oCkk<@F$aRLx^mayJZDtX zqioOe>s7w-TPr)-uri&t{>vAoB5@&P6yQe@ncB0h7c?Mg1}F69IilyDn#lQ?W6EAI z4>vcVe|F$!dhLK;7o)FiX6gWc60|7NSAY|{OaGeSXGg=WwixN~^KB<1#{Y2nSBK_W zWufeGT-?OobiXiqfcRfzUVa904vLKBgS<32R*?JUcu)(&pt4u2FiQ)V-uOA6nXb+#Wwc6R3S5hp+N#N zuQ8a0k}?6$-?~V)hRNLMCK3X}STzS+XnBK77pKc5`@d&yFTBc})(A=7ANX%(!F7^; zFYK1bHCCbV*G`=IjZKw7+Kc5hzDZe8(9p zx|relDjd(BJ8`@evDpQk#ga=tY;z^~bv4LiCDy|iiAb;Uu|dCyqEohnO-(T$G1FY& zvPp3h7^q_pjExww(12q|chdWb3Wm??tM!H2(}z*A_%T@GIE#1TefV9-?N1&@fg6?c zX#XQ$9WuAKBd2kZ#~BJjR?te>b_?<6-0iIqRZaz|#>CD#g05K6#y?^DAqs^p3fnJf zCBn15-+Rckb@~6%=eQHdG7u0s} znf^OXL$DAvo!35WHIeT&_*(z`iY=ZW zEXrPN428dd!7`XEHY5%XU8>+uzI3JS3M7N$69j}xBFMDAWkvGBXnY*3lzD&YcR}5C zs=ic;YRo;aM81~bHm@cJ6R9>eFhkV zNniEsw~0j}e9b7&JnNZ6=u4N&{W;;nA!m_Cc6fN|a{7tqay;8qz+-(X^7RB_G?A^t zzxY=eKoKSv$)g>^!9F~>m>I~>@IXw3}#5i=MPd%=X z!!yO+@Iu7s(;2M|zBYnTtt>Z`Z8)?oNLb-v;v^vE|$wQL_ONlNdk zTuipKv=m6ku3lJf?sKm71r5J{L^{|R>lZsC*&gYg{93g8&D&+3%d5Smkwzc$!~G6= zrsD|{pN`?-SYUdgmbK08xcDq6I74~g^w_mjWuBT=M#9^Q)+Gr!_ocnYtnEn1h!H5 z9LJk3beS~PW~B)6O<{T8^X_KmfQ`R%o%h88y^-75?#}V~62?HKH5=zE7L8JVpX&px zldmJIf#`0aJBI#Ly{7=^E6)iyL^NmW0-J1LP-o|q3t5G$Uah zmzOqGwUg;ppaOXU0PlnM1>rTvNoT+BuGb!2?zcWaKAm=62gXUAgC09mE7uK~LZBB) zpjlsP#?o~pI?iY#;mKat5l9=Q2~GZ-tTphxBvFVhWd^>|)6n{!r3$~cK}_RIHcS@3 z2VqBF9BGT1Sw@naiGPbe+`EbEYDz4P@ALN*hDC;fOEW>TnD z#7E1_{PA>qQpCWQ$3TER?bVv|=J=kHx!kt_pH-?JPAeDGeoJo2-u!`3Y(KjDT;z+X zVX3oUjcDSTbZ$Q`QB(dL1~RYC#Vuc`M%jJ95HZ9x#UUgV1vc|ij>+5ym4v4z%ZR|y zoKRq27^8MA9ciT1*1Z2wOEC;3#}9s1VzI`s);X`KH?j~{$jRf4)Zboqr*e>gqL%ac z<)k|YIKUZRr}g8jNepg?==IT@Kzal`qELXb_qgRDEwT1pej?J+gs#sGJ653wd_(X;9cPnM36qvL#a}HmePIC}R zPbbo=7KkBX+=}}8-g2&mJJYZjodRc7r`n&mct?wi#+J(+G!{3P9E_|&ubtOcBm67b zZ|760=Wj8>Ki_5HF-;9<7?ePi^JL8JjW2yXC+?ab>Zw(H79-~I{=m9r)3Q#AAWYVT z5JA~vmZ4&0^%$~_w!nU8$}PU1w+#t5ZIXzLp_e%w$nqO)$tQe%-{@2#kjNe2XG@yd zH1NI8feVbnZ12)8p8<$!x4f&9J*}Lm#nYv;mjj=4r-B2Nj%*_F-8CxpfSCi+WbQe; zOq6Wp@yf=E2rl%|8!5VxxJuz4gnzfuS@>Yoc^teZ5V(}yoNK^m(mIUPL-)CK$pV$p z!4!L6DPZGOY(>Gbn0uOnoGF{=D*+^N7^B5nI(cL^W5w~azMyXROX)jbO~Q<4lx~SE zP&@zTt*V}=i1*d0vgvJ4DG#ar@@v6|KMKEkM+S%^DlK)M8mTi!gx0*So>fmYC>uJP zzx!Efxid{?%VH3fz1q8_Y;62s`kmNlLF^LO6sXa56+Ryy1gdQ)c{~AEV~`5h>9<0A z@8ymcQ%!f~RBk{_f`Z%2aB^xj3Ke~b_(c8Ww8D{64Q2x@s#m9K>f%YWb65ren@R$h zfxgM}9GS8z8(IT8{qD~#?W#Nc$>wW)vlYXfWyWibK0?r`8c-ZF5v(q%QeIM=V9~qe z@j3WKvmHBKs8J@kIaOslU36f}>okB4V$%RI(0}8BIJKdKkvh4h(8cn@C>bV63_%Yj%>oaHo6<@LxCG#(=xO9v63sx-Y=%mHv5~)$O7x}bY|%r z`+!w{E)eP8!_t+VPN3p8%W(9e@tXq4=%0)oBh^hy_vsT+qH-tXLOKZQ6`~4c#RMy(HzlIeC^jL z6HlgpJrcNwDzN?mV@~}{tMJ9`u%cwV|5TBNq$|d^e2prbXY;X5FfF3fTMocd`o!0(Hxv3AH-rnL9f_dog@plGx=mkXelQA28 zi<9qG=7B;W0bQV)_*0C$1OcjghHrpc6g(t5kQ>a2Ldng4J2V5hgmk+d$NE0 zCU`k3Tb+;7JEa+Gj^5Vs4RZEd80^PC@Ju5Lm^nK$7o2J=uH6Y4g_gs3ZbsMVOA4iI zQ_|w{fb=L?`57tZ;K1>?jS!}~qsBu!_;slrL!~7*#2@SYszFWoEKIkd zTuW#PGK`S(ojixti24nu{oEJOnzCh0(mC!B#rMtaLrLuU!YRAiOJd2v)7-Ym6sM`x zH8s`fbBnGxlxwmJ)D;MbH4Cc1O;=p{_Fyc$w1l~<3!_DYVU=QxT$V$+C{ zR0CuAEfVC(^Qf}No)f~sc<~IdZw0+DG&54i6BsSki{9!eQ##WsL9CmpMntR40F5=f zw5aTI!tq%K>OEl=ljSTMQGC3EQQRl)GH&+w<{AEJAD6<)&~$=?u`3H?MhSa`dr)ybJW(gl92D5jl&k7%ms7Kt!q?;< zkFSwT~(1QNYM_Cv$0=ld30P0io9-z-rvEJpuQIV@8q$3_ z8WsY?u)m2U1ALzX;Tn1%GrbQi)LrC_0w>jq5*%{3h4Gm7mT(%ZD|Vgh(TOivh5YriNXEKErwMIIea|_C zBGN3X=hFqIoi-OUq+oj3z@j0YgzuE5(%KycJ*u-BJ+qRMPnxYg!B08<5p$^Zb2Cx@ zOa74JW0+a5)nJXYId{wH*Tu3)MkNU}0##MZ$7HVcwtVUqLq7N$+yKP6#G`xt{OeSMZ!r z9@UzyJHr^I<}y-O2%8|p$1fsTDAoLyl*5&=fILA$^n7YO-_NrH^j^&w5!J_hV%l9| z5PF#^a7C>B)sP1=wF8;LlSl#~G_lvoC<ByFsuOHw;c|$DxqHswK*m}!p6!*Y8p23B@(JYIFubs+Ncvr zb@MSeW3eN33R;>rg3{C2n{oF7Py~&z0?y9nF6NlQDOtQ06z;2)ac`WwY=I?wYve7XB<3EgQd%2*#gCkTc|BqJs^8?5SZm*_qjx3 zcUgBn?NMs4IFN`=mjZ_C5~Vv8*i)%HC^I1a$!Lr6`LcB!wNx29o~C^1xiJ*99hf2wl2gkfF*Kr%{?u}BT2J0N7Y1gvD>K1;dwLLML{-)@k$jsW&(bE}n=2U-uZ zZXqhP;lKgI=3e*-O8jb-PP<}VB`WjX=i$XhyTmGWNrh~_O5U6rl!76V;d~BN8NEVW zF85tIwW85JA_22Cqtp&(-NfB>&iEjlvi9_(xNTspLJoQmcG=aaX# z8hPlpPwH16SnFonsZ@~uu1r8CE6ddG+>uei2sl_3DXXed`r*sG zK^?ZUuwetF3!NVH=6&!fWQK+h%$}N2P5=6cW_?jzVf&HAylw@^N`EuzvpDkP3i6cO zN%N3vMfkK6r#baHEZSV<`ZY}a4y&27c!CV$A`d*W14wGIP7Pt~A2LEU<${gTX>&kE z0q85H+W1cVa;JJYPC(qy$q9?YQaQ3zH#EOD&ID*!3L1Pf7`tiWYt3AI@I|d0$^*I>2f`pQw3aV@vaRDTC_U4E;AQ^5ens2E%T)`O{`s;VyM3g;l;!I;%#~D&#r%s*>M-=<*c*~o9dr;?^ zZuqe^L+FzI-GetGNv`Zl$L1CwJx1FReD;@>@CoBVAD-DS1nE$Q zBDTG{GE&Jr#>!kYIjNAfT04xGYs2qHeY+L!E~vwQ9SSRNLONS@Y-4dkOxk4-b{}|M zZhR(kJrdMwAn2vLAyAU*!3A6@j9D*h*%F@4u92i93wjmm9w)|Xq%?5qsVK*2J-PAE zS4hiMe&zcQgk{<@Ggp5$w{`u{vK(q%{kC(Qv#L8f5f}-!yV?su78afYXvy1a{|CuP zjWd_RS^Xe+#K+*fZeuq+tgOrC0BlvLswD&&_A(fWWWB49uKExx|YGfVFSg=+U$qV0sVJgla#$y<&~A{;dq)w6murojZWi?q zr}FF#{S3X!I#{C7xg0nmM22~tqTBpA!hNt=Kd|x{3fo`aUZGv{-Q1bR>WT=k7;$d7 z-%xt(R90Vj%FNAsh8Y*d>Bc;I z-~0kHL0#;c5)t`a%2E;Rjg*wc!e*R_Ymh1VC2SE-lnn5YN>< zOSus#PGtZ|rg7oO5A1`py{70Gv030MkPtL>rqxW8iI%tq1RwCD_k==zv#QFumPyKy*tXE;Y3y`wBhkqhJBKrTDW1d1Vfuj$7I^Iuyk~%LZ8j z_%5Z|C%Cgt-6a)qVCiQkZYz@6^$VkC9r6KL}?7Ferc= z+dAE?K;+dMa_X0=r&a_{q?@g_Wq2W^&?X99lNt>DP3O@K@A>4_gN%4Ckkp#1^0r>K1uUgt> znDf@Y4%!Ql!ZPN_TDmSIk`^1K_21fm&@KB168ON(Q>Yd7t99X{nL|F`Utzt>&KCMD z#YGX{uXg#)d4A~g`d+`+Ga6HKduBw043pT$vT=D1f=Fi3$N>2E1v2PRfRSn?Ppxz- zv0IJ2nSz-kN#_>ZkQck{OsR~L<7N$eVHCOss6rG3s0z?Y1@oJQ zI4KEMXWb!=VT+HTcBRJ*pIbmv%$&~HdBgk1zC5{{m~0UFlw9@HFmC#k+?Ut=erEd* z=X|XUZn$={!1@n z{&Q@W((pI^lmN)<*MdzI9Zh9;9?fYa|5|?Dg32PNRMlzh0G*UXM%mExOPf&3|2O}8SvTSksPgZhs46v1z_KrIRn z6wWCsvi_^vvpb}^HB{%PEjC64zX|@rt`wLK@vKE`pOJ^LX`&oAXJY^gfxF`nrD1)a zgOgK2d^7}^-r)PuR7Djzo|iXaQB=W5e|$*tROMEY#h3b3y1^n55fM;hnymlAv!iQe z6&98mkMncPxVYi|ib%bfs9Lcm&p1-&VXN%WOC&@@D0}-4HV)%Ue5Kd>mpo>!HM>ymGF| z-e7lHM&;Zm^)AM+o!GDJnsB_m);b{)#VhO{`(}cV$Caq!WMGSBekl?*K#^O4QR zVA>j8_~E04pqEoxeM_OGNO=`nrB=KSc^Phbbx0rH)6!}(nl-ciY43xg;G{u(YmE4G zxv)WbXy^j3{haYHG2TymGQ_+yU0NL*R znR-^o-K>o`o5nMVk!L_sHA#rs;t8gM2Bw2K*vcZ9=cxL%~iTHpLgOX&=`83^(VPn8u~w3E{KJsHhU zJu+m`Tnz(&t1n`aqDGkn5aDzens$$k-58Rk*;B(+CCf!&cJWAL$o-)3g*?z{p?i;UXdcOY+?ii$M8Cs&l1=GCx z+Ewbh(2N2PAjOk#bKof)h$FO3^-{R(@p_Ywm=nXSy5FDKnZht{L8a_{qG68S1M(h; zbcG6Ug)$`3)@$%4MtQ!X!iV?2`f?!{zh6k8@-NH+iqD(Bu)nrWf(zah*2_PHh$Af4 za@Of4Kb9!%S+QXBo1$zZQqN@)JhrAhQs0SoS92Zh)O-|}_bHK53)GFAdi61fwR#E` zHa55T-t%}hF>JGWFYBr>oYAQ+YY^(HU3};l)al$K?cgw0pR%HQ&Tgb${soTF&~v%& zF9>nl>Ff-Us8^ z!yam4`Jza1eoxsF%axH3685R@n(K#Y@LU{1o0z(dZvLZCeh0(D#C-Uynj3zBzWEFr ztXdLqY@QY@Bs~X(NRAv0w!bANwEH=aVanygVxyp*rJ2o{$aM97s|!Q@JrucUDgmH+ zYoHi&|MGOY^0=34T8+|Lzh(RS0w$epLnG&*c9W>{v-mhg@2Ose#eIkliA;_}L$gxd zaGrE}O0KqkrBxwR(tATWPKNZlm#b*wvm~N3<=%tt%15|nl^0qNg)!dgYI}U3g!;VI zZkDGu7C7m>>*=2pq+!)NVm)43vOaEb=Dd9HNH(`r{vq)$STtEGhRO$+!jY4UCefe& zE;?ON;WChX<9^0^0IE6eXvh(ZcwjQr2Bok`KV=2pEPzsv9pM7hpfGg*5avHym^{ie zwIrXd5)I3?h{Au1x=M4g$P+1c&}K^y+44+J{k)ZUY)=$eEOn&vM!0$ZZ3M6YW0R#f z@x}y5Zt+SZ$pTJm^k6WluX<+4Bnb0?5hZ&pg|g7fakt{~g(bYf597159H5jeAgBgn znpO5T%;SEc_6^Whd$^sH<(sa-8zc3Uy@=$fWKF$@3kXMHBb9S0>k8S&Nztfy-uEN)8l~zDEtv0;$Pl8V5K!YhV=>WgCfQt zP!ftrONTBmEr}W#;i}2w6m~DJ^Bz8`yaQcQ;K7I0NWr)AEs61H(z_>VyeH8zlS^xB zemSe$cS7K<2TE|<4)s0-3QUrI>QPsm~D`|R|L1iwI=mG(rB9>n8 z774c$Cxo5%atSSKm-cIK_ufia^;Ow=ztXG2YyJG~Xal%{3VryX#L4>~3HpRY@1`0~Ln zWslW{S9#r6uM_S1FxO{jq=Kar2--}&13T63Z!E*gr;~h|Oxn%ryH2@yov9UWo_6_? zT%Cd?R@whZ63kCsMZoDO9uXzv+*Z<`!6_uCC?BR@zo|6qRmQ=`mrZ0gUA}5$P{L>p z7Rt|lRUSPL>2D=kIiT|N_BI!aFS|KDb}1Ec`?!2{kFy<~0eR%F|4}~81o>kIP0FW? z4K@dy7431Sr>FDiQnlDdlQdW!EV^#$>bKay!8iZwG`GFmG*H?|HUC+RXA2E2ntJu| zh3Lmvf6M>XfmL~p>tGy||25S|E`SyB(Sgl3dYxn>iqZ_|r z8W2v$&~b?Va7&)D;@)@1F{2ulU+WXt*c|PyFF(3xJM%q?^6}aRL~;n&pBOOSMn>?r zGpJkvhJt{YXt1EWC?Q$T+|$pqh)>OSjDLuL1jd-mkkZJl*?;c%=8W*%XaQ?;7~+Sn zJtbOsJ!b$43%>h5bl7dDWaE;P3(46)(k%r@H_PjjP3y4!)$&a34+%`uNv);VMCV8S z^WXbPh|sS6Ww??utOxQ9!N!3Xfv^3Se|DV#A7lXJrsSw`P2SAK#l*;+Yz8TTT0TpK zV9{oVRm*m%Ua)Psf3fB_DJGe^i&Y)V>8gh^>!ZNBN%5|EwI(1Jb9bb$=0UaJog?&3 z)=Pk>Y}<Khht4 zecNdceLI{htzx-7k*GTT^C!YBx<)LZ3#f$S*>a!kLl_@(y;-Z+vjcyKB#!KRI@zu^ z2?-`45ChJFE8Eog7NgLzbHh{fi z)-_9JhCm~95ekVXX7_80p`QtHaO6(5xN^Psb`pz-omcBc%Nlt_#3#@R*yA2Nc;I<| z4&P;=S+Mex-S3w8P3?=QiLBNV&h`zHX{kQg8A7us9#s=&1b;Rv2kc;?koblv*)q{B&0 zmPval@Q;Zc@eFOgSlX0O&%v!HJ|3;<%Re#&CL_(S)Gdjij;`Ln{{QRbH&%M z`6W+$dl~foe%}%T0N4E}`pcY~%JcT0x>-QBbXa3R$IUG^Ng?F598gu&sd>t{PU9H;tqN7$^H*&3bD`Ku-D?h0dr(fy7Hjpb9u_ z1Zr2+&yrt)id{OX@SK??jXfZ&hl}O z4CXWyA!|I(by|T^W)~wv$EHE}ZF#(&<+3>Y-Hj_4M`d{Bds>-EFV~j6` z71uN`u|+Os-Pa0_Y9_xqtr!Dv4uABF*;2FoEu9%>DlqXhrqjD`r2fLClN5k3KO#i_ zcNLl$;017Y7oE+HYuzA&e3=h5OPcZBT%LMnI=5r0_@n@I(Et92i{L>TN})JvMH1Pg zwSM)Dz}KR(&J_w3+FC$}U&io4o!J9`q^VBp-o9^YgNAoq`UpbqWa!&KzFBbTB^i~v zLYw9bO=I`iEX0I!0~l>rkm5!lJZ{^Hu5t)t6&0!{qVn5cOW7ENvU3p^f>1Fx+ruI# z?xZ)fAE72_k&szenk^%JZn)4wSdm& z1FlAi{!QA7FSqk5p>E59=@#|o4IqHLaa}W&y!5!e_$|{M?^A;zD;C1f>a&{6mT+nH z#lPNm+E@Q=e7ww?sqn82u3PS@kG(RswW|O9oHj)H2H4Oc%niwp3{Lf$Z?)Y-qF|ByU~ByGBodT4Vo}84COe+LZ%T z_Q8!|8np&2=@aK`4;;yFM5-;t(a{)Xv<(^pIPplZ@~Kyh#pHla%ILL`$3|a%GOXC7 zTo|2~AC6o=qd>G|F+!}Hk=LNjcG*F3R)CxaDsno4W?xvLplJ2IczPuru_lJoS-*xn@1osdj^i-DVW1|(e@2=+^~ z!>H``bBqcf&ZuS`C21O??bH&xeLyjebmTP@*c^TlADP_z{Ri-~i5}1UJ zuJFKZsx3hJDY?SARuJ2;Il)jCv9vzH>#~+M8^2!Xb)q4|o}d~OjGm@?<`-Juuv5kR zuE|~z#EE)(kQ>YSTRdLRLj*|<$%3jWM*(nNjefv4eE0|6i1n`n1~9~i{nzZB(r4lCcg^Jlo0PCMF9Zce8~O0~FDN)ShtH!WK7#9FT} z)_79Bz{m$UtyGhZjZ266(welh?S6J1I#e4p?T!G9sr1`nLzsFzQbCN{epwSaQ;`bQ zYd65*BntJ80Gz=hLDH$Dv6kXo@zj?qk8o>>@-7Djcs%P1@fANg6dKV~54aN&I*V1kYM;m!#Swn-TtNhv>z zA+!yXE&^+atG3BXzpIRuf$k$n{r z2rFakO`j!NQJW$oqHX5Po$cK{gIrw6HQ?8^M|g(iE8Yp`A&y-3+=5Didx7%fsrM{A z^AJV+sRnPpaVj0V#SFe@_LH z(U{!aPu(!EDs4VvX8+^2k_N9KaMsCEzFfV%B31tUEg(Py@AnWdRH9Cz$b@|3skqH3 zxz%!gE?TK-r$OG%=MJ4o!Rvk7()pK4Cjt%&v6L8g_Kh|4lIwGwuQw1!C2lQ@3oBW@ z$MtsB6AgS-AJ~D&4(#io5~)t^L{8D~?9zLcn%%*V}D^ynZ=#lE1SBw zehh z&z_ja>7z^{vuw=V1t^}bXwpI)8R^O^~llU81%WGhw{i5lP~R(D-c9ui4b+vNiRgH|bw%Pfu8RBDJJ zLmIBUSMx2%ygTWy~R#htF$uzk08DPE`BAX9q> z9G{N_4xfx`Zkbc`bon^81e!~e+nEl=%^WeP7yta+bT#U8l4ua6^HP183hwUYfhRt% zVtTK>p~1Fo+)XC_ao)V2P>vhOwR&95kq#!xoXnyEQr*6nLW$vt_ejIlPU@p2CbhXt z>DbMYtk$#&rxkZ?E=uy_TFw9%W@XlGt6_meKA?q zlunM>q*HAt3;E@z@oSH$d4c7FSl>@o;pPQ|W5yntNOdOc*7ZSbwK&K} zZ&28#BVQ!)xxJvrlZOo*gTyy-w@jP#0@p0h*Q@noyc%_gXz;C)>s+N^g+}24b4%`s zHAsTZ``(F;ngg#Bd=N^dB>d8d`J$C=cEr%5kRg~n=X2(&=FKVOIwPi9V$j4N-ssBR zRYhaoo0QcC)qOl2op=;@J3rv8vLY zuru<$7^n)AY=Na&jWB|4Xwv6$6aWVP-b3hh7Gtx1cf1dD%ozIqXz(=GQ0~Dl(A*5a z`Z_fTM~%PGUcO=vh|7J#>%%D0o8{suM6c1=KX4Rq@HZ*`*ZyD|f;*<2CP3KddSc~- zvt5#!u%GFs*;7Yc931pI2p-=@bP-~(U(#;Ad)ZkOe2v$jpjxB;^ill&%W*Nb zf8+_(KorLbxw5C!1-cgX(Rb6|9l0bcD=J+w#oGYC`!R$q)tHCbzSh;cmk!5IX@eB- zcV$5-l5=e*5?J__?TfBkLaS!8#kZtn6=~@RrmJk_faiQ7mFXd=#bG?-(j;NG)6g;?sC=3Ekwk+0EW|(kFW#eqWWQL@LzPOE-GrtlT?st< zlVl37+DXlMcI=W=g0u>;r?W&=V!Y>_`y(NsKf^3_Zw58FjwB$po~^HULqA$uSUfjv z%Dvo~gr#BGxbN`xeE!Z=wDhGzt56wNR>s)2#R>RRMM^1i+fP*l6cmuOi5fWmLuc;@A`wf@TG zHV1;=1Vn&GS)v7tnAdwYwM$pHEJT$_w0g$eaxSIFTdPdNsHQ2~PT- zsM_NhHM!DlSEiX+05V}NhnM-t4-j8uY8ai))Hqm;6<1+hQ9@P9U%s9Hi0-3_@^Lu7 zq5Z6KPyyG>bh}^ZPBU*WUTTweUk>bRzmF%) zkEE>T1oxZoQ4MwZzFqcIBVPcyuw%nHL{$e<4mh}C0-ziN9*OHqjfUe360hL2krr7P z6*AqOFZp;n#<2)ou46NAAX>SN#+f$guLx$>{2_V zVX?;mGU86B;y+F(Q|;x^a4ie`pP}lXx7(L4^`&9-cPo;`NC9MheQ;hnTU#s_)TWqy zKk@bF<7K$%0nif_kO_avgMw{kYr^Z^_8jo9mX zj&9K-fUp68SX{e&`BltWIpAC93nZnpTha^tEQLm`a3=6t9)gFE-!+uOKzJesui+$= zi0pD?yel7J9(3JOcapgcE^u7Rf9J=$>|&%I@#$(eSK8$KjU;bAtM=fl2% zOu%aRHIKym-8&+dj|5_bTxNON=qPkQTGn8?Yd+6doHldG4HW_ZAvxt(z+?YIYS3Z@ zFSY9|)q_fz_)V+3c2f89bDr1Y1zq=f49fIHqZWuIeXazvr!?=L1FaOZ=K!PRN|+XT3WQl&yW}NUY_kyA4Uq4RJvJXI7AUl zC(%i+gMrGNUgc<21*N?$s_Zl2tyayy5%UJEfIZaGV zbEd_Dh9gg6Y@;(!s{=IQQ6daym;ue}1MrOt-gYVz8KuOxuz3@@&jJgb()9Lv5n(@s zWBQ)W=(rN`nAe&BFQH{H&P`aIuTmS+cR3c1;+8K*F4j%~1&jTbyvI|eUh&@M9_LN< z6Lg<)^cbeZYOT;rOiWadOWtD_7(Q>X_P3&Jq7Nf&n<5TNS^SufyT^v=?5iWC0P!TQ0wWY`8s_ zZtLblL%};!pyjP7p&=wqkyyK18LiQ^SLY1PXVRPFvPSaz3y=yo`-2-03|O+(48ZS1 zK+FuhN*q|>l|aNziCO7}*^49dqT@awNdTP|g4exHLAh#&S zf=&M2K_%h4E?BI~jNl?o9Pb7Q%5i|>*5rFEQSTRIRHzBeH>u2eVcEVnBw4_}amdc4 zEZqVaZN_pLqW;OD|N6qAj^P1Exycq-Ydj+5)9#H>idG(hgFA9V*~GIY+6lt&;?B_L zF1M=Wc@Q%_%`ZWdPK*Qpi$xDVmD!_z%w@w*WgM?R{yd+-u<*v@Umd%GM3MGI6#$0W zy^g~@^Hs-CaY2>w0AU*x0`J{lg8wmp#*f7oZ~-p>bonCyTaAsEl3i9hBEwh^YIEh~ zNlMvVTiWniql>4ml?VS>4|;*!=}+uc3Kw&#<+1^`d2=MFOgCV@NUfG66A+kvYFu2o z^VF=wasi0qfnq}#wAf8zLT0MqcnRK>#3_kQKUY9FCY=ihy?Eu>J{eR~;M42Z4M#o= zW2^#dKPTVHM96CdzwW+Jeg+Zw&lyPqUVNfWf1efw-ybn?29baYf&$#x6+!&i2sC5- zKMI=u=u1!;bKAg-E4u!7#vRjF#6Q=y=6uPYj8$rSqLp2Nf!c6CXcq(jA6G$t22lKS zrLv7sn}JWcY9v;mck=(o$G{9Om->?j!HXQ8@WGE}@*2Yj4|#crRP0C=kQ?};m;OR>Y7fK4@Y=BDdS$C&z8pB%L@|4E+ z)vAVcF{)CrH-AW^$>t}?7FtDQ#>}Z^L4`c8JfJ?;|2k+1fXY#kWLYz{IhYf`h-)1m zm{?@OA3E;Y6d~M@A;$~d4PHqDrl?@nTnqX?Duurij#RcV)ISH5X;j>3l~eq+Rubr$ zOML5fz&~Erkfhe6u1uv-j3?aa`_!Bmoq+i7&~n4w1%eI5Xb|ByA&YvSOtt{5pnGZq zV%fz1`d|EAa6xb{y06O)X@lThb9`wxv`Ur~Ji3LO0X1;8-vDE(lA!K-BI{l@xahHx*s$hB9R zy4>IbwH;04uN3mBNYXFJzZO0z@6tCm#_8ZT2kmc1-^F)~9>(8v3Jtg}t^@D4u*OeR zk7bFcyEq=wl1*G>f9=!@-}p!I4S!uo55?X8+kD6t)Zxz zMgtMPsD%687oMX0kIjhyF4erCzolF(DB{{;?;=U<{qEFD2INt^juWFtzdZhqR2$rg z#6$4IuON8fu)pm<1?H*=d;&8n9_Jf?2Rzv#)coT#<^_I3eUIe5PZazEl_&=lzda3U z_@eOncc|Dn>?4cbTr`L@81Qr|L{<=;4rcDGXVjsc8*#EPoYa^8F$)b?gufY&3lo=r z&0qj2oa|40Y5w8WD*VR(nhrL{!=D_6t6zEd<(d*4sOZo6_ zE?N&-0>i18yt>Du4-4K}l6DKu{qyF89pM|5?lPO5BYe9f8es!ong6}cSMb*?1k6hOY0#MisCZBZy84V$I!$-3%=MYt_-`m>rm5g-}gVavP@%c6XKnG;N_6rO~$=0mXBA5b0 zJ4x)w>%bsKORx5k1Wy3Cpb-{N{PQT(@LvqgVUvH)8jcb04MoaUD=O*NzgBT%pWxh% zqjr>%9=P!h&vl3vFu8&+k`MT7!T%cI{QXQrxHplvszXl%xaF6@B0F9PIQ$0zmQ`r{ zE6fG7e_PFT*qP#(s&i+Fr&IX|436WM_|gAeIDBB#x6t+5h&aF}0(U4tQK7X1m*LY7 zpUtbmzhxME+n=4}2!6B(W*AHS%bGxwG6c^3K-B53)Z}{uipsb=W@Op6z#p_w1)>1= zHzitg{0nXYN?3rf@@w9-3%<`?swmVLin(bHNEvloM6ZTkZCHst7`1sS*La)08+QK) z5fEBX0>al+XkQ3^fIBCJinBgMNEIb}K^-)FWcnu3OQi4ly@tPgO;Uoet{kr(cdS?* z15%1xRMInKAjx1YiM>4{_|-16Eu)5>>}I>5GbY#>fgQ*_uNaRKjvwMYWn~JOkPThlT9b~q-n1X2 z4%8a<-A;YQh79Zb9rll9bdMw%TRJIv#sW*gwV~$ikuYo4Me26jtJ-wwxR@k~Lx=BH zX~KsX$4({$NxWcnq$BG_$z`ph(k+uN$_ieSArc}dh5FC9$1V+PemXHS!VACaS=Ir? zTmU`lN(*P3Q79Z7g*D#^lnrRGW-%xkGGb$W!f7iE-KaKC?(iKpfBynHc5^*OLCFHv>4kBfzP@(` zHO`mEmfA40NT~gEwm2$~nWsT5893}|6w_^nN#SZa_`l5WD7n<;L-FB-aZgiDg1&y= z`EgFRK>Hvjo9}xdS3GlnOE9|MGPhOwI&jbgfphNIReX0LkhL52pfp=O?FjPcns)|k z@P#rbz99FLKdI3tzXu?k+Vl)!q+g?ynhfD;s>-K z!}c1^NB~bj;e>_UT1MuvrkGOI~I z{}-zqZu0}}zc+xqyu!Sn#Pu4Oi(~>L2C;^A(!VG5|K;ZZ-15xpD5P&^FmMobqo@19 zURQH`@22;&0hcie|mw1=}WphB4oz{_) zfhD3oWA0Y~3+yk4`R#Aa`47B0 zG2>7R=c`@7Q}2HPPupJ!EUvY#ZhRBJtK{Uey?bSSCE9?eL-F-Nk9EL|LL`wFpr+m4 zD!1ijb1e#+fPv^<3LGon|NmdK>#nHTzyS_<(3Dl)T`OyG*A zFTiv5gPYlSUjS2k$+I((qORGQ8P9=h=&b_^U`G{W4_Zh_JA4NP@IBXGMgc*B?X9gB zfrAPe$9g19>i$%8H%^>5@d|K!P;~=v^UZVMSxEgWiTTNT>9XV0E1{|>csXP%g# zXj1UN0d(B|OW^7F;Y_*n?P^18BiGI~nPvx^L0GYFGH^sfSK|X4xKKfhUNr6j2cTo5 zPDEB2mVSJ6)GI`^8o12>bY^-$X2o_A+;sdb!LURZv_W)=ao79=aD#LQ%231h522Rc{Il2Ux$S0W`)!+L?2zXwF zldL~5bprGErB6>!Yg$^)1kRGY1m5_!99W24EIx0WzSB%mu`#aZqpRwzQ`I+5P1pAa zPCzZlyu2)Ms#fR~U`@lt{EtA!2d5f|9$SotO!ok-P)utxD{Az1-%7Ml;v9X!N#d@=X6j7nUDf(TJ*lLy}<`A zx$!FospkNy2cB~+0bX$csbcYwqvqoeQVPT8!~Jh>X0R|YG$hWbzkW5aqj}<7Rfh%! zMkW>x0R=S1Ii^Eze%jvV#E?Kz8o|M_xqEhx3Yu0V6$%aybQI3ayT5rZc3U7y8(SL; zcJDapfz2Io!$uu68bVl8D!FN>XZx*K1_q|{o-U3dI1C<5;G;zVma=g$mr0Dv>?g4u Tc<6A40SG)@{an^LB{Ts5b6nz6 literal 0 HcmV?d00001 diff --git a/docs/content/secure/secure-api-access-with-jwt.md b/docs/content/secure/secure-api-access-with-jwt.md new file mode 100644 index 000000000..5e853a637 --- /dev/null +++ b/docs/content/secure/secure-api-access-with-jwt.md @@ -0,0 +1,204 @@ +--- +title: 'Secure API Access with JWT' +description: 'Traefik Hub API Gateway - Learn how to configure the JWT Authentication middleware for Ingress management.' +--- + +# Secure API Access with JWT + +!!! info "Traefik Hub Feature" + This middleware is available exclusively in [Traefik Hub](https://traefik.io/traefik-hub/). Learn more about [Traefik Hub's advanced features](https://doc.traefik.io/traefik-hub/api-gateway/intro). + +JSON Web Token (JWT) (defined in the [RFC 7519](https://tools.ietf.org/html/rfc7519)) allows +Traefik Hub API Gateway to secure the API access using a token signed using either a private signing secret or a plublic/private key. + +Traefik Hub API Gateway provides many kinds of sources to perform the token validation: + +- Setting a secret value in the middleware configuration (option `signingSecret`). +- Setting a public key: In that case, users should sign their token using a private key, and the public key can be used to verify the signature (option `publicKey`). +- Setting a [JSON Web Key (JWK)](https://datatracker.ietf.org/doc/html/rfc7517) file to define a set of JWK to be used to verify the signature of the incoming JWT (option `jwksFile`). +- Setting a [JSON Web Key (JWK)](https://datatracker.ietf.org/doc/html/rfc7517) URL to define the URL of the host serving a JWK set (option `jwksUrl`). + +!!! note "One single source" + The JWT middleware does not allow you to set more than one way to validate the incoming tokens. + When a Hub API Gateway receives a request that must be validated using the JWT middleware, it verifies the token using the source configured as described above. + If the token is successfully checked, the request is accepted. + +!!! note "Claim Usage" + A JWT can contain metadata in the form of claims (key-value pairs). + The claims contained in the JWT can be used for advanced use-cases such as adding an Authorization layer using the `claims`. + + More information in the [dedicated section](../reference/routing-configuration/http/middlewares/jwt.md#claims). + +## Verify a JWT with a secret + +To allow the Traefik Hub API Gateway to validate a JWT with a secret value stored in a Kubernetes Secret, apply the following configuration: + +```yaml tab="Middleware JWT" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: test-jwt + namespace: apps +spec: + plugin: + jwt: + signingSecret: "urn:k8s:secret:jwt:signingSecret" +``` + +```yaml tab="Kubernetes Secret" +apiVersion: v1 +kind: Secret +metadata: + name: jwt + namespace: apps +stringData: + signingSecret: mysuperlongsecret +``` + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: my-app + namespace: apps +spec: + entryPoints: + - websecure + routes: + - match: Path(`/my-app`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: test-jwt +``` + +```yaml tab="Service & Deployment" +kind: Deployment +apiVersion: apps/v1 +metadata: + name: whoami + namespace: apps +spec: + replicas: 3 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: apps +spec: + ports: + - port: 80 + name: whoami + selector: + app: whoami +``` + +## Verify a JWT using an Identity Provider + +To allow the Traefik Hub API Gateway to validate a JWT using an Identity Provider, such as Keycloak and Azure AD in the examples below, apply the following configuration: + +```yaml tab="JWKS with Keycloak URL" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: test-jwt + namespace: apps +spec: + plugin: + jwt: + # Replace KEYCLOAK_URL and REALM_NAME with your values + jwksUrl: https://KEYCLOAK_URL/realms/REALM_NAME/protocol/openid-connect/certs + # Forward the content of the claim grp in the header Group + forwardHeaders: + Group: grp + # Check the value of the claim grp before sending the request to the backend + claims: Equals(`grp`, `admin`) +``` + +```yaml tab="JWKS with Azure AD URL" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: test-jwt + namespace: apps +spec: + plugin: + jwt: + jwksUrl: https://login.microsoftonline.com/common/discovery/v2.0/keys +``` + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: my-app + namespace: apps +spec: + entryPoints: + - websecure + routes: + - match: Path(`/my-app`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: test-jwt +``` + +```yaml tab="Service & Deployment" +kind: Deployment +apiVersion: apps/v1 +metadata: + name: whoami + namespace: apps +spec: + replicas: 3 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: apps +spec: + ports: + - port: 80 + name: whoami + selector: + app: whoami +``` + +!!! note "Advanced Configuration" + Advanced options are described in the [reference page](../reference/routing-configuration/http/middlewares/jwt.md). + + For example, the metadata recovered from the Identity Provider can be used to restrict the access to the applications. + To do so, you can use the `claims` option, more information in the [dedicated section](../reference/routing-configuration/http/middlewares/jwt.md#claims). + +{!traefik-for-business-applications.md!} diff --git a/docs/content/secure/secure-api-access-with-oidc.md b/docs/content/secure/secure-api-access-with-oidc.md new file mode 100644 index 000000000..f7b18def9 --- /dev/null +++ b/docs/content/secure/secure-api-access-with-oidc.md @@ -0,0 +1,110 @@ +--- +title: 'Secure API Access with OIDC' +description: 'Traefik Hub API Gateway - The OIDC Authentication middleware secures your applications by delegating the authentication to an external provider.' +--- + +# Secure API Access with OIDC + +!!! info "Traefik Hub Feature" + This middleware is available exclusively in [Traefik Hub](https://traefik.io/traefik-hub/). Learn more about [Traefik Hub's advanced features](https://doc.traefik.io/traefik-hub/api-gateway/intro). + +OpenID Connect Authentication is built on top of the OAuth2 Authorization Code Flow (defined in [OAuth 2.0 RFC 6749, section 4.1](https://tools.ietf.org/html/rfc6749#section-4.1)). +It allows an application to be secured by delegating authentication to an external provider (Keycloak, Okta etc.) +and obtaining the end user's session claims and scopes for authorization purposes. + +To authenticate the user, the middleware redirects through the authentication provider. +Once the authentication is complete, users are redirected back to the middleware before being authorized to access the upstream application, as described in the diagram below: + +![OpenID Connect authentication flow](../assets/img/secure/oidc-auth-flow.png) + +
+ +To allow the OIDC Middleware to use the credentials provided by the requests, apply the following configuration: + +```yaml tab="Middleware OIDC" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: oidc-login + namespace: apps +spec: + plugin: + oidc: + issuer: MY_ISSUER_URL + clientId: "urn:k8s:secret:oidc-client:client_id" + clientSecret: "urn:k8s:secret:oidc-client:client_secret" + redirectUrl: /oidc/callback +``` + +```yaml tab="Kubernetes Secrets" +apiVersion: v1 +kind: Secret +metadata: + name: oidc-client +stringData: + client_id: my-oauth-client-ID # Set your ClientID here + client_secret: my-oauth-client-secret # Set your client secret here +``` + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: secure-applications-apigateway-oauth2-client-credentials + namespace: apps +spec: + entryPoints: + - websecure + routes: + - match: Path(`/my-app`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: oidc-login +``` + +```yaml tab="Service & Deployment" +kind: Deployment +apiVersion: apps/v1 +metadata: + name: whoami + namespace: apps +spec: + replicas: 3 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: apps +spec: + ports: + - port: 80 + name: whoami + selector: + app: whoami +``` + +!!! note "Advanced Configuration" + + Advanced options are described in the [reference page](../reference/routing-configuration/http/middlewares/oidc.md). + + For example, you can find how to customize the session storage: + - Using a cookie ([Options `session`](../reference/routing-configuration/http/middlewares/oidc.md#configuration-options) (default behavior)) + - Using a [Redis store](../reference/routing-configuration/http/middlewares/oidc.md#sessionstore). + +{!traefik-for-business-applications.md!} diff --git a/docs/content/secure/secure-api-access-with-waf.md b/docs/content/secure/secure-api-access-with-waf.md new file mode 100644 index 000000000..455839b99 --- /dev/null +++ b/docs/content/secure/secure-api-access-with-waf.md @@ -0,0 +1,190 @@ +--- +title: 'Secure API Access with WAF' +description: 'Traefik Hub API Gateway - Learn how to configure the Coraza Web Application Firewall middleware to protect your applications from common web attacks.' +--- + +# Secure API Access with WAF + +!!! info "Traefik Hub Feature" + This middleware is available exclusively in [Traefik Hub](https://traefik.io/traefik-hub/). Learn more about [Traefik Hub's advanced features](https://doc.traefik.io/traefik-hub/api-gateway/intro). + +The [Coraza Web Application Firewall](https://coraza.io/) middleware in Traefik Hub API Gateway provides comprehensive protection against common web application attacks. The middleware supports the Coraza rule syntax and is compatible with [OWASP Core Rule Set (CRS)](https://coreruleset.org/docs/), allowing you to leverage proven security rules maintained by the security community. + +## Basic WAF Protection + +To protect your applications with custom security rules, apply the following configuration: + +```yaml tab="Middleware WAF" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: waf-protection + namespace: apps +spec: + plugin: + coraza: + directives: + - SecRuleEngine On + - SecRule REQUEST_URI "@streq /admin" "id:101,phase:1,t:lowercase,log,deny" + - SecRule ARGS "@detectSQLi" "id:102,phase:2,block,msg:'SQL Injection Attack Detected',logdata:'Matched Data: %{MATCHED_VAR} found within %{MATCHED_VAR_NAME}'" +``` + +This configuration implements three security directives that work together to protect an application: + +- **SecRuleEngine On**: Activates the WAF engine to begin processing incoming requests. Without this directive, all other rules remain inactive regardless of their configuration. + +- **Admin Path Protection**: The second rule blocks all access to `/admin` paths by examining the request URI. This completely prevents access to administrative interfaces that often contain sensitive functionality like user management, system configuration, or database administration tools. The rule triggers during phase 1 (request headers processing) and applies lowercase transformation to catch variations like `/Admin` or `/ADMIN`. + +- **SQL Injection Detection**: The third rule scans request parameters (query strings and form data) for SQL injection patterns using Coraza's built-in detection engine. The `ARGS` variable covers query string parameters like `?id=1` and form data from POST requests like `username=admin&password=123`, but does not include cookies. SQL injection attacks attempt to manipulate database queries by injecting malicious SQL code through user inputs. When detected, the rule blocks the request and logs detailed information about the attempted attack, including which parameter contained the malicious payload. + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: protected-app + namespace: apps +spec: + entryPoints: + - websecure + routes: + - match: Path(`/my-app`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: waf-protection +``` + +```yaml tab="Service & Deployment" +kind: Deployment +apiVersion: apps/v1 +metadata: + name: whoami + namespace: apps +spec: + replicas: 3 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: apps +spec: + ports: + - port: 80 + name: whoami + selector: + app: whoami +``` + +## Advanced Protection with OWASP Core Rule Set + +To implement comprehensive protection using the OWASP Core Rule Set, which provides battle-tested rules against common attack patterns, apply the following configuration: + +```yaml tab="Middleware WAF with CRS" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: waf-crs-protection + namespace: apps +spec: + plugin: + coraza: + crsEnabled: true + directives: + - SecRuleEngine On + - SecDefaultAction "phase:1,log,auditlog,deny,status:403" + - SecDefaultAction "phase:2,log,auditlog,deny,status:403" + - SecAction "id:900110, phase:1, pass, t:none, nolog, setvar:tx.inbound_anomaly_score_threshold=5, setvar:tx.outbound_anomaly_score_threshold=4" + - SecAction "id:900200, phase:1, pass, t:none, nolog, setvar:'tx.allowed_methods=GET POST'" + - Include @owasp_crs/REQUEST-911-METHOD-ENFORCEMENT.conf + - Include @owasp_crs/REQUEST-949-BLOCKING-EVALUATION.conf +``` + +This advanced configuration implements [OWASP Core Rule Set (CRS)](https://coreruleset.org/docs/) protection with anomaly scoring: + +- **SecDefaultAction for Phase 1 & 2**: Sets default behavior for request processing phases. Phase 1 processes request headers while Phase 2 processes request body. When rules match, they log the event to both standard and audit logs, then deny the request with a 403 status code. + +- **Anomaly Score Configuration**: The first `SecAction` sets anomaly score thresholds where `inbound_anomaly_score_threshold=5` means requests scoring 5 or higher are blocked, and `outbound_anomaly_score_threshold=4` applies the same logic to responses. This scoring system allows multiple suspicious patterns to accumulate points rather than blocking on first detection, reducing false positives while maintaining security. + +- **Allowed Methods Configuration**: The second `SecAction` restricts HTTP methods to only `GET` and `POST` requests. This prevents potentially dangerous methods like `PUT`, `DELETE`, `PATCH`, or `OPTIONS` that could modify server resources or reveal system information. + +- **METHOD-ENFORCEMENT Rule Set**: The `REQUEST-911-METHOD-ENFORCEMENT.conf` file enforces the allowed HTTP methods policy defined above. It checks incoming requests against the permitted methods and contributes to the anomaly score for disallowed methods. + +- **BLOCKING-EVALUATION Rule Set**: The `REQUEST-949-BLOCKING-EVALUATION.conf` file evaluates the accumulated anomaly score against the configured thresholds. If the total score exceeds the threshold, it triggers the blocking action, preventing the request from reaching your application. + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: crs-protected-app + namespace: apps +spec: + entryPoints: + - websecure + routes: + - match: Path(`/my-app`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: waf-crs-protection +``` + +```yaml tab="Service & Deployment" +kind: Deployment +apiVersion: apps/v1 +metadata: + name: whoami + namespace: apps +spec: + replicas: 3 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: apps +spec: + ports: + - port: 80 + name: whoami + selector: + app: whoami +``` + +!!! warning + Starting with Traefik Hub v3.11.0, Coraza requires read/write permissions to `/tmp`. This requirement stems from upstream changes in the Coraza engine. + +!!! note "Advanced Configuration" + Advanced options and detailed rule configuration are described in the [reference page](../reference/routing-configuration/http/middlewares/waf.md). + + The WAF middleware supports extensive customization through Coraza directives. You can create custom rules, tune detection thresholds, configure logging levels, and integrate with external threat intelligence feeds. For comprehensive rule writing guidance, consult the [Coraza documentation](https://coraza.io/docs/tutorials/introduction/) and [OWASP CRS documentation](https://coreruleset.org/docs/). + +{!traefik-for-business-applications.md!} diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index e1ccb61ee..9f72b634a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -193,6 +193,10 @@ nav: - 'Kubernetes': 'expose/kubernetes.md' - 'Docker': 'expose/docker.md' - 'Swarm': 'expose/swarm.md' + - 'Secure': + - 'Secure Access with JWT Traefik Hub API Gateway': 'secure/secure-api-access-with-jwt.md' + - 'Secure Access with OIDC Traefik Hub API Gateway': 'secure/secure-api-access-with-oidc.md' + - 'Secure Access with a WAF Traefik Hub API Gateway': 'secure/secure-api-access-with-waf.md' - 'Observe': - 'Overview': 'observe/overview.md' - 'Logs & Access Logs': 'observe/logs-and-access-logs.md' From cff924f4fdcee5664a9dd50e111b29c6e2f3e0c0 Mon Sep 17 00:00:00 2001 From: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:48:04 +0200 Subject: [PATCH 02/12] Fix broken links in documentation --- .../http/middlewares/distributed-ratelimit.md | 2 -- docs/content/user-guides/docker-compose/acme-tls/index.md | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/content/reference/routing-configuration/http/middlewares/distributed-ratelimit.md b/docs/content/reference/routing-configuration/http/middlewares/distributed-ratelimit.md index cb4484779..aa718680b 100644 --- a/docs/content/reference/routing-configuration/http/middlewares/distributed-ratelimit.md +++ b/docs/content/reference/routing-configuration/http/middlewares/distributed-ratelimit.md @@ -113,8 +113,6 @@ The `sourceCriterion` option defines what criterion is used to group requests as If several strategies are defined at the same time, an error will be raised. If none are set, the default is to use the request's remote address field (as an `ipStrategy`). -Check out the [OIDC + RateLimit & DistributedRateLimit guide](../../../../secure/middleware/drl-oidc.md) to see this option in action. - ### ipStrategy The `ipStrategy` option defines two parameters that configures how Traefik determines the client IP: `depth`, and `excludedIPs`. diff --git a/docs/content/user-guides/docker-compose/acme-tls/index.md b/docs/content/user-guides/docker-compose/acme-tls/index.md index 382244c62..942d17cc1 100644 --- a/docs/content/user-guides/docker-compose/acme-tls/index.md +++ b/docs/content/user-guides/docker-compose/acme-tls/index.md @@ -6,7 +6,7 @@ description: "Learn how to create a certificate with the Let's Encrypt TLS chall # Docker-compose with Let's Encrypt: TLS Challenge This guide aims to demonstrate how to create a certificate with the Let's Encrypt TLS challenge to use https on a simple service exposed with Traefik. -Please also read the [basic example](../basic-example) for details on how to expose such a service. +Please also read the [basic example](../basic-example/) for details on how to expose such a service. ## Prerequisite From f9f825163a70e51ee95bf9ce14a8aefe0c205c62 Mon Sep 17 00:00:00 2001 From: Mark Ormesher Date: Fri, 12 Sep 2025 08:44:04 +0100 Subject: [PATCH 03/12] fix config examples in entrypoints.md --- .../install-configuration/entrypoints.md | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/content/reference/install-configuration/entrypoints.md b/docs/content/reference/install-configuration/entrypoints.md index 62adb7c1b..e7badffbf 100644 --- a/docs/content/reference/install-configuration/entrypoints.md +++ b/docs/content/reference/install-configuration/entrypoints.md @@ -25,10 +25,11 @@ entryPoints: websecure: address: :443 - tls: {} - middlewares: - - auth@kubernetescrd - - strip@kubernetescrd + http: + tls: {} + middlewares: + - auth@kubernetescrd + - strip@kubernetescrd ``` ```toml tab="File (TOML)" @@ -151,18 +152,20 @@ are applied after the ones declared on the Entrypoint) entryPoints: web: address: :80 - middlewares: - - auth@kubernetescrd - - strip@file + http: + middlewares: + - auth@kubernetescrd + - strip@file ``` ```yaml tab="Helm Chart Values" ports: web: port: :80 - middlewares: - - auth@kubernetescrd - - strip@file + http: + middlewares: + - auth@kubernetescrd + - strip@file ``` ### encodeQuerySemicolons From 27a820950a316451da3d52091cdfac7138e4ee8a Mon Sep 17 00:00:00 2001 From: Nicolas Mengin Date: Fri, 12 Sep 2025 10:02:05 +0200 Subject: [PATCH 04/12] Reorganize the menu entries --- docs/mkdocs.yml | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 9f72b634a..297c90f26 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -129,6 +129,13 @@ plugins: 'middlewares/tcp/inflightconn.md': 'reference/routing-configuration/tcp/middlewares/inflightconn.md' 'middlewares/tcp/ipwhitelist.md': 'reference/routing-configuration/tcp/middlewares/ipallowlist.md' 'middlewares/tcp/ipallowlist.md': 'reference/routing-configuration/tcp/middlewares/ipallowlist.md' + ## User Guides + 'user-guides/crd-acme/index.md': 'expose/kubernetes.md' + 'user-guides/cert-manager.md': 'expose/kubernetes.md' + 'user-guides/docker-compose/basic-example/index.md': 'expose/docker.md' + 'user-guides/docker-compose/acme-tls/index.md': 'expose/docker.md' + 'user-guides/docker-compose/acme-http/index.md': 'expose/docker.md' + 'user-guides/docker-compose/acme-dns/index.md': 'expose/docker.md' # References # Static Configuration 'reference/static-configuration/overview.md': 'reference/install-configuration/configuration-options.md' @@ -251,7 +258,7 @@ nav: - 'Health Check (CLI & Ping)': 'reference/install-configuration/observability/healthcheck.md' - 'Options List': 'reference/install-configuration/configuration-options.md' - 'Routing Configuration': - - 'General' : + - 'Common Configuration' : - 'Configuration Methods' : 'reference/routing-configuration/dynamic-configuration-methods.md' - 'HTTP' : - 'Router' : @@ -341,24 +348,16 @@ nav: - 'ECS' : 'reference/routing-configuration/other-providers/ecs.md' - 'KV' : 'reference/routing-configuration/other-providers/kv.md' - 'File' : 'reference/routing-configuration/other-providers/file.md' - - 'Security': - - 'Content-Length': 'security/content-length.md' - - 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md' + - 'Security': + - 'Content-Length': 'security/content-length.md' + - 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md' + - 'Deprecation Notices': + - 'Releases': 'deprecation/releases.md' + - 'Features': 'deprecation/features.md' - 'User Guides': - 'FastProxy': 'user-guides/fastproxy.md' - - 'Kubernetes and Let''s Encrypt': 'user-guides/crd-acme/index.md' - - 'Kubernetes and cert-manager': 'user-guides/cert-manager.md' - 'gRPC Examples': 'user-guides/grpc.md' - 'WebSocket Examples': 'user-guides/websocket.md' - - 'Docker': - - 'Basic Example': 'user-guides/docker-compose/basic-example/index.md' - - 'HTTPS with Let''s Encrypt': - - 'TLS Challenge': 'user-guides/docker-compose/acme-tls/index.md' - - 'HTTP Challenge': 'user-guides/docker-compose/acme-http/index.md' - - 'DNS Challenge': 'user-guides/docker-compose/acme-dns/index.md' - - 'Deprecation Notices': - - 'Releases': 'deprecation/releases.md' - - 'Features': 'deprecation/features.md' - 'Contributing': - 'Thank You!': 'contributing/thank-you.md' - 'Submitting Issues': 'contributing/submitting-issues.md' @@ -369,4 +368,4 @@ nav: - 'Data Collection': 'contributing/data-collection.md' - 'Advocating': 'contributing/advocating.md' - 'Maintainers': 'contributing/maintainers.md' - - 'Frequently Asked Questions': 'getting-started/faq.md' + - 'FAQ': 'getting-started/faq.md' From ffd01fc88ab7f83647798094d5fb5171a4380bfa Mon Sep 17 00:00:00 2001 From: Matteo Bongiovanni <40599507+MatBon01@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:50:05 +0100 Subject: [PATCH 05/12] Fix conflict in IngressRouteTCP documentation --- .../kubernetes/crd/tcp/ingressroutetcp.md | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/tcp/ingressroutetcp.md b/docs/content/reference/routing-configuration/kubernetes/crd/tcp/ingressroutetcp.md index 061ed1615..47d595e1e 100644 --- a/docs/content/reference/routing-configuration/kubernetes/crd/tcp/ingressroutetcp.md +++ b/docs/content/reference/routing-configuration/kubernetes/crd/tcp/ingressroutetcp.md @@ -57,33 +57,6 @@ spec: ## Configuration Options -<<<<<<< HEAD -| Field | Description | Default | Required | -|-----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------| -|
`entryPoints` | List of entrypoints names. | | No | -| `routes` | List of routes. | | Yes | -| `routes[n].match` | Defines the [rule](../../../tcp/router/rules-and-priority.md#rules) of the underlying router. | | Yes | -| `routes[n].priority` | Defines the [priority](../../../tcp/router/rules-and-priority.md#priority) to disambiguate rules of the same length, for route matching. | | No | -| `routes[n].middlewares[n].name` | Defines the [MiddlewareTCP](./middlewaretcp.md) name. | | Yes | -| `routes[n].middlewares[n].namespace` | Defines the [MiddlewareTCP](./middlewaretcp.md) namespace. | "" | No | -| `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions. | | No | -| `routes[n].services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). | | Yes | -| `routes[n].services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. | | Yes | -| `routes[n].services[n].weight` | Defines the weight to apply to the server load balancing. | 1 | No | -| `routes[n].services[n].serversTransport` | Defines the [ServersTransportTCP](./serverstransporttcp.md).
The `ServersTransport` namespace is assumed to be the [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) namespace. | | No | -| `routes[n].services[n].nativeLB` | Controls, when creating the load-balancer, whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. See [here](#nativelb) for more information. | false | No | -| `routes[n].services[n].nodePortLB` | Controls, when creating the load-balancer, whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is `NodePort`. It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. | false | No | -| `tls` | Defines [TLS](../../../../install-configuration/tls/certificate-resolvers/overview.md) certificate configuration. | | No | -| `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace). | "" | No | -| `tls.options` | Defines the reference to a [TLSOption](../http/tlsoption.md). | "" | No | -| `tls.options.name` | Defines the [TLSOption](../http/tlsoption.md) name. | "" | No | -| `tls.options.namespace` | Defines the [TLSOption](../http/tlsoption.md) namespace. | "" | No | -| `tls.certResolver` | Defines the reference to a [CertResolver](../../../../install-configuration/tls/certificate-resolvers/overview.md). | "" | No | -| `tls.domains` | List of domains. | "" | No | -| `tls.domains[n].main` | Defines the main domain name. | "" | No | -| `tls.domains[n].sans` | List of SANs (alternative domains). | "" | No | -| `tls.passthrough` | If `true`, delegates the TLS termination to the backend. | false | No | -======= | Field | Description | Default | Required | |-------------------------------------|-----------------------------|-------------------------------------------|-----------------------| | `entryPoints` | List of entrypoints names. | | No | @@ -111,7 +84,6 @@ spec: | `tls.domains[n].main` | Defines the main domain name. | "" | No | | `tls.domains[n].sans` | List of SANs (alternative domains). | "" | No | | `tls.passthrough` | If `true`, delegates the TLS termination to the backend. | false | No | ->>>>>>> 9c932124f (Add anchors in reference tables) ### ExternalName Service From fed86bd816be1ecb0a7d25a61dbd18f40f28cac3 Mon Sep 17 00:00:00 2001 From: Harold Ozouf Date: Tue, 16 Sep 2025 16:16:07 +0200 Subject: [PATCH 06/12] Refactor plugins system --- cmd/traefik/plugins.go | 41 ++- .../configuration-options.md | 1 + pkg/plugins/builder.go | 10 +- pkg/plugins/downloader.go | 160 ++++++++ pkg/plugins/downloader_test.go | 159 ++++++++ pkg/plugins/{client.go => manager.go} | 343 ++++++------------ pkg/plugins/manager_test.go | 341 +++++++++++++++++ pkg/plugins/plugins.go | 31 +- pkg/plugins/types.go | 3 + 9 files changed, 828 insertions(+), 261 deletions(-) create mode 100644 pkg/plugins/downloader.go create mode 100644 pkg/plugins/downloader_test.go rename pkg/plugins/{client.go => manager.go} (52%) create mode 100644 pkg/plugins/manager_test.go diff --git a/cmd/traefik/plugins.go b/cmd/traefik/plugins.go index 2c3d5dd25..ef939d2a0 100644 --- a/cmd/traefik/plugins.go +++ b/cmd/traefik/plugins.go @@ -2,43 +2,62 @@ package main import ( "fmt" + "net/http" + "path/filepath" + "time" + "github.com/hashicorp/go-retryablehttp" + "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/static" + "github.com/traefik/traefik/v3/pkg/logs" "github.com/traefik/traefik/v3/pkg/plugins" ) const outputDir = "./plugins-storage/" func createPluginBuilder(staticConfiguration *static.Configuration) (*plugins.Builder, error) { - client, plgs, localPlgs, err := initPlugins(staticConfiguration) + manager, plgs, localPlgs, err := initPlugins(staticConfiguration) if err != nil { return nil, err } - return plugins.NewBuilder(client, plgs, localPlgs) + return plugins.NewBuilder(manager, plgs, localPlgs) } -func initPlugins(staticCfg *static.Configuration) (*plugins.Client, map[string]plugins.Descriptor, map[string]plugins.LocalDescriptor, error) { +func initPlugins(staticCfg *static.Configuration) (*plugins.Manager, map[string]plugins.Descriptor, map[string]plugins.LocalDescriptor, error) { err := checkUniquePluginNames(staticCfg.Experimental) if err != nil { return nil, nil, nil, err } - var client *plugins.Client + var manager *plugins.Manager plgs := map[string]plugins.Descriptor{} if hasPlugins(staticCfg) { - opts := plugins.ClientOptions{ + httpClient := retryablehttp.NewClient() + httpClient.Logger = logs.NewRetryableHTTPLogger(log.Logger) + httpClient.HTTPClient = &http.Client{Timeout: 10 * time.Second} + httpClient.RetryMax = 3 + + // Create separate downloader for HTTP operations + archivesPath := filepath.Join(outputDir, "archives") + downloader, err := plugins.NewRegistryDownloader(plugins.RegistryDownloaderOptions{ + HTTPClient: httpClient.HTTPClient, + ArchivesPath: archivesPath, + }) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to create plugin downloader: %w", err) + } + + opts := plugins.ManagerOptions{ Output: outputDir, } - - var err error - client, err = plugins.NewClient(opts) + manager, err = plugins.NewManager(downloader, opts) if err != nil { - return nil, nil, nil, fmt.Errorf("unable to create plugins client: %w", err) + return nil, nil, nil, fmt.Errorf("unable to create plugins manager: %w", err) } - err = plugins.SetupRemotePlugins(client, staticCfg.Experimental.Plugins) + err = plugins.SetupRemotePlugins(manager, staticCfg.Experimental.Plugins) if err != nil { return nil, nil, nil, fmt.Errorf("unable to set up plugins environment: %w", err) } @@ -57,7 +76,7 @@ func initPlugins(staticCfg *static.Configuration) (*plugins.Client, map[string]p localPlgs = staticCfg.Experimental.LocalPlugins } - return client, plgs, localPlgs, nil + return manager, plgs, localPlgs, nil } func checkUniquePluginNames(e *static.Experimental) error { diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md index dd4a0f798..14673995a 100644 --- a/docs/content/reference/install-configuration/configuration-options.md +++ b/docs/content/reference/install-configuration/configuration-options.md @@ -128,6 +128,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | experimental.localplugins._name_.settings.mounts | Directory to mount to the wasm guest. | | | experimental.localplugins._name_.settings.useunsafe | Allow the plugin to use unsafe package. | false | | experimental.otlplogs | Enables the OpenTelemetry logs integration. | false | +| experimental.plugins._name_.hash | plugin's hash to validate' | | | experimental.plugins._name_.modulename | plugin's module name. | | | experimental.plugins._name_.settings | Plugin's settings (works only for wasm plugins). | | | experimental.plugins._name_.settings.envs | Environment variables to forward to the wasm guest. | | diff --git a/pkg/plugins/builder.go b/pkg/plugins/builder.go index 8559dec90..96a4bf21e 100644 --- a/pkg/plugins/builder.go +++ b/pkg/plugins/builder.go @@ -28,7 +28,7 @@ type Builder struct { } // NewBuilder creates a new Builder. -func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) { +func NewBuilder(manager *Manager, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) { ctx := context.Background() pb := &Builder{ @@ -37,9 +37,9 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[ } for pName, desc := range plugins { - manifest, err := client.ReadManifest(desc.ModuleName) + manifest, err := manager.ReadManifest(desc.ModuleName) if err != nil { - _ = client.ResetAll() + _ = manager.ResetAll() return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err) } @@ -52,7 +52,7 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[ switch manifest.Type { case typeMiddleware: - middleware, err := newMiddlewareBuilder(logCtx, client.GoPath(), manifest, desc.ModuleName, desc.Settings) + middleware, err := newMiddlewareBuilder(logCtx, manager.GoPath(), manifest, desc.ModuleName, desc.Settings) if err != nil { return nil, err } @@ -60,7 +60,7 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[ pb.middlewareBuilders[pName] = middleware case typeProvider: - pBuilder, err := newProviderBuilder(logCtx, manifest, client.GoPath(), desc.Settings) + pBuilder, err := newProviderBuilder(logCtx, manifest, manager.GoPath(), desc.Settings) if err != nil { return nil, fmt.Errorf("%s: %w", desc.ModuleName, err) } diff --git a/pkg/plugins/downloader.go b/pkg/plugins/downloader.go new file mode 100644 index 000000000..3dce1df4c --- /dev/null +++ b/pkg/plugins/downloader.go @@ -0,0 +1,160 @@ +package plugins + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" +) + +// PluginDownloader defines the interface for downloading and validating plugins from remote sources. +type PluginDownloader interface { + // Download downloads a plugin archive and returns its hash. + Download(ctx context.Context, pName, pVersion string) (string, error) + // Check checks the plugin archive integrity against a known hash. + Check(ctx context.Context, pName, pVersion, hash string) error +} + +// RegistryDownloaderOptions holds configuration options for creating a RegistryDownloader. +type RegistryDownloaderOptions struct { + HTTPClient *http.Client + ArchivesPath string +} + +// RegistryDownloader implements PluginDownloader for HTTP-based plugin downloads. +type RegistryDownloader struct { + httpClient *http.Client + baseURL *url.URL + archives string +} + +// NewRegistryDownloader creates a new HTTP-based plugin downloader. +func NewRegistryDownloader(opts RegistryDownloaderOptions) (*RegistryDownloader, error) { + baseURL, err := url.Parse(pluginsURL) + if err != nil { + return nil, err + } + + httpClient := opts.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + + return &RegistryDownloader{ + httpClient: httpClient, + baseURL: baseURL, + archives: opts.ArchivesPath, + }, nil +} + +// Download downloads a plugin archive. +func (d *RegistryDownloader) Download(ctx context.Context, pName, pVersion string) (string, error) { + filename := d.buildArchivePath(pName, pVersion) + + var hash string + _, err := os.Stat(filename) + if err != nil && !os.IsNotExist(err) { + return "", fmt.Errorf("failed to read archive %s: %w", filename, err) + } + + if err == nil { + hash, err = computeHash(filename) + if err != nil { + return "", fmt.Errorf("failed to compute hash: %w", err) + } + } + + endpoint, err := d.baseURL.Parse(path.Join(d.baseURL.Path, "download", pName, pVersion)) + if err != nil { + return "", fmt.Errorf("failed to parse endpoint URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + if hash != "" { + req.Header.Set(hashHeader, hash) + } + + resp, err := d.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to call service: %w", err) + } + + defer func() { _ = resp.Body.Close() }() + + switch resp.StatusCode { + case http.StatusNotModified: + return hash, nil + case http.StatusOK: + err = os.MkdirAll(filepath.Dir(filename), 0o755) + if err != nil { + return "", fmt.Errorf("failed to create directory: %w", err) + } + + var file *os.File + file, err = os.Create(filename) + if err != nil { + return "", fmt.Errorf("failed to create file %q: %w", filename, err) + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return "", fmt.Errorf("failed to write response: %w", err) + } + + hash, err = computeHash(filename) + if err != nil { + return "", fmt.Errorf("failed to compute hash: %w", err) + } + default: + data, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("error: %d: %s", resp.StatusCode, string(data)) + } + + return hash, nil +} + +// Check checks the plugin archive integrity. +func (d *RegistryDownloader) Check(ctx context.Context, pName, pVersion, hash string) error { + endpoint, err := d.baseURL.Parse(path.Join(d.baseURL.Path, "validate", pName, pVersion)) + if err != nil { + return fmt.Errorf("failed to parse endpoint URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + if hash != "" { + req.Header.Set(hashHeader, hash) + } + + resp, err := d.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to call service: %w", err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode == http.StatusOK { + return nil + } + + return errors.New("plugin integrity check failed") +} + +// buildArchivePath builds the path to a plugin archive file. +func (d *RegistryDownloader) buildArchivePath(pName, pVersion string) string { + return filepath.Join(d.archives, filepath.FromSlash(pName), pVersion+".zip") +} diff --git a/pkg/plugins/downloader_test.go b/pkg/plugins/downloader_test.go new file mode 100644 index 000000000..bcbd89424 --- /dev/null +++ b/pkg/plugins/downloader_test.go @@ -0,0 +1,159 @@ +package plugins + +import ( + "archive/zip" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHTTPPluginDownloader_Download(t *testing.T) { + tests := []struct { + name string + serverResponse func(w http.ResponseWriter, r *http.Request) + fileAlreadyExists bool + expectError bool + }{ + { + name: "successful download", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/zip") + w.WriteHeader(http.StatusOK) + + require.NoError(t, fillDummyZip(w)) + }, + }, + { + name: "not modified response", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "", http.StatusNotModified) + }, + fileAlreadyExists: true, + }, + { + name: "server error", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "internal server error", http.StatusInternalServerError) + }, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(test.serverResponse)) + defer server.Close() + + tempDir := t.TempDir() + archivesPath := filepath.Join(tempDir, "archives") + + if test.fileAlreadyExists { + createDummyZip(t, archivesPath) + } + + baseURL, err := url.Parse(server.URL) + require.NoError(t, err) + + downloader := &RegistryDownloader{ + httpClient: server.Client(), + baseURL: baseURL, + archives: archivesPath, + } + + ctx := t.Context() + hash, err := downloader.Download(ctx, "test/plugin", "v1.0.0") + + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotEmpty(t, hash) + + // Check if archive file was created + archivePath := downloader.buildArchivePath("test/plugin", "v1.0.0") + assert.FileExists(t, archivePath) + } + }) + } +} + +func TestHTTPPluginDownloader_Check(t *testing.T) { + tests := []struct { + name string + serverResponse func(w http.ResponseWriter, r *http.Request) + expectError require.ErrorAssertionFunc + }{ + { + name: "successful check", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }, + expectError: require.NoError, + }, + { + name: "failed check", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + }, + expectError: require.Error, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(test.serverResponse)) + defer server.Close() + + tempDir := t.TempDir() + archivesPath := filepath.Join(tempDir, "archives") + + baseURL, err := url.Parse(server.URL) + require.NoError(t, err) + + downloader := &RegistryDownloader{ + httpClient: server.Client(), + baseURL: baseURL, + archives: archivesPath, + } + + ctx := t.Context() + + err = downloader.Check(ctx, "test/plugin", "v1.0.0", "testhash") + test.expectError(t, err) + }) + } +} + +func createDummyZip(t *testing.T, path string) { + t.Helper() + + err := os.MkdirAll(path+"/test/plugin/", 0o755) + require.NoError(t, err) + + zipfile, err := os.Create(path + "/test/plugin/v1.0.0.zip") + require.NoError(t, err) + defer zipfile.Close() + + err = fillDummyZip(zipfile) + require.NoError(t, err) +} + +func fillDummyZip(w io.Writer) error { + writer := zip.NewWriter(w) + + file, err := writer.Create("test.txt") + if err != nil { + return err + } + + _, _ = file.Write([]byte("test content")) + _ = writer.Close() + return nil +} diff --git a/pkg/plugins/client.go b/pkg/plugins/manager.go similarity index 52% rename from pkg/plugins/client.go rename to pkg/plugins/manager.go index 8d119ddec..2bbb9cbe7 100644 --- a/pkg/plugins/client.go +++ b/pkg/plugins/manager.go @@ -9,17 +9,10 @@ import ( "errors" "fmt" "io" - "net/http" - "net/url" "os" - "path" "path/filepath" "strings" - "time" - "github.com/hashicorp/go-retryablehttp" - "github.com/rs/zerolog/log" - "github.com/traefik/traefik/v3/pkg/logs" "golang.org/x/mod/module" "golang.org/x/mod/zip" "gopkg.in/yaml.v3" @@ -39,31 +32,26 @@ const ( hashHeader = "X-Plugin-Hash" ) -// ClientOptions the options of a Traefik plugins client. -type ClientOptions struct { +// ManagerOptions the options of a Traefik plugins manager. +type ManagerOptions struct { Output string } -// Client a Traefik plugins client. -type Client struct { - HTTPClient *http.Client - baseURL *url.URL +// Manager manages Traefik plugins lifecycle operations including storage, and manifest reading. +type Manager struct { + downloader PluginDownloader - archives string stateFile string - goPath string - sources string + + archives string + sources string + goPath string } -// NewClient creates a new Traefik plugins client. -func NewClient(opts ClientOptions) (*Client, error) { - baseURL, err := url.Parse(pluginsURL) - if err != nil { - return nil, err - } - +// NewManager creates a new Traefik plugins manager. +func NewManager(downloader PluginDownloader, opts ManagerOptions) (*Manager, error) { sourcesRootPath := filepath.Join(filepath.FromSlash(opts.Output), sourcesFolder) - err = resetDirectory(sourcesRootPath) + err := resetDirectory(sourcesRootPath) if err != nil { return nil, err } @@ -79,31 +67,48 @@ func NewClient(opts ClientOptions) (*Client, error) { return nil, fmt.Errorf("failed to create archives directory %s: %w", archivesPath, err) } - client := retryablehttp.NewClient() - client.Logger = logs.NewRetryableHTTPLogger(log.Logger) - client.HTTPClient = &http.Client{Timeout: 10 * time.Second} - client.RetryMax = 3 - - return &Client{ - HTTPClient: client.StandardClient(), - baseURL: baseURL, - - archives: archivesPath, - stateFile: filepath.Join(archivesPath, stateFilename), - - goPath: goPath, - sources: filepath.Join(goPath, goPathSrc), + return &Manager{ + downloader: downloader, + stateFile: filepath.Join(archivesPath, stateFilename), + archives: archivesPath, + sources: filepath.Join(goPath, goPathSrc), + goPath: goPath, }, nil } +// InstallPlugin download and unzip the given plugin. +func (m *Manager) InstallPlugin(ctx context.Context, plugin Descriptor) error { + hash, err := m.downloader.Download(ctx, plugin.ModuleName, plugin.Version) + if err != nil { + return fmt.Errorf("unable to download plugin %s: %w", plugin.ModuleName, err) + } + + if plugin.Hash != "" { + if plugin.Hash != hash { + return fmt.Errorf("invalid hash for plugin %s, expected %s, got %s", plugin.ModuleName, plugin.Hash, hash) + } + } else { + err = m.downloader.Check(ctx, plugin.ModuleName, plugin.Version, hash) + if err != nil { + return fmt.Errorf("unable to check archive integrity of the plugin %s: %w", plugin.ModuleName, err) + } + } + + if err = m.unzip(plugin.ModuleName, plugin.Version); err != nil { + return fmt.Errorf("unable to unzip plugin %s: %w", plugin.ModuleName, err) + } + + return nil +} + // GoPath gets the plugins GoPath. -func (c *Client) GoPath() string { - return c.goPath +func (m *Manager) GoPath() string { + return m.goPath } // ReadManifest reads a plugin manifest. -func (c *Client) ReadManifest(moduleName string) (*Manifest, error) { - return ReadManifest(c.goPath, moduleName) +func (m *Manager) ReadManifest(moduleName string) (*Manifest, error) { + return ReadManifest(m.goPath, moduleName) } // ReadManifest reads a plugin manifest. @@ -126,114 +131,74 @@ func ReadManifest(goPath, moduleName string) (*Manifest, error) { return m, nil } -// Download downloads a plugin archive. -func (c *Client) Download(ctx context.Context, pName, pVersion string) (string, error) { - filename := c.buildArchivePath(pName, pVersion) - - var hash string - _, err := os.Stat(filename) - if err != nil && !os.IsNotExist(err) { - return "", fmt.Errorf("failed to read archive %s: %w", filename, err) - } - - if err == nil { - hash, err = computeHash(filename) - if err != nil { - return "", fmt.Errorf("failed to compute hash: %w", err) - } - } - - endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "download", pName, pVersion)) - if err != nil { - return "", fmt.Errorf("failed to parse endpoint URL: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) - if err != nil { - return "", fmt.Errorf("failed to create request: %w", err) - } - - if hash != "" { - req.Header.Set(hashHeader, hash) - } - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return "", fmt.Errorf("failed to call service: %w", err) - } - - defer func() { _ = resp.Body.Close() }() - - switch resp.StatusCode { - case http.StatusNotModified: - // noop - return hash, nil - - case http.StatusOK: - err = os.MkdirAll(filepath.Dir(filename), 0o755) - if err != nil { - return "", fmt.Errorf("failed to create directory: %w", err) - } - - var file *os.File - file, err = os.Create(filename) - if err != nil { - return "", fmt.Errorf("failed to create file %q: %w", filename, err) - } - - defer func() { _ = file.Close() }() - - _, err = io.Copy(file, resp.Body) - if err != nil { - return "", fmt.Errorf("failed to write response: %w", err) - } - - hash, err = computeHash(filename) - if err != nil { - return "", fmt.Errorf("failed to compute hash: %w", err) - } - - return hash, nil - - default: - data, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("error: %d: %s", resp.StatusCode, string(data)) - } -} - -// Check checks the plugin archive integrity. -func (c *Client) Check(ctx context.Context, pName, pVersion, hash string) error { - endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "validate", pName, pVersion)) - if err != nil { - return fmt.Errorf("failed to parse endpoint URL: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - if hash != "" { - req.Header.Set(hashHeader, hash) - } - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return fmt.Errorf("failed to call service: %w", err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode == http.StatusOK { +// CleanArchives cleans plugins archives. +func (m *Manager) CleanArchives(plugins map[string]Descriptor) error { + if _, err := os.Stat(m.stateFile); os.IsNotExist(err) { return nil } - return errors.New("plugin integrity check failed") + stateFile, err := os.Open(m.stateFile) + if err != nil { + return fmt.Errorf("failed to open state file %s: %w", m.stateFile, err) + } + + previous := make(map[string]string) + err = json.NewDecoder(stateFile).Decode(&previous) + if err != nil { + return fmt.Errorf("failed to decode state file %s: %w", m.stateFile, err) + } + + for pName, pVersion := range previous { + for _, desc := range plugins { + if desc.ModuleName == pName && desc.Version != pVersion { + archivePath := m.buildArchivePath(pName, pVersion) + if err = os.RemoveAll(archivePath); err != nil { + return fmt.Errorf("failed to remove archive %s: %w", archivePath, err) + } + } + } + } + + return nil } -// Unzip unzip a plugin archive. -func (c *Client) Unzip(pName, pVersion string) error { - err := c.unzipModule(pName, pVersion) +// WriteState writes the plugins state files. +func (m *Manager) WriteState(plugins map[string]Descriptor) error { + state := make(map[string]string) + + for _, descriptor := range plugins { + state[descriptor.ModuleName] = descriptor.Version + } + + mp, err := json.MarshalIndent(state, "", " ") + if err != nil { + return fmt.Errorf("unable to marshal plugin state: %w", err) + } + + return os.WriteFile(m.stateFile, mp, 0o600) +} + +// ResetAll resets all plugins related directories. +func (m *Manager) ResetAll() error { + if m.goPath == "" { + return errors.New("goPath is empty") + } + + err := resetDirectory(filepath.Join(m.goPath, "..")) + if err != nil { + return fmt.Errorf("unable to reset plugins GoPath directory %s: %w", m.goPath, err) + } + + err = resetDirectory(m.archives) + if err != nil { + return fmt.Errorf("unable to reset plugins archives directory: %w", err) + } + + return nil +} + +func (m *Manager) unzip(pName, pVersion string) error { + err := m.unzipModule(pName, pVersion) if err == nil { return nil } @@ -241,18 +206,18 @@ func (c *Client) Unzip(pName, pVersion string) error { // Unzip as a generic archive if the module unzip fails. // This is useful for plugins that have vendor directories or other structures. // This is also useful for wasm plugins. - return c.unzipArchive(pName, pVersion) + return m.unzipArchive(pName, pVersion) } -func (c *Client) unzipModule(pName, pVersion string) error { - src := c.buildArchivePath(pName, pVersion) - dest := filepath.Join(c.sources, filepath.FromSlash(pName)) +func (m *Manager) unzipModule(pName, pVersion string) error { + src := m.buildArchivePath(pName, pVersion) + dest := filepath.Join(m.sources, filepath.FromSlash(pName)) return zip.Unzip(dest, module.Version{Path: pName, Version: pVersion}, src) } -func (c *Client) unzipArchive(pName, pVersion string) error { - zipPath := c.buildArchivePath(pName, pVersion) +func (m *Manager) unzipArchive(pName, pVersion string) error { + zipPath := m.buildArchivePath(pName, pVersion) archive, err := zipa.OpenReader(zipPath) if err != nil { @@ -261,10 +226,10 @@ func (c *Client) unzipArchive(pName, pVersion string) error { defer func() { _ = archive.Close() }() - dest := filepath.Join(c.sources, filepath.FromSlash(pName)) + dest := filepath.Join(m.sources, filepath.FromSlash(pName)) for _, f := range archive.File { - err = unzipFile(f, dest) + err = m.unzipFile(f, dest) if err != nil { return fmt.Errorf("unable to unzip %s: %w", f.Name, err) } @@ -273,7 +238,7 @@ func (c *Client) unzipArchive(pName, pVersion string) error { return nil } -func unzipFile(f *zipa.File, dest string) error { +func (m *Manager) unzipFile(f *zipa.File, dest string) error { rc, err := f.Open() if err != nil { return err @@ -341,74 +306,8 @@ func unzipFile(f *zipa.File, dest string) error { return nil } -// CleanArchives cleans plugins archives. -func (c *Client) CleanArchives(plugins map[string]Descriptor) error { - if _, err := os.Stat(c.stateFile); os.IsNotExist(err) { - return nil - } - - stateFile, err := os.Open(c.stateFile) - if err != nil { - return fmt.Errorf("failed to open state file %s: %w", c.stateFile, err) - } - - previous := make(map[string]string) - err = json.NewDecoder(stateFile).Decode(&previous) - if err != nil { - return fmt.Errorf("failed to decode state file %s: %w", c.stateFile, err) - } - - for pName, pVersion := range previous { - for _, desc := range plugins { - if desc.ModuleName == pName && desc.Version != pVersion { - archivePath := c.buildArchivePath(pName, pVersion) - if err = os.RemoveAll(archivePath); err != nil { - return fmt.Errorf("failed to remove archive %s: %w", archivePath, err) - } - } - } - } - - return nil -} - -// WriteState writes the plugins state files. -func (c *Client) WriteState(plugins map[string]Descriptor) error { - m := make(map[string]string) - - for _, descriptor := range plugins { - m[descriptor.ModuleName] = descriptor.Version - } - - mp, err := json.MarshalIndent(m, "", " ") - if err != nil { - return fmt.Errorf("unable to marshal plugin state: %w", err) - } - - return os.WriteFile(c.stateFile, mp, 0o600) -} - -// ResetAll resets all plugins related directories. -func (c *Client) ResetAll() error { - if c.goPath == "" { - return errors.New("goPath is empty") - } - - err := resetDirectory(filepath.Join(c.goPath, "..")) - if err != nil { - return fmt.Errorf("unable to reset plugins GoPath directory %s: %w", c.goPath, err) - } - - err = resetDirectory(c.archives) - if err != nil { - return fmt.Errorf("unable to reset plugins archives directory: %w", err) - } - - return nil -} - -func (c *Client) buildArchivePath(pName, pVersion string) string { - return filepath.Join(c.archives, filepath.FromSlash(pName), pVersion+".zip") +func (m *Manager) buildArchivePath(pName, pVersion string) string { + return filepath.Join(m.archives, filepath.FromSlash(pName), pVersion+".zip") } func resetDirectory(dir string) error { diff --git a/pkg/plugins/manager_test.go b/pkg/plugins/manager_test.go new file mode 100644 index 000000000..5100b2a6b --- /dev/null +++ b/pkg/plugins/manager_test.go @@ -0,0 +1,341 @@ +package plugins + +import ( + zipa "archive/zip" + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// mockDownloader is a test implementation of PluginDownloader +type mockDownloader struct { + downloadFunc func(ctx context.Context, pName, pVersion string) (string, error) + checkFunc func(ctx context.Context, pName, pVersion, hash string) error +} + +func (m *mockDownloader) Download(ctx context.Context, pName, pVersion string) (string, error) { + if m.downloadFunc != nil { + return m.downloadFunc(ctx, pName, pVersion) + } + return "mockhash", nil +} + +func (m *mockDownloader) Check(ctx context.Context, pName, pVersion, hash string) error { + if m.checkFunc != nil { + return m.checkFunc(ctx, pName, pVersion, hash) + } + return nil +} + +func TestPluginManager_ReadManifest(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{} + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + moduleName := "github.com/test/plugin" + pluginPath := filepath.Join(manager.goPath, "src", moduleName) + err = os.MkdirAll(pluginPath, 0o755) + require.NoError(t, err) + + manifest := &Manifest{ + DisplayName: "Test Plugin", + Type: "middleware", + Import: "github.com/test/plugin", + Summary: "A test plugin", + TestData: map[string]interface{}{ + "test": "data", + }, + } + + manifestPath := filepath.Join(pluginPath, pluginManifest) + manifestData, err := yaml.Marshal(manifest) + require.NoError(t, err) + err = os.WriteFile(manifestPath, manifestData, 0o644) + require.NoError(t, err) + + readManifest, err := manager.ReadManifest(moduleName) + require.NoError(t, err) + assert.Equal(t, manifest.DisplayName, readManifest.DisplayName) + assert.Equal(t, manifest.Type, readManifest.Type) + assert.Equal(t, manifest.Import, readManifest.Import) + assert.Equal(t, manifest.Summary, readManifest.Summary) +} + +func TestPluginManager_ReadManifest_NotFound(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{} + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + _, err = manager.ReadManifest("nonexistent/plugin") + assert.Error(t, err) +} + +func TestPluginManager_CleanArchives(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{} + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + testPlugin1 := "test/plugin1" + testPlugin2 := "test/plugin2" + + archive1Dir := filepath.Join(manager.archives, "test", "plugin1") + archive2Dir := filepath.Join(manager.archives, "test", "plugin2") + err = os.MkdirAll(archive1Dir, 0o755) + require.NoError(t, err) + err = os.MkdirAll(archive2Dir, 0o755) + require.NoError(t, err) + + archive1Old := filepath.Join(archive1Dir, "v1.0.0.zip") + archive1New := filepath.Join(archive1Dir, "v2.0.0.zip") + archive2 := filepath.Join(archive2Dir, "v1.0.0.zip") + + err = os.WriteFile(archive1Old, []byte("old archive"), 0o644) + require.NoError(t, err) + err = os.WriteFile(archive1New, []byte("new archive"), 0o644) + require.NoError(t, err) + err = os.WriteFile(archive2, []byte("archive 2"), 0o644) + require.NoError(t, err) + + state := map[string]string{ + testPlugin1: "v1.0.0", + testPlugin2: "v1.0.0", + } + stateData, err := json.MarshalIndent(state, "", " ") + require.NoError(t, err) + err = os.WriteFile(manager.stateFile, stateData, 0o600) + require.NoError(t, err) + + currentPlugins := map[string]Descriptor{ + "plugin1": { + ModuleName: testPlugin1, + Version: "v2.0.0", + }, + "plugin2": { + ModuleName: testPlugin2, + Version: "v1.0.0", + }, + } + + err = manager.CleanArchives(currentPlugins) + require.NoError(t, err) + + assert.NoFileExists(t, archive1Old) + assert.FileExists(t, archive1New) + assert.FileExists(t, archive2) +} + +func TestPluginManager_WriteState(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{} + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + plugins := map[string]Descriptor{ + "plugin1": { + ModuleName: "test/plugin1", + Version: "v1.0.0", + }, + "plugin2": { + ModuleName: "test/plugin2", + Version: "v2.0.0", + }, + } + + err = manager.WriteState(plugins) + require.NoError(t, err) + + assert.FileExists(t, manager.stateFile) + + data, err := os.ReadFile(manager.stateFile) + require.NoError(t, err) + + var state map[string]string + err = json.Unmarshal(data, &state) + require.NoError(t, err) + + expectedState := map[string]string{ + "test/plugin1": "v1.0.0", + "test/plugin2": "v2.0.0", + } + assert.Equal(t, expectedState, state) +} + +func TestPluginManager_ResetAll(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{} + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + testFile := filepath.Join(manager.GoPath(), "test.txt") + err = os.WriteFile(testFile, []byte("test"), 0o644) + require.NoError(t, err) + + archiveFile := filepath.Join(manager.archives, "test.zip") + err = os.WriteFile(archiveFile, []byte("archive"), 0o644) + require.NoError(t, err) + + err = manager.ResetAll() + require.NoError(t, err) + + assert.DirExists(t, manager.archives) + assert.NoFileExists(t, testFile) + assert.NoFileExists(t, archiveFile) +} + +func TestPluginManager_InstallPlugin(t *testing.T) { + tests := []struct { + name string + plugin Descriptor + downloadFunc func(ctx context.Context, pName, pVersion string) (string, error) + checkFunc func(ctx context.Context, pName, pVersion, hash string) error + setupArchive func(t *testing.T, archivePath string) + expectError bool + errorMsg string + }{ + { + name: "successful installation", + plugin: Descriptor{ + ModuleName: "github.com/test/plugin", + Version: "v1.0.0", + Hash: "expected-hash", + }, + downloadFunc: func(ctx context.Context, pName, pVersion string) (string, error) { + return "expected-hash", nil + }, + checkFunc: func(ctx context.Context, pName, pVersion, hash string) error { + return nil + }, + setupArchive: func(t *testing.T, archivePath string) { + t.Helper() + + // Create a valid zip archive + err := os.MkdirAll(filepath.Dir(archivePath), 0o755) + require.NoError(t, err) + + file, err := os.Create(archivePath) + require.NoError(t, err) + defer file.Close() + + // Write a minimal zip file with a test file + writer := zipa.NewWriter(file) + defer writer.Close() + + fileWriter, err := writer.Create("test-module-v1.0.0/main.go") + require.NoError(t, err) + _, err = fileWriter.Write([]byte("package main\n\nfunc main() {}\n")) + require.NoError(t, err) + }, + expectError: false, + }, + { + name: "download error", + plugin: Descriptor{ + ModuleName: "github.com/test/plugin", + Version: "v1.0.0", + }, + downloadFunc: func(ctx context.Context, pName, pVersion string) (string, error) { + return "", assert.AnError + }, + expectError: true, + errorMsg: "unable to download plugin", + }, + { + name: "check error", + plugin: Descriptor{ + ModuleName: "github.com/test/plugin", + Version: "v1.0.0", + Hash: "expected-hash", + }, + downloadFunc: func(ctx context.Context, pName, pVersion string) (string, error) { + return "actual-hash", nil + }, + checkFunc: func(ctx context.Context, pName, pVersion, hash string) error { + return assert.AnError + }, + expectError: true, + errorMsg: "invalid hash for plugin", + }, + { + name: "unzip error - invalid archive", + plugin: Descriptor{ + ModuleName: "github.com/test/plugin", + Version: "v1.0.0", + }, + downloadFunc: func(ctx context.Context, pName, pVersion string) (string, error) { + return "test-hash", nil + }, + checkFunc: func(ctx context.Context, pName, pVersion, hash string) error { + return nil + }, + setupArchive: func(t *testing.T, archivePath string) { + t.Helper() + + // Create an invalid zip archive + err := os.MkdirAll(filepath.Dir(archivePath), 0o755) + require.NoError(t, err) + err = os.WriteFile(archivePath, []byte("invalid zip content"), 0o644) + require.NoError(t, err) + }, + expectError: true, + errorMsg: "unable to unzip plugin", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{ + downloadFunc: test.downloadFunc, + checkFunc: test.checkFunc, + } + + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + // Setup archive if needed + if test.setupArchive != nil { + archivePath := filepath.Join(manager.archives, + filepath.FromSlash(test.plugin.ModuleName), + test.plugin.Version+".zip") + test.setupArchive(t, archivePath) + } + + ctx := t.Context() + err = manager.InstallPlugin(ctx, test.plugin) + + if test.expectError { + assert.Error(t, err) + if test.errorMsg != "" { + assert.Contains(t, err.Error(), test.errorMsg) + } + } else { + assert.NoError(t, err) + + // Verify that plugin sources were extracted + sourcePath := filepath.Join(manager.sources, filepath.FromSlash(test.plugin.ModuleName)) + assert.DirExists(t, sourcePath) + } + }) + } +} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 367b6c46c..f7d543154 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -13,13 +13,13 @@ import ( const localGoPath = "./plugins-local/" // SetupRemotePlugins setup remote plugins environment. -func SetupRemotePlugins(client *Client, plugins map[string]Descriptor) error { +func SetupRemotePlugins(manager *Manager, plugins map[string]Descriptor) error { err := checkRemotePluginsConfiguration(plugins) if err != nil { return fmt.Errorf("invalid configuration: %w", err) } - err = client.CleanArchives(plugins) + err = manager.CleanArchives(plugins) if err != nil { return fmt.Errorf("unable to clean archives: %w", err) } @@ -27,35 +27,20 @@ func SetupRemotePlugins(client *Client, plugins map[string]Descriptor) error { ctx := context.Background() for pAlias, desc := range plugins { - log.Ctx(ctx).Debug().Msgf("Loading of plugin: %s: %s@%s", pAlias, desc.ModuleName, desc.Version) + log.Ctx(ctx).Debug().Msgf("Installing plugin: %s: %s@%s", pAlias, desc.ModuleName, desc.Version) - hash, err := client.Download(ctx, desc.ModuleName, desc.Version) - if err != nil { - _ = client.ResetAll() - return fmt.Errorf("unable to download plugin %s: %w", desc.ModuleName, err) - } - - err = client.Check(ctx, desc.ModuleName, desc.Version, hash) - if err != nil { - _ = client.ResetAll() - return fmt.Errorf("unable to check archive integrity of the plugin %s: %w", desc.ModuleName, err) + if err = manager.InstallPlugin(ctx, desc); err != nil { + _ = manager.ResetAll() + return fmt.Errorf("unable to install plugin %s: %w", pAlias, err) } } - err = client.WriteState(plugins) + err = manager.WriteState(plugins) if err != nil { - _ = client.ResetAll() + _ = manager.ResetAll() return fmt.Errorf("unable to write plugins state: %w", err) } - for _, desc := range plugins { - err = client.Unzip(desc.ModuleName, desc.Version) - if err != nil { - _ = client.ResetAll() - return fmt.Errorf("unable to unzip archive: %w", err) - } - } - return nil } diff --git a/pkg/plugins/types.go b/pkg/plugins/types.go index ccae8dce4..75bb589b3 100644 --- a/pkg/plugins/types.go +++ b/pkg/plugins/types.go @@ -24,6 +24,9 @@ type Descriptor struct { // Version (required) Version string `description:"plugin's version." json:"version,omitempty" toml:"version,omitempty" yaml:"version,omitempty" export:"true"` + // Hash (optional) + Hash string `description:"plugin's hash to validate'" json:"hash,omitempty" toml:"hash,omitempty" yaml:"hash,omitempty" export:"true"` + // Settings (optional) Settings Settings `description:"Plugin's settings (works only for wasm plugins)." json:"settings,omitempty" toml:"settings,omitempty" yaml:"settings,omitempty" export:"true"` } From 660acf3b424d4af00c651f872a5bf40999dad2c9 Mon Sep 17 00:00:00 2001 From: Nicolas Mengin Date: Wed, 17 Sep 2025 14:56:06 +0200 Subject: [PATCH 07/12] Create Traefik Service CRD sub-resource documentation page. --- .../kubernetes/crd/http/ingressroute.md | 346 +------------- .../kubernetes/crd/http/service.md | 430 ++++++++++++++++++ .../kubernetes/crd/http/traefikservice.md | 120 ++--- docs/mkdocs.yml | 1 + 4 files changed, 458 insertions(+), 439 deletions(-) create mode 100644 docs/content/reference/routing-configuration/kubernetes/crd/http/service.md diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/http/ingressroute.md b/docs/content/reference/routing-configuration/kubernetes/crd/http/ingressroute.md index 29a89e5da..9356189d0 100644 --- a/docs/content/reference/routing-configuration/kubernetes/crd/http/ingressroute.md +++ b/docs/content/reference/routing-configuration/kubernetes/crd/http/ingressroute.md @@ -87,37 +87,8 @@ spec: | `routes[n].`
`observability.`
`accesslogs`
| Defines whether the route will produce [access-logs](../../../../install-configuration/observability/logs-and-accesslogs.md). See [here](../../../http/router/observability.md) for more information. | false | No | | `routes[n].`
`observability.`
`metrics`
| Defines whether the route will produce [metrics](../../../../install-configuration/observability/metrics.md). See [here](../../../http/router/observability.md) for more information. | false | No | | `routes[n].`
`observability.`
`tracing`
| Defines whether the route will produce [traces](../../../../install-configuration/observability/tracing.md). See [here](../../../http/router/observability.md) for more information. | false | No | -| `routes[n].`
`services`
| List of any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
More information [here](#externalname-service). | | No | -| `routes[n].`
`services[m].`
`kind`
| Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
**TraefikService**: Traefik Service.
More information [here](#externalname-service). | "Service" | No | -| `routes[n].`
`services[m].`
`name`
| Service name.
The character `@` is not authorized.
More information [here](#middleware). | | Yes | -| `routes[n].`
`services[m].`
`namespace`
| Service namespace.
Can be empty if the service belongs to the same namespace as the IngressRoute.
More information [here](#externalname-service). | | No | -| `routes[n].`
`services[m].`
`port`
| Service port (number or port name).
Evaluated only if the kind is **Service**. | | No | -| `routes[n].`
`services[m].`
`responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | -| `routes[n].`
`services[m].`
`scheme`
| Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | -| `routes[n].`
`services[m].`
`serversTransport`
| Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | -| `routes[n].`
`services[m].`
`passHostHeader`
| Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | -| `routes[n].`
`services[m].`
`healthCheck.scheme`
| Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `routes[n].`
`services[m].`
`healthCheck.mode`
| Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "http" | No | -| `routes[n].`
`services[m].`
`healthCheck.path`
| Server URL path for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `routes[n].`
`services[m].`
`healthCheck.interval`
| Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | -| `routes[n].`
`services[m].`
`healthCheck.unhealthyInterval`
| Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | -| `routes[n].`
`services[m].`
`healthCheck.method`
| HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "GET" | No | -| `routes[n].`
`services[m].`
`healthCheck.status`
| Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | -| `routes[n].`
`services[m].`
`healthCheck.port`
| URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | | No | -| `routes[n].`
`services[m].`
`healthCheck.timeout`
| Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "5s" | No | -| `routes[n].`
`services[m].`
`healthCheck.hostname`
| Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `routes[n].`
`services[m].`
`healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | true | No | -| `routes[n].`
`services[m].`
`healthCheck.headers`
| Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service)). | | No | -| `routes[n].`
`services[m].`
`sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
Evaluated only if the kind is **Service**. | "" | No | -| `routes[n].`
`services[m].`
`sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | -| `routes[n].`
`services[m].`
`sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | -| `routes[n].`
`services[m].`
`sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | -| `routes[n].`
`services[m].`
`sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | -| `routes[n].`
`services[m].`
`strategy`
| Load balancing strategy between the servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind is **Service**. | "RoundRobin" | No | -| `routes[n].`
`services[m].`
`weight`
| Service weight.
To use only to refer to WRR TraefikService | "" | No | -| `routes[n].`
`services[m].`
`nativeLB`
| Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | -| `routes[n].`
`services[m].`
`nodePortLB`
| Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | | `tls` | TLS configuration.
Can be an empty value(`{}`):
A self signed is generated in such a case
(or the [default certificate](tlsstore.md) is used if it is defined.) | | No | +| `routes[n].`
`services`
| List of any combination of [TraefikService](./traefikservice.md) and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
Exhaustive list of option in the [`Service`](./service.md#configuration-options) documentation. | | No | | `tls.secretName` | [Secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the same namesapce as the `IngressRoute`) | "" | No | | `tls.`
`options.name`
| Name of the [`TLSOption`](tlsoption.md) to use.
More information [here](#tls-options). | "" | No | | `tls.`
`options.namespace`
| Namespace of the [`TLSOption`](tlsoption.md) to use. | "" | No | @@ -126,113 +97,6 @@ spec: | `tls.`
`domains[n].main`
| Main domain name | "" | Yes | | `tls.`
`domains[n].sans`
| List of alternative domains (SANs) | | No | -### ExternalName Service - -Traefik backends creation needs a port to be set, however Kubernetes [ExternalName Service](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) could be defined without any port. Accordingly, Traefik supports defining a port in two ways: - -- only on `IngressRoute` service -- on both sides, you'll be warned if the ports don't match, and the `IngressRoute` service port is used - -Thus, in case of two sides port definition, Traefik expects a match between ports. - -=== "Ports defined on Resource" - - ```yaml tab="IngressRoute" - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: apps - - spec: - entryPoints: - - foo - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - port: 80 - ``` - - ```yaml tab="Service ExternalName" - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: apps - - spec: - externalName: external.domain - type: ExternalName - ``` - -=== "Port defined on the Service" - - ```yaml tab="IngressRoute" - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: apps - - spec: - entryPoints: - - foo - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - ``` - - ```yaml tab="Service ExternalName" - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: apps - - spec: - externalName: external.domain - type: ExternalName - ports: - - port: 80 - ``` - -=== "Port defined on both sides" - - ```yaml tab="IngressRoute" - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: apps - - spec: - entryPoints: - - foo - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - port: 80 - ``` - - ```yaml tab="Service ExternalName" - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: apps - - spec: - externalName: external.domain - type: ExternalName - ports: - - port: 80 - ``` ### Middleware @@ -282,109 +146,6 @@ same namespace as the IngressRoute) - `Service` (default value): to reference a [Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service/) - `TraefikService`: to reference an object [`TraefikService`](../http/traefikservice.md) -### Port Definition - -Traefik backends creation needs a port to be set, however Kubernetes [ExternalName Service](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) could be defined without any port. Accordingly, Traefik supports defining a port in two ways: - -- only on `IngressRoute` service -- on both sides, you'll be warned if the ports don't match, and the `IngressRoute` service port is used - -Thus, in case of two sides port definition, Traefik expects a match between ports. - -??? example - - ```yaml tab="IngressRoute" - --- - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: default - - spec: - entryPoints: - - foo - - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - port: 80 - - --- - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: default - spec: - externalName: external.domain - type: ExternalName - ``` - - ```yaml tab="ExternalName Service" - --- - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: default - - spec: - entryPoints: - - foo - - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - - --- - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: default - spec: - externalName: external.domain - type: ExternalName - ports: - - port: 80 - ``` - - ```yaml tab="Both sides" - --- - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: default - - spec: - entryPoints: - - foo - - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - port: 80 - - --- - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: default - spec: - externalName: external.domain - type: ExternalName - ports: - - port: 80 - ``` ### TLS Options @@ -455,108 +216,3 @@ TLS options references, a conflict occurs, such as in the example below. If that happens, both mappings are discarded, and the host name (`example.net` in the example) for these routers gets associated with the default TLS options instead. - -### Load Balancing - -You can declare and use Kubernetes Service load balancing as detailed below: - -```yaml tab="IngressRoute" -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: ingressroutebar - namespace: default - -spec: - entryPoints: - - web - routes: - - match: Host(`example.com`) && PathPrefix(`/foo`) - kind: Rule - services: - - name: svc1 - namespace: default - - name: svc2 - namespace: default -``` - -```yaml tab="K8s Service" -apiVersion: v1 -kind: Service -metadata: - name: svc1 - namespace: default - -spec: - ports: - - name: http - port: 80 - selector: - app: traefiklabs - task: app1 ---- -apiVersion: v1 -kind: Service -metadata: - name: svc2 - namespace: default - -spec: - ports: - - name: http - port: 80 - selector: - app: traefiklabs - task: app2 -``` - -!!! important "Kubernetes Service Native Load-Balancing" - - To avoid creating the server load-balancer with the pod IPs and use Kubernetes Service clusterIP directly, - one should set the service `NativeLB` option to true. - Please note that, by default, Traefik reuses the established connections to the backends for performance purposes. This can prevent the requests load balancing between the replicas from behaving as one would expect when the option is set. - By default, `NativeLB` is false. - - ??? example "Example" - - ```yaml - --- - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: default - - spec: - entryPoints: - - foo - - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: svc - port: 80 - # Here, nativeLB instructs to build the server load-balancer with the Kubernetes Service clusterIP only. - nativeLB: true - - --- - apiVersion: v1 - kind: Service - metadata: - name: svc - namespace: default - spec: - type: ClusterIP - ... - ``` - -### Configuring Backend Protocol - -There are 3 ways to configure the backend protocol for communication between Traefik and your pods: - -- Setting the scheme explicitly (http/https/h2c) -- Configuring the name of the kubernetes service port to start with https (https) -- Setting the kubernetes service port to use port 443 (https) - -If you do not configure the above, Traefik will assume an http connection. diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md b/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md new file mode 100644 index 000000000..14de09555 --- /dev/null +++ b/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md @@ -0,0 +1,430 @@ +--- +title: "Kubernetes Service" +description: "A Service is a not Traefik CRD, it allows you to describe the Service option in an IngressRoute or a Traefik Service." +--- + +`Service` is the implementation of a [Traefik HTTP service](../../../http/load-balancing/service.md). + +There is no dedicated CRD, a `Service` is part of: + +- [`IngressRoute`](./ingressroute.md) +- [`TraefikService`](./traefikservice.md) + +Note that, before creating `IngressRoute` or `TraefikService` objects, you need to apply the [Traefik Kubernetes CRDs](https://doc.traefik.io/traefik/reference/dynamic-configuration/kubernetes-crd/#definitions) to your Kubernetes cluster. + +This registers the Traefik-specific resources. + +## Configuration Example + +You can declare a `Service` either as part of an `IngressRoute` or a `TraefikService` as detailed below: + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test-name + namespace: apps +spec: + entryPoints: + - web + routes: + - kind: Rule + # Rule on the Host + match: Host(`test.example.com`) + services: + # Target a Kubernetes Service + - kind: Service + name: foo + namespace: apps + # Customize the connection between Traefik and the backend + passHostHeader: true + port: 80 + responseForwarding: + flushInterval: 1ms + scheme: https + sticky: + cookie: + httpOnly: true + name: cookie + secure: true + strategy: RoundRobin +``` + +```yaml tab="TraefikService" +apiVersion: traefik.io/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: apps + +spec: + weighted: + services: + # Target a Kubernetes Service + - kind: Service + name: foo + namespace: apps + # Customize the connection between Traefik and the backend + passHostHeader: true + port: 80 + responseForwarding: + flushInterval: 1ms + scheme: https + sticky: + cookie: + httpOnly: true + name: cookie + secure: true + strategy: RoundRobin +``` + +## Configuration Options + +| Field | Description | Default | Required | +|:---------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| +| `kind` | Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
**TraefikService**: Traefik Service.
More information [here](#externalname-service). | "Service" | No | +| `name` | Service name.
The character `@` is not authorized.
More information [here](#middleware). | | Yes | +| `namespace` | Service namespace.
Can be empty if the service belongs to the same namespace as the IngressRoute.
More information [here](#externalname-service). | | No | +| `port` | Service port (number or port name).
Evaluated only if the kind is **Service**. | | No | +| `responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | +| `scheme` | Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | +| `serversTransport` | Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | +| `passHostHeader` | Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | +| `healthCheck.scheme` | Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.mode` | Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "http" | No | +| `healthCheck.path` | Server URL path for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.interval` | Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | +| `healthCheck.unhealthyInterval` | Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | +| `healthCheck.method` | HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "GET" | No | +| `healthCheck.status` | Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | +| `healthCheck.port` | URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | | No | +| `healthCheck.timeout` | Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "5s" | No | +| `healthCheck.hostname` | Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | true | No | +| `healthCheck.headers` | Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service)). | | No | +| `sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
Evaluated only if the kind is **Service**. | "" | No | +| `sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | +| `sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | +| `sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | +| `sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | +| `strategy` | Load balancing strategy between the servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind is **Service**. | "RoundRobin" | No | +| `nativeLB` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | +| `nodePortLB` | Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | + + +### ExternalName Service + +Traefik backends creation needs a port to be set, however Kubernetes [ExternalName Service](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) could be defined without any port. Accordingly, Traefik supports defining a port in two ways: + +- only on `IngressRoute` service +- on both sides, you'll be warned if the ports don't match, and the `IngressRoute` service port is used + +Thus, in case of two sides port definition, Traefik expects a match between ports. + +=== "Ports defined on Resource" + + ```yaml tab="IngressRoute" + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: apps + + spec: + entryPoints: + - foo + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + port: 80 + ``` + + ```yaml tab="Service ExternalName" + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: apps + + spec: + externalName: external.domain + type: ExternalName + ``` + +=== "Port defined on the Service" + + ```yaml tab="IngressRoute" + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: apps + + spec: + entryPoints: + - foo + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + ``` + + ```yaml tab="Service ExternalName" + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: apps + + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + +=== "Port defined on both sides" + + ```yaml tab="IngressRoute" + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: apps + + spec: + entryPoints: + - foo + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + port: 80 + ``` + + ```yaml tab="Service ExternalName" + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: apps + + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + +### Port Definition + +Traefik backends creation needs a port to be set, however Kubernetes [ExternalName Service](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) could be defined without any port. Accordingly, Traefik supports defining a port in two ways: + +- only on `IngressRoute` service +- on both sides, you'll be warned if the ports don't match, and the `IngressRoute` service port is used + +Thus, in case of two sides port definition, Traefik expects a match between ports. + +??? example + + ```yaml tab="IngressRoute" + --- + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + port: 80 + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ``` + + ```yaml tab="ExternalName Service" + --- + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + + ```yaml tab="Both sides" + --- + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + port: 80 + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + +### Load Balancing + +You can declare and use Kubernetes Service load balancing as detailed below: + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: ingressroutebar + namespace: default + +spec: + entryPoints: + - web + routes: + - match: Host(`example.com`) && PathPrefix(`/foo`) + kind: Rule + services: + - name: svc1 + namespace: default + - name: svc2 + namespace: default +``` + +```yaml tab="K8s Service" +apiVersion: v1 +kind: Service +metadata: + name: svc1 + namespace: default + +spec: + ports: + - name: http + port: 80 + selector: + app: traefiklabs + task: app1 +--- +apiVersion: v1 +kind: Service +metadata: + name: svc2 + namespace: default + +spec: + ports: + - name: http + port: 80 + selector: + app: traefiklabs + task: app2 +``` + +!!! important "Kubernetes Service Native Load-Balancing" + + To avoid creating the server load-balancer with the pod IPs and use Kubernetes Service clusterIP directly, + one should set the service `NativeLB` option to true. + Please note that, by default, Traefik reuses the established connections to the backends for performance purposes. This can prevent the requests load balancing between the replicas from behaving as one would expect when the option is set. + By default, `NativeLB` is false. + + ??? example "Example" + + ```yaml + --- + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: svc + port: 80 + # Here, nativeLB instructs to build the server load-balancer with the Kubernetes Service clusterIP only. + nativeLB: true + + --- + apiVersion: v1 + kind: Service + metadata: + name: svc + namespace: default + spec: + type: ClusterIP + ... + ``` + +### Configuring Backend Protocol + +There are 3 ways to configure the backend protocol for communication between Traefik and your pods: + +- Setting the scheme explicitly (http/https/h2c) +- Configuring the name of the kubernetes service port to start with https (https) +- Setting the kubernetes service port to use port 443 (https) + +If you do not configure the above, Traefik will assume an http connection. diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md b/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md index 2cdf94c9c..5ffec45cc 100644 --- a/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md +++ b/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md @@ -150,36 +150,8 @@ data: | Field | Description | Default | Required | |:---------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| -| `services` | List of any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
. | | No | -| `services[m].`
`kind`
| Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
- **TraefikService**: Traefik Service. | "" | No | -| `services[m].`
`name`
| Service name.
The character `@` is not authorized. | "" | Yes | -| `services[m].`
`namespace`
| Service namespace. | "" | No | -| `services[m].`
`port`
| Service port (number or port name).
Evaluated only if the kind is **Service**. | "" | No | -| `services[m].`
`responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | -| `services[m].`
`scheme`
| Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | -| `services[m].`
`serversTransport`
| Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | -| `services[m].`
`passHostHeader`
| Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | -| `services[m].`
`healthCheck.scheme`
| Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "" | No | -| `services[m].`
`healthCheck.mode`
| Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "http" | No | -| `services[m].`
`healthCheck.path`
| Server URL path for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "" | No | -| `services[m].`
`healthCheck.interval`
| Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName]`ExternalName`. | "100ms" | No | -| `services[m].`
`healthCheck.unhealthyInterval`
| Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName]`ExternalName`. | "100ms" | No | -| `services[m].`
`healthCheck.method`
| HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "GET" | No | -| `services[m].`
`healthCheck.status`
| Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | -| `services[m].`
`healthCheck.port`
| URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | | No | -| `services[m].`
`healthCheck.timeout`
| Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "5s" | No | -| `services[m].`
`healthCheck.hostname`
| Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "" | No | -| `services[m].`
`healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | true | No | -| `services[m].`
`healthCheck.headers`
| Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | | No | -| `services[m].`
`sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
Evaluated only if the kind is **Service**. | Abbreviation of a sha1
(ex: `_1d52e`). | No | -| `services[m].`
`sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | -| `services[m].`
`sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | -| `services[m].`
`sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy.
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | -| `services[m].`
`sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | -| `services[m].`
`strategy`
| Load balancing strategy between the servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind is **Service**. | "RoundRobin" | No | -| `services[m].`
`weight`
| Service weight.
To use only to refer to WRR TraefikService | "" | No | -| `services[m].`
`nativeLB`
| Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | -| `services[m].`
`nodePortLB`
| Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | +| `services` | List of any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
. Exhaustive list of option in the [`Service`](./service.md#configuration-options) documentation. | | No | +| `services[m].weight` | Service weight. | "" | No | | `sticky.`
`cookie.name`
| Name of the cookie used for the stickiness at the WRR service level.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
More information about WRR stickiness [here](#stickiness-on-multiple-levels) | Abbreviation of a sha1
(ex: `_1d52e`). | No | | `sticky.`
`cookie.httpOnly`
| Allow the cookie used for the stickiness at the WRR service level to be accessed by client-side APIs, such as JavaScript.
More information about WRR stickiness [here](#stickiness-on-multiple-levels) | false | No | | `sticky.`
`cookie.secure`
| Allow the cookie used for the stickiness at the WRR service level to be only transmitted over an encrypted connection (i.e. HTTPS).
More information about WRR stickiness [here](#stickiness-on-multiple-levels) | false | No | @@ -305,6 +277,8 @@ spec: mirroring: name: svc1 # svc1 receives 100% of the traffic port: 80 + mirrorBody: true # Set to false by default + maxBodySize: 1M mirrors: - name: svc2 # svc2 receives a copy of 20% of this traffic port: 80 @@ -367,73 +341,31 @@ spec: ### Configuration Options -!!!note "Main and mirrored services" +#### Main Service Options - The main service properties are set as the option root level. +The main service properties are set as the option root level. - The mirrored services properties are set in the `mirrors` list. +The main service provides the same options as a [`Service`](./service.md). + +The exhaustive list of the service options is described in the [`Service`](./service.md#configuration-options) documentation. +The mirror main service dedicated option are described below. | Field | Description | Default | Required | |:--------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| -| `kind` | Kind of the main service.
Two values allowed:
- **Service**: Kubernetes Service
- **TraefikService**: Traefik Service.
More information [here](#services) | "" | No | -| `name` | Main service name.
The character `@` is not authorized. | "" | Yes | -| `namespace` | Main service namespace.
More information [here](#services). | "" | No | -| `port` | Main service port (number or port name).
Evaluated only if the kind of the main service is **Service**. | "" | No | -| `responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind of the main service is **Service**. | 100ms | No | -| `scheme` | Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind of the main service is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | -| `serversTransport` | Name of ServersTransport resource to use to configure the transport between Traefik and the main service's servers.
Evaluated only if the kind of the main service is **Service**. | "" | No | -| `passHostHeader` | Forward client Host header to main service's server.
Evaluated only if the kind of the main service is **Service**. | true | No | -| `healthCheck.scheme` | Server URL scheme for the health check endpoint.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `healthCheck.mode` | Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "http" | No | -| `healthCheck.path` | Server URL path for the health check endpoint.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `healthCheck.interval` | Frequency of the health check calls for healthy targets.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "100ms" | No | -| `healthCheck.unhealthyInterval` | Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "100ms" | No | -| `healthCheck.method` | HTTP method for the health check endpoint.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "GET" | No | -| `healthCheck.status` | Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind of the main service is **Service**. | | No | -| `healthCheck.port` | URL port for the health check endpoint.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | | No | -| `healthCheck.timeout` | Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "5s" | No | -| `healthCheck.hostname` | Value in the Host header of the health check request.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | true | No | -| `healthCheck.headers` | Map of header to send to the health check endpoint
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | | No | -| `sticky.`
`cookie.name`
| Name of the cookie used for the stickiness on the main service.
Evaluated only if the kind of the main service is **Service**. | Abbreviation of a sha1
(ex: `_1d52e`). | No | -| `sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind of the main service is **Service**. | false | No | -| `sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind of the main service is **Service**. | false | No | -| `sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy.
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind of the main service is **Service**. | "" | No | -| `sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind of the main service is **Service**. | 0 | No | -| `strategy` | Load balancing strategy between the main service's servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind of the main service is **Service**. | "RoundRobin" | No | -| `weight` | Service weight.
To use only to refer to WRR TraefikService | "" | No | -| `nativeLB` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind of the main service is **Service**. | false | No | -| `nodePortLB` | Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind of the main service is **Service**. | false | No | -| `maxBodySize` | Maximum size allowed for the body of the request.
If the body is larger, the request is not mirrored.
-1 means unlimited size. | -1 | No | -| `mirrors` | List of mirrored services to target.
It can be any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
More information [here](#services). | | No | -| `mirrors[m].`
`kind`
| Kind of the mirrored service targeted.
Two values allowed:
- **Service**: Kubernetes Service
- **TraefikService**: Traefik Service.
More information [here](#services) | "" | No | -| `mirrors[m].`
`name`
| Mirrored service name.
The character `@` is not authorized. | "" | Yes | -| `mirrors[m].`
`namespace`
| Mirrored service namespace.
More information [here](#services). | "" | No | -| `mirrors[m].`
`port`
| Mirrored service port (number or port name).
Evaluated only if the kind of the mirrored service is **Service**. | "" | No | -| `mirrors[m].`
`percent`
| Part of the traffic to mirror in percent (from 0 to 100) | 0 | No | -| `mirrors[m].`
`responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind of the mirrored service is **Service**. | 100ms | No | -| `mirrors[m].`
`scheme`
| Scheme to use for the request to the mirrored service.
Evaluated only if the kind of the mirrored service is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | -| `mirrors[m].`
`serversTransport`
| Name of ServersTransport resource to use to configure the transport between Traefik and the mirrored service servers.
Evaluated only if the kind of the mirrored service is **Service**. | "" | No | -| `mirrors[m].`
`passHostHeader`
| Forward client Host header to the mirrored service servers.
Evaluated only if the kind of the mirrored service is **Service**. | true | No | -| `mirrors[m].`
`healthCheck.scheme`
| Server URL scheme for the health check endpoint.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `mirrors[m].`
`healthCheck.mode`
| Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "http" | No | -| `mirrors[m].`
`healthCheck.path`
| Server URL path for the health check endpoint.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `mirrors[m].`
`healthCheck.interval`
| Frequency of the health check calls.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "100ms" | No | -| `mirrors[m].`
`healthCheck.unhealthyInterval`
| Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "100ms" | No | -| `mirrors[m].`
`healthCheck.method`
| HTTP method for the health check endpoint.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "GET" | No | -| `mirrors[m].`
`healthCheck.status`
| Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind of the mirrored service is **Service**. | | No | -| `mirrors[m].`
`healthCheck.port`
| URL port for the health check endpoint.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | | No | -| `mirrors[m].`
`healthCheck.timeout`
| Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "5s" | No | -| `mirrors[m].`
`healthCheck.hostname`
| Value in the Host header of the health check request.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `mirrors[m].`
`healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | true | No | -| `mirrors[m].`
`healthCheck.headers`
| Map of header to send to the health check endpoint
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | | No | -| `mirrors[m].`
`sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
Evaluated only if the kind of the mirrored service is **Service**. | "" | No | -| `mirrors[m].`
`sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind of the mirrored service is **Service**. | false | No | -| `mirrors[m].`
`sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind of the mirrored service is **Service**. | false | No | -| `mirrors[m].`
`sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy.
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind of the mirrored service is **Service**. | "" | No | -| `mirrors[m].`
`sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind of the mirrored service is **Service**. | 0 | No | -| `mirrors[m].`
`strategy`
| Load balancing strategy between the servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind of the mirrored service is **Service**. | "RoundRobin" | No | -| `mirrors[m].`
`weight`
| Service weight.
To use only to refer to WRR TraefikService | "" | No | -| `mirrors[m].`
`nativeLB`
| Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind of the mirrored service is **Service**. | false | No | -| `mirrors[m].`
`nodePortLB`
| Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind of the mirrored service is **Service**. | false | No | | `mirrorBody` | Defines whether the request body should be mirrored. | true | No | +| `maxBodySize` | Maximum size allowed for the body of the request.
If the body is larger, the request is not mirrored.
-1 means unlimited size. | -1 | No | +| `mirrors` | List of mirrored services to target.
It can be any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
Exhaustive list of option in the [`Service`](./service.md#configuration-options) documentation. | | Yes | + +#### Mirrored Services Options + +The mirrored services properties are set in the `mirrors` list. + +A mirrored service provides the same options as a [`Service`](./service.md). + +The exhaustive list of the service options is described in the [`Service`](./service.md#configuration-options) documentation. +The mirrorerd service dedicated option are described below. + + +| Field | Description | Default | Required | +|:--------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| +| `mirrors[m].percent` | Traffic percentage to route to the service. | 0 | No | diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 297c90f26..addb2f0ea 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -325,6 +325,7 @@ nav: - 'Kubernetes CRD' : - 'HTTP' : - 'IngressRoute' : 'reference/routing-configuration/kubernetes/crd/http/ingressroute.md' + - 'Service' : 'reference/routing-configuration/kubernetes/crd/http/service.md' - 'TraefikService' : 'reference/routing-configuration/kubernetes/crd/http/traefikservice.md' - 'ServersTransport' : 'reference/routing-configuration/kubernetes/crd/http/serverstransport.md' - 'Middleware' : 'reference/routing-configuration/kubernetes/crd/http/middleware.md' From 5df4c270a7652107f71b47f8dda4985a3936f356 Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 19 Sep 2025 10:36:05 +0200 Subject: [PATCH 08/12] Use client conn to build the proxy protocol header Co-authored-by: Simon Delicata --- pkg/tcp/dialer.go | 27 +++++++++++++------ pkg/tcp/dialer_test.go | 61 ++++++++++++++++++++++++++++++++++++------ pkg/tcp/proxy.go | 8 +++--- 3 files changed, 77 insertions(+), 19 deletions(-) diff --git a/pkg/tcp/dialer.go b/pkg/tcp/dialer.go index 2a754d3b4..bc4855b5b 100644 --- a/pkg/tcp/dialer.go +++ b/pkg/tcp/dialer.go @@ -19,12 +19,20 @@ import ( "github.com/traefik/traefik/v3/pkg/config/dynamic" traefiktls "github.com/traefik/traefik/v3/pkg/tls" "github.com/traefik/traefik/v3/pkg/types" - "golang.org/x/net/proxy" ) -type Dialer interface { - proxy.Dialer +// ClientConn is the interface that provides information about the client connection. +type ClientConn interface { + // LocalAddr returns the local network address, if known. + LocalAddr() net.Addr + // RemoteAddr returns the remote network address, if known. + RemoteAddr() net.Addr +} + +// Dialer is an interface to dial a network connection, with support for PROXY protocol and termination delay. +type Dialer interface { + Dial(network, addr string, clientConn ClientConn) (c net.Conn, err error) TerminationDelay() time.Duration } @@ -34,18 +42,20 @@ type tcpDialer struct { proxyProtocol *dynamic.ProxyProtocol } +// TerminationDelay returns the termination delay duration. func (d tcpDialer) TerminationDelay() time.Duration { return d.terminationDelay } -func (d tcpDialer) Dial(network, addr string) (net.Conn, error) { +// Dial dials a network connection and optionally sends a PROXY protocol header. +func (d tcpDialer) Dial(network, addr string, clientConn ClientConn) (net.Conn, error) { conn, err := d.dialer.Dial(network, addr) if err != nil { return nil, err } - if d.proxyProtocol != nil && d.proxyProtocol.Version > 0 && d.proxyProtocol.Version < 3 { - header := proxyproto.HeaderProxyFromAddrs(byte(d.proxyProtocol.Version), conn.RemoteAddr(), conn.LocalAddr()) + if d.proxyProtocol != nil && clientConn != nil && d.proxyProtocol.Version > 0 && d.proxyProtocol.Version < 3 { + header := proxyproto.HeaderProxyFromAddrs(byte(d.proxyProtocol.Version), clientConn.RemoteAddr(), clientConn.LocalAddr()) if _, err := header.WriteTo(conn); err != nil { _ = conn.Close() return nil, fmt.Errorf("writing PROXY Protocol header: %w", err) @@ -60,8 +70,9 @@ type tcpTLSDialer struct { tlsConfig *tls.Config } -func (d tcpTLSDialer) Dial(network, addr string) (net.Conn, error) { - conn, err := d.tcpDialer.Dial(network, addr) +// Dial dials a network connection with the wrapped tcpDialer and performs a TLS handshake. +func (d tcpTLSDialer) Dial(network, addr string, clientConn ClientConn) (net.Conn, error) { + conn, err := d.tcpDialer.Dial(network, addr, clientConn) if err != nil { return nil, err } diff --git a/pkg/tcp/dialer_test.go b/pkg/tcp/dialer_test.go index 62af030db..07d8a7b12 100644 --- a/pkg/tcp/dialer_test.go +++ b/pkg/tcp/dialer_test.go @@ -160,7 +160,7 @@ func TestNoTLS(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) require.NoError(t, err) _, err = conn.Write([]byte("ping\n")) @@ -209,7 +209,7 @@ func TestTLS(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) require.NoError(t, err) _, err = conn.Write([]byte("ping\n")) @@ -260,7 +260,7 @@ func TestTLSWithInsecureSkipVerify(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) require.NoError(t, err) _, err = conn.Write([]byte("ping\n")) @@ -329,7 +329,7 @@ func TestMTLS(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) require.NoError(t, err) _, err = conn.Write([]byte("ping\n")) @@ -463,7 +463,7 @@ func TestSpiffeMTLS(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) if test.wantError { require.Error(t, err) @@ -510,10 +510,13 @@ func TestProxyProtocol(t *testing.T) { require.NoError(t, err) var version int + var localAddr, remoteAddr string proxyBackendListener := proxyproto.Listener{ Listener: backendListener, ValidateHeader: func(h *proxyproto.Header) error { version = int(h.Version) + localAddr = h.DestinationAddr.String() + remoteAddr = h.SourceAddr.String() return nil }, Policy: func(upstream net.Addr) (proxyproto.Policy, error) { @@ -544,7 +547,18 @@ func TestProxyProtocol(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + clientConn := &fakeClientConn{ + localAddr: &net.TCPAddr{ + IP: net.ParseIP("2.2.2.2"), + Port: 12345, + }, + remoteAddr: &net.TCPAddr{ + IP: net.ParseIP("1.1.1.1"), + Port: 12345, + }, + } + + conn, err := dialer.Dial("tcp", ":"+port, clientConn) require.NoError(t, err) defer conn.Close() @@ -558,6 +572,8 @@ func TestProxyProtocol(t *testing.T) { assert.Equal(t, 4, n) assert.Equal(t, "PONG", string(buf[:4])) assert.Equal(t, test.version, version) + assert.Equal(t, "2.2.2.2:12345", localAddr) + assert.Equal(t, "1.1.1.1:12345", remoteAddr) }) } } @@ -586,10 +602,13 @@ func TestProxyProtocolWithTLS(t *testing.T) { require.NoError(t, err) var version int + var localAddr, remoteAddr string proxyBackendListener := proxyproto.Listener{ Listener: backendListener, ValidateHeader: func(h *proxyproto.Header) error { version = int(h.Version) + localAddr = h.DestinationAddr.String() + remoteAddr = h.SourceAddr.String() return nil }, Policy: func(upstream net.Addr) (proxyproto.Policy, error) { @@ -646,7 +665,18 @@ func TestProxyProtocolWithTLS(t *testing.T) { }, true) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + clientConn := &fakeClientConn{ + localAddr: &net.TCPAddr{ + IP: net.ParseIP("2.2.2.2"), + Port: 12345, + }, + remoteAddr: &net.TCPAddr{ + IP: net.ParseIP("1.1.1.1"), + Port: 12345, + }, + } + + conn, err := dialer.Dial("tcp", ":"+port, clientConn) require.NoError(t, err) defer conn.Close() @@ -660,6 +690,8 @@ func TestProxyProtocolWithTLS(t *testing.T) { assert.Equal(t, 4, n) assert.Equal(t, "PONG", string(buf[:4])) assert.Equal(t, test.version, version) + assert.Equal(t, "2.2.2.2:12345", localAddr) + assert.Equal(t, "1.1.1.1:12345", remoteAddr) }) } } @@ -695,7 +727,7 @@ func TestProxyProtocolDisabled(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) require.NoError(t, err) _, err = conn.Write([]byte("ping")) @@ -709,6 +741,19 @@ func TestProxyProtocolDisabled(t *testing.T) { assert.Equal(t, "PONG", string(buf[:4])) } +type fakeClientConn struct { + remoteAddr *net.TCPAddr + localAddr *net.TCPAddr +} + +func (f fakeClientConn) LocalAddr() net.Addr { + return f.localAddr +} + +func (f fakeClientConn) RemoteAddr() net.Addr { + return f.remoteAddr +} + // fakeSpiffePKI simulates a SPIFFE aware PKI and allows generating multiple valid SVIDs. type fakeSpiffePKI struct { caPrivateKey *rsa.PrivateKey diff --git a/pkg/tcp/proxy.go b/pkg/tcp/proxy.go index 75bdfcd4d..aa979d303 100644 --- a/pkg/tcp/proxy.go +++ b/pkg/tcp/proxy.go @@ -34,7 +34,7 @@ func (p *Proxy) ServeTCP(conn WriteCloser) { // needed because of e.g. server.trackedConnection defer conn.Close() - connBackend, err := p.dialBackend() + connBackend, err := p.dialBackend(conn) if err != nil { log.Error().Err(err).Msg("Error while dialing backend") return @@ -62,8 +62,10 @@ func (p *Proxy) ServeTCP(conn WriteCloser) { <-errChan } -func (p *Proxy) dialBackend() (WriteCloser, error) { - conn, err := p.dialer.Dial("tcp", p.address) +func (p *Proxy) dialBackend(clientConn net.Conn) (WriteCloser, error) { + // The clientConn is passed to the dialer so that it can use information from it if needed, + // to build a PROXY protocol header. + conn, err := p.dialer.Dial("tcp", p.address, clientConn) if err != nil { return nil, err } From 2580d0f95ceb29f71fe882838a5da018fb73b654 Mon Sep 17 00:00:00 2001 From: "Massimiliano D." <126668030+mdeliatf@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:00:44 +0200 Subject: [PATCH 09/12] Update hub-button-app to use a local script Co-authored-by: Firespray-31 <147506444+Firespray-31@users.noreply.github.com> --- .../traefiklabs-hub-button-app/main-v1.js | 22 +++++++- .../traefiklabs-hub-button-app/main-v1.js.map | 2 +- webui/src/App.tsx | 21 +++---- webui/src/contexts/version.tsx | 40 ++++++++++++++ webui/src/hooks/use-version.tsx | 13 ----- webui/src/layout/Navigation.tsx | 55 ++++++++++++++++--- webui/src/types/API.d.ts | 8 +++ 7 files changed, 124 insertions(+), 37 deletions(-) create mode 100644 webui/src/contexts/version.tsx delete mode 100644 webui/src/hooks/use-version.tsx create mode 100644 webui/src/types/API.d.ts diff --git a/webui/public/traefiklabs-hub-button-app/main-v1.js b/webui/public/traefiklabs-hub-button-app/main-v1.js index 9a90cc6b2..e140dab34 100644 --- a/webui/public/traefiklabs-hub-button-app/main-v1.js +++ b/webui/public/traefiklabs-hub-button-app/main-v1.js @@ -1,3 +1,23 @@ /* eslint-disable */ -!function(){var e={110:function(e,t,n){"use strict";var r=n(441),a={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},l={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},o={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},i={};function u(e){return r.isMemo(e)?o:i[e.$$typeof]||a}i[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},i[r.Memo]=o;var s=Object.defineProperty,c=Object.getOwnPropertyNames,f=Object.getOwnPropertySymbols,d=Object.getOwnPropertyDescriptor,p=Object.getPrototypeOf,h=Object.prototype;e.exports=function e(t,n,r){if("string"!==typeof n){if(h){var a=p(n);a&&a!==h&&e(t,a,r)}var o=c(n);f&&(o=o.concat(f(n)));for(var i=u(t),m=u(n),v=0;v