2017-05-31 21:02:38 +00:00
using Newtonsoft.Json ;
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Net ;
2017-12-06 03:50:23 +00:00
using System.Net.Http ;
2018-03-06 19:23:53 +00:00
using System.Net.Http.Headers ;
2017-05-31 21:02:38 +00:00
using System.Text ;
using System.Threading ;
2018-03-06 19:23:53 +00:00
using System.Threading.Tasks ;
2017-05-31 21:02:38 +00:00
namespace Maki.Rest
{
2017-08-09 21:23:43 +00:00
public class WebRequest : IDisposable
2017-05-31 21:02:38 +00:00
{
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 JSON_CONTENT_TYPE = @"application/json" ;
2018-03-06 19:23:53 +00:00
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 ;
2017-05-31 21:02:38 +00:00
2017-12-06 03:50:23 +00:00
private static HttpClient HttpClient ;
2017-08-09 21:23:43 +00:00
public readonly HttpMethod Method ;
2018-03-06 19:23:53 +00:00
private bool HasBody = > Method = = HttpMethod . PUT | | Method = = HttpMethod . POST | | Method = = HttpMethod . PATCH ;
2017-08-09 21:23:43 +00:00
public string ContentType { get ; set ; } = GENERIC_CONTENT_TYPE ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
[Obsolete]
public long ContentLength = > Response . Content . Headers . ContentLength ? ? BytesDownloaded ;
[Obsolete]
2017-05-31 21:02:38 +00:00
internal static string Authorisation { get ; set ; }
2017-10-13 20:05:50 +00:00
private readonly Dictionary < string , string > Headers = new Dictionary < string , string > ( ) ;
private readonly Dictionary < string , string > Parameters = new Dictionary < string , string > ( ) ;
private readonly Dictionary < string , byte [ ] > Files = new Dictionary < string , byte [ ] > ( ) ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
private long BytesUploaded = 0 ;
private long BytesDownloaded = 0 ;
private HttpResponseMessage Response ;
private MemoryStream RawRequestBody ;
2017-10-13 20:05:50 +00:00
private Stream RequestStream ;
private Stream ResponseStream ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
private CancellationTokenSource AbortToken ;
private CancellationTokenSource TimeoutToken ;
public int Retries { get ; private set ; } = 0 ;
private byte [ ] PrivateResponseBytes ;
public byte [ ] ResponseBytes
2017-05-31 21:02:38 +00:00
{
get
{
2018-03-06 19:23:53 +00:00
if ( PrivateResponseBytes = = null )
2017-10-13 20:05:50 +00:00
using ( MemoryStream ms = new MemoryStream ( ) )
{
byte [ ] bytes = new byte [ 4096 ] ;
int read = 0 ;
while ( ( read = ResponseStream . Read ( bytes , 0 , bytes . Length ) ) > 0 )
ms . Write ( bytes , 0 , read ) ;
ms . Seek ( 0 , SeekOrigin . Begin ) ;
2018-03-06 19:23:53 +00:00
PrivateResponseBytes = new byte [ ms . Length ] ;
ms . Read ( PrivateResponseBytes , 0 , PrivateResponseBytes . Length ) ;
2017-10-13 20:05:50 +00:00
}
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
return PrivateResponseBytes ;
2017-05-31 21:02:38 +00:00
}
}
2018-03-06 19:23:53 +00:00
private string PrivateResponseString = string . Empty ;
public string ResponseString
2017-05-31 21:02:38 +00:00
{
get
{
2018-03-06 19:23:53 +00:00
if ( string . IsNullOrEmpty ( PrivateResponseString ) )
PrivateResponseString = Encoding . UTF8 . GetString ( ResponseBytes ) ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
return PrivateResponseString ;
2017-05-31 21:02:38 +00:00
}
}
2017-08-09 21:23:43 +00:00
public T ResponseJson < T > ( ) = >
2018-03-06 19:23:53 +00:00
JsonConvert . DeserializeObject < T > ( ResponseString ) ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
[Obsolete]
public short Status = > ( short ) Response ? . StatusCode ;
2017-05-31 21:02:38 +00:00
static WebRequest ( )
2018-03-06 19:23:53 +00:00
= > CreateHttpClientInstance ( ) ;
public WebRequest ( HttpMethod method , string url )
2017-05-31 21:02:38 +00:00
{
2018-03-06 19:23:53 +00:00
Method = method ;
Url = url ;
2017-12-06 03:50:23 +00:00
}
private static void CreateHttpClientInstance ( )
{
HttpClient ? . Dispose ( ) ;
HttpClient = new HttpClient ( new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods . GZip | DecompressionMethods . Deflate
} ) ;
HttpClient . DefaultRequestHeaders . UserAgent . ParseAdd ( USER_AGENT ) ;
HttpClient . DefaultRequestHeaders . ExpectContinue = true ;
2018-03-06 19:23:53 +00:00
HttpClient . Timeout = new TimeSpan ( 0 , 0 , 0 , 0 , System . Threading . Timeout . Infinite ) ;
2017-05-31 21:02:38 +00:00
}
2018-03-06 19:23:53 +00:00
public void AddRaw ( Stream stream )
2017-05-31 21:02:38 +00:00
{
2018-03-06 19:23:53 +00:00
if ( stream = = null )
throw new ArgumentNullException ( nameof ( stream ) ) ;
RawRequestBody ? . Dispose ( ) ;
RawRequestBody = new MemoryStream ( ) ;
stream . CopyTo ( RawRequestBody ) ;
2017-05-31 21:02:38 +00:00
}
2018-03-06 19:23:53 +00:00
public void AddRaw ( byte [ ] bytes )
{
using ( MemoryStream ms = new MemoryStream ( bytes ) )
AddRaw ( ms ) ;
}
2017-05-31 21:02:38 +00:00
2017-08-09 21:23:43 +00:00
public void AddRaw ( string str ) = >
2017-05-31 21:02:38 +00:00
AddRaw ( Encoding . UTF8 . GetBytes ( str ) ) ;
2017-08-09 21:23:43 +00:00
public void AddJson ( object obj )
2017-05-31 21:02:38 +00:00
{
ContentType = JSON_CONTENT_TYPE ;
AddRaw ( JsonConvert . SerializeObject ( obj ) ) ;
}
2017-08-09 21:23:43 +00:00
public void AddParam ( string name , string contents ) = >
2017-10-13 20:05:50 +00:00
Parameters . Add ( name , contents ) ;
2017-05-31 21:02:38 +00:00
2017-08-09 21:23:43 +00:00
public void AddFile ( string name , byte [ ] bytes ) = >
2017-10-13 20:05:50 +00:00
Files . Add ( name , bytes ) ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
public void AddHeader ( string name , string value )
2017-05-31 21:02:38 +00:00
{
2018-03-06 19:23:53 +00:00
if ( string . IsNullOrEmpty ( name ) )
throw new ArgumentNullException ( nameof ( name ) ) ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
if ( value = = null )
throw new ArgumentNullException ( nameof ( value ) ) ;
if ( Headers . ContainsKey ( name ) )
Headers [ name ] = value ;
else
Headers . Add ( name , value ) ;
}
public void Abort ( )
{
IsAborted = true ;
IsCompleted = true ;
try
2017-05-31 21:02:38 +00:00
{
2018-03-06 19:23:53 +00:00
AbortToken ? . Cancel ( ) ;
2017-05-31 21:02:38 +00:00
}
2018-03-06 19:23:53 +00:00
catch ( ObjectDisposedException )
{
// just do nothign in this case
}
}
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
private System . Net . Http . HttpMethod FromInternalHttpMethod ( HttpMethod method )
{
switch ( method )
{
case HttpMethod . GET :
return System . Net . Http . HttpMethod . Get ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
case HttpMethod . DELETE :
return System . Net . Http . HttpMethod . Delete ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
case HttpMethod . POST :
return System . Net . Http . HttpMethod . Post ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
case HttpMethod . PATCH :
return new System . Net . Http . HttpMethod ( @"PATCH" ) ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
case HttpMethod . PUT :
return System . Net . Http . HttpMethod . Put ;
}
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
throw new InvalidOperationException ( $"Unsupported HTTP method {method}." ) ;
}
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
private void PrivatePerform ( )
{
using ( AbortToken = new CancellationTokenSource ( ) )
using ( TimeoutToken = new CancellationTokenSource ( ) )
using ( CancellationTokenSource linkedToken = CancellationTokenSource . CreateLinkedTokenSource ( AbortToken . Token , TimeoutToken . Token ) )
2017-05-31 21:02:38 +00:00
{
2018-03-06 19:23:53 +00:00
try
2017-05-31 21:02:38 +00:00
{
2018-03-06 19:23:53 +00:00
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 ) ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
if ( HasBody )
2017-05-31 21:02:38 +00:00
{
2018-03-06 19:23:53 +00:00
Stream bodyContent ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
if ( RawRequestBody = = null )
2017-05-31 21:02:38 +00:00
{
2018-03-06 19:23:53 +00:00
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 ) ;
2017-05-31 21:02:38 +00:00
}
2018-03-06 19:23:53 +00:00
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 ( ) ;
}
2017-05-31 21:02:38 +00:00
}
2018-03-06 19:23:53 +00:00
ReportProgress ( ) ;
using ( request )
Response = HttpClient . SendAsync ( request , HttpCompletionOption . ResponseHeadersRead , linkedToken . Token ) . Result ;
ResponseStream = new MemoryStream ( ) ;
if ( HasBody )
2017-05-31 21:02:38 +00:00
{
2018-03-06 19:23:53 +00:00
ReportProgress ( ) ;
UploadProgress ? . Invoke ( 0 , BytesUploaded ) ;
}
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
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 ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
Complete ( ex ) ;
}
}
}
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
public async Task PerformAsync ( )
{
if ( IsCompleted )
throw new InvalidOperationException ( $"{nameof(WebRequest)} has already been run, you can't reuse WebRequest objects." ) ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
try
{
await Task . Factory . StartNew ( PrivatePerform , TaskCreationOptions . LongRunning ) ;
}
catch ( AggregateException ex )
{
if ( ex . InnerExceptions . Count ! = 1 )
throw ex ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
while ( ex . InnerExceptions . Count = = 1 )
{
AggregateException innerEx = ex . InnerException as AggregateException ;
ex = innerEx ? ? throw innerEx . InnerException ;
}
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
throw ex ;
}
}
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
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 ;
2017-05-31 21:02:38 +00:00
}
2018-03-06 19:23:53 +00:00
}
}
}
private void Complete ( Exception exception = null )
{
if ( IsAborted | | IsCompleted )
return ;
bool allowRetry = true ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
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 ;
2017-05-31 21:02:38 +00:00
}
}
2018-03-06 19:23:53 +00:00
if ( exception ! = null )
if ( allowRetry & & Retries < MAX_RETRIES & & BytesDownloaded < 1 )
{
+ + Retries ;
PrivatePerform ( ) ;
}
2017-05-31 21:02:38 +00:00
try
{
2018-03-06 19:23:53 +00:00
// process
} catch ( Exception ex )
2017-05-31 21:02:38 +00:00
{
2018-03-06 19:23:53 +00:00
exception = exception = = null ? ex : new AggregateException ( exception , ex ) ;
2017-05-31 21:02:38 +00:00
}
2018-03-06 19:23:53 +00:00
IsCompleted = true ;
if ( exception ! = null )
{
IsAborted = true ;
Failed ? . Invoke ( exception ) ;
throw exception ;
}
Finished ? . Invoke ( ) ;
2017-05-31 21:02:38 +00:00
}
2018-03-06 19:23:53 +00:00
#region Timeout
private long LastReportedAction = 0 ;
private long TimeSinceLastAction = > ( DateTime . Now . Ticks - LastReportedAction ) / TimeSpan . TicksPerMillisecond ;
2017-05-31 21:02:38 +00:00
2018-03-06 19:23:53 +00:00
private void ReportProgress ( )
{
LastReportedAction = DateTime . Now . Ticks ;
TimeoutToken . CancelAfter ( Timeout ) ;
}
#endregion
2017-10-13 20:05:50 +00:00
2018-03-06 19:23:53 +00:00
#region Disposal
public bool IsDisposed { get ; private set ; } = false ;
2017-05-31 21:02:38 +00:00
private void Dispose ( bool disposing )
{
2017-10-13 20:05:50 +00:00
if ( IsDisposed )
return ;
IsDisposed = true ;
2018-03-06 19:23:53 +00:00
// TODO: reimplement disposal
2017-10-13 20:05:50 +00:00
if ( disposing )
2017-11-13 09:42:07 +00:00
GC . SuppressFinalize ( this ) ;
2017-05-31 21:02:38 +00:00
}
~ WebRequest ( )
2017-10-13 20:05:50 +00:00
= > Dispose ( false ) ;
2017-05-31 21:02:38 +00:00
public void Dispose ( )
2017-10-13 20:05:50 +00:00
= > Dispose ( true ) ;
2017-05-31 21:02:38 +00:00
#endregion
}
}