diff options
author | Hubert Chathi <hubert@uhoreg.ca> | 2020-08-26 18:28:18 -0400 |
---|---|---|
committer | Hubert Chathi <hubert@uhoreg.ca> | 2020-08-26 18:28:18 -0400 |
commit | 0cd4203bfcbb44a082ad6480fb71597f0fb8f3fa (patch) | |
tree | 7e890342c1f076d9f3e4e58ab952169c3f15fc18 /lib | |
parent | replace login/logout examples with mix tasks (diff) |
add module for handling .well-known
Diffstat (limited to 'lib')
-rw-r--r-- | lib/polyjuice/client/well_known.ex | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/lib/polyjuice/client/well_known.ex b/lib/polyjuice/client/well_known.ex new file mode 100644 index 0000000..c63803f --- /dev/null +++ b/lib/polyjuice/client/well_known.ex @@ -0,0 +1,168 @@ +# Copyright 2020 Hubert Chathi <hubert@uhoreg.ca> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defmodule Polyjuice.Client.WellKnown do + @moduledoc """ + Look up and interpret a server's `.well-known` file. + + https://matrix.org/docs/spec/client_server/latest#well-known-uri + """ + + @doc """ + Get and parse a server's `.well-known` file based on its server name. + + If the homeserver information is correct, returns a tuple of the form `{:ok, + homeserver_url, identity_server_url, well_known}`, where + `identity_server_url` is either a string giving the identity server's URL, + `nil` indicating no identity server was specified, or `:fail_error` + indicating an error in fetching the identity server URL; and `well_known` is + the full contents of the well_known file. + + This function is essentially equivalent to calling `get_for_server` followed + by `get_homeserver` and `get_identity_server`. + + """ + @spec discover_server(server_name :: String.t()) :: + {:ok, String.t(), String.t() | nil | :fail_error, map} + | {:error, :ignore} + | {:error, :fail_prompt} + | {:error, :fail_error} + def discover_server(server_name) when is_binary(server_name) do + with {:ok, json} <- get_for_server(server_name), + {:ok, hs_url} <- get_homeserver(json) do + case get_identity_server(json) do + {:ok, is_url} -> + {:ok, hs_url, is_url, json} + + {:error, err} -> + {:ok, hs_url, err, json} + end + else + err -> + err + end + end + + @doc """ + Get a server's `.well-known` file based on its server name. + """ + @spec get_for_server(server_name :: String.t()) :: + map + | {:error, :ignore} + | {:error, :fail_prompt} + | {:error, :fail_error} + def get_for_server(server_name) when is_binary(server_name) do + url = + %URI{ + scheme: "https", + host: server_name, + authority: server_name, + path: "/.well-known/matrix/client", + port: 443 + } + |> to_string() + + case :hackney.request(:get, url, [], "", follow_redirect: true) do + {:ok, 404, _, _} -> + {:error, :ignore} + + {:ok, code, _headers, _client_ref} when code != 200 -> + {:error, :fail_prompt} + + {:ok, 200, _headers, client_ref} -> + with {:ok, body} <- :hackney.body(client_ref), + {:ok, %{}} = ret <- Jason.decode(body) do + ret + else + _ -> + {:error, :fail_prompt} + end + + _ -> + # if we can't find the server, treat it like a 404 + {:error, :ignore} + end + end + + @doc """ + Get the homeserver that's specified in a `.well-known` file. + """ + @spec get_homeserver(map) :: {:ok, String.t()} | {:error, :fail_error} | {:error, :fail_prompt} + def get_homeserver(%{} = well_known) do + with %{"m.homeserver" => %{"base_url" => base_url}} <- well_known do + with %{host: host, scheme: scheme, path: path} = parsed_url + when host != nil and (scheme == "http" or scheme == "https") <- URI.parse(base_url), + slashed_url = + (if path != nil and !String.ends_with?(path, "/") do + %{parsed_url | path: path <> "/"} + else + parsed_url + end), + {:ok, %{"versions" => v}} when is_list(v) <- + URI.merge(slashed_url, "_matrix/client/versions") |> validate_server() do + {:ok, to_string(slashed_url)} + else + _ -> {:error, :fail_error} + end + else + _ -> + {:error, :fail_prompt} + end + end + + @doc """ + Get the identity server that's specified in a `.well-known` file. + """ + @spec get_identity_server(map) :: {:ok, String.t()} | {:error, :fail_error} + def get_identity_server(%{} = well_known) do + case well_known do + %{"m.identity_server" => is_info} -> + with %{"base_url" => base_url} <- is_info, + %{host: host, scheme: scheme, path: path} = parsed_url + when host != nil and (scheme == "http" or scheme == "https") <- URI.parse(base_url), + slashed_url = + (if path != nil and !String.ends_with?(path, "/") do + %{parsed_url | path: path <> "/"} + else + parsed_url + end), + {:ok, %{}} <- URI.merge(slashed_url, "_matrix/identity/api/v1") |> validate_server() do + {:ok, to_string(slashed_url)} + else + _ -> {:error, :fail_error} + end + + _ -> + {:ok, nil} + end + end + + # check that a given URL returns something JSON-y + defp validate_server(%URI{} = url) do + case :hackney.request(:get, to_string(url), [], "", []) do + {:ok, 200, headers, client_ref} -> + with "application/json" <- Polyjuice.Client.Endpoint.get_header(headers, "content-type"), + {:ok, body} <- :hackney.body(client_ref), + {:ok, json} <- Jason.decode(body) do + {:ok, json} + else + _ -> + {:error, :fail_error} + end + + _ -> + {:error, :fail_error} + end + end +end |