Table of Contents

Middlewares in Chameleon are implemented as Stored-Procedures. They are executed one by one when Chameleon receives a new request. This is done in a stored-procedure named [chameleon].[RunMiddlewares] that in turn is invoke by [chameleon].[Run], the main entry of Chameleon which is called by Chameleon hosts.

Chameleon contains two pipelines: application pipeline and error pipeline. When an error is raised, execution in the application pipeline is aborted and continues in error pipeline. Each pipeline has its own middlewares. Thus, middlewares are divided into two categories: application middlewares, error middlewares.

List of middlewares are as follows:

No matter of their categoris, all of the middlewares follow the same signature. They all have a single int parameter named @context_id and their name MUST end in Middleware keyword.

      CREATE OR ALTER procedure [chameleon].[FooMiddleware]
(
	@context_id	int
)
    

When Chameleon invokes a middleware, it passes context-id of current request to the middleware. It is through this context-id that a middleware can access details of the request in Chameleon tables like chameleon._request, chameleon._response, etc.

Note: In order to learn more about request context-id, click Here.

Application Middlewares

Application middlewares are middlewares that are called when no error is raised by a previous middleware. If a middleware raises an error (intentionally or by mistake), the pipeline is aborted mid-way and the error pipeline continues request processing.

Application middlewares is defined in a global setting named Chameleon.Middlewares.

Default value of this setting is as follows:

      StaticFiles,Awt,CookieAuthentication,SetLanguage,Cors,Routing,LogRequests,CacheGet,Mvc,CacheSet

    

Note 1: Middlewares in this setting are mentioned withought their Middleware suffix.

Note 2: Order of middlewares in the setting matters and specifies their priorities. Chameleon executes middlewares based on their order in this setting.

  • StaticFiles
  • Awt
  • CookieAuthentication
  • SetLanguage
  • Cors
  • Routing
  • LogRequests
  • CacheGet
  • Mvc
  • CacheSet

Note: When changing list of middlewares, it is neccessary to execute a stored-procedure named [chameleon].[CreateRunMiddlewaresSproc] so that the request processing pipeline is updated.

      exec [chameleon].[CreateRunMiddlewaresSproc] @dropExisting = 1

    

Setting 1 value for the @dropExisting parameter drops the middleware pipeline stored-procedure and re-creates it.

StaticFilesMiddleware

This middlware is responsible to serving static files as the name implies. Using the request URL, the middleware looks for a file with the same name in the chameleon.Files and chameleon.Folders. If found, it adds file content to the response (chameleon._response) and ends the pipeline (by setting [end] to 1 (true) in chameleon._response).

[Chameleon.Middlewares.StaticFiles.BufferSize]: int (default = 32768)

This setting controls file-streaming from SQL Server towards Chameleon Host. Its default value is 32768 (32KB). When file-streaming is enabled, a Chameleon Host can directly pipes DataReader it receives after calling chameleon.Run to current HttpContext.Response. This way, file content is written directly from SQL Server to the web response and memory is used more efficiently (since its not needed to load the whole file content into memory and then stream it onto the web response).

[Chameleon.Middlewares.StaticFiles.wwwRootFolderId]: int

This setting specifies what record in chameleon.Folders is assumed as the root of the website whose static files should be served by StaticFilesMiddleware.

In order to learn more about Chameleon file-system click here.

AwtMiddleware

This middleware is an Authentication-Web-Token middleware that processes authentication token sent through request headers and establishes user context if the token is valid and not expired. An AWT token is similar to JWT token and works in a similar way.

Like CookieAuthenticationMiddleware, AwtMiddleware uses authentication setting specified in Chameleon.Authentication item in chameleon.Settings table. Also, it uses [chameleon].[Authentication.Decrypt] stored-procedure to decrypt received AWT token.

If AWT token is valid (not expired), in order to create user context, AwtMiddleware adds the same two user items to request data as CookieAuthenticationMiddleware does i.e. user.identity.username and user.identity.claims.

CookieAuthenticationMiddleware

This middleware is responsible for restoring user context specified in cookies. It looks cookies for an authentication cookie and if found, tries to decrypt and validate it. If the cookie validation step succeeds, it restores user context by inserting appropriate records to chameleon._requestData.

CookieAuthenticationMiddleware first loads cookie authentication settings - like its name, expire time in minutes) from Chameleon.Authentication setting. Then decrypts authentication cookie using [chameleon].[Authentication.Decrypt] stored-procedure. Then checks whether authentication cookie is not expired by checking its createdAt using expireMinutes setting. If cookie is still valid (is not expired), it restores user context by adding the following two values into request data table (chameleon._requestData):

  • user.identity.username: username specified in received authentication cookie
  • user.identity.claims: user claims found in received authentication cookie.

[Chameleon.Authentication]: JSON

Chameleons authentication settings and cookie authentication settings are stored in this key in chameleon.Settings table.

Example:

      {
	"expireMinutes": 10080,
	"cookie": {
		"name": "_chmAuth",
		"domain": "",
		"path": "/",
		"sameSite": "Lax",
		"secure": true,
		"httpOnly": true,
		"overwriteExisting": true
	},
	"encryption": {
		"algorithm": "AES",
		"key": "**********",
		"iv": "**********"
	},
	"hash": {
		"algorithm": "HMACSHA256",
		"key": "********"
	}
}

    

Cookie settings are described in the following table:

Property Description
name Cookie authentication name. Default is _chmAuth
domain Cookie Domain. Default is ''.
path Cookie path. Default is '/'
sameSite Cookie sameSite policy. Default is Lax
secure Cookie SSL security. Default is true
httpOnly Whether cookie is http-only or not. Default is true
overwriteExisting` Rewrite existing cookie or not

Other authentication settings:

Property Description
expireMinutes How long authentication cookie is assumed valid (in minutes). Default is 10080 minutes (7 days/one week).

Encryption settings (encryption property):

Property Description
algorithm Encryption algorithm. Default is AES.
key Encryption algorithm key.
iv Encryption algorithm iv.

Note 1: Authentication cookie's encryption/decryption is carried out by [chameleon].[Authentication.Authenticate] and [chameleon].[Authentication.Decrypt] stored-procedures respectively.

Note 2: In essence, the main encryption setting is just algorithm and other encryption settings (like key and iv) depend on algorithm i.e. one algorithm might require separate properties than another algorithm. Currently, Chameleon supports just AES algorithm in its authentication inrastructure.

CorsMiddleware

This middleware adds appropriate CORS headers to response (chameleon._responseHeaders table) if there is an Origin request header that conforms to CORS settings.

[Chameleon.Middlewares.Cors]: JSON Array

In this setting any client website that intends to be able to send requests to current application is defined. Each client is defined using its domain (website address that is sent through origin request header).

Property Description
origin Client website address. * means all clients.
methods List of HTTP methods that a client is allowed to send his request with. * means all HTTP methods.
headers List of headers that is allowed to be sent to clients in the response. * means all headers.
credentials Whether or not cookies should be sent to a client or not.

Example: The following settings enables CORS for all origins (any client).

      [{ "origin": "*" }]
    

CORS Response headers written onto the response are as follows:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Credentials

SetLanguageMiddleware

This middleware adds language specified in current request to request data (chameleon._requestData table). There are 3 sources for specifying language for a request ordered by priority:

  • A querystring parameter named lang
  • A form parameter named lang
  • A route item named lang

RoutingMiddleware

This middleware checks whether current request matches one of the routes defined in chameleon.Routes table. If it finds a match, it adds routes' parameters to chameleon._routeValues table. For example for a request like /product/edit/12 the following route values are inserted to chameleon._routeValues table.

name value
controller product
action edit
id 123
$route_id id of matched route in chameleon.Routes table

It also adds the following item into request data (chameleon._requestData):

name value
$route_sproc name of stored-procedure that maps to current route and will later be invoked by MvcMiddleware.

For example, for the /product/edit/123, name of the stored-procedure added to request data (chameleon._requestData) will be dbo.USP_Product_Edit.

RoutingMiddleware carries out its job using chm.FindRoute CLR procedure. It simply calls chm.FindRoute passing it current context-id, request path and method and then inserting returning route items (created by chm.FindRoute) into chameleon._routeValues table.

      insert into @routeValues(name, value, is_default)
exec chm.FindRoute @context_id, @path, @HttpMethod, 0, @sproc out

    

After routing, any later middleware in the pipeline can read route values from chameleon._routeValues table by current context-id.

Note: In order to learn more about Routing click Here.

MvcMiddleware

This middleware uses routing data provided by RoutingMiddleware and calls the stored-procedure whose name extracted by RoutingMiddleware. If it cannot find the stored-procedure, it adds two error items as below to chameleon._errors table so that the error is later handled by an error middleware.

  • error_procedure: name of procedure that reported the error (MvcMiddleware)
  • error_message: error message (like SPROC not found; Route has no SPROC)

In order to learn more about MVC and controller/action execution in Chameleon click here.

CacheGetMiddleware

This middleware checks whether an entry in the Chameleon cache (chameleon.Cache table) exists for current GET request. If it finds an entry and if the cached item is not expired, it uses content and headers specified in the cache item for the response of current request and ends pipeline.

Like CacheSetMiddleware, CacheGetMiddleware uses [chameleon].[Cache.GetKey] UDF for the key of cached entry.

Notes:

  • CacheGetMiddleware is used in conjunction with CacheSetMiddleware.
  • When CacheGetMiddleware ends pipeline because of a cache hit, it adds the following header to the list of response headers to let the client know that the response content is resovled from cache:

x-chameleon-source: cache

CacheSetMiddleware

This middleware caches response content and headers provided by previous middlewares in Chameleon Cache (chameleon.Cache table). Upon execution, it checks whether request data (chameleon._requestData table) contains an item named cache. The item is expcted to be a JSON string specifying caching details like its duration. If so, CacheSetMiddleware adds a new entry (or updates existing entry) to chameleon.Cache table. For the value of the new entry, it uses [text] column in chameleon._response table.

For the key of the new cache entry, CacheSetMiddleware uses [chameleon].[Cache.GetKey] UDF. This UDF returns a base64 string from querystring and routing data. To leanr more about UDF click here.

For the content of the cache entry, CacheSetMiddleware creates a JSON string in the format below:

      {
	"content": "...",
	"headers": [
		{ "key": "Content-Type", "value": "application/html" },
		{ "key": "Content-Length", "value": "1320" },
		...
	]
}
    

Notes:

  • CacheSetMiddleware is used in conjunction with CacheGetMiddleware.
  • CacheSetMiddleware should be set after middlewares that provide response content (like MvcMiddleware).
  • CacheSetMiddleware just works with textual response. It just caches [text] column of chameleon._response, not the [body].
  • CacheSetMiddleware caches response only if the request is sent using a GET http method.

LogRequestsMiddleware

This middleware is able to log requests in a table named [chameleon].[RequestLogs]. This could be useful when debgging. Strcuture of [chameleon].[RequestLogs] is as follows:

Column Type Descrption
Id int PK, IDENTITY
context_id int context-id
requestDate datetime log date/time
IP varchar(50) Client IP address
url nvarchar(1000) URL
method varchar(20) Http Method
form nvarchar(max) Form parameters (in POST requests)
body nvarchar(max) Request Body
headers nvarchar(1000) Request Headers
cookies nvarchar(max) Request Cookies

[Chameleon.Middlewares.LogRequests]: boolean (default = false)

This setting enables/disables LogRequestsMiddleware.

[Chameleon.Middlewares.LogRequests.MaxLog]: int (default = 1000)

This setting determines maximum number of logs that could be kept in the request log table. If logs exceeds this limit, the log table will be truncated (previous logs will be purged).

[Chameleon.Middlewares.LogRequests.ActiveMaxLog]: boolean (default = true)

This setting enables/disables active max log checking. If it is true (default), LogRequestsMiddleware actively checks number of logs in request log table each time it intends to add a new log entry. If it is false, requests will be logged without any limit (no log will be removed even if the number of logs exceeds MaxLog).

Error Middlewares

Error middlewares are only executed when an error is raised inside aplication pipeline. In that case, application pipeline is aborted and execution continues in error pipeline.

Error middlewares are special middlewares whose purpose revolves around errors, i.e. their main job usually is logging errors or providing custom error pages if no response is already provided for current request by previous middlewares.

Like Chameleon.Middlewares setting that list of application middlewares are specified in it, there is a global setting named Chamaleon.Middlewares.ErrorMiddlewares that specifies list of error middlewares. Its default value when Chameleon is installed is as follows:

      LogErrors,CustomErrors

    

As it was described in application middlewares section, list of error middlewares could be customized too. When mentioning name of an error middleware, the Middleware suffix must be ommitted. Also, the middlewares are executed in order.

Chameleon comes with two error middlewares named LogErrors and CustomErrors. In this section, these two middlewares are described.

LogErrorsMiddleware

This middleware logs any errors that is raised not-intentionally or errors that a middleware might have added to chameleon._errors table due to any logical business checking it performed. So, the errors are divded into two categories: unhandled SQL errors (for example selecting a table or column that does not exsit), and manual errors (logical errors that other middlewares may report).

The central location where errors are stored so that they could be processed by an error middleware later is a table named chameleon._errors which was explained earlier in Tables section.

As it was explained in chameleon._errors section, Chameleon adds details of the error into chameleon._errors table.

LogErrorsMiddleware basically inserts error details into a log table named chameleon.ErrorLogs. Structure of this table is as below:

Column Type Description
id int PK (identity)
context_id int request context id
LogDate datetime date/time of error
Number int error number
Description nvarchar(2000) error description or message.
Line int line number where error is raised.
State int error state
Severity int error severity
Procedure nvarchar(200) Name of procedure in which error was raised.
Data nvarchar(max) Optional data related to the raised error in JSON format. This includes any record exists in chameleon._errors table for current context whose key is not equal to error_line, error_message, error_state, error_number, error_procedure, error_severity.

[Chameleon.Middlewares.LogErrors]: boolean (default = true)

This boolean setting enales/disables logging errors. Default is true.

[Chameleon.Middlewares.LogErrors.MaxLog]: int (default = 10000)

Maximum number of log entries that should be kept in ErrorLogs table when ActiveMaxLog is on (enabled). If ActiveMaxLog is on (default), each time LogErrorsMiddleware decides to log an error, it checks the number of logs in ErrorLogs table. if it exceeds MaxLog, ErrorMiddleware purges older logs first (truncates ErrorLogs table) and then inserts current log.

[Chameleon.Middlewares.LogErrors.ActiveMaxLog]: boolean (default = true)

This boolean setting enales/disables active max logging. When ActiveMaxLog is on (default), the number of log entries in ErrorLogs is limited to a number specified in MaxLog setting. Otherwise (ActiveMaxLog is off), there will not be any limit to the number of entries in ErrorLogs table.

Note: When ActiveMaxLog is on, it imposes a performance hit on the database since each time an error is going to be logged, it should count the number of older logs in ErrorLogs table first and check if the number exceeds MaxLog or not (to purge them if so).

Thus, it is more efficient to use a scheduled Job to purge older logs on an hourly, daily or weekly basis or whatever basis that is desired. This way ErrorLogsMiddleware does not need to get involved with distinguishing whether logs should be purged or not.

ActiveMaxLog setting is a simple option to dynamically purge logs when number of log entries exceeds a limit (maxLog). When using a SQL Job, we can safely turn ActiveMaxLog off by setting it to false or 0. The default setting for ActiveMaxLog is true (it is on), since we don't know whether a purge log job exists or not.

CustomErrorsMiddleware

The main purpose of this middleware is providing custom error pages when no response is provided for current request. CustomErrorsMiddleware looks for a key in the following format in chameleon.Settings table to know how it should provide custom error page.

Chameleon.Middlewares.CustomErrors.{HttpStatusCode}

For example, if http response status is 404, CustomErrorsMiddleware checks a Chameleon.Middlewares.CustomErrors.404 key in chameleon.Settings table.

There are four types of custom errors, each one is distinguished by its first character:

Type Start Char Description Example
File / Relative path to a static file inside Chameleon's File-System. e.g. /errors/404.html /docs/error/404.html /
View ~ name of a view in chameleon.Views table. e.g. ~404, ~500. If a view is specified for an error, CustomErrorMiddleware returns that as the response of current request by calling chm.ViewResult SPROC. ~404
Redirect * Absolute or relative URL to which the user should be redirected. */page-not-found
Content Any other character Custom plain content. <p>404 Error! Page Not Found</p>

Note: When using static files with relative paths as custom error pages, the root directory is get from the following setting:

Chameleon.Middlewares.StaticFiles.wwwRootFolderId

Chameleon comes with four default custom error views for the most used http response status codes, i.e. 404, 401, 403 and 500.

Custom Middleware

Definition

Chameleon enables developers to write their own custom middlewares and plug it into Chameleon request pipeline.

Declaration

A Chameleon middleware is nothing but a stored-procedure with a single int parameter named @context_id. Also, its name MUST end in Middleware.

      CREATE OR ALTER procedure [chameleon].[MyMiddleware]
(
	@context_id	int
)

    

When a middleware is called, Chameleon passes current request's context-id to each middleware. Using the received context-id, the middleware will be able to access any context tables it desires (request, response, routing, etc.). The develoepr is free to do whatever logic he wants inside his middleware SPROC. Middlewares can interact each other using chameleon._requestData table as well.

Setup and order

Application middlewares and their order is defined in a global setting named Chameleon.Middlewares. By default, when Chameleon is installed, this setting is set with the following value:

      StaticFiles,Awt,CookieAuthentication,SetLanguage,Cors,Routing,LogRequests,CacheGet,Mvc,CacheSet

    

Error middlewares are defined in another global setting named Chamaleon.Middlewares.ErrorMiddlewares. Its default values is:

      LogErrors,CustomErrors

    

Chameleon should normally read these two settings, split middlewares lists and invoke them one by one. This is the normal way of dealing with middlewares' settings. However, ton enhance performance, Chameleon solidifies its pipeline. This is explained in the next section.

Updating pipeline

Chameleon does not read middleware lists' settings, splits them and invoke middlewares on a per request basis. Instead, it statically embeds middlewares' invokation in its chameleon.RunMiddlewares stored procedure. The sproc's duty is invoking middlewares as the name implies.

The middlewares solidification or in simpler terms, creating chameleon.RunMiddlewares stored-procedure with middlewares invocation hard-coded inside it, is a manual job and should performed by developer.

Chameleon provides a CreateRunMiddlewaresSproc stored procedure and does this job. It has a single bit parameter named @dropExisting that tells CreateRunMiddlewaresSproc whether to drop chameleon.RunMiddlewares stored-procedure if it already exists.

      CREATE OR ALTER PROCEDURE [chameleon].[CreateRunMiddlewaresSproc]
(
	@dropExisting bit
)

    

After adding the custom middleware inside Chameleon.Middlewares setting, we should manually call [chameleon].[CreateRunMiddlewaresSproc] with a 1 argument, so that Chameleon's middleware invokation system is updated.

      exec [chameleon].[CreateRunMiddlewaresSproc] 1