|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
}; |
|
|