Skip to content

Commit 3cb927d

Browse files
georgendGeorge Ndungu
andauthored
Invoke-GraphRequest cmdlet to allow direct requests using underlying HttpClient (#245)
- 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) Co-authored-by: George Ndungu <gendungu@microsoft.com>
1 parent 91adca8 commit 3cb927d

24 files changed

Lines changed: 2970 additions & 4 deletions

src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs

Lines changed: 1074 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
using System;
6+
using System.Diagnostics;
7+
using System.Management.Automation;
8+
using System.Threading;
9+
using Debugger = System.Diagnostics.Debugger;
10+
11+
namespace Microsoft.Graph.PowerShell.Authentication.Helpers
12+
{
13+
internal static class AttachDebugger
14+
{
15+
internal static void Break(this PSCmdlet invokedCmdLet)
16+
{
17+
while (!Debugger.IsAttached)
18+
{
19+
Console.Error.WriteLine($"Waiting for debugger to attach to process {Process.GetCurrentProcess().Id}");
20+
for (var i = 0; i < 50; i++)
21+
{
22+
if (Debugger.IsAttached)
23+
{
24+
break;
25+
}
26+
27+
Thread.Sleep(100);
28+
Console.Error.Write(".");
29+
}
30+
31+
Console.Error.WriteLine();
32+
}
33+
34+
Debugger.Break();
35+
}
36+
}
37+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
using System;
6+
using System.IO;
7+
8+
namespace Microsoft.Graph.PowerShell.Authentication.Helpers
9+
{
10+
internal class BufferingStreamReader : Stream
11+
{
12+
internal BufferingStreamReader(Stream baseStream)
13+
{
14+
_baseStream = baseStream;
15+
_streamBuffer = new MemoryStream();
16+
_length = long.MaxValue;
17+
_copyBuffer = new byte[4096];
18+
}
19+
20+
private readonly Stream _baseStream;
21+
private readonly MemoryStream _streamBuffer;
22+
private readonly byte[] _copyBuffer;
23+
24+
public override bool CanRead => true;
25+
26+
public override bool CanSeek => true;
27+
28+
public override bool CanWrite => false;
29+
30+
public override void Flush()
31+
{
32+
_streamBuffer.SetLength(0);
33+
}
34+
35+
public override long Length => _length;
36+
37+
private long _length;
38+
39+
public override long Position
40+
{
41+
get => _streamBuffer.Position;
42+
43+
set => _streamBuffer.Position = value;
44+
}
45+
46+
public override int Read(byte[] buffer, int offset, int count)
47+
{
48+
var previousPosition = Position;
49+
var consumedStream = false;
50+
var totalCount = count;
51+
while ((!consumedStream) &&
52+
((Position + totalCount) > _streamBuffer.Length))
53+
{
54+
// If we don't have enough data to fill this from memory, cache more.
55+
// We try to read 4096 bytes from base stream every time, so at most we
56+
// may cache 4095 bytes more than what is required by the Read operation.
57+
var bytesRead = _baseStream.Read(_copyBuffer, 0, _copyBuffer.Length);
58+
59+
if (_streamBuffer.Position < _streamBuffer.Length)
60+
{
61+
// Win8: 651902 no need to -1 here as Position refers to the place
62+
// where we can start writing from.
63+
_streamBuffer.Position = _streamBuffer.Length;
64+
}
65+
66+
_streamBuffer.Write(_copyBuffer, 0, bytesRead);
67+
68+
totalCount -= bytesRead;
69+
if (bytesRead < _copyBuffer.Length)
70+
{
71+
consumedStream = true;
72+
}
73+
}
74+
75+
// Reset our backing store to its official position, as reading
76+
// for the CopyTo updates the position.
77+
_streamBuffer.Seek(previousPosition, SeekOrigin.Begin);
78+
79+
// Read from the backing store into the requested buffer.
80+
var read = _streamBuffer.Read(buffer, offset, count);
81+
82+
if (read < count)
83+
{
84+
SetLength(Position);
85+
}
86+
87+
return read;
88+
}
89+
90+
public override long Seek(long offset, SeekOrigin origin)
91+
{
92+
return _streamBuffer.Seek(offset, origin);
93+
}
94+
95+
public override void SetLength(long value)
96+
{
97+
_length = value;
98+
}
99+
100+
public override void Write(byte[] buffer, int offset, int count)
101+
{
102+
throw new NotSupportedException();
103+
}
104+
}
105+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
using System;
6+
using System.Globalization;
7+
using System.Management.Automation;
8+
using System.Net.Http;
9+
using System.Net.Http.Headers;
10+
using System.Text;
11+
12+
using Microsoft.Graph.PowerShell.Authentication.Models;
13+
using Microsoft.Win32;
14+
15+
namespace Microsoft.Graph.PowerShell.Authentication.Helpers
16+
{
17+
internal static class ContentHelper
18+
{
19+
#region Constants
20+
21+
// default codepage encoding for web content. See RFC 2616.
22+
private const string DefaultCodePage = "ISO-8859-1";
23+
24+
#endregion Constants
25+
26+
#region Fields
27+
internal static RestReturnType CheckReturnType(this HttpResponseMessage response)
28+
{
29+
if (response == null) throw new ArgumentNullException(nameof(response));
30+
31+
var rt = RestReturnType.Detect;
32+
var contentType = response.GetContentType();
33+
if (string.IsNullOrEmpty(contentType))
34+
rt = RestReturnType.Detect;
35+
else if (ContentHelper.IsJson(contentType))
36+
rt = RestReturnType.Json;
37+
else if (ContentHelper.IsXml(contentType))
38+
rt = RestReturnType.Xml;
39+
40+
return rt;
41+
}
42+
// used to split contentType arguments
43+
private static readonly char[] ContentTypeParamSeparator = { ';' };
44+
45+
#endregion Fields
46+
47+
#region Internal Methods
48+
49+
internal static string GetContentType(this HttpResponseMessage response)
50+
{
51+
if (response == null)
52+
{
53+
throw new ArgumentNullException(nameof(response));
54+
}
55+
// ContentType may not exist in response header. Return null if not.
56+
return response.Content.Headers.ContentType?.MediaType;
57+
}
58+
59+
internal static Encoding GetDefaultEncoding()
60+
{
61+
return GetEncodingOrDefault(null);
62+
}
63+
64+
internal static Encoding GetEncodingOrDefault(string characterSet)
65+
{
66+
// get the name of the codepage to use for response content
67+
var codepage = string.IsNullOrEmpty(characterSet) ? DefaultCodePage : characterSet;
68+
Encoding encoding;
69+
try
70+
{
71+
encoding = Encoding.GetEncoding(codepage);
72+
}
73+
catch (ArgumentException)
74+
{
75+
// 0, default code page
76+
encoding = Encoding.GetEncoding(0);
77+
}
78+
79+
return encoding;
80+
}
81+
82+
internal static bool IsJson(string contentType)
83+
{
84+
contentType = GetContentTypeSignature(contentType);
85+
return CheckIsJson(contentType);
86+
}
87+
88+
internal static bool IsText(string contentType)
89+
{
90+
contentType = GetContentTypeSignature(contentType);
91+
return CheckIsText(contentType);
92+
}
93+
94+
internal static bool IsXml(string contentType)
95+
{
96+
contentType = GetContentTypeSignature(contentType);
97+
return CheckIsXml(contentType);
98+
}
99+
100+
#endregion Internal Methods
101+
102+
#region Private Helper Methods
103+
104+
private static bool CheckIsJson(string contentType)
105+
{
106+
if (string.IsNullOrEmpty(contentType))
107+
return false;
108+
109+
// the correct type for JSON content, as specified in RFC 4627
110+
var isJson = contentType.Equals("application/json", StringComparison.OrdinalIgnoreCase);
111+
112+
// add in these other "javascript" related types that
113+
// sometimes get sent down as the mime type for JSON content
114+
switch (contentType.ToLower(CultureInfo.InvariantCulture))
115+
{
116+
case "text/json":
117+
case "application/x-javascript":
118+
case "text/x-javascript":
119+
case "application/javascript":
120+
case "text/javascript":
121+
isJson = true;
122+
break;
123+
}
124+
return isJson;
125+
}
126+
127+
private static bool CheckIsText(string contentType)
128+
{
129+
if (string.IsNullOrEmpty(contentType))
130+
return false;
131+
132+
// any text, xml or json types are text
133+
var isText = contentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase)
134+
|| CheckIsXml(contentType)
135+
|| CheckIsJson(contentType);
136+
137+
// Further content type analysis is available on Windows
138+
if (Platform.IsWindows && !isText)
139+
{
140+
// Media types registered with Windows as having a perceived type of text, are text
141+
using (var contentTypeKey = Registry.ClassesRoot.OpenSubKey(@"MIME\Database\Content Type\" + contentType))
142+
{
143+
if (contentTypeKey?.GetValue("Extension") is string extension)
144+
{
145+
using (var extensionKey = Registry.ClassesRoot.OpenSubKey(extension))
146+
{
147+
if (extensionKey != null)
148+
{
149+
var perceivedType = extensionKey.GetValue("PerceivedType") as string;
150+
isText = perceivedType == "text";
151+
}
152+
}
153+
}
154+
}
155+
}
156+
return isText;
157+
}
158+
159+
private static bool CheckIsXml(string contentType)
160+
{
161+
if (string.IsNullOrEmpty(contentType))
162+
return false;
163+
164+
// RFC 3023: Media types with the suffix "+xml" are XML
165+
switch (contentType.ToLower(CultureInfo.InvariantCulture))
166+
{
167+
case "application/xml":
168+
case "application/xml-external-parsed-entity":
169+
case "application/xml-dtd":
170+
case var x when x.EndsWith("+xml"):
171+
return true;
172+
default:
173+
return false;
174+
}
175+
}
176+
177+
private static string GetContentTypeSignature(string contentType)
178+
{
179+
if (string.IsNullOrEmpty(contentType))
180+
return null;
181+
182+
var sig = contentType.Split(ContentTypeParamSeparator, 2)[0].ToUpperInvariant();
183+
return sig;
184+
}
185+
186+
#endregion Private Helper Methods
187+
}
188+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
namespace Microsoft.Graph.PowerShell.Authentication.Helpers
6+
{
7+
public static class Errors
8+
{
9+
public const string InvokeGraphHttpResponseException = nameof(InvokeGraphHttpResponseException);
10+
public const string InvokeGraphContentTypeException = nameof(InvokeGraphContentTypeException);
11+
public const string InvokeGraphRequestInvalidHost = nameof(InvokeGraphRequestInvalidHost);
12+
public const string InvokeGraphRequestSessionConflictException = nameof(InvokeGraphRequestSessionConflictException);
13+
public const string InvokeGraphRequestBodyMissingWhenMethodIsSpecified = nameof(InvokeGraphRequestBodyMissingWhenMethodIsSpecified);
14+
public const string InvokeGraphRequestOutFileMissingException = nameof(InvokeGraphRequestOutFileMissingException);
15+
public const string InvokeGraphRequestAuthenticationTokenConflictException = nameof(InvokeGraphRequestAuthenticationTokenConflictException);
16+
public const string InvokeGraphRequestAuthenticationCredentialNotSuppliedException = nameof(InvokeGraphRequestAuthenticationCredentialNotSuppliedException);
17+
public const string InvokeGraphRequestBodyConflictException = nameof(InvokeGraphRequestBodyConflictException);
18+
public const string InvokeGraphRequestFileNotFilesystemPathException = nameof(InvokeGraphRequestFileNotFilesystemPathException);
19+
public const string InvokeGraphRequestInputFileMultiplePathsResolvedException = nameof(InvokeGraphRequestInputFileMultiplePathsResolvedException);
20+
public const string InvokeGraphRequestInputFileNoPathResolvedException = nameof(InvokeGraphRequestInputFileNoPathResolvedException);
21+
public const string InvokeGraphRequestInputFileNotFilePathException = nameof(InvokeGraphRequestInputFileNotFilePathException);
22+
}
23+
}

0 commit comments

Comments
 (0)