From 9ca2c3b8c06a2658c583946ec8e2cac28a2ba62a Mon Sep 17 00:00:00 2001 From: Krzysztof Cwalina Date: Wed, 7 Feb 2018 17:23:44 -0800 Subject: [PATCH] Added some APIs needed by my AzCopy Sample (#2106) --- .../System/Azure/Key.cs | 19 +++++ .../System/Azure/StorageAccessSignature.cs | 62 ++++++++++++++ .../RemoveTranstomation.cs | 61 ++++++++++++++ .../System/Buffers/Text/BufferWriter_ints.cs | 34 ++++++++ src/System.Text.Http.Parser/HttpParser.cs | 25 +++++- .../IHttpResponseLineHandler.cs | 10 +++ src/System.Text.Http/System.Text.Http.csproj | 3 + .../Text/Http/BufferWriterHttpExtensions.cs | 83 +++++++++++++++++++ 8 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 src/System.Buffers.Experimental/RemoveTranstomation.cs create mode 100644 src/System.Buffers.Experimental/System/Buffers/Text/BufferWriter_ints.cs create mode 100644 src/System.Text.Http.Parser/IHttpResponseLineHandler.cs create mode 100644 src/System.Text.Http/System/Text/Http/BufferWriterHttpExtensions.cs diff --git a/src/System.Azure.Experimental/System/Azure/Key.cs b/src/System.Azure.Experimental/System/Azure/Key.cs index 91a09ed7221..fa02a3f29cc 100644 --- a/src/System.Azure.Experimental/System/Azure/Key.cs +++ b/src/System.Azure.Experimental/System/Azure/Key.cs @@ -25,5 +25,24 @@ public static byte[] ComputeKeyBytes(string key) } return keyBytes; } + + public static byte[] ComputeKeyBytes(this ReadOnlySpan key) + { + int size = key.Length * 2; + var buffer = size < 128 ? stackalloc byte[size] : new byte[size]; + + if (Encodings.Utf16.ToUtf8(key.AsBytes(), buffer, out int consumed, out int written) != OperationStatus.Done) + { + throw new NotImplementedException("need to resize buffer"); + } + + var keyBytes = new byte[64]; + var result = Base64.DecodeFromUtf8(buffer.Slice(0, written), keyBytes, out consumed, out written); + if (result != OperationStatus.Done || written != 64) + { + throw new NotImplementedException("need to resize buffer"); + } + return keyBytes; + } } } diff --git a/src/System.Azure.Experimental/System/Azure/StorageAccessSignature.cs b/src/System.Azure.Experimental/System/Azure/StorageAccessSignature.cs index 6cb6b0ec13d..71f13f5d2a0 100644 --- a/src/System.Azure.Experimental/System/Azure/StorageAccessSignature.cs +++ b/src/System.Azure.Experimental/System/Azure/StorageAccessSignature.cs @@ -7,9 +7,71 @@ using System.Binary.Base64Experimental; using System.Buffers.Text; using System.Buffers; +using System.Buffers.Transformations; namespace System.Azure.Authentication { + public struct StorageAuthorizationHeader : IWritable + { + private static TransformationFormat s_toBase64 = new TransformationFormat(Base64Experimental.BytesToUtf8Encoder); + private static TransformationFormat s_removeCR = new TransformationFormat(new RemoveTransformation(13)); + + public Sha256 Hash; + public string HttpVerb; + public string AccountName; + public string CanonicalizedResource; + public WritableBytes CanonicalizedHeaders; + public long ContentLength; + + public bool TryWrite(Span buffer, out int written, StandardFormat format = default) + { + try + { + var writer = BufferWriter.Create(buffer); + writer.Write("SharedKey "); + writer.Write(AccountName); + writer.Write(':'); + + int signatureStart = writer.WrittenCount; + + writer.Write(HttpVerb); + if (ContentLength == 0) + { + writer.Write("\n\n\n\n\n\n\n\n\n\n\n\n"); + } + else + { + writer.Write("\n\n\n"); + writer.Write(ContentLength.ToString()); + writer.Write("\n\n\n\n\n\n\n\n\n"); + } + writer.WriteBytes(CanonicalizedHeaders, s_removeCR); + + // write canonicalized resource + writer.Write('/'); + writer.Write(AccountName); + writer.Write('/'); + writer.Write(CanonicalizedResource); + + // compute hash + Hash.Append(writer.Written.Slice(signatureStart)); + writer.WrittenCount = signatureStart; + + // write hash + writer.WriteBytes(Hash, s_toBase64); + + written = writer.WrittenCount; + return true; + } + catch (BufferWriter.BufferTooSmallException) + { + buffer.Clear(); + written = 0; + return false; + } + } + } + public static class StorageAccessSignature { private static Utf8String s_emptyHeaders = (Utf8String)"\n\n\n\n\n\n\n\n\n\n\nx-ms-date:"; // this wont be needed once we have UTF8 literals diff --git a/src/System.Buffers.Experimental/RemoveTranstomation.cs b/src/System.Buffers.Experimental/RemoveTranstomation.cs new file mode 100644 index 00000000000..48b54fc4646 --- /dev/null +++ b/src/System.Buffers.Experimental/RemoveTranstomation.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace System.Buffers.Transformations +{ + public struct WritableBytes : IWritable + { + readonly ReadOnlyMemory _bytes; + + public WritableBytes(ReadOnlyMemory bytes) + { + _bytes = bytes; + } + + public bool TryWrite(Span buffer, out int written, StandardFormat format = default) + { + if (format != default) throw new InvalidOperationException(); + + if (!_bytes.Span.TryCopyTo(buffer)) + { + written = 0; + return false; + } + written = _bytes.Length; + return true; + } + } + + public class RemoveTransformation : IBufferTransformation + { + byte _value; + public RemoveTransformation(byte valueToRemove) + { + _value = valueToRemove; + } + public OperationStatus Execute(ReadOnlySpan input, Span output, out int consumed, out int written) + { + written = 0; + for (consumed = 0; consumed < input.Length; consumed++) + { + if (input[consumed] == _value) continue; + if (written >= output.Length) return OperationStatus.DestinationTooSmall; + output[written++] = input[consumed]; + } + return OperationStatus.Done; + } + + public OperationStatus Transform(Span buffer, int dataLength, out int written) + { + written = 0; + for (int consumed = 0; consumed < dataLength; consumed++) + { + if (buffer[consumed] == _value) continue; + if (written >= buffer.Length) return OperationStatus.DestinationTooSmall; + buffer[written++] = buffer[consumed]; + } + return OperationStatus.Done; + } + } +} + diff --git a/src/System.Buffers.Experimental/System/Buffers/Text/BufferWriter_ints.cs b/src/System.Buffers.Experimental/System/Buffers/Text/BufferWriter_ints.cs new file mode 100644 index 00000000000..f54a0377717 --- /dev/null +++ b/src/System.Buffers.Experimental/System/Buffers/Text/BufferWriter_ints.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public ref partial struct BufferWriter + { + public bool TryWrite(int value, StandardFormat format = default) + { + if (!Utf8Formatter.TryFormat(value, Free, out int written, format)) return false; + _written += written; + return true; + } + + public void Write(int value, StandardFormat format = default) + { + while (!TryWrite(value, format)) Resize(); + } + + public bool TryWriteLine(long value, StandardFormat format = default) + { + if (!Utf8Formatter.TryFormat(value, Free, out int written, format)) return false; + if (!NewLine.TryCopyTo(Free.Slice(written))) return false; + _written += written + NewLine.Length; + return true; + } + + public void WriteLine(long value, StandardFormat format = default) + { + while (!TryWriteLine(value, format)) Resize(); + } + } +} diff --git a/src/System.Text.Http.Parser/HttpParser.cs b/src/System.Text.Http.Parser/HttpParser.cs index 8ca34766f02..cc1d37872c0 100644 --- a/src/System.Text.Http.Parser/HttpParser.cs +++ b/src/System.Text.Http.Parser/HttpParser.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Buffers; +using System.Buffers.Text; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -22,6 +23,7 @@ public class HttpParser : IHttpParser private const byte ByteTab = (byte)'\t'; private const byte ByteQuestionMark = (byte)'?'; private const byte BytePercentage = (byte)'%'; + static readonly byte[] s_Eol = Encoding.UTF8.GetBytes("\r\n"); private readonly bool _showErrorDetails; @@ -76,7 +78,7 @@ public unsafe bool ParseRequestLine(T handler, in ReadOnlyBuffer buffer return true; } - static readonly byte[] s_Eol = Encoding.UTF8.GetBytes("\r\n"); + public unsafe bool ParseRequestLine(ref T handler, in ReadOnlyBuffer buffer, out int consumed) where T : IHttpRequestLineHandler { // Prepare the first span @@ -475,6 +477,27 @@ public unsafe bool ParseHeaders(ref T handler, ReadOnlyBuffer buffer, o } } + public bool ParseResponseLine(ref T handler, ref ReadOnlyBuffer buffer, out int consumedBytes) where T : IHttpResponseLineHandler + { + var first = buffer.First.Span; + var eol = first.IndexOf(s_Eol); + if (eol == -1) + { + throw new NotImplementedException(); + } + first = first.Slice(0, eol); + int codeStart = first.IndexOf((byte)' ') + 1; + var codeSlice = first.Slice(codeStart); + if (!Utf8Parser.TryParse(codeSlice, out ushort code, out consumedBytes)) + { + throw new Exception("no status code"); + } + + handler.OnStartLine(Parser.Http.Version.Http11, code, codeSlice.Slice(consumedBytes + 1)); + consumedBytes = eol + s_Eol.Length; + return true; + } + [MethodImpl(MethodImplOptions.NoInlining)] private static void ReadTwoChars(ReadOnlyBuffer buffer, int consumedBytes, out int ch1, out int ch2) { diff --git a/src/System.Text.Http.Parser/IHttpResponseLineHandler.cs b/src/System.Text.Http.Parser/IHttpResponseLineHandler.cs new file mode 100644 index 00000000000..6c3cf050ec5 --- /dev/null +++ b/src/System.Text.Http.Parser/IHttpResponseLineHandler.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace System.Text.Http.Parser +{ + public interface IHttpResponseLineHandler + { + void OnStartLine(Http.Version version, ushort code, ReadOnlySpan status); + } +} diff --git a/src/System.Text.Http/System.Text.Http.csproj b/src/System.Text.Http/System.Text.Http.csproj index 219d3804d08..e860159eaf9 100644 --- a/src/System.Text.Http/System.Text.Http.csproj +++ b/src/System.Text.Http/System.Text.Http.csproj @@ -5,6 +5,9 @@ netstandard1.1 HTTP parser parsing .NET non-allocating corefxlab + + 7.2 + diff --git a/src/System.Text.Http/System/Text/Http/BufferWriterHttpExtensions.cs b/src/System.Text.Http/System/Text/Http/BufferWriterHttpExtensions.cs new file mode 100644 index 00000000000..7bc3539fa59 --- /dev/null +++ b/src/System.Text.Http/System/Text/Http/BufferWriterHttpExtensions.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Buffers; +using System.Buffers.Text; + +namespace System.Text.Http.Formatter +{ + public static class BufferWriterHttpExtensions + { + public static readonly byte[] s_httpNewline = new byte[] { 13, 10 }; + public static readonly byte[] s_eoh = new byte[] { 13, 10, 13, 10 }; + static byte[] s_Get = new byte[] { (byte)'G', (byte)'E', (byte)'T' }; + static byte[] s_Put = new byte[] { (byte)'P', (byte)'U', (byte)'T' }; + + public static BufferWriter AsHttpWriter(this Span buffer) + { + var writer = BufferWriter.Create(buffer); + writer.NewLine = s_httpNewline; + return writer; + } + + public static void WriteRequestLine(ref this BufferWriter writer, Parser.Http.Method verb, string path) + { + writer.WriteBytes(verb.AsBytes()); + writer.Write(" /"); + + writer.Write(path); + writer.WriteLine(" HTTP/1.1"); + } + + public static void WriteHeader(ref this BufferWriter writer, string headerName, string headerValue) + { + writer.Write(headerName); + writer.Write(":"); + writer.WriteLine(headerValue); + } + + public static void WriteHeader(ref this BufferWriter writer, string headerName, int headerValue) + { + writer.Write(headerName); + writer.Write(":"); + // TODO (Pri 0): this allocation needs to be eliminated + writer.WriteLine(headerValue); + } + + public static void WriteHeader(ref this BufferWriter writer, string headerName, long headerValue) + { + writer.Write(headerName); + writer.Write(":"); + + writer.WriteLine(headerValue); + } + + public static void WriteHeader(ref this BufferWriter writer, string headerName, DateTime headerValue, StandardFormat format) + { + writer.Write(headerName); + writer.Write(":"); + writer.WriteLine(headerValue, format); + } + + public static void WriteHeader(ref this BufferWriter writer, string headerName, T headerValue, StandardFormat format) + where T : IWritable + { + writer.Write(headerName); + writer.Write(":"); + writer.WriteBytes(headerValue, format); + writer.WriteLine(""); + } + + public static void WriteEoh(ref this BufferWriter writer) + { + writer.WriteLine(""); + } + + static ReadOnlySpan AsBytes(this Parser.Http.Method verb) + { + if (verb == Parser.Http.Method.Get) return s_Get; + if (verb == Parser.Http.Method.Put) return s_Put; + throw new NotImplementedException(); + } + } +}