using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading; namespace Maki.Rest { internal class WebRequest : IDisposable { 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"; private const string FORM_CONTENT_TYPE = @"multipart/form-data"; internal readonly HttpMethod Method; internal readonly string Url; internal string ContentType { get; set; } = GENERIC_CONTENT_TYPE; // TODO: make this not static internal static string Authorisation { get; set; } private readonly Dictionary headers = new Dictionary(); private readonly Dictionary parameters = new Dictionary(); private readonly Dictionary files = new Dictionary(); private readonly Dictionary mimeTypes = new Dictionary() { { "png", "image/png" }, { "jpg", "image/jpeg" }, { "jpeg", "image/jpeg" }, { "gif", "image/gif" }, }; private byte[] rawContent = new byte[0]; private HttpWebRequest wRequest; private Stream requestStream; private HttpWebResponse wResponse; private Stream responseStream; private byte[] rawResponse; internal byte[] RawResponse { get { if (rawResponse == null) { rawResponse = new byte[4096]; responseStream.Read(rawResponse, 0, rawResponse.Length); } return rawResponse; } } private string responseString = string.Empty; internal string Response { get { if (string.IsNullOrEmpty(responseString)) responseString = Encoding.UTF8.GetString(RawResponse).Trim('\0'); return responseString; } } internal T ResponseJson() => JsonConvert.DeserializeObject(Response); internal short Status => (short)wResponse?.StatusCode; static WebRequest() { ServicePointManager.Expect100Continue = false; } internal WebRequest(HttpMethod method, string url) { Method = method; Url = url; } internal void AddRaw(byte[] bytes) => rawContent = bytes; internal void AddRaw(string str) => AddRaw(Encoding.UTF8.GetBytes(str)); internal void AddJson(object obj) { ContentType = JSON_CONTENT_TYPE; AddRaw(JsonConvert.SerializeObject(obj)); } internal void AddParam(string name, string contents) => parameters.Add(name, contents); internal void AddFile(string name, byte[] bytes) => files.Add(name, bytes); internal void Perform() { StringBuilder urlBuilder = new StringBuilder(); if (!Url.StartsWith("http://") || !Url.StartsWith("https://")) { urlBuilder.Append(RestEndpoints.BASE_URL); urlBuilder.Append(RestEndpoints.BASE_PATH); } urlBuilder.Append(Url); if (Method == HttpMethod.GET || Method == HttpMethod.DELETE) if (parameters.Count > 1) { if (!Url.Contains('?')) urlBuilder.Append(@"?"); foreach (KeyValuePair param in parameters) urlBuilder.Append($@"{param.Key}={param.Value}&"); } string url = urlBuilder.ToString().TrimEnd('&'); wRequest = System.Net.WebRequest.Create(url) as HttpWebRequest; wRequest.Method = Method.ToString(); wRequest.UserAgent = USER_AGENT; wRequest.KeepAlive = true; //wRequest.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; wRequest.ReadWriteTimeout = Timeout.Infinite; wRequest.Timeout = Timeout.Infinite; if (!string.IsNullOrEmpty(Authorisation)) wRequest.Headers[HttpRequestHeader.Authorization] = Authorisation; foreach (KeyValuePair header in headers) wRequest.Headers[header.Key] = header.Value; if (Method == HttpMethod.POST || Method == HttpMethod.PUT || Method == HttpMethod.PATCH) { requestStream = wRequest.GetRequestStream(); if (parameters.Count + files.Count < 1) requestStream.Write(rawContent, 0, rawContent.Length); else { string boundary = $@"-----------------------------{DateTime.Now.Ticks}"; ContentType = $@"{FORM_CONTENT_TYPE}; boundary={boundary}"; if (parameters.Count >= 1) { StringBuilder postBodyBuilder = new StringBuilder(); byte[] postBody = new byte[0]; foreach (KeyValuePair 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 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); } } wRequest.ContentType = ContentType; try { wResponse = wRequest.GetResponse() as HttpWebResponse; } catch (WebException ex) { wResponse = ex.Response as HttpWebResponse; } responseStream = wResponse.GetResponseStream(); } #region IDisposable private bool isDisposed = false; private void Dispose(bool disposing) { if (!isDisposed) { isDisposed = true; requestStream?.Dispose(); wRequest?.Abort(); responseStream?.Dispose(); wResponse?.Close(); } } ~WebRequest() { Dispose(false); } /// /// Disconnects and releases all unmanaged objects /// public void Dispose() { Dispose(true); GC.SuppressFinalize(true); } #endregion } }