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