244 lines
9.9 KiB
C#
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;
|
|
}
|
|
|
|
}
|
|
}
|