Javascript

Javascript code is executed to generate a resulting value. Full scripts can be executed, or a macro can contain script code which is run.

A script is assumed, if the value is prefixed with %{script} then the entire rest of the value is executed as a script.
When using the GUI to enter values, a radio button allows you to select which language to use for scripting in the editor handles prefixing the string, and changing the editor to javascript mode for syntax hiliting.

If you start a string with %{file} then the actual contents of the script will be loaded from the file (meaning the file needs to exist on the gateway where it is executed).

Groovy scripts

Like with javascript, you can use groovy scripts to execute code - if the value is prefixed with %{script:groovy} then the rest of the value is executed as a groovy script. You can find more information about the groovy language here: http://groovy-lang.org/documentation.html and this page: http://groovy-lang.org/differences.html contains a quick list of the main differences between groovy and java languages.

Python scripts

By prefixing with %{script:python} you can execute python code - note that "jython" (http://www.jython.org/) is used for executing python scripts within the gateway - you can find very good information on integration with java here: https://wiki.python.org/jython/UserGuide

Request context

Script code has access to a variable called context which is of an instance of the class StateHolder - see Plugins for a description of its contents. It contains access to all data that the gateway uses internally, so it allows you to do anything (and also to mess up anything so be careful about what you do). 

Remember the saying - With Great Power comes Great Responsibility - take care what you do and ensure that you do not do stupid things like calling eval() on a string originating from outside, or constructing code dynamically based upon input from a browser query field.

If you e.g. call context.macro(context.getQueryOrPostParam('country')) and someone posts country with the value "%{script}java.lang.System.exit(1)" then you have just created a remote code execution vulnerability.

As long as you do not trust input from the outside, you are perfectly safe.


Global variables

All script code has access to a java.util.concurrent.ConcurrentHashMap called sharedState in which they can store data between requests - this data is globally shared between all scripts and all requests, so it can be used for caching data.

Request attributes within the context (accessed via context.requestAttributes is a hashMap containing request specific attributes - this is local for the request, and disappears once the request is finished.

All scripts run within separate thread contexts, so any global variables a script assigns and reuses between runs are not shared between threads.

This is to ensure separation of requests so ensuring that scripts that are not thread-safe will work anyway when executed concurrently by multiple threads.
If your scripts contain variables that must be shared between threads, such as cached values, place them in the sharedState variable which is accessible from all scripts, for all requests and all script languages.

Return values

A script can return the value it generates, it can be e.g. true or false for scripts that run inside a condition, or it can be a string for a script that outputs an HTTP header value. In some cases (like when using python scripts that call a function to generate a result) it is easier to assign the result to a variable, to support this, Ceptor's script engine checks the return value from the script - if that is null, it will look in the variable "__result__" (two underscores + the word result + two underscores) - and if that exists after executing the script - its value will be used.

Using macros from scripts

If you need to use a macro from a script, you can call context.macro("xxxxx") - e.g. context.macro("https://%{HTTP_HOST}/myurl") will parse the macro and replace %{HTTP_HOST} with the appropriate value.

A word on performance

Executing scripts is significantly slower (usually several orders of magnitude slower) than using macros - so try to keep things simple and use macros whenever you can.

Ceptor precompiles the scripts on the first run, and reuses the precompiled version - When using Python scripts, this compilation can take several seconds on the first startup, since the Jython engine is quite expensive to start.

Javascript tends to be faster than both python and groovy after tends of thousands invocations once the JIT has optimized it

Literal values

If you (like with %{script}) prefix the string with %{literal} then the rest of the string is taken literally, meaning it is not parsed or processed in any way.
Note that this requires minimum version 5.66 to work.

Functions to call from scripts

These are some of the built-in functions that can be called from your scripts;

contex.respond(<http response code>, <http response reason>, <content type>, <body>)

Sends a response to the client, the response body can be a String, a byte[] or a ByteBuffer.


context.gateway.sendAndLogError(context, <http response code>, <http response reason>, <exception text>);
context.gateway.sendAndLogError(context, <http response code>, <http response reason>, <java throwable>);

Sends an error response back to the client, the exception text / throwable will appear in the access log, and a warning will be logged in the system log with error send back.


context.gateway.sendError(context, <http response code>, <http response reason>, <exception text>);
context.gateway.sendError(context, <http response code>, <http response reason>, <java throwable>);

Sends an error response back to the client, the exception text / throwable will appear in the access log, no error/warning will be logged in the system log.


context.gateway.isFromSSLAccelerator(context);

Returns true if request went through a configured SSL accelerator, false if not.


context.gateway.getCanonicalPath(context);

Returns the canonical path - where /./ and /../ are canonicalized and resolved/removed.


context.gateway.getEncodedCertificates(context, forceRenegotiateFlag);

Return the client SSL certificate - if forceRenegotiateFlag is set to true, SSL renegotiation will be done.


context.gateway.isSecure(context);

Returns true if request is considered secure (meaning HTTPS was used, or request passed through SSL accelerator where HTTPS was used).


context.gateway.getGeoIP(context);

Returns LocatorInfo object, containing GeoIP information about the client. Note that this call blocks while looking GeoIP info up in the database via the session controller, so it can take a few milliseconds to resolve.


context.gateway.isIPMatchingRange(context, range name, ip address);

Checks if the provided IP address is within the configured IP Range or not


context.isResponseStartedOrSent();

Returns true if a response is already started or sent - if a response has started, the HTTP headers cannot be modified since they are already sent to the client.


context.getQueryOrPostParam(<parameter name>);

Returns the value of the POST or query parameter - note that POST parameter content is only available if a previous location has been configured to read the request content / POST data.


context.getSessionVariable(<variable name>);

Returns the value (or null if not found) of the state variable in the Ceptor Session.

context.proxyTo(long maxRequestTimeMsecs, Config.Destination destination);
context.proxyTo(long maxRequestTimeMsecs, String destinationName);
context.proxyToServer(long maxRequestTimeMsecs, String scheme, String hostname, int port);
context.proxyToServer(long maxRequestTimeMsecs, String scheme, String hostname, int port, boolean ignoreSSLCert); // Requires minimum Ceptor v6.4.1
context.proxyToServer(long maxRequestTimeMsecs, String scheme, String hostname, int port, boolean ignoreSSLCert, int timeoutSeconds); // Requires minimum Ceptor v6.4.8
context.proxyToServer(long maxRequestTimeMsecs, String scheme, String hostname, int port, boolean ignoreSSLCert, int timeoutSeconds, String templateJSON); // Requires minimum Ceptor v6.4.8


Proxy requests to a server immediately.

When proxying from a script, all other actions for a location not yet completed are aborted and not done - so it depends on where you invoke it.

Since a location script is run as one of the first things when processing a location, any headers, authorization, authentication checks etc. that would otherwise occur later will not be done if proxying is invoked from a location script.
If proxying is invoked from e.g. an authorization script however, that is one of the last actions done as part of processing a location, so anything else (authentication, parameter checking, http header modification etc.) will already be done or queued up if proxying is invoked from such a script.

If in doubt, make sure all actions needed are done from an earlier location before you have a script which starts proxying programmatically.

Also note that compression of responses is not done when proxying is initated from a script.



context.agent

Allows access to the java API for Ceptor Agent - see Ceptor Agent for Java

When accessing state variables from within a session, note that by default not all state variables are visible to gateway/dispatcher agents. These agents typically use a "restricted" connection to the session controller - this means that certain content is removed to reduce the risk of compromising sensitive data in case a gateway machine in a DMZ zone is compromised for any reason.

To make state variables visible for gateways, add them to safeStateVariables in the session controller configuration - see Session Controller Properties for more details.


context.trace.trace(<message>);
context.trace.trace(<message>, <exception>);

Allows access to the trace, if Gateway Tracing is enabled for the request. Call with message and eventual exception.


context.addToMDC(key, value);

Stores a value in the SLF4J MDC which is then available for logging (requires minimum Ceptor v6.0.1).


context.getDeployedAPIsForLocation(<location>);

/**
* Returns the list of deployed APIs for a given location - often <code>currentLocation</code> can be used to choose the current active location - otherwise you can get to locations via <code>config.locationList</code>
* 
* @param location Location to return APIs from
* 
* @return List of API versions currently deployed
*/
public List<Config.APIManagement.APIVersion> getDeployedAPIsForLocation(Config.Location location);

Returns the list of deployed APIs for a given location - often currentLocation can be used to choose the current active location - otherwise you can get to locations via config.locationList

This can be used to find which APIs are available for use within a Location - i.e. the ones that are deployed in the given environment.

/** API Version ID */
public String id;
/** Name of API Version */
public String name;
/** API Version description */
public String description;
/** Base Path */
public String basePath;
/** Type of API - wsdl, openapi or plainhttp */
public io.ceptor.apimanager.Config.APIType type;
/** For openapi's this contains the swagger/openapi spec */
public JSONObject openAPISpec;
/** Contains the wsdl for soap APIs */
public String wsdl;
/** List of environments this API is deployed in */
public Set<String> deployedEnvironments;
/** Name of API Group this API version is within */
public String apiGroupName;
/** The ID of the API containing this version */
public String apiId;
/** The name of the API containing this API version */
public String apiName;
/** Printable name containing both the API Group, API and API version naming */
public String prettyName;


/** True, if CORS is enabled */
public boolean corsEnabled;
/** List of header parameter names contained by all operations within this API */
public Set<String> headerParameterNames;


/** True, if subscriptions are required to access this API */
public boolean subscriptionRequired;
			
/** If OpenAPI spec is loaded from remote, this is the URL it is loaded from */
public String remoteUrl;
/** Time of last update (milliseconds since 1/1 1970 */
public long remoteLastUpdatedAt;
/** Exception that occured if loading of the remote spec failed */
public Throwable lastRemoteLoadError;

/** Full API Version JSON */
public JSONObject json;


Examples

// Send response to client
context.gateway.sendResponse(context, 200, "OK", "application/json", '{"status": "ok"}');

// Send error back to client, error will be logged in access log and a warning in the system log
context.gateway.sendAndLogError(context, 403, "Access denied", "Didn't provide the magic password");

var locator = context.gateway.getGeoIP(context);
if (locator) {
  var countryCode = locator.getCountryCode();
  var countryName = locator.getCountryName();
  var city = locator.getCity();
  var postalCode = locator.getPostalCode();
  var region = locator.getRegion();
  var latitude = locator.getLatitude();
  var longitude = locator.gteLongitude();
  var isp = locator.getISP();
  var organisation = locator.getOrganization();
  var ip = locator.getIP();
  // Distance in kilometers from central london
  var distanceInKilometers = locator.distanceFrom(51.509865, -0.118092);
}

// If gateway trace is enabled, add this text to the trace
context.trace.trace("Some tracing information here");


Macros

Macros such as %{HTTP_HOST} are replaced with the contents they represent - in this case, the contents of the Host HTTP header sent from the client.
a macro is in the format %{<name>} or %{<prefix>:<name>}
The following prefixes exist:

For information about compreq.* prefixes refer to the documentation for the RetrieveURL Task (also called "Task - Call service / URL").

and the following macros exist

Examples:
%{requestcookie:sessionid} for the request cookie named sessionid.
%{HTTP_HOST} for the HTTP Host request header.
%{urlencode:REQUEST_PATH} for an URL encoded version of the request path.

Macros for Logging

The following macros are only valid after the response has been written, so they are mostly useful for access logging:

Macros specific for API Gateway

These macros are for use with the API Gateway.

Special macros

In addition to the regular macros, there are also two special macros

%{rewrite:....} macro

Rewrite is used to change a value into another value by rewriting it if it matches a specified regular expression.
It is in this format:

%{rewrite:_name_;"_regex";"_flags_";"_value_"}

_name_ is replaced with a macro name, e.g. REMOTE_USER or responseheader:location.
_flags is replaced with flags as input to regular expression construction, these flags exist: (refer to java.util.regex.Pattern javadoc for details)

_value_ is replaced by the value that the regular expression is rewritten to.
Example:

%{rewrite:responseheader:location;"http\:\/\/(.*)$";"CASE_INSENSITIVE";"https://$1"}

This example rewrites the response header to https://xxxxx if it matches the pattern http://xxxxx - notice that $1 is used in the value which is replaced by the regex expression in the first set of parenthesis - please refer to the documentation on java regex patterns for additional information.

%{script:.....} macro

The script macro is used to embed javascript code into expressions for more complex expression handling that the one available in the macros

Example:
%{script:if (context.httpExchange.getRequestHeaders().getFirst('Content-Type') == 'application/json') 'application/json'; else 'text/html';} evaluates to application/json if the request Content-Type header was set to the same, and text/html otherwise.

Another example:
%{script:if (context.httpExchange.getRequestHeaders().getFirst('Content-Type') == 'application/json') '{"error":"access.denied"\}'; else '<html><head><title>No access</title><body><h1>Invalid userid/password</h1></body></html>';} returns an html or json error message depending on the request content-type.

Note that when running scripts embedded within the %{script:.....} macro, only the javascript language can be used - to run python or groovy scripts, use %{script:python}..... or %{script:groovy}..... instead.