using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using HttpServer.Exceptions;
using HttpServer.Parser;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace HttpServer
{
///
/// Contains a connection to a browser/client.
///
///
/// Remember to after you have hooked the event.
///
/// TODO: Maybe this class should be broken up into HttpClientChannel and HttpClientContext?
public class HttpClientContext : IHttpClientContext ,IDisposable
{
private readonly byte[] _buffer;
private int _bytesLeft;
private ILogWriter _log;
private readonly IHttpRequestParser _parser;
private readonly int _bufferSize;
private IHttpRequest _currentRequest;
private readonly Socket _sock;
public bool Available = true;
private bool _endWhenDone = false;
///
/// This context have been cleaned, which means that it can be reused.
///
public event EventHandler Cleaned = delegate { };
///
/// Context have been started (a new client have connected)
///
public event EventHandler Started = delegate { };
///
/// Initializes a new instance of the class.
///
/// true if the connection is secured (SSL/TLS)
/// client that connected.
/// Stream used for communication
/// Used to create a .
/// Size of buffer to use when reading data. Must be at least 4096 bytes.
/// If fails
/// Stream must be writable and readable.
public HttpClientContext(bool secured, IPEndPoint remoteEndPoint,
Stream stream, IRequestParserFactory parserFactory, int bufferSize, Socket sock)
{
Check.Require(remoteEndPoint, "remoteEndPoint");
Check.NotEmpty(remoteEndPoint.Address.ToString(), "remoteEndPoint.Address");
Check.Require(stream, "stream");
Check.Require(parserFactory, "parser");
Check.Min(4096, bufferSize, "bufferSize");
Check.Require(sock, "socket");
if (!stream.CanWrite || !stream.CanRead)
throw new ArgumentException("Stream must be writable and readable.");
_bufferSize = bufferSize;
RemoteAddress = remoteEndPoint.Address.ToString();
RemotePort = remoteEndPoint.Port.ToString();
_log = NullLogWriter.Instance;
_parser = parserFactory.CreateParser(_log);
_parser.RequestCompleted += OnRequestCompleted;
_parser.RequestLineReceived += OnRequestLine;
_parser.HeaderReceived += OnHeaderReceived;
_parser.BodyBytesReceived += OnBodyBytesReceived;
_currentRequest = new HttpRequest();
IsSecured = secured;
_stream = stream;
_buffer = new byte[bufferSize];
// by Fumi.Iseki
SSLCommonName = "";
if (secured)
{
SslStream _ssl = (SslStream)_stream;
X509Certificate _cert1 = _ssl.RemoteCertificate;
if (_cert1 != null)
{
X509Certificate2 _cert2 = new X509Certificate2(_cert1);
if (_cert2 != null) SSLCommonName = _cert2.GetNameInfo(X509NameType.SimpleName, false);
}
}
}
public bool EndWhenDone
{
get { return _endWhenDone; }
set { _endWhenDone = value;}
}
///
/// Process incoming body bytes.
///
///
/// Bytes
protected virtual void OnBodyBytesReceived(object sender, BodyEventArgs e)
{
_currentRequest.AddToBody(e.Buffer, e.Offset, e.Count);
}
///
///
///
///
///
protected virtual void OnHeaderReceived(object sender, HeaderEventArgs e)
{
if (string.Compare(e.Name, "expect", true) == 0 && e.Value.Contains("100-continue"))
{
Respond("HTTP/1.0", HttpStatusCode.Continue, "Please continue mate.");
}
_currentRequest.AddHeader(e.Name, e.Value);
}
private void OnRequestLine(object sender, RequestLineEventArgs e)
{
_currentRequest.Method = e.HttpMethod;
_currentRequest.HttpVersion = e.HttpVersion;
_currentRequest.UriPath = e.UriPath;
}
///
/// Overload to specify own type.
///
///
/// Must be specified before the context is being used.
///
protected IHttpRequest CurrentRequest
{ get { return _currentRequest; } set { _currentRequest = value; } }
///
/// Start reading content.
///
///
/// Make sure to call base.Start() if you override this method.
///
public virtual void Start()
{
try
{
_stream.BeginRead(_buffer, 0, _bufferSize, OnReceive, null);
}
catch (IOException err)
{
LogWriter.Write(this, LogPrio.Debug, err.ToString());
}
Started(this, EventArgs.Empty);
}
///
/// Clean up context.
///
///
/// Make sure to call base.Cleanup() if you override the method.
///
public virtual void Cleanup()
{
if (Stream == null)
return;
Stream.Dispose();
Stream = null;
_currentRequest.Clear();
_bytesLeft = 0;
Cleaned(this, EventArgs.Empty);
_parser.Clear();
}
public void Close()
{
Cleanup();
Available = true;
}
///
/// Using SSL or other encryption method.
///
[Obsolete("Use IsSecured instead.")]
public bool Secured
{
get { return IsSecured; }
}
///
/// Using SSL or other encryption method.
///
public bool IsSecured { get; internal set; }
//
//
// by Fumi.Iseki
public string SSLCommonName { get; internal set; }
///
/// Specify which logger to use.
///
public ILogWriter LogWriter
{
get { return _log; }
set
{
_log = value ?? NullLogWriter.Instance;
_parser.LogWriter = _log;
}
}
private Stream _stream;
///
/// Gets or sets the network stream.
///
internal Stream Stream
{
get { return _stream; }
set { _stream = value; }
}
///
/// Gets or sets IP address that the client connected from.
///
internal string RemoteAddress { get; set; }
///
/// Gets or sets port that the client connected from.
///
internal string RemotePort { get; set; }
///
/// Disconnect from client
///
/// error to report in the event.
public void Disconnect(SocketError error)
{
// disconnect may not throw any exceptions
try
{
//_sock.Disconnect(true);
if (error == SocketError.Success)
{
if (Stream is ReusableSocketNetworkStream)
((NetworkStream)Stream).Flush();
}
//Stream.Close();
Disconnected(this, new DisconnectedEventArgs(error));
}
catch (Exception err)
{
LogWriter.Write(this, LogPrio.Error, "Disconnect threw an exception: " + err);
}
}
private void OnReceive(IAsyncResult ar)
{
try
{
int bytesRead = Stream.EndRead(ar);
if (bytesRead == 0)
{
Disconnect(SocketError.ConnectionReset);
return;
}
_bytesLeft += bytesRead;
if (_bytesLeft > _buffer.Length)
{
#if DEBUG
throw new BadRequestException("Too large HTTP header: " + Encoding.UTF8.GetString(_buffer, 0, bytesRead));
#else
throw new BadRequestException("Too large HTTP header: " + _bytesLeft);
#endif
}
#if DEBUG
#pragma warning disable 219
string temp = Encoding.ASCII.GetString(_buffer, 0, _bytesLeft);
LogWriter.Write(this, LogPrio.Trace, "Received: " + temp);
#pragma warning restore 219
#endif
int offset = _parser.Parse(_buffer, 0, _bytesLeft);
if (Stream == null)
return; // "Connection: Close" in effect.
// try again to see if we can parse another message (check parser to see if it is looking for a new message)
int oldOffset = offset;
while (_parser.CurrentState == RequestParserState.FirstLine && offset != 0 && _bytesLeft - offset > 0)
{
#if DEBUG
temp = Encoding.ASCII.GetString(_buffer, offset, _bytesLeft - offset);
LogWriter.Write(this, LogPrio.Trace, "Processing: " + temp);
#endif
offset = _parser.Parse(_buffer, offset, _bytesLeft - offset);
if (Stream == null)
return; // "Connection: Close" in effect.
}
// need to be able to move prev bytes, so restore offset.
if (offset == 0)
offset = oldOffset;
// copy unused bytes to the beginning of the array
if (offset > 0 && _bytesLeft > offset)
Buffer.BlockCopy(_buffer, offset, _buffer, 0, _bytesLeft - offset);
_bytesLeft -= offset;
if (Stream != null && Stream.CanRead)
Stream.BeginRead(_buffer, _bytesLeft, _buffer.Length - _bytesLeft, OnReceive, null);
else
{
_log.Write(this, LogPrio.Warning, "Could not read any more from the socket.");
Disconnect(SocketError.Success);
}
}
catch (BadRequestException err)
{
LogWriter.Write(this, LogPrio.Warning, "Bad request, responding with it. Error: " + err);
try
{
Respond("HTTP/1.0", HttpStatusCode.BadRequest, err.Message);
}
catch(Exception err2)
{
LogWriter.Write(this, LogPrio.Fatal, "Failed to reply to a bad request. " + err2);
}
Disconnect(SocketError.NoRecovery);
}
catch (IOException err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive: " + err.Message);
if (err.InnerException is SocketException)
Disconnect((SocketError) ((SocketException) err.InnerException).ErrorCode);
else
Disconnect(SocketError.ConnectionReset);
}
catch (ObjectDisposedException err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive : " + err.Message);
Disconnect(SocketError.NotSocket);
}
catch (NullReferenceException err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive : NullRef: " + err.Message);
Disconnect(SocketError.NoRecovery);
}
catch (Exception err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive: " + err.Message);
Disconnect(SocketError.NoRecovery);
}
}
private void OnRequestCompleted(object source, EventArgs args)
{
_currentRequest.AddHeader("remote_addr", RemoteAddress);
_currentRequest.AddHeader("remote_port", RemotePort);
// load cookies if they exist
RequestCookies cookies = _currentRequest.Headers["cookie"] != null
? new RequestCookies(_currentRequest.Headers["cookie"])
: new RequestCookies(String.Empty);
_currentRequest.SetCookies(cookies);
_currentRequest.Body.Seek(0, SeekOrigin.Begin);
RequestReceived(this, new RequestEventArgs(_currentRequest));
_currentRequest.Clear();
}
///
/// Send a response.
///
/// Either or
/// HTTP status code
/// reason for the status code.
/// HTML body contents, can be null or empty.
/// A content type to return the body as, i.e. 'text/html' or 'text/plain', defaults to 'text/html' if null or empty
/// If is invalid.
public void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body, string contentType)
{
if (string.IsNullOrEmpty(contentType))
contentType = "text/html";
if (string.IsNullOrEmpty(httpVersion) || !httpVersion.StartsWith("HTTP/1"))
throw new ArgumentException("Invalid HTTP version");
if (string.IsNullOrEmpty(reason))
reason = statusCode.ToString();
string response = string.IsNullOrEmpty(body)
? httpVersion + " " + (int) statusCode + " " + reason + "\r\n\r\n"
: string.Format("{0} {1} {2}\r\nContent-Type: {5}\r\nContent-Length: {3}\r\n\r\n{4}",
httpVersion, (int) statusCode, reason ?? statusCode.ToString(),
body.Length, body, contentType);
byte[] buffer = Encoding.ASCII.GetBytes(response);
Send(buffer);
}
///
/// Send a response.
///
/// Either or
/// HTTP status code
/// reason for the status code.
public void Respond(string httpVersion, HttpStatusCode statusCode, string reason)
{
Respond(httpVersion, statusCode, reason, null, null);
}
///
/// Send a response.
///
///
public void Respond(string body)
{
if (body == null)
throw new ArgumentNullException("body");
Respond("HTTP/1.1", HttpStatusCode.OK, HttpStatusCode.OK.ToString(), body, null);
}
///
/// send a whole buffer
///
/// buffer to send
///
public void Send(byte[] buffer)
{
if (buffer == null)
throw new ArgumentNullException("buffer");
Send(buffer, 0, buffer.Length);
}
///
/// Send data using the stream
///
/// Contains data to send
/// Start position in buffer
/// number of bytes to send
///
///
public void Send(byte[] buffer, int offset, int size)
{
if (offset + size > buffer.Length)
throw new ArgumentOutOfRangeException("offset", offset, "offset + size is beyond end of buffer.");
if (Stream != null && Stream.CanWrite)
{
try
{
Stream.Write(buffer, offset, size);
}
catch (IOException)
{
}
}
}
///
/// The context have been disconnected.
///
///
/// Event can be used to clean up a context, or to reuse it.
///
public event EventHandler Disconnected = delegate{};
///
/// A request have been received in the context.
///
public event EventHandler RequestReceived = delegate{};
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool unmanaged)
{
if (unmanaged)
{
if (Stream != null)
{
try
{
if (Stream.CanWrite)
Stream.Flush();
Cleanup();
}
catch (IOException)
{
}
}
}
}
~HttpClientContext()
{
Dispose();
}
}
}