1 package org.metricshub.http;
2
3 /*-
4 * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5 * HTTP Java Client
6 * ჻჻჻჻჻჻
7 * Copyright (C) 2023 MetricsHub
8 * ჻჻჻჻჻჻
9 * Licensed under the Apache License, Version 2.0 (the "License");
10 * you may not use this file except in compliance with the License.
11 * You may obtain a copy of the License at
12 *
13 * http://www.apache.org/licenses/LICENSE-2.0
14 *
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
20 * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
21 */
22
23 import java.net.Authenticator;
24 import java.net.PasswordAuthentication;
25 import java.util.concurrent.ConcurrentHashMap;
26
27 /**
28 * The ThreadSafeNoCacheAuthenticator class is used to provide the specified username/password for the
29 * Web site itself, or for the proxy.
30 * <p>
31 * Additionally, it works around the incredibly annoying limitation of Java's UrlConnection
32 * class and its underlying authentication mechanism, which is... gasp... not Thread-safe!
33 * </p>
34 * <p>
35 * Also, this class works around the fact that Java's HTTP authentication mechanism uses a
36 * cache for all credentials that worked once! Very annoying... again!
37 * </p>
38 * <p>
39 * It must be known that authentication in Java's HTTP client (the HttpUrlConnection class)
40 * is based on the Authenticator class, whose only method to specify credentials stores
41 * the credentials in a static property.
42 * </p>
43 * <p>
44 * <code>Authenticator.setDefault(Authenticator a)</code> is a joke... an annoying joke!
45 * </p>
46 * <p>
47 * Anyway, we will store the credentials in a ConcurrentHashMap where the key is the current Thread.
48 * It's the developer's responsibility to remove the credentials that he doesn't want to keep
49 * in memory.
50 * </p>
51 * <p>
52 * And we override the non-API AuthCache class to disable the caching mechanism, which was causing
53 * unwanted behavior (like getting a successful HTTP response when specifying a wrong password,
54 * simply because you provided the proper password once before...)
55 * </p>
56 *
57 * To use this class, you will need to do the following:
58 * <br>
59 * <code>
60 * Authenticator.setDefault(ThreadSafeAuthenticator.getInstance()); <br>
61 * <br>
62 * ThreadSafeAuthenticator.setCredentials(username, password, proxyUsername, proxyPassword); <br>
63 * <br>
64 * // Then go ahead with your HttpUrlConnection request <br>
65 * </code>
66 *
67 * @author bertrand
68 */
69 @SuppressWarnings("restriction")
70 class ThreadSafeNoCacheAuthenticator extends Authenticator {
71
72 /**
73 * One set of credentials that will be stored in the ConcurrentHashMap,
74 * associated to the Thread making the request
75 *
76 * @author bertrand
77 */
78 private static class CredEntry {
79
80 public String username;
81 public char[] password;
82 public String proxyUsername;
83 public char[] proxyPassword;
84
85 CredEntry(String pUsername, char[] pPassword, String pProxyUsername, char[] pProxyPassword) {
86 username = pUsername;
87 password = pPassword;
88 proxyUsername = pProxyUsername;
89 proxyPassword = pProxyPassword;
90 }
91 }
92
93 /**
94 * Only this class can instantiate itself, as only one instance will ever be required
95 */
96 private ThreadSafeNoCacheAuthenticator() {
97 /* Nothing do to, it's just ro declare the Constructor as private */
98 }
99
100 /**
101 * There will be only one instance of this class, because there is no need
102 * for other instances, as everything is managed in static properties
103 */
104 private static ThreadSafeNoCacheAuthenticator oneSingleAuthenticator;
105
106 static {
107 // Create the one instance required, as store it statically
108 oneSingleAuthenticator = new ThreadSafeNoCacheAuthenticator();
109 }
110
111 /**
112 * @return the one single instance of ThreadSafeAuthenticator, to be used in
113 * <code>Authenticator.setDefault(ThreadSafeAuthenticator.getInstance()</code>
114 */
115 public static ThreadSafeNoCacheAuthenticator getInstance() {
116 return oneSingleAuthenticator;
117 }
118
119 /**
120 * The ConcurrentHashMap that stores the credentials to be used for each Thread.
121 * Each Thread needs to call <code>setCredentials(...)</code> with the credentials that will be
122 * used for upcoming HttpUrlConnection requests.
123 */
124 private static ConcurrentHashMap<Thread, CredEntry> credList = new ConcurrentHashMap<Thread, CredEntry>();
125
126 /**
127 * Sets the credentials that will be used in HttpUrlConnection requests made by this Thread.
128 * <p>
129 * <b>Warning!</b> Each Thread needs to set the credentials it needs to use with HttpUrlConnection.
130 * </p>
131 *
132 * @param pUsername The username to be used to authenticate with the HTTP server
133 * @param pPassword The associated password
134 * @param pProxyUsername The user name to be used to authenticate with the proxy server
135 * @param pProxyPassword The associated password
136 */
137 public static void setCredentials(String pUsername, char[] pPassword, String pProxyUsername, char[] pProxyPassword) {
138 credList.put(Thread.currentThread(), new CredEntry(pUsername, pPassword, pProxyUsername, pProxyPassword));
139 }
140
141 /**
142 * Sets the credentials that will be used in HttpUrlConnection requests made by this Thread.
143 * <p>
144 * <b>Warning!</b> Each Thread needs to set the credentials it needs to use with HttpUrlConnection.
145 * </p>
146 *
147 * @param pUsername The username to be used to authenticate with the HTTP server
148 * @param pPassword The associated password
149 */
150 public static void setCredentials(String pUsername, char[] pPassword) {
151 credList.put(Thread.currentThread(), new CredEntry(pUsername, pPassword, null, null));
152 }
153
154 /**
155 * Removes the credentials associated to this Thread.
156 * <p>
157 * <b>IMPORTANT!</b> Make sure to clear the credentials once finished with the HttpUrlConnection,
158 * both for security reasons and to avoid memory leaks!
159 * </p>
160 */
161 public static void clearCredentials() {
162 credList.remove(Thread.currentThread());
163 }
164
165 public PasswordAuthentication getPasswordAuthentication() {
166 // Get the credentials of the current Thread
167 CredEntry credEntry = credList.get(Thread.currentThread());
168 if (credEntry == null) {
169 // No credentials? Return null
170 return null;
171 }
172
173 // Return the specified username and password for the server, if it's the server requesting it
174 if (getRequestorType() == RequestorType.SERVER && credEntry.username != null && credEntry.password != null) {
175 return (new PasswordAuthentication(credEntry.username, credEntry.password));
176 } else if (
177 getRequestorType() == RequestorType.PROXY && credEntry.proxyUsername != null && credEntry.proxyPassword != null
178 ) {
179 // Or send the username and password for the proxy if it's the proxy requesting it
180 return (new PasswordAuthentication(credEntry.proxyUsername, credEntry.proxyPassword));
181 }
182
183 // Return null if we don't have the necessary credentials (which is the default implementation)
184 return null;
185 }
186
187 /**
188 * This class ensures that no cache is used for credentials to authenticate with an HTTP server
189 * <br>
190 * Courtesy of: https://stackoverflow.com/a/6049879/8494773
191 * @author so_mv
192 *
193 */
194 static class DisabledCache implements sun.net.www.protocol.http.AuthCache {
195
196 public void put(String pkey, sun.net.www.protocol.http.AuthCacheValue value) {}
197
198 public sun.net.www.protocol.http.AuthCacheValue get(String pkey, String skey) {
199 return null;
200 }
201
202 public void remove(String pkey, sun.net.www.protocol.http.AuthCacheValue entry) {}
203 }
204
205 static {
206 sun.net.www.protocol.http.AuthCacheValue.setAuthCache(new DisabledCache());
207 }
208 }