Imported from Sharp Chat repository.
This commit is contained in:
commit
7b29e76789
|
@ -0,0 +1 @@
|
|||
* text=auto
|
|
@ -0,0 +1,217 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
SharpChat.Common/version.txt
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
build/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
|
||||
# JetBrains Rider cache/options directory
|
||||
.idea/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
## TODO: Comment the next line if you want to checkin your
|
||||
## web deploy settings but do note that will include unencrypted
|
||||
## passwords
|
||||
#*.pubxml
|
||||
|
||||
*.publishproj
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
|
||||
# Windows Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Windows Store app package directory
|
||||
AppPackages/
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
[Ss]tyle[Cc]op.*
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# LightSwitch generated files
|
||||
GeneratedArtifacts/
|
||||
_Pvt_Extensions/
|
||||
ModelManifest.xml
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30114.105
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hamakaze", "Hamakaze\Hamakaze.csproj", "{B8B0DBA0-7B91-454A-BE4E-D10F383162CA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{B8B0DBA0-7B91-454A-BE4E-D10F383162CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B8B0DBA0-7B91-454A-BE4E-D10F383162CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B8B0DBA0-7B91-454A-BE4E-D10F383162CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B8B0DBA0-7B91-454A-BE4E-D10F383162CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -0,0 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,26 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
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.") { }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
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);
|
||||