From 0267513dcada5a743cacf2d78814bae7a3dce8cb Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 25 Jun 2025 10:40:06 -0700 Subject: [PATCH 1/3] Update factory --- .../io/split/client/SplitFactoryImpl.java | 35 +++++++++++++++- .../io/split/client/SplitFactoryImplTest.java | 40 ++++++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java index 9932cbf8..978f1603 100644 --- a/client/src/main/java/io/split/client/SplitFactoryImpl.java +++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java @@ -92,6 +92,7 @@ import io.split.telemetry.synchronizer.TelemetrySynchronizer; import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.BearerToken; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.config.RequestConfig; @@ -113,11 +114,14 @@ import org.slf4j.LoggerFactory; import pluggable.CustomStorageWrapper; +import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.security.KeyStore; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import java.util.HashSet; @@ -518,8 +522,28 @@ public boolean isDestroyed() { protected static SplitHttpClient buildSplitHttpClient(String apiToken, SplitClientConfig config, SDKMetadata sdkMetadata, RequestDecorator requestDecorator) throws URISyntaxException { + + SSLContext sslContext; + if (config.proxyMTLSAuth() != null) { + _log.debug("Proxy setup using mTLS"); + try { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + InputStream keystoreStream = java.nio.file.Files.newInputStream(Paths.get(config.proxyMTLSAuth().getP12File())); + keyStore.load(keystoreStream, config.proxyMTLSAuth().getP12FilePassKey().toCharArray()); + sslContext = SSLContexts.custom() + .loadKeyMaterial(keyStore, config.proxyMTLSAuth().getP12FilePassKey().toCharArray()) + .build(); + } catch (Exception e) { + _log.error("Exception caught while processing p12 file for Proxy mTLS auth: ", e); + _log.warn("Ignoring p12 mTLS config and switching to default context"); + sslContext = SSLContexts.createSystemDefault(); + } + } else { + sslContext = SSLContexts.createSystemDefault(); + } + SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create() - .setSslContext(SSLContexts.createSystemDefault()) + .setSslContext(sslContext) .setTlsVersions(TLS.V_1_1, TLS.V_1_2) .build(); @@ -604,6 +628,15 @@ private static HttpClientBuilder setupProxy(HttpClientBuilder httpClientbuilder, httpClientbuilder.setDefaultCredentialsProvider(credsProvider); } + if (config.proxyToken() != null) { + _log.debug("Proxy setup using token"); + BasicCredentialsProvider credsProvider = new BasicCredentialsProvider(); + AuthScope siteScope = new AuthScope(config.proxy().getHostName(), config.proxy().getPort()); + Credentials siteCreds = new BearerToken(config.proxyToken()); + credsProvider.setCredentials(siteScope, siteCreds); + httpClientbuilder.setDefaultCredentialsProvider(credsProvider); + } + return httpClientbuilder; } diff --git a/client/src/test/java/io/split/client/SplitFactoryImplTest.java b/client/src/test/java/io/split/client/SplitFactoryImplTest.java index a6da1069..2a691a4f 100644 --- a/client/src/test/java/io/split/client/SplitFactoryImplTest.java +++ b/client/src/test/java/io/split/client/SplitFactoryImplTest.java @@ -1,5 +1,6 @@ package io.split.client; +import io.split.client.dtos.ProxyMTLSAuth; import io.split.client.impressions.ImpressionsManager; import io.split.client.utils.FileTypeEnum; import io.split.integrations.IntegrationsConfig; @@ -10,7 +11,6 @@ import junit.framework.TestCase; import org.awaitility.Awaitility; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; import static org.mockito.Mockito.when; @@ -102,9 +102,45 @@ public void testFactoryInstantiationWithProxy() throws Exception { .proxyHost(ENDPOINT) .build(); SplitFactoryImpl splitFactory = new SplitFactoryImpl(API_KEY, splitClientConfig); - assertNotNull(splitFactory.client()); assertNotNull(splitFactory.manager()); + + splitClientConfig = SplitClientConfig.builder() + .enableDebug() + .impressionsMode(ImpressionsManager.Mode.DEBUG) + .impressionsRefreshRate(1) + .endpoint(ENDPOINT,EVENTS_ENDPOINT) + .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) + .authServiceURL(AUTH_SERVICE) + .setBlockUntilReadyTimeout(1000) + .proxyPort(6060) + .proxyToken("12345") + .proxyHost(ENDPOINT) + .build(); + SplitFactoryImpl splitFactory2 = new SplitFactoryImpl(API_KEY, splitClientConfig); + assertNotNull(splitFactory2.client()); + assertNotNull(splitFactory2.manager()); + + splitClientConfig = SplitClientConfig.builder() + .enableDebug() + .impressionsMode(ImpressionsManager.Mode.DEBUG) + .impressionsRefreshRate(1) + .endpoint(ENDPOINT,EVENTS_ENDPOINT) + .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) + .authServiceURL(AUTH_SERVICE) + .setBlockUntilReadyTimeout(1000) + .proxyPort(6060) + .proxyScheme("https") + .proxyMtlsAuth(new ProxyMTLSAuth.Builder().proxyP12File("file").proxyP12FilePassKey("pass").build()) + .proxyHost(ENDPOINT) + .build(); + SplitFactoryImpl splitFactory3 = new SplitFactoryImpl(API_KEY, splitClientConfig); + assertNotNull(splitFactory3.client()); + assertNotNull(splitFactory3.manager()); + + splitFactory.destroy(); + splitFactory2.destroy(); + splitFactory3.destroy(); } @Test From 436dc2aec2a6cae6a2fe488c3b00d36410d93f08 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 25 Jun 2025 16:01:51 -0700 Subject: [PATCH 2/3] Added proxy tests --- .../io/split/client/SplitFactoryImplTest.java | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/client/src/test/java/io/split/client/SplitFactoryImplTest.java b/client/src/test/java/io/split/client/SplitFactoryImplTest.java index 2a691a4f..c99c2ff0 100644 --- a/client/src/test/java/io/split/client/SplitFactoryImplTest.java +++ b/client/src/test/java/io/split/client/SplitFactoryImplTest.java @@ -4,11 +4,21 @@ import io.split.client.impressions.ImpressionsManager; import io.split.client.utils.FileTypeEnum; import io.split.integrations.IntegrationsConfig; +import io.split.service.SplitHttpClientImpl; import io.split.storages.enums.OperationMode; import io.split.storages.pluggable.domain.UserStorageWrapper; import io.split.telemetry.storage.TelemetryStorage; import io.split.telemetry.synchronizer.TelemetrySynchronizer; import junit.framework.TestCase; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.BearerToken; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.MinimalHttpClient; +import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; +import org.apache.hc.core5.http.HttpHost; import org.awaitility.Awaitility; import org.junit.Assert; import org.junit.Test; @@ -24,6 +34,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URISyntaxException; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -105,6 +116,37 @@ public void testFactoryInstantiationWithProxy() throws Exception { assertNotNull(splitFactory.client()); assertNotNull(splitFactory.manager()); + Field splitHttpClientField = SplitFactoryImpl.class.getDeclaredField("_splitHttpClient"); + splitHttpClientField.setAccessible(true); + SplitHttpClientImpl client = (SplitHttpClientImpl) splitHttpClientField.get(splitFactory); + + Field httpClientField = SplitHttpClientImpl.class.getDeclaredField("_client"); + httpClientField.setAccessible(true); + Class InternalHttp = Class.forName("org.apache.hc.client5.http.impl.classic.InternalHttpClient"); + + Field routePlannerField = InternalHttp.getDeclaredField("routePlanner"); + routePlannerField.setAccessible(true); + DefaultProxyRoutePlanner routePlanner = (DefaultProxyRoutePlanner) routePlannerField.get(InternalHttp.cast(httpClientField.get(client))); + + Field proxyField = DefaultProxyRoutePlanner.class.getDeclaredField("proxy"); + proxyField.setAccessible(true); + HttpHost proxy = (HttpHost) proxyField.get(routePlanner); + + Assert.assertEquals("http", proxy.getSchemeName()); + Assert.assertEquals(ENDPOINT, proxy.getHostName()); + Assert.assertEquals(6060, proxy.getPort()); + + Field credentialsProviderField = InternalHttp.getDeclaredField("credentialsProvider"); + credentialsProviderField.setAccessible(true); + BasicCredentialsProvider credentialsProvider = (BasicCredentialsProvider) credentialsProviderField.get(InternalHttp.cast(httpClientField.get(client))); + + Field credMapField = BasicCredentialsProvider.class.getDeclaredField("credMap"); + credMapField.setAccessible(true); + ConcurrentHashMap credMap = (ConcurrentHashMap) credMapField.get(credentialsProvider); + + Assert.assertEquals("test", credMap.entrySet().stream().iterator().next().getValue().getUserName()); + assertNotNull(credMap.entrySet().stream().iterator().next().getValue().getUserPassword()); + splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) @@ -114,13 +156,31 @@ public void testFactoryInstantiationWithProxy() throws Exception { .authServiceURL(AUTH_SERVICE) .setBlockUntilReadyTimeout(1000) .proxyPort(6060) - .proxyToken("12345") + .proxyToken("123456789") .proxyHost(ENDPOINT) .build(); SplitFactoryImpl splitFactory2 = new SplitFactoryImpl(API_KEY, splitClientConfig); assertNotNull(splitFactory2.client()); assertNotNull(splitFactory2.manager()); + Field splitHttpClientField2 = SplitFactoryImpl.class.getDeclaredField("_splitHttpClient"); + splitHttpClientField2.setAccessible(true); + SplitHttpClientImpl client2 = (SplitHttpClientImpl) splitHttpClientField2.get(splitFactory2); + + Field httpClientField2 = SplitHttpClientImpl.class.getDeclaredField("_client"); + httpClientField2.setAccessible(true); + Class InternalHttp2 = Class.forName("org.apache.hc.client5.http.impl.classic.InternalHttpClient"); + + Field credentialsProviderField2 = InternalHttp.getDeclaredField("credentialsProvider"); + credentialsProviderField2.setAccessible(true); + BasicCredentialsProvider credentialsProvider2 = (BasicCredentialsProvider) credentialsProviderField2.get(InternalHttp2.cast(httpClientField2.get(client2))); + + Field credMapField2 = BasicCredentialsProvider.class.getDeclaredField("credMap"); + credMapField2.setAccessible(true); + ConcurrentHashMap credMap2 = (ConcurrentHashMap) credMapField2.get(credentialsProvider2); + + Assert.assertEquals("123456789", credMap2.entrySet().stream().iterator().next().getValue().getToken()); + splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) From b42fddc0e37fe47a50f2c4dbb2cc84fbe836f63b Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 26 Jun 2025 12:48:39 -0700 Subject: [PATCH 3/3] updated test --- .../io/split/client/SplitFactoryImplTest.java | 81 +++++++++++++++--- client/src/test/resources/keyStore.p12 | Bin 0 -> 3011 bytes 2 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 client/src/test/resources/keyStore.p12 diff --git a/client/src/test/java/io/split/client/SplitFactoryImplTest.java b/client/src/test/java/io/split/client/SplitFactoryImplTest.java index c99c2ff0..4597b615 100644 --- a/client/src/test/java/io/split/client/SplitFactoryImplTest.java +++ b/client/src/test/java/io/split/client/SplitFactoryImplTest.java @@ -14,11 +14,11 @@ import org.apache.hc.client5.http.auth.BearerToken; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.MinimalHttpClient; +import org.apache.hc.client5.http.impl.io.DefaultHttpClientConnectionOperator; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.config.Registry; import org.awaitility.Awaitility; import org.junit.Assert; import org.junit.Test; @@ -34,6 +34,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URISyntaxException; +import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -98,12 +99,12 @@ public void testFactoryInstantiationIntegrationsConfig() throws Exception { } @Test - public void testFactoryInstantiationWithProxy() throws Exception { + public void testFactoryInstantiationWithProxyCredentials() throws Exception { SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) .impressionsRefreshRate(1) - .endpoint(ENDPOINT,EVENTS_ENDPOINT) + .endpoint(ENDPOINT, EVENTS_ENDPOINT) .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) .authServiceURL(AUTH_SERVICE) .setBlockUntilReadyTimeout(1000) @@ -147,11 +148,16 @@ public void testFactoryInstantiationWithProxy() throws Exception { Assert.assertEquals("test", credMap.entrySet().stream().iterator().next().getValue().getUserName()); assertNotNull(credMap.entrySet().stream().iterator().next().getValue().getUserPassword()); - splitClientConfig = SplitClientConfig.builder() + splitFactory.destroy(); + } + + @Test + public void testFactoryInstantiationWithProxyToken() throws Exception { + SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) .impressionsRefreshRate(1) - .endpoint(ENDPOINT,EVENTS_ENDPOINT) + .endpoint(ENDPOINT, EVENTS_ENDPOINT) .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) .authServiceURL(AUTH_SERVICE) .setBlockUntilReadyTimeout(1000) @@ -171,7 +177,7 @@ public void testFactoryInstantiationWithProxy() throws Exception { httpClientField2.setAccessible(true); Class InternalHttp2 = Class.forName("org.apache.hc.client5.http.impl.classic.InternalHttpClient"); - Field credentialsProviderField2 = InternalHttp.getDeclaredField("credentialsProvider"); + Field credentialsProviderField2 = InternalHttp2.getDeclaredField("credentialsProvider"); credentialsProviderField2.setAccessible(true); BasicCredentialsProvider credentialsProvider2 = (BasicCredentialsProvider) credentialsProviderField2.get(InternalHttp2.cast(httpClientField2.get(client2))); @@ -181,7 +187,12 @@ public void testFactoryInstantiationWithProxy() throws Exception { Assert.assertEquals("123456789", credMap2.entrySet().stream().iterator().next().getValue().getToken()); - splitClientConfig = SplitClientConfig.builder() + splitFactory2.destroy(); + } + + @Test + public void testFactoryInstantiationWithProxyMtls() throws Exception { + SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) .impressionsRefreshRate(1) @@ -191,15 +202,61 @@ public void testFactoryInstantiationWithProxy() throws Exception { .setBlockUntilReadyTimeout(1000) .proxyPort(6060) .proxyScheme("https") - .proxyMtlsAuth(new ProxyMTLSAuth.Builder().proxyP12File("file").proxyP12FilePassKey("pass").build()) + .proxyMtlsAuth(new ProxyMTLSAuth.Builder().proxyP12File("src/test/resources/keyStore.p12").proxyP12FilePassKey("split").build()) .proxyHost(ENDPOINT) .build(); SplitFactoryImpl splitFactory3 = new SplitFactoryImpl(API_KEY, splitClientConfig); assertNotNull(splitFactory3.client()); assertNotNull(splitFactory3.manager()); - splitFactory.destroy(); - splitFactory2.destroy(); + Field splitHttpClientField3 = SplitFactoryImpl.class.getDeclaredField("_splitHttpClient"); + splitHttpClientField3.setAccessible(true); + SplitHttpClientImpl client3 = (SplitHttpClientImpl) splitHttpClientField3.get(splitFactory3); + + Field httpClientField3 = SplitHttpClientImpl.class.getDeclaredField("_client"); + httpClientField3.setAccessible(true); + Class InternalHttp3 = Class.forName("org.apache.hc.client5.http.impl.classic.InternalHttpClient"); + + Field connManagerField = InternalHttp3.getDeclaredField("connManager"); + connManagerField.setAccessible(true); + PoolingHttpClientConnectionManager connManager = (PoolingHttpClientConnectionManager) connManagerField.get(InternalHttp3.cast(httpClientField3.get(client3))); + + Field connectionOperatorField = PoolingHttpClientConnectionManager.class.getDeclaredField("connectionOperator"); + connectionOperatorField.setAccessible(true); + DefaultHttpClientConnectionOperator connectionOperator = (DefaultHttpClientConnectionOperator) connectionOperatorField.get(connManager); + + Field tlsSocketStrategyLookupField = DefaultHttpClientConnectionOperator.class.getDeclaredField("tlsSocketStrategyLookup"); + tlsSocketStrategyLookupField.setAccessible(true); + Registry tlsSocketStrategyLookup = (Registry) tlsSocketStrategyLookupField.get(connectionOperator); + + Field mapField = Registry.class.getDeclaredField("map"); + mapField.setAccessible(true); + Class map = mapField.get(tlsSocketStrategyLookup).getClass(); + + Class value = ((ConcurrentHashMap) map.cast(mapField.get(tlsSocketStrategyLookup))).get("https").getClass(); + + Field arg1Field = value.getDeclaredField("arg$1"); + arg1Field.setAccessible(true); + Class sslConnectionSocketFactory = arg1Field.get(((ConcurrentHashMap) map.cast(mapField.get(tlsSocketStrategyLookup))).get("https")).getClass(); + + Field socketFactoryField = sslConnectionSocketFactory.getDeclaredField("socketFactory"); + socketFactoryField.setAccessible(true); + Class socketFactory = socketFactoryField.get(arg1Field.get(((ConcurrentHashMap) map.cast(mapField.get(tlsSocketStrategyLookup))).get("https"))).getClass(); + + Field contextField = socketFactory.getDeclaredField("context"); + contextField.setAccessible(true); + Class context = Class.forName("sun.security.ssl.SSLContextImpl"); + + Field keyManagerField = context.getDeclaredField("keyManager"); + keyManagerField.setAccessible(true); + Class keyManager = keyManagerField.get(contextField.get(socketFactoryField.get(arg1Field.get(((ConcurrentHashMap) map.cast(mapField.get(tlsSocketStrategyLookup))).get("https"))))).getClass(); + + Field credentialsMapField = keyManager.getDeclaredField("credentialsMap"); + credentialsMapField.setAccessible(true); + HashMap credentialsMap = (HashMap) credentialsMapField.get(keyManagerField.get(contextField.get(socketFactoryField.get(arg1Field.get(((ConcurrentHashMap) map.cast(mapField.get(tlsSocketStrategyLookup))).get("https")))))); + + assertNotNull(credentialsMap.get("1")); + splitFactory3.destroy(); } diff --git a/client/src/test/resources/keyStore.p12 b/client/src/test/resources/keyStore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..ce2b34171195aa55cb4640301caff60021c29543 GIT binary patch literal 3011 zcmai$XE+-Q7srLfiVkefQoQc-zR$fM-w)?G&-wj7oNvE#U|4V)DS!-y1qYH-iN)&2p3nj)0C`xj8xRY2 zxyFt#ENJH6h&&Gq>c2)kqyUoZ*8Xn-fI0m$fM{W6FsXk>8kisuLdY$m_vbJ2>$zO| zl_j;C@WK|fr!>V*E6Tg-pV4aCIT>Xf8RIG|MAKc zq7JM#CtS`-CKsFIE4~mg2x>y@v{hXPJdkJIa=^x#4b*T!B?MVcHRClaQBId^JVP$8- zb;}*$A_nmkGCevGVq5>|v*)E}vO||q(p5M;_!Cu_i`Ji)kyD`=;!FI7> z-0VSnOwAVPWt+%ve%2BT98->zPm-fcb6kfTetjK(xyrjq^kuoRtEln}qI+5;4puY7 zUT&H-JhO6T3z;0x591ST9X2pD;sV~7KX}zRV}t>~tW|r+yte~em24=KL{snEDYrhn z3c&a6Q{KbQDOe^&sDyoPrZyG*tsF-F&T)J_L{W2B>RHm%)2#wx-D;Y}CR!`#F@b5K zC?MAKFJ>^jhC%;cL2@tm6$Zdde;aF3;dCa2m-+hLB?VQnss~DhPfSsG4t4RTK4n2M zV1vLEZ{9aQ>KSIRAeNLSpeIzdh96e8q0$9J#NH8PHE4)wnB&-)Tyo|`bad5oK93X@ zhZP4lwN>vFjeMO8b^P>ELdJjzQ<(vL&fiR}(UHxvrEp}!T-wb(AA4Wvn3AVzo zBKWrXUNC1niMLIuNkI!RBcsu8b&F*l&E<$07M4+nc4Tef{(6O;mM(`p)wtnF3lC9% ztB}ji4KI|Sq+Gj*#AZy}GZzWb=pX}$rP&ycd{v<_;!(GaR@ibDpp2RlQQNs+ z8P7&2ahiSFw0o|W6}IszL~<7O&==%fW0M(5sxEQ6wroWD5&GCFfOEk1cTqdNYm#@; z+PN0y@m(FjH@{Mme1^cC15RQL$FJ!PUTm9h+BuBhIlgpq#>!_1pK3)PQC61xlCXL0B+S=Bqa6EdkzvrNh z%9j&J56|&u{Bf!wC>@$w_I@U#F{!a#h+1<~>1$KZt*@8vg<~P_eJrE9jgEIpnBB=Z zyM;aZxT!|nid;$;(Jrn`{+h|QL`0lQL@r*8;goeFv4!E!nV)e4lv#*x!-Pd8TcuCx z&lWl^jz_Mf!!BXFj_PGYemjYf)%J%~j-0+Gycm#i}@Lu2|y}Y$xipYe< zJ&1uiL$7Lu925NY#PaUW_q3o42EdgytI!NChR12k3LcRIB8VA?A6nn4JTm5$v7nb zYg=8-RcX`6m`BmvoR=yoGJ|KUFxX&FABCr*=D|f$gfU}SWh~2}c7zZu&1qX?{1Flz zJHQnTPnxnerGu32{h+>IFs^pKZ8{JVcdT~s?#Xhk?7JK>T>)>7(b7qZSb%0v z@fd#qq-tto;*e(l>!6Dn;WR?e*S}N~H?^_K&LyM8xDXx(R}s_^@lxEhG_y5ohJKe( z$Zw-m)46td=s%wRTl^p|AQt3)jZxQipB((Zn?OSiAiWMnuOamRi5_QqI&Fy9a!`)a zr>bzy*89Jr|HKUTwt7IxpCXkSvA&y30Ahjd1Gj?t96YU9F(X4}adYvVLMPt+lcsF+ zD|KTD+Jcss;c+v#t-_>9an!+yr&FMUj@HNivb&?z=`rqeqRT_W5lOvKr20N5>tYVow!p^NZ3ML7w%)0{jkgCa?A zMRDDrQiQ^&;)k*n8rr6GYMxGiYBaM_>%6XuQ;4uk=eUMlB0TWv1W3g_*vZT4jKbbs z4Ib*ad(8H$tM4uS6p7jsv zBN62Z&NcGBiBP{UZ0xb`Z3d*Q7bgZR?plA6<1aus=#Mi|s9YTNM2#%Qr!B0d zb$IsB>oE(GRx}+OG1P*&a>nuU7uznfb!fSA$wSxA8r@lgJo5GQ&%f`p-pwQS^((4P z3jch8dE8HBOGf^zFvDNUUZ8P{F8NOUG_hPAya(ld4M!#*33%EG0gjziPv}JcJkxh! zHQ`E8I$eW(zdOUDc)%(gT&9j9xydhg#_;n|?k2{Y+;~eRzY4d4GmztR+X15qE}r^< zQ6%Xmtxd$6_g)4}2##}wFvK(pgWiOjS~p6xOno`hEu;d{42lrxZJi^>ethfjhRI@Z z9b~aI0eL*JQ)Rqtn1r|@d7;z$L0__ULxAi6qJZ4FP!DE#n00=(LyQ;m>av7PT3A*SB{QaTHYokV&5hjvM>0h7fL~40{@7W zidl*U$YkAs6?2MNDbMJD7NPR;hTQ0F(qWn~DH!cPZ!akTL;~df?BtVavp$^q`+Z`glu93|cY==qQo6o2hs&b=H2zzy{{fimcNqWx literal 0 HcmV?d00001