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
Chameleon
s 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
).
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.
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