| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import * as http2 from 'http2'; |
| import { log } from './logging'; |
| import { LogVerbosity } from './constants'; |
| import { getErrorMessage } from './error'; |
| const LEGAL_KEY_REGEX = /^[0-9a-z_.-]+$/; |
| const LEGAL_NON_BINARY_VALUE_REGEX = /^[ -~]*$/; |
|
|
| export type MetadataValue = string | Buffer; |
| export type MetadataObject = Map<string, MetadataValue[]>; |
|
|
| function isLegalKey(key: string): boolean { |
| return LEGAL_KEY_REGEX.test(key); |
| } |
|
|
| function isLegalNonBinaryValue(value: string): boolean { |
| return LEGAL_NON_BINARY_VALUE_REGEX.test(value); |
| } |
|
|
| function isBinaryKey(key: string): boolean { |
| return key.endsWith('-bin'); |
| } |
|
|
| function isCustomMetadata(key: string): boolean { |
| return !key.startsWith('grpc-'); |
| } |
|
|
| function normalizeKey(key: string): string { |
| return key.toLowerCase(); |
| } |
|
|
| function validate(key: string, value?: MetadataValue): void { |
| if (!isLegalKey(key)) { |
| throw new Error('Metadata key "' + key + '" contains illegal characters'); |
| } |
|
|
| if (value !== null && value !== undefined) { |
| if (isBinaryKey(key)) { |
| if (!Buffer.isBuffer(value)) { |
| throw new Error("keys that end with '-bin' must have Buffer values"); |
| } |
| } else { |
| if (Buffer.isBuffer(value)) { |
| throw new Error( |
| "keys that don't end with '-bin' must have String values" |
| ); |
| } |
| if (!isLegalNonBinaryValue(value)) { |
| throw new Error( |
| 'Metadata string value "' + value + '" contains illegal characters' |
| ); |
| } |
| } |
| } |
| } |
|
|
| export interface MetadataOptions { |
| |
| idempotentRequest?: boolean; |
| |
| |
| waitForReady?: boolean; |
| |
| |
| cacheableRequest?: boolean; |
| |
| corked?: boolean; |
| } |
|
|
| |
| |
| |
| export class Metadata { |
| protected internalRepr: MetadataObject = new Map<string, MetadataValue[]>(); |
| private options: MetadataOptions; |
|
|
| constructor(options: MetadataOptions = {}) { |
| this.options = options; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| set(key: string, value: MetadataValue): void { |
| key = normalizeKey(key); |
| validate(key, value); |
| this.internalRepr.set(key, [value]); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| add(key: string, value: MetadataValue): void { |
| key = normalizeKey(key); |
| validate(key, value); |
|
|
| const existingValue: MetadataValue[] | undefined = |
| this.internalRepr.get(key); |
|
|
| if (existingValue === undefined) { |
| this.internalRepr.set(key, [value]); |
| } else { |
| existingValue.push(value); |
| } |
| } |
|
|
| |
| |
| |
| |
| remove(key: string): void { |
| key = normalizeKey(key); |
| |
| this.internalRepr.delete(key); |
| } |
|
|
| |
| |
| |
| |
| |
| get(key: string): MetadataValue[] { |
| key = normalizeKey(key); |
| |
| return this.internalRepr.get(key) || []; |
| } |
|
|
| |
| |
| |
| |
| |
| getMap(): { [key: string]: MetadataValue } { |
| const result: { [key: string]: MetadataValue } = {}; |
|
|
| for (const [key, values] of this.internalRepr) { |
| if (values.length > 0) { |
| const v = values[0]; |
| result[key] = Buffer.isBuffer(v) ? Buffer.from(v) : v; |
| } |
| } |
| return result; |
| } |
|
|
| |
| |
| |
| |
| clone(): Metadata { |
| const newMetadata = new Metadata(this.options); |
| const newInternalRepr = newMetadata.internalRepr; |
|
|
| for (const [key, value] of this.internalRepr) { |
| const clonedValue: MetadataValue[] = value.map(v => { |
| if (Buffer.isBuffer(v)) { |
| return Buffer.from(v); |
| } else { |
| return v; |
| } |
| }); |
|
|
| newInternalRepr.set(key, clonedValue); |
| } |
|
|
| return newMetadata; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| merge(other: Metadata): void { |
| for (const [key, values] of other.internalRepr) { |
| const mergedValue: MetadataValue[] = ( |
| this.internalRepr.get(key) || [] |
| ).concat(values); |
|
|
| this.internalRepr.set(key, mergedValue); |
| } |
| } |
|
|
| setOptions(options: MetadataOptions) { |
| this.options = options; |
| } |
|
|
| getOptions(): MetadataOptions { |
| return this.options; |
| } |
|
|
| |
| |
| |
| toHttp2Headers(): http2.OutgoingHttpHeaders { |
| |
| const result: http2.OutgoingHttpHeaders = {}; |
|
|
| for (const [key, values] of this.internalRepr) { |
| |
| |
| result[key] = values.map(bufToString); |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
| |
| toJSON() { |
| const result: { [key: string]: MetadataValue[] } = {}; |
| for (const [key, values] of this.internalRepr) { |
| result[key] = values; |
| } |
| return result; |
| } |
|
|
| |
| |
| |
| |
| |
| static fromHttp2Headers(headers: http2.IncomingHttpHeaders): Metadata { |
| const result = new Metadata(); |
| for (const key of Object.keys(headers)) { |
| |
| if (key.charAt(0) === ':') { |
| continue; |
| } |
|
|
| const values = headers[key]; |
|
|
| try { |
| if (isBinaryKey(key)) { |
| if (Array.isArray(values)) { |
| values.forEach(value => { |
| result.add(key, Buffer.from(value, 'base64')); |
| }); |
| } else if (values !== undefined) { |
| if (isCustomMetadata(key)) { |
| values.split(',').forEach(v => { |
| result.add(key, Buffer.from(v.trim(), 'base64')); |
| }); |
| } else { |
| result.add(key, Buffer.from(values, 'base64')); |
| } |
| } |
| } else { |
| if (Array.isArray(values)) { |
| values.forEach(value => { |
| result.add(key, value); |
| }); |
| } else if (values !== undefined) { |
| result.add(key, values); |
| } |
| } |
| } catch (error) { |
| const message = `Failed to add metadata entry ${key}: ${values}. ${getErrorMessage( |
| error |
| )}. For more information see https://github.com/grpc/grpc-node/issues/1173`; |
| log(LogVerbosity.ERROR, message); |
| } |
| } |
|
|
| return result; |
| } |
| } |
|
|
| const bufToString = (val: string | Buffer): string => { |
| return Buffer.isBuffer(val) ? val.toString('base64') : val; |
| }; |
|
|