1 package org.jsoup.helper; 2 3 import org.jspecify.annotations.Nullable; 4 5 import java.lang.reflect.Constructor; 6 import java.net.Authenticator; 7 import java.net.HttpURLConnection; 8 import java.net.PasswordAuthentication; 9 10 /** 11 Handles per request Authenticator-based authentication. Loads the class `org.jsoup.helper.RequestAuthHandler` if 12 per-request Authenticators are supported (Java 9+), or installs a system-wide Authenticator that delegates to a request 13 ThreadLocal. 14 */ 15 class AuthenticationHandler extends Authenticator { 16 static final int MaxAttempts = 5; // max authentication attempts per request. allows for multiple auths (e.g. proxy and server) in one request, but saves otherwise 20 requests if credentials are incorrect. 17 static AuthShim handler; 18 19 static { 20 try { 21 //noinspection unchecked 22 Class<AuthShim> perRequestClass = (Class<AuthShim>) Class.forName("org.jsoup.helper.RequestAuthHandler"); 23 Constructor<AuthShim> constructor = perRequestClass.getConstructor(); 24 handler = constructor.newInstance(); 25 } catch (ClassNotFoundException e) { 26 handler = new GlobalHandler(); 27 } catch (Exception e) { 28 throw new IllegalStateException(e); 29 } 30 } 31 32 @Nullable RequestAuthenticator auth; 33 int attemptCount = 0; 34 AuthenticationHandler()35 AuthenticationHandler() {} 36 AuthenticationHandler(RequestAuthenticator auth)37 AuthenticationHandler(RequestAuthenticator auth) { 38 this.auth = auth; 39 } 40 41 /** 42 Authentication callback, called by HttpURLConnection - either as system-wide default (Java 8) or per HttpURLConnection (Java 9+) 43 * @return credentials, or null if not attempting to auth. 44 */ getPasswordAuthentication()45 @Nullable @Override public final PasswordAuthentication getPasswordAuthentication() { 46 AuthenticationHandler delegate = handler.get(this); 47 if (delegate == null) return null; // this request has no auth handler 48 delegate.attemptCount++; 49 // if the password returned fails, Java will repeatedly retry the request with a new password auth hit (because 50 // it may be an interactive prompt, and the user could eventually get it right). But in Jsoup's context, the 51 // auth will either be correct or not, so just abandon 52 if (delegate.attemptCount > MaxAttempts) 53 return null; 54 if (delegate.auth == null) 55 return null; // detached - would have been the Global Authenticator (not a delegate) 56 57 RequestAuthenticator.Context ctx = new RequestAuthenticator.Context( 58 this.getRequestingURL(), this.getRequestorType(), this.getRequestingPrompt()); 59 return delegate.auth.authenticate(ctx); 60 } 61 62 interface AuthShim { enable(RequestAuthenticator auth, HttpURLConnection con)63 void enable(RequestAuthenticator auth, HttpURLConnection con); 64 remove()65 void remove(); 66 get(AuthenticationHandler helper)67 @Nullable AuthenticationHandler get(AuthenticationHandler helper); 68 } 69 70 /** 71 On Java 8 we install a system-wide Authenticator, which pulls the delegating Auth from a ThreadLocal pool. 72 */ 73 static class GlobalHandler implements AuthShim { 74 static ThreadLocal<AuthenticationHandler> authenticators = new ThreadLocal<>(); 75 static { Authenticator.setDefault(new AuthenticationHandler())76 Authenticator.setDefault(new AuthenticationHandler()); 77 } 78 enable(RequestAuthenticator auth, HttpURLConnection con)79 @Override public void enable(RequestAuthenticator auth, HttpURLConnection con) { 80 authenticators.set(new AuthenticationHandler(auth)); 81 } 82 remove()83 @Override public void remove() { 84 authenticators.remove(); 85 } 86 get(AuthenticationHandler helper)87 @Override public AuthenticationHandler get(AuthenticationHandler helper) { 88 return authenticators.get(); 89 } 90 } 91 } 92