// 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;
}
}
}
}