backlog! (also indefinitely abandoning this)
This commit is contained in:
parent
e135ad7159
commit
1ad0dab43c
10 changed files with 449 additions and 198 deletions
|
@ -706,7 +706,7 @@ namespace Maki
|
||||||
{
|
{
|
||||||
wr.Perform();
|
wr.Perform();
|
||||||
|
|
||||||
if (wr.Status != 200 || wr.Response.Length < 1)
|
if (wr.Status != 200 || wr.ResponseString.Length < 1)
|
||||||
throw new DiscordException("Failed to load message from API");
|
throw new DiscordException("Failed to load message from API");
|
||||||
|
|
||||||
message = wr.ResponseJson<Message>();
|
message = wr.ResponseJson<Message>();
|
||||||
|
|
|
@ -58,7 +58,7 @@ namespace Maki
|
||||||
|
|
||||||
wr.Perform();
|
wr.Perform();
|
||||||
|
|
||||||
if (wr.Status != 200 || wr.Response.Length < 1)
|
if (wr.Status != 200 || wr.ResponseString.Length < 1)
|
||||||
// TODO: elaborate
|
// TODO: elaborate
|
||||||
//throw new DiscordException("Failed to send message");
|
//throw new DiscordException("Failed to send message");
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -60,7 +60,7 @@ namespace Maki
|
||||||
});
|
});
|
||||||
wr.Perform();
|
wr.Perform();
|
||||||
|
|
||||||
if (wr.Status != 200 || wr.Response.Length < 1)
|
if (wr.Status != 200 || wr.ResponseString.Length < 1)
|
||||||
// TODO: elaborate
|
// TODO: elaborate
|
||||||
throw new DiscordException("Failed to edit role");
|
throw new DiscordException("Failed to edit role");
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ namespace Maki
|
||||||
});
|
});
|
||||||
wr.Perform();
|
wr.Perform();
|
||||||
|
|
||||||
if (wr.Status != 200 || wr.Response.Length < 1)
|
if (wr.Status != 200 || wr.ResponseString.Length < 1)
|
||||||
throw new DiscordException("Failed to create role");
|
throw new DiscordException("Failed to create role");
|
||||||
|
|
||||||
roleStruct = wr.ResponseJson<Role>();
|
roleStruct = wr.ResponseJson<Role>();
|
||||||
|
|
|
@ -35,6 +35,15 @@
|
||||||
<NoWarn>0649</NoWarn>
|
<NoWarn>0649</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Threading.Tasks, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Microsoft.Threading.Tasks.Extensions, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop, Version=1.0.168.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||||
<HintPath>$(SolutionDir)\packages\Newtonsoft.Json.10.0.3\lib\net40\Newtonsoft.Json.dll</HintPath>
|
<HintPath>$(SolutionDir)\packages\Newtonsoft.Json.10.0.3\lib\net40\Newtonsoft.Json.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
@ -42,6 +51,7 @@
|
||||||
<Reference Include="System.IO, Version=2.6.10.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
<Reference Include="System.IO, Version=2.6.10.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
<HintPath>..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.IO.dll</HintPath>
|
<HintPath>..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.IO.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="System.Net" />
|
||||||
<Reference Include="System.Net.Http, Version=2.2.29.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
<Reference Include="System.Net.Http, Version=2.2.29.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
<HintPath>..\..\packages\Microsoft.Net.Http.2.2.29\lib\net40\System.Net.Http.dll</HintPath>
|
<HintPath>..\..\packages\Microsoft.Net.Http.2.2.29\lib\net40\System.Net.Http.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
|
|
@ -5,8 +5,10 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Maki.Rest
|
namespace Maki.Rest
|
||||||
{
|
{
|
||||||
|
@ -15,47 +17,95 @@ namespace Maki.Rest
|
||||||
private const string USER_AGENT = @"DiscordBot (https://github.com/flashwave/maki, 1.0.0.0)";
|
private const string USER_AGENT = @"DiscordBot (https://github.com/flashwave/maki, 1.0.0.0)";
|
||||||
private const string GENERIC_CONTENT_TYPE = @"application/octet-stream";
|
private const string GENERIC_CONTENT_TYPE = @"application/octet-stream";
|
||||||
private const string JSON_CONTENT_TYPE = @"application/json";
|
private const string JSON_CONTENT_TYPE = @"application/json";
|
||||||
private const string FORM_CONTENT_TYPE = @"multipart/form-data";
|
|
||||||
|
|
||||||
private const long BUFFER_SIZE = 8192000;
|
private const int MAX_RETRIES = 1;
|
||||||
|
private const int TIMEOUT = 10000;
|
||||||
|
|
||||||
|
public int Timeout { get; set; } = TIMEOUT;
|
||||||
|
|
||||||
|
public event Action Started;
|
||||||
|
public event Action Finished;
|
||||||
|
public event Action<Exception> Failed;
|
||||||
|
|
||||||
|
public event Action<long, long> DownloadProgress;
|
||||||
|
public event Action<long, long> UploadProgress;
|
||||||
|
|
||||||
|
public bool IsAborted { get; private set; }
|
||||||
|
|
||||||
|
public string Accept { get; set; }
|
||||||
|
|
||||||
|
private bool PrivateCompleted;
|
||||||
|
public bool IsCompleted
|
||||||
|
{
|
||||||
|
get => PrivateCompleted;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
PrivateCompleted = value;
|
||||||
|
|
||||||
|
if (!PrivateCompleted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Started = null;
|
||||||
|
Finished = null;
|
||||||
|
DownloadProgress = null;
|
||||||
|
UploadProgress = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string PrivateUrl;
|
||||||
|
private string Url
|
||||||
|
{
|
||||||
|
get => PrivateUrl;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!value.StartsWith(@"http://") && !value.StartsWith(@"https://"))
|
||||||
|
value = RestEndpoints.BASE_URL + RestEndpoints.BASE_PATH + value;
|
||||||
|
|
||||||
|
PrivateUrl = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int BUFFER_SIZE = 4096;
|
||||||
|
private byte[] Buffer;
|
||||||
|
|
||||||
private static HttpClient HttpClient;
|
private static HttpClient HttpClient;
|
||||||
|
|
||||||
public readonly HttpMethod Method;
|
public readonly HttpMethod Method;
|
||||||
public readonly string Url;
|
private bool HasBody => Method == HttpMethod.PUT || Method == HttpMethod.POST || Method == HttpMethod.PATCH;
|
||||||
|
|
||||||
public string UserAgent { get; set; } = USER_AGENT;
|
|
||||||
|
|
||||||
public string ContentType { get; set; } = GENERIC_CONTENT_TYPE;
|
public string ContentType { get; set; } = GENERIC_CONTENT_TYPE;
|
||||||
public long ContentLength => HttpWebResponse.ContentLength < 1 ? BUFFER_SIZE : HttpWebResponse.ContentLength;
|
|
||||||
|
|
||||||
// TODO: make this not static
|
[Obsolete]
|
||||||
|
public long ContentLength => Response.Content.Headers.ContentLength ?? BytesDownloaded;
|
||||||
|
|
||||||
|
[Obsolete]
|
||||||
internal static string Authorisation { get; set; }
|
internal static string Authorisation { get; set; }
|
||||||
|
|
||||||
private readonly Dictionary<string, string> Headers = new Dictionary<string, string>();
|
private readonly Dictionary<string, string> Headers = new Dictionary<string, string>();
|
||||||
private readonly Dictionary<string, string> Parameters = new Dictionary<string, string>();
|
private readonly Dictionary<string, string> Parameters = new Dictionary<string, string>();
|
||||||
private readonly Dictionary<string, byte[]> Files = new Dictionary<string, byte[]>();
|
private readonly Dictionary<string, byte[]> Files = new Dictionary<string, byte[]>();
|
||||||
|
|
||||||
private readonly Dictionary<string, string> MimeTypes = new Dictionary<string, string>()
|
private long BytesUploaded = 0;
|
||||||
{
|
private long BytesDownloaded = 0;
|
||||||
{ "png", "image/png" },
|
|
||||||
{ "jpg", "image/jpeg" },
|
private HttpResponseMessage Response;
|
||||||
{ "jpeg", "image/jpeg" },
|
|
||||||
{ "gif", "image/gif" },
|
private MemoryStream RawRequestBody;
|
||||||
};
|
|
||||||
|
|
||||||
private byte[] RawRequestBody = new byte[0];
|
|
||||||
private HttpWebRequest HttpWebRequest;
|
|
||||||
private Stream RequestStream;
|
private Stream RequestStream;
|
||||||
private HttpWebResponse HttpWebResponse;
|
|
||||||
private Stream ResponseStream;
|
private Stream ResponseStream;
|
||||||
|
|
||||||
private byte[] RawResponseValue;
|
private CancellationTokenSource AbortToken;
|
||||||
public byte[] RawResponse
|
private CancellationTokenSource TimeoutToken;
|
||||||
|
|
||||||
|
public int Retries { get; private set; } = 0;
|
||||||
|
|
||||||
|
private byte[] PrivateResponseBytes;
|
||||||
|
public byte[] ResponseBytes
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (RawResponseValue == null)
|
if (PrivateResponseBytes == null)
|
||||||
using (MemoryStream ms = new MemoryStream())
|
using (MemoryStream ms = new MemoryStream())
|
||||||
{
|
{
|
||||||
byte[] bytes = new byte[4096];
|
byte[] bytes = new byte[4096];
|
||||||
|
@ -65,36 +115,39 @@ namespace Maki.Rest
|
||||||
ms.Write(bytes, 0, read);
|
ms.Write(bytes, 0, read);
|
||||||
|
|
||||||
ms.Seek(0, SeekOrigin.Begin);
|
ms.Seek(0, SeekOrigin.Begin);
|
||||||
RawResponseValue = new byte[ms.Length];
|
PrivateResponseBytes = new byte[ms.Length];
|
||||||
ms.Read(RawResponseValue, 0, RawResponseValue.Length);
|
ms.Read(PrivateResponseBytes, 0, PrivateResponseBytes.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return RawResponseValue;
|
return PrivateResponseBytes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResponseString = string.Empty;
|
private string PrivateResponseString = string.Empty;
|
||||||
|
public string ResponseString
|
||||||
public string Response
|
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(ResponseString))
|
if (string.IsNullOrEmpty(PrivateResponseString))
|
||||||
ResponseString = Encoding.UTF8.GetString(RawResponse);
|
PrivateResponseString = Encoding.UTF8.GetString(ResponseBytes);
|
||||||
|
|
||||||
return ResponseString;
|
return PrivateResponseString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public T ResponseJson<T>() =>
|
public T ResponseJson<T>() =>
|
||||||
JsonConvert.DeserializeObject<T>(Response);
|
JsonConvert.DeserializeObject<T>(ResponseString);
|
||||||
|
|
||||||
public short Status =>
|
[Obsolete]
|
||||||
(short)HttpWebResponse?.StatusCode;
|
public short Status => (short)Response?.StatusCode;
|
||||||
|
|
||||||
static WebRequest()
|
static WebRequest()
|
||||||
|
=> CreateHttpClientInstance();
|
||||||
|
|
||||||
|
public WebRequest(HttpMethod method, string url)
|
||||||
{
|
{
|
||||||
CreateHttpClientInstance();
|
Method = method;
|
||||||
|
Url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CreateHttpClientInstance()
|
private static void CreateHttpClientInstance()
|
||||||
|
@ -108,17 +161,25 @@ namespace Maki.Rest
|
||||||
|
|
||||||
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT);
|
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT);
|
||||||
HttpClient.DefaultRequestHeaders.ExpectContinue = true;
|
HttpClient.DefaultRequestHeaders.ExpectContinue = true;
|
||||||
HttpClient.Timeout = new TimeSpan(0, 0, 0, 0, Timeout.Infinite);
|
HttpClient.Timeout = new TimeSpan(0, 0, 0, 0, System.Threading.Timeout.Infinite);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebRequest(HttpMethod method, string url)
|
public void AddRaw(Stream stream)
|
||||||
{
|
{
|
||||||
Method = method;
|
if (stream == null)
|
||||||
Url = url;
|
throw new ArgumentNullException(nameof(stream));
|
||||||
|
|
||||||
|
RawRequestBody?.Dispose();
|
||||||
|
RawRequestBody = new MemoryStream();
|
||||||
|
|
||||||
|
stream.CopyTo(RawRequestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddRaw(byte[] bytes) =>
|
public void AddRaw(byte[] bytes)
|
||||||
RawRequestBody = bytes;
|
{
|
||||||
|
using (MemoryStream ms = new MemoryStream(bytes))
|
||||||
|
AddRaw(ms);
|
||||||
|
}
|
||||||
|
|
||||||
public void AddRaw(string str) =>
|
public void AddRaw(string str) =>
|
||||||
AddRaw(Encoding.UTF8.GetBytes(str));
|
AddRaw(Encoding.UTF8.GetBytes(str));
|
||||||
|
@ -135,144 +196,293 @@ namespace Maki.Rest
|
||||||
public void AddFile(string name, byte[] bytes) =>
|
public void AddFile(string name, byte[] bytes) =>
|
||||||
Files.Add(name, bytes);
|
Files.Add(name, bytes);
|
||||||
|
|
||||||
public void Perform()
|
public void AddHeader(string name, string value)
|
||||||
{
|
{
|
||||||
StringBuilder urlBuilder = new StringBuilder();
|
if (string.IsNullOrEmpty(name))
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
|
||||||
if (!Url.StartsWith("http://") && !Url.StartsWith("https://"))
|
if (value == null)
|
||||||
{
|
throw new ArgumentNullException(nameof(value));
|
||||||
urlBuilder.Append(RestEndpoints.BASE_URL);
|
|
||||||
urlBuilder.Append(RestEndpoints.BASE_PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
urlBuilder.Append(Url);
|
if (Headers.ContainsKey(name))
|
||||||
|
Headers[name] = value;
|
||||||
if (Method == HttpMethod.GET
|
|
||||||
|| Method == HttpMethod.DELETE)
|
|
||||||
if (Parameters.Count > 1)
|
|
||||||
{
|
|
||||||
if (!Url.Contains('?'))
|
|
||||||
urlBuilder.Append(@"?");
|
|
||||||
|
|
||||||
foreach (KeyValuePair<string, string> param in Parameters)
|
|
||||||
urlBuilder.Append($@"{param.Key}={param.Value}&");
|
|
||||||
}
|
|
||||||
|
|
||||||
string url = urlBuilder.ToString().TrimEnd('&');
|
|
||||||
|
|
||||||
HttpWebRequest = System.Net.WebRequest.Create(url) as HttpWebRequest;
|
|
||||||
HttpWebRequest.Method = Method.ToString();
|
|
||||||
HttpWebRequest.UserAgent = UserAgent;
|
|
||||||
HttpWebRequest.KeepAlive = true;
|
|
||||||
HttpWebRequest.ReadWriteTimeout = Timeout.Infinite;
|
|
||||||
HttpWebRequest.Timeout = Timeout.Infinite;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Authorisation) && url.StartsWith(RestEndpoints.BASE_URL + RestEndpoints.BASE_PATH))
|
|
||||||
HttpWebRequest.Headers[HttpRequestHeader.Authorization] = Authorisation;
|
|
||||||
|
|
||||||
foreach (KeyValuePair<string, string> header in Headers)
|
|
||||||
HttpWebRequest.Headers[header.Key] = header.Value;
|
|
||||||
|
|
||||||
if (Method == HttpMethod.POST
|
|
||||||
|| Method == HttpMethod.PUT
|
|
||||||
|| Method == HttpMethod.PATCH)
|
|
||||||
{
|
|
||||||
RequestStream = HttpWebRequest.GetRequestStream();
|
|
||||||
|
|
||||||
if (Parameters.Count + Files.Count < 1)
|
|
||||||
RequestStream.Write(RawRequestBody, 0, RawRequestBody.Length);
|
|
||||||
else
|
else
|
||||||
|
Headers.Add(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Abort()
|
||||||
{
|
{
|
||||||
string boundary = $@"-----------------------------{DateTime.Now.Ticks}";
|
IsAborted = true;
|
||||||
ContentType = $@"{FORM_CONTENT_TYPE}; boundary={boundary}";
|
IsCompleted = true;
|
||||||
|
|
||||||
if (Parameters.Count >= 1)
|
|
||||||
{
|
|
||||||
StringBuilder postBodyBuilder = new StringBuilder();
|
|
||||||
byte[] postBody = new byte[0];
|
|
||||||
|
|
||||||
foreach (KeyValuePair<string, string> param in Parameters)
|
|
||||||
{
|
|
||||||
postBodyBuilder.AppendLine($@"--{boundary}");
|
|
||||||
postBodyBuilder.AppendLine($@"Content-Disposition: form-data; name=""{param.Key}""");
|
|
||||||
postBodyBuilder.AppendLine();
|
|
||||||
postBodyBuilder.AppendLine(param.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
postBody = Encoding.UTF8.GetBytes(postBodyBuilder.ToString());
|
|
||||||
RequestStream.Write(postBody, 0, postBody.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Files.Count >= 1)
|
|
||||||
{
|
|
||||||
byte[] boundaryBytes = Encoding.UTF8.GetBytes($@"--{boundary}");
|
|
||||||
byte[] newLineBytes = Encoding.UTF8.GetBytes("\r\n");
|
|
||||||
|
|
||||||
foreach (KeyValuePair<string, byte[]> file in Files)
|
|
||||||
{
|
|
||||||
string cType = GENERIC_CONTENT_TYPE;
|
|
||||||
string fileExt = Path.GetExtension(file.Key).ToLower().TrimStart('.');
|
|
||||||
|
|
||||||
if (MimeTypes.ContainsKey(fileExt))
|
|
||||||
cType = MimeTypes[fileExt];
|
|
||||||
|
|
||||||
byte[] cDisposBytes = Encoding.UTF8.GetBytes($@"Content-Disposition: form-data; name=""{file.Key}""; filename=""{file.Key}""");
|
|
||||||
byte[] cTypeBytes = Encoding.UTF8.GetBytes($@"Content-Type: {cType}");
|
|
||||||
|
|
||||||
// Boundary + newline
|
|
||||||
RequestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
|
|
||||||
RequestStream.Write(newLineBytes, 0, newLineBytes.Length);
|
|
||||||
|
|
||||||
// Disposition header + newline
|
|
||||||
RequestStream.Write(cDisposBytes, 0, cDisposBytes.Length);
|
|
||||||
RequestStream.Write(newLineBytes, 0, newLineBytes.Length);
|
|
||||||
|
|
||||||
// Type header + newline
|
|
||||||
RequestStream.Write(cTypeBytes, 0, cTypeBytes.Length);
|
|
||||||
RequestStream.Write(newLineBytes, 0, newLineBytes.Length);
|
|
||||||
|
|
||||||
// newline + contents + newline
|
|
||||||
RequestStream.Write(newLineBytes, 0, newLineBytes.Length);
|
|
||||||
RequestStream.Write(file.Value, 0, file.Value.Length);
|
|
||||||
RequestStream.Write(newLineBytes, 0, newLineBytes.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] closingBound = Encoding.UTF8.GetBytes($@"--{boundary}--");
|
|
||||||
RequestStream.Write(closingBound, 0, closingBound.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpWebRequest.ContentType = ContentType;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
HttpWebResponse = HttpWebRequest.GetResponse() as HttpWebResponse;
|
AbortToken?.Cancel();
|
||||||
} catch (WebException ex)
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
HttpWebResponse = ex.Response as HttpWebResponse;
|
// just do nothign in this case
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseStream = HttpWebResponse.GetResponseStream();
|
private System.Net.Http.HttpMethod FromInternalHttpMethod(HttpMethod method)
|
||||||
|
{
|
||||||
|
switch (method)
|
||||||
|
{
|
||||||
|
case HttpMethod.GET:
|
||||||
|
return System.Net.Http.HttpMethod.Get;
|
||||||
|
|
||||||
|
case HttpMethod.DELETE:
|
||||||
|
return System.Net.Http.HttpMethod.Delete;
|
||||||
|
|
||||||
|
case HttpMethod.POST:
|
||||||
|
return System.Net.Http.HttpMethod.Post;
|
||||||
|
|
||||||
|
case HttpMethod.PATCH:
|
||||||
|
return new System.Net.Http.HttpMethod(@"PATCH");
|
||||||
|
|
||||||
|
case HttpMethod.PUT:
|
||||||
|
return System.Net.Http.HttpMethod.Put;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region IDisposable
|
throw new InvalidOperationException($"Unsupported HTTP method {method}.");
|
||||||
|
}
|
||||||
|
|
||||||
private bool IsDisposed = false;
|
private void PrivatePerform()
|
||||||
|
{
|
||||||
|
using (AbortToken = new CancellationTokenSource())
|
||||||
|
using (TimeoutToken = new CancellationTokenSource())
|
||||||
|
using (CancellationTokenSource linkedToken = CancellationTokenSource.CreateLinkedTokenSource(AbortToken.Token, TimeoutToken.Token))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string requestUri = Url;
|
||||||
|
HttpRequestMessage request = new HttpRequestMessage(FromInternalHttpMethod(Method), requestUri);
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, string> h in Headers)
|
||||||
|
request.Headers.Add(h.Key, h.Value);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Accept))
|
||||||
|
request.Headers.Accept.TryParseAdd(Accept);
|
||||||
|
|
||||||
|
if (HasBody)
|
||||||
|
{
|
||||||
|
Stream bodyContent;
|
||||||
|
|
||||||
|
if (RawRequestBody == null)
|
||||||
|
{
|
||||||
|
MultipartFormDataContent formData = new MultipartFormDataContent();
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, string> p in Parameters)
|
||||||
|
formData.Add(new StringContent(p.Value), p.Key);
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, byte[]> f in Files)
|
||||||
|
{
|
||||||
|
ByteArrayContent bac = new ByteArrayContent(f.Value);
|
||||||
|
bac.Headers.Add("Content-Type", GENERIC_CONTENT_TYPE);
|
||||||
|
formData.Add(bac, f.Key, f.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyContent = formData.ReadAsStreamAsync().Result;
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
if (Parameters.Count > 0 || Files.Count > 0)
|
||||||
|
throw new InvalidOperationException($"You cannot use {nameof(AddRaw)} at the same time as {nameof(AddParam)} or {nameof(AddFile)}");
|
||||||
|
|
||||||
|
bodyContent = new MemoryStream();
|
||||||
|
RawRequestBody.Seek(0, SeekOrigin.Begin);
|
||||||
|
RawRequestBody.CopyTo(bodyContent);
|
||||||
|
bodyContent.Seek(0, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Content = new StreamContent(RequestStream);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(ContentType))
|
||||||
|
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(ContentType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (Parameters.Count > 1)
|
||||||
|
{
|
||||||
|
StringBuilder urlBuilder = new StringBuilder();
|
||||||
|
urlBuilder.Append(Url);
|
||||||
|
|
||||||
|
if (!Url.Contains('?'))
|
||||||
|
urlBuilder.Append('?');
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, string> param in Parameters)
|
||||||
|
{
|
||||||
|
urlBuilder.Append(param.Key);
|
||||||
|
urlBuilder.Append('=');
|
||||||
|
urlBuilder.Append(param.Value);
|
||||||
|
urlBuilder.Append('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
urlBuilder.Length -= 1;
|
||||||
|
requestUri = urlBuilder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReportProgress();
|
||||||
|
|
||||||
|
using (request)
|
||||||
|
Response = HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, linkedToken.Token).Result;
|
||||||
|
|
||||||
|
ResponseStream = new MemoryStream();
|
||||||
|
|
||||||
|
if (HasBody)
|
||||||
|
{
|
||||||
|
ReportProgress();
|
||||||
|
UploadProgress?.Invoke(0, BytesUploaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleResponse(linkedToken.Token);
|
||||||
|
} catch (Exception) when (AbortToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Complete(new WebException(string.Format("Request to {0} was aborted by the user.", Url), WebExceptionStatus.RequestCanceled));
|
||||||
|
} catch (Exception) when (TimeoutToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Complete(new WebException(string.Format("Request to {0} timed out after {1:N0} seconds idle (read {2:N0} bytes).", Url, TimeSinceLastAction / 1000, BytesDownloaded), WebExceptionStatus.Timeout));
|
||||||
|
} catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (IsCompleted)
|
||||||
|
throw;
|
||||||
|
|
||||||
|
Complete(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PerformAsync()
|
||||||
|
{
|
||||||
|
if (IsCompleted)
|
||||||
|
throw new InvalidOperationException($"{nameof(WebRequest)} has already been run, you can't reuse WebRequest objects.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Factory.StartNew(PrivatePerform, TaskCreationOptions.LongRunning);
|
||||||
|
}
|
||||||
|
catch (AggregateException ex)
|
||||||
|
{
|
||||||
|
if (ex.InnerExceptions.Count != 1)
|
||||||
|
throw ex;
|
||||||
|
|
||||||
|
while (ex.InnerExceptions.Count == 1)
|
||||||
|
{
|
||||||
|
AggregateException innerEx = ex.InnerException as AggregateException;
|
||||||
|
ex = innerEx ?? throw innerEx.InnerException;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Perform()
|
||||||
|
=> PerformAsync().Wait();
|
||||||
|
|
||||||
|
private void HandleResponse(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (Stream responseStream = Response.Content.ReadAsStreamAsync().Result)
|
||||||
|
{
|
||||||
|
Started?.Invoke();
|
||||||
|
|
||||||
|
Buffer = new byte[BUFFER_SIZE];
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
int read = responseStream.Read(Buffer, 0, BUFFER_SIZE);
|
||||||
|
|
||||||
|
ReportProgress();
|
||||||
|
|
||||||
|
if (read > 0)
|
||||||
|
{
|
||||||
|
ResponseStream.Write(Buffer, 0, read);
|
||||||
|
BytesDownloaded += read;
|
||||||
|
DownloadProgress?.Invoke(BytesDownloaded, Response.Content.Headers.ContentLength ?? BytesDownloaded);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
ResponseStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Complete(Exception exception = null)
|
||||||
|
{
|
||||||
|
if (IsAborted || IsCompleted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool allowRetry = true;
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
allowRetry = exception is WebException && (exception as WebException)?.Status == WebExceptionStatus.Timeout;
|
||||||
|
} else if (!Response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
exception = new WebException($@"HTTP {Response.StatusCode}");
|
||||||
|
|
||||||
|
switch (Response.StatusCode)
|
||||||
|
{
|
||||||
|
case HttpStatusCode.NotFound:
|
||||||
|
case HttpStatusCode.MethodNotAllowed:
|
||||||
|
case HttpStatusCode.Forbidden:
|
||||||
|
case HttpStatusCode.Unauthorized:
|
||||||
|
allowRetry = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
if (allowRetry && Retries < MAX_RETRIES && BytesDownloaded < 1)
|
||||||
|
{
|
||||||
|
++Retries;
|
||||||
|
PrivatePerform();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// process
|
||||||
|
} catch (Exception ex)
|
||||||
|
{
|
||||||
|
exception = exception == null ? ex : new AggregateException(exception, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
IsCompleted = true;
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
IsAborted = true;
|
||||||
|
Failed?.Invoke(exception);
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
Finished?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Timeout
|
||||||
|
private long LastReportedAction = 0;
|
||||||
|
private long TimeSinceLastAction => (DateTime.Now.Ticks - LastReportedAction) / TimeSpan.TicksPerMillisecond;
|
||||||
|
|
||||||
|
private void ReportProgress()
|
||||||
|
{
|
||||||
|
LastReportedAction = DateTime.Now.Ticks;
|
||||||
|
TimeoutToken.CancelAfter(Timeout);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Disposal
|
||||||
|
public bool IsDisposed { get; private set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disconnects and releases all unmanaged objects
|
|
||||||
/// </summary>
|
|
||||||
private void Dispose(bool disposing)
|
private void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (IsDisposed)
|
if (IsDisposed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
RequestStream?.Dispose();
|
// TODO: reimplement disposal
|
||||||
HttpWebRequest?.Abort();
|
|
||||||
ResponseStream?.Dispose();
|
|
||||||
HttpWebResponse?.Close();
|
|
||||||
|
|
||||||
if (disposing)
|
if (disposing)
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
|
@ -283,7 +493,6 @@ namespace Maki.Rest
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
=> Dispose(true);
|
=> Dispose(true);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,14 @@
|
||||||
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
<bindingRedirect oldVersion="0.0.0.0-2.2.29.0" newVersion="2.2.29.0" />
|
<bindingRedirect oldVersion="0.0.0.0-2.2.29.0" newVersion="2.2.29.0" />
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-2.6.10.0" newVersion="2.6.10.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Threading.Tasks" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-2.6.10.0" newVersion="2.6.10.0" />
|
||||||
|
</dependentAssembly>
|
||||||
</assemblyBinding>
|
</assemblyBinding>
|
||||||
</runtime>
|
</runtime>
|
||||||
</configuration>
|
</configuration>
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<packages>
|
<packages>
|
||||||
<package id="Microsoft.Bcl" version="1.1.10" targetFramework="net40" />
|
<package id="Microsoft.Bcl" version="1.1.10" targetFramework="net40" />
|
||||||
|
<package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net40" />
|
||||||
<package id="Microsoft.Bcl.Build" version="1.0.14" targetFramework="net40" />
|
<package id="Microsoft.Bcl.Build" version="1.0.14" targetFramework="net40" />
|
||||||
<package id="Microsoft.Net.Http" version="2.2.29" targetFramework="net40" />
|
<package id="Microsoft.Net.Http" version="2.2.29" targetFramework="net40" />
|
||||||
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net40" />
|
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net40" />
|
||||||
|
|
|
@ -34,12 +34,6 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
|
||||||
<Reference Include="System.Xml.Linq" />
|
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
|
||||||
<Reference Include="Microsoft.CSharp" />
|
|
||||||
<Reference Include="System.Data" />
|
|
||||||
<Reference Include="System.Xml" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
|
|
|
@ -1,13 +1,45 @@
|
||||||
using Maki;
|
using Maki.Rest;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace MakiTest
|
namespace Maki.Testing
|
||||||
{
|
{
|
||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
TestWebRequest(false, HttpMethod.GET, WEB_REQUEST_GET);
|
||||||
|
TestWebRequest(true, HttpMethod.GET, WEB_REQUEST_GET);
|
||||||
|
|
||||||
|
//TestMainLibrary();
|
||||||
|
|
||||||
|
Console.WriteLine("Stopped, press any key to close the program...");
|
||||||
|
Console.ReadKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region WebRequest tests
|
||||||
|
private const string WEB_REQUEST_SECURE = @"https://";
|
||||||
|
private const string WEB_REQUEST_INSECURE = @"http://";
|
||||||
|
private const string WEB_REQUEST_BASE = @"httpbin.org";
|
||||||
|
|
||||||
|
private const string WEB_REQUEST_GET = WEB_REQUEST_BASE + @"/get";
|
||||||
|
private const string WEB_REQUEST_FAIL = @"thisshouldneveresolve.flash.moe";
|
||||||
|
|
||||||
|
private static void TestWebRequest(bool secure, HttpMethod method, string url)
|
||||||
|
{
|
||||||
|
url = (secure ? WEB_REQUEST_SECURE : WEB_REQUEST_INSECURE) + url;
|
||||||
|
|
||||||
|
using (WebRequest wr = new WebRequest(method, url))
|
||||||
|
{
|
||||||
|
wr.Perform();
|
||||||
|
|
||||||
|
Console.WriteLine(wr.ResponseString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private static void TestMainLibrary()
|
||||||
{
|
{
|
||||||
string[] tokenInfo = File.ReadAllLines("token.txt");
|
string[] tokenInfo = File.ReadAllLines("token.txt");
|
||||||
string token = tokenInfo[0] ?? string.Empty;
|
string token = tokenInfo[0] ?? string.Empty;
|
||||||
|
@ -48,9 +80,6 @@ namespace MakiTest
|
||||||
|
|
||||||
mre.WaitOne();
|
mre.WaitOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("Stopped, press any key to close the program...");
|
|
||||||
Console.ReadKey();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue