From 1cd15e8d85feb308527c3df560734fc2ca1bc13c Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Mon, 12 Oct 2020 15:48:53 +0200 Subject: [PATCH] Fixes #5379 - Better handling for wrong SNI. (#5398) * Fixes #5379 - Better handling for wrong SNI. Reworked the SNI logic. Added support for IP addresses in the SAN extension of certificates in the X509 class. Fixed keystores to have CN=localhost and SAN with ip=127.0.0.1 and ip=[::1]. Fixed tests that were not using the correct Host header. Signed-off-by: Simone Bordet --- .../client/HostnameVerificationTest.java | 23 ++- .../src/test/resources/client_keystore.p12 | Bin 4541 -> 4629 bytes jetty-client/src/test/resources/keystore.p12 | Bin 2573 -> 2613 bytes .../src/test/resources/readme_keystores.txt | 4 +- .../keystore/keystore-create.adoc | 3 + .../protocols/protocols-ssl.adoc | 91 +++++++----- .../proxy/ForwardProxyTLSServerTest.java | 20 +-- .../src/test/resources/client_keystore.p12 | Bin 6879 -> 7031 bytes .../test/resources/client_proxy_keystore.p12 | Bin 3541 -> 3625 bytes .../test/resources/client_server_keystore.p12 | Bin 3551 -> 3627 bytes .../src/test/resources/proxy_keystore.p12 | Bin 3549 -> 3589 bytes .../src/test/resources/readme_keystores.txt | 22 +++ .../src/test/resources/server_keystore.p12 | Bin 3549 -> 3589 bytes .../jetty/server/SecureRequestCustomizer.java | 33 +++-- .../jetty/server/ConnectorTimeoutTest.java | 26 ++-- .../jetty/server/HttpServerTestBase.java | 64 +++++---- .../ssl/SelectChannelServerSslTest.java | 6 +- .../ssl/SniSslConnectionFactoryTest.java | 134 +++++++----------- .../ssl/SslContextFactoryReloadTest.java | 4 +- jetty-server/src/test/resources/keystore.p12 | Bin 2565 -> 2613 bytes .../util/ssl/SniX509ExtendedKeyManager.java | 37 +++-- .../jetty/util/ssl/SslContextFactory.java | 6 +- .../java/org/eclipse/jetty/util/ssl/X509.java | 95 +++++++++---- .../jetty/websocket/core/WebSocketTester.java | 2 +- .../jetty/http/client/HttpClientTest.java | 9 +- .../src/test/resources/keystore.p12 | Bin 2565 -> 2597 bytes 26 files changed, 343 insertions(+), 236 deletions(-) create mode 100644 jetty-proxy/src/test/resources/readme_keystores.txt diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HostnameVerificationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HostnameVerificationTest.java index fc1edcc5eff2..ee8ab99ae1d1 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HostnameVerificationTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HostnameVerificationTest.java @@ -27,10 +27,14 @@ import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -49,7 +53,7 @@ */ public class HostnameVerificationTest { - private SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(); + private final SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(); private Server server; private HttpClient client; private NetworkConnector connector; @@ -64,7 +68,13 @@ public void setUp() throws Exception SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server(); serverSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); serverSslContextFactory.setKeyStorePassword("storepwd"); - connector = new ServerConnector(server, serverSslContextFactory); + HttpConfiguration httpConfig = new HttpConfiguration(); + SecureRequestCustomizer customizer = new SecureRequestCustomizer(); + customizer.setSniHostCheck(false); + httpConfig.addCustomizer(customizer); + HttpConnectionFactory http = new HttpConnectionFactory(httpConfig); + SslConnectionFactory ssl = new SslConnectionFactory(serverSslContextFactory, http.getProtocol()); + connector = new ServerConnector(server, 1, 1, ssl, http); server.addConnector(connector); server.setHandler(new DefaultHandler() { @@ -102,14 +112,17 @@ public void tearDown() throws Exception /** * This test is supposed to verify that hostname verification works as described in: - * http://www.ietf.org/rfc/rfc2818.txt section 3.1. It uses a certificate with a common name different to localhost - * and sends a request to localhost. This should fail with an SSLHandshakeException. + * http://www.ietf.org/rfc/rfc2818.txt section 3.1. + * It uses a certificate with a common name "localhost" and SAN=127.0.0.1, + * and sends a request to 127.0.0.2. + * This should fail with on the client an SSLHandshakeException, because SNI + * host checking on the server side is disabled. */ @Test public void simpleGetWithHostnameVerificationEnabledTest() { clientSslContextFactory.setEndpointIdentificationAlgorithm("HTTPS"); - String uri = "https://localhost:" + connector.getLocalPort() + "/"; + String uri = "https://127.0.0.2:" + connector.getLocalPort() + "/"; ExecutionException x = assertThrows(ExecutionException.class, () -> client.GET(uri)); Throwable cause = x.getCause(); diff --git a/jetty-client/src/test/resources/client_keystore.p12 b/jetty-client/src/test/resources/client_keystore.p12 index 429720049b8b52e4799787f0f4899e49d42bf3e3..90f717671652d2c7cf3a45ef50132f1f5f3b1536 100644 GIT binary patch delta 4547 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 delta 4459 zcmV-x5tQzgB)uadFoF@e0s#Xsf)R2C2`Yw2hW8Bt2LYgh5n}{`5nnKZ5nGWWMt{n$ zj(h4F`-u!q)9(A>YL#{Bl=}h$0K-rOf&|Dm)GyoOvmB!5<$na-@GoMvcqwEnXeh|t zO~|)FrK|NnWxqO?d{*nPk%>+kE#6YCj3!g>%(_x^Xs z+&^BxlEX^$INyRitM|~N(#c2BIov;w(M-@YI9pNG9`Vw&^qqnS8k(lq8^(}UOML;=?%pQXY0clwfPGH5?-D=@)9c_B*PG54D-*?cFi)?N8di>+QDeghy#v4h9u@TXY z$@f&nOzZ?f^)GVzn~vAX=Q>MH#xe;hm$oHH^}0KsQiejTRvd`F`{9C+HD3_Vsq*75w`NPoJ>On3!|wIZ-5YXMfa(D3m1*9i_n( zbY|aktoh2`M+}xD&K3j%oeI+d8b@BoZ*xa&x!r&-WmFRD?vEHYO+XjThH#Ftg$u{w zv96|jTAwMb!zyZpBuzXVlK5yNN;KMjh_8$HQjvK-A(VotRY@=pmH_WSQ^E{uIQ%&2 znf-VRS;*LaW7wn(eSg&|H{!|~gJkchlyC%l)E@!F#GsMCs501e9&P7Z()+6E`10Zw zK%Zpfa9+lM8J?$eY6l3Umj$kVvM=>;(`0M*kCE^vWIOSsE?RM9Ukr+S>RE~soqSc8 zvhL*MC1z!wUj->e_3J`tRZHT@!L=407+p)C_h2yA&5OyArGIwUlb6WlP|5}2>cHse z%&&ON&r_>qC+=q&5CJFj?Mrm`FBVYZ86t!CT%WUF2!c7?fb8_b3Ae*u9%%k!*RmhgpGj;s~g1zd)3wwg;-^=GeI5x3XD zYh*=1>>x;n3m zR_aqTMxiG}yKlVtV0r2}#>cn;?p21}`PShcH+oDYM^ji7XB=X+;h^i_6=)iT)^g?bE#Y;O>wf{vPDx)^T3k$*zev_YSUn0T z;{XPB1UWfmO%(gjJGqza0i3JYdtA{1;`QGqsSFhOqr^iX+< z)uMCts>YWOek}Kn#F6;CHr~Sq8u z88ncXwtr2=!V*~1+x#FR+a|-Eg@U=nuWn$AFLX^ema{^t30IvC3hbxzz8~d&$3ey6 zFmKB=)P2k$dphFUX`v~C5eYt9v zsb7Hw(x=Zk_D5%NF+MOE1_>&LNQU*3@|Aw`Z-bpJ+i2?%v z!%%>N3z+2vTrcpWaYBpFT)7`dwISw)3%dybYIg$=`kk~OHu{Ky_5gS?c|RmKbq9PeFBJ+1qSBdlwsG2m#5tTJ9xd5-XA$zQ#ywxTbA2aSGF&`Iz&SCgQ zOp2q65P^@UWd8OQu^^wB?${96e>wkDId(>iIdZ6(OW9@Yp ze^gZDT?*?liRa=`e^58eZ>uRPe+$XP;Tnr25h&2Ec^%(pe!ub1ruYm8(K}O59MOUB zO^r10ctiC}u|(BBIBK;p>MPB@<0{UW{&GfsVSER-bX3>{!V8?5)DV|HPDtXc%N%cGN*ikyG5+h6-Er4Ae=_g6lJr^h z=%R45Ksj{rURFsN_xcV;#cD*pfuKL}!5TK0XXATSJSpuQ{w=ddoroLv^o{Gy*$}Mv zC>T4iU!HUU#|P8z%42`_RREg;8bg$#cr9ms?i#_-EFrqp2tVU9`4vM>{bLhxL4cT8 z17!LrcvOzr2X-hjV#J-|f2DuMr5vQBD9-uWxRXZ8!wy1*pS8-5Hry|$x+l=E+_8k8 z%R7TR{vWD_B>@KkJ7a55w1?kZfbWXN(jio8Cu}c<>|NqfSY_iUmeb1*CnW?2kLQ{^x4L0Ivq#Kxw(u83?U%-gV zVi~-W0i*K6#Sqp2nVKBu7%@Q5zuyyqVqY{?#&<7Ojo~hjnK!0Wr3b$H@oh{HD(Ukj zKlAyEMJQjwLV@rE6$+jd{`{-AYQHmcj1pxs^C{&^j<(nzMTh=d`H`lW4P4$dQz|QR z!?})4@_S>SERkjPfA9{By+Xbv-`RT@Zae(r$uR~>;`Tmc5|TZ{f%-`LFLew)*yoT) zd_wnJ*?5W4MLYf0`i&(i3Md4@7Zu7|%gB`mZpfvSbG$S6cO7(ZGbC1`ry&k0h>3xg zt(@yqwE_FJ=Q^RV;SyySfna8H@VH&l_?9O6MO?t9Nb95LTA!wFFE{Y*SBRrtEu{gqcso8w*r@6MO(okuaFemD?2e~5aCH+pSiUP8c=^*rl}*!*n$BH$n ze|y12i32JCcEGfJwd|hkoWV_@#-07)j{4xlP)0=u%%*DI1RI_S_GE(2#9oC?#3E$I zBcfIL&7veK-RPpl&Rk6;d&-lYn?^q~u;>S_hHAA5^=GItihf^7+f|H8RWEMRh1pM* z41H6mtUAMPS*%wY+Rm`AP`Wc|n}KHifAMgD7qe={LbtE2xY# z6@=cT>TAefCVGu~ufg6~7i2beU~Hkh3R=V4NCl_ZFj`dUMrE|63zVsrYpN5~aiW`2y${8qrg&iixf0b5y zs;|==Gp5gu;8TQpRa}L9f<<4J#P}ueqTw46r;I5N|DoJ-CJVIDP)~+RyC&wQlnYSP>q07jgv2^5QOZ4iQ=8|p zb}LXtU$L(#IG&RtW=c{-s)0;Yf4-OM)}EcgB0+M-0Wa~~SxQ}b%|6K7O?)jg+h55> zSd@ljU8il*V6(UFbDGz+bpgx9P`-M3dCM?)RvQJsaUpSS_cOs#(F6lJaQEODTl61G zUx$P#>A`jXQAGu=kQ0@MCK1wev#`?70LO0+jD%_LJ>*~)`|+tAmJWV8f9eV@5*^&) z&c{ViFO~QL$_5{n_L6mw)k8tC*qfD`F!={E>^UEbFK~UWHFc$dgh^VK5nX|Ftx=cQ z9ATHPbkRCtFZ{sK0LL@Ze==VEzG6Eu?F2Zd-E7qj|BDD!bH*ld<|+j*QS5#zv%S7w zEaxQAnRa-`*aIR;Hfc7$Z^1lVTC{6}VRX&6N=p%=jK)pCncD+FS~U{h46#LxGUbUA znC6rTvQPwOK&22T4w1DoROF7;>!Zwt|w&Hb7N4vRo+vn%Yny+Hg#nf-Gd=S$_;0J!X`Q5`i_bK z242rn=JHi!AK&C^a{3+>|757t&rn%XQ~m9>;cjbfhnjf0f6h84Dq}R;*Wj}-VB@w; zs{ZPzUz!aH04EbAgWck1_~_0VzlIvsA|sz_L8;D^Pa|bR+)7gF)4LJ^s;q673DlEP zui$~b3SPmHYf*Go@qtF)Hn74oqplEi_|*s>yxdUM4&qGEKwvRAjPZzG^}?$4 zIYA1dfb`Nd<1&U9pm(wEc;kr1I)k|?-piMyZ=}t{n=n2wAutIB1uG5%0vZJX1QbR3 xce^` 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 diff --git a/jetty-proxy/src/test/resources/client_proxy_keystore.p12 b/jetty-proxy/src/test/resources/client_proxy_keystore.p12 index 0b55322162e14311ba11213cc5afd58179b638e2..f1d4c688d479d86dc12f9feba7d509cd86ce9598 100644 GIT binary patch delta 3569 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 delta 3485 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 LqyP;90|ADhfxW^a diff --git a/jetty-proxy/src/test/resources/client_server_keystore.p12 b/jetty-proxy/src/test/resources/client_server_keystore.p12 index f7b6729309f67c3fb458a9fbef83f3bd6d3d7b71..4b0d99ba4d2236840e558c3fee76c0413054897d 100644 GIT binary patch delta 3569 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 delta 3493 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 Z6HUxF%X`#JhmpK7I3kBs)B*zmhM)zHsqFv& delta 3459 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 diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java index 360767fcdedf..adca50e32562 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java @@ -40,7 +40,6 @@ import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.annotation.Name; -import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.X509; import org.slf4j.Logger; @@ -63,7 +62,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer private boolean _sniRequired; private boolean _sniHostCheck; - private long _stsMaxAge = -1; + private long _stsMaxAge; private boolean _stsIncludeSubDomains; private HttpField _stsField; @@ -247,26 +246,32 @@ protected void customize(SSLEngine sslEngine, Request request) { SSLSession sslSession = sslEngine.getSession(); - if (_sniHostCheck || _sniRequired) + if (isSniRequired() || isSniHostCheck()) { - X509 x509 = (X509)sslSession.getValue(SniX509ExtendedKeyManager.SNI_X509); + String sniHost = (String)sslSession.getValue(SslContextFactory.Server.SNI_HOST); + X509 cert = new X509(null, (X509Certificate)sslSession.getLocalCertificates()[0]); + String serverName = request.getServerName(); if (LOG.isDebugEnabled()) - LOG.debug("Host {} with SNI {}", request.getServerName(), x509); + LOG.debug("Host={}, SNI={}, SNI Certificate={}", serverName, sniHost, cert); - if (x509 == null) + if (isSniRequired()) { - if (_sniRequired) - throw new BadMessageException(400, "SNI required"); + if (sniHost == null) + throw new BadMessageException(400, "Invalid SNI"); + if (!cert.matches(sniHost)) + throw new BadMessageException(400, "Invalid SNI"); } - else if (_sniHostCheck && !x509.matches(request.getServerName())) + + if (isSniHostCheck()) { - throw new BadMessageException(400, "Host does not match SNI"); + if (!cert.matches(serverName)) + throw new BadMessageException(400, "Invalid SNI"); } } - request.setAttributes(new SslAttributes(request, sslSession, request.getAttributes())); + request.setAttributes(new SslAttributes(request, sslSession)); } - + /** * Customizes the request attributes for general secure settings. * The default impl calls {@link Request#setSecure(boolean)} with true @@ -325,9 +330,9 @@ private class SslAttributes extends Attributes.Wrapper private String _sessionId; private String _sessionAttribute; - public SslAttributes(Request request, SSLSession sslSession, Attributes attributes) + private SslAttributes(Request request, SSLSession sslSession) { - super(attributes); + super(request.getAttributes()); this._request = request; this._session = sslSession; diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java index 4540da0d5b52..f20fbbd98472 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java @@ -106,7 +106,7 @@ public void testMaxIdleWithRequest10() throws Exception { os.write(( "GET / HTTP/1.0\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: keep-alive\r\n" + "\r\n").getBytes("utf-8")); os.flush(); @@ -141,7 +141,7 @@ public void testMaxIdleWithRequest11() throws Exception byte[] contentB = content.getBytes("utf-8"); os.write(( "POST /echo HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: " + contentB.length + "\r\n" + "\r\n").getBytes("utf-8")); @@ -189,7 +189,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques os.write(( "GET / HTTP/1.0\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: close\r\n" + "\r\n").getBytes("utf-8")); os.flush(); @@ -250,7 +250,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques byte[] contentB = content.getBytes("utf-8"); os.write(( "POST /echo HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: " + contentB.length + "\r\n" + "connection: close\r\n" + @@ -295,7 +295,7 @@ public void testNoBlockingTimeoutRead() throws Exception OutputStream os = client.getOutputStream(); os.write(("GET / HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Type: text/plain\r\n" + "Connection: close\r\n" + @@ -356,7 +356,7 @@ public void testBlockingTimeoutRead() throws Exception long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); os.write(("GET / HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Type: text/plain\r\n" + "Connection: close\r\n" + @@ -413,7 +413,7 @@ public void testNoBlockingTimeoutWrite() throws Exception os.write(( "GET / HTTP/1.0\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: keep-alive\r\n" + "Connection: close\r\n" + "\r\n").getBytes("utf-8")); @@ -456,7 +456,7 @@ public void testBlockingTimeoutWrite() throws Exception os.write(( "GET / HTTP/1.0\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: keep-alive\r\n" + "Connection: close\r\n" + "\r\n").getBytes("utf-8")); @@ -561,7 +561,7 @@ public void testMaxIdleDelayedDispatch() throws Exception long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); os.write(( "GET / HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: keep-alive\r\n" + "Content-Length: 20\r\n" + "Content-Type: text/plain\r\n" + @@ -600,7 +600,7 @@ public void testMaxIdleDispatch() throws Exception long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); os.write(( "GET / HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: keep-alive\r\n" + "Content-Length: 20\r\n" + "Content-Type: text/plain\r\n" + @@ -643,7 +643,7 @@ public void testMaxIdleWithSlowRequest() throws Exception byte[] contentB = content.getBytes("utf-8"); os.write(( "GET / HTTP/1.0\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: keep-alive\r\n" + "Content-Length: " + (contentB.length * 20) + "\r\n" + "Content-Type: text/plain\r\n" + @@ -684,7 +684,7 @@ public void testMaxIdleWithSlowResponse() throws Exception os.write(( "GET / HTTP/1.0\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: keep-alive\r\n" + "Connection: close\r\n" + "\r\n").getBytes("utf-8")); @@ -716,7 +716,7 @@ public void testMaxIdleWithWait() throws Exception os.write(( "GET / HTTP/1.0\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: keep-alive\r\n" + "Connection: close\r\n" + "\r\n").getBytes("utf-8")); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java index c30d4e09826b..44c32c6ed5f9 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java @@ -138,7 +138,11 @@ public void testSimple() throws Exception { OutputStream os = client.getOutputStream(); - os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1)); + String request = "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + os.write(request.getBytes(StandardCharsets.ISO_8859_1)); os.flush(); // Read the response. @@ -159,7 +163,7 @@ public void testOPTIONS() throws Exception OutputStream os = client.getOutputStream(); os.write(("OPTIONS * HTTP/1.1\r\n" + - "Host: " + _serverURI.getHost() + "\r\n" + + "Host: localhost\r\n" + "Connection: close\r\n" + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); os.flush(); @@ -666,7 +670,7 @@ public void testFlush() throws Exception String test = encoding[e] + "x" + b + "x" + w + "x" + c; try { - URL url = new URL(_scheme + "://" + _serverURI.getHost() + ":" + _serverURI.getPort() + "/?writes=" + w + "&block=" + b + (e == 0 ? "" : ("&encoding=" + encoding[e])) + (c == 0 ? "&chars=true" : "")); + URL url = new URL(_scheme + "://localhost:" + _serverURI.getPort() + "/?writes=" + w + "&block=" + b + (e == 0 ? "" : ("&encoding=" + encoding[e])) + (c == 0 ? "&chars=true" : "")); InputStream in = (InputStream)url.getContent(); String response = IO.toString(in, e == 0 ? null : encoding[e]); @@ -698,7 +702,7 @@ public void testBlockingWhileReadingRequestContent() throws Exception os.write(( "GET /data?writes=1024&block=256 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: close\r\n" + "content-type: unknown\r\n" + "content-length: 30\r\n" + @@ -758,7 +762,7 @@ public void testBlockingReadBadChunk() throws Exception os.write(( "GET /data HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: unknown\r\n" + "transfer-encoding: chunked\r\n" + "\r\n" @@ -807,7 +811,7 @@ public void testBlockingWhileWritingResponseContent() throws Exception os.write(( "GET /data?writes=256&block=1024 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: close\r\n" + "content-type: unknown\r\n" + "\r\n" @@ -848,7 +852,7 @@ public void testCloseWhileWriteBlocked() throws Exception os.write(( "GET /data?encoding=iso-8859-1&writes=100&block=100000 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: close\r\n" + "content-type: unknown\r\n" + "\r\n" @@ -875,7 +879,7 @@ public void testCloseWhileWriteBlocked() throws Exception InputStream is = client.getInputStream(); os.write(("GET /data?writes=1&block=1024 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: close\r\n" + "content-type: unknown\r\n" + "\r\n" @@ -901,10 +905,10 @@ public void testBigBlocks() throws Exception os.write(( "GET /r1 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "\r\n" + "GET /r2 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: close\r\n" + "\r\n" ).getBytes()); @@ -1049,7 +1053,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques { request += "GET /data?writes=1&block=16&id=" + i + " HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "user-agent: testharness/1.0 (blah foo/bar)\r\n" + "accept-encoding: nothing\r\n" + "cookie: aaa=1234567890\r\n" + @@ -1058,7 +1062,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques request += "GET /data?writes=1&block=16 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "user-agent: testharness/1.0 (blah foo/bar)\r\n" + "accept-encoding: nothing\r\n" + "cookie: aaa=bbbbbb\r\n" + @@ -1095,7 +1099,7 @@ public void testRecycledWriters() throws Exception os.write(( "POST /echo?charset=utf-8 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); @@ -1106,7 +1110,7 @@ public void testRecycledWriters() throws Exception os.write(( "POST /echo?charset=utf-8 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + "\r\n" @@ -1120,7 +1124,7 @@ public void testRecycledWriters() throws Exception byte[] contentB = content.getBytes("utf-8"); os.write(( "POST /echo?charset=utf-16 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: " + contentB.length + "\r\n" + "connection: close\r\n" + @@ -1182,21 +1186,21 @@ public void testHead() throws Exception //@checkstyle-disable-check : IllegalTokenText os.write(( "POST /R1 HTTP/1.1\r\n" + - "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "Host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + "\r\n" + "123456789\n" + "HEAD /R2 HTTP/1.1\r\n" + - "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "Host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + "\r\n" + "ABCDEFGHI\n" + "POST /R3 HTTP/1.1\r\n" + - "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "Host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + "Connection: close\r\n" + @@ -1224,7 +1228,7 @@ public void testRecycledReaders() throws Exception os.write(( "POST /echo/0?charset=utf-8 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); @@ -1235,7 +1239,7 @@ public void testRecycledReaders() throws Exception os.write(( "POST /echo/1?charset=utf-8 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + "\r\n" @@ -1249,7 +1253,7 @@ public void testRecycledReaders() throws Exception byte[] contentB = content.getBytes(StandardCharsets.UTF_16); os.write(( "POST /echo/2?charset=utf-8 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-16\r\n" + "content-length: " + contentB.length + "\r\n" + "connection: close\r\n" + @@ -1279,7 +1283,7 @@ public void testBlockedClient() throws Exception // Send a request with chunked input and expect 100 os.write(( "GET / HTTP/1.1\r\n" + - "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "Host: localhost:" + _serverURI.getPort() + "\r\n" + "Transfer-Encoding: chunked\r\n" + "Expect: 100-continue\r\n" + "Connection: Keep-Alive\r\n" + @@ -1316,7 +1320,7 @@ public void testCommittedError() throws Exception // Send a request os.write(("GET / HTTP/1.1\r\n" + - "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "Host: localhost:" + _serverURI.getPort() + "\r\n" + "\r\n" ).getBytes()); os.flush(); @@ -1452,7 +1456,7 @@ public void testAvailable() throws Exception os.write(( "GET /data?writes=1024&block=256 HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: close\r\n" + "content-type: unknown\r\n" + "content-length: 30\r\n" + @@ -1608,7 +1612,7 @@ public void run() { out.write(bytes, 0, bytes.length); } - out.write("GET / HTTP/1.1\r\nHost: last\r\nConnection: close\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1)); + out.write("GET /last HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1)); out.flush(); } catch (Exception e) @@ -1636,8 +1640,8 @@ public void testWriteBodyAfterNoBodyResponse() throws Exception Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()); final OutputStream out = client.getOutputStream(); - out.write("GET / HTTP/1.1\r\nHost: test\r\n\r\n".getBytes()); - out.write("GET / HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n".getBytes()); + out.write("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n".getBytes()); + out.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n".getBytes()); out.flush(); BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); @@ -1679,7 +1683,7 @@ public void testWriteBodyAfterNoBodyResponse() throws Exception while (line != null); } - private class WriteBodyAfterNoBodyResponseHandler extends AbstractHandler + private static class WriteBodyAfterNoBodyResponseHandler extends AbstractHandler { @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException @@ -1722,7 +1726,7 @@ public void testSuspendedPipeline() throws Exception // write an initial request os.write(( "GET / HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "\r\n" ).getBytes()); os.flush(); @@ -1732,7 +1736,7 @@ public void testSuspendedPipeline() throws Exception // write an pipelined request os.write(( "GET / HTTP/1.1\r\n" + - "host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + + "host: localhost:" + _serverURI.getPort() + "\r\n" + "connection: close\r\n" + "\r\n" ).getBytes()); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SelectChannelServerSslTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SelectChannelServerSslTest.java index a8cce77e1e3b..ab017ea8e313 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SelectChannelServerSslTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SelectChannelServerSslTest.java @@ -253,7 +253,11 @@ public void testSecureRequestCustomizer() throws Exception { OutputStream os = client.getOutputStream(); - os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1)); + String request = "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + os.write(request.getBytes(StandardCharsets.ISO_8859_1)); os.flush(); // Read the response. diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java index e9d1666d5bff..8e6f12f42633 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.BiConsumer; import java.util.function.Consumer; import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIServerName; @@ -59,11 +60,11 @@ import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -77,55 +78,39 @@ public class SniSslConnectionFactoryTest { private Server _server; private ServerConnector _connector; - private HttpConfiguration _httpsConfiguration; - private int _port; - @BeforeEach - public void before() + private void start(String keystorePath) throws Exception + { + start(ssl -> ssl.setKeyStorePath(keystorePath)); + } + + private void start(Consumer sslConfig) throws Exception + { + start((ssl, customizer) -> sslConfig.accept(ssl)); + } + + private void start(BiConsumer config) throws Exception { _server = new Server(); - HttpConfiguration httpConfig = new HttpConfiguration(); - httpConfig.setSecureScheme("https"); - httpConfig.setSecurePort(8443); - httpConfig.setOutputBufferSize(32768); - _httpsConfiguration = new HttpConfiguration(httpConfig); - SecureRequestCustomizer src = new SecureRequestCustomizer(); - src.setSniHostCheck(true); - _httpsConfiguration.addCustomizer(src); - _httpsConfiguration.addCustomizer((connector, hc, request) -> + HttpConfiguration httpConfiguration = new HttpConfiguration(); + SecureRequestCustomizer secureRequestCustomizer = new SecureRequestCustomizer(); + httpConfiguration.addCustomizer(secureRequestCustomizer); + httpConfiguration.addCustomizer((connector, httpConfig, request) -> { - EndPoint endp = request.getHttpChannel().getEndPoint(); - if (endp instanceof SslConnection.DecryptedEndPoint) + EndPoint endPoint = request.getHttpChannel().getEndPoint(); + SslConnection.DecryptedEndPoint sslEndPoint = (SslConnection.DecryptedEndPoint)endPoint; + SslConnection sslConnection = sslEndPoint.getSslConnection(); + SSLEngine sslEngine = sslConnection.getSSLEngine(); + SSLSession session = sslEngine.getSession(); + for (Certificate c : session.getLocalCertificates()) { - try - { - SslConnection.DecryptedEndPoint sslEndp = (SslConnection.DecryptedEndPoint)endp; - SslConnection sslConnection = sslEndp.getSslConnection(); - SSLEngine sslEngine = sslConnection.getSSLEngine(); - SSLSession session = sslEngine.getSession(); - for (Certificate c : session.getLocalCertificates()) - { - request.getResponse().getHttpFields().add("X-Cert", ((X509Certificate)c).getSubjectDN().toString()); - } - } - catch (Throwable th) - { - th.printStackTrace(); - } + request.getResponse().getHttpFields().add("X-CERT", ((X509Certificate)c).getSubjectDN().toString()); } }); - } - - protected void start(String keystorePath) throws Exception - { - start(ssl -> ssl.setKeyStorePath(keystorePath)); - } - protected void start(Consumer sslConfig) throws Exception - { SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - sslConfig.accept(sslContextFactory); + config.accept(sslContextFactory, secureRequestCustomizer); File keystoreFile = sslContextFactory.getKeyStoreResource().getFile(); if (!keystoreFile.exists()) @@ -133,10 +118,10 @@ protected void start(Consumer sslConfig) throws Except sslContextFactory.setKeyStorePassword("storepwd"); - ServerConnector https = _connector = new ServerConnector(_server, + _connector = new ServerConnector(_server, new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), - new HttpConnectionFactory(_httpsConfiguration)); - _server.addConnector(https); + new HttpConnectionFactory(httpConfiguration)); + _server.addConnector(_connector); _server.setHandler(new AbstractHandler() { @@ -151,36 +136,31 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques }); _server.start(); - _port = https.getLocalPort(); } @AfterEach - public void after() throws Exception + public void after() { - if (_server != null) - _server.stop(); - } - - @Test - public void testConnect() throws Exception - { - start("src/test/resources/keystore_sni.p12"); - String response = getResponse("127.0.0.1", null); - assertThat(response, Matchers.containsString("X-HOST: 127.0.0.1")); + LifeCycle.stop(_server); } @Test public void testSNIConnectNoWild() throws Exception { - start("src/test/resources/keystore_sni_nowild.p12"); + start((ssl, customizer) -> + { + // Disable the host check because this keystore has no CN and no SAN. + ssl.setKeyStorePath("src/test/resources/keystore_sni_nowild.p12"); + customizer.setSniHostCheck(false); + }); String response = getResponse("www.acme.org", null); assertThat(response, Matchers.containsString("X-HOST: www.acme.org")); - assertThat(response, Matchers.containsString("X-Cert: OU=default")); + assertThat(response, Matchers.containsString("X-CERT: OU=default")); response = getResponse("www.example.com", null); assertThat(response, Matchers.containsString("X-HOST: www.example.com")); - assertThat(response, Matchers.containsString("X-Cert: OU=example")); + assertThat(response, Matchers.containsString("X-CERT: OU=example")); } @Test @@ -202,6 +182,9 @@ public void testSNIConnect() throws Exception response = getResponse("www.san.com", "san example"); assertThat(response, Matchers.containsString("X-HOST: www.san.com")); + + response = getResponse("wrongHost", "wrongHost", null); + assertThat(response, Matchers.containsString("HTTP/1.1 400 ")); } @Test @@ -226,7 +209,7 @@ public void testBadSNIConnect() throws Exception String response = getResponse("www.example.com", "some.other.com", "www.example.com"); assertThat(response, Matchers.containsString("HTTP/1.1 400 ")); - assertThat(response, Matchers.containsString("Host does not match SNI")); + assertThat(response, Matchers.containsString("Invalid SNI")); } @Test @@ -249,15 +232,12 @@ public void testWrongSNIRejectedConnection() throws Exception @Test public void testWrongSNIRejectedBadRequest() throws Exception { - start(ssl -> + start((ssl, customizer) -> { ssl.setKeyStorePath("src/test/resources/keystore_sni.p12"); // Do not allow unmatched SNI. ssl.setSniRequired(false); - _httpsConfiguration.getCustomizers().stream() - .filter(SecureRequestCustomizer.class::isInstance) - .map(SecureRequestCustomizer.class::cast) - .forEach(src -> src.setSniRequired(true)); + customizer.setSniRequired(true); }); // Wrong SNI host. @@ -274,7 +254,7 @@ public void testWrongSNIRejectedBadRequest() throws Exception @Test public void testWrongSNIRejectedFunction() throws Exception { - start(ssl -> + start((ssl, customizer) -> { ssl.setKeyStorePath("src/test/resources/keystore_sni.p12"); // Do not allow unmatched SNI. @@ -285,10 +265,7 @@ public void testWrongSNIRejectedFunction() throws Exception return SniX509ExtendedKeyManager.SniSelector.DELEGATE; return ssl.sniSelect(keyType, issuers, session, sniHost, certificates); }); - _httpsConfiguration.getCustomizers().stream() - .filter(SecureRequestCustomizer.class::isInstance) - .map(SecureRequestCustomizer.class::cast) - .forEach(src -> src.setSniRequired(true)); + customizer.setSniRequired(true); }); // Wrong SNI host. @@ -318,7 +295,6 @@ public void testWrongSNIRejectedConnectionWithNonSNIKeystore() throws Exception // Good SNI host. HttpTester.Response response = HttpTester.parseResponse(getResponse("localhost", "localhost", null)); - assertNotNull(response); assertThat(response.getStatus(), is(200)); } @@ -332,7 +308,7 @@ public void testSameConnectionRequestsForManyDomains() throws Exception SslContextFactory clientContextFactory = new SslContextFactory.Client(true); clientContextFactory.start(); SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory(); - try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port)) + try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _connector.getLocalPort())) { SNIHostName serverName = new SNIHostName("m.san.com"); SSLParameters params = sslSocket.getSSLParameters(); @@ -376,7 +352,7 @@ public void testSameConnectionRequestsForManyDomains() throws Exception response = HttpTester.parseResponse(input); assertNotNull(response); assertThat(response.getStatus(), is(400)); - assertThat(response.getContent(), containsString("Host does not match SNI")); + assertThat(response.getContent(), containsString("Invalid SNI")); } finally { @@ -392,7 +368,7 @@ public void testSameConnectionRequestsForManyWildDomains() throws Exception SslContextFactory clientContextFactory = new SslContextFactory.Client(true); clientContextFactory.start(); SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory(); - try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port)) + try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _connector.getLocalPort())) { SNIHostName serverName = new SNIHostName("www.domain.com"); SSLParameters params = sslSocket.getSSLParameters(); @@ -436,7 +412,7 @@ public void testSameConnectionRequestsForManyWildDomains() throws Exception response = HttpTester.parseResponse(input); assertNotNull(response); assertThat(response.getStatus(), is(400)); - assertThat(response.getContent(), containsString("Host does not match SNI")); + assertThat(response.getContent(), containsString("Invalid SNI")); } finally { @@ -449,7 +425,7 @@ public void testSocketCustomization() throws Exception { start("src/test/resources/keystore_sni.p12"); - final Queue history = new LinkedBlockingQueue<>(); + Queue history = new LinkedBlockingQueue<>(); _connector.addBean(new SocketCustomizationListener() { @@ -478,8 +454,8 @@ protected void customize(Socket socket, Class connection, } }); - String response = getResponse("127.0.0.1", null); - assertThat(response, Matchers.containsString("X-HOST: 127.0.0.1")); + String response = getResponse("www.example.com", null); + assertThat(response, Matchers.containsString("X-HOST: www.example.com")); assertEquals("customize connector class org.eclipse.jetty.io.ssl.SslConnection,false", history.poll()); assertEquals("customize ssl class org.eclipse.jetty.io.ssl.SslConnection,false", history.poll()); @@ -501,7 +477,7 @@ private String getResponse(String sniHost, String reqHost, String cn) throws Exc SslContextFactory clientContextFactory = new SslContextFactory.Client(true); clientContextFactory.start(); SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory(); - try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port)) + try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _connector.getLocalPort())) { if (sniHost != null) { @@ -521,7 +497,7 @@ private String getResponse(String sniHost, String reqHost, String cn) throws Exc assertThat(cert.getSubjectX500Principal().getName("CANONICAL"), Matchers.startsWith("cn=" + cn)); } - String response = "GET /ctx/path HTTP/1.0\r\nHost: " + reqHost + ":" + _port + "\r\n\r\n"; + String response = "GET /ctx/path HTTP/1.0\r\nHost: " + reqHost + ":" + _connector.getLocalPort() + "\r\n\r\n"; sslSocket.getOutputStream().write(response.getBytes(StandardCharsets.ISO_8859_1)); return IO.toString(sslSocket.getInputStream()); } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java index 5f67f80f1524..bf36a2a5fa3f 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java @@ -75,7 +75,9 @@ private void start(Handler handler) throws Exception sslContextFactory.setKeyStorePassword("storepwd"); HttpConfiguration httpsConfig = new HttpConfiguration(); - httpsConfig.addCustomizer(new SecureRequestCustomizer()); + SecureRequestCustomizer customizer = new SecureRequestCustomizer(); + customizer.setSniHostCheck(false); + httpsConfig.addCustomizer(customizer); connector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), new HttpConnectionFactory(httpsConfig)); diff --git a/jetty-server/src/test/resources/keystore.p12 b/jetty-server/src/test/resources/keystore.p12 index 5c14fee0c85c1cfaa8de3e56d380988e4c3672a8..01a1aea534e2e511e52396db50a52632434e2d81 100644 GIT binary patch delta 2515 zcmV;^2`u)76txs0FoFs(0s#Xsf(hyd2`Yw2hW8Bt2LYgh3EKpM3D+=!3Dc1xMt_wI zf5GOSGYZwLu{j$<$y|1u783#k0K-rOf&|FYIB#bQmO?<>tPshme5u?n$$%+sPc1Kx z*OFe4S5w-Ywa&=m0yPLIQ+d)K40pKJ=B{%|5Ez4mub=6I_5|VlgKATVSJAkL=5i#7 zrnkHFEr1GLIA>M=f%UZYO_ef6KA{X~Q)#qsscNXO4$ugrf1a{B-*5OTgUic8 z+HDhJeMi#U>FTF@2>Otf+YtTu<)M&CD~w#CH?I>^56HAcG=b{t0Bw})zki~w<{9PH zF{5?k0Xs4_E<`=TkT!va|^Go{R$8&|V}6W~5%g#ixw1_&5H=B=sKY#-T+$$e8WLVy4}bbok(UzFU>S3j zAr6&4d$+LFkXwSuGf` ztpk0BbJ}Rr0W4fVR)3TBQwgD>)a=uDE6!cZ^33`2C$bgV{tL$95WazMs#G1?+upIZ0S0MH4mst0`0x}j8H<9yljp2do^1M{c6}qCU$cVL47A+35r_c zT^`qviejkBU;U`Cy6L}_nUenPVzn=?->?jCdVJ|q$>g7ve$Gbsy=gitX_!V;-A9XS z^|*c>nt*dXIe+}z?1YY}stxFaN=l$660Byu?_f(&krJ=Rj*Te1VOGJfj_E8ov@e;3 z+mVXmm>*NtN!V1cW8hz)r;;zhhP1{qU!rJ^7Wld|;EB5G96E#sjbXstFW8M1stU}5 zK+mh$3nTyFrEEsQfR==MK^Mr+?i}o41*a=JeBN2;)jGS!ugW zj0Eq5d(tZp0v*qJ0ptcfU!-d&uuhBVY`@i-R$6M?%KbT+hk$Nb9;lERJG|VvQIXB9 z&m*1#M%J8->uLaOx5F@p;Ug&YFE02&|*mR-+J&>mW}s z%T~+^LGejd`M~>6T7mvJfRP;mzP>2J$VsJAhs zHT6#YJ&q_@3qemNF+MOE1_>&LNQUN1Q5{>9|WmaRsPXr{z^LUsCMVC;DAFM2+ zFl4l#KO(EnhiE%bi%zcLV4(O%Mmc-%BZQ+)ENjS3eh$C`SM~lz4GM}e-&ZNP(~0mI zqp}L^qzdHAS0uRultsfiX2gXpa>;R&g%3=^5!1Wh$|hR`X5$mwL` z-RtD|z%;LmD=`7|XAw-MG^dVhzfXr=1n={Re71xl;-rL0 zyp+~B%C=DA1y~be)|9vh+wlH>XYuArRQoqL|mc)CE z0{>l)dF3|U*pe>~3Z^n2%rAAdzE0Y_~Od=EQ*PEn=~kHw8QPK$fEn54+0 z^Lk=2#?v}hF%yr+#;TlsG!cuDvE{7g-p<@G^uA@!MVps4kS%?e&I;6-AxqZ~a~(Qb zTZ1v;xw>EH*?MGh^XY0t(3X~sgyYw!L=o>aWhOVFV?!iLtn#Asf1&MttG#t722Ln! zq(J7|eT|SdZG=RVuj4|R&TfMq-vprs7&3{64fKiSX4|77Puz~M*NG)F$cvrS7d^KD zbYVoVNv?E_m5#ka4*p4pw2^g|J!jSr`ohGl&qtx)t?y4SUJSS`(c-4?h(IJdEY#x9 zi>Fds014=aali#6bxzQ>t~d?7A`!nekfY_yh|E3p-Ihk<^lr&hM;9?z?}d9 delta 2467 zcmV;U30(HI6onKbFoFsJ0s#Xsf(g0?2`Yw2hW8Bt2LYgh39AHx38yfE38RrBMt_uA zwFSC3>$4}_?1eVvhG?|2l~n=*0K-rOf&|E6|FW)O>3*K6r*YOiYv1SnL$N3?#jFBm zTJBo(WGA&Zs;cCB@JST|2s%3_2-kf0Rbgj5zsKzNxe3i3PEDdC3#$Jfyry25vLTOw zvk@5?OHKt<1M-0Q>%un7P9v||Re!Ub`^ErM3NOrZM4N-QSO2nb;LX1#xX8|O)^@QJ z81HfyYD}<1K*>=Ys;%`%AfDnX9H*C)Qk7|M;NY7rEOjD#1-4#4<@euv-Q(5!+7>AB zN2NIljY{@maIPeG?B+>7EKFxE;1cRDSO;F^^YvM?LFTe3*5pf(7nnMm2Y;k@6f4!V z&DzG)l89RgUj)L6)M!b>a#P?qe-cW$%P>K0j3^l`8cL05W9MdafQ4OtpbF^C&Mep=j)&s=I_?P zuS_ zM3F{}x7uRerB;&0d0DXFt~;FoeG{Wzd-Jy}5;I3xwlXuiDgjG_`-2D5KX<}#93QCp z5#9; zgcuHf+u9kFhAhn{s(F&wrGA&}-7(A<|Pr4I-ft={znjS0lb6NUBpcSk*67DWg9nRf>ju z_;L1YbLzCT2%gN@pMzlN7(DUiJ1fBiXGaA$5dcVQz{~?d&fdS8)W>Qny%$X1$$GxwV+>GP|Wc#75xC z!o5YRJ+ui*p|xj2AZBmizX#`1o(Bdn8bnIdgIGe0ynpd?i`OCpW^*%GCh1F#U<)HU zQ692YpY6_SmH5f_LkJ+fT#Jae)QGHy>uU58$24YSfu3PT${Ry6L5U^dn{pQEO4V}@ zFDi$<8E={!!5ldIP2%%Uz~qbjl63GlJ*LD@^UR{F+MOE1_>&LNQUN1K_yPyQa1bS)s8AyQw%%Ms*M)&uNBdTKAx@02Y80F#F$#XLnwRA_#jmj)*>@ z;%#e3gK|N+%-i&)hJsN&R_&R#J)`>_c(vkdp=g!r9O2!i=wCbGuKOr&P5l(q6azKn zf52N~%AM{`W-1=^!H;i+=MLa*Pr(`oP+=MX=$#iQZnOe$An4?Ok3_=bR=nSfMKREc0S_(5E= z9KQz(x`1b0gi$st#0Lcm>uHDjyfWfmq==a5l&mLE8e5Lq0IPi$S7`At@b^>dydLzl z?l{qN%;SlsT!Qw~8mMg@if8TEf07fG&{Gs1{x2ciJmhihX<)cgdiMaKke@Dd}+i3OqC!l^43WoXqVVce`$QpTS=t*;(Qo; zrr-b^`WovIO4k7x9E$sY7VV*W>%BLg3=os4_Vfy6m#`yw>HIb((m zTi#|4tMfasaGhGVMMf+mIiN5;Fd;Ar1_dh)0|FWa00b1^dz7{>D{Ql_rn*)$iMo^l hL-id56bMkA5=vf6&r$2Zm1M7z!!eK64*~-LhM*wCnwtOs diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SniX509ExtendedKeyManager.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SniX509ExtendedKeyManager.java index 5b1f998b65ac..64a356b0d308 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SniX509ExtendedKeyManager.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SniX509ExtendedKeyManager.java @@ -30,6 +30,8 @@ import java.util.Objects; import java.util.function.UnaryOperator; import java.util.stream.Collectors; +import javax.net.ssl.ExtendedSSLSession; +import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIMatcher; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLHandshakeException; @@ -47,7 +49,6 @@ */ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager { - public static final String SNI_X509 = "org.eclipse.jetty.util.ssl.snix509"; private static final Logger LOG = LoggerFactory.getLogger(SniX509ExtendedKeyManager.class); private final X509ExtendedKeyManager _delegate; @@ -116,14 +117,29 @@ protected String chooseServerAlias(String keyType, Principal[] issuers, Collecti Arrays.stream(mangledAliases) .forEach(alias -> aliasMap.put(getAliasMapper().apply(alias), alias)); - // Find our SNIMatcher. There should only be one and it always matches (always returns true - // from AliasSNIMatcher.matches), but it will capture the SNI Host if one was presented. - String host = matchers == null ? null : matchers.stream() - .filter(SslContextFactory.AliasSNIMatcher.class::isInstance) - .map(SslContextFactory.AliasSNIMatcher.class::cast) - .findFirst() - .map(SslContextFactory.AliasSNIMatcher::getHost) - .orElse(null); + String host = null; + if (session instanceof ExtendedSSLSession) + { + host = ((ExtendedSSLSession)session).getRequestedServerNames().stream() + .findAny() + .filter(SNIHostName.class::isInstance) + .map(SNIHostName.class::cast) + .map(SNIHostName::getAsciiName) + .orElse(null); + } + if (host == null) + { + // Find our SNIMatcher. There should only be one and it always matches (always returns true + // from AliasSNIMatcher.matches), but it will capture the SNI Host if one was presented. + host = matchers == null ? null : matchers.stream() + .filter(SslContextFactory.AliasSNIMatcher.class::isInstance) + .map(SslContextFactory.AliasSNIMatcher.class::cast) + .findFirst() + .map(SslContextFactory.AliasSNIMatcher::getHost) + .orElse(null); + } + if (session != null && host != null) + session.putValue(SslContextFactory.Server.SNI_HOST, host); try { @@ -152,9 +168,6 @@ protected String chooseServerAlias(String keyType, Principal[] issuers, Collecti return null; } - if (session != null) - session.putValue(SNI_X509, x509); - // Convert the selected alias back to the original // value before the alias mapping performed above. String mangledAlias = aliasMap.get(alias); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index 8a2c3fda48e5..8ad949f8685b 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -1864,7 +1864,7 @@ public SSLParameters customize(SSLParameters sslParams) sslParams.setEndpointIdentificationAlgorithm(getEndpointIdentificationAlgorithm()); sslParams.setUseCipherSuitesOrder(isUseCipherSuitesOrder()); if (!_certHosts.isEmpty() || !_certWilds.isEmpty()) - sslParams.setSNIMatchers(Collections.singletonList(new AliasSNIMatcher())); + sslParams.setSNIMatchers(List.of(new AliasSNIMatcher())); if (_selectedCipherSuites != null) sslParams.setCipherSuites(_selectedCipherSuites); if (_selectedProtocols != null) @@ -2032,7 +2032,7 @@ private Factory(KeyStore keyStore, KeyStore trustStore, SSLContext context) } } - class AliasSNIMatcher extends SNIMatcher + static class AliasSNIMatcher extends SNIMatcher { private String _host; @@ -2095,6 +2095,8 @@ protected void checkConfiguration() @ManagedObject public static class Server extends SslContextFactory implements SniX509ExtendedKeyManager.SniSelector { + public static final String SNI_HOST = "org.eclipse.jetty.util.ssl.sniHost"; + private boolean _needClientAuth; private boolean _wantClientAuth; private boolean _sniRequired; diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java index 73f59a8cd3fd..26a0158710ef 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java @@ -18,14 +18,13 @@ package org.eclipse.jetty.util.ssl; -import java.security.cert.CertificateParsingException; +import java.net.InetAddress; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import javax.security.auth.x500.X500Principal; @@ -37,17 +36,15 @@ public class X509 { private static final Logger LOG = LoggerFactory.getLogger(X509.class); - /* * @see {@link X509Certificate#getKeyUsage()} */ private static final int KEY_USAGE__KEY_CERT_SIGN = 5; - /* - * * @see {@link X509Certificate#getSubjectAlternativeNames()} */ private static final int SUBJECT_ALTERNATIVE_NAMES__DNS_NAME = 2; + private static final int SUBJECT_ALTERNATIVE_NAMES__IP_ADDRESS = 7; public static boolean isCertSign(X509Certificate x509) { @@ -63,39 +60,62 @@ public static boolean isCertSign(X509Certificate x509) private final String _alias; private final Set _hosts = new LinkedHashSet<>(); private final Set _wilds = new LinkedHashSet<>(); + private final Set _addresses = new LinkedHashSet<>(); - public X509(String alias, X509Certificate x509) throws CertificateParsingException, InvalidNameException + public X509(String alias, X509Certificate x509) { _alias = alias; _x509 = x509; - // Look for alternative name extensions - Collection> altNames = x509.getSubjectAlternativeNames(); - if (altNames != null) + try { - for (List list : altNames) + // Look for alternative name extensions + Collection> altNames = x509.getSubjectAlternativeNames(); + if (altNames != null) { - if (((Number)list.get(0)).intValue() == SUBJECT_ALTERNATIVE_NAMES__DNS_NAME) + for (List list : altNames) { - String cn = list.get(1).toString(); + int nameType = ((Number)list.get(0)).intValue(); + switch (nameType) + { + case SUBJECT_ALTERNATIVE_NAMES__DNS_NAME: + { + String name = list.get(1).toString(); + if (LOG.isDebugEnabled()) + LOG.debug("Certificate alias={} SAN dns={} in {}", alias, name, this); + addName(name); + break; + } + case SUBJECT_ALTERNATIVE_NAMES__IP_ADDRESS: + { + String address = list.get(1).toString(); + if (LOG.isDebugEnabled()) + LOG.debug("Certificate alias={} SAN ip={} in {}", alias, address, this); + addAddress(address); + break; + } + default: + break; + } + } + } + + // If no names found, look up the CN from the subject + LdapName name = new LdapName(x509.getSubjectX500Principal().getName(X500Principal.RFC2253)); + for (Rdn rdn : name.getRdns()) + { + if (rdn.getType().equalsIgnoreCase("CN")) + { + String cn = rdn.getValue().toString(); if (LOG.isDebugEnabled()) - LOG.debug("Certificate SAN alias={} CN={} in {}", alias, cn, this); + LOG.debug("Certificate CN alias={} CN={} in {}", alias, cn, this); addName(cn); } } } - - // If no names found, look up the CN from the subject - LdapName name = new LdapName(x509.getSubjectX500Principal().getName(X500Principal.RFC2253)); - for (Rdn rdn : name.getRdns()) + catch (Exception x) { - if (rdn.getType().equalsIgnoreCase("CN")) - { - String cn = rdn.getValue().toString(); - if (LOG.isDebugEnabled()) - LOG.debug("Certificate CN alias={} CN={} in {}", alias, cn, this); - addName(cn); - } + throw new IllegalArgumentException(x); } } @@ -111,6 +131,28 @@ protected void addName(String cn) } } + private void addAddress(String host) + { + // Class InetAddress handles IPV6 brackets and IPv6 short forms, so that [::1] + // would match 0:0:0:0:0:0:0:1 as well as 0000:0000:0000:0000:0000:0000:0000:0001. + InetAddress address = toInetAddress(host); + if (address != null) + _addresses.add(address); + } + + private InetAddress toInetAddress(String address) + { + try + { + return InetAddress.getByName(address); + } + catch (Throwable x) + { + LOG.trace("IGNORED", x); + return null; + } + } + public String getAlias() { return _alias; @@ -144,6 +186,11 @@ public boolean matches(String host) if (_wilds.contains(domain)) return true; } + + InetAddress address = toInetAddress(host); + if (address != null) + return _addresses.contains(address); + return false; } diff --git a/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketTester.java b/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketTester.java index 4eb84f8cc0aa..37893add4230 100644 --- a/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketTester.java +++ b/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketTester.java @@ -89,7 +89,7 @@ protected Socket newClient(int port, String extensions) throws Exception protected static HttpFields.Mutable newUpgradeRequest(String extensions) { HttpFields.Mutable fields = HttpFields.build() - .add(HttpHeader.HOST, "127.0.0.1") + .add(HttpHeader.HOST, "localhost") .add(HttpHeader.UPGRADE, "websocket") .add(HttpHeader.CONNECTION, "Upgrade") .add(HttpHeader.SEC_WEBSOCKET_KEY, NON_RANDOM_KEY) diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java index 35c79d7b6a81..1a47f5b16606 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java @@ -47,6 +47,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http2.FlowControlStrategy; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.Net; @@ -349,6 +350,10 @@ public void testClientCannotValidateServerCertificate(Transport transport) throw Assumptions.assumeTrue(scenario.transport.isTlsBased()); scenario.startServer(new EmptyServerHandler()); + // Disable validations on the server to be sure + // that the test failure happens during the + // validation of the certificate on the client. + scenario.httpConfig.getCustomizer(SecureRequestCustomizer.class).setSniHostCheck(false); // Use a default SslContextFactory, requests should fail because the server certificate is unknown. SslContextFactory.Client clientTLS = scenario.newClientSslContextFactory(); @@ -361,9 +366,9 @@ public void testClientCannotValidateServerCertificate(Transport transport) throw assertThrows(ExecutionException.class, () -> { - // Use IP address since the certificate contains a host name. + // Use an IP address not present in the certificate. int serverPort = ((ServerConnector)scenario.connector).getLocalPort(); - scenario.client.newRequest("https://127.0.0.1:" + serverPort) + scenario.client.newRequest("https://127.0.0.2:" + serverPort) .timeout(5, TimeUnit.SECONDS) .send(); }); diff --git a/tests/test-http-client-transport/src/test/resources/keystore.p12 b/tests/test-http-client-transport/src/test/resources/keystore.p12 index 089e9704a7a83ab924343e74900eeee8b264e82a..8934437fa14490265dfdb5d9a55bfaa88d039dcd 100644 GIT binary patch delta 2499 zcmV;!2|V_N6r~g*FoFsp0s#Xsf(hCN2`Yw2hW8Bt2LYgh3Cje63CA#k3B!>hMt_+e zq;Ygay&)bx^Vh*K1T4(NTBHI40K-rOf&|Et;?!(0yYpqk);dU-j*jJ#NUJbZ!dgeR?gYbVDRgEm_YI6c)S-znn}*(Ae%egRD=lmY&z-zNl<$juMv!q ze_UaHUVF$Na~X$L3nPF7+Oc!$Fn%n0vSr?k%;Y3lgAXrIr663Ex}4kfLIXs@rzP%9Nu-HH z=bLMU&AXIzFTnE}NwMO~N)TH{Y|T}Qd8_$Z32P1CV?X>8N~fk>uh^bu(gc?0?VsrG z%j$4Il=JQGfm?p6AN>&iBY#OHMkUX|5eY+mNq=Z{IbXd}F* ze9E9lO99G=#G^SG-uO0BjH-+}pm8DmiUPNWUv5dEn*@B@CEhE1!Bvnr z+@(Ej&O?fxza9;A5M)}_x0lx+GrA%_?`*Bcc^oY!{DZDX3-(NyNztlN%asuV;`_WL@k#&inBk+4*M` z<6MUID)dwcDfsz8JT$t#W#ztYsi?B3wc6nipXy%whN&1XZ1Y593oP4cIL)yM`z9r? zWEzf%5Ed_WUwY)-Ke5KNT)YArjEmNV8Xuw@-JzXSNzQ!X*ngJ}o|9?Iw~Gz@e$`(W zI>0XEiGRwLhS8QU$&szPKv~hz?&rVrQsZlFw)kGXx6To>do^(l#lm$2lM@NY!&LyJ zyJHs}G_1ll4fp>>2n@(`&;v-q_SQ50XC1jCYXaHg(T*K4l$W9576RTl(J zuMxsa+iObVv~Yk^{NQ#P9`lLHJeWk--OOQ8#L>tzAHS`k_eUuZFZcG%#s|`^47`ZA zCpdF}&KFWXs?7GvcF1Ytrf1LnmBz6r$~c<;z|G%aeSheDlmfYdz6Qm{1sV zBhztX$A1j?rtBaYiQ-ruyyXx9ha5!|)XY5btJB`-OfRMLqz>A+gH$@f6!FNYmwuz$%Ad>lsQ30 z38v<@td6;27=2*@F+MOE1_>&LNQU`5)FoFa+ldAN1OO>%N9daX!+ZhbB*#SlO|wXCwbolp*3eaOEuv?1rd>N|ntT!J4j>lEP@x4; z51r{bZSkgsFip3}BRwBh+IYI`;<3dHdxHeV|-O6xK_9eYal0+Eq0)kA)nx4A$;W-2A#GC`hoDTu2BkR(v(0-k+hX@|c2$pcCtE0qKwsYDPheVI~F-#eZy4e~yLExgH z&e=fW7T5(hlN+}TT7ZqqE#LGGAsH>sHX(oK%-naYD6WA^chM73(>e3E7b=* z%w$-&L7jJNxL-BNV#xnh2%WkLe}yQ9i(FTLs@{B@lM5 zgWE+&fHqGbF?70=)GC?WRd#h=5!@eh)^?UFMTNEdH*5Djas(4N#M*_@#*FZr2Y@af znRPGNA=3WLrN=Y*tgNk_Ffi5~FVul<*FnxO9Up2!qZ~VBR!luLE}7y3e^plId3kT=tsWN-VAf1c~4qc<;wdD+q6 zkcSDcPX67_AM@B6_Qlu}21ptUx9Dli%@V9Mt{$LaH7cMtW4umbwQCO#E8>omX3QWY zVqj+QgL-l z1EE6Yc;VeR`E%`QWv)985!!idCYC5BSVrh36}andGO}0ZSNZR~oL-ubVJ&J@yv8s- zFd;Ar1_dh)0|FWa00b1-76?>L0vib{=}$d~zA>COy!U_v6iz|T9ES2Q4NJDPBr<5A NgS)3G-vR>xhM+&`$tC~* delta 2467 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