From f55195009b9660fc755a8aeab008b7f323775a75 Mon Sep 17 00:00:00 2001 From: George Ndungu Date: Wed, 20 May 2020 00:56:17 +0300 Subject: [PATCH 01/16] Add GertGraphClient and InvokeGraph request Cmdlests --- .../Authentication/Cmdlets/GetGraphClient.cs | 34 +++++++++++ .../Cmdlets/InvokeGraphRequest.cs | 57 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/Authentication/Authentication/Cmdlets/GetGraphClient.cs create mode 100644 src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs diff --git a/src/Authentication/Authentication/Cmdlets/GetGraphClient.cs b/src/Authentication/Authentication/Cmdlets/GetGraphClient.cs new file mode 100644 index 00000000000..1f6d4df55a7 --- /dev/null +++ b/src/Authentication/Authentication/Cmdlets/GetGraphClient.cs @@ -0,0 +1,34 @@ +using System.Management.Automation; +using System.Net.Http; +using Microsoft.Graph.PowerShell.Authentication.Helpers; + +namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "GraphClient", DefaultParameterSetName = Constants.UserParameterSet)] + [OutputType(typeof(IAuthContext))] + public class GetGraphClient : PSCmdlet + { + protected override void BeginProcessing() + { + base.BeginProcessing(); + } + + protected override void ProcessRecord() + { + base.ProcessRecord(); + IAuthContext authConfig = GraphSession.Instance.AuthContext; + HttpClient client = HttpHelpers.GetGraphHttpClient(authConfig); + WriteObject(client); + } + + protected override void EndProcessing() + { + base.EndProcessing(); + } + + protected override void StopProcessing() + { + base.StopProcessing(); + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs new file mode 100644 index 00000000000..b8adca35776 --- /dev/null +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -0,0 +1,57 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ +namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets +{ + + using System; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Management.Automation; + using Newtonsoft.Json; + using Microsoft.Graph.PowerShell.Authentication.Helpers; + + [Cmdlet(VerbsLifecycle.Invoke, "GraphRequest", DefaultParameterSetName = Constants.UserParameterSet)] + [OutputType(typeof(IAuthContext))] + public class InvokeGraphRequest : PSCmdlet + { + [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 1)] + public object Body { get; set; } + + [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 2)] + public HttpMethod Method { get; set; } + + [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 3)] + public Uri Uri { get; set; } + + protected override void BeginProcessing() + { + base.BeginProcessing(); + } + protected override void ProcessRecord() + { + base.ProcessRecord(); + IAuthContext authConfig = GraphSession.Instance.AuthContext; + HttpClient client = HttpHelpers.GetGraphHttpClient(authConfig); + var requestMessage = new HttpRequestMessage(this.Method, this.Uri) + { + Content = new global::System.Net.Http.StringContent( + null != Body ? JsonConvert.SerializeObject(Body) : @"{}", global::System.Text.Encoding.UTF8) + }; + requestMessage.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + var responseMessage = client.SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead).GetAwaiter().GetResult(); + var responseString = responseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + WriteObject(responseMessage); + } + + protected override void EndProcessing() + { + base.EndProcessing(); + } + + protected override void StopProcessing() + { + base.StopProcessing(); + } + } +} From 608dc7c1e1a6b42226ee0fba556d1fa6219de992 Mon Sep 17 00:00:00 2001 From: George Ndungu Date: Tue, 2 Jun 2020 16:30:22 +0300 Subject: [PATCH 02/16] Validate passed in URL. Use Http enum for validation. --- .../Authentication/Cmdlets/GetGraphClient.cs | 34 ----- .../Cmdlets/InvokeGraphRequest.cs | 140 +++++++++++++++--- .../Microsoft.Graph.Authentication.psd1 | 2 +- 3 files changed, 122 insertions(+), 54 deletions(-) delete mode 100644 src/Authentication/Authentication/Cmdlets/GetGraphClient.cs diff --git a/src/Authentication/Authentication/Cmdlets/GetGraphClient.cs b/src/Authentication/Authentication/Cmdlets/GetGraphClient.cs deleted file mode 100644 index 1f6d4df55a7..00000000000 --- a/src/Authentication/Authentication/Cmdlets/GetGraphClient.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Management.Automation; -using System.Net.Http; -using Microsoft.Graph.PowerShell.Authentication.Helpers; - -namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets -{ - [Cmdlet(VerbsCommon.Get, "GraphClient", DefaultParameterSetName = Constants.UserParameterSet)] - [OutputType(typeof(IAuthContext))] - public class GetGraphClient : PSCmdlet - { - protected override void BeginProcessing() - { - base.BeginProcessing(); - } - - protected override void ProcessRecord() - { - base.ProcessRecord(); - IAuthContext authConfig = GraphSession.Instance.AuthContext; - HttpClient client = HttpHelpers.GetGraphHttpClient(authConfig); - WriteObject(client); - } - - protected override void EndProcessing() - { - base.EndProcessing(); - } - - protected override void StopProcessing() - { - base.StopProcessing(); - } - } -} \ No newline at end of file diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index b8adca35776..7a43f494909 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -3,45 +3,132 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets { - using System; + using System.Text; + using System.Threading; using System.Net.Http; + using System.Collections; using System.Net.Http.Headers; + using System.Collections.Generic; + using System.Management.Automation; + using System.Management.Automation.Language; + using System.Management.Automation.Runspaces; + using Newtonsoft.Json; using Microsoft.Graph.PowerShell.Authentication.Helpers; - + + [Cmdlet(VerbsLifecycle.Invoke, "GraphRequest", DefaultParameterSetName = Constants.UserParameterSet)] - [OutputType(typeof(IAuthContext))] + [OutputType(typeof(Hashtable))] public class InvokeGraphRequest : PSCmdlet { - [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 1)] - public object Body { get; set; } + /// + /// Contains the values of HttpVerbs + /// + public enum HttpVerb + { + GET, + POST, + PUT, + PATCH, + DELETE + } + private readonly HttpClient _client; + public InvokeGraphRequest() + { + this.Method = HttpVerb.GET; + var authConfig = GraphSession.Instance.AuthContext; + this._client = HttpHelpers.GetGraphHttpClient(authConfig); + } - [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 2)] - public HttpMethod Method { get; set; } + /// + /// Http Method + /// + [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 1, Mandatory = true)] + public HttpVerb Method { get; set; } - [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 3)] + /// + /// Uri to call using the Graph HttpClient can be segments such as /beta/me + /// or fully qualified url such as https://graph.microsoft.com/beta/me + /// + [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 2, Mandatory = true)] public Uri Uri { get; set; } + /// + /// Optional Http Body + /// + [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 3)] + public object Body { get; set; } + protected override void BeginProcessing() { + ValidateParameters(); base.BeginProcessing(); } + protected override void ProcessRecord() { base.ProcessRecord(); - IAuthContext authConfig = GraphSession.Instance.AuthContext; - HttpClient client = HttpHelpers.GetGraphHttpClient(authConfig); - var requestMessage = new HttpRequestMessage(this.Method, this.Uri) + var authConfig = GraphSession.Instance.AuthContext; + var client = HttpHelpers.GetGraphHttpClient(authConfig); + try + { + var httpMethod = GetHttpMethod(); + var requestMessage = new HttpRequestMessage(httpMethod, this.Uri) + { + Content = new StringContent(null != Body ? JsonConvert.SerializeObject(Body) : @"{}", Encoding.UTF8, + CoreConstants.MimeTypeNames.Application.Json) + }; + var responseMessage = client.SendAsync(requestMessage).GetAwaiter().GetResult(); + var responseString = responseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + var result = JsonConvert.DeserializeObject(responseString); + if (responseMessage.IsSuccessStatusCode) + { + WriteObject(result); + } + else + { + WriteError(new ErrorRecord(new Exception(responseString), Guid.NewGuid().ToString(), + ErrorCategory.InvalidResult, null)); + } + } + catch (HttpRequestException httpRequestException) + { + WriteError(new ErrorRecord(httpRequestException, ErrorCategory.ConnectionError.ToString(), + ErrorCategory.InvalidResult, null)); + throw; + } + catch (Exception exception) + { + WriteError(new ErrorRecord(exception, ErrorCategory.ConnectionError.ToString(), ErrorCategory.InvalidOperation, null)); + throw; + } + } + + /// + /// Maps from HttpVerb to System.Net.Http.HttpMethod + /// + /// System.Net.Http.HttpMethod + private System.Net.Http.HttpMethod GetHttpMethod() + { + switch (Method) { - Content = new global::System.Net.Http.StringContent( - null != Body ? JsonConvert.SerializeObject(Body) : @"{}", global::System.Text.Encoding.UTF8) - }; - requestMessage.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - var responseMessage = client.SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead).GetAwaiter().GetResult(); - var responseString = responseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - WriteObject(responseMessage); + case HttpVerb.GET: + return System.Net.Http.HttpMethod.Get; + case HttpVerb.POST: + return System.Net.Http.HttpMethod.Post; + case HttpVerb.PATCH: + // System.Net.Http.HttpMethod does not contain HttpMethod.Patch in netstandard2.0 + // https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpmethod?view=netstandard-2.0 + return new System.Net.Http.HttpMethod("PATCH"); + case HttpVerb.PUT: + return System.Net.Http.HttpMethod.Put; + case HttpVerb.DELETE: + return System.Net.Http.HttpMethod.Delete; + default: + throw new ArgumentOutOfRangeException(); + } } protected override void EndProcessing() @@ -53,5 +140,20 @@ protected override void StopProcessing() { base.StopProcessing(); } + + private void ValidateParameters() + { + if (Uri == null) + { + ThrowTerminatingError(new ErrorRecord(new ArgumentNullException(nameof(Uri), $"Must specify {Uri}"), Guid.NewGuid().ToString(), + ErrorCategory.InvalidArgument, null)); + } + + if (Uri.IsAbsoluteUri && _client.BaseAddress.Host != Uri.Host) + { + ThrowTerminatingError(new ErrorRecord(new ArgumentNullException(nameof(Uri), $"Invalid Host {Uri.Host}"), Guid.NewGuid().ToString(), + ErrorCategory.InvalidArgument, null)); + } + } } -} +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 b/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 index 27bcd9a5efb..c586fd7eaa1 100644 --- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 +++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 @@ -72,7 +72,7 @@ FormatsToProcess = './Microsoft.Graph.Authentication.format.ps1xml' FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = 'Connect-Graph', 'Disconnect-Graph', 'Get-MgContext' +CmdletsToExport = 'Connect-Graph', 'Disconnect-Graph', 'Get-MgContext', 'Invoke-GraphRequest' # Variables to export from this module # VariablesToExport = @() From f8e2eb27ccbcb05d2a3c2f8556eb470b61bb5080 Mon Sep 17 00:00:00 2001 From: George Ndungu Date: Mon, 8 Jun 2020 18:15:36 +0300 Subject: [PATCH 03/16] Adding help messages and extra validation. --- .../Cmdlets/InvokeGraphRequest.cs | 37 +++++++++++++++---- .../Authentication/Helpers/AttachDebugger.cs | 28 ++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 src/Authentication/Authentication/Helpers/AttachDebugger.cs diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index 7a43f494909..7baa5d5e08c 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -23,6 +23,7 @@ namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets [OutputType(typeof(Hashtable))] public class InvokeGraphRequest : PSCmdlet { + /// /// Contains the values of HttpVerbs /// @@ -45,24 +46,42 @@ public InvokeGraphRequest() /// /// Http Method /// - [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 1, Mandatory = true)] + [Parameter(ParameterSetName = Constants.UserParameterSet, + Position = 1, + Mandatory = true, + HelpMessage = "Http Method")] public HttpVerb Method { get; set; } /// /// Uri to call using the Graph HttpClient can be segments such as /beta/me /// or fully qualified url such as https://graph.microsoft.com/beta/me /// - [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 2, Mandatory = true)] + [Parameter(ParameterSetName = Constants.UserParameterSet, + Position = 2, + Mandatory = true, + HelpMessage = "Uri to call can be segments such as /beta/me or fully qualified https://graph.microsoft.com/beta/me")] public Uri Uri { get; set; } /// /// Optional Http Body /// - [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 3)] + [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 3, HelpMessage = "Request Body. Required when Method is Post or Patch")] public object Body { get; set; } + /// + /// Wait for .NET debugger to attach + /// + [Parameter(Mandatory = false, + DontShow = true, + HelpMessage = "Wait for .NET debugger to attach")] + public SwitchParameter Break { get; set; } + protected override void BeginProcessing() { + if (Break) + { + AttachDebugger.Break(); + } ValidateParameters(); base.BeginProcessing(); } @@ -70,8 +89,6 @@ protected override void BeginProcessing() protected override void ProcessRecord() { base.ProcessRecord(); - var authConfig = GraphSession.Instance.AuthContext; - var client = HttpHelpers.GetGraphHttpClient(authConfig); try { var httpMethod = GetHttpMethod(); @@ -80,7 +97,7 @@ protected override void ProcessRecord() Content = new StringContent(null != Body ? JsonConvert.SerializeObject(Body) : @"{}", Encoding.UTF8, CoreConstants.MimeTypeNames.Application.Json) }; - var responseMessage = client.SendAsync(requestMessage).GetAwaiter().GetResult(); + var responseMessage = _client.SendAsync(requestMessage).GetAwaiter().GetResult(); var responseString = responseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult(); var result = JsonConvert.DeserializeObject(responseString); if (responseMessage.IsSuccessStatusCode) @@ -151,7 +168,13 @@ private void ValidateParameters() if (Uri.IsAbsoluteUri && _client.BaseAddress.Host != Uri.Host) { - ThrowTerminatingError(new ErrorRecord(new ArgumentNullException(nameof(Uri), $"Invalid Host {Uri.Host}"), Guid.NewGuid().ToString(), + ThrowTerminatingError(new ErrorRecord(new ArgumentException(nameof(Uri), $"Invalid Host {Uri.Host}"), Guid.NewGuid().ToString(), + ErrorCategory.InvalidArgument, null)); + } + // When PATCH or POST is specified, ensure a body is present + if (this.Method == HttpVerb.PATCH || this.Method == HttpVerb.POST && this.Body == null) + { + ThrowTerminatingError(new ErrorRecord(new ArgumentNullException(nameof(Body), $"{nameof(this.Body)} is required when Method is {this.Method}"), Guid.NewGuid().ToString(), ErrorCategory.InvalidArgument, null)); } } diff --git a/src/Authentication/Authentication/Helpers/AttachDebugger.cs b/src/Authentication/Authentication/Helpers/AttachDebugger.cs new file mode 100644 index 00000000000..51934eae713 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/AttachDebugger.cs @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + internal static class AttachDebugger + { + internal static void Break() + { + while (!System.Diagnostics.Debugger.IsAttached) + { + System.Console.Error.WriteLine($"Waiting for debugger to attach to process {System.Diagnostics.Process.GetCurrentProcess().Id}"); + for (int i = 0; i < 50; i++) + { + if (System.Diagnostics.Debugger.IsAttached) + { + break; + } + System.Threading.Thread.Sleep(100); + System.Console.Error.Write("."); + } + System.Console.Error.WriteLine(); + } + System.Diagnostics.Debugger.Break(); + } + } +} \ No newline at end of file From d8f0cc7441036c7ee58da685afda6a80921b7c1e Mon Sep 17 00:00:00 2001 From: George Ndungu Date: Tue, 23 Jun 2020 01:29:47 +0300 Subject: [PATCH 04/16] - Enable users to specify their own Token (UserProvidedToken) - Enable users to download files to current directory either with custom name or inferred name. - Enable users to pass their own headers. - When Errors happen, print out the whole HttpResponse including headers. - Enable -PassThru, -Verbose (with useful messages) --- .../Cmdlets/InvokeGraphRequest.cs | 965 ++++++++++++++++-- .../Authentication/Helpers/AttachDebugger.cs | 19 +- .../Helpers/BufferingStreamReader.cs | 101 ++ .../Authentication/Helpers/ContentHelper.cs | 313 ++++++ .../Helpers/GraphRequestSession.cs | 47 + .../Authentication/Helpers/HttpHelpers.cs | 36 +- .../Helpers/HttpKnownHeaderNames.cs | 106 ++ .../Helpers/HttpMessageFormatter.cs | 359 +++++++ .../Helpers/HttpResponseException.cs | 22 + .../Helpers/InvokeGraphRequestAuthProvider.cs | 26 + .../Helpers/InvokeGraphRequestUserAgent.cs | 83 ++ .../Authentication/Helpers/PathUtils.cs | 41 + .../Authentication/Helpers/StreamHelper.cs | 178 ++++ .../Helpers/WebResponseHelper.cs | 66 ++ .../Microsoft.Graph.Authentication.csproj | 8 +- .../Microsoft.Graph.Authentication.nuspec | 2 +- .../Microsoft.Graph.Authentication.psd1 | 7 +- .../Models/GraphRequestAuthenticationType.cs | 8 + .../Models/GraphRequestMethod.cs | 14 + .../Authentication/Models/RestReturnType.cs | 21 + 20 files changed, 2317 insertions(+), 105 deletions(-) create mode 100644 src/Authentication/Authentication/Helpers/BufferingStreamReader.cs create mode 100644 src/Authentication/Authentication/Helpers/ContentHelper.cs create mode 100644 src/Authentication/Authentication/Helpers/GraphRequestSession.cs create mode 100644 src/Authentication/Authentication/Helpers/HttpKnownHeaderNames.cs create mode 100644 src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs create mode 100644 src/Authentication/Authentication/Helpers/HttpResponseException.cs create mode 100644 src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs create mode 100644 src/Authentication/Authentication/Helpers/InvokeGraphRequestUserAgent.cs create mode 100644 src/Authentication/Authentication/Helpers/PathUtils.cs create mode 100644 src/Authentication/Authentication/Helpers/StreamHelper.cs create mode 100644 src/Authentication/Authentication/Helpers/WebResponseHelper.cs create mode 100644 src/Authentication/Authentication/Models/GraphRequestAuthenticationType.cs create mode 100644 src/Authentication/Authentication/Models/GraphRequestMethod.cs create mode 100644 src/Authentication/Authentication/Models/RestReturnType.cs diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index 7baa5d5e08c..39257355ca1 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -1,87 +1,217 @@ -// ------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. -// ------------------------------------------------------------------------------ -namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets -{ - using System; - using System.Text; - using System.Threading; - using System.Net.Http; - using System.Collections; - using System.Net.Http.Headers; - using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Management.Automation; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; - using System.Management.Automation; - using System.Management.Automation.Language; - using System.Management.Automation.Runspaces; +using Microsoft.Graph.PowerShell.Authentication.Helpers; +using Microsoft.Graph.PowerShell.Authentication.Models; +using Microsoft.PowerShell.Commands; - using Newtonsoft.Json; - using Microsoft.Graph.PowerShell.Authentication.Helpers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using DriveNotFoundException = System.Management.Automation.DriveNotFoundException; +namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets +{ [Cmdlet(VerbsLifecycle.Invoke, "GraphRequest", DefaultParameterSetName = Constants.UserParameterSet)] - [OutputType(typeof(Hashtable))] public class InvokeGraphRequest : PSCmdlet { + private readonly CancellationTokenSource _cancelToken; + private readonly InvokeGraphRequestUserAgent _graphRequestUserAgent; + private string _originalFilePath; - /// - /// Contains the values of HttpVerbs - /// - public enum HttpVerb - { - GET, - POST, - PUT, - PATCH, - DELETE - } - private readonly HttpClient _client; public InvokeGraphRequest() { - this.Method = HttpVerb.GET; - var authConfig = GraphSession.Instance.AuthContext; - this._client = HttpHelpers.GetGraphHttpClient(authConfig); + _cancelToken = new CancellationTokenSource(); + _graphRequestUserAgent = new InvokeGraphRequestUserAgent(this); + Method = GraphRequestMethod.GET; + Authentication = GraphRequestAuthenticationType.Default; } /// - /// Http Method + /// Http Method /// [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 1, Mandatory = true, HelpMessage = "Http Method")] - public HttpVerb Method { get; set; } + public GraphRequestMethod Method { get; set; } /// - /// Uri to call using the Graph HttpClient can be segments such as /beta/me - /// or fully qualified url such as https://graph.microsoft.com/beta/me + /// Uri to call using the Graph HttpClient can be segments such as /beta/me + /// or fully qualified url such as https://graph.microsoft.com/beta/me /// [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 2, Mandatory = true, - HelpMessage = "Uri to call can be segments such as /beta/me or fully qualified https://graph.microsoft.com/beta/me")] + HelpMessage = + "Uri to call can be segments such as /beta/me or fully qualified https://graph.microsoft.com/beta/me")] public Uri Uri { get; set; } /// - /// Optional Http Body + /// Optional Http Body /// - [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 3, HelpMessage = "Request Body. Required when Method is Post or Patch")] + [Parameter(ParameterSetName = Constants.UserParameterSet, + Position = 3, + HelpMessage = "Request Body. Required when Method is Post or Patch", + ValueFromPipeline = true)] public object Body { get; set; } /// - /// Wait for .NET debugger to attach + /// Optional Custom Headers + /// + [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + Position = 4, + HelpMessage = "Optional Custom Headers")] + public IDictionary Headers { get; set; } + + /// + /// Output file where the response body will be saved + /// + [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + Position = 5, + HelpMessage = "Output file where the response body will be saved")] + public string OutFile { get; set; } + + /// + /// Infer Download FileName from ContentDisposition Header + /// + [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + Position = 6, + HelpMessage = "Infer Download FileName")] + public SwitchParameter InferOutFileName { get; set; } + + /// + /// Gets or sets the InFile property. + /// + [Parameter] + [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + Position = 7, + HelpMessage = "Infile to Send in the Request")] + public virtual string InFile { get; set; } + + /// + /// Indicates that the cmdlet returns the results, in addition to writing them to a file. + /// only valid when the OutFile parameter is also used. + /// + [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + Position = 8, + HelpMessage = + "Indicates that the cmdlet returns the results, in addition to writing them to a file. Only valid when the OutFile parameter is also used. ")] + public SwitchParameter PassThru { get; set; } + + /// + /// OAuth or Bearer Token to use instead of already acquired token + /// + [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + Position = 9, + HelpMessage = "OAuth or Bearer Token to use instead of already acquired token")] + public string Token { get; set; } + + /// + /// Add headers to Request Header collection without validation + /// + [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + Position = 10, + HelpMessage = "Add headers to Request Header collection without validation")] + public SwitchParameter SkipHeaderValidation { get; set; } + + /// + /// Custom Content Type + /// + [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + Position = 11, + HelpMessage = "Custom Content Type")] + public virtual string ContentType { get; set; } + + /// + /// Graph Authentication Type + /// + [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + Position = 12, + HelpMessage = "Graph Authentication Type")] + public GraphRequestAuthenticationType Authentication { get; set; } + + /// + /// Gets or sets the SessionVariable property. + /// + [Parameter(Position = 13, ParameterSetName = Constants.UserParameterSet, + Mandatory = false, HelpMessage = "Session Variable")] + [Alias("SV")] + public string SessionVariable { get; set; } + + /// + /// Response Headers Variable + /// + [Parameter(Position = 14, ParameterSetName = Constants.UserParameterSet, + Mandatory = false, + HelpMessage = "Response Headers Variable")] + [Alias("RHV")] + public string ResponseHeadersVariable { get; set; } + + /// + /// Response Status Code Variable + /// + [Parameter(Position = 15, ParameterSetName = Constants.UserParameterSet, + Mandatory = false, + HelpMessage = "Response Status Code Variable")] + public string StatusCodeVariable { get; set; } + + /// + /// Gets or sets whether to skip checking HTTP status for error codes. + /// + [Parameter(Position = 16, ParameterSetName = Constants.UserParameterSet, Mandatory = false, HelpMessage = "Skip Checking Http Errors")] + public virtual SwitchParameter SkipHttpErrorCheck { get; set; } + /// + /// Gets or sets the Session property. + /// + [Parameter(Mandatory = false, + Position = 17, ParameterSetName = Constants.UserParameterSet, + HelpMessage = "Custom Graph Request Session")] + public GraphRequestSession GraphRequestSession { get; set; } + + /// + /// Custom User Specified User Agent + /// + [Parameter(Mandatory = false, + Position = 18, ParameterSetName = Constants.UserParameterSet, + HelpMessage = "Custom User Specified User Agent")] + public string UserAgent { get; set; } + + /// + /// Wait for .NET debugger to attach /// [Parameter(Mandatory = false, DontShow = true, HelpMessage = "Wait for .NET debugger to attach")] public SwitchParameter Break { get; set; } + internal string QualifiedOutFile => QualifyFilePath(OutFile); + + internal bool ShouldSaveToOutFile => !string.IsNullOrEmpty(OutFile); + + internal bool ShouldWriteToPipeline => (!ShouldSaveToOutFile && !InferOutFileName) || PassThru; + + internal bool ShouldCheckHttpStatus => !SkipHttpErrorCheck; + + #region CmdLet LifeCycle protected override void BeginProcessing() { if (Break) { - AttachDebugger.Break(); + AttachDebugger.Break(this); } + ValidateParameters(); base.BeginProcessing(); } @@ -91,23 +221,37 @@ protected override void ProcessRecord() base.ProcessRecord(); try { - var httpMethod = GetHttpMethod(); - var requestMessage = new HttpRequestMessage(httpMethod, this.Uri) - { - Content = new StringContent(null != Body ? JsonConvert.SerializeObject(Body) : @"{}", Encoding.UTF8, - CoreConstants.MimeTypeNames.Application.Json) - }; - var responseMessage = _client.SendAsync(requestMessage).GetAwaiter().GetResult(); - var responseString = responseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - var result = JsonConvert.DeserializeObject(responseString); - if (responseMessage.IsSuccessStatusCode) + PrepareSession(); + using var client = GetHttpClient(); + ValidateRequestUri(client); + using var httpRequestMessage = GetRequest(Uri); + using var httpRequestMessageFormatter = new HttpMessageFormatter(httpRequestMessage); + + FillRequestStream(httpRequestMessage); + try { - WriteObject(result); + ReportRequestStatus(httpRequestMessageFormatter.HttpRequestMessage); + + var httpResponseMessage = GetResponse(client, httpRequestMessage); + using var httpResponseMessageFormatter = new HttpMessageFormatter(httpResponseMessage); + ReportResponseStatus(httpResponseMessageFormatter.HttpResponseMessage); + + var isSuccess = httpResponseMessage.IsSuccessStatusCode; + if (ShouldCheckHttpStatus && !isSuccess) + { + var httpErrorRecord = GenerateHttpErrorRecord(httpResponseMessageFormatter, httpRequestMessage); + ThrowTerminatingError(httpErrorRecord); + } + ProcessResponse(httpResponseMessage); } - else + catch (HttpRequestException ex) { - WriteError(new ErrorRecord(new Exception(responseString), Guid.NewGuid().ToString(), - ErrorCategory.InvalidResult, null)); + var er = new ErrorRecord(ex, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, httpRequestMessage); + if (ex.InnerException != null) + { + er.ErrorDetails = new ErrorDetails(ex.InnerException.Message); + } + ThrowTerminatingError(er); } } catch (HttpRequestException httpRequestException) @@ -118,65 +262,704 @@ protected override void ProcessRecord() } catch (Exception exception) { - WriteError(new ErrorRecord(exception, ErrorCategory.ConnectionError.ToString(), ErrorCategory.InvalidOperation, null)); + WriteError(new ErrorRecord(exception, ErrorCategory.ConnectionError.ToString(), + ErrorCategory.InvalidOperation, null)); throw; } } - /// - /// Maps from HttpVerb to System.Net.Http.HttpMethod - /// - /// System.Net.Http.HttpMethod - private System.Net.Http.HttpMethod GetHttpMethod() + protected override void EndProcessing() + { + base.EndProcessing(); + } + + protected override void StopProcessing() { - switch (Method) + base.StopProcessing(); + } + #endregion + private static ErrorRecord GenerateHttpErrorRecord(HttpMessageFormatter httpResponseMessageFormatter, HttpRequestMessage httpRequestMessage) + { + var currentResponse = httpResponseMessageFormatter.HttpResponseMessage; + var errorMessage = StringFormatCurrentCulture("ResponseStatusCodeFailure {0} {1}", currentResponse.StatusCode, currentResponse.ReasonPhrase); + var httpException = new HttpResponseException(errorMessage, currentResponse); + var errorRecord = new ErrorRecord(httpException, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, httpRequestMessage); + var detailMsg = httpResponseMessageFormatter.ReadAsStringAsync() + .GetAwaiter() + .GetResult(); + if (!string.IsNullOrEmpty(detailMsg)) { - case HttpVerb.GET: - return System.Net.Http.HttpMethod.Get; - case HttpVerb.POST: - return System.Net.Http.HttpMethod.Post; - case HttpVerb.PATCH: - // System.Net.Http.HttpMethod does not contain HttpMethod.Patch in netstandard2.0 - // https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpmethod?view=netstandard-2.0 - return new System.Net.Http.HttpMethod("PATCH"); - case HttpVerb.PUT: - return System.Net.Http.HttpMethod.Put; - case HttpVerb.DELETE: - return System.Net.Http.HttpMethod.Delete; - default: - throw new ArgumentOutOfRangeException(); + errorRecord.ErrorDetails = new ErrorDetails(detailMsg); } + + return errorRecord; } - protected override void EndProcessing() + private void ReportRequestStatus(HttpRequestMessage requestMessage) { - base.EndProcessing(); + long requestContentLength = 0; + if (requestMessage.Content != null) + { + requestContentLength = requestMessage.Content.Headers.ContentLength.Value; + } + var reqVerboseMsg = StringFormatCurrentCulture("{0} {1} with {2}-byte payload", + requestMessage.Method, + requestMessage.RequestUri, + requestContentLength); + WriteVerbose(reqVerboseMsg); } - protected override void StopProcessing() + private void ReportResponseStatus(HttpResponseMessage responseMessage) { - base.StopProcessing(); + var contentType = ContentHelper.GetContentType(responseMessage); + var respVerboseMsg = StringFormatCurrentCulture("received {0}-byte response of content type {1}", + responseMessage.Content.Headers.ContentLength, + contentType); + WriteVerbose(respVerboseMsg); } - private void ValidateParameters() + private static string StringFormatCurrentCulture(string format, params object[] args) { - if (Uri == null) + return string.Format(CultureInfo.CurrentCulture, format, args); + } + + private static string FormatDictionary(IDictionary content) + { + if (content == null) + throw new ArgumentNullException(nameof(content)); + + var bodyBuilder = new StringBuilder(); + foreach (string key in content.Keys) + { + if (0 < bodyBuilder.Length) bodyBuilder.Append("&"); + + var value = content[key]; + + // URLEncode the key and value + var encodedKey = WebUtility.UrlEncode(key); + var encodedValue = string.Empty; + if (value != null) encodedValue = WebUtility.UrlEncode(value.ToString()); + + bodyBuilder.AppendFormat("{0}={1}", encodedKey, encodedValue); + } + + return bodyBuilder.ToString(); + } + + private HttpRequestMessage GetRequest(Uri uri) + { + var requestUri = PrepareUri(uri); + var httpMethod = GetHttpMethod(Method); + // create the base WebRequest object + var request = new HttpRequestMessage(httpMethod, requestUri); + + // pull in session data + if (GraphRequestSession.Headers.Count > 0) + { + GraphRequestSession.ContentHeaders.Clear(); + foreach (var entry in GraphRequestSession.Headers) + { + if (HttpKnownHeaderNames.ContentHeaders.Contains(entry.Key)) + { + GraphRequestSession.ContentHeaders.Add(entry.Key, entry.Value); + } + else + { + if (SkipHeaderValidation) + { + request.Headers.TryAddWithoutValidation(entry.Key, entry.Value); + } + else + { + request.Headers.Add(entry.Key, entry.Value); + } + } + } + } + + // Set 'Transfer-Encoding: chunked' if 'Transfer-Encoding' is specified + if (GraphRequestSession.Headers.ContainsKey(HttpKnownHeaderNames.TransferEncoding)) + { + request.Headers.TransferEncodingChunked = true; + } + + // Set 'User-Agent' if WebSession.Headers doesn't already contain it + if (GraphRequestSession.Headers.TryGetValue(HttpKnownHeaderNames.UserAgent, out var userAgent)) + { + GraphRequestSession.UserAgent = userAgent; + } + else + { + if (SkipHeaderValidation) + { + request.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.UserAgent, + GraphRequestSession.UserAgent); + } + else + { + request.Headers.Add(HttpKnownHeaderNames.UserAgent, GraphRequestSession.UserAgent); + } + } + + return request; + } + + private Uri PrepareUri(Uri uri) + { + // before creating the web request, + // preprocess Body if content is a dictionary and method is GET (set as query) + LanguagePrimitives.TryConvertTo(Body, out IDictionary bodyAsDictionary); + if (bodyAsDictionary != null && Method == GraphRequestMethod.GET) + { + var uriBuilder = new UriBuilder(uri); + if (uriBuilder.Query != null && uriBuilder.Query.Length > 1) + uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + FormatDictionary(bodyAsDictionary); + else + uriBuilder.Query = FormatDictionary(bodyAsDictionary); + + uri = uriBuilder.Uri; + // set body to null to prevent later FillRequestStream + Body = null; + } + + return uri; + } + + private static RestReturnType CheckReturnType(HttpResponseMessage response) + { + if (response == null) throw new ArgumentNullException(nameof(response)); + + var rt = RestReturnType.Detect; + var contentType = ContentHelper.GetContentType(response); + if (string.IsNullOrEmpty(contentType)) + rt = RestReturnType.Detect; + else if (ContentHelper.IsJson(contentType)) + rt = RestReturnType.Json; + else if (ContentHelper.IsXml(contentType)) rt = RestReturnType.Xml; + + return rt; + } + + internal void ProcessResponse(HttpResponseMessage response) + { + if (response == null) throw new ArgumentNullException(nameof(response)); + + var baseResponseStream = StreamHelper.GetResponseStream(response); + + if (ShouldWriteToPipeline) + { + using var responseStream = new BufferingStreamReader(baseResponseStream); + // determine the response type + var returnType = CheckReturnType(response); + + // Try to get the response encoding from the ContentType header. + Encoding encoding = null; + var charSet = response.Content.Headers.ContentType?.CharSet; + if (!string.IsNullOrEmpty(charSet)) + { + // NOTE: Don't use ContentHelper.GetEncoding; it returns a + // default which bypasses checking for a meta charset value. + StreamHelper.TryGetEncoding(charSet, out encoding); + } + + if (string.IsNullOrEmpty(charSet) && returnType == RestReturnType.Json) + { + encoding = Encoding.UTF8; + } + + Exception ex = null; + + var str = StreamHelper.DecodeStream(responseStream, ref encoding); + + string encodingVerboseName; + try + { + encodingVerboseName = string.IsNullOrEmpty(encoding.HeaderName) + ? encoding.EncodingName + : encoding.HeaderName; + } + catch (NotSupportedException) + { + encodingVerboseName = encoding.EncodingName; + } + + // NOTE: Tests use this verbose output to verify the encoding. + WriteVerbose(StringFormatCurrentCulture("Content encoding: {0}", encodingVerboseName)); + var convertSuccess = TryConvert(str, out var obj, ref ex); + if (!convertSuccess) + { + // fallback to string + obj = str; + } + + WriteObject(obj); + } + + if (ShouldSaveToOutFile) + { + StreamHelper.SaveStreamToFile(baseResponseStream, QualifiedOutFile, this, _cancelToken.Token); + } + + if (InferOutFileName.IsPresent) + { + if (response.Content.Headers.ContentDisposition != null) + { + if (!string.IsNullOrWhiteSpace(response.Content.Headers.ContentDisposition.FileName)) + { + var fileName = response.Content.Headers.ContentDisposition.FileNameStar; + var fullFileName = QualifyFilePath(fileName); + WriteVerbose(string.Format(CultureInfo.InvariantCulture, "Inferred File Name {0} Saving to {1}", fileName, fullFileName)); + StreamHelper.SaveStreamToFile(baseResponseStream, fullFileName, this, + _cancelToken.Token); + } + } + else + { + WriteVerbose("Could not Infer File Name"); + } + } + + if (!string.IsNullOrEmpty(StatusCodeVariable)) + { + var vi = SessionState.PSVariable; + vi.Set(StatusCodeVariable, (int)response.StatusCode); + } + + if (!string.IsNullOrEmpty(ResponseHeadersVariable)) + { + var vi = SessionState.PSVariable; + vi.Set(ResponseHeadersVariable, WebResponseHelper.GetHeadersDictionary(response)); + } + } + + private static bool TryConvert(string str, out object obj, ref Exception exRef) + { + var converted = false; + try + { + obj = JsonConvert.DeserializeObject(str); + if (obj == null) + JToken.Parse(str); + else + converted = true; + } + catch (JsonException ex) + { + var msg = string.Format(CultureInfo.CurrentCulture, "JsonDeserializationFailed", ex.Message); + exRef = new ArgumentException(msg, ex); + obj = null; + } + catch (Exception jsonParseException) + { + exRef = jsonParseException; + obj = null; + } + + return converted; + } + + private IAuthenticationProvider GetAuthProvider() + { + if (Authentication == GraphRequestAuthenticationType.UserProvidedToken) + { + return new InvokeGraphRequestAuthProvider(GraphRequestSession); + } + + return AuthenticationHelpers.GetAuthProvider(GraphSession.Instance.AuthContext); + } + + private HttpClient GetHttpClient() + { + var provider = GetAuthProvider(); + var client = HttpHelpers.GetGraphHttpClient(provider); + return client; + } + + private HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (request == null) throw new ArgumentNullException(nameof(request)); + + var cancellationToken = _cancelToken.Token; + var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .GetAwaiter() + .GetResult(); + return response; + } + + private long SetRequestContent(HttpRequestMessage request, IDictionary content) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + if (content == null) + throw new ArgumentNullException(nameof(content)); + + var body = FormatDictionary(content); + return SetRequestContent(request, body); + } + + private long SetRequestContent(HttpRequestMessage request, string content) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (content == null) + return 0; + + Encoding encoding = null; + if (ContentType != null) + // If Content-Type contains the encoding format (as CharSet), use this encoding format + // to encode the Body of the WebRequest sent to the server. Default Encoding format + // would be used if Charset is not supplied in the Content-Type property. + try + { + var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(ContentType); + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.CharSet)) + encoding = Encoding.GetEncoding(mediaTypeHeaderValue.CharSet); + } + catch (FormatException ex) + { + if (!SkipHeaderValidation) + { + var outerEx = new ValidationMetadataException("ContentTypeException", ex); + var er = new ErrorRecord(outerEx, "WebCmdletContentTypeException", + ErrorCategory.InvalidArgument, ContentType); + ThrowTerminatingError(er); + } + } + catch (ArgumentException ex) + { + if (!SkipHeaderValidation) + { + var outerEx = new ValidationMetadataException("ContentTypeException", ex); + var er = new ErrorRecord(outerEx, "WebCmdletContentTypeException", + ErrorCategory.InvalidArgument, ContentType); + ThrowTerminatingError(er); + } + } + + var bytes = StreamHelper.EncodeToBytes(content, encoding); + var byteArrayContent = new ByteArrayContent(bytes); + request.Content = byteArrayContent; + + return byteArrayContent.Headers.ContentLength.Value; + } + + private void FillRequestStream(HttpRequestMessage request) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + if (ContentType != null) + { + GraphRequestSession.ContentHeaders[HttpKnownHeaderNames.ContentType] = ContentType; + } + else if (Method == GraphRequestMethod.POST) + { + GraphRequestSession.ContentHeaders.TryGetValue(HttpKnownHeaderNames.ContentType, out var contentType); + if (string.IsNullOrWhiteSpace(contentType)) + { + // Assume application/json of not set by user + GraphRequestSession.ContentHeaders[HttpKnownHeaderNames.ContentType] = + CoreConstants.MimeTypeNames.Application.Json; + } + } + + // coerce body into a usable form + if (Body != null) + { + var content = Body; + + // make sure we're using the base object of the body, not the PSObject wrapper + var psBody = Body as PSObject; + if (psBody != null) + { + content = psBody.BaseObject; + } + else if (content is IDictionary dictionary && request.Method != HttpMethod.Get) + { + SetRequestContent(request, dictionary); + } + else if (content is Stream stream) + { + SetRequestContent(request, stream); + } + else if (content is byte[] bytes) + { + SetRequestContent(request, bytes); + } + else + { + SetRequestContent(request, + (string)LanguagePrimitives.ConvertTo(content, typeof(string), CultureInfo.InvariantCulture)); + } + } + else if (InFile != null) // copy InFile data + { + try + { + // open the input file + SetRequestContent(request, new FileStream(InFile, FileMode.Open, FileAccess.Read, FileShare.Read)); + } + catch (UnauthorizedAccessException) + { + var msg = string.Format(CultureInfo.InvariantCulture, "AccessDenied", + _originalFilePath); + throw new UnauthorizedAccessException(msg); + } + } + + // Add the content headers + if (request.Content == null) + { + request.Content = new StringContent(string.Empty); + request.Content.Headers.Clear(); + } + + foreach (var entry in GraphRequestSession.ContentHeaders) { - ThrowTerminatingError(new ErrorRecord(new ArgumentNullException(nameof(Uri), $"Must specify {Uri}"), Guid.NewGuid().ToString(), - ErrorCategory.InvalidArgument, null)); + if (!string.IsNullOrWhiteSpace(entry.Value)) + { + if (SkipHeaderValidation) + request.Content.Headers.TryAddWithoutValidation(entry.Key, entry.Value); + else + try + { + request.Content.Headers.Add(entry.Key, entry.Value); + } + catch (FormatException ex) + { + var outerEx = new ValidationMetadataException("ContentTypeException", ex); + var er = new ErrorRecord(outerEx, "WebCmdletContentTypeException", + ErrorCategory.InvalidArgument, ContentType); + ThrowTerminatingError(er); + } + } + } + } + + private static long SetRequestContent(HttpRequestMessage request, byte[] content) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + if (content == null) + return 0; + + var byteArrayContent = new ByteArrayContent(content); + request.Content = byteArrayContent; + + return byteArrayContent.Headers.ContentLength.Value; + } + + private static long SetRequestContent(HttpRequestMessage request, Stream contentStream) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + if (contentStream == null) + throw new ArgumentNullException(nameof(contentStream)); + + var streamContent = new StreamContent(contentStream); + request.Content = streamContent; + + return streamContent.Headers.ContentLength.Value; + } + + /// + /// Maps from HttpVerb to System.Net.Http.HttpMethod + /// + /// System.Net.Http.HttpMethod + private static HttpMethod GetHttpMethod(GraphRequestMethod graphRequestMethod) + { + return new HttpMethod(graphRequestMethod.ToString().ToUpperInvariant()); + } + /// + /// Prepare GraphRequestSession to be used downstream. + /// + internal virtual void PrepareSession() + { + // Create a new GraphRequestSession object to work with if one is not supplied + GraphRequestSession ??= new GraphRequestSession(); + if (SessionVariable != null) + { + // save the session back to the PS environment if requested + var vi = SessionState.PSVariable; + vi.Set(SessionVariable, GraphRequestSession); } - if (Uri.IsAbsoluteUri && _client.BaseAddress.Host != Uri.Host) + if (Authentication == GraphRequestAuthenticationType.UserProvidedToken && !string.IsNullOrWhiteSpace(Token)) { - ThrowTerminatingError(new ErrorRecord(new ArgumentException(nameof(Uri), $"Invalid Host {Uri.Host}"), Guid.NewGuid().ToString(), - ErrorCategory.InvalidArgument, null)); + GraphRequestSession.Token = Token; + GraphRequestSession.AuthenticationType = Authentication; } + + // + // Handle Custom User Agents + // + GraphRequestSession.UserAgent = UserAgent ?? _graphRequestUserAgent.UserAgent; + + // Store the other supplied headers + if (Headers != null) + { + foreach (var key in Headers.Keys) + { + var value = Headers[key]; + + // null is not valid value for header. + // We silently ignore header if value is null. + if (!(value is null)) + { + // add the header value (or overwrite it if already present) + GraphRequestSession.Headers[key] = value; + } + } + } + } + /// + /// Validate the Request Uri must have the same Host as GraphHttpClient BaseAddress. + /// + /// + private void ValidateRequestUri(HttpClient httpClient) + { + if (Uri == null) + { + var error = GetValidationError($"Must specify {nameof(Uri)}", "InvokeGraphRequestInvalidHost", + nameof(Uri)); + ThrowTerminatingError(error); + } + // Ensure that the Passed in Uri has the same Host as the HttpClient. + if (Uri.IsAbsoluteUri && httpClient.BaseAddress.Host != Uri.Host) + { + var error = GetValidationError($"Invalid Host {Uri.Host}", "InvokeGraphRequestInvalidHost", + nameof(Uri)); + ThrowTerminatingError(error); + } + } + /// + /// Validate Passed In Parameters + /// + private void ValidateParameters() + { + if (GraphRequestSession != null && SessionVariable != null) + { + var error = GetValidationError( + "The cmdlet cannot run because the following conflicting parameters are specified: Session and SessionVariable. Specify either Session or SessionVariable, then retry.", + "WebCmdletSessionConflictException"); + ThrowTerminatingError(error); + } + // When PATCH or POST is specified, ensure a body is present - if (this.Method == HttpVerb.PATCH || this.Method == HttpVerb.POST && this.Body == null) + if (Method == GraphRequestMethod.PATCH || Method == GraphRequestMethod.POST && Body == null) + { + var error = GetValidationError($"{nameof(Body)} is required when Method is {Method}", + "InvokeGraphRequestBodyMissingWhenMethodIsSpecified", nameof(Body)); + ThrowTerminatingError(error); + } + + if (PassThru && OutFile == null) + { + var error = GetValidationError($"{nameof(OutFile)} is missing", + "InvokeGraphRequestOutFileMissingException", nameof(PassThru)); + ThrowTerminatingError(error); + } + + if (Authentication == GraphRequestAuthenticationType.Default && !string.IsNullOrWhiteSpace(Token)) { - ThrowTerminatingError(new ErrorRecord(new ArgumentNullException(nameof(Body), $"{nameof(this.Body)} is required when Method is {this.Method}"), Guid.NewGuid().ToString(), - ErrorCategory.InvalidArgument, null)); + var error = GetValidationError("AuthenticationTokenConflict", + "WebCmdletAuthenticationTokenConflictException"); + ThrowTerminatingError(error); } + // Token shouldn't be null when UserProvidedToken is specified + if (Authentication == GraphRequestAuthenticationType.UserProvidedToken && string.IsNullOrWhiteSpace(Token)) + { + var error = GetValidationError("AuthenticationCredentialNotSupplied", + "WebCmdletAuthenticationCredentialNotSuppliedException"); + ThrowTerminatingError(error); + } + + // Only Body or InFile can be specified at a time + if (Body != null && InFile != null) + { + var error = GetValidationError("BodyConflict", + "WebCmdletBodyConflictException"); + ThrowTerminatingError(error); + } + + // Ensure InFile is an Existing Item + if (InFile != null) + { + ErrorRecord errorRecord = null; + + try + { + var providerPaths = GetResolvedProviderPathFromPSPath(InFile, out var provider); + + if (!provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase)) + { + errorRecord = GetValidationError("NotFilesystemPath", + "WebCmdletInFileNotFilesystemPathException", InFile); + } + else + { + if (providerPaths.Count > 1) + { + errorRecord = GetValidationError("MultiplePathsResolved", + "WebCmdletInFileMultiplePathsResolvedException", InFile); + } + else if (providerPaths.Count == 0) + { + errorRecord = GetValidationError("NoPathResolved", + "WebCmdletInFileNoPathResolvedException", InFile); + } + else + { + if (Directory.Exists(providerPaths[0])) + errorRecord = GetValidationError("DirectoryPathSpecified", + "WebCmdletInFileNotFilePathException", InFile); + + _originalFilePath = InFile; + InFile = providerPaths[0]; + } + } + } + catch (ItemNotFoundException pathNotFound) + { + errorRecord = new ErrorRecord(pathNotFound.ErrorRecord, pathNotFound); + } + catch (ProviderNotFoundException providerNotFound) + { + errorRecord = new ErrorRecord(providerNotFound.ErrorRecord, providerNotFound); + } + catch (DriveNotFoundException driveNotFound) + { + errorRecord = new ErrorRecord(driveNotFound.ErrorRecord, driveNotFound); + } + + if (errorRecord != null) ThrowTerminatingError(errorRecord); + } + } + + private ErrorRecord GetValidationError(string msg, string errorId) + { + var ex = new ValidationMetadataException(msg); + var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); + return error; + } + + private ErrorRecord GetValidationError(string msg, string errorId, params object[] args) + { + msg = string.Format(CultureInfo.InvariantCulture, msg, args); + var ex = new ValidationMetadataException(msg); + var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); + return error; + } + /// + /// Generate a fully qualified file path + /// + /// + /// + private string QualifyFilePath(string path) + { + var resolvedFilePath = PathUtils.ResolveFilePath(path, this, true); + return resolvedFilePath; } } } \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/AttachDebugger.cs b/src/Authentication/Authentication/Helpers/AttachDebugger.cs index 51934eae713..6e8ece20edc 100644 --- a/src/Authentication/Authentication/Helpers/AttachDebugger.cs +++ b/src/Authentication/Authentication/Helpers/AttachDebugger.cs @@ -2,25 +2,34 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +using System; +using System.Management.Automation; + +using Microsoft.Graph.PowerShell.Authentication.Cmdlets; + namespace Microsoft.Graph.PowerShell.Authentication.Helpers { internal static class AttachDebugger { - internal static void Break() + internal static void Break(PSCmdlet invokedCmdLet) { while (!System.Diagnostics.Debugger.IsAttached) { - System.Console.Error.WriteLine($"Waiting for debugger to attach to process {System.Diagnostics.Process.GetCurrentProcess().Id}"); - for (int i = 0; i < 50; i++) + Console.Error.WriteLine($"Waiting for debugger to attach to process {System.Diagnostics.Process.GetCurrentProcess().Id}"); + //invokedCmdLet.WriteDebug($"Waiting for debugger to attach to process {System.Diagnostics.Process.GetCurrentProcess().Id}"); + for (var i = 0; i < 50; i++) { if (System.Diagnostics.Debugger.IsAttached) { break; } System.Threading.Thread.Sleep(100); - System.Console.Error.Write("."); + //invokedCmdLet.WriteDebug("."); + Console.Error.Write("."); } - System.Console.Error.WriteLine(); + //invokedCmdLet.WriteDebug(Environment.NewLine); + Console.Error.WriteLine(); } System.Diagnostics.Debugger.Break(); } diff --git a/src/Authentication/Authentication/Helpers/BufferingStreamReader.cs b/src/Authentication/Authentication/Helpers/BufferingStreamReader.cs new file mode 100644 index 00000000000..c4fb4f58950 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/BufferingStreamReader.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + internal class BufferingStreamReader : Stream + { + internal BufferingStreamReader(Stream baseStream) + { + _baseStream = baseStream; + _streamBuffer = new MemoryStream(); + _length = long.MaxValue; + _copyBuffer = new byte[4096]; + } + + private readonly Stream _baseStream; + private readonly MemoryStream _streamBuffer; + private readonly byte[] _copyBuffer; + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override void Flush() + { + _streamBuffer.SetLength(0); + } + + public override long Length => _length; + + private long _length; + + public override long Position + { + get => _streamBuffer.Position; + + set => _streamBuffer.Position = value; + } + + public override int Read(byte[] buffer, int offset, int count) + { + long previousPosition = Position; + bool consumedStream = false; + int totalCount = count; + while ((!consumedStream) && + ((Position + totalCount) > _streamBuffer.Length)) + { + // If we don't have enough data to fill this from memory, cache more. + // We try to read 4096 bytes from base stream every time, so at most we + // may cache 4095 bytes more than what is required by the Read operation. + int bytesRead = _baseStream.Read(_copyBuffer, 0, _copyBuffer.Length); + + if (_streamBuffer.Position < _streamBuffer.Length) + { + // Win8: 651902 no need to -1 here as Position refers to the place + // where we can start writing from. + _streamBuffer.Position = _streamBuffer.Length; + } + + _streamBuffer.Write(_copyBuffer, 0, bytesRead); + + totalCount -= bytesRead; + if (bytesRead < _copyBuffer.Length) + { + consumedStream = true; + } + } + + // Reset our backing store to its official position, as reading + // for the CopyTo updates the position. + _streamBuffer.Seek(previousPosition, SeekOrigin.Begin); + + // Read from the backing store into the requested buffer. + int read = _streamBuffer.Read(buffer, offset, count); + + if (read < count) + { + SetLength(Position); + } + + return read; + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _streamBuffer.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _length = value; + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/ContentHelper.cs b/src/Authentication/Authentication/Helpers/ContentHelper.cs new file mode 100644 index 00000000000..c90d1bddc39 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/ContentHelper.cs @@ -0,0 +1,313 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using Microsoft.Win32; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + internal static class ContentHelper + { + #region Constants + + // default codepage encoding for web content. See RFC 2616. + private const string _defaultCodePage = "ISO-8859-1"; + + #endregion Constants + + #region Fields + + // used to split contentType arguments + private static readonly char[] s_contentTypeParamSeparator = {';'}; + + #endregion Fields + + #region Internal Methods + + internal static string GetContentType(HttpResponseMessage response) + { + // ContentType may not exist in response header. Return null if not. + return response.Content.Headers.ContentType?.MediaType; + } + + internal static Encoding GetDefaultEncoding() + { + return GetEncodingOrDefault(null); + } + + internal static Encoding GetEncoding(HttpResponseMessage response) + { + // ContentType may not exist in response header. + var charSet = response.Content.Headers.ContentType?.CharSet; + return GetEncodingOrDefault(charSet); + } + + internal static Encoding GetEncodingOrDefault(string characterSet) + { + // get the name of the codepage to use for response content + var codepage = string.IsNullOrEmpty(characterSet) ? _defaultCodePage : characterSet; + Encoding encoding = null; + + try + { + encoding = Encoding.GetEncoding(codepage); + } + catch (ArgumentException) + { + // 0, default code page + encoding = Encoding.GetEncoding(0); + } + + return encoding; + } + + internal static StringBuilder GetRawContentHeader(HttpResponseMessage response) + { + var raw = new StringBuilder(); + + var protocol = WebResponseHelper.GetProtocol(response); + if (!string.IsNullOrEmpty(protocol)) + { + var statusCode = WebResponseHelper.GetStatusCode(response); + var statusDescription = WebResponseHelper.GetStatusDescription(response); + raw.AppendFormat("{0} {1} {2}", protocol, statusCode, statusDescription); + raw.AppendLine(); + } + + HttpHeaders[] headerCollections = + { + response.Headers, + response.Content == null ? null : response.Content.Headers + }; + + foreach (var headerCollection in headerCollections) + { + if (headerCollection == null) + { + continue; + } + + foreach (var header in headerCollection) + { + // Headers may have multiple entries with different values + foreach (var headerValue in header.Value) + { + raw.Append(header.Key); + raw.Append(": "); + raw.Append(headerValue); + raw.AppendLine(); + } + } + } + + raw.AppendLine(); + return raw; + } + + internal static bool IsJson(string contentType) + { + contentType = GetContentTypeSignature(contentType); + return CheckIsJson(contentType); + } + + internal static bool IsText(string contentType) + { + contentType = GetContentTypeSignature(contentType); + return CheckIsText(contentType); + } + + internal static bool IsXml(string contentType) + { + contentType = GetContentTypeSignature(contentType); + return CheckIsXml(contentType); + } + + #endregion Internal Methods + + #region Private Helper Methods + + private static bool CheckIsJson(string contentType) + { + if (string.IsNullOrEmpty(contentType)) + return false; + + // the correct type for JSON content, as specified in RFC 4627 + var isJson = contentType.Equals("application/json", StringComparison.OrdinalIgnoreCase); + + // add in these other "javascript" related types that + // sometimes get sent down as the mime type for JSON content + isJson |= contentType.Equals("text/json", StringComparison.OrdinalIgnoreCase) + || contentType.Equals("application/x-javascript", StringComparison.OrdinalIgnoreCase) + || contentType.Equals("text/x-javascript", StringComparison.OrdinalIgnoreCase) + || contentType.Equals("application/javascript", StringComparison.OrdinalIgnoreCase) + || contentType.Equals("text/javascript", StringComparison.OrdinalIgnoreCase); + + return isJson; + } + + private static bool CheckIsText(string contentType) + { + if (string.IsNullOrEmpty(contentType)) + return false; + + // any text, xml or json types are text + var isText = contentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) + || CheckIsXml(contentType) + || CheckIsJson(contentType); + + // Further content type analysis is available on Windows + if (Platform.IsWindows && !isText) + { + // Media types registered with Windows as having a perceived type of text, are text + using (var contentTypeKey = + Registry.ClassesRoot.OpenSubKey(@"MIME\Database\Content Type\" + contentType)) + { + if (contentTypeKey != null) + { + var extension = contentTypeKey.GetValue("Extension") as string; + if (extension != null) + { + using (var extensionKey = Registry.ClassesRoot.OpenSubKey(extension)) + { + if (extensionKey != null) + { + var perceivedType = extensionKey.GetValue("PerceivedType") as string; + isText = perceivedType == "text"; + } + } + } + } + } + } + + return isText; + } + + private static bool CheckIsXml(string contentType) + { + if (string.IsNullOrEmpty(contentType)) + return false; + + // RFC 3023: Media types with the suffix "+xml" are XML + var isXml = contentType.Equals("application/xml", StringComparison.OrdinalIgnoreCase) + || contentType.Equals("application/xml-external-parsed-entity", + StringComparison.OrdinalIgnoreCase) + || contentType.Equals("application/xml-dtd", StringComparison.OrdinalIgnoreCase); + + isXml |= contentType.EndsWith("+xml", StringComparison.OrdinalIgnoreCase); + return isXml; + } + + private static string GetContentTypeSignature(string contentType) + { + if (string.IsNullOrEmpty(contentType)) + return null; + + var sig = contentType.Split(s_contentTypeParamSeparator, 2)[0].ToUpperInvariant(); + return sig; + } + + #endregion Private Helper Methods + } + + internal static + class StringUtil + { + // Typical padding is at most a screen's width, any more than that and we won't bother caching. + private const int IndentCacheMax = 120; + + private const int DashCacheMax = 120; + + private static readonly string[] IndentCache = new string[IndentCacheMax]; + + private static readonly string[] DashCache = new string[DashCacheMax]; + + internal static + string + Format(string formatSpec, object o) + { + return string.Format(CultureInfo.CurrentCulture, formatSpec, o); + } + + internal static + string + Format(string formatSpec, object o1, object o2) + { + return string.Format(CultureInfo.CurrentCulture, formatSpec, o1, o2); + } + + internal static + string + Format(string formatSpec, params object[] o) + { + return string.Format(CultureInfo.CurrentCulture, formatSpec, o); + } + + internal static + string + TruncateToBufferCellWidth(PSHostRawUserInterface rawUI, string toTruncate, int maxWidthInBufferCells) + { + Debug.Assert(rawUI != null, "need a reference"); + Debug.Assert(maxWidthInBufferCells >= 0, "maxWidthInBufferCells must be positive"); + + string result; + var i = Math.Min(toTruncate.Length, maxWidthInBufferCells); + + do + { + result = toTruncate.Substring(0, i); + var cellCount = rawUI.LengthInBufferCells(result); + if (cellCount <= maxWidthInBufferCells) + { + // the segment from start..i fits + + break; + } + + // The segment does not fit, back off a tad until it does + // We need to back off 1 by 1 because there could theoretically + // be characters taking more 2 buffer cells + --i; + } while (true); + + return result; + } + + internal static string Padding(int countOfSpaces) + { + if (countOfSpaces >= IndentCacheMax) + return new string(' ', countOfSpaces); + + var result = IndentCache[countOfSpaces]; + + if (result == null) + { + Interlocked.CompareExchange(ref IndentCache[countOfSpaces], new string(' ', countOfSpaces), null); + result = IndentCache[countOfSpaces]; + } + + return result; + } + + internal static string DashPadding(int count) + { + if (count >= DashCacheMax) + return new string('-', count); + + var result = DashCache[count]; + + if (result == null) + { + Interlocked.CompareExchange(ref DashCache[count], new string('-', count), null); + result = DashCache[count]; + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/GraphRequestSession.cs b/src/Authentication/Authentication/Helpers/GraphRequestSession.cs new file mode 100644 index 00000000000..c21c603966b --- /dev/null +++ b/src/Authentication/Authentication/Helpers/GraphRequestSession.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +using Microsoft.Graph.PowerShell.Authentication.Cmdlets; +using Microsoft.Graph.PowerShell.Authentication.Models; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + public class GraphRequestSession + { + /// + /// Gets or sets the Header property. + /// + public Dictionary Headers { get; set; } + + /// + /// Gets or sets the content Headers when using HttpClient. + /// + public Dictionary ContentHeaders { get; set; } + /// + /// Gets or Sets the User Agent when using HttpClient + /// + public string UserAgent { get; set; } + /// + /// Gets or Sets a User Specified JWT Token + /// + public string Token { get; set; } + + /// + /// Gets or Sets the AuthenticationType to be used for the current Session + /// + public GraphRequestAuthenticationType AuthenticationType { get; set; } + + /// + /// Construct a new instance of a WebRequestSession object. + /// + public GraphRequestSession() + { + // build the headers collection + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + ContentHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/HttpHelpers.cs b/src/Authentication/Authentication/Helpers/HttpHelpers.cs index 55f490fdb5d..8fe189e320e 100644 --- a/src/Authentication/Authentication/Helpers/HttpHelpers.cs +++ b/src/Authentication/Authentication/Helpers/HttpHelpers.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Graph.PowerShell.Authentication.Helpers { + using System; using Microsoft.Graph.PowerShell.Authentication.Cmdlets; using System.Collections.Generic; using System.Linq; @@ -46,10 +47,41 @@ public static HttpClient GetGraphHttpClient(IAuthContext authConfig = null) HttpClient httpClient = GraphClientFactory.Create(defaultHandlers); // Prepend new SDKVersionHeaders - IEnumerable previousSDKHeaders = httpClient.DefaultRequestHeaders.GetValues(CoreConstants.Headers.SdkVersionHeaderName); + PrependNewSDKVersionHeaders(httpClient); + + return httpClient; + } + + /// + /// Prepend new SDKVersionHeaders + /// + /// + private static void PrependNewSDKVersionHeaders(HttpClient httpClient) + { + IEnumerable previousSDKHeaders = + httpClient.DefaultRequestHeaders.GetValues(CoreConstants.Headers.SdkVersionHeaderName); httpClient.DefaultRequestHeaders.Remove(CoreConstants.Headers.SdkVersionHeaderName); - httpClient.DefaultRequestHeaders.Add(CoreConstants.Headers.SdkVersionHeaderName, previousSDKHeaders.Prepend(AuthModuleVersionHeaderValue)); + httpClient.DefaultRequestHeaders.Add(CoreConstants.Headers.SdkVersionHeaderName, + previousSDKHeaders.Prepend(AuthModuleVersionHeaderValue)); + } + + /// + /// Creates a pre-configured Microsoft Graph . + /// with a custom authenticationProvider + /// + /// Custom AuthProvider + /// + public static HttpClient GetGraphHttpClient(IAuthenticationProvider customAuthProvider) + { + IList defaultHandlers = GraphClientFactory.CreateDefaultHandlers(customAuthProvider); + + // Register ODataQueryOptionsHandler after AuthHandler. + defaultHandlers.Insert(1, (new ODataQueryOptionsHandler())); + HttpClient httpClient = GraphClientFactory.Create(defaultHandlers); + + // Prepend new SDKVersionHeaders + PrependNewSDKVersionHeaders(httpClient); return httpClient; } } diff --git a/src/Authentication/Authentication/Helpers/HttpKnownHeaderNames.cs b/src/Authentication/Authentication/Helpers/HttpKnownHeaderNames.cs new file mode 100644 index 00000000000..03c38ec11ce --- /dev/null +++ b/src/Authentication/Authentication/Helpers/HttpKnownHeaderNames.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + internal static class HttpKnownHeaderNames + { + #region Known_HTTP_Header_Names + + // Known HTTP Header Names. + // List comes from corefx/System/Net/HttpKnownHeaderNames.cs + public const string Accept = "Accept"; + public const string AcceptCharset = "Accept-Charset"; + public const string AcceptEncoding = "Accept-Encoding"; + public const string AcceptLanguage = "Accept-Language"; + public const string AcceptRanges = "Accept-Ranges"; + public const string Age = "Age"; + public const string Allow = "Allow"; + public const string Authorization = "Authorization"; + public const string CacheControl = "Cache-Control"; + public const string Connection = "Connection"; + public const string ContentDisposition = "Content-Disposition"; + public const string ContentEncoding = "Content-Encoding"; + public const string ContentLanguage = "Content-Language"; + public const string ContentLength = "Content-Length"; + public const string ContentLocation = "Content-Location"; + public const string ContentMD5 = "Content-MD5"; + public const string ContentRange = "Content-Range"; + public const string ContentType = "Content-Type"; + public const string Cookie = "Cookie"; + public const string Cookie2 = "Cookie2"; + public const string Date = "Date"; + public const string ETag = "ETag"; + public const string Expect = "Expect"; + public const string Expires = "Expires"; + public const string From = "From"; + public const string Host = "Host"; + public const string IfMatch = "If-Match"; + public const string IfModifiedSince = "If-Modified-Since"; + public const string IfNoneMatch = "If-None-Match"; + public const string IfRange = "If-Range"; + public const string IfUnmodifiedSince = "If-Unmodified-Since"; + public const string KeepAlive = "Keep-Alive"; + public const string LastModified = "Last-Modified"; + public const string Location = "Location"; + public const string MaxForwards = "Max-Forwards"; + public const string Origin = "Origin"; + public const string P3P = "P3P"; + public const string Pragma = "Pragma"; + public const string ProxyAuthenticate = "Proxy-Authenticate"; + public const string ProxyAuthorization = "Proxy-Authorization"; + public const string ProxyConnection = "Proxy-Connection"; + public const string Range = "Range"; + public const string Referer = "Referer"; // NB: The spelling-mistake "Referer" for "Referrer" must be matched. + public const string RetryAfter = "Retry-After"; + public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; + public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; + public const string SecWebSocketKey = "Sec-WebSocket-Key"; + public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; + public const string SecWebSocketVersion = "Sec-WebSocket-Version"; + public const string Server = "Server"; + public const string SetCookie = "Set-Cookie"; + public const string SetCookie2 = "Set-Cookie2"; + public const string TE = "TE"; + public const string Trailer = "Trailer"; + public const string TransferEncoding = "Transfer-Encoding"; + public const string Upgrade = "Upgrade"; + public const string UserAgent = "User-Agent"; + public const string Vary = "Vary"; + public const string Via = "Via"; + public const string WWWAuthenticate = "WWW-Authenticate"; + public const string Warning = "Warning"; + public const string XAspNetVersion = "X-AspNet-Version"; + public const string XPoweredBy = "X-Powered-By"; + + #endregion Known_HTTP_Header_Names + + private static HashSet s_contentHeaderSet = null; + + internal static HashSet ContentHeaders + { + get + { + if (s_contentHeaderSet == null) + { + s_contentHeaderSet = new HashSet(StringComparer.OrdinalIgnoreCase); + + s_contentHeaderSet.Add(HttpKnownHeaderNames.Allow); + s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentDisposition); + s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentEncoding); + s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentLanguage); + s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentLength); + s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentLocation); + s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentMD5); + s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentRange); + s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentType); + s_contentHeaderSet.Add(HttpKnownHeaderNames.Expires); + s_contentHeaderSet.Add(HttpKnownHeaderNames.LastModified); + } + + return s_contentHeaderSet; + } + } + } +} diff --git a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs new file mode 100644 index 00000000000..eab42c59dfe --- /dev/null +++ b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs @@ -0,0 +1,359 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + /// + /// Derived class which can encapsulate an + /// or an as an entity with media type "application/http". + /// + internal class HttpMessageFormatter : HttpContent + { + private const string SP = " "; + private const string ColonSP = ": "; + private const string CRLF = "\r\n"; + private const string CommaSeparator = ", "; + + private const int DefaultHeaderAllocation = 2 * 1024; + + private const string DefaultMediaType = "application/http"; + + private const string MsgTypeParameter = "msgtype"; + private const string DefaultRequestMsgType = "request"; + private const string DefaultResponseMsgType = "response"; + + // Set of header fields that only support single values such as Set-Cookie. + private static readonly HashSet _singleValueHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Cookie", + "Set-Cookie", + "X-Powered-By", + }; + + // Set of header fields that should get serialized as space-separated values such as User-Agent. + private static readonly HashSet _spaceSeparatedValueHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "User-Agent", + }; + + // Set of header fields that should not get serialized + private static readonly HashSet _neverSerializedHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "SdkVersion", + "FeatureFlag", + "Authorization", + "Cache-Control", + "Transfer-Encoding", + "Duration", + "Strict-Transport-Security", + "Date" + }; + + private bool _contentConsumed; + private Lazy> _streamTask; + + /// + /// Initializes a new instance of the class encapsulating an + /// . + /// + /// The instance to encapsulate. + public HttpMessageFormatter(HttpRequestMessage httpRequest) + { + HttpRequestMessage = httpRequest ?? throw new ArgumentNullException("httpRequest"); + Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType); + Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultRequestMsgType)); + + InitializeStreamTask(); + } + + /// + /// Initializes a new instance of the class encapsulating an + /// . + /// + /// The instance to encapsulate. + public HttpMessageFormatter(HttpResponseMessage httpResponse) + { + HttpResponseMessage = httpResponse ?? throw new ArgumentNullException("httpResponse"); + Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType); + Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultResponseMsgType)); + + InitializeStreamTask(); + } + + private HttpContent Content + { + get { return HttpRequestMessage != null ? HttpRequestMessage.Content : HttpResponseMessage.Content; } + } + + /// + /// Gets the HTTP request message. + /// + public HttpRequestMessage HttpRequestMessage { get; private set; } + + /// + /// Gets the HTTP response message. + /// + public HttpResponseMessage HttpResponseMessage { get; private set; } + + private void InitializeStreamTask() + { + _streamTask = new Lazy>(() => Content?.ReadAsStreamAsync()); + } + + /// + /// Validates whether the content contains an HTTP Request or an HTTP Response. + /// + /// The content to validate. + /// if set to true if the content is either an HTTP Request or an HTTP Response. + /// Indicates whether validation failure should result in an or not. + /// true if content is either an HTTP Request or an HTTP Response + internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequest, bool throwOnError) + { + if (content == null) + { + throw new ArgumentNullException("content"); + } + + MediaTypeHeaderValue contentType = content.Headers.ContentType; + if (contentType != null) + { + if (!contentType.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase)) + { + if (throwOnError) + { + throw new ArgumentException("HttpMessageInvalidMediaType", "content"); + } + else + { + return false; + } + } + + foreach (NameValueHeaderValue parameter in contentType.Parameters) + { + if (parameter.Name.Equals(MsgTypeParameter, StringComparison.OrdinalIgnoreCase)) + { + string msgType = UnquoteToken(parameter.Value); + if (!msgType.Equals(isRequest ? DefaultRequestMsgType : DefaultResponseMsgType, StringComparison.OrdinalIgnoreCase)) + { + if (throwOnError) + { + throw new ArgumentException("HttpMessageInvalidMediaType", "content"); + } + else + { + return false; + } + } + + return true; + } + } + } + + if (throwOnError) + { + throw new ArgumentException("HttpMessageInvalidMediaType", "content"); + } + else + { + return false; + } + } + + + public static string UnquoteToken(string token) + { + if (String.IsNullOrWhiteSpace(token)) + { + return token; + } + + if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1) + { + return token.Substring(1, token.Length - 2); + } + + return token; + } + + + /// + /// Asynchronously serializes the object's content to the given . + /// + /// The to which to write. + /// The associated . + /// A instance that is asynchronously serializing the object's content. + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + byte[] header = SerializeHeader(); + await stream.WriteAsync(header, 0, header.Length); + + if (Content != null) + { + Stream readStream = await _streamTask.Value; + ValidateStreamForReading(readStream); + await Content.CopyToAsync(stream); + } + } + + /// + /// Computes the length of the stream if possible. + /// + /// The computed length of the stream. + /// true if the length has been computed; otherwise false. + protected override bool TryComputeLength(out long length) + { + // We have four states we could be in: + // 1. We have content, but the task is still running or finished without success + // 2. We have content, the task has finished successfully, and the stream came back as a null or non-seekable + // 3. We have content, the task has finished successfully, and the stream is seekable, so we know its length + // 4. We don't have content (streamTask.Value == null) + // + // For #1 and #2, we return false. + // For #3, we return true & the size of our headers + the content length + // For #4, we return true & the size of our headers + + bool hasContent = _streamTask.Value != null; + length = 0; + + // Cases #1, #2, #3 + // We serialize header to a StringBuilder so that we can determine the length + // following the pattern for HttpContent to try and determine the message length. + // The perf overhead is no larger than for the other HttpContent implementations. + byte[] header = SerializeHeader(); + length += header.Length; + return true; + } + + /// + /// Serializes the HTTP request line. + /// + /// Where to write the request line. + /// The HTTP request. + private static void SerializeRequestLine(StringBuilder message, HttpRequestMessage httpRequest) + { + Contract.Assert(message != null, "message cannot be null"); + message.Append(httpRequest.Method + SP); + message.Append(httpRequest.RequestUri.PathAndQuery + SP); + message.Append($"HTTP/{(httpRequest.Version != null ? httpRequest.Version.ToString(2) : "1.1")}{CRLF}"); + + // Only insert host header if not already present. + if (httpRequest.Headers.Host == null) + { + message.Append($"HTTP{ColonSP}{httpRequest.RequestUri.Authority}{CRLF}"); + } + } + + /// + /// Serializes the HTTP status line. + /// + /// Where to write the status line. + /// The HTTP response. + private static void SerializeStatusLine(StringBuilder message, HttpResponseMessage httpResponse) + { + Contract.Assert(message != null, "message cannot be null"); + message.Append($"HTTP/{(httpResponse.Version != null ? httpResponse.Version.ToString(2) : "1.1")}{SP}"); + message.Append((int)httpResponse.StatusCode + SP); + message.Append(httpResponse.ReasonPhrase + CRLF); + } + + /// + /// Serializes the header fields. + /// + /// Where to write the status line. + /// The headers to write. + private static void SerializeHeaderFields(StringBuilder message, HttpHeaders headers) + { + Contract.Assert(message != null, "message cannot be null"); + if (headers != null) + { + foreach (KeyValuePair> header in headers) + { + if (_neverSerializedHeaderFields.Contains(header.Key)) + { + continue; + } + if (_singleValueHeaderFields.Contains(header.Key)) + { + foreach (string value in header.Value) + { + message.Append(header.Key + ColonSP + value + CRLF); + } + } + else if (_spaceSeparatedValueHeaderFields.Contains(header.Key)) + { + message.Append(header.Key + ColonSP + String.Join(SP, header.Value) + CRLF); + } + else + { + message.Append(header.Key + ColonSP + String.Join(CommaSeparator, header.Value) + CRLF); + } + } + } + } + + private byte[] SerializeHeader() + { + StringBuilder message = new StringBuilder(DefaultHeaderAllocation); + HttpHeaders headers; + HttpContent content; + if (HttpRequestMessage != null) + { + SerializeRequestLine(message, HttpRequestMessage); + headers = HttpRequestMessage.Headers; + content = HttpRequestMessage.Content; + } + else + { + SerializeStatusLine(message, HttpResponseMessage); + headers = HttpResponseMessage.Headers; + content = HttpResponseMessage.Content; + } + + SerializeHeaderFields(message, headers); + if (content != null) + { + SerializeHeaderFields(message, content.Headers); + } + + message.Append(CRLF); + return Encoding.UTF8.GetBytes(message.ToString()); + } + + private void ValidateStreamForReading(Stream stream) + { + // If the content needs to be written to a target stream a 2nd time, then the stream must support + // seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target + // stream (e.g. a NetworkStream). + if (_contentConsumed) + { + if (stream != null && stream.CanRead) + { + stream.Position = 0; + } + else + { + throw new InvalidOperationException("HttpMessageContentAlreadyRead"); + } + } + + _contentConsumed = true; + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/HttpResponseException.cs b/src/Authentication/Authentication/Helpers/HttpResponseException.cs new file mode 100644 index 00000000000..83fd4bca483 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/HttpResponseException.cs @@ -0,0 +1,22 @@ +using System.Net.Http; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + public sealed class HttpResponseException : HttpRequestException + { + /// + /// Constructor for HttpResponseException. + /// + /// Message for the exception. + /// Response from the HTTP server. + public HttpResponseException(string message, HttpResponseMessage response) : base(message) + { + Response = response; + } + + /// + /// HTTP error response. + /// + public HttpResponseMessage Response { get; private set; } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs b/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs new file mode 100644 index 00000000000..437b8737d5b --- /dev/null +++ b/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + public class InvokeGraphRequestAuthProvider : IAuthenticationProvider + { + private readonly GraphRequestSession _session; + + public InvokeGraphRequestAuthProvider(GraphRequestSession session) + { + _session = session; + } + + public Task AuthenticateRequestAsync(HttpRequestMessage request) + { + var authenticationHeader = new AuthenticationHeaderValue("Bearer", _session.Token); + request.Headers.Authorization = authenticationHeader; + return Task.CompletedTask; + } + } +} diff --git a/src/Authentication/Authentication/Helpers/InvokeGraphRequestUserAgent.cs b/src/Authentication/Authentication/Helpers/InvokeGraphRequestUserAgent.cs new file mode 100644 index 00000000000..8d5a2bb9be5 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/InvokeGraphRequestUserAgent.cs @@ -0,0 +1,83 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Management.Automation; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + internal class InvokeGraphRequestUserAgent + { + private readonly PSCmdlet _cmdLet; + private string _windowsUserAgent; + + internal InvokeGraphRequestUserAgent(PSCmdlet cmdLet) + { + _cmdLet = cmdLet; + } + + internal string UserAgent + { + get + { + // format the user-agent string from the various component parts + var userAgent = string.Format(CultureInfo.InvariantCulture, + "{0} ({1}; {2}; {3}) {4}", + Compatibility, PlatformName, OS, Culture, App); + return userAgent; + } + } + + internal static string Compatibility => ("Mozilla/5.0"); + + internal string App + { + get + { + var app = string.Format(CultureInfo.InvariantCulture, + "PowerShell/{0}", this._cmdLet.Host.Version); + return app; + } + } + + internal string PlatformName + { + get + { + if (Platform.IsWindows) + { + // only generate the windows user agent once + if (_windowsUserAgent == null) + { + // find the version in the windows operating system description + var pattern = new Regex(@"\d+(\.\d+)+"); + var versionText = pattern.Match(OS).Value; + var windowsPlatformVersion = new Version(versionText); + _windowsUserAgent = $"Windows NT {windowsPlatformVersion.Major}.{windowsPlatformVersion.Minor}"; + } + + return _windowsUserAgent; + } + else if (Platform.IsMacOS) + { + return "Macintosh"; + } + else if (Platform.IsLinux) + { + return "Linux"; + } + else + { + // unknown/unsupported platform + Debug.Assert(false, "Unable to determine Operating System Platform"); + return string.Empty; + } + } + } + + internal static string OS => RuntimeInformation.OSDescription.Trim(); + + internal static string Culture => (CultureInfo.CurrentCulture.Name); + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/PathUtils.cs b/src/Authentication/Authentication/Helpers/PathUtils.cs new file mode 100644 index 00000000000..d56fc4e9105 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/PathUtils.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Management.Automation; +using System.Text; +using Microsoft.Graph.PowerShell.Authentication.Cmdlets; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + /// + /// Defines generic utilities and helper methods for PowerShell. + /// + internal static class PathUtils + { + public static string ResolveFilePath(string filePath, InvokeGraphRequest command, bool isLiteralPath) + { + string path = null; + try + { + var filePaths = new List(); + if (isLiteralPath) + { + filePaths.Add(command.SessionState.Path.GetUnresolvedProviderPathFromPSPath(filePath, out _, out _)); + } + else + { + filePaths.AddRange(command.SessionState.Path.GetResolvedProviderPathFromPSPath(filePath, out _)); + } + + path = filePaths[0]; + } + catch (ItemNotFoundException) + { + path = null; + } + + return path; + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/StreamHelper.cs b/src/Authentication/Authentication/Helpers/StreamHelper.cs new file mode 100644 index 00000000000..8156385bb35 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/StreamHelper.cs @@ -0,0 +1,178 @@ +using System; +using System.IO; +using System.Management.Automation; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Graph.PowerShell.Authentication.Cmdlets; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + internal class StreamHelper + { + internal const int DefaultReadBuffer = 100000; + + internal const int ChunkSize = 10000; + + public static byte[] EncodeToBytes(string str, Encoding encoding) + { + if (encoding == null) + { + // just use the default encoding if one wasn't provided + encoding = ContentHelper.GetDefaultEncoding(); + } + + return encoding.GetBytes(str); + } + + internal static Stream GetResponseStream(HttpResponseMessage response) + { + var responseStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + return responseStream; + } + + internal static bool TryGetEncoding(string characterSet, out Encoding encoding) + { + var result = false; + try + { + encoding = Encoding.GetEncoding(characterSet); + result = true; + } + catch (ArgumentException) + { + encoding = null; + } + + return result; + } + + internal static string DecodeStream(BufferingStreamReader responseStream, ref Encoding encoding) + { + var isDefaultEncoding = false; + if (encoding == null) + { + // Use the default encoding if one wasn't provided + encoding = ContentHelper.GetDefaultEncoding(); + isDefaultEncoding = true; + } + + var content = StreamToString(responseStream, encoding); + if (isDefaultEncoding) + { + do + { + // check for a charset attribute on the meta element to override the default. + + var localEncoding = Encoding.UTF8; + responseStream.Seek(0, SeekOrigin.Begin); + content = StreamToString(responseStream, localEncoding); + // report the encoding used. + encoding = localEncoding; + } while (false); + } + + return content; + } + + private static string StreamToString(Stream stream, Encoding encoding) + { + var result = new StringBuilder(ChunkSize); + var decoder = encoding.GetDecoder(); + + var useBufferSize = 64; + if (useBufferSize < encoding.GetMaxCharCount(10)) + { + useBufferSize = encoding.GetMaxCharCount(10); + } + + var chars = new char[useBufferSize]; + var bytes = new byte[useBufferSize * 4]; + int bytesRead; + do + { + // Read at most the number of bytes that will fit in the input buffer. The + // return value is the actual number of bytes read, or zero if no bytes remain. + bytesRead = stream.Read(bytes, 0, useBufferSize * 4); + + var completed = false; + var byteIndex = 0; + + while (!completed) + { + // If this is the last input data, flush the decoder's internal buffer and state. + var flush = bytesRead == 0; + decoder.Convert(bytes, byteIndex, bytesRead - byteIndex, + chars, 0, useBufferSize, flush, + out var bytesUsed, out var charsUsed, out completed); + + // The conversion produced the number of characters indicated by charsUsed. Write that number + // of characters to our result buffer + result.Append(chars, 0, charsUsed); + + // Increment byteIndex to the next block of bytes in the input buffer, if any, to convert. + byteIndex += bytesUsed; + + // The behavior of decoder.Convert changed start .NET 3.1-preview2. + // The change was made in https://github.com/dotnet/coreclr/pull/27229 + // The recommendation from .NET team is to not check for 'completed' if 'flush' is false. + // Break out of the loop if all bytes have been read. + if (!flush && bytesRead == byteIndex) + { + break; + } + } + } while (bytesRead != 0); + + return result.ToString(); + } + + public static void SaveStreamToFile(Stream baseResponseStream, string filePath, + InvokeGraphRequest invokeGraphRequest, CancellationToken token) + { + // If the web cmdlet should resume, append the file instead of overwriting. + const FileMode fileMode = FileMode.Create; + using (var output = new FileStream(filePath, fileMode, FileAccess.Write, FileShare.Read)) + { + WriteToStream(baseResponseStream, output, invokeGraphRequest, token); + } + } + + private static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet, + CancellationToken cancellationToken) + { + if (cmdlet == null) + { + throw new ArgumentNullException(nameof(cmdlet)); + } + + var copyTask = input.CopyToAsync(output); + + var record = new ProgressRecord( + 000000000, + "WriteRequestProgressActivity", + "WriteRequestProgressStatus"); + try + { + do + { + record.StatusDescription = StringUtil.Format("WriteRequestProgressStatus", output.Position); + cmdlet.WriteProgress(record); + + Task.Delay(1000).Wait(cancellationToken); + } while (!copyTask.IsCompleted && !cancellationToken.IsCancellationRequested); + + if (copyTask.IsCompleted) + { + record.StatusDescription = StringUtil.Format("WriteRequestComplete", output.Position); + cmdlet.WriteProgress(record); + } + } + catch (OperationCanceledException) + { + } + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/WebResponseHelper.cs b/src/Authentication/Authentication/Helpers/WebResponseHelper.cs new file mode 100644 index 00000000000..f78ef1c19c3 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/WebResponseHelper.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using Microsoft.Graph.PowerShell.Authentication.Cmdlets; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + internal class WebResponseHelper + { + internal static string GetCharacterSet(HttpResponseMessage response) + { + var characterSet = response.Content.Headers.ContentType.CharSet; + return characterSet; + } + + internal static Dictionary> GetHeadersDictionary(HttpResponseMessage response) + { + var headers = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var entry in response.Headers) + { + headers[entry.Key] = entry.Value; + } + + // In CoreFX, HttpResponseMessage separates content related headers, such as Content-Type to + // HttpResponseMessage.Content.Headers. The remaining headers are in HttpResponseMessage.Headers. + // The keys in both should be unique with no duplicates between them. + // Added for backwards compatibility with PowerShell 5.1 and earlier. + if (response.Content != null) + { + foreach (var entry in response.Content.Headers) + { + headers[entry.Key] = entry.Value; + } + } + + return headers; + } + + internal static string GetProtocol(HttpResponseMessage response) + { + var protocol = string.Format(CultureInfo.InvariantCulture, + "HTTP/{0}", response.Version); + return protocol; + } + + internal static int GetStatusCode(HttpResponseMessage response) + { + var statusCode = (int) response.StatusCode; + return statusCode; + } + + internal static string GetStatusDescription(HttpResponseMessage response) + { + var statusDescription = response.StatusCode.ToString(); + return statusDescription; + } + + internal static bool IsText(HttpResponseMessage response) + { + // ContentType may not exist in response header. + var contentType = response.Content.Headers.ContentType?.MediaType; + return ContentHelper.IsText(contentType); + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj b/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj index 91228ce3b41..d88bd1ab35e 100644 --- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj +++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj @@ -1,7 +1,7 @@ - 0.5.0 - 7.1 + 0.5.2 + 8 netstandard2.0 Library Microsoft.Graph.Authentication @@ -22,11 +22,13 @@ - + + + diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec b/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec index 4f87002b4fd..79463ca7bd5 100644 --- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec +++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec @@ -1,7 +1,7 @@ - 0.5.0 + 0.5.2 Microsoft.Graph.Authentication Microsoft Graph PowerShell authentication module Microsoft diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 b/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 index c586fd7eaa1..baa7348b351 100644 --- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 +++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 @@ -3,7 +3,7 @@ # # Generated by: Microsoft # -# Generated on: 5/4/2020 +# Generated on: 17-Jun-20 # @{ @@ -12,7 +12,7 @@ RootModule = './Microsoft.Graph.Authentication.psm1' # Version number of this module. -ModuleVersion = '0.5.1' +ModuleVersion = '0.5.2' # Supported PSEditions CompatiblePSEditions = 'Core', 'Desktop' @@ -72,7 +72,8 @@ FormatsToProcess = './Microsoft.Graph.Authentication.format.ps1xml' FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = 'Connect-Graph', 'Disconnect-Graph', 'Get-MgContext', 'Invoke-GraphRequest' +CmdletsToExport = 'Connect-Graph', 'Disconnect-Graph', 'Get-MgContext', + 'Invoke-GraphRequest' # Variables to export from this module # VariablesToExport = @() diff --git a/src/Authentication/Authentication/Models/GraphRequestAuthenticationType.cs b/src/Authentication/Authentication/Models/GraphRequestAuthenticationType.cs new file mode 100644 index 00000000000..0b52ff12211 --- /dev/null +++ b/src/Authentication/Authentication/Models/GraphRequestAuthenticationType.cs @@ -0,0 +1,8 @@ +namespace Microsoft.Graph.PowerShell.Authentication.Models +{ + public enum GraphRequestAuthenticationType + { + Default = 0, + UserProvidedToken = 1 + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Models/GraphRequestMethod.cs b/src/Authentication/Authentication/Models/GraphRequestMethod.cs new file mode 100644 index 00000000000..360d8e1b764 --- /dev/null +++ b/src/Authentication/Authentication/Models/GraphRequestMethod.cs @@ -0,0 +1,14 @@ +namespace Microsoft.Graph.PowerShell.Authentication.Models +{ + /// + /// Contains the values of HttpVerbs + /// + public enum GraphRequestMethod + { + GET = 0, + POST = 1, + PUT = 2, + PATCH = 3, + DELETE = 4 + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Models/RestReturnType.cs b/src/Authentication/Authentication/Models/RestReturnType.cs new file mode 100644 index 00000000000..db4a7fb8b28 --- /dev/null +++ b/src/Authentication/Authentication/Models/RestReturnType.cs @@ -0,0 +1,21 @@ +namespace Microsoft.Graph.PowerShell.Authentication.Models +{ + public enum RestReturnType + { + /// + /// Return type not defined in response, + /// best effort detect. + /// + Detect = 0, + + /// + /// Json return type. + /// + Json = 1, + + /// + /// Xml return type. + /// + Xml = 2 + } +} \ No newline at end of file From e1647c8a572b7b135bc8ff027ab06a7d74dc4379 Mon Sep 17 00:00:00 2001 From: George Ndungu Date: Wed, 8 Jul 2020 02:07:25 +0300 Subject: [PATCH 05/16] Fixup suggested changes. --- .../Cmdlets/InvokeGraphRequest.cs | 447 ++++++++++-------- .../Authentication/Helpers/AttachDebugger.cs | 3 - .../Authentication/Helpers/ContentHelper.cs | 178 +------ .../Authentication/Helpers/StreamHelper.cs | 54 +-- .../Authentication/Helpers/StringUtil.cs | 56 +++ .../Helpers/WebResponseHelper.cs | 14 +- .../Microsoft.Graph.Authentication.csproj | 2 +- 7 files changed, 346 insertions(+), 408 deletions(-) create mode 100644 src/Authentication/Authentication/Helpers/StringUtil.cs diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index 39257355ca1..c71c370c106 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Linq; using System.Management.Automation; using System.Net; using System.Net.Http; @@ -19,22 +20,19 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using DriveNotFoundException = System.Management.Automation.DriveNotFoundException; - namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets { [Cmdlet(VerbsLifecycle.Invoke, "GraphRequest", DefaultParameterSetName = Constants.UserParameterSet)] public class InvokeGraphRequest : PSCmdlet { - private readonly CancellationTokenSource _cancelToken; + private readonly CancellationTokenSource _cancellationTokenSource; private readonly InvokeGraphRequestUserAgent _graphRequestUserAgent; private string _originalFilePath; public InvokeGraphRequest() { - _cancelToken = new CancellationTokenSource(); + _cancellationTokenSource = new CancellationTokenSource(); _graphRequestUserAgent = new InvokeGraphRequestUserAgent(this); - Method = GraphRequestMethod.GET; Authentication = GraphRequestAuthenticationType.Default; } @@ -43,9 +41,8 @@ public InvokeGraphRequest() /// [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 1, - Mandatory = true, HelpMessage = "Http Method")] - public GraphRequestMethod Method { get; set; } + public GraphRequestMethod Method { get; set; } = GraphRequestMethod.GET; /// /// Uri to call using the Graph HttpClient can be segments such as /beta/me @@ -70,50 +67,54 @@ public InvokeGraphRequest() /// /// Optional Custom Headers /// - [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, Position = 4, HelpMessage = "Optional Custom Headers")] public IDictionary Headers { get; set; } /// - /// Output file where the response body will be saved + /// Relative or absolute path where the response body will be saved. /// - [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, Position = 5, HelpMessage = "Output file where the response body will be saved")] - public string OutFile { get; set; } + public string OutputFilePath { get; set; } /// - /// Infer Download FileName from ContentDisposition Header + /// Infer Download FileName from ContentDisposition Header, /// - [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, Position = 6, - HelpMessage = "Infer Download FileName")] - public SwitchParameter InferOutFileName { get; set; } + HelpMessage = "Infer output filename")] + public SwitchParameter InferOutputFileName { get; set; } /// - /// Gets or sets the InFile property. + /// Gets or sets the InputFilePath property to send in the request /// - [Parameter] - [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, Position = 7, - HelpMessage = "Infile to Send in the Request")] - public virtual string InFile { get; set; } + HelpMessage = "Input file to send in the request")] + public virtual string InputFilePath { get; set; } /// /// Indicates that the cmdlet returns the results, in addition to writing them to a file. /// only valid when the OutFile parameter is also used. /// - [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, Position = 8, - HelpMessage = - "Indicates that the cmdlet returns the results, in addition to writing them to a file. Only valid when the OutFile parameter is also used. ")] + HelpMessage = "Indicates that the cmdlet returns the results, in addition to writing them to a file. Only valid when the OutFile parameter is also used. ")] public SwitchParameter PassThru { get; set; } /// /// OAuth or Bearer Token to use instead of already acquired token /// - [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, Position = 9, HelpMessage = "OAuth or Bearer Token to use instead of already acquired token")] public string Token { get; set; } @@ -121,7 +122,8 @@ public InvokeGraphRequest() /// /// Add headers to Request Header collection without validation /// - [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, Position = 10, HelpMessage = "Add headers to Request Header collection without validation")] public SwitchParameter SkipHeaderValidation { get; set; } @@ -129,7 +131,8 @@ public InvokeGraphRequest() /// /// Custom Content Type /// - [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, Position = 11, HelpMessage = "Custom Content Type")] public virtual string ContentType { get; set; } @@ -137,24 +140,30 @@ public InvokeGraphRequest() /// /// Graph Authentication Type /// - [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, Position = 12, HelpMessage = "Graph Authentication Type")] public GraphRequestAuthenticationType Authentication { get; set; } /// - /// Gets or sets the SessionVariable property. + /// Specifies a web request session. Enter the variable name, including the dollar sign ($). + /// You can't use the SessionVariable and WebSession parameters in the same command. /// - [Parameter(Position = 13, ParameterSetName = Constants.UserParameterSet, - Mandatory = false, HelpMessage = "Session Variable")] + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, + Position = 13, + HelpMessage = "Specifies a web request session. Enter the variable name, including the dollar sign ($)." + + "You can't use the SessionVariable and GraphRequestSession parameters in the same command.")] [Alias("SV")] public string SessionVariable { get; set; } /// /// Response Headers Variable /// - [Parameter(Position = 14, ParameterSetName = Constants.UserParameterSet, - Mandatory = false, + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, + Position = 14, HelpMessage = "Response Headers Variable")] [Alias("RHV")] public string ResponseHeadersVariable { get; set; } @@ -172,11 +181,13 @@ public InvokeGraphRequest() /// [Parameter(Position = 16, ParameterSetName = Constants.UserParameterSet, Mandatory = false, HelpMessage = "Skip Checking Http Errors")] public virtual SwitchParameter SkipHttpErrorCheck { get; set; } + /// /// Gets or sets the Session property. /// [Parameter(Mandatory = false, - Position = 17, ParameterSetName = Constants.UserParameterSet, + Position = 17, + ParameterSetName = Constants.UserParameterSet, HelpMessage = "Custom Graph Request Session")] public GraphRequestSession GraphRequestSession { get; set; } @@ -184,7 +195,8 @@ public InvokeGraphRequest() /// Custom User Specified User Agent /// [Parameter(Mandatory = false, - Position = 18, ParameterSetName = Constants.UserParameterSet, + Position = 18, + ParameterSetName = Constants.UserParameterSet, HelpMessage = "Custom User Specified User Agent")] public string UserAgent { get; set; } @@ -196,11 +208,11 @@ public InvokeGraphRequest() HelpMessage = "Wait for .NET debugger to attach")] public SwitchParameter Break { get; set; } - internal string QualifiedOutFile => QualifyFilePath(OutFile); + internal string QualifiedOutFile => QualifyFilePath(OutputFilePath); - internal bool ShouldSaveToOutFile => !string.IsNullOrEmpty(OutFile); + internal bool ShouldSaveToOutFile => !string.IsNullOrEmpty(OutputFilePath); - internal bool ShouldWriteToPipeline => (!ShouldSaveToOutFile && !InferOutFileName) || PassThru; + internal bool ShouldWriteToPipeline => (!ShouldSaveToOutFile && !InferOutputFileName) || PassThru; internal bool ShouldCheckHttpStatus => !SkipHttpErrorCheck; @@ -222,36 +234,45 @@ protected override void ProcessRecord() try { PrepareSession(); - using var client = GetHttpClient(); - ValidateRequestUri(client); - using var httpRequestMessage = GetRequest(Uri); - using var httpRequestMessageFormatter = new HttpMessageFormatter(httpRequestMessage); - - FillRequestStream(httpRequestMessage); - try - { - ReportRequestStatus(httpRequestMessageFormatter.HttpRequestMessage); - - var httpResponseMessage = GetResponse(client, httpRequestMessage); - using var httpResponseMessageFormatter = new HttpMessageFormatter(httpResponseMessage); - ReportResponseStatus(httpResponseMessageFormatter.HttpResponseMessage); - - var isSuccess = httpResponseMessage.IsSuccessStatusCode; - if (ShouldCheckHttpStatus && !isSuccess) - { - var httpErrorRecord = GenerateHttpErrorRecord(httpResponseMessageFormatter, httpRequestMessage); - ThrowTerminatingError(httpErrorRecord); - } - ProcessResponse(httpResponseMessage); - } - catch (HttpRequestException ex) + using (var client = GetHttpClient()) { - var er = new ErrorRecord(ex, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, httpRequestMessage); - if (ex.InnerException != null) + ValidateRequestUri(client); + using (var httpRequestMessage = GetRequest(Uri)) { - er.ErrorDetails = new ErrorDetails(ex.InnerException.Message); + using (var httpRequestMessageFormatter = new HttpMessageFormatter(httpRequestMessage)) + { + FillRequestStream(httpRequestMessage); + try + { + ReportRequestStatus(httpRequestMessageFormatter.HttpRequestMessage); + var httpResponseMessage = GetResponse(client, httpRequestMessage); + using (var httpResponseMessageFormatter = new HttpMessageFormatter(httpResponseMessage)) + { + ReportResponseStatus(httpResponseMessageFormatter.HttpResponseMessage); + var isSuccess = httpResponseMessage.IsSuccessStatusCode; + if (ShouldCheckHttpStatus && !isSuccess) + { + var httpErrorRecord = + GenerateHttpErrorRecord(httpResponseMessageFormatter, httpRequestMessage); + ThrowTerminatingError(httpErrorRecord); + } + ProcessResponse(httpResponseMessage); + } + } + catch (HttpRequestException ex) + { + var er = new ErrorRecord(ex, "WebCmdletWebResponseException", + ErrorCategory.InvalidOperation, + httpRequestMessage); + if (ex.InnerException != null) + { + er.ErrorDetails = new ErrorDetails(ex.InnerException.Message); + } + + ThrowTerminatingError(er); + } + } } - ThrowTerminatingError(er); } } catch (HttpRequestException httpRequestException) @@ -275,13 +296,15 @@ protected override void EndProcessing() protected override void StopProcessing() { + _cancellationTokenSource.Cancel(); base.StopProcessing(); } + #endregion private static ErrorRecord GenerateHttpErrorRecord(HttpMessageFormatter httpResponseMessageFormatter, HttpRequestMessage httpRequestMessage) { var currentResponse = httpResponseMessageFormatter.HttpResponseMessage; - var errorMessage = StringFormatCurrentCulture("ResponseStatusCodeFailure {0} {1}", currentResponse.StatusCode, currentResponse.ReasonPhrase); + var errorMessage = StringUtil.FormatCurrentCulture("ResponseStatusCodeFailure {0} {1}", currentResponse.StatusCode, currentResponse.ReasonPhrase); var httpException = new HttpResponseException(errorMessage, currentResponse); var errorRecord = new ErrorRecord(httpException, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, httpRequestMessage); var detailMsg = httpResponseMessageFormatter.ReadAsStringAsync() @@ -294,58 +317,37 @@ private static ErrorRecord GenerateHttpErrorRecord(HttpMessageFormatter httpResp return errorRecord; } - + /// + /// When -Verbose is specified, print out response status + /// + /// private void ReportRequestStatus(HttpRequestMessage requestMessage) { - long requestContentLength = 0; - if (requestMessage.Content != null) - { - requestContentLength = requestMessage.Content.Headers.ContentLength.Value; - } - var reqVerboseMsg = StringFormatCurrentCulture("{0} {1} with {2}-byte payload", + var requestContentLength = requestMessage.Content?.Headers.ContentLength.Value ?? 0; + + var reqVerboseMsg = StringUtil.FormatCurrentCulture("{0} {1} with {2}-byte payload", requestMessage.Method, requestMessage.RequestUri, requestContentLength); WriteVerbose(reqVerboseMsg); } - + /// + /// When -Verbose is specified, print out response status + /// + /// private void ReportResponseStatus(HttpResponseMessage responseMessage) { - var contentType = ContentHelper.GetContentType(responseMessage); - var respVerboseMsg = StringFormatCurrentCulture("received {0}-byte response of content type {1}", + var contentType = responseMessage.GetContentType(); + var respVerboseMsg = StringUtil.FormatCurrentCulture("received {0}-byte response of content type {1}", responseMessage.Content.Headers.ContentLength, contentType); WriteVerbose(respVerboseMsg); } - - private static string StringFormatCurrentCulture(string format, params object[] args) - { - return string.Format(CultureInfo.CurrentCulture, format, args); - } - - private static string FormatDictionary(IDictionary content) - { - if (content == null) - throw new ArgumentNullException(nameof(content)); - - var bodyBuilder = new StringBuilder(); - foreach (string key in content.Keys) - { - if (0 < bodyBuilder.Length) bodyBuilder.Append("&"); - - var value = content[key]; - - // URLEncode the key and value - var encodedKey = WebUtility.UrlEncode(key); - var encodedValue = string.Empty; - if (value != null) encodedValue = WebUtility.UrlEncode(value.ToString()); - - bodyBuilder.AppendFormat("{0}={1}", encodedKey, encodedValue); - } - - return bodyBuilder.ToString(); - } - + /// + /// Compose a request, setting Uri and Headers. + /// + /// + /// private HttpRequestMessage GetRequest(Uri uri) { var requestUri = PrepareUri(uri); @@ -400,10 +402,13 @@ private HttpRequestMessage GetRequest(Uri uri) request.Headers.Add(HttpKnownHeaderNames.UserAgent, GraphRequestSession.UserAgent); } } - return request; } - + /// + /// Compose Request Uri + /// + /// + /// private Uri PrepareUri(Uri uri) { // before creating the web request, @@ -413,9 +418,13 @@ private Uri PrepareUri(Uri uri) { var uriBuilder = new UriBuilder(uri); if (uriBuilder.Query != null && uriBuilder.Query.Length > 1) - uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + FormatDictionary(bodyAsDictionary); + { + uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + bodyAsDictionary.FormatDictionary(); + } else - uriBuilder.Query = FormatDictionary(bodyAsDictionary); + { + uriBuilder.Query = bodyAsDictionary.FormatDictionary(); + } uri = uriBuilder.Uri; // set body to null to prevent later FillRequestStream @@ -424,22 +433,10 @@ private Uri PrepareUri(Uri uri) return uri; } - - private static RestReturnType CheckReturnType(HttpResponseMessage response) - { - if (response == null) throw new ArgumentNullException(nameof(response)); - - var rt = RestReturnType.Detect; - var contentType = ContentHelper.GetContentType(response); - if (string.IsNullOrEmpty(contentType)) - rt = RestReturnType.Detect; - else if (ContentHelper.IsJson(contentType)) - rt = RestReturnType.Json; - else if (ContentHelper.IsXml(contentType)) rt = RestReturnType.Xml; - - return rt; - } - + /// + /// Process Http Response + /// + /// internal void ProcessResponse(HttpResponseMessage response) { if (response == null) throw new ArgumentNullException(nameof(response)); @@ -450,8 +447,7 @@ internal void ProcessResponse(HttpResponseMessage response) { using var responseStream = new BufferingStreamReader(baseResponseStream); // determine the response type - var returnType = CheckReturnType(response); - + var returnType = response.CheckReturnType(); // Try to get the response encoding from the ContentType header. Encoding encoding = null; var charSet = response.Content.Headers.ContentType?.CharSet; @@ -484,7 +480,7 @@ internal void ProcessResponse(HttpResponseMessage response) } // NOTE: Tests use this verbose output to verify the encoding. - WriteVerbose(StringFormatCurrentCulture("Content encoding: {0}", encodingVerboseName)); + WriteVerbose(StringUtil.FormatCurrentCulture("Content encoding: {0}", encodingVerboseName)); var convertSuccess = TryConvert(str, out var obj, ref ex); if (!convertSuccess) { @@ -497,10 +493,10 @@ internal void ProcessResponse(HttpResponseMessage response) if (ShouldSaveToOutFile) { - StreamHelper.SaveStreamToFile(baseResponseStream, QualifiedOutFile, this, _cancelToken.Token); + StreamHelper.SaveStreamToFile(baseResponseStream, QualifiedOutFile, this, _cancellationTokenSource.Token); } - if (InferOutFileName.IsPresent) + if (InferOutputFileName.IsPresent) { if (response.Content.Headers.ContentDisposition != null) { @@ -508,9 +504,8 @@ internal void ProcessResponse(HttpResponseMessage response) { var fileName = response.Content.Headers.ContentDisposition.FileNameStar; var fullFileName = QualifyFilePath(fileName); - WriteVerbose(string.Format(CultureInfo.InvariantCulture, "Inferred File Name {0} Saving to {1}", fileName, fullFileName)); - StreamHelper.SaveStreamToFile(baseResponseStream, fullFileName, this, - _cancelToken.Token); + WriteVerbose(StringUtil.FormatCurrentCulture("Inferred File Name {0} Saving to {1}", fileName, fullFileName)); + StreamHelper.SaveStreamToFile(baseResponseStream, fullFileName, this, _cancellationTokenSource.Token); } } else @@ -528,7 +523,7 @@ internal void ProcessResponse(HttpResponseMessage response) if (!string.IsNullOrEmpty(ResponseHeadersVariable)) { var vi = SessionState.PSVariable; - vi.Set(ResponseHeadersVariable, WebResponseHelper.GetHeadersDictionary(response)); + vi.Set(ResponseHeadersVariable, response.GetHeadersDictionary()); } } @@ -539,13 +534,17 @@ private static bool TryConvert(string str, out object obj, ref Exception exRe { obj = JsonConvert.DeserializeObject(str); if (obj == null) + { JToken.Parse(str); + } else + { converted = true; + } } catch (JsonException ex) { - var msg = string.Format(CultureInfo.CurrentCulture, "JsonDeserializationFailed", ex.Message); + var msg = StringUtil.FormatCurrentCulture("JsonDeserializationFailed", ex.Message); exRef = new ArgumentException(msg, ex); obj = null; } @@ -557,58 +556,94 @@ private static bool TryConvert(string str, out object obj, ref Exception exRe return converted; } - + /// + /// Gets a Custom AuthProvider or configured default provided depending on Auth Scheme specified. + /// + /// private IAuthenticationProvider GetAuthProvider() { if (Authentication == GraphRequestAuthenticationType.UserProvidedToken) { return new InvokeGraphRequestAuthProvider(GraphRequestSession); } - return AuthenticationHelpers.GetAuthProvider(GraphSession.Instance.AuthContext); } - + /// + /// Gets a Graph HttpClient with a custom or default authprovider. + /// + /// private HttpClient GetHttpClient() { var provider = GetAuthProvider(); var client = HttpHelpers.GetGraphHttpClient(provider); return client; } - + /// + /// Executes the HTTP Request and returns a response + /// + /// + /// + /// private HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request) { - if (client == null) throw new ArgumentNullException(nameof(client)); + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } - if (request == null) throw new ArgumentNullException(nameof(request)); + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } - var cancellationToken = _cancelToken.Token; + var cancellationToken = _cancellationTokenSource.Token; var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .GetAwaiter() .GetResult(); return response; } - + /// + /// Set the request content + /// + /// + /// + /// private long SetRequestContent(HttpRequestMessage request, IDictionary content) { if (request == null) + { throw new ArgumentNullException(nameof(request)); + } + if (content == null) + { throw new ArgumentNullException(nameof(content)); - - var body = FormatDictionary(content); + } + var body = content.FormatDictionary(); return SetRequestContent(request, body); } - + /// + /// Set the request content + /// + /// + /// + /// private long SetRequestContent(HttpRequestMessage request, string content) { if (request == null) + { throw new ArgumentNullException(nameof(request)); + } if (content == null) + { return 0; + } Encoding encoding = null; + // When contentType is set, coerce to correct encoding. if (ContentType != null) + { // If Content-Type contains the encoding format (as CharSet), use this encoding format // to encode the Body of the WebRequest sent to the server. Default Encoding format // would be used if Charset is not supplied in the Content-Type property. @@ -616,7 +651,9 @@ private long SetRequestContent(HttpRequestMessage request, string content) { var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(ContentType); if (!string.IsNullOrEmpty(mediaTypeHeaderValue.CharSet)) + { encoding = Encoding.GetEncoding(mediaTypeHeaderValue.CharSet); + } } catch (FormatException ex) { @@ -638,6 +675,7 @@ private long SetRequestContent(HttpRequestMessage request, string content) ThrowTerminatingError(er); } } + } var bytes = StreamHelper.EncodeToBytes(content, encoding); var byteArrayContent = new ByteArrayContent(bytes); @@ -645,7 +683,12 @@ private long SetRequestContent(HttpRequestMessage request, string content) return byteArrayContent.Headers.ContentLength.Value; } - + /// + /// Hydrate the request with the requisite data. + /// for Body handle Dictionaries, Streams and Byte Arrays, coerce + /// into a string if none of the above types. + /// + /// private void FillRequestStream(HttpRequestMessage request) { if (request == null) throw new ArgumentNullException(nameof(request)); @@ -658,7 +701,7 @@ private void FillRequestStream(HttpRequestMessage request) GraphRequestSession.ContentHeaders.TryGetValue(HttpKnownHeaderNames.ContentType, out var contentType); if (string.IsNullOrWhiteSpace(contentType)) { - // Assume application/json of not set by user + // Assume application/json if not set by user GraphRequestSession.ContentHeaders[HttpKnownHeaderNames.ContentType] = CoreConstants.MimeTypeNames.Application.Json; } @@ -668,10 +711,8 @@ private void FillRequestStream(HttpRequestMessage request) if (Body != null) { var content = Body; - // make sure we're using the base object of the body, not the PSObject wrapper - var psBody = Body as PSObject; - if (psBody != null) + if (Body is PSObject psBody) { content = psBody.BaseObject; } @@ -689,16 +730,16 @@ private void FillRequestStream(HttpRequestMessage request) } else { - SetRequestContent(request, - (string)LanguagePrimitives.ConvertTo(content, typeof(string), CultureInfo.InvariantCulture)); + // Assume its a string + SetRequestContent(request, (string)LanguagePrimitives.ConvertTo(content, typeof(string), CultureInfo.InvariantCulture)); } } - else if (InFile != null) // copy InFile data + else if (InputFilePath != null) // copy InputFilePath data { try { // open the input file - SetRequestContent(request, new FileStream(InFile, FileMode.Open, FileAccess.Read, FileShare.Read)); + SetRequestContent(request, new FileStream(InputFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)); } catch (UnauthorizedAccessException) { @@ -715,28 +756,34 @@ private void FillRequestStream(HttpRequestMessage request) request.Content.Headers.Clear(); } - foreach (var entry in GraphRequestSession.ContentHeaders) + foreach (var entry in GraphRequestSession.ContentHeaders.Where(header => !string.IsNullOrWhiteSpace(header.Value))) { - if (!string.IsNullOrWhiteSpace(entry.Value)) + if (SkipHeaderValidation) { - if (SkipHeaderValidation) - request.Content.Headers.TryAddWithoutValidation(entry.Key, entry.Value); - else - try - { - request.Content.Headers.Add(entry.Key, entry.Value); - } - catch (FormatException ex) - { - var outerEx = new ValidationMetadataException("ContentTypeException", ex); - var er = new ErrorRecord(outerEx, "WebCmdletContentTypeException", - ErrorCategory.InvalidArgument, ContentType); - ThrowTerminatingError(er); - } + request.Content.Headers.TryAddWithoutValidation(entry.Key, entry.Value); + } + else + { + try + { + request.Content.Headers.Add(entry.Key, entry.Value); + } + catch (FormatException ex) + { + var outerEx = new ValidationMetadataException("ContentTypeException", ex); + var er = new ErrorRecord(outerEx, "WebCmdletContentTypeException", + ErrorCategory.InvalidArgument, ContentType); + ThrowTerminatingError(er); + } } } } - + /// + /// Sets the body of the to be a byte array + /// + /// + /// + /// private static long SetRequestContent(HttpRequestMessage request, byte[] content) { if (request == null) @@ -749,7 +796,12 @@ private static long SetRequestContent(HttpRequestMessage request, byte[] content return byteArrayContent.Headers.ContentLength.Value; } - + /// + /// Sets the body of the request to be a Stream + /// + /// + /// + /// private static long SetRequestContent(HttpRequestMessage request, Stream contentStream) { if (request == null) @@ -777,7 +829,7 @@ private static HttpMethod GetHttpMethod(GraphRequestMethod graphRequestMethod) internal virtual void PrepareSession() { // Create a new GraphRequestSession object to work with if one is not supplied - GraphRequestSession ??= new GraphRequestSession(); + GraphRequestSession = GraphRequestSession ?? new GraphRequestSession(); if (SessionVariable != null) { // save the session back to the PS environment if requested @@ -790,7 +842,7 @@ internal virtual void PrepareSession() GraphRequestSession.Token = Token; GraphRequestSession.AuthenticationType = Authentication; } - + // // Handle Custom User Agents // @@ -841,7 +893,7 @@ private void ValidateParameters() if (GraphRequestSession != null && SessionVariable != null) { var error = GetValidationError( - "The cmdlet cannot run because the following conflicting parameters are specified: Session and SessionVariable. Specify either Session or SessionVariable, then retry.", + "The cmdlet cannot run because the following conflicting parameters are specified: GraphRequestSession and SessionVariable. Specify either GraphRequestSession or SessionVariable, then retry.", "WebCmdletSessionConflictException"); ThrowTerminatingError(error); } @@ -854,9 +906,9 @@ private void ValidateParameters() ThrowTerminatingError(error); } - if (PassThru && OutFile == null) + if (PassThru && OutputFilePath == null) { - var error = GetValidationError($"{nameof(OutFile)} is missing", + var error = GetValidationError($"{nameof(OutputFilePath)} is missing", "InvokeGraphRequestOutFileMissingException", nameof(PassThru)); ThrowTerminatingError(error); } @@ -875,48 +927,48 @@ private void ValidateParameters() ThrowTerminatingError(error); } - // Only Body or InFile can be specified at a time - if (Body != null && InFile != null) + // Only Body or InputFilePath can be specified at a time + if (Body != null && InputFilePath != null) { var error = GetValidationError("BodyConflict", "WebCmdletBodyConflictException"); ThrowTerminatingError(error); } - // Ensure InFile is an Existing Item - if (InFile != null) + // Ensure InputFilePath is an Existing Item + if (InputFilePath != null) { ErrorRecord errorRecord = null; try { - var providerPaths = GetResolvedProviderPathFromPSPath(InFile, out var provider); + var providerPaths = GetResolvedProviderPathFromPSPath(InputFilePath, out var provider); if (!provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase)) { errorRecord = GetValidationError("NotFilesystemPath", - "WebCmdletInFileNotFilesystemPathException", InFile); + "WebCmdletInFileNotFilesystemPathException", InputFilePath); } else { if (providerPaths.Count > 1) { errorRecord = GetValidationError("MultiplePathsResolved", - "WebCmdletInFileMultiplePathsResolvedException", InFile); + "WebCmdletInFileMultiplePathsResolvedException", InputFilePath); } else if (providerPaths.Count == 0) { errorRecord = GetValidationError("NoPathResolved", - "WebCmdletInFileNoPathResolvedException", InFile); + "WebCmdletInFileNoPathResolvedException", InputFilePath); } else { if (Directory.Exists(providerPaths[0])) errorRecord = GetValidationError("DirectoryPathSpecified", - "WebCmdletInFileNotFilePathException", InFile); + "WebCmdletInFileNotFilePathException", InputFilePath); - _originalFilePath = InFile; - InFile = providerPaths[0]; + _originalFilePath = InputFilePath; + InputFilePath = providerPaths[0]; } } } @@ -928,7 +980,7 @@ private void ValidateParameters() { errorRecord = new ErrorRecord(providerNotFound.ErrorRecord, providerNotFound); } - catch (DriveNotFoundException driveNotFound) + catch (System.Management.Automation.DriveNotFoundException driveNotFound) { errorRecord = new ErrorRecord(driveNotFound.ErrorRecord, driveNotFound); } @@ -936,17 +988,28 @@ private void ValidateParameters() if (errorRecord != null) ThrowTerminatingError(errorRecord); } } - + /// + /// Composes a validation error + /// + /// + /// + /// private ErrorRecord GetValidationError(string msg, string errorId) { var ex = new ValidationMetadataException(msg); var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); return error; } - + /// + /// Composes a validation error + /// + /// + /// + /// + /// private ErrorRecord GetValidationError(string msg, string errorId, params object[] args) { - msg = string.Format(CultureInfo.InvariantCulture, msg, args); + msg = StringUtil.FormatCurrentCulture(msg, args); var ex = new ValidationMetadataException(msg); var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); return error; diff --git a/src/Authentication/Authentication/Helpers/AttachDebugger.cs b/src/Authentication/Authentication/Helpers/AttachDebugger.cs index 6e8ece20edc..30b514414be 100644 --- a/src/Authentication/Authentication/Helpers/AttachDebugger.cs +++ b/src/Authentication/Authentication/Helpers/AttachDebugger.cs @@ -17,7 +17,6 @@ internal static void Break(PSCmdlet invokedCmdLet) while (!System.Diagnostics.Debugger.IsAttached) { Console.Error.WriteLine($"Waiting for debugger to attach to process {System.Diagnostics.Process.GetCurrentProcess().Id}"); - //invokedCmdLet.WriteDebug($"Waiting for debugger to attach to process {System.Diagnostics.Process.GetCurrentProcess().Id}"); for (var i = 0; i < 50; i++) { if (System.Diagnostics.Debugger.IsAttached) @@ -25,10 +24,8 @@ internal static void Break(PSCmdlet invokedCmdLet) break; } System.Threading.Thread.Sleep(100); - //invokedCmdLet.WriteDebug("."); Console.Error.Write("."); } - //invokedCmdLet.WriteDebug(Environment.NewLine); Console.Error.WriteLine(); } System.Diagnostics.Debugger.Break(); diff --git a/src/Authentication/Authentication/Helpers/ContentHelper.cs b/src/Authentication/Authentication/Helpers/ContentHelper.cs index c90d1bddc39..5d2fc5d0640 100644 --- a/src/Authentication/Authentication/Helpers/ContentHelper.cs +++ b/src/Authentication/Authentication/Helpers/ContentHelper.cs @@ -1,12 +1,9 @@ using System; -using System.Diagnostics; -using System.Globalization; using System.Management.Automation; -using System.Management.Automation.Host; using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using System.Threading; +using Microsoft.Graph.PowerShell.Authentication.Models; using Microsoft.Win32; namespace Microsoft.Graph.PowerShell.Authentication.Helpers @@ -16,21 +13,38 @@ internal static class ContentHelper #region Constants // default codepage encoding for web content. See RFC 2616. - private const string _defaultCodePage = "ISO-8859-1"; + private const string DefaultCodePage = "ISO-8859-1"; #endregion Constants #region Fields + internal static RestReturnType CheckReturnType(this HttpResponseMessage response) + { + if (response == null) throw new ArgumentNullException(nameof(response)); + + var rt = RestReturnType.Detect; + var contentType = response.GetContentType(); + if (string.IsNullOrEmpty(contentType)) + rt = RestReturnType.Detect; + else if (ContentHelper.IsJson(contentType)) + rt = RestReturnType.Json; + else if (ContentHelper.IsXml(contentType)) rt = RestReturnType.Xml; + return rt; + } // used to split contentType arguments - private static readonly char[] s_contentTypeParamSeparator = {';'}; + private static readonly char[] ContentTypeParamSeparator = { ';' }; #endregion Fields #region Internal Methods - internal static string GetContentType(HttpResponseMessage response) + internal static string GetContentType(this HttpResponseMessage response) { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } // ContentType may not exist in response header. Return null if not. return response.Content.Headers.ContentType?.MediaType; } @@ -40,17 +54,10 @@ internal static Encoding GetDefaultEncoding() return GetEncodingOrDefault(null); } - internal static Encoding GetEncoding(HttpResponseMessage response) - { - // ContentType may not exist in response header. - var charSet = response.Content.Headers.ContentType?.CharSet; - return GetEncodingOrDefault(charSet); - } - internal static Encoding GetEncodingOrDefault(string characterSet) { // get the name of the codepage to use for response content - var codepage = string.IsNullOrEmpty(characterSet) ? _defaultCodePage : characterSet; + var codepage = string.IsNullOrEmpty(characterSet) ? DefaultCodePage : characterSet; Encoding encoding = null; try @@ -66,49 +73,6 @@ internal static Encoding GetEncodingOrDefault(string characterSet) return encoding; } - internal static StringBuilder GetRawContentHeader(HttpResponseMessage response) - { - var raw = new StringBuilder(); - - var protocol = WebResponseHelper.GetProtocol(response); - if (!string.IsNullOrEmpty(protocol)) - { - var statusCode = WebResponseHelper.GetStatusCode(response); - var statusDescription = WebResponseHelper.GetStatusDescription(response); - raw.AppendFormat("{0} {1} {2}", protocol, statusCode, statusDescription); - raw.AppendLine(); - } - - HttpHeaders[] headerCollections = - { - response.Headers, - response.Content == null ? null : response.Content.Headers - }; - - foreach (var headerCollection in headerCollections) - { - if (headerCollection == null) - { - continue; - } - - foreach (var header in headerCollection) - { - // Headers may have multiple entries with different values - foreach (var headerValue in header.Value) - { - raw.Append(header.Key); - raw.Append(": "); - raw.Append(headerValue); - raw.AppendLine(); - } - } - } - - raw.AppendLine(); - return raw; - } - internal static bool IsJson(string contentType) { contentType = GetContentTypeSignature(contentType); @@ -208,106 +172,10 @@ private static string GetContentTypeSignature(string contentType) if (string.IsNullOrEmpty(contentType)) return null; - var sig = contentType.Split(s_contentTypeParamSeparator, 2)[0].ToUpperInvariant(); + var sig = contentType.Split(ContentTypeParamSeparator, 2)[0].ToUpperInvariant(); return sig; } #endregion Private Helper Methods } - - internal static - class StringUtil - { - // Typical padding is at most a screen's width, any more than that and we won't bother caching. - private const int IndentCacheMax = 120; - - private const int DashCacheMax = 120; - - private static readonly string[] IndentCache = new string[IndentCacheMax]; - - private static readonly string[] DashCache = new string[DashCacheMax]; - - internal static - string - Format(string formatSpec, object o) - { - return string.Format(CultureInfo.CurrentCulture, formatSpec, o); - } - - internal static - string - Format(string formatSpec, object o1, object o2) - { - return string.Format(CultureInfo.CurrentCulture, formatSpec, o1, o2); - } - - internal static - string - Format(string formatSpec, params object[] o) - { - return string.Format(CultureInfo.CurrentCulture, formatSpec, o); - } - - internal static - string - TruncateToBufferCellWidth(PSHostRawUserInterface rawUI, string toTruncate, int maxWidthInBufferCells) - { - Debug.Assert(rawUI != null, "need a reference"); - Debug.Assert(maxWidthInBufferCells >= 0, "maxWidthInBufferCells must be positive"); - - string result; - var i = Math.Min(toTruncate.Length, maxWidthInBufferCells); - - do - { - result = toTruncate.Substring(0, i); - var cellCount = rawUI.LengthInBufferCells(result); - if (cellCount <= maxWidthInBufferCells) - { - // the segment from start..i fits - - break; - } - - // The segment does not fit, back off a tad until it does - // We need to back off 1 by 1 because there could theoretically - // be characters taking more 2 buffer cells - --i; - } while (true); - - return result; - } - - internal static string Padding(int countOfSpaces) - { - if (countOfSpaces >= IndentCacheMax) - return new string(' ', countOfSpaces); - - var result = IndentCache[countOfSpaces]; - - if (result == null) - { - Interlocked.CompareExchange(ref IndentCache[countOfSpaces], new string(' ', countOfSpaces), null); - result = IndentCache[countOfSpaces]; - } - - return result; - } - - internal static string DashPadding(int count) - { - if (count >= DashCacheMax) - return new string('-', count); - - var result = DashCache[count]; - - if (result == null) - { - Interlocked.CompareExchange(ref DashCache[count], new string('-', count), null); - result = DashCache[count]; - } - - return result; - } - } } \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/StreamHelper.cs b/src/Authentication/Authentication/Helpers/StreamHelper.cs index 8156385bb35..8e3d99b54dd 100644 --- a/src/Authentication/Authentication/Helpers/StreamHelper.cs +++ b/src/Authentication/Authentication/Helpers/StreamHelper.cs @@ -79,54 +79,8 @@ internal static string DecodeStream(BufferingStreamReader responseStream, ref En private static string StreamToString(Stream stream, Encoding encoding) { - var result = new StringBuilder(ChunkSize); - var decoder = encoding.GetDecoder(); - - var useBufferSize = 64; - if (useBufferSize < encoding.GetMaxCharCount(10)) - { - useBufferSize = encoding.GetMaxCharCount(10); - } - - var chars = new char[useBufferSize]; - var bytes = new byte[useBufferSize * 4]; - int bytesRead; - do - { - // Read at most the number of bytes that will fit in the input buffer. The - // return value is the actual number of bytes read, or zero if no bytes remain. - bytesRead = stream.Read(bytes, 0, useBufferSize * 4); - - var completed = false; - var byteIndex = 0; - - while (!completed) - { - // If this is the last input data, flush the decoder's internal buffer and state. - var flush = bytesRead == 0; - decoder.Convert(bytes, byteIndex, bytesRead - byteIndex, - chars, 0, useBufferSize, flush, - out var bytesUsed, out var charsUsed, out completed); - - // The conversion produced the number of characters indicated by charsUsed. Write that number - // of characters to our result buffer - result.Append(chars, 0, charsUsed); - - // Increment byteIndex to the next block of bytes in the input buffer, if any, to convert. - byteIndex += bytesUsed; - - // The behavior of decoder.Convert changed start .NET 3.1-preview2. - // The change was made in https://github.com/dotnet/coreclr/pull/27229 - // The recommendation from .NET team is to not check for 'completed' if 'flush' is false. - // Break out of the loop if all bytes have been read. - if (!flush && bytesRead == byteIndex) - { - break; - } - } - } while (bytesRead != 0); - - return result.ToString(); + using var reader = new StreamReader(stream, encoding); + return reader.ReadToEnd(); } public static void SaveStreamToFile(Stream baseResponseStream, string filePath, @@ -158,7 +112,7 @@ private static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet, { do { - record.StatusDescription = StringUtil.Format("WriteRequestProgressStatus", output.Position); + record.StatusDescription = StringUtil.FormatCurrentCulture("WriteRequestProgressStatus", output.Position); cmdlet.WriteProgress(record); Task.Delay(1000).Wait(cancellationToken); @@ -166,7 +120,7 @@ private static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet, if (copyTask.IsCompleted) { - record.StatusDescription = StringUtil.Format("WriteRequestComplete", output.Position); + record.StatusDescription = StringUtil.FormatCurrentCulture("WriteRequestComplete", output.Position); cmdlet.WriteProgress(record); } } diff --git a/src/Authentication/Authentication/Helpers/StringUtil.cs b/src/Authentication/Authentication/Helpers/StringUtil.cs new file mode 100644 index 00000000000..f136486b45b --- /dev/null +++ b/src/Authentication/Authentication/Helpers/StringUtil.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.Management.Automation.Host; +using System.Net; +using System.Text; +using System.Threading; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + internal static class StringUtil + { + internal static string FormatCurrentCulture(string formatSpec, params object[] args) + { + return string.Format(CultureInfo.CurrentCulture, formatSpec, args); + } + /// + /// Formats a Dictionary into a UrlEncoded string + /// + /// + /// + internal static string FormatDictionary(this IDictionary content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + var bodyBuilder = new StringBuilder(); + foreach (string key in content.Keys) + { + if (0 < bodyBuilder.Length) + { + bodyBuilder.Append("&"); + } + + var value = content[key]; + + // URLEncode the key and value + var encodedKey = WebUtility.UrlEncode(key); + var encodedValue = string.Empty; + if (value != null) + { + encodedValue = WebUtility.UrlEncode(value.ToString()); + } + + bodyBuilder.AppendFormat("{0}={1}", encodedKey, encodedValue); + } + + return bodyBuilder.ToString(); + } + + } + +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/WebResponseHelper.cs b/src/Authentication/Authentication/Helpers/WebResponseHelper.cs index f78ef1c19c3..633efca567f 100644 --- a/src/Authentication/Authentication/Helpers/WebResponseHelper.cs +++ b/src/Authentication/Authentication/Helpers/WebResponseHelper.cs @@ -6,15 +6,15 @@ namespace Microsoft.Graph.PowerShell.Authentication.Helpers { - internal class WebResponseHelper + internal static class WebResponseHelper { - internal static string GetCharacterSet(HttpResponseMessage response) + internal static string GetCharacterSet(this HttpResponseMessage response) { var characterSet = response.Content.Headers.ContentType.CharSet; return characterSet; } - internal static Dictionary> GetHeadersDictionary(HttpResponseMessage response) + internal static Dictionary> GetHeadersDictionary(this HttpResponseMessage response) { var headers = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var entry in response.Headers) @@ -37,26 +37,26 @@ internal static Dictionary> GetHeadersDictionary(Htt return headers; } - internal static string GetProtocol(HttpResponseMessage response) + internal static string GetProtocol(this HttpResponseMessage response) { var protocol = string.Format(CultureInfo.InvariantCulture, "HTTP/{0}", response.Version); return protocol; } - internal static int GetStatusCode(HttpResponseMessage response) + internal static int GetStatusCode(this HttpResponseMessage response) { var statusCode = (int) response.StatusCode; return statusCode; } - internal static string GetStatusDescription(HttpResponseMessage response) + internal static string GetStatusDescription(this HttpResponseMessage response) { var statusDescription = response.StatusCode.ToString(); return statusDescription; } - internal static bool IsText(HttpResponseMessage response) + internal static bool IsText(this HttpResponseMessage response) { // ContentType may not exist in response header. var contentType = response.Content.Headers.ContentType?.MediaType; diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj b/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj index d88bd1ab35e..5a1ba1f940d 100644 --- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj +++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj @@ -1,7 +1,7 @@ 0.5.2 - 8 + 7.1 netstandard2.0 Library Microsoft.Graph.Authentication From e87963a2ae5717bd0967681cae3996d1300e24d6 Mon Sep 17 00:00:00 2001 From: George Ndungu Date: Wed, 8 Jul 2020 05:02:50 +0300 Subject: [PATCH 06/16] Add license headers to all files. Move strings to resource file. --- .../Cmdlets/InvokeGraphRequest.cs | 206 +++--- .../Authentication/Helpers/AttachDebugger.cs | 27 +- .../Helpers/BufferingStreamReader.cs | 14 +- .../Authentication/Helpers/ContentHelper.cs | 52 +- .../Authentication/Helpers/Errors.cs | 23 + .../Helpers/GraphRequestSession.cs | 7 +- .../Helpers/HttpKnownHeaderNames.cs | 48 +- .../Helpers/HttpMessageFormatter.cs | 607 +++++++++--------- .../Helpers/HttpResponseException.cs | 6 +- .../Helpers/InvokeGraphRequestAuthProvider.cs | 6 +- .../Helpers/InvokeGraphRequestUserAgent.cs | 18 +- .../Authentication/Helpers/PathUtils.cs | 15 +- .../Authentication/Helpers/StreamHelper.cs | 47 +- .../Authentication/Helpers/StringUtil.cs | 60 +- .../Helpers/WebResponseHelper.cs | 36 +- .../Microsoft.Graph.Authentication.csproj | 13 + .../Properties/Resources.Designer.cs | 279 ++++++++ .../Authentication/Properties/Resources.resx | 193 ++++++ 18 files changed, 1112 insertions(+), 545 deletions(-) create mode 100644 src/Authentication/Authentication/Helpers/Errors.cs create mode 100644 src/Authentication/Authentication/Properties/Resources.Designer.cs create mode 100644 src/Authentication/Authentication/Properties/Resources.resx diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index c71c370c106..5ce42fad051 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -1,3 +1,7 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + using System; using System.Collections; using System.Collections.Generic; @@ -9,12 +13,14 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Resources; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Graph.PowerShell.Authentication.Helpers; using Microsoft.Graph.PowerShell.Authentication.Models; +using Microsoft.Graph.PowerShell.Authentication.Properties; using Microsoft.PowerShell.Commands; using Newtonsoft.Json; @@ -221,7 +227,7 @@ protected override void BeginProcessing() { if (Break) { - AttachDebugger.Break(this); + this.Break(); } ValidateParameters(); @@ -261,7 +267,7 @@ protected override void ProcessRecord() } catch (HttpRequestException ex) { - var er = new ErrorRecord(ex, "WebCmdletWebResponseException", + var er = new ErrorRecord(ex, Errors.InvokeGraphHttpResponseException, ErrorCategory.InvalidOperation, httpRequestMessage); if (ex.InnerException != null) @@ -304,9 +310,9 @@ protected override void StopProcessing() private static ErrorRecord GenerateHttpErrorRecord(HttpMessageFormatter httpResponseMessageFormatter, HttpRequestMessage httpRequestMessage) { var currentResponse = httpResponseMessageFormatter.HttpResponseMessage; - var errorMessage = StringUtil.FormatCurrentCulture("ResponseStatusCodeFailure {0} {1}", currentResponse.StatusCode, currentResponse.ReasonPhrase); + var errorMessage = Resources.ResponseStatusCodeFailure.FormatCurrentCulture(currentResponse.StatusCode, currentResponse.ReasonPhrase); var httpException = new HttpResponseException(errorMessage, currentResponse); - var errorRecord = new ErrorRecord(httpException, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, httpRequestMessage); + var errorRecord = new ErrorRecord(httpException, Errors.InvokeGraphHttpResponseException, ErrorCategory.InvalidOperation, httpRequestMessage); var detailMsg = httpResponseMessageFormatter.ReadAsStringAsync() .GetAwaiter() .GetResult(); @@ -325,8 +331,7 @@ private void ReportRequestStatus(HttpRequestMessage requestMessage) { var requestContentLength = requestMessage.Content?.Headers.ContentLength.Value ?? 0; - var reqVerboseMsg = StringUtil.FormatCurrentCulture("{0} {1} with {2}-byte payload", - requestMessage.Method, + var reqVerboseMsg = Resources.InvokeGraphRequestVerboseMessage.FormatCurrentCulture(requestMessage.Method, requestMessage.RequestUri, requestContentLength); WriteVerbose(reqVerboseMsg); @@ -338,8 +343,7 @@ private void ReportRequestStatus(HttpRequestMessage requestMessage) private void ReportResponseStatus(HttpResponseMessage responseMessage) { var contentType = responseMessage.GetContentType(); - var respVerboseMsg = StringUtil.FormatCurrentCulture("received {0}-byte response of content type {1}", - responseMessage.Content.Headers.ContentLength, + var respVerboseMsg = Resources.InvokeGraphResponseVerboseMessage.FormatCurrentCulture(responseMessage.Content.Headers.ContentLength, contentType); WriteVerbose(respVerboseMsg); } @@ -441,59 +445,61 @@ internal void ProcessResponse(HttpResponseMessage response) { if (response == null) throw new ArgumentNullException(nameof(response)); - var baseResponseStream = StreamHelper.GetResponseStream(response); + var baseResponseStream = response.GetResponseStream(); if (ShouldWriteToPipeline) { - using var responseStream = new BufferingStreamReader(baseResponseStream); - // determine the response type - var returnType = response.CheckReturnType(); - // Try to get the response encoding from the ContentType header. - Encoding encoding = null; - var charSet = response.Content.Headers.ContentType?.CharSet; - if (!string.IsNullOrEmpty(charSet)) + using (var responseStream = new BufferingStreamReader(baseResponseStream)) { - // NOTE: Don't use ContentHelper.GetEncoding; it returns a - // default which bypasses checking for a meta charset value. - StreamHelper.TryGetEncoding(charSet, out encoding); - } + // determine the response type + var returnType = response.CheckReturnType(); + // Try to get the response encoding from the ContentType header. + Encoding encoding = null; + var charSet = response.Content.Headers.ContentType?.CharSet; + if (!string.IsNullOrEmpty(charSet)) + { + // NOTE: Don't use ContentHelper.GetEncoding; it returns a + // default which bypasses checking for a meta charset value. + charSet.TryGetEncoding(out encoding); + } - if (string.IsNullOrEmpty(charSet) && returnType == RestReturnType.Json) - { - encoding = Encoding.UTF8; - } + if (string.IsNullOrEmpty(charSet) && returnType == RestReturnType.Json) + { + encoding = Encoding.UTF8; + } - Exception ex = null; + Exception ex = null; - var str = StreamHelper.DecodeStream(responseStream, ref encoding); + var str = responseStream.DecodeStream(ref encoding); - string encodingVerboseName; - try - { - encodingVerboseName = string.IsNullOrEmpty(encoding.HeaderName) - ? encoding.EncodingName - : encoding.HeaderName; - } - catch (NotSupportedException) - { - encodingVerboseName = encoding.EncodingName; - } + string encodingVerboseName; + try + { + encodingVerboseName = string.IsNullOrEmpty(encoding.HeaderName) + ? encoding.EncodingName + : encoding.HeaderName; + } + catch (NotSupportedException) + { + encodingVerboseName = encoding.EncodingName; + } - // NOTE: Tests use this verbose output to verify the encoding. - WriteVerbose(StringUtil.FormatCurrentCulture("Content encoding: {0}", encodingVerboseName)); - var convertSuccess = TryConvert(str, out var obj, ref ex); - if (!convertSuccess) - { - // fallback to string - obj = str; - } + // NOTE: Tests use this verbose output to verify the encoding. + WriteVerbose(Resources.ContentEncodingVerboseMessage.FormatCurrentCulture(encodingVerboseName)); + var convertSuccess = str.TryConvert(out var obj, ref ex); + if (!convertSuccess) + { + // fallback to string + obj = str; + } - WriteObject(obj); + WriteObject(obj); + } } if (ShouldSaveToOutFile) { - StreamHelper.SaveStreamToFile(baseResponseStream, QualifiedOutFile, this, _cancellationTokenSource.Token); + baseResponseStream.SaveStreamToFile(QualifiedOutFile, this, _cancellationTokenSource.Token); } if (InferOutputFileName.IsPresent) @@ -504,13 +510,13 @@ internal void ProcessResponse(HttpResponseMessage response) { var fileName = response.Content.Headers.ContentDisposition.FileNameStar; var fullFileName = QualifyFilePath(fileName); - WriteVerbose(StringUtil.FormatCurrentCulture("Inferred File Name {0} Saving to {1}", fileName, fullFileName)); - StreamHelper.SaveStreamToFile(baseResponseStream, fullFileName, this, _cancellationTokenSource.Token); + WriteVerbose(Resources.InferredFileNameVerboseMessage.FormatCurrentCulture(fileName, fullFileName)); + baseResponseStream.SaveStreamToFile(fullFileName, this, _cancellationTokenSource.Token); } } else { - WriteVerbose("Could not Infer File Name"); + WriteVerbose(Resources.InferredFileNameErrorMessage); } } @@ -527,35 +533,7 @@ internal void ProcessResponse(HttpResponseMessage response) } } - private static bool TryConvert(string str, out object obj, ref Exception exRef) - { - var converted = false; - try - { - obj = JsonConvert.DeserializeObject(str); - if (obj == null) - { - JToken.Parse(str); - } - else - { - converted = true; - } - } - catch (JsonException ex) - { - var msg = StringUtil.FormatCurrentCulture("JsonDeserializationFailed", ex.Message); - exRef = new ArgumentException(msg, ex); - obj = null; - } - catch (Exception jsonParseException) - { - exRef = jsonParseException; - obj = null; - } - return converted; - } /// /// Gets a Custom AuthProvider or configured default provided depending on Auth Scheme specified. /// @@ -569,7 +547,7 @@ private IAuthenticationProvider GetAuthProvider() return AuthenticationHelpers.GetAuthProvider(GraphSession.Instance.AuthContext); } /// - /// Gets a Graph HttpClient with a custom or default authprovider. + /// Gets a Graph HttpClient with a custom or default auth provider. /// /// private HttpClient GetHttpClient() @@ -659,8 +637,8 @@ private long SetRequestContent(HttpRequestMessage request, string content) { if (!SkipHeaderValidation) { - var outerEx = new ValidationMetadataException("ContentTypeException", ex); - var er = new ErrorRecord(outerEx, "WebCmdletContentTypeException", + var outerEx = new ValidationMetadataException(Resources.ContentTypeExceptionErrorMessage, ex); + var er = new ErrorRecord(outerEx, Errors.InvokeGraphContentTypeException, ErrorCategory.InvalidArgument, ContentType); ThrowTerminatingError(er); } @@ -669,15 +647,15 @@ private long SetRequestContent(HttpRequestMessage request, string content) { if (!SkipHeaderValidation) { - var outerEx = new ValidationMetadataException("ContentTypeException", ex); - var er = new ErrorRecord(outerEx, "WebCmdletContentTypeException", + var outerEx = new ValidationMetadataException(Resources.ContentTypeExceptionErrorMessage, ex); + var er = new ErrorRecord(outerEx, Errors.InvokeGraphContentTypeException, ErrorCategory.InvalidArgument, ContentType); ThrowTerminatingError(er); } } } - var bytes = StreamHelper.EncodeToBytes(content, encoding); + var bytes = content.EncodeToBytes(encoding); var byteArrayContent = new ByteArrayContent(bytes); request.Content = byteArrayContent; @@ -743,8 +721,7 @@ private void FillRequestStream(HttpRequestMessage request) } catch (UnauthorizedAccessException) { - var msg = string.Format(CultureInfo.InvariantCulture, "AccessDenied", - _originalFilePath); + var msg = Resources.AccessDenied.FormatCurrentCulture(_originalFilePath); throw new UnauthorizedAccessException(msg); } } @@ -770,8 +747,8 @@ private void FillRequestStream(HttpRequestMessage request) } catch (FormatException ex) { - var outerEx = new ValidationMetadataException("ContentTypeException", ex); - var er = new ErrorRecord(outerEx, "WebCmdletContentTypeException", + var outerEx = new ValidationMetadataException(Resources.ContentTypeExceptionErrorMessage, ex); + var er = new ErrorRecord(outerEx, Errors.InvokeGraphContentTypeException, ErrorCategory.InvalidArgument, ContentType); ThrowTerminatingError(er); } @@ -873,14 +850,14 @@ private void ValidateRequestUri(HttpClient httpClient) { if (Uri == null) { - var error = GetValidationError($"Must specify {nameof(Uri)}", "InvokeGraphRequestInvalidHost", + var error = GetValidationError(Resources.InvokeGraphRequestMissingUriErrorMessage.FormatCurrentCulture(nameof(Uri)), Errors.InvokeGraphRequestInvalidHost, nameof(Uri)); ThrowTerminatingError(error); } // Ensure that the Passed in Uri has the same Host as the HttpClient. if (Uri.IsAbsoluteUri && httpClient.BaseAddress.Host != Uri.Host) { - var error = GetValidationError($"Invalid Host {Uri.Host}", "InvokeGraphRequestInvalidHost", + var error = GetValidationError(Resources.InvokeGraphRequestInvalidHostErrorMessage.FormatCurrentCulture(Uri.Host), Errors.InvokeGraphRequestInvalidHost, nameof(Uri)); ThrowTerminatingError(error); } @@ -893,45 +870,47 @@ private void ValidateParameters() if (GraphRequestSession != null && SessionVariable != null) { var error = GetValidationError( - "The cmdlet cannot run because the following conflicting parameters are specified: GraphRequestSession and SessionVariable. Specify either GraphRequestSession or SessionVariable, then retry.", - "WebCmdletSessionConflictException"); + Resources.GraphRequestSessionConflict, + Errors.InvokeGraphRequestSessionConflictException); ThrowTerminatingError(error); } // When PATCH or POST is specified, ensure a body is present if (Method == GraphRequestMethod.PATCH || Method == GraphRequestMethod.POST && Body == null) { - var error = GetValidationError($"{nameof(Body)} is required when Method is {Method}", - "InvokeGraphRequestBodyMissingWhenMethodIsSpecified", nameof(Body)); + var error = GetValidationError(Resources.BodyMissingWhenMethodIsSpecified.FormatCurrentCulture(nameof(Body), Method), + Errors.InvokeGraphRequestBodyMissingWhenMethodIsSpecified, + nameof(Body)); ThrowTerminatingError(error); } if (PassThru && OutputFilePath == null) { - var error = GetValidationError($"{nameof(OutputFilePath)} is missing", - "InvokeGraphRequestOutFileMissingException", nameof(PassThru)); + var error = GetValidationError(Resources.PassThruWithOutputFilePathMissing.FormatCurrentCulture(nameof(PassThru), nameof(OutputFilePath)), + Errors.InvokeGraphRequestOutFileMissingException, + nameof(PassThru)); ThrowTerminatingError(error); } if (Authentication == GraphRequestAuthenticationType.Default && !string.IsNullOrWhiteSpace(Token)) { - var error = GetValidationError("AuthenticationTokenConflict", - "WebCmdletAuthenticationTokenConflictException"); + var error = GetValidationError(Resources.AuthenticationTokenConflict.FormatCurrentCulture(GraphRequestSession, nameof(Token)), + Errors.InvokeGraphRequestAuthenticationTokenConflictException); ThrowTerminatingError(error); } // Token shouldn't be null when UserProvidedToken is specified if (Authentication == GraphRequestAuthenticationType.UserProvidedToken && string.IsNullOrWhiteSpace(Token)) { - var error = GetValidationError("AuthenticationCredentialNotSupplied", - "WebCmdletAuthenticationCredentialNotSuppliedException"); + var error = GetValidationError(Resources.AuthenticationCredentialNotSupplied.FormatCurrentCulture(Authentication, nameof(Token)), + Errors.InvokeGraphRequestAuthenticationTokenConflictException); ThrowTerminatingError(error); } // Only Body or InputFilePath can be specified at a time if (Body != null && InputFilePath != null) { - var error = GetValidationError("BodyConflict", - "WebCmdletBodyConflictException"); + var error = GetValidationError(Resources.BodyConflict.FormatCurrentCulture(nameof(Body), nameof(InputFilePath)), + Errors.InvokeGraphRequestBodyConflictException); ThrowTerminatingError(error); } @@ -946,26 +925,28 @@ private void ValidateParameters() if (!provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase)) { - errorRecord = GetValidationError("NotFilesystemPath", - "WebCmdletInFileNotFilesystemPathException", InputFilePath); + errorRecord = GetValidationError(Resources.NotFileSystemPath.FormatCurrentCulture(InputFilePath), + Errors.InvokeGraphRequestFileNotFilesystemPathException, InputFilePath); } else { if (providerPaths.Count > 1) { - errorRecord = GetValidationError("MultiplePathsResolved", - "WebCmdletInFileMultiplePathsResolvedException", InputFilePath); + errorRecord = GetValidationError(Resources.MultiplePathsResolved.FormatCurrentCulture(InputFilePath), + Errors.InvokeGraphRequestInputFileMultiplePathsResolvedException, InputFilePath); } else if (providerPaths.Count == 0) { - errorRecord = GetValidationError("NoPathResolved", - "WebCmdletInFileNoPathResolvedException", InputFilePath); + errorRecord = GetValidationError(Resources.NoPathResolved.FormatCurrentCulture(InputFilePath), + Errors.InvokeGraphRequestInputFileNoPathResolvedException, InputFilePath); } else { if (Directory.Exists(providerPaths[0])) - errorRecord = GetValidationError("DirectoryPathSpecified", - "WebCmdletInFileNotFilePathException", InputFilePath); + { + errorRecord = GetValidationError(Resources.DirectoryPathSpecified.FormatCurrentCulture(providerPaths[0]), + Errors.InvokeGraphRequestInputFileNotFilePathException, InputFilePath); + } _originalFilePath = InputFilePath; InputFilePath = providerPaths[0]; @@ -985,7 +966,8 @@ private void ValidateParameters() errorRecord = new ErrorRecord(driveNotFound.ErrorRecord, driveNotFound); } - if (errorRecord != null) ThrowTerminatingError(errorRecord); + if (errorRecord != null) + ThrowTerminatingError(errorRecord); } } /// @@ -1009,7 +991,7 @@ private ErrorRecord GetValidationError(string msg, string errorId) /// private ErrorRecord GetValidationError(string msg, string errorId, params object[] args) { - msg = StringUtil.FormatCurrentCulture(msg, args); + msg = msg.FormatCurrentCulture(args); var ex = new ValidationMetadataException(msg); var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); return error; diff --git a/src/Authentication/Authentication/Helpers/AttachDebugger.cs b/src/Authentication/Authentication/Helpers/AttachDebugger.cs index 30b514414be..a8d3bbb36a8 100644 --- a/src/Authentication/Authentication/Helpers/AttachDebugger.cs +++ b/src/Authentication/Authentication/Helpers/AttachDebugger.cs @@ -1,34 +1,37 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ using System; +using System.Diagnostics; using System.Management.Automation; - -using Microsoft.Graph.PowerShell.Authentication.Cmdlets; +using System.Threading; +using Debugger = System.Diagnostics.Debugger; namespace Microsoft.Graph.PowerShell.Authentication.Helpers { internal static class AttachDebugger { - internal static void Break(PSCmdlet invokedCmdLet) + internal static void Break(this PSCmdlet invokedCmdLet) { - while (!System.Diagnostics.Debugger.IsAttached) + while (!Debugger.IsAttached) { - Console.Error.WriteLine($"Waiting for debugger to attach to process {System.Diagnostics.Process.GetCurrentProcess().Id}"); + Console.Error.WriteLine($"Waiting for debugger to attach to process {Process.GetCurrentProcess().Id}"); for (var i = 0; i < 50; i++) { - if (System.Diagnostics.Debugger.IsAttached) + if (Debugger.IsAttached) { break; } - System.Threading.Thread.Sleep(100); + + Thread.Sleep(100); Console.Error.Write("."); } + Console.Error.WriteLine(); } - System.Diagnostics.Debugger.Break(); + + Debugger.Break(); } } } \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/BufferingStreamReader.cs b/src/Authentication/Authentication/Helpers/BufferingStreamReader.cs index c4fb4f58950..887c69f057a 100644 --- a/src/Authentication/Authentication/Helpers/BufferingStreamReader.cs +++ b/src/Authentication/Authentication/Helpers/BufferingStreamReader.cs @@ -1,3 +1,7 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + using System; using System.IO; @@ -41,16 +45,16 @@ public override long Position public override int Read(byte[] buffer, int offset, int count) { - long previousPosition = Position; - bool consumedStream = false; - int totalCount = count; + var previousPosition = Position; + var consumedStream = false; + var totalCount = count; while ((!consumedStream) && ((Position + totalCount) > _streamBuffer.Length)) { // If we don't have enough data to fill this from memory, cache more. // We try to read 4096 bytes from base stream every time, so at most we // may cache 4095 bytes more than what is required by the Read operation. - int bytesRead = _baseStream.Read(_copyBuffer, 0, _copyBuffer.Length); + var bytesRead = _baseStream.Read(_copyBuffer, 0, _copyBuffer.Length); if (_streamBuffer.Position < _streamBuffer.Length) { @@ -73,7 +77,7 @@ public override int Read(byte[] buffer, int offset, int count) _streamBuffer.Seek(previousPosition, SeekOrigin.Begin); // Read from the backing store into the requested buffer. - int read = _streamBuffer.Read(buffer, offset, count); + var read = _streamBuffer.Read(buffer, offset, count); if (read < count) { diff --git a/src/Authentication/Authentication/Helpers/ContentHelper.cs b/src/Authentication/Authentication/Helpers/ContentHelper.cs index 5d2fc5d0640..d3f996721fd 100644 --- a/src/Authentication/Authentication/Helpers/ContentHelper.cs +++ b/src/Authentication/Authentication/Helpers/ContentHelper.cs @@ -1,8 +1,14 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + using System; +using System.Globalization; using System.Management.Automation; using System.Net.Http; using System.Net.Http.Headers; using System.Text; + using Microsoft.Graph.PowerShell.Authentication.Models; using Microsoft.Win32; @@ -28,7 +34,8 @@ internal static RestReturnType CheckReturnType(this HttpResponseMessage response rt = RestReturnType.Detect; else if (ContentHelper.IsJson(contentType)) rt = RestReturnType.Json; - else if (ContentHelper.IsXml(contentType)) rt = RestReturnType.Xml; + else if (ContentHelper.IsXml(contentType)) + rt = RestReturnType.Xml; return rt; } @@ -58,8 +65,7 @@ internal static Encoding GetEncodingOrDefault(string characterSet) { // get the name of the codepage to use for response content var codepage = string.IsNullOrEmpty(characterSet) ? DefaultCodePage : characterSet; - Encoding encoding = null; - + Encoding encoding; try { encoding = Encoding.GetEncoding(codepage); @@ -105,12 +111,16 @@ private static bool CheckIsJson(string contentType) // add in these other "javascript" related types that // sometimes get sent down as the mime type for JSON content - isJson |= contentType.Equals("text/json", StringComparison.OrdinalIgnoreCase) - || contentType.Equals("application/x-javascript", StringComparison.OrdinalIgnoreCase) - || contentType.Equals("text/x-javascript", StringComparison.OrdinalIgnoreCase) - || contentType.Equals("application/javascript", StringComparison.OrdinalIgnoreCase) - || contentType.Equals("text/javascript", StringComparison.OrdinalIgnoreCase); - + switch (contentType.ToLower(CultureInfo.InvariantCulture)) + { + case "text/json": + case "application/x-javascript": + case "text/x-javascript": + case "application/javascript": + case "text/javascript": + isJson = true; + break; + } return isJson; } @@ -128,13 +138,11 @@ private static bool CheckIsText(string contentType) if (Platform.IsWindows && !isText) { // Media types registered with Windows as having a perceived type of text, are text - using (var contentTypeKey = - Registry.ClassesRoot.OpenSubKey(@"MIME\Database\Content Type\" + contentType)) + using (var contentTypeKey = Registry.ClassesRoot.OpenSubKey(@"MIME\Database\Content Type\" + contentType)) { if (contentTypeKey != null) { - var extension = contentTypeKey.GetValue("Extension") as string; - if (extension != null) + if (contentTypeKey.GetValue("Extension") is string extension) { using (var extensionKey = Registry.ClassesRoot.OpenSubKey(extension)) { @@ -148,7 +156,6 @@ private static bool CheckIsText(string contentType) } } } - return isText; } @@ -158,13 +165,16 @@ private static bool CheckIsXml(string contentType) return false; // RFC 3023: Media types with the suffix "+xml" are XML - var isXml = contentType.Equals("application/xml", StringComparison.OrdinalIgnoreCase) - || contentType.Equals("application/xml-external-parsed-entity", - StringComparison.OrdinalIgnoreCase) - || contentType.Equals("application/xml-dtd", StringComparison.OrdinalIgnoreCase); - - isXml |= contentType.EndsWith("+xml", StringComparison.OrdinalIgnoreCase); - return isXml; + switch (contentType.ToLower(CultureInfo.InvariantCulture)) + { + case "application/xml": + case "application/xml-external-parsed-entity": + case "application/xml-dtd": + case var x when x.EndsWith("+xml"): + return true; + default: + return false; + } } private static string GetContentTypeSignature(string contentType) diff --git a/src/Authentication/Authentication/Helpers/Errors.cs b/src/Authentication/Authentication/Helpers/Errors.cs new file mode 100644 index 00000000000..36bac6a647e --- /dev/null +++ b/src/Authentication/Authentication/Helpers/Errors.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + public static class Errors + { + public const string InvokeGraphHttpResponseException = nameof(InvokeGraphHttpResponseException); + public const string InvokeGraphContentTypeException = nameof(InvokeGraphContentTypeException); + public const string InvokeGraphRequestInvalidHost = nameof(InvokeGraphRequestInvalidHost); + public const string InvokeGraphRequestSessionConflictException = nameof(InvokeGraphRequestSessionConflictException); + public const string InvokeGraphRequestBodyMissingWhenMethodIsSpecified = nameof(InvokeGraphRequestBodyMissingWhenMethodIsSpecified); + public const string InvokeGraphRequestOutFileMissingException = nameof(InvokeGraphRequestOutFileMissingException); + public const string InvokeGraphRequestAuthenticationTokenConflictException = nameof(InvokeGraphRequestAuthenticationTokenConflictException); + public const string InvokeGraphRequestAuthenticationCredentialNotSuppliedException = nameof(InvokeGraphRequestAuthenticationCredentialNotSuppliedException); + public const string InvokeGraphRequestBodyConflictException = nameof(InvokeGraphRequestBodyConflictException); + public const string InvokeGraphRequestFileNotFilesystemPathException = nameof(InvokeGraphRequestFileNotFilesystemPathException); + public const string InvokeGraphRequestInputFileMultiplePathsResolvedException = nameof(InvokeGraphRequestInputFileMultiplePathsResolvedException); + public const string InvokeGraphRequestInputFileNoPathResolvedException = nameof(InvokeGraphRequestInputFileNoPathResolvedException); + public const string InvokeGraphRequestInputFileNotFilePathException = nameof(InvokeGraphRequestInputFileNotFilePathException); + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/GraphRequestSession.cs b/src/Authentication/Authentication/Helpers/GraphRequestSession.cs index c21c603966b..4fc787d6c62 100644 --- a/src/Authentication/Authentication/Helpers/GraphRequestSession.cs +++ b/src/Authentication/Authentication/Helpers/GraphRequestSession.cs @@ -1,5 +1,6 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ using System; using System.Collections.Generic; @@ -20,10 +21,12 @@ public class GraphRequestSession /// Gets or sets the content Headers when using HttpClient. /// public Dictionary ContentHeaders { get; set; } + /// /// Gets or Sets the User Agent when using HttpClient /// public string UserAgent { get; set; } + /// /// Gets or Sets a User Specified JWT Token /// diff --git a/src/Authentication/Authentication/Helpers/HttpKnownHeaderNames.cs b/src/Authentication/Authentication/Helpers/HttpKnownHeaderNames.cs index 03c38ec11ce..f9d589c2272 100644 --- a/src/Authentication/Authentication/Helpers/HttpKnownHeaderNames.cs +++ b/src/Authentication/Authentication/Helpers/HttpKnownHeaderNames.cs @@ -1,4 +1,8 @@ -using System; +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System; using System.Collections.Generic; using System.Text; @@ -73,34 +77,30 @@ internal static class HttpKnownHeaderNames public const string Warning = "Warning"; public const string XAspNetVersion = "X-AspNet-Version"; public const string XPoweredBy = "X-Powered-By"; - + public const string StrictTransportSecurity = "Strict-Transport-Security"; + public const string Duration = "Duration"; + public const string FeatureFlag = "FeatureFlag"; + public const string SdkVersion = "SdkVersion"; #endregion Known_HTTP_Header_Names - private static HashSet s_contentHeaderSet = null; + private static HashSet _contentHeaderSet = null; - internal static HashSet ContentHeaders - { - get + internal static HashSet ContentHeaders => + _contentHeaderSet ?? (_contentHeaderSet = new HashSet(StringComparer.OrdinalIgnoreCase) { - if (s_contentHeaderSet == null) - { - s_contentHeaderSet = new HashSet(StringComparer.OrdinalIgnoreCase); + HttpKnownHeaderNames.Allow, + HttpKnownHeaderNames.ContentDisposition, + HttpKnownHeaderNames.ContentEncoding, + HttpKnownHeaderNames.ContentLanguage, + HttpKnownHeaderNames.ContentLength, + HttpKnownHeaderNames.ContentLocation, + HttpKnownHeaderNames.ContentMD5, + HttpKnownHeaderNames.ContentRange, + HttpKnownHeaderNames.ContentType, + HttpKnownHeaderNames.Expires, + HttpKnownHeaderNames.LastModified + }); - s_contentHeaderSet.Add(HttpKnownHeaderNames.Allow); - s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentDisposition); - s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentEncoding); - s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentLanguage); - s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentLength); - s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentLocation); - s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentMD5); - s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentRange); - s_contentHeaderSet.Add(HttpKnownHeaderNames.ContentType); - s_contentHeaderSet.Add(HttpKnownHeaderNames.Expires); - s_contentHeaderSet.Add(HttpKnownHeaderNames.LastModified); - } - return s_contentHeaderSet; - } - } } } diff --git a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs index eab42c59dfe..36151d1c2f1 100644 --- a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs +++ b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs @@ -4,356 +4,361 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; +using Microsoft.Graph.PowerShell.Authentication.Properties; namespace Microsoft.Graph.PowerShell.Authentication.Helpers { - /// - /// Derived class which can encapsulate an - /// or an as an entity with media type "application/http". - /// - internal class HttpMessageFormatter : HttpContent - { - private const string SP = " "; - private const string ColonSP = ": "; - private const string CRLF = "\r\n"; - private const string CommaSeparator = ", "; - - private const int DefaultHeaderAllocation = 2 * 1024; - - private const string DefaultMediaType = "application/http"; - - private const string MsgTypeParameter = "msgtype"; - private const string DefaultRequestMsgType = "request"; - private const string DefaultResponseMsgType = "response"; - - // Set of header fields that only support single values such as Set-Cookie. - private static readonly HashSet _singleValueHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase) + /// + /// Derived class which can encapsulate an + /// or an as an entity with media type "application/http". + /// + internal class HttpMessageFormatter : HttpContent { - "Cookie", - "Set-Cookie", - "X-Powered-By", - }; + private const string SP = " "; + private const string ColonSP = ": "; + private const string CRLF = "\r\n"; + private const string CommaSeparator = ", "; - // Set of header fields that should get serialized as space-separated values such as User-Agent. - private static readonly HashSet _spaceSeparatedValueHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "User-Agent", - }; + private const int DefaultHeaderAllocation = 2 * 1024; - // Set of header fields that should not get serialized - private static readonly HashSet _neverSerializedHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "SdkVersion", - "FeatureFlag", - "Authorization", - "Cache-Control", - "Transfer-Encoding", - "Duration", - "Strict-Transport-Security", - "Date" - }; - - private bool _contentConsumed; - private Lazy> _streamTask; + private const string DefaultMediaType = "application/http"; - /// - /// Initializes a new instance of the class encapsulating an - /// . - /// - /// The instance to encapsulate. - public HttpMessageFormatter(HttpRequestMessage httpRequest) - { - HttpRequestMessage = httpRequest ?? throw new ArgumentNullException("httpRequest"); - Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType); - Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultRequestMsgType)); + private const string MsgTypeParameter = "msgtype"; + private const string DefaultRequestMsgType = "request"; + private const string DefaultResponseMsgType = "response"; - InitializeStreamTask(); - } + // Set of header fields that only support single values such as Set-Cookie. + private static readonly HashSet SingleValueHeaderFields = + new HashSet(StringComparer.OrdinalIgnoreCase) + { + HttpKnownHeaderNames.Cookie, + HttpKnownHeaderNames.SetCookie, + HttpKnownHeaderNames.XPoweredBy + }; + + // Set of header fields that should get serialized as space-separated values such as User-Agent. + private static readonly HashSet SpaceSeparatedValueHeaderFields = + new HashSet(StringComparer.OrdinalIgnoreCase) + { + HttpKnownHeaderNames.UserAgent, + }; - /// - /// Initializes a new instance of the class encapsulating an - /// . - /// - /// The instance to encapsulate. - public HttpMessageFormatter(HttpResponseMessage httpResponse) - { - HttpResponseMessage = httpResponse ?? throw new ArgumentNullException("httpResponse"); - Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType); - Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultResponseMsgType)); + // Set of header fields that should not get serialized + private static readonly HashSet NeverSerializedHeaderFields = + new HashSet(StringComparer.OrdinalIgnoreCase) + { + HttpKnownHeaderNames.SdkVersion, + HttpKnownHeaderNames.FeatureFlag, + HttpKnownHeaderNames.Authorization, + HttpKnownHeaderNames.CacheControl, + HttpKnownHeaderNames.TransferEncoding, + HttpKnownHeaderNames.Duration, + HttpKnownHeaderNames.StrictTransportSecurity, + HttpKnownHeaderNames.Date + }; + + private bool _contentConsumed; + private Lazy> _streamTask; + + /// + /// Initializes a new instance of the class encapsulating an + /// . + /// + /// The instance to encapsulate. + public HttpMessageFormatter(HttpRequestMessage httpRequest) + { + HttpRequestMessage = httpRequest ?? throw new ArgumentNullException(nameof(httpRequest)); + Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType); + Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultRequestMsgType)); - InitializeStreamTask(); - } + InitializeStreamTask(); + } - private HttpContent Content - { - get { return HttpRequestMessage != null ? HttpRequestMessage.Content : HttpResponseMessage.Content; } - } + /// + /// Initializes a new instance of the class encapsulating an + /// . + /// + /// The instance to encapsulate. + public HttpMessageFormatter(HttpResponseMessage httpResponse) + { + HttpResponseMessage = httpResponse ?? throw new ArgumentNullException(nameof(httpResponse)); + Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType); + Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultResponseMsgType)); - /// - /// Gets the HTTP request message. - /// - public HttpRequestMessage HttpRequestMessage { get; private set; } + InitializeStreamTask(); + } - /// - /// Gets the HTTP response message. - /// - public HttpResponseMessage HttpResponseMessage { get; private set; } + private HttpContent Content => HttpRequestMessage != null ? HttpRequestMessage.Content : HttpResponseMessage.Content; - private void InitializeStreamTask() - { - _streamTask = new Lazy>(() => Content?.ReadAsStreamAsync()); - } + /// + /// Gets the HTTP request message. + /// + public HttpRequestMessage HttpRequestMessage { get; private set; } - /// - /// Validates whether the content contains an HTTP Request or an HTTP Response. - /// - /// The content to validate. - /// if set to true if the content is either an HTTP Request or an HTTP Response. - /// Indicates whether validation failure should result in an or not. - /// true if content is either an HTTP Request or an HTTP Response - internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequest, bool throwOnError) - { - if (content == null) - { - throw new ArgumentNullException("content"); - } - - MediaTypeHeaderValue contentType = content.Headers.ContentType; - if (contentType != null) - { - if (!contentType.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase)) + /// + /// Gets the HTTP response message. + /// + public HttpResponseMessage HttpResponseMessage { get; private set; } + + private void InitializeStreamTask() { - if (throwOnError) - { - throw new ArgumentException("HttpMessageInvalidMediaType", "content"); - } - else - { - return false; - } + _streamTask = new Lazy>(() => Content?.ReadAsStreamAsync()); } - foreach (NameValueHeaderValue parameter in contentType.Parameters) + /// + /// Validates whether the content contains an HTTP Request or an HTTP Response. + /// + /// The content to validate. + /// if set to true if the content is either an HTTP Request or an HTTP Response. + /// Indicates whether validation failure should result in an or not. + /// true if content is either an HTTP Request or an HTTP Response + internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequest, bool throwOnError) { - if (parameter.Name.Equals(MsgTypeParameter, StringComparison.OrdinalIgnoreCase)) - { - string msgType = UnquoteToken(parameter.Value); - if (!msgType.Equals(isRequest ? DefaultRequestMsgType : DefaultResponseMsgType, StringComparison.OrdinalIgnoreCase)) + if (content == null) { - if (throwOnError) - { - throw new ArgumentException("HttpMessageInvalidMediaType", "content"); - } - else - { - return false; - } + throw new ArgumentNullException(nameof(content)); } - return true; - } + var contentType = content.Headers.ContentType; + if (contentType != null) + { + if (!contentType.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase)) + { + if (throwOnError) + { + throw new ArgumentException(Resources.HttpMessageInvalidMediaType.FormatCurrentCulture(contentType), nameof(content)); + } + else + { + return false; + } + } + + foreach (var parameter in contentType.Parameters) + { + if (parameter.Name.Equals(MsgTypeParameter, StringComparison.OrdinalIgnoreCase)) + { + string msgType = UnquoteToken(parameter.Value); + if (!msgType.Equals(isRequest ? DefaultRequestMsgType : DefaultResponseMsgType, StringComparison.OrdinalIgnoreCase)) + { + if (throwOnError) + { + throw new ArgumentException(Resources.HttpMessageInvalidMediaType.FormatCurrentCulture(msgType), nameof(content)); + } + else + { + return false; + } + } + + return true; + } + } + } + + if (throwOnError) + { + throw new ArgumentException(Resources.HttpMessageInvalidMediaType, "content"); + } + else + { + return false; + } } - } - - if (throwOnError) - { - throw new ArgumentException("HttpMessageInvalidMediaType", "content"); - } - else - { - return false; - } - } - public static string UnquoteToken(string token) - { - if (String.IsNullOrWhiteSpace(token)) - { - return token; - } + public static string UnquoteToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + return token; + } - if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1) - { - return token.Substring(1, token.Length - 2); - } + if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1) + { + return token.Substring(1, token.Length - 2); + } - return token; - } + return token; + } - /// - /// Asynchronously serializes the object's content to the given . - /// - /// The to which to write. - /// The associated . - /// A instance that is asynchronously serializing the object's content. - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) - { - if (stream == null) - { - throw new ArgumentNullException("stream"); - } - - byte[] header = SerializeHeader(); - await stream.WriteAsync(header, 0, header.Length); - - if (Content != null) - { - Stream readStream = await _streamTask.Value; - ValidateStreamForReading(readStream); - await Content.CopyToAsync(stream); - } - } + /// + /// Asynchronously serializes the object's content to the given . + /// + /// The to which to write. + /// The associated . + /// A instance that is asynchronously serializing the object's content. + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } - /// - /// Computes the length of the stream if possible. - /// - /// The computed length of the stream. - /// true if the length has been computed; otherwise false. - protected override bool TryComputeLength(out long length) - { - // We have four states we could be in: - // 1. We have content, but the task is still running or finished without success - // 2. We have content, the task has finished successfully, and the stream came back as a null or non-seekable - // 3. We have content, the task has finished successfully, and the stream is seekable, so we know its length - // 4. We don't have content (streamTask.Value == null) - // - // For #1 and #2, we return false. - // For #3, we return true & the size of our headers + the content length - // For #4, we return true & the size of our headers - - bool hasContent = _streamTask.Value != null; - length = 0; - - // Cases #1, #2, #3 - // We serialize header to a StringBuilder so that we can determine the length - // following the pattern for HttpContent to try and determine the message length. - // The perf overhead is no larger than for the other HttpContent implementations. - byte[] header = SerializeHeader(); - length += header.Length; - return true; - } + byte[] header = SerializeHeader(); + await stream.WriteAsync(header, 0, header.Length); - /// - /// Serializes the HTTP request line. - /// - /// Where to write the request line. - /// The HTTP request. - private static void SerializeRequestLine(StringBuilder message, HttpRequestMessage httpRequest) - { - Contract.Assert(message != null, "message cannot be null"); - message.Append(httpRequest.Method + SP); - message.Append(httpRequest.RequestUri.PathAndQuery + SP); - message.Append($"HTTP/{(httpRequest.Version != null ? httpRequest.Version.ToString(2) : "1.1")}{CRLF}"); - - // Only insert host header if not already present. - if (httpRequest.Headers.Host == null) - { - message.Append($"HTTP{ColonSP}{httpRequest.RequestUri.Authority}{CRLF}"); - } - } + if (Content != null) + { + Stream readStream = await _streamTask.Value; + ValidateStreamForReading(readStream); + await Content.CopyToAsync(stream); + } + } - /// - /// Serializes the HTTP status line. - /// - /// Where to write the status line. - /// The HTTP response. - private static void SerializeStatusLine(StringBuilder message, HttpResponseMessage httpResponse) - { - Contract.Assert(message != null, "message cannot be null"); - message.Append($"HTTP/{(httpResponse.Version != null ? httpResponse.Version.ToString(2) : "1.1")}{SP}"); - message.Append((int)httpResponse.StatusCode + SP); - message.Append(httpResponse.ReasonPhrase + CRLF); - } + /// + /// Computes the length of the stream if possible. + /// + /// The computed length of the stream. + /// true if the length has been computed; otherwise false. + protected override bool TryComputeLength(out long length) + { + // We have four states we could be in: + // 1. We have content, but the task is still running or finished without success + // 2. We have content, the task has finished successfully, and the stream came back as a null or non-seekable + // 3. We have content, the task has finished successfully, and the stream is seekable, so we know its length + // 4. We don't have content (streamTask.Value == null) + // + // For #1 and #2, we return false. + // For #3, we return true & the size of our headers + the content length + // For #4, we return true & the size of our headers + + bool hasContent = _streamTask.Value != null; + length = 0; + + // Cases #1, #2, #3 + // We serialize header to a StringBuilder so that we can determine the length + // following the pattern for HttpContent to try and determine the message length. + // The perf overhead is no larger than for the other HttpContent implementations. + byte[] header = SerializeHeader(); + length += header.Length; + return true; + } - /// - /// Serializes the header fields. - /// - /// Where to write the status line. - /// The headers to write. - private static void SerializeHeaderFields(StringBuilder message, HttpHeaders headers) - { - Contract.Assert(message != null, "message cannot be null"); - if (headers != null) - { - foreach (KeyValuePair> header in headers) + /// + /// Serializes the HTTP request line. + /// + /// Where to write the request line. + /// The HTTP request. + private static void SerializeRequestLine(StringBuilder message, HttpRequestMessage httpRequest) { - if (_neverSerializedHeaderFields.Contains(header.Key)) - { - continue; - } - if (_singleValueHeaderFields.Contains(header.Key)) - { - foreach (string value in header.Value) + Contract.Assert(message != null, "message cannot be null"); + message.Append(httpRequest.Method + SP); + message.Append(httpRequest.RequestUri.PathAndQuery + SP); + message.Append($"HTTP/{(httpRequest.Version != null ? httpRequest.Version.ToString(2) : "1.1")}{CRLF}"); + + // Only insert host header if not already present. + if (httpRequest.Headers.Host == null) { - message.Append(header.Key + ColonSP + value + CRLF); + message.Append($"HTTP{ColonSP}{httpRequest.RequestUri.Authority}{CRLF}"); } - } - else if (_spaceSeparatedValueHeaderFields.Contains(header.Key)) - { - message.Append(header.Key + ColonSP + String.Join(SP, header.Value) + CRLF); - } - else - { - message.Append(header.Key + ColonSP + String.Join(CommaSeparator, header.Value) + CRLF); - } } - } - } - private byte[] SerializeHeader() - { - StringBuilder message = new StringBuilder(DefaultHeaderAllocation); - HttpHeaders headers; - HttpContent content; - if (HttpRequestMessage != null) - { - SerializeRequestLine(message, HttpRequestMessage); - headers = HttpRequestMessage.Headers; - content = HttpRequestMessage.Content; - } - else - { - SerializeStatusLine(message, HttpResponseMessage); - headers = HttpResponseMessage.Headers; - content = HttpResponseMessage.Content; - } - - SerializeHeaderFields(message, headers); - if (content != null) - { - SerializeHeaderFields(message, content.Headers); - } - - message.Append(CRLF); - return Encoding.UTF8.GetBytes(message.ToString()); - } + /// + /// Serializes the HTTP status line. + /// + /// Where to write the status line. + /// The HTTP response. + private static void SerializeStatusLine(StringBuilder message, HttpResponseMessage httpResponse) + { + Contract.Assert(message != null, "message cannot be null"); + message.Append($"HTTP/{(httpResponse.Version != null ? httpResponse.Version.ToString(2) : "1.1")}{SP}"); + message.Append((int)httpResponse.StatusCode + SP); + message.Append(httpResponse.ReasonPhrase + CRLF); + } - private void ValidateStreamForReading(Stream stream) - { - // If the content needs to be written to a target stream a 2nd time, then the stream must support - // seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target - // stream (e.g. a NetworkStream). - if (_contentConsumed) - { - if (stream != null && stream.CanRead) + /// + /// Serializes the header fields. + /// + /// Where to write the status line. + /// The headers to write. + private static void SerializeHeaderFields(StringBuilder message, HttpHeaders headers) { - stream.Position = 0; + Contract.Assert(message != null, "message cannot be null"); + if (headers != null) + { + foreach (KeyValuePair> header in headers) + { + if (NeverSerializedHeaderFields.Contains(header.Key)) + { + continue; + } + if (SingleValueHeaderFields.Contains(header.Key)) + { + foreach (string value in header.Value) + { + message.Append(header.Key + ColonSP + value + CRLF); + } + } + else if (SpaceSeparatedValueHeaderFields.Contains(header.Key)) + { + message.Append(header.Key + ColonSP + String.Join(SP, header.Value) + CRLF); + } + else + { + message.Append(header.Key + ColonSP + String.Join(CommaSeparator, header.Value) + CRLF); + } + } + } } - else + + private byte[] SerializeHeader() { - throw new InvalidOperationException("HttpMessageContentAlreadyRead"); + StringBuilder message = new StringBuilder(DefaultHeaderAllocation); + HttpHeaders headers; + HttpContent content; + if (HttpRequestMessage != null) + { + SerializeRequestLine(message, HttpRequestMessage); + headers = HttpRequestMessage.Headers; + content = HttpRequestMessage.Content; + } + else + { + SerializeStatusLine(message, HttpResponseMessage); + headers = HttpResponseMessage.Headers; + content = HttpResponseMessage.Content; + } + + SerializeHeaderFields(message, headers); + if (content != null) + { + SerializeHeaderFields(message, content.Headers); + } + + message.Append(CRLF); + return Encoding.UTF8.GetBytes(message.ToString()); } - } - _contentConsumed = true; + private void ValidateStreamForReading(Stream stream) + { + // If the content needs to be written to a target stream a 2nd time, then the stream must support + // seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target + // stream (e.g. a NetworkStream). + if (_contentConsumed) + { + if (stream != null && stream.CanRead) + { + stream.Position = 0; + } + else + { + throw new InvalidOperationException("HttpMessageContentAlreadyRead"); + } + } + + _contentConsumed = true; + } } - } } \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/HttpResponseException.cs b/src/Authentication/Authentication/Helpers/HttpResponseException.cs index 83fd4bca483..b99dea64b63 100644 --- a/src/Authentication/Authentication/Helpers/HttpResponseException.cs +++ b/src/Authentication/Authentication/Helpers/HttpResponseException.cs @@ -1,4 +1,8 @@ -using System.Net.Http; +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System.Net.Http; namespace Microsoft.Graph.PowerShell.Authentication.Helpers { diff --git a/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs b/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs index 437b8737d5b..b93d8b636e2 100644 --- a/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs +++ b/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs @@ -1,4 +1,8 @@ -using System; +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; diff --git a/src/Authentication/Authentication/Helpers/InvokeGraphRequestUserAgent.cs b/src/Authentication/Authentication/Helpers/InvokeGraphRequestUserAgent.cs index 8d5a2bb9be5..65c5724d2d3 100644 --- a/src/Authentication/Authentication/Helpers/InvokeGraphRequestUserAgent.cs +++ b/src/Authentication/Authentication/Helpers/InvokeGraphRequestUserAgent.cs @@ -1,4 +1,8 @@ -using System; +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System; using System.Diagnostics; using System.Globalization; using System.Management.Automation; @@ -16,7 +20,10 @@ internal InvokeGraphRequestUserAgent(PSCmdlet cmdLet) { _cmdLet = cmdLet; } - + /// + /// Full UserAgent which Includes the Operating System, Current Culture + /// and full app name including powershell version and invoked command. + /// internal string UserAgent { get @@ -30,13 +37,16 @@ internal string UserAgent } internal static string Compatibility => ("Mozilla/5.0"); - + /// + /// Indicates the App which includes the PowerShell version + /// and the command name. + /// internal string App { get { var app = string.Format(CultureInfo.InvariantCulture, - "PowerShell/{0}", this._cmdLet.Host.Version); + "PowerShell/{0} {1}", this._cmdLet.Host.Version, this._cmdLet.MyInvocation.MyCommand.Name); return app; } } diff --git a/src/Authentication/Authentication/Helpers/PathUtils.cs b/src/Authentication/Authentication/Helpers/PathUtils.cs index d56fc4e9105..716aa4cdd87 100644 --- a/src/Authentication/Authentication/Helpers/PathUtils.cs +++ b/src/Authentication/Authentication/Helpers/PathUtils.cs @@ -1,15 +1,15 @@ -using System; +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Management.Automation; -using System.Text; using Microsoft.Graph.PowerShell.Authentication.Cmdlets; namespace Microsoft.Graph.PowerShell.Authentication.Helpers { /// - /// Defines generic utilities and helper methods for PowerShell. + /// Defines generic utilities and helper methods for PowerShell. /// internal static class PathUtils { @@ -21,13 +21,14 @@ public static string ResolveFilePath(string filePath, InvokeGraphRequest command var filePaths = new List(); if (isLiteralPath) { - filePaths.Add(command.SessionState.Path.GetUnresolvedProviderPathFromPSPath(filePath, out _, out _)); + filePaths.Add( + command.SessionState.Path.GetUnresolvedProviderPathFromPSPath(filePath, out _, out _)); } else { filePaths.AddRange(command.SessionState.Path.GetResolvedProviderPathFromPSPath(filePath, out _)); } - + path = filePaths[0]; } catch (ItemNotFoundException) diff --git a/src/Authentication/Authentication/Helpers/StreamHelper.cs b/src/Authentication/Authentication/Helpers/StreamHelper.cs index 8e3d99b54dd..ecaf3fd7d92 100644 --- a/src/Authentication/Authentication/Helpers/StreamHelper.cs +++ b/src/Authentication/Authentication/Helpers/StreamHelper.cs @@ -1,3 +1,7 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + using System; using System.IO; using System.Management.Automation; @@ -5,18 +9,24 @@ using System.Text; using System.Threading; using System.Threading.Tasks; - using Microsoft.Graph.PowerShell.Authentication.Cmdlets; +using Microsoft.Graph.PowerShell.Authentication.Properties; namespace Microsoft.Graph.PowerShell.Authentication.Helpers { - internal class StreamHelper + internal static class StreamHelper { internal const int DefaultReadBuffer = 100000; internal const int ChunkSize = 10000; - public static byte[] EncodeToBytes(string str, Encoding encoding) + /// + /// Encode specified string to bytes using the provided encoding + /// + /// + /// + /// + internal static byte[] EncodeToBytes(this string str, Encoding encoding) { if (encoding == null) { @@ -27,13 +37,13 @@ public static byte[] EncodeToBytes(string str, Encoding encoding) return encoding.GetBytes(str); } - internal static Stream GetResponseStream(HttpResponseMessage response) + internal static Stream GetResponseStream(this HttpResponseMessage response) { var responseStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); return responseStream; } - internal static bool TryGetEncoding(string characterSet, out Encoding encoding) + internal static bool TryGetEncoding(this string characterSet, out Encoding encoding) { var result = false; try @@ -49,7 +59,7 @@ internal static bool TryGetEncoding(string characterSet, out Encoding encoding) return result; } - internal static string DecodeStream(BufferingStreamReader responseStream, ref Encoding encoding) + internal static string DecodeStream(this BufferingStreamReader responseStream, ref Encoding encoding) { var isDefaultEncoding = false; if (encoding == null) @@ -59,7 +69,7 @@ internal static string DecodeStream(BufferingStreamReader responseStream, ref En isDefaultEncoding = true; } - var content = StreamToString(responseStream, encoding); + var content = responseStream.StreamToString(encoding); if (isDefaultEncoding) { do @@ -68,7 +78,7 @@ internal static string DecodeStream(BufferingStreamReader responseStream, ref En var localEncoding = Encoding.UTF8; responseStream.Seek(0, SeekOrigin.Begin); - content = StreamToString(responseStream, localEncoding); + content = responseStream.StreamToString(localEncoding); // report the encoding used. encoding = localEncoding; } while (false); @@ -77,24 +87,26 @@ internal static string DecodeStream(BufferingStreamReader responseStream, ref En return content; } - private static string StreamToString(Stream stream, Encoding encoding) + internal static string StreamToString(this Stream stream, Encoding encoding) { - using var reader = new StreamReader(stream, encoding); - return reader.ReadToEnd(); + using (var reader = new StreamReader(stream, encoding)) + { + return reader.ReadToEnd(); + } } - public static void SaveStreamToFile(Stream baseResponseStream, string filePath, + internal static void SaveStreamToFile(this Stream baseResponseStream, string filePath, InvokeGraphRequest invokeGraphRequest, CancellationToken token) { // If the web cmdlet should resume, append the file instead of overwriting. const FileMode fileMode = FileMode.Create; using (var output = new FileStream(filePath, fileMode, FileAccess.Write, FileShare.Read)) { - WriteToStream(baseResponseStream, output, invokeGraphRequest, token); + baseResponseStream.WriteToStream(output, invokeGraphRequest, token); } } - private static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet, + private static void WriteToStream(this Stream input, Stream output, PSCmdlet cmdlet, CancellationToken cancellationToken) { if (cmdlet == null) @@ -112,15 +124,16 @@ private static void WriteToStream(Stream input, Stream output, PSCmdlet cmdlet, { do { - record.StatusDescription = StringUtil.FormatCurrentCulture("WriteRequestProgressStatus", output.Position); + record.StatusDescription = + Resources.WriteRequestProgressStatus.FormatCurrentCulture(output.Position); cmdlet.WriteProgress(record); - Task.Delay(1000).Wait(cancellationToken); + Task.Delay(1000, cancellationToken).Wait(cancellationToken); } while (!copyTask.IsCompleted && !cancellationToken.IsCancellationRequested); if (copyTask.IsCompleted) { - record.StatusDescription = StringUtil.FormatCurrentCulture("WriteRequestComplete", output.Position); + record.StatusDescription = Resources.WriteRequestComplete.FormatCurrentCulture(output.Position); cmdlet.WriteProgress(record); } } diff --git a/src/Authentication/Authentication/Helpers/StringUtil.cs b/src/Authentication/Authentication/Helpers/StringUtil.cs index f136486b45b..b2774029a76 100644 --- a/src/Authentication/Authentication/Helpers/StringUtil.cs +++ b/src/Authentication/Authentication/Helpers/StringUtil.cs @@ -1,22 +1,34 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + + using System; using System.Collections; -using System.Diagnostics; using System.Globalization; -using System.Management.Automation.Host; using System.Net; using System.Text; -using System.Threading; +using Microsoft.Graph.PowerShell.Authentication.Properties; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Graph.PowerShell.Authentication.Helpers { internal static class StringUtil { - internal static string FormatCurrentCulture(string formatSpec, params object[] args) + /// + /// Formats the specified string, using the current culture. + /// + /// + /// + /// + internal static string FormatCurrentCulture(this string formatSpec, params object[] args) { return string.Format(CultureInfo.CurrentCulture, formatSpec, args); } + /// - /// Formats a Dictionary into a UrlEncoded string + /// Formats a Dictionary into a UrlEncoded string /// /// /// @@ -51,6 +63,42 @@ internal static string FormatDictionary(this IDictionary content) return bodyBuilder.ToString(); } - } + /// + /// Convert json string to object. + /// + /// + /// + /// + /// + /// + internal static bool TryConvert(this string jsonString, out object obj, ref Exception exRef) + { + var converted = false; + try + { + obj = JsonConvert.DeserializeObject(jsonString); + if (obj == null) + { + JToken.Parse(jsonString); + } + else + { + converted = true; + } + } + catch (JsonException ex) + { + var msg = Resources.JsonSerializationFailed.FormatCurrentCulture(ex.Message); + exRef = new ArgumentException(msg, ex); + obj = default; + } + catch (Exception jsonParseException) + { + exRef = jsonParseException; + obj = default; + } + return converted; + } + } } \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/WebResponseHelper.cs b/src/Authentication/Authentication/Helpers/WebResponseHelper.cs index 633efca567f..e1bd263f312 100644 --- a/src/Authentication/Authentication/Helpers/WebResponseHelper.cs +++ b/src/Authentication/Authentication/Helpers/WebResponseHelper.cs @@ -1,3 +1,7 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + using System; using System.Collections.Generic; using System.Globalization; @@ -8,12 +12,6 @@ namespace Microsoft.Graph.PowerShell.Authentication.Helpers { internal static class WebResponseHelper { - internal static string GetCharacterSet(this HttpResponseMessage response) - { - var characterSet = response.Content.Headers.ContentType.CharSet; - return characterSet; - } - internal static Dictionary> GetHeadersDictionary(this HttpResponseMessage response) { var headers = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -36,31 +34,5 @@ internal static Dictionary> GetHeadersDictionary(thi return headers; } - - internal static string GetProtocol(this HttpResponseMessage response) - { - var protocol = string.Format(CultureInfo.InvariantCulture, - "HTTP/{0}", response.Version); - return protocol; - } - - internal static int GetStatusCode(this HttpResponseMessage response) - { - var statusCode = (int) response.StatusCode; - return statusCode; - } - - internal static string GetStatusDescription(this HttpResponseMessage response) - { - var statusDescription = response.StatusCode.ToString(); - return statusDescription; - } - - internal static bool IsText(this HttpResponseMessage response) - { - // ContentType may not exist in response header. - var contentType = response.Content.Headers.ContentType?.MediaType; - return ContentHelper.IsText(contentType); - } } } \ No newline at end of file diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj b/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj index 5a1ba1f940d..7e5ca01a26f 100644 --- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj +++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj @@ -34,6 +34,19 @@ + + + True + True + Resources.resx + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + diff --git a/src/Authentication/Authentication/Properties/Resources.Designer.cs b/src/Authentication/Authentication/Properties/Resources.Designer.cs new file mode 100644 index 00000000000..07e9ff5e230 --- /dev/null +++ b/src/Authentication/Authentication/Properties/Resources.Designer.cs @@ -0,0 +1,279 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Graph.PowerShell.Authentication.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Graph.PowerShell.Authentication.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Access to the path '{0}' is denied.. + /// + internal static string AccessDenied { + get { + return ResourceManager.GetString("AccessDenied", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The cmdlet cannot run because the following parameter is not specified: {0} The supplied Authentication type requires a {1} Specify {1}, then retry.. + /// + internal static string AuthenticationCredentialNotSupplied { + get { + return ResourceManager.GetString("AuthenticationCredentialNotSupplied", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The cmdlet cannot run because the following conflicting parameters are specified: {0} and Token. Specify either {0} or {1} then retry.. + /// + internal static string AuthenticationTokenConflict { + get { + return ResourceManager.GetString("AuthenticationTokenConflict", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The cmdlet cannot run because the following conflicting parameters are specified: {0} and {1} Specify either {0} or {1}, then retry.. + /// + internal static string BodyConflict { + get { + return ResourceManager.GetString("BodyConflict", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is required when Method is {1}. + /// + internal static string BodyMissingWhenMethodIsSpecified { + get { + return ResourceManager.GetString("BodyMissingWhenMethodIsSpecified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Content encoding: {0}. + /// + internal static string ContentEncodingVerboseMessage { + get { + return ResourceManager.GetString("ContentEncodingVerboseMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The cmdlet cannot run because the -ContentType parameter is not a valid Content-Type header. Specify a valid Content-Type for -ContentType, then retry. To suppress header validation, supply the -SkipHeaderValidation parameter.. + /// + internal static string ContentTypeExceptionErrorMessage { + get { + return ResourceManager.GetString("ContentTypeExceptionErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Path '{0}' resolves to a directory. Specify a path including a file name, and then retry the command.. + /// + internal static string DirectoryPathSpecified { + get { + return ResourceManager.GetString("DirectoryPathSpecified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The cmdlet cannot run because the following conflicting parameters are specified: GraphRequestSession and SessionVariable. Specify either GraphRequestSession or SessionVariable, then retry.. + /// + internal static string GraphRequestSessionConflict { + get { + return ResourceManager.GetString("GraphRequestSessionConflict", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Http Message Invalid Media Type {0}. + /// + internal static string HttpMessageInvalidMediaType { + get { + return ResourceManager.GetString("HttpMessageInvalidMediaType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not Infer File Name. + /// + internal static string InferredFileNameErrorMessage { + get { + return ResourceManager.GetString("InferredFileNameErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Inferred File Name {0} Saving to {1}. + /// + internal static string InferredFileNameVerboseMessage { + get { + return ResourceManager.GetString("InferredFileNameVerboseMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid Host {0}. + /// + internal static string InvokeGraphRequestInvalidHostErrorMessage { + get { + return ResourceManager.GetString("InvokeGraphRequestInvalidHostErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Must specify {0}. + /// + internal static string InvokeGraphRequestMissingUriErrorMessage { + get { + return ResourceManager.GetString("InvokeGraphRequestMissingUriErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} {1} with {2}-byte payload. + /// + internal static string InvokeGraphRequestVerboseMessage { + get { + return ResourceManager.GetString("InvokeGraphRequestVerboseMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to received {0}-byte response of content type {1}. + /// + internal static string InvokeGraphResponseVerboseMessage { + get { + return ResourceManager.GetString("InvokeGraphResponseVerboseMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Conversion from JSON failed with error: {0}. + /// + internal static string JsonSerializationFailed { + get { + return ResourceManager.GetString("JsonSerializationFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Path '{0}' can be resolved to multiple paths.. + /// + internal static string MultiplePathsResolved { + get { + return ResourceManager.GetString("MultiplePathsResolved", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Path '{0}' cannot be resolved to a file.. + /// + internal static string NoPathResolved { + get { + return ResourceManager.GetString("NoPathResolved", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Path '{0}' is not a file system path. Please specify the path to a file in the file system.. + /// + internal static string NotFileSystemPath { + get { + return ResourceManager.GetString("NotFileSystemPath", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} specified without {1}. If {0} is specified {1} must be specified as well.. + /// + internal static string PassThruWithOutputFilePathMissing { + get { + return ResourceManager.GetString("PassThruWithOutputFilePathMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Response status code does not indicate success: {0} ({1}).. + /// + internal static string ResponseStatusCodeFailure { + get { + return ResourceManager.GetString("ResponseStatusCodeFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Graph Request completed. (Number of bytes processed: {0}). + /// + internal static string WriteRequestComplete { + get { + return ResourceManager.GetString("WriteRequestComplete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Number of bytes processed: {0}. + /// + internal static string WriteRequestProgressStatus { + get { + return ResourceManager.GetString("WriteRequestProgressStatus", resourceCulture); + } + } + } +} diff --git a/src/Authentication/Authentication/Properties/Resources.resx b/src/Authentication/Authentication/Properties/Resources.resx new file mode 100644 index 00000000000..1b47cf37cc9 --- /dev/null +++ b/src/Authentication/Authentication/Properties/Resources.resx @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Access to the path '{0}' is denied. + + + The cmdlet cannot run because the following parameter is not specified: {0} The supplied Authentication type requires a {1} Specify {1}, then retry. + + + The cmdlet cannot run because the following conflicting parameters are specified: {0} and Token. Specify either {0} or {1} then retry. + + + The cmdlet cannot run because the following conflicting parameters are specified: {0} and {1} Specify either {0} or {1}, then retry. + + + {0} is required when Method is {1} + + + Content encoding: {0} + + + The cmdlet cannot run because the -ContentType parameter is not a valid Content-Type header. Specify a valid Content-Type for -ContentType, then retry. To suppress header validation, supply the -SkipHeaderValidation parameter. + + + Path '{0}' resolves to a directory. Specify a path including a file name, and then retry the command. + + + The cmdlet cannot run because the following conflicting parameters are specified: GraphRequestSession and SessionVariable. Specify either GraphRequestSession or SessionVariable, then retry. + + + Could not Infer File Name + + + Inferred File Name {0} Saving to {1} + + + Invalid Host {0} + + + Must specify {0} + + + {0} {1} with {2}-byte payload + + + received {0}-byte response of content type {1} + + + Path '{0}' can be resolved to multiple paths. + + + Path '{0}' cannot be resolved to a file. + + + Path '{0}' is not a file system path. Please specify the path to a file in the file system. + + + {0} specified without {1}. If {0} is specified {1} must be specified as well. + + + Response status code does not indicate success: {0} ({1}). + + + Http Message Invalid Media Type {0} + Http Message Invalid Media Type {0} + + + Conversion from JSON failed with error: {0} + + + Graph Request completed. (Number of bytes processed: {0}) + + + Number of bytes processed: {0} + + \ No newline at end of file From 425d234425b1b144d08f63bac498019eb81aeee7 Mon Sep 17 00:00:00 2001 From: George Ndungu Date: Wed, 8 Jul 2020 05:04:50 +0300 Subject: [PATCH 07/16] Formatting --- .../Cmdlets/InvokeGraphRequest.cs | 332 ++++++++++-------- 1 file changed, 186 insertions(+), 146 deletions(-) diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index 5ce42fad051..6897e4042bb 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -1,30 +1,19 @@ -// ------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. -// ------------------------------------------------------------------------------ - using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Management.Automation; -using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Resources; using System.Text; using System.Threading; -using System.Threading.Tasks; - using Microsoft.Graph.PowerShell.Authentication.Helpers; using Microsoft.Graph.PowerShell.Authentication.Models; using Microsoft.Graph.PowerShell.Authentication.Properties; using Microsoft.PowerShell.Commands; - -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using DriveNotFoundException = System.Management.Automation.DriveNotFoundException; namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets { @@ -89,7 +78,7 @@ public InvokeGraphRequest() public string OutputFilePath { get; set; } /// - /// Infer Download FileName from ContentDisposition Header, + /// Infer Download FileName from ContentDisposition Header, /// [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, @@ -113,7 +102,8 @@ public InvokeGraphRequest() [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, Position = 8, - HelpMessage = "Indicates that the cmdlet returns the results, in addition to writing them to a file. Only valid when the OutFile parameter is also used. ")] + HelpMessage = + "Indicates that the cmdlet returns the results, in addition to writing them to a file. Only valid when the OutFile parameter is also used. ")] public SwitchParameter PassThru { get; set; } /// @@ -165,7 +155,7 @@ public InvokeGraphRequest() public string SessionVariable { get; set; } /// - /// Response Headers Variable + /// Response Headers Variable /// [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, @@ -175,7 +165,7 @@ public InvokeGraphRequest() public string ResponseHeadersVariable { get; set; } /// - /// Response Status Code Variable + /// Response Status Code Variable /// [Parameter(Position = 15, ParameterSetName = Constants.UserParameterSet, Mandatory = false, @@ -185,11 +175,12 @@ public InvokeGraphRequest() /// /// Gets or sets whether to skip checking HTTP status for error codes. /// - [Parameter(Position = 16, ParameterSetName = Constants.UserParameterSet, Mandatory = false, HelpMessage = "Skip Checking Http Errors")] + [Parameter(Position = 16, ParameterSetName = Constants.UserParameterSet, Mandatory = false, + HelpMessage = "Skip Checking Http Errors")] public virtual SwitchParameter SkipHttpErrorCheck { get; set; } /// - /// Gets or sets the Session property. + /// Gets or sets the Session property. /// [Parameter(Mandatory = false, Position = 17, @@ -218,101 +209,20 @@ public InvokeGraphRequest() internal bool ShouldSaveToOutFile => !string.IsNullOrEmpty(OutputFilePath); - internal bool ShouldWriteToPipeline => (!ShouldSaveToOutFile && !InferOutputFileName) || PassThru; + internal bool ShouldWriteToPipeline => !ShouldSaveToOutFile && !InferOutputFileName || PassThru; internal bool ShouldCheckHttpStatus => !SkipHttpErrorCheck; - #region CmdLet LifeCycle - protected override void BeginProcessing() - { - if (Break) - { - this.Break(); - } - - ValidateParameters(); - base.BeginProcessing(); - } - - protected override void ProcessRecord() - { - base.ProcessRecord(); - try - { - PrepareSession(); - using (var client = GetHttpClient()) - { - ValidateRequestUri(client); - using (var httpRequestMessage = GetRequest(Uri)) - { - using (var httpRequestMessageFormatter = new HttpMessageFormatter(httpRequestMessage)) - { - FillRequestStream(httpRequestMessage); - try - { - ReportRequestStatus(httpRequestMessageFormatter.HttpRequestMessage); - var httpResponseMessage = GetResponse(client, httpRequestMessage); - using (var httpResponseMessageFormatter = new HttpMessageFormatter(httpResponseMessage)) - { - ReportResponseStatus(httpResponseMessageFormatter.HttpResponseMessage); - var isSuccess = httpResponseMessage.IsSuccessStatusCode; - if (ShouldCheckHttpStatus && !isSuccess) - { - var httpErrorRecord = - GenerateHttpErrorRecord(httpResponseMessageFormatter, httpRequestMessage); - ThrowTerminatingError(httpErrorRecord); - } - ProcessResponse(httpResponseMessage); - } - } - catch (HttpRequestException ex) - { - var er = new ErrorRecord(ex, Errors.InvokeGraphHttpResponseException, - ErrorCategory.InvalidOperation, - httpRequestMessage); - if (ex.InnerException != null) - { - er.ErrorDetails = new ErrorDetails(ex.InnerException.Message); - } - - ThrowTerminatingError(er); - } - } - } - } - } - catch (HttpRequestException httpRequestException) - { - WriteError(new ErrorRecord(httpRequestException, ErrorCategory.ConnectionError.ToString(), - ErrorCategory.InvalidResult, null)); - throw; - } - catch (Exception exception) - { - WriteError(new ErrorRecord(exception, ErrorCategory.ConnectionError.ToString(), - ErrorCategory.InvalidOperation, null)); - throw; - } - } - - protected override void EndProcessing() - { - base.EndProcessing(); - } - - protected override void StopProcessing() - { - _cancellationTokenSource.Cancel(); - base.StopProcessing(); - } - - #endregion - private static ErrorRecord GenerateHttpErrorRecord(HttpMessageFormatter httpResponseMessageFormatter, HttpRequestMessage httpRequestMessage) + private static ErrorRecord GenerateHttpErrorRecord(HttpMessageFormatter httpResponseMessageFormatter, + HttpRequestMessage httpRequestMessage) { var currentResponse = httpResponseMessageFormatter.HttpResponseMessage; - var errorMessage = Resources.ResponseStatusCodeFailure.FormatCurrentCulture(currentResponse.StatusCode, currentResponse.ReasonPhrase); + var errorMessage = + Resources.ResponseStatusCodeFailure.FormatCurrentCulture(currentResponse.StatusCode, + currentResponse.ReasonPhrase); var httpException = new HttpResponseException(errorMessage, currentResponse); - var errorRecord = new ErrorRecord(httpException, Errors.InvokeGraphHttpResponseException, ErrorCategory.InvalidOperation, httpRequestMessage); + var errorRecord = new ErrorRecord(httpException, Errors.InvokeGraphHttpResponseException, + ErrorCategory.InvalidOperation, httpRequestMessage); var detailMsg = httpResponseMessageFormatter.ReadAsStringAsync() .GetAwaiter() .GetResult(); @@ -323,8 +233,9 @@ private static ErrorRecord GenerateHttpErrorRecord(HttpMessageFormatter httpResp return errorRecord; } + /// - /// When -Verbose is specified, print out response status + /// When -Verbose is specified, print out response status /// /// private void ReportRequestStatus(HttpRequestMessage requestMessage) @@ -336,19 +247,22 @@ private void ReportRequestStatus(HttpRequestMessage requestMessage) requestContentLength); WriteVerbose(reqVerboseMsg); } + /// - /// When -Verbose is specified, print out response status + /// When -Verbose is specified, print out response status /// /// private void ReportResponseStatus(HttpResponseMessage responseMessage) { var contentType = responseMessage.GetContentType(); - var respVerboseMsg = Resources.InvokeGraphResponseVerboseMessage.FormatCurrentCulture(responseMessage.Content.Headers.ContentLength, + var respVerboseMsg = Resources.InvokeGraphResponseVerboseMessage.FormatCurrentCulture( + responseMessage.Content.Headers.ContentLength, contentType); WriteVerbose(respVerboseMsg); } + /// - /// Compose a request, setting Uri and Headers. + /// Compose a request, setting Uri and Headers. /// /// /// @@ -406,10 +320,12 @@ private HttpRequestMessage GetRequest(Uri uri) request.Headers.Add(HttpKnownHeaderNames.UserAgent, GraphRequestSession.UserAgent); } } + return request; } + /// - /// Compose Request Uri + /// Compose Request Uri /// /// /// @@ -437,8 +353,9 @@ private Uri PrepareUri(Uri uri) return uri; } + /// - /// Process Http Response + /// Process Http Response /// /// internal void ProcessResponse(HttpResponseMessage response) @@ -510,7 +427,8 @@ internal void ProcessResponse(HttpResponseMessage response) { var fileName = response.Content.Headers.ContentDisposition.FileNameStar; var fullFileName = QualifyFilePath(fileName); - WriteVerbose(Resources.InferredFileNameVerboseMessage.FormatCurrentCulture(fileName, fullFileName)); + WriteVerbose( + Resources.InferredFileNameVerboseMessage.FormatCurrentCulture(fileName, fullFileName)); baseResponseStream.SaveStreamToFile(fullFileName, this, _cancellationTokenSource.Token); } } @@ -523,7 +441,7 @@ internal void ProcessResponse(HttpResponseMessage response) if (!string.IsNullOrEmpty(StatusCodeVariable)) { var vi = SessionState.PSVariable; - vi.Set(StatusCodeVariable, (int)response.StatusCode); + vi.Set(StatusCodeVariable, (int) response.StatusCode); } if (!string.IsNullOrEmpty(ResponseHeadersVariable)) @@ -535,7 +453,7 @@ internal void ProcessResponse(HttpResponseMessage response) /// - /// Gets a Custom AuthProvider or configured default provided depending on Auth Scheme specified. + /// Gets a Custom AuthProvider or configured default provided depending on Auth Scheme specified. /// /// private IAuthenticationProvider GetAuthProvider() @@ -544,10 +462,12 @@ private IAuthenticationProvider GetAuthProvider() { return new InvokeGraphRequestAuthProvider(GraphRequestSession); } + return AuthenticationHelpers.GetAuthProvider(GraphSession.Instance.AuthContext); } + /// - /// Gets a Graph HttpClient with a custom or default auth provider. + /// Gets a Graph HttpClient with a custom or default auth provider. /// /// private HttpClient GetHttpClient() @@ -556,8 +476,9 @@ private HttpClient GetHttpClient() var client = HttpHelpers.GetGraphHttpClient(provider); return client; } + /// - /// Executes the HTTP Request and returns a response + /// Executes the HTTP Request and returns a response /// /// /// @@ -580,8 +501,9 @@ private HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage re .GetResult(); return response; } + /// - /// Set the request content + /// Set the request content /// /// /// @@ -597,11 +519,13 @@ private long SetRequestContent(HttpRequestMessage request, IDictionary content) { throw new ArgumentNullException(nameof(content)); } + var body = content.FormatDictionary(); return SetRequestContent(request, body); } + /// - /// Set the request content + /// Set the request content /// /// /// @@ -661,10 +585,11 @@ private long SetRequestContent(HttpRequestMessage request, string content) return byteArrayContent.Headers.ContentLength.Value; } + /// - /// Hydrate the request with the requisite data. - /// for Body handle Dictionaries, Streams and Byte Arrays, coerce - /// into a string if none of the above types. + /// Hydrate the request with the requisite data. + /// for Body handle Dictionaries, Streams and Byte Arrays, coerce + /// into a string if none of the above types. /// /// private void FillRequestStream(HttpRequestMessage request) @@ -709,7 +634,8 @@ private void FillRequestStream(HttpRequestMessage request) else { // Assume its a string - SetRequestContent(request, (string)LanguagePrimitives.ConvertTo(content, typeof(string), CultureInfo.InvariantCulture)); + SetRequestContent(request, + (string) LanguagePrimitives.ConvertTo(content, typeof(string), CultureInfo.InvariantCulture)); } } else if (InputFilePath != null) // copy InputFilePath data @@ -717,7 +643,8 @@ private void FillRequestStream(HttpRequestMessage request) try { // open the input file - SetRequestContent(request, new FileStream(InputFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)); + SetRequestContent(request, + new FileStream(InputFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)); } catch (UnauthorizedAccessException) { @@ -733,7 +660,8 @@ private void FillRequestStream(HttpRequestMessage request) request.Content.Headers.Clear(); } - foreach (var entry in GraphRequestSession.ContentHeaders.Where(header => !string.IsNullOrWhiteSpace(header.Value))) + foreach (var entry in GraphRequestSession.ContentHeaders.Where(header => + !string.IsNullOrWhiteSpace(header.Value))) { if (SkipHeaderValidation) { @@ -755,8 +683,9 @@ private void FillRequestStream(HttpRequestMessage request) } } } + /// - /// Sets the body of the to be a byte array + /// Sets the body of the to be a byte array /// /// /// @@ -773,8 +702,9 @@ private static long SetRequestContent(HttpRequestMessage request, byte[] content return byteArrayContent.Headers.ContentLength.Value; } + /// - /// Sets the body of the request to be a Stream + /// Sets the body of the request to be a Stream /// /// /// @@ -800,8 +730,9 @@ private static HttpMethod GetHttpMethod(GraphRequestMethod graphRequestMethod) { return new HttpMethod(graphRequestMethod.ToString().ToUpperInvariant()); } + /// - /// Prepare GraphRequestSession to be used downstream. + /// Prepare GraphRequestSession to be used downstream. /// internal virtual void PrepareSession() { @@ -842,28 +773,35 @@ internal virtual void PrepareSession() } } } + /// - /// Validate the Request Uri must have the same Host as GraphHttpClient BaseAddress. + /// Validate the Request Uri must have the same Host as GraphHttpClient BaseAddress. /// /// private void ValidateRequestUri(HttpClient httpClient) { if (Uri == null) { - var error = GetValidationError(Resources.InvokeGraphRequestMissingUriErrorMessage.FormatCurrentCulture(nameof(Uri)), Errors.InvokeGraphRequestInvalidHost, + var error = GetValidationError( + Resources.InvokeGraphRequestMissingUriErrorMessage.FormatCurrentCulture(nameof(Uri)), + Errors.InvokeGraphRequestInvalidHost, nameof(Uri)); ThrowTerminatingError(error); } + // Ensure that the Passed in Uri has the same Host as the HttpClient. if (Uri.IsAbsoluteUri && httpClient.BaseAddress.Host != Uri.Host) { - var error = GetValidationError(Resources.InvokeGraphRequestInvalidHostErrorMessage.FormatCurrentCulture(Uri.Host), Errors.InvokeGraphRequestInvalidHost, + var error = GetValidationError( + Resources.InvokeGraphRequestInvalidHostErrorMessage.FormatCurrentCulture(Uri.Host), + Errors.InvokeGraphRequestInvalidHost, nameof(Uri)); ThrowTerminatingError(error); } } + /// - /// Validate Passed In Parameters + /// Validate Passed In Parameters /// private void ValidateParameters() { @@ -878,7 +816,8 @@ private void ValidateParameters() // When PATCH or POST is specified, ensure a body is present if (Method == GraphRequestMethod.PATCH || Method == GraphRequestMethod.POST && Body == null) { - var error = GetValidationError(Resources.BodyMissingWhenMethodIsSpecified.FormatCurrentCulture(nameof(Body), Method), + var error = GetValidationError( + Resources.BodyMissingWhenMethodIsSpecified.FormatCurrentCulture(nameof(Body), Method), Errors.InvokeGraphRequestBodyMissingWhenMethodIsSpecified, nameof(Body)); ThrowTerminatingError(error); @@ -886,7 +825,9 @@ private void ValidateParameters() if (PassThru && OutputFilePath == null) { - var error = GetValidationError(Resources.PassThruWithOutputFilePathMissing.FormatCurrentCulture(nameof(PassThru), nameof(OutputFilePath)), + var error = GetValidationError( + Resources.PassThruWithOutputFilePathMissing.FormatCurrentCulture(nameof(PassThru), + nameof(OutputFilePath)), Errors.InvokeGraphRequestOutFileMissingException, nameof(PassThru)); ThrowTerminatingError(error); @@ -894,14 +835,17 @@ private void ValidateParameters() if (Authentication == GraphRequestAuthenticationType.Default && !string.IsNullOrWhiteSpace(Token)) { - var error = GetValidationError(Resources.AuthenticationTokenConflict.FormatCurrentCulture(GraphRequestSession, nameof(Token)), + var error = GetValidationError( + Resources.AuthenticationTokenConflict.FormatCurrentCulture(GraphRequestSession, nameof(Token)), Errors.InvokeGraphRequestAuthenticationTokenConflictException); ThrowTerminatingError(error); } + // Token shouldn't be null when UserProvidedToken is specified if (Authentication == GraphRequestAuthenticationType.UserProvidedToken && string.IsNullOrWhiteSpace(Token)) { - var error = GetValidationError(Resources.AuthenticationCredentialNotSupplied.FormatCurrentCulture(Authentication, nameof(Token)), + var error = GetValidationError( + Resources.AuthenticationCredentialNotSupplied.FormatCurrentCulture(Authentication, nameof(Token)), Errors.InvokeGraphRequestAuthenticationTokenConflictException); ThrowTerminatingError(error); } @@ -909,7 +853,8 @@ private void ValidateParameters() // Only Body or InputFilePath can be specified at a time if (Body != null && InputFilePath != null) { - var error = GetValidationError(Resources.BodyConflict.FormatCurrentCulture(nameof(Body), nameof(InputFilePath)), + var error = GetValidationError( + Resources.BodyConflict.FormatCurrentCulture(nameof(Body), nameof(InputFilePath)), Errors.InvokeGraphRequestBodyConflictException); ThrowTerminatingError(error); } @@ -925,26 +870,30 @@ private void ValidateParameters() if (!provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase)) { - errorRecord = GetValidationError(Resources.NotFileSystemPath.FormatCurrentCulture(InputFilePath), + errorRecord = GetValidationError( + Resources.NotFileSystemPath.FormatCurrentCulture(InputFilePath), Errors.InvokeGraphRequestFileNotFilesystemPathException, InputFilePath); } else { if (providerPaths.Count > 1) { - errorRecord = GetValidationError(Resources.MultiplePathsResolved.FormatCurrentCulture(InputFilePath), + errorRecord = GetValidationError( + Resources.MultiplePathsResolved.FormatCurrentCulture(InputFilePath), Errors.InvokeGraphRequestInputFileMultiplePathsResolvedException, InputFilePath); } else if (providerPaths.Count == 0) { - errorRecord = GetValidationError(Resources.NoPathResolved.FormatCurrentCulture(InputFilePath), + errorRecord = GetValidationError( + Resources.NoPathResolved.FormatCurrentCulture(InputFilePath), Errors.InvokeGraphRequestInputFileNoPathResolvedException, InputFilePath); } else { if (Directory.Exists(providerPaths[0])) { - errorRecord = GetValidationError(Resources.DirectoryPathSpecified.FormatCurrentCulture(providerPaths[0]), + errorRecord = GetValidationError( + Resources.DirectoryPathSpecified.FormatCurrentCulture(providerPaths[0]), Errors.InvokeGraphRequestInputFileNotFilePathException, InputFilePath); } @@ -961,7 +910,7 @@ private void ValidateParameters() { errorRecord = new ErrorRecord(providerNotFound.ErrorRecord, providerNotFound); } - catch (System.Management.Automation.DriveNotFoundException driveNotFound) + catch (DriveNotFoundException driveNotFound) { errorRecord = new ErrorRecord(driveNotFound.ErrorRecord, driveNotFound); } @@ -970,8 +919,9 @@ private void ValidateParameters() ThrowTerminatingError(errorRecord); } } + /// - /// Composes a validation error + /// Composes a validation error /// /// /// @@ -982,8 +932,9 @@ private ErrorRecord GetValidationError(string msg, string errorId) var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); return error; } + /// - /// Composes a validation error + /// Composes a validation error /// /// /// @@ -996,8 +947,9 @@ private ErrorRecord GetValidationError(string msg, string errorId, params object var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); return error; } + /// - /// Generate a fully qualified file path + /// Generate a fully qualified file path /// /// /// @@ -1006,5 +958,93 @@ private string QualifyFilePath(string path) var resolvedFilePath = PathUtils.ResolveFilePath(path, this, true); return resolvedFilePath; } + + #region CmdLet LifeCycle + + protected override void BeginProcessing() + { + if (Break) + { + this.Break(); + } + + ValidateParameters(); + base.BeginProcessing(); + } + + protected override void ProcessRecord() + { + base.ProcessRecord(); + try + { + PrepareSession(); + using (var client = GetHttpClient()) + { + ValidateRequestUri(client); + using (var httpRequestMessage = GetRequest(Uri)) + { + using (var httpRequestMessageFormatter = new HttpMessageFormatter(httpRequestMessage)) + { + FillRequestStream(httpRequestMessage); + try + { + ReportRequestStatus(httpRequestMessageFormatter.HttpRequestMessage); + var httpResponseMessage = GetResponse(client, httpRequestMessage); + using (var httpResponseMessageFormatter = new HttpMessageFormatter(httpResponseMessage)) + { + ReportResponseStatus(httpResponseMessageFormatter.HttpResponseMessage); + var isSuccess = httpResponseMessage.IsSuccessStatusCode; + if (ShouldCheckHttpStatus && !isSuccess) + { + var httpErrorRecord = + GenerateHttpErrorRecord(httpResponseMessageFormatter, httpRequestMessage); + ThrowTerminatingError(httpErrorRecord); + } + + ProcessResponse(httpResponseMessage); + } + } + catch (HttpRequestException ex) + { + var er = new ErrorRecord(ex, Errors.InvokeGraphHttpResponseException, + ErrorCategory.InvalidOperation, + httpRequestMessage); + if (ex.InnerException != null) + { + er.ErrorDetails = new ErrorDetails(ex.InnerException.Message); + } + + ThrowTerminatingError(er); + } + } + } + } + } + catch (HttpRequestException httpRequestException) + { + WriteError(new ErrorRecord(httpRequestException, ErrorCategory.ConnectionError.ToString(), + ErrorCategory.InvalidResult, null)); + throw; + } + catch (Exception exception) + { + WriteError(new ErrorRecord(exception, ErrorCategory.ConnectionError.ToString(), + ErrorCategory.InvalidOperation, null)); + throw; + } + } + + protected override void EndProcessing() + { + base.EndProcessing(); + } + + protected override void StopProcessing() + { + _cancellationTokenSource.Cancel(); + base.StopProcessing(); + } + + #endregion } } \ No newline at end of file From 276f48c803696b7513f000366eea117a2860fc5c Mon Sep 17 00:00:00 2001 From: George Ndungu Date: Sun, 12 Jul 2020 02:40:39 +0300 Subject: [PATCH 08/16] Validate that Uri is not an empty string. Perf improvement, check for token length first. --- .../Authentication/Cmdlets/InvokeGraphRequest.cs | 10 +++++++++- .../Authentication/Helpers/ContentHelper.cs | 15 ++++++--------- .../Helpers/HttpMessageFormatter.cs | 2 +- .../Authentication/Helpers/WebResponseHelper.cs | 2 +- .../Properties/Resources.Designer.cs | 9 +++++++++ .../Authentication/Properties/Resources.resx | 3 +++ 6 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index 6897e4042bb..9130024b92a 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -447,7 +447,7 @@ internal void ProcessResponse(HttpResponseMessage response) if (!string.IsNullOrEmpty(ResponseHeadersVariable)) { var vi = SessionState.PSVariable; - vi.Set(ResponseHeadersVariable, response.GetHeadersDictionary()); + vi.Set(ResponseHeadersVariable, response.GetHttpResponseHeaders()); } } @@ -789,6 +789,14 @@ private void ValidateRequestUri(HttpClient httpClient) ThrowTerminatingError(error); } + if (string.IsNullOrWhiteSpace(Uri.ToString())) + { + var error = GetValidationError( + Resources.InvokeGraphRequestInvalidUriErrorMessage.FormatCurrentCulture(nameof(Uri)), + Errors.InvokeGraphRequestInvalidHost, + nameof(Uri)); + ThrowTerminatingError(error); + } // Ensure that the Passed in Uri has the same Host as the HttpClient. if (Uri.IsAbsoluteUri && httpClient.BaseAddress.Host != Uri.Host) { diff --git a/src/Authentication/Authentication/Helpers/ContentHelper.cs b/src/Authentication/Authentication/Helpers/ContentHelper.cs index d3f996721fd..8526d0cca9a 100644 --- a/src/Authentication/Authentication/Helpers/ContentHelper.cs +++ b/src/Authentication/Authentication/Helpers/ContentHelper.cs @@ -34,7 +34,7 @@ internal static RestReturnType CheckReturnType(this HttpResponseMessage response rt = RestReturnType.Detect; else if (ContentHelper.IsJson(contentType)) rt = RestReturnType.Json; - else if (ContentHelper.IsXml(contentType)) + else if (ContentHelper.IsXml(contentType)) rt = RestReturnType.Xml; return rt; @@ -140,17 +140,14 @@ private static bool CheckIsText(string contentType) // Media types registered with Windows as having a perceived type of text, are text using (var contentTypeKey = Registry.ClassesRoot.OpenSubKey(@"MIME\Database\Content Type\" + contentType)) { - if (contentTypeKey != null) + if (contentTypeKey?.GetValue("Extension") is string extension) { - if (contentTypeKey.GetValue("Extension") is string extension) + using (var extensionKey = Registry.ClassesRoot.OpenSubKey(extension)) { - using (var extensionKey = Registry.ClassesRoot.OpenSubKey(extension)) + if (extensionKey != null) { - if (extensionKey != null) - { - var perceivedType = extensionKey.GetValue("PerceivedType") as string; - isText = perceivedType == "text"; - } + var perceivedType = extensionKey.GetValue("PerceivedType") as string; + isText = perceivedType == "text"; } } } diff --git a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs index 36151d1c2f1..7e303d23deb 100644 --- a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs +++ b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs @@ -184,7 +184,7 @@ public static string UnquoteToken(string token) return token; } - if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1) + if (token.Length > 1 && token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal)) { return token.Substring(1, token.Length - 2); } diff --git a/src/Authentication/Authentication/Helpers/WebResponseHelper.cs b/src/Authentication/Authentication/Helpers/WebResponseHelper.cs index e1bd263f312..35fa71ca59b 100644 --- a/src/Authentication/Authentication/Helpers/WebResponseHelper.cs +++ b/src/Authentication/Authentication/Helpers/WebResponseHelper.cs @@ -12,7 +12,7 @@ namespace Microsoft.Graph.PowerShell.Authentication.Helpers { internal static class WebResponseHelper { - internal static Dictionary> GetHeadersDictionary(this HttpResponseMessage response) + internal static Dictionary> GetHttpResponseHeaders(this HttpResponseMessage response) { var headers = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var entry in response.Headers) diff --git a/src/Authentication/Authentication/Properties/Resources.Designer.cs b/src/Authentication/Authentication/Properties/Resources.Designer.cs index 07e9ff5e230..cee84d64c11 100644 --- a/src/Authentication/Authentication/Properties/Resources.Designer.cs +++ b/src/Authentication/Authentication/Properties/Resources.Designer.cs @@ -177,6 +177,15 @@ internal static string InvokeGraphRequestInvalidHostErrorMessage { } } + /// + /// Looks up a localized string similar to Empty string not allowed for {0}. + /// + internal static string InvokeGraphRequestInvalidUriErrorMessage { + get { + return ResourceManager.GetString("InvokeGraphRequestInvalidUriErrorMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Must specify {0}. /// diff --git a/src/Authentication/Authentication/Properties/Resources.resx b/src/Authentication/Authentication/Properties/Resources.resx index 1b47cf37cc9..f892a93e4d4 100644 --- a/src/Authentication/Authentication/Properties/Resources.resx +++ b/src/Authentication/Authentication/Properties/Resources.resx @@ -190,4 +190,7 @@ Number of bytes processed: {0} + + Empty string not allowed for {0} + \ No newline at end of file From eb3798234feae4ee17f8a31c6baa57fdd788e3fc Mon Sep 17 00:00:00 2001 From: George Date: Sun, 12 Jul 2020 03:55:39 +0300 Subject: [PATCH 09/16] Remove header exclusion list. --- .../Helpers/HttpMessageFormatter.cs | 129 ++++++++---------- 1 file changed, 58 insertions(+), 71 deletions(-) diff --git a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs index 7e303d23deb..94fba72da4a 100644 --- a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs +++ b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs @@ -1,26 +1,22 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; +using System; using System.Collections.Generic; using System.Diagnostics.Contracts; -// ------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. -// ------------------------------------------------------------------------------ - using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; -using Microsoft.Graph.PowerShell.Authentication.Properties; +using Microsoft.Graph.PowerShell.Authentication.Properties + ; // ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ namespace Microsoft.Graph.PowerShell.Authentication.Helpers { /// - /// Derived class which can encapsulate an - /// or an as an entity with media type "application/http". + /// Derived class which can encapsulate an + /// or an as an entity with media type "application/http". /// internal class HttpMessageFormatter : HttpContent { @@ -50,31 +46,21 @@ internal class HttpMessageFormatter : HttpContent private static readonly HashSet SpaceSeparatedValueHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase) { - HttpKnownHeaderNames.UserAgent, + HttpKnownHeaderNames.UserAgent }; // Set of header fields that should not get serialized private static readonly HashSet NeverSerializedHeaderFields = - new HashSet(StringComparer.OrdinalIgnoreCase) - { - HttpKnownHeaderNames.SdkVersion, - HttpKnownHeaderNames.FeatureFlag, - HttpKnownHeaderNames.Authorization, - HttpKnownHeaderNames.CacheControl, - HttpKnownHeaderNames.TransferEncoding, - HttpKnownHeaderNames.Duration, - HttpKnownHeaderNames.StrictTransportSecurity, - HttpKnownHeaderNames.Date - }; + new HashSet(StringComparer.OrdinalIgnoreCase); private bool _contentConsumed; private Lazy> _streamTask; /// - /// Initializes a new instance of the class encapsulating an - /// . + /// Initializes a new instance of the class encapsulating an + /// . /// - /// The instance to encapsulate. + /// The instance to encapsulate. public HttpMessageFormatter(HttpRequestMessage httpRequest) { HttpRequestMessage = httpRequest ?? throw new ArgumentNullException(nameof(httpRequest)); @@ -85,10 +71,10 @@ public HttpMessageFormatter(HttpRequestMessage httpRequest) } /// - /// Initializes a new instance of the class encapsulating an - /// . + /// Initializes a new instance of the class encapsulating an + /// . /// - /// The instance to encapsulate. + /// The instance to encapsulate. public HttpMessageFormatter(HttpResponseMessage httpResponse) { HttpResponseMessage = httpResponse ?? throw new ArgumentNullException(nameof(httpResponse)); @@ -98,17 +84,18 @@ public HttpMessageFormatter(HttpResponseMessage httpResponse) InitializeStreamTask(); } - private HttpContent Content => HttpRequestMessage != null ? HttpRequestMessage.Content : HttpResponseMessage.Content; + private HttpContent Content => + HttpRequestMessage != null ? HttpRequestMessage.Content : HttpResponseMessage.Content; /// - /// Gets the HTTP request message. + /// Gets the HTTP request message. /// - public HttpRequestMessage HttpRequestMessage { get; private set; } + public HttpRequestMessage HttpRequestMessage { get; } /// - /// Gets the HTTP response message. + /// Gets the HTTP response message. /// - public HttpResponseMessage HttpResponseMessage { get; private set; } + public HttpResponseMessage HttpResponseMessage { get; } private void InitializeStreamTask() { @@ -116,11 +103,11 @@ private void InitializeStreamTask() } /// - /// Validates whether the content contains an HTTP Request or an HTTP Response. + /// Validates whether the content contains an HTTP Request or an HTTP Response. /// /// The content to validate. /// if set to true if the content is either an HTTP Request or an HTTP Response. - /// Indicates whether validation failure should result in an or not. + /// Indicates whether validation failure should result in an or not. /// true if content is either an HTTP Request or an HTTP Response internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequest, bool throwOnError) { @@ -136,29 +123,29 @@ internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequ { if (throwOnError) { - throw new ArgumentException(Resources.HttpMessageInvalidMediaType.FormatCurrentCulture(contentType), nameof(content)); - } - else - { - return false; + throw new ArgumentException( + Resources.HttpMessageInvalidMediaType.FormatCurrentCulture(contentType), nameof(content)); } + + return false; } foreach (var parameter in contentType.Parameters) { if (parameter.Name.Equals(MsgTypeParameter, StringComparison.OrdinalIgnoreCase)) { - string msgType = UnquoteToken(parameter.Value); - if (!msgType.Equals(isRequest ? DefaultRequestMsgType : DefaultResponseMsgType, StringComparison.OrdinalIgnoreCase)) + var msgType = UnquoteToken(parameter.Value); + if (!msgType.Equals(isRequest ? DefaultRequestMsgType : DefaultResponseMsgType, + StringComparison.OrdinalIgnoreCase)) { if (throwOnError) { - throw new ArgumentException(Resources.HttpMessageInvalidMediaType.FormatCurrentCulture(msgType), nameof(content)); - } - else - { - return false; + throw new ArgumentException( + Resources.HttpMessageInvalidMediaType.FormatCurrentCulture(msgType), + nameof(content)); } + + return false; } return true; @@ -170,10 +157,8 @@ internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequ { throw new ArgumentException(Resources.HttpMessageInvalidMediaType, "content"); } - else - { - return false; - } + + return false; } @@ -184,7 +169,8 @@ public static string UnquoteToken(string token) return token; } - if (token.Length > 1 && token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal)) + if (token.Length > 1 && token.StartsWith("\"", StringComparison.Ordinal) && + token.EndsWith("\"", StringComparison.Ordinal)) { return token.Substring(1, token.Length - 2); } @@ -194,11 +180,11 @@ public static string UnquoteToken(string token) /// - /// Asynchronously serializes the object's content to the given . + /// Asynchronously serializes the object's content to the given . /// - /// The to which to write. - /// The associated . - /// A instance that is asynchronously serializing the object's content. + /// The to which to write. + /// The associated . + /// A instance that is asynchronously serializing the object's content. protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { if (stream == null) @@ -206,19 +192,19 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon throw new ArgumentNullException(nameof(stream)); } - byte[] header = SerializeHeader(); + var header = SerializeHeader(); await stream.WriteAsync(header, 0, header.Length); if (Content != null) { - Stream readStream = await _streamTask.Value; + var readStream = await _streamTask.Value; ValidateStreamForReading(readStream); await Content.CopyToAsync(stream); } } /// - /// Computes the length of the stream if possible. + /// Computes the length of the stream if possible. /// /// The computed length of the stream. /// true if the length has been computed; otherwise false. @@ -234,20 +220,20 @@ protected override bool TryComputeLength(out long length) // For #3, we return true & the size of our headers + the content length // For #4, we return true & the size of our headers - bool hasContent = _streamTask.Value != null; + var hasContent = _streamTask.Value != null; length = 0; // Cases #1, #2, #3 // We serialize header to a StringBuilder so that we can determine the length // following the pattern for HttpContent to try and determine the message length. // The perf overhead is no larger than for the other HttpContent implementations. - byte[] header = SerializeHeader(); + var header = SerializeHeader(); length += header.Length; return true; } /// - /// Serializes the HTTP request line. + /// Serializes the HTTP request line. /// /// Where to write the request line. /// The HTTP request. @@ -266,7 +252,7 @@ private static void SerializeRequestLine(StringBuilder message, HttpRequestMessa } /// - /// Serializes the HTTP status line. + /// Serializes the HTTP status line. /// /// Where to write the status line. /// The HTTP response. @@ -274,12 +260,12 @@ private static void SerializeStatusLine(StringBuilder message, HttpResponseMessa { Contract.Assert(message != null, "message cannot be null"); message.Append($"HTTP/{(httpResponse.Version != null ? httpResponse.Version.ToString(2) : "1.1")}{SP}"); - message.Append((int)httpResponse.StatusCode + SP); + message.Append((int) httpResponse.StatusCode + SP); message.Append(httpResponse.ReasonPhrase + CRLF); } /// - /// Serializes the header fields. + /// Serializes the header fields. /// /// Where to write the status line. /// The headers to write. @@ -288,26 +274,27 @@ private static void SerializeHeaderFields(StringBuilder message, HttpHeaders hea Contract.Assert(message != null, "message cannot be null"); if (headers != null) { - foreach (KeyValuePair> header in headers) + foreach (var header in headers) { if (NeverSerializedHeaderFields.Contains(header.Key)) { continue; } + if (SingleValueHeaderFields.Contains(header.Key)) { - foreach (string value in header.Value) + foreach (var value in header.Value) { message.Append(header.Key + ColonSP + value + CRLF); } } else if (SpaceSeparatedValueHeaderFields.Contains(header.Key)) { - message.Append(header.Key + ColonSP + String.Join(SP, header.Value) + CRLF); + message.Append(header.Key + ColonSP + string.Join(SP, header.Value) + CRLF); } else { - message.Append(header.Key + ColonSP + String.Join(CommaSeparator, header.Value) + CRLF); + message.Append(header.Key + ColonSP + string.Join(CommaSeparator, header.Value) + CRLF); } } } @@ -315,7 +302,7 @@ private static void SerializeHeaderFields(StringBuilder message, HttpHeaders hea private byte[] SerializeHeader() { - StringBuilder message = new StringBuilder(DefaultHeaderAllocation); + var message = new StringBuilder(DefaultHeaderAllocation); HttpHeaders headers; HttpContent content; if (HttpRequestMessage != null) From 8c3a30a2446b9edc44273a0825cf2e5abef3b7ae Mon Sep 17 00:00:00 2001 From: George Date: Sun, 12 Jul 2020 04:39:34 +0300 Subject: [PATCH 10/16] Use SecureString to handle Tokens. --- .../Authentication/Cmdlets/InvokeGraphRequest.cs | 13 ++++++++----- .../Authentication/Helpers/GraphRequestSession.cs | 4 ++-- .../Helpers/InvokeGraphRequestAuthProvider.cs | 4 +++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index 9130024b92a..d416f0ca725 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -7,12 +7,15 @@ using System.Management.Automation; using System.Net.Http; using System.Net.Http.Headers; +using System.Security; using System.Text; using System.Threading; + using Microsoft.Graph.PowerShell.Authentication.Helpers; using Microsoft.Graph.PowerShell.Authentication.Models; using Microsoft.Graph.PowerShell.Authentication.Properties; using Microsoft.PowerShell.Commands; + using DriveNotFoundException = System.Management.Automation.DriveNotFoundException; namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets @@ -113,7 +116,7 @@ public InvokeGraphRequest() ParameterSetName = Constants.UserParameterSet, Position = 9, HelpMessage = "OAuth or Bearer Token to use instead of already acquired token")] - public string Token { get; set; } + public SecureString Token { get; set; } /// /// Add headers to Request Header collection without validation @@ -745,7 +748,7 @@ internal virtual void PrepareSession() vi.Set(SessionVariable, GraphRequestSession); } - if (Authentication == GraphRequestAuthenticationType.UserProvidedToken && !string.IsNullOrWhiteSpace(Token)) + if (Authentication == GraphRequestAuthenticationType.UserProvidedToken && Token != null) { GraphRequestSession.Token = Token; GraphRequestSession.AuthenticationType = Authentication; @@ -841,16 +844,16 @@ private void ValidateParameters() ThrowTerminatingError(error); } - if (Authentication == GraphRequestAuthenticationType.Default && !string.IsNullOrWhiteSpace(Token)) + if (Authentication == GraphRequestAuthenticationType.Default && Token != null) { var error = GetValidationError( - Resources.AuthenticationTokenConflict.FormatCurrentCulture(GraphRequestSession, nameof(Token)), + Resources.AuthenticationTokenConflict.FormatCurrentCulture(Authentication, nameof(Token)), Errors.InvokeGraphRequestAuthenticationTokenConflictException); ThrowTerminatingError(error); } // Token shouldn't be null when UserProvidedToken is specified - if (Authentication == GraphRequestAuthenticationType.UserProvidedToken && string.IsNullOrWhiteSpace(Token)) + if (Authentication == GraphRequestAuthenticationType.UserProvidedToken && Token == null) { var error = GetValidationError( Resources.AuthenticationCredentialNotSupplied.FormatCurrentCulture(Authentication, nameof(Token)), diff --git a/src/Authentication/Authentication/Helpers/GraphRequestSession.cs b/src/Authentication/Authentication/Helpers/GraphRequestSession.cs index 4fc787d6c62..d142b9798cf 100644 --- a/src/Authentication/Authentication/Helpers/GraphRequestSession.cs +++ b/src/Authentication/Authentication/Helpers/GraphRequestSession.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; - +using System.Security; using Microsoft.Graph.PowerShell.Authentication.Cmdlets; using Microsoft.Graph.PowerShell.Authentication.Models; @@ -30,7 +30,7 @@ public class GraphRequestSession /// /// Gets or Sets a User Specified JWT Token /// - public string Token { get; set; } + public SecureString Token { get; set; } /// /// Gets or Sets the AuthenticationType to be used for the current Session diff --git a/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs b/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs index b93d8b636e2..72326ce7c1f 100644 --- a/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs +++ b/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; +using System.Management.Automation.Language; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -22,7 +24,7 @@ public InvokeGraphRequestAuthProvider(GraphRequestSession session) public Task AuthenticateRequestAsync(HttpRequestMessage request) { - var authenticationHeader = new AuthenticationHeaderValue("Bearer", _session.Token); + var authenticationHeader = new AuthenticationHeaderValue("Bearer", new NetworkCredential(string.Empty, _session.Token).Password); request.Headers.Authorization = authenticationHeader; return Task.CompletedTask; } From d9d9a7d71c77e70f3295c4c1db7c76f1ff050630 Mon Sep 17 00:00:00 2001 From: George Date: Sun, 12 Jul 2020 04:42:52 +0300 Subject: [PATCH 11/16] Operator precedence. --- src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index d416f0ca725..78623597404 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -825,7 +825,7 @@ private void ValidateParameters() } // When PATCH or POST is specified, ensure a body is present - if (Method == GraphRequestMethod.PATCH || Method == GraphRequestMethod.POST && Body == null) + if ((Method == GraphRequestMethod.PATCH || Method == GraphRequestMethod.POST) && Body == null) { var error = GetValidationError( Resources.BodyMissingWhenMethodIsSpecified.FormatCurrentCulture(nameof(Body), Method), From 2f9908c1d1ff8f21f7d8fce50b6bf5d7cfe026df Mon Sep 17 00:00:00 2001 From: George Date: Sun, 12 Jul 2020 04:45:00 +0300 Subject: [PATCH 12/16] Avoid converting to dict and then discarding without using. --- .../Authentication/Cmdlets/InvokeGraphRequest.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index 78623597404..02ebf135137 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -336,8 +336,7 @@ private Uri PrepareUri(Uri uri) { // before creating the web request, // preprocess Body if content is a dictionary and method is GET (set as query) - LanguagePrimitives.TryConvertTo(Body, out IDictionary bodyAsDictionary); - if (bodyAsDictionary != null && Method == GraphRequestMethod.GET) + if (Method == GraphRequestMethod.GET && LanguagePrimitives.TryConvertTo(Body, out IDictionary bodyAsDictionary)) { var uriBuilder = new UriBuilder(uri); if (uriBuilder.Query != null && uriBuilder.Query.Length > 1) From 06880684c5e71465d95c6e85496d82f704a711e7 Mon Sep 17 00:00:00 2001 From: George Date: Sun, 12 Jul 2020 04:58:42 +0300 Subject: [PATCH 13/16] Try to infer encoding from charset, otherwise default to system default. --- .../Cmdlets/InvokeGraphRequest.cs | 2 -- .../Helpers/HttpMessageFormatter.cs | 2 +- .../Authentication/Helpers/StreamHelper.cs | 17 ----------------- 3 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index 02ebf135137..6d985679dcb 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -377,8 +377,6 @@ internal void ProcessResponse(HttpResponseMessage response) var charSet = response.Content.Headers.ContentType?.CharSet; if (!string.IsNullOrEmpty(charSet)) { - // NOTE: Don't use ContentHelper.GetEncoding; it returns a - // default which bypasses checking for a meta charset value. charSet.TryGetEncoding(out encoding); } diff --git a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs index 94fba72da4a..994764ad83f 100644 --- a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs +++ b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs @@ -155,7 +155,7 @@ internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequ if (throwOnError) { - throw new ArgumentException(Resources.HttpMessageInvalidMediaType, "content"); + throw new ArgumentException(Resources.HttpMessageInvalidMediaType, nameof(content)); } return false; diff --git a/src/Authentication/Authentication/Helpers/StreamHelper.cs b/src/Authentication/Authentication/Helpers/StreamHelper.cs index ecaf3fd7d92..7329861cdb3 100644 --- a/src/Authentication/Authentication/Helpers/StreamHelper.cs +++ b/src/Authentication/Authentication/Helpers/StreamHelper.cs @@ -61,29 +61,12 @@ internal static bool TryGetEncoding(this string characterSet, out Encoding encod internal static string DecodeStream(this BufferingStreamReader responseStream, ref Encoding encoding) { - var isDefaultEncoding = false; if (encoding == null) { // Use the default encoding if one wasn't provided encoding = ContentHelper.GetDefaultEncoding(); - isDefaultEncoding = true; } - var content = responseStream.StreamToString(encoding); - if (isDefaultEncoding) - { - do - { - // check for a charset attribute on the meta element to override the default. - - var localEncoding = Encoding.UTF8; - responseStream.Seek(0, SeekOrigin.Begin); - content = responseStream.StreamToString(localEncoding); - // report the encoding used. - encoding = localEncoding; - } while (false); - } - return content; } From 0a16c43e1ba790b0b454fce06789267dd813d773 Mon Sep 17 00:00:00 2001 From: George Date: Sun, 12 Jul 2020 05:00:39 +0300 Subject: [PATCH 14/16] Simplify to trim op. --- .../Helpers/HttpMessageFormatter.cs | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs index 994764ad83f..d19a8fb690e 100644 --- a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs +++ b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs @@ -134,7 +134,7 @@ internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequ { if (parameter.Name.Equals(MsgTypeParameter, StringComparison.OrdinalIgnoreCase)) { - var msgType = UnquoteToken(parameter.Value); + var msgType = parameter.Value.Trim('"'); if (!msgType.Equals(isRequest ? DefaultRequestMsgType : DefaultResponseMsgType, StringComparison.OrdinalIgnoreCase)) { @@ -160,25 +160,7 @@ internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequ return false; } - - - public static string UnquoteToken(string token) - { - if (string.IsNullOrWhiteSpace(token)) - { - return token; - } - - if (token.Length > 1 && token.StartsWith("\"", StringComparison.Ordinal) && - token.EndsWith("\"", StringComparison.Ordinal)) - { - return token.Substring(1, token.Length - 2); - } - - return token; - } - - + /// /// Asynchronously serializes the object's content to the given . /// From 05507f899188d7d8085945d1b5e0cb173844c439 Mon Sep 17 00:00:00 2001 From: George Date: Sun, 12 Jul 2020 06:19:31 +0300 Subject: [PATCH 15/16] Extra handlign of Body and Inputfile. Extra validation. --- .../Cmdlets/InvokeGraphRequest.cs | 36 +++++++++++++------ .../Properties/Resources.Designer.cs | 9 +++++ .../Authentication/Properties/Resources.resx | 3 ++ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs index 6d985679dcb..fd98edb37e1 100644 --- a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -73,6 +73,7 @@ public InvokeGraphRequest() /// /// Relative or absolute path where the response body will be saved. + /// Not allowed when InferOutputFileName is specified. /// [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, @@ -81,7 +82,8 @@ public InvokeGraphRequest() public string OutputFilePath { get; set; } /// - /// Infer Download FileName from ContentDisposition Header, + /// Infer Download FileName from ContentDisposition Header. + /// Not allowed when OutputFilePath is specified. /// [Parameter(Mandatory = false, ParameterSetName = Constants.UserParameterSet, @@ -211,14 +213,20 @@ public InvokeGraphRequest() internal string QualifiedOutFile => QualifyFilePath(OutputFilePath); internal bool ShouldSaveToOutFile => !string.IsNullOrEmpty(OutputFilePath); - - internal bool ShouldWriteToPipeline => !ShouldSaveToOutFile && !InferOutputFileName || PassThru; + /// + /// Only write to pipeline if outfile is not specified, inference is not specified but PassThru is set. + /// + internal bool ShouldWriteToPipeline => (!ShouldSaveToOutFile && !InferOutputFileName) || PassThru; internal bool ShouldCheckHttpStatus => !SkipHttpErrorCheck; private static ErrorRecord GenerateHttpErrorRecord(HttpMessageFormatter httpResponseMessageFormatter, HttpRequestMessage httpRequestMessage) { + // Load into buffer to avoid stream already consumed issues. + httpResponseMessageFormatter.LoadIntoBufferAsync() + .GetAwaiter() + .GetResult(); var currentResponse = httpResponseMessageFormatter.HttpResponseMessage; var errorMessage = Resources.ResponseStatusCodeFailure.FormatCurrentCulture(currentResponse.StatusCode, @@ -343,7 +351,7 @@ private Uri PrepareUri(Uri uri) { uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + bodyAsDictionary.FormatDictionary(); } - else + else if (bodyAsDictionary != null) { uriBuilder.Query = bodyAsDictionary.FormatDictionary(); } @@ -441,7 +449,7 @@ internal void ProcessResponse(HttpResponseMessage response) if (!string.IsNullOrEmpty(StatusCodeVariable)) { var vi = SessionState.PSVariable; - vi.Set(StatusCodeVariable, (int) response.StatusCode); + vi.Set(StatusCodeVariable, (int)response.StatusCode); } if (!string.IsNullOrEmpty(ResponseHeadersVariable)) @@ -496,9 +504,10 @@ private HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage re } var cancellationToken = _cancellationTokenSource.Token; - var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + var response = client.SendAsync(request, cancellationToken) .GetAwaiter() .GetResult(); + return response; } @@ -619,7 +628,7 @@ private void FillRequestStream(HttpRequestMessage request) { content = psBody.BaseObject; } - else if (content is IDictionary dictionary && request.Method != HttpMethod.Get) + if (content is IDictionary dictionary && request.Method != HttpMethod.Get) { SetRequestContent(request, dictionary); } @@ -635,7 +644,7 @@ private void FillRequestStream(HttpRequestMessage request) { // Assume its a string SetRequestContent(request, - (string) LanguagePrimitives.ConvertTo(content, typeof(string), CultureInfo.InvariantCulture)); + (string)LanguagePrimitives.ConvertTo(content, typeof(string), CultureInfo.InvariantCulture)); } } else if (InputFilePath != null) // copy InputFilePath data @@ -822,7 +831,7 @@ private void ValidateParameters() } // When PATCH or POST is specified, ensure a body is present - if ((Method == GraphRequestMethod.PATCH || Method == GraphRequestMethod.POST) && Body == null) + if ((Method == GraphRequestMethod.PATCH || Method == GraphRequestMethod.POST) && (Body == null && string.IsNullOrWhiteSpace(InputFilePath))) { var error = GetValidationError( Resources.BodyMissingWhenMethodIsSpecified.FormatCurrentCulture(nameof(Body), Method), @@ -859,7 +868,7 @@ private void ValidateParameters() } // Only Body or InputFilePath can be specified at a time - if (Body != null && InputFilePath != null) + if (Body != null && !string.IsNullOrWhiteSpace(InputFilePath)) { var error = GetValidationError( Resources.BodyConflict.FormatCurrentCulture(nameof(Body), nameof(InputFilePath)), @@ -867,6 +876,13 @@ private void ValidateParameters() ThrowTerminatingError(error); } + if (InferOutputFileName.IsPresent && !string.IsNullOrWhiteSpace(OutputFilePath)) + { + var error = GetValidationError( + Resources.InferFileNameOutFilePathConflict.FormatCurrentCulture(nameof(InferOutputFileName), nameof(OutputFilePath)), + Errors.InvokeGraphRequestBodyConflictException); + ThrowTerminatingError(error); + } // Ensure InputFilePath is an Existing Item if (InputFilePath != null) { diff --git a/src/Authentication/Authentication/Properties/Resources.Designer.cs b/src/Authentication/Authentication/Properties/Resources.Designer.cs index cee84d64c11..9489b3d6b04 100644 --- a/src/Authentication/Authentication/Properties/Resources.Designer.cs +++ b/src/Authentication/Authentication/Properties/Resources.Designer.cs @@ -150,6 +150,15 @@ internal static string HttpMessageInvalidMediaType { } } + /// + /// Looks up a localized string similar to The cmdlet cannot run because the following conflicting parameters are specified: {0} and {1}. Specify either {0} or {1} then retry.. + /// + internal static string InferFileNameOutFilePathConflict { + get { + return ResourceManager.GetString("InferFileNameOutFilePathConflict", resourceCulture); + } + } + /// /// Looks up a localized string similar to Could not Infer File Name. /// diff --git a/src/Authentication/Authentication/Properties/Resources.resx b/src/Authentication/Authentication/Properties/Resources.resx index f892a93e4d4..2329dc6b40b 100644 --- a/src/Authentication/Authentication/Properties/Resources.resx +++ b/src/Authentication/Authentication/Properties/Resources.resx @@ -193,4 +193,7 @@ Empty string not allowed for {0} + + The cmdlet cannot run because the following conflicting parameters are specified: {0} and {1}. Specify either {0} or {1} then retry. + \ No newline at end of file From 208a7ea2dea5ea5ad9f3f1b6aea37ab038205f21 Mon Sep 17 00:00:00 2001 From: George <1641829+finsharp@users.noreply.github.com> Date: Mon, 13 Jul 2020 18:05:22 +0300 Subject: [PATCH 16/16] incorrectly formatted file. --- .../Authentication/Helpers/HttpMessageFormatter.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs index d19a8fb690e..229e247d512 100644 --- a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs +++ b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs @@ -1,4 +1,8 @@ -using System; +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.IO; @@ -7,10 +11,7 @@ using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; -using Microsoft.Graph.PowerShell.Authentication.Properties - ; // ------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. -// ------------------------------------------------------------------------------ +using Microsoft.Graph.PowerShell.Authentication.Properties; namespace Microsoft.Graph.PowerShell.Authentication.Helpers { @@ -330,4 +331,4 @@ private void ValidateStreamForReading(Stream stream) _contentConsumed = true; } } -} \ No newline at end of file +}