//
|
// HttpListenerResponse.cs
|
// Copied from System.Net.HttpListenerResponse.cs
|
//
|
// Author:
|
// Gonzalo Paniagua Javier (gonzalo@novell.com)
|
//
|
// Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
|
// Copyright (c) 2012 sta.blockhead (sta.blockhead@gmail.com)
|
//
|
// Permission is hereby granted, free of charge, to any person obtaining
|
// a copy of this software and associated documentation files (the
|
// "Software"), to deal in the Software without restriction, including
|
// without limitation the rights to use, copy, modify, merge, publish,
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
// permit persons to whom the Software is furnished to do so, subject to
|
// the following conditions:
|
//
|
// The above copyright notice and this permission notice shall be
|
// included in all copies or substantial portions of the Software.
|
//
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
//
|
|
using System;
|
using System.Globalization;
|
using System.IO;
|
using System.Net;
|
using System.Text;
|
|
namespace WebSocketSharp.Net
|
{
|
public sealed class HttpListenerResponse : IDisposable
|
{
|
#region Private Fields
|
|
bool chunked;
|
bool cl_set;
|
Encoding content_encoding;
|
long content_length;
|
string content_type;
|
HttpListenerContext context;
|
CookieCollection cookies;
|
bool disposed;
|
bool force_close_chunked;
|
WebHeaderCollection headers;
|
bool keep_alive;
|
string location;
|
ResponseStream output_stream;
|
int status_code;
|
string status_description;
|
Version version;
|
|
#endregion
|
|
#region Internal Fields
|
|
internal bool HeadersSent;
|
|
#endregion
|
|
#region Constructor
|
|
internal HttpListenerResponse(HttpListenerContext context)
|
{
|
this.context = context;
|
Init();
|
}
|
|
#endregion
|
|
#region Internal Property
|
|
internal bool ForceCloseChunked
|
{
|
get { return force_close_chunked; }
|
}
|
|
#endregion
|
|
#region Public Properties
|
|
public Encoding ContentEncoding
|
{
|
get
|
{
|
if (content_encoding == null)
|
content_encoding = Encoding.Default;
|
return content_encoding;
|
}
|
set
|
{
|
if (disposed)
|
throw new ObjectDisposedException(GetType().ToString());
|
|
// TODO: is null ok?
|
if (HeadersSent)
|
throw new InvalidOperationException("Cannot be changed after headers are sent.");
|
|
content_encoding = value;
|
}
|
}
|
|
public long ContentLength64
|
{
|
get { return content_length; }
|
set
|
{
|
if (disposed)
|
throw new ObjectDisposedException(GetType().ToString());
|
|
if (HeadersSent)
|
throw new InvalidOperationException("Cannot be changed after headers are sent.");
|
|
if (value < 0)
|
throw new ArgumentOutOfRangeException("Must be >= 0", "value");
|
|
cl_set = true;
|
content_length = value;
|
}
|
}
|
|
public string ContentType
|
{
|
get { return content_type; }
|
set
|
{
|
// TODO: is null ok?
|
if (disposed)
|
throw new ObjectDisposedException(GetType().ToString());
|
|
if (HeadersSent)
|
throw new InvalidOperationException("Cannot be changed after headers are sent.");
|
|
content_type = value;
|
}
|
}
|
|
// RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html
|
public CookieCollection Cookies
|
{
|
get
|
{
|
if (cookies == null)
|
cookies = new CookieCollection();
|
return cookies;
|
}
|
set { cookies = value; } // null allowed?
|
}
|
|
public WebHeaderCollection Headers
|
{
|
get { return headers; }
|
set
|
{
|
/**
|
* "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, or
|
* WWW-Authenticate header using the Headers property, an exception will be
|
* thrown. Use the KeepAlive or ContentLength64 properties to set these headers.
|
* You cannot set the Transfer-Encoding or WWW-Authenticate headers manually."
|
*/
|
// TODO: check if this is marked readonly after headers are sent.
|
headers = value;
|
}
|
}
|
|
public bool KeepAlive
|
{
|
get { return keep_alive; }
|
set
|
{
|
if (disposed)
|
throw new ObjectDisposedException(GetType().ToString());
|
|
if (HeadersSent)
|
throw new InvalidOperationException("Cannot be changed after headers are sent.");
|
|
keep_alive = value;
|
}
|
}
|
|
public Stream OutputStream
|
{
|
get
|
{
|
if (output_stream == null)
|
output_stream = context.Connection.GetResponseStream();
|
return output_stream;
|
}
|
}
|
|
public Version ProtocolVersion
|
{
|
get { return version; }
|
set
|
{
|
if (disposed)
|
throw new ObjectDisposedException(GetType().ToString());
|
|
if (HeadersSent)
|
throw new InvalidOperationException("Cannot be changed after headers are sent.");
|
|
if (value == null)
|
throw new ArgumentNullException("value");
|
|
if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1))
|
throw new ArgumentException("Must be 1.0 or 1.1", "value");
|
|
if (disposed)
|
throw new ObjectDisposedException(GetType().ToString());
|
|
version = value;
|
}
|
}
|
|
public string RedirectLocation
|
{
|
get { return location; }
|
set
|
{
|
if (disposed)
|
throw new ObjectDisposedException(GetType().ToString());
|
|
if (HeadersSent)
|
throw new InvalidOperationException("Cannot be changed after headers are sent.");
|
|
location = value;
|
}
|
}
|
|
public bool SendChunked
|
{
|
get { return chunked; }
|
set
|
{
|
if (disposed)
|
throw new ObjectDisposedException(GetType().ToString());
|
|
if (HeadersSent)
|
throw new InvalidOperationException("Cannot be changed after headers are sent.");
|
|
chunked = value;
|
}
|
}
|
|
public int StatusCode
|
{
|
get { return status_code; }
|
set
|
{
|
if (disposed)
|
throw new ObjectDisposedException(GetType().ToString());
|
|
if (HeadersSent)
|
throw new InvalidOperationException("Cannot be changed after headers are sent.");
|
|
if (value < 100 || value > 999)
|
throw new ProtocolViolationException("StatusCode must be between 100 and 999.");
|
|
status_code = value;
|
status_description = Ext.GetStatusDescription(value);
|
}
|
}
|
|
public string StatusDescription
|
{
|
get { return status_description; }
|
set
|
{
|
status_description = value;
|
}
|
}
|
|
#endregion
|
|
#region Private Methods
|
|
void Close(bool force)
|
{
|
disposed = true;
|
context.Connection.Close(force);
|
}
|
|
void IDisposable.Dispose()
|
{
|
Close(true); // TODO: Abort or Close?
|
}
|
|
bool FindCookie(Cookie cookie)
|
{
|
string name = cookie.Name;
|
string domain = cookie.Domain;
|
string path = cookie.Path;
|
foreach (Cookie c in cookies)
|
{
|
if (name != c.Name)
|
continue;
|
if (domain != c.Domain)
|
continue;
|
if (path == c.Path)
|
return true;
|
}
|
|
return false;
|
}
|
|
void Init()
|
{
|
headers = new WebHeaderCollection();
|
keep_alive = true;
|
status_code = 200;
|
status_description = "OK";
|
version = HttpVersion.Version11;
|
}
|
|
#endregion
|
|
#region Internal Method
|
|
internal void SendHeaders(bool closing, MemoryStream ms)
|
{
|
Encoding encoding = content_encoding;
|
if (encoding == null)
|
encoding = Encoding.Default;
|
|
if (content_type != null)
|
{
|
if (content_encoding != null && content_type.IndexOf("charset=", StringComparison.Ordinal) == -1)
|
{
|
string enc_name = content_encoding.WebName;
|
headers.SetInternal("Content-Type", content_type + "; charset=" + enc_name);
|
}
|
else
|
{
|
headers.SetInternal("Content-Type", content_type);
|
}
|
}
|
|
if (headers["Server"] == null)
|
headers.SetInternal("Server", "Mono-HTTPAPI/1.0");
|
|
CultureInfo inv = CultureInfo.InvariantCulture;
|
if (headers["Date"] == null)
|
headers.SetInternal("Date", DateTime.UtcNow.ToString("r", inv));
|
|
if (!chunked)
|
{
|
if (!cl_set && closing)
|
{
|
cl_set = true;
|
content_length = 0;
|
}
|
|
if (cl_set)
|
headers.SetInternal("Content-Length", content_length.ToString(inv));
|
}
|
|
Version v = context.Request.ProtocolVersion;
|
if (!cl_set && !chunked && v >= HttpVersion.Version11)
|
chunked = true;
|
|
/* Apache forces closing the connection for these status codes:
|
* HttpStatusCode.BadRequest 400
|
* HttpStatusCode.RequestTimeout 408
|
* HttpStatusCode.LengthRequired 411
|
* HttpStatusCode.RequestEntityTooLarge 413
|
* HttpStatusCode.RequestUriTooLong 414
|
* HttpStatusCode.InternalServerError 500
|
* HttpStatusCode.ServiceUnavailable 503
|
*/
|
bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 ||
|
status_code == 413 || status_code == 414 || status_code == 500 ||
|
status_code == 503);
|
|
if (conn_close == false)
|
conn_close = !context.Request.KeepAlive;
|
|
// They sent both KeepAlive: true and Connection: close!?
|
if (!keep_alive || conn_close)
|
{
|
headers.SetInternal("Connection", "close");
|
conn_close = true;
|
}
|
|
if (chunked)
|
headers.SetInternal("Transfer-Encoding", "chunked");
|
|
int reuses = context.Connection.Reuses;
|
if (reuses >= 100)
|
{
|
force_close_chunked = true;
|
if (!conn_close)
|
{
|
headers.SetInternal("Connection", "close");
|
conn_close = true;
|
}
|
}
|
|
if (!conn_close)
|
{
|
headers.SetInternal("Keep-Alive", String.Format("timeout=15,max={0}", 100 - reuses));
|
if (context.Request.ProtocolVersion <= HttpVersion.Version10)
|
headers.SetInternal("Connection", "keep-alive");
|
}
|
|
if (location != null)
|
headers.SetInternal("Location", location);
|
|
if (cookies != null)
|
{
|
foreach (Cookie cookie in cookies)
|
headers.SetInternal("Set-Cookie", cookie.ToClientString());
|
}
|
|
StreamWriter writer = new StreamWriter(ms, encoding, 256);
|
writer.Write("HTTP/{0} {1} {2}\r\n", version, status_code, status_description);
|
string headers_str = headers.ToStringMultiValue();
|
writer.Write(headers_str);
|
writer.Flush();
|
int preamble = (encoding.CodePage == 65001) ? 3 : encoding.GetPreamble().Length;
|
if (output_stream == null)
|
output_stream = context.Connection.GetResponseStream();
|
|
/* Assumes that the ms was at position 0 */
|
ms.Position = preamble;
|
HeadersSent = true;
|
}
|
|
#endregion
|
|
#region Public Methods
|
|
public void Abort()
|
{
|
if (disposed)
|
return;
|
|
Close(true);
|
}
|
|
public void AddHeader(string name, string value)
|
{
|
if (name == null)
|
throw new ArgumentNullException("name");
|
|
if (name == "")
|
throw new ArgumentException("'name' cannot be empty", "name");
|
|
// TODO: check for forbidden headers and invalid characters
|
if (value.Length > 65535)
|
throw new ArgumentOutOfRangeException("value");
|
|
headers.Set(name, value);
|
}
|
|
public void AppendCookie(Cookie cookie)
|
{
|
if (cookie == null)
|
throw new ArgumentNullException("cookie");
|
|
Cookies.Add(cookie);
|
}
|
|
public void AppendHeader(string name, string value)
|
{
|
if (name == null)
|
throw new ArgumentNullException("name");
|
|
if (name == "")
|
throw new ArgumentException("'name' cannot be empty", "name");
|
|
if (value.Length > 65535)
|
throw new ArgumentOutOfRangeException("value");
|
|
headers.Add(name, value);
|
}
|
|
public void Close()
|
{
|
if (disposed)
|
return;
|
|
Close(false);
|
}
|
|
public void Close(byte[] responseEntity, bool willBlock)
|
{
|
if (disposed)
|
return;
|
|
if (responseEntity == null)
|
throw new ArgumentNullException("responseEntity");
|
|
// TODO: if willBlock -> BeginWrite + Close ?
|
ContentLength64 = responseEntity.Length;
|
OutputStream.Write(responseEntity, 0, (int)content_length);
|
Close(false);
|
}
|
|
public void CopyFrom(HttpListenerResponse templateResponse)
|
{
|
headers.Clear();
|
headers.Add(templateResponse.headers);
|
content_length = templateResponse.content_length;
|
status_code = templateResponse.status_code;
|
status_description = templateResponse.status_description;
|
keep_alive = templateResponse.keep_alive;
|
version = templateResponse.version;
|
}
|
|
public void Redirect(string url)
|
{
|
StatusCode = 302; // Found
|
location = url;
|
}
|
|
public void SetCookie(Cookie cookie)
|
{
|
if (cookie == null)
|
throw new ArgumentNullException("cookie");
|
|
if (cookies != null)
|
{
|
if (FindCookie(cookie))
|
throw new ArgumentException("The cookie already exists.");
|
}
|
else
|
{
|
cookies = new CookieCollection();
|
}
|
|
cookies.Add(cookie);
|
}
|
|
#endregion
|
}
|
}
|