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
126 AfterSessionComplete?.Invoke(_session);
\r
129 catch (Exception e)
\r
131 File.AppendAllText("debug.log", $"[{DateTime.Now:g}] " + e + "\r\n");
\r
133 #else // ReSharper disable once EmptyGeneralCatchClause
\r
144 private void ReceiveRequest()
\r
146 var requestLine = _clientStream.ReadLine();
\r
147 if (requestLine == "")
\r
149 _session.Request.RequestLine = requestLine;
\r
150 _session.Request.Headers = _clientStream.ReadHeaders();
\r
153 private void ReceiveRequestBody()
\r
155 if (_session.Request.ContentLength != -1 || _session.Request.TransferEncoding != null)
\r
156 _session.Request.ReadBody(_clientStream);
\r
159 private void SendRequest()
\r
161 _server = ConnectServer();
\r
162 _serverStream = new HttpStream(_server).
\r
163 WriteLines(_session.Request.RequestLine + _session.Request.ModifiedHeaders);
\r
166 private void SendRequestBody()
\r
168 _serverStream.Write(_session.Request.Body);
\r
171 private void ReceiveResponse()
\r
173 var statusLine = _serverStream.ReadLine();
\r
174 if (statusLine == "")
\r
176 _session.Response.StatusLine = statusLine;
\r
177 _session.Response.Headers = _serverStream.ReadHeaders();
\r
179 _session.Response.ReadBody(_serverStream);
\r
182 private bool HasBody
\r
186 var code = _session.Response.StatusCode;
\r
187 return (!(_session.Request.Method == "HEAD" ||
\r
188 code.StartsWith("1") || code == "204" || code == "304"));
\r
192 private void SendResponse()
\r
194 _clientStream.WriteLines(_session.Response.StatusLine + _session.Response.ModifiedHeaders)
\r
195 .Write(_session.Response.Body);
\r
198 private void HandleConnect()
\r
202 if (!ParseAuthority(_session.Request.PathAndQuery, ref host, ref port))
\r
204 _server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
\r
205 _server.Connect(host, port);
\r
206 _clientStream.WriteLines("HTTP/1.0 200 Connection established\r\n\r\n");
\r
209 Task.Run(() => { TunnnelSockets(_client, _server); }),
\r
210 Task.Run(() => { TunnnelSockets(_server, _client); })
\r
212 Task.WaitAll(tasks);
\r
215 private void TunnnelSockets(Socket from, Socket to)
\r
219 var buf = new byte[8192];
\r
222 var n = from.Receive(buf);
\r
225 var sent = to.Send(buf, n, SocketFlags.None);
\r
229 to.Shutdown(SocketShutdown.Send);
\r
231 catch (SocketException)
\r
236 private static readonly Regex HostAndPortRegex =
\r
237 new Regex("http://([^:/]+)(?::(\\d+))?/", RegexOptions.Compiled);
\r
239 private Socket ConnectServer()
\r
241 string host = null;
\r
243 if (IsEnableUpstreamProxy)
\r
245 host = UpstreamProxyHost;
\r
246 port = UpstreamProxyPort;
\r
249 var m = HostAndPortRegex.Match(_session.Request.RequestLine);
\r
252 host = m.Groups[1].Value;
\r
253 if (m.Groups[2].Success)
\r
254 port = int.Parse(m.Groups[2].Value);
\r
255 _session.Request.RequestLine = _session.Request.RequestLine.Remove(m.Index, m.Length - 1);
\r
257 if (host == null && !ParseAuthority(_session.Request.Host, ref host, ref port))
\r
258 throw new HttpProxyAbort("Can't find destination host");
\r
260 var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
\r
261 socket.Connect(host, port);
\r
265 private static readonly Regex AuthorityRegex = new Regex("([^:]+)(?::(\\d+))?");
\r
267 private bool ParseAuthority(string authority, ref string host, ref int port)
\r
269 if (string.IsNullOrEmpty(authority))
\r
271 var m = AuthorityRegex.Match(authority);
\r
274 host = m.Groups[1].Value;
\r
275 if (m.Groups[2].Success)
\r
276 port = int.Parse(m.Groups[2].Value);
\r
280 private void Close()
\r
282 _serverStream?.Close();
\r
283 _clientStream?.Close();
\r
289 public class Session
\r
291 public Request Request { get; set; } = new Request();
\r
292 public Response Response { get; set; } = new Response();
\r
295 public class Message
\r
297 private string _headers;
\r
298 public byte[] Body { get; set; }
\r
300 private static readonly Regex CharsetRegx = new Regex("charset=([\\w-]+)",
\r
301 RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
\r
303 public int ContentLength { get; set; } = -1;
\r
304 public string TransferEncoding { get; set; }
\r
305 public string ContentType { get; set; }
\r
306 public string ContentEncoding { get; set; }
\r
307 public string Host { get; set; }
\r
309 public string Headers
\r
315 SetHeaders(_headers);
\r
319 public virtual string ModifiedHeaders => SetConnectionClose(Headers);
\r
321 private string SetConnectionClose(string headers)
\r
323 return InsertHeader(RemoveHeaders(headers,
\r
324 new[] {"connection", "keep-alive", "proxy-connection"}), "Connection: close\r\n");
\r
327 protected string RemoveHeaders(string headers, string[] fields)
\r
329 foreach (var f in fields)
\r
331 var m = MatchField(f, headers);
\r
334 headers = headers.Remove(m.Index, m.Length);
\r
339 protected string InsertHeader(string headers, string header)
\r
341 return headers.Insert(headers.Length - 2, header);
\r
344 protected virtual void SetHeaders(string headers)
\r
346 var s = GetField("content-length");
\r
349 ContentLength = int.TryParse(s, out var len) ? len : -1;
\r
351 TransferEncoding = GetField("transfer-encoding")?.ToLower(CultureInfo.InvariantCulture);
\r
352 ContentType = GetField("content-type");
\r
353 ContentEncoding = GetField("content-encoding");
\r
354 Host = GetField("host");
\r
357 protected Match MatchField(string name, string headers)
\r
359 var regex = new Regex("^" + name + ":\\s*([^\r]+)\r\n",
\r
360 RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Multiline);
\r
361 return regex.Match(headers);
\r
364 protected string GetField(string name)
\r
366 var m = MatchField(name, Headers);
\r
367 return m.Success ? m.Groups[1].Value : null;
\r
370 public string BodyAsString
\r
376 var m = CharsetRegx.Match(ContentType ?? "");
\r
377 var encoding = Encoding.ASCII;
\r
380 var name = m.Groups[1].Value;
\r
381 if (name == "utf8")
\r
383 encoding = Encoding.GetEncoding(name);
\r
385 return encoding.GetString(Body);
\r
389 public void ReadBody(HttpStream stream)
\r
391 if (TransferEncoding != null && TransferEncoding.Contains("chunked"))
\r
393 Body = stream.ReadChunked();
\r
395 else if (ContentLength == 0)
\r
398 else if (ContentLength > 0)
\r
400 var buf = new byte[ContentLength];
\r
401 stream.Read(buf, 0, ContentLength);
\r
406 Body = stream.ReadToEnd();
\r
408 if (ContentEncoding == null)
\r
410 var dc = new MemoryStream();
\r
413 if (ContentEncoding == "gzip")
\r
414 new GZipStream(new MemoryStream(Body), CompressionMode.Decompress).CopyTo(dc);
\r
415 else if (ContentEncoding == "deflate")
\r
416 new DeflateStream(new MemoryStream(Body), CompressionMode.Decompress).CopyTo(dc);
\r
418 catch (Exception ex)
\r
420 throw new HttpProxyAbort($"Fail to decode {ContentEncoding}: " + ex.Message);
\r
422 Body = dc.ToArray();
\r
426 public class Request : Message
\r
428 private string _requestLine;
\r
430 public string RequestLine
\r
432 get => _requestLine;
\r
435 _requestLine = value;
\r
436 var f = _requestLine.Split(' ');
\r
438 throw new HttpProxyAbort("Invalid request line");
\r
440 PathAndQuery = f.Length < 2 ? "" : f[1];
\r
444 public string Method { get; private set; }
\r
445 public string PathAndQuery { get; private set; }
\r
448 public class Response : Message
\r
450 private string _statusLine;
\r
452 public override string ModifiedHeaders =>
\r
453 InsertContentLength(RemoveHeaders(base.ModifiedHeaders,
\r
454 new[] {"transfer-encoding", "content-encoding", "content-length"}));
\r
456 private string InsertContentLength(string headers)
\r
458 return Body == null ? headers : InsertHeader(headers, $"Content-Length: {Body.Length}\r\n");
\r
461 public string StatusLine
\r
463 get => _statusLine;
\r
466 _statusLine = value;
\r
467 var f = _statusLine.Split(' ');
\r
469 throw new HttpProxyAbort("Invalid status line");
\r
470 StatusCode = _statusLine.Split(' ')[1];
\r
474 public string StatusCode { get; private set; }
\r
477 private class HttpProxyAbort : Exception
\r
479 public HttpProxyAbort(string message) : base(message)
\r
484 public class HttpStream
\r
486 private readonly Socket _socket;
\r
487 private readonly byte[] _buffer = new byte[4096];
\r
488 private int _available;
\r
489 private int _position;
\r
491 public HttpStream(Socket socket)
\r
494 socket.NoDelay = true;
\r
497 public string ReadLine()
\r
499 var sb = new StringBuilder();
\r
501 while ((ch = ReadByte()) != -1)
\r
503 sb.Append((char)ch);
\r
507 return sb.ToString();
\r
510 private int ReadByte()
\r
512 if (_position < _available)
\r
513 return _buffer[_position++];
\r
514 _available = _socket.Receive(_buffer, 0, _buffer.Length, SocketFlags.None);
\r
516 return _available == 0 ? -1 : _buffer[_position++];
\r
519 public HttpStream WriteLines(string s)
\r
521 var buf = Encoding.ASCII.GetBytes(s);
\r
522 Write(buf, 0, buf.Length);
\r
526 public string ReadHeaders()
\r
528 var sb = new StringBuilder();
\r
534 } while (line != "\r\n");
\r
535 return sb.ToString();
\r
538 public byte[] ReadChunked()
\r
540 var buf = new MemoryStream();
\r
543 var size = ReadLine();
\r
544 if (size.Length < 3)
\r
546 var ext = size.IndexOf(';');
\r
547 size = ext == -1 ? size.Substring(0, size.Length - 2) : size.Substring(0, ext);
\r
548 if (!int.TryParse(size, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var val))
\r
549 throw new HttpProxyAbort("Can't parse chunk size: " + size);
\r
552 var chunk = new byte[val];
\r
553 Read(chunk, 0, chunk.Length);
\r
554 buf.Write(chunk, 0, chunk.Length);
\r
561 } while (line != "" && line != "\r\n");
\r
562 return buf.ToArray();
\r
565 public byte[] ReadToEnd()
\r
567 var result = new MemoryStream();
\r
568 var buf = new byte[4096];
\r
570 while ((len = Read(buf, 0, buf.Length)) > 0)
\r
571 result.Write(buf, 0, len);
\r
572 return result.ToArray();
\r
575 public HttpStream Write(byte[] body)
\r
578 Write(body, 0, body.Length);
\r
582 public int Read(byte[] buf, int offset, int count)
\r
588 if (_position < _available)
\r
590 n = Math.Min(count, _available - _position);
\r
591 Buffer.BlockCopy(_buffer, _position, buf, 0, n);
\r
596 n = _socket.Receive(buf, offset, count, SocketFlags.None);
\r
598 return total == 0 ? n : total;
\r
603 } while (count > 0);
\r
607 public void Write(byte[] buf, int offset, int count)
\r
611 var n = _socket.Send(buf, offset, count, SocketFlags.None);
\r
616 } while (count > 0);
\r
619 public HttpStream Close()
\r