Compare commits
10 commits
Author | SHA1 | Date | |
---|---|---|---|
flash | 1051a26494 | ||
flash | 6f50ec66a9 | ||
flash | e6dffe06e6 | ||
flash | 9790f77a16 | ||
flash | 08f9a2c5a1 | ||
flash | ea83c8cca0 | ||
flash | bfd1819798 | ||
flash | 2de19035ff | ||
flash | 3f8c2781ee | ||
flash | 23f0bd478f |
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,6 +1,13 @@
|
||||||
## Ignore Visual Studio temporary files, build results, and
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
## files generated by popular Visual Studio add-ons.
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
|
welcome.txt
|
||||||
|
mariadb.txt
|
||||||
|
login_key.txt
|
||||||
|
http-motd.txt
|
||||||
|
_webdb.txt
|
||||||
|
msz_url.txt
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.user
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
|
@ -1,26 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpAcceptEncodingHeader : HttpHeader {
|
|
||||||
public const string NAME = @"Accept-Encoding";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value => string.Join(@", ", Encodings);
|
|
||||||
|
|
||||||
public HttpEncoding[] Encodings { get; }
|
|
||||||
|
|
||||||
public HttpAcceptEncodingHeader(string encodings) : this(
|
|
||||||
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public HttpAcceptEncodingHeader(string[] encodings) : this(
|
|
||||||
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Select(HttpEncoding.Parse)
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public HttpAcceptEncodingHeader(IEnumerable<HttpEncoding> encodings) {
|
|
||||||
Encodings = (encodings ?? throw new ArgumentNullException(nameof(encodings))).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpConnectionHeader : HttpHeader {
|
|
||||||
public const string NAME = @"Connection";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value { get; }
|
|
||||||
|
|
||||||
public const string CLOSE = @"close";
|
|
||||||
public const string KEEP_ALIVE = @"keep-alive";
|
|
||||||
|
|
||||||
public HttpConnectionHeader(string mode) {
|
|
||||||
Value = mode ?? throw new ArgumentNullException(nameof(mode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpContentEncodingHeader : HttpHeader {
|
|
||||||
public const string NAME = @"Content-Encoding";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value => string.Join(@", ", Encodings);
|
|
||||||
|
|
||||||
public string[] Encodings { get; }
|
|
||||||
|
|
||||||
public HttpContentEncodingHeader(string encodings) : this(
|
|
||||||
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public HttpContentEncodingHeader(string[] encodings) {
|
|
||||||
Encodings = encodings ?? throw new ArgumentNullException(nameof(encodings));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpContentLengthHeader : HttpHeader {
|
|
||||||
public const string NAME = @"Content-Length";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value => Stream?.Length ?? Length;
|
|
||||||
|
|
||||||
private Stream Stream { get; }
|
|
||||||
private long Length { get; }
|
|
||||||
|
|
||||||
public HttpContentLengthHeader(Stream stream) {
|
|
||||||
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
|
||||||
if(!stream.CanRead || !stream.CanSeek)
|
|
||||||
throw new ArgumentException(@"Body must readable and seekable.", nameof(stream));
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpContentLengthHeader(long length) {
|
|
||||||
Length = length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpContentLengthHeader(string length) {
|
|
||||||
if(!long.TryParse(length, out long ll))
|
|
||||||
throw new ArgumentException(@"Invalid length value.", nameof(length));
|
|
||||||
Length = ll;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpContentTypeHeader : HttpHeader {
|
|
||||||
public const string NAME = @"Content-Type";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value => MediaType.ToString();
|
|
||||||
|
|
||||||
public HttpMediaType MediaType { get; }
|
|
||||||
|
|
||||||
public HttpContentTypeHeader(string mediaType) {
|
|
||||||
MediaType = HttpMediaType.Parse(mediaType ?? throw new ArgumentNullException(nameof(mediaType)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpContentTypeHeader(HttpMediaType mediaType) {
|
|
||||||
MediaType = mediaType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpCustomHeader : HttpHeader {
|
|
||||||
public override string Name { get; }
|
|
||||||
public override object Value { get; }
|
|
||||||
|
|
||||||
public HttpCustomHeader(string name, object value) {
|
|
||||||
Name = NormaliseName(name ?? throw new ArgumentNullException(nameof(name)));
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpDateHeader : HttpHeader {
|
|
||||||
public const string NAME = @"Date";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value { get; }
|
|
||||||
|
|
||||||
public DateTimeOffset DateTime { get; }
|
|
||||||
|
|
||||||
public HttpDateHeader(string dateString) {
|
|
||||||
Value = dateString ?? throw new ArgumentNullException(nameof(dateString));
|
|
||||||
DateTime = DateTimeOffset.ParseExact(dateString, @"r", CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public abstract class HttpHeader {
|
|
||||||
public abstract string Name { get; }
|
|
||||||
public abstract object Value { get; }
|
|
||||||
|
|
||||||
public override string ToString() {
|
|
||||||
return string.Format(@"{0}: {1}", Name, Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string NormaliseName(string name) {
|
|
||||||
if(string.IsNullOrWhiteSpace(name))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
string[] parts = name.ToLowerInvariant().Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
||||||
for(int i = 0; i < parts.Length; ++i)
|
|
||||||
parts[i] = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(parts[i]);
|
|
||||||
return string.Join('-', parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HttpHeader Create(string name, object value) {
|
|
||||||
return name switch {
|
|
||||||
HttpTeHeader.NAME => new HttpTeHeader(value.ToString()),
|
|
||||||
HttpDateHeader.NAME => new HttpDateHeader(value.ToString()),
|
|
||||||
HttpHostHeader.NAME => new HttpHostHeader(value.ToString()),
|
|
||||||
HttpServerHeader.NAME => new HttpServerHeader(value.ToString()),
|
|
||||||
HttpUserAgentHeader.NAME => new HttpUserAgentHeader(value.ToString()),
|
|
||||||
HttpKeepAliveHeader.NAME => new HttpKeepAliveHeader(value.ToString()),
|
|
||||||
HttpConnectionHeader.NAME => new HttpConnectionHeader(value.ToString()),
|
|
||||||
HttpContentTypeHeader.NAME => new HttpContentTypeHeader(value.ToString()),
|
|
||||||
HttpContentLengthHeader.NAME => new HttpContentLengthHeader(value.ToString()),
|
|
||||||
HttpAcceptEncodingHeader.NAME => new HttpAcceptEncodingHeader(value.ToString()),
|
|
||||||
HttpContentEncodingHeader.NAME => new HttpContentEncodingHeader(value.ToString()),
|
|
||||||
HttpTransferEncodingHeader.NAME => new HttpTransferEncodingHeader(value.ToString()),
|
|
||||||
_ => new HttpCustomHeader(name, value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpHostHeader : HttpHeader {
|
|
||||||
public const string NAME = @"Host";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value {
|
|
||||||
get {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
sb.Append(Host);
|
|
||||||
if(Port != -1)
|
|
||||||
sb.AppendFormat(@":{0}", Port);
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Host { get; }
|
|
||||||
public int Port { get; }
|
|
||||||
public bool IsSecure { get; }
|
|
||||||
|
|
||||||
public HttpHostHeader(string host, int port) {
|
|
||||||
Host = host;
|
|
||||||
Port = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpHostHeader(string hostAndPort) {
|
|
||||||
string[] parts = hostAndPort.Split(':', 2, StringSplitOptions.TrimEntries);
|
|
||||||
Host = parts.ElementAtOrDefault(0) ?? throw new ArgumentNullException(nameof(hostAndPort));
|
|
||||||
if(!ushort.TryParse(parts.ElementAtOrDefault(1), out ushort port))
|
|
||||||
throw new FormatException(@"Host is not in valid format.");
|
|
||||||
Port = port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpKeepAliveHeader : HttpHeader {
|
|
||||||
public const string NAME = @"Keep-Alive";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value {
|
|
||||||
get {
|
|
||||||
List<string> parts = new();
|
|
||||||
if(MaxIdle != TimeSpan.MaxValue)
|
|
||||||
parts.Add(string.Format(@"timeout={0}", MaxIdle.TotalSeconds));
|
|
||||||
if(MaxRequests >= 0)
|
|
||||||
parts.Add(string.Format(@"max={0}", MaxRequests));
|
|
||||||
return string.Join(@", ", parts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public TimeSpan MaxIdle { get; } = TimeSpan.MaxValue;
|
|
||||||
public int MaxRequests { get; } = -1;
|
|
||||||
|
|
||||||
public HttpKeepAliveHeader(string value) {
|
|
||||||
IEnumerable<string> kvps = (value ?? throw new ArgumentNullException(nameof(value))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
||||||
|
|
||||||
foreach(string kvp in kvps) {
|
|
||||||
string[] parts = kvp.Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
||||||
if(parts[0] == @"timeout" && int.TryParse(parts[1], out int timeout))
|
|
||||||
MaxIdle = TimeSpan.FromSeconds(timeout);
|
|
||||||
else if(parts[0] == @"max" && int.TryParse(parts[1], out int max))
|
|
||||||
MaxRequests = max;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpServerHeader : HttpHeader {
|
|
||||||
public const string NAME = @"Server";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value { get; }
|
|
||||||
|
|
||||||
public HttpServerHeader(string server) {
|
|
||||||
Value = server ?? throw new ArgumentNullException(nameof(server));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpTeHeader : HttpHeader {
|
|
||||||
public const string NAME = @"TE";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value => string.Join(@", ", Encodings);
|
|
||||||
|
|
||||||
public HttpEncoding[] Encodings { get; }
|
|
||||||
|
|
||||||
public HttpTeHeader(string encodings) : this(
|
|
||||||
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public HttpTeHeader(string[] encodings) : this(
|
|
||||||
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Select(HttpEncoding.Parse)
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public HttpTeHeader(IEnumerable<HttpEncoding> encodings) {
|
|
||||||
Encodings = (encodings ?? throw new ArgumentNullException(nameof(encodings))).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpTransferEncodingHeader : HttpHeader {
|
|
||||||
public const string NAME = @"Transfer-Encoding";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value => string.Join(@", ", Encodings);
|
|
||||||
|
|
||||||
public string[] Encodings { get; }
|
|
||||||
|
|
||||||
public HttpTransferEncodingHeader(string encodings) : this(
|
|
||||||
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public HttpTransferEncodingHeader(string[] encodings) {
|
|
||||||
Encodings = encodings ?? throw new ArgumentNullException(nameof(encodings));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Hamakaze.Headers {
|
|
||||||
public class HttpUserAgentHeader : HttpHeader {
|
|
||||||
public const string NAME = @"User-Agent";
|
|
||||||
|
|
||||||
public override string Name => NAME;
|
|
||||||
public override object Value { get; }
|
|
||||||
|
|
||||||
public HttpUserAgentHeader(string userAgent) {
|
|
||||||
if(userAgent == null)
|
|
||||||
throw new ArgumentNullException(nameof(userAgent));
|
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(userAgent) || userAgent.Equals(HttpClient.USER_AGENT))
|
|
||||||
Value = HttpClient.USER_AGENT;
|
|
||||||
else
|
|
||||||
Value = string.Format(@"{0} {1}", userAgent, HttpClient.USER_AGENT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
using Hamakaze.Headers;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public class HttpClient : IDisposable {
|
|
||||||
public const string PRODUCT_STRING = @"HMKZ";
|
|
||||||
public const string VERSION_MAJOR = @"1";
|
|
||||||
public const string VERSION_MINOR = @"0";
|
|
||||||
public const string USER_AGENT = PRODUCT_STRING + @"/" + VERSION_MAJOR + @"." + VERSION_MINOR;
|
|
||||||
|
|
||||||
private static HttpClient InstanceValue { get; set; }
|
|
||||||
public static HttpClient Instance {
|
|
||||||
get {
|
|
||||||
if(InstanceValue == null)
|
|
||||||
InstanceValue = new HttpClient();
|
|
||||||
return InstanceValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpConnectionManager Connections { get; }
|
|
||||||
private HttpTaskManager Tasks { get; }
|
|
||||||
|
|
||||||
public string DefaultUserAgent { get; set; } = USER_AGENT;
|
|
||||||
public bool ReuseConnections { get; set; } = true;
|
|
||||||
public IEnumerable<HttpEncoding> AcceptedEncodings { get; set; } = new[] { HttpEncoding.GZip, HttpEncoding.Deflate, HttpEncoding.Brotli };
|
|
||||||
|
|
||||||
public HttpClient() {
|
|
||||||
Connections = new HttpConnectionManager();
|
|
||||||
Tasks = new HttpTaskManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpTask CreateTask(
|
|
||||||
HttpRequestMessage request,
|
|
||||||
Action<HttpTask, HttpResponseMessage> onComplete = null,
|
|
||||||
Action<HttpTask, Exception> onError = null,
|
|
||||||
Action<HttpTask> onCancel = null,
|
|
||||||
Action<HttpTask, long, long> onDownloadProgress = null,
|
|
||||||
Action<HttpTask, long, long> onUploadProgress = null,
|
|
||||||
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
|
|
||||||
bool disposeRequest = true,
|
|
||||||
bool disposeResponse = true
|
|
||||||
) {
|
|
||||||
if(request == null)
|
|
||||||
throw new ArgumentNullException(nameof(request));
|
|
||||||
if(string.IsNullOrWhiteSpace(request.UserAgent))
|
|
||||||
request.UserAgent = DefaultUserAgent;
|
|
||||||
if(!request.HasHeader(HttpAcceptEncodingHeader.NAME))
|
|
||||||
request.AcceptedEncodings = AcceptedEncodings;
|
|
||||||
request.Connection = ReuseConnections ? HttpConnectionHeader.KEEP_ALIVE : HttpConnectionHeader.CLOSE;
|
|
||||||
|
|
||||||
HttpTask task = new(Connections, request, disposeRequest, disposeResponse);
|
|
||||||
|
|
||||||
if(onComplete != null)
|
|
||||||
task.OnComplete += onComplete;
|
|
||||||
if(onError != null)
|
|
||||||
task.OnError += onError;
|
|
||||||
if(onCancel != null)
|
|
||||||
task.OnCancel += onCancel;
|
|
||||||
if(onDownloadProgress != null)
|
|
||||||
task.OnDownloadProgress += onDownloadProgress;
|
|
||||||
if(onUploadProgress != null)
|
|
||||||
task.OnUploadProgress += onUploadProgress;
|
|
||||||
if(onStateChange != null)
|
|
||||||
task.OnStateChange += onStateChange;
|
|
||||||
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RunTask(HttpTask task) {
|
|
||||||
Tasks.RunTask(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendRequest(
|
|
||||||
HttpRequestMessage request,
|
|
||||||
Action<HttpTask, HttpResponseMessage> onComplete = null,
|
|
||||||
Action<HttpTask, Exception> onError = null,
|
|
||||||
Action<HttpTask> onCancel = null,
|
|
||||||
Action<HttpTask, long, long> onDownloadProgress = null,
|
|
||||||
Action<HttpTask, long, long> onUploadProgress = null,
|
|
||||||
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
|
|
||||||
bool disposeRequest = true,
|
|
||||||
bool disposeResponse = true
|
|
||||||
) {
|
|
||||||
RunTask(CreateTask(request, onComplete, onError, onCancel, onDownloadProgress, onUploadProgress, onStateChange, disposeRequest, disposeResponse));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Send(
|
|
||||||
HttpRequestMessage request,
|
|
||||||
Action<HttpTask, HttpResponseMessage> onComplete = null,
|
|
||||||
Action<HttpTask, Exception> onError = null,
|
|
||||||
Action<HttpTask> onCancel = null,
|
|
||||||
Action<HttpTask, long, long> onDownloadProgress = null,
|
|
||||||
Action<HttpTask, long, long> onUploadProgress = null,
|
|
||||||
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
|
|
||||||
bool disposeRequest = true,
|
|
||||||
bool disposeResponse = true
|
|
||||||
) {
|
|
||||||
Instance.SendRequest(request, onComplete, onError, onCancel, onDownloadProgress, onUploadProgress, onStateChange, disposeRequest, disposeResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDisposed;
|
|
||||||
~HttpClient()
|
|
||||||
=> DoDispose();
|
|
||||||
public void Dispose() {
|
|
||||||
DoDispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
private void DoDispose() {
|
|
||||||
if(IsDisposed)
|
|
||||||
return;
|
|
||||||
IsDisposed = true;
|
|
||||||
|
|
||||||
Tasks.Dispose();
|
|
||||||
Connections.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Security;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Security.Authentication;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public class HttpConnection : IDisposable {
|
|
||||||
public IPEndPoint EndPoint { get; }
|
|
||||||
public Stream Stream { get; }
|
|
||||||
|
|
||||||
public Socket Socket { get; }
|
|
||||||
public NetworkStream NetworkStream { get; }
|
|
||||||
public SslStream SslStream { get; }
|
|
||||||
|
|
||||||
public string Host { get; }
|
|
||||||
public bool IsSecure { get; }
|
|
||||||
|
|
||||||
public bool HasTimedOut => MaxRequests == 0 || (DateTimeOffset.Now - LastOperation) > MaxIdle;
|
|
||||||
|
|
||||||
public int MaxRequests { get; set; } = -1;
|
|
||||||
public TimeSpan MaxIdle { get; set; } = TimeSpan.MaxValue;
|
|
||||||
public DateTimeOffset LastOperation { get; private set; } = DateTimeOffset.Now;
|
|
||||||
|
|
||||||
public bool InUse { get; private set; }
|
|
||||||
|
|
||||||
public HttpConnection(string host, IPEndPoint endPoint, bool secure) {
|
|
||||||
Host = host ?? throw new ArgumentNullException(nameof(host));
|
|
||||||
EndPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint));
|
|
||||||
IsSecure = secure;
|
|
||||||
|
|
||||||
if(endPoint.AddressFamily is not AddressFamily.InterNetwork and not AddressFamily.InterNetworkV6)
|
|
||||||
throw new ArgumentException(@"Address must be an IPv4 or IPv6 address.", nameof(endPoint));
|
|
||||||
|
|
||||||
Socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) {
|
|
||||||
NoDelay = true,
|
|
||||||
Blocking = true,
|
|
||||||
};
|
|
||||||
Socket.Connect(endPoint);
|
|
||||||
|
|
||||||
NetworkStream = new NetworkStream(Socket, true);
|
|
||||||
|
|
||||||
if(IsSecure) {
|
|
||||||
SslStream = new SslStream(NetworkStream, false, (s, ce, ch, e) => e == SslPolicyErrors.None, null);
|
|
||||||
Stream = SslStream;
|
|
||||||
SslStream.AuthenticateAsClient(Host, null, SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, true);
|
|
||||||
} else
|
|
||||||
Stream = NetworkStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MarkUsed() {
|
|
||||||
LastOperation = DateTimeOffset.Now;
|
|
||||||
if(MaxRequests > 0)
|
|
||||||
--MaxRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Acquire() {
|
|
||||||
return !InUse && (InUse = true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Release() {
|
|
||||||
InUse = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDisposed;
|
|
||||||
~HttpConnection()
|
|
||||||
=> DoDispose();
|
|
||||||
public void Dispose() {
|
|
||||||
DoDispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
private void DoDispose() {
|
|
||||||
if(IsDisposed)
|
|
||||||
return;
|
|
||||||
IsDisposed = true;
|
|
||||||
Stream.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public class HttpConnectionManager : IDisposable {
|
|
||||||
private List<HttpConnection> Connections { get; } = new();
|
|
||||||
private Mutex Lock { get; } = new();
|
|
||||||
|
|
||||||
public HttpConnectionManager() {
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AcquireLock() {
|
|
||||||
if(!Lock.WaitOne(10000))
|
|
||||||
throw new HttpConnectionManagerLockException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReleaseLock() {
|
|
||||||
Lock.ReleaseMutex();
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpConnection CreateConnection(string host, IPEndPoint endPoint, bool secure) {
|
|
||||||
if(host == null)
|
|
||||||
throw new ArgumentNullException(nameof(host));
|
|
||||||
if(endPoint == null)
|
|
||||||
throw new ArgumentNullException(nameof(endPoint));
|
|
||||||
HttpConnection conn = null;
|
|
||||||
AcquireLock();
|
|
||||||
try {
|
|
||||||
conn = CreateConnectionInternal(host, endPoint, secure);
|
|
||||||
} finally {
|
|
||||||
ReleaseLock();
|
|
||||||
}
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpConnection CreateConnectionInternal(string host, IPEndPoint endPoint, bool secure) {
|
|
||||||
HttpConnection conn = new(host, endPoint, secure);
|
|
||||||
Connections.Add(conn);
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpConnection GetConnection(string host, IPEndPoint endPoint, bool secure) {
|
|
||||||
if(host == null)
|
|
||||||
throw new ArgumentNullException(nameof(host));
|
|
||||||
if(endPoint == null)
|
|
||||||
throw new ArgumentNullException(nameof(endPoint));
|
|
||||||
HttpConnection conn = null;
|
|
||||||
AcquireLock();
|
|
||||||
try {
|
|
||||||
conn = GetConnectionInternal(host, endPoint, secure);
|
|
||||||
} finally {
|
|
||||||
ReleaseLock();
|
|
||||||
}
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpConnection GetConnectionInternal(string host, IPEndPoint endPoint, bool secure) {
|
|
||||||
CleanConnectionsInternal();
|
|
||||||
HttpConnection conn = Connections.FirstOrDefault(c => host.Equals(c.Host) && endPoint.Equals(c.EndPoint) && c.IsSecure == secure && c.Acquire());
|
|
||||||
if(conn == null) {
|
|
||||||
conn = CreateConnectionInternal(host, endPoint, secure);
|
|
||||||
conn.Acquire();
|
|
||||||
}
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EndConnection(HttpConnection conn) {
|
|
||||||
if(conn == null)
|
|
||||||
throw new ArgumentNullException(nameof(conn));
|
|
||||||
AcquireLock();
|
|
||||||
try {
|
|
||||||
EndConnectionInternal(conn);
|
|
||||||
} finally {
|
|
||||||
ReleaseLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EndConnectionInternal(HttpConnection conn) {
|
|
||||||
Connections.Remove(conn);
|
|
||||||
conn.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CleanConnection() {
|
|
||||||
AcquireLock();
|
|
||||||
try {
|
|
||||||
CleanConnectionsInternal();
|
|
||||||
} finally {
|
|
||||||
ReleaseLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanConnectionsInternal() {
|
|
||||||
IEnumerable<HttpConnection> conns = Connections.Where(x => x.HasTimedOut).ToArray();
|
|
||||||
foreach(HttpConnection conn in conns) {
|
|
||||||
Connections.Remove(conn);
|
|
||||||
conn.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDisposed;
|
|
||||||
~HttpConnectionManager()
|
|
||||||
=> DoDispose();
|
|
||||||
public void Dispose() {
|
|
||||||
DoDispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
private void DoDispose() {
|
|
||||||
if(IsDisposed)
|
|
||||||
return;
|
|
||||||
IsDisposed = true;
|
|
||||||
|
|
||||||
Lock.Dispose();
|
|
||||||
|
|
||||||
foreach(HttpConnection conn in Connections)
|
|
||||||
conn.Dispose();
|
|
||||||
Connections.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public readonly struct HttpEncoding : IComparable<HttpEncoding?>, IEquatable<HttpEncoding?> {
|
|
||||||
public const string DEFLATE = @"deflate";
|
|
||||||
public const string GZIP = @"gzip";
|
|
||||||
public const string XGZIP = @"x-gzip";
|
|
||||||
public const string BROTLI = @"br";
|
|
||||||
public const string IDENTITY = @"identity";
|
|
||||||
public const string CHUNKED = @"chunked";
|
|
||||||
public const string ANY = @"*";
|
|
||||||
|
|
||||||
public static readonly HttpEncoding Any = new(ANY);
|
|
||||||
public static readonly HttpEncoding None = new(ANY, 0f);
|
|
||||||
public static readonly HttpEncoding Deflate = new(DEFLATE);
|
|
||||||
public static readonly HttpEncoding GZip = new(GZIP);
|
|
||||||
public static readonly HttpEncoding Brotli = new(BROTLI);
|
|
||||||
public static readonly HttpEncoding Identity = new(IDENTITY);
|
|
||||||
|
|
||||||
public string Name { get; }
|
|
||||||
public float Quality { get; }
|
|
||||||
|
|
||||||
public HttpEncoding(string name, float quality = 1f) {
|
|
||||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
|
||||||
Quality = quality;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpEncoding WithQuality(float quality) {
|
|
||||||
return new HttpEncoding(Name, quality);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HttpEncoding Parse(string encoding) {
|
|
||||||
string[] parts = encoding.Split(';', StringSplitOptions.TrimEntries);
|
|
||||||
float quality = 1f;
|
|
||||||
encoding = parts[0];
|
|
||||||
|
|
||||||
for(int i = 1; i < parts.Length; ++i)
|
|
||||||
if(parts[i].StartsWith(@"q=")) {
|
|
||||||
if(!float.TryParse(parts[i], out quality))
|
|
||||||
quality = 1f;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new HttpEncoding(encoding, quality);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
sb.Append(Name);
|
|
||||||
if(Quality is >= 0f and < 1f)
|
|
||||||
sb.AppendFormat(CultureInfo.InvariantCulture, @";q={0:0.0}", Quality);
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CompareTo(HttpEncoding? other) {
|
|
||||||
if(!other.HasValue || other.Value.Quality < Quality)
|
|
||||||
return -1;
|
|
||||||
if(other.Value.Quality > Quality)
|
|
||||||
return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(HttpEncoding? other) {
|
|
||||||
return other.HasValue && Name.Equals(other.Value.Name) && Quality.Equals(other.Value.Quality);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public class HttpException : Exception {
|
|
||||||
public HttpException(string message) : base(message) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HttpConnectionManagerException : HttpException {
|
|
||||||
public HttpConnectionManagerException(string message) : base(message) { }
|
|
||||||
}
|
|
||||||
public class HttpConnectionManagerLockException : HttpConnectionManagerException {
|
|
||||||
public HttpConnectionManagerLockException() : base(@"Failed to lock the connection manager in time.") { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HttpTaskException : HttpException {
|
|
||||||
public HttpTaskException(string message) : base(message) { }
|
|
||||||
}
|
|
||||||
public class HttpTaskAlreadyStartedException : HttpTaskException {
|
|
||||||
public HttpTaskAlreadyStartedException() : base(@"Task has already started.") { }
|
|
||||||
}
|
|
||||||
public class HttpTaskInvalidStateException : HttpTaskException {
|
|
||||||
public HttpTaskInvalidStateException() : base(@"Task has ended up in an invalid state.") { }
|
|
||||||
}
|
|
||||||
public class HttpTaskNoAddressesException : HttpTaskException {
|
|
||||||
public HttpTaskNoAddressesException() : base(@"Could not find any addresses for this host.") { }
|
|
||||||
}
|
|
||||||
public class HttpTaskNoConnectionException : HttpTaskException {
|
|
||||||
public HttpTaskNoConnectionException() : base(@"Was unable to create a connection with this host.") { }
|
|
||||||
}
|
|
||||||
public class HttpTaskRequestFailedException : HttpTaskException {
|
|
||||||
public HttpTaskRequestFailedException() : base(@"Request failed for unknown reasons.") { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HttpTaskManagerException : HttpException {
|
|
||||||
public HttpTaskManagerException(string message) : base(message) { }
|
|
||||||
}
|
|
||||||
public class HttpTaskManagerLockException : HttpTaskManagerException {
|
|
||||||
public HttpTaskManagerLockException() : base(@"Failed to reserve a thread.") { }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public readonly struct HttpMediaType : IComparable<HttpMediaType?>, IEquatable<HttpMediaType?> {
|
|
||||||
public const string TYPE_APPLICATION = @"application";
|
|
||||||
public const string TYPE_AUDIO = @"audio";
|
|
||||||
public const string TYPE_IMAGE = @"image";
|
|
||||||
public const string TYPE_MESSAGE = @"message";
|
|
||||||
public const string TYPE_MULTIPART = @"multipart";
|
|
||||||
public const string TYPE_TEXT = @"text";
|
|
||||||
public const string TYPE_VIDEO = @"video";
|
|
||||||
|
|
||||||
public static readonly HttpMediaType OctetStream = new(TYPE_APPLICATION, @"octet-stream");
|
|
||||||
public static readonly HttpMediaType FWIF = new(TYPE_APPLICATION, @"x.fwif");
|
|
||||||
public static readonly HttpMediaType JSON = new(TYPE_APPLICATION, @"json");
|
|
||||||
public static readonly HttpMediaType HTML = new(TYPE_TEXT, @"html", args: new[] { Param.UTF8 });
|
|
||||||
|
|
||||||
public string Type { get; }
|
|
||||||
public string Subtype { get; }
|
|
||||||
public string Suffix { get; }
|
|
||||||
public IEnumerable<Param> Params { get; }
|
|
||||||
|
|
||||||
public HttpMediaType(string type, string subtype, string suffix = null, IEnumerable<Param> args = null) {
|
|
||||||
Type = type ?? throw new ArgumentNullException(nameof(type));
|
|
||||||
Subtype = subtype ?? throw new ArgumentNullException(nameof(subtype));
|
|
||||||
Suffix = suffix ?? string.Empty;
|
|
||||||
Params = args ?? Enumerable.Empty<Param>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetParamValue(string name) {
|
|
||||||
foreach(Param param in Params)
|
|
||||||
if(param.Name.ToLowerInvariant() == name)
|
|
||||||
return param.Value;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static explicit operator HttpMediaType(string mediaTypeString) => Parse(mediaTypeString);
|
|
||||||
|
|
||||||
public static HttpMediaType Parse(string mediaTypeString) {
|
|
||||||
if(mediaTypeString == null)
|
|
||||||
throw new ArgumentNullException(nameof(mediaTypeString));
|
|
||||||
|
|
||||||
int slashIndex = mediaTypeString.IndexOf('/');
|
|
||||||
if(slashIndex == -1)
|
|
||||||
return OctetStream;
|
|
||||||
|
|
||||||
string type = mediaTypeString[..slashIndex];
|
|
||||||
string subtype = mediaTypeString[(slashIndex + 1)..];
|
|
||||||
string suffix = null;
|
|
||||||
IEnumerable<Param> args = null;
|
|
||||||
|
|
||||||
int paramIndex = subtype.IndexOf(';');
|
|
||||||
if(paramIndex != -1) {
|
|
||||||
args = subtype[(paramIndex + 1)..]
|
|
||||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
||||||
.Select(Param.Parse);
|
|
||||||
subtype = subtype[..paramIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
int suffixIndex = subtype.IndexOf('+');
|
|
||||||
if(suffixIndex != -1) {
|
|
||||||
suffix = subtype[(suffixIndex + 1)..];
|
|
||||||
subtype = subtype[..suffixIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
return new HttpMediaType(type, subtype, suffix, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
sb.AppendFormat(@"{0}/{1}", Type, Subtype);
|
|
||||||
if(!string.IsNullOrWhiteSpace(Suffix))
|
|
||||||
sb.AppendFormat(@"+{0}", Suffix);
|
|
||||||
if(Params.Any())
|
|
||||||
sb.AppendFormat(@";{0}", string.Join(';', Params));
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CompareTo(HttpMediaType? other) {
|
|
||||||
if(!other.HasValue)
|
|
||||||
return -1;
|
|
||||||
int type = Type.CompareTo(other.Value.Type);
|
|
||||||
if(type != 0)
|
|
||||||
return type;
|
|
||||||
int subtype = Subtype.CompareTo(other.Value.Subtype);
|
|
||||||
if(subtype != 0)
|
|
||||||
return subtype;
|
|
||||||
int suffix = Suffix.CompareTo(other.Value.Suffix);
|
|
||||||
if(suffix != 0)
|
|
||||||
return suffix;
|
|
||||||
int paramCount = Params.Count();
|
|
||||||
int args = paramCount - other.Value.Params.Count();
|
|
||||||
if(args != 0)
|
|
||||||
return args;
|
|
||||||
for(int i = 0; i < paramCount; ++i) {
|
|
||||||
args = Params.ElementAt(i).CompareTo(other.Value.Params.ElementAt(i));
|
|
||||||
if(args != 0)
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(HttpMediaType? other) {
|
|
||||||
if(!other.HasValue)
|
|
||||||
return false;
|
|
||||||
if(!Type.Equals(other.Value.Type) || !Subtype.Equals(other.Value.Subtype) || !Suffix.Equals(other.Value.Suffix))
|
|
||||||
return false;
|
|
||||||
int paramCount = Params.Count();
|
|
||||||
if(paramCount != other.Value.Params.Count())
|
|
||||||
return false;
|
|
||||||
for(int i = 0; i < paramCount; ++i)
|
|
||||||
if(!Params.ElementAt(i).Equals(other.Value.Params.ElementAt(i)))
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly struct Param : IComparable<Param?>, IEquatable<Param?> {
|
|
||||||
public const string CHARSET = @"charset";
|
|
||||||
|
|
||||||
public static readonly Param ASCII = new(CHARSET, @"us-ascii");
|
|
||||||
public static readonly Param UTF8 = new(CHARSET, @"utf-8");
|
|
||||||
|
|
||||||
public string Name { get; }
|
|
||||||
public string Value { get; }
|
|
||||||
|
|
||||||
public Param(string name, string value) {
|
|
||||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
|
||||||
Value = value ?? throw new ArgumentNullException(nameof(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() {
|
|
||||||
return string.Format(@"{0}={1}", Name, Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static explicit operator Param(string paramStr) => Parse(paramStr);
|
|
||||||
|
|
||||||
public static Param Parse(string paramStr) {
|
|
||||||
string[] parts = (paramStr ?? throw new ArgumentNullException(nameof(paramStr))).Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
||||||
return new Param(parts[0], parts[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CompareTo(Param? other) {
|
|
||||||
if(!other.HasValue)
|
|
||||||
return -1;
|
|
||||||
int name = Name.CompareTo(other.Value.Name);
|
|
||||||
return name != 0
|
|
||||||
? name
|
|
||||||
: Value.CompareTo(other.Value.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Param? other) {
|
|
||||||
return other.HasValue && Name.Equals(other.Value.Name) && Value.Equals(other.Value.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
using Hamakaze.Headers;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public abstract class HttpMessage : IDisposable {
|
|
||||||
public abstract string ProtocolVersion { get; }
|
|
||||||
public abstract IEnumerable<HttpHeader> Headers { get; }
|
|
||||||
public abstract Stream Body { get; }
|
|
||||||
|
|
||||||
public virtual bool HasBody => Body != null;
|
|
||||||
|
|
||||||
protected bool OwnsBodyStream { get; set; }
|
|
||||||
|
|
||||||
public virtual IEnumerable<HttpHeader> GetHeader(string header) {
|
|
||||||
header = HttpHeader.NormaliseName(header);
|
|
||||||
return Headers.Where(h => h.Name == header);
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual bool HasHeader(string header) {
|
|
||||||
header = HttpHeader.NormaliseName(header);
|
|
||||||
return Headers.Any(h => h.Name == header);
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual string GetHeaderLine(string header) {
|
|
||||||
return string.Join(@", ", GetHeader(header).Select(h => h.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDisposed;
|
|
||||||
~HttpMessage()
|
|
||||||
=> DoDispose();
|
|
||||||
public void Dispose() {
|
|
||||||
DoDispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
protected void DoDispose() {
|
|
||||||
if(IsDisposed)
|
|
||||||
return;
|
|
||||||
IsDisposed = true;
|
|
||||||
if(OwnsBodyStream && Body != null)
|
|
||||||
Body.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,190 +0,0 @@
|
||||||
using Hamakaze.Headers;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public class HttpRequestMessage : HttpMessage {
|
|
||||||
public const string GET = @"GET";
|
|
||||||
public const string PUT = @"PUT";
|
|
||||||
public const string HEAD = @"HEAD";
|
|
||||||
public const string POST = @"POST";
|
|
||||||
public const string DELETE = @"DELETE";
|
|
||||||
|
|
||||||
public override string ProtocolVersion => @"1.1";
|
|
||||||
|
|
||||||
public string Method { get; }
|
|
||||||
public string RequestTarget { get; }
|
|
||||||
|
|
||||||
public bool IsSecure { get; }
|
|
||||||
|
|
||||||
public string Host { get; }
|
|
||||||
public ushort Port { get; }
|
|
||||||
public bool IsDefaultPort { get; }
|
|
||||||
|
|
||||||
public override IEnumerable<HttpHeader> Headers => HeaderList;
|
|
||||||
private List<HttpHeader> HeaderList { get; } = new();
|
|
||||||
|
|
||||||
private Stream BodyStream { get; set; }
|
|
||||||
public override Stream Body {
|
|
||||||
get {
|
|
||||||
if(BodyStream == null) {
|
|
||||||
OwnsBodyStream = true;
|
|
||||||
SetBody(new MemoryStream());
|
|
||||||
}
|
|
||||||
return BodyStream;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly string[] HEADERS_READONLY = new[] {
|
|
||||||
HttpHostHeader.NAME, HttpContentLengthHeader.NAME,
|
|
||||||
};
|
|
||||||
private static readonly string[] HEADERS_SINGLE = new[] {
|
|
||||||
HttpUserAgentHeader.NAME, HttpConnectionHeader.NAME, HttpAcceptEncodingHeader.NAME,
|
|
||||||
};
|
|
||||||
|
|
||||||
public IEnumerable<HttpEncoding> AcceptedEncodings {
|
|
||||||
get => HeaderList.Where(x => x.Name == HttpAcceptEncodingHeader.NAME).Cast<HttpAcceptEncodingHeader>().FirstOrDefault()?.Encodings
|
|
||||||
?? Enumerable.Empty<HttpEncoding>();
|
|
||||||
|
|
||||||
set {
|
|
||||||
HeaderList.RemoveAll(x => x.Name == HttpAcceptEncodingHeader.NAME);
|
|
||||||
HeaderList.Add(new HttpAcceptEncodingHeader(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string UserAgent {
|
|
||||||
get => HeaderList.FirstOrDefault(x => x.Name == HttpUserAgentHeader.NAME)?.Value.ToString()
|
|
||||||
?? string.Empty;
|
|
||||||
set {
|
|
||||||
HeaderList.RemoveAll(x => x.Name == HttpUserAgentHeader.NAME);
|
|
||||||
HeaderList.Add(new HttpUserAgentHeader(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Connection {
|
|
||||||
get => HeaderList.FirstOrDefault(x => x.Name == HttpConnectionHeader.NAME)?.Value.ToString()
|
|
||||||
?? string.Empty;
|
|
||||||
set {
|
|
||||||
HeaderList.RemoveAll(x => x.Name == HttpConnectionHeader.NAME);
|
|
||||||
HeaderList.Add(new HttpConnectionHeader(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpMediaType ContentType {
|
|
||||||
get => HeaderList.Where(x => x.Name == HttpContentTypeHeader.NAME).Cast<HttpContentTypeHeader>().FirstOrDefault()?.MediaType
|
|
||||||
?? HttpMediaType.OctetStream;
|
|
||||||
set {
|
|
||||||
HeaderList.RemoveAll(x => x.Name == HttpContentTypeHeader.NAME);
|
|
||||||
HeaderList.Add(new HttpContentTypeHeader(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpRequestMessage(string method, string uri) : this(
|
|
||||||
method, new Uri(uri)
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public const ushort HTTP = 80;
|
|
||||||
public const ushort HTTPS = 443;
|
|
||||||
|
|
||||||
public HttpRequestMessage(string method, Uri uri) {
|
|
||||||
Method = method ?? throw new ArgumentNullException(nameof(method));
|
|
||||||
RequestTarget = uri.PathAndQuery;
|
|
||||||
IsSecure = uri.Scheme.Equals(@"https", StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
Host = uri.Host;
|
|
||||||
ushort defaultPort = (IsSecure ? HTTPS : HTTP);
|
|
||||||
Port = uri.Port == -1 ? defaultPort : (ushort)uri.Port;
|
|
||||||
IsDefaultPort = Port == defaultPort;
|
|
||||||
HeaderList.Add(new HttpHostHeader(Host, IsDefaultPort ? -1 : Port));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsHeaderReadOnly(string name)
|
|
||||||
=> HEADERS_READONLY.Contains(name ?? throw new ArgumentNullException(nameof(name)));
|
|
||||||
public static bool IsHeaderSingleInstance(string name)
|
|
||||||
=> HEADERS_SINGLE.Contains(name ?? throw new ArgumentNullException(nameof(name)));
|
|
||||||
|
|
||||||
public void SetHeader(string name, object value) {
|
|
||||||
name = HttpHeader.NormaliseName(name ?? throw new ArgumentNullException(nameof(name)));
|
|
||||||
if(IsHeaderReadOnly(name))
|
|
||||||
throw new ArgumentException(@"This header is read-only.", nameof(name));
|
|
||||||
HeaderList.RemoveAll(x => x.Name == name);
|
|
||||||
HeaderList.Add(HttpHeader.Create(name, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddHeader(string name, object value) {
|
|
||||||
name = HttpHeader.NormaliseName(name ?? throw new ArgumentNullException(nameof(name)));
|
|
||||||
if(IsHeaderReadOnly(name))
|
|
||||||
throw new ArgumentException(@"This header is read-only.", nameof(name));
|
|
||||||
if(IsHeaderSingleInstance(name))
|
|
||||||
HeaderList.RemoveAll(x => x.Name == name);
|
|
||||||
HeaderList.Add(HttpHeader.Create(name, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveHeader(string name) {
|
|
||||||
name = HttpHeader.NormaliseName(name ?? throw new ArgumentNullException(nameof(name)));
|
|
||||||
if(IsHeaderReadOnly(name))
|
|
||||||
throw new ArgumentException(@"This header is read-only.", nameof(name));
|
|
||||||
HeaderList.RemoveAll(x => x.Name == name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetBody(Stream stream) {
|
|
||||||
if(stream == null) {
|
|
||||||
if(OwnsBodyStream)
|
|
||||||
BodyStream?.Dispose();
|
|
||||||
OwnsBodyStream = false;
|
|
||||||
BodyStream = null;
|
|
||||||
HeaderList.RemoveAll(x => x.Name == HttpContentLengthHeader.NAME);
|
|
||||||
} else {
|
|
||||||
if(!stream.CanRead || !stream.CanSeek)
|
|
||||||
throw new ArgumentException(@"Body must readable and seekable.", nameof(stream));
|
|
||||||
if(OwnsBodyStream)
|
|
||||||
BodyStream?.Dispose();
|
|
||||||
OwnsBodyStream = false;
|
|
||||||
BodyStream = stream;
|
|
||||||
HeaderList.Add(new HttpContentLengthHeader(BodyStream));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetBody(byte[] buffer) {
|
|
||||||
SetBody(new MemoryStream(buffer));
|
|
||||||
OwnsBodyStream = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetBody(string str, Encoding encoding = null) {
|
|
||||||
SetBody((encoding ?? Encoding.UTF8).GetBytes(str));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteTo(Stream stream, Action<long, long> onProgress = null) {
|
|
||||||
using(StreamWriter sw = new(stream, new ASCIIEncoding(), leaveOpen: true)) {
|
|
||||||
sw.NewLine = "\r\n";
|
|
||||||
sw.Write(Method);
|
|
||||||
sw.Write(' ');
|
|
||||||
sw.Write(RequestTarget);
|
|
||||||
sw.Write(@" HTTP/");
|
|
||||||
sw.WriteLine(ProtocolVersion);
|
|
||||||
foreach(HttpHeader header in Headers)
|
|
||||||
sw.WriteLine(header);
|
|
||||||
sw.WriteLine();
|
|
||||||
sw.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(BodyStream != null) {
|
|
||||||
const int bufferSize = 8192;
|
|
||||||
byte[] buffer = new byte[bufferSize];
|
|
||||||
int read;
|
|
||||||
long totalRead = 0;
|
|
||||||
|
|
||||||
onProgress?.Invoke(totalRead, BodyStream.Length);
|
|
||||||
|
|
||||||
BodyStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
while((read = BodyStream.Read(buffer, 0, bufferSize)) > 0) {
|
|
||||||
stream.Write(buffer, 0, read);
|
|
||||||
totalRead += read;
|
|
||||||
onProgress?.Invoke(totalRead, BodyStream.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,265 +0,0 @@
|
||||||
using Hamakaze.Headers;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public class HttpResponseMessage : HttpMessage {
|
|
||||||
public override string ProtocolVersion { get; }
|
|
||||||
public int StatusCode { get; }
|
|
||||||
public string StatusMessage { get; }
|
|
||||||
|
|
||||||
public override IEnumerable<HttpHeader> Headers { get; }
|
|
||||||
|
|
||||||
public override Stream Body { get; }
|
|
||||||
|
|
||||||
public string Connection
|
|
||||||
=> Headers.FirstOrDefault(x => x.Name == HttpConnectionHeader.NAME)?.Value.ToString() ?? string.Empty;
|
|
||||||
public string Server
|
|
||||||
=> Headers.FirstOrDefault(x => x.Name == HttpServerHeader.NAME)?.Value.ToString() ?? string.Empty;
|
|
||||||
public DateTimeOffset Date
|
|
||||||
=> Headers.Where(x => x.Name == HttpDateHeader.NAME).Cast<HttpDateHeader>().FirstOrDefault()?.DateTime ?? DateTimeOffset.MinValue;
|
|
||||||
public HttpMediaType ContentType
|
|
||||||
=> Headers.Where(x => x.Name == HttpContentTypeHeader.NAME).Cast<HttpContentTypeHeader>().FirstOrDefault()?.MediaType
|
|
||||||
?? HttpMediaType.OctetStream;
|
|
||||||
public Encoding ResponseEncoding
|
|
||||||
=> Encoding.GetEncoding(ContentType.GetParamValue(@"charset") ?? @"iso8859-1");
|
|
||||||
public IEnumerable<string> ContentEncodings
|
|
||||||
=> Headers.Where(x => x.Name == HttpContentEncodingHeader.NAME).Cast<HttpContentEncodingHeader>().FirstOrDefault()?.Encodings
|
|
||||||
?? Enumerable.Empty<string>();
|
|
||||||
public IEnumerable<string> TransferEncodings
|
|
||||||
=> Headers.Where(x => x.Name == HttpTransferEncodingHeader.NAME).Cast<HttpTransferEncodingHeader>().FirstOrDefault()?.Encodings
|
|
||||||
?? Enumerable.Empty<string>();
|
|
||||||
|
|
||||||
public HttpResponseMessage(
|
|
||||||
int statusCode, string statusMessage, string protocolVersion,
|
|
||||||
IEnumerable<HttpHeader> headers, Stream body
|
|
||||||
) {
|
|
||||||
ProtocolVersion = protocolVersion ?? throw new ArgumentNullException(nameof(protocolVersion));
|
|
||||||
StatusCode = statusCode;
|
|
||||||
StatusMessage = statusMessage ?? string.Empty;
|
|
||||||
Headers = (headers ?? throw new ArgumentNullException(nameof(headers))).ToArray();
|
|
||||||
OwnsBodyStream = true;
|
|
||||||
Body = body;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] GetBodyBytes() {
|
|
||||||
if(Body == null)
|
|
||||||
return null;
|
|
||||||
if(Body is MemoryStream msBody)
|
|
||||||
return msBody.ToArray();
|
|
||||||
using MemoryStream ms = new();
|
|
||||||
if(Body.CanSeek)
|
|
||||||
Body.Seek(0, SeekOrigin.Begin);
|
|
||||||
Body.CopyTo(ms);
|
|
||||||
return ms.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetBodyString() {
|
|
||||||
byte[] bytes = GetBodyBytes();
|
|
||||||
return bytes == null || bytes.Length < 1
|
|
||||||
? string.Empty
|
|
||||||
: ResponseEncoding.GetString(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// there's probably a less stupid way to do this, be my guest and call me an idiot
|
|
||||||
private static void ProcessEncoding(Stack<string> encodings, Stream stream, bool transfer) {
|
|
||||||
using MemoryStream temp = new();
|
|
||||||
bool inTemp = false;
|
|
||||||
|
|
||||||
while(encodings.TryPop(out string encoding)) {
|
|
||||||
Stream target = (inTemp = !inTemp) ? temp : stream,
|
|
||||||
source = inTemp ? stream : temp;
|
|
||||||
|
|
||||||
target.SetLength(0);
|
|
||||||
source.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
switch(encoding) {
|
|
||||||
case HttpEncoding.GZIP:
|
|
||||||
case HttpEncoding.XGZIP:
|
|
||||||
using(GZipStream gzs = new(source, CompressionMode.Decompress, true))
|
|
||||||
gzs.CopyTo(target);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HttpEncoding.DEFLATE:
|
|
||||||
using(DeflateStream def = new(source, CompressionMode.Decompress, true))
|
|
||||||
def.CopyTo(target);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HttpEncoding.BROTLI:
|
|
||||||
if(transfer)
|
|
||||||
goto default;
|
|
||||||
using(BrotliStream br = new(source, CompressionMode.Decompress, true))
|
|
||||||
br.CopyTo(target);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HttpEncoding.IDENTITY:
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HttpEncoding.CHUNKED:
|
|
||||||
if(!transfer)
|
|
||||||
goto default;
|
|
||||||
throw new IOException(@"Invalid use of chunked encoding type in Transfer-Encoding header.");
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new IOException(@"Unsupported encoding supplied.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(inTemp) {
|
|
||||||
stream.SetLength(0);
|
|
||||||
temp.Seek(0, SeekOrigin.Begin);
|
|
||||||
temp.CopyTo(stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HttpResponseMessage ReadFrom(Stream stream, Action<long, long> onProgress = null) {
|
|
||||||
// ignore this function, it doesn't exist
|
|
||||||
string readLine() {
|
|
||||||
const ushort crlf = 0x0D0A;
|
|
||||||
using MemoryStream ms = new();
|
|
||||||
int byt; ushort lastTwo = 0;
|
|
||||||
|
|
||||||
for(; ; ) {
|
|
||||||
byt = stream.ReadByte();
|
|
||||||
if(byt == -1 && ms.Length == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
ms.WriteByte((byte)byt);
|
|
||||||
|
|
||||||
lastTwo <<= 8;
|
|
||||||
lastTwo |= (byte)byt;
|
|
||||||
if(lastTwo == crlf) {
|
|
||||||
ms.SetLength(ms.Length - 2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Encoding.ASCII.GetString(ms.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
long contentLength = -1;
|
|
||||||
Stack<string> transferEncodings = null;
|
|
||||||
Stack<string> contentEncodings = null;
|
|
||||||
|
|
||||||
// Read initial header
|
|
||||||
string line = readLine();
|
|
||||||
if(line == null)
|
|
||||||
throw new IOException(@"Failed to read initial HTTP header.");
|
|
||||||
if(!line.StartsWith(@"HTTP/"))
|
|
||||||
throw new IOException(@"Response is not a valid HTTP message.");
|
|
||||||
string[] parts = line[5..].Split(' ', 3);
|
|
||||||
if(!int.TryParse(parts.ElementAtOrDefault(1), out int statusCode))
|
|
||||||
throw new IOException(@"Invalid HTTP status code format.");
|
|
||||||
string protocolVersion = parts.ElementAtOrDefault(0);
|
|
||||||
string statusMessage = parts.ElementAtOrDefault(2);
|
|
||||||
|
|
||||||
// Read header key-value pairs
|
|
||||||
List<HttpHeader> headers = new();
|
|
||||||
|
|
||||||
while((line = readLine()) != null) {
|
|
||||||
if(string.IsNullOrWhiteSpace(line))
|
|
||||||
break;
|
|
||||||
|
|
||||||
parts = line.Split(':', 2, StringSplitOptions.TrimEntries);
|
|
||||||
if(parts.Length < 2)
|
|
||||||
throw new IOException(@"Invalid HTTP header in response.");
|
|
||||||
|
|
||||||
string hName = HttpHeader.NormaliseName(parts.ElementAtOrDefault(0) ?? string.Empty),
|
|
||||||
hValue = parts.ElementAtOrDefault(1);
|
|
||||||
if(string.IsNullOrEmpty(hName))
|
|
||||||
throw new IOException(@"Invalid HTTP header name.");
|
|
||||||
|
|
||||||
HttpHeader header = HttpHeader.Create(hName, hValue);
|
|
||||||
|
|
||||||
if(header is HttpContentLengthHeader hclh)
|
|
||||||
contentLength = (long)hclh.Value;
|
|
||||||
else if(header is HttpTransferEncodingHeader hteh)
|
|
||||||
transferEncodings = new Stack<string>(hteh.Encodings);
|
|
||||||
else if(header is HttpContentEncodingHeader hceh)
|
|
||||||
contentEncodings = new Stack<string>(hceh.Encodings);
|
|
||||||
|
|
||||||
headers.Add(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(statusCode is < 200 or 201 or 204 or 205)
|
|
||||||
contentLength = 0;
|
|
||||||
|
|
||||||
Stream body = null;
|
|
||||||
long totalRead = 0;
|
|
||||||
const int buffer_size = 8192;
|
|
||||||
byte[] buffer = new byte[buffer_size];
|
|
||||||
int read;
|
|
||||||
|
|
||||||
void readBuffer(long length = -1) {
|
|
||||||
if(length == 0)
|
|
||||||
return;
|
|
||||||
long remaining = length;
|
|
||||||
int bufferRead = buffer_size;
|
|
||||||
if(bufferRead > length)
|
|
||||||
bufferRead = (int)length;
|
|
||||||
|
|
||||||
if(totalRead < 1)
|
|
||||||
onProgress?.Invoke(0, contentLength);
|
|
||||||
|
|
||||||
while((read = stream.Read(buffer, 0, bufferRead)) > 0) {
|
|
||||||
body.Write(buffer, 0, read);
|
|
||||||
|
|
||||||
totalRead += read;
|
|
||||||
onProgress?.Invoke(totalRead, contentLength);
|
|
||||||
|
|
||||||
if(length >= 0) {
|
|
||||||
remaining -= read;
|
|
||||||
if(remaining < 1)
|
|
||||||
break;
|
|
||||||
if(bufferRead > remaining)
|
|
||||||
bufferRead = (int)remaining;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read body
|
|
||||||
if(transferEncodings != null && transferEncodings.Any() && transferEncodings.Peek() == HttpEncoding.CHUNKED) {
|
|
||||||
// oh no the poop is chunky
|
|
||||||
transferEncodings.Pop();
|
|
||||||
body = new MemoryStream();
|
|
||||||
|
|
||||||
while((line = readLine()) != null) {
|
|
||||||
if(string.IsNullOrWhiteSpace(line))
|
|
||||||
break;
|
|
||||||
if(!int.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int chunkLength))
|
|
||||||
throw new IOException(@"Failed to decode chunk length.");
|
|
||||||
if(chunkLength == 0) // final chunk
|
|
||||||
break;
|
|
||||||
readBuffer(chunkLength);
|
|
||||||
readLine();
|
|
||||||
}
|
|
||||||
readLine();
|
|
||||||
} else if(contentLength != 0) {
|
|
||||||
body = new MemoryStream();
|
|
||||||
readBuffer(contentLength);
|
|
||||||
readLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(body != null)
|
|
||||||
// Check if body is empty and null it again if so
|
|
||||||
if(body.Length == 0) {
|
|
||||||
body.Dispose();
|
|
||||||
body = null;
|
|
||||||
} else {
|
|
||||||
if(transferEncodings != null)
|
|
||||||
ProcessEncoding(transferEncodings, body, true);
|
|
||||||
if(contentEncodings != null)
|
|
||||||
ProcessEncoding(contentEncodings, body, false);
|
|
||||||
|
|
||||||
body.Seek(0, SeekOrigin.Begin);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new HttpResponseMessage(statusCode, statusMessage, protocolVersion, headers, body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,189 +0,0 @@
|
||||||
using Hamakaze.Headers;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public class HttpTask {
|
|
||||||
public TaskState State { get; private set; } = TaskState.Initial;
|
|
||||||
|
|
||||||
public bool IsStarted
|
|
||||||
=> State != TaskState.Initial;
|
|
||||||
public bool IsFinished
|
|
||||||
=> State == TaskState.Finished;
|
|
||||||
public bool IsCancelled
|
|
||||||
=> State == TaskState.Cancelled;
|
|
||||||
public bool IsErrored
|
|
||||||
=> Exception != null;
|
|
||||||
|
|
||||||
public Exception Exception { get; private set; }
|
|
||||||
|
|
||||||
public HttpRequestMessage Request { get; }
|
|
||||||
public HttpResponseMessage Response { get; private set; }
|
|
||||||
private HttpConnectionManager Connections { get; }
|
|
||||||
|
|
||||||
private IEnumerable<IPAddress> Addresses { get; set; }
|
|
||||||
private HttpConnection Connection { get; set; }
|
|
||||||
|
|
||||||
public bool DisposeRequest { get; set; }
|
|
||||||
public bool DisposeResponse { get; set; }
|
|
||||||
|
|
||||||
public event Action<HttpTask, HttpResponseMessage> OnComplete;
|
|
||||||
public event Action<HttpTask, Exception> OnError;
|
|
||||||
public event Action<HttpTask> OnCancel;
|
|
||||||
public event Action<HttpTask, long, long> OnUploadProgress;
|
|
||||||
public event Action<HttpTask, long, long> OnDownloadProgress;
|
|
||||||
public event Action<HttpTask, TaskState> OnStateChange;
|
|
||||||
|
|
||||||
public HttpTask(HttpConnectionManager conns, HttpRequestMessage request, bool disposeRequest, bool disposeResponse) {
|
|
||||||
Connections = conns ?? throw new ArgumentNullException(nameof(conns));
|
|
||||||
Request = request ?? throw new ArgumentNullException(nameof(request));
|
|
||||||
DisposeRequest = disposeRequest;
|
|
||||||
DisposeResponse = disposeResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Run() {
|
|
||||||
if(IsStarted)
|
|
||||||
throw new HttpTaskAlreadyStartedException();
|
|
||||||
while(NextStep());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Cancel() {
|
|
||||||
State = TaskState.Cancelled;
|
|
||||||
OnStateChange?.Invoke(this, State);
|
|
||||||
OnCancel?.Invoke(this);
|
|
||||||
if(DisposeResponse)
|
|
||||||
Response?.Dispose();
|
|
||||||
if(DisposeRequest)
|
|
||||||
Request?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Error(Exception ex) {
|
|
||||||
Exception = ex;
|
|
||||||
OnError?.Invoke(this, ex);
|
|
||||||
Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool NextStep() {
|
|
||||||
if(IsCancelled)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
switch(State) {
|
|
||||||
case TaskState.Initial:
|
|
||||||
State = TaskState.Lookup;
|
|
||||||
OnStateChange?.Invoke(this, State);
|
|
||||||
DoLookup();
|
|
||||||
break;
|
|
||||||
case TaskState.Lookup:
|
|
||||||
State = TaskState.Request;
|
|
||||||
OnStateChange?.Invoke(this, State);
|
|
||||||
DoRequest();
|
|
||||||
break;
|
|
||||||
case TaskState.Request:
|
|
||||||
State = TaskState.Response;
|
|
||||||
OnStateChange?.Invoke(this, State);
|
|
||||||
DoResponse();
|
|
||||||
break;
|
|
||||||
case TaskState.Response:
|
|
||||||
State = TaskState.Finished;
|
|
||||||
OnStateChange?.Invoke(this, State);
|
|
||||||
OnComplete?.Invoke(this, Response);
|
|
||||||
if(DisposeResponse)
|
|
||||||
Response?.Dispose();
|
|
||||||
if(DisposeRequest)
|
|
||||||
Request?.Dispose();
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
Error(new HttpTaskInvalidStateException());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DoLookup() {
|
|
||||||
try {
|
|
||||||
Addresses = Dns.GetHostAddresses(Request.Host);
|
|
||||||
} catch(Exception ex) {
|
|
||||||
Error(ex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!Addresses.Any())
|
|
||||||
Error(new HttpTaskNoAddressesException());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DoRequest() {
|
|
||||||
Exception exception = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
foreach(IPAddress addr in Addresses) {
|
|
||||||
int tries = 0;
|
|
||||||
IPEndPoint endPoint = new(addr, Request.Port);
|
|
||||||
|
|
||||||
exception = null;
|
|
||||||
Connection = Connections.GetConnection(Request.Host, endPoint, Request.IsSecure);
|
|
||||||
|
|
||||||
retry:
|
|
||||||
++tries;
|
|
||||||
try {
|
|
||||||
Request.WriteTo(Connection.Stream, (p, t) => OnUploadProgress?.Invoke(this, p, t));
|
|
||||||
break;
|
|
||||||
} catch(IOException ex) {
|
|
||||||
Connection.Dispose();
|
|
||||||
Connection = Connections.GetConnection(Request.Host, endPoint, Request.IsSecure);
|
|
||||||
|
|
||||||
if(tries < 2)
|
|
||||||
goto retry;
|
|
||||||
|
|
||||||
exception = ex;
|
|
||||||
continue;
|
|
||||||
} finally {
|
|
||||||
Connection.MarkUsed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(Exception ex) {
|
|
||||||
Error(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(exception != null)
|
|
||||||
Error(exception);
|
|
||||||
else if(Connection == null)
|
|
||||||
Error(new HttpTaskNoConnectionException());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DoResponse() {
|
|
||||||
try {
|
|
||||||
Response = HttpResponseMessage.ReadFrom(Connection.Stream, (p, t) => OnDownloadProgress?.Invoke(this, p, t));
|
|
||||||
} catch(Exception ex) {
|
|
||||||
Error(ex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Response.Connection == HttpConnectionHeader.CLOSE)
|
|
||||||
Connection.Dispose();
|
|
||||||
if(Response == null)
|
|
||||||
Error(new HttpTaskRequestFailedException());
|
|
||||||
|
|
||||||
HttpKeepAliveHeader hkah = Response.Headers.Where(x => x.Name == HttpKeepAliveHeader.NAME).Cast<HttpKeepAliveHeader>().FirstOrDefault();
|
|
||||||
if(hkah != null) {
|
|
||||||
Connection.MaxIdle = hkah.MaxIdle;
|
|
||||||
Connection.MaxRequests = hkah.MaxRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
Connection.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum TaskState {
|
|
||||||
Initial = 0,
|
|
||||||
Lookup = 10,
|
|
||||||
Request = 20,
|
|
||||||
Response = 30,
|
|
||||||
Finished = 40,
|
|
||||||
|
|
||||||
Cancelled = -1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Hamakaze {
|
|
||||||
public class HttpTaskManager : IDisposable {
|
|
||||||
private Semaphore Lock { get; set; }
|
|
||||||
|
|
||||||
public HttpTaskManager(int maxThreads = 5) {
|
|
||||||
Lock = new Semaphore(maxThreads, maxThreads);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RunTask(HttpTask task) {
|
|
||||||
if(task == null)
|
|
||||||
throw new ArgumentNullException(nameof(task));
|
|
||||||
if(!Lock.WaitOne())
|
|
||||||
throw new HttpTaskManagerLockException();
|
|
||||||
new Thread(() => {
|
|
||||||
try {
|
|
||||||
task.Run();
|
|
||||||
} finally {
|
|
||||||
Lock?.Release();
|
|
||||||
}
|
|
||||||
}).Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDisposed;
|
|
||||||
~HttpTaskManager()
|
|
||||||
=> DoDispose();
|
|
||||||
public void Dispose() {
|
|
||||||
DoDispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
private void DoDispose() {
|
|
||||||
if(IsDisposed)
|
|
||||||
return;
|
|
||||||
IsDisposed = true;
|
|
||||||
Lock.Dispose();
|
|
||||||
Lock = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
1323
Protocol-draft.md
1323
Protocol-draft.md
File diff suppressed because it is too large
Load diff
1225
Protocol.md
1225
Protocol.md
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,10 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 16
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 16.0.29025.244
|
VisualStudioVersion = 17.2.32630.192
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpChat", "SharpChat\SharpChat.csproj", "{DDB24C19-B802-4C96-AC15-0449C6FC77F2}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpChat", "SharpChat\SharpChat.csproj", "{DDB24C19-B802-4C96-AC15-0449C6FC77F2}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hamakaze", "Hamakaze\Hamakaze.csproj", "{6059200F-141C-42A5-AA3F-E38C9721AEC8}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -17,10 +15,6 @@ Global
|
||||||
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Release|Any CPU.Build.0 = Release|Any CPU
|
{DDB24C19-B802-4C96-AC15-0449C6FC77F2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{6059200F-141C-42A5-AA3F-E38C9721AEC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{6059200F-141C-42A5-AA3F-E38C9721AEC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{6059200F-141C-42A5-AA3F-E38C9721AEC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{6059200F-141C-42A5-AA3F-E38C9721AEC8}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -144,19 +144,24 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshFlashiiBans() {
|
public void RefreshFlashiiBans() {
|
||||||
FlashiiBan.GetList(bans => {
|
FlashiiBan.GetList(SockChatServer.HttpClient).ContinueWith(x => {
|
||||||
if(!bans.Any())
|
if(x.IsFaulted) {
|
||||||
|
Logger.Write($@"Ban Refresh: {x.Exception}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!x.Result.Any())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lock(BanList) {
|
lock(BanList) {
|
||||||
foreach(FlashiiBan fb in bans) {
|
foreach(FlashiiBan fb in x.Result) {
|
||||||
if(!BanList.OfType<BannedUser>().Any(x => x.UserId == fb.UserId))
|
if(!BanList.OfType<BannedUser>().Any(x => x.UserId == fb.UserId))
|
||||||
Add(new BannedUser(fb));
|
Add(new BannedUser(fb));
|
||||||
if(!BanList.OfType<BannedIPAddress>().Any(x => x.Address.ToString() == fb.UserIP))
|
if(!BanList.OfType<BannedIPAddress>().Any(x => x.Address.ToString() == fb.UserIP))
|
||||||
Add(new BannedIPAddress(fb));
|
Add(new BannedIPAddress(fb));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, ex => Logger.Write($@"Ban Refresh: {ex}"));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IBan> All() {
|
public IEnumerable<IBan> All() {
|
||||||
|
|
|
@ -26,7 +26,7 @@ namespace SharpChat {
|
||||||
Channels = new ChannelManager(this);
|
Channels = new ChannelManager(this);
|
||||||
Events = new ChatEventManager(this);
|
Events = new ChatEventManager(this);
|
||||||
|
|
||||||
BumpTimer = new Timer(e => FlashiiBump.Submit(Users.WithActiveConnections()), null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
|
BumpTimer = new Timer(e => FlashiiBump.Submit(SockChatServer.HttpClient, Users.WithActiveConnections()), null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update() {
|
public void Update() {
|
||||||
|
|
|
@ -82,7 +82,6 @@ namespace SharpChat {
|
||||||
|
|
||||||
public string TargetName => @"@log";
|
public string TargetName => @"@log";
|
||||||
|
|
||||||
[Obsolete]
|
|
||||||
public ChatChannel Channel {
|
public ChatChannel Channel {
|
||||||
get {
|
get {
|
||||||
lock(Channels)
|
lock(Channels)
|
||||||
|
|
|
@ -20,6 +20,8 @@ namespace SharpChat {
|
||||||
public DateTimeOffset LastPing { get; set; } = DateTimeOffset.MinValue;
|
public DateTimeOffset LastPing { get; set; } = DateTimeOffset.MinValue;
|
||||||
public ChatUser User { get; set; }
|
public ChatUser User { get; set; }
|
||||||
|
|
||||||
|
private static int CloseCode { get; set; } = 1000;
|
||||||
|
|
||||||
public string TargetName => @"@log";
|
public string TargetName => @"@log";
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,6 +71,9 @@ namespace SharpChat {
|
||||||
public bool HasTimedOut
|
public bool HasTimedOut
|
||||||
=> DateTimeOffset.Now - LastPing > SessionTimeOut;
|
=> DateTimeOffset.Now - LastPing > SessionTimeOut;
|
||||||
|
|
||||||
|
public void PrepareForRestart()
|
||||||
|
=> CloseCode = 1012;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
=> Dispose(true);
|
=> Dispose(true);
|
||||||
|
|
||||||
|
@ -80,7 +85,7 @@ namespace SharpChat {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
Connection.Close();
|
Connection.Close(CloseCode);
|
||||||
|
|
||||||
if(disposing)
|
if(disposing)
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
using Hamakaze;
|
using Microsoft.Win32.SafeHandles;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SharpChat.Flashii {
|
namespace SharpChat.Flashii {
|
||||||
public class FlashiiAuthRequest {
|
public class FlashiiAuthRequest {
|
||||||
|
@ -47,13 +49,15 @@ namespace SharpChat.Flashii {
|
||||||
[JsonPropertyName(@"perms")]
|
[JsonPropertyName(@"perms")]
|
||||||
public ChatUserPermissions Permissions { get; set; }
|
public ChatUserPermissions Permissions { get; set; }
|
||||||
|
|
||||||
public static void Attempt(FlashiiAuthRequest authRequest, Action<FlashiiAuth> onComplete, Action<Exception> onError) {
|
public static async Task<FlashiiAuth> Attempt(HttpClient httpClient, FlashiiAuthRequest authRequest) {
|
||||||
|
if(httpClient == null)
|
||||||
|
throw new ArgumentNullException(nameof(httpClient));
|
||||||
if(authRequest == null)
|
if(authRequest == null)
|
||||||
throw new ArgumentNullException(nameof(authRequest));
|
throw new ArgumentNullException(nameof(authRequest));
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if(authRequest.UserId >= 10000) {
|
if (authRequest.UserId >= 10000)
|
||||||
onComplete(new FlashiiAuth {
|
return new FlashiiAuth {
|
||||||
Success = true,
|
Success = true,
|
||||||
UserId = authRequest.UserId,
|
UserId = authRequest.UserId,
|
||||||
Username = @"Misaka-" + (authRequest.UserId - 10000),
|
Username = @"Misaka-" + (authRequest.UserId - 10000),
|
||||||
|
@ -61,21 +65,21 @@ namespace SharpChat.Flashii {
|
||||||
Rank = 0,
|
Rank = 0,
|
||||||
SilencedUntil = DateTimeOffset.MinValue,
|
SilencedUntil = DateTimeOffset.MinValue,
|
||||||
Permissions = ChatUserPermissions.SendMessage | ChatUserPermissions.EditOwnMessage | ChatUserPermissions.DeleteOwnMessage,
|
Permissions = ChatUserPermissions.SendMessage | ChatUserPermissions.EditOwnMessage | ChatUserPermissions.DeleteOwnMessage,
|
||||||
});
|
};
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
HttpRequestMessage hrm = new HttpRequestMessage(@"POST", FlashiiUrls.AUTH);
|
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, FlashiiUrls.AuthURL) {
|
||||||
hrm.AddHeader(@"X-SharpChat-Signature", authRequest.Hash);
|
Content = new ByteArrayContent(authRequest.GetJSON()),
|
||||||
hrm.SetBody(authRequest.GetJSON());
|
Headers = {
|
||||||
HttpClient.Send(hrm, (t, r) => {
|
{ @"X-SharpChat-Signature", authRequest.Hash },
|
||||||
try {
|
},
|
||||||
onComplete(JsonSerializer.Deserialize<FlashiiAuth>(r.GetBodyBytes()));
|
};
|
||||||
} catch(Exception ex) {
|
|
||||||
onError(ex);
|
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||||
}
|
|
||||||
}, (t, e) => onError(e));
|
return JsonSerializer.Deserialize<FlashiiAuth>(
|
||||||
|
await response.Content.ReadAsByteArrayAsync()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
using Hamakaze;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SharpChat.Flashii {
|
namespace SharpChat.Flashii {
|
||||||
public class FlashiiBan {
|
public class FlashiiBan {
|
||||||
|
@ -20,21 +21,19 @@ namespace SharpChat.Flashii {
|
||||||
[JsonPropertyName(@"username")]
|
[JsonPropertyName(@"username")]
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
|
|
||||||
public static void GetList(Action<IEnumerable<FlashiiBan>> onComplete, Action<Exception> onError) {
|
public static async Task<IEnumerable<FlashiiBan>> GetList(HttpClient httpClient) {
|
||||||
if(onComplete == null)
|
if(httpClient == null)
|
||||||
throw new ArgumentNullException(nameof(onComplete));
|
throw new ArgumentNullException(nameof(httpClient));
|
||||||
if(onError == null)
|
|
||||||
throw new ArgumentNullException(nameof(onError));
|
|
||||||
|
|
||||||
HttpRequestMessage hrm = new HttpRequestMessage(@"GET", FlashiiUrls.BANS);
|
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, FlashiiUrls.BansURL) {
|
||||||
hrm.AddHeader(@"X-SharpChat-Signature", STRING.GetSignedHash());
|
Headers = {
|
||||||
HttpClient.Send(hrm, (t, r) => {
|
{ @"X-SharpChat-Signature", STRING.GetSignedHash() },
|
||||||
try {
|
},
|
||||||
onComplete(JsonSerializer.Deserialize<IEnumerable<FlashiiBan>>(r.GetBodyBytes()));
|
};
|
||||||
} catch(Exception ex) {
|
|
||||||
onError(ex);
|
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||||
}
|
|
||||||
}, (t, e) => onError(e));
|
return JsonSerializer.Deserialize<IEnumerable<FlashiiBan>>(await response.Content.ReadAsByteArrayAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
using Hamakaze;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SharpChat.Flashii {
|
namespace SharpChat.Flashii {
|
||||||
public class FlashiiBump {
|
public class FlashiiBump {
|
||||||
|
@ -13,14 +14,16 @@ namespace SharpChat.Flashii {
|
||||||
[JsonPropertyName(@"ip")]
|
[JsonPropertyName(@"ip")]
|
||||||
public string UserIP { get; set; }
|
public string UserIP { get; set; }
|
||||||
|
|
||||||
public static void Submit(IEnumerable<ChatUser> users) {
|
public static void Submit(HttpClient httpClient, IEnumerable<ChatUser> users) {
|
||||||
List<FlashiiBump> bups = users.Where(u => u.HasSessions).Select(x => new FlashiiBump { UserId = x.UserId, UserIP = x.RemoteAddresses.First().ToString() }).ToList();
|
List<FlashiiBump> bups = users.Where(u => u.HasSessions).Select(x => new FlashiiBump { UserId = x.UserId, UserIP = x.RemoteAddresses.First().ToString() }).ToList();
|
||||||
|
|
||||||
if (bups.Any())
|
if (bups.Any())
|
||||||
Submit(bups);
|
Submit(httpClient, bups);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Submit(IEnumerable<FlashiiBump> users) {
|
public static void Submit(HttpClient httpClient, IEnumerable<FlashiiBump> users) {
|
||||||
|
if(httpClient == null)
|
||||||
|
throw new ArgumentNullException(nameof(httpClient));
|
||||||
if(users == null)
|
if(users == null)
|
||||||
throw new ArgumentNullException(nameof(users));
|
throw new ArgumentNullException(nameof(users));
|
||||||
if(!users.Any())
|
if(!users.Any())
|
||||||
|
@ -28,10 +31,17 @@ namespace SharpChat.Flashii {
|
||||||
|
|
||||||
byte[] data = JsonSerializer.SerializeToUtf8Bytes(users);
|
byte[] data = JsonSerializer.SerializeToUtf8Bytes(users);
|
||||||
|
|
||||||
HttpRequestMessage hrm = new HttpRequestMessage(@"POST", FlashiiUrls.BUMP);
|
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, FlashiiUrls.BumpURL) {
|
||||||
hrm.AddHeader(@"X-SharpChat-Signature", data.GetSignedHash());
|
Content = new ByteArrayContent(data),
|
||||||
hrm.SetBody(data);
|
Headers = {
|
||||||
HttpClient.Send(hrm, onError: (t, e) => Logger.Write($@"Flashii Bump Error: {e}"));
|
{ @"X-SharpChat-Signature", data.GetSignedHash() },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
httpClient.SendAsync(request).ContinueWith(x => {
|
||||||
|
if(x.IsFaulted)
|
||||||
|
Logger.Write($@"Flashii Bump Error: {x.Exception}");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,35 @@
|
||||||
namespace SharpChat.Flashii {
|
using System.IO;
|
||||||
public static class FlashiiUrls {
|
|
||||||
public const string BASE_URL =
|
|
||||||
#if DEBUG
|
|
||||||
@"https://misuzu.misaka.nl/_sockchat";
|
|
||||||
#else
|
|
||||||
@"https://flashii.net/_sockchat";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public const string AUTH = BASE_URL + @"/verify";
|
namespace SharpChat.Flashii {
|
||||||
public const string BANS = BASE_URL + @"/bans";
|
public static class FlashiiUrls {
|
||||||
public const string BUMP = BASE_URL + @"/bump";
|
private const string BASE_URL_FILE = @"msz_url.txt";
|
||||||
|
private const string BASE_URL_FALLBACK = @"https://flashii.net";
|
||||||
|
|
||||||
|
private const string AUTH = @"/_sockchat/verify";
|
||||||
|
private const string BANS = @"/_sockchat/bans";
|
||||||
|
private const string BUMP = @"/_sockchat/bump";
|
||||||
|
|
||||||
|
public static string AuthURL { get; }
|
||||||
|
public static string BansURL { get; }
|
||||||
|
public static string BumpURL { get; }
|
||||||
|
|
||||||
|
static FlashiiUrls() {
|
||||||
|
AuthURL = GetURL(AUTH);
|
||||||
|
BansURL = GetURL(BANS);
|
||||||
|
BumpURL = GetURL(BUMP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetBaseURL() {
|
||||||
|
if(!File.Exists(BASE_URL_FILE))
|
||||||
|
return BASE_URL_FALLBACK;
|
||||||
|
string url = File.ReadAllText(BASE_URL_FILE).Trim().Trim('/');
|
||||||
|
if(string.IsNullOrEmpty(url))
|
||||||
|
return BASE_URL_FALLBACK;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetURL(string path) {
|
||||||
|
return GetBaseURL() + path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,8 @@ namespace SharpChat.Packet {
|
||||||
sb.Append(User.Pack());
|
sb.Append(User.Pack());
|
||||||
sb.Append('\t');
|
sb.Append('\t');
|
||||||
sb.Append(Channel.Name);
|
sb.Append(Channel.Name);
|
||||||
/*sb.Append('\t');
|
|
||||||
sb.Append(SockChatServer.EXT_VERSION);
|
|
||||||
sb.Append('\t');
|
sb.Append('\t');
|
||||||
sb.Append(Session.Id);*/
|
sb.Append(SockChatServer.MSG_LENGTH_MAX);
|
||||||
|
|
||||||
return new[] { sb.ToString() };
|
return new[] { sb.ToString() };
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,8 +48,6 @@ namespace SharpChat.Packet {
|
||||||
Message.Flags.HasFlag(ChatMessageFlags.Action) ? '0' : '1',
|
Message.Flags.HasFlag(ChatMessageFlags.Action) ? '0' : '1',
|
||||||
Message.Flags.HasFlag(ChatMessageFlags.Private) ? '1' : '0'
|
Message.Flags.HasFlag(ChatMessageFlags.Private) ? '1' : '0'
|
||||||
);
|
);
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(Message.TargetName);
|
|
||||||
|
|
||||||
yield return sb.ToString();
|
yield return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,6 @@ namespace SharpChat.Packet {
|
||||||
sb.Append((int)SockChatServerPacket.ContextClear);
|
sb.Append((int)SockChatServerPacket.ContextClear);
|
||||||
sb.Append('\t');
|
sb.Append('\t');
|
||||||
sb.Append((int)Mode);
|
sb.Append((int)Mode);
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(Channel?.TargetName ?? string.Empty);
|
|
||||||
|
|
||||||
yield return sb.ToString();
|
yield return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,7 @@ namespace SharpChat.Packet {
|
||||||
sb.Append('\t');
|
sb.Append('\t');
|
||||||
sb.Append((int)SockChatServerMovePacket.UserJoined);
|
sb.Append((int)SockChatServerMovePacket.UserJoined);
|
||||||
sb.Append('\t');
|
sb.Append('\t');
|
||||||
sb.Append(User.UserId);
|
sb.Append(User.Pack());
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(User.DisplayName);
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(User.Colour);
|
|
||||||
sb.Append('\t');
|
sb.Append('\t');
|
||||||
sb.Append(SequenceId);
|
sb.Append(SequenceId);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
using Hamakaze;
|
using SharpChat.Flashii;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat {
|
||||||
|
@ -17,12 +19,10 @@ namespace SharpChat {
|
||||||
Console.WriteLine(@"============================================ DEBUG ==");
|
Console.WriteLine(@"============================================ DEBUG ==");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
HttpClient.Instance.DefaultUserAgent = @"SharpChat/0.9";
|
|
||||||
|
|
||||||
Database.ReadConfig();
|
Database.ReadConfig();
|
||||||
|
|
||||||
using ManualResetEvent mre = new ManualResetEvent(false);
|
using ManualResetEvent mre = new ManualResetEvent(false);
|
||||||
using SockChatServer scs = new SockChatServer(PORT);
|
using SockChatServer scs = new SockChatServer(mre, PORT);
|
||||||
Console.CancelKeyPress += (s, e) => { e.Cancel = true; mre.Set(); };
|
Console.CancelKeyPress += (s, e) => { e.Cancel = true; mre.Set(); };
|
||||||
mre.WaitOne();
|
mre.WaitOne();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,4 @@
|
||||||
<PackageReference Include="MySqlConnector" Version="1.3.11" />
|
<PackageReference Include="MySqlConnector" Version="1.3.11" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Hamakaze\Hamakaze.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -8,11 +8,13 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat {
|
||||||
public class SockChatServer : IDisposable {
|
public class SockChatServer : IDisposable {
|
||||||
public const int EXT_VERSION = 2;
|
|
||||||
public const int MSG_LENGTH_MAX = 5000;
|
public const int MSG_LENGTH_MAX = 5000;
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
@ -37,6 +39,8 @@ namespace SharpChat {
|
||||||
public IWebSocketServer Server { get; }
|
public IWebSocketServer Server { get; }
|
||||||
public ChatContext Context { get; }
|
public ChatContext Context { get; }
|
||||||
|
|
||||||
|
public static HttpClient HttpClient { get; }
|
||||||
|
|
||||||
private IReadOnlyCollection<IChatCommand> Commands { get; } = new IChatCommand[] {
|
private IReadOnlyCollection<IChatCommand> Commands { get; } = new IChatCommand[] {
|
||||||
new AFKCommand(),
|
new AFKCommand(),
|
||||||
};
|
};
|
||||||
|
@ -49,9 +53,20 @@ namespace SharpChat {
|
||||||
return Sessions.FirstOrDefault(x => x.Connection == conn);
|
return Sessions.FirstOrDefault(x => x.Connection == conn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SockChatServer(ushort port) {
|
static SockChatServer() {
|
||||||
|
// "fuck it"
|
||||||
|
HttpClient = new HttpClient();
|
||||||
|
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(@"SharpChat");
|
||||||
|
}
|
||||||
|
|
||||||
|
private ManualResetEvent Shutdown { get; }
|
||||||
|
private bool IsShuttingDown = false;
|
||||||
|
|
||||||
|
public SockChatServer(ManualResetEvent mre, ushort port) {
|
||||||
Logger.Write("Starting Sock Chat server...");
|
Logger.Write("Starting Sock Chat server...");
|
||||||
|
|
||||||
|
Shutdown = mre ?? throw new ArgumentNullException(nameof(mre));
|
||||||
|
|
||||||
Context = new ChatContext(this);
|
Context = new ChatContext(this);
|
||||||
|
|
||||||
Context.Channels.Add(new ChatChannel(@"Lounge"));
|
Context.Channels.Add(new ChatChannel(@"Lounge"));
|
||||||
|
@ -66,6 +81,11 @@ namespace SharpChat {
|
||||||
Server = new SharpChatWebSocketServer($@"ws://0.0.0.0:{port}");
|
Server = new SharpChatWebSocketServer($@"ws://0.0.0.0:{port}");
|
||||||
|
|
||||||
Server.Start(sock => {
|
Server.Start(sock => {
|
||||||
|
if(IsShuttingDown || IsDisposed) {
|
||||||
|
sock.Close(1013);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sock.OnOpen = () => OnOpen(sock);
|
sock.OnOpen = () => OnOpen(sock);
|
||||||
sock.OnClose = () => OnClose(sock);
|
sock.OnClose = () => OnClose(sock);
|
||||||
sock.OnError = err => OnError(sock, err);
|
sock.OnError = err => OnError(sock, err);
|
||||||
|
@ -162,11 +182,20 @@ namespace SharpChat {
|
||||||
if(args.Length < 3 || !long.TryParse(args[1], out long aUserId))
|
if(args.Length < 3 || !long.TryParse(args[1], out long aUserId))
|
||||||
break;
|
break;
|
||||||
|
|
||||||
FlashiiAuth.Attempt(new FlashiiAuthRequest {
|
FlashiiAuth.Attempt(HttpClient, new FlashiiAuthRequest {
|
||||||
UserId = aUserId,
|
UserId = aUserId,
|
||||||
Token = args[2],
|
Token = args[2],
|
||||||
IPAddress = sess.RemoteAddress.ToString(),
|
IPAddress = sess.RemoteAddress.ToString(),
|
||||||
}, auth => {
|
}).ContinueWith(authTask => {
|
||||||
|
if(authTask.IsFaulted) {
|
||||||
|
Logger.Write($@"<{sess.Id}> Auth task fail: {authTask.Exception}");
|
||||||
|
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
|
||||||
|
sess.Dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlashiiAuth auth = authTask.Result;
|
||||||
|
|
||||||
if(!auth.Success) {
|
if(!auth.Success) {
|
||||||
Logger.Debug($@"<{sess.Id}> Auth fail: {auth.Reason}");
|
Logger.Debug($@"<{sess.Id}> Auth fail: {auth.Reason}");
|
||||||
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
|
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
|
||||||
|
@ -214,10 +243,6 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess);
|
Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess);
|
||||||
}, ex => {
|
|
||||||
Logger.Write($@"<{sess.Id}> Auth task fail: {ex}");
|
|
||||||
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
|
|
||||||
sess.Dispose();
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -459,7 +484,7 @@ namespace SharpChat {
|
||||||
if(whoChanSB.Length > 2)
|
if(whoChanSB.Length > 2)
|
||||||
whoChanSB.Length -= 2;
|
whoChanSB.Length -= 2;
|
||||||
|
|
||||||
user.Send(new LegacyCommandResponse(LCR.USERS_LISTING_CHANNEL, false, whoChanSB));
|
user.Send(new LegacyCommandResponse(LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB));
|
||||||
} else {
|
} else {
|
||||||
foreach(ChatUser whoUser in Context.Users.All()) {
|
foreach(ChatUser whoUser in Context.Users.All()) {
|
||||||
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
|
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
|
||||||
|
@ -529,7 +554,7 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
string createChanName = string.Join('_', parts.Skip(createChanHasHierarchy ? 2 : 1));
|
string createChanName = string.Join('_', parts.Skip(createChanHasHierarchy ? 2 : 1));
|
||||||
ChatChannel createChan = new() {
|
ChatChannel createChan = new ChatChannel {
|
||||||
Name = createChanName,
|
Name = createChanName,
|
||||||
IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent),
|
IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent),
|
||||||
Rank = createChanHierarchy,
|
Rank = createChanHierarchy,
|
||||||
|
@ -807,6 +832,25 @@ namespace SharpChat {
|
||||||
user.Send(new LegacyCommandResponse(LCR.IP_ADDRESS, false, ipUser.Username, ip));
|
user.Send(new LegacyCommandResponse(LCR.IP_ADDRESS, false, ipUser.Username, ip));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case @"shutdown":
|
||||||
|
case @"restart":
|
||||||
|
if(user.UserId != 1) {
|
||||||
|
user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_ALLOWED, true, $@"/{commandName}"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(IsShuttingDown)
|
||||||
|
break;
|
||||||
|
IsShuttingDown = true;
|
||||||
|
|
||||||
|
if(commandName == @"restart")
|
||||||
|
lock(SessionsLock)
|
||||||
|
Sessions.ForEach(s => s.PrepareForRestart());
|
||||||
|
|
||||||
|
Context.Update();
|
||||||
|
Shutdown.Set();
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_FOUND, true, commandName));
|
user.Send(new LegacyCommandResponse(LCR.COMMAND_NOT_FOUND, true, commandName));
|
||||||
break;
|
break;
|
||||||
|
@ -816,21 +860,25 @@ namespace SharpChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
~SockChatServer()
|
~SockChatServer()
|
||||||
=> DoDispose();
|
=> Dispose(false);
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose()
|
||||||
DoDispose();
|
=> Dispose(true);
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DoDispose() {
|
private void Dispose(bool disposing) {
|
||||||
if(IsDisposed)
|
if(IsDisposed)
|
||||||
return;
|
return;
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
|
|
||||||
Sessions?.Clear();
|
lock(SessionsLock)
|
||||||
|
Sessions.ForEach(s => s.Dispose());
|
||||||
|
|
||||||
Server?.Dispose();
|
Server?.Dispose();
|
||||||
Context?.Dispose();
|
Context?.Dispose();
|
||||||
|
HttpClient?.Dispose();
|
||||||
|
|
||||||
|
if(disposing)
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue