OSDN Git Service

debug.logの競合状態を解消する
[kancollesniffer/KancolleSniffer.git] / KancolleSniffer / Net / HttpProxy.cs
1 // Copyright (c) 2015 Kazuhiro Fujieda <fujieda@users.osdn.me>\r
2 //\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
6 //\r
7 //    http://www.apache.org/licenses/LICENSE-2.0\r
8 //\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
14 \r
15 using System;\r
16 using System.Collections;\r
17 using System.Globalization;\r
18 using System.IO;\r
19 using System.IO.Compression;\r
20 using System.Net;\r
21 using System.Net.Sockets;\r
22 using System.Text;\r
23 using System.Text.RegularExpressions;\r
24 using System.Threading.Tasks;\r
25 \r
26 namespace KancolleSniffer.Net\r
27 {\r
28     public class HttpProxy\r
29     {\r
30         private static HttpProxy _httpProxy;\r
31         public static int LocalPort { get; set; }\r
32         public static string UpstreamProxyHost { get; set; }\r
33         public static int UpstreamProxyPort { get; set; }\r
34         public static bool IsEnableUpstreamProxy { get; set; }\r
35         public static bool IsInListening { get; private set; }\r
36         public static event Action<Session> AfterSessionComplete;\r
37 \r
38         private TcpListener _listener;\r
39 #if DEBUG\r
40         private static readonly object SyncObj = new object();\r
41 #endif\r
42         public static void Startup(int port, bool dummy0, bool dummy1)\r
43         {\r
44             LocalPort = port;\r
45             _httpProxy = new HttpProxy();\r
46             _httpProxy.Start();\r
47         }\r
48 \r
49         public void Start()\r
50         {\r
51             _listener = new TcpListener(IPAddress.Loopback, LocalPort);\r
52             _listener.Start();\r
53             LocalPort = ((IPEndPoint)_listener.LocalEndpoint).Port;\r
54             IsInListening = true;\r
55             Task.Run(AcceptClient);\r
56         }\r
57 \r
58         public static void Shutdown()\r
59         {\r
60             _httpProxy?.Stop();\r
61         }\r
62 \r
63         public void Stop()\r
64         {\r
65             IsInListening = false;\r
66             _listener.Server.Close();\r
67             _listener.Stop();\r
68         }\r
69 \r
70         public void AcceptClient()\r
71         {\r
72             try\r
73             {\r
74                 while (true)\r
75                 {\r
76                     var client = _listener.AcceptSocket();\r
77                     Task.Run(() => new HttpClient(client).ProcessRequest());\r
78                 }\r
79             }\r
80             catch (SocketException)\r
81             {\r
82             }\r
83             finally\r
84             {\r
85                 Stop();\r
86             }\r
87         }\r
88 \r
89         private class HttpClient\r
90         {\r
91             private readonly Socket _client;\r
92             private Socket _server;\r
93             private string _host;\r
94             private int _port;\r
95             private Session _session;\r
96             private HttpStream _clientStream;\r
97             private HttpStream _serverStream;\r
98 \r
99             public HttpClient(Socket client)\r
100             {\r
101                 _client = client;\r
102             }\r
103 \r
104             public void ProcessRequest()\r
105             {\r
106                 try\r
107                 {\r
108                     do\r
109                     {\r
110                         _clientStream = new HttpStream(_client);\r
111                         _session = new Session();\r
112                         if (CheckServerTimeOut())\r
113                             return;\r
114                         ReceiveRequest();\r
115                         if (_session.Request.Method == null)\r
116                             return;\r
117                         if (_session.Request.Method == "CONNECT")\r
118                         {\r
119                             HandleConnect();\r
120                             return;\r
121                         }\r
122                         if (_session.Request.Host.StartsWith("localhost") ||\r
123                             _session.Request.Host.StartsWith("127.0.0.1"))\r
124                         {\r
125                             LogServer.Process(_client, _session.Request.RequestLine);\r
126                             return;\r
127                         }\r
128                         SendRequest();\r
129                         ReceiveRequestBody();\r
130                         SendRequestBody();\r
131                         ReceiveResponse();\r
132                         if (_session.Response.StatusCode == null)\r
133                             return;\r
134                         AfterSessionComplete?.Invoke(_session);\r
135                         SendResponse();\r
136                     } while (_client.Connected && _server.Connected &&\r
137                              _session.Request.IsKeepAlive && _session.Response.IsKeepAlive);\r
138                 }\r
139 #if DEBUG\r
140                 catch (Exception e)\r
141                 {\r
142                     lock (SyncObj)\r
143                         File.AppendAllText("debug.log", $"[{DateTime.Now:g}] " + e + "\r\n");\r
144                 }\r
145 #else // ReSharper disable once EmptyGeneralCatchClause\r
146                 catch\r
147                 {\r
148                 }\r
149 #endif\r
150                 finally\r
151                 {\r
152                     Close();\r
153                 }\r
154             }\r
155 \r
156             private bool CheckServerTimeOut()\r
157             {\r
158                 if (_server == null)\r
159                     return false;\r
160                 var readList = new ArrayList {_client, _server};\r
161                 // ReSharper disable once AssignNullToNotNullAttribute\r
162                 Socket.Select(readList, null, null, -1);\r
163                 return readList.Count == 1 && readList[0] == _server && _server.Available == 0;\r
164             }\r
165 \r
166             private void ReceiveRequest()\r
167             {\r
168                 var requestLine = _clientStream.ReadLine();\r
169                 if (requestLine == "")\r
170                     return;\r
171                 _session.Request.RequestLine = requestLine;\r
172                 _session.Request.Headers = _clientStream.ReadHeaders();\r
173             }\r
174 \r
175             private void ReceiveRequestBody()\r
176             {\r
177                 if (_session.Request.ContentLength != -1 || _session.Request.TransferEncoding != null)\r
178                     _session.Request.ReadBody(_clientStream);\r
179             }\r
180 \r
181             private void SendRequest()\r
182             {\r
183                 GetHostAndPort(out var host, out var port);\r
184                 if (_server == null || host != _host || port != _port || IsSocketDead(_server))\r
185                 {\r
186                     SocketClose(_server);\r
187                     _server = ConnectServer(host, port);\r
188                     _host = host;\r
189                     _port = port;\r
190                 }\r
191                 _serverStream =\r
192                     new HttpStream(_server).WriteLines(_session.Request.RequestLine + _session.Request.ModifiedHeaders);\r
193             }\r
194 \r
195             private Socket ConnectServer(string host, int port)\r
196             {\r
197                 var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);\r
198                 socket.Connect(host, port);\r
199                 return socket;\r
200             }\r
201 \r
202             private void GetHostAndPort(out string host, out int port)\r
203             {\r
204                 if (IsEnableUpstreamProxy)\r
205                 {\r
206                     host = UpstreamProxyHost;\r
207                     port = UpstreamProxyPort;\r
208                 }\r
209                 else\r
210                 {\r
211                     MakeRequestUrlRelative(out host, out port);\r
212                     if (host == null && !ParseAuthority(_session.Request.Host, ref host, ref port))\r
213                         throw new HttpProxyAbort("Can't find destination host");\r
214                 }\r
215             }\r
216 \r
217             private static readonly Regex HostAndPortRegex =\r
218                 new Regex("http://([^:/]+)(?::(\\d+))?/", RegexOptions.Compiled);\r
219 \r
220             private void MakeRequestUrlRelative(out string host, out int port)\r
221             {\r
222                 host = null;\r
223                 port = 80;\r
224                 var m = HostAndPortRegex.Match(_session.Request.RequestLine);\r
225                 if (!m.Success)\r
226                     return;\r
227                 host = m.Groups[1].Value;\r
228                 if (m.Groups[2].Success)\r
229                     port = int.Parse(m.Groups[2].Value);\r
230                 _session.Request.RequestLine = _session.Request.RequestLine.Remove(m.Index, m.Length - 1);\r
231             }\r
232 \r
233             bool IsSocketDead(Socket s) => (s.Poll(1000, SelectMode.SelectRead) && s.Available == 0) || !s.Connected;\r
234 \r
235             private void SendRequestBody()\r
236             {\r
237                 _serverStream.Write(_session.Request.Body);\r
238             }\r
239 \r
240             private void ReceiveResponse()\r
241             {\r
242                 var statusLine = _serverStream.ReadLine();\r
243                 if (statusLine == "")\r
244                     return;\r
245                 _session.Response.StatusLine = statusLine;\r
246                 _session.Response.Headers = _serverStream.ReadHeaders();\r
247                 if (HasBody)\r
248                     _session.Response.ReadBody(_serverStream);\r
249             }\r
250 \r
251             private bool HasBody\r
252             {\r
253                 get\r
254                 {\r
255                     var code = _session.Response.StatusCode;\r
256                     return (!(_session.Request.Method == "HEAD" ||\r
257                               code.StartsWith("1") || code == "204" || code == "304"));\r
258                 }\r
259             }\r
260 \r
261             private void SendResponse()\r
262             {\r
263                 _clientStream.WriteLines(_session.Response.StatusLine + _session.Response.ModifiedHeaders)\r
264                     .Write(_session.Response.Body);\r
265             }\r
266 \r
267             private void HandleConnect()\r
268             {\r
269                 var host = "";\r
270                 var port = 443;\r
271                 if (!ParseAuthority(_session.Request.PathAndQuery, ref host, ref port))\r
272                     return;\r
273                 _server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);\r
274                 _server.Connect(host, port);\r
275                 _clientStream.WriteLines("HTTP/1.0 200 Connection established\r\n\r\n");\r
276                 Task[] tasks =\r
277                 {\r
278                     Task.Run(() => { TunnelSockets(_client, _server); }),\r
279                     Task.Run(() => { TunnelSockets(_server, _client); })\r
280                 };\r
281                 Task.WaitAll(tasks);\r
282             }\r
283 \r
284             private void TunnelSockets(Socket from, Socket to)\r
285             {\r
286                 try\r
287                 {\r
288                     var buf = new byte[8192];\r
289                     while (true)\r
290                     {\r
291                         var n = from.Receive(buf);\r
292                         if (n == 0)\r
293                             break;\r
294                         var sent = to.Send(buf, n, SocketFlags.None);\r
295                         if (sent < n)\r
296                             break;\r
297                     }\r
298                     to.Shutdown(SocketShutdown.Send);\r
299                 }\r
300                 catch (SocketException)\r
301                 {\r
302                 }\r
303             }\r
304 \r
305             private static readonly Regex AuthorityRegex = new Regex("([^:]+)(?::(\\d+))?");\r
306 \r
307             private bool ParseAuthority(string authority, ref string host, ref int port)\r
308             {\r
309                 if (string.IsNullOrEmpty(authority))\r
310                     return false;\r
311                 var m = AuthorityRegex.Match(authority);\r
312                 if (!m.Success)\r
313                     return false;\r
314                 host = m.Groups[1].Value;\r
315                 if (m.Groups[2].Success)\r
316                     port = int.Parse(m.Groups[2].Value);\r
317                 return true;\r
318             }\r
319 \r
320             private void Close()\r
321             {\r
322                 SocketClose(_server);\r
323                 SocketClose(_client);\r
324             }\r
325 \r
326             private void SocketClose(Socket socket)\r
327             {\r
328                 if (socket == null)\r
329                     return;\r
330                 try\r
331                 {\r
332                     socket.Shutdown(SocketShutdown.Both);\r
333                 }\r
334                 // ReSharper disable EmptyGeneralCatchClause\r
335                 catch\r
336 \r
337                 {\r
338                 }\r
339                 try\r
340                 {\r
341                     socket.Close();\r
342                 }\r
343                 catch\r
344                     // ReSharper restore EmptyGeneralCatchClause\r
345                 {\r
346                 }\r
347             }\r
348         }\r
349 \r
350         public class Session\r
351         {\r
352             public Request Request { get; set; } = new Request();\r
353             public Response Response { get; set; } = new Response();\r
354         }\r
355 \r
356         public class Message\r
357         {\r
358             private string _headers;\r
359             public byte[] Body { get; set; }\r
360 \r
361             private static readonly Regex CharsetRegex = new Regex("charset=([\\w-]+)",\r
362                 RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);\r
363 \r
364             public int ContentLength { get; set; } = -1;\r
365             public string TransferEncoding { get; set; }\r
366             public string ContentType { get; set; }\r
367             public string ContentEncoding { get; set; }\r
368             public string Host { get; set; }\r
369             public bool IsKeepAlive;\r
370 \r
371             public string Headers\r
372             {\r
373                 get => _headers;\r
374                 set\r
375                 {\r
376                     _headers = value;\r
377                     SetHeaders(_headers);\r
378                 }\r
379             }\r
380 \r
381             public virtual string ModifiedHeaders => RemoveHeaders(Headers, new[] {"proxy-connection"});\r
382 \r
383             protected string RemoveHeaders(string headers, string[] fields)\r
384             {\r
385                 foreach (var f in fields)\r
386                 {\r
387                     var m = MatchField(f, headers);\r
388                     if (!m.Success)\r
389                         continue;\r
390                     headers = headers.Remove(m.Index, m.Length);\r
391                 }\r
392                 return headers;\r
393             }\r
394 \r
395             protected string InsertHeader(string headers, string header)\r
396             {\r
397                 return headers.Insert(headers.Length - 2, header);\r
398             }\r
399 \r
400             protected virtual void SetHeaders(string headers)\r
401             {\r
402                 var s = GetField("content-length");\r
403                 if (s != null)\r
404                 {\r
405                     ContentLength = int.TryParse(s, out var len) ? len : -1;\r
406                 }\r
407                 TransferEncoding = GetField("transfer-encoding")?.ToLower(CultureInfo.InvariantCulture);\r
408                 ContentType = GetField("content-type");\r
409                 ContentEncoding = GetField("content-encoding");\r
410                 Host = GetField("host");\r
411                 IsKeepAlive = GetField("connection")?.ToLower(CultureInfo.InvariantCulture) != "close";\r
412             }\r
413 \r
414             protected Match MatchField(string name, string headers)\r
415             {\r
416                 var regex = new Regex("^" + name + ":\\s*([^\r]+)\r\n",\r
417                     RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Multiline);\r
418                 return regex.Match(headers);\r
419             }\r
420 \r
421             protected string GetField(string name)\r
422             {\r
423                 var m = MatchField(name, Headers);\r
424                 return m.Success ? m.Groups[1].Value : null;\r
425             }\r
426 \r
427             public string BodyAsString\r
428             {\r
429                 get\r
430                 {\r
431                     if (Body == null)\r
432                         return "";\r
433                     var m = CharsetRegex.Match(ContentType ?? "");\r
434                     var encoding = Encoding.ASCII;\r
435                     if (m.Success)\r
436                     {\r
437                         var name = m.Groups[1].Value;\r
438                         if (name == "utf8")\r
439                             name = "UTF-8";\r
440                         encoding = Encoding.GetEncoding(name);\r
441                     }\r
442                     return encoding.GetString(Body);\r
443                 }\r
444             }\r
445 \r
446             public void ReadBody(HttpStream stream)\r
447             {\r
448                 if (TransferEncoding != null && TransferEncoding.Contains("chunked"))\r
449                 {\r
450                     Body = stream.ReadChunked();\r
451                 }\r
452                 else if (ContentLength == 0)\r
453                 {\r
454                 }\r
455                 else if (ContentLength > 0)\r
456                 {\r
457                     var buf = new byte[ContentLength];\r
458                     stream.Read(buf, 0, ContentLength);\r
459                     Body = buf;\r
460                 }\r
461                 else\r
462                 {\r
463                     Body = stream.ReadToEnd();\r
464                 }\r
465                 if (ContentEncoding == null)\r
466                     return;\r
467                 var dc = new MemoryStream();\r
468                 try\r
469                 {\r
470                     if (ContentEncoding == "gzip")\r
471                         new GZipStream(new MemoryStream(Body), CompressionMode.Decompress).CopyTo(dc);\r
472                     else if (ContentEncoding == "deflate")\r
473                         new DeflateStream(new MemoryStream(Body), CompressionMode.Decompress).CopyTo(dc);\r
474                 }\r
475                 catch (Exception ex)\r
476                 {\r
477                     throw new HttpProxyAbort($"Fail to decode {ContentEncoding}: " + ex.Message);\r
478                 }\r
479                 Body = dc.ToArray();\r
480             }\r
481         }\r
482 \r
483         public class Request : Message\r
484         {\r
485             private string _requestLine;\r
486 \r
487             public string RequestLine\r
488             {\r
489                 get => _requestLine;\r
490                 set\r
491                 {\r
492                     _requestLine = value;\r
493                     var f = _requestLine.Split(' ');\r
494                     if (f.Length < 3)\r
495                         throw new HttpProxyAbort("Invalid request line");\r
496                     Method = f[0];\r
497                     PathAndQuery = f.Length < 2 ? "" : f[1];\r
498                 }\r
499             }\r
500 \r
501             public string Method { get; private set; }\r
502             public string PathAndQuery { get; private set; }\r
503         }\r
504 \r
505         public class Response : Message\r
506         {\r
507             private string _statusLine;\r
508 \r
509             public override string ModifiedHeaders =>\r
510                 InsertContentLength(RemoveHeaders(base.ModifiedHeaders,\r
511                     new[] {"transfer-encoding", "content-encoding", "content-length"}));\r
512 \r
513             private string InsertContentLength(string headers)\r
514             {\r
515                 return Body == null ? headers : InsertHeader(headers, $"Content-Length: {Body.Length}\r\n");\r
516             }\r
517 \r
518             public string StatusLine\r
519             {\r
520                 get => _statusLine;\r
521                 set\r
522                 {\r
523                     _statusLine = value;\r
524                     var f = _statusLine.Split(' ');\r
525                     if (f.Length < 3)\r
526                         throw new HttpProxyAbort("Invalid status line");\r
527                     StatusCode = _statusLine.Split(' ')[1];\r
528                 }\r
529             }\r
530 \r
531             public string StatusCode { get; private set; }\r
532         }\r
533 \r
534         private class HttpProxyAbort : Exception\r
535         {\r
536             public HttpProxyAbort(string message) : base(message)\r
537             {\r
538             }\r
539         }\r
540 \r
541         public class HttpStream\r
542         {\r
543             private readonly Socket _socket;\r
544             private readonly byte[] _buffer = new byte[4096];\r
545             private int _available;\r
546             private int _position;\r
547 \r
548             public HttpStream(Socket socket)\r
549             {\r
550                 _socket = socket;\r
551                 socket.NoDelay = true;\r
552             }\r
553 \r
554             public string ReadLine()\r
555             {\r
556                 var sb = new StringBuilder();\r
557                 int ch;\r
558                 while ((ch = ReadByte()) != -1)\r
559                 {\r
560                     sb.Append((char)ch);\r
561                     if (ch == '\n')\r
562                         break;\r
563                 }\r
564                 return sb.ToString();\r
565             }\r
566 \r
567             private int ReadByte()\r
568             {\r
569                 if (_position < _available)\r
570                     return _buffer[_position++];\r
571                 _available = _socket.Receive(_buffer, 0, _buffer.Length, SocketFlags.None);\r
572                 _position = 0;\r
573                 return _available == 0 ? -1 : _buffer[_position++];\r
574             }\r
575 \r
576             public HttpStream WriteLines(string s)\r
577             {\r
578                 var buf = Encoding.ASCII.GetBytes(s);\r
579                 Write(buf, 0, buf.Length);\r
580                 return this;\r
581             }\r
582 \r
583             public string ReadHeaders()\r
584             {\r
585                 var sb = new StringBuilder();\r
586                 string line;\r
587                 do\r
588                 {\r
589                     line = ReadLine();\r
590                     sb.Append(line);\r
591                 } while (line != "\r\n");\r
592                 return sb.ToString();\r
593             }\r
594 \r
595             public byte[] ReadChunked()\r
596             {\r
597                 var buf = new MemoryStream();\r
598                 while (true)\r
599                 {\r
600                     var size = ReadLine();\r
601                     if (size.Length < 3)\r
602                         break;\r
603                     var ext = size.IndexOf(';');\r
604                     size = ext == -1 ? size.Substring(0, size.Length - 2) : size.Substring(0, ext);\r
605                     if (!int.TryParse(size, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var val))\r
606                         throw new HttpProxyAbort("Can't parse chunk size: " + size);\r
607                     if (val == 0)\r
608                         break;\r
609                     var chunk = new byte[val];\r
610                     Read(chunk, 0, chunk.Length);\r
611                     buf.Write(chunk, 0, chunk.Length);\r
612                     ReadLine();\r
613                 }\r
614                 string line;\r
615                 do\r
616                 {\r
617                     line = ReadLine();\r
618                 } while (line != "" && line != "\r\n");\r
619                 return buf.ToArray();\r
620             }\r
621 \r
622             public byte[] ReadToEnd()\r
623             {\r
624                 var result = new MemoryStream();\r
625                 var buf = new byte[4096];\r
626                 int len;\r
627                 while ((len = Read(buf, 0, buf.Length)) > 0)\r
628                     result.Write(buf, 0, len);\r
629                 return result.ToArray();\r
630             }\r
631 \r
632             public HttpStream Write(byte[] body)\r
633             {\r
634                 if (body != null)\r
635                     Write(body, 0, body.Length);\r
636                 return this;\r
637             }\r
638 \r
639             public int Read(byte[] buf, int offset, int count)\r
640             {\r
641                 var total = 0;\r
642                 do\r
643                 {\r
644                     int n;\r
645                     if (_position < _available)\r
646                     {\r
647                         n = Math.Min(count, _available - _position);\r
648                         Buffer.BlockCopy(_buffer, _position, buf, 0, n);\r
649                         _position += n;\r
650                     }\r
651                     else\r
652                     {\r
653                         n = _socket.Receive(buf, offset, count, SocketFlags.None);\r
654                         if (n == 0)\r
655                             return total == 0 ? n : total;\r
656                     }\r
657                     count -= n;\r
658                     offset += n;\r
659                     total += n;\r
660                 } while (count > 0);\r
661                 return total;\r
662             }\r
663 \r
664             public void Write(byte[] buf, int offset, int count)\r
665             {\r
666                 do\r
667                 {\r
668                     var n = _socket.Send(buf, offset, count, SocketFlags.None);\r
669                     if (n == 0)\r
670                         return;\r
671                     count -= n;\r
672                     offset += n;\r
673                 } while (count > 0);\r
674             }\r
675         }\r
676     }\r
677 }