1 // Copyright (c) 2015 Kazuhiro Fujieda <fujieda@users.osdn.me>
\r
3 // Licensed under the Apache License, Version 2.0 (the "License");
\r
4 // you may not use this file except in compliance with the License.
\r
5 // You may obtain a copy of the License at
\r
7 // http://www.apache.org/licenses/LICENSE-2.0
\r
9 // Unless required by applicable law or agreed to in writing, software
\r
10 // distributed under the License is distributed on an "AS IS" BASIS,
\r
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\r
12 // See the License for the specific language governing permissions and
\r
13 // limitations under the License.
\r
16 using System.Globalization;
\r
18 using System.IO.Compression;
\r
20 using System.Net.Sockets;
\r
22 using System.Text.RegularExpressions;
\r
23 using System.Threading.Tasks;
\r
25 namespace KancolleSniffer
\r
27 public class HttpProxy
\r
29 private static HttpProxy _httpProxy;
\r
30 public static int LocalPort { get; set; }
\r
31 public static string UpstreamProxyHost { get; set; }
\r
32 public static int UpstreamProxyPort { get; set; }
\r
33 public static bool IsEnableUpstreamProxy { get; set; }
\r
34 public static bool IsInListening { get; private set; }
\r
35 public static event Action<Session> AfterSessionComplete;
\r
37 private TcpListener _listener;
\r
39 public static void Startup(int port, bool dummy0, bool dummy1)
\r
42 _httpProxy = new HttpProxy();
\r
48 _listener = new TcpListener(IPAddress.Loopback, LocalPort);
\r
50 LocalPort = ((IPEndPoint)_listener.LocalEndpoint).Port;
\r
51 IsInListening = true;
\r
52 Task.Run(() => AcceptClient());
\r
55 public static void Shutdown()
\r
62 IsInListening = false;
\r
63 _listener.Server.Close();
\r
67 public void AcceptClient()
\r
73 var client = _listener.AcceptSocket();
\r
74 Task.Run(() => new HttpClient(client).ProcessRequest());
\r
77 catch (SocketException)
\r
86 private class HttpClient
\r
88 private readonly Socket _client;
\r
89 private Socket _server;
\r
90 private readonly Session _session;
\r
91 private readonly HttpStream _clientStream;
\r
92 private HttpStream _serverStream;
\r
94 public HttpClient(Socket client)
\r
97 _clientStream = new HttpStream(client);
\r
98 _session = new Session();
\r
101 public void ProcessRequest()
\r
106 if (_session.Request.Method == null)
\r
108 if (_session.Request.Method == "CONNECT")
\r
113 if (_session.Request.Host.StartsWith("localhost"))
\r
115 LogServer.Process(_client, _session.Request.RequestLine);
\r
119 ReceiveRequestBody();
\r
124 AfterSessionComplete?.Invoke(_session);
\r
126 catch (Exception e)
\r
128 File.AppendAllText("debug.log", $"[{DateTime.Now:g}] " + e + "\r\n");
\r
136 private void ReceiveRequest()
\r
138 var requestLine = _clientStream.ReadLine();
\r
139 if (requestLine == "")
\r
141 _session.Request.RequestLine = requestLine;
\r
142 _session.Request.Headers = _clientStream.ReadHeaders();
\r
145 private void ReceiveRequestBody()
\r
147 if (_session.Request.ContentLength != -1 || _session.Request.TransferEncoding != null)
\r
148 _session.Request.ReadBody(_clientStream);
\r
151 private void SendRequest()
\r
153 _server = ConnectServer();
\r
154 _serverStream = new HttpStream(_server).
\r
155 WriteLines(_session.Request.RequestLine + _session.Request.ModifiedHeaders);
\r
158 private void SendRequestBody()
\r
160 _serverStream.Write(_session.Request.Body);
\r
163 private void ReceiveResponse()
\r
165 _session.Response.StatusLine = _serverStream.ReadLine();
\r
166 _session.Response.Headers = _serverStream.ReadHeaders();
\r
168 _session.Response.ReadBody(_serverStream);
\r
171 private bool HasBody
\r
175 var code = _session.Response.StatusCode;
\r
176 return (!(_session.Request.Method == "HEAD" ||
\r
177 code.StartsWith("1") || code == "204" || code == "304"));
\r
181 private void SendResponse()
\r
183 _clientStream.WriteLines(_session.Response.StatusLine + _session.Response.ModifiedHeaders)
\r
184 .Write(_session.Response.Body);
\r
187 private void HandleConnect()
\r
191 if (!ParseAuthority(_session.Request.PathAndQuery, ref host, ref port))
\r
193 _server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
\r
194 _server.Connect(host, port);
\r
195 _clientStream.WriteLines("HTTP/1.0 200 Connection established\r\n\r\n");
\r
198 Task.Run(() => { TunnnelSockets(_client, _server); }),
\r
199 Task.Run(() => { TunnnelSockets(_server, _client); })
\r
201 Task.WaitAll(tasks);
\r
204 private void TunnnelSockets(Socket from, Socket to)
\r
208 var buf = new byte[8192];
\r
211 var n = from.Receive(buf);
\r
214 var sent = to.Send(buf, n, SocketFlags.None);
\r
218 to.Shutdown(SocketShutdown.Send);
\r
220 catch (SocketException)
\r
225 private static readonly Regex HostAndPortRegex =
\r
226 new Regex("http://([^:/]+)(?::(\\d+))?/", RegexOptions.Compiled);
\r
228 private Socket ConnectServer()
\r
230 string host = null;
\r
232 if (IsEnableUpstreamProxy)
\r
234 host = UpstreamProxyHost;
\r
235 port = UpstreamProxyPort;
\r
238 var m = HostAndPortRegex.Match(_session.Request.RequestLine);
\r
241 host = m.Groups[1].Value;
\r
242 if (m.Groups[2].Success)
\r
243 port = int.Parse(m.Groups[2].Value);
\r
244 _session.Request.RequestLine = _session.Request.RequestLine.Remove(m.Index, m.Length - 1);
\r
246 if (host == null && !ParseAuthority(_session.Request.Host, ref host, ref port))
\r
247 throw new HttpProxyAbort("Can't find destination host");
\r
249 var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
\r
250 socket.Connect(host, port);
\r
254 private static readonly Regex AuthorityRegex = new Regex("([^:]+)(?::(\\d+))?");
\r
256 private bool ParseAuthority(string authority, ref string host, ref int port)
\r
258 if (string.IsNullOrEmpty(authority))
\r
260 var m = AuthorityRegex.Match(authority);
\r
263 host = m.Groups[1].Value;
\r
264 if (m.Groups[2].Success)
\r
265 port = int.Parse(m.Groups[2].Value);
\r
269 private void Close()
\r
271 _serverStream?.Close();
\r
272 _clientStream?.Close();
\r
278 public class Session
\r
280 public Request Request { get; set; } = new Request();
\r
281 public Response Response { get; set; } = new Response();
\r
284 public class Message
\r
286 private string _headers;
\r
287 public byte[] Body { get; set; }
\r
289 private static readonly Regex CharsetRegx = new Regex("charset=([\\w-]+)",
\r
290 RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
\r
292 public int ContentLength { get; set; } = -1;
\r
293 public string TransferEncoding { get; set; }
\r
294 public string ContentType { get; set; }
\r
295 public string ContentEncoding { get; set; }
\r
296 public string Host { get; set; }
\r
298 public string Headers
\r
304 SetHeaders(_headers);
\r
308 public virtual string ModifiedHeaders => SetConnectionClose(Headers);
\r
310 private string SetConnectionClose(string headers)
\r
312 return InsertHeader(RemoveHeaders(headers,
\r
313 new[] {"connection", "keep-alive", "proxy-connection"}), "Connection: close\r\n");
\r
316 protected string RemoveHeaders(string headers, string[] fields)
\r
318 foreach (var f in fields)
\r
320 var m = MatchField(f, headers);
\r
323 headers = headers.Remove(m.Index, m.Length);
\r
328 protected string InsertHeader(string headers, string header)
\r
330 return headers.Insert(headers.Length - 2, header);
\r
333 protected virtual void SetHeaders(string headers)
\r
335 var s = GetField("content-length");
\r
338 ContentLength = int.TryParse(s, out var len) ? len : -1;
\r
340 TransferEncoding = GetField("transfer-encoding")?.ToLower(CultureInfo.InvariantCulture);
\r
341 ContentType = GetField("content-type");
\r
342 ContentEncoding = GetField("content-encoding");
\r
343 Host = GetField("host");
\r
346 protected Match MatchField(string name, string headers)
\r
348 var regex = new Regex("^" + name + ":\\s*([^\r]+)\r\n",
\r
349 RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Multiline);
\r
350 return regex.Match(headers);
\r
353 protected string GetField(string name)
\r
355 var m = MatchField(name, Headers);
\r
356 return m.Success ? m.Groups[1].Value : null;
\r
359 public string BodyAsString
\r
365 var m = CharsetRegx.Match(ContentType ?? "");
\r
366 var encoding = Encoding.ASCII;
\r
369 var name = m.Groups[1].Value;
\r
370 if (name == "utf8")
\r
372 encoding = Encoding.GetEncoding(name);
\r
374 return encoding.GetString(Body);
\r
378 public void ReadBody(HttpStream stream)
\r
380 if (TransferEncoding != null && TransferEncoding.Contains("chunked"))
\r
382 Body = stream.ReadChunked();
\r
384 else if (ContentLength == 0)
\r
387 else if (ContentLength > 0)
\r
389 var buf = new byte[ContentLength];
\r
390 stream.Read(buf, 0, ContentLength);
\r
395 Body = stream.ReadToEnd();
\r
397 if (ContentEncoding == null)
\r
399 var dc = new MemoryStream();
\r
402 if (ContentEncoding == "gzip")
\r
403 new GZipStream(new MemoryStream(Body), CompressionMode.Decompress).CopyTo(dc);
\r
404 else if (ContentEncoding == "deflate")
\r
405 new DeflateStream(new MemoryStream(Body), CompressionMode.Decompress).CopyTo(dc);
\r
407 catch (Exception ex)
\r
409 throw new HttpProxyAbort($"Fail to decode {ContentEncoding}: " + ex.Message);
\r
411 Body = dc.ToArray();
\r
415 public class Request : Message
\r
417 private string _requestLine;
\r
419 public string RequestLine
\r
421 get => _requestLine;
\r
424 _requestLine = value;
\r
425 var f = _requestLine.Split(' ');
\r
427 throw new HttpProxyAbort("Invalid request line");
\r
429 PathAndQuery = f.Length < 2 ? "" : f[1];
\r
433 public string Method { get; private set; }
\r
434 public string PathAndQuery { get; private set; }
\r
437 public class Response : Message
\r
439 private string _statusLine;
\r
441 public override string ModifiedHeaders =>
\r
442 InsertContentLength(RemoveHeaders(base.ModifiedHeaders,
\r
443 new[] {"transfer-encoding", "content-encoding", "content-length"}));
\r
445 private string InsertContentLength(string headers)
\r
447 return Body == null ? headers : InsertHeader(headers, $"Content-Length: {Body.Length}\r\n");
\r
450 public string StatusLine
\r
452 get => _statusLine;
\r
455 _statusLine = value;
\r
456 var f = _statusLine.Split(' ');
\r
458 throw new HttpProxyAbort("Invalid status line");
\r
459 StatusCode = _statusLine.Split(' ')[1];
\r
463 public string StatusCode { get; private set; }
\r
466 private class HttpProxyAbort : Exception
\r
468 public HttpProxyAbort(string message) : base(message)
\r
473 public class HttpStream
\r
475 private readonly Socket _socket;
\r
476 private readonly byte[] _buffer = new byte[4096];
\r
477 private int _available;
\r
478 private int _position;
\r
480 public HttpStream(Socket socket)
\r
483 socket.NoDelay = true;
\r
486 public string ReadLine()
\r
488 var sb = new StringBuilder();
\r
490 while ((ch = ReadByte()) != -1)
\r
492 sb.Append((char)ch);
\r
496 return sb.ToString();
\r
499 private int ReadByte()
\r
501 if (_position < _available)
\r
502 return _buffer[_position++];
\r
503 _available = _socket.Receive(_buffer, 0, _buffer.Length, SocketFlags.None);
\r
505 return _available == 0 ? -1 : _buffer[_position++];
\r
508 public HttpStream WriteLines(string s)
\r
510 var buf = Encoding.ASCII.GetBytes(s);
\r
511 Write(buf, 0, buf.Length);
\r
515 public string ReadHeaders()
\r
517 var sb = new StringBuilder();
\r
523 } while (line != "\r\n");
\r
524 return sb.ToString();
\r
527 public byte[] ReadChunked()
\r
529 var buf = new MemoryStream();
\r
532 var size = ReadLine();
\r
533 if (size.Length < 3)
\r
535 var ext = size.IndexOf(';');
\r
536 size = ext == -1 ? size.Substring(0, size.Length - 2) : size.Substring(0, ext);
\r
537 if (!int.TryParse(size, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var val))
\r
538 throw new HttpProxyAbort("Can't parse chunk size: " + size);
\r
541 var chunk = new byte[val];
\r
542 Read(chunk, 0, chunk.Length);
\r
543 buf.Write(chunk, 0, chunk.Length);
\r
550 } while (line != "" && line != "\r\n");
\r
551 return buf.ToArray();
\r
554 public byte[] ReadToEnd()
\r
556 var result = new MemoryStream();
\r
557 var buf = new byte[4096];
\r
559 while ((len = Read(buf, 0, buf.Length)) > 0)
\r
560 result.Write(buf, 0, len);
\r
561 return result.ToArray();
\r
564 public HttpStream Write(byte[] body)
\r
567 Write(body, 0, body.Length);
\r
571 public int Read(byte[] buf, int offset, int count)
\r
577 if (_position < _available)
\r
579 n = Math.Min(count, _available - _position);
\r
580 Buffer.BlockCopy(_buffer, _position, buf, 0, n);
\r
585 n = _socket.Receive(buf, offset, count, SocketFlags.None);
\r
587 return total == 0 ? n : total;
\r
592 } while (count > 0);
\r
596 public void Write(byte[] buf, int offset, int count)
\r
600 var n = _socket.Send(buf, offset, count, SocketFlags.None);
\r
605 } while (count > 0);
\r
608 public HttpStream Close()
\r