//----------------------------------------------------------------------- // // Copyright (c) Andrew Arnott. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.Messaging { using System; using System.Collections.Specialized; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Net; using System.Net.Mime; using System.ServiceModel.Channels; using System.Web; /// /// A property store of details of an incoming HTTP request. /// /// /// This serves a very similar purpose to , except that /// ASP.NET does not let us fully initialize that class, so we have to write one /// of our one. /// public class HttpRequestInfo { /// /// The key/value pairs found in the entity of a POST request. /// private NameValueCollection form; /// /// The key/value pairs found in the querystring of the incoming request. /// private NameValueCollection queryString; /// /// Backing field for the property. /// private NameValueCollection queryStringBeforeRewriting; /// /// Backing field for the property. /// private IDirectedProtocolMessage message; /// /// Initializes a new instance of the class. /// /// The ASP.NET structure to copy from. public HttpRequestInfo(HttpRequest request) { Requires.NotNull(request, "request"); Contract.Ensures(this.HttpMethod == request.HttpMethod); Contract.Ensures(this.Url == request.Url); Contract.Ensures(this.RawUrl == request.RawUrl); Contract.Ensures(this.UrlBeforeRewriting != null); Contract.Ensures(this.Headers != null); Contract.Ensures(this.InputStream == request.InputStream); Contract.Ensures(this.form == request.Form); Contract.Ensures(this.queryString == request.QueryString); this.HttpMethod = request.HttpMethod; this.Url = request.Url; this.UrlBeforeRewriting = GetPublicFacingUrl(request); this.RawUrl = request.RawUrl; this.Headers = GetHeaderCollection(request.Headers); this.InputStream = request.InputStream; // These values would normally be calculated, but we'll reuse them from // HttpRequest since they're already calculated, and there's a chance () // that ASP.NET does a better job of being comprehensive about gathering // these as well. this.form = request.Form; this.queryString = request.QueryString; Reporting.RecordRequestStatistics(this); } /// /// Initializes a new instance of the class. /// /// The HTTP method (i.e. GET or POST) of the incoming request. /// The URL being requested. /// The raw URL that appears immediately following the HTTP verb in the request, /// before any URL rewriting takes place. /// Headers in the HTTP request. /// The entity stream, if any. (POST requests typically have these). Use null for GET requests. public HttpRequestInfo(string httpMethod, Uri requestUrl, string rawUrl, WebHeaderCollection headers, Stream inputStream) { Requires.NotNullOrEmpty(httpMethod, "httpMethod"); Requires.NotNull(requestUrl, "requestUrl"); Requires.NotNull(rawUrl, "rawUrl"); Requires.NotNull(headers, "headers"); this.HttpMethod = httpMethod; this.Url = requestUrl; this.UrlBeforeRewriting = requestUrl; this.RawUrl = rawUrl; this.Headers = headers; this.InputStream = inputStream; Reporting.RecordRequestStatistics(this); } /// /// Initializes a new instance of the class. /// /// Details on the incoming HTTP request. public HttpRequestInfo(HttpListenerRequest listenerRequest) { Requires.NotNull(listenerRequest, "listenerRequest"); this.HttpMethod = listenerRequest.HttpMethod; this.Url = listenerRequest.Url; this.UrlBeforeRewriting = listenerRequest.Url; this.RawUrl = listenerRequest.RawUrl; this.Headers = new WebHeaderCollection(); foreach (string key in listenerRequest.Headers) { this.Headers[key] = listenerRequest.Headers[key]; } this.InputStream = listenerRequest.InputStream; Reporting.RecordRequestStatistics(this); } /// /// Initializes a new instance of the class. /// /// The WCF incoming request structure to get the HTTP information from. /// The URI of the service endpoint. public HttpRequestInfo(HttpRequestMessageProperty request, Uri requestUri) { Requires.NotNull(request, "request"); Requires.NotNull(requestUri, "requestUri"); this.HttpMethod = request.Method; this.Headers = request.Headers; this.Url = requestUri; this.UrlBeforeRewriting = requestUri; this.RawUrl = MakeUpRawUrlFromUrl(requestUri); Reporting.RecordRequestStatistics(this); } /// /// Initializes a new instance of the class. /// internal HttpRequestInfo() { Contract.Ensures(this.HttpMethod == "GET"); Contract.Ensures(this.Headers != null); this.HttpMethod = "GET"; this.Headers = new WebHeaderCollection(); } /// /// Initializes a new instance of the class. /// /// The HttpWebRequest (that was never used) to copy from. internal HttpRequestInfo(WebRequest request) { Requires.NotNull(request, "request"); this.HttpMethod = request.Method; this.Url = request.RequestUri; this.UrlBeforeRewriting = request.RequestUri; this.RawUrl = MakeUpRawUrlFromUrl(request.RequestUri); this.Headers = GetHeaderCollection(request.Headers); this.InputStream = null; Reporting.RecordRequestStatistics(this); } /// /// Initializes a new instance of the class. /// /// The message being passed in through a mock transport. May be null. /// The HTTP method that the incoming request came in on, whether or not is null. internal HttpRequestInfo(IDirectedProtocolMessage message, HttpDeliveryMethods httpMethod) { this.message = message; this.HttpMethod = MessagingUtilities.GetHttpVerb(httpMethod); } /// /// Gets or sets the message that is being sent over a mock transport (for testing). /// internal virtual IDirectedProtocolMessage Message { get { return this.message; } set { this.message = value; } } /// /// Gets or sets the verb in the request (i.e. GET, POST, etc.) /// internal string HttpMethod { get; set; } /// /// Gets or sets the entire URL of the request, after any URL rewriting. /// internal Uri Url { get; set; } /// /// Gets or sets the raw URL that appears immediately following the HTTP verb in the request, /// before any URL rewriting takes place. /// internal string RawUrl { get; set; } /// /// Gets or sets the full public URL used by the remote client to initiate this request, /// before any URL rewriting and before any changes made by web farm load distributors. /// internal Uri UrlBeforeRewriting { get; set; } /// /// Gets the query part of the URL (The ? and everything after it), after URL rewriting. /// internal string Query { get { return this.Url != null ? this.Url.Query : null; } } /// /// Gets or sets the collection of headers that came in with the request. /// internal WebHeaderCollection Headers { get; set; } /// /// Gets or sets the entity, or body of the request, if any. /// internal Stream InputStream { get; set; } /// /// Gets the key/value pairs found in the entity of a POST request. /// internal NameValueCollection Form { get { Contract.Ensures(Contract.Result() != null); if (this.form == null) { ContentType contentType = string.IsNullOrEmpty(this.Headers[HttpRequestHeader.ContentType]) ? null : new ContentType(this.Headers[HttpRequestHeader.ContentType]); if (this.HttpMethod == "POST" && contentType != null && string.Equals(contentType.MediaType, Channel.HttpFormUrlEncoded, StringComparison.Ordinal)) { StreamReader reader = new StreamReader(this.InputStream); long originalPosition = 0; if (this.InputStream.CanSeek) { originalPosition = this.InputStream.Position; } this.form = HttpUtility.ParseQueryString(reader.ReadToEnd()); if (this.InputStream.CanSeek) { this.InputStream.Seek(originalPosition, SeekOrigin.Begin); } } else { this.form = new NameValueCollection(); } } return this.form; } } /// /// Gets the key/value pairs found in the querystring of the incoming request. /// internal NameValueCollection QueryString { get { if (this.queryString == null) { this.queryString = this.Query != null ? HttpUtility.ParseQueryString(this.Query) : new NameValueCollection(); } return this.queryString; } } /// /// Gets the query data from the original request (before any URL rewriting has occurred.) /// /// A containing all the parameters in the query string. internal NameValueCollection QueryStringBeforeRewriting { get { if (this.queryStringBeforeRewriting == null) { // This request URL may have been rewritten by the host site. // For openid protocol purposes, we really need to look at // the original query parameters before any rewriting took place. if (!this.IsUrlRewritten) { // No rewriting has taken place. this.queryStringBeforeRewriting = this.QueryString; } else { // Rewriting detected! Recover the original request URI. ErrorUtilities.VerifyInternal(this.UrlBeforeRewriting != null, "UrlBeforeRewriting is null, so the query string cannot be determined."); this.queryStringBeforeRewriting = HttpUtility.ParseQueryString(this.UrlBeforeRewriting.Query); } } return this.queryStringBeforeRewriting; } } /// /// Gets a value indicating whether the request's URL was rewritten by ASP.NET /// or some other module. /// /// /// true if this request's URL was rewritten; otherwise, false. /// internal bool IsUrlRewritten { get { return this.Url != this.UrlBeforeRewriting; } } /// /// Gets the public facing URL for the given incoming HTTP request. /// /// The request. /// The server variables to consider part of the request. /// /// The URI that the outside world used to create this request. /// /// /// Although the value can be obtained from /// , it's useful to be able to pass them /// in so we can simulate injected values from our unit tests since the actual property /// is a read-only kind of . /// internal static Uri GetPublicFacingUrl(HttpRequest request, NameValueCollection serverVariables) { Requires.NotNull(request, "request"); Requires.NotNull(serverVariables, "serverVariables"); // Due to URL rewriting, cloud computing (i.e. Azure) // and web farms, etc., we have to be VERY careful about what // we consider the incoming URL. We want to see the URL as it would // appear on the public-facing side of the hosting web site. // HttpRequest.Url gives us the internal URL in a cloud environment, // So we use a variable that (at least from what I can tell) gives us // the public URL: if (serverVariables["HTTP_HOST"] != null) { ErrorUtilities.VerifySupported(request.Url.Scheme == Uri.UriSchemeHttps || request.Url.Scheme == Uri.UriSchemeHttp, "Only HTTP and HTTPS are supported protocols."); string scheme = serverVariables["HTTP_X_FORWARDED_PROTO"] ?? request.Url.Scheme; Uri hostAndPort = new Uri(scheme + Uri.SchemeDelimiter + serverVariables["HTTP_HOST"]); UriBuilder publicRequestUri = new UriBuilder(request.Url); publicRequestUri.Scheme = scheme; publicRequestUri.Host = hostAndPort.Host; publicRequestUri.Port = hostAndPort.Port; // CC missing Uri.Port contract that's on UriBuilder.Port return publicRequestUri.Uri; } else { // Failover to the method that works for non-web farm enviroments. // We use Request.Url for the full path to the server, and modify it // with Request.RawUrl to capture both the cookieless session "directory" if it exists // and the original path in case URL rewriting is going on. We don't want to be // fooled by URL rewriting because we're comparing the actual URL with what's in // the return_to parameter in some cases. // Response.ApplyAppPathModifier(builder.Path) would have worked for the cookieless // session, but not the URL rewriting problem. return new Uri(request.Url, request.RawUrl); } } /// /// Gets the query or form data from the original request (before any URL rewriting has occurred.) /// /// A set of name=value pairs. [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Expensive call")] internal NameValueCollection GetQueryOrFormFromContext() { NameValueCollection query; if (this.HttpMethod == "GET") { query = this.QueryStringBeforeRewriting; } else { query = this.Form; } return query; } /// /// Gets the public facing URL for the given incoming HTTP request. /// /// The request. /// The URI that the outside world used to create this request. private static Uri GetPublicFacingUrl(HttpRequest request) { Requires.NotNull(request, "request"); return GetPublicFacingUrl(request, request.ServerVariables); } /// /// Makes up a reasonable guess at the raw URL from the possibly rewritten URL. /// /// A full URL. /// A raw URL that might have come in on the HTTP verb. private static string MakeUpRawUrlFromUrl(Uri url) { Requires.NotNull(url, "url"); return url.AbsolutePath + url.Query + url.Fragment; } /// /// Converts a NameValueCollection to a WebHeaderCollection. /// /// The collection a HTTP headers. /// A new collection of the given headers. private static WebHeaderCollection GetHeaderCollection(NameValueCollection pairs) { Requires.NotNull(pairs, "pairs"); WebHeaderCollection headers = new WebHeaderCollection(); foreach (string key in pairs) { try { headers.Add(key, pairs[key]); } catch (ArgumentException ex) { Logger.Messaging.WarnFormat( "{0} thrown when trying to add web header \"{1}: {2}\". {3}", ex.GetType().Name, key, pairs[key], ex.Message); } } return headers; } #if CONTRACTS_FULL /// /// Verifies conditions that should be true for any valid state of this object. /// [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")] [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] [ContractInvariantMethod] private void ObjectInvariant() { } #endif } }