Plugins

The Ceptor Gateway supports a number of ways of extending it, either using custom Java code or scripts (e.g. javascript code).

Context (or StateHolder)

The various sorts of plugins and script in the Gateway, get fed with a StateHolder object which contains all information about the current request - plugins manipulate the content of this object, e.g. by using the Agent within to authenticate, or by changing the request attributes.

AuthenticatorScript

When scripts are called, the variable "context" points to this object.

The StateHolder class has the following contents:

package io.ceptor.gateway.state;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import org.jboss.logging.MDC;

import dk.itp.security.passticket.IAgent;
import dk.itp.security.utils.UniqueId;
import dk.portalprotect.geolocator.LocatorInfo;
import io.ceptor.config.gateway.Config;
import io.ceptor.config.gateway.Config.CookieSnapper;
import io.ceptor.config.gateway.Config.Location;
import io.ceptor.gateway.Gateway;
import io.undertow.client.ClientExchange;
import io.undertow.client.ClientRequest;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.FormData;
import io.undertow.util.Headers;

public class StateHolder {
	public ConcurrentHashMap<String, ?> state = new ConcurrentHashMap<>();
	
	public UniqueId sessionId;
	/** Will contain the request/session ID once processing is started - can be used by scripts */
	public String id;
	
	public Config config;
	public IAgent agent;
	/** HttpExchange for the original request - will be null for ping requests */
	public HttpServerExchange httpExchange;
	/** Will only be present for ping requests - null for all normal requests */
	public ClientRequest pingRequest;
	
	public Gateway gateway;
	
	/** If any exception occurred during processing, e.g. authentication failure - it is available here for scripts to use */
	public Throwable exception;
	
	public List<Location.Headers> pendingRequestHeaders = new ArrayList<>();
	public List<Location.Cookies> pendingRequestCookies = new ArrayList<>();
	
	public List<Location.Headers> pendingResponseHeaders = new ArrayList<>();
	public List<Location.Cookies> pendingResponseCookies = new ArrayList<>();
	public HashMap<String, String> requestAttributes = new HashMap<>();
	public HashMap<String, CompletedRequest> completedRequests = new HashMap<>();
	
	public List<CookieSnapper> cookieSnappers = new ArrayList<>();
	
	/** The original Path before being rewritten, if any */
	public volatile String originalPath;
	/** The original URI before being rewritten, if any */
	public volatile String originalURI;
	/** The original scheme before being rewritten, if any */
	public volatile String originalScheme;
	/** The original host header before it was rewritten, if any */
	public volatile String originalHost;
	/** Original non-rewritten URL as it was before rewriting it */
	public volatile String originalUrlBeforeRewrite;
	
	/** Time that this request started to be proxied */
	public volatile long proxyStartTimestamp;
	
	public LocatorInfo geoip;
	
	public Tracer trace;
	
	public volatile Config.Destination currentDestination;	
	public volatile Config.Location currentLocation;

	/** The exception that was set as input to setAndLogError - this might go in the accesslog */
	public volatile Throwable error;
	
	/** When request content has already been read, we can find it here - this is done if some plugin or location needs access to it, e.g. POST data. */
	public volatile byte[] prefetchedRequestContent;
	/** If request content contained form input data, it is here */
	public volatile FormData formData;
	
	/** Determines if session was authenticated when the request arrived */
	public volatile boolean wasAuthenticatedAtRequestTime;
	/** True, if the session originated from a cookie */
	public volatile boolean sessionOriginatedFromCookie;
	/** True, if session fixation cookie was already added */
	public volatile boolean fixationCookieAdded;
	
	/** API Manager related information - if available for client */
	public volatile io.ceptor.apimanager.Config.APIPartner apiPartner;
	public volatile io.ceptor.apimanager.Config.APIPartnerApplication apiPartnerApplication;
	
	/** The ID of the API that the API Gateway served, or null if none was used */
	public volatile String apiId;
	/** The API descriptor, if an API was processed - useful for extracting extra data for logging */
	public MethodAndPathToAPIVersion apiDescriptor;

	/** These variables are added to the SLF4J MDC, and are then available for use in logging definitions */
	public Map<String,String> logContextVariables = new HashMap<>();

	
	/** Any response hooks picked up while processing locations are stored here and called when a response is received as a result of proxying a call */
	public List<Config.ResponseHook> responseHooks = new ArrayList<>();
	
	/** This is meant for use by response hooks - The response status - only available after reading response headers */
	public int proxyResponseStatusCode;
	/** This is meant for use by response hooks - The response headers read from the server we proxied to */
	public HeaderMap proxyResponseHeaders;
	/** This is meant for use by response hooks - the connection to the server */
	public ClientExchange proxyClientExchange;
	/** This is meant for use by response hooks - the currently executing response hook */
	public Config.ResponseHook currentResponseHook;

	/**
	 * Returns the query or POST parameter - if the request contains a body with query parameters, they will be searched first, then the POST parameters.
	 * 
	 * @param name Name of query parameter
	 * @return Value, or null if not found.
	 */
	public String getQueryOrPostParam(String name);
	
	/**
	 * Returns a Deque<String> list of the query of POST parameter values - if the request contains a body with query parameters, they will be searched first, then the POST parameters.
	 * 
	 * @param name Name of query parameter
	 * @return List of values, or null if not found.
	 */
	public Deque<String> getQueryOrPostParams(String name);

	/**
	 * Returns the path template parameter within the path, if found.
	 * @param pathTemplate Template - e.g. /mypath/{username}/other/{shoesize}
	 * @param parameterName Name of parameter, e.g. username to look for in the path
	 * 
	 * @return Parameter value, or null if not found
	 */
	public String getPathTemplateParam(String pathTemplate, String parameterName);

	public String getSessionVariable(String name) throws PTException {
		return agent.getStateVariable(id, name);
	}
	
	public boolean isResponseStartedOrSent() {
		return isResponseSent || httpExchange.isResponseStarted();
	}
	
	public String macro(String macro) throws ScriptException, ParseException, IOException {
		Mapper<StateHolder> mapper = Mapper.parse(macro, new StateHolderReplacerFactory());
		return mapper.map(callback, this);
	}


	public void respond(int responseCode, String reason, String contentType, String body) {
		gateway.sendResponse(this, responseCode, reason, contentType, body);
	}
	
	public void respond(int responseCode, String reason, String contentType, byte[] body) {
		gateway.sendResponse(this, responseCode, reason, contentType, body);
	}
	
	public void respond(int responseCode, String reason, String contentType, ByteBuffer body) {
		gateway.sendResponse(this, responseCode, reason, contentType, body);
	}


	/**
	 * Add a new entry to the SLF4J MDC (Multiple Diagnostics Context) - this is then available for logging
	 * 
	 * @param key Key to add
	 * @param value The value
	 */
	public void addToMDC(String key, String value);
}

// CompletedRequest contains the result of a service call task within a pipeline
public final class CompletedRequest {
	public volatile long startedAt = System.currentTimeMillis();
	public volatile long endedAt;
	public volatile int tries;
	public volatile boolean isCancelled;
	public volatile Exception exception;
	public volatile byte[] response;
	public volatile int responseCode;
	public volatile String responseReason;
	public volatile HeaderMap responseHeaders;

    /* Returns the response converted to a string in the character set indicated by the content-type response header */
	public String getResponseAsString();
}


Session Resolver

A "Session Resolver" plugin is able to look at the request, and based upon input it will extract a session ID if present.

If not present, it gets a chance to create a new session, and it will be called if a session is no longer valid and its ID should be removed so it can e.g. remove a session cookie.

The plugin must implement the interface io.ceptor.session.ISessionResolver which looks like this:

package io.ceptor.session;

import dk.itp.security.passticket.PTException;
import io.ceptor.gateway.state.StateHolder;

/**
 * 
 * Find a session within the request - a session could be coming from a cookie, a request parameter, an http header, an SSL client certificate, an OAuth bearer token etc.
 *  
 */
public interface ISessionResolver {
	/**
	 * Look at the request, and extract a session ID if possible - the ID should be placed in state.sessionId when returning.
	 * 
	 * @param context
	 */
	public void resolveSession(StateHolder context);
	
	/**
	 * If supported by this resolver, create a new session, modify the request/response headers accordingly. The new session ID should be
	 * in state.sessionId when returning.
	 * 
	 * If this plugin does not know how to create a new session, just ignore it and do nothing.
	 * 
	 * @param context
	 */
	public void createSession(StateHolder context) throws PTException;
	
	/**
	 * If supported by this resolver, remove trace of the old session (e.g. cookie), modify the response headers accordingly.
	 * 
	 * If this plugin does not know how to delete a session cookie or similar, just ignore it and do nothing.
	 * 
	 * @param context
	 */
	public void removeSession(StateHolder context) throws PTException;
}



By default, the following Session Resolvers exist:

  • io.ceptor.session.SessionResolverBearerToken
    Allows use of an OAuth2 bearer token in the HTTP request header Authorization.
  • io.ceptor.session.SessionResolverApiKey
    Uses an API key instead of a session ID to identify a session.
  • io.ceptor.session.SessionResolverCookie
    Supports use of a session cookie 
  • io.ceptor.session.SessionResolverDomainRedirect
    Supports using Domain Redirect URLs (URLs ending with ".domainRedirect") which allows sharing sessions between two or more domains, e.g. www.mydomain1.com and www.myotherdomain.com 
  • io.ceptor.session.SessionResolverSSLClientCert
    Allows using an SSL client certificate as a "ticket", with no other session ID - if no session currently exists for the incoming client certificate, one will be created and the user will be authenticated assuming the authentication plugin allows access. 
  • io.ceptor.session.SessionResolverScript
    Allows implementing a session resolver using Javascript - makes it easy to add custom functionality to the gateway. 


Authentication Plugin

An authentication plugin allows authenticating a user based upon information available in the request.

Do not confuse Gateway Authentication Plugins with Authentication Plugins for the Session Controller - despite the name they are 2 separate entities.

In the gateway, this plugin has full access to the request and response and can do anything with it - in the Session Controller which is a separate module, an Authentication Plugin is able to handle authentication from a number of sources - e.g. from standalone applications, from the Ceptor Gateway, or from Radius.

Often the gateway authentication plugin uses the Agent API to call an authentication plugin on the Session Controller to authenticate a user within a session.


Gateway Authentication Plugins must implement the interface io.ceptor.authentication.IAuthenticator which look like this:

package io.ceptor.authentication;

import java.io.IOException;
import java.text.ParseException;

import dk.itp.security.passticket.PTException;
import io.ceptor.config.gateway.Config.Location;
import io.ceptor.gateway.state.StateHolder;
import portalprotect.org.json.JSONException;

/**
 * 
 * Plugin which handles and allows authentication of users - authentication could be done based upon e.g basic auth,
 * OAUTH2, SPNEGO, NTLM, SSL client certificates or any other means where the gateway can do the authentication.
 * 
 * Please note that in cases where a new session is not required, but a ticker or token is used instead of a session,
 * ISessionResolver should be used instead - this authentication is used for authenticating a user where a session has
 * previously been established.
 *  
 */
public interface IAuthenticator {
	public enum AuthenticationAction {
		CONTINUE, RESPOND, SUCCESS
	}
	
	/**
	 * Allows the plugin to authenticate the user - it is called on every request, so it should start by figuring out if authentication
	 * is really needed or not before attempting to do anything.
	 * 
	 * @param context
	 * @return Action telling what to do - if CONTINUE, processing continues, if RESPOND, the plugin must have prepared and sent a response handling the communication with the client.
	 */
	AuthenticationAction authenticate(StateHolder context) throws PTException;
	
	/**
	 * Initializes the plugin for a specific location.
	 * @param location
	 */
	void init(Location location) throws ParseException, IOException, JSONException;
	
	/**
	 * If the authentication method requires access to the request content/body, true must be returned - in this case the request body is read before the authentication plugin is called.
	 * @return
	 */
	default boolean requiresRequestContent() {
		return false;
	}
}


By default, the following authentication plugins exist:

  • io.ceptor.authentication.AuthenticatorBasicAuth
    Allows authenticating using HTTP Basic Authentication - meaning the HTTP Request Header "Authorization" with contents "Basic xxxxxxx" where xxxxxxx is a base64 encoded version of userid:password. Please note that the password is not encrypted, but just encoded using base64 encoding, which makes this a relatively weak authentication method so the entire transport must be appropriately encrypted, using HTTPS.
  • io.ceptor.authentication.AuthenticatorForms
    Supports using FORMS authentication, where user can authenticate using HTTP query or POST parameters. Is also often used to simply redirect to another forms page somewhere else - e.g. in a login application if the user is not authenticated.
  • io.ceptor.authentication.AuthenticatorNTLM
    Supports NTLM authentication in an intranet environment.
  • io.ceptor.authentication.AuthenticatorSPNEGO
    Supports SPNEGO/Kerberos with NTLM fallback types of authentication in intranet environments.
  • io.ceptor.authentication.AuthenticatorOpenIDConnect
    Allows use of OpenID Connect authentication - supports redirecting to authentication providers and handles the response from them - supports use of Authorization Code flow to obtain an ID/Access token from an authorization code. 
  • io.ceptor.authentication.AuthenticatorSSLClientCert
    Allows authenticating clients using SSL Client certificate.
  • io.ceptor.authentication.AuthenticatorWebSSO
    Allows using ADFS / SAML WebSSO / WSTrust to authenticate users - supports acting both as an identity provider, and a relying party. 
  • io.ceptor.authentication.AuthenticatorLTPAToken
    Allows using a cookie with an LTPA Token to authenticate users - enables easier SSO with IBM products such as WebSphere, Liberty Server and iNotes.
  • io.ceptor.authentication.AuthenticatorAPIKey
    Allows using an API Key to authenticate a caller - usually used when making REST calls.
  • io.ceptor.authentication.AuthenticatorScript
    Supports a javascript based authentication where you can write a script that authenticates any way you want based upon the incoming HTTP request.

AuthenticatorScript

For the AuthenticatorScript, you can script authentication based upon the incoming request. Below is an example that looks in HTTP headers or query parameters named X-User and X-Password.
If found, it will attempt to authenticate the user.

authenticate();

function authenticate() {
   // If already authenticated, just continue
   
   if (context.agent.isLoggedOn(context.id))
      return 'CONTINUE';
      
   try {
        var user = context.httpExchange.getRequestHeaders().getFirst('X-User');
        var pass = context.httpExchange.getRequestHeaders().getFirst('X-Password');
        if (user === null) {
            user = context.httpExchange.getQueryParameters().get('X-User');
			if (user !== null)
				user == user.getFirst();
        }
        if (pass === null) {
            pass = context.httpExchange.getQueryParameters().get('X-Password');
			if (pass !== null)
				pass = pass.getFirst();
        }
        
        if (user === null || pass === null) {
            // Ignore if user or password is not supplied - making this type of authentication optional
            return 'CONTINUE';
        } else {
            context.trace.trace('About to logon from script with user: ' + user);
            // 9 is authentication plugin type
            context.agent.logon(context.id, 9, user, pass, null);
            return 'SUCCESS';
        }
    } catch(err) {
        context.gateway.sendAndLogError(state, 401, 'Authentication Required', err);
        return 'RESPOND';
    }
}


Target Authentication Plugin

A target authentication plugin is responsible for adding authentication information when the request is about to be proxied onwards to a target server within a Destination.

This allows setting specific types of authentication up towards applications sitting behind the gateway, so the gateway can emulate whatever these applications expects in case they are already expecting some kind of authentication.

A Target Authentication Pluign needs to implement the interface io.ceptor.authentication.target.ITargetAuthenticator, which looks like this:

package io.ceptor.authentication.target;

import java.io.IOException;
import java.text.ParseException;

import dk.itp.security.passticket.PTException;
import io.ceptor.config.gateway.Config.Destination;
import io.ceptor.gateway.state.StateHolder;
import io.undertow.client.ClientRequest;
import portalprotect.org.json.JSONException;

/**
 * 
 * Plugin to handle forwarding of authentication information to a backend server.
 *  
 */
public interface ITargetAuthenticator {
	/**
	 * Initializes the plugin for a specific destination.
	 * @param destination
	 */
	public void init(Destination destination) throws ParseException, IOException, JSONException;

	/**
	 * Adds authentication information to the request.
	 * @param context
	 * @throws PTException
	 */
	public void addAuthenticationInfo(StateHolder context, ClientRequest request);
	
	/**
	 * Signals if an IO thread is needed when executing
	 * @return True if IO thread is needed - remember not to block in that case
	 */
	public boolean needsIOThread();
}



The following standard target authenticator plugins exist:

  • io.ceptor.authentication.target.TAuthenticatorBasicAuth
    This plugin can perform basic authentication against the target server - it can supply both a configured value, pick one from the session, or it can forward the session ID of the authenticated user as userid. This can be configured differently for anonymous and authenticated users. Often, this plugin is used to make it easier to integrate with various application servers, e.g. when using WebLogic SSPI plugins, PP Agent within WebLogic is triggered by the session ID being transmitted in the basic authorization header as if it were a userid. 
  • io.ceptor.authentication.target.TAuthenticatorBearerToken
    Allows forwarding of a bearer token obtained from the client to the target server, or creation of a new token/ticket which can be sent in a bearer token to the target server. 
  • io.ceptor.authentication.target.TAuthenticatorSPNEGO
    Allows using SPNEGO/Kerberos authentication towards the target server - can either user authenticated users userid/password, or a configured one or one taken from other state variables within the session. 
  • io.ceptor.authentication.target.TAuthenticatorSSL
    Allows adding SSL certificate information to the request HTTP headers - e.g. if user was authenticated using an SSL client certificate, the used cypher, SSL session ID and client certificate can be added as HTTP headers. Note that SSL certificate authentication is not done by this plugin, since it would require access to the end-users private key - instead, the information is forwarded in HTTP headers.
    If you need to use a specific SSL client certificate in the communication with the target server, you can set that up in the SSL settings in the destination / target server configuration.
  • io.ceptor.authentication.target.TAuthenticatorLTPAToken
    Allows forwarding of an LTPA token obtained from the client to the target server, or creation of a new LTPAToken2 cookie which can be sent to the target server. 

Java Request Wrapper

A request wrapper plugin, is a plugin which can process the request body, and optionally transform it.

It must implement io.undertow.server.ConduitWrapper<StreamSourceConduit> - below is an example which dumps the entire request contents, but does not otherwise modify the it.

RequestDumper.java
package io.ceptor.gateway.plugin;

import org.xnio.conduits.StreamSourceConduit;

import io.ceptor.gateway.state.StateHolder;
import io.undertow.server.ConduitWrapper;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.ConduitFactory;

public class RequestDumper implements ConduitWrapper<StreamSourceConduit> {
	@SuppressWarnings("unused")
	private StateHolder context;
	
	public RequestDumper(StateHolder context) {
		this.context = context;
	}
	
	@Override
	public StreamSourceConduit wrap(ConduitFactory<StreamSourceConduit> factory, HttpServerExchange exchange) {
		return new RequestDumperStreamSourceConduit(factory.create());
	}
}


RequestDumperStreamSourceConduit.java
package io.ceptor.gateway.plugin;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import org.jboss.logging.Logger;
import org.xnio.IoUtils;
import org.xnio.channels.StreamSinkChannel;
import org.xnio.conduits.AbstractStreamSourceConduit;
import org.xnio.conduits.ConduitReadableByteChannel;
import org.xnio.conduits.StreamSourceConduit;

import dk.itp.security.utils.HexDump;

public class RequestDumperStreamSourceConduit extends AbstractStreamSourceConduit<StreamSourceConduit> {
	private static Logger log = Logger.getLogger(RequestDumperStreamSourceConduit.class);
	
    public RequestDumperStreamSourceConduit(StreamSourceConduit next) {
        super(next);
    }

    public long transferTo(final long position, final long count, final FileChannel target) throws IOException {
        return target.transferFrom(new ConduitReadableByteChannel(this), position, count);
    }

    public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException {
        return IoUtils.transfer(new ConduitReadableByteChannel(this), count, throughBuffer, target);
    }

    @Override
    public int read(ByteBuffer dst) throws IOException {
        int pos = dst.position();
        int res = super.read(dst);
        if (res > 0) {
            byte[] d = new byte[res];
            for (int i = 0; i < res; ++i) {
                d[i] = dst.get(i + pos);
            }
            log.info("Read:\n" + HexDump.xdump(d));        }
        return res;
    }

    @Override
    public long read(ByteBuffer[] dsts, int offs, int len) throws IOException {
        for (int i = offs; i < len; ++i) {
            if (dsts[i].hasRemaining()) {
                return read(dsts[i]);
            }
        }
        return 0;
    }
}



Java Response Wrapper

Like with the request wrapper, the response wrapper can modify the response content being sent back to the client - it needs to implement the interface io.undertow.server.ConduitWrapper<StreamSinkConduit> - the example below dumps the entire response without modifying it.

ResponseDumper.java
package io.ceptor.gateway.plugin;

import org.xnio.conduits.StreamSinkConduit;

import io.ceptor.gateway.state.StateHolder;
import io.undertow.server.ConduitWrapper;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.ConduitFactory;

public class ResponseDumper implements ConduitWrapper<StreamSinkConduit> {
	@SuppressWarnings("unused")
	private StateHolder context;
	
	public ResponseDumper(StateHolder context) {
		this.context = context;
	}
	
	@Override
	public StreamSinkConduit wrap(ConduitFactory<StreamSinkConduit> factory, HttpServerExchange exchange) {
		return new ResponseDumperStreamSinkConduit(factory.create());
	}
}


ResponseDumperStreamSinkConduit.java
package io.ceptor.gateway.plugin;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import org.jboss.logging.Logger;
import org.xnio.IoUtils;
import org.xnio.channels.StreamSourceChannel;
import org.xnio.conduits.AbstractStreamSinkConduit;
import org.xnio.conduits.ConduitWritableByteChannel;
import org.xnio.conduits.Conduits;
import org.xnio.conduits.StreamSinkConduit;

import dk.itp.security.utils.HexDump;

public class ResponseDumperStreamSinkConduit extends AbstractStreamSinkConduit<StreamSinkConduit> {
	private static Logger log = Logger.getLogger(ResponseDumperStreamSinkConduit.class);
	
    public ResponseDumperStreamSinkConduit(StreamSinkConduit next) {
        super(next);
    }

    @Override
    public int write(ByteBuffer src) throws IOException {
        int pos = src.position();
        int res = super.write(src);
        if (res > 0) {
            byte[] d = new byte[res];
            for (int i = 0; i < res; ++i) {
                d[i] = src.get(i + pos);
            }
            log.info("Write:\n" + HexDump.xdump(d));
        }
        return res;
    }

    @Override
    public long write(ByteBuffer[] dsts, int offs, int len) throws IOException {
        for (int i = offs; i < len; ++i) {
            if (dsts[i].hasRemaining()) {
                return write(dsts[i]);
            }
        }
        return 0;
    }

    @Override
    public long transferFrom(final FileChannel src, final long position, final long count) throws IOException {
        return src.transferTo(position, count, new ConduitWritableByteChannel(this));
    }

    @Override
    public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException {
        return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this));
    }

    @Override
    public int writeFinal(ByteBuffer src) throws IOException {
        return Conduits.writeFinalBasic(this, src);
    }

    @Override
    public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException {
        return Conduits.writeFinalBasic(this, srcs, offset, length);
    }
}



Script Request Wrapper

Like the java version of the request wrapper, you can also write a javascript version - it allows the same functionality, but the gateway implements the needed java interfaces, so the javascript code is slightly simpler - below is an example of the javascript methods your script needs to implement:

//
// Example javascript that simply writes request contents and dumps the contents written
//

/*
 * Called to check if we want to process the data for this request - gives us a chance to say we are not interested
 * 
 * Must return true for write() to be called later,
 */
function appliesToThisRequest(context) {
	return true;
}

/*
 * Called when we have bytes available to read from the request.
 * 
 * This is called from the IO thread, so do NOT block here
 * 
 * Parameters are:
 * state - StateHolder
 * buf   - ByteBuffer
 * next  - Next reader in the chain, call read(buf) to read from the request
 */
function read(context, buf, next) {
    var pos = buf.position();
    var res = next.read(buf);
    if (res > 0) {
    	var ba = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, res);
        for (var i = 0; i < res; ++i) {
            ba[i] = buf.get(i + pos);
        }
        context.trace.trace("Read from javacript: " + new java.lang.String(ba));
    }
    return res;
}

/**
 * Called to signal that no further bytes will be read
 * 
 * This is called from the IO thread, so do NOT block here
 * 
 * Parameters are:
 * state - StateHolder
 * next  - Next reader in the chain, call terminateReads() on it when done
 */
function terminateReads(context, next) {
	next.terminateReads();
}



Script Response Wrapper

Like the javascript version of the request wrapper, this allows wrapping of the response contents.

//
// Example javascript that simply writes response contents and dumps the contents written
//

/*
 * Called to check if we want to process the data for this request - gives us a chance to say we are not interested
 * 
 * Must return true for write() to be called later,
 */
function appliesToThisRequest(context) {
//	var url = context.gateway.getRequestURL(context);
	
	return true;
}

/**
 * Called when we have bytes to write to the response.
 * 
 * This is called from the IO thread, so do NOT block here
 * 
 * Parameters are:
 * context - StateHolder
 * buf     - ByteBuffer
 * next    - Next writer in the chain, call write(buf) to write the real response
 */
function write(context, buf, next) {
    var pos = buf.position();
    var res = next.write(buf);
    if (res > 0) {
    	var ba = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, res);
        for (var i = 0; i < res; ++i) {
            ba[i] = buf.get(i + pos);
        }
        context.trace.trace("Write from javacript: " + new java.lang.String(ba));
    }
    return res;
}

/**
 * Called to signal that no further bytes will be written
 * 
 * This is called from the IO thread, so do NOT block here
 * 
 * Parameters are:
 * context - StateHolder
 * next    - Next writer in the chain, call terminateWrites() on it when done
 */
function terminateWrites(context, next) {
	next.terminateWrites();
}


Authorization Script

An authorization script, can be used to enforce authorization checking using advanced methods that go beyond the usual role-based access checking.

With an authorization script, you can look at any part of the request, or the users authenticated session and decide if you want to allow access or not based upon the contents of both.
This naive example simply denies access if the Content-Type of the request is 'YouShallNotPass' and allows access otherwise. 

if (context.httpExchange.getRequestHeaders().getFirst('Content-Type') == 'YouShallNotPass')
  false;
else
  true;


Ping Verification Script

A Ping verification script is called when "pinging" target servers to check if they are up or not - this allows the script to look at response body contents and not just rely on http response code to determine if the server is fit for sending requests to or not.

The script is called with input set to contain the response body, and state set to the usual Plugins#StateHolder instance. via state, the plugin can access the full response headers.

if (input.indexOf('Looks good to me') != -1)
  true;
else
  false;


IP Checker Script

Script must return true or false to indicate if it is ok to swith to the new IP address.

The variable input contains the new IP address the client is switching to. The variable state contains all information about the request.

Example:

if (input == '127.0.0.1') true; else false;

Another example - here checking the original IP:

if (input == '127.0.0.1' || context.gateway.getClientSourceIP(state) == '10.0.0.1') true;

Error Page Script

When the gateway is sending an error page, you have the option to specify a body (or add HTTP headers to the response).

This script is defined in the gateway settings, so if you need to modify the contents sent depending on location or URL, you need to modify the script to take that into account. The context variable contains the StateHolder object (see above) which has references to the currentLocation and currentDestination which you can use for this.

By default, if no script is present, no body will be sent to the client.

This script will be called with a string containing a JSON object as input, e.g.

{
  "status": 503,
  "reason": null,
  "message": "Unable to find live host for destination: demoapp",
  "exception": ""
}


Below is an example script, which returns a JSON errormessage if the content-type is application/json, or if the Accept header explicitly contains application/json.

If not json, it will return a simple HTML body containing the error information.

If you wish to do so, you can uncommit the lines below and add error diagnostics information to the response as well - this will be an exception and its stacktrace.

getBody();

function getBody() {
    var Headers = Java.type('io.undertow.util.Headers');
    var HtmlEncoder = Java.type('dk.itp.security.utils.HtmlEncoder');
    var XmlEncoder = Java.type('dk.itp.security.utils.XMLEncoder');
    
    var errorInfo = JSON.parse(input);
    
    var reqHeaders = context.httpExchange.getRequestHeaders();
    if (reqHeaders.getFirst('Content-Type') == 'application/json' ||
        (reqHeaders.getFirst('Accept') !== null && reqHeaders.getFirst('Accept').contains('application/json'))) {
        context.httpExchange.getResponseHeaders().add(Headers.CONTENT_TYPE, 'application/json');
        
        var resp = {
            'status': errorInfo.status,
            'error': errorInfo.message
 //           ,'diagnostics': errorInfo.exception
        };
        
        return JSON.stringify(resp);
    }
    if (reqHeaders.getFirst('Content-Type') == 'application/xml' ||
        (reqHeaders.getFirst('Accept') == 'application/xml')) {
        context.httpExchange.getResponseHeaders().add(Headers.CONTENT_TYPE, 'application/xml');
        
        return  '<error>\n' +
                '  <status>'+errorInfo.status+'</status>\n' +
                '  <message>'+XmlEncoder.encode(errorInfo.message)+'</message>\n' +
//                '  <diagnostics>'+XmlEncoder.encode(errorInfo.exception)+'</diagnostics>\n' +
                '</error>';
    }
    
    context.httpExchange.getResponseHeaders().add(Headers.CONTENT_TYPE, 'text/html');
    
    return "<html><head><title>Error " +  errorInfo.status + "</title></head><body>Error " +
         HtmlEncoder.encode(errorInfo.status + " " + errorInfo.message) +
//        "<pre>" + HtmlEncoder.encode(errorInfo.exception) + "</pre>" +
        "</body></html>";
}

© Ceptor ApS. All Rights Reserved.