/* * Secucore * * Copyright (C) 2023 Trevor Hall * All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. * */ using SecuCore.Proxies; using System.Text; using SecuCore.Sockets; using System.Text.RegularExpressions; using System.Threading.Tasks; using System; using System.Collections.Generic; using System.Linq; using System.IO.Compression; using System.IO; namespace SecuCore.Clients { class HTTPCookie { public string Name; public string Value; public string Domain; public string Path; public DateTime Expires; } public class HTTPClient { public string UA = "SecuCore/1.0 HTTPClient"; public string LastHeaders { get { return _lastHeaders; } } public string LastRedirect { get { return _lastHeaders; } } private Proxy _proxy; private TCPSocket _tcpSocket; private TCPNetworkStream _tcpns; private SecuredTCPStream _stcps; private string _lastErrorText = ""; private string _lastHeaders = ""; private string _lastRedirect = ""; private List _cookies; private bool _connectedWithTLS = false; public string LastError { get { return _lastErrorText; } } public int ConnectTimeout { get; set; } = 10000; public int WriteTimeout { get; set; } = 10000; public int ReadTimeout { get; set; } = 10000; private int _wsrbs = 8192; private int _wssbs = 8192; public int WsaSockRecieveBufSize { get { return _wsrbs; } set { if (_wsrbs != value) { _wsrbs = value; if (_tcpSocket != null) { _tcpSocket.SetWSASockBufferSizes(_wsrbs, _wssbs); } } } } public int WsaSockSendBufSize { get { return _wssbs; } set { if (_wssbs != value) { _wssbs = value; _tcpSocket?.SetWSASockBufferSizes(_wsrbs, _wssbs); } } } public TLS.TLSRecordLayer.ValidateServerCertificate validationCallback; public TLS.TLSRecordLayer.ClientCertificateRequest certificateRequestCallback; public HTTPClient(string proxy = "", ProxyProtocol proxyProtocol = ProxyProtocol.HTTP) { _cookies = new List(); if (!string.IsNullOrEmpty(proxy)) this._proxy = new Proxy(proxy, proxyProtocol); } public void SetCallbacks(TLS.TLSRecordLayer.ValidateServerCertificate valCallback, TLS.TLSRecordLayer.ClientCertificateRequest certReqCallback) { this.validationCallback = valCallback; this.certificateRequestCallback = certReqCallback; } public static string UrlEncode(string value) { StringBuilder result = new StringBuilder(); foreach (char symbol in value) { if ((symbol >= '0' && symbol <= '9') || (symbol >= 'a' && symbol <= 'z') || (symbol >= 'A' && symbol <= 'Z') || symbol == '-' || symbol == '_') { result.Append(symbol); } else { result.Append('%' + string.Format("{0:X2}", (int)symbol)); } } return result.ToString(); } public string BuildGETRequest(string host, string path) { List<(string Name, string Value)> headers = new List<(string Name, string Value)> { ("Host", host), ("Connection", "keep-alive"), ("User-Agent", UA), ("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), ("Accept-Encoding", "gzip") }; StringBuilder sb = new StringBuilder(); sb.Append(string.Format("GET {0} HTTP/1.1\r\n", path)); StringBuilder csb = new StringBuilder(); foreach (HTTPCookie hc in _cookies) { if (hc.Domain.EndsWith(host)) { if (path.StartsWith(hc.Path)) { csb.Append(String.Format("{0}={1}", hc.Name, hc.Value)); if (hc != _cookies.Last()) csb.Append("; "); } } } if (csb.Length > 0) headers.Add(("Cookie", csb.ToString())); foreach ((string Name, string Value) in headers) { sb.Append(string.Format("{0}: {1}\r\n", Name, Value)); } sb.Append("\r\n"); return sb.ToString(); } public string BuildPOSTRequest(string host, string path, List<(string,string)> postvals) { StringBuilder psb = new StringBuilder(); foreach((string,string) nv in postvals) { psb.Append(String.Format("{0}={1}", UrlEncode(nv.Item1), UrlEncode(nv.Item2))); if (nv != postvals.Last()) psb.Append("&"); } string urlencoded_postdata = psb.ToString(); List<(string, string)> headers = new List<(string, string)> { ("Host", host), ("Connection", "keep-alive"), ("User-Agent", UA), ("Content-Type", "application/x-www-form-urlencoded"), ("Content-Length", urlencoded_postdata.Length.ToString()), ("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), ("Accept-Encoding", "gzip") }; StringBuilder sb = new StringBuilder(); sb.Append(string.Format("POST {0} HTTP/1.1\r\n", path)); StringBuilder csb = new StringBuilder(); foreach (HTTPCookie hc in _cookies) { if (hc.Domain.EndsWith(host)) { if (path.StartsWith(hc.Path)) { csb.Append(String.Format("{0}={1}", hc.Name, hc.Value)); if (hc != _cookies.Last()) csb.Append("; "); } } } if (csb.Length > 0) headers.Add(("Cookie", csb.ToString())); foreach ((string,string) hv in headers) { sb.Append(string.Format("{0}: {1}\r\n", hv.Item1, hv.Item2)); } sb.Append("\r\n"); sb.Append(urlencoded_postdata); return sb.ToString(); } private void ParseCookies() { //parse cookies MatchCollection cookmc = Regex.Matches(_lastHeaders, @"[Ss]et\-[Cc]ookie:\ ?([^\r\n]+)"); if (cookmc.Count > 0) { foreach (Match cm in cookmc) { string cookievalues = cm.Groups[1].Value; string[] cookiepairs = cookievalues.Split(';'); HTTPCookie httpc = new HTTPCookie(); httpc.Expires = DateTime.Now.AddYears(1); bool cookievalid = true; foreach (string cookpair in cookiepairs) { string trimmed = cookpair.Trim(); if (trimmed.Length > 0) { if (cookpair == cookiepairs.First()) { //name and value if (trimmed.Contains("=")) { httpc.Name = trimmed.Substring(0, trimmed.IndexOf("=")); httpc.Value = trimmed.Substring(trimmed.IndexOf("=") + 1); } else { cookievalid = false; break; } } else { string trimmedtl = trimmed.ToLower(); if (trimmed.Contains("=")) { if (trimmedtl.StartsWith("domain=")) { httpc.Domain = trimmed.Substring(trimmed.IndexOf("=") + 1); } else if (trimmedtl.StartsWith("expires=")) { DateTime.TryParse(trimmed.Substring(trimmed.IndexOf("=") + 1), out httpc.Expires); } else if (trimmedtl.StartsWith("path=")) { httpc.Path = trimmed.Substring(trimmed.IndexOf("=") + 1); } } } } } if (cookievalid) { bool found = false; foreach (HTTPCookie cookie in _cookies) { if (cookie.Name == httpc.Name && cookie.Domain == httpc.Domain) { //replace the value cookie.Value = httpc.Value; cookie.Path = httpc.Path; cookie.Expires = httpc.Expires; found = true; break; } } if (!found) _cookies.Add(httpc); } } } } public Task GetAsync(string url, string referer) { return RequestAsync(url, referer, false, null); } public Task PostAsync(string url, string referer, List<(string, string)> postvals) { return RequestAsync(url, referer, true, postvals); } public async Task RequestAsync(string url, string referer, bool post, List<(string, string)> postvals) { _lastRedirect = ""; //parse destination url try { Match m = Regex.Match(url, @"(https?)\:\/\/([^\:\/\r\n]+)(\/[^\r\n\ ]+)?"); if(m.Success) { string protocol = m.Groups[1].Value; string host = m.Groups[2].Value; string path = m.Groups[3].Value; if(string.IsNullOrEmpty(path)) path = "/"; int destprt = (protocol == "https" ? 443 : 80); if(!await ConnectAsync(host, destprt).ConfigureAwait(false)) { _lastErrorText = "Failed to connect"; return ""; } string request; if(post) { request = BuildPOSTRequest(host, path, postvals); } else { request = BuildGETRequest(host, path); } INetStream ins = (_connectedWithTLS ? _stcps : _tcpns); byte[] requestdata = System.Text.Encoding.UTF8.GetBytes(request); await ins.WriteAsync(requestdata, 0, requestdata.Length).ConfigureAwait(false); _lastHeaders = System.Text.Encoding.UTF8.GetString(await ins.ReadUntilCRLFCRLF().ConfigureAwait(false)); int contentLength = 0; Match clm = Regex.Match(_lastHeaders, @"[Cc]ontent-[Ll]ength\: (\d+)"); if(clm.Success) { string clenStr = clm.Groups[1].Value; int.TryParse(clenStr, out contentLength); } //check for redirect Match rlm = Regex.Match(_lastHeaders, @"[Ll]ocation:\ ?([^\r\n]+)"); if(rlm.Success) _lastRedirect = rlm.Groups[1].Value; if (_lastRedirect == url) _lastRedirect = null; //parse cookies ParseCookies(); bool chunked = _lastHeaders.ToLower().Contains("transfer-encoding: chunked"); bool gzip = _lastHeaders.ToLower().Contains("content-encoding: gzip"); //get response body if any string content = null; if (contentLength >= 0) { byte[] rbuf = new byte[contentLength]; int rlen = await ins.ReadAsync(rbuf, 0, contentLength).ConfigureAwait(false); if (rlen > 0) { byte[] responseData = new byte[rlen]; Buffer.BlockCopy(rbuf, 0, responseData, 0, rlen); if (gzip) { byte[] decompressed = Decompress(responseData); content = Encoding.UTF8.GetString(decompressed, 0, decompressed.Length); } else { content = Encoding.UTF8.GetString(responseData, 0, responseData.Length); } } } if (!string.IsNullOrEmpty(_lastRedirect)) { if (!_lastRedirect.ToLower().StartsWith("http")) _lastRedirect = protocol + "://" + host + _lastRedirect; return await GetAsync(_lastRedirect, url).ConfigureAwait(false); } else { if (chunked) { using (MemoryStream chunk_ms = new MemoryStream()) { string nextchunkhex = System.Text.Encoding.UTF8.GetString(await ins.ReadUntilCRLF().ConfigureAwait(false)); if (nextchunkhex.Length < 0) return ""; int nextchunki = Convert.ToInt32("0x" + nextchunkhex.Substring(0, nextchunkhex.Length - 2), 16); nextchunki += 2; while (nextchunki > 0) { byte[] rbuf = new byte[nextchunki]; int cpos = 0; int remaining = nextchunki; while (remaining > 0) { int bytesread = await ins.ReadAsync(rbuf, cpos, remaining); if (bytesread <= 0) break; remaining -= bytesread; cpos += bytesread; } if (cpos <= 0) break; byte[] chunkdata = new byte[nextchunki - 2]; Buffer.BlockCopy(rbuf, 0, chunkdata, 0, nextchunki - 2); chunk_ms.Write(chunkdata, 0, chunkdata.Length); nextchunki = 0; byte[] nextchunkb = await ins.ReadUntilCRLF().ConfigureAwait(false); if (nextchunkb != null) { nextchunkhex = System.Text.Encoding.UTF8.GetString(nextchunkb, 0, nextchunkb.Length); string nexthvstr = nextchunkhex.Substring(0, nextchunkhex.Length); nextchunki = Convert.ToInt32("0x" + nexthvstr.Substring(0, nexthvstr.Length - 2), 16); if (nextchunki == 0) break; if (nextchunki > 0) nextchunki += 2; } } byte[] responseData = chunk_ms.ToArray(); if (gzip) { byte[] decompressed = Decompress(responseData); return Encoding.UTF8.GetString(decompressed, 0, decompressed.Length); } else { return Encoding.UTF8.GetString(responseData, 0, responseData.Length); } } } else { return content; } } } }catch (Exception ex) { _lastErrorText = ex.Message; } finally { try { if(_tcpSocket != null) _tcpSocket.Dispose(); } catch (Exception ex) { } _tcpSocket = null; } return ""; } static byte[] Decompress(byte[] gzip) { using (GZipStream stream = new GZipStream(new MemoryStream(gzip), CompressionMode.Decompress)) { const int size = 4096; byte[] buffer = new byte[size]; using (MemoryStream memory = new MemoryStream()) { int count = 0; do { count = stream.Read(buffer, 0, size); if (count > 0) { memory.Write(buffer, 0, count); } } while (count > 0); return memory.ToArray(); } } } public async Task ConnectAsync(string remotehost, int destprt) { if(_tcpSocket != null) { if (_tcpSocket.Connected()) _tcpSocket.Dispose(); } if (_tcpSocket == null) { _tcpSocket = new TCPSocket(remotehost, destprt, this._proxy, this.WsaSockRecieveBufSize, this.WsaSockSendBufSize); } _tcpSocket.ConnectTimeout = this.ConnectTimeout; _tcpSocket.WriteTimeout = this.WriteTimeout; _tcpSocket.ReadTimeout = this.ReadTimeout; if (!await _tcpSocket.ConnectAsync().ConfigureAwait(false)) return false; _tcpns = _tcpSocket.GetStream(); if (destprt == 443) { SecuredTCPStream stcps = new SecuredTCPStream(_tcpSocket); this._stcps = stcps; _connectedWithTLS = false; try { _connectedWithTLS = await stcps.NegotiateTLSConnection(remotehost, this.validationCallback, this.certificateRequestCallback).ConfigureAwait(false); } catch (Exception){} if (_connectedWithTLS) return true; } else { return true; } return false; } public async Task Dispose() { if (_stcps != null) { try { await _stcps.DisposeAsync().ConfigureAwait(false); } catch (Exception) { } } if (_tcpns != null) { try { _tcpns.Dispose(); } catch (Exception) { } } if (_tcpSocket != null) { try { _tcpSocket.Dispose(); } catch (Exception) { } } } } }