// Copyright (C) 2015, The Duplicati Team // http://www.duplicati.com, info@duplicati.com // // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation; either version 2.1 of the // License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using Duplicati.Library.Utility; using Newtonsoft.Json; using System; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Net.Http.Headers; namespace Duplicati.Library { public class JSONWebHelper : IDisposable { /// /// The User-Agent default value /// public static readonly string USER_AGENT = string.Format("Duplicati v{0}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); /// /// The currently set User-Agent value /// private readonly string m_user_agent; /// /// The URL to perform the OAuth login /// public string OAuthLoginUrl { get; protected set; } /// /// Gets the current User-Agent value /// public string UserAgent { get { return m_user_agent; } } /// /// A callback method to help set up the request /// public event Action CreateSetupHelper; /// /// The internal client used to perform the requests /// protected readonly HttpClient m_client = new HttpClient(); /// /// Constructs a new JSONWebHelper /// /// The User-Agent string to use public JSONWebHelper(string useragent = null) { m_user_agent = useragent ?? USER_AGENT; } /// /// Method used to create the request /// /// The target url /// The method to use /// A created request public virtual Task CreateRequestAsync(string url, string method, CancellationToken cancelToken) { var req = new HttpRequestMessage(new HttpMethod(method ?? "GET"), url); req.Headers.UserAgent.Clear(); req.Headers.UserAgent.ParseAdd(UserAgent); if (CreateSetupHelper != null) CreateSetupHelper(req); return Task.FromResult(req); } /// /// Performs a multipart post and parses the response as JSON /// /// The parsed JSON item. /// The url to post to. /// The optional setup callback method. /// Token to cancel the operation. /// The multipart items. /// The return type parameter. public virtual async Task PostMultipartAndGetJSONDataAsync(string url, Action setup, CancellationToken cancelToken, params MultipartItem[] parts) { var response = await PostMultipartAsync(url, setup, cancelToken, parts).ConfigureAwait(false); return await ReadJSONResponseAsync(response, cancelToken); } /// /// List of protected headers that need special handling to se /// /// The header name /// true if the header is protected; false otherwise private static bool IsProtectedHeader(string k) => new [] { "Content-Type", "Content-Disposition", "Content-Length" } .Any(x => string.Equals(x, k, StringComparison.OrdinalIgnoreCase)); /// /// Performs a multipart post /// /// The response. /// The url to post to. /// The multipart items. /// The optional setup callback method. /// Token to cancel the operation. public virtual async Task PostMultipartAsync(string url, Action setup, CancellationToken cancelToken, params MultipartItem[] parts) { using(var mpdata = new MultipartFormDataContent(CreateBoundary())) { foreach(var p in parts) { var sc = new StreamContent(p.ContentData); foreach(var h in p.Headers) if (!IsProtectedHeader(h.Key)) sc.Headers.Add(h.Key, h.Value); if (p.ContentLength >= 0) sc.Headers.ContentLength = p.ContentLength; if (!string.IsNullOrWhiteSpace(p.ContentTypeName)) sc.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse(p.Headers["Content-Disposition"]); if (!string.IsNullOrWhiteSpace(p.ContentType)) sc.Headers.ContentType = MediaTypeHeaderValue.Parse(p.ContentType); mpdata.Add(sc); } var req = await CreateRequestAsync(url, "POST", cancelToken); setup?.Invoke(req); req.Content = mpdata; return await m_client.SendAsync(req, cancelToken); } } /// /// Creates a random form bondary string /// private static string CreateBoundary() => "----DuplicatiFormBoundary" + Guid.NewGuid().ToString("N"); /// /// Executes a web request and json-deserializes the results as the specified type /// /// The deserialized JSON data. /// The remote URL /// Token to cancel the operation. /// A callback method that can be used to customize the request, e.g. by setting the method, content-type and headers. /// A callback method that can be used to submit data into the body of the request. /// The type of data to return. public virtual Task GetJSONDataAsync(string url, CancellationToken cancelToken) => GetJSONDataAsync(url, null, cancelToken); /// /// Executes a web request and json-deserializes the results as the specified type /// /// The deserialized JSON data. /// The remote URL /// Token to cancel the operation. /// A callback method that can be used to customize the request, e.g. by setting the method, content-type and headers. /// A callback method that can be used to submit data into the body of the request. /// The type of data to return. public virtual async Task GetJSONDataAsync(string url, Action setup, CancellationToken cancelToken) { var req = await CreateRequestAsync(url, null, cancelToken); setup?.Invoke(req); return await ReadJSONResponseAsync(req, cancelToken).ConfigureAwait(false); } /// /// Executes a web request by POST'ing the supplied object and json-deserializes the results as the specified type /// /// The deserialized JSON data. /// The remote URL /// The data to json-serialize and POST in the request /// Alternate HTTP method to use /// Token to cancel the operation. /// The type of data to return. public virtual Task PostAndGetJSONDataAsync(string url, object item, string method, CancellationToken cancelToken) { var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(item)); return GetJSONDataAsync( url, req => { req.Method = new HttpMethod(method ?? "POST"); var sc = new StreamContent(new MemoryStream(data)); sc.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); sc.Headers.ContentLength = data.Length; req.Content = sc; }, cancelToken ); } /// /// Performs a POST request with the given data, serialized as JSON, and returns the deserialized JSON result /// /// The remote URL /// The JSON object /// The alternate method /// The cancellation token /// The type of data to return. /// The deserialized JSON data. public virtual Task ReadJSONResponseAsync(string url, object requestdata, string method, CancellationToken cancelToken) { if (requestdata is string) throw new ArgumentException("Cannot send string object as data"); if (method == null && requestdata != null) method = "POST"; return PostAndGetJSONDataAsync(url, requestdata, method, cancelToken); } /// /// Performs the request and returns the deserialized JSON result /// /// /// The cancellation token /// The type of data to return. /// The deserialized JSON data. public virtual async Task ReadJSONResponseAsync(HttpRequestMessage req, CancellationToken cancelToken) => await ReadJSONResponseAsync(await m_client.SendAsync(req, cancelToken), cancelToken); /// /// Extracts the JSON data an deserializes it, handling invalid JSON data with an improved exception message /// /// The response to read from /// The cancellation token /// The type of data to return. /// The deserialized JSON data. public virtual async Task ReadJSONResponseAsync(HttpResponseMessage resp, CancellationToken cancelToken) { using(var ps = new StreamPeekReader(await resp.Content.ReadAsStreamAsync())) { try { using (var tr = new System.IO.StreamReader(ps)) using (var jr = new Newtonsoft.Json.JsonTextReader(tr)) return new Newtonsoft.Json.JsonSerializer().Deserialize(jr); } catch (Exception ex) { // If we get invalid JSON, report the peek value if (ex is Newtonsoft.Json.JsonReaderException) throw new IOException($"Invalid JSON data: \"{ps.PeekData()}\"", ex); // Otherwise, we have no additional help to offer throw; } } } /// /// Use this method to register an exception handler, /// which can throw another, more meaningful exception /// /// The exception being processed. protected virtual Task ParseExceptionAsync(Exception ex) { return Task.FromResult(true); } /// /// Performs a POST request with the given data, response even if the error code is an error /// /// The remote URL /// The JSON object /// The alternate method /// The cancellation token /// The response. public async Task GetResponseWithoutExceptionAsync(string url, object requestdata, string method, CancellationToken cancelToken) { if (requestdata is string) throw new ArgumentException("Cannot send string object as data"); if (method == null && requestdata != null) method = "POST"; return await GetResponseWithoutExceptionAsync(await CreateRequestAsync(url, method, cancelToken), requestdata, cancelToken); } /// /// /// /// The request to execute /// The content, either a stream or an object /// The cancellation token /// The response. public async Task GetResponseWithoutExceptionAsync(HttpRequestMessage req, object requestdata, CancellationToken cancelToken) { if (requestdata != null) { if (requestdata is Stream stream) { var sc = new StreamContent(stream); sc.Headers.ContentLength = stream.Length; sc.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream"); req.Content = sc; } else { var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(requestdata)); var sc = new StreamContent(new MemoryStream(data)); sc.Headers.ContentLength = data.Length; sc.Headers.ContentLength = data.Length; sc.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; charset=UTF-8"); req.Content = sc; } } return await m_client.SendAsync(req, cancelToken); } public async Task GetResponseAsync(string url, object requestdata, string method, CancellationToken cancelToken) { if (requestdata is string) throw new ArgumentException("Cannot send string object as data"); if (method == null && requestdata != null) method = "POST"; return await GetResponseAsync(await CreateRequestAsync(url, method, cancelToken), requestdata, cancelToken); } public async Task GetResponseAsync(HttpRequestMessage req, object requestdata, CancellationToken cancelToken) { try { var resp = await GetResponseWithoutExceptionAsync(req, requestdata, cancelToken); if (!resp.IsSuccessStatusCode) throw new HttpRequestStatusException(resp); resp.EnsureSuccessStatusCode(); return resp; } catch (Exception ex) { await ParseExceptionAsync(ex); throw; } } public void Dispose() { m_client.Dispose(); } public class HttpRequestStatusException : HttpRequestException { public readonly HttpResponseMessage Response; public HttpRequestStatusException(HttpResponseMessage resp) : base(resp.ReasonPhrase) { Response = resp; } } /// /// A utility class that shadows the real stream but provides access /// to the first 2kb of the stream to use in error reporting /// protected class StreamPeekReader : Stream { private readonly Stream m_base; private readonly byte[] m_peekbuffer = new byte[1024 * 2]; private int m_peekbytes = 0; public StreamPeekReader(Stream source) { m_base = source; } public string PeekData() { if (m_peekbuffer.Length == 0) return string.Empty; return Encoding.UTF8.GetString(m_peekbuffer, 0, m_peekbytes); } public override bool CanRead => m_base.CanRead; public override bool CanSeek => m_base.CanSeek; public override bool CanWrite => m_base.CanWrite; public override long Length => m_base.Length; public override long Position { get => m_base.Position; set => m_base.Position = value; } public override void Flush() => m_base.Flush(); public override long Seek(long offset, SeekOrigin origin) => m_base.Seek(offset, origin); public override void SetLength(long value) => m_base.SetLength(value); public override void Write(byte[] buffer, int offset, int count) => m_base.Write(buffer, offset, count); public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) => m_base.BeginRead(buffer, offset, count, callback, state); public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) => m_base.BeginWrite(buffer, offset, count, callback, state); public override bool CanTimeout => m_base.CanTimeout; public override void Close() => m_base.Close(); public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => m_base.CopyToAsync(destination, bufferSize, cancellationToken); protected override void Dispose(bool disposing) => base.Dispose(disposing); public override int EndRead(IAsyncResult asyncResult) => m_base.EndRead(asyncResult); public override void EndWrite(IAsyncResult asyncResult) => m_base.EndWrite(asyncResult); public override Task FlushAsync(CancellationToken cancellationToken) => m_base.FlushAsync(cancellationToken); public override int ReadTimeout { get => m_base.ReadTimeout; set => m_base.ReadTimeout = value; } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => m_base.WriteAsync(buffer, offset, count, cancellationToken); public override int WriteTimeout { get => m_base.WriteTimeout; set => m_base.WriteTimeout = value; } public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { var br = 0; if (m_peekbytes < m_peekbuffer.Length - 1) { var maxb = Math.Min(count, m_peekbuffer.Length - m_peekbytes); br = await m_base.ReadAsync(m_peekbuffer, m_peekbytes, maxb, cancellationToken); Array.Copy(m_peekbuffer, m_peekbytes, buffer, offset, br); m_peekbytes += br; offset += br; count -= br; if (count == 0 || br < maxb) return br; } return await m_base.ReadAsync(buffer, offset, count, cancellationToken) + br; } public override int Read(byte[] buffer, int offset, int count) { var br = 0; if (m_peekbytes < m_peekbuffer.Length - 1) { var maxb = Math.Min(count, m_peekbuffer.Length - m_peekbytes); br = m_base.Read(m_peekbuffer, m_peekbytes, maxb); Array.Copy(m_peekbuffer, m_peekbytes, buffer, offset, br); m_peekbytes += br; offset += br; count -= br; if (count == 0 || br < maxb) return br; } return m_base.Read(buffer, offset, count) + br; } } } }