From b86e9271f1aec757b29a064fa25cd612b6c1d2b4 Mon Sep 17 00:00:00 2001 From: Hadrien <26697460+ketsapiwiq@users.noreply.github.com> Date: Wed, 11 Nov 2020 16:42:35 +0100 Subject: add post register endpoint + tests --- lib/polyjuice/client/endpoint/post_register.ex | 101 +++++++++++++++++++++ .../client/endpoint/post_register_test.exs | 92 +++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 lib/polyjuice/client/endpoint/post_register.ex create mode 100644 test/polyjuice/client/endpoint/post_register_test.exs diff --git a/lib/polyjuice/client/endpoint/post_register.ex b/lib/polyjuice/client/endpoint/post_register.ex new file mode 100644 index 0000000..df10a8c --- /dev/null +++ b/lib/polyjuice/client/endpoint/post_register.ex @@ -0,0 +1,101 @@ +# Copyright 2019 Hubert Chathi +# +# 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.Endpoint.PostRegister do + @moduledoc """ + Register a user + + https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-register + """ + + @type t :: %__MODULE__{ + kind: :guest | :user, + auth: %{type: String.t(), session: String.t() | nil}, + username: String.t() | nil, + password: String.t() | nil, + device_id: String.t() | nil, + initial_device_display_name: String.t() | nil, + inhibit_login: boolean() + } + + @enforce_keys [:auth] + defstruct [ + :kind, + :auth, + :username, + :password, + :device_id, + :initial_device_display_name, + :inhibit_login + ] + + defimpl Polyjuice.Client.Endpoint.Proto do + def http_spec(%Polyjuice.Client.Endpoint.PostRegister{ + kind: kind, + auth: auth, + username: username, + password: password, + device_id: device_id, + initial_device_display_name: initial_device_display_name, + inhibit_login: inhibit_login + }) do + body = + [ + if(auth != nil, do: [{"auth", auth}], else: []), + if(username != nil, do: [{"username", username}], else: []), + if(password != nil, do: [{"password", password}], else: []), + if(device_id != nil, do: [{"device_id", device_id}], else: []), + if(inhibit_login != nil, + do: [{"inhibit_login", Atom.to_string(inhibit_login)}], + else: [] + ), + if(initial_device_display_name != nil, + do: [{"initial_device_display_name", initial_device_display_name}], + else: [] + ) + ] + |> Enum.concat() + |> Map.new() + |> Jason.encode_to_iodata!() + + query = [ + kind: + if kind == nil do + "user" + else + Atom.to_string(kind) + end + ] + + Polyjuice.Client.Endpoint.HttpSpec.post( + :r0, + "register", + body: body, + query: query, + auth_required: false + ) + end + + def transform_http_result(req, status_code, resp_headers, body) do + Polyjuice.Client.Endpoint.parse_response(req, status_code, resp_headers, body) + end + end + + # defimpl Polyjuice.Client.Endpoint.BodyParser do + # def parse(_req, parsed) do + # {:ok, Map.get(parsed, "user_id"), Map.get(parsed, "access_token"), + # Map.get(parsed, "device_id")} + # end + # end +end diff --git a/test/polyjuice/client/endpoint/post_register_test.exs b/test/polyjuice/client/endpoint/post_register_test.exs new file mode 100644 index 0000000..a7eda85 --- /dev/null +++ b/test/polyjuice/client/endpoint/post_register_test.exs @@ -0,0 +1,92 @@ +# Copyright 2019 Hubert Chathi +# +# 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.Endpoint.PostRegisterTest do + use ExUnit.Case + + test "POST register" do + endpoint = %Polyjuice.Client.Endpoint.PostRegister{ + kind: :user, + auth: %{ + "type" => "m.login.dummy" + }, + username: "alice", + password: "12345", + device_id: "ABCDEF", + initial_device_display_name: "test_device", + inhibit_login: false + } + + http_spec = Polyjuice.Client.Endpoint.Proto.http_spec(endpoint) + + assert %{http_spec | body: nil} == %Polyjuice.Client.Endpoint.HttpSpec{ + auth_required: false, + body: nil, + headers: [ + {"Accept", "application/json"}, + {"Accept-Encoding", "gzip, deflate"}, + {"Content-Type", "application/json"} + ], + method: :post, + query: [kind: "user"], + path: "_matrix/client/r0/register" + } + + assert Jason.decode!(http_spec.body) == %{ + "password" => "12345", + "auth" => %{"type" => "m.login.dummy"}, + "device_id" => "ABCDEF", + "inhibit_login" => "false", + "initial_device_display_name" => "test_device", + "username" => "alice" + } + + assert Polyjuice.Client.Endpoint.Proto.transform_http_result( + endpoint, + 200, + [{"Content-Type", "application/json"}], + ~s( + {"user_id":"@alice:example.com", + "access_token":"1234567890", + "device_id":"ABCDEF", + "well_known": + {"m.homeserver": + {"base_url":"https://example.com"}, + "m.identity_server": + {"base_url":"https://example.com"} + } + } + ) + ) == { + :ok, + %{ + "access_token" => "1234567890", + "device_id" => "ABCDEF", + "user_id" => "@alice:example.com", + "well_known" => %{ + "m.homeserver" => %{"base_url" => "https://example.com"}, + "m.identity_server" => %{"base_url" => "https://example.com"} + } + } + } + + assert Polyjuice.Client.Endpoint.Proto.transform_http_result( + endpoint, + 500, + [], + "Aaah!" + ) == + {:error, 500, %{"body" => "Aaah!", "errcode" => "CA_UHOREG_POLYJUICE_BAD_RESPONSE"}} + end +end -- cgit v1.2.3 From f2ee1f306ad4b5a7bcc33d8e024b6f155c8bdab6 Mon Sep 17 00:00:00 2001 From: Hadrien <26697460+ketsapiwiq@users.noreply.github.com> Date: Wed, 11 Nov 2020 16:43:57 +0100 Subject: register interface for client and lowlevel --- lib/polyjuice/client.ex | 100 ++++++++++++++++++++++++++++++++++++++ lib/polyjuice/client/low_level.ex | 40 +++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/lib/polyjuice/client.ex b/lib/polyjuice/client.ex index 361914d..54e2670 100644 --- a/lib/polyjuice/client.ex +++ b/lib/polyjuice/client.ex @@ -723,6 +723,106 @@ defmodule Polyjuice.Client do end end + @type register_opts() :: [ + kind: :guest | :user, + username: String.t() | nil, + password: String.t() | nil, + device_id: String.t() | nil, + initial_device_display_name: String.t() | nil, + inhibit_login: boolean() + ] + + @doc """ + Register a user. + + `opts` is a keyword list of options: + + - `username:` (string) the basis for the localpart of the desired Matrix ID + - `password:` (string) the desired password for the account + - `device_id:` (string) the device ID to use + - `initial_device_display_name:` (string) the display name to use for the device + - `inhibit_login:` (boolean) don't login after successful register + - `kind:` (atom) kind of account to register. Defaults to user. One of: ["guest", "user"] + """ + @spec register( + client :: Polyjuice.Client.t(), + opts :: register_opts() + ) :: {:ok, map()} | any + def register(%Polyjuice.Client{} = client, opts \\ []) do + case Polyjuice.Client.API.call( + client, + %Polyjuice.Client.Endpoint.PostRegister{ + auth: %{type: "m.login.dummy"}, + username: Keyword.get(opts, :username), + password: Keyword.get(opts, :password), + kind: Keyword.get(opts, :kind), + device_id: Keyword.get(opts, :device_id), + initial_device_display_name: Keyword.get(opts, :initial_device_display_name), + inhibit_login: Keyword.get(opts, :inhibit_login, false) + } + ) do + ret = + {:ok, + %{"access_token" => access_token, "user_id" => user_id, "device_id" => device_id} = + http_ret} -> + GenServer.cast( + client.pid, + {:set, %{access_token: access_token, user_id: user_id, device_id: device_id}} + ) + + # do not send logged_in event if the inhibit_login option is set to true + unless Keyword.get(opts, :inhibit_login) == true do + if client.opts.storage do + Polyjuice.Client.Storage.kv_put( + client.opts.storage, + "ca.uhoreg.polyjuice", + "access_token", + access_token + ) + + Polyjuice.Client.Storage.kv_put( + client.opts.storage, + "ca.uhoreg.polyjuice", + "user_id", + user_id + ) + + Polyjuice.Client.Storage.kv_put( + client.opts.storage, + "ca.uhoreg.polyjuice", + "device_id", + device_id + ) + end + + if client.opts.handler do + Polyjuice.Client.Handler.handle( + client.opts.handler, + :logged_in, + {user_id, device_id, Map.drop(http_ret, ["user_id", "device_id"])} + ) + end + + if client.opts.sync && client.opts.handler do + # make sure we don't already have a sync process running + kill_sync(client.id) + + supervisor_name = process_name(client.id, :supervisor) + + DynamicSupervisor.start_child( + supervisor_name, + sync_child_spec(client.base_url, client.id, client.pid, client.opts) + ) + end + end + + ret + + ret -> + ret + end + end + @doc false def kill_sync(id) do supervisor_name = process_name(id, :supervisor) diff --git a/lib/polyjuice/client/low_level.ex b/lib/polyjuice/client/low_level.ex index 6a6521d..51a9154 100644 --- a/lib/polyjuice/client/low_level.ex +++ b/lib/polyjuice/client/low_level.ex @@ -212,4 +212,44 @@ defmodule Polyjuice.Client.LowLevel do %Polyjuice.Client.Endpoint.PostLogout{} ) end + + @type register_opts() :: [ + kind: :guest | :user, + username: String.t() | nil, + password: String.t() | nil, + device_id: String.t() | nil, + initial_device_display_name: String.t() | nil, + inhibit_login: boolean() + ] + + @doc """ + Register a user. + + `opts` is a keyword list of options: + + - `username:` (string) the basis for the localpart of the desired Matrix ID + - `password:` (string) the desired password for the account + - `device_id:` (string) the device ID to use + - `initial_device_display_name:` (string) the display name to use for the device + - `inhibit_login:` (boolean) don't login after successful register + - `kind:` (atom) kind of account to register. Defaults to user. One of: ["guest", "user"] + """ + @spec register( + client :: Polyjuice.Client.LowLevel.t(), + opts :: register_opts() + ) :: {:ok, map()} | any + def register(client, opts \\ []) do + Polyjuice.Client.API.call( + client, + %Polyjuice.Client.Endpoint.PostRegister{ + kind: Keyword.get(opts, :kind, :user), + auth: %{type: "m.login.dummy"}, + username: Keyword.get(opts, :username), + password: Keyword.get(opts, :password), + device_id: Keyword.get(opts, :device_id), + initial_device_display_name: Keyword.get(opts, :initial_device_display_name), + inhibit_login: Keyword.get(opts, :inhibit_login, false) + } + ) + end end -- cgit v1.2.3 From 52744181524f196a01d4013d6d918feba31ea192 Mon Sep 17 00:00:00 2001 From: Hadrien <26697460+ketsapiwiq@users.noreply.github.com> Date: Wed, 11 Nov 2020 16:45:04 +0100 Subject: register interface tests for client and lowlevel --- test/polyjuice/client/low_level_test.exs | 72 +++++++++++++ test/polyjuice/client_test.exs | 167 +++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) diff --git a/test/polyjuice/client/low_level_test.exs b/test/polyjuice/client/low_level_test.exs index 7840e83..2c4b942 100644 --- a/test/polyjuice/client/low_level_test.exs +++ b/test/polyjuice/client/low_level_test.exs @@ -171,4 +171,76 @@ defmodule Polyjuice.Client.LowLevelTest do File.rm_rf(tmpdir) end end + + test "register" do + {:ok, tmpdir} = TestUtil.mktmpdir("register-") + + try do + tmpdir_charlist = to_charlist(tmpdir) + + :inets.start() + + {:ok, httpd_pid} = + :inets.start( + :httpd, + port: 0, + server_name: 'sync.test', + server_root: tmpdir_charlist, + document_root: tmpdir_charlist, + bind_address: {127, 0, 0, 1}, + modules: [:mod_esi], + erl_script_alias: {'', [Polyjuice.ClientTest.Httpd]} + ) + + port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port) + + client = + Polyjuice.Client.LowLevel.create( + "http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd/", + test: true + ) + + opts = [username: "alice", password: "password", device_id: "foo"] + + assert Polyjuice.Client.LowLevel.register( + client, + opts + ) == + {:ok, + %{ + "access_token" => "m.id.user_login", + "device_id" => "foo", + "user_id" => "@alice:example.org" + }} + + # Test error when registering if username is taken + + {:ok, httpd_pid} = + :inets.start( + :httpd, + port: 0, + server_name: 'sync.test', + server_root: tmpdir_charlist, + document_root: tmpdir_charlist, + bind_address: {127, 0, 0, 1}, + modules: [:mod_esi], + erl_script_alias: {'', [Polyjuice.ClientTest.Httpd.UserAlreadyTaken]} + ) + + port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port) + + client = + Polyjuice.Client.LowLevel.create( + "http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd.UserAlreadyTaken/", + test: true + ) + + assert {:error, 400, %{"errcode" => "M_USER_IN_USE", "error" => _}} = + Polyjuice.Client.LowLevel.register(client, opts) + + :inets.stop(:httpd, httpd_pid) + after + File.rm_rf(tmpdir) + end + end end diff --git a/test/polyjuice/client_test.exs b/test/polyjuice/client_test.exs index aad44a3..a87fa4a 100644 --- a/test/polyjuice/client_test.exs +++ b/test/polyjuice/client_test.exs @@ -44,6 +44,9 @@ defmodule Polyjuice.ClientTest do "client/r0/logout" -> handle_logout(session_id, env, input) + "client/r0/register" -> + handle_register(session_id, env, input) + _ -> :mod_esi.deliver( session_id, @@ -70,6 +73,43 @@ defmodule Polyjuice.ClientTest do 'Content-Type: application/json\r\n\r\n{}' ) end + + defp handle_register(session_id, _env, {_, input}) do + inhibit_login = to_string(input) |> Jason.decode!() |> Map.get("inhibit_login") + username = to_string(input) |> Jason.decode!() |> Map.get("username") + + response = + if inhibit_login == "true" do + 'Content-Type: application/json\r\n\r\n{"user_id":"@#{username}:example.org"}' + else + 'Content-Type: application/json\r\n\r\n{"user_id":"@#{username}:example.org","access_token":"m.id.user_login","device_id":"foo"}' + end + + :mod_esi.deliver( + session_id, + response + ) + end + end + + defmodule Httpd.LoggedOut do + # just tells the user that they were logged out, no matter what + def _matrix(session_id, _env, _input) do + :mod_esi.deliver( + session_id, + 'Status: 401 Unauthorized\r\nContent-Type: application/json\r\n\r\n{"errcode":"M_UNKNOWN_TOKEN","error":"Unknown token"}' + ) + end + end + + defmodule Httpd.UserAlreadyTaken do + # just tells the user that the user_id is already taken, no matter what + def _matrix(session_id, _env, _input) do + :mod_esi.deliver( + session_id, + 'Status: 400 Unauthorized\r\nContent-Type: application/json\r\n\r\n{"errcode":"M_USER_IN_USE","error":"User ID already taken."}' + ) + end end test "transaction_id is unique" do @@ -430,4 +470,131 @@ defmodule Polyjuice.ClientTest do assert Polyjuice.Client.API.get_user_and_device(client) == {"@alice:example.org", "DEVICEID"} end + + test "register" do + {:ok, tmpdir} = TestUtil.mktmpdir("register-") + + storage = Polyjuice.Client.Storage.Ets.open() + + try do + tmpdir_charlist = to_charlist(tmpdir) + + :inets.start() + + {:ok, httpd_pid} = + :inets.start( + :httpd, + port: 0, + server_name: 'sync.test', + server_root: tmpdir_charlist, + document_root: tmpdir_charlist, + bind_address: {127, 0, 0, 1}, + modules: [:mod_esi], + erl_script_alias: {'', [Polyjuice.ClientTest.Httpd]} + ) + + port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port) + + {:ok, client_pid} = + Polyjuice.Client.start_link( + "http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd/", + access_token: nil, + sync: false, + storage: storage, + handler: self(), + test: true + ) + + client = Polyjuice.Client.get_client(client_pid) + + opts = [username: "alice", password: "password", device_id: "foo"] + + assert Polyjuice.Client.register(client, opts) == + {:ok, + %{ + "access_token" => "m.id.user_login", + "device_id" => "foo", + "user_id" => "@alice:example.org" + }} + + assert GenServer.call(client.pid, :get_state) == %{ + access_token: "m.id.user_login", + user_id: "@alice:example.org", + device_id: "foo" + } + + assert_receive({:polyjuice_client, :logged_in, {"@alice:example.org", "foo", _}}) + + assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "access_token") == + "m.id.user_login" + + assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "user_id") == + "@alice:example.org" + + assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "device_id") == "foo" + + Polyjuice.Client.log_out(client) + + # Test inhibit_login: :true + opts = [ + username: "bob", + password: "password", + device_id: "foo", + inhibit_login: true + ] + + assert Polyjuice.Client.register(client, opts) == + {:ok, + %{ + "user_id" => "@bob:example.org" + }} + + assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "access_token") == + nil + + assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "user_id") == nil + assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "device_id") == nil + + assert GenServer.call(client.pid, :get_state) |> Map.get(:access_token) == nil + + # Test error when registering if username is taken + {:ok, httpd_pid} = + :inets.start( + :httpd, + port: 0, + server_name: 'sync.test', + server_root: tmpdir_charlist, + document_root: tmpdir_charlist, + bind_address: {127, 0, 0, 1}, + modules: [:mod_esi], + erl_script_alias: {'', [Polyjuice.ClientTest.Httpd.UserAlreadyTaken]} + ) + + port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port) + + {:ok, client_pid} = + Polyjuice.Client.start_link( + "http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd.UserAlreadyTaken/", + access_token: nil, + sync: false, + storage: storage, + handler: self(), + test: true + ) + + client = Polyjuice.Client.get_client(client_pid) + + opts = [username: "alice", password: "password", device_id: "foo"] + + assert {:error, 400, %{"errcode" => "M_USER_IN_USE", "error" => _}} = + Polyjuice.Client.register(client, opts) + + Polyjuice.Client.API.stop(client) + + :inets.stop(:httpd, httpd_pid) + after + Polyjuice.Client.Storage.close(storage) + File.rm_rf(tmpdir) + end + end end -- cgit v1.2.3 From 01c79449177f3a6473b3522d6a648f64540922c9 Mon Sep 17 00:00:00 2001 From: Pierre de Lacroix Date: Wed, 3 Mar 2021 18:37:48 +0100 Subject: hotfix to squash --- lib/polyjuice/client.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/polyjuice/client.ex b/lib/polyjuice/client.ex index 54e2670..f027fd0 100644 --- a/lib/polyjuice/client.ex +++ b/lib/polyjuice/client.ex @@ -811,7 +811,13 @@ defmodule Polyjuice.Client do DynamicSupervisor.start_child( supervisor_name, - sync_child_spec(client.base_url, client.id, client.pid, client.opts) + sync_child_spec( + client.base_url, + client.id, + client.pid, + client.hackney_opts, + client.opts + ) ) end end -- cgit v1.2.3