View Javadoc
1   /*
2    * Copyright 2018 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.buffer.ByteBufAllocator;
19  import io.netty.util.IllegalReferenceCountException;
20  
21  import javax.net.ssl.X509KeyManager;
22  import java.util.concurrent.ConcurrentHashMap;
23  
24  /**
25   * {@link OpenSslKeyMaterialProvider} that will cache the {@link OpenSslKeyMaterial} to reduce the overhead
26   * of parsing the chain and the key for generation of the material.
27   * <p>
28   * Cache reads are lock-free ({@link ConcurrentHashMap#get} + {@code retain()});
29   * if a concurrent eviction releases the material in between, {@code retain()}
30   * detects the dead reference count and the read falls back to a cache miss.
31   * Mutations (insert, evict, destroy) use {@link ConcurrentHashMap#compute} /
32   * {@link ConcurrentHashMap#computeIfPresent} for atomicity.
33   */
34  final class OpenSslCachingKeyMaterialProvider extends OpenSslKeyMaterialProvider {
35  
36      private final int maxCachedEntries;
37      private final ConcurrentHashMap<String, OpenSslKeyMaterial> cache =
38              new ConcurrentHashMap<String, OpenSslKeyMaterial>();
39      private volatile boolean destroyed;
40  
41      OpenSslCachingKeyMaterialProvider(X509KeyManager keyManager, String password, int maxEntries) {
42          super(keyManager, password);
43          maxCachedEntries = maxEntries;
44      }
45  
46      /**
47       * Lock-free cache lookup. If a concurrent eviction releases the material between
48       * {@code get} and {@code retain}, the dead reference count is detected and treated
49       * as a cache miss.
50       */
51      private OpenSslKeyMaterial getAndRetain(String alias) {
52          OpenSslKeyMaterial m = cache.get(alias);
53          if (m != null) {
54              try {
55                  return m.retain();
56              } catch (IllegalReferenceCountException e) {
57                  return null;
58              }
59          }
60          return null;
61      }
62  
63      /**
64       * Atomically inserts material if absent, or retains the existing entry.
65       */
66      private OpenSslKeyMaterial putIfAbsentAndRetain(String alias, OpenSslKeyMaterial material) {
67          return cache.compute(alias, (k, existing) -> {
68              if (existing != null) {
69                  existing.retain();
70                  return existing;
71              }
72              material.retain();
73              return material;
74          });
75      }
76  
77      /**
78       * Atomically removes and releases the entry for the given alias.
79       */
80      private void removeAndRelease(String alias) {
81          cache.computeIfPresent(alias, (k, v) -> {
82              v.release();
83              return null;
84          });
85      }
86  
87      private void evictStaleEntries() {
88          for (String alias : cache.keySet()) {
89              if (keyManager().getCertificateChain(alias) == null) {
90                  removeAndRelease(alias);
91              }
92          }
93      }
94  
95      @Override
96      OpenSslKeyMaterial chooseKeyMaterial(ByteBufAllocator allocator, String alias) throws Exception {
97          OpenSslKeyMaterial material = getAndRetain(alias);
98          if (material == null) {
99              material = super.chooseKeyMaterial(allocator, alias);
100             if (material == null) {
101                 return null;
102             }
103 
104             if (cache.size() >= maxCachedEntries) {
105                 evictStaleEntries();
106                 if (cache.size() >= maxCachedEntries) {
107                     return material;
108                 }
109             }
110             // Returns the newly created material, or an existing entry if another thread inserted first.
111             OpenSslKeyMaterial old = putIfAbsentAndRetain(alias, material);
112             if (old != material) {
113                 material.release();
114                 material = old;
115             } else if (destroyed) {
116                 // We may have inserted an entry after the provider has been destroyed. Help with the cleanup.
117                 removeAndReleaseAllEntries();
118             }
119         }
120         return material;
121     }
122 
123     int cacheSize() {
124         return cache.size();
125     }
126 
127     @Override
128     void destroy() {
129         destroyed = true;
130         try {
131             removeAndReleaseAllEntries();
132         } finally {
133             super.destroy();
134         }
135     }
136 
137     private void removeAndReleaseAllEntries() {
138         do  {
139             for (String alias : cache.keySet()) {
140                 removeAndRelease(alias);
141             }
142         } while (!cache.isEmpty());
143     }
144 }