diff options
14 files changed, 325 insertions, 136 deletions
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs b/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs index f08b9ba..81589c7 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs +++ b/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs @@ -129,7 +129,8 @@ namespace DotNetOpenAuth.ApplicationBlock { } var response = consumer.PrepareAuthorizedRequestAndSend(GetContactsEndpoint, accessToken); - XDocument result = XDocument.Parse(response.Body); + string body = response.GetResponseReader().ReadToEnd(); + XDocument result = XDocument.Parse(body); return result; } diff --git a/src/DotNetOpenAuth.Test/Mocks/MockHttpRequest.cs b/src/DotNetOpenAuth.Test/Mocks/MockHttpRequest.cs index 6c853af..78e66ca 100644 --- a/src/DotNetOpenAuth.Test/Mocks/MockHttpRequest.cs +++ b/src/DotNetOpenAuth.Test/Mocks/MockHttpRequest.cs @@ -72,7 +72,7 @@ namespace DotNetOpenAuth.Test.Mocks { sw.Write(responseBody); sw.Flush(); stream.Seek(0, SeekOrigin.Begin); - this.RegisterMockResponse(new DirectWebResponse(responseUri, responseUri, headers ?? new WebHeaderCollection(), HttpStatusCode.OK, contentType, contentEncoding, stream)); + this.RegisterMockResponse(new CachedDirectWebResponse(responseUri, responseUri, headers ?? new WebHeaderCollection(), HttpStatusCode.OK, contentType, contentEncoding, stream)); } internal void RegisterMockXrdsResponses(IDictionary<string, string> requestUriAndResponseBody) { @@ -169,7 +169,7 @@ namespace DotNetOpenAuth.Test.Mocks { var redirectionHeaders = new WebHeaderCollection { { HttpResponseHeader.Location, redirectLocation.AbsoluteUri }, }; - DirectWebResponse response = new DirectWebResponse(origin, origin, redirectionHeaders, HttpStatusCode.Redirect, null, null, new MemoryStream()); + DirectWebResponse response = new CachedDirectWebResponse(origin, origin, redirectionHeaders, HttpStatusCode.Redirect, null, null, new MemoryStream()); this.RegisterMockResponse(response); } @@ -182,7 +182,7 @@ namespace DotNetOpenAuth.Test.Mocks { } else { ////Assert.Fail("Unexpected HTTP request: {0}", uri); Logger.WarnFormat("Unexpected HTTP request: {0}", request.RequestUri); - return new DirectWebResponse(request.RequestUri, request.RequestUri, new WebHeaderCollection(), HttpStatusCode.NotFound, "text/html", null, new MemoryStream()); + return new CachedDirectWebResponse(request.RequestUri, request.RequestUri, new WebHeaderCollection(), HttpStatusCode.NotFound, "text/html", null, new MemoryStream()); } } } diff --git a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs index 07db3f6..10e9795 100644 --- a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs @@ -111,7 +111,7 @@ namespace DotNetOpenAuth.Test.ChannelElements { writer.Flush(); ms.Seek(0, SeekOrigin.Begin); Channel_Accessor channelAccessor = Channel_Accessor.AttachShadow(this.channel); - IDictionary<string, string> deserializedFields = channelAccessor.ReadFromResponseInternal(new DirectWebResponse { ResponseStream = ms }); + IDictionary<string, string> deserializedFields = channelAccessor.ReadFromResponseInternal(new CachedDirectWebResponse { CachedResponseStream = ms }); Assert.AreEqual(fields.Count, deserializedFields.Count); foreach (string key in fields.Keys) { Assert.AreEqual(fields[key], deserializedFields[key]); @@ -246,7 +246,7 @@ namespace DotNetOpenAuth.Test.ChannelElements { { "Location", request.Location.AbsoluteUri }, { "Timestamp", XmlConvert.ToString(request.Timestamp, XmlDateTimeSerializationMode.Utc) }, }; - rawResponse = new DirectWebResponse { + rawResponse = new CachedDirectWebResponse { Body = MessagingUtilities.CreateQueryString(responseFields), }; return rawResponse; diff --git a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs index 60cbe3e..ff601ea 100644 --- a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs @@ -93,8 +93,8 @@ namespace DotNetOpenAuth.Test.OpenId.ChannelElements { { "var1", "value1" }, { "var2", "value2" }, }; - var response = new DirectWebResponse { - ResponseStream = new MemoryStream(KeyValueFormEncoding.GetBytes(fields)), + var response = new CachedDirectWebResponse { + CachedResponseStream = new MemoryStream(KeyValueFormEncoding.GetBytes(fields)), }; Assert.IsTrue(MessagingUtilities.AreEquivalent(fields, this.accessor.ReadFromResponseInternal(response))); } diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index a5b17bc..32f0834 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -72,6 +72,7 @@ <Compile Include="Configuration\UntrustedWebRequestSection.cs" /> <Compile Include="Configuration\HostNameOrRegexCollection.cs" /> <Compile Include="Configuration\HostNameElement.cs" /> + <Compile Include="Messaging\CachedDirectWebResponse.cs" /> <Compile Include="Messaging\IExtensionMessage.cs" /> <Compile Include="Messaging\IMessage.cs" /> <Compile Include="Messaging\DirectWebResponse.cs" /> @@ -83,6 +84,7 @@ <Compile Include="Messaging\IDirectSslWebRequestHandler.cs" /> <Compile Include="Messaging\IProtocolMessageWithExtensions.cs" /> <Compile Include="Messaging\InternalErrorException.cs" /> + <Compile Include="Messaging\NetworkDirectWebResponse.cs" /> <Compile Include="Messaging\Reflection\IMessagePartEncoder.cs" /> <Compile Include="OAuth\ChannelElements\OAuthConsumerMessageFactory.cs" /> <Compile Include="OAuth\ChannelElements\ITokenGenerator.cs" /> diff --git a/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs b/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs new file mode 100644 index 0000000..0a70a70 --- /dev/null +++ b/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs @@ -0,0 +1,158 @@ +//----------------------------------------------------------------------- +// <copyright file="CachedDirectWebResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System; + using System.Diagnostics; + using System.IO; + using System.Net; + using System.Text; + + /// <summary> + /// Cached details on the response from a direct web request to a remote party. + /// </summary> + [DebuggerDisplay("{Status} {ContentType.MediaType}: {Body.Substring(4,50)}")] + internal class CachedDirectWebResponse : DirectWebResponse { + /// <summary> + /// A seekable, repeatable response stream. + /// </summary> + private MemoryStream responseStream; + + /// <summary> + /// Initializes a new instance of the <see cref="CachedDirectWebResponse"/> class. + /// </summary> + internal CachedDirectWebResponse() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="CachedDirectWebResponse"/> class. + /// </summary> + /// <param name="requestUri">The request URI.</param> + /// <param name="response">The response.</param> + /// <param name="maximumBytesToRead">The maximum bytes to read.</param> + internal CachedDirectWebResponse(Uri requestUri, HttpWebResponse response, int maximumBytesToRead) + : base(requestUri, response) { + this.responseStream = CacheNetworkStreamAndClose(response, maximumBytesToRead); + + // BUGBUG: if the response was exactly maximumBytesToRead, we'll incorrectly believe it was truncated. + this.ResponseTruncated = this.responseStream.Length == maximumBytesToRead; + } + + /// <summary> + /// Initializes a new instance of the <see cref="CachedDirectWebResponse"/> class. + /// </summary> + /// <param name="requestUri">The request URI.</param> + /// <param name="responseUri">The final URI to respond to the request.</param> + /// <param name="headers">The headers.</param> + /// <param name="statusCode">The status code.</param> + /// <param name="contentType">Type of the content.</param> + /// <param name="contentEncoding">The content encoding.</param> + /// <param name="responseStream">The response stream.</param> + internal CachedDirectWebResponse(Uri requestUri, Uri responseUri, WebHeaderCollection headers, HttpStatusCode statusCode, string contentType, string contentEncoding, MemoryStream responseStream) + : base(requestUri, responseUri, headers, statusCode, contentType, contentEncoding) { + ErrorUtilities.VerifyArgumentNotNull(responseStream, "responseStream"); + + this.responseStream = responseStream; + } + + /// <summary> + /// Gets or sets the body of the response as a string. + /// </summary> + public string Body { + get { return this.ResponseStream != null ? this.GetResponseReader().ReadToEnd() : null; } + set { this.SetResponse(value); } + } + + /// <summary> + /// Gets a value indicating whether the cached response stream was + /// truncated to a maximum allowable length. + /// </summary> + public bool ResponseTruncated { get; private set; } + + /// <summary> + /// Gets the body of the HTTP response. + /// </summary> + public override Stream ResponseStream { + get { return this.responseStream; } + } + + /// <summary> + /// Gets or sets the cached response stream. + /// </summary> + internal MemoryStream CachedResponseStream { + get { return this.responseStream; } + set { this.responseStream = value; } + } + + /// <summary> + /// Creates a text reader for the response stream. + /// </summary> + /// <returns>The text reader, initialized for the proper encoding.</returns> + public override StreamReader GetResponseReader() { + this.ResponseStream.Seek(0, SeekOrigin.Begin); + string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding]; + if (string.IsNullOrEmpty(contentEncoding)) { + return new StreamReader(this.ResponseStream); + } else { + return new StreamReader(this.ResponseStream, Encoding.GetEncoding(contentEncoding)); + } + } + + /// <summary> + /// Gets an offline snapshot version of this instance. + /// </summary> + /// <param name="maximumBytesToCache">The maximum bytes from the response stream to cache.</param> + /// <returns>A snapshot version of this instance.</returns> + /// <remarks> + /// If this instance is a <see cref="NetworkDirectWebResponse"/> creating a snapshot + /// will automatically close and dispose of the underlying response stream. + /// If this instance is a <see cref="CachedDirectWebResponse"/>, the result will + /// be the self same instance. + /// </remarks> + internal override CachedDirectWebResponse GetSnapshot(int maximumBytesToCache) { + return this; + } + + /// <summary> + /// Sets the response to some string, encoded as UTF-8. + /// </summary> + /// <param name="body">The string to set the response to.</param> + internal void SetResponse(string body) { + if (body == null) { + this.responseStream = null; + return; + } + + Encoding encoding = Encoding.UTF8; + this.Headers[HttpResponseHeader.ContentEncoding] = encoding.HeaderName; + this.responseStream = new MemoryStream(); + StreamWriter writer = new StreamWriter(this.ResponseStream, encoding); + writer.Write(body); + writer.Flush(); + this.ResponseStream.Seek(0, SeekOrigin.Begin); + } + + /// <summary> + /// Caches the network stream and closes it if it is open. + /// </summary> + /// <param name="response">The response whose stream is to be cloned.</param> + /// <param name="maximumBytesToRead">The maximum bytes to cache.</param> + /// <returns>The seekable Stream instance that contains a copy of what was returned in the HTTP response.</returns> + private static MemoryStream CacheNetworkStreamAndClose(HttpWebResponse response, int maximumBytesToRead) { + ErrorUtilities.VerifyArgumentNotNull(response, "response"); + + // Now read and cache the network stream + Stream networkStream = response.GetResponseStream(); + MemoryStream cachedStream = new MemoryStream(response.ContentLength < 0 ? 4 * 1024 : Math.Min((int)response.ContentLength, maximumBytesToRead)); + cachedStream.Seek(0, SeekOrigin.Begin); + + networkStream.Dispose(); + response.Close(); + + return cachedStream; + } + } +} diff --git a/src/DotNetOpenAuth/Messaging/Channel.cs b/src/DotNetOpenAuth/Messaging/Channel.cs index 6484baf..2171a4e 100644 --- a/src/DotNetOpenAuth/Messaging/Channel.cs +++ b/src/DotNetOpenAuth/Messaging/Channel.cs @@ -23,6 +23,11 @@ namespace DotNetOpenAuth.Messaging { /// </summary> public abstract class Channel { /// <summary> + /// The encoding to use when writing out POST entity strings. + /// </summary> + private static readonly Encoding PostEntityEncoding = new UTF8Encoding(false); + + /// <summary> /// The maximum allowable size for a 301 Redirect response before we send /// a 200 OK response with a scripted form POST with the parameters instead /// in order to ensure successfully sending a large payload to another server @@ -50,11 +55,6 @@ namespace DotNetOpenAuth.Messaging { "; /// <summary> - /// The encoding to use when writing out POST entity strings. - /// </summary> - private static readonly Encoding PostEntityEncoding = new UTF8Encoding(false); - - /// <summary> /// A tool that can figure out what kind of message is being received /// so it can be deserialized. /// </summary> diff --git a/src/DotNetOpenAuth/Messaging/DirectWebResponse.cs b/src/DotNetOpenAuth/Messaging/DirectWebResponse.cs index 11d16b7..11e1f75 100644 --- a/src/DotNetOpenAuth/Messaging/DirectWebResponse.cs +++ b/src/DotNetOpenAuth/Messaging/DirectWebResponse.cs @@ -17,30 +17,16 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Details on the response from a direct web request to a remote party. /// </summary> - [Serializable] - [DebuggerDisplay("{Status} {ContentType.MediaType}: {Body.Substring(4,50)}")] - public class DirectWebResponse : IDisposable { + public abstract class DirectWebResponse : IDisposable { /// <summary> /// The encoding to use in reading a response that does not declare its own content encoding. /// </summary> private const string DefaultContentEncoding = "ISO-8859-1"; /// <summary> - /// The network response object, used to initialize this instance, that still needs - /// to be closed if applicable. - /// </summary> - private HttpWebResponse httpWebResponse; - - /// <summary> - /// An object to be locked whenever the <see cref="httpWebResponse"/> or the - /// <see cref="ResponseStream"/> members are being accessed. - /// </summary> - private object responseLock = new object(); - - /// <summary> /// Initializes a new instance of the <see cref="DirectWebResponse"/> class. /// </summary> - internal DirectWebResponse() { + protected internal DirectWebResponse() { this.Status = HttpStatusCode.OK; this.Headers = new WebHeaderCollection(); } @@ -50,7 +36,7 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <param name="requestUri">The original request URI.</param> /// <param name="response">The response to initialize from. The network stream is used by this class directly.</param> - internal DirectWebResponse(Uri requestUri, HttpWebResponse response) { + protected DirectWebResponse(Uri requestUri, HttpWebResponse response) { ErrorUtilities.VerifyArgumentNotNull(requestUri, "requestUri"); ErrorUtilities.VerifyArgumentNotNull(response, "response"); @@ -62,8 +48,6 @@ namespace DotNetOpenAuth.Messaging { this.FinalUri = response.ResponseUri; this.Status = response.StatusCode; this.Headers = response.Headers; - this.httpWebResponse = response; - this.ResponseStream = response.GetResponseStream(); } /// <summary> @@ -75,12 +59,9 @@ namespace DotNetOpenAuth.Messaging { /// <param name="statusCode">The status code.</param> /// <param name="contentType">Type of the content.</param> /// <param name="contentEncoding">The content encoding.</param> - /// <param name="responseStream">The response stream.</param> - internal DirectWebResponse(Uri requestUri, Uri responseUri, WebHeaderCollection headers, HttpStatusCode statusCode, string contentType, string contentEncoding, Stream responseStream) { + protected DirectWebResponse(Uri requestUri, Uri responseUri, WebHeaderCollection headers, HttpStatusCode statusCode, string contentType, string contentEncoding) { ErrorUtilities.VerifyArgumentNotNull(requestUri, "requestUri"); - ErrorUtilities.VerifyArgumentNotNull(responseStream, "responseStream"); this.RequestUri = requestUri; - this.ResponseStream = responseStream; this.Status = statusCode; if (!string.IsNullOrEmpty(contentType)) { this.ContentType = new ContentType(contentType); @@ -115,12 +96,6 @@ namespace DotNetOpenAuth.Messaging { public Uri FinalUri { get; private set; } /// <summary> - /// Gets a value indicating whether the response stream is incomplete due - /// to a length limitation imposed by the HttpWebRequest or calling method. - /// </summary> - public bool IsResponseTruncated { get; internal set; } - - /// <summary> /// Gets the headers that must be included in the response to the user agent. /// </summary> /// <remarks> @@ -138,15 +113,7 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Gets the body of the HTTP response. /// </summary> - public Stream ResponseStream { get; internal set; } - - /// <summary> - /// Gets or sets the body of the response as a string. - /// </summary> - public string Body { - get { return this.ResponseStream != null ? this.GetResponseReader().ReadToEnd() : null; } - set { this.SetResponse(value); } - } + public abstract Stream ResponseStream { get; } /// <summary> /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. @@ -165,25 +132,8 @@ namespace DotNetOpenAuth.Messaging { foreach (string header in this.Headers) { sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "\t{0}: {1}", header, this.Headers[header])); } - sb.AppendLine("Response:"); - sb.AppendLine(this.Body); - return sb.ToString(); - } - /// <summary> - /// Creates a text reader for the response stream. - /// </summary> - /// <returns>The text reader, initialized for the proper encoding.</returns> - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Costly operation")] - public StreamReader GetResponseReader() { - this.CacheNetworkStreamAndClose(); - this.ResponseStream.Seek(0, SeekOrigin.Begin); - string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding]; - if (string.IsNullOrEmpty(contentEncoding)) { - return new StreamReader(this.ResponseStream); - } else { - return new StreamReader(this.ResponseStream, Encoding.GetEncoding(contentEncoding)); - } + return sb.ToString(); } /// <summary> @@ -191,71 +141,38 @@ namespace DotNetOpenAuth.Messaging { /// </summary> public void Dispose() { this.Dispose(true); - GC.SuppressFinalize(true); - } - - /// <summary> - /// Caches the network stream and closes it if it is open. - /// </summary> - internal void CacheNetworkStreamAndClose() { - this.CacheNetworkStreamAndClose(int.MaxValue); + GC.SuppressFinalize(this); } /// <summary> - /// Caches the network stream and closes it if it is open. + /// Creates a text reader for the response stream. /// </summary> - /// <param name="maximumBytesToRead">The maximum bytes to cache.</param> - internal void CacheNetworkStreamAndClose(int maximumBytesToRead) { - lock (this.responseLock) { - if (this.httpWebResponse != null) { - // Now read and cache the network stream - Stream networkStream = this.ResponseStream; - this.ResponseStream = new MemoryStream(this.httpWebResponse.ContentLength < 0 ? 4 * 1024 : Math.Min((int)this.httpWebResponse.ContentLength, maximumBytesToRead)); - //// BUGBUG: strictly speaking, is the response were exactly the limit, we'd report it as truncated here. - this.IsResponseTruncated = networkStream.CopyTo(this.ResponseStream, maximumBytesToRead) == maximumBytesToRead; - this.ResponseStream.Seek(0, SeekOrigin.Begin); - - networkStream.Dispose(); - this.httpWebResponse.Close(); - this.httpWebResponse = null; - } - } - } + /// <returns>The text reader, initialized for the proper encoding.</returns> + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Costly operation")] + public abstract StreamReader GetResponseReader(); /// <summary> - /// Sets the response to some string, encoded as UTF-8. + /// Gets an offline snapshot version of this instance. /// </summary> - /// <param name="body">The string to set the response to.</param> - internal void SetResponse(string body) { - if (body == null) { - this.ResponseStream = null; - return; - } - - Encoding encoding = Encoding.UTF8; - this.Headers[HttpResponseHeader.ContentEncoding] = encoding.HeaderName; - this.ResponseStream = new MemoryStream(); - StreamWriter writer = new StreamWriter(this.ResponseStream, encoding); - writer.Write(body); - writer.Flush(); - this.ResponseStream.Seek(0, SeekOrigin.Begin); - } + /// <param name="maximumBytesToCache">The maximum bytes from the response stream to cache.</param> + /// <returns>A snapshot version of this instance.</returns> + /// <remarks> + /// If this instance is a <see cref="NetworkDirectWebResponse"/> creating a snapshot + /// will automatically close and dispose of the underlying response stream. + /// If this instance is a <see cref="CachedDirectWebResponse"/>, the result will + /// be the self same instance. + /// </remarks> + internal abstract CachedDirectWebResponse GetSnapshot(int maximumBytesToCache); /// <summary> /// Releases unmanaged and - optionally - managed resources /// </summary> /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected void Dispose(bool disposing) { + protected virtual void Dispose(bool disposing) { if (disposing) { - lock (this.responseLock) { - if (this.ResponseStream != null) { - this.ResponseStream.Dispose(); - this.ResponseStream = null; - } - if (this.httpWebResponse != null) { - this.httpWebResponse.Close(); - this.httpWebResponse = null; - } + Stream responseStream = this.ResponseStream; + if (responseStream != null) { + responseStream.Dispose(); } } } diff --git a/src/DotNetOpenAuth/Messaging/NetworkDirectWebResponse.cs b/src/DotNetOpenAuth/Messaging/NetworkDirectWebResponse.cs new file mode 100644 index 0000000..d0618fa --- /dev/null +++ b/src/DotNetOpenAuth/Messaging/NetworkDirectWebResponse.cs @@ -0,0 +1,108 @@ +//----------------------------------------------------------------------- +// <copyright file="NetworkDirectWebResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System; + using System.Diagnostics; + using System.IO; + using System.Net; + using System.Text; + + /// <summary> + /// A live network HTTP response + /// </summary> + [DebuggerDisplay("{Status} {ContentType.MediaType}")] + internal class NetworkDirectWebResponse : DirectWebResponse, IDisposable { + /// <summary> + /// The network response object, used to initialize this instance, that still needs + /// to be closed if applicable. + /// </summary> + private HttpWebResponse httpWebResponse; + + /// <summary> + /// The incoming network response stream. + /// </summary> + private Stream responseStream; + + /// <summary> + /// A value indicating whether a stream reader has already been + /// created on this instance. + /// </summary> + private bool streamReadBegun; + + /// <summary> + /// Initializes a new instance of the <see cref="NetworkDirectWebResponse"/> class. + /// </summary> + /// <param name="requestUri">The request URI.</param> + /// <param name="response">The response.</param> + internal NetworkDirectWebResponse(Uri requestUri, HttpWebResponse response) + : base(requestUri, response) { + this.httpWebResponse = response; + this.responseStream = response.GetResponseStream(); + } + + /// <summary> + /// Gets the body of the HTTP response. + /// </summary> + public override Stream ResponseStream { + get { return this.responseStream; } + } + + /// <summary> + /// Creates a text reader for the response stream. + /// </summary> + /// <returns>The text reader, initialized for the proper encoding.</returns> + public override StreamReader GetResponseReader() { + this.streamReadBegun = true; + if (this.responseStream == null) { + throw new ObjectDisposedException(GetType().Name); + } + + string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding]; + if (string.IsNullOrEmpty(contentEncoding)) { + return new StreamReader(this.ResponseStream); + } else { + return new StreamReader(this.ResponseStream, Encoding.GetEncoding(contentEncoding)); + } + } + + /// <summary> + /// Gets an offline snapshot version of this instance. + /// </summary> + /// <param name="maximumBytesToCache">The maximum bytes from the response stream to cache.</param> + /// <returns>A snapshot version of this instance.</returns> + /// <remarks> + /// If this instance is a <see cref="NetworkDirectWebResponse"/> creating a snapshot + /// will automatically close and dispose of the underlying response stream. + /// If this instance is a <see cref="CachedDirectWebResponse"/>, the result will + /// be the self same instance. + /// </remarks> + internal override CachedDirectWebResponse GetSnapshot(int maximumBytesToCache) { + ErrorUtilities.VerifyOperation(!this.streamReadBegun, "Network stream reading has already begun."); + this.streamReadBegun = true; + var result = new CachedDirectWebResponse(this.RequestUri, this.httpWebResponse, maximumBytesToCache); + this.Dispose(); + return result; + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected override void Dispose(bool disposing) { + if (disposing) { + if (this.responseStream != null) { + this.responseStream.Dispose(); + this.responseStream = null; + } + if (this.httpWebResponse != null) { + this.httpWebResponse.Close(); + this.httpWebResponse = null; + } + } + } + } +} diff --git a/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs b/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs index 1567a64..1a28006 100644 --- a/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs +++ b/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs @@ -64,9 +64,8 @@ namespace DotNetOpenAuth.Messaging { try { Logger.DebugFormat("HTTP {0} {1}", request.Method, request.RequestUri); - using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { - return new DirectWebResponse(request.RequestUri, response); - } + HttpWebResponse response = (HttpWebResponse)request.GetResponse(); + return new NetworkDirectWebResponse(request.RequestUri, response); } catch (WebException ex) { if (Logger.IsErrorEnabled) { if (ex.Response != null) { diff --git a/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs b/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs index 19042b0..b24cad2 100644 --- a/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs +++ b/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs @@ -206,7 +206,7 @@ namespace DotNetOpenAuth.Messaging { /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param> /// <param name="requireSsl">if set to <c>true</c> all requests made with this instance must be completed using SSL.</param> /// <returns> - /// An instance of <see cref="DirectWebResponse"/> describing the response. + /// An instance of <see cref="CachedDirectWebResponse"/> describing the response. /// </returns> public DirectWebResponse GetResponse(HttpWebRequest request, bool requireSsl) { ErrorUtilities.VerifyArgumentNotNull(request, "request"); @@ -223,8 +223,7 @@ namespace DotNetOpenAuth.Messaging { int i; for (i = 0; i < this.MaximumRedirections; i++) { this.EnsureAllowableRequestUri(request.RequestUri, requireSsl); - DirectWebResponse response = this.chainedWebRequestHandler.GetResponse(request); - response.CacheNetworkStreamAndClose(this.MaximumBytesToRead); + CachedDirectWebResponse response = this.chainedWebRequestHandler.GetResponse(request).GetSnapshot(this.MaximumBytesToRead); if (response.Status == HttpStatusCode.MovedPermanently || response.Status == HttpStatusCode.Redirect || response.Status == HttpStatusCode.RedirectMethod || diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs index 2957db3..5dc96bb 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs @@ -155,7 +155,8 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { protected override IDictionary<string, string> ReadFromResponseInternal(DirectWebResponse response) { ErrorUtilities.VerifyArgumentNotNull(response, "response"); - return HttpUtility.ParseQueryString(response.Body).ToDictionary(); + string body = response.GetResponseReader().ReadToEnd(); + return HttpUtility.ParseQueryString(body).ToDictionary(); } /// <summary> diff --git a/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs b/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs index 5d2550e..9b77215 100644 --- a/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs +++ b/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs @@ -22,7 +22,7 @@ namespace DotNetOpenAuth.Yadis { /// <param name="requestUri">The user-supplied identifier.</param> /// <param name="initialResponse">The initial response.</param> /// <param name="finalResponse">The final response.</param> - public DiscoveryResult(Uri requestUri, DirectWebResponse initialResponse, DirectWebResponse finalResponse) { + public DiscoveryResult(Uri requestUri, CachedDirectWebResponse initialResponse, CachedDirectWebResponse finalResponse) { this.RequestUri = requestUri; this.NormalizedUri = initialResponse.FinalUri; if (finalResponse == null) { diff --git a/src/DotNetOpenAuth/Yadis/Yadis.cs b/src/DotNetOpenAuth/Yadis/Yadis.cs index 89a74f6..7d63a52 100644 --- a/src/DotNetOpenAuth/Yadis/Yadis.cs +++ b/src/DotNetOpenAuth/Yadis/Yadis.cs @@ -30,6 +30,12 @@ namespace DotNetOpenAuth.Yadis { internal static readonly RequestCachePolicy IdentifierDiscoveryCachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.CacheIfAvailable); /// <summary> + /// The maximum number of bytes to read from an HTTP response + /// in searching for a link to a YADIS document. + /// </summary> + private const int MaximumResultToScan = 1024 * 1024; + + /// <summary> /// Performs YADIS discovery on some identifier. /// </summary> /// <param name="requestHandler">The mechanism to use for sending HTTP requests.</param> @@ -42,14 +48,13 @@ namespace DotNetOpenAuth.Yadis { /// is not protected by SSL. /// </returns> public static DiscoveryResult Discover(IDirectWebRequestHandler requestHandler, UriIdentifier uri, bool requireSsl) { - DirectWebResponse response; + CachedDirectWebResponse response; try { if (requireSsl && !string.Equals(uri.Uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { Logger.WarnFormat("Discovery on insecure identifier '{0}' aborted.", uri); return null; } - response = Request(requestHandler, uri, requireSsl, ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds); - response.CacheNetworkStreamAndClose(); + response = Request(requestHandler, uri, requireSsl, ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds).GetSnapshot(MaximumResultToScan); if (response.Status != System.Net.HttpStatusCode.OK) { Logger.ErrorFormat("HTTP error {0} {1} while performing discovery on {2}.", (int)response.Status, response.Status, uri); return null; @@ -59,7 +64,7 @@ namespace DotNetOpenAuth.Yadis { Logger.WarnFormat("Unsafe OpenId URL detected ({0}). Request aborted. {1}", uri, ex); return null; } - DirectWebResponse response2 = null; + CachedDirectWebResponse response2 = null; if (IsXrdsDocument(response)) { Logger.Debug("An XRDS response was received from GET at user-supplied identifier."); response2 = response; @@ -79,8 +84,7 @@ namespace DotNetOpenAuth.Yadis { } if (url != null) { if (!requireSsl || string.Equals(url.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { - response2 = Request(requestHandler, url, requireSsl, ContentTypes.Xrds); - response2.CacheNetworkStreamAndClose(); + response2 = Request(requestHandler, url, requireSsl, ContentTypes.Xrds).GetSnapshot(MaximumResultToScan); if (response2.Status != HttpStatusCode.OK) { Logger.ErrorFormat("HTTP error {0} {1} while performing discovery on {2}.", (int)response2.Status, response2.Status, uri); return null; @@ -148,7 +152,7 @@ namespace DotNetOpenAuth.Yadis { /// <returns> /// <c>true</c> if the response constains an XRDS document; otherwise, <c>false</c>. /// </returns> - private static bool IsXrdsDocument(DirectWebResponse response) { + private static bool IsXrdsDocument(CachedDirectWebResponse response) { if (response.ContentType.MediaType == ContentTypes.Xrds) { return true; } |