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 == "CONNECT")
\r
112 ReceiveRequestBody();
\r
117 AfterSessionComplete?.Invoke(_session);
\r
119 catch (SocketException)
\r
122 catch (IOException)
\r
125 catch (HttpProxyAbort)
\r
134 private void ReceiveRequest()
\r
136 var requestLine = _clientStream.ReadLine();
\r
137 _session.Request.RequestLine = requestLine;
\r
138 _session.Request.Headers = _clientStream.ReadHeaders();
\r
141 private void ReceiveRequestBody()
\r
143 if (_session.Request.ContentLength != -1 || _session.Request.TransferEncoding != null)
\r
144 _session.Request.ReadBody(_clientStream);
\r
147 private void SendRequest()
\r
149 _server = ConnectServer();
\r
150 _serverStream = new HttpStream(_server).
\r
151 WriteLines(_session.Request.RequestLine + _session.Request.ModifiedHeaders);
\r
154 private void SendRequestBody()
\r
156 _serverStream.Write(_session.Request.Body);
\r
159 private void ReceiveResponse()
\r
161 _session.Response.StatusLine = _serverStream.ReadLine();
\r
162 _session.Response.Headers = _serverStream.ReadHeaders();
\r
164 _session.Response.ReadBody(_serverStream);
\r
167 private bool HasBody
\r
171 var code = _session.Response.StatusCode;
\r
172 return (!(_session.Request.Method == "HEAD" ||
\r
173 code.StartsWith("1") || code == "204" || code == "304"));
\r
177 private void SendResponse()
\r
179 _clientStream.WriteLines(_session.Response.StatusLine + _session.Response.ModifiedHeaders)
\r
180 .Write(_session.Response.Body);
\r
183 private void HandleConnect()
\r
187 if (!ParseAuthority(_session.Request.PathAndQuery, ref host, ref port))
\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
194 Task.Run(() => { TunnnelSockets(_client, _server); }),
\r
195 Task.Run(() => { TunnnelSockets(_server, _client); })
\r
197 Task.WaitAll(tasks);
\r
200 private void TunnnelSockets(Socket from, Socket to)
\r
204 var buf = new byte[8192];
\r
207 var n = from.Receive(buf);
\r
210 var sent = to.Send(buf, n, SocketFlags.None);
\r
214 to.Shutdown(SocketShutdown.Send);
\r
216 catch (SocketException)
\r
221 private static readonly Regex HostAndPortRegex =
\r
222 new Regex("http://([^:/]+)(?::(\\d+))?/", RegexOptions.Compiled);
\r
224 private Socket ConnectServer()
\r
226 string host = null;
\r
228 if (IsEnableUpstreamProxy)
\r
230 host = UpstreamProxyHost;
\r
231 port = UpstreamProxyPort;
\r
234 var m = HostAndPortRegex.Match(_session.Request.RequestLine);
\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
242 if (host == null && !ParseAuthority(_session.Request.Host, ref host, ref port))
\r
243 throw new HttpProxyAbort("Can't find destination host");
\r
245 var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
\r
246 socket.Connect(host, port);
\r
250 private static readonly Regex AuthorityRegex = new Regex("([^:]+)(?::(\\d+))?");
\r
252 private bool ParseAuthority(string authority, ref string host, ref int port)
\r
254 if (string.IsNullOrEmpty(authority))
\r
256 var m = AuthorityRegex.Match(authority);
\r
259 host = m.Groups[1].Value;
\r
260 if (m.Groups[2].Success)
\r
261 port = int.Parse(m.Groups[2].Value);
\r
265 private void Close()
\r
267 _serverStream?.Close();
\r
268 _clientStream?.Close();
\r
274 public class Session
\r
276 public Request Request { get; set; } = new Request();
\r
277 public Response Response { get; set; } = new Response();
\r
280 public class Message
\r
282 private string _headers;
\r
283 public byte[] Body { get; set; }
\r
285 private static readonly Regex CharsetRegx = new Regex("charset=([\\w-]+)",
\r
286 RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
\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
294 public string Headers
\r
296 get { return _headers; }
\r
300 SetHeaders(_headers);
\r
304 public virtual string ModifiedHeaders => SetConnectionClose(Headers);
\r
306 private string SetConnectionClose(string headers)
\r
308 return InsertHeader(RemoveHeaders(headers,
\r
309 new[] {"connection", "keep-alive", "proxy-connection"}), "Connection: close\r\n");
\r
312 protected string RemoveHeaders(string headers, string[] fields)
\r
314 foreach (var f in fields)
\r
316 var m = MatchField(f, headers);
\r
319 headers = headers.Remove(m.Index, m.Length);
\r
324 protected string InsertHeader(string headers, string header)
\r
326 return headers.Insert(headers.Length - 2, header);
\r
329 protected virtual void SetHeaders(string headers)
\r
331 var s = GetField("content-length");
\r
335 ContentLength = int.TryParse(s, out len) ? len : -1;
\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
343 protected Match MatchField(string name, string headers)
\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
350 protected string GetField(string name)
\r
352 var m = MatchField(name, Headers);
\r
353 return m.Success ? m.Groups[1].Value : null;
\r
356 public string BodyAsString
\r
362 var m = CharsetRegx.Match(ContentType ?? "");
\r
363 var encoding = Encoding.ASCII;
\r
366 var name = m.Groups[1].Value;
\r
367 if (name == "utf8")
\r
369 encoding = Encoding.GetEncoding(name);
\r
371 return encoding.GetString(Body);
\r
375 public void ReadBody(HttpStream stream)
\r
377 if (TransferEncoding != null && TransferEncoding.Contains("chunked"))
\r
379 Body = stream.ReadChunked();
\r
381 else if (ContentLength == 0)
\r
384 else if (ContentLength > 0)
\r
386 var buf = new byte[ContentLength];
\r
387 stream.Read(buf, 0, ContentLength);
\r
392 Body = stream.ReadToEnd();
\r
394 if (ContentEncoding == null)
\r
396 var dc = new MemoryStream();
\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
404 catch (Exception ex)
\r
406 throw new HttpProxyAbort($"Fail to decode {ContentEncoding}: " + ex.Message);
\r
408 Body = dc.ToArray();
\r
412 public class Request : Message
\r
414 private string _requestLine;
\r
416 public string RequestLine
\r
418 get { return _requestLine; }
\r
421 _requestLine = value;
\r
422 var f = _requestLine.Split(' ');
\r
424 throw new HttpProxyAbort("Invalid request line");
\r
426 PathAndQuery = f.Length < 2 ? "" : f[1];
\r
430 public string Method { get; private set; }
\r
431 public string PathAndQuery { get; private set; }
\r
434 public class Response : Message
\r
436 private string _statusLine;
\r
438 public override string ModifiedHeaders =>
\r
439 InsertContentLength(RemoveHeaders(base.ModifiedHeaders,
\r
440 new[] {"transfer-encoding", "content-encoding", "content-length"}));
\r
442 private string InsertContentLength(string headers)
\r
444 return Body == null ? headers : InsertHeader(headers, $"Content-Length: {Body.Length}\r\n");
\r
447 public string StatusLine
\r
449 get { return _statusLine; }
\r
452 _statusLine = value;
\r
453 var f = _statusLine.Split(' ');
\r
455 throw new HttpProxyAbort("Invalid status line");
\r
456 StatusCode = _statusLine.Split(' ')[1];
\r
460 public string StatusCode { get; private set; }
\r
463 private class HttpProxyAbort : Exception
\r
465 public HttpProxyAbort(string message) : base(message)
\r
470 public class HttpStream
\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
477 public HttpStream(Socket socket)
\r
480 socket.NoDelay = true;
\r
483 public string ReadLine()
\r
485 var sb = new StringBuilder();
\r
487 while ((ch = ReadByte()) != -1)
\r
489 sb.Append((char)ch);
\r
493 return sb.ToString();
\r
496 private int ReadByte()
\r
498 if (_position < _available)
\r
499 return _buffer[_position++];
\r
500 _available = _socket.Receive(_buffer, 0, _buffer.Length, SocketFlags.None);
\r
502 return _available == 0 ? -1 : _buffer[_position++];
\r
505 public HttpStream WriteLines(string s)
\r
507 var buf = Encoding.ASCII.GetBytes(s);
\r
508 Write(buf, 0, buf.Length);
\r
512 public string ReadHeaders()
\r
514 var sb = new StringBuilder();
\r
520 } while (line != "\r\n");
\r
521 return sb.ToString();
\r
524 public byte[] ReadChunked()
\r
526 var buf = new MemoryStream();
\r
529 var size = ReadLine();
\r
530 if (size.Length < 3)
\r
532 var ext = size.IndexOf(';');
\r
533 size = ext == -1 ? size.Substring(0, size.Length - 2) : size.Substring(0, ext);
\r
535 if (!int.TryParse(size, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out val))
\r
536 throw new HttpProxyAbort("Can't parse chunk size: " + size);
\r
542 var chunk = new byte[val];
\r
543 Read(chunk, 0, chunk.Length);
\r
544 buf.Write(chunk, 0, chunk.Length);
\r
551 } while (line != "" && line != "\r\n");
\r
552 return buf.ToArray();
\r
555 public byte[] ReadToEnd()
\r
557 var result = new MemoryStream();
\r
558 var buf = new byte[4096];
\r
560 while ((len = Read(buf, 0, buf.Length)) > 0)
\r
561 result.Write(buf, 0, len);
\r
562 return result.ToArray();
\r
565 public HttpStream Write(byte[] body)
\r
568 Write(body, 0, body.Length);
\r
572 public int Read(byte[] buf, int offset, int count)
\r
580 if (_position < _available)
\r
582 n = Math.Min(count, _available - _position);
\r
583 Buffer.BlockCopy(_buffer, _position, buf, 0, n);
\r
588 n = _socket.Receive(buf, offset, count, SocketFlags.None);
\r
590 return total == 0 ? n : total;
\r
595 } while (count > 0);
\r
598 catch (IOException)
\r
604 public void Write(byte[] buf, int offset, int count)
\r
610 var n = _socket.Send(buf, offset, count, SocketFlags.None);
\r
615 } while (count > 0);
\r
617 catch (IOException)
\r
622 public HttpStream Close()
\r