@azure/functions
Version:
Microsoft Azure Functions NodeJS Framework
99 lines (87 loc) • 3.92 kB
text/typescript
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.
import { HeaderName } from '../constants';
import { AzFuncSystemError } from '../errors';
const space = ' ';
// See "LEXICAL TOKENS" section for definition of ctl chars and quoted string: https://www.w3.org/Protocols/rfc822/3_Lexical.html
const ctlChars = '\\u0000-\\u001F\\u007F';
const quotedString = '(?:[^"\\\\]|\\\\.)*';
// General description of content type header, including defintion of tspecials and token https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
const tspecials = '\\(\\)<>@,;:\\\\"\\/\\[\\]\\?\\.=';
const token = `[^${space}${ctlChars}${tspecials}]+`;
const start = '^\\s*'; // allows leading whitespace
const end = '\\s*(.*)$'; // gets the rest of the string except leading whitespace
const semicolonEnd = `\\s*;?${end}`; // allows optional semicolon and otherwise gets the rest of the string
/**
* @param data a full header, e.g. "Content-Type: text/html; charset=UTF-8"
* @param headerName the header name, e.g. "Content-Type"
* @returns the header value, e.g. "text/html; charset=UTF-8" or null if not found
*/
export function getHeaderValue(data: string, headerName: string): string | null {
const match = new RegExp(`${start}${headerName}\\s*:${end}`, 'i').exec(data);
if (match) {
return match[1].trim();
}
return null;
}
/**
* @param data a content type, e.g. "text/html; charset=UTF-8"
* @returns an array containing the media type (e.g. text/html) and an object with the parameters
*/
export function parseContentType(data: string): [string, HeaderParams] {
const match = new RegExp(`${start}(${token}\\/${token})${semicolonEnd}`, 'i').exec(data);
if (!match) {
throw new AzFuncSystemError(`${HeaderName.contentType} must begin with format "type/subtype".`);
} else {
return [match[1], parseHeaderParams(match[2])];
}
}
/**
* @param data a content disposition, e.g. "form-data; name=myfile; filename=test.txt"
* @returns an array containing the disposition (e.g. form-data) and an object with the parameters
*/
export function parseContentDisposition(data: string): [string, HeaderParams] {
const match = new RegExp(`${start}(${token})${semicolonEnd}`, 'i').exec(data);
if (!match) {
throw new AzFuncSystemError(`${HeaderName.contentDisposition} must begin with disposition type.`);
} else {
return [match[1], parseHeaderParams(match[2])];
}
}
function parseHeaderParams(data: string): HeaderParams {
const result = new HeaderParams();
while (data) {
// try to find an unquoted name=value pair first
const regexp = new RegExp(`${start}(${token})=(${token})${semicolonEnd}`, 'i');
let match = regexp.exec(data);
// if that didn't work, try to find a quoted name="value" pair instead
if (!match) {
const quotedPartsRegexp = new RegExp(`${start}(${token})="(${quotedString})"${semicolonEnd}`, 'i');
match = quotedPartsRegexp.exec(data);
}
if (match) {
result.add(match[1], match[2].replace(/\\"/g, '"')); // un-escape any quotes
data = match[3];
} else {
break;
}
}
return result;
}
export class HeaderParams {
#params: { [name: string]: string } = {};
get(name: string): string {
const result = this.#params[name.toLowerCase()];
if (result === undefined) {
throw new AzFuncSystemError(`Failed to find parameter with name "${name}".`);
} else {
return result;
}
}
has(name: string): boolean {
return this.#params[name.toLowerCase()] !== undefined;
}
add(name: string, value: string): void {
this.#params[name.toLowerCase()] = value;
}
}