lucinda/headers

API encapsulating HTTP request and response headers, useful also for cache/cors validation

v2.0.2 2022-06-12 10:11 UTC

This package is auto-updated.

Last update: 2024-11-12 15:59:03 UTC


README

Table of contents:

About

This API encapsulates HTTP request headers received from client and response headers to send back, offering an ability to bind them for cache and CORS validation.

diagram

That task can be achieved using following steps:

  • configuration: setting up an XML file where cache/CORS validation policies are configured
  • binding points: binding user-defined components defined in XML/code to API prototypes in order to gain necessary abilities
  • initialization: using Wrapper to read above XML into a Policy, read HTTP request headers into a Request then initialize Response, encapsulating HTTP response headers logic.
  • validation: using above to perform cache/CORS validation and set Response accordingly
  • display: sending back response to caller using Response headers compiled above (or set individually by user)

API is fully PSR-4 compliant, only requiring PHP8.1+ interpreter and SimpleXML extension. To quickly see how it works, check:

  • installation: describes how to install API on your computer, in light of steps above
  • unit tests: API has 100% Unit Test coverage, using UnitTest API instead of PHPUnit for greater flexibility
  • examples: shows a deep example of API functionality based on unit tests

All classes inside belong to Lucinda\Headers namespace!

Configuration

To configure this API you must have a XML with following tags inside:

  • headers: (mandatory) configures the api globally
  • routes: (optional) configures API based on route requested, required for CORS requests validation

Headers

Maximal syntax of this tag is:

<headers no_cache="..." cache_expiration="..." allow_credentials="..." cors_max_age="..." allowed_request_headers="..." allowed_response_headers="..."/>

Where:

  • headers: (mandatory) holds global header validation policies
    • no_cache: (optional) disables HTTP caching for all pages in site (can be 0 or 1; 0 is default), unless specifically activated in route matching page requested
    • cache_expiration: (optional) duration in seconds all page responses in site will be cached without revalidation (must be a positive number)
    • allow_credentials: (optional) whether or not credentials are allowed in CORS requests (can be 0 or 1; 0 is default)
    • cors_max_age: (optional) duration in seconds CORS responses will be cached (must be a positive number)
    • allowed_request_headers: (~optional) list of non-standard request headers your site support separated by commas. If none are provided and a CORS Access-Control-Request-Headers is requested, headers listed there are assumed as supported!
    • allowed_response_headers: (optional) list of response headers to expose separated by commas

Example:

<headers no_cache="1" cache_expiration="10" allow_credentials="1" cors_max_age="5" allowed_request_headers="X-Custom-Header, Upgrade-Insecure-Requests" allowed_response_headers="Content-Length, X-Kuma-Revision"/>

Routes

Minimal syntax of this tag is:

<routes>
    <route id="..." no_cache="..." cache_expiration="..." allowed_methods="..."/>
    ...
</routes>

Where:

  • routes: (mandatory) holds list of site routes, each identified by a route tag
    • route: (mandatory) holds policies about a specific route
      • id: (mandatory) page relative url (eg: administration)
      • no_cache: (optional) disables HTTP caching for respective route (can be 0 or 1; 0 is default)
      • cache_expiration: (optional) duration in seconds respective route responses in site will be cached without revalidation (must be a positive number)
      • allowed_methods: (optional) list of HTTP request methods supported by respective route. If none are provided and a CORS Access-Control-Request-Method is requested, that method is assumed as supported! Example:
<routes>
    <route id="index" no_cache="0" cache_expiration="10" allowed_methods="GET"/>
    <route id="login" no_cache="1" allowed_methods="GET,POST"/>    
</routes>

Binding Points

In order to remain flexible and achieve highest performance, API takes no more assumptions than those absolutely required! It offers developers instead an ability to bind programmatically to its prototypes via validateCache method of Wrapper:

Execution

Initialization

Now that policies have been configured, they can be bound to request and response using Wrapper, which creates then works with three objects:

  • Policy: encapsulates validation policies detected from XML
  • Request: encapsulates HTTP request headers received from client in accordance to RFC-7231 specification
  • Response: encapsulates HTTP response headers to send back to client in accordance to RFC-7231 specification

Once set, Policy and Request become immutable (since the past cannot be changed). Policy will only be used internally while Request will only expose getters. Response, on the other hand, is only instanced while setting remains in developer's responsibility. This is because there is no default linking between request and response headers, unless you are performing validation. In light of above, public methods defined by Wrapper are:

Obviously, developers need to know headers received from client and set headers to send back in response, but the way they link depends on your application. There are some particular cases, however, in which request and response headers (and HTTP status) are bound logically:

Cache Validation

The purpose of cache validation is to communicate with client browser's cache based on headers and make your site display instantly whenever possible. The language of communication is identified by RFC-7232 and RFC-7234 specifications both your site (via this API) and your browser must obey.

How Cache Validation Works

The way it works is too complex to be written here, so what follows next only covers the typical use case. HTTP standard allows you following simple method of communication based on conditional headers:

  • client-server: give me X page @ your site
  • server-client: here is my response to page X using 200 OK status header, identified uniquely by value of ETag response header (or last modified at GMT date defined by value of Last-Modified response header)
  • client: ok, I've received response and saved to my cache and link request with ETag (or Last-Modified).
  • ...(some time passes)...
  • client-server: give me X page @ your site once again. I'll also send formerly received ETag/Last-Modified that matched it as If-None-Match/If-Modified-Since request headers, so that you check if page has changed or not!
  • server-client: response remained the same, so I'll save your bandwidth and only answer with a 304 Not Modified status header and no response body
  • client: ok, so I'll display page from my browser's cache
  • ...(some time passes)...
  • client-server: give me X page @ your site once again plus same request headers
  • server-client: response has changed, so this time I'll answer with a 200 OK status header, full response body, along with the new ETag (or Last-Modified
  • client: ok, I've received response and saved to my cache and link request with the new ETag (or Last-Modified).

Above method has a disadvantage by assuming cache to be stale, thus requiring a server roundtrip to check if requested resource has changed (revalidation). HTTP standard thus comes with an alternate fastest solution that uses Cache-Control response header:

  • client-server: give me X page @ your site
  • server-client: here is my response to page X and assume it as fresh for X seconds defined by max-age directive of Cache-Control
  • client: ok, I've received response and saved to my cache. On future requests to same page, I won't ask server unless X seconds have passed and display response from cache!

The two methods of communication described above are not mutually exclusive. Mature applications use both, with different policies based on page requested: some pages can be assumed to be stale by default, others allow some freshness and finally a few may not even be compatible with caching because output changes on every request

How Is Cache Validation Implemented

To set cache-related response headers, using following Response methods:

To read cache-related request headers, using following Request methods:

Fortunately, all of this is done automatically by API once you are running validateCache method of Wrapper object. This method:

  • configures Cache-Control response header based on namesake request header and XML settings encapsulated by Policy
  • sets ETag response header based on Cacheable representation of requested resource, if exists
  • sets Last-Modified response header based on Cacheable representation of requested resource, if exists
  • reads cache-related request headers, matches them with Cacheable representations and returns HTTP status code according to RFC specifications

So from developers' perspective you only need to:

  • (optional) set up no_cache and cache_expiration XML tag attributes according to configuration stage
  • (mandatory) implement a Cacheable representation of requested resource (the way it can be converted to an etag or last time it was modified)
  • (mandatory) use http status code returned by validateCache when response is rendered

Possible http status codes returned are:

  • 200: this means response is ok and must come along with a body
  • 304: this means response has not modified and must come without a body
  • 412: this means a conditional header has failed, thus application should exit with an error

CORS Validation

CORS preliminary request is triggered automatically by client browser when it encounteres a situation where preflight is mandated by security reasons.

How CORS Validation Works

What triggers a preflight request falls outside the scope of this API documentation. If you want to learn more, check:

How Is CORS Validation Implemented

To set CORS-related response headers, using following Response methods:

To read CORS-related request headers, using following Request methods:

Fortunately, all of this is done automatically by API once you are running validateCORS method of Wrapper object. This method:

  • requires developers to put origin hostname (eg: https://www.google.com) as argument. This cannot be set in XML since it may differ by development environment! If none is provided, any Origin is considered valid!
  • sets CORS response headers based on allow_credentials, cors_max_age, allowed_request_headers, allowed_response_headers XML attributes encapsulated by Policy and CORS request headers received

Display

Once response headers, status and body (if any) become available, you are finally able to send headers back to client. Example:

$wrapper = new Lucinda\Headers\Wrapper("configuration.xml", $_SERVER["REQUEST_URI"], getallheaders());
// developer now reads request headers, sets response headers, compiles $responseBody then applies cache validation:
$httpStatus = $wrapper->validateCache(new MyCacheable($responseBody), $_SERVER["REQUEST_METHOD"]);
// now response is ready for display
http_response_code(httpStatus);
$headers = $wrapper->getResponse()->toArray();
foreach ($headers as $name=>$value) {
    header($name.": ".$value);
}
if ($httpStatus!=304) {
    echo $responseBody;
}

Installation

First choose a folder, associate it to a domain then write this command there using console:

composer require lucinda/headers

Then create a configuration.xml file holding configuration settings (see configuration above) and a index.php file (see initialization and display) in project root with following code:

$wrapper = new Lucinda\Headers\Wrapper("configuration.xml", $_SERVER["REQUEST_URI"], getallheaders());
// if request is CORS, response can be done immediately
if ($_SERVER["REQUEST_METHOD"]=="OPTIONS") {
    $wrapper->validateCORS((!empty($_SERVER['HTTPS'])?"https":"http")."://".$_SERVER["SERVER_NAME"]);
    $headers = $wrapper->getResponse()->toArray();
    foreach ($headers as $name=>$value) {
        header($name.": ".$value);
    }
    exit();
}
// developer reads request headers, sets response headers, compiles $responseBody
// developer creates a Cacheable instance (MyCacheable), able to convert $responseBody into an ETag string, then performs cache validation
$httpStatus = $wrapper->validateCache(new MyCacheable($responseBody), $_SERVER["REQUEST_METHOD"]);
// now response is ready for display
http_response_code(httpStatus);
$headers = $wrapper->getResponse()->toArray();
foreach ($headers as $name=>$value) {
    header($name.": ".$value);
}
if ($httpStatus!=304) {
    echo $responseBody;
}

Then make sure domain is available to world-wide-web and all request that point to it are rerouted to index.php:

RewriteEngine on
RewriteRule ^(.*)$ index.php

Unit Tests

For tests and examples, check following files/folders in API sources:

Examples

To see examples how request headers are parsed by Request, check its matching UnitTest. To see how response headers are set by Response, check its matching UnitTest. To see detailed examples of each headers and understand them in greatest detail, there is no better documentation than the one provided by Mozilla!

Reference Guide

Class Request

Class Request encapsulates HTTP request headers received from client. Each method inside (minus __construct) corresponds to a header:

Following limitations apply:

  • multiple ETags are not supported

Class Response

Class Response encapsulates HTTP response headers to send back. Each method inside (minus toArray) corresponds to a header:

Following limitations apply:

  • multiple ETags are not supported

Interface Cacheable

Class Cacheable defines blueprints for cache validation via methods:

Usage example:

https://github.com/aherne/lucinda-framework-engine/blob/master/src/AbstractCacheable.php https://github.com/aherne/lucinda-framework/blob/master/src/Cacheables/Etag.php