/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Management.Automation; using System.Net; using System.IO; using System.Text; using System.Collections; using System.Globalization; using System.Security.Cryptography; using System.Threading; using System.Web; using System.Xml; using mshtml; namespace Microsoft.PowerShell.Commands { /// /// Base class for Invoke-RestMethod and Invoke-WebRequest commands. /// public abstract partial class WebRequestPSCmdlet : PSCmdlet { #region Abstract Methods /// /// Read the supplied WebResponse object and push the /// resulting output into the pipeline. /// /// Instance of a WebResponse object to be processed internal abstract void ProcessResponse(WebResponse response); #endregion Abstract Methods /// /// the WebRequest we will GetResponse for. /// private WebRequest _webRequest; #region Virtual Methods internal virtual WebRequest GetRequest(Uri uri) { uri = PrepareUri(uri); // create the base WebRequest object WebRequest request = WebRequest.Create(uri); // pull in session data if (0 < WebSession.Headers.Count) { try { HttpWebRequest webRequest = request as HttpWebRequest; request.Headers.Clear(); foreach (string key in WebSession.Headers.Keys) { bool setHeaderViaProperty = TryMapHeaderToProperty(webRequest, key); if (!setHeaderViaProperty) { request.Headers[key] = WebSession.Headers[key]; } } } catch (NotImplementedException) { } } // set the credentials used by this request if (WebSession.UseDefaultCredentials) { // the UseDefaultCredentials flag overrides other supplied credentials request.UseDefaultCredentials = true; } else if (null != WebSession.Credentials) { request.Credentials = WebSession.Credentials; } if (null != WebSession.Proxy) { request.Proxy = WebSession.Proxy; } // set the method if the parameter was provided if (WebRequestMethod.Default != Method) { request.Method = Method.ToString().ToUpperInvariant(); } // pull in http specific properties HttpWebRequest httpRequest = request as HttpWebRequest; if (null != httpRequest) { httpRequest.CookieContainer = WebSession.Cookies; httpRequest.UserAgent = WebSession.UserAgent; if (null != WebSession.Certificates) { httpRequest.ClientCertificates = WebSession.Certificates; } if (-1 < WebSession.MaximumRedirection) { if (WebSession.MaximumRedirection == 0) { httpRequest.AllowAutoRedirect = false; } else { httpRequest.MaximumAutomaticRedirections = WebSession.MaximumRedirection; } } // check timeout setting (in seconds instead of milliseconds as in HttpWebRequest) if (TimeoutSec == 0) { // A zero timeout means infinite httpRequest.Timeout = Timeout.Infinite; } else if (TimeoutSec > 0) { // just to make sure if (TimeoutSec > Int32.MaxValue / 1000) { httpRequest.Timeout = Int32.MaxValue; } else { httpRequest.Timeout = TimeoutSec * 1000; } } // check keep-alive setting if (DisableKeepAlive) { // default value is true, so only need to set if false. httpRequest.KeepAlive = false; } if (null != TransferEncoding) { httpRequest.SendChunked = true; httpRequest.TransferEncoding = TransferEncoding; } } return (request); } private bool TryMapHeaderToProperty(HttpWebRequest webRequest, string key) { bool setHeaderViaProperty = false; // Perform header-to-property overrides if (webRequest != null) { if (String.Equals("Accept", key, StringComparison.OrdinalIgnoreCase)) { webRequest.Accept = WebSession.Headers[key]; setHeaderViaProperty = true; } if (String.Equals("Connection", key, StringComparison.OrdinalIgnoreCase)) { webRequest.Connection = WebSession.Headers[key]; setHeaderViaProperty = true; } if (String.Equals("Content-Length", key, StringComparison.OrdinalIgnoreCase)) { webRequest.ContentLength = Convert.ToInt64(WebSession.Headers[key], CultureInfo.InvariantCulture); setHeaderViaProperty = true; } if (String.Equals("Content-Type", key, StringComparison.OrdinalIgnoreCase)) { webRequest.ContentType = WebSession.Headers[key]; setHeaderViaProperty = true; } if (String.Equals("Date", key, StringComparison.OrdinalIgnoreCase)) { webRequest.Date = DateTime.Parse(WebSession.Headers[key], CultureInfo.InvariantCulture); setHeaderViaProperty = true; } if (String.Equals("Expect", key, StringComparison.OrdinalIgnoreCase)) { webRequest.Expect = WebSession.Headers[key]; setHeaderViaProperty = true; } if (String.Equals("Host", key, StringComparison.OrdinalIgnoreCase)) { webRequest.Host = WebSession.Headers[key]; setHeaderViaProperty = true; } if (String.Equals("If-Modified-Since", key, StringComparison.OrdinalIgnoreCase)) { webRequest.IfModifiedSince = DateTime.Parse(WebSession.Headers[key], CultureInfo.InvariantCulture); setHeaderViaProperty = true; } if (String.Equals("Referer", key, StringComparison.OrdinalIgnoreCase)) { webRequest.Referer = WebSession.Headers[key]; setHeaderViaProperty = true; } if (String.Equals("Transfer-Encoding", key, StringComparison.OrdinalIgnoreCase)) { webRequest.SendChunked = true; if (String.Equals("Chunked", WebSession.Headers[key], StringComparison.OrdinalIgnoreCase)) { // .NET Doesn't support setting both the header and the property webRequest.SendChunked = true; } else { webRequest.TransferEncoding = WebSession.Headers[key]; } setHeaderViaProperty = true; } if (String.Equals("User-Agent", key, StringComparison.OrdinalIgnoreCase)) { WebSession.UserAgent = WebSession.Headers[key]; webRequest.UserAgent = WebSession.Headers[key]; setHeaderViaProperty = true; } } return setHeaderViaProperty; } internal virtual void FillRequestStream(WebRequest request) { if (null == request) { throw new ArgumentNullException("request"); } // set the content type if (null != ContentType) { request.ContentType = ContentType; } // ContentType == null else if (Method == WebRequestMethod.Post) { // Win8:545310 Invoke-WebRequest does not properly set MIME type for POST if (String.IsNullOrEmpty(request.ContentType)) { request.ContentType = "application/x-www-form-urlencoded"; } } // coerce body into a usable form if (null != Body) { object content = Body; // make sure we're using the base object of the body, not the PSObject wrapper PSObject psBody = Body as PSObject; if (null != psBody) { content = psBody.BaseObject; } if (null != content as HtmlWebResponseObject) { HtmlWebResponseObject html = content as HtmlWebResponseObject; // use the form it's the only one present if (html.Forms.Count == 1) { SetRequestContent(request, html.Forms[0].Fields); } } else if (null != content as FormObject) { FormObject form = content as FormObject; SetRequestContent(request, form.Fields); } else if (null != content as IDictionary && request.Method != WebRequestMethods.Http.Get) { IDictionary dictionary = content as IDictionary; SetRequestContent(request, dictionary); } else if (null != content as XmlNode) { XmlNode xmlNode = content as XmlNode; SetRequestContent(request, xmlNode); } else if (null != content as Stream) { Stream stream = content as Stream; SetRequestContent(request, stream); } else if (null != content as byte[]) { byte[] bytes = content as byte[]; SetRequestContent(request, bytes); } else { SetRequestContent(request, (string)LanguagePrimitives.ConvertTo(content, typeof(string), CultureInfo.InvariantCulture)); } } else if (null != InFile) // copy InFile data { try { // open the input file using (FileStream fs = new FileStream(InFile, FileMode.Open)) { SetRequestContent(request, fs); } } catch (UnauthorizedAccessException) { string msg = string.Format(CultureInfo.InvariantCulture, WebCmdletStrings.AccessDenied, _originalFilePath); throw new UnauthorizedAccessException(msg); } } else { request.ContentLength = 0; } } internal virtual WebResponse GetResponse(WebRequest request) { if (null == request) { throw new ArgumentNullException("request"); } // Construct TimeoutState HttpWebRequest httpRequest = request as HttpWebRequest; TimeoutState timeoutState = null; if (httpRequest != null && httpRequest.Timeout > 0) { timeoutState = new TimeoutState(httpRequest); } // Construct WebRequestState _webRequest = request; WebRequestState requestState = new WebRequestState(request); // Call asynchronous GetResponse IAsyncResult asyncResult = (IAsyncResult)request.BeginGetResponse(new AsyncCallback(ResponseCallback), requestState); // Set timeout if necessary if (timeoutState != null) { ThreadPool.RegisterWaitForSingleObject(asyncResult.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), timeoutState, timeoutState.httpRequest.Timeout, true); } // Wait on signal requestState.waithandle.WaitOne(-1, false); requestState.waithandle.Close(); _webRequest = null; // The current thread will be waked up in three cases: // 1. the EngGetResponse is done. In this case, we EITHER get the response (requestState.response != null), // OR a WebException is raised (requestState.webException != null). // 2. the ^C is typed, a PipelineStoppedException is raised. StopProcessing will abort the request. In this // case, there will be a WebException with status 'RequestCancelled'. // 3. the time is up. The TimeoutCallback method will abort the request. In this case, there will also be a // WebException with status 'RequestCancelled' and timeoutState.abort will be true. if (requestState.webException != null) { // Case 3. We wrap the exception to be 'Timeout' WebException if (timeoutState != null && timeoutState.abort && requestState.webException.Status.Equals(WebExceptionStatus.RequestCanceled)) { throw new WebException(WebCmdletStrings.RequestTimeout, WebExceptionStatus.Timeout); } // Case 1 or 2 throw requestState.webException; } return (requestState.response); } internal virtual void UpdateSession(WebResponse response) { if (null == response) { throw new ArgumentNullException("response"); } // process HTTP specific session data HttpWebResponse httpResponse = response as HttpWebResponse; if ((null != WebSession) && (null != httpResponse)) { // save response cookies into the session WebSession.Cookies.Add(httpResponse.Cookies); } } #endregion Virtual Methods #region Overrides /// /// the main execution method for cmdlets derived from WebRequestPSCmdlet. /// protected override void ProcessRecord() { try { // Set cmdlet context for write progress ValidateParameters(); PrepareSession(); WebRequest request = GetRequest(Uri); FillRequestStream(request); // Some web sites (e.g. Twitter) will return exception on POST when Expect100 is sent // Default behaviour is continue to send body content anyway after a short period // Here it send the two part as a whole. ServicePointManager.Expect100Continue = false; try { string reqVerboseMsg = String.Format(CultureInfo.CurrentCulture, "{0} {1} with {2}-byte payload", request.Method, request.RequestUri, request.ContentLength); WriteVerbose(reqVerboseMsg); WebResponse response = GetResponse(request); try { string contentType = ContentHelper.GetContentType(response); string respVerboseMsg = String.Format(CultureInfo.CurrentCulture, "received {0}-byte response of content type {1}", response.ContentLength, contentType); WriteVerbose(respVerboseMsg); ProcessResponse(response); UpdateSession(response); // If we hit our maximum redirection count, generate an error. // Errors with redirection counts of greater than 0 are handled automatically by .NET, but are // impossible to detect programmatically when we hit this limit. By handling this ourselves // (and still writing out the result), users can debug actual HTTP redirect problems. HttpWebRequest httpRequest = request as HttpWebRequest; if ((httpRequest != null) && (httpRequest.AllowAutoRedirect == false)) { HttpWebResponse webResponse = response as HttpWebResponse; if ((webResponse.StatusCode == HttpStatusCode.Found) || (webResponse.StatusCode == HttpStatusCode.Moved) || webResponse.StatusCode == HttpStatusCode.MovedPermanently) { ErrorRecord er = new ErrorRecord(new InvalidOperationException(), "MaximumRedirectExceeded", ErrorCategory.InvalidOperation, httpRequest); er.ErrorDetails = new ErrorDetails(WebCmdletStrings.MaximumRedirectionCountExceeded); WriteError(er); } } } finally { // Choosing to close the stream instead of Dispose as the // response object is being written to the output pipe. if (response != null) { response.Close(); } } } catch (WebException ex) { WebException exThrown = ex; string detailMsg = String.Empty; try { if (ex.Response != null && ex.Response.ContentLength > 0) { Stream input = StreamHelper.GetResponseStream(ex.Response); StreamReader reader = new StreamReader(input); detailMsg = reader.ReadToEnd(); // If we were asked to not use the IE engine (or this is Invoke-RestMethod), use a simple // regex replace to remove tags. if (UseBasicParsing || (this is InvokeRestMethodCommand)) { detailMsg = System.Text.RegularExpressions.Regex.Replace(detailMsg, "<[^>]*>", ""); } else { // Otherwise, use IE to clean it up, as errors often come back as HTML VerifyInternetExplorerAvailable(false); try { IHTMLDocument2 _parsedHtml = (IHTMLDocument2)new HTMLDocument(); _parsedHtml.write(detailMsg); detailMsg = _parsedHtml.body.outerText; } catch (System.Runtime.InteropServices.COMException) { } } } } // catch all catch (Exception) { } ErrorRecord er = new ErrorRecord(exThrown, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, request); if (!String.IsNullOrEmpty(detailMsg)) { er.ErrorDetails = new ErrorDetails(detailMsg); } ThrowTerminatingError(er); } } catch (CryptographicException ex) { ErrorRecord er = new ErrorRecord(ex, "WebCmdletCertificateException", ErrorCategory.SecurityError, null); ThrowTerminatingError(er); } catch (NotSupportedException ex) { ErrorRecord er = new ErrorRecord(ex, "WebCmdletIEDomNotSupportedException", ErrorCategory.NotImplemented, null); ThrowTerminatingError(er); } } /// /// Implementing ^C, after start the BeginGetResponse /// protected override void StopProcessing() { if (_webRequest != null) { _webRequest.Abort(); } } #endregion Overrides #region Helper Methods /// /// Call back method for BeginGetResponse /// /// private static void ResponseCallback(IAsyncResult asyncResult) { WebRequestState myRequestState = (WebRequestState)asyncResult.AsyncState; try { myRequestState.response = myRequestState.request.EndGetResponse(asyncResult); } catch (WebException ex) { myRequestState.response = null; myRequestState.webException = ex; } finally { myRequestState.waithandle.Set(); } } /// /// Call back method for timeout /// /// /// private static void TimeoutCallback(object state, bool timeout) { if (timeout) { TimeoutState timeoutState = state as TimeoutState; if (timeoutState != null) { timeoutState.abort = true; timeoutState.httpRequest.Abort(); } } } /// /// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream. /// /// The WebRequest who's content is to be set /// A byte array containing the content data. /// The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property /// /// Because this function sets the request's ContentLength property and writes content data into the requests's stream, /// it should be called one time maximum on a given request. /// internal long SetRequestContent(WebRequest request, Byte[] content) { if (request == null) throw new ArgumentNullException("request"); if (content != null) { if (request.ContentLength == 0) { request.ContentLength = content.Length; } StreamHelper.WriteToStream(content, request.GetRequestStream()); } else { request.ContentLength = 0; } return request.ContentLength; } /// /// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream. /// /// The WebRequest who's content is to be set /// A String object containing the content data. /// The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property /// /// Because this function sets the request's ContentLength property and writes content data into the requests's stream, /// it should be called one time maximum on a given request. /// internal long SetRequestContent(WebRequest request, String content) { if (request == null) throw new ArgumentNullException("request"); if (content != null) { Encoding encoding = null; if (null != ContentType) { // 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. System.Net.Mime.ContentType mimeContentType = new System.Net.Mime.ContentType(ContentType); if (!String.IsNullOrEmpty(mimeContentType.CharSet)) { try { encoding = Encoding.GetEncoding(mimeContentType.CharSet); } catch (ArgumentException ex) { ErrorRecord er = new ErrorRecord(ex, "WebCmdletEncodingException", ErrorCategory.InvalidArgument, ContentType); ThrowTerminatingError(er); } } } Byte[] bytes = StreamHelper.EncodeToBytes(content, encoding); if (request.ContentLength == 0) { request.ContentLength = bytes.Length; } StreamHelper.WriteToStream(bytes, request.GetRequestStream()); } else { request.ContentLength = 0; } return request.ContentLength; } internal long SetRequestContent(WebRequest request, XmlNode xmlNode) { if (request == null) throw new ArgumentNullException("request"); if (xmlNode != null) { Byte[] bytes = null; XmlDocument doc = xmlNode as XmlDocument; if (doc != null && (doc.FirstChild as XmlDeclaration) != null) { XmlDeclaration decl = doc.FirstChild as XmlDeclaration; Encoding encoding = Encoding.GetEncoding(decl.Encoding); bytes = StreamHelper.EncodeToBytes(doc.OuterXml, encoding); } else { bytes = StreamHelper.EncodeToBytes(xmlNode.OuterXml); } if (request.ContentLength == 0) { request.ContentLength = bytes.Length; } StreamHelper.WriteToStream(bytes, request.GetRequestStream()); } else { request.ContentLength = 0; } return request.ContentLength; } /// /// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream. /// /// The WebRequest who's content is to be set /// A Stream object containing the content data. /// The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property /// /// Because this function sets the request's ContentLength property and writes content data into the requests's stream, /// it should be called one time maximum on a given request. /// internal long SetRequestContent(WebRequest request, Stream contentStream) { if (request == null) throw new ArgumentNullException("request"); if (contentStream == null) throw new ArgumentNullException("contentStream"); if (request.ContentLength == 0) { request.ContentLength = contentStream.Length; } StreamHelper.WriteToStream(contentStream, request.GetRequestStream(), this); return request.ContentLength; } internal long SetRequestContent(WebRequest request, IDictionary content) { if (request == null) throw new ArgumentNullException("request"); if (content == null) throw new ArgumentNullException("content"); string body = FormatDictionary(content); return (SetRequestContent(request, body)); } #endregion Helper Methods #region private State class /// /// The web request state is used when place asynchronous call to get response of a web request. /// private class WebRequestState { public WebRequest request; public WebResponse response; public ManualResetEvent waithandle; public WebException webException; public WebRequestState(WebRequest webRequest) { request = webRequest; response = null; webException = null; waithandle = new ManualResetEvent(false); } } /// /// The timeout state is used when the request is a http request and need to timeout /// private class TimeoutState { public HttpWebRequest httpRequest; public bool abort; public TimeoutState(HttpWebRequest request) { httpRequest = request; abort = false; } } #endregion private State class } }