lucinda / headers
API encapsulating HTTP request and response headers, useful also for cache/cors validation
Requires
- php: ^8.1
- ext-simplexml: *
Requires (Dev)
- lucinda/unit-testing: ^2.0
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.
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:
- route: (mandatory) holds policies about a specific route
<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: validating Request headers based on Policy in order to communicate with client browser cache and set Response headers in accordance to
- CORS validation: validating Request headers based on Policy in order to answer a CORS request and set Response headers in accordance to CORS protocol specifications
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:
- simple requests, to understand how can a preflight request be avoided
- preflighted requests, to understand what triggers a preflight request and what happens next
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:
- test.php: runs unit tests in console
- unit-tests.xml: sets up unit tests and mocks "loggers" tag
- tests: unit tests for classes from src folder
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