diff --git a/php/utils/servers-rooms.php b/php/utils/servers-rooms.php index 57f86ae..fe287e8 100644 --- a/php/utils/servers-rooms.php +++ b/php/utils/servers-rooms.php @@ -218,6 +218,13 @@ */ private array $room_hints = []; + /** + * @var bool $merge_error + * + * Flag specifying whether the server is invalidated as a result of merging. + */ + private bool $merge_error = false; + private function __construct() {} /** @@ -266,8 +273,10 @@ /** * Absorbs extra info from another instance of the same server. * @param CommunityServer $server + * + * @return True if successful, false in case of mistmatch. */ - private function merge_from($server) { + private function merge_from($server): bool { // Merge room hint information. $this->room_hints = [ ...$this->room_hints, @@ -275,8 +284,24 @@ ]; // Merge public key information. - if (!$this->has_pubkey() && $server->has_pubkey()) { - $this->pubkey = $server->pubkey; + // In case of error, set the `merge_error` flag. + if (!$this->set_pubkey($server->pubkey)) { + if ($this->merge_error) { + return false; + } + $base_url = $this->base_url; + $other_base_url = $server->base_url; + $pubkey_old = $this->pubkey; + $pubkey_new = $server->pubkey; + log_error( + "Key collision for $base_url:" . + "Have $pubkey_old, fetched $pubkey_new" . + "from server $other_base_url" + ); + + $this->merge_error = true; + + return false; } // Prefer HTTPS URLs over HTTP. @@ -288,14 +313,27 @@ if (filter_var($this->get_hostname(), FILTER_VALIDATE_IP)) { $this->base_url = $this->get_scheme() . "://" . $server->get_hostname(); } + + return true; } /** - * Re-introduces the server to a consistent state after merging. + * Re-introduces the servers to a consistent state after merging. + * @param CommunityServer[] $servers + * @return CommunityServer[] */ - private function merge_consistency() { + private static function ensure_merge_consistency(array $servers) { + // Exclude servers with merge errors. + $servers = array_filter($servers, function(\CommunityServer $server) { + return !$server->merge_error; + }); + // Remove duplicate room hints; does not require sorting. - $this->room_hints = array_unique($this->room_hints); + foreach ($servers as $server) { + $server->room_hints = array_unique($server->room_hints); + } + + return $servers; } /** @@ -336,7 +374,7 @@ CommunityServer::merge_by($servers, "get_hostname"); - foreach ($servers as $server) $server->merge_consistency(); + $servers = CommunityServer::ensure_merge_consistency($servers); return $servers; } @@ -351,7 +389,7 @@ CommunityServer::merge_by($servers, "get_pubkey"); - foreach ($servers as $server) $server->merge_consistency(); + $servers = CommunityServer::ensure_merge_consistency($servers); return $servers; } @@ -400,6 +438,7 @@ foreach ($join_urls as $join_url) { $server = new CommunityServer(); + // Call must succeed with no default public key. $server->initialize_from_url($join_url); $servers[] = $server; @@ -465,8 +504,7 @@ // Synchronous for-loop for now. foreach ($servers as $server) { if (!($server->fetch_rooms())) continue; - // Accept failures to fetch pubkey if already known. - if (!$server->fetch_or_has_pubkey()) continue; + if (!($server->fetch_pubkey())) continue; $reachable_servers[] = $server; } @@ -515,28 +553,44 @@ return $this->pubkey; } - function set_pubkey($pubkey) { + /** + * Attempts to set the server public key. + * @param string $pubkey SOGS public key. + * @return bool True if successful, false in case of mismatch. + */ + function set_pubkey(string $pubkey): bool { if ($this->has_pubkey() && $this->pubkey != $pubkey) { - $base_url = $this->base_url; - throw new ValueError("Pubkey mismatch for $base_url"); + return false; } $this->pubkey = $pubkey; + + return true; } - function set_pubkey_from_url($join_url) { - $url_components = parse_url($join_url); - parse_str($url_components['query'], $query_components); - $this->pubkey = $query_components['public_key']; + /** + * Attempts to read the server public key from a join URL. + * @param string $join_url Join URL for any of the server's rooms. + * @return bool True if successful, false in case of mismatch. + */ + function set_pubkey_from_url(string $join_url): bool { + return $this->set_pubkey(url_get_pubkey($join_url)); } /** - * Sets base url, pubkey, and room hint from join URL. + * Learns server info from a room's join URL. + * The base URL and public key are saved, + * and the room token is added as a fallback for room polling. + * @param string $join_url Room join URL to initialize with. + * @return bool True if successful, false in case of public key mismatch. */ - function initialize_from_url($join_url) { + function initialize_from_url($join_url): bool { + if (!$this->set_pubkey_from_url($join_url)) { + return false; + } $this->base_url = url_get_base($join_url); - $this->set_pubkey_from_url($join_url); $this->room_hints[] = url_get_token($join_url); + return true; } /** @@ -641,21 +695,9 @@ } /** - * Fetch the server's SOGS public key if absent. - * @return bool True if pubkey is present or has been fetched, false otherwise. - */ - function fetch_or_has_pubkey(): bool { - $base_url = $this->base_url; - // Do not use 'or' here; I learned the hard way. - $result = $this->fetch_pubkey() || $this->has_pubkey(); - if (!$result) log_warning("Failed to fetch pubkey for $base_url."); - return $result; - } - - /** - * Attempt to fetch own public key by parsing SOGS HTML preview. + * Attempt to fetch server public key by parsing SOGS HTML preview. * - * @return bool True if successful, false otherwise. + * @return bool True iff no key conflict has arised and we have a pubkey. */ function fetch_pubkey() { if (empty($this->rooms)) { @@ -672,17 +714,29 @@ if (!$room_view) { log_debug("Failed to fetch room preview from $preview_url."); - return false; + return $has_pubkey; } $links = parse_join_links($room_view); + $link = $links[0]; - if (!isset($links[0])) { + if (!isset($link)) { log_debug("Could not locate join link in preview at $preview_url."); - return false; + return $has_pubkey; } - $this->set_pubkey_from_url($links[0]); + if (!$this->set_pubkey_from_url($link)) { + // More information needs to be logged for errors + // in case of lack of context due to lower verbosity. + $base_url = $this->base_url; + $pubkey_old = $this->pubkey; + $pubkey_new = url_get_pubkey($link); + log_error( + "Key collision for $base_url:" . + "Have $pubkey_old, fetched $pubkey_new from $preview_url" + ); + return false; + } return true; } diff --git a/php/utils/utils.php b/php/utils/utils.php index 5d0c5e9..f2004f8 100644 --- a/php/utils/utils.php +++ b/php/utils/utils.php @@ -189,6 +189,16 @@ return str_replace("/", "", $token); } + /** + * Extracts the server public key from a join URL. + * @param string $join_url Join URL for Session Community. + * @return string SOGS public key + */ + function url_get_pubkey(string $join_url) { + $url_components = parse_url($join_url); + parse_str($url_components['query'], $query_components); + return $query_components['public_key']; + } /** * Extracts join links that match $REGEX_JOIN_LINK.