//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.Messaging {
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Mime;
using System.ServiceModel.Web;
using System.Text;
using System.Threading;
using System.Web;
///
/// A protocol message (request or response) that passes from this
/// to a remote party via the user agent using a redirect or form
/// POST submission, OR a direct message response.
///
///
/// An instance of this type describes the HTTP response that must be sent
/// in response to the current HTTP request.
/// It is important that this response make up the entire HTTP response.
/// A hosting ASPX page should not be allowed to render its normal HTML output
/// after this response is sent. The normal rendered output of an ASPX page
/// can be canceled by calling after this message
/// is sent on the response stream.
///
public class OutgoingWebResponse {
///
/// The encoder to use for serializing the response body.
///
private static Encoding bodyStringEncoder = new UTF8Encoding(false);
///
/// Initializes a new instance of the class.
///
internal OutgoingWebResponse() {
this.Status = HttpStatusCode.OK;
this.Headers = new WebHeaderCollection();
this.Cookies = new HttpCookieCollection();
}
///
/// Initializes a new instance of the class
/// based on the contents of an .
///
/// The to clone.
/// The maximum bytes to read from the response stream.
protected internal OutgoingWebResponse(HttpWebResponse response, int maximumBytesToRead) {
Requires.NotNull(response, "response");
this.Status = response.StatusCode;
this.Headers = response.Headers;
this.Cookies = new HttpCookieCollection();
this.ResponseStream = new MemoryStream(response.ContentLength < 0 ? 4 * 1024 : (int)response.ContentLength);
using (Stream responseStream = response.GetResponseStream()) {
// BUGBUG: strictly speaking, is the response were exactly the limit, we'd report it as truncated here.
this.IsResponseTruncated = responseStream.CopyUpTo(this.ResponseStream, maximumBytesToRead) == maximumBytesToRead;
this.ResponseStream.Seek(0, SeekOrigin.Begin);
}
}
///
/// Gets the headers that must be included in the response to the user agent.
///
///
/// The headers in this collection are not meant to be a comprehensive list
/// of exactly what should be sent, but are meant to augment whatever headers
/// are generally included in a typical response.
///
public WebHeaderCollection Headers { get; internal set; }
///
/// Gets the body of the HTTP response.
///
public Stream ResponseStream { get; internal set; }
///
/// Gets a value indicating whether the response stream is incomplete due
/// to a length limitation imposed by the HttpWebRequest or calling method.
///
public bool IsResponseTruncated { get; internal set; }
///
/// Gets the cookies collection to add as headers to the HTTP response.
///
public HttpCookieCollection Cookies { get; internal set; }
///
/// Gets or sets the body of the response as a string.
///
public string Body {
get { return this.ResponseStream != null ? this.GetResponseReader().ReadToEnd() : null; }
set { this.SetResponse(value, null); }
}
///
/// Gets the HTTP status code to use in the HTTP response.
///
public HttpStatusCode Status { get; internal set; }
///
/// Gets or sets a reference to the actual protocol message that
/// is being sent via the user agent.
///
internal IProtocolMessage OriginalMessage { get; set; }
///
/// Creates a text reader for the response stream.
///
/// The text reader, initialized for the proper encoding.
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Costly operation")]
public 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));
}
}
///
/// Automatically sends the appropriate response to the user agent
/// and ends execution on the current page or handler.
///
/// Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.
///
/// Requires a current HttpContext.
///
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void Send() {
Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
this.Send(HttpContext.Current);
}
///
/// Automatically sends the appropriate response to the user agent
/// and ends execution on the current page or handler.
///
/// The context of the HTTP request whose response should be set.
/// Typically this is .
/// Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void Send(HttpContext context) {
this.Respond(new HttpContextWrapper(context), true);
}
///
/// Automatically sends the appropriate response to the user agent
/// and ends execution on the current page or handler.
///
/// The context of the HTTP request whose response should be set.
/// Typically this is .
/// Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void Send(HttpContextBase context) {
this.Respond(context, true);
}
///
/// Automatically sends the appropriate response to the user agent
/// and signals ASP.NET to short-circuit the page execution pipeline
/// now that the response has been completed.
/// Not safe to call from ASP.NET web forms.
///
///
/// Requires a current HttpContext.
/// This call is not safe to make from an ASP.NET web form (.aspx file or code-behind) because
/// ASP.NET will render HTML after the protocol message has been sent, which will corrupt the response.
/// Use the method instead for web forms.
///
public virtual void Respond() {
Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
this.Respond(HttpContext.Current);
}
///
/// Automatically sends the appropriate response to the user agent
/// and signals ASP.NET to short-circuit the page execution pipeline
/// now that the response has been completed.
/// Not safe to call from ASP.NET web forms.
///
/// The context of the HTTP request whose response should be set.
/// Typically this is .
///
/// This call is not safe to make from an ASP.NET web form (.aspx file or code-behind) because
/// ASP.NET will render HTML after the protocol message has been sent, which will corrupt the response.
/// Use the method instead for web forms.
///
public void Respond(HttpContext context) {
Requires.NotNull(context, "context");
this.Respond(new HttpContextWrapper(context));
}
///
/// Automatically sends the appropriate response to the user agent
/// and signals ASP.NET to short-circuit the page execution pipeline
/// now that the response has been completed.
/// Not safe to call from ASP.NET web forms.
///
/// The context of the HTTP request whose response should be set.
/// Typically this is .
///
/// This call is not safe to make from an ASP.NET web form (.aspx file or code-behind) because
/// ASP.NET will render HTML after the protocol message has been sent, which will corrupt the response.
/// Use the method instead for web forms.
///
public virtual void Respond(HttpContextBase context) {
Requires.NotNull(context, "context");
this.Respond(context, false);
}
///
/// Submits this response to a WCF response context. Only available when no response body is included.
///
/// The response context to apply the response to.
public virtual void Respond(OutgoingWebResponseContext responseContext) {
Requires.NotNull(responseContext, "responseContext");
if (this.ResponseStream != null) {
throw new NotSupportedException(Strings.ResponseBodyNotSupported);
}
responseContext.StatusCode = this.Status;
responseContext.SuppressEntityBody = true;
foreach (string header in this.Headers) {
responseContext.Headers[header] = this.Headers[header];
}
}
///
/// Automatically sends the appropriate response to the user agent.
///
/// The response to set to this message.
public virtual void Send(HttpListenerResponse response) {
Requires.NotNull(response, "response");
response.StatusCode = (int)this.Status;
MessagingUtilities.ApplyHeadersToResponse(this.Headers, response);
foreach (HttpCookie httpCookie in this.Cookies) {
var cookie = new Cookie(httpCookie.Name, httpCookie.Value) {
Expires = httpCookie.Expires,
Path = httpCookie.Path,
HttpOnly = httpCookie.HttpOnly,
Secure = httpCookie.Secure,
Domain = httpCookie.Domain,
};
response.AppendCookie(cookie);
}
if (this.ResponseStream != null) {
response.ContentLength64 = this.ResponseStream.Length;
this.ResponseStream.CopyTo(response.OutputStream);
}
response.OutputStream.Close();
}
///
/// Gets the URI that, when requested with an HTTP GET request,
/// would transmit the message that normally would be transmitted via a user agent redirect.
///
/// The channel to use for encoding.
///
/// The URL that would transmit the original message. This URL may exceed the normal 2K limit,
/// and should therefore be broken up manually and POSTed as form fields when it exceeds this length.
///
///
/// This is useful for desktop applications that will spawn a user agent to transmit the message
/// rather than cause a redirect.
///
internal Uri GetDirectUriRequest(Channel channel) {
Requires.NotNull(channel, "channel");
var message = this.OriginalMessage as IDirectedProtocolMessage;
if (message == null) {
throw new InvalidOperationException(); // this only makes sense for directed messages (indirect responses)
}
var fields = channel.MessageDescriptions.GetAccessor(message).Serialize();
UriBuilder builder = new UriBuilder(message.Recipient);
MessagingUtilities.AppendQueryArgs(builder, fields);
return builder.Uri;
}
///
/// Sets the response to some string, encoded as UTF-8.
///
/// The string to set the response to.
/// Type of the content. May be null.
internal void SetResponse(string body, ContentType contentType) {
if (body == null) {
this.ResponseStream = null;
return;
}
if (contentType == null) {
contentType = new ContentType("text/html");
contentType.CharSet = bodyStringEncoder.WebName;
} else if (contentType.CharSet != bodyStringEncoder.WebName) {
// clone the original so we're not tampering with our inputs if it came as a parameter.
contentType = new ContentType(contentType.ToString());
contentType.CharSet = bodyStringEncoder.WebName;
}
this.Headers[HttpResponseHeader.ContentType] = contentType.ToString();
this.ResponseStream = new MemoryStream();
StreamWriter writer = new StreamWriter(this.ResponseStream, bodyStringEncoder);
writer.Write(body);
writer.Flush();
this.ResponseStream.Seek(0, SeekOrigin.Begin);
this.Headers[HttpResponseHeader.ContentLength] = this.ResponseStream.Length.ToString(CultureInfo.InvariantCulture);
}
///
/// Automatically sends the appropriate response to the user agent
/// and signals ASP.NET to short-circuit the page execution pipeline
/// now that the response has been completed.
///
/// The context of the HTTP request whose response should be set.
/// Typically this is .
/// If set to false, this method calls
/// rather than
/// to avoid a .
protected internal void Respond(HttpContext context, bool endRequest) {
this.Respond(new HttpContextWrapper(context), endRequest);
}
///
/// Automatically sends the appropriate response to the user agent
/// and signals ASP.NET to short-circuit the page execution pipeline
/// now that the response has been completed.
///
/// The context of the HTTP request whose response should be set.
/// Typically this is .
/// If set to false, this method calls
/// rather than
/// to avoid a .
protected internal virtual void Respond(HttpContextBase context, bool endRequest) {
Requires.NotNull(context, "context");
context.Response.Clear();
context.Response.StatusCode = (int)this.Status;
MessagingUtilities.ApplyHeadersToResponse(this.Headers, context.Response);
if (this.ResponseStream != null) {
try {
this.ResponseStream.CopyTo(context.Response.OutputStream);
} catch (HttpException ex) {
if (ex.ErrorCode == -2147467259 && context.Response.Output != null) {
// Test scenarios can generate this, since the stream is being spoofed:
// System.Web.HttpException: OutputStream is not available when a custom TextWriter is used.
context.Response.Output.Write(this.Body);
} else {
throw;
}
}
}
foreach (string cookieName in this.Cookies) {
var cookie = this.Cookies[cookieName];
context.Response.AppendCookie(cookie);
}
if (endRequest) {
// This approach throws an exception in order that
// no more code is executed in the calling page.
// Microsoft no longer recommends this approach.
context.Response.End();
} else if (context.ApplicationInstance != null) {
// This approach doesn't throw an exception, but
// still tells ASP.NET to short-circuit most of the
// request handling pipeline to speed things up.
context.ApplicationInstance.CompleteRequest();
}
}
}
}