You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
671 lines
21 KiB
671 lines
21 KiB
using Apewer.Network;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Security;
|
|
using System.Net.Sockets;
|
|
using System.Security.Authentication;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace Apewer.Web
|
|
{
|
|
|
|
/// <summary>Socket 连接。</summary>
|
|
public sealed class MiniConnection
|
|
{
|
|
|
|
// 超时毫秒数。
|
|
const int RequestHeadTimeout = 15 * 1000;
|
|
const int RequestBodyTimeout = 3600 * 1000;
|
|
const int ResponseHeadTimeout = 15 * 1000;
|
|
const int ResponseBodyTimeout = 3600 * 1000;
|
|
const int StreamReadTimeout = 3 * 1000;
|
|
const int StreamWriteTimeout = 3 * 1000;
|
|
|
|
// 缓冲。
|
|
const int BufferSize = 8192;
|
|
const int HeadMax = 32768;
|
|
|
|
// 持久变量。
|
|
Timer _timer;
|
|
int _reuses = 0;
|
|
|
|
// 临时连接变量,每次请求时初始化。
|
|
MiniContext _context;
|
|
ArrayBuilder<byte> _head = null;
|
|
byte[] _buffer;
|
|
MiniReader _instream = null;
|
|
MiniWriter _outstream = null;
|
|
bool _headsent = false;
|
|
bool _bodysent = false;
|
|
bool _forceClose = false;
|
|
bool _chunked = false;
|
|
int _headread = 0;
|
|
|
|
internal MiniConnection(MiniServer server, Socket socket)
|
|
{
|
|
_server = server;
|
|
_socket = socket;
|
|
|
|
// init
|
|
_netstream = new NetworkStream(socket, false);
|
|
_netstream.ReadTimeout = StreamReadTimeout;
|
|
_netstream.WriteTimeout = StreamWriteTimeout;
|
|
_local = socket.LocalEndPoint as IPEndPoint;
|
|
_remote = socket.RemoteEndPoint as IPEndPoint;
|
|
_stream = _netstream;
|
|
|
|
// SSL 证书。
|
|
var certificate = server.SslCertificate;
|
|
if (certificate != null)
|
|
{
|
|
_sslstream = CreateSslStream(_netstream, certificate, server.SslProtocols);
|
|
_stream = _sslstream;
|
|
}
|
|
}
|
|
|
|
void Timeout(int duration = System.Threading.Timeout.Infinite)
|
|
{
|
|
const int Infinite = System.Threading.Timeout.Infinite;
|
|
if (duration > 0)
|
|
{
|
|
if (_timer == null) _timer = new Timer(obj => CloseSocket(), null, Infinite, Infinite);
|
|
else _timer.Change(duration, Infinite);
|
|
}
|
|
else
|
|
{
|
|
if (_timer != null)
|
|
{
|
|
_timer.Change(Infinite, Infinite);
|
|
_timer.Dispose();
|
|
_timer = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
#region socket
|
|
|
|
MiniServer _server = null;
|
|
Socket _socket = null;
|
|
Stream _stream;
|
|
NetworkStream _netstream;
|
|
SslStream _sslstream;
|
|
IPEndPoint _local;
|
|
IPEndPoint _remote;
|
|
|
|
/// <summary>服务器。</summary>
|
|
public MiniServer Server { get => _server; }
|
|
|
|
/// <summary>本地网络终结点。</summary>
|
|
public IPEndPoint LocalEndPoint { get => _local; }
|
|
|
|
/// <summary>远端网络终结点。</summary>
|
|
public IPEndPoint RemoteEndPoint { get => _remote; }
|
|
|
|
internal Stream Stream { get => _stream; }
|
|
|
|
void CloseSocket()
|
|
{
|
|
Timeout();
|
|
|
|
if (_sslstream != null)
|
|
{
|
|
try { _sslstream.Close(); } catch { }
|
|
try { _sslstream.Dispose(); } catch { }
|
|
}
|
|
|
|
if (_netstream != null)
|
|
{
|
|
try { _netstream.Close(); } catch { }
|
|
try { _netstream.Dispose(); } catch { }
|
|
}
|
|
|
|
if (_socket != null)
|
|
{
|
|
try { _socket.LingerState = new LingerOption(false, 0); } catch { }
|
|
try { _socket.Shutdown(SocketShutdown.Both); } catch { }
|
|
try { _socket.Disconnect(false); } catch { }
|
|
try { _socket.Close(1); } catch { }
|
|
_socket = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>关闭 Socket 连接。</summary>
|
|
public void Close(bool force = false)
|
|
{
|
|
if (_socket == null) return;
|
|
SendHead();
|
|
SendBody();
|
|
|
|
if (force)
|
|
{
|
|
CloseSocket();
|
|
return;
|
|
}
|
|
|
|
if (!force && !_context.Request.KeepAlive) force = true;
|
|
if (!force && TextUtility.Lower(_context.Response.Headers.GetValue("connection", true)) == "close") force = true;
|
|
if (force)
|
|
{
|
|
CloseSocket();
|
|
return;
|
|
}
|
|
|
|
_reuses++;
|
|
BeginRead();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region request
|
|
|
|
// 发起读取。
|
|
internal void BeginRead()
|
|
{
|
|
// reset
|
|
_context = new MiniContext(this);
|
|
_head = new ArrayBuilder<byte>(BufferSize);
|
|
if (_buffer == null) _buffer = new byte[BufferSize];
|
|
_instream = null;
|
|
_outstream = null;
|
|
_headsent = false;
|
|
_bodysent = false;
|
|
_forceClose = false;
|
|
_chunked = _server.Chunked;
|
|
_headread = 0;
|
|
|
|
try
|
|
{
|
|
Timeout(RequestHeadTimeout);
|
|
if (_server.SynchronousIO)
|
|
{
|
|
SyncRead();
|
|
ProcessHead();
|
|
}
|
|
else
|
|
{
|
|
_stream.BeginRead(_buffer, 0, BufferSize, ar2 => ((MiniConnection)ar2.AsyncState).ProcessHead(ar2), this);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
Timeout();
|
|
CloseSocket();
|
|
}
|
|
}
|
|
|
|
void SyncRead()
|
|
{
|
|
while (true)
|
|
{
|
|
var nread = _stream.Read(_buffer, 0, BufferSize);
|
|
if (nread > 0) _head.Add(_buffer, 0, nread);
|
|
else throw new Exception("未接收到完整的请求头。");
|
|
|
|
var head = ReadHead();
|
|
if (head) return;
|
|
|
|
if (_head.Count > HeadMax)
|
|
{
|
|
Send(400);
|
|
Close(true);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 读取头,返回标记表示已经完成头的读取。
|
|
bool ReadHead()
|
|
{
|
|
var count = _head.Count;
|
|
if (count < 5) return false;
|
|
|
|
var array = _head.Origin;
|
|
var end = count - 3;
|
|
if (_headread > 3) _headread -= 3;
|
|
for (var i = _headread; i < end; i++)
|
|
{
|
|
if (array[i] != 13) continue;
|
|
if (array[i + 1] != 10) continue;
|
|
if (array[i + 2] != 13) continue;
|
|
if (array[i + 3] != 10) continue;
|
|
|
|
// head
|
|
var head = new byte[i];
|
|
Buffer.BlockCopy(array, 0, head, 0, i);
|
|
var text = Encoding.ASCII.GetString(head);
|
|
var lines = text.Split('\r', '\n');
|
|
var first = true;
|
|
foreach (var line in lines)
|
|
{
|
|
if (line.IsEmpty()) continue;
|
|
if (first)
|
|
{
|
|
var split = line.Split(' ');
|
|
var segs = new List<string>(3);
|
|
foreach (var seg in split)
|
|
{
|
|
if (string.IsNullOrEmpty(seg)) continue;
|
|
segs.Add(seg);
|
|
}
|
|
|
|
// GET /index.html HTTP/1.1
|
|
if (segs.Count != 3)
|
|
{
|
|
Send(400);
|
|
Close(true);
|
|
throw new Exception("请求的报文无效。");
|
|
}
|
|
|
|
_context.Request.Method = split[0];
|
|
_context.Request.Path = split[1];
|
|
_context.Request.Version = split[2];
|
|
}
|
|
else
|
|
{
|
|
var colon = line.IndexOf(":");
|
|
if (colon > 1)
|
|
{
|
|
var name = TextUtility.Trim(line.Substring(0, colon));
|
|
var value = TextUtility.Trim(line.Substring(colon + 1));
|
|
if (!string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(value))
|
|
{
|
|
_context.Request.Headers.Add(name, value);
|
|
}
|
|
}
|
|
}
|
|
first = false;
|
|
}
|
|
|
|
// body
|
|
var remains = count - i - 4;
|
|
var reader = GetRequestStream();
|
|
if (remains > 0)
|
|
{
|
|
var contentLength = _context.Request.ContentLength;
|
|
if (contentLength > 0)
|
|
{
|
|
var body = new byte[remains];
|
|
Buffer.BlockCopy(array, i + 4, body, 0, remains);
|
|
reader.RemainsBytes = body;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ProcessHead(IAsyncResult ar)
|
|
{
|
|
if (_socket == null) return;
|
|
|
|
// 读取头。
|
|
while (true)
|
|
{
|
|
int nread = -1;
|
|
try
|
|
{
|
|
nread = _stream.EndRead(ar);
|
|
}
|
|
catch
|
|
{
|
|
// 读取失败,发送 400,并关闭 Socket。
|
|
Send(400);
|
|
Close(true);
|
|
return;
|
|
}
|
|
|
|
// 未读取到内容,表示已完成。
|
|
if (nread < 1) break;
|
|
|
|
// 将读取的数据加入缓冲。
|
|
_head.Add(_buffer, 0, nread);
|
|
|
|
// 缓冲区容量已溢出。
|
|
if (_head.Length > HeadMax)
|
|
{
|
|
Send(400);
|
|
Close(true);
|
|
return;
|
|
}
|
|
|
|
// 继续读取。
|
|
if (nread < BufferSize) break;
|
|
_stream.BeginRead(_buffer, 0, BufferSize, ar2 => ((MiniConnection)ar2.AsyncState).ProcessHead(ar2), this);
|
|
return;
|
|
}
|
|
|
|
ProcessHead();
|
|
}
|
|
|
|
void ProcessHead()
|
|
{
|
|
// 处理过程中禁用计时器。
|
|
Timeout();
|
|
|
|
// 检查读取的数据。
|
|
if (_head.Count < 1)
|
|
{
|
|
Close(true);
|
|
return;
|
|
}
|
|
|
|
// Expect: 100-continue
|
|
|
|
// 解析头。
|
|
var bytes = _head.Export();
|
|
|
|
|
|
// 检查 HTTP 方法。
|
|
var method = ParseMethod(_context.Request.Method);
|
|
if (method == HttpMethod.NULL)
|
|
{
|
|
Send(405);
|
|
Close(true);
|
|
return;
|
|
}
|
|
|
|
// 检查 HTTP 协议版本。
|
|
var version = TextUtility.Upper(_context.Request.Version);
|
|
switch (version)
|
|
{
|
|
case "HTTP/1":
|
|
case "HTTP/1.0":
|
|
_context.Request.Http11 = false;
|
|
break;
|
|
case "HTTP/1.1":
|
|
_context.Request.Http11 = true;
|
|
break;
|
|
default:
|
|
Send(505);
|
|
Close(true);
|
|
return;
|
|
}
|
|
|
|
// 保持连接。
|
|
_context.Request.KeepAlive = _context.Request.Http11 && TextUtility.Lower(_context.Request.Headers.GetValue("Connection")) == "keep-alive";
|
|
|
|
// 启用压缩。
|
|
if (_server.Compression)
|
|
{
|
|
var headerValue = _context.Request.Headers.GetValue("Accept-Encoding");
|
|
if (!string.IsNullOrEmpty(headerValue))
|
|
{
|
|
var split = headerValue.ToLower().Split(',');
|
|
foreach (var seg in split)
|
|
{
|
|
switch (seg)
|
|
{
|
|
case "gzip": _context.Request.Gzip = true; break;
|
|
case "brotli": _context.Request.Brotli = true; break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// URL
|
|
var host = _context.Request.Headers.GetValue("Host");
|
|
var port = 0;
|
|
var local = LocalEndPoint;
|
|
if (local != null)
|
|
{
|
|
if (string.IsNullOrEmpty(host)) host = local.Address.ToString();
|
|
port = local.Port;
|
|
}
|
|
_context.Request.Url = new Uri($"http://{host}{_context.Request.Path}");
|
|
|
|
// Handler
|
|
var handler = _server.Handler;
|
|
if (handler == null)
|
|
{
|
|
Send(501);
|
|
Close(true);
|
|
return;
|
|
}
|
|
|
|
// Invoke
|
|
Timeout();
|
|
handler.Invoke(_context);
|
|
SendHead();
|
|
SendBody();
|
|
}
|
|
|
|
internal void SendContinue()
|
|
{
|
|
Timeout(ResponseHeadTimeout);
|
|
var http11 = _context.Request.Http11;
|
|
var text = http11 ? "HTTP/1.1 100 Continue\r\n\r\n" : "HTTP/1.0 100 Continue\r\n\r\n";
|
|
var bytes = Encoding.ASCII.GetBytes(text);
|
|
_socket.Send(bytes);
|
|
}
|
|
|
|
internal MiniReader GetRequestStream()
|
|
{
|
|
if (_instream != null) return _instream;
|
|
|
|
var headers = _context.Request.Headers;
|
|
var length = -1L;
|
|
var value = headers.GetValue("Content-Length");
|
|
if (!string.IsNullOrEmpty(value))
|
|
{
|
|
var num = value.Int64();
|
|
if (num.ToString() == value) length = num;
|
|
}
|
|
_instream = new MiniReader(this, length);
|
|
|
|
Timeout(RequestBodyTimeout);
|
|
return _instream;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region response
|
|
|
|
/// <summary>对响应体分块。</summary>
|
|
public bool Chunked { get => _chunked && _context.Request.Http11; }
|
|
|
|
internal void SendHead()
|
|
{
|
|
if (_headsent) return; else _headsent = true;
|
|
_instream?.Close();
|
|
|
|
var lines = new ArrayBuilder<string>(32);
|
|
var response = _context.Response;
|
|
var headers = _context.Response.Headers;
|
|
|
|
// redirect
|
|
var location = response.Location;
|
|
if (string.IsNullOrEmpty(location)) location = null;
|
|
|
|
// version
|
|
var isHttp11 = _context.Request.Http11;
|
|
var version = isHttp11 ? "1.1" : "1.0";
|
|
|
|
// status
|
|
var status = response.Status;
|
|
if (status == 0) status = 200;
|
|
if (location != null) status = 302;
|
|
var statusDesc = NetworkUtility.HttpStatusDescription(status);
|
|
if (statusDesc.IsEmpty()) statusDesc = "OK";
|
|
lines.Add($"HTTP/{version} {status} {statusDesc}");
|
|
|
|
// keep-alive
|
|
var forceClose = true;
|
|
var keepAlive = false;
|
|
if (isHttp11)
|
|
{
|
|
if (status == 400 || status == 408 || status == 411 || status == 413 || status == 414 || status == 500 || status == 503) forceClose = true;
|
|
if (_reuses > 128) forceClose = true;
|
|
|
|
// request: keep-alive
|
|
keepAlive = _context.Request.KeepAlive;
|
|
if (!keepAlive) forceClose = true;
|
|
|
|
// response: keep-alive
|
|
if (!_context.Response.KeepAlive) forceClose = true;
|
|
|
|
if (keepAlive)
|
|
{
|
|
if (forceClose)
|
|
{
|
|
lines.Add("Connection: close");
|
|
}
|
|
else
|
|
{
|
|
lines.Add("Connection: keep-alive");
|
|
lines.Add($"Keep-Alive: timeout=15, max={128 - _reuses}");
|
|
}
|
|
}
|
|
if (Chunked) lines.Add("Transfer-Encoding: chunked");
|
|
}
|
|
|
|
// redirect
|
|
if (location != null) lines.Add($"Location: {location}");
|
|
|
|
// content-type
|
|
var contentType = response.ContentType;
|
|
if (string.IsNullOrEmpty(contentType)) contentType = headers.GetValue("content-type", true);
|
|
if (!string.IsNullOrEmpty(contentType)) lines.Add("Content-Type:" + contentType);
|
|
|
|
// content-length
|
|
var contentLength = response.ContentLength;
|
|
if (contentLength >= 0) lines.Add("Content-length:" + contentLength);
|
|
|
|
// 自定义头
|
|
foreach (var header in headers)
|
|
{
|
|
var key = header.Key;
|
|
if (key.IsEmpty()) continue;
|
|
|
|
var lower = key.Lower();
|
|
if (lower == "content-length") continue;
|
|
if (lower == "content-type") continue;
|
|
if (keepAlive)
|
|
{
|
|
if (lower == "connection") continue;
|
|
if (lower == "keep-alive") continue;
|
|
}
|
|
if (location == null)
|
|
{
|
|
if (lower == "location") continue;
|
|
}
|
|
|
|
var value = header.Value;
|
|
if (value.IsEmpty()) continue;
|
|
|
|
lines.Add(key + ":" + value);
|
|
}
|
|
|
|
// send
|
|
Timeout(ResponseHeadTimeout);
|
|
var text = string.Join("\r\n", lines.Export());
|
|
text += "\r\n\r\n";
|
|
var bytes = TextUtility.Bytes(text);
|
|
_socket?.Send(bytes);
|
|
_forceClose = forceClose;
|
|
}
|
|
|
|
internal void SendBody()
|
|
{
|
|
if (_bodysent) return; else _bodysent = true;
|
|
_outstream?.Close();
|
|
Close(_forceClose);
|
|
}
|
|
|
|
internal MiniWriter GetResponseStream()
|
|
{
|
|
if (_outstream != null) return _outstream;
|
|
|
|
SendHead();
|
|
_outstream = new MiniWriter(this);
|
|
|
|
Timeout(ResponseBodyTimeout);
|
|
return _outstream;
|
|
}
|
|
|
|
/// <summary>发送状态码,并关闭 Socket 连接。</summary>
|
|
internal void Send(int status)
|
|
{
|
|
var response = _context.Response;
|
|
response.Status = status;
|
|
response.Close();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region static
|
|
|
|
static HttpMethod ParseMethod(string method)
|
|
{
|
|
if (!string.IsNullOrEmpty(method))
|
|
{
|
|
var upper = TextUtility.Upper(method);
|
|
if (upper.Contains("OPTIONS")) return HttpMethod.OPTIONS;
|
|
else if (upper.Contains("POST")) return HttpMethod.POST;
|
|
else if (upper.Contains("GET")) return HttpMethod.GET;
|
|
else if (upper.Contains("CONNECT")) return HttpMethod.CONNECT;
|
|
else if (upper.Contains("DELETE")) return HttpMethod.DELETE;
|
|
else if (upper.Contains("HEAD")) return HttpMethod.HEAD;
|
|
else if (upper.Contains("PATCH")) return HttpMethod.PATCH;
|
|
else if (upper.Contains("PUT")) return HttpMethod.PUT;
|
|
else if (upper.Contains("TRACE")) return HttpMethod.TRACE;
|
|
}
|
|
return HttpMethod.NULL;
|
|
}
|
|
|
|
static SslStream CreateSslStream(Stream stream, X509Certificate certificate, SslProtocols protocols)
|
|
{
|
|
var sslStream = new SslStream(stream, false, (t, c, ch, e) =>
|
|
{
|
|
if (c == null) return true;
|
|
var c2 = c as X509Certificate2;
|
|
if (c2 == null) c2 = new X509Certificate2(c.GetRawCertData());
|
|
return true;
|
|
});
|
|
sslStream.AuthenticateAsServer(certificate, true, protocols, false);
|
|
return sslStream;
|
|
}
|
|
|
|
/// <summary>证书验证。忽略所有错误。</summary>
|
|
static bool ApproveAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/// <summary>证书验证。</summary>
|
|
static X509Certificate ApproveFirst(object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers)
|
|
{
|
|
if (localCertificates != null)
|
|
{
|
|
for (var i = 0; i < localCertificates.Count; i++)
|
|
{
|
|
var certificate = localCertificates[i];
|
|
if (certificate != null) return certificate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static long GetContentLength(StringPairs headers)
|
|
{
|
|
if (headers != null)
|
|
{
|
|
var value = headers.GetValue("Content-Length", true);
|
|
if (!string.IsNullOrEmpty(value))
|
|
{
|
|
var num = value.Int64();
|
|
if (num.ToString() == value) return num;
|
|
}
|
|
}
|
|
return -1L;
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
|
|
}
|
|
|