diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2012-01-12 08:40:50 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2012-01-12 08:42:14 -0800 |
commit | af21cdaf77ca72f54e04f22268b740ce262582fa (patch) | |
tree | 9b158e3bff1f56264bccb9e45c8b807816beece6 /src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs | |
parent | a73f2a4830aaa2afcb3f13da2206d9b011dad7fb (diff) | |
download | DotNetOpenAuth-af21cdaf77ca72f54e04f22268b740ce262582fa.zip DotNetOpenAuth-af21cdaf77ca72f54e04f22268b740ce262582fa.tar.gz DotNetOpenAuth-af21cdaf77ca72f54e04f22268b740ce262582fa.tar.bz2 |
Renamed assembly DotNetOpenAuth.Messaging(.UI) to DotNetOpenAuth.Core(.UI)
Diffstat (limited to 'src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs')
-rw-r--r-- | src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs | 423 |
1 files changed, 423 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs new file mode 100644 index 0000000..0cf37a5 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs @@ -0,0 +1,423 @@ +//----------------------------------------------------------------------- +// <copyright file="HttpRequestInfo.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +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; + + /// <summary> + /// A property store of details of an incoming HTTP request. + /// </summary> + /// <remarks> + /// This serves a very similar purpose to <see cref="HttpRequest"/>, except that + /// ASP.NET does not let us fully initialize that class, so we have to write one + /// of our one. + /// </remarks> + public class HttpRequestInfo { + /// <summary> + /// The key/value pairs found in the entity of a POST request. + /// </summary> + private NameValueCollection form; + + /// <summary> + /// The key/value pairs found in the querystring of the incoming request. + /// </summary> + private NameValueCollection queryString; + + /// <summary> + /// Backing field for the <see cref="QueryStringBeforeRewriting"/> property. + /// </summary> + private NameValueCollection queryStringBeforeRewriting; + + /// <summary> + /// Backing field for the <see cref="Message"/> property. + /// </summary> + private IDirectedProtocolMessage message; + + /// <summary> + /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class. + /// </summary> + /// <param name="request">The ASP.NET structure to copy from.</param> + 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 (<g>) + // 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); + } + + /// <summary> + /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class. + /// </summary> + /// <param name="httpMethod">The HTTP method (i.e. GET or POST) of the incoming request.</param> + /// <param name="requestUrl">The URL being requested.</param> + /// <param name="rawUrl">The raw URL that appears immediately following the HTTP verb in the request, + /// before any URL rewriting takes place.</param> + /// <param name="headers">Headers in the HTTP request.</param> + /// <param name="inputStream">The entity stream, if any. (POST requests typically have these). Use <c>null</c> for GET requests.</param> + 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); + } + + /// <summary> + /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class. + /// </summary> + /// <param name="listenerRequest">Details on the incoming HTTP request.</param> + 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); + } + + /// <summary> + /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class. + /// </summary> + /// <param name="request">The WCF incoming request structure to get the HTTP information from.</param> + /// <param name="requestUri">The URI of the service endpoint.</param> + 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); + } + + /// <summary> + /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class. + /// </summary> + internal HttpRequestInfo() { + Contract.Ensures(this.HttpMethod == "GET"); + Contract.Ensures(this.Headers != null); + + this.HttpMethod = "GET"; + this.Headers = new WebHeaderCollection(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class. + /// </summary> + /// <param name="request">The HttpWebRequest (that was never used) to copy from.</param> + 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); + } + + /// <summary> + /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class. + /// </summary> + /// <param name="message">The message being passed in through a mock transport. May be null.</param> + /// <param name="httpMethod">The HTTP method that the incoming request came in on, whether or not <paramref name="message"/> is null.</param> + internal HttpRequestInfo(IDirectedProtocolMessage message, HttpDeliveryMethods httpMethod) { + this.message = message; + this.HttpMethod = MessagingUtilities.GetHttpVerb(httpMethod); + } + + /// <summary> + /// Gets or sets the message that is being sent over a mock transport (for testing). + /// </summary> + internal virtual IDirectedProtocolMessage Message { + get { return this.message; } + set { this.message = value; } + } + + /// <summary> + /// Gets or sets the verb in the request (i.e. GET, POST, etc.) + /// </summary> + internal string HttpMethod { get; set; } + + /// <summary> + /// Gets or sets the entire URL of the request, after any URL rewriting. + /// </summary> + internal Uri Url { get; set; } + + /// <summary> + /// Gets or sets the raw URL that appears immediately following the HTTP verb in the request, + /// before any URL rewriting takes place. + /// </summary> + internal string RawUrl { get; set; } + + /// <summary> + /// 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. + /// </summary> + internal Uri UrlBeforeRewriting { get; set; } + + /// <summary> + /// Gets the query part of the URL (The ? and everything after it), after URL rewriting. + /// </summary> + internal string Query { + get { return this.Url != null ? this.Url.Query : null; } + } + + /// <summary> + /// Gets or sets the collection of headers that came in with the request. + /// </summary> + internal WebHeaderCollection Headers { get; set; } + + /// <summary> + /// Gets or sets the entity, or body of the request, if any. + /// </summary> + internal Stream InputStream { get; set; } + + /// <summary> + /// Gets the key/value pairs found in the entity of a POST request. + /// </summary> + internal NameValueCollection Form { + get { + Contract.Ensures(Contract.Result<NameValueCollection>() != 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; + } + } + + /// <summary> + /// Gets the key/value pairs found in the querystring of the incoming request. + /// </summary> + internal NameValueCollection QueryString { + get { + if (this.queryString == null) { + this.queryString = this.Query != null ? HttpUtility.ParseQueryString(this.Query) : new NameValueCollection(); + } + + return this.queryString; + } + } + + /// <summary> + /// Gets the query data from the original request (before any URL rewriting has occurred.) + /// </summary> + /// <returns>A <see cref="NameValueCollection"/> containing all the parameters in the query string.</returns> + 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; + } + } + + /// <summary> + /// Gets a value indicating whether the request's URL was rewritten by ASP.NET + /// or some other module. + /// </summary> + /// <value> + /// <c>true</c> if this request's URL was rewritten; otherwise, <c>false</c>. + /// </value> + internal bool IsUrlRewritten { + get { return this.Url != this.UrlBeforeRewriting; } + } + + /// <summary> + /// Gets the public facing URL for the given incoming HTTP request. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="serverVariables">The server variables to consider part of the request.</param> + /// <returns> + /// The URI that the outside world used to create this request. + /// </returns> + /// <remarks> + /// Although the <paramref name="serverVariables"/> value can be obtained from + /// <see cref="HttpRequest.ServerVariables"/>, 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 <see cref="NameValueCollection"/>. + /// </remarks> + 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); + } + } + + /// <summary> + /// Gets the query or form data from the original request (before any URL rewriting has occurred.) + /// </summary> + /// <returns>A set of name=value pairs.</returns> + [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; + } + + /// <summary> + /// Gets the public facing URL for the given incoming HTTP request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>The URI that the outside world used to create this request.</returns> + private static Uri GetPublicFacingUrl(HttpRequest request) { + Requires.NotNull(request, "request"); + return GetPublicFacingUrl(request, request.ServerVariables); + } + + /// <summary> + /// Makes up a reasonable guess at the raw URL from the possibly rewritten URL. + /// </summary> + /// <param name="url">A full URL.</param> + /// <returns>A raw URL that might have come in on the HTTP verb.</returns> + private static string MakeUpRawUrlFromUrl(Uri url) { + Requires.NotNull(url, "url"); + return url.AbsolutePath + url.Query + url.Fragment; + } + + /// <summary> + /// Converts a NameValueCollection to a WebHeaderCollection. + /// </summary> + /// <param name="pairs">The collection a HTTP headers.</param> + /// <returns>A new collection of the given headers.</returns> + 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 + /// <summary> + /// Verifies conditions that should be true for any valid state of this object. + /// </summary> + [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 + } +} |