View Javadoc
1   /*
2    * Copyright 2021 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.ssl;
17  
18  import io.netty.internal.tcnative.SSL;
19  import io.netty.util.AsciiString;
20  
21  import java.util.ArrayList;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  
28  /**
29   * {@link OpenSslSessionCache} that is used by the client-side.
30   */
31  final class OpenSslClientSessionCache extends OpenSslSessionCache {
32      private final Map<HostPort, Set<NativeSslSession>> sessions = new HashMap<HostPort, Set<NativeSslSession>>();
33  
34      OpenSslClientSessionCache(OpenSslEngineMap engineMap) {
35          super(engineMap);
36      }
37  
38      @Override
39      protected boolean sessionCreated(NativeSslSession session) {
40          assert Thread.holdsLock(this);
41          HostPort hostPort = keyFor(session.getPeerHost(), session.getPeerPort());
42          if (hostPort == null) {
43              return false;
44          }
45          Set<NativeSslSession> sessionsForHost = sessions.get(hostPort);
46          if (sessionsForHost == null) {
47              // Let's start with something small as usually the server does not provide too many of these per hostPort
48              // mapping.
49              sessionsForHost = new HashSet<NativeSslSession>(4);
50              sessions.put(hostPort, sessionsForHost);
51          }
52          sessionsForHost.add(session);
53          return true;
54      }
55  
56      @Override
57      protected void sessionRemoved(NativeSslSession session) {
58          assert Thread.holdsLock(this);
59          HostPort hostPort = keyFor(session.getPeerHost(), session.getPeerPort());
60          if (hostPort == null) {
61              return;
62          }
63          Set<NativeSslSession> sessionsForHost = sessions.get(hostPort);
64          if (sessionsForHost != null) {
65              sessionsForHost.remove(session);
66              if (sessionsForHost.isEmpty()) {
67                  sessions.remove(hostPort);
68              }
69          }
70      }
71  
72      @Override
73      boolean setSession(long ssl, OpenSslSession session, String host, int port) {
74          HostPort hostPort = keyFor(host, port);
75          if (hostPort == null) {
76              return false;
77          }
78          NativeSslSession nativeSslSession = null;
79          final boolean reused;
80          boolean singleUsed = false;
81          synchronized (this) {
82              Set<NativeSslSession> sessionsForHost = sessions.get(hostPort);
83              if (sessionsForHost == null) {
84                  return false;
85              }
86              if (sessionsForHost.isEmpty()) {
87                  sessions.remove(hostPort);
88                  // There is no session that we can use.
89                  return false;
90              }
91  
92              List<NativeSslSession> toBeRemoved = null;
93              // Loop through all the sessions that might be usable and check if we can use one of these.
94              for (NativeSslSession sslSession : sessionsForHost) {
95                  if (sslSession.isValid()) {
96                      nativeSslSession = sslSession;
97                      break;
98                  } else {
99                      if (toBeRemoved == null) {
100                         toBeRemoved = new ArrayList<NativeSslSession>(2);
101                     }
102                     toBeRemoved.add(sslSession);
103                 }
104             }
105 
106             // Remove everything that is not valid anymore
107             if (toBeRemoved != null) {
108                 for (NativeSslSession sslSession : toBeRemoved) {
109                     removeSessionWithId(sslSession.sessionId());
110                 }
111             }
112             if (nativeSslSession == null) {
113                 // Couldn't find a valid session that could be used.
114                 return false;
115             }
116 
117             // Try to set the session, if true is returned OpenSSL incremented the reference count
118             // of the underlying SSL_SESSION*.
119             reused = SSL.setSession(ssl, nativeSslSession.session());
120             if (reused) {
121                 singleUsed = nativeSslSession.shouldBeSingleUse();
122             }
123         }
124 
125         if (reused) {
126             if (singleUsed) {
127                 // Should only be used once
128                 nativeSslSession.invalidate();
129                 session.invalidate();
130             }
131             nativeSslSession.setLastAccessedTime(System.currentTimeMillis());
132             session.setSessionDetails(nativeSslSession.getCreationTime(), nativeSslSession.getLastAccessedTime(),
133                     nativeSslSession.sessionId(), nativeSslSession.keyValueStorage);
134         }
135         return reused;
136     }
137 
138     private static HostPort keyFor(String host, int port) {
139         if (host == null && port < 1) {
140             return null;
141         }
142         return new HostPort(host, port);
143     }
144 
145     @Override
146     synchronized void clear() {
147         super.clear();
148         sessions.clear();
149     }
150 
151     /**
152      * Host / Port tuple used to find a {@link OpenSslSession} in the cache.
153      */
154     private static final class HostPort {
155         private final int hash;
156         private final String host;
157         private final int port;
158 
159         HostPort(String host, int port) {
160             this.host = host;
161             this.port = port;
162             // Calculate a hashCode that does ignore case.
163             this.hash = 31 * AsciiString.hashCode(host) + port;
164         }
165 
166         @Override
167         public int hashCode() {
168             return hash;
169         }
170 
171         @Override
172         public boolean equals(Object obj) {
173             if (!(obj instanceof HostPort)) {
174                 return false;
175             }
176             HostPort other = (HostPort) obj;
177             return port == other.port && host.equalsIgnoreCase(other.host);
178         }
179 
180         @Override
181         public String toString() {
182             return "HostPort{" +
183                     "host='" + host + '\'' +
184                     ", port=" + port +
185                     '}';
186         }
187     }
188 }