572 lines
12 KiB
C#
572 lines
12 KiB
C#
#region License
|
|
/*
|
|
* HttpConnection.cs
|
|
*
|
|
* This code is derived from HttpConnection.cs (System.Net) of Mono
|
|
* (http://www.mono-project.com).
|
|
*
|
|
* The MIT License
|
|
*
|
|
* Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
|
|
* Copyright (c) 2012-2021 sta.blockhead
|
|
*
|
|
* 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.
|
|
*/
|
|
#endregion
|
|
|
|
#region Authors
|
|
/*
|
|
* Authors:
|
|
* - Gonzalo Paniagua Javier <gonzalo@novell.com>
|
|
*/
|
|
#endregion
|
|
|
|
#region Contributors
|
|
/*
|
|
* Contributors:
|
|
* - Liryna <liryna.stark@gmail.com>
|
|
* - Rohan Singh <rohan-singh@hotmail.com>
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Security;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace WebSocketSharp.Net
|
|
{
|
|
internal sealed class HttpConnection
|
|
{
|
|
#region Private Fields
|
|
|
|
private int _attempts;
|
|
private byte[] _buffer;
|
|
private static readonly int _bufferLength;
|
|
private HttpListenerContext _context;
|
|
private StringBuilder _currentLine;
|
|
private InputState _inputState;
|
|
private RequestStream _inputStream;
|
|
private LineState _lineState;
|
|
private EndPointListener _listener;
|
|
private EndPoint _localEndPoint;
|
|
private static readonly int _maxInputLength;
|
|
private ResponseStream _outputStream;
|
|
private int _position;
|
|
private EndPoint _remoteEndPoint;
|
|
private MemoryStream _requestBuffer;
|
|
private int _reuses;
|
|
private bool _secure;
|
|
private Socket _socket;
|
|
private Stream _stream;
|
|
private object _sync;
|
|
private int _timeout;
|
|
private Dictionary<int, bool> _timeoutCanceled;
|
|
private Timer _timer;
|
|
|
|
#endregion
|
|
|
|
#region Static Constructor
|
|
|
|
static HttpConnection ()
|
|
{
|
|
_bufferLength = 8192;
|
|
_maxInputLength = 32768;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal Constructors
|
|
|
|
internal HttpConnection (Socket socket, EndPointListener listener)
|
|
{
|
|
_socket = socket;
|
|
_listener = listener;
|
|
|
|
var netStream = new NetworkStream (socket, false);
|
|
|
|
if (listener.IsSecure) {
|
|
var sslConf = listener.SslConfiguration;
|
|
var sslStream = new SslStream (
|
|
netStream,
|
|
false,
|
|
sslConf.ClientCertificateValidationCallback
|
|
);
|
|
|
|
sslStream.AuthenticateAsServer (
|
|
sslConf.ServerCertificate,
|
|
sslConf.ClientCertificateRequired,
|
|
sslConf.EnabledSslProtocols,
|
|
sslConf.CheckCertificateRevocation
|
|
);
|
|
|
|
_secure = true;
|
|
_stream = sslStream;
|
|
}
|
|
else {
|
|
_stream = netStream;
|
|
}
|
|
|
|
_buffer = new byte[_bufferLength];
|
|
_localEndPoint = socket.LocalEndPoint;
|
|
_remoteEndPoint = socket.RemoteEndPoint;
|
|
_sync = new object ();
|
|
_timeoutCanceled = new Dictionary<int, bool> ();
|
|
_timer = new Timer (onTimeout, this, Timeout.Infinite, Timeout.Infinite);
|
|
|
|
init (90000); // 90k ms for first request, 15k ms from then on.
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public Properties
|
|
|
|
public bool IsClosed {
|
|
get {
|
|
return _socket == null;
|
|
}
|
|
}
|
|
|
|
public bool IsLocal {
|
|
get {
|
|
return ((IPEndPoint) _remoteEndPoint).Address.IsLocal ();
|
|
}
|
|
}
|
|
|
|
public bool IsSecure {
|
|
get {
|
|
return _secure;
|
|
}
|
|
}
|
|
|
|
public IPEndPoint LocalEndPoint {
|
|
get {
|
|
return (IPEndPoint) _localEndPoint;
|
|
}
|
|
}
|
|
|
|
public IPEndPoint RemoteEndPoint {
|
|
get {
|
|
return (IPEndPoint) _remoteEndPoint;
|
|
}
|
|
}
|
|
|
|
public int Reuses {
|
|
get {
|
|
return _reuses;
|
|
}
|
|
}
|
|
|
|
public Stream Stream {
|
|
get {
|
|
return _stream;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private Methods
|
|
|
|
private void close ()
|
|
{
|
|
lock (_sync) {
|
|
if (_socket == null)
|
|
return;
|
|
|
|
disposeTimer ();
|
|
disposeRequestBuffer ();
|
|
disposeStream ();
|
|
closeSocket ();
|
|
}
|
|
|
|
_context.Unregister ();
|
|
_listener.RemoveConnection (this);
|
|
}
|
|
|
|
private void closeSocket ()
|
|
{
|
|
try {
|
|
_socket.Shutdown (SocketShutdown.Both);
|
|
}
|
|
catch {
|
|
}
|
|
|
|
_socket.Close ();
|
|
|
|
_socket = null;
|
|
}
|
|
|
|
private void disposeRequestBuffer ()
|
|
{
|
|
if (_requestBuffer == null)
|
|
return;
|
|
|
|
_requestBuffer.Dispose ();
|
|
|
|
_requestBuffer = null;
|
|
}
|
|
|
|
private void disposeStream ()
|
|
{
|
|
if (_stream == null)
|
|
return;
|
|
|
|
_stream.Dispose ();
|
|
|
|
_stream = null;
|
|
}
|
|
|
|
private void disposeTimer ()
|
|
{
|
|
if (_timer == null)
|
|
return;
|
|
|
|
try {
|
|
_timer.Change (Timeout.Infinite, Timeout.Infinite);
|
|
}
|
|
catch {
|
|
}
|
|
|
|
_timer.Dispose ();
|
|
|
|
_timer = null;
|
|
}
|
|
|
|
private void init (int timeout)
|
|
{
|
|
_timeout = timeout;
|
|
|
|
_context = new HttpListenerContext (this);
|
|
_currentLine = new StringBuilder (64);
|
|
_inputState = InputState.RequestLine;
|
|
_inputStream = null;
|
|
_lineState = LineState.None;
|
|
_outputStream = null;
|
|
_position = 0;
|
|
_requestBuffer = new MemoryStream ();
|
|
}
|
|
|
|
private static void onRead (IAsyncResult asyncResult)
|
|
{
|
|
var conn = (HttpConnection) asyncResult.AsyncState;
|
|
var current = conn._attempts;
|
|
|
|
if (conn._socket == null)
|
|
return;
|
|
|
|
lock (conn._sync) {
|
|
if (conn._socket == null)
|
|
return;
|
|
|
|
conn._timer.Change (Timeout.Infinite, Timeout.Infinite);
|
|
conn._timeoutCanceled[current] = true;
|
|
|
|
var nread = 0;
|
|
|
|
try {
|
|
nread = conn._stream.EndRead (asyncResult);
|
|
}
|
|
catch (Exception) {
|
|
// TODO: Logging.
|
|
|
|
conn.close ();
|
|
|
|
return;
|
|
}
|
|
|
|
if (nread <= 0) {
|
|
conn.close ();
|
|
|
|
return;
|
|
}
|
|
|
|
conn._requestBuffer.Write (conn._buffer, 0, nread);
|
|
var len = (int) conn._requestBuffer.Length;
|
|
|
|
if (conn.processInput (conn._requestBuffer.GetBuffer (), len)) {
|
|
if (!conn._context.HasErrorMessage)
|
|
conn._context.Request.FinishInitialization ();
|
|
|
|
if (conn._context.HasErrorMessage) {
|
|
conn._context.SendError ();
|
|
|
|
return;
|
|
}
|
|
|
|
var url = conn._context.Request.Url;
|
|
HttpListener lsnr;
|
|
|
|
if (conn._listener.TrySearchHttpListener (url, out lsnr)) {
|
|
if (!lsnr.AuthenticateContext (conn._context))
|
|
return;
|
|
|
|
if (!lsnr.RegisterContext (conn._context)) {
|
|
conn._context.ErrorStatusCode = 503;
|
|
conn._context.SendError ();
|
|
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
conn._context.ErrorStatusCode = 404;
|
|
conn._context.SendError ();
|
|
|
|
return;
|
|
}
|
|
|
|
conn.BeginReadRequest ();
|
|
}
|
|
}
|
|
|
|
private static void onTimeout (object state)
|
|
{
|
|
var conn = (HttpConnection) state;
|
|
var current = conn._attempts;
|
|
|
|
if (conn._socket == null)
|
|
return;
|
|
|
|
lock (conn._sync) {
|
|
if (conn._socket == null)
|
|
return;
|
|
|
|
if (conn._timeoutCanceled[current])
|
|
return;
|
|
|
|
conn._context.ErrorStatusCode = 408;
|
|
conn._context.SendError ();
|
|
}
|
|
}
|
|
|
|
private bool processInput (byte[] data, int length)
|
|
{
|
|
// This method returns a bool:
|
|
// - true Done processing
|
|
// - false Need more input
|
|
|
|
try {
|
|
while (true) {
|
|
int nread;
|
|
var line = readLineFrom (data, _position, length, out nread);
|
|
|
|
_position += nread;
|
|
|
|
if (line == null)
|
|
break;
|
|
|
|
if (line.Length == 0) {
|
|
if (_inputState == InputState.RequestLine)
|
|
continue;
|
|
|
|
if (_position > _maxInputLength)
|
|
_context.ErrorMessage = "Headers too long";
|
|
|
|
return true;
|
|
}
|
|
|
|
if (_inputState == InputState.RequestLine) {
|
|
_context.Request.SetRequestLine (line);
|
|
_inputState = InputState.Headers;
|
|
}
|
|
else {
|
|
_context.Request.AddHeader (line);
|
|
}
|
|
|
|
if (_context.HasErrorMessage)
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception ex) {
|
|
_context.ErrorMessage = ex.Message;
|
|
|
|
return true;
|
|
}
|
|
|
|
if (_position >= _maxInputLength) {
|
|
_context.ErrorMessage = "Headers too long";
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private string readLineFrom (
|
|
byte[] buffer, int offset, int length, out int nread
|
|
)
|
|
{
|
|
nread = 0;
|
|
|
|
for (var i = offset; i < length; i++) {
|
|
nread++;
|
|
|
|
var b = buffer[i];
|
|
|
|
if (b == 13) {
|
|
_lineState = LineState.Cr;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (b == 10) {
|
|
_lineState = LineState.Lf;
|
|
|
|
break;
|
|
}
|
|
|
|
_currentLine.Append ((char) b);
|
|
}
|
|
|
|
if (_lineState != LineState.Lf)
|
|
return null;
|
|
|
|
var ret = _currentLine.ToString ();
|
|
|
|
_currentLine.Length = 0;
|
|
_lineState = LineState.None;
|
|
|
|
return ret;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal Methods
|
|
|
|
internal void BeginReadRequest ()
|
|
{
|
|
_attempts++;
|
|
|
|
_timeoutCanceled.Add (_attempts, false);
|
|
_timer.Change (_timeout, Timeout.Infinite);
|
|
|
|
try {
|
|
_stream.BeginRead (_buffer, 0, _bufferLength, onRead, this);
|
|
}
|
|
catch (Exception) {
|
|
// TODO: Logging.
|
|
|
|
close ();
|
|
}
|
|
}
|
|
|
|
internal void Close (bool force)
|
|
{
|
|
if (_socket == null)
|
|
return;
|
|
|
|
lock (_sync) {
|
|
if (_socket == null)
|
|
return;
|
|
|
|
if (force) {
|
|
if (_outputStream != null)
|
|
_outputStream.Close (true);
|
|
|
|
close ();
|
|
|
|
return;
|
|
}
|
|
|
|
GetResponseStream ().Close (false);
|
|
|
|
if (_context.Response.CloseConnection) {
|
|
close ();
|
|
|
|
return;
|
|
}
|
|
|
|
if (!_context.Request.FlushInput ()) {
|
|
close ();
|
|
|
|
return;
|
|
}
|
|
|
|
disposeRequestBuffer ();
|
|
_context.Unregister ();
|
|
|
|
_reuses++;
|
|
|
|
init (15000);
|
|
BeginReadRequest ();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
|
|
public void Close ()
|
|
{
|
|
Close (false);
|
|
}
|
|
|
|
public RequestStream GetRequestStream (long contentLength, bool chunked)
|
|
{
|
|
lock (_sync) {
|
|
if (_socket == null)
|
|
return null;
|
|
|
|
if (_inputStream != null)
|
|
return _inputStream;
|
|
|
|
var buff = _requestBuffer.GetBuffer ();
|
|
var len = (int) _requestBuffer.Length;
|
|
var cnt = len - _position;
|
|
|
|
disposeRequestBuffer ();
|
|
|
|
_inputStream = chunked
|
|
? new ChunkedRequestStream (
|
|
_stream, buff, _position, cnt, _context
|
|
)
|
|
: new RequestStream (
|
|
_stream, buff, _position, cnt, contentLength
|
|
);
|
|
|
|
return _inputStream;
|
|
}
|
|
}
|
|
|
|
public ResponseStream GetResponseStream ()
|
|
{
|
|
lock (_sync) {
|
|
if (_socket == null)
|
|
return null;
|
|
|
|
if (_outputStream != null)
|
|
return _outputStream;
|
|
|
|
var lsnr = _context.Listener;
|
|
var ignore = lsnr != null ? lsnr.IgnoreWriteExceptions : true;
|
|
_outputStream = new ResponseStream (_stream, _context.Response, ignore);
|
|
|
|
return _outputStream;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|