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-04-12 14:28:34 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:

Class Prototype Ability Gained
Cacheable (mandatory) HTTP caching by etag or unix time representations

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:

Method Arguments Returns Description
__construct \SimpleXMLElement $xml, string $requestedPage, array $requestHeaders void Creates Policy based on XML and requested page, sets up Request object based on request headers and initializes Response object
getRequest void Request Gets object encapsulating HTTP request headers received
validateCache Cacheable $cacheable, string $requestMethod int Performs HTTP cache validation based on user-defined Cacheable representation of requested resource
validateCORS string $origin = null void Performs CORS request validation based on user-defined origin (PROTOCOL://HOSTNAME, eg: https://www.google.com). If none provided, Access-Control-Allow-Origin will equal "*" (all origins supported)!
getResponse void Response Gets object encapsulating HTTP response headers to send back

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:

Header Method
Cache-Control setCacheControl
ETag setEtag
Last-Modified setLastModifiedTime

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

Header Method
Cache-Control getCacheControl
If-Match getIfMatch
If-None-Match getIfNoneMatch
If-Modified-Since getIfModifiedSince
If-Unmodified-Since getIfUnmodifiedSince

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:

Header Method
Access-Control-Allow-Credentials setAccessControlAllowCredentials
Access-Control-Allow-Headers addAccessControlAllowHeader
Access-Control-Allow-Methods addAccessControlAllowMethod
Access-Control-Allow-Origin setAccessControlAllowOrigin
Access-Control-Expose-Headers addAccessControlExposeHeaders
Access-Control-Max-Age setAccessControlMaxAge

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

Header Method
Access-Control-Request-Headers getAccessControlRequestHeaders
Access-Control-Request-Method getAccessControlRequestMethod
Origin getOrigin

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:

Method Arguments Returns Description Header
__construct array $headers void Reads headers received from client -
getAccept() void array Gets content types accepted by client Accept
getAcceptCharset() void array Gets charsets accepted by client Accept-Charset
getAcceptEncoding() void array Gets encodings accepted by client Accept-Encoding
getAcceptLanguage() void array Gets languages accepted by client Accept-Language
getTE() void array Gets transfer encodings accepted by client TE
getAuthorization() void ?Request\Authorization Gets credentials for user authentication Authorization
getCacheControl() void ?Request\CacheControl Gets HTTP caching settings requested by client Cache-Control
getDNT() void bool Gets whether or not client does not want to be tracked DNT
getDate() void ?int Gets UNIX timestamp of date request came with Date
getExpect() void bool Gets whether client is about to send a large request Expect
getSaveData() void bool ... Save-Data
getForwardedIP() void ?string Gets origin IP that got forwarded by proxy X-Forwarded-For
getForwardedProxy() void ?string Gets proxy IP that forwarded client, if present X-Forwarded-For
getForwardedHost() void ?string Gets origin Host that got forwarded by proxy X-Forwarded-Host
getForwardedProtocol() void ?string Gets origin protocol that got forwarded by proxy X-Forwarded-Proto
getFrom() void ?string Gets email address of client From
getHost() void ?string Gets hostname requested by client Host
getIfRangeDate() void ?int Gets UNIX timestamp of range condition, if present If-Range
getIfRangeEtag() void ?string Gets ETag of range condition, if present If-Range
getRange() void ?Request\Range Gets bytes range requested by client from a big document Range
getReferer() void ?string Gets address of the previous web page from which a link to the currently requested page was followed Referer
getUserAgent() void ?string Gets signature of client browser User-Agent
getWantDigest() void array Gets details of digest client wants in response Want-Digest
getIfMatch() void ?string Gets ETag that must condition response to be sent only if matches that of requested resource If-Match
getIfNoneMatch() void ?string Gets ETag that must condition response to be sent only if it does not match that of requested resource If-None-Match
getIfModifiedSince() void ?int Gets UNIX timestamp that must condition response to be sent only if matches that of requested resource If-Modified-Since
getIfUnmodifiedSince() void ?int Gets UNIX timestamp that must condition response to be sent only if it does not match that of requested resource If-Unmodified-Since
getAccessControlRequestHeaders() void array Gets headers that will be requested later as part of a CORS preliminary request Access-Control-Request-Headers
getAccessControlRequestMethod() void ?string Gets HTTP method that will be used later as part of a CORS preliminary request Access-Control-Request-Method
getOrigin() void ?string Gets client hostname to validate access to requested resource, sent automatically in CORS preliminary request Origin
getCustomHeaders() void array Gets all non-standard headers requested by client as header name:value array (any)

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:

Method Arguments Returns Description Header
addAcceptPatch string $mimeType, string $charset=null void Adds a content type for whom PATCH requests are accepted Accept-Patch
setAcceptRanges bool $value void Sets whether or not range requests are accepted Accept-Ranges
addAllow string $requestMethod void Sets a request method server accepts for requested resource Allow
addClearSiteData string $directive = "*" void Sets a browsing data (cookies, storage, cache) to be cleared on client Clear-Site-Data
setCacheControl void Response\CacheControl Sets HTTP caching settings to be used by client Cache-Control
setContentDisposition string $type Response\ContentDisposition Sets how content will be displayed (inline or attachment) Content-Disposition
addContentEncoding string $contentEncoding void Adds an encoding applied in compressing response Content-Encoding
addContentLanguage string $language void Adds a language to associate response with Content-Language
setContentLength int $length void Sets byte length of response Content-Length
setContentLocation string $url void Sets alternate uri for the returned data Content-Location
setContentRange string $unit = "bytes", int $start = null, int $end = null, int $size = null void Sets returning document range Content-Range
setContentType string $mimeType, string $charset = null void Sets content type of response Content-Type
setContentTypeOptions void void Indicates that content types should not be changed or followed (anti-sniffing solution) X-Content-Type-Options
setCrossOriginResourcePolicy string $option void Sets policy to block no-cors cross-origin/cross-site requests to the given resource Cross-Origin-Resource-Policy
addDigest string $algorithm, string $value void Adds a digest to response Digest
setEtag string $value void Sets ETag to associate response to requested resource with ETag
setExpirationTime int $unixTime void Sets UNIX time by which response to be cached by client browser should require revalidation (deprecated) Expires
setLastModifiedTime int $unixTime void Sets UNIX time requested resource was last modified, to associate response with Last-Modified
setLocation string $url void Sets url client should redirect to. Location
setReferrerPolicy string $option void Sets much Referer information should be included with requests. Referrer-Policy
setRentryAfterDate int $unixTime void Sets UNIX time client should wait before making a follow-up request Rentry-After
setRentryAfterDelay int $delay void Sets seconds client should wait before making a follow-up request Rentry-After
setSourceMap string $url void Links response to a source map enabling the browser to present the reconstructed original in the debugger. SourceMap
setStrictTransportSecurity void Response\StrictTransportSecurity Informs client that current website only accepts HTTPS Strict-Transport-Security
addTimingAllowOrigin string $url = "*" void Adds an origins allowed to see values from Resource Timing API Timing-Allow-Origin
setTk string $status void Sets tracking status that applied to the corresponding request Tk
setTrailer string $headerNames void Allows the sender to include additional fields at the end of chunked messages Trailer
addTransferEncoding string $contentEncoding void Adds form of encoding used to safely transfer the payload body to the user. Transfer-Encoding
addVary string $headerName = "*" void Adds a request header to decide in future whether a cached response can be used Vary
setWWWAuthenticate string $type, string $realm="" Response\WwwAuthenticate Defines the authentication method that should be used to gain access to a resource WWW-Authenticate
setDNSPrefetchControl bool $value = true void Activates DNS prefetching on client X-DNS-Prefetch-Control
setFrameOptions string $option void Indicates whether or not a browser should be allowed to render a page in a frame / iframe / embed / object X-Frame-Options
setAccessControlAllowCredentials void void Answers to CORS request by signaling credentials are to be exposed Access-Control-Allow-Credentials
addAccessControlAllowHeader string $headerName void Adds allowed request header to answer a CORS request Access-Control-Allow-Headers
addAccessControlAllowMethod string $requestMethod void Adds allowed request method to answer a CORS request Access-Control-Allow-Methods
setAccessControlAllowOrigin string $origin = "*" void Sets allowed origin to answer a CORS request Access-Control-Allow-Origin
addAccessControlExposeHeader string $headerName = "*" void Adds response header client should expose to answer a CORS request Access-Control-Expose-Headers
setAccessControlMaxAge int $duration void Sets how long response to a CORS request should be cached (in seconds) Access-Control-Max-Age
setCustomHeader string $name, string $value void Sets a custom header by name and value (this may trigger CORS requests) (any)
toArray void array Converts all headers set to a name:value array ready to be sent back to client -

Following limitations apply:

  • multiple ETags are not supported

Interface Cacheable

Class Cacheable defines blueprints for cache validation via methods:

Method Arguments Returns Description
getEtag void string Gets string representation of resource to be cached
getTime void int Gets unix time representation of resource to be cached

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