diff --git a/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs b/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs index 0434bfe8a..7eb9bfc20 100644 --- a/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs +++ b/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs @@ -16,21 +16,18 @@ // 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 System; +using System.Globalization; +using System.Threading.Tasks; using Duplicati.Server.Serialization; namespace Duplicati.Server.WebServer { - public class BodyWriter : System.IO.StreamWriter, IDisposable + public class BodyWriter : IDisposable, IAsyncDisposable { private readonly HttpServer.IHttpResponse m_resp; private readonly string m_jsonp; private static readonly object SUCCESS_RESPONSE = new { Status = "OK" }; - - // We override the format provider so all JSON output uses US format - public override IFormatProvider FormatProvider - { - get { return System.Globalization.CultureInfo.InvariantCulture; } - } + private readonly System.IO.StreamWriter m_bodyStreamWriter; public BodyWriter(HttpServer.IHttpResponse resp, HttpServer.IHttpRequest request) : this(resp, request.QueryString["jsonp"].Value) @@ -38,25 +35,13 @@ namespace Duplicati.Server.WebServer } public BodyWriter(HttpServer.IHttpResponse resp, string jsonp) - : base(resp.Body, resp.Encoding) { + m_bodyStreamWriter = new System.IO.StreamWriter(resp.Body, resp.Encoding); m_resp = resp; m_jsonp = jsonp; if (!m_resp.HeadersSent) m_resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0"); } - - protected override void Dispose (bool disposing) - { - if (!m_resp.HeadersSent) - { - base.Flush(); - m_resp.ContentLength = base.BaseStream.Length; - m_resp.Send(); - } - base.Dispose(disposing); - } - public void SetOK() { m_resp.Reason = "OK"; @@ -74,22 +59,61 @@ namespace Duplicati.Server.WebServer if (!m_resp.HeadersSent) m_resp.ContentType = "application/json"; - using(this) + Task.Run(async () => { + + if (!string.IsNullOrEmpty(m_jsonp)) + { + await m_bodyStreamWriter.WriteAsync(m_jsonp); + await m_bodyStreamWriter.WriteAsync('('); + } + + var oldCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + await Serializer.SerializeJsonAsync(m_bodyStreamWriter, o, true); + CultureInfo.CurrentCulture = oldCulture; + + if (!string.IsNullOrEmpty(m_jsonp)) + { + await m_bodyStreamWriter.WriteAsync(')'); + await m_bodyStreamWriter.FlushAsync(); + } + }).GetAwaiter().GetResult(); + } + + public void Dispose() + { + Task.Run(async () => { + await DisposeAsync(); + }).GetAwaiter().GetResult(); + } + + private bool disposed = false; + public async ValueTask DisposeAsync() + { + if(disposed) return; + disposed = true; + + if (!m_resp.HeadersSent) { - if (!string.IsNullOrEmpty(m_jsonp)) - { - this.Write(m_jsonp); - this.Write('('); - } - - Serializer.SerializeJson(this, o, true); - - if (!string.IsNullOrEmpty(m_jsonp)) - { - this.Write(')'); - this.Flush(); - } + await m_bodyStreamWriter.FlushAsync(); + m_resp.ContentLength = m_bodyStreamWriter.BaseStream.Length; + m_resp.Send(); } + await m_bodyStreamWriter.DisposeAsync(); + } + + internal void Flush() + { + Task.Run(async () => { + await m_bodyStreamWriter.FlushAsync(); + }).GetAwaiter().GetResult(); + } + + internal void Write(string v) + { + Task.Run(async () => { + await m_bodyStreamWriter.WriteAsync(v); + }).GetAwaiter().GetResult(); } } diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs b/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs index 36eb7870c..f9a34216e 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -39,6 +41,23 @@ namespace Duplicati.Server.Serialization } } + public static async Task SerializeJsonAsync(System.IO.TextWriter tw, object o, bool preventDispose = false) + { + Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(m_jsonSettings); + StringBuilder sb = new StringBuilder(); + StringWriter sw = new StringWriter(sb); + var jsonWriter = new JsonTextWriter(sw); + using (preventDispose ? null : jsonWriter) + { + jsonWriter.Formatting = m_jsonFormatting; + jsonSerializer.Serialize(jsonWriter, o); + await jsonWriter.FlushAsync(); + } + await sw.FlushAsync(); + await tw.WriteAsync(sb.ToString()); + await tw.FlushAsync(); + } + public static T Deserialize(System.IO.TextReader sr) { Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(m_jsonSettings); diff --git a/Duplicati/WebserverCore/LegacyHttpRequestShim.cs b/Duplicati/WebserverCore/LegacyHttpRequestShim.cs index 3c13a4570..470ef82f6 100644 --- a/Duplicati/WebserverCore/LegacyHttpRequestShim.cs +++ b/Duplicati/WebserverCore/LegacyHttpRequestShim.cs @@ -23,21 +23,38 @@ class LegacyHttpRequestShim : HttpServer.IHttpRequest public HttpForm Form => throw new NotImplementedException(); - public NameValueCollection Headers => throw new NotImplementedException(); + public NameValueCollection Headers + { + get + { + var headers = new NameValueCollection(); + foreach (var pair in request.Headers) + { + foreach (var value in pair.Value) + { + headers.Add(pair.Key, value); + } + } + return headers; + } + } public string HttpVersion { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public bool IsAjax => throw new NotImplementedException(); - public string Method { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public string Method { get => request.Method; set => throw new NotImplementedException(); } public HttpParam Param => throw new NotImplementedException(); - public HttpInput QueryString { get { + public HttpInput QueryString + { + get + { var input = new HttpInput("QueryString"); - foreach (var kvp in request.Query ) { input.Add(kvp.Key, kvp.Value); } + foreach (var kvp in request.Query) { input.Add(kvp.Key, kvp.Value); } return input; - } + } } public bool Secure => throw new NotImplementedException(); diff --git a/Duplicati/WebserverCore/LegacyHttpResponseShim.cs b/Duplicati/WebserverCore/LegacyHttpResponseShim.cs index a9f4c633b..acf6690ec 100644 --- a/Duplicati/WebserverCore/LegacyHttpResponseShim.cs +++ b/Duplicati/WebserverCore/LegacyHttpResponseShim.cs @@ -16,10 +16,10 @@ class LegacyHttpResponseShim : HttpServer.IHttpResponse public ConnectionType Connection { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public Encoding Encoding { get => Encoding.UTF8; set => throw new NotImplementedException(); } public int KeepAlive { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public HttpStatusCode Status { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public string Reason { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public HttpStatusCode Status { get => throw new NotImplementedException(); set => response.StatusCode = (int)value; } + public string Reason { get => throw new NotImplementedException(); set { /*NOP*/ } } public long ContentLength { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public string ContentType { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public string ContentType { get => throw new NotImplementedException(); set => response.ContentType = value; } public bool HeadersSent => response.HasStarted; @@ -44,7 +44,7 @@ class LegacyHttpResponseShim : HttpServer.IHttpResponse public void Send() { - throw new NotImplementedException(); + response.StartAsync(); } public void SendBody(byte[] buffer, int offset, int count) diff --git a/Duplicati/WebserverCore/RestHandlerCore.cs b/Duplicati/WebserverCore/RestHandlerCore.cs index a03f6e192..3c1d74290 100644 --- a/Duplicati/WebserverCore/RestHandlerCore.cs +++ b/Duplicati/WebserverCore/RestHandlerCore.cs @@ -24,6 +24,7 @@ using Org.BouncyCastle.Bcpg.Sig; using System.Collections.Concurrent; using System.Net; using System.Reflection; +using System.Xml.Linq; namespace Duplicati.WebserverCore { @@ -32,9 +33,7 @@ namespace Duplicati.WebserverCore public const string API_URI_PATH = "/api/v1"; public static readonly int API_URI_SEGMENTS = API_URI_PATH.Split(new char[] { '/' }).Length; - private static readonly Dictionary _modules = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public static IDictionary Modules { get { return _modules; } } + private readonly Dictionary _modules = new Dictionary(StringComparer.OrdinalIgnoreCase); public RESTHandlerCoreController(IEnumerable restMethods) { @@ -49,7 +48,7 @@ namespace Duplicati.WebserverCore [Route(API_URI_PATH + "/{module?}/{key?}")] [AcceptVerbs("GET", "POST", "PUT", "DELETE", "PATCH")] - public async Task Index(string? module, string? key) + public async Task Index(string? module, string? key) { module = (module != null && module != "") ? module : "help"; @@ -142,7 +141,7 @@ namespace Duplicati.WebserverCore } } - return Ok(); + return null; } @@ -213,7 +212,7 @@ namespace Duplicati.WebserverCore { foreach (var t in lst) { - services.AddSingleton(t); + services.AddSingleton((IRESTMethod?)Activator.CreateInstance(t) ?? throw new NullReferenceException("RestMethod " + nameof(t) + " is null")); } });