OSDN Git Service

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