-
Notifications
You must be signed in to change notification settings - Fork 18
/
HttpRequestProcessor.cs
269 lines (250 loc) · 9.28 KB
/
HttpRequestProcessor.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using static WebOne.HttpUtil;
using static WebOne.Program;
namespace WebOne
{
/// <summary>
/// Parser of raw HTTP traffic, which decodes, process, and codes back HTTP's bytes.
/// </summary>
class HttpRequestProcessor
{
/// <summary>
/// Initialize instance of raw HTTP traffic parser (processor).
/// </summary>
public HttpRequestProcessor() { }
/// <summary>
/// Process incoming TCP/IP traffic from client.
/// </summary>
/// <param name="Backend">TcpClient used to communicate with client.</param>
/// <param name="Logger">Log writer.</param>
public void ProcessClientRequest(TcpClient Backend, LogWriter Logger)
{
ProcessClientRequest(Backend as object, Logger);
}
/// <summary>
/// Process incoming TCP/IP traffic from client.
/// </summary>
/// <param name="Backend">HttpUtil.SslClient used to communicate with client.</param>
/// <param name="Logger">Log writer.</param>
/// <param name="SslLogPrefix">Prefix to be shown in log entries.</param>
public void ProcessClientRequest(SslClient Backend, LogWriter Logger, string SslLogPrefix)
{
ProcessClientRequest(Backend as object, Logger, SslLogPrefix);
}
/// <summary>
/// Process incoming TCP/IP traffic from client.
/// </summary>
/// <param name="Backend">HttpListenerRequest, TcpClient or HttpUtil.SslClient used to communicate with client.</param>
/// <param name="Logger">Log writer.</param>
/// <param name="SslLogPrefix">Used only on HTTPS requests. Prefix to be shown in log entries.</param>
private void ProcessClientRequest(object Backend, LogWriter Logger, string SslLogPrefix = "SSL")
{
#if DEBUG
if (Backend is SslClient)
Logger.WriteLine("Got a secure request.");
else
Logger.WriteLine("Got a request.");
#endif
// Prepare data stream
Stream ClientStream;
switch (Backend)
{
case TcpClient tcpc:
ClientStream = tcpc.GetStream();
break;
case SslClient sslc:
ClientStream = sslc.Stream;
break;
default:
throw new ArgumentException("Incorrect backend.", nameof(Backend));
}
// Read text part of HTTP request (until double line feed).
BinaryReader br = new(ClientStream);
List<char> rqChars = new();
while (true)
{
rqChars.Add(br.ReadChar());
if (rqChars.Count < 2) continue;
if (rqChars[rqChars.Count - 1] == '\r')
{
if (rqChars[rqChars.Count - 3] == '\r' && rqChars[rqChars.Count - 2] == '\n')
{
rqChars.Add(br.ReadChar());
break;
}
}
}
// Process HTTP command and headers.
HttpRequest Request = null;
bool IsCommand = true;
foreach (string HttpRequestLine in new string(rqChars.ToArray()).Split("\r\n"))
{
if (string.IsNullOrWhiteSpace(HttpRequestLine)) continue;
if (IsCommand)
{
// First line - HTTP command.
if (string.IsNullOrEmpty(HttpRequestLine))
{
Logger.WriteLine("<Close empty connection.");
return;
}
string[] HttpCommandParts = HttpRequestLine.Split(' ');
if (HttpRequestLine.StartsWith("CONNECT") && HttpRequestLine.Contains('\n'))
{
//fix "Dropped: Non-HTTP connection: CONNECT www.sannata.org:443 HTTP/1.0User-Agent: Mozilla/3.04Gold (WinNT; U)"
string HttpRequestLineNetscapeBug = HttpRequestLine.Substring(0, HttpRequestLine.IndexOf('\n'));
HttpCommandParts = HttpRequestLineNetscapeBug.Split(' ');
}
else if (HttpCommandParts.Length != 3 || HttpCommandParts[2].Length != 8)
{
if (Backend is SslClient)
{
// Non-HTTP protocol inside SSL tunnel.
// This is used by MSN Messenger, IRCS, POP3-SSL, SMTP-SSL, IMAP-SSL and other apps supporting HTTPS-proxies.
Logger.WriteLine("<Dropped: Non-HTTPS connection: {0}", HttpRequestLine);
throw new Exception("Write your domain:port to webone.conf/[NonHttpSslServers], please.");
}
else
{
Logger.WriteLine("<Dropped: Non-HTTP connection: {0}", HttpRequestLine);
return;
}
}
// First line is valid, start work with the Request.
Request = new()
{
HttpMethod = HttpCommandParts[0],
RawUrl = HttpCommandParts[1],
ProtocolVersionString = HttpCommandParts[2],
Headers = new(),
IsSecureConnection = Backend is SslClient
};
// Define Client & Proxy IP addresses
if (Backend is HttpListenerRequest hlr)
{
Request.RemoteEndPoint = hlr.RemoteEndPoint;
Request.LocalEndPoint = hlr.LocalEndPoint;
}
else if (Backend is TcpClient tcpc)
{
Request.RemoteEndPoint = tcpc.Client.RemoteEndPoint as IPEndPoint;
Request.LocalEndPoint = tcpc.Client.LocalEndPoint as IPEndPoint;
}
else if (Backend is SslClient sslc)
{
Request.RemoteEndPoint = sslc.RemoteEndPoint;
Request.LocalEndPoint = sslc.LocalEndPoint;
}
// Okay, ready to parse headers.
IsCommand = false;
continue;
}
else
{
// Other lines - request headers, load all of them.
if (string.IsNullOrWhiteSpace(HttpRequestLine)) continue;
string HeaderName = HttpRequestLine.Substring(0, HttpRequestLine.IndexOf(": "));
string HeaderValue = HttpRequestLine.Substring(HttpRequestLine.IndexOf(": ") + 2);
if (HeaderName.Contains('\n')) HeaderName = HeaderName.Substring(0, HeaderName.IndexOf('\n'));
if (HeaderValue.Contains('\n')) HeaderValue = HeaderValue.Substring(0, HeaderValue.IndexOf('\n'));
Request.Headers.Add(HeaderName, HeaderValue);
// The '\n' character removing is need because of Arachne 1.97 bug.
}
}
if (Request == null)
{
Logger.WriteLine("<Dropped (unknown HTTP derivative).");
return;
}
if (Request.RawUrl.StartsWith("ftp:/") && !Request.RawUrl.StartsWith("ftp://"))
{
Logger.WriteLine("<Dropped (bad FTP protocol address)."); //IBM WebExplorer bug
return;
}
// Define URI from HTTP Command and HTTP Host header.
RequestKind Kind = GetKindOfRequest(Request.RawUrl, Request.Headers["Host"], null, Request.HttpMethod == "CONNECT");
Request.Kind = Kind;
string Host = Request.Headers["Host"] ?? Variables["Proxy"];
switch (Kind)
{
case RequestKind.StandardProxy:
Request.Url = new Uri(Request.RawUrl);
break;
case RequestKind.StandardHttp:
case RequestKind.StandardRemote:
try { Request.Url = new Uri("http://" + Host + Request.RawUrl); }
catch { Request.Url = new Uri(Request.RawUrl); }
break;
case RequestKind.AlternateProxy:
string url = Request.RawUrl[1..];
if (url.Contains(":/") && !url.Contains("://")) url = url.Replace(":/", "://");
Request.Url = new Uri(url);
break;
case RequestKind.StandardSslProxy:
Request.Url = null;
break;
}
// Configure content transfer stream
if (Request.Headers["Content-Length"] != null && Request.Headers["Content-Length"] != "0")
{
// If there's a payload, convert it to a HttpRequestContentStream.
Request.InputStream = new HttpRequestContentStream(ClientStream, int.Parse(Request.Headers["Content-Length"]));
/*
* NetworkStream/SslStream is not suitable for HTTP request bodies. It have no length, and read operation is endless.
* What is suitable - .NET's internal HttpRequestStream and ChunkedInputStream:HttpRequestStream.
* See .NET source: https://source.dot.net/System.Net.HttpListener/R/d562e26091bc9f8d.html
* They are reading traffic only until HTTP Content-Length or last HTTP Chunk into a correct .NET Stream format.
*
* WebOne.HttpRequestContentStream is a very lightweight alternative to System.Net.HttpRequestStream.
*/
}
else if (false)
{
// Will be used in future for chunked payload transfer.
// See note above.
}
else
{
// No payload in request - original NetworkStream is suitable.
Request.InputStream = ClientStream;
}
// Configure persistent connection mode (a.k.a. Keep-Alive)
if ((Request.Headers["Connection"] ?? "").ToLower().Contains("keep-alive") ||
(Request.Headers["Proxy-Connection"] ?? "").ToLower().Contains("keep-alive"))
{ Request.KeepAlive = true; }
// Ready to start HTTP transit process.
HttpResponse Response;
if (Backend is HttpListenerResponse) Response = new(Backend as HttpListenerResponse);
else if (Backend is TcpClient) Response = new((TcpClient)Backend);
else if (Backend is SslClient) Response = new(((SslClient)Backend).Stream);
else throw new ArgumentException("Incorrect backend.", nameof(Backend));
HttpTransit Transit = new(Request, Response, Logger);
if (Backend is SslClient)
Logger.WriteLine(">[{3}] {0} {1} ({2})", Request.HttpMethod, Request.RawUrl, Transit.GetClientIdString(), SslLogPrefix);
else
Logger.WriteLine(">{0} {1} ({2})", Request.HttpMethod, Request.RawUrl, Transit.GetClientIdString());
Transit.ProcessTransit();
// Restart processing if the connection is persistent. Or exit if not.
if (Request.KeepAlive && Response.KeepAlive)
{
if (Backend is TcpClient tcpb) if (!tcpb.Connected)
{
Logger.WriteLine("<Done (connection closed by client).");
return;
}
Logger.WriteLine("<Done.");
ProcessClientRequest(Backend, new(), Request.Headers["Host"] ?? "Keep-Alive, no Host");
return;
}
else
{
Logger.WriteLine("<Done (connection close).");
return;
}
}
}
}