about summary refs log tree commit diff
path: root/LibMatrix
diff options
context:
space:
mode:
Diffstat (limited to 'LibMatrix')
-rw-r--r--LibMatrix/Extensions/JsonConverters.cs29
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Multi.cs (renamed from LibMatrix/Extensions/HttpClientExtensions.cs)44
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Single.cs230
-rw-r--r--LibMatrix/Homeservers/FederationClient.cs2
-rw-r--r--LibMatrix/Homeservers/RemoteHomeServer.cs2
-rw-r--r--LibMatrix/LibMatrix.csproj4
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs4
-rw-r--r--LibMatrix/Services/HomeserverResolverService.cs2
8 files changed, 284 insertions, 33 deletions
diff --git a/LibMatrix/Extensions/JsonConverters.cs b/LibMatrix/Extensions/JsonConverters.cs
new file mode 100644
index 0000000..eed3fb2
--- /dev/null
+++ b/LibMatrix/Extensions/JsonConverters.cs
@@ -0,0 +1,29 @@
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Extensions;
+
+public class JsonFloatStringConverter : JsonConverter<float> {
+    public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        => float.Parse(reader.GetString()!);
+
+    public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options)
+        => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
+}
+
+public class JsonDoubleStringConverter : JsonConverter<double> {
+    public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        => double.Parse(reader.GetString()!);
+
+    public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
+        => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
+}
+
+public class JsonDecimalStringConverter : JsonConverter<decimal> {
+    public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        => decimal.Parse(reader.GetString()!);
+
+    public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
+        => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
+}
\ No newline at end of file
diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/MatrixHttpClient.Multi.cs
index 64b4f6a..e7a2044 100644
--- a/LibMatrix/Extensions/HttpClientExtensions.cs
+++ b/LibMatrix/Extensions/MatrixHttpClient.Multi.cs
@@ -1,8 +1,10 @@
+#define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances
+// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
 using System.Net.Http.Headers;
 using System.Reflection;
+using System.Security.Cryptography.X509Certificates;
 using System.Text;
 using System.Text.Json;
 using System.Text.Json.Serialization;
@@ -25,7 +27,16 @@ public static class HttpClientExtensions {
     }
 }
 
-public class MatrixHttpClient : HttpClient {
+#region Per-instance HTTP client code
+
+#if !SINGLE_HTTPCLIENT
+public class MatrixHttpClient() : HttpClient(handler) {
+    private static readonly SocketsHttpHandler handler = new() {
+        PooledConnectionLifetime = TimeSpan.FromMinutes(15),
+        MaxConnectionsPerServer = 256,
+        EnableMultipleHttp2Connections = true
+    };
+    
     public Dictionary<string, string> AdditionalQueryParameters { get; set; } = new();
     internal string? AssertedUserId { get; set; }
 
@@ -44,7 +55,7 @@ public class MatrixHttpClient : HttpClient {
 
     public async Task<HttpResponseMessage> SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
         if(debug) await _rateLimitSemaphore.WaitAsync(cancellationToken);
-        // Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})");
+        Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})");
         if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
         if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri);
         // if (AssertedUserId is not null) request.RequestUri = request.RequestUri.AddQuery("user_id", AssertedUserId);
@@ -73,6 +84,8 @@ public class MatrixHttpClient : HttpClient {
         finally {
             if(debug) _rateLimitSemaphore.Release();
         }
+        
+        Console.WriteLine($"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.Content.Headers.ContentLength ?? 0)})");
 
         return responseMessage;
     }
@@ -191,27 +204,6 @@ public class MatrixHttpClient : HttpClient {
         await foreach (var resp in result) yield return resp;
     }
 }
+#endif
 
-public class JsonFloatStringConverter : JsonConverter<float> {
-    public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-        => float.Parse(reader.GetString()!);
-
-    public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options)
-        => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
-}
-
-public class JsonDoubleStringConverter : JsonConverter<double> {
-    public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-        => double.Parse(reader.GetString()!);
-
-    public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
-        => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
-}
-
-public class JsonDecimalStringConverter : JsonConverter<decimal> {
-    public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-        => decimal.Parse(reader.GetString()!);
-
-    public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
-        => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
-}
\ No newline at end of file
+#endregion
\ No newline at end of file
diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
new file mode 100644
index 0000000..c9cd260
--- /dev/null
+++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -0,0 +1,230 @@
+#define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances
+// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using ArcaneLibs;
+using ArcaneLibs.Extensions;
+
+namespace LibMatrix.Extensions;
+
+#if SINGLE_HTTPCLIENT
+// TODO: Add URI wrapper for 
+public class MatrixHttpClient {
+    private static readonly HttpClient Client;
+
+    static MatrixHttpClient() {
+        try {
+            var handler = new SocketsHttpHandler {
+                PooledConnectionLifetime = TimeSpan.FromMinutes(15),
+                MaxConnectionsPerServer = 4096,
+                EnableMultipleHttp2Connections = true
+            };
+            Client = new HttpClient(handler) {
+                DefaultRequestVersion = new Version(3, 0)
+            };
+        }
+        catch (PlatformNotSupportedException e) {
+            Console.WriteLine("Failed to create HttpClient with connection pooling, continuing without connection pool!");
+            Console.WriteLine("Original exception (safe to ignore!):");
+            Console.WriteLine(e);
+
+            Client = new HttpClient {
+                DefaultRequestVersion = new Version(3, 0)
+            };
+        }
+        catch (Exception e) {
+            Console.WriteLine("Failed to create HttpClient:");
+            Console.WriteLine(e);
+            throw;
+        }
+    }
+
+#if SYNC_HTTPCLIENT
+    internal SemaphoreSlim _rateLimitSemaphore { get; } = new(1, 1);
+#endif
+
+    public Dictionary<string, string> AdditionalQueryParameters { get; set; } = new();
+
+    public Uri? BaseAddress { get; set; }
+
+    // default headers, not bound to client
+    public HttpRequestHeaders DefaultRequestHeaders { get; set; } =
+        typeof(HttpRequestHeaders).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [], null)?.Invoke([]) as HttpRequestHeaders ??
+        throw new InvalidOperationException("Failed to create HttpRequestHeaders");
+
+    private JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) {
+        options ??= new JsonSerializerOptions();
+        options.Converters.Add(new JsonFloatStringConverter());
+        options.Converters.Add(new JsonDoubleStringConverter());
+        options.Converters.Add(new JsonDecimalStringConverter());
+        options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
+        return options;
+    }
+
+    public async Task<HttpResponseMessage> SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
+#if SYNC_HTTPCLIENT
+        await _rateLimitSemaphore.WaitAsync(cancellationToken);
+#endif
+
+        Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})");
+
+        if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
+        if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri);
+        foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value);
+        foreach (var (key, value) in DefaultRequestHeaders) request.Headers.Add(key, value);
+
+        request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true);
+
+        HttpResponseMessage? responseMessage;
+        try {
+            responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+        }
+        catch (Exception e) {
+            Console.WriteLine(
+                $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}");
+            throw;
+        }
+#if SYNC_HTTPCLIENT
+        finally {
+            _rateLimitSemaphore.Release();
+        }
+#endif
+
+        Console.WriteLine(
+            $"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.Content.Headers.ContentLength ?? 0)})");
+
+        return responseMessage;
+    }
+
+    public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) {
+        var responseMessage = await SendUnhandledAsync(request, cancellationToken);
+        if (responseMessage.IsSuccessStatusCode) return responseMessage;
+
+        //error handling
+        var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
+        if (content.Length == 0)
+            throw new MatrixException() {
+                ErrorCode = "M_UNKNOWN",
+                Error = "Unknown error, server returned no content"
+            };
+        if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content);
+        //we have a matrix error
+
+        MatrixException? ex = null;
+        try {
+            ex = JsonSerializer.Deserialize<MatrixException>(content);
+        }
+        catch (JsonException e) {
+            throw new LibMatrixException() {
+                ErrorCode = "M_INVALID_JSON",
+                Error = e.Message + "\nBody:\n" + await responseMessage.Content.ReadAsStringAsync(cancellationToken)
+            };
+        }
+
+        Debug.Assert(ex != null, nameof(ex) + " != null");
+        ex.RawContent = content;
+        // Console.WriteLine($"Failed to send request: {ex}");
+        if (ex?.RetryAfterMs is null) throw ex!;
+        //we have a ratelimit error
+        await Task.Delay(ex.RetryAfterMs.Value, cancellationToken);
+        request.ResetSendStatus();
+        return await SendAsync(request, cancellationToken);
+    }
+
+    // GetAsync
+    public Task<HttpResponseMessage> GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken? cancellationToken = null) =>
+        SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None);
+
+    // GetFromJsonAsync
+    public async Task<T?> TryGetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) {
+        try {
+            return await GetFromJsonAsync<T>(requestUri, options, cancellationToken);
+        }
+        catch (JsonException e) {
+            Console.WriteLine($"Failed to deserialize response from {requestUri}: {e.Message}");
+            return default;
+        }
+        catch (HttpRequestException e) {
+            Console.WriteLine($"Failed to get {requestUri}: {e.Message}");
+            return default;
+        }
+    }
+
+    public async Task<T> GetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) {
+        options = GetJsonSerializerOptions(options);
+        var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
+        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+        var response = await SendAsync(request, cancellationToken);
+        response.EnsureSuccessStatusCode();
+        await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
+
+        return await JsonSerializer.DeserializeAsync<T>(responseStream, options, cancellationToken) ??
+               throw new InvalidOperationException("Failed to deserialize response");
+    }
+
+    // GetStreamAsync
+    public new async Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) {
+        var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
+        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+        var response = await SendAsync(request, cancellationToken);
+        response.EnsureSuccessStatusCode();
+        return await response.Content.ReadAsStreamAsync(cancellationToken);
+    }
+
+    public async Task<HttpResponseMessage> PutAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
+        CancellationToken cancellationToken = default) where T : notnull {
+        options = GetJsonSerializerOptions(options);
+        var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
+        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+        request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options),
+            Encoding.UTF8, "application/json");
+        return await SendAsync(request, cancellationToken);
+    }
+
+    public async Task<HttpResponseMessage> PostAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
+        CancellationToken cancellationToken = default) where T : notnull {
+        options ??= new JsonSerializerOptions();
+        options.Converters.Add(new JsonFloatStringConverter());
+        options.Converters.Add(new JsonDoubleStringConverter());
+        options.Converters.Add(new JsonDecimalStringConverter());
+        options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
+        var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
+        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+        request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options),
+            Encoding.UTF8, "application/json");
+        return await SendAsync(request, cancellationToken);
+    }
+
+    public async IAsyncEnumerable<T?> GetAsyncEnumerableFromJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, JsonSerializerOptions? options = null) {
+        options = GetJsonSerializerOptions(options);
+        var res = await GetAsync(requestUri);
+        var result = JsonSerializer.DeserializeAsyncEnumerable<T>(await res.Content.ReadAsStreamAsync(), options);
+        await foreach (var resp in result) yield return resp;
+    }
+
+    public async Task<bool> CheckSuccessStatus(string url) {
+        //cors causes failure, try to catch
+        try {
+            var resp = await Client.GetAsync(url);
+            return resp.IsSuccessStatusCode;
+        }
+        catch (Exception e) {
+            Console.WriteLine($"Failed to check success status: {e.Message}");
+            return false;
+        }
+    }
+
+    public async Task<HttpResponseMessage> PostAsync(string uri, HttpContent? content, CancellationToken cancellationToken = default) {
+        var request = new HttpRequestMessage(HttpMethod.Post, uri) {
+            Content = content
+        };
+        return await SendAsync(request, cancellationToken);
+    }
+}
+#endif
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs
index dc0d1f6..22653e4 100644
--- a/LibMatrix/Homeservers/FederationClient.cs
+++ b/LibMatrix/Homeservers/FederationClient.cs
@@ -9,7 +9,7 @@ public class FederationClient {
     public FederationClient(string federationEndpoint, string? proxy = null) {
         HttpClient = new MatrixHttpClient {
             BaseAddress = new Uri(proxy?.TrimEnd('/') ?? federationEndpoint.TrimEnd('/')),
-            Timeout = TimeSpan.FromSeconds(120)
+            // Timeout = TimeSpan.FromSeconds(120) // TODO: Re-implement this
         };
         if (proxy is not null) HttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", federationEndpoint);
     }
diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index 8669ca7..ecf3e3a 100644
--- a/LibMatrix/Homeservers/RemoteHomeServer.cs
+++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -19,7 +19,7 @@ public class RemoteHomeserver {
         WellKnownUris = wellKnownUris;
         ClientHttpClient = new MatrixHttpClient {
             BaseAddress = new Uri(proxy?.TrimEnd('/') ?? wellKnownUris.Client?.TrimEnd('/') ?? throw new InvalidOperationException($"No client URI for {baseUrl}!")),
-            Timeout = TimeSpan.FromSeconds(300)
+            // Timeout = TimeSpan.FromSeconds(300) // TODO: Re-implement this
         };
 
         if (proxy is not null) ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", baseUrl);
diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index b85df52..d0511ea 100644
--- a/LibMatrix/LibMatrix.csproj
+++ b/LibMatrix/LibMatrix.csproj
@@ -11,8 +11,8 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0"/>
-        <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0"/>
+        <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
+        <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
     </ItemGroup>
 
     <ItemGroup>
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index f15327c..fe2ee8d 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -531,14 +531,14 @@ public class GenericRoom {
         if (!string.IsNullOrEmpty(to)) uri = uri.AddQuery("to", to);
 
         // Console.WriteLine($"Getting related events from {uri}");
-        var result = await Homeserver.ClientHttpClient.GetFromJsonAsync<RecursedBatchedChunkedStateEventResponse>(uri);
+        var result = await Homeserver.ClientHttpClient.GetFromJsonAsync<RecursedBatchedChunkedStateEventResponse>(uri.ToString());
         while (result!.Chunk.Count > 0) {
             foreach (var resp in result.Chunk) {
                 yield return resp;
             }
 
             if (result.NextBatch is null) break;
-            result = await Homeserver.ClientHttpClient.GetFromJsonAsync<RecursedBatchedChunkedStateEventResponse>(uri.AddQuery("from", result.NextBatch));
+            result = await Homeserver.ClientHttpClient.GetFromJsonAsync<RecursedBatchedChunkedStateEventResponse>(uri.AddQuery("from", result.NextBatch).ToString());
         }
     }
 
diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs
index 05ce733..f899230 100644
--- a/LibMatrix/Services/HomeserverResolverService.cs
+++ b/LibMatrix/Services/HomeserverResolverService.cs
@@ -14,7 +14,7 @@ namespace LibMatrix.Services;
 
 public class HomeserverResolverService {
     private readonly MatrixHttpClient _httpClient = new() {
-        Timeout = TimeSpan.FromSeconds(60)
+        // Timeout = TimeSpan.FromSeconds(60) // TODO: Re-implement this
     };
 
     private static readonly SemaphoreCache<WellKnownUris> WellKnownCache = new();