...
Note |
---|
Note that touching request or response body can seriously hurt performance so it is highly discouraged to do so unless you have a very good reason for it. |
Below is an example plugin that can sign/encrypt requests and decrypt/validate responses:
Code Block | ||
---|---|---|
| ||
package dk.itp.tunnel; import java.io.IOException; import java.util.Hashtable;portalprotect.wss.dispatcher; import java.utilio.LinkedListByteArrayOutputStream; import java.utilio.ListIOException; import java.util.Properties; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import dkorg.itp.security.utils.UniqueId;slf4j.Logger; import org.slf4j.LoggerFactory; import dk.itp.statistics.Statisticsportalprotect.wss.agent.WSSAgent; import dk.itp.tunnelsecurity.TunnelServletpassticket.NoServersAvailableExceptionPTException; import dk.itp.tunnelsecurity.TunnelServletutils.ServerEntryHtmlEncoder; /** * * This interface must be implemented by tunnel/dispatcher plugins.<br> * A plugin can modify requests/responses before/after they are sent to the web server.<br><br> * * Users of this interface should extend the AbstractTunnelPlugin class and extend the * required methods instead of implementing this interface directly. This will allow * for easier transitions to new versions * * @author Kim Rasmussen * @version $Revision$ * * <pre> * PortalProtect - Security import dk.itp.security.utils.Listed; import dk.itp.security.utils.StringMatcher; import dk.itp.security.utils.UniqueId; import dk.itp.statistics.Statistics; import dk.itp.tunnel.AbstractTunnelPlugin; import dk.itp.tunnel.TunnelServlet; import dk.itp.tunnel.TunnelServlet.ServerEntry; /** * Plugin to the dispatcher/tunnel which handes WS-Security, i.e. it modifies the request/response contents to add * or remove WS-Security encryption. * * @author Kim Rasmussen * @version $Revision$ * * <pre> * PortalProtect - Security infrastructure * Copyright(c) 20012010, IT Practice A/S, All rights reserved. * * This source code is confidential. * </pre> */ @SuppressWarnings({"rawtypes"}) public interface ITunnelPlugin public class WSSPlugin extends AbstractTunnelPlugin { publicprivate staticLogger classcat ResponseHeaderInformation { public int statusCode; public String reasonText; public List<String> names = new LinkedList<String>(); public List<String> values= LoggerFactory.getLogger(getClass()); protected String[] urlPatterns; protected String[] wsdlUrlPatterns; protected String signer; protected Statistics statistics; protected class RequestInfo { ByteArrayOutputStream bout = new LinkedList<String>ByteArrayOutputStream(); } PTException loginError; /**} private *static RequestThreadLocal<RequestInfo> staterequestInfoTL information= meant for sharing between plugins. * Can be used by a plugin to abort any additonal calls to other plugins, or to share state between plugins for the specific request.<br> * State can also be saved here between calls in the flow and re used by the same plugin when it is called later for the same request. */ public static class PluginRequestStateInformation { /** If set to true, PP will stop calling plugins, including the plugin which originally set this boolean, so use with care. * Note that this flag is not respected by the tunnel when processing response content which will proceed as normal. */ public boolean doNotCallOtherPlugins = false; /** If set, only the plugin of this class (the plugin should use getClass()) will be called. doNotCallOtherPlugins * has priority, but if that is false and onlyCallThisPlugin is not-null, only this particular plugin will be * called for the rest of the request. */ public Class onlyCallThisPlugin = null; /** If set to true, it is a hint to other plugins that a plugin has deemed the request to be failed, so they should not .e.g. log users in */ public boolean requestConsideredFailing = false; /** State for this particular request shared between plugins */ public Properties state = new Properties(); } /** * Overrides the server selection, allows the plugin to change the server to something else.<br> * To select the proper server, the plugin should call tunnel.selectBestServer() to let it choose which server to used, it will * make sure to select a server that is up, and reuse the same server is there is already a selected one present. Note that it * will return a cookie value which needs to be set on the response by the plugin if non-empty. * * @param request Request as received from the servlet * @param response Empty response * @param content The content read in case of a POST request (else <code>null</code>) * @param sessionId Session ID for this user * @param serverEntry Entry of the server chosen for this request * @param tunnel The tunnel servlet, can be used by plugins to call e.g. isFromSSLAccelerator() * @param alternateServers Hashtable<String, TunnelServlet.AlternateServer> All configured alternate servers * @param defaultServers Hashtable<String, ServerEntry> All configured default (non-alternate) servers */ public ServerEntry overrideServerSelection(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, TunnelServlet.ServerEntry serverEntry, TunnelServlet tunnel, Hashtable alternateServers, Hashtable defaultServers) throws IOException, NoServersAvailableException; /** * Allows the plugin to modify the request before it is sent to the web server * * @param request Request as received from the servlet * @param response Empty response * @param content The content read in case of a POST request (else <code>null</code>) * @param sessionId Session ID for this user * @param requestToWebServer The request that this method can modify, it does not contain any request contents, but only the headers. * @param serverEntry Entry of the server chosen for this request * @param tunnel The tunnel servlet, can be used by plugins to call e.g. isFromSSLAccelerator() * @return true, if the request should not be passed on to the webserver, if the plugin has handled it. False if the request should be sent to the server. */ public boolean modifyRequest(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, StringBuffer requestToWebServer, TunnelServlet.ServerEntry serverEntry, TunnelServlet tunnel) throws IOException; /** * Allows the plugin to modify the POST data in the request before it is sent to the web server. * This method is called before modifyRequest, and only if the request is a POST/PUT request that has any data. * It is called before modifyRequest to make sure the correct content-length is available which needs to be calculated before calling modifyRequest. * * @param request Request as received from the servlet * @param response Empty response * @param content The content read in case of a POST request (else <code>null</code>) * @param sessionId Session ID for this user * @param serverEntry Entry of the server chosen for this request * @param tunnel The tunnel servlet, can be used by plugins to call e.g. isFromSSLAccelerator() * @return The new request (POST data) to send to the server. */ public byte[] modifyRequestContent(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, TunnelServlet.ServerEntry serverEntry, TunnelServlet tunnel) throws IOException; /** * Allows the plugin to modify the response headers - this method is called just after the initial headers are received, before they are parsed and before * the response content is read. * * @param request Request * @param response Response to modify * @param content The content read in case of a POST request (else <code>null</code>) * @param sessionId Session ID for this user * @param serverEntry Entry of the server chosen for this request * @param headerInfo List of non-yet-parsed response status code and HTTP headers - the plugin can choose to modify them. * @param tunnel The tunnel servlet, can be used by plugins to call e.g. isFromSSLAccelerator() */ public void modifyResponseHeaders(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, TunnelServlet.ServerEntry serverEntry, ResponseHeaderInformation headerInfo, TunnelServlet tunnel) throws IOException; /** * Allows the plugin to modify the response - this request is called after the initial headers are received, but before the actual response content * is read from the webserver * * @param request Request * @param response Response to modify * @param content The content read in case of a POST request (else <code>null</code>) * @param sessionId Session ID for this user * @param serverEntry Entry of the server chosen for this request * @param tunnel The tunnel servlet, can be used by plugins to call e.g. isFromSSLAccelerator() * @return true, if the response should be returned immediately, false if the response contents should be read from the webserver and sent back to the browser. */ public boolean modifyResponse(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, TunnelServlet.ServerEntry serverEntry, TunnelServlet tunnel) throws IOException; /** * Gives the plugin a chance to access the Dispatcher/Tunnel configuration. Called * each time the configuration for the Dispatcher/Tunnel changes * * @param props Configuration */ public void setConfiguration(Properties props); /** * This method is called when the Dispatcher/Tunnel is reconfigured and possible * changed the instance of the statistics object. This object can be used to meassure * statistics if needed * * @param stats Statistics object for meassuring */ public void setStatistics( Statistics stats ); /** * This method can be used by the plugin to provide a status text that will be * accessible along with the Dispatcher/Tunnel status through the PortalProtect * administration. * * Optionally, a plugin can implement <code>public String getStatusText(String action);</code> which allows it to * perform an action such as displaying specific status, clearing cache data, refreshing data or whatever else the * plugin may want to do. That method is not in the interface since it is not required. * * @return Status text if any (or null) */ public String getStatusText(); /** * Asks the plugin if it is interested in the contents of the response. * If so, the dispatcher will not stream the response data to the browser, but instead to the plugin, and it is then * the plugin's own problem to stream the (modified) response back to the browser. * <p> * Note that when streaming is enabled, the dispatcher will no longer be able to compress the response, and it * will remove any content length header otherwise put on the response by the application server. * <p><b> * Note that only a single plugin is able to modify the response contents at a time - the first plugin in the * chain that returns true wins, and the rest of the plugins are not asked.</b> * * @param request Request * @param response Response * @param content Content of the POST request, or null * @param sessionId Session ID for this user * @param serverEntry Entry of the server chosen for this request * @param tunnel The tunnel servlet, can be used by plugins to call e.g. isFromSSLAccelerator() * @return true if plugin wants the response contents * @throws IOException */ public boolean isInterestedInResponseContents(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, TunnelServlet.ServerEntry serverEntry, TunnelServlet tunnel) throws IOException; /** * This tells the plugin that a block of bytes has been received. This method is called as long as there is data read from the response.<br> * Note that if your plugin buffers data up, take care about setting max safety sizes or you risk multi-megabyte responses eating all the memory. * * @param request Request * @param response Response * @param content Content of the POST request, or null * @param sessionId Session ID for this user * @param serverEntry Entry of the server chosen for this request * @param tunnel The tunnel servlet, can be used by plugins to call e.g. isFromSSLAccelerator() * @param responseBuffer Buffer containing contents of the response. Note that the byte array might not be filled - use the responseBufferSize. * @param responseBufferSize Number of bytes in the buffer * @throws IOException */ public void responseContentRead(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, TunnelServlet.ServerEntry serverEntry, TunnelServlet tunnel, byte[] responseBuffer, int responseBufferSize) throws IOException; /** * This tells the plugin that the reading of the response has finished, so this is its last chance to write anything else to the response. * If the plugin has been collecting the response data up for processing, it can do it now, and write the finished result to the http servlet response. * * @param request Request * @param response Response * @param content Content of the POST request, or null * @param sessionId Session ID for this user * @param serverEntry Entry of the server chosen for this request * @param tunnel The tunnel servlet, can be used by plugins to call e.g. isFromSSLAccelerator() * @throws IOException */ public void responseContentFinished(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, TunnelServlet.ServerEntry serverEntry, TunnelServlet tunnel) throws IOException; new ThreadLocal<RequestInfo>(); private boolean isRequestForUs(HttpServletRequest request) { if (request.getHeader("SOAPAction") == null) { return false; } // Copy it so if setConfiguration is called we do not risk the array size being changed. String[] urlPatterns = this.urlPatterns; String uri = request.getRequestURI(); for(int i = 0; i < urlPatterns.length; i++) { if (StringMatcher.match(urlPatterns[i], uri)) { return true; } } return false; } private boolean isWSDL(HttpServletRequest request) { // Copy it so if setConfiguration is called we do not risk the array size being changed. String[] urlPatterns = this.wsdlUrlPatterns; String uri = request.getRequestURI(); String qstr = request.getQueryString(); if (qstr != null && qstr.length() > 0) uri += "?"+qstr; for(int i = 0; i < urlPatterns.length; i++) { if (StringMatcher.match(urlPatterns[i], uri)) { return true; } } return false; } public byte[] modifyRequestContent(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, ServerEntry serverEntry, TunnelServlet tunnel) throws IOException { requestInfoTL.set(null); if (!isRequestForUs(request)) return content; // This request seems to be useful for us, so lets process it. String xml = new String(content, "UTF8"); try { if (cat.isDebugEnabled()) { cat.debug("XML Input: " + xml); } xml = WSSAgent.logonWithSOAP(tunnel.getAgent(), sessionId.toString(), xml); if (xml != null) { if (cat.isDebugEnabled()) { cat.debug("XML input modified to: " + xml); } return xml.getBytes("UTF8"); } } catch (PTException e) { cat.warn("Unable to login with SOAP request contents", e); } return content; } public boolean modifyRequest(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, StringBuffer requestToWebServer, ServerEntry serverEntry, TunnelServlet tunnel) throws IOException { RequestInfo info = requestInfoTL.get(); if (info != null && info.loginError != null) { String fault = generateSoapFault(info.loginError); response.getOutputStream().print(fault); return true; } return false; } public boolean isInterestedInResponseContents(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, ServerEntry serverEntry, TunnelServlet tunnel) throws IOException { if (!isWSDL(request) && !isRequestForUs(request)) return false; RequestInfo info = requestInfoTL.get(); if (info == null) requestInfoTL.set(new RequestInfo()); return true; } public void responseContentRead(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, ServerEntry serverEntry, TunnelServlet tunnel, byte[] responseBuffer, int responseBufferSize) throws IOException { RequestInfo info = requestInfoTL.get(); info.bout.write(responseBuffer, 0, responseBufferSize); } public void responseContentFinished(HttpServletRequest request, HttpServletResponse response, byte[] content, UniqueId sessionId, ServerEntry serverEntry, TunnelServlet tunnel) throws IOException { RequestInfo info = requestInfoTL.get(); String xml = new String(info.bout.toByteArray(), "UTF8"); if (cat.isDebugEnabled()) { cat.debug("Response: " + xml); } String sessionID = sessionId.toString(); if (isWSDL(request)) { // TODO: Cache the result for another time try { xml = WSSAgent.attachPoliciesToWSDL(tunnel.getAgent(), sessionID, xml, request.getRequestURL().toString()); response.getOutputStream().write(xml.getBytes("UTF8")); } catch(PTException e) { cat.warn("Problem attaching policy to WSDL", e); // Write proper soapfault here @SuppressWarnings("unused") String fault = generateSoapFault(e); response.getOutputStream().print("<html><body>Problem attaching policy to WSDL: " + HtmlEncoder.encode(e.toString()) + "</body></html>"); } } else { try { String encryptionKey = tunnel.getAgent().getStateVariable(sessionID, "encryptedWithKey"); if (encryptionKey != null) xml = WSSAgent.signAndEncryptSOAP(tunnel.getAgent(), sessionId.toString(), xml, signer, encryptionKey); else xml = WSSAgent.signSOAP(tunnel.getAgent(), sessionID, xml, signer); if (cat.isDebugEnabled()) { cat.debug("XML Output modified to: " + xml); } response.getOutputStream().write(xml.getBytes("UTF8")); } catch(PTException e) { cat.warn("Problem signing response XML", e); // Write proper soapfault here String fault = generateSoapFault(e); response.getOutputStream().print(fault); } } requestInfoTL.set(null); } public void setStatistics(Statistics stats) { super.setStatistics(stats); this.statistics = stats; } public void setConfiguration(Properties props) { super.setConfiguration(props); urlPatterns = new Listed<String>(props.getProperty("wss.urls", "")).toArray(); wsdlUrlPatterns = new Listed<String>(props.getProperty("wss.wsdlurls", "")).toArray(); signer = props.getProperty("wss.signer", "unknown"); } protected String generateSoapFault(PTException e) { String fault = "<env:Envelope xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\">\n"+ "<env:Header/><env:Body>\n<env:Fault>\n<env:Code><env:Value>env:Sender</env:Value></env:Code>\n"+ "<env:Reason><env:Text>\n"+ e.toString()+ "</env:Text></env:Reason>\n</env:Fault></env:Body></env:Envelope>"; return fault; } } |