1 package org.jsoup.integration.servlets; 2 3 import javax.servlet.Filter; 4 import javax.servlet.FilterChain; 5 import javax.servlet.FilterConfig; 6 import javax.servlet.ServletException; 7 import javax.servlet.ServletRequest; 8 import javax.servlet.ServletResponse; 9 import javax.servlet.http.HttpServletRequest; 10 import javax.servlet.http.HttpServletResponse; 11 import java.io.IOException; 12 import java.nio.charset.StandardCharsets; 13 import java.util.Base64; 14 15 /** 16 A filter to test basic authenticated requests. If the request header "X-Wants-Authentication" is set, or if 17 alwaysWantsAuth is enabled, the filter is invoked, and requests must send the correct user authentication details. 18 */ 19 public class AuthFilter implements Filter { 20 public static final String WantsServerAuthentication = "X-Wants-ServerAuthentication"; 21 public static final String ServerUser = "admin"; 22 public static final String ServerRealm = "jsoup test server authentication realm"; 23 private static volatile String ServerPassword = newServerPassword(); 24 25 public static final String WantsProxyAuthentication = "X-Wants-ProxyAuthentication"; 26 public static final String ProxyUser = "foxyproxy"; 27 public static final String ProxyRealm = "jsoup test proxy authentication realm"; 28 private static volatile String ProxyPassword = newProxyPassword(); 29 30 private final boolean alwaysWantsAuth; // we run a particular port that always wants auth - so the CONNECT tunnels can be authed. (The Java proxy tunnel CONNECT request strips the wants-auth headers) 31 private final boolean forProxy; 32 private final String wantsHeader; 33 private final String authorizationHeader; 34 35 /** 36 Creates an Authentication Filter with hardcoded credential expectations. 37 * @param alwaysWantsAuth true if this filter should always check for authentication, regardless of the Wants Auth header 38 * @param forProxy true if this wraps a Proxy and should use Proxy-Authenticate headers, credentials etc. False 39 * if wrapping the web server. 40 */ AuthFilter(boolean alwaysWantsAuth, boolean forProxy)41 public AuthFilter(boolean alwaysWantsAuth, boolean forProxy) { 42 this.alwaysWantsAuth = alwaysWantsAuth; 43 this.forProxy = forProxy; 44 45 wantsHeader = forProxy ? WantsProxyAuthentication : WantsServerAuthentication; 46 authorizationHeader = forProxy ? "Proxy-Authorization" : "Authorization"; 47 } 48 newPassword()49 private static String newPassword() { 50 return "pass-" + Math.random(); 51 } 52 53 // passwords get rotated in tests so that Java's auth cache is invalidated and a new auth callback occurs. 54 // requires tests hitting these are called serially. newServerPassword()55 public static String newServerPassword() { 56 return ServerPassword = newPassword() + "-server"; 57 } 58 newProxyPassword()59 public static String newProxyPassword() { 60 return ProxyPassword = newPassword() + "-proxy"; 61 } 62 init(FilterConfig filterConfig)63 @Override public void init(FilterConfig filterConfig) throws ServletException {} 64 65 @Override doFilter(ServletRequest request, ServletResponse response, FilterChain chain)66 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 67 HttpServletRequest req = (HttpServletRequest) request; 68 HttpServletResponse res = (HttpServletResponse) response; 69 70 boolean accessGranted = checkAuth(req); 71 if (accessGranted) { 72 chain.doFilter(request, response); 73 return; 74 } 75 76 // Wants but failed auth - send appropriate header: 77 if (forProxy) { 78 res.setHeader("Proxy-Authenticate", "Basic realm=\"" + ProxyRealm + "\""); 79 // ^^ Duped in ProxyServlet for CONNECT 80 res.sendError(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED); 81 } else { 82 res.setHeader("WWW-Authenticate", "Basic realm=\"" + ServerRealm + "\""); 83 res.sendError(HttpServletResponse.SC_UNAUTHORIZED); 84 } 85 } 86 destroy()87 @Override public void destroy() {} 88 checkAuth(HttpServletRequest req)89 public boolean checkAuth(HttpServletRequest req) { 90 if (alwaysWantsAuth || req.getHeader(wantsHeader) != null) { 91 String authHeader = req.getHeader(authorizationHeader); 92 if (authHeader != null) { 93 int space = authHeader.indexOf(' '); 94 if (space > 0) { 95 String value = authHeader.substring(space + 1); 96 String expected = forProxy ? 97 (ProxyUser + ":" + ProxyPassword) : 98 (ServerUser + ":" + ServerPassword); 99 String base64 = Base64.getEncoder().encodeToString(expected.getBytes(StandardCharsets.UTF_8)); 100 return base64.equals(value); // if passed auth 101 } 102 } 103 return false; // unexpected header value 104 } 105 return true; // auth not required 106 } 107 } 108