View Javadoc
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 }