From 84e4d014c466cab1656bbd38cc9d7b31b3a13de4 Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Thu, 8 Feb 2024 17:46:09 +0100 Subject: [PATCH] Ensure key / values are shared between resumed sessions (#13819) Motivation: When a session is resumed we need to also ensure we preserve the values that were put into the internal storage of the session. Modifications: Ensure we preserve the values / keys of the internal storage on resumption Result: Correctly implement session caching and reuse --------- Co-authored-by: Chris Vest --- .../handler/ssl/ExtendedOpenSslSession.java | 15 +++- .../ssl/OpenSslClientSessionCache.java | 11 +-- .../io/netty/handler/ssl/OpenSslSession.java | 33 ++++++++- .../handler/ssl/OpenSslSessionCache.java | 36 +++++++--- .../handler/ssl/OpenSslSessionContext.java | 4 +- .../ssl/ReferenceCountedOpenSslEngine.java | 65 ++++++++--------- .../ssl/ConstantTrustManagerFactory.java | 49 +++++++++++++ .../ssl/EmptyExtendedX509TrustManager.java | 70 +++++++++++++++++++ .../io/netty/handler/ssl/SSLEngineTest.java | 68 +++++++++++++++++- 9 files changed, 294 insertions(+), 57 deletions(-) create mode 100644 handler/src/test/java/io/netty/handler/ssl/ConstantTrustManagerFactory.java create mode 100644 handler/src/test/java/io/netty/handler/ssl/EmptyExtendedX509TrustManager.java diff --git a/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java b/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java index 1a9a16533a41..13fa009bb264 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java +++ b/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java @@ -64,14 +64,25 @@ public List getStatusResponses() { return Collections.emptyList(); } + @Override + public void prepareHandshake() { + wrapped.prepareHandshake(); + } + + @Override + public Map keyValueStorage() { + return wrapped.keyValueStorage(); + } + @Override public OpenSslSessionId sessionId() { return wrapped.sessionId(); } @Override - public void setSessionDetails(long creationTime, long lastAccessedTime, OpenSslSessionId id) { - wrapped.setSessionDetails(creationTime, lastAccessedTime, id); + public void setSessionDetails(long creationTime, long lastAccessedTime, OpenSslSessionId id, + Map keyValueStorage) { + wrapped.setSessionDetails(creationTime, lastAccessedTime, id, keyValueStorage); } @Override diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientSessionCache.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientSessionCache.java index 24ef762e50b5..ff7d1d546f1f 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientSessionCache.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientSessionCache.java @@ -55,10 +55,10 @@ protected void sessionRemoved(NativeSslSession session) { } @Override - void setSession(long ssl, OpenSslSession session, String host, int port) { + boolean setSession(long ssl, OpenSslSession session, String host, int port) { HostPort hostPort = keyFor(host, port); if (hostPort == null) { - return; + return false; } final NativeSslSession nativeSslSession; final boolean reused; @@ -66,11 +66,11 @@ void setSession(long ssl, OpenSslSession session, String host, int port) { synchronized (this) { nativeSslSession = sessions.get(hostPort); if (nativeSslSession == null) { - return; + return false; } if (!nativeSslSession.isValid()) { removeSessionWithId(nativeSslSession.sessionId()); - return; + return false; } // Try to set the session, if true is returned OpenSSL incremented the reference count // of the underlying SSL_SESSION*. @@ -88,8 +88,9 @@ void setSession(long ssl, OpenSslSession session, String host, int port) { } nativeSslSession.setLastAccessedTime(System.currentTimeMillis()); session.setSessionDetails(nativeSslSession.getCreationTime(), nativeSslSession.getLastAccessedTime(), - nativeSslSession.sessionId()); + nativeSslSession.sessionId(), nativeSslSession.keyValueStorage); } + return reused; } private static HostPort keyFor(String host, int port) { diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslSession.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslSession.java index 5b4e655a6095..d7ed0fabfd35 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslSession.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSession.java @@ -18,12 +18,19 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import java.security.cert.Certificate; +import java.util.Map; /** * {@link SSLSession} that is specific to our native implementation. */ interface OpenSslSession extends SSLSession { + /** + * Called on a handshake session before being exposed to a {@link javax.net.ssl.TrustManager}. + * Session data must be cleared by this call. + */ + void prepareHandshake(); + /** * Return the {@link OpenSslSessionId} that can be used to identify this session. */ @@ -36,9 +43,31 @@ interface OpenSslSession extends SSLSession { void setLocalCertificate(Certificate[] localCertificate); /** - * Set the {@link OpenSslSessionId} for the {@link OpenSslSession}. + * Set the details for the session which might come from a cache. + * + * @param creationTime the time at which the session was created. + * @param lastAccessedTime the time at which the session was last accessed via the session infrastructure (cache). + * @param id the {@link OpenSslSessionId} + * @param keyValueStorage the key value store. See {@link #keyValueStorage()}. + */ + void setSessionDetails(long creationTime, long lastAccessedTime, OpenSslSessionId id, + Map keyValueStorage); + + /** + * Return the underlying {@link Map} that is used by the following methods: + * + *
    + *
  • {@link #putValue(String, Object)}
  • + *
  • {@link #removeValue(String)}
  • + *
  • {@link #getValue(String)}
  • + *
  • {@link #getValueNames()}
  • + *
+ * + * The {@link Map} must be thread-safe! + * + * @return storage */ - void setSessionDetails(long creationTime, long lastAccessedTime, OpenSslSessionId id); + Map keyValueStorage(); /** * Set the last access time which will be returned by {@link #getLastAccessedTime()}. diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionCache.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionCache.java index 65fbcda243b5..7a04419a5b08 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionCache.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionCache.java @@ -31,7 +31,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** @@ -148,10 +147,14 @@ public boolean sessionCreated(long ssl, long sslSession) { // We couldn't find the engine itself. return false; } + OpenSslSession openSslSession = (OpenSslSession) engine.getSession(); + // Create the native session that we will put into our cache. We will share the key-value storage + // with the already existing session instance. NativeSslSession session = new NativeSslSession(sslSession, engine.getPeerHost(), engine.getPeerPort(), - getSessionTimeout() * 1000L); - ((OpenSslSession) engine.getSession()).setSessionDetails( - session.creationTime, session.lastAccessedTime, session.sessionId()); + getSessionTimeout() * 1000L, openSslSession.keyValueStorage()); + + openSslSession.setSessionDetails( + session.creationTime, session.lastAccessedTime, session.sessionId(), session.keyValueStorage); synchronized (this) { // Mimic what OpenSSL is doing and expunge every 255 new sessions // See https://www.openssl.org/docs/man1.0.2/man3/SSL_CTX_flush_sessions.html @@ -209,14 +212,15 @@ public final long getSession(long ssl, byte[] sessionId) { if (engine != null) { OpenSslSession sslSession = (OpenSslSession) engine.getSession(); sslSession.setSessionDetails(session.getCreationTime(), - session.getLastAccessedTime(), session.sessionId()); + session.getLastAccessedTime(), session.sessionId(), session.keyValueStorage); } return session.session(); } - void setSession(long ssl, OpenSslSession session, String host, int port) { + boolean setSession(long ssl, OpenSslSession session, String host, int port) { // Do nothing by default as this needs special handling for the client side. + return false; } /** @@ -293,6 +297,9 @@ static final class NativeSslSession implements OpenSslSession { static final ResourceLeakDetector LEAK_DETECTOR = ResourceLeakDetectorFactory.instance() .newResourceLeakDetector(NativeSslSession.class); private final ResourceLeakTracker leakTracker; + + final Map keyValueStorage; + private final long session; private final String peerHost; private final int peerPort; @@ -303,17 +310,30 @@ static final class NativeSslSession implements OpenSslSession { private volatile boolean valid = true; private boolean freed; - NativeSslSession(long session, String peerHost, int peerPort, long timeout) { + NativeSslSession(long session, String peerHost, int peerPort, long timeout, + Map keyValueStorage) { this.session = session; this.peerHost = peerHost; this.peerPort = peerPort; this.timeout = timeout; this.id = new OpenSslSessionId(io.netty.internal.tcnative.SSLSession.getSessionId(session)); + this.keyValueStorage = keyValueStorage; leakTracker = LEAK_DETECTOR.track(this); } @Override - public void setSessionDetails(long creationTime, long lastAccessedTime, OpenSslSessionId id) { + public Map keyValueStorage() { + return keyValueStorage; + } + + @Override + public void prepareHandshake() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSessionDetails(long creationTime, long lastAccessedTime, + OpenSslSessionId id, Map keyValueStorage) { throw new UnsupportedOperationException(); } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java index e5ecab5b8150..baa83d3ed1f2 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java @@ -216,8 +216,8 @@ final boolean isInCache(OpenSslSessionId id) { return sessionCache.containsSessionWithId(id); } - void setSessionFromCache(long ssl, OpenSslSession session, String host, int port) { - sessionCache.setSession(ssl, session, host, port); + boolean setSessionFromCache(long ssl, OpenSslSession session, String host, int port) { + return sessionCache.setSession(ssl, session, host, port); } final void destroy() { diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java index 3f7224f1df47..cd777173630b 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java @@ -51,6 +51,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import javax.crypto.spec.SecretKeySpec; @@ -1980,7 +1981,11 @@ private SSLEngineResult.HandshakeStatus handshake() throws SSLException { engineMap.add(this); if (!sessionSet) { - parentContext.sessionContext().setSessionFromCache(ssl, session, getPeerHost(), getPeerPort()); + if (!parentContext.sessionContext().setSessionFromCache(ssl, session, getPeerHost(), getPeerPort())) { + // The session was not reused via the cache. Call prepareHandshake() to ensure we remove all previous + // stored key-value pairs. + session.prepareHandshake(); + } sessionSet = true; } @@ -2365,7 +2370,7 @@ private final class DefaultOpenSslSession implements OpenSslSession { private volatile int applicationBufferSize = MAX_PLAINTEXT_LENGTH; private volatile Certificate[] localCertificateChain; - private Map values; + private volatile Map keyValueStorage = new ConcurrentHashMap(); DefaultOpenSslSession(OpenSslSessionContext sessionContext) { this.sessionContext = sessionContext; @@ -2375,18 +2380,34 @@ private SSLSessionBindingEvent newSSLSessionBindingEvent(String name) { return new SSLSessionBindingEvent(session, name); } + @Override + public void prepareHandshake() { + keyValueStorage.clear(); + } + @Override public void setSessionDetails( - long creationTime, long lastAccessedTime, OpenSslSessionId sessionId) { + long creationTime, long lastAccessedTime, OpenSslSessionId sessionId, + Map keyValueStorage) { synchronized (ReferenceCountedOpenSslEngine.this) { if (this.id == OpenSslSessionId.NULL_ID) { this.id = sessionId; this.creationTime = creationTime; this.lastAccessed = lastAccessedTime; + + // Update the key value storage. It's fine to just drop the previous stored values on the floor + // as the JDK does the same in the sense that it will use a new SSLSessionImpl instance once the + // handshake was done + this.keyValueStorage = keyValueStorage; } } } + @Override + public Map keyValueStorage() { + return keyValueStorage; + } + @Override public OpenSslSessionId sessionId() { synchronized (ReferenceCountedOpenSslEngine.this) { @@ -2458,16 +2479,7 @@ public void putValue(String name, Object value) { checkNotNull(name, "name"); checkNotNull(value, "value"); - final Object old; - synchronized (this) { - Map values = this.values; - if (values == null) { - // Use size of 2 to keep the memory overhead small - values = this.values = new HashMap(2); - } - old = values.put(name, value); - } - + final Object old = keyValueStorage.put(name, value); if (value instanceof SSLSessionBindingListener) { // Use newSSLSessionBindingEvent so we always use the wrapper if needed. ((SSLSessionBindingListener) value).valueBound(newSSLSessionBindingEvent(name)); @@ -2478,39 +2490,19 @@ public void putValue(String name, Object value) { @Override public Object getValue(String name) { checkNotNull(name, "name"); - synchronized (this) { - if (values == null) { - return null; - } - return values.get(name); - } + return keyValueStorage.get(name); } @Override public void removeValue(String name) { checkNotNull(name, "name"); - - final Object old; - synchronized (this) { - Map values = this.values; - if (values == null) { - return; - } - old = values.remove(name); - } - + final Object old = keyValueStorage.remove(name); notifyUnbound(old, name); } @Override public String[] getValueNames() { - synchronized (this) { - Map values = this.values; - if (values == null || values.isEmpty()) { - return EMPTY_STRINGS; - } - return values.keySet().toArray(EMPTY_STRINGS); - } + return keyValueStorage.keySet().toArray(EMPTY_STRINGS); } private void notifyUnbound(Object value, String name) { @@ -2532,6 +2524,7 @@ public void handshakeFinished(byte[] id, String cipher, String protocol, byte[] if (!isDestroyed()) { if (this.id == OpenSslSessionId.NULL_ID) { // if the handshake finished and it was not a resumption let ensure we try to set the id + this.id = id == null ? OpenSslSessionId.NULL_ID : new OpenSslSessionId(id); // Once the handshake was done the lastAccessed and creationTime should be the same if we // did not set it earlier via setSessionDetails(...) diff --git a/handler/src/test/java/io/netty/handler/ssl/ConstantTrustManagerFactory.java b/handler/src/test/java/io/netty/handler/ssl/ConstantTrustManagerFactory.java new file mode 100644 index 000000000000..5a5f80d3128c --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/ConstantTrustManagerFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.ssl; + +import io.netty.handler.ssl.util.SimpleTrustManagerFactory; +import io.netty.util.internal.ObjectUtil; + +import java.security.KeyStore; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.TrustManager; + +/** + * Always returns the same trust manager instance. + */ +final class ConstantTrustManagerFactory extends SimpleTrustManagerFactory { + private final TrustManager tm; + + ConstantTrustManagerFactory(TrustManager tm) { + this.tm = ObjectUtil.checkNotNull(tm, "tm"); + } + + @Override + protected void engineInit(KeyStore keyStore) { + // NOOP + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { + // NOOP + } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return new TrustManager[] { tm }; + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/EmptyExtendedX509TrustManager.java b/handler/src/test/java/io/netty/handler/ssl/EmptyExtendedX509TrustManager.java new file mode 100644 index 000000000000..6ed5fc946436 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/EmptyExtendedX509TrustManager.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.ssl; + +import io.netty.util.internal.EmptyArrays; + +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; + +/** + * Test utility for making extended trust manager instances. + */ +class EmptyExtendedX509TrustManager extends X509ExtendedTrustManager { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + throw new CertificateException("Not trusted"); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + throw new CertificateException("Not trusted"); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + throw new CertificateException("Not trusted"); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + throw new CertificateException("Not trusted"); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + throw new CertificateException("Not trusted"); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + throw new CertificateException("Not trusted"); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return EmptyArrays.EMPTY_X509_CERTIFICATES; + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java index 5f797a202832..3d78db962f23 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java @@ -3306,8 +3306,16 @@ private void doHandshakeVerifyReusedAndClose(SSLEngineTestParam param, String ho } assertSessionReusedForEngine(clientEngine, serverEngine, reuse); + String key = "key"; if (reuse) { if (clientSessionReused != SessionReusedState.NOT_REUSED) { + // We should see the previous stored value on session reuse. + // This is broken in conscrypt. + // TODO: Open an issue in the conscrypt project. + if (!Conscrypt.isEngineSupported(clientEngine)) { + assertEquals(Boolean.TRUE, clientEngine.getSession().getValue(key)); + } + Matcher creationTimeMatcher; if (clientSessionReused == SessionReusedState.REUSED) { // If we know for sure it was reused so the accessedTime needs to be larger. @@ -3323,6 +3331,8 @@ private void doHandshakeVerifyReusedAndClose(SSLEngineTestParam param, String ho // If we don't sleep and execution is very fast we will see test-failures once we go into the // reuse branch. Thread.sleep(1); + + clientEngine.getSession().putValue(key, Boolean.TRUE); } closeOutboundAndInbound(param.type(), clientEngine, serverEngine); } finally { @@ -3612,8 +3622,34 @@ private void testSessionAfterHandshake0( clientContextBuilder.keyManager(ssc.key(), ssc.cert()); } } + + final String handshakeKey = "handshake"; + TrustManagerFactory tmf = new ConstantTrustManagerFactory(new EmptyExtendedX509TrustManager() { + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, + String authType, SSLEngine engine) { + // This is broken in conscrypt. + // TODO: Open an issue in the conscrypt project. + if (!Conscrypt.isEngineSupported(engine)) { + assertEquals(0, engine.getHandshakeSession().getValueNames().length); + } + engine.getHandshakeSession().putValue(handshakeKey, Boolean.TRUE); + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, + String authType, SSLEngine engine) { + // This is broken in conscrypt. + // TODO: Open an issue in the conscrypt project. + if (!Conscrypt.isEngineSupported(engine)) { + assertEquals(0, engine.getHandshakeSession().getValueNames().length); + } + engine.getHandshakeSession().putValue(handshakeKey, Boolean.TRUE); + } + }); + clientSslCtx = wrapContext(param, clientContextBuilder - .trustManager(InsecureTrustManagerFactory.INSTANCE) + .trustManager(tmf) .sslProvider(sslClientProvider()) .sslContextProvider(clientSslContextProvider()) .protocols(param.protocols()) @@ -3626,7 +3662,7 @@ private void testSessionAfterHandshake0( if (mutualAuth) { serverContextBuilder.clientAuth(ClientAuth.REQUIRE); } - serverSslCtx = wrapContext(param, serverContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE) + serverSslCtx = wrapContext(param, serverContextBuilder.trustManager(tmf) .sslProvider(sslServerProvider()) .sslContextProvider(serverSslContextProvider()) .protocols(param.protocols()) @@ -3649,6 +3685,16 @@ private void testSessionAfterHandshake0( SSLSession clientSession = clientEngine.getSession(); SSLSession serverSession = serverEngine.getSession(); + // The values should not have been carried over. + // This is broken in conscrypt. + // TODO: Open an issue in the conscrypt project. + if (!Conscrypt.isEngineSupported(clientEngine)) { + assertNull(clientSession.getValue(key)); + } + if (!Conscrypt.isEngineSupported(serverEngine)) { + assertNull(serverSession.getValue(key)); + } + clientSession.removeValue(key); serverSession.removeValue(key); @@ -3690,6 +3736,24 @@ private void testSessionAfterHandshake0( } Object value = new Object(); + // This is broken in conscrypt. + // TODO: Open an issue in the conscrypt project. + if (!Conscrypt.isEngineSupported(clientEngine)) { + assertEquals(1, clientSession.getValueNames().length); + assertEquals(clientSession.getValue(handshakeKey), Boolean.TRUE); + clientSession.removeValue(handshakeKey); + } + + if (mutualAuth) { + // This is broken in conscrypt. + // TODO: Open an issue in the conscrypt project. + if (!Conscrypt.isEngineSupported(serverEngine)) { + // Server trust manager factory is only called if server authenticates clients. + assertEquals(1, serverSession.getValueNames().length); + assertEquals(serverSession.getValue(handshakeKey), Boolean.TRUE); + serverSession.removeValue(handshakeKey); + } + } assertEquals(0, clientSession.getValueNames().length); clientSession.putValue("test", value);