Handling requests with application-specific code

Developing Plugins

Preliminaries

This note describes how the server can be customized to handle application-specific needs using plugin modules written in JavaScript.

Writing your own plugin and configuring the server to make use of it can be accomplished by following these instructions. The basic steps to understand are:

  1. How to write a module that meets the method interface requirements of a plugin.
  2. How to access the server's configuration hierarchy.
  3. How to use the work order to respond to requests.
  4. How to make your plugin visible to the server.
  5. How to route requests to your plugin.

Plugin method interface

Plugin modules must be written using JavaScript with CommonJS module syntax. This is the format used by Node.js at the time of this writing (2018). The module must export a class with this signature:

module.exports = class PluginClassname {
constructor(hostConfig) { /* plugin properties are defined here */ }
async startup() { /* plugin memory is initialized here */ }
async shutdown() { /* plugin state can be saved to storage here */ }
async processingSequence(workOrder) { /* the real work for each request/response cycle goes here */ }
}

During server startup your plugin module is loaded, and the exported class is instantiated, and its constructor is called. This is where you should declare and initialize all of the variables and external libraries that your plugin will be using. One plugin object is instantiated for each cluster worker created by the server.

Global variables declared outside of the constructor should be avoided as they provide no useful purpose. Each cluster worker operates in its own memory space, so no variables, either global or class-specific are sharable between cluster instances.

Each of the three plugin methods are called synchronously and will not yield control back to the server until its code has been fully executed. It is your responsibility to enforce this requirement by awaiting the return of any async functions you use.

The processingSequence method is called each time an incoming server request matches a path pattern that is declared in the configuration file. This is where the real work of your plugin occurs. This method's single parameter is a workOrder object which contains references to the incoming and outgoing headers and bodies for the current request/response cycle. More about this below. The server does not expect this method to return a value.

The startup method is called during server initialization, after the configuration file has been read into memory. This method is optional and may be omitted if not needed. The startup method is the appropriate place to open sharable sockets or network database connections. Placing this type of network intensive operation (discovery, authorization, and buffer initialization) in the startup method, will greatly reduce the effort needed to handle rapid-fire HTTP requests from users during normal operation.

The shutdown method is the appropriate place to serialize plugin state variables, close database connections, and remove event listeners. This method is optional and may be omitted if not needed. Note that when a cluster worker is accidentally terminated due to an unhandled error, the shutdown method is not guaranteed to be called.

Both the startup and shutdown method are called for each hostname for each cluster worker. Care must be taken when starting up and shutting down plugins that are used by multiple virtual hosts: remember that each host will have its own instance of your plugin and any properties defined in its class.

Also note, when your plugin is configured under the server section, its configured variables and routing will be identical for all virtual hosts. This design allows you to easily supply your plugin with standard configuration defaults across all hostnames.

On the other hand, when your plugin is configured under separate hostname sections, each hostname will be supplied with variables and routing from that configuration section. This design allows you the flexibility to configure each hostname with distinct values.

Accessing the hostConfig Object

A plugin's constructor is provided with a reference to the hostConfig object, which contains a hierarchical structure of the values in the configuration file that was used to start the server. Most importantly to this topic, are the values specifically set for the plugin.

For example, if your plugin is called plug-and-socket and the webmaster has defined a configuration file with three variables called database-name, authorization-id and password, their configured values can be accessed by the constructor — and saved to the object for use in the other class methods — using the following code.

Sample configuration1

server {
plugins {
plug-and-socket {
config {
database-name plugsdb
authorization-id superplug
password gulprepus
}
}
}
}

Sample plugin constructor1

module.exports = class PlugAndSocket {
constructor(hostConfig) {
var config = hostConfig.pluginsConfig.plugAndSocket;
this.pluginName = config.pluginName; // 'plug-and-socket'
this.pluginVersion = config.pluginVersion; // '1.2.3'
this.databaseName = config.databaseName; // 'plugsdb'
this.authorizationId = config.authorizationId; // 'superplug'
this.password = config.password; // 'gulprepus'
}
}

As demonstrated above, the variables in the configuration file use a different naming convention from the variables in JavaScript code. This name mangling is deterministic: all letters and digits are kept as is, except for HYPHENs, which are discarded, and the first letter after a HYPHEN, which is capitalized. More simply, configuration variables are camelCased into legal JavaScript variable names.

More involved configurations can be declared as well, which have subsections and attributes. When these are declared in a configuration, they are converted into subordinate objects with their own properties. Here is an example showing subsections and attributes:

Sample configuration2

server {
plugins {
plug-and-socket {
config {
database *name=plugdb *id=superplug *password=gulprepus
errors {
max-bugs 1
max-pugs 100
max-shrugs 10000
}
}
}
}
}

Sample plugin constructor2

module.exports = class PlugAndSocket {
constructor(hostConfig) {
var config = hostConfig.pluginsConfig.plugAndSocket;
this.pluginName = config.pluginName; // 'plug-and-socket'
this.pluginVersion = config.pluginVersion; // '1.2.3'
this.databaseName = config.database.name; // 'plugsdb'
this.authorizationId = config.database.id; // 'superplug'
this.password = config.database.password; // 'gulprepus'
this.maxBugs = config.errors.maxBugs; // '1'
this.maxPugs = config.errors.maxPugs; // '100'
this.maxShrugs = config.errors.maxShrugs; // '10000'
}
}

The hostConfig Hierarchy

The hostConfig object, in addition to the variables declared specifically for the plugin, contains the values for the rest of the configuration hierarchy. JavaScript does not have private scope or true constants, so it is technically possible for a plugin to manipulate any of these values. As a rule, plugin developers should honor the intention that all configuration values be treated as read-only variables.

The first level of the hostConfig hierarchy is shown here. See the accompanying notes for full descriptions of their meaning and acceptable values.

HostConfig {        
hostname // DNS name without scheme or port, see server name indication
aliases // an array of hostname aliases, see hosts
documentRoot // path to the location of files to be served, see hosts
encodingCache // path to the location of cached gzip files, see hosts
dynamicCache // path to the location of cached files created by blue process handler, see blue processor
landingPage // 'index.html' or resource to use when requested resourcePath is '/', see hosts

// host
serverConfig // see server
registrationConfig // see registration
loggingConfig // see logging
tlsConfig // private key and certificate, see hosts
restrictionsConfig // see restrictions
modulesConfig // see modules
contentTypesConfig // see content types
rbacConfig // see RBAC

// request
methodsConfig // see methods
ipAccessConfig // see IP access
resourceMaskConfig // see resource masks
forbiddenConfig // see forbidden
acceptTypesConfig // see accept types
userAgentConfig // see user agent
crossOriginConfig // see cross origin
acceptLanguageConfig // see accept language

// plugins
routerConfig // see router

// response
contentEncodingsConfig // see content encoding
charsetConfig // see charsets
cacheControlConfig // see cache control
pushPriorityConfig // see push priority
customErrorsConfig // see custom errors
}

Work Orders

Your plugin's processingSequence method is called by the server with one argument, a WorkOrder object.

This data structure has references to the raw request headers, processed headers, state variables, incoming request body, outgoing response headers, outgoing response body, and response status code. All of your custom plugin logic will be driven by this object and will happen with the code you provide inside the processingSequence method.

Study the separate note on Work Orders for a detailed description of its properties and methods. Also, study the separate Processing Sequence note to understand your responsibilities as a developer to properly set the outgoing payload type and response status code.

Make your plugin visible to the server

The standard location for public domain plugins published through NPM is /srv/rwserve-plugins. Nevertheless, private plugin modules can be written, tested, and deployed from any location on the server. Linux guidelines stipulate that websites should put their publicly available documents under the /srv directory, and by convention, the DNS name of the website is used as the first part of the file system path after that. Following this guidance, a good place to place your plugin's JavaScript file, would be in a sub-directory under that DNS name. For example, if your website is www.sockit.com, a plugin named "plug-and-socket" would be put in /srv/www.sockit.com/plugins/plug-and-socket/index.js.

The webmaster declares the main entry point for the plugin in the server configuration file, using the location entity within the plugin's named subsection. Continuing our example, the configuration file would look like this:

server {
plugins {
plug-and-socket {
location `/srv/www.sockit.com/plugins/plug-and-socket/index.js`
config {
database-name plugsdb
authorization-id superplug
password gulprepus
}
}
}
}

Ownership and permissions are important. The file must be readable by the server. Also, remember that the server runs under the system user rwserve, so be sure to change the owner of your plugin files like this:

chown -R rwserve:rwserve /srv/www.sockit.com/plugins/plug-and-socket    
chmod -R 644 /srv/www.sockit.com/plugins/plug-and-socket

Routing requests to your plugin

Once your plugin has been written and made available using the previous instructions, the final step is to choose which requests to send to it. This is the job of the Router module. Routes are declared in the configuration file using a combination of path-patterns plus methods. When an incoming request's path matches a declared path-pattern, and when the same request's method matches one of the declared methods, then the plugin will be called. See the separate note for the router to learn how and where routes are configured.

Review

Key points to remember:

  • Plugins must export a single class using CommonJS module format.
  • Requests are routed to plugins based on path-pattern + method configuration settings.
  • Responses are transmitted to the open stream by the server, using the outgoingPayload, responseHeaders and statusCode provided in the work order by the plugin.

Handling requests with application-specific code