1 #region Copyright notice and license
2 // Copyright 2015 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 #endregion
16 
17 using System;
18 using System.Collections;
19 using System.Collections.Generic;
20 using System.Text;
21 using System.Text.RegularExpressions;
22 
23 using Grpc.Core.Internal;
24 using Grpc.Core.Utils;
25 
26 namespace Grpc.Core
27 {
28     /// <summary>
29     /// A collection of metadata entries that can be exchanged during a call.
30     /// gRPC supports these types of metadata:
31     /// <list type="bullet">
32     /// <item><term>Request headers</term><description>are sent by the client at the beginning of a remote call before any request messages are sent.</description></item>
33     /// <item><term>Response headers</term><description>are sent by the server at the beginning of a remote call handler before any response messages are sent.</description></item>
34     /// <item><term>Response trailers</term><description>are sent by the server at the end of a remote call along with resulting call status.</description></item>
35     /// </list>
36     /// </summary>
37     public sealed class Metadata : IList<Metadata.Entry>
38     {
39         /// <summary>
40         /// All binary headers should have this suffix.
41         /// </summary>
42         public const string BinaryHeaderSuffix = "-bin";
43 
44         /// <summary>
45         /// An read-only instance of metadata containing no entries.
46         /// </summary>
47         public static readonly Metadata Empty = new Metadata().Freeze();
48 
49         /// <summary>
50         /// To be used in initial metadata to request specific compression algorithm
51         /// for given call. Direct selection of compression algorithms is an internal
52         /// feature and is not part of public API.
53         /// </summary>
54         internal const string CompressionRequestAlgorithmMetadataKey = "grpc-internal-encoding-request";
55 
56         readonly List<Entry> entries;
57         bool readOnly;
58 
59         /// <summary>
60         /// Initializes a new instance of <c>Metadata</c>.
61         /// </summary>
Metadata()62         public Metadata()
63         {
64             this.entries = new List<Entry>();
65         }
66 
67         /// <summary>
68         /// Makes this object read-only.
69         /// </summary>
70         /// <returns>this object</returns>
Freeze()71         internal Metadata Freeze()
72         {
73             this.readOnly = true;
74             return this;
75         }
76 
77         // TODO: add support for access by key
78 
79         #region IList members
80 
81 
82         /// <summary>
83         /// <see cref="T:IList`1"/>
84         /// </summary>
IndexOf(Metadata.Entry item)85         public int IndexOf(Metadata.Entry item)
86         {
87             return entries.IndexOf(item);
88         }
89 
90         /// <summary>
91         /// <see cref="T:IList`1"/>
92         /// </summary>
Insert(int index, Metadata.Entry item)93         public void Insert(int index, Metadata.Entry item)
94         {
95             GrpcPreconditions.CheckNotNull(item);
96             CheckWriteable();
97             entries.Insert(index, item);
98         }
99 
100         /// <summary>
101         /// <see cref="T:IList`1"/>
102         /// </summary>
RemoveAt(int index)103         public void RemoveAt(int index)
104         {
105             CheckWriteable();
106             entries.RemoveAt(index);
107         }
108 
109         /// <summary>
110         /// <see cref="T:IList`1"/>
111         /// </summary>
112         public Metadata.Entry this[int index]
113         {
114             get
115             {
116                 return entries[index];
117             }
118 
119             set
120             {
121                 GrpcPreconditions.CheckNotNull(value);
122                 CheckWriteable();
123                 entries[index] = value;
124             }
125         }
126 
127         /// <summary>
128         /// <see cref="T:IList`1"/>
129         /// </summary>
Add(Metadata.Entry item)130         public void Add(Metadata.Entry item)
131         {
132             GrpcPreconditions.CheckNotNull(item);
133             CheckWriteable();
134             entries.Add(item);
135         }
136 
137         /// <summary>
138         /// <see cref="T:IList`1"/>
139         /// </summary>
Add(string key, string value)140         public void Add(string key, string value)
141         {
142             Add(new Entry(key, value));
143         }
144 
145         /// <summary>
146         /// <see cref="T:IList`1"/>
147         /// </summary>
Add(string key, byte[] valueBytes)148         public void Add(string key, byte[] valueBytes)
149         {
150             Add(new Entry(key, valueBytes));
151         }
152 
153         /// <summary>
154         /// <see cref="T:IList`1"/>
155         /// </summary>
Clear()156         public void Clear()
157         {
158             CheckWriteable();
159             entries.Clear();
160         }
161 
162         /// <summary>
163         /// <see cref="T:IList`1"/>
164         /// </summary>
Contains(Metadata.Entry item)165         public bool Contains(Metadata.Entry item)
166         {
167             return entries.Contains(item);
168         }
169 
170         /// <summary>
171         /// <see cref="T:IList`1"/>
172         /// </summary>
CopyTo(Metadata.Entry[] array, int arrayIndex)173         public void CopyTo(Metadata.Entry[] array, int arrayIndex)
174         {
175             entries.CopyTo(array, arrayIndex);
176         }
177 
178         /// <summary>
179         /// <see cref="T:IList`1"/>
180         /// </summary>
181         public int Count
182         {
183             get { return entries.Count; }
184         }
185 
186         /// <summary>
187         /// <see cref="T:IList`1"/>
188         /// </summary>
189         public bool IsReadOnly
190         {
191             get { return readOnly; }
192         }
193 
194         /// <summary>
195         /// <see cref="T:IList`1"/>
196         /// </summary>
Remove(Metadata.Entry item)197         public bool Remove(Metadata.Entry item)
198         {
199             CheckWriteable();
200             return entries.Remove(item);
201         }
202 
203         /// <summary>
204         /// <see cref="T:IList`1"/>
205         /// </summary>
GetEnumerator()206         public IEnumerator<Metadata.Entry> GetEnumerator()
207         {
208             return entries.GetEnumerator();
209         }
210 
System.Collections.IEnumerable.GetEnumerator()211         IEnumerator System.Collections.IEnumerable.GetEnumerator()
212         {
213             return entries.GetEnumerator();
214         }
215 
CheckWriteable()216         private void CheckWriteable()
217         {
218             GrpcPreconditions.CheckState(!readOnly, "Object is read only");
219         }
220 
221         #endregion
222 
223         /// <summary>
224         /// Metadata entry
225         /// </summary>
226         public class Entry
227         {
228             private static readonly Regex ValidKeyRegex = new Regex("^[.a-z0-9_-]+$");
229 
230             readonly string key;
231             readonly string value;
232             readonly byte[] valueBytes;
233 
Entry(string key, string value, byte[] valueBytes)234             private Entry(string key, string value, byte[] valueBytes)
235             {
236                 this.key = key;
237                 this.value = value;
238                 this.valueBytes = valueBytes;
239             }
240 
241             /// <summary>
242             /// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct with a binary value.
243             /// </summary>
244             /// <param name="key">Metadata key, needs to have suffix indicating a binary valued metadata entry.</param>
245             /// <param name="valueBytes">Value bytes.</param>
Entry(string key, byte[] valueBytes)246             public Entry(string key, byte[] valueBytes)
247             {
248                 this.key = NormalizeKey(key);
249                 GrpcPreconditions.CheckArgument(HasBinaryHeaderSuffix(this.key),
250                     "Key for binary valued metadata entry needs to have suffix indicating binary value.");
251                 this.value = null;
252                 GrpcPreconditions.CheckNotNull(valueBytes, "valueBytes");
253                 this.valueBytes = new byte[valueBytes.Length];
254                 Buffer.BlockCopy(valueBytes, 0, this.valueBytes, 0, valueBytes.Length);  // defensive copy to guarantee immutability
255             }
256 
257             /// <summary>
258             /// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct holding an ASCII value.
259             /// </summary>
260             /// <param name="key">Metadata key, must not use suffix indicating a binary valued metadata entry.</param>
261             /// <param name="value">Value string. Only ASCII characters are allowed.</param>
Entry(string key, string value)262             public Entry(string key, string value)
263             {
264                 this.key = NormalizeKey(key);
265                 GrpcPreconditions.CheckArgument(!HasBinaryHeaderSuffix(this.key),
266                     "Key for ASCII valued metadata entry cannot have suffix indicating binary value.");
267                 this.value = GrpcPreconditions.CheckNotNull(value, "value");
268                 this.valueBytes = null;
269             }
270 
271             /// <summary>
272             /// Gets the metadata entry key.
273             /// </summary>
274             public string Key
275             {
276                 get
277                 {
278                     return this.key;
279                 }
280             }
281 
282             /// <summary>
283             /// Gets the binary value of this metadata entry.
284             /// </summary>
285             public byte[] ValueBytes
286             {
287                 get
288                 {
289                     if (valueBytes == null)
290                     {
291                         return MarshalUtils.GetBytesASCII(value);
292                     }
293 
294                     // defensive copy to guarantee immutability
295                     var bytes = new byte[valueBytes.Length];
296                     Buffer.BlockCopy(valueBytes, 0, bytes, 0, valueBytes.Length);
297                     return bytes;
298                 }
299             }
300 
301             /// <summary>
302             /// Gets the string value of this metadata entry.
303             /// </summary>
304             public string Value
305             {
306                 get
307                 {
308                     GrpcPreconditions.CheckState(!IsBinary, "Cannot access string value of a binary metadata entry");
309                     return value ?? MarshalUtils.GetStringASCII(valueBytes);
310                 }
311             }
312 
313             /// <summary>
314             /// Returns <c>true</c> if this entry is a binary-value entry.
315             /// </summary>
316             public bool IsBinary
317             {
318                 get
319                 {
320                     return value == null;
321                 }
322             }
323 
324             /// <summary>
325             /// Returns a <see cref="System.String"/> that represents the current <see cref="Grpc.Core.Metadata.Entry"/>.
326             /// </summary>
ToString()327             public override string ToString()
328             {
329                 if (IsBinary)
330                 {
331                     return string.Format("[Entry: key={0}, valueBytes={1}]", key, valueBytes);
332                 }
333 
334                 return string.Format("[Entry: key={0}, value={1}]", key, value);
335             }
336 
337             /// <summary>
338             /// Gets the serialized value for this entry. For binary metadata entries, this leaks
339             /// the internal <c>valueBytes</c> byte array and caller must not change contents of it.
340             /// </summary>
GetSerializedValueUnsafe()341             internal byte[] GetSerializedValueUnsafe()
342             {
343                 return valueBytes ?? MarshalUtils.GetBytesASCII(value);
344             }
345 
346             /// <summary>
347             /// Creates a binary value or ascii value metadata entry from data received from the native layer.
348             /// We trust C core to give us well-formed data, so we don't perform any checks or defensive copying.
349             /// </summary>
CreateUnsafe(string key, byte[] valueBytes)350             internal static Entry CreateUnsafe(string key, byte[] valueBytes)
351             {
352                 if (HasBinaryHeaderSuffix(key))
353                 {
354                     return new Entry(key, null, valueBytes);
355                 }
356                 return new Entry(key, MarshalUtils.GetStringASCII(valueBytes), null);
357             }
358 
NormalizeKey(string key)359             private static string NormalizeKey(string key)
360             {
361                 var normalized = GrpcPreconditions.CheckNotNull(key, "key").ToLowerInvariant();
362                 GrpcPreconditions.CheckArgument(ValidKeyRegex.IsMatch(normalized),
363                     "Metadata entry key not valid. Keys can only contain lowercase alphanumeric characters, underscores, hyphens and dots.");
364                 return normalized;
365             }
366 
367             /// <summary>
368             /// Returns <c>true</c> if the key has "-bin" binary header suffix.
369             /// </summary>
HasBinaryHeaderSuffix(string key)370             private static bool HasBinaryHeaderSuffix(string key)
371             {
372                 // We don't use just string.EndsWith because its implementation is extremely slow
373                 // on CoreCLR and we've seen significant differences in gRPC benchmarks caused by it.
374                 // See https://github.com/dotnet/coreclr/issues/5612
375 
376                 int len = key.Length;
377                 if (len >= 4 &&
378                     key[len - 4] == '-' &&
379                     key[len - 3] == 'b' &&
380                     key[len - 2] == 'i' &&
381                     key[len - 1] == 'n')
382                 {
383                     return true;
384                 }
385                 return false;
386             }
387         }
388     }
389 }
390