UNPKG

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