UNPKG

8.87 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 =
122 this.internalRepr.get(key);
123
124 if (existingValue === undefined) {
125 this.internalRepr.set(key, [value]);
126 } else {
127 existingValue.push(value);
128 }
129 }
130
131 /**
132 * Removes the given key and any associated values. Normalizes the key.
133 * @param key The key whose values should be removed.
134 */
135 remove(key: string): void {
136 key = normalizeKey(key);
137 // validate(key);
138 this.internalRepr.delete(key);
139 }
140
141 /**
142 * Gets a list of all values associated with the key. Normalizes the key.
143 * @param key The key whose value should be retrieved.
144 * @return A list of values associated with the given key.
145 */
146 get(key: string): MetadataValue[] {
147 key = normalizeKey(key);
148 // validate(key);
149 return this.internalRepr.get(key) || [];
150 }
151
152 /**
153 * Gets a plain object mapping each key to the first value associated with it.
154 * This reflects the most common way that people will want to see metadata.
155 * @return A key/value mapping of the metadata.
156 */
157 getMap(): { [key: string]: MetadataValue } {
158 const result: { [key: string]: MetadataValue } = {};
159
160 for (const [key, values] of this.internalRepr) {
161 if (values.length > 0) {
162 const v = values[0];
163 result[key] = Buffer.isBuffer(v) ? Buffer.from(v) : v;
164 }
165 }
166 return result;
167 }
168
169 /**
170 * Clones the metadata object.
171 * @return The newly cloned object.
172 */
173 clone(): Metadata {
174 const newMetadata = new Metadata(this.options);
175 const newInternalRepr = newMetadata.internalRepr;
176
177 for (const [key, value] of this.internalRepr) {
178 const clonedValue: MetadataValue[] = value.map(v => {
179 if (Buffer.isBuffer(v)) {
180 return Buffer.from(v);
181 } else {
182 return v;
183 }
184 });
185
186 newInternalRepr.set(key, clonedValue);
187 }
188
189 return newMetadata;
190 }
191
192 /**
193 * Merges all key-value pairs from a given Metadata object into this one.
194 * If both this object and the given object have values in the same key,
195 * values from the other Metadata object will be appended to this object's
196 * values.
197 * @param other A Metadata object.
198 */
199 merge(other: Metadata): void {
200 for (const [key, values] of other.internalRepr) {
201 const mergedValue: MetadataValue[] = (
202 this.internalRepr.get(key) || []
203 ).concat(values);
204
205 this.internalRepr.set(key, mergedValue);
206 }
207 }
208
209 setOptions(options: MetadataOptions) {
210 this.options = options;
211 }
212
213 getOptions(): MetadataOptions {
214 return this.options;
215 }
216
217 /**
218 * Creates an OutgoingHttpHeaders object that can be used with the http2 API.
219 */
220 toHttp2Headers(): http2.OutgoingHttpHeaders {
221 // NOTE: Node <8.9 formats http2 headers incorrectly.
222 const result: http2.OutgoingHttpHeaders = {};
223
224 for (const [key, values] of this.internalRepr) {
225 // We assume that the user's interaction with this object is limited to
226 // through its public API (i.e. keys and values are already validated).
227 result[key] = values.map(bufToString);
228 }
229
230 return result;
231 }
232
233 /**
234 * This modifies the behavior of JSON.stringify to show an object
235 * representation of the metadata map.
236 */
237 toJSON() {
238 const result: { [key: string]: MetadataValue[] } = {};
239 for (const [key, values] of this.internalRepr) {
240 result[key] = values;
241 }
242 return result;
243 }
244
245 /**
246 * Returns a new Metadata object based fields in a given IncomingHttpHeaders
247 * object.
248 * @param headers An IncomingHttpHeaders object.
249 */
250 static fromHttp2Headers(headers: http2.IncomingHttpHeaders): Metadata {
251 const result = new Metadata();
252 for (const key of Object.keys(headers)) {
253 // Reserved headers (beginning with `:`) are not valid keys.
254 if (key.charAt(0) === ':') {
255 continue;
256 }
257
258 const values = headers[key];
259
260 try {
261 if (isBinaryKey(key)) {
262 if (Array.isArray(values)) {
263 values.forEach(value => {
264 result.add(key, Buffer.from(value, 'base64'));
265 });
266 } else if (values !== undefined) {
267 if (isCustomMetadata(key)) {
268 values.split(',').forEach(v => {
269 result.add(key, Buffer.from(v.trim(), 'base64'));
270 });
271 } else {
272 result.add(key, Buffer.from(values, 'base64'));
273 }
274 }
275 } else {
276 if (Array.isArray(values)) {
277 values.forEach(value => {
278 result.add(key, value);
279 });
280 } else if (values !== undefined) {
281 result.add(key, values);
282 }
283 }
284 } catch (error) {
285 const message = `Failed to add metadata entry ${key}: ${values}. ${getErrorMessage(
286 error
287 )}. For more information see https://github.com/grpc/grpc-node/issues/1173`;
288 log(LogVerbosity.ERROR, message);
289 }
290 }
291
292 return result;
293 }
294}
295
296const bufToString = (val: string | Buffer): string => {
297 return Buffer.isBuffer(val) ? val.toString('base64') : val;
298};