|
1 | | -local function get_hash_key_from_path(path) |
| 1 | +local room_id_pattern = "(![A-Za-z0-9._=%%%-/]+:[A-Za-z0-9.%-]+)" |
| 2 | +local whoami_cache = {} |
| 3 | + |
| 4 | +local function normalize_matrix_room_separators(path) |
| 5 | + return path:gsub("%%21", "!"):gsub("%%3[Aa]", ":") |
| 6 | +end |
| 7 | + |
| 8 | +local function get_room_id_from_namespace_path(path, namespace) |
| 9 | + local _, _, room_id = string.find(path, "^/_matrix/" .. namespace .. "/.-" .. room_id_pattern) |
| 10 | + |
| 11 | + return room_id |
| 12 | +end |
| 13 | + |
| 14 | +local function get_room_id_from_path(path) |
| 15 | + local normalized_path = normalize_matrix_room_separators(path) |
| 16 | + |
| 17 | + local key = get_room_id_from_namespace_path(normalized_path, "client") |
| 18 | + if key ~= nil then |
| 19 | + return key |
| 20 | + end |
| 21 | + |
| 22 | + key = get_room_id_from_namespace_path(normalized_path, "federation") |
| 23 | + if key ~= nil then |
| 24 | + return key |
| 25 | + end |
| 26 | + |
2 | 27 | local _, _, key = string.find(path, "/_matrix/client/v3/rooms/([^/]+)/messages") |
3 | 28 |
|
4 | 29 | return key |
5 | 30 | end |
6 | 31 |
|
7 | | -local function parse_username_from_token(token) |
8 | | - local _, _, username = string.find(token, "[^_]+_([^_]+)_.*$") |
9 | | - if username ~= nil then |
10 | | - return username |
11 | | - end |
| 32 | +local function get_room_id_from_request(headers) |
| 33 | + local path = headers:get(":path") |
12 | 34 |
|
13 | | - return token |
| 35 | + return get_room_id_from_path(path) |
14 | 36 | end |
15 | 37 |
|
16 | | -local function get_auth_token(auth_header, path) |
| 38 | +local function get_access_token(auth_header, path) |
17 | 39 | if auth_header ~= nil and string.len(auth_header) > 0 then |
18 | | - return parse_username_from_token(auth_header) |
| 40 | + local _, _, bearer_token = string.find(auth_header, "^[Bb]earer%s+(.+)$") |
| 41 | + if bearer_token ~= nil then |
| 42 | + return bearer_token |
| 43 | + end |
| 44 | + |
| 45 | + return auth_header |
19 | 46 | end |
20 | 47 |
|
21 | 48 | local _, _, token_param = string.find(path, "access_token=([^&]+)") |
22 | 49 | if token_param ~= nil then |
23 | | - return parse_username_from_token(token_param) |
| 50 | + return token_param |
24 | 51 | end |
25 | 52 |
|
26 | 53 | return auth_header |
27 | 54 | end |
28 | 55 |
|
29 | | -local function get_hash_key_from_request(headers) |
| 56 | +local function get_access_token_from_request(headers) |
30 | 57 | local path = headers:get(":path") |
31 | 58 |
|
32 | | - local result = get_hash_key_from_path(path) |
33 | | - if result ~= nil then |
34 | | - return result |
| 59 | + return get_access_token(headers:get("authorization"), path) |
| 60 | +end |
| 61 | + |
| 62 | +local function log_hash_fallback(request_handle, headers, fallback_type, request_id) |
| 63 | + request_handle:logWarn( |
| 64 | + "synapse_envoy_" .. fallback_type .. "_hash_fallback: method=" |
| 65 | + .. tostring(headers:get(":method")) |
| 66 | + .. " path=" |
| 67 | + .. tostring(headers:get(":path")) |
| 68 | + .. " authority=" |
| 69 | + .. tostring(headers:get(":authority")) |
| 70 | + .. " request_id=" |
| 71 | + .. tostring(request_id) |
| 72 | + ) |
| 73 | +end |
| 74 | + |
| 75 | +local function set_request_id_hash_key_with_fallback_log(request_handle, headers, fallback_type) |
| 76 | + local request_id = headers:get("x-request-id") |
| 77 | + |
| 78 | + log_hash_fallback(request_handle, headers, fallback_type, request_id) |
| 79 | + headers:add("X-Hash-Key", request_id) |
| 80 | +end |
| 81 | + |
| 82 | +local function get_option(options, key, default) |
| 83 | + if options ~= nil and options[key] ~= nil then |
| 84 | + return options[key] |
| 85 | + end |
| 86 | + |
| 87 | + return default |
| 88 | +end |
| 89 | + |
| 90 | +local function log(request_handle, options, level, message) |
| 91 | + if not get_option(options, "logging_enabled", false) then |
| 92 | + return |
| 93 | + end |
| 94 | + |
| 95 | + local prefixed_message = "whoami_sync_worker_router: " .. message |
| 96 | + if level == "error" then |
| 97 | + request_handle:logErr(prefixed_message) |
| 98 | + return |
| 99 | + end |
| 100 | + |
| 101 | + request_handle:logWarn(prefixed_message) |
| 102 | +end |
| 103 | + |
| 104 | +local function truncate_token(token, options) |
| 105 | + local token_length = get_option(options, "logging_token_length", 8) |
| 106 | + if token == nil or string.len(token) <= token_length then |
| 107 | + return token |
| 108 | + end |
| 109 | + |
| 110 | + return string.sub(token, 1, token_length) .. "..." |
| 111 | +end |
| 112 | + |
| 113 | +local function extract_localpart(user_id) |
| 114 | + if user_id == nil or string.sub(user_id, 1, 1) ~= "@" then |
| 115 | + return nil |
35 | 116 | end |
36 | 117 |
|
37 | | - return get_auth_token(headers:get("authorization"), path) |
| 118 | + local colon_index = string.find(user_id, ":", 2, true) |
| 119 | + if colon_index == nil then |
| 120 | + return nil |
| 121 | + end |
| 122 | + |
| 123 | + return string.sub(user_id, 2, colon_index - 1) |
| 124 | +end |
| 125 | + |
| 126 | +local function extract_user_id_from_whoami_body(body) |
| 127 | + local _, _, user_id = string.find(body, '"user_id"%s*:%s*"([^"]+)"') |
| 128 | + |
| 129 | + return user_id |
| 130 | +end |
| 131 | + |
| 132 | +local function get_cached_username(token, options) |
| 133 | + local entry = whoami_cache[token] |
| 134 | + if entry == nil then |
| 135 | + return nil |
| 136 | + end |
| 137 | + |
| 138 | + if entry.expires_at > os.time() then |
| 139 | + return entry.username |
| 140 | + end |
| 141 | + |
| 142 | + whoami_cache[token] = nil |
| 143 | + return nil |
| 144 | +end |
| 145 | + |
| 146 | +local function cache_username(token, username, options) |
| 147 | + local ttl_seconds = get_option(options, "cache_ttl_seconds", 300) |
| 148 | + whoami_cache[token] = { |
| 149 | + username = username, |
| 150 | + expires_at = os.time() + ttl_seconds |
| 151 | + } |
| 152 | +end |
| 153 | + |
| 154 | +local function lookup_whoami(request_handle, token, options) |
| 155 | + local headers = request_handle:headers() |
| 156 | + local authority = headers:get(":authority") |
| 157 | + if authority == nil then |
| 158 | + authority = "synapse-client-reader-headless" |
| 159 | + end |
| 160 | + |
| 161 | + log(request_handle, options, "warn", "performing whoami lookup for token " .. truncate_token(token, options)) |
| 162 | + |
| 163 | + local ok, response_headers, response_body = pcall(function() |
| 164 | + return request_handle:httpCall( |
| 165 | + get_option(options, "whoami_cluster", "httpd"), |
| 166 | + { |
| 167 | + [":method"] = "GET", |
| 168 | + [":path"] = get_option(options, "whoami_path", "/_matrix/client/v3/account/whoami"), |
| 169 | + [":authority"] = authority, |
| 170 | + ["authorization"] = "Bearer " .. token |
| 171 | + }, |
| 172 | + "", |
| 173 | + get_option(options, "timeout_ms", 5000) |
| 174 | + ) |
| 175 | + end) |
| 176 | + |
| 177 | + if not ok then |
| 178 | + log(request_handle, options, "error", "whoami lookup failed: " .. tostring(response_headers)) |
| 179 | + return nil |
| 180 | + end |
| 181 | + |
| 182 | + local status = response_headers[":status"] |
| 183 | + if status ~= "200" then |
| 184 | + if status == "401" then |
| 185 | + log(request_handle, options, "warn", "whoami lookup returned 401 for token " .. truncate_token(token, options)) |
| 186 | + else |
| 187 | + log(request_handle, options, "error", "whoami lookup returned status " .. tostring(status)) |
| 188 | + end |
| 189 | + return nil |
| 190 | + end |
| 191 | + |
| 192 | + local user_id = extract_user_id_from_whoami_body(response_body) |
| 193 | + local username = extract_localpart(user_id) |
| 194 | + if username ~= nil then |
| 195 | + log(request_handle, options, "warn", "whoami lookup success: " .. user_id .. " -> " .. username) |
| 196 | + end |
| 197 | + |
| 198 | + return username |
| 199 | +end |
| 200 | + |
| 201 | +local function get_user_identifier_from_request(request_handle, options) |
| 202 | + local headers = request_handle:headers() |
| 203 | + local token = get_access_token_from_request(headers) |
| 204 | + if token == nil or string.len(token) == 0 then |
| 205 | + log(request_handle, options, "warn", "no token found in request") |
| 206 | + return nil |
| 207 | + end |
| 208 | + |
| 209 | + local cached_username = get_cached_username(token, options) |
| 210 | + if cached_username ~= nil then |
| 211 | + log(request_handle, options, "warn", "cache hit for token " .. truncate_token(token, options) .. " -> " .. cached_username) |
| 212 | + return cached_username |
| 213 | + end |
| 214 | + |
| 215 | + local username = lookup_whoami(request_handle, token, options) |
| 216 | + if username ~= nil then |
| 217 | + cache_username(token, username, options) |
| 218 | + return username |
| 219 | + end |
| 220 | + |
| 221 | + log(request_handle, options, "warn", "whoami lookup failed, falling back to token-based routing") |
| 222 | + return token |
38 | 223 | end |
39 | 224 |
|
40 | 225 | return { |
41 | | - get_hash_key_from_request = get_hash_key_from_request |
| 226 | + get_access_token_from_request = get_access_token_from_request, |
| 227 | + get_room_id_from_request = get_room_id_from_request, |
| 228 | + set_request_id_hash_key_with_fallback_log = set_request_id_hash_key_with_fallback_log, |
| 229 | + get_user_identifier_from_request = get_user_identifier_from_request |
42 | 230 | } |
0 commit comments