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") || _session.Request.Host.StartsWith("127.0.0.1"))
\r
115 LogServer.Process(_client, _session.Request.RequestLine);
\r
119 ReceiveRequestBody();
\r
122 if (_session.Response.StatusCode == null)
\r
124 AfterSessionComplete?.Invoke(_session);
\r
128 catch (Exception e)
\r
130 File.AppendAllText("debug.log", $"[{DateTime.Now:g}] " + e + "\r\n");
\r
132 #else // ReSharper disable once EmptyGeneralCatchClause
\r
143 private void ReceiveRequest()
\r
145 var requestLine = _clientStream.ReadLine();
\r
146 if (requestLine == "")
\r
148 _session.Request.RequestLine = requestLine;
\r
149 _session.Request.Headers = _clientStream.ReadHeaders();
\r
152 private void ReceiveRequestBody()
\r
154 if (_session.Request.ContentLength != -1 || _session.Request.TransferEncoding != null)
\r
155 _session.Request.ReadBody(_clientStream);
\r
158 private void SendRequest()
\r
160 _server = ConnectServer();
\r
162 new HttpStream(_server).WriteLines(_session.Request.RequestLine + _session.Request.ModifiedHeaders);
\r
165 private void SendRequestBody()
\r
167 _serverStream.Write(_session.Request.Body);
\r
170 private void ReceiveResponse()
\r
172 var statusLine = _serverStream.ReadLine();
\r
173 if (statusLine == "")
\r
175 _session.Response.StatusLine = statusLine;
\r
176 _session.Response.Headers = _serverStream.ReadHeaders();
\r
178 _session.Response.ReadBody(_serverStream);
\r
181 private bool HasBody
\r
185 var code = _session.Response.StatusCode;
\r
186 return (!(_session.Request.Method == "HEAD" ||
\r
187 code.StartsWith("1") || code == "204" || code == "304"));
\r
191 private void SendResponse()
\r
193 _clientStream.WriteLines(_session.Response.StatusLine + _session.Response.ModifiedHeaders)
\r
194 .Write(_session.Response.Body);
\r
197 private void HandleConnect()
\r
201 if (!ParseAuthority(_session.Request.PathAndQuery, ref host, ref port))
\r
203 _server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
\r
204 _server.Connect(host, port);
\r
205 _clientStream.WriteLines("HTTP/1.0 200 Connection established\r\n\r\n");
\r
208 Task.Run(() => { TunnnelSockets(_client, _server); }),
\r
209 Task.Run(() => { TunnnelSockets(_server, _client); })
\r
211 Task.WaitAll(tasks);
\r
214 private void TunnnelSockets(Socket from, Socket to)
\r
218 var buf = new byte[8192];
\r
221 var n = from.Receive(buf);
\r
224 var sent = to.Send(buf, n, SocketFlags.None);
\r
228 to.Shutdown(SocketShutdown.Send);
\r
230 catch (SocketException)
\r
235 private static readonly Regex HostAndPortRegex =
\r
236 new Regex("http://([^:/]+)(?::(\\d+))?/", RegexOptions.Compiled);
\r
238 private Socket ConnectServer()
\r
240 string host = null;
\r
242 if (IsEnableUpstreamProxy)
\r
244 host = UpstreamProxyHost;
\r
245 port = UpstreamProxyPort;
\r
248 var m = HostAndPortRegex.Match(_session.Request.RequestLine);
\r
251 host = m.Groups[1].Value;
\r
252 if (m.Groups[2].Success)
\r
253 port = int.Parse(m.Groups[2].Value);
\r
254 _session.Request.RequestLine = _session.Request.RequestLine.Remove(m.Index, m.Length - 1);
\r
256 if (host == null && !ParseAuthority(_session.Request.Host, ref host, ref port))
\r
257 throw new HttpProxyAbort("Can't find destination host");
\r
259 var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
\r
260 socket.Connect(host, port);
\r
264 private static readonly Regex AuthorityRegex = new Regex("([^:]+)(?::(\\d+))?");
\r
266 private bool ParseAuthority(string authority, ref string host, ref int port)
\r
268 if (string.IsNullOrEmpty(authority))
\r
270 var m = AuthorityRegex.Match(authority);
\r
273 host = m.Groups[1].Value;
\r
274 if (m.Groups[2].Success)
\r
275 port = int.Parse(m.Groups[2].Value);
\r
279 private void Close()
\r
281 SocketClose(_server);
\r
282 SocketClose(_client);
\r
285 private void SocketClose(Socket socket)
\r
287 if (socket == null)
\r
291 socket.Shutdown(SocketShutdown.Both);
\r
293 // ReSharper disable EmptyGeneralCatchClause
\r
303 // ReSharper restore EmptyGeneralCatchClause
\r
309 public class Session
\r
311 public Request Request { get; set; } = new Request();
\r
312 public Response Response { get; set; } = new Response();
\r
315 public class Message
\r
317 private string _headers;
\r
318 public byte[] Body { get; set; }
\r
320 private static readonly Regex CharsetRegx = new Regex("charset=([\\w-]+)",
\r
321 RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
\r
323 public int ContentLength { get; set; } = -1;
\r
324 public string TransferEncoding { get; set; }
\r
325 public string ContentType { get; set; }
\r
326 public string ContentEncoding { get; set; }
\r
327 public string Host { get; set; }
\r
329 public string Headers
\r
335 SetHeaders(_headers);
\r
339 public virtual string ModifiedHeaders => SetConnectionClose(Headers);
\r
341 private string SetConnectionClose(string headers)
\r
343 return InsertHeader(RemoveHeaders(headers,
\r
344 new[] {"connection", "keep-alive", "proxy-connection"}), "Connection: close\r\n");
\r
347 protected string RemoveHeaders(string headers, string[] fields)
\r
349 foreach (var f in fields)
\r
351 var m = MatchField(f, headers);
\r
354 headers = headers.Remove(m.Index, m.Length);
\r
359 protected string InsertHeader(string headers, string header)
\r
361 return headers.Insert(headers.Length - 2, header);
\r
364 protected virtual void SetHeaders(string headers)
\r
366 var s = GetField("content-length");
\r
369 ContentLength = int.TryParse(s, out var len) ? len : -1;
\r
371 TransferEncoding = GetField("transfer-encoding")?.ToLower(CultureInfo.InvariantCulture);
\r
372 ContentType = GetField("content-type");
\r
373 ContentEncoding = GetField("content-encoding");
\r
374 Host = GetField("host");
\r
377 protected Match MatchField(string name, string headers)
\r
379 var regex = new Regex("^" + name + ":\\s*([^\r]+)\r\n",
\r
380 RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Multiline);
\r
381 return regex.Match(headers);
\r
384 protected string GetField(string name)
\r
386 var m = MatchField(name, Headers);
\r
387 return m.Success ? m.Groups[1].Value : null;
\r
390 public string BodyAsString
\r
396 var m = CharsetRegx.Match(ContentType ?? "");
\r
397 var encoding = Encoding.ASCII;
\r
400 var name = m.Groups[1].Value;
\r
401 if (name == "utf8")
\r
403 encoding = Encoding.GetEncoding(name);
\r
405 return encoding.GetString(Body);
\r
409 public void ReadBody(HttpStream stream)
\r
411 if (TransferEncoding != null && TransferEncoding.Contains("chunked"))
\r
413 Body = stream.ReadChunked();
\r
415 else if (ContentLength == 0)
\r
418 else if (ContentLength > 0)
\r
420 var buf = new byte[ContentLength];
\r
421 stream.Read(buf, 0, ContentLength);
\r
426 Body = stream.ReadToEnd();
\r
428 if (ContentEncoding == null)
\r
430 var dc = new MemoryStream();
\r
433 if (ContentEncoding == "gzip")
\r
434 new GZipStream(new MemoryStream(Body), CompressionMode.Decompress).CopyTo(dc);
\r
435 else if (ContentEncoding == "deflate")
\r
436 new DeflateStream(new MemoryStream(Body), CompressionMode.Decompress).CopyTo(dc);
\r
438 catch (Exception ex)
\r
440 throw new HttpProxyAbort($"Fail to decode {ContentEncoding}: " + ex.Message);
\r
442 Body = dc.ToArray();
\r
446 public class Request : Message
\r
448 private string _requestLine;
\r
450 public string RequestLine
\r
452 get => _requestLine;
\r
455 _requestLine = value;
\r
456 var f = _requestLine.Split(' ');
\r
458 throw new HttpProxyAbort("Invalid request line");
\r
460 PathAndQuery = f.Length < 2 ? "" : f[1];
\r
464 public string Method { get; private set; }
\r
465 public string PathAndQuery { get; private set; }
\r
468 public class Response : Message
\r
470 private string _statusLine;
\r
472 public override string ModifiedHeaders =>
\r
473 InsertContentLength(RemoveHeaders(base.ModifiedHeaders,
\r
474 new[] {"transfer-encoding", "content-encoding", "content-length"}));
\r
476 private string InsertContentLength(string headers)
\r
478 return Body == null ? headers : InsertHeader(headers, $"Content-Length: {Body.Length}\r\n");
\r
481 public string StatusLine
\r
483 get => _statusLine;
\r
486 _statusLine = value;
\r
487 var f = _statusLine.Split(' ');
\r
489 throw new HttpProxyAbort("Invalid status line");
\r
490 StatusCode = _statusLine.Split(' ')[1];
\r
494 public string StatusCode { get; private set; }
\r
497 private class HttpProxyAbort : Exception
\r
499 public HttpProxyAbort(string message) : base(message)
\r
504 public class HttpStream
\r
506 private readonly Socket _socket;
\r
507 private readonly byte[] _buffer = new byte[4096];
\r
508 private int _available;
\r
509 private int _position;
\r
511 public HttpStream(Socket socket)
\r
514 socket.NoDelay = true;
\r
517 public string ReadLine()
\r
519 var sb = new StringBuilder();
\r
521 while ((ch = ReadByte()) != -1)
\r
523 sb.Append((char)ch);
\r
527 return sb.ToString();
\r
530 private int ReadByte()
\r
532 if (_position < _available)
\r
533 return _buffer[_position++];
\r
534 _available = _socket.Receive(_buffer, 0, _buffer.Length, SocketFlags.None);
\r
536 return _available == 0 ? -1 : _buffer[_position++];
\r
539 public HttpStream WriteLines(string s)
\r
541 var buf = Encoding.ASCII.GetBytes(s);
\r
542 Write(buf, 0, buf.Length);
\r
546 public string ReadHeaders()
\r
548 var sb = new StringBuilder();
\r
554 } while (line != "\r\n");
\r
555 return sb.ToString();
\r
558 public byte[] ReadChunked()
\r
560 var buf = new MemoryStream();
\r
563 var size = ReadLine();
\r
564 if (size.Length < 3)
\r
566 var ext = size.IndexOf(';');
\r
567 size = ext == -1 ? size.Substring(0, size.Length - 2) : size.Substring(0, ext);
\r
568 if (!int.TryParse(size, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var val))
\r
569 throw new HttpProxyAbort("Can't parse chunk size: " + size);
\r
572 var chunk = new byte[val];
\r
573 Read(chunk, 0, chunk.Length);
\r
574 buf.Write(chunk, 0, chunk.Length);
\r
581 } while (line != "" && line != "\r\n");
\r
582 return buf.ToArray();
\r
585 public byte[] ReadToEnd()
\r
587 var result = new MemoryStream();
\r
588 var buf = new byte[4096];
\r
590 while ((len = Read(buf, 0, buf.Length)) > 0)
\r
591 result.Write(buf, 0, len);
\r
592 return result.ToArray();
\r
595 public HttpStream Write(byte[] body)
\r
598 Write(body, 0, body.Length);
\r
602 public int Read(byte[] buf, int offset, int count)
\r
608 if (_position < _available)
\r
610 n = Math.Min(count, _available - _position);
\r
611 Buffer.BlockCopy(_buffer, _position, buf, 0, n);
\r
616 n = _socket.Receive(buf, offset, count, SocketFlags.None);
\r
618 return total == 0 ? n : total;
\r
623 } while (count > 0);
\r
627 public void Write(byte[] buf, int offset, int count)
\r
631 var n = _socket.Send(buf, offset, count, SocketFlags.None);
\r
636 } while (count > 0);
\r