Dispatcher Plugins
Dispatcher is deprecated and should no longer be used - you should be using Ceptor Gateway instead.
It is possible to write plugins in the dispatcher and adding their classnames to the “tunnelplugins” configuration - see Dispatcher Properties for more info about the configuration.
A plugin needs to implement the ITunnelPlugin
interface and must extend the AbstractTunnelPlugin
class
Below yoy can see the full source for the interface.
package dk.itp.tunnel;
import java.io.IOException;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import dk.itp.security.utils.UniqueId;
import dk.itp.statistics.Statistics;
import dk.itp.tunnel.TunnelServlet.NoServersAvailableException;
import dk.itp.tunnel.TunnelServlet.ServerEntry;
/**
*
* 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 infrastructure
* Copyright(c) 2001, IT Practice A/S, All rights reserved.
*
* This source code is confidential.
* </pre>
*/
@SuppressWarnings({"rawtypes"})
public interface ITunnelPlugin {
public static class ResponseHeaderInformation {
public int statusCode;
public String reasonText;
public List<String> names = new LinkedList<String>();
public List<String> values = new LinkedList<String>();
}
/**
* Request state 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;
}
The plugins can modify anything as part of the request headers or body as well as the response.
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.
Also beware of memory usage since buffering up the responses might have consequences for large responses so you should consider adding limits if you need to.
Below is an example plugin that can sign/encrypt requests and decrypt/validate responses:
package dk.itp.portalprotect.wss.dispatcher;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dk.itp.portalprotect.wss.agent.WSSAgent;
import dk.itp.security.passticket.PTException;
import dk.itp.security.utils.HtmlEncoder;
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.
*/
public class WSSPlugin extends AbstractTunnelPlugin {
private Logger cat = LoggerFactory.getLogger(getClass());
protected String[] urlPatterns;
protected String[] wsdlUrlPatterns;
protected String signer;
protected Statistics statistics;
protected class RequestInfo {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
PTException loginError;
}
private static ThreadLocal<RequestInfo> requestInfoTL = 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(fault);
}
} 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;
}
}
© Ceptor ApS. All Rights Reserved.