package at.gv.egovernment.moa.id.proxy.servlet;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
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.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
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.LoginParameterResolverException;
import at.gv.egovernment.moa.id.proxy.LoginParameterResolverFactory;
import at.gv.egovernment.moa.id.proxy.MOAIDProxyInitializer;
import at.gv.egovernment.moa.id.proxy.NotAllowedException;
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";
/** Name of the Attribute for the SAMLARTIFACT */
private static final String ATT_SAML_ARTIFACT = "SamlArtifact";
/**
* @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) {
// check if SAML Artifact was already used in this session (in case of page reload)
HttpSession session = req.getSession();
if(null != session && req.getParameter(PARAM_SAMLARTIFACT).equals(session.getAttribute(ATT_SAML_ARTIFACT))) {
tunnelRequest(req, resp);
} else
// it is the first time that the SAML Artifact was used
login(req, resp);
}
else
tunnelRequest(req, resp);
}
catch (MOAIDException ex) {
handleError(ex.getMessage(), ex, req, resp);
}
catch (Throwable ex) {
handleError(ex.getMessage(), ex, req, resp);
}
}
/**
* Login to online application at first call of servlet for a user session.
*
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 {
super.init(servletConfig);
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. "/errorpage-proxy.jsp"
)