ln.http/ln.http/client/HttpClient.cs

244 lines
9.9 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using ln.http.content;
using ln.logging;
using ln.patterns;
using ln.type;
using Newtonsoft.Json;
namespace ln.http.client
{
public class HttpClient
{
private CookieContainer _cookies = new CookieContainer();
private HeaderContainer _defaultHeaders = new HeaderContainer();
private ILogWriter _logWriter;
public HeaderContainer DefaultHeaders => _defaultHeaders;
public CookieContainer Cookies => _cookies;
public HttpClient() :this(null) { }
public bool FollowRedirects { get; set; }
public HttpClient(ILogWriter logWriter)
{
_logWriter = logWriter ?? new LogWriter(new ConsoleLogSink(true));
_defaultHeaders.Add("User-Agent", "Mozilla/5.0 (ln.http.client.HttpClient)");
}
private delegate bool TryConnectDelegate(Uri uri, IPAddress ip, int port, out IDisposable disposable,
out Stream connectionStream);
public Optional<HttpClientResponse> Request(HttpMethod method, Uri uri, IEnumerable<Header> headers,
Stream body)
{
TryConnectDelegate tryConnectDelegate = null;
switch (uri.Scheme)
{
case "http":
tryConnectDelegate = this.TryConnect;
break;
case "https":
tryConnectDelegate = this.TryConnectTLS;
break;
default:
throw new NotSupportedException($"URI scheme { uri.Scheme } not supported by HttpClient");
}
foreach (var address in GetIPAddresses(uri))
{
if (tryConnectDelegate(uri, address, uri.Port, out IDisposable disposable, out Stream connectionStream))
{
try
{
return _Request(connectionStream, method, uri, headers, body);
}
finally
{
connectionStream?.Dispose();
disposable?.Dispose();
}
}
}
return new SocketException();
}
private IPAddress[] GetIPAddresses(Uri uri)
{
switch (uri.HostNameType)
{
case UriHostNameType.Dns:
return Dns.GetHostAddresses(uri.Host);
case UriHostNameType.IPv4:
case UriHostNameType.IPv6:
return new[] { IPAddress.Parse(uri.Host), };
default:
return new IPAddress[0];
}
}
private bool TryConnect(Uri uri, IPAddress ip, int port, out IDisposable disposable, out Stream connectionStream)
{
try
{
TcpClient tcpClient = new TcpClient();
tcpClient.Connect(new IPEndPoint(ip, port));
disposable = tcpClient;
connectionStream = tcpClient.GetStream();
return true;
}
catch (SocketException se)
{
}
disposable = null;
connectionStream = null;
return false;
}
private bool TryConnectTLS(Uri uri, IPAddress ip, int port, out IDisposable disposable, out Stream connectionStream)
{
if (TryConnect(uri, ip, port, out IDisposable rawDisposable, out Stream rawConnectionStream))
{
SslStream sslStream = new SslStream(rawConnectionStream, false);
sslStream.AuthenticateAsClient(uri.Host);
disposable = rawDisposable;
connectionStream = sslStream;
return true;
}
disposable = null;
connectionStream = null;
return false;
}
public Optional<HttpClientResponse> _Request(Stream connectionStream, HttpMethod method, Uri uri,
IEnumerable<Header> headers, Stream body)
{
HeaderContainer requestHeaders = new HeaderContainer(_defaultHeaders);
requestHeaders.Set("Host", uri.Host);
if (body is Stream)
requestHeaders.Set("Content-Length", body.Length.ToString());
connectionStream.WriteBytes(Encoding.ASCII.GetBytes(String.Format("{0} {1} HTTP/1.1\r\n",
method.ToString(),
uri.PathAndQuery
)));
requestHeaders.CopyTo(connectionStream);
if ((method != HttpMethod.HEAD) && (body is Stream))
body.CopyTo(connectionStream);
connectionStream.Flush();
if (!ReadResponseLine(connectionStream, out HttpVersion httpVersion, out HttpStatusCode statusCode,
out string reason))
return new ProtocolViolationException();
HeaderContainer responseHeaders = new HeaderContainer(connectionStream);
Stream contentStream = null;
if (method != HttpMethod.HEAD)
{
if (responseHeaders.TryGetValue("Transfer-Encoding", out string transferEncoding) &&
(transferEncoding.Equals("chunked")))
contentStream = new HttpContentStreamChunked(connectionStream);
else if (responseHeaders.TryGetInteger("Content-Length", out int contentLength))
contentStream = new HttpContentStream(connectionStream, contentLength);
}
if (FollowRedirects)
{
switch (statusCode)
{
case HttpStatusCode.Found:
case HttpStatusCode.MovedPermanently:
case HttpStatusCode.TemporaryRedirect:
case HttpStatusCode.PermanentRedirect:
if (body != null)
body.Position = 0;
return Request(method, new Uri(responseHeaders.Get("Location")), headers, body);
case HttpStatusCode.SeeOther:
return Get(responseHeaders.Get("Location"), headers);
}
}
return new HttpClientResponse(method, uri, statusCode, reason, responseHeaders, contentStream);
}
public Optional<HttpClientResponse> Get(Uri uri) => Request(HttpMethod.GET, uri, null, null);
public Optional<HttpClientResponse> Get(Uri uri, IEnumerable<Header> headers) => Request(HttpMethod.GET, uri, headers, null);
public Optional<HttpClientResponse> Post(Uri uri, Stream body) => Request(HttpMethod.POST, uri, null, body);
public Optional<HttpClientResponse> Post(Uri uri, IEnumerable<Header> headers, Stream body) => Request(HttpMethod.POST, uri, headers, body);
public Optional<HttpClientResponse> Get(string uri) => Request(HttpMethod.GET, new Uri(uri), null, null);
public Optional<HttpClientResponse> Get(string uri, IEnumerable<Header> headers) => Request(HttpMethod.GET, new Uri(uri), headers, null);
public Optional<HttpClientResponse> Post(string uri, Stream body) => Request(HttpMethod.POST, new Uri(uri), null, body);
public Optional<HttpClientResponse> Post(string uri, IEnumerable<Header> headers, Stream body) => Request(HttpMethod.POST, new Uri(uri), headers, body);
public Optional<HttpClientResponse> Post(string uri, Newtonsoft.Json.JsonToken json) => Post(uri, null, json);
public Optional<HttpClientResponse> Post(string uri, IEnumerable<Header> headers,
Newtonsoft.Json.JsonToken json)
{
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json.ToString())))
return Request(HttpMethod.POST, new Uri(uri), headers, stream);
}
public Optional<HttpClientResponse> Post(string uri, IEnumerable<Header> headers, string body)
{
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(body)))
return Request(HttpMethod.POST, new Uri(uri), headers, stream);
}
public Optional<HttpClientResponse> Post<T>(string uri, IEnumerable<Header> headers, T body) =>
Post(new Uri(uri), headers, body);
public Optional<HttpClientResponse> Post<T>(Uri uri, IEnumerable<Header> headers, T body)
{
HeaderContainer headerContainer = new HeaderContainer(headers);
if (!headerContainer.Contains("Content-Type"))
headerContainer.Add("Content-Type", "application/json");
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(body))))
return Request(HttpMethod.POST, uri, headerContainer, stream);
}
public static bool ReadResponseLine(Stream stream, out HttpVersion httpVersion, out HttpStatusCode statusCode,
out string reason)
{
string requestLine = HttpConnection.ReadLine(stream);
if (requestLine is null)
{
httpVersion = HttpVersion.None;
statusCode = 0;
reason = null;
return false;
}
int idxSpace1 = requestLine.IndexOf(' ');
int idxSpace2 = requestLine.IndexOf(' ', idxSpace1 + 1);
if ((idxSpace1 > 0) && (idxSpace2 > 0))
{
httpVersion = HttpVersionSupport.Parse(requestLine.Substring(0, idxSpace1));
statusCode = (HttpStatusCode)int.Parse(requestLine.Substring(idxSpace1 + 1, (idxSpace2 - idxSpace1 - 1)));
reason = requestLine.Substring(idxSpace2 + 1);
return true;
}
httpVersion = HttpVersion.None;
statusCode = 0;
reason = null;
return false;
}
}
}