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.Runtime.InteropServices; 21 using System.Text; 22 using Grpc.Core.Api.Utils; 23 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 static readonly Encoding EncodingASCII = System.Text.Encoding.ASCII; 56 57 readonly List<Entry> entries; 58 bool readOnly; 59 60 /// <summary> 61 /// Initializes a new instance of <c>Metadata</c>. 62 /// </summary> Metadata()63 public Metadata() 64 { 65 this.entries = new List<Entry>(); 66 } 67 68 /// <summary> 69 /// Makes this object read-only. 70 /// </summary> 71 /// <returns>this object</returns> Freeze()72 internal Metadata Freeze() 73 { 74 this.readOnly = true; 75 return this; 76 } 77 78 /// <summary> 79 /// Gets the last metadata entry with the specified key. 80 /// If there are no matching entries then <c>null</c> is returned. 81 /// </summary> Get(string key)82 public Entry Get(string key) 83 { 84 for (int i = entries.Count - 1; i >= 0; i--) 85 { 86 if (entries[i].Key == key) 87 { 88 return entries[i]; 89 } 90 } 91 92 return null; 93 } 94 95 /// <summary> 96 /// Gets the string value of the last metadata entry with the specified key. 97 /// If the metadata entry is binary then an exception is thrown. 98 /// If there are no matching entries then <c>null</c> is returned. 99 /// </summary> GetValue(string key)100 public string GetValue(string key) 101 { 102 return Get(key)?.Value; 103 } 104 105 /// <summary> 106 /// Gets the bytes value of the last metadata entry with the specified key. 107 /// If the metadata entry is not binary the string value will be returned as ASCII encoded bytes. 108 /// If there are no matching entries then <c>null</c> is returned. 109 /// </summary> GetValueBytes(string key)110 public byte[] GetValueBytes(string key) 111 { 112 return Get(key)?.ValueBytes; 113 } 114 115 /// <summary> 116 /// Gets all metadata entries with the specified key. 117 /// </summary> GetAll(string key)118 public IEnumerable<Entry> GetAll(string key) 119 { 120 for (int i = 0; i < entries.Count; i++) 121 { 122 if (entries[i].Key == key) 123 { 124 yield return entries[i]; 125 } 126 } 127 } 128 129 /// <summary> 130 /// Adds a new ASCII-valued metadata entry. See <c>Metadata.Entry</c> constructor for params. 131 /// </summary> Add(string key, string value)132 public void Add(string key, string value) 133 { 134 Add(new Entry(key, value)); 135 } 136 137 /// <summary> 138 /// Adds a new binary-valued metadata entry. See <c>Metadata.Entry</c> constructor for params. 139 /// </summary> Add(string key, byte[] valueBytes)140 public void Add(string key, byte[] valueBytes) 141 { 142 Add(new Entry(key, valueBytes)); 143 } 144 145 #region IList members 146 147 148 /// <summary> 149 /// <see cref="T:IList`1"/> 150 /// </summary> IndexOf(Metadata.Entry item)151 public int IndexOf(Metadata.Entry item) 152 { 153 return entries.IndexOf(item); 154 } 155 156 /// <summary> 157 /// <see cref="T:IList`1"/> 158 /// </summary> Insert(int index, Metadata.Entry item)159 public void Insert(int index, Metadata.Entry item) 160 { 161 GrpcPreconditions.CheckNotNull(item); 162 CheckWriteable(); 163 entries.Insert(index, item); 164 } 165 166 /// <summary> 167 /// <see cref="T:IList`1"/> 168 /// </summary> RemoveAt(int index)169 public void RemoveAt(int index) 170 { 171 CheckWriteable(); 172 entries.RemoveAt(index); 173 } 174 175 /// <summary> 176 /// <see cref="T:IList`1"/> 177 /// </summary> 178 public Metadata.Entry this[int index] 179 { 180 get 181 { 182 return entries[index]; 183 } 184 185 set 186 { 187 GrpcPreconditions.CheckNotNull(value); 188 CheckWriteable(); 189 entries[index] = value; 190 } 191 } 192 193 /// <summary> 194 /// <see cref="T:IList`1"/> 195 /// </summary> Add(Metadata.Entry item)196 public void Add(Metadata.Entry item) 197 { 198 GrpcPreconditions.CheckNotNull(item); 199 CheckWriteable(); 200 entries.Add(item); 201 } 202 203 /// <summary> 204 /// <see cref="T:IList`1"/> 205 /// </summary> Clear()206 public void Clear() 207 { 208 CheckWriteable(); 209 entries.Clear(); 210 } 211 212 /// <summary> 213 /// <see cref="T:IList`1"/> 214 /// </summary> Contains(Metadata.Entry item)215 public bool Contains(Metadata.Entry item) 216 { 217 return entries.Contains(item); 218 } 219 220 /// <summary> 221 /// <see cref="T:IList`1"/> 222 /// </summary> CopyTo(Metadata.Entry[] array, int arrayIndex)223 public void CopyTo(Metadata.Entry[] array, int arrayIndex) 224 { 225 entries.CopyTo(array, arrayIndex); 226 } 227 228 /// <summary> 229 /// <see cref="T:IList`1"/> 230 /// </summary> 231 public int Count 232 { 233 get { return entries.Count; } 234 } 235 236 /// <summary> 237 /// <see cref="T:IList`1"/> 238 /// </summary> 239 public bool IsReadOnly 240 { 241 get { return readOnly; } 242 } 243 244 /// <summary> 245 /// <see cref="T:IList`1"/> 246 /// </summary> Remove(Metadata.Entry item)247 public bool Remove(Metadata.Entry item) 248 { 249 CheckWriteable(); 250 return entries.Remove(item); 251 } 252 253 /// <summary> 254 /// <see cref="T:IList`1"/> 255 /// </summary> GetEnumerator()256 public IEnumerator<Metadata.Entry> GetEnumerator() 257 { 258 return entries.GetEnumerator(); 259 } 260 System.Collections.IEnumerable.GetEnumerator()261 IEnumerator System.Collections.IEnumerable.GetEnumerator() 262 { 263 return entries.GetEnumerator(); 264 } 265 CheckWriteable()266 private void CheckWriteable() 267 { 268 GrpcPreconditions.CheckState(!readOnly, "Object is read only"); 269 } 270 271 #endregion 272 273 /// <summary> 274 /// Metadata entry 275 /// </summary> 276 public class Entry 277 { 278 readonly string key; 279 readonly string value; 280 readonly byte[] valueBytes; 281 Entry(string key, string value, byte[] valueBytes)282 private Entry(string key, string value, byte[] valueBytes) 283 { 284 this.key = key; 285 this.value = value; 286 this.valueBytes = valueBytes; 287 } 288 289 /// <summary> 290 /// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct with a binary value. 291 /// </summary> 292 /// <param name="key">Metadata key. Gets converted to lowercase. Needs to have suffix indicating a binary valued metadata entry. Can only contain lowercase alphanumeric characters, underscores, hyphens and dots.</param> 293 /// <param name="valueBytes">Value bytes.</param> Entry(string key, byte[] valueBytes)294 public Entry(string key, byte[] valueBytes) 295 { 296 this.key = NormalizeKey(key); 297 GrpcPreconditions.CheckArgument(HasBinaryHeaderSuffix(this.key), 298 "Key for binary valued metadata entry needs to have suffix indicating binary value."); 299 this.value = null; 300 GrpcPreconditions.CheckNotNull(valueBytes, "valueBytes"); 301 this.valueBytes = new byte[valueBytes.Length]; 302 Buffer.BlockCopy(valueBytes, 0, this.valueBytes, 0, valueBytes.Length); // defensive copy to guarantee immutability 303 } 304 305 /// <summary> 306 /// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct with an ASCII value. 307 /// </summary> 308 /// <param name="key">Metadata key. Gets converted to lowercase. Must not use suffix indicating a binary valued metadata entry. Can only contain lowercase alphanumeric characters, underscores, hyphens and dots.</param> 309 /// <param name="value">Value string. Only ASCII characters are allowed.</param> Entry(string key, string value)310 public Entry(string key, string value) 311 { 312 this.key = NormalizeKey(key); 313 GrpcPreconditions.CheckArgument(!HasBinaryHeaderSuffix(this.key), 314 "Key for ASCII valued metadata entry cannot have suffix indicating binary value."); 315 this.value = GrpcPreconditions.CheckNotNull(value, "value"); 316 this.valueBytes = null; 317 } 318 319 /// <summary> 320 /// Gets the metadata entry key. 321 /// </summary> 322 public string Key 323 { 324 get 325 { 326 return this.key; 327 } 328 } 329 330 /// <summary> 331 /// Gets the binary value of this metadata entry. 332 /// If the metadata entry is not binary the string value will be returned as ASCII encoded bytes. 333 /// </summary> 334 public byte[] ValueBytes 335 { 336 get 337 { 338 if (valueBytes == null) 339 { 340 return EncodingASCII.GetBytes(value); 341 } 342 343 // defensive copy to guarantee immutability 344 var bytes = new byte[valueBytes.Length]; 345 Buffer.BlockCopy(valueBytes, 0, bytes, 0, valueBytes.Length); 346 return bytes; 347 } 348 } 349 350 /// <summary> 351 /// Gets the string value of this metadata entry. 352 /// If the metadata entry is binary then an exception is thrown. 353 /// </summary> 354 public string Value 355 { 356 get 357 { 358 GrpcPreconditions.CheckState(!IsBinary, "Cannot access string value of a binary metadata entry"); 359 return value; 360 } 361 } 362 363 /// <summary> 364 /// Returns <c>true</c> if this entry is a binary-value entry. 365 /// </summary> 366 public bool IsBinary 367 { 368 get 369 { 370 return value == null; 371 } 372 } 373 374 /// <summary> 375 /// Returns a <see cref="System.String"/> that represents the current <see cref="Grpc.Core.Metadata.Entry"/>. 376 /// </summary> ToString()377 public override string ToString() 378 { 379 if (IsBinary) 380 { 381 return string.Format("[Entry: key={0}, valueBytes={1}]", key, valueBytes); 382 } 383 384 return string.Format("[Entry: key={0}, value={1}]", key, value); 385 } 386 387 /// <summary> 388 /// Gets the serialized value for this entry. For binary metadata entries, this leaks 389 /// the internal <c>valueBytes</c> byte array and caller must not change contents of it. 390 /// </summary> GetSerializedValueUnsafe()391 internal byte[] GetSerializedValueUnsafe() 392 { 393 return valueBytes ?? EncodingASCII.GetBytes(value); 394 } 395 396 /// <summary> 397 /// Creates a binary value or ascii value metadata entry from data received from the native layer. 398 /// We trust C core to give us well-formed data, so we don't perform any checks or defensive copying. 399 /// </summary> CreateUnsafe(string key, IntPtr source, int length)400 internal static Entry CreateUnsafe(string key, IntPtr source, int length) 401 { 402 if (HasBinaryHeaderSuffix(key)) 403 { 404 byte[] arr; 405 if (length == 0) 406 { 407 arr = EmptyByteArray; 408 } 409 else 410 { // create a local copy in a fresh array 411 arr = new byte[length]; 412 Marshal.Copy(source, arr, 0, length); 413 } 414 return new Entry(key, null, arr); 415 } 416 else 417 { 418 string s = EncodingASCII.GetString(source, length); 419 return new Entry(key, s, null); 420 } 421 } 422 423 static readonly byte[] EmptyByteArray = new byte[0]; 424 NormalizeKey(string key)425 private static string NormalizeKey(string key) 426 { 427 GrpcPreconditions.CheckNotNull(key, "key"); 428 429 GrpcPreconditions.CheckArgument(IsValidKey(key, out bool isLowercase), 430 "Metadata entry key not valid. Keys can only contain lowercase alphanumeric characters, underscores, hyphens and dots."); 431 if (isLowercase) 432 { 433 // save allocation of a new string if already lowercase 434 return key; 435 } 436 437 return key.ToLowerInvariant(); 438 } 439 IsValidKey(string input, out bool isLowercase)440 private static bool IsValidKey(string input, out bool isLowercase) 441 { 442 isLowercase = true; 443 for (int i = 0; i < input.Length; i++) 444 { 445 char c = input[i]; 446 if ('a' <= c && c <= 'z' || 447 '0' <= c && c <= '9' || 448 c == '.' || 449 c == '_' || 450 c == '-' ) 451 continue; 452 453 if ('A' <= c && c <= 'Z') 454 { 455 isLowercase = false; 456 continue; 457 } 458 459 return false; 460 } 461 462 return true; 463 } 464 465 /// <summary> 466 /// Returns <c>true</c> if the key has "-bin" binary header suffix. 467 /// </summary> HasBinaryHeaderSuffix(string key)468 private static bool HasBinaryHeaderSuffix(string key) 469 { 470 // We don't use just string.EndsWith because its implementation is extremely slow 471 // on CoreCLR and we've seen significant differences in gRPC benchmarks caused by it. 472 // See https://github.com/dotnet/coreclr/issues/5612 473 474 int len = key.Length; 475 if (len >= 4 && 476 key[len - 4] == '-' && 477 key[len - 3] == 'b' && 478 key[len - 2] == 'i' && 479 key[len - 1] == 'n') 480 { 481 return true; 482 } 483 return false; 484 } 485 } 486 } 487 } 488