From 8450d2c12b4bf12a9a13da9b6f737d99fe50f441 Mon Sep 17 00:00:00 2001 From: joshua bauer Date: Mon, 15 May 2017 11:49:49 -0700 Subject: [PATCH] Cleanup configuration. Added SSL support. --- .../reference.conf => conf/application.conf | 60 +++++++--- pom.xml | 2 +- .../sinistral/proteus/ProteusApplication.java | 41 ++++++- .../server/endpoints/EndpointInfo.java | 6 +- .../proteus/services/SwaggerService.java | 2 +- .../proteus/utilities/SecurityOps.java | 108 +++++++++++------- src/main/resources/development.cer | Bin 0 -> 1403 bytes src/main/resources/development.jks | Bin 0 -> 3916 bytes src/main/resources/development.ts | Bin 0 -> 1471 bytes .../sinistral/proteus}/proteus-logo.svg | 0 src/main/resources/reference.conf | 22 +++- src/test/resources/development.jks | Bin 0 -> 3916 bytes src/test/resources/development.ts | Bin 0 -> 1471 bytes 13 files changed, 163 insertions(+), 78 deletions(-) rename src/test/resources/reference.conf => conf/application.conf (50%) create mode 100644 src/main/resources/development.cer create mode 100644 src/main/resources/development.jks create mode 100644 src/main/resources/development.ts rename src/main/resources/{ => io/sinistral/proteus}/proteus-logo.svg (100%) create mode 100644 src/test/resources/development.jks create mode 100644 src/test/resources/development.ts diff --git a/src/test/resources/reference.conf b/conf/application.conf similarity index 50% rename from src/test/resources/reference.conf rename to conf/application.conf index d5f39d1..576dc9f 100644 --- a/src/test/resources/reference.conf +++ b/conf/application.conf @@ -6,32 +6,23 @@ application { version = "1.0" name="proteus" - # tmpdir - tmpdir =${java.io.tmpdir} - # path (a.k.a. as contextPath) path = "/v1" - # localhost host = "localhost" + + ports { + http = 8090 + https = 8443 + } - # HTTP ports - port = 8090 - - # uncomment to enabled HTTPS - # securePort = 8443 - - # we do UTF-8 charset = UTF-8 - - # date format - dateFormat = dd-MMM-yyyy - + fallbackHandler = "io.sinistral.proteus.server.handlers.ServerFallbackHandler" defaultResponseListener = "io.sinistral.proteus.server.handlers.ServerDefaultResponseListener" - - redirect_https = "" + + tmpdir = ${java.io.tmpdir}/${application.name} } @@ -45,11 +36,19 @@ globalHeaders Server = ${application.name} } +health { + statusPath = "/internal/status" +} + + assets { + # the base path assets will be server from path = "/public" + # the directory to load the assets from dir = "./assets" cache { + # cache timeout for the assets time = 500 } } @@ -57,17 +56,29 @@ assets { swagger { - swagger: "2.0" + # the path that has an index.html template and theme css files + resourcePrefix="io/sinistral/proteus/swagger" + # swagger version + swagger="2.0" info { + # swagger info title title = ${application.name} + # swagger info version version = ${application.version} } + # swagger-ui theme from ostranme's swagger-ui-themes, the following are built-in [feeling-blue, flattop, material, monokai, muted, newspaper, outline] + # specifying a different name causes the SwaggerService to search in {swagger.resourcePrefix}/themes for a file named "theme-{swagger.theme}.css" theme="default" + # where the swagger endpoints will be mounted basePath= ${application.path}"/swagger" + # where redoc will be mounted relative to swagger base path + redocPath= "redoc" + #the name of the spec file specFilename="swagger.json" consumes = ["application/json"] produces = ["application/json"] - schemes = ["http"] + port = ${application.ports.http} + } undertow @@ -84,6 +95,17 @@ undertow socket { backlog = 10000 } + + + ssl { + enabled=true + keystorePath="development.jks" + truststorePath="development.ts" + keystorePassword="password" + truststorePassword="password" + } + + # x AvailableProcessors ioThreads = 16 workerThreads = 200 diff --git a/pom.xml b/pom.xml index b1ffcea..7d1cd24 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ src/main/resources - true + false diff --git a/src/main/java/io/sinistral/proteus/ProteusApplication.java b/src/main/java/io/sinistral/proteus/ProteusApplication.java index 1830c11..8d32ad3 100644 --- a/src/main/java/io/sinistral/proteus/ProteusApplication.java +++ b/src/main/java/io/sinistral/proteus/ProteusApplication.java @@ -3,6 +3,7 @@ */ package io.sinistral.proteus; import java.net.URL; +import java.security.KeyStore; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -40,6 +41,7 @@ import io.sinistral.proteus.server.handlers.HandlerGenerator; import io.sinistral.proteus.services.AssetsService; import io.sinistral.proteus.services.SwaggerService; +import io.sinistral.proteus.utilities.SecurityOps; import io.undertow.Undertow; import io.undertow.UndertowOptions; import io.undertow.server.HttpHandler; @@ -78,6 +80,7 @@ public class ProteusApplication protected Class rootHandlerClass; protected HttpHandler rootHandler; protected AtomicBoolean running = new AtomicBoolean(false); + protected List ports = new ArrayList<>(); public ProteusApplication() @@ -229,19 +232,45 @@ public void buildServer() handler = rootHandler; } - this.undertow = Undertow.builder() - .addHttpListener(config.getInt("application.port"),config.getString("application.host")) + Undertow.Builder undertowBuilder = Undertow.builder() + .addHttpListener(config.getInt("application.ports.http"),config.getString("application.host")) .setBufferSize(16 * 1024) .setIoThreads( config.getInt("undertow.ioThreads") ) - .setServerOption(UndertowOptions.ENABLE_HTTP2, true) + .setServerOption(UndertowOptions.ENABLE_HTTP2, false) .setServerOption(UndertowOptions.ALWAYS_SET_DATE, true) .setSocketOption(org.xnio.Options.BACKLOG, config.getInt("undertow.socket.backlog") ) .setServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, false) .setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, false) .setServerOption(UndertowOptions.MAX_ENTITY_SIZE, config.getBytes("undertow.server.maxEntitySize") ) .setWorkerThreads( config.getInt("undertow.workerThreads") ) - .setHandler( handler ) - .build(); + .setHandler( handler ); + + ports.add(config.getInt("application.ports.http")); + + if( config.getBoolean("undertow.ssl.enabled") ) + { + try + { + KeyStore keyStore = SecurityOps.loadKeyStore(config.getString("undertow.ssl.keystorePath"), config.getString("undertow.ssl.keystorePassword") ); + KeyStore trustStore = SecurityOps.loadKeyStore(config.getString("undertow.ssl.truststorePath"), config.getString("undertow.ssl.truststorePassword") ); + + + undertowBuilder.addHttpsListener(config.getInt("application.ports.https"), config.getString("application.host"), + SecurityOps.createSSLContext( + keyStore, + trustStore, + config.getString("undertow.ssl.keystorePassword") + )); + + ports.add(config.getInt("application.ports.https")); + + } catch (Exception e) + { + log.error(e.getMessage(),e); + } + } + + this.undertow = undertowBuilder.build(); } @@ -327,7 +356,7 @@ public void printStatus() sb.append("\n"); - sb.append("\nListening on port " + config.getInt("application.port")); + sb.append("\nListening on: " + this.ports); sb.append("\n"); diff --git a/src/main/java/io/sinistral/proteus/server/endpoints/EndpointInfo.java b/src/main/java/io/sinistral/proteus/server/endpoints/EndpointInfo.java index 55aef6c..c1ac903 100644 --- a/src/main/java/io/sinistral/proteus/server/endpoints/EndpointInfo.java +++ b/src/main/java/io/sinistral/proteus/server/endpoints/EndpointInfo.java @@ -153,10 +153,10 @@ public int hashCode() public int compareTo(EndpointInfo other) { return new CompareToBuilder() + .append(this.pathTemplate, other.pathTemplate) .append(this.controllerName, other.controllerName) - .append(this.method, other.method) .append(this.controllerMethod, other.controllerMethod) - .append(this.pathTemplate, other.pathTemplate) + .append(this.method, other.method) .toComparison(); } @@ -166,7 +166,7 @@ public int compareTo(EndpointInfo other) { @Override public String toString() { - return String.format("%-8s %-40s %-26s %-26s %s", this.method, this.pathTemplate, "[" + this.consumes + "]", "[" + this.produces+ "]", "("+this.controllerName+"."+this.controllerMethod+ ")"); + return String.format("\t%-8s %-40s %-26s %-26s %s", this.method, this.pathTemplate, "[" + this.consumes + "]", "[" + this.produces+ "]", "("+this.controllerName+"."+this.controllerMethod+ ")"); } /** diff --git a/src/main/java/io/sinistral/proteus/services/SwaggerService.java b/src/main/java/io/sinistral/proteus/services/SwaggerService.java index 0141ac3..10a2227 100644 --- a/src/main/java/io/sinistral/proteus/services/SwaggerService.java +++ b/src/main/java/io/sinistral/proteus/services/SwaggerService.java @@ -94,7 +94,7 @@ public class SwaggerService extends BaseService implements Supplierm_0Nv zJ1@UH4<^EaA;OL!!i6EijUmDe7cr0%=QT7pFf}waG&3+UF^>}GH8M9aHZp;7>Fk;& zCMD$HU}R-rZerqRFlb`pVrpVyWN1FT-1zlN!<#o(o}9YUXUk_t&7J8*=M!9uShqRd zE|X^rePZkq$2xE2i!X}y7xJ$(g+1^$O54p@-CCl0=He^=l91cCf6SVyd%8Q!O|_9t z)@#)*(SuoCoU2W_+4fS_YvpWq)rW^d zPwzavuf9+6oi)d{(qDU{w7!NXi zq3m^(4*%jAo0Li$4*S@IW?H@qpMEu1>**oxH>Q8wjxMWwzSy4M`CI3&mA@1&@d(X$ zGV#>cun)D2k5-B0^hZaCo39T)EFmHAN+_=_PDLLmyeA1l&ii);OHFfs{lVN^U{&Aj4DTC9_q;s# zmTiI5o|+w2PuRAkr7YiF_C6r=(cLB`Zr<0oSW6E7Yix5|)0LQbl8Kp-fpM{-fxLk% zFo($Uv52vVFkENVT%I`Nuh)uWdhw5nQ)a3}yhKh!z&r&^M2rlU=DlLdvqZV=KXmv$ z+~%3CXv%4_C)K-9xs=mb({Xb{gtK#NMuXA=Vc*$O!AzQepYVz4u4%43QT4d0Ze@1o zY0>boElXZYeOM#>>+4^o7k_hNqt`v>l4Lw$Gx3H(;1st-0(YD$u1b{j9DSHtvVbbW8L#db<%Y=QMdZ>ZHQP_FU!n$7kPdR$tok z#?Qvd=a=>pmX7N1eWy2_$hP|T$~kiSn?FU5z0>ZMH~&p&SLF2ZT0h5Rw(tJC;`4)U zSF6sdV!6LEIr8)$CJnZ_C;#`$sNPLD`(*B7=WU5!?FG3K)l7X2>#z6UbMij%f3}vR zpnuNqr@J*5#z&i-I4PvAStM2_B0Sr4M-jX68s4V(vnQgo4E4*L%y-!y&%4*KW1|>n z%x&>=`A2(dwbk1mpJ|HKDu>vJ5vN#9jxUAj}c8`%Y6Dd}8Vgrz%_Se9BsU||uE4oN|3RZSxk{}!dY2in8eALZwP4F1dP zspd8_008oM2$nn+LIb9d022X;#EI#Nh(G`!mfVj6l}ey6X`#3i)e+~HR7*0+kJ2!Y zdf4}Q*SVGx6Gr9auNz+QHnb#R#b{4fh=;@Xj};d?ki` zGKqsc|N2YjZj8F1(=@@Z=2!9&*Ttf5&O*p8Tv2V@Tp)3)BNbGiGD=IsSNLJy4}5k) z;lz-u{HovBl7dpAf@%lCalv+;_vy-RKB4ylti;>O`<%LdZ!nvw-#P_CGZ7Sj`gTx) zZ>F|@k^tNlZP6r`_P2$5Ga-H9!Qg#$AibXZ0nosKHsmnczN|o)OS<>!=vK5?|bDto&+U-fLCGCKL6O7-;F`tV$mHAvJ%Yby}VzTTAqdvgoDTEdJ@w zX$D$2TB_x|zO}{ECbq~asu_!l8{-7C~fDe1!&Ks{DsNdGg zQ@{SRN|yaQ*;QhDxW}DCj-{h}ZX7@<6>XkrV;}kCiZ$5cF$4c+Ow9VE%`L-3zYpOK z#d}3dg=~F@Ump?bYzQ}=8x{yWPHnQuEo>gjt}ASyE+?3E<|>?BqfUyRZMv#4!3AX> z0lN$6f*Z`HQRZ?sI!8F7os+h!vL8R`Gtrd&`i5GDo(~INh|*ffa`jCx2e2J{#C(4n z1TL&7IvZY28mEIyFDakM2iR)*(ER#D`DR?(JF%W>Vh;BZAExaYd8PN#Ix77GTIcDw zeEZq!O{H4ISTnEtAl`ozEVGvp!2BvFz^jUVQXn{WZAWHJU;13FH{Wa5W?*gG6{zw( zn60L_)+cie*I*EtxhzN=ivLw4*y(8I`A&7t8S`q%)*6m^D|MQd)6;zLrQ-|Z*oL{S z3rElBFKxL8@R-&KSH#RV-RsPCwxRSN_|WA0YOG^=sW5}fG7CIL_gsMct^%EDUiV$O zOcs0;aREWUE3FPU2)IjoBvM+{`3FN>URJknBr93Q!5idYB6f!MoypOr~ z-AvtlWXbPsFBQ%Slhv1){vp|y4X)8>DtP#n+k^prGtl(@nmN7l3YQ=i_^CDY?yAVCHBF8QCOmr48>wDw-u>zgn6IBfqXI_4N zQqutDu>-AnVah_&Z}(c7Kd6def{$Jw3(rPqo9|Go)0}{r~Zrd0aq!)WQo2|>{G8F@$?M#M6l{c_YJp)4U_LMIJpdi?|p#QZkGkjyuA9YOmE zN^M67tyu3I#j(mo<8o_LE0sfoqsW1`sr6i=SC7bu$KsM-@KwBrZoH8%_#MlHD0r=^ zFLB<$oWPS?zSjEa6>`nnEmg^)=Vuk1MvH9f$wA^hz%Z?ay3_k{a>)g-v1HCo>nl9A zitf}sZ5-q#3gb4DUI_a}uDU!|w-cx1J`J?Y}Ueib{TCb2L0ayR1Tz8ik8~2Kx7edyhvJQjk1UJg2`TU9^%ej4ns+W~dk@U+3XAC} z=W?LEj|IWWt;5e`MT*rdiu&X@4b{Z59w$C8O1a9(3C<9)3SFoQkP=0j#;k8{_D_@A z7>GVxe==+JCm<>4YZP0el*Px#YQ57gRUEJ4yugg0BGpe+(@J%;%*~z(PYI-AKn=hh zKI+88uFWPHl3Vc6UQr*+=a%lz6&g~}(D3TKyT_TJdYOJkK^7I}h?=Th&So;3h51O_ zWwv=&4@mzZz2}yPStZqi#^^v+Va?6wX{=qF*Y+kEGg37^HVtiqUgQQhw13EUX+YSxm1c zE{8?5KI#>e;0Rop*oNZy%wb>Am?f|4Deg(>lM1vAE-my9q4uA6(S%5t&EF-1x1n z=V1BAmYLCh`pjXpue7xLGaiMCJl;A`K1Q0Ofj5S16+2hyl#SC}{J`ieSRm4OK<`t) z!WP%5a0CA-$%!pRd;MAE9kQ$)5>}2-M7m{xY5%M%eP-!&0RX5snG*OmZgGvNh>7gg zO4l~U*2m24g|8(|w{zj~0749D%&w`>N*kRaKqkRkzDm<6PYZ7}WP``wCoT%bg#Z(0 zjWauF8E%p3=V(gPcG75z2UCeG)GOK2S2K|WzDxwoAUuB(^43C~^?h_XsVcJgr zUT7fen{p6E008s=f(3;_up~}pB*a9-#6Z9M!BP-vFu8bwe!_2RA|gr<0OAX!`@7mm zEP-G~QcI){(mxdW|Ag!xg!CVT;vaei0{{Mc*-@!_PC8h;Hu*4L^SYjeTTz{qP&KC6R*XrTiYNMtj4Y4+_z)#LP zfuLs1c{C4^<*KZKYROc_vxEFWe6Y~y#I|Yh-TC>mC%IB1DOS2d zv0yHE%^Y)|cQSdc9EGlthtjk(c-b_Bvbgc7kGaPJE-A@ajFS699IDOxLBL2cvK=K z(U8uf$I#Um3u(~+D*@CUT=t<_(eoApd7@_WHjM5>^(M(hem zEJ1`N|4^zCs*)6m_usE`0HpZdn>cV4^qOx=&y!SU7HV&Qr?;F2K=0T}(&7r<@Q1|o z8>v{{SKLPA&sd1By`$WfyVMXD2Cbep3-&y{okJLmt|q_8uwS*|Q# z`6;P)=0L&ygxCa)x?~rZVPX;@01=K4!UN&@+Yl}qAPbNQFbfi^bSb!kSG~XKv=ZcA zC}_R)-y!q<4GG1;r@Zysrzt!ns7$6WrE-R+d7-Oxa6@M>= ze~Hn!h~0u%>}r*UMXD|?d??~aM0B-J(g-ucs_EGd`(Yj9`N~{%dCs} z9kmjUL%c{qxx=X>-VZ?yx~Xn|zY)vkh|-8s$kJz$9K?Phgp^{U-9mF`mURnH1@}gN z3WaD2g25Yt7eCj3tJF?5?V6})8l6i#13r3y=pK19K#sA)KKT_mnSZ|Zxo)*)8zWG6o~%pf8b$bx2KERt&tB$chB#yX7+*@{D@(VDRK z+aHjNF|7?O`@y}P)x(^r#Q?s#*-&aCSG&$17mT0c8kK1)I4^@IsA ztd|1Io|b3de%|_v#i4w6+5hW@m+LC5IQ4XIGnev`ly9*|_vSnI=LvtFr>niOPt09t z@4-fc?T&0#4ED?$;@3ZVv3hE!^A@)B-Al{Z_jW0rPMIPd`$$>j zHT_c;oBly}v!P9A&5itbYhoF?b22WzPf|(0y|Ztcj-@Fym%=3;p&3smp86X0p_cK{ zDzTjY=m>H1_2GvlBm`aw<+a60$@{Uj1QqeDDmbpaQ}<}tzxIMY<^K*76ej!Mo9@Qr z@IfurAo|DM`H$+aM!cQ(Btgh||IT2kY3{E-n41f%>YJV6edFk!mj~anEs)w%v%~5M z+m^JH<-5z?2ZTPl+oZ(J`}!7Z$>D#EZH{ZY5))4{F*7nSE><*#Ul~6KDMOT5(J-{!wwtOqGb2$cYG;r+|rwk-^fuS4?@9D7XEG4&R5{Jku3TIZgJY zdKW5}avEzoZf=Nhc5cmRPN|nMYjp3piUG_LL=? zRYmRd8o$0(ha+UHd;X|Sy6z_GR{y>BQRU+BW!(iSUOd{zP8fRb?7PkCOIzOf**N+9(q6*S zQ60YT^rjQpR{vf(M^1n9r|7YF+P(7TzX|P%oIYOb=a|g)-G5hne$ee|)mc?6_g5xI zp8mt6!B+R=|9%G-S1R%#Ir80i4{%$U$fSG=i3#02lJk8YhS%f@vz@ycgxqC z6DK`g`<`!Sir}il74LQ&h%hs|;iI_s_xUAj}c8`%Y6Dd}8Vgrz%_Se9BsU||uE4oN|3RZSxk{}!dY2in8eALZwP4F1dP zspd8_008oM2$nn+LIb9d022X;#EI#Nh(G`!mfVj6l}ey6X`#3i)e+~HR7*0+kJ2!Y zdf4}Q*SVGx6Gr9auNz+QHnb#R#b{4fh=;@Xj};d?ki` zGKqsc|N2YjZj8F1(=@@Z=2!9&*Ttf5&O*p8Tv2V@Tp)3)BNbGiGD=IsSNLJy4}5k) z;lz-u{HovBl7dpAf@%lCalv+;_vy-RKB4ylti;>O`<%LdZ!nvw-#P_CGZ7Sj`gTx) zZ>F|@k^tNlZP6r`_P2$5Ga-H9!Qg#$AibXZ0nosKHsmnczN|o)OS<>!=vK5?|bDto&+U-fLCGCKL6O7-;F`tV$mHAvJ%Yby}VzTTAqdvgoDTEdJ@w zX$D$2TB_x|zO}{ECbq~asu_!l8{-7C~fDe1!&Ks{DsNdGg zQ@{SRN|yaQ*;QhDxW}DCj-{h}ZX7@<6>XkrV;}kCiZ$5cF$4c+Ow9VE%`L-3zYpOK z#d}3dg=~F@Ump?bYzQ}=8x{yWPHnQuEo>gjt}ASyE+?3E<|>?BqfUyRZMv#4!3AX> z0lN$6f*Z`HQRZ?sI!8F7os+h!vL8R`Gtrd&`i5GDo(~INh|*ffa`jCx2e2J{#C(4n z1TL&7IvZY28mEIyFDakM2iR)*(ER#D`DR?(JF%W>Vh;BZAExaYd8PN#Ix77GTIcDw zeEZq!O{H4ISTnEtAl`ozEVGvp!2BvFz^jUVQXn{WZAWHJU;13FH{Wa5W?*gG6{zw( zn60L_)+cie*I*EtxhzN=ivLw4*y(8I`A&7t8S`q%)*6m^D|MQd)6;zLrQ-|Z*oL{S z3rElBFKxL8@R-&KSH#RV-RsPCwxRSN_|WA0YOG^=sW5}fG7CIL_gsMct^%EDUiV$O zOcs0;aREWUE3FPU2)IjoBvM+{`3FN>URJknBr93Q!5idYB6f!MoypOr~ z-AvtlWXbPsFBQ%Slhv1){vp|y4X)8>DtP#n+k^prGtl(@nmN7l3YQ=i_^CDY?yAVCHBF8QCOmr48>wDw-u>zgn6IBfqXI_4N zQqutDu>-AnVah_&Z}(c7Kd6def{$Jw3(rPqo9|Go)0}{r~Zrd0aq!)WQo2|>{G8F@$?M#M6l{c_YJp)4U_LMIJpdi?|p#QZkGkjyuA9YOmE zN^M67tyu3I#j(mo<8o_LE0sfoqsW1`sr6i=SC7bu$KsM-@KwBrZoH8%_#MlHD0r=^ zFLB<$oWPS?zSjEa6>`nnEmg^)=Vuk1MvH9f$wA^hz%Z?ay3_k{a>)g-v1HCo>nl9A zitf}sZ5-q#3gb4DUI_a}uDU!|w-cx1J`J?Y}Ueib{TCb2L0ayR1Tz8ik8~2Kx7edyhvJQjk1UJg2`TU9^%ej4ns+W~dk@U+3XAC} z=W?LEj|IWWt;5e`MT*rdiu&X@4b{Z59w$C8O1a9(3C<9)3SFoQkP=0j#;k8{_D_@A z7>GVxe==+JCm<>4YZP0el*Px#YQ57gRUEJ4yugg0BGpe+(@J%;%*~z(PYI-AKn=hh zKI+88uFWPHl3Vc6UQr*+=a%lz6&g~}(D3TKyT_TJdYOJkK^7I}h?=Th&So;3h51O_ zWwv=&4@mzZz2}yPStZqi#^^v+Va?6wX{=qF*Y+kEGg37^HVtiqUgQQhw13EUX+YSxm1c zE{8?5KI#>e;0Rop*oNZy%wb>Am?f|4Deg(>lM1vAE-my9q4uA6(S%5t&EF-1x1n z=V1BAmYLCh`pjXpue7xLGaiMCJl;A`K1Q0Ofj5S16+2hyl#SC}{J`ieSRm4OK<`t) z!WP%5a0CA-$%!pRd;MAE9kQ$)5>}2-M7m{xY5%M%eP-!&0RX5snG*OmZgGvNh>7gg zO4l~U*2m24g|8(|w{zj~0749D%&w`>N*kRaKqkRkzDm<6PYZ7}WP``wCoT%bg#Z(0 zjWauF8E%p3=V(gPcG75z2UCeG)GOK2S2K|WzDxwoAUuB(^43C~^?h_XsVcJgr zUT7fen{p6E008s=f(3;_up~}pB*a9-#6Z9M!BP-vFu8bwe!_2RA|gr<0OAX!`@7mm zEP-G~QcI){(mxdW|Ag!xg!CVT;vaei0{{Mc*-@!_PC8h;Hu*4L^SYjeTTz{qP&KC6R*XrTiYNMtj4Y4+_z)#LP zfuLs1c{C4^<*KZKYROc_vxEFWe6Y~y#I|Yh-TC>mC%IB1DOS2d zv0yHE%^Y)|cQSdc9EGlthtjk(c-b_Bvbgc7kGaPJE-A@ajFS699IDOxLBL2cvK=K z(U8uf$I#Um3u(~+D*@CUT=t<_(eoApd7@_WHjM5>^(M(hem zEJ1`N|4^zCs*)6m_usE`0HpZdn>cV4^qOx=&y!SU7HV&Qr?;F2K=0T}(&7r<@Q1|o z8>v{{SKLPA&sd1By`$WfyVMXD2Cbep3-&y{okJLmt|q_8uwS*|Q# z`6;P)=0L&ygxCa)x?~rZVPX;@01=K4!UN&@+Yl}qAPbNQFbfi^bSb!kSG~XKv=ZcA zC}_R)-y!q<4GG1;r@Zysrzt!ns7$6WrE-R+d7-Oxa6@M>= ze~Hn!h~0u%>}r*UMXD|?d??~aM0B-J(g-ucs_EGd`(Yj9`N~{%dCs} z9kmjUL%c{qxx=X>-VZ?yx~Xn|zY)vkh|-8s$kJz$9K?Phgp^{U-9mF`mURnH1@}gN z3WaD2g25Yt7eCj3tJF?5?V6})8l6i#13r3y=pK19K#sA)KKT_mnSZ|Zxo)*)8zWG6o~%pf8b$bx2KERt&tB$chB#yX7+*@{D@(VDRK z+aHjNF|7?O`@y}P)x(^r#Q?s#*-&aCSG&$17mT0c8kK1)I4^@IsA ztd|1Io|b3de%|_v#i4w6+5hW@m+LC5IQ4XIGnev`ly9*|_vSnI=LvtFr>niOPt09t z@4-fc?T&0#4ED?$;@3ZVv3hE!^A@)B-Al{Z_jW0rPMIPd`$$>j zHT_c;oBly}v!P9A&5itbYhoF?b22WzPf|(0y|Ztcj-@Fym%=3;p&3smp86X0p_cK{ zDzTjY=m>H1_2GvlBm`aw<+a60$@{Uj1QqeDDmbpaQ}<}tzxIMY<^K*76ej!Mo9@Qr z@IfurAo|DM`H$+aM!cQ(Btgh||IT2kY3{E-n41f%>YJV6edFk!mj~anEs)w%v%~5M z+m^JH<-5z?2ZTPl+oZ(J`}!7Z$>D#EZH{ZY5))4{F*7nSE><*#Ul~6KDMOT5(J-{!wwtOqGb2$cYG;r+|rwk-^fuS4?@9D7XEG4&R5{Jku3TIZgJY zdKW5}avEzoZf=Nhc5cmRPN|nMYjp3piUG_LL=? zRYmRd8o$0(ha+UHd;X|Sy6z_GR{y>BQRU+BW!(iSUOd{zP8fRb?7PkCOIzOf**N+9(q6*S zQ60YT^rjQpR{vf(M^1n9r|7YF+P(7TzX|P%oIYOb=a|g)-G5hne$ee|)mc?6_g5xI zp8mt6!B+R=|9%G-S1R%#Ir80i4{%$U$fSG=i3#02lJk8YhS%f@vz@ycgxqC z6DK`g`<`!Sir}il74LQ&h%hs|;iI_s_