package at.gv.egovernment.moa.id.proxy.servlet; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.net.HttpURLConnection; import java.net.URLEncoder; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.net.ssl.SSLSocketFactory; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import at.gv.egovernment.moa.id.AuthenticationException; import at.gv.egovernment.moa.id.BuildException; import at.gv.egovernment.moa.id.MOAIDException; import at.gv.egovernment.moa.id.ParseException; import at.gv.egovernment.moa.id.ServiceException; import at.gv.egovernment.moa.id.config.ConfigurationException; import at.gv.egovernment.moa.id.config.ConnectionParameter; import at.gv.egovernment.moa.id.config.proxy.ProxyConfigurationProvider; import at.gv.egovernment.moa.id.config.proxy.OAConfiguration; import at.gv.egovernment.moa.id.config.proxy.OAProxyParameter; import at.gv.egovernment.moa.id.data.AuthenticationData; import at.gv.egovernment.moa.id.data.CookieManager; import at.gv.egovernment.moa.id.proxy.ConnectionBuilder; import at.gv.egovernment.moa.id.proxy.ConnectionBuilderFactory; import at.gv.egovernment.moa.id.proxy.LoginParameterResolver; import at.gv.egovernment.moa.id.proxy.LoginParameterResolverFactory; import at.gv.egovernment.moa.id.proxy.MOAIDProxyInitializer; import at.gv.egovernment.moa.id.proxy.invoke.GetAuthenticationDataInvoker; import at.gv.egovernment.moa.id.util.MOAIDMessageProvider; import at.gv.egovernment.moa.id.util.SSLUtils; import at.gv.egovernment.moa.logging.Logger; import at.gv.egovernment.moa.util.Base64Utils; /** * Servlet requested for logging in at an online application, * and then for proxying requests to the online application. * @author Paul Ivancsics * @version $Id$ */ public class ProxyServlet extends HttpServlet { /** Name of the Parameter for the Target */ private static final String PARAM_TARGET = "Target"; /** Name of the Parameter for the SAMLArtifact */ private static final String PARAM_SAMLARTIFACT = "SAMLArtifact"; /** Name of the Attribute for the PublicURLPrefix */ private static final String ATT_PUBLIC_URLPREFIX = "PublicURLPrefix"; /** Name of the Attribute for the RealURLPrefix */ private static final String ATT_REAL_URLPREFIX = "RealURLPrefix"; /** Name of the Attribute for the SSLSocketFactory */ private static final String ATT_SSL_SOCKET_FACTORY = "SSLSocketFactory"; /** Name of the Attribute for the LoginHeaders */ private static final String ATT_LOGIN_HEADERS = "LoginHeaders"; /** Name of the Attribute for the LoginParameters */ private static final String ATT_LOGIN_PARAMETERS = "LoginParameters"; /** * @see javax.servlet.http.HttpServlet#service(HttpServletRequest, HttpServletResponse) */ protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Logger.debug("getRequestURL:" + req.getRequestURL().toString()); try { if (req.getParameter(PARAM_SAMLARTIFACT) != null && req.getParameter(PARAM_TARGET) != null) login(req, resp); else tunnelRequest(req, resp); } catch (MOAIDException ex) { handleError(resp, ex.toString(), ex); } catch (Throwable ex) { handleError(resp, ex.toString(), ex); } } /** * Login to online application at first call of servlet for a user session.
* * @param req * @param resp * @throws ConfigurationException when wrong configuration is encountered * @throws ProxyException when wrong configuration is encountered * @throws BuildException while building the request for MOA-ID Auth * @throws ServiceException while invoking MOA-ID Auth * @throws ParseException while parsing the response from MOA-ID Auth */ private void login(HttpServletRequest req, HttpServletResponse resp) throws ConfigurationException, ProxyException, BuildException, ServiceException, ParseException, AuthenticationException { String samlArtifact = req.getParameter(PARAM_SAMLARTIFACT); Logger.debug("moa-id-proxy login " + PARAM_SAMLARTIFACT + ": " + samlArtifact); // String target = req.getParameter(PARAM_TARGET); parameter given but not processed // get authentication data from the MOA-ID Auth component AuthenticationData authData = new GetAuthenticationDataInvoker().getAuthenticationData(samlArtifact); String urlRequested = req.getRequestURL().toString(); // read configuration data ProxyConfigurationProvider proxyConf = ProxyConfigurationProvider.getInstance(); OAProxyParameter oaParam = proxyConf.getOnlineApplicationParameter(urlRequested); if (oaParam == null) { throw new ProxyException("proxy.02", new Object[] { urlRequested }); } String publicURLPrefix = oaParam.getPublicURLPrefix(); Logger.debug("OA: " + publicURLPrefix); OAConfiguration oaConf = oaParam.getOaConfiguration(); ConnectionParameter oaConnParam = oaParam.getConnectionParameter(); String realURLPrefix = oaConnParam.getUrl(); // resolve login parameters to be forwarded to online application LoginParameterResolver lpr = LoginParameterResolverFactory.getLoginParameterResolver(publicURLPrefix); String clientIPAddress = req.getRemoteAddr(); Map loginHeaders = null; Map loginParameters = null; if (oaConf.getAuthType().equals(OAConfiguration.PARAM_AUTH)) loginParameters = lpr.getAuthenticationParameters(oaConf, authData, clientIPAddress); else loginHeaders = lpr.getAuthenticationHeaders(oaConf, authData, clientIPAddress); // setup SSLSocketFactory for communication with the online application SSLSocketFactory ssf = null; if (oaConnParam.isHTTPSURL()) { try { ssf = SSLUtils.getSSLSocketFactory(proxyConf, oaConnParam); } catch (Throwable ex) { throw new ProxyException("proxy.05", new Object[] { oaConnParam.getUrl(), ex.toString()}, ex); } } try { // for stateless online application, store data in HttpSession String loginType = oaConf.getLoginType(); Logger.debug("Login type: " + loginType); if (loginType.equals(OAConfiguration.LOGINTYPE_STATELESS)) { HttpSession session = req.getSession(); int sessionTimeOut = oaParam.getSessionTimeOut(); if (sessionTimeOut == 0) sessionTimeOut = 60 * 60; // default 1 h session.setMaxInactiveInterval(sessionTimeOut); session.setAttribute(ATT_PUBLIC_URLPREFIX, publicURLPrefix); session.setAttribute(ATT_REAL_URLPREFIX, realURLPrefix); session.setAttribute(ATT_SSL_SOCKET_FACTORY, ssf); session.setAttribute(ATT_LOGIN_HEADERS, loginHeaders); session.setAttribute(ATT_LOGIN_PARAMETERS, loginParameters); Logger.debug("moa-id-proxy: HTTPSession angelegt"); } // tunnel request to the online application int respcode = tunnelRequest(req, resp, loginHeaders, loginParameters, publicURLPrefix, realURLPrefix, ssf); if (respcode == 401) { Logger.debug("Got 401, trying again"); respcode = tunnelRequest(req, resp, loginHeaders, loginParameters, publicURLPrefix, realURLPrefix, ssf); if (respcode == 401) throw new ProxyException("proxy.12", new Object[] { realURLPrefix}); } } catch (ProxyException ex) { throw new ProxyException("proxy.12", new Object[] { realURLPrefix}); } catch (Throwable ex) { throw new ProxyException("proxy.04", new Object[] { urlRequested, ex.toString()}, ex); } } /** * Tunnels a request to the stateless online application using data stored in the HTTP session. * @param req HTTP request * @param resp HTTP response * @throws IOException if an I/O error occurs */ private void tunnelRequest(HttpServletRequest req, HttpServletResponse resp) throws ProxyException, IOException { Logger.debug("Tunnel request (stateless)"); HttpSession session = req.getSession(false); if (session == null) throw new ProxyException("proxy.07", null); String publicURLPrefix = (String) session.getAttribute(ATT_PUBLIC_URLPREFIX); String realURLPrefix = (String) session.getAttribute(ATT_REAL_URLPREFIX); SSLSocketFactory ssf = (SSLSocketFactory) session.getAttribute(ATT_SSL_SOCKET_FACTORY); Map loginHeaders = (Map) session.getAttribute(ATT_LOGIN_HEADERS); Map loginParameters = (Map) session.getAttribute(ATT_LOGIN_PARAMETERS); if (publicURLPrefix == null || realURLPrefix == null) throw new ProxyException("proxy.08", new Object[] { req.getRequestURL().toString()}); int respcode = tunnelRequest(req, resp, loginHeaders, loginParameters, publicURLPrefix, realURLPrefix, ssf); if (respcode == 401) { Logger.debug("Got 401, trying again"); respcode = tunnelRequest(req, resp, loginHeaders, loginParameters, publicURLPrefix, realURLPrefix, ssf); if (respcode == 401) throw new ProxyException("proxy.12", new Object[] { realURLPrefix}); } } /** * Tunnels a request to the online application using given URL mapping and SSLSocketFactory. * This method returns the ResponseCode of the request to the online application. * @param req HTTP request * @param resp HTTP response * @param loginHeaders header field/values to be inserted for purposes of authentication; * may be null * @param loginParameters parameter name/values to be inserted for purposes of authentication; * may be null * @param publicURLPrefix prefix of request URL to be substituted for the realURLPrefix * @param realURLPrefix prefix of online application URL to substitute the publicURLPrefix * @param ssf SSLSocketFactory to use * @throws IOException if an I/O error occurs */ private int tunnelRequest(HttpServletRequest req, HttpServletResponse resp, Map loginHeaders, Map loginParameters, String publicURLPrefix, String realURLPrefix, SSLSocketFactory ssf) throws IOException { // collect headers from request Map headers = new HashMap(); for (Enumeration enum = req.getHeaderNames(); enum.hasMoreElements();) { String headerKey = (String) enum.nextElement(); //We ignore any Basic-Auth-Headers from the client if (headerKey.equalsIgnoreCase("Authorization")) { Logger.debug("Ignoring authorization-header from browser: " +req.getHeader(headerKey) ); } else headers.put(headerKey, req.getHeader(headerKey)); } // collect login headers, possibly overwriting headers from request if (loginHeaders != null) { for (Iterator iter = loginHeaders.keySet().iterator(); iter.hasNext();) { String headerKey = (String) iter.next(); headers.put(headerKey, loginHeaders.get(headerKey)); } } // collect parameters from request Map parameters = new HashMap(); for (Enumeration enum = req.getParameterNames(); enum.hasMoreElements();) { String paramName = (String) enum.nextElement(); parameters.put(paramName, req.getParameter(paramName)); } // collect login parameters, possibly overwriting parameters from request if (loginParameters != null) { for (Iterator iter = loginParameters.keySet().iterator(); iter.hasNext();) { String paramName = (String) iter.next(); parameters.put(paramName, loginParameters.get(paramName)); } } headers.remove("content-length"); parameters.remove(PARAM_SAMLARTIFACT); parameters.remove(PARAM_TARGET); ConnectionBuilder cb = ConnectionBuilderFactory.getConnectionBuilder(publicURLPrefix); HttpURLConnection conn = cb.buildConnection(req, publicURLPrefix, realURLPrefix, ssf, parameters); //Set Cookies... String cookieString = CookieManager.getInstance().getCookie(req.getSession().getId()); if (cookieString!=null) { //If we get Cookies from Client, we put them throgh if they dont exist/conflict with the stored Cookies for (Iterator iter = headers.keySet().iterator(); iter.hasNext();) { String headerKey = (String) iter.next(); String headerValue = (String) headers.get(headerKey); if (headerKey.equalsIgnoreCase("Cookie")) CookieManager.getInstance().saveOldCookies(req.getSession().getId(), headerValue); } cookieString = CookieManager.getInstance().getCookie(req.getSession().getId()); headers.put("cookie", cookieString); } // set headers as request properties of URLConnection for (Iterator iter = headers.keySet().iterator(); iter.hasNext();) { String headerKey = (String) iter.next(); String headerValue = (String) headers.get(headerKey); conn.setRequestProperty(headerKey, headerValue); Logger.debug("Req header " + headerKey + ": " + headers.get(headerKey)); if (Logger.isDebugEnabled() && isBasicAuthenticationHeader(headerKey, headerValue)) { String credentials = headerValue.substring(6); String userIDPassword = new String(Base64Utils.decode(credentials, false)); Logger.debug(":UserID:Password: :" + userIDPassword + ":"); } } // Write out parameters into output stream of URLConnection. // On GET request, do not send parameters in any case, // otherwise HttpURLConnection would send a POST. if (!"get".equalsIgnoreCase(req.getMethod()) && !parameters.isEmpty()) { boolean firstParam = true; StringWriter sb = new StringWriter(); for (Iterator iter = parameters.keySet().iterator(); iter.hasNext();) { String paramname = (String) iter.next(); String value = URLEncoder.encode((String) parameters.get(paramname)); if (firstParam) firstParam = false; else sb.write("&"); sb.write(paramname); sb.write("="); sb.write(value); Logger.debug("Req param " + paramname + ": " + value); } PrintWriter reqOut = new PrintWriter(conn.getOutputStream()); reqOut.write(sb.toString()); reqOut.flush(); reqOut.close(); } // connect conn.connect(); // Read response status and content type. // If the connection returns a 401 disconnect and return // otherwise the attempt to read data from that connection // will result in an error if (conn.getResponseCode()==HttpURLConnection.HTTP_UNAUTHORIZED) { Logger.debug("Found 401... searching cookies"); String headerKey; int i = 1; CookieManager cm = CookieManager.getInstance(); while ((headerKey = conn.getHeaderFieldKey(i)) != null) { String headerValue = conn.getHeaderField(i); if (headerKey.equalsIgnoreCase("set-cookie")) { cm.saveCookie(req.getSession().getId(), headerValue); cm.add401(req.getSession().getId(),headerValue); Logger.debug("Cookie " + headerValue); Logger.debug("CookieSession " + req.getSession().getId()); } i++; } conn.disconnect(); return conn.getResponseCode(); } resp.setStatus(conn.getResponseCode()); resp.setContentType(conn.getContentType()); // Read response headers // Omit response header "content-length" if response header "Transfer-encoding: chunked" is set. // Otherwise, the connection will not be kept alive, resulting in subsequent missing requests. // See JavaDoc of javax.servlet.http.HttpServlet: // When using HTTP 1.1 chunked encoding (which means that the response has a Transfer-Encoding header), do not set the Content-Length header. Map respHeaders = new HashMap(); boolean chunked = false; String contentLengthKey = null; String transferEncodingKey = null; int i = 1; String headerKey; while ((headerKey = conn.getHeaderFieldKey(i)) != null) { String headerValue = conn.getHeaderField(i); respHeaders.put(headerKey, headerValue); if (isTransferEncodingChunkedHeader(headerKey, headerValue)) { chunked = true; transferEncodingKey = headerKey; } CookieManager cm = CookieManager.getInstance(); if (headerKey.equalsIgnoreCase("set-cookie")) { cm.saveCookie(req.getSession().getId(), headerValue); Logger.debug("Cookie " + headerValue); Logger.debug("CookieSession " + req.getSession().getId()); } if ("content-length".equalsIgnoreCase(headerKey)) contentLengthKey = headerKey; Logger.debug("Resp header " + headerKey + ": " + headerValue); i++; } if (chunked && contentLengthKey != null) { respHeaders.remove(transferEncodingKey); Logger.debug("Resp header " + transferEncodingKey + " REMOVED"); } //Get a Hash-Map of all 401-set-cookies HashMap cookies401 = CookieManager.getInstance().get401(req.getSession().getId()); for (Iterator iter = respHeaders.keySet().iterator(); iter.hasNext();) { headerKey = (String) iter.next(); if (headerKey.equalsIgnoreCase("Set-Cookie")) { String headerValue = (String) respHeaders.get(headerKey); Logger.debug("Found 'Set-Cookie' in ResponseHeaders: " + headerValue); if(!cookies401.containsKey(headerValue.substring(0, headerValue.indexOf("=")))) { // If we dont already have a Set-Cookie-Value for THAT Cookie we create one... CookieManager.getInstance().add401(req.getSession().getId(), headerValue); } } } //write out all Responseheaders != "set-cookie" for (Iterator iter = respHeaders.keySet().iterator(); iter.hasNext();) { headerKey = (String) iter.next(); if (!headerKey.equalsIgnoreCase("Set-Cookie")) resp.addHeader(headerKey, (String) respHeaders.get(headerKey)); } //write out all Responseheaders = "set-cookie" cookies401 = CookieManager.getInstance().get401(req.getSession().getId()); Iterator cookie_i = cookies401.values().iterator(); while (cookie_i.hasNext()) { String element = (String) cookie_i.next(); resp.addHeader("Set-Cookie", element); } //Delete all "Set-Cookie" - Values CookieManager.getInstance().clear401(req.getSession().getId()); // read response stream Logger.debug("Resp from " + conn.getURL().toString() + ": status " + conn.getResponseCode()); // Load content unless the server lets us know that the content is NOT MODIFIED... if (conn.getResponseCode()!=HttpURLConnection.HTTP_NOT_MODIFIED) { BufferedInputStream respIn = new BufferedInputStream(conn.getInputStream()); Logger.debug("Got Inputstream"); BufferedOutputStream respOut = new BufferedOutputStream(resp.getOutputStream()); Logger.debug("Got Outputstream"); int ch; while ((ch = respIn.read()) >= 0) respOut.write(ch); respOut.close(); respIn.close(); } else Logger.debug("Found 304 NOT MODIFIED..."); conn.disconnect(); Logger.debug("Request done"); return conn.getResponseCode(); } /** * Determines whether a HTTP header is a basic authentication header of the kind "Authorization: Basic ..." * * @param headerKey header name * @param headerValue header value * @return true for a basic authentication header */ private boolean isBasicAuthenticationHeader(String headerKey, String headerValue) { if (!"authorization".equalsIgnoreCase(headerKey)) return false; if (headerValue.length() < "basic".length()) return false; String authenticationSchema = headerValue.substring(0, "basic".length()); return "basic".equalsIgnoreCase(authenticationSchema); } /** * Determines whether a HTTP header is "Transfer-encoding" header with value containing "chunked" * * @param headerKey header name * @param headerValue header value * @return true for a "Transfer-encoding: chunked" header */ private boolean isTransferEncodingChunkedHeader(String headerKey, String headerValue) { if (!"transfer-encoding".equalsIgnoreCase(headerKey)) return false; return headerValue.indexOf("chunked") >= 0 || headerValue.indexOf("Chunked") >= 0 || headerValue.indexOf("CHUNKED") >= 0; } /** * Calls the web application initializer. * * @see javax.servlet.Servlet#init(ServletConfig) */ public void init(ServletConfig servletConfig) throws ServletException { try { MOAIDProxyInitializer.initialize(); Logger.info(MOAIDMessageProvider.getInstance().getMessage("proxy.00", null)); } catch (Exception ex) { Logger.fatal(MOAIDMessageProvider.getInstance().getMessage("proxy.06", null), ex); throw new ServletException(ex); } } /** * Handles an error in proxying the request. * * @param resp the HttpServletResponse * @param errorMessage error message to be used * @param ex the exception to be logged */ private void handleError(HttpServletResponse resp, String errorMessage, Throwable ex) { Logger.error(errorMessage, ex); String htmlCode = "" + "" + MOAIDMessageProvider.getInstance().getMessage("proxy.10", null) + "" + "

" + MOAIDMessageProvider.getInstance().getMessage("proxy.10", null) + "

" + "

" + MOAIDMessageProvider.getInstance().getMessage("proxy.11", null) + "

" + "

" + errorMessage + "

" + ""; resp.setContentType("text/html"); try { OutputStream respOut = resp.getOutputStream(); respOut.write(htmlCode.getBytes()); respOut.flush(); } catch (IOException ioex) { Logger.error("", ioex); } } }