Skip to content

Invoke-GraphRequest cmdlet to allow direct requests using underlying HttpClient #245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,074 changes: 1,074 additions & 0 deletions src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions src/Authentication/Authentication/Helpers/AttachDebugger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// ------------------------------------------------------------------------------
// 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 System.Threading;
using Debugger = System.Diagnostics.Debugger;

namespace Microsoft.Graph.PowerShell.Authentication.Helpers
{
internal static class AttachDebugger
{
internal static void Break(this PSCmdlet invokedCmdLet)
{
while (!Debugger.IsAttached)
{
Console.Error.WriteLine($"Waiting for debugger to attach to process {Process.GetCurrentProcess().Id}");
for (var i = 0; i < 50; i++)
{
if (Debugger.IsAttached)
{
break;
}

Thread.Sleep(100);
Console.Error.Write(".");
}

Console.Error.WriteLine();
}

Debugger.Break();
}
}
}
105 changes: 105 additions & 0 deletions src/Authentication/Authentication/Helpers/BufferingStreamReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// ------------------------------------------------------------------------------
// 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;

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)
{
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.
var 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.
var 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();
}
}
}
188 changes: 188 additions & 0 deletions src/Authentication/Authentication/Helpers/ContentHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// ------------------------------------------------------------------------------
// 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;

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
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[] ContentTypeParamSeparator = { ';' };

#endregion Fields

#region Internal Methods

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;
}

internal static Encoding GetDefaultEncoding()
{
return GetEncodingOrDefault(null);
}

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;
try
{
encoding = Encoding.GetEncoding(codepage);
}
catch (ArgumentException)
{
// 0, default code page
encoding = Encoding.GetEncoding(0);
}

return encoding;
}

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
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;
}

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?.GetValue("Extension") is string extension)
{
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
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)
{
if (string.IsNullOrEmpty(contentType))
return null;

var sig = contentType.Split(ContentTypeParamSeparator, 2)[0].ToUpperInvariant();
return sig;
}

#endregion Private Helper Methods
}
}
23 changes: 23 additions & 0 deletions src/Authentication/Authentication/Helpers/Errors.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading