UNPKG

8.99 kBPlain TextView Raw
1/*
2 * Copyright 2019 gRPC authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 */
17
18import * as http2 from 'http2';
19import { log } from './logging';
20import { LogVerbosity } from './constants';
21import { getErrorMessage } from './error';
22const LEGAL_KEY_REGEX = /^[0-9a-z_.-]+$/;
23const LEGAL_NON_BINARY_VALUE_REGEX = /^[ -~]*$/;
24
25export type MetadataValue = string | Buffer;
26export type MetadataObject = Map<string, MetadataValue[]>;
27
28function isLegalKey(key: string): boolean {
29 return LEGAL_KEY_REGEX.test(key);
30}
31
32function isLegalNonBinaryValue(value: string): boolean {
33 return LEGAL_NON_BINARY_VALUE_REGEX.test(value);
34}
35
36function isBinaryKey(key: string): boolean {
37 return key.endsWith('-bin');
38}
39
40function isCustomMetadata(key: string): boolean {
41 return !key.startsWith('grpc-');
42}
43
44function normalizeKey(key: string): string {
45 return key.toLowerCase();
46}
47
48function validate(key: string, value?: MetadataValue): void {
49 if (!isLegalKey(key)) {
50 throw new Error('Metadata key "' + key + '" contains illegal characters');
51 }
52
53 if (value !== null && value !== undefined) {
54 if (isBinaryKey(key)) {
55 if (!Buffer.isBuffer(value)) {
56 throw new Error("keys that end with '-bin' must have Buffer values");
57 }
58 } else {
59 if (Buffer.isBuffer(value)) {
60 throw new Error(
61 "keys that don't end with '-bin' must have String values"
62 );
63 }
64 if (!isLegalNonBinaryValue(value)) {
65 throw new Error(
66 'Metadata string value "' + value + '" contains illegal characters'
67 );
68 }
69 }
70 }
71}
72
73export interface MetadataOptions {
74 /* Signal that the request is idempotent. Defaults to false */
75 idempotentRequest?: boolean;
76 /* Signal that the call should not return UNAVAILABLE before it has
77 * started. Defaults to false. */
78 waitForReady?: boolean;
79 /* Signal that the call is cacheable. GRPC is free to use GET verb.
80 * Defaults to false */
81 cacheableRequest?: boolean;
82 /* Signal that the initial metadata should be corked. Defaults to false. */
83 corked?: boolean;
84}
85
86/**
87 * A class for storing metadata. Keys are normalized to lowercase ASCII.
88 */
89export class Metadata {
90 protected internalRepr: MetadataObject = new Map<string, MetadataValue[]>();
91 private options: MetadataOptions;
92
93 constructor(options: MetadataOptions = {}) {
94 this.options = options;
95 }
96
97 /**
98 * Sets the given value for the given key by replacing any other values
99 * associated with that key. Normalizes the key.
100 * @param key The key to whose value should be set.
101 * @param value The value to set. Must be a buffer if and only
102 * if the normalized key ends with '-bin'.
103 */
104 set(key: string, value: MetadataValue): void {
105 key = normalizeKey(key);
106 validate(key, value);
107 this.internalRepr.set(key, [value]);
108 }
109
110 /**
111 * Adds the given value for the given key by appending to a list of previous
112 * values associated with that key. Normalizes the key.
113 * @param key The key for which a new value should be appended.
114 * @param value The value to add. Must be a buffer if and only
115 * if the normalized key ends with '-bin'.
116 */
117 add(key: string, value: MetadataValue): void {
118 key = normalizeKey(key);
119 validate(key, value);
120
121 const existingValue: MetadataValue[] | undefined = this.internalRepr.get(key);
122
123 if (existingValue === undefined) {
124 this.internalRepr.set(key, [value]);
125 } else {
126 existingValue.push(value);
127 }
128 }
129
130 /**
131 * Removes the given key and any associated values. Normalizes the key.
132 * @param key The key whose values should be removed.
133 */
134 remove(key: string): void {
135 key = normalizeKey(key);
136 // validate(key);
137 this.internalRepr.delete(key);
138 }
139
140 /**
141 * Gets a list of all values associated with the key. Normalizes the key.
142 * @param key The key whose value should be retrieved.
143 * @return A list of values associated with the given key.
144 */
145 get(key: string): MetadataValue[] {
146 key = normalizeKey(key);
147 // validate(key);
148 return this.internalRepr.get(key) || [];
149 }
150
151 /**
152 * Gets a plain object mapping each key to the first value associated with it.
153 * This reflects the most common way that people will want to see metadata.
154 * @return A key/value mapping of the metadata.
155 */
156 getMap(): { [key: string]: MetadataValue } {
157 const result: { [key: string]: MetadataValue } = {};
158
159 for (const [key, values] of this.internalRepr) {
160 if (values.length > 0) {
161 const v = values[0];
162 result[key] = Buffer.isBuffer(v) ? Buffer.from(v) : v;
163 }
164 }
165 return result;
166 }
167
168 /**
169 * Clones the metadata object.
170 * @return The newly cloned object.
171 */
172 clone(): Metadata {
173 const newMetadata = new Metadata(this.options);
174 const newInternalRepr = newMetadata.internalRepr;
175
176 for (const [key, value] of this.internalRepr) {
177 const clonedValue: MetadataValue[] = value.map((v) => {
178 if (Buffer.isBuffer(v)) {
179 return Buffer.from(v);
180 } else {
181 return v;
182 }
183 });
184
185 newInternalRepr.set(key, clonedValue);
186 }
187
188 return newMetadata;
189 }
190
191 /**
192 * Merges all key-value pairs from a given Metadata object into this one.
193 * If both this object and the given object have values in the same key,
194 * values from the other Metadata object will be appended to this object's
195 * values.
196 * @param other A Metadata object.
197 */
198 merge(other: Metadata): void {
199 for (const [key, values] of other.internalRepr) {
200 const mergedValue: MetadataValue[] = (
201 this.internalRepr.get(key) || []
202 ).concat(values);
203
204 this.internalRepr.set(key, mergedValue);
205 }
206 }
207
208 setOptions(options: MetadataOptions) {
209 this.options = options;
210 }
211
212 getOptions(): MetadataOptions {
213 return this.options;
214 }
215
216 /**
217 * Creates an OutgoingHttpHeaders object that can be used with the http2 API.
218 */
219 toHttp2Headers(): http2.OutgoingHttpHeaders {
220 // NOTE: Node <8.9 formats http2 headers incorrectly.
221 const result: http2.OutgoingHttpHeaders = {};
222
223 for (const [key, values] of this.internalRepr) {
224 // We assume that the user's interaction with this object is limited to
225 // through its public API (i.e. keys and values are already validated).
226 result[key] = values.map(bufToString);
227 }
228
229 return result;
230 }
231
232 // For compatibility with the other Metadata implementation
233 private _getCoreRepresentation() {
234 return this.internalRepr;
235 }
236
237 /**
238 * This modifies the behavior of JSON.stringify to show an object
239 * representation of the metadata map.
240 */
241 toJSON() {
242 const result: { [key: string]: MetadataValue[] } = {};
243 for (const [key, values] of this.internalRepr) {
244 result[key] = values;
245 }
246 return result;
247 }
248
249 /**
250 * Returns a new Metadata object based fields in a given IncomingHttpHeaders
251 * object.
252 * @param headers An IncomingHttpHeaders object.
253 */
254 static fromHttp2Headers(headers: http2.IncomingHttpHeaders): Metadata {
255 const result = new Metadata();
256 for (const key of Object.keys(headers)) {
257 // Reserved headers (beginning with `:`) are not valid keys.
258 if (key.charAt(0) === ':') {
259 continue;
260 }
261
262 const values = headers[key];
263
264 try {
265 if (isBinaryKey(key)) {
266 if (Array.isArray(values)) {
267 values.forEach((value) => {
268 result.add(key, Buffer.from(value, 'base64'));
269 });
270 } else if (values !== undefined) {
271 if (isCustomMetadata(key)) {
272 values.split(',').forEach((v) => {
273 result.add(key, Buffer.from(v.trim(), 'base64'));
274 });
275 } else {
276 result.add(key, Buffer.from(values, 'base64'));
277 }
278 }
279 } else {
280 if (Array.isArray(values)) {
281 values.forEach((value) => {
282 result.add(key, value);
283 });
284 } else if (values !== undefined) {
285 result.add(key, values);
286 }
287 }
288 } catch (error) {
289 const message = `Failed to add metadata entry ${key}: ${values}. ${getErrorMessage(error)}. For more information see https://github.com/grpc/grpc-node/issues/1173`;
290 log(LogVerbosity.ERROR, message);
291 }
292 }
293
294 return result;
295 }
296}
297
298const bufToString = (val: string | Buffer): string => {
299 return Buffer.isBuffer(val) ? val.toString('base64') : val
300};