/********************************************************************++
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
{
    /// <summary>
    /// Base class for Invoke-RestMethod and Invoke-WebRequest commands.
    /// </summary>
    public abstract partial class WebRequestPSCmdlet : PSCmdlet
    {
        #region Abstract Methods

        /// <summary>
        /// Read the supplied WebResponse object and push the
        /// resulting output into the pipeline.
        /// </summary>
        /// <param name="response">Instance of a WebResponse object to be processed</param>
        internal abstract void ProcessResponse(WebResponse response);

        #endregion Abstract Methods

        /// <summary>
        /// the WebRequest we will GetResponse for.
        /// </summary>
        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 (NoProxy)
            {
                handler.UseProxy = false;
            }
            else if (WebSession.Proxy != null)
            {
                request.Proxy = WebSession.Proxy;
            }

            switch (ParameterSetName)
            {
                case "StandardMethodNoProxy":
                    goto case "StandardMethod";
                case "StandardMethod":
                    if (WebRequestMethod.Default != Method)
                    {
                        // set the method if the parameter was provided
                        request.Method = Method.ToString().ToUpperInvariant();
                    }
                    break;
                case "CustomMethodNoProxy":
                    goto case "CustomMethod";
                case "CustomMethod":
                    // set the method if the parameter was provided
                    request.Method = CustomMethod.ToUpperInvariant();
                    break;
            }

            // 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 ((IsStandardMethodSet() && Method == WebRequestMethod.Post)
                      || (IsCustomMethodSet() && CustomMethod.ToUpperInvariant() == "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

        /// <summary>
        /// the main execution method for cmdlets derived from WebRequestPSCmdlet.
        /// </summary>
        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);
            }
        }

        /// <summary>
        /// Implementing ^C, after start the BeginGetResponse
        /// </summary>
        protected override void StopProcessing()
        {
            if (_webRequest != null)
            {
                _webRequest.Abort();
            }
        }

        #endregion Overrides

        #region Helper Methods

        /// <summary>
        /// Call back method for BeginGetResponse
        /// </summary>
        /// <param name="asyncResult"></param>
        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();
            }
        }

        /// <summary>
        /// Call back method for timeout
        /// </summary>
        /// <param name="state"></param>
        /// <param name="timeout"></param>
        private static void TimeoutCallback(object state, bool timeout)
        {
            if (timeout)
            {
                TimeoutState timeoutState = state as TimeoutState;
                if (timeoutState != null)
                {
                    timeoutState.abort = true;
                    timeoutState.httpRequest.Abort();
                }
            }
        }

        /// <summary>
        /// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream.
        /// </summary>
        /// <param name="request">The WebRequest who's content is to be set</param>
        /// <param name="content">A byte array containing the content data.</param>
        /// <returns>The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property</returns>
        /// <remarks>
        /// 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.
        /// </remarks>
        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;
        }

        /// <summary>
        /// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream.
        /// </summary>
        /// <param name="request">The WebRequest who's content is to be set</param>
        /// <param name="content">A String object containing the content data.</param>
        /// <returns>The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property</returns>
        /// <remarks>
        /// 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.
        /// </remarks>
        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;
        }

        /// <summary>
        /// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream.
        /// </summary>
        /// <param name="request">The WebRequest who's content is to be set</param>
        /// <param name="contentStream">A Stream object containing the content data.</param>
        /// <returns>The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property</returns>
        /// <remarks>
        /// 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.
        /// </remarks>
        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

        /// <summary>
        /// The web request state is used when place asynchronous call to get response of a web request.
        /// </summary>
        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);
            }
        }

        /// <summary>
        /// The timeout state is used when the request is a http request and need to timeout
        /// </summary>
        private class TimeoutState
        {
            public HttpWebRequest httpRequest;
            public bool abort;

            public TimeoutState(HttpWebRequest request)
            {
                httpRequest = request;
                abort = false;
            }
        }

        #endregion private State class
    }
}