From dd45e938564249a5e6897bd92dd29808d8990868 Mon Sep 17 00:00:00 2001 From: rudolf Date: Fri, 24 Oct 2003 08:34:56 +0000 Subject: MOA-ID version 1.1 (initial) git-svn-id: https://joinup.ec.europa.eu/svn/moa-idspss/trunk@19 d688527b-c9ab-4aba-bd8d-4036d912da1d --- .../moa/id/proxy/servlet/ProxyServlet.java | 531 +++++++++++++++++++++ 1 file changed, 531 insertions(+) create mode 100644 id.server/src/at/gv/egovernment/moa/id/proxy/servlet/ProxyServlet.java (limited to 'id.server/src/at/gv/egovernment/moa/id/proxy/servlet/ProxyServlet.java') diff --git a/id.server/src/at/gv/egovernment/moa/id/proxy/servlet/ProxyServlet.java b/id.server/src/at/gv/egovernment/moa/id/proxy/servlet/ProxyServlet.java new file mode 100644 index 000000000..c52de2ba8 --- /dev/null +++ b/id.server/src/at/gv/egovernment/moa/id/proxy/servlet/ProxyServlet.java @@ -0,0 +1,531 @@ +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); + } +} + +} -- cgit v1.2.3