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