jf-http-headers
Version:
Class for manipulating HTTP headers.
288 lines (280 loc) • 8.29 kB
JavaScript
const headerLine = /^([^:]+)\s*:\s*(.+)\s*$/;
const statusLine = /^HTTP\/(\d+)\.(\d+)\s+(\d+)\s+(.+)$/;
/**
* Encabezados con un tipo específico.
*
* * `array` : Puede tener múltiples valores.
* * `first` : Su valor es único y si se encuentra más de uno, persiste el primero.
*
* Si el encabezado no se encuentra en esta variable, se asume que puede ser
* especificado varias veces y se concatenarán con ',' los valores.
*
* @type {Object}
*
* @see https://dxr.mozilla.org/mozilla/source/netwerk/protocol/http/src/nsHttpHeaderArray.cpp
*/
const types = {
'authorization' : 'first',
'content-length' : 'first',
'content-type' : 'first',
'from' : 'first',
'host' : 'first',
'if-modified-since' : 'first',
'if-unmodified-since' : 'first',
'location' : 'first',
'max-forwards' : 'first',
'proxy-authenticate' : 'array',
'proxy-authorization' : 'first',
'referer' : 'first',
'set-cookie' : 'array',
'user-agent' : 'first',
'www-authenticate' : 'array'
};
/**
* Maneja encabezados HTTP.
*
* @namespace jf
* @class jf.HttpHeaders
*/
module.exports = class jfHttpHeaders {
/**
* Constructor de la clase
*
* @method constructor
*
* @param {Object?} headers Encabezados a usar para inicializar la clase.
* @param {Object?} map Mapa a usar para realizar la conversión de una cabecera
* cuando no se quiera usar el formateo por defecto.
*
* @constructor
*/
constructor(headers = {}, map = {})
{
/**
* Encabezados manejados por la clase.
*
* @property headers
* @type {Object}
*/
this.headers = {};
/**
* Mapa a usar para realizar la conversión de una cabecera cuando no
* se quiera usar el formateo por defecto.
*
* @property map
* @type {Object}
*/
this.map = Object.assign(
{
jf : 'jf',
www : 'WWW'
},
map
);
/**
* Estado de una petición.
* Se asigna usando el método `parseRawHeaders` si en el
* texto pasado viene la primera línea de la respuesta HTTP.
*
* @type {Object}
*/
this.status = {};
//------------------------------------------------------------------------------
this.set(headers);
}
/**
* Iterador que permite usar un bucle for..of
* para iterar sobre los encabezados.
*
* @return {Object}
*/
[Symbol.iterator]()
{
let _current = 0;
let _headers = Object.keys(this.headers).sort();
return {
next()
{
let _value = _headers[_current++];
return {
done : _value === undefined,
value : _value
}
}
}
}
/**
* Elimina un encabezado.
*
* @param {String} name Nombre del encabezado a eliminar.
*/
del(name)
{
delete this.headers[this.constructor.format(name, this.map)];
}
/**
* Devuelve el encabezado solicitado.
*
* @param {String} name Nombre del encabezado a devolver.
*
* @return {String|undefined}
*/
get(name)
{
return this.headers[this.constructor.format(name, this.map)];
}
/**
* Analiza un listado de encabezados.
* Cada elemento de la lista es un texto con toda la línea del encabezado:
*
* ```
* [
* 'Content-Length: 200',
* 'Content-Type: image/gif'
* ]
* ```
*
* @param {String[]} headers Encabezados a analizar.
*/
parse(headers)
{
if (Array.isArray(headers))
{
let _lastHeader = '';
headers.forEach(
header =>
{
if (header[0] === ' ' || header[0] === '\t')
{
if (_lastHeader)
{
this.set(
_lastHeader,
this.get(_lastHeader) + ' ' + header.trim(),
true
);
}
}
else
{
let _parts = header.match(headerLine);
if (_parts)
{
this.set(_lastHeader = _parts[1], _parts[2].trim());
}
else
{
_parts = header.match(statusLine);
if (_parts)
{
this.status = {
code : _parts[3],
text : _parts[4],
version : {
major : _parts[1],
minor : _parts[2]
}
};
}
}
}
}
);
}
}
/**
* Agrega un encabezado a la colección.
*
* @param {String|Object} header Nombre del encabezado o mapa con los encabezados.
* @param {String} value Valor del encabezado.
* @param {Boolean} overwrite Indica si se debe sobrescribir o seguir la recomendación del RFC2068.
*/
set(header, value, overwrite = false)
{
if (header && typeof header === 'object')
{
for (let _header in header)
{
if (header.hasOwnProperty(_header))
{
this.set(_header, header[_header], true);
}
}
}
else
{
const _formatted = this.constructor.format(header, this.map);
const _headers = this.headers;
if (overwrite === true)
{
_headers[_formatted] = value;
}
else if (_formatted in _headers)
{
switch (types[header.toLowerCase()])
{
case 'array':
_headers[_formatted] += '\n' + value;
break;
case 'first':
break;
default:
_headers[_formatted] += ', ' + value;
break
}
}
else
{
_headers[_formatted] = value;
}
}
}
/**
* @override
*/
toString()
{
const _headers = [];
for (let _header of this)
{
_headers.push(`${_header}: ${this.get(_header)}`);
}
return _headers.join('\r\n');
}
/**
* Convierte un encabezado a formato Camel-Case para homogeneizar los encabezados.
*
* ```
* content-length --> Content-Length
* ```
*
* @param {String} name Nombre del encabezado a convertir.
* @param {Object} map Mapa a usar para realizar la conversión de una cabecera
* cuando no se quiera usar el formateo por defecto.
*
* @return {String} Texto convertido.
*/
static format(name, map = { jf : 'jf' })
{
if (name in map)
{
name = map[name];
}
else
{
name = name
.split('-')
.map(
part =>
{
const _part = part.toLowerCase();
return _part in map
? map[_part]
: part[0].toUpperCase() + _part.substr(1);
}
)
.join('-');
}
return name;
}
};