about summary refs log tree commit diff
path: root/LibMatrix/Services/HomeserverResolverService.cs
blob: 42ad0a1ab894dec2ee33064894ffc6fa881f2a5a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using ArcaneLibs.Collections;
using ArcaneLibs.Extensions;
using LibMatrix.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace LibMatrix.Services;

public class HomeserverResolverService {
    private readonly MatrixHttpClient _httpClient = new() {
        Timeout = TimeSpan.FromMilliseconds(10000)
    };

    private static readonly SemaphoreCache<WellKnownUris> WellKnownCache = new();

    private readonly ILogger<HomeserverResolverService> _logger;

    public HomeserverResolverService(ILogger<HomeserverResolverService> logger) {
        _logger = logger;
        if (logger is NullLogger<HomeserverResolverService>) {
            var stackFrame = new StackTrace(true).GetFrame(1);
            Console.WriteLine(
                $"WARN | Null logger provided to HomeserverResolverService!\n{stackFrame.GetMethod().DeclaringType} at {stackFrame.GetFileName()}:{stackFrame.GetFileLineNumber()}");
        }
    }

    private static SemaphoreSlim _wellKnownSemaphore = new(1, 1);

    public async Task<WellKnownUris> ResolveHomeserverFromWellKnown(string homeserver) {
        ArgumentNullException.ThrowIfNull(homeserver);

        return await WellKnownCache.GetOrAdd(homeserver, async () => {
            await _wellKnownSemaphore.WaitAsync();
            _logger.LogTrace($"Resolving homeserver well-knowns: {homeserver}");
            var client = _tryResolveClientEndpoint(homeserver);

            var res = new WellKnownUris();

            // try {
            res.Client = await client ?? throw new Exception("Could not resolve client URL.");
            // }
            // catch (Exception e) {
            // _logger.LogError(e, "Error resolving client well-known for {hs}", homeserver);
            // }

            var server = _tryResolveServerEndpoint(homeserver);

            // try {
            res.Server = await server ?? throw new Exception("Could not resolve server URL.");
            // }
            // catch (Exception e) {
            // _logger.LogError(e, "Error resolving server well-known for {hs}", homeserver);
            // }

            _logger.LogInformation("Resolved well-knowns for {hs}: {json}", homeserver, res.ToJson(indent: false));
            _wellKnownSemaphore.Release();
            return res;
        });
    }

    private async Task<string?> _tryResolveClientEndpoint(string homeserver) {
        ArgumentNullException.ThrowIfNull(homeserver);
        _logger.LogTrace("Resolving client well-known: {homeserver}", homeserver);
        ClientWellKnown? clientWellKnown = null;
        // check if homeserver has a client well-known
        if (homeserver.StartsWith("https://")) {
            clientWellKnown = await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client");
        }
        else if (homeserver.StartsWith("http://")) {
            clientWellKnown = await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client");
        }
        else {
            clientWellKnown ??= await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"https://{homeserver}/.well-known/matrix/client");
            clientWellKnown ??= await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"http://{homeserver}/.well-known/matrix/client");

            if (clientWellKnown is null) {
                if (await _httpClient.CheckSuccessStatus($"https://{homeserver}/_matrix/client/versions"))
                    return $"https://{homeserver}";
                if (await _httpClient.CheckSuccessStatus($"http://{homeserver}/_matrix/client/versions"))
                    return $"http://{homeserver}";
            }
        }

        if (!string.IsNullOrWhiteSpace(clientWellKnown?.Homeserver.BaseUrl))
            return clientWellKnown.Homeserver.BaseUrl;

        _logger.LogInformation("No client well-known...");
        return null;
    }

    private async Task<string?> _tryResolveServerEndpoint(string homeserver) {
        // TODO: implement SRV delegation via DoH: https://developers.google.com/speed/public-dns/docs/doh/json
        ArgumentNullException.ThrowIfNull(homeserver);
        _logger.LogTrace($"Resolving server well-known: {homeserver}");
        ServerWellKnown? serverWellKnown = null;
        // check if homeserver has a server well-known
        if (homeserver.StartsWith("https://")) {
            serverWellKnown = await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server");
        }
        else if (homeserver.StartsWith("http://")) {
            serverWellKnown = await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server");
        }
        else {
            serverWellKnown ??= await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"https://{homeserver}/.well-known/matrix/server");
            serverWellKnown ??= await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"http://{homeserver}/.well-known/matrix/server");
        }

        _logger.LogInformation("Server well-known for {hs}: {json}", homeserver, serverWellKnown?.ToJson() ?? "null");

        if (!string.IsNullOrWhiteSpace(serverWellKnown?.Homeserver)) {
            var resolved = serverWellKnown.Homeserver;
            if (resolved.StartsWith("https://") || resolved.StartsWith("http://"))
                return resolved;
            if (await _httpClient.CheckSuccessStatus($"https://{resolved}/_matrix/federation/v1/version"))
                return $"https://{resolved}";
            if (await _httpClient.CheckSuccessStatus($"http://{resolved}/_matrix/federation/v1/version"))
                return $"http://{resolved}";
            _logger.LogWarning("Server well-known points to invalid server: {resolved}", resolved);
        }

        // fallback: most servers host C2S and S2S on the same domain
        var clientUrl = await _tryResolveClientEndpoint(homeserver);
        if (clientUrl is not null && await _httpClient.CheckSuccessStatus($"{clientUrl}/_matrix/federation/v1/version"))
            return clientUrl;

        _logger.LogInformation("No server well-known...");
        return null;
    }

    public async Task<string?> ResolveMediaUri(string homeserver, string mxc) {
        if (homeserver is null) throw new ArgumentNullException(nameof(homeserver));
        if (mxc is null) throw new ArgumentNullException(nameof(mxc));
        if (!mxc.StartsWith("mxc://")) throw new InvalidDataException("mxc must start with mxc://");
        homeserver = (await ResolveHomeserverFromWellKnown(homeserver)).Client;
        return mxc.Replace("mxc://", $"{homeserver}/_matrix/media/v3/download/");
    }

    public class WellKnownUris {
        public string? Client { get; set; }
        public string? Server { get; set; }
    }

    public class ClientWellKnown {
        [JsonPropertyName("m.homeserver")]
        public WellKnownHomeserver Homeserver { get; set; }

        public class WellKnownHomeserver {
            [JsonPropertyName("base_url")]
            public string BaseUrl { get; set; }
        }
    }

    public class ServerWellKnown {
        [JsonPropertyName("m.server")]
        public string Homeserver { get; set; }
    }
}