diff --git a/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs new file mode 100644 index 00000000000..fd98edb37e1 --- /dev/null +++ b/src/Authentication/Authentication/Cmdlets/InvokeGraphRequest.cs @@ -0,0 +1,1074 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +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 +{ + [Cmdlet(VerbsLifecycle.Invoke, "GraphRequest", DefaultParameterSetName = Constants.UserParameterSet)] + public class InvokeGraphRequest : PSCmdlet + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly InvokeGraphRequestUserAgent _graphRequestUserAgent; + private string _originalFilePath; + + public InvokeGraphRequest() + { + _cancellationTokenSource = new CancellationTokenSource(); + _graphRequestUserAgent = new InvokeGraphRequestUserAgent(this); + Authentication = GraphRequestAuthenticationType.Default; + } + + /// + /// Http Method + /// + [Parameter(ParameterSetName = Constants.UserParameterSet, + Position = 1, + HelpMessage = "Http Method")] + public GraphRequestMethod Method { get; set; } = GraphRequestMethod.GET; + + /// + /// 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")] + public Uri Uri { get; set; } + + /// + /// Optional Http Body + /// + [Parameter(ParameterSetName = Constants.UserParameterSet, + Position = 3, + HelpMessage = "Request Body. Required when Method is Post or Patch", + ValueFromPipeline = true)] + public object Body { get; set; } + + /// + /// Optional Custom Headers + /// + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, + Position = 4, + HelpMessage = "Optional Custom Headers")] + public IDictionary Headers { get; set; } + + /// + /// Relative or absolute path where the response body will be saved. + /// Not allowed when InferOutputFileName is specified. + /// + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, + Position = 5, + HelpMessage = "Output file where the response body will be saved")] + public string OutputFilePath { get; set; } + + /// + /// Infer Download FileName from ContentDisposition Header. + /// Not allowed when OutputFilePath is specified. + /// + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, + Position = 6, + HelpMessage = "Infer output filename")] + public SwitchParameter InferOutputFileName { get; set; } + + /// + /// Gets or sets the InputFilePath property to send in the request + /// + [Parameter(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, + Position = 7, + 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, + 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 SecureString 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; } + + /// + /// 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(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(Mandatory = false, + ParameterSetName = Constants.UserParameterSet, + Position = 14, + 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(OutputFilePath); + + internal bool ShouldSaveToOutFile => !string.IsNullOrEmpty(OutputFilePath); + /// + /// 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, + currentResponse.ReasonPhrase); + var httpException = new HttpResponseException(errorMessage, currentResponse); + var errorRecord = new ErrorRecord(httpException, Errors.InvokeGraphHttpResponseException, + ErrorCategory.InvalidOperation, httpRequestMessage); + var detailMsg = httpResponseMessageFormatter.ReadAsStringAsync() + .GetAwaiter() + .GetResult(); + if (!string.IsNullOrEmpty(detailMsg)) + { + errorRecord.ErrorDetails = new ErrorDetails(detailMsg); + } + + return errorRecord; + } + + /// + /// When -Verbose is specified, print out response status + /// + /// + private void ReportRequestStatus(HttpRequestMessage requestMessage) + { + var requestContentLength = requestMessage.Content?.Headers.ContentLength.Value ?? 0; + + var reqVerboseMsg = Resources.InvokeGraphRequestVerboseMessage.FormatCurrentCulture(requestMessage.Method, + requestMessage.RequestUri, + requestContentLength); + WriteVerbose(reqVerboseMsg); + } + + /// + /// 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, + contentType); + WriteVerbose(respVerboseMsg); + } + + /// + /// Compose a request, setting Uri and Headers. + /// + /// + /// + 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; + } + + /// + /// Compose Request Uri + /// + /// + /// + private Uri PrepareUri(Uri uri) + { + // before creating the web request, + // preprocess Body if content is a dictionary and method is GET (set as query) + if (Method == GraphRequestMethod.GET && LanguagePrimitives.TryConvertTo(Body, out IDictionary bodyAsDictionary)) + { + var uriBuilder = new UriBuilder(uri); + if (uriBuilder.Query != null && uriBuilder.Query.Length > 1) + { + uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + bodyAsDictionary.FormatDictionary(); + } + else if (bodyAsDictionary != null) + { + uriBuilder.Query = bodyAsDictionary.FormatDictionary(); + } + + uri = uriBuilder.Uri; + // set body to null to prevent later FillRequestStream + Body = null; + } + + return uri; + } + + /// + /// Process Http Response + /// + /// + internal void ProcessResponse(HttpResponseMessage response) + { + if (response == null) throw new ArgumentNullException(nameof(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)) + { + charSet.TryGetEncoding(out encoding); + } + + if (string.IsNullOrEmpty(charSet) && returnType == RestReturnType.Json) + { + encoding = Encoding.UTF8; + } + + Exception ex = null; + + var str = responseStream.DecodeStream(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(Resources.ContentEncodingVerboseMessage.FormatCurrentCulture(encodingVerboseName)); + var convertSuccess = str.TryConvert(out var obj, ref ex); + if (!convertSuccess) + { + // fallback to string + obj = str; + } + + WriteObject(obj); + } + } + + if (ShouldSaveToOutFile) + { + baseResponseStream.SaveStreamToFile(QualifiedOutFile, this, _cancellationTokenSource.Token); + } + + if (InferOutputFileName.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( + Resources.InferredFileNameVerboseMessage.FormatCurrentCulture(fileName, fullFileName)); + baseResponseStream.SaveStreamToFile(fullFileName, this, _cancellationTokenSource.Token); + } + } + else + { + WriteVerbose(Resources.InferredFileNameErrorMessage); + } + } + + 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, response.GetHttpResponseHeaders()); + } + } + + + /// + /// 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 auth provider. + /// + /// + 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 (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + var cancellationToken = _cancellationTokenSource.Token; + var response = client.SendAsync(request, 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 = 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. + 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(Resources.ContentTypeExceptionErrorMessage, ex); + var er = new ErrorRecord(outerEx, Errors.InvokeGraphContentTypeException, + ErrorCategory.InvalidArgument, ContentType); + ThrowTerminatingError(er); + } + } + catch (ArgumentException ex) + { + if (!SkipHeaderValidation) + { + var outerEx = new ValidationMetadataException(Resources.ContentTypeExceptionErrorMessage, ex); + var er = new ErrorRecord(outerEx, Errors.InvokeGraphContentTypeException, + ErrorCategory.InvalidArgument, ContentType); + ThrowTerminatingError(er); + } + } + } + + var bytes = content.EncodeToBytes(encoding); + var byteArrayContent = new ByteArrayContent(bytes); + request.Content = byteArrayContent; + + 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)); + 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 if 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 + if (Body is PSObject psBody) + { + content = psBody.BaseObject; + } + 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 + { + // Assume its a string + SetRequestContent(request, + (string)LanguagePrimitives.ConvertTo(content, typeof(string), CultureInfo.InvariantCulture)); + } + } + else if (InputFilePath != null) // copy InputFilePath data + { + try + { + // open the input file + SetRequestContent(request, + new FileStream(InputFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)); + } + catch (UnauthorizedAccessException) + { + var msg = Resources.AccessDenied.FormatCurrentCulture(_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.Where(header => + !string.IsNullOrWhiteSpace(header.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(Resources.ContentTypeExceptionErrorMessage, ex); + var er = new ErrorRecord(outerEx, Errors.InvokeGraphContentTypeException, + 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) + throw new ArgumentNullException(nameof(request)); + if (content == null) + return 0; + + var byteArrayContent = new ByteArrayContent(content); + request.Content = byteArrayContent; + + 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) + 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 = 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 (Authentication == GraphRequestAuthenticationType.UserProvidedToken && Token != 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( + Resources.InvokeGraphRequestMissingUriErrorMessage.FormatCurrentCulture(nameof(Uri)), + Errors.InvokeGraphRequestInvalidHost, + nameof(Uri)); + 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) + { + var error = GetValidationError( + Resources.InvokeGraphRequestInvalidHostErrorMessage.FormatCurrentCulture(Uri.Host), + Errors.InvokeGraphRequestInvalidHost, + nameof(Uri)); + ThrowTerminatingError(error); + } + } + + /// + /// Validate Passed In Parameters + /// + private void ValidateParameters() + { + if (GraphRequestSession != null && SessionVariable != null) + { + var error = GetValidationError( + 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 && string.IsNullOrWhiteSpace(InputFilePath))) + { + var error = GetValidationError( + Resources.BodyMissingWhenMethodIsSpecified.FormatCurrentCulture(nameof(Body), Method), + Errors.InvokeGraphRequestBodyMissingWhenMethodIsSpecified, + nameof(Body)); + ThrowTerminatingError(error); + } + + if (PassThru && OutputFilePath == null) + { + var error = GetValidationError( + Resources.PassThruWithOutputFilePathMissing.FormatCurrentCulture(nameof(PassThru), + nameof(OutputFilePath)), + Errors.InvokeGraphRequestOutFileMissingException, + nameof(PassThru)); + ThrowTerminatingError(error); + } + + if (Authentication == GraphRequestAuthenticationType.Default && Token != null) + { + var error = GetValidationError( + Resources.AuthenticationTokenConflict.FormatCurrentCulture(Authentication, nameof(Token)), + Errors.InvokeGraphRequestAuthenticationTokenConflictException); + ThrowTerminatingError(error); + } + + // Token shouldn't be null when UserProvidedToken is specified + if (Authentication == GraphRequestAuthenticationType.UserProvidedToken && Token == null) + { + 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 && !string.IsNullOrWhiteSpace(InputFilePath)) + { + var error = GetValidationError( + Resources.BodyConflict.FormatCurrentCulture(nameof(Body), nameof(InputFilePath)), + Errors.InvokeGraphRequestBodyConflictException); + 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) + { + ErrorRecord errorRecord = null; + + try + { + var providerPaths = GetResolvedProviderPathFromPSPath(InputFilePath, out var provider); + + if (!provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase)) + { + errorRecord = GetValidationError( + Resources.NotFileSystemPath.FormatCurrentCulture(InputFilePath), + Errors.InvokeGraphRequestFileNotFilesystemPathException, InputFilePath); + } + else + { + if (providerPaths.Count > 1) + { + errorRecord = GetValidationError( + Resources.MultiplePathsResolved.FormatCurrentCulture(InputFilePath), + Errors.InvokeGraphRequestInputFileMultiplePathsResolvedException, InputFilePath); + } + else if (providerPaths.Count == 0) + { + errorRecord = GetValidationError( + Resources.NoPathResolved.FormatCurrentCulture(InputFilePath), + Errors.InvokeGraphRequestInputFileNoPathResolvedException, InputFilePath); + } + else + { + if (Directory.Exists(providerPaths[0])) + { + errorRecord = GetValidationError( + Resources.DirectoryPathSpecified.FormatCurrentCulture(providerPaths[0]), + Errors.InvokeGraphRequestInputFileNotFilePathException, InputFilePath); + } + + _originalFilePath = InputFilePath; + InputFilePath = 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); + } + } + + /// + /// 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 = msg.FormatCurrentCulture(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; + } + + #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 diff --git a/src/Authentication/Authentication/Helpers/AttachDebugger.cs b/src/Authentication/Authentication/Helpers/AttachDebugger.cs new file mode 100644 index 00000000000..a8d3bbb36a8 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/AttachDebugger.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/BufferingStreamReader.cs b/src/Authentication/Authentication/Helpers/BufferingStreamReader.cs new file mode 100644 index 00000000000..887c69f057a --- /dev/null +++ b/src/Authentication/Authentication/Helpers/BufferingStreamReader.cs @@ -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(); + } + } +} \ 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..8526d0cca9a --- /dev/null +++ b/src/Authentication/Authentication/Helpers/ContentHelper.cs @@ -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 + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..d142b9798cf --- /dev/null +++ b/src/Authentication/Authentication/Helpers/GraphRequestSession.cs @@ -0,0 +1,50 @@ +// ------------------------------------------------------------------------------ +// 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.Security; +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 SecureString 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..f9d589c2272 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/HttpKnownHeaderNames.cs @@ -0,0 +1,106 @@ +// ------------------------------------------------------------------------------ +// 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; + +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"; + 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 _contentHeaderSet = null; + + internal static HashSet ContentHeaders => + _contentHeaderSet ?? (_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 + }); + + + } +} diff --git a/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs new file mode 100644 index 00000000000..229e247d512 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/HttpMessageFormatter.cs @@ -0,0 +1,334 @@ +// ------------------------------------------------------------------------------ +// 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; +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) + { + 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 + }; + + // Set of header fields that should not get serialized + private static readonly HashSet NeverSerializedHeaderFields = + new HashSet(StringComparer.OrdinalIgnoreCase); + + 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(); + } + + /// + /// 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)); + + InitializeStreamTask(); + } + + private HttpContent Content => + HttpRequestMessage != null ? HttpRequestMessage.Content : HttpResponseMessage.Content; + + /// + /// Gets the HTTP request message. + /// + public HttpRequestMessage HttpRequestMessage { get; } + + /// + /// Gets the HTTP response message. + /// + public HttpResponseMessage HttpResponseMessage { get; } + + 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(nameof(content)); + } + + 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)); + } + + return false; + } + + foreach (var parameter in contentType.Parameters) + { + if (parameter.Name.Equals(MsgTypeParameter, StringComparison.OrdinalIgnoreCase)) + { + var msgType = parameter.Value.Trim('"'); + if (!msgType.Equals(isRequest ? DefaultRequestMsgType : DefaultResponseMsgType, + StringComparison.OrdinalIgnoreCase)) + { + if (throwOnError) + { + throw new ArgumentException( + Resources.HttpMessageInvalidMediaType.FormatCurrentCulture(msgType), + nameof(content)); + } + + return false; + } + + return true; + } + } + } + + if (throwOnError) + { + throw new ArgumentException(Resources.HttpMessageInvalidMediaType, nameof(content)); + } + + return false; + } + + /// + /// 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)); + } + + var header = SerializeHeader(); + await stream.WriteAsync(header, 0, header.Length); + + if (Content != null) + { + var 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 + + 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. + var 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 (var header in headers) + { + if (NeverSerializedHeaderFields.Contains(header.Key)) + { + continue; + } + + if (SingleValueHeaderFields.Contains(header.Key)) + { + 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); + } + else + { + message.Append(header.Key + ColonSP + string.Join(CommaSeparator, header.Value) + CRLF); + } + } + } + } + + private byte[] SerializeHeader() + { + var 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; + } + } +} diff --git a/src/Authentication/Authentication/Helpers/HttpResponseException.cs b/src/Authentication/Authentication/Helpers/HttpResponseException.cs new file mode 100644 index 00000000000..b99dea64b63 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/HttpResponseException.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------------ +// 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 +{ + 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..72326ce7c1f --- /dev/null +++ b/src/Authentication/Authentication/Helpers/InvokeGraphRequestAuthProvider.cs @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------------ +// 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.Management.Automation.Language; +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 +{ + public class InvokeGraphRequestAuthProvider : IAuthenticationProvider + { + private readonly GraphRequestSession _session; + + public InvokeGraphRequestAuthProvider(GraphRequestSession session) + { + _session = session; + } + + public Task AuthenticateRequestAsync(HttpRequestMessage request) + { + var authenticationHeader = new AuthenticationHeaderValue("Bearer", new NetworkCredential(string.Empty, _session.Token).Password); + 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..65c5724d2d3 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/InvokeGraphRequestUserAgent.cs @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------------ +// 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; +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; + } + /// + /// Full UserAgent which Includes the Operating System, Current Culture + /// and full app name including powershell version and invoked command. + /// + 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"); + /// + /// Indicates the App which includes the PowerShell version + /// and the command name. + /// + internal string App + { + get + { + var app = string.Format(CultureInfo.InvariantCulture, + "PowerShell/{0} {1}", this._cmdLet.Host.Version, this._cmdLet.MyInvocation.MyCommand.Name); + 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..716aa4cdd87 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/PathUtils.cs @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------------ +// 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.Management.Automation; +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..7329861cdb3 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/StreamHelper.cs @@ -0,0 +1,128 @@ +// ------------------------------------------------------------------------------ +// 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; +using System.Net.Http; +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 static class StreamHelper + { + internal const int DefaultReadBuffer = 100000; + + internal const int ChunkSize = 10000; + + /// + /// Encode specified string to bytes using the provided encoding + /// + /// + /// + /// + internal static byte[] EncodeToBytes(this 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(this HttpResponseMessage response) + { + var responseStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + return responseStream; + } + + internal static bool TryGetEncoding(this 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(this BufferingStreamReader responseStream, ref Encoding encoding) + { + if (encoding == null) + { + // Use the default encoding if one wasn't provided + encoding = ContentHelper.GetDefaultEncoding(); + } + var content = responseStream.StreamToString(encoding); + return content; + } + + internal static string StreamToString(this Stream stream, Encoding encoding) + { + using (var reader = new StreamReader(stream, encoding)) + { + return reader.ReadToEnd(); + } + } + + 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)) + { + baseResponseStream.WriteToStream(output, invokeGraphRequest, token); + } + } + + private static void WriteToStream(this 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 = + Resources.WriteRequestProgressStatus.FormatCurrentCulture(output.Position); + cmdlet.WriteProgress(record); + + Task.Delay(1000, cancellationToken).Wait(cancellationToken); + } while (!copyTask.IsCompleted && !cancellationToken.IsCancellationRequested); + + if (copyTask.IsCompleted) + { + record.StatusDescription = Resources.WriteRequestComplete.FormatCurrentCulture(output.Position); + cmdlet.WriteProgress(record); + } + } + catch (OperationCanceledException) + { + } + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Helpers/StringUtil.cs b/src/Authentication/Authentication/Helpers/StringUtil.cs new file mode 100644 index 00000000000..b2774029a76 --- /dev/null +++ b/src/Authentication/Authentication/Helpers/StringUtil.cs @@ -0,0 +1,104 @@ +// ------------------------------------------------------------------------------ +// 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.Globalization; +using System.Net; +using System.Text; +using Microsoft.Graph.PowerShell.Authentication.Properties; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + internal static class StringUtil + { + /// + /// 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 + /// + /// + /// + 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(); + } + + /// + /// 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 new file mode 100644 index 00000000000..35fa71ca59b --- /dev/null +++ b/src/Authentication/Authentication/Helpers/WebResponseHelper.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------------ +// 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; +using System.Net.Http; +using Microsoft.Graph.PowerShell.Authentication.Cmdlets; + +namespace Microsoft.Graph.PowerShell.Authentication.Helpers +{ + internal static class WebResponseHelper + { + internal static Dictionary> GetHttpResponseHeaders(this 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; + } + } +} \ 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 e6b8160de1c..f23d91f04ec 100644 --- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj +++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj @@ -27,12 +27,27 @@ + + + + + True + True + Resources.resx + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + 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 0eef99381d2..e249bdf23e5 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', 'Get-MgProfile', 'Select-MgProfile' +CmdletsToExport = 'Connect-Graph', 'Disconnect-Graph', 'Get-MgContext', 'Get-MgProfile', 'Select-MgProfile', '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 diff --git a/src/Authentication/Authentication/Properties/Resources.Designer.cs b/src/Authentication/Authentication/Properties/Resources.Designer.cs new file mode 100644 index 00000000000..9489b3d6b04 --- /dev/null +++ b/src/Authentication/Authentication/Properties/Resources.Designer.cs @@ -0,0 +1,297 @@ +//------------------------------------------------------------------------------ +// +// 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 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. + /// + 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 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}. + /// + 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..2329dc6b40b --- /dev/null +++ b/src/Authentication/Authentication/Properties/Resources.resx @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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} + + + 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