├── priv └── static │ ├── robots.txt │ └── favicon.ico ├── example └── hello_riverside │ ├── test │ ├── test_helper.exs │ └── hello_riverside_test.exs │ ├── run.sh │ ├── lib │ ├── hello_riverside.ex │ └── hello_riverside │ │ ├── handler.ex │ │ └── application.ex │ ├── config │ ├── config.exs │ └── dev.exs │ ├── .formatter.exs │ ├── README.md │ ├── mix.exs │ ├── .gitignore │ └── mix.lock ├── config ├── config.exs ├── dev.exs └── test.exs ├── test ├── test_helper.exs ├── riverside_test.exs ├── config_test.exs ├── max_connection_test.exs ├── echo_test.exs ├── auth │ ├── query_test.exs │ ├── bearer_token_test.exs │ └── basic_test.exs ├── limitter_test.exs ├── direct_relay_test.exs ├── stats_test.exs └── channel_broadcast_test.exs ├── .formatter.exs ├── lib ├── riverside │ ├── io │ │ ├── random │ │ │ ├── real.ex │ │ │ └── sandbox.ex │ │ ├── timestamp │ │ │ ├── real.ex │ │ │ └── sandbox.ex │ │ ├── timestamp.ex │ │ └── random.ex │ ├── codec │ │ ├── raw_text.ex │ │ ├── raw_binary.ex │ │ ├── json.ex │ │ └── message_pack.ex │ ├── codec.ex │ ├── router.ex │ ├── supervisor.ex │ ├── exception_guard.ex │ ├── test │ │ ├── test_server.ex │ │ └── test_client.ex │ ├── session │ │ └── transmission_limitter.ex │ ├── util │ │ └── cowboy_util.ex │ ├── auth_error.ex │ ├── peer_address.ex │ ├── auth_request.ex │ ├── endpoint_supervisor.ex │ ├── local_delivery.ex │ ├── session.ex │ ├── stats.ex │ ├── config.ex │ └── connection.ex └── riverside.ex ├── .gitignore ├── LICENSE ├── mix.exs ├── CHANGELOG.md ├── mix.lock └── README.md /priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /example/hello_riverside/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | import_config "#{Mix.env()}.exs" 3 | -------------------------------------------------------------------------------- /example/hello_riverside/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mix run --no-halt 4 | -------------------------------------------------------------------------------- /example/hello_riverside/lib/hello_riverside.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloRiverside do 2 | end 3 | -------------------------------------------------------------------------------- /priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyokato/riverside/HEAD/priv/static/favicon.ico -------------------------------------------------------------------------------- /example/hello_riverside/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | import_config "#{config_env()}.exs" 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Registry.start_link(keys: :duplicate, name: Riverside.PubSub) 3 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /example/hello_riverside/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/riverside_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RiversideTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /example/hello_riverside/test/hello_riverside_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloRiversideTest do 2 | use ExUnit.Case 3 | doctest HelloRiverside 4 | 5 | test "greets the world" do 6 | assert HelloRiverside.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :riverside, Example.Handler, 4 | authentication: {:basic, "exmaple.org"}, 5 | codec: Riverside.Codec.MessagePack, 6 | connection_timeout: 60_000 7 | 8 | config :logger, 9 | level: :debug, 10 | truncate: 4096 11 | -------------------------------------------------------------------------------- /example/hello_riverside/lib/hello_riverside/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloRiverside.Handler do 2 | use Riverside, otp_app: :hello_riverside 3 | 4 | @impl Riverside 5 | def handle_message(msg, session, state) do 6 | deliver_me(msg) 7 | {:ok, session, state} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/riverside/io/random/real.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.IO.Random.Real do 2 | @behaviour Riverside.IO.Random.Behaviour 3 | 4 | def hex(len) do 5 | SecureRandom.hex(len) 6 | end 7 | 8 | def bigint() do 9 | :rand.uniform(9_223_372_036_854_775_808) 10 | end 11 | 12 | def uuid() do 13 | UUID.uuid4() 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/riverside/codec/raw_text.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Codec.RawText do 2 | @behaviour Riverside.Codec 3 | 4 | require Logger 5 | 6 | @impl Riverside.Codec 7 | def frame_type do 8 | :text 9 | end 10 | 11 | @impl Riverside.Codec 12 | def encode(msg), do: {:ok, msg} 13 | 14 | @impl Riverside.Codec 15 | def decode(data), do: {:ok, data} 16 | end 17 | -------------------------------------------------------------------------------- /lib/riverside/codec/raw_binary.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Codec.RawBinary do 2 | @behaviour Riverside.Codec 3 | 4 | require Logger 5 | 6 | @impl Riverside.Codec 7 | def frame_type do 8 | :binary 9 | end 10 | 11 | @impl Riverside.Codec 12 | def encode(msg), do: {:ok, msg} 13 | 14 | @impl Riverside.Codec 15 | def decode(data), do: {:ok, data} 16 | end 17 | -------------------------------------------------------------------------------- /lib/riverside/codec.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Codec do 2 | @type frame_type :: :text | :binary 3 | 4 | @callback frame_type :: frame_type 5 | 6 | @callback decode(binary) :: 7 | {:ok, any} 8 | | {:error, :invalid_message} 9 | 10 | @callback encode(any) :: 11 | {:ok, binary} 12 | | {:error, :invalid_message} 13 | end 14 | -------------------------------------------------------------------------------- /example/hello_riverside/lib/hello_riverside/application.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloRiverside.Application do 2 | use Application 3 | 4 | @impl true 5 | def start(_type, _args) do 6 | [ 7 | {Riverside, [handler: HelloRiverside.Handler]} 8 | ] 9 | |> Supervisor.start_link( 10 | strategy: :one_for_one, 11 | name: HelloRiverside.Supervisor 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/riverside/io/timestamp/real.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.IO.Timestamp.Real do 2 | @behaviour Riverside.IO.Timestamp.Behaviour 3 | 4 | @impl Riverside.IO.Timestamp.Behaviour 5 | def seconds do 6 | DateTime.utc_now() |> DateTime.to_unix(:second) 7 | end 8 | 9 | @impl Riverside.IO.Timestamp.Behaviour 10 | def milli_seconds do 11 | DateTime.utc_now() |> DateTime.to_unix(:millisecond) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/riverside/io/timestamp.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.IO.Timestamp do 2 | defmodule Behaviour do 3 | @callback seconds() :: non_neg_integer 4 | @callback milli_seconds() :: non_neg_integer 5 | end 6 | 7 | @impl_mod Application.get_env(:riverside, :timestamp_module, Riverside.IO.Timestamp.Real) 8 | 9 | @behaviour Behaviour 10 | 11 | @impl Behaviour 12 | def seconds() do 13 | @impl_mod.seconds() 14 | end 15 | 16 | @impl Behaviour 17 | def milli_seconds() do 18 | @impl_mod.milli_seconds() 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /example/hello_riverside/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :hello_riverside, HelloRiverside.Handler, 4 | port: 3000, 5 | path: "/ws", 6 | max_connections: 10000, 7 | max_connection_age: :infinity, 8 | idle_timeout: 120_000, 9 | reuse_port: false, 10 | show_debug_logs: true, 11 | # tls: true, 12 | # tls_certfile: "/path/to/fullchain.pem", 13 | # tls_keyfile: "/path/to/privkey.pem", 14 | transmission_limit: [ 15 | capacity: 50, 16 | duration: 2000 17 | ] 18 | 19 | config :logger, 20 | level: :debug, 21 | truncate: 4096 22 | -------------------------------------------------------------------------------- /lib/riverside/router.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Router do 2 | @moduledoc """ 3 | Default Router module 4 | """ 5 | 6 | use Plug.Router 7 | 8 | plug(Plug.Static, 9 | at: "/", 10 | from: :riverside, 11 | only: ~w(favicon.ico robots.txt) 12 | ) 13 | 14 | plug(:match) 15 | plug(:dispatch) 16 | 17 | # just for health check 18 | get "/health" do 19 | conn 20 | |> put_resp_content_type("text/plain") 21 | |> send_resp(200, "OK") 22 | end 23 | 24 | match _ do 25 | send_resp(conn, 404, "not found") 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/riverside/io/random.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.IO.Random do 2 | @impl_mod Application.get_env(:riverside, :random_module, Riverside.IO.Random.Real) 3 | 4 | defmodule Behaviour do 5 | @callback hex(non_neg_integer) :: String.t() 6 | @callback bigint() :: non_neg_integer 7 | @callback uuid() :: String.t() 8 | end 9 | 10 | @behaviour Behaviour 11 | 12 | def hex(len) do 13 | @impl_mod.hex(len) 14 | end 15 | 16 | def bigint() do 17 | @impl_mod.bigint() 18 | end 19 | 20 | def uuid() do 21 | @impl_mod.uuid() 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :riverside, Example.Handler, codec: Riverside.Codec.MessagePack 4 | 5 | config :riverside, 6 | timestamp_module: Riverside.IO.Timestamp.Sandbox, 7 | random_module: Riverside.IO.Random.Sandbox 8 | 9 | config :riverside, TestMaxConnectionHandler, max_connections: 1 10 | 11 | # config :riverside, TestAuthBasicHandler, [] 12 | 13 | # config :riverside, TestAuthBearerTokenHandler, [] 14 | 15 | # config :riverside, TestDirectRelayHandler, [] 16 | 17 | # config :riverside, TestChannelBroadcastHandler, [] 18 | 19 | config :logger, 20 | level: :warn, 21 | truncate: 4096 22 | -------------------------------------------------------------------------------- /example/hello_riverside/README.md: -------------------------------------------------------------------------------- 1 | # HelloRiverside 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `hello_riverside` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:hello_riverside, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at [https://hexdocs.pm/hello_riverside](https://hexdocs.pm/hello_riverside). 21 | 22 | -------------------------------------------------------------------------------- /lib/riverside/codec/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Codec.JSON do 2 | @behaviour Riverside.Codec 3 | 4 | @impl Riverside.Codec 5 | def frame_type do 6 | :text 7 | end 8 | 9 | @impl Riverside.Codec 10 | def encode(msg) do 11 | case Poison.encode(msg) do 12 | {:ok, value} -> 13 | {:ok, value} 14 | 15 | {:error, _exception} -> 16 | {:error, :invalid_message} 17 | end 18 | end 19 | 20 | @impl Riverside.Codec 21 | def decode(data) do 22 | case Poison.decode(data) do 23 | {:ok, value} -> 24 | {:ok, value} 25 | 26 | {:error, _exception} -> 27 | {:error, :invalid_message} 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /example/hello_riverside/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloRiverside.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :hello_riverside, 7 | version: "0.1.0", 8 | elixir: "~> 1.12", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger], 18 | mod: {HelloRiverside.Application, []} 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | {:riverside, path: "../.."} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/riverside/codec/message_pack.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Codec.MessagePack do 2 | @behaviour Riverside.Codec 3 | 4 | @impl Riverside.Codec 5 | def frame_type do 6 | :binary 7 | end 8 | 9 | @impl Riverside.Codec 10 | def encode(msg) do 11 | case Msgpax.pack(msg) do 12 | {:ok, value} -> 13 | {:ok, value} 14 | 15 | {:error, _exception} -> 16 | {:error, :invalid_message} 17 | end 18 | end 19 | 20 | @impl Riverside.Codec 21 | def decode(data) do 22 | case Msgpax.unpack(data) do 23 | {:ok, value} -> 24 | {:ok, value} 25 | 26 | {:error, _exception} -> 27 | {:error, :invalid_message} 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/riverside/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Supervisor do 2 | use Supervisor 3 | 4 | def start_link(args) do 5 | Supervisor.start_link(__MODULE__, args, name: __MODULE__) 6 | end 7 | 8 | def init(opts) do 9 | children(opts) 10 | |> Supervisor.init(strategy: :one_for_one) 11 | end 12 | 13 | defp children(opts) do 14 | [ 15 | {Registry, keys: :duplicate, name: Riverside.PubSub}, 16 | Riverside.Stats, 17 | {Riverside.EndpointSupervisor, opts}, 18 | {TheEnd.AcceptanceStopper, 19 | [ 20 | timeout: 0, 21 | endpoint: Riverside.Supervisor, 22 | gatherer: TheEnd.ListenerGatherer.Plug 23 | ]} 24 | ] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/riverside/exception_guard.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.ExceptionGuard do 2 | require Logger 3 | 4 | def guard(log_header, error_resp, func) do 5 | try do 6 | func.() 7 | rescue 8 | err -> 9 | stacktrace = System.stacktrace() |> Exception.format_stacktrace() 10 | Logger.error("#{log_header} rescued error - #{inspect(err)}, stacktrace - #{stacktrace}") 11 | error_resp.() 12 | catch 13 | error_type, value when error_type in [:throw, :exit] -> 14 | stacktrace = System.stacktrace() |> Exception.format_stacktrace() 15 | Logger.error("#{log_header} caught error - #{inspect(value)}, stacktrace - #{stacktrace}") 16 | error_resp.() 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /example/hello_riverside/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | hello_riverside-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /lib/riverside/test/test_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Test.TestServer do 2 | @spec start( 3 | handelr :: module, 4 | port :: non_neg_integer, 5 | path :: String.t() 6 | ) :: {:ok, pid} 7 | def start(handler, port, path) do 8 | :cowboy.start_clear( 9 | :test_server, 10 | [{:port, port}], 11 | %{ 12 | env: %{ 13 | dispatch: dispatch(handler, path) 14 | } 15 | } 16 | ) 17 | end 18 | 19 | defp dispatch(handler, path) do 20 | :cowboy_router.compile([ 21 | {:_, 22 | [ 23 | {path, Riverside.Connection, [handler: handler]} 24 | ]} 25 | ]) 26 | end 27 | 28 | @spec stop(pid) :: no_return 29 | def stop(pid) do 30 | :cowboy.stop_listener(:test_server) 31 | Process.exit(pid, :shutdown) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/riverside/session/transmission_limitter.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Session.TransmissionLimitter do 2 | @type t :: %__MODULE__{count: non_neg_integer, started_at: non_neg_integer} 3 | 4 | defstruct count: 0, 5 | started_at: 0 6 | 7 | @spec new() :: t 8 | def new() do 9 | %__MODULE__{count: 0, started_at: Riverside.IO.Timestamp.milli_seconds()} 10 | end 11 | 12 | @spec countup(t, non_neg_integer, non_neg_integer) :: 13 | {:ok, t} 14 | | {:error, :too_many_messages} 15 | 16 | def countup(limitter, duration, capacity) do 17 | now = Riverside.IO.Timestamp.milli_seconds() 18 | 19 | if limitter.started_at + duration < now do 20 | {:ok, %{limitter | count: 1, started_at: now}} 21 | else 22 | count = limitter.count + 1 23 | 24 | if count < capacity do 25 | {:ok, %{limitter | count: count}} 26 | else 27 | {:error, :too_many_messages} 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/riverside/util/cowboy_util.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Util.CowboyUtil do 2 | @spec queries(:cowboy_req.req()) :: map 3 | 4 | def queries(req) do 5 | queries = :cowboy_req.parse_qs(req) 6 | queries |> Map.new(&{elem(&1, 0), elem(&1, 1)}) 7 | end 8 | 9 | @spec headers(:cowboy_req.req()) :: map 10 | 11 | def headers(req) do 12 | headers = :cowboy_req.headers(req) 13 | headers |> Map.new(&{elem(&1, 0), elem(&1, 1)}) 14 | end 15 | 16 | def response(req, code, headers) do 17 | :cowboy_req.reply(code, headers, req) 18 | end 19 | 20 | @spec peer(:cowboy_req.req()) :: {:inet.ip_address(), :inet.port_number(), String.t()} 21 | 22 | def peer(req) do 23 | {address, port} = :cowboy_req.peer(req) 24 | {address, port, x_forwarded_for(req)} 25 | end 26 | 27 | @spec x_forwarded_for(:cowboy_req.req()) :: String.t() 28 | 29 | def x_forwarded_for(req) do 30 | case :cowboy_req.parse_header("x-forwarded-for", req) do 31 | [head | _tail] -> head 32 | _ -> "" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Lyo Kato 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /test/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Riverside.PortConfigTest do 2 | use ExUnit.Case 3 | 4 | use Plug.Test 5 | 6 | test "port_config" do 7 | assert Riverside.Config.get_port(8080) == 8080 8 | 9 | # integer convertible value 10 | assert Riverside.Config.get_port("8080") == 8080 11 | 12 | assert_raise ArgumentError, fn -> 13 | assert Riverside.Config.get_port("NOT_CONVERTIBLE") 14 | end 15 | 16 | # default value is picked 17 | assert Riverside.Config.get_port({:system, "TEST_PORT", 8080}) == 8080 18 | 19 | temp = System.get_env("TEST_PORT") 20 | System.put_env("TEST_PORT", "3000") 21 | assert Riverside.Config.get_port({:system, "TEST_PORT", 80}) == 3000 22 | 23 | System.put_env("TEST_PORT", "INVALID_TYPE") 24 | 25 | assert_raise ArgumentError, fn -> 26 | Riverside.Config.get_port({:system, "TEST_PORT", 80}) 27 | end 28 | 29 | assert_raise ArgumentError, fn -> 30 | Riverside.Config.get_port({:system, "OTHER_PORT", "INVALID_TYPE"}) 31 | end 32 | 33 | if temp != nil do 34 | System.put_env("TEST_PORT", temp) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/riverside/auth_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.AuthError do 2 | @type t :: %__MODULE__{ 3 | code: pos_integer, 4 | headers: map 5 | } 6 | 7 | defstruct code: 401, headers: %{} 8 | 9 | def auth_error_with_code(code \\ 401) do 10 | %__MODULE__{ 11 | code: code, 12 | headers: %{} 13 | } 14 | end 15 | 16 | def put_auth_error_header(err, type, value) do 17 | update_in(err.headers, fn headers -> Map.put(headers, type, value) end) 18 | end 19 | 20 | def put_auth_error_basic_header(err, realm) do 21 | put_auth_error_header( 22 | err, 23 | "WWW-Authenticate", 24 | "Basic realm=\"#{realm}\"" 25 | ) 26 | end 27 | 28 | def put_auth_error_bearer_header(err, realm, error \\ nil) do 29 | if error != nil do 30 | put_auth_error_header( 31 | err, 32 | "WWW-Authenticate", 33 | "Bearer realm=\"#{realm}\" error=\"#{error}\"" 34 | ) 35 | else 36 | put_auth_error_header( 37 | err, 38 | "WWW-Authenticate", 39 | "Bearer realm=\"#{realm}\"" 40 | ) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/riverside/peer_address.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.PeerAddress do 2 | @moduledoc ~S""" 3 | Represents a peer's address 4 | 5 | This data has following three field 6 | 7 | * address 8 | * port 9 | * x_forwarded_for 10 | 11 | """ 12 | 13 | alias Riverside.Util.CowboyUtil 14 | 15 | @type t :: %__MODULE__{ 16 | address: String.t(), 17 | port: :inet.port_number(), 18 | x_forwarded_for: String.t() 19 | } 20 | 21 | defstruct address: nil, 22 | port: 0, 23 | x_forwarded_for: nil 24 | 25 | @doc ~S""" 26 | Pick a peer's address from a cowboy request. 27 | """ 28 | 29 | @spec gather(:cowboy_req.req()) :: t 30 | 31 | def gather(req) do 32 | {address, port, x_forwarded_for} = CowboyUtil.peer(req) 33 | 34 | %__MODULE__{ 35 | address: "#{:inet_parse.ntoa(address)}", 36 | port: port, 37 | x_forwarded_for: x_forwarded_for 38 | } 39 | end 40 | end 41 | 42 | defimpl String.Chars, for: Riverside.PeerAddress do 43 | alias Riverside.PeerAddress 44 | 45 | def to_string(%PeerAddress{address: address, port: port, x_forwarded_for: x_forwarded_for}) do 46 | Poison.encode!(%{ 47 | ip: address, 48 | port: port, 49 | x_forwarded_for: x_forwarded_for 50 | }) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/max_connection_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestMaxConnectionHandler do 2 | require Logger 3 | use Riverside, otp_app: :riverside 4 | 5 | @impl Riverside 6 | def authenticate(_req) do 7 | {:ok, 1, %{}} 8 | end 9 | 10 | @impl Riverside 11 | def handle_message(msg, session, state) do 12 | deliver_me(msg) 13 | {:ok, session, state} 14 | end 15 | end 16 | 17 | defmodule Riverside.MaxConnectionTest do 18 | use ExUnit.Case 19 | 20 | alias Riverside.Test.TestServer 21 | alias Riverside.Test.TestClient 22 | 23 | setup do 24 | Riverside.IO.Timestamp.Sandbox.start_link() 25 | Riverside.IO.Timestamp.Sandbox.mode(:real) 26 | 27 | Riverside.IO.Random.Sandbox.start_link() 28 | Riverside.IO.Random.Sandbox.mode(:real) 29 | 30 | Riverside.Stats.start_link() 31 | 32 | {:ok, pid} = TestServer.start(TestMaxConnectionHandler, 3000, "/") 33 | 34 | ExUnit.Callbacks.on_exit(fn -> 35 | Riverside.Test.TestServer.stop(pid) 36 | end) 37 | 38 | :ok 39 | end 40 | 41 | test "over limit connections" do 42 | result1 = TestClient.start_link(host: "localhost", port: 3000, path: "/") 43 | assert elem(result1, 0) == :ok 44 | result2 = TestClient.start_link(host: "localhost", port: 3000, path: "/") 45 | assert elem(result2, 0) == :error 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :riverside, 7 | version: "2.2.1", 8 | elixir: "~> 1.11", 9 | package: package(), 10 | build_embedded: Mix.env() == :prod, 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps() 13 | ] 14 | end 15 | 16 | def application do 17 | [ 18 | extra_applications: [ 19 | :cowboy, 20 | :logger, 21 | :msgpax, 22 | :plug, 23 | :poison, 24 | :secure_random, 25 | :elixir_uuid 26 | ] 27 | ] 28 | end 29 | 30 | defp deps do 31 | [ 32 | {:cowboy, "~> 2.9"}, 33 | {:ex_doc, "~> 0.25", only: :dev, runtime: false}, 34 | {:msgpax, "~> 2.3"}, 35 | {:plug, "~> 1.12"}, 36 | {:plug_cowboy, "~> 2.5"}, 37 | {:poison, "~> 5.0"}, 38 | {:secure_random, "~> 0.5"}, 39 | {:socket, "~> 0.3"}, 40 | {:the_end, "~> 1.1"}, 41 | {:elixir_uuid, "~> 1.2"} 42 | ] 43 | end 44 | 45 | defp package() do 46 | [ 47 | description: "A plain WebSocket server framework.", 48 | licenses: ["MIT"], 49 | links: %{ 50 | "Github" => "https://github.com/lyokato/riverside", 51 | "Docs" => "https://hexdocs.pm/riverside/Riverside.html" 52 | }, 53 | maintainers: ["Lyo Kato"] 54 | ] 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/echo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestEchoHandler do 2 | require Logger 3 | use Riverside, otp_app: :riverside 4 | 5 | @impl Riverside 6 | def authenticate(_req) do 7 | {:ok, 1, %{}} 8 | end 9 | 10 | @impl Riverside 11 | def handle_message(msg, session, state) do 12 | deliver_me(msg) 13 | {:ok, session, state} 14 | end 15 | end 16 | 17 | defmodule Riverside.EchoTest do 18 | use ExUnit.Case 19 | 20 | alias Riverside.Test.TestServer 21 | alias Riverside.Test.TestClient 22 | 23 | setup do 24 | Riverside.IO.Timestamp.Sandbox.start_link() 25 | Riverside.IO.Timestamp.Sandbox.mode(:real) 26 | 27 | Riverside.IO.Random.Sandbox.start_link() 28 | Riverside.IO.Random.Sandbox.mode(:real) 29 | 30 | Riverside.Stats.start_link() 31 | 32 | {:ok, pid} = TestServer.start(TestEchoHandler, 3000, "/") 33 | 34 | ExUnit.Callbacks.on_exit(fn -> 35 | Riverside.Test.TestServer.stop(pid) 36 | end) 37 | 38 | :ok 39 | end 40 | 41 | test "echo" do 42 | {:ok, client} = TestClient.start_link(host: "localhost", port: 3000, path: "/") 43 | 44 | TestClient.test_message(%{ 45 | sender: client, 46 | message: %{"content" => "Hello"}, 47 | receivers: [ 48 | %{ 49 | receiver: client, 50 | tests: [ 51 | fn msg -> 52 | assert Map.has_key?(msg, "content") 53 | assert msg["content"] == "Hello" 54 | end 55 | ] 56 | } 57 | ] 58 | }) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [2.2.1] - 2021/12/11 4 | 5 | ### CHANGED 6 | 7 | - fixing dialyzer error(https://github.com/lyokato/riverside/pull/62). thanks to FFEvan 8 | 9 | ## [2.2.0] - 2021/11/24 10 | 11 | ### CHANGED 12 | 13 | - allow to set cowboy options(https://github.com/lyokato/riverside/pull/61). thanks to PhilWaldmann 14 | 15 | ## [2.0.0] - 2021/10/26 16 | 17 | ### CHANGED 18 | 19 | - bumped up libraries riverside depends on. and made riverside work as well with them. 20 | - removed metrics endpoints related prometheus features. 21 | 22 | ## [1.2.6] - 2020/05/26 23 | 24 | ### CHANGED 25 | 26 | - removed some warnings 27 | 28 | ## [1.2.5] - 2020/05/26 29 | 30 | ### CHANGED 31 | 32 | - fix typo in error tuple(https://github.com/lyokato/riverside/pull/55) thanks to bakkdoor 33 | 34 | ## [1.2.4] - 2020/04/12 35 | 36 | ### CHANGED 37 | 38 | - idle timeout configuration for cowboy(https://github.com/lyokato/riverside/pull/54) thanks to Daniela Ivanova 39 | 40 | ## [1.2.3] - 2019/03/06 41 | 42 | ### CHANGED 43 | 44 | - runtime port config(https://github.com/lyokato/riverside/pull/52) 45 | - removed deprecation warnings(https://github.com/lyokato/riverside/pull/50) thanks to yurikoval 46 | - dependency update: now requires plug_cowboy(2.0) and plug(1.7) 47 | 48 | ## [1.2.2] - 2019/02/22 49 | 50 | ### CHANGED 51 | 52 | - update ExDoc dependency 0.15 -> 0.19 53 | 54 | ## [1.2.1] - 2019/02/22 55 | 56 | ### CHANGED 57 | 58 | - merged 2 PRs from yurikoval 59 | - (1) uuid -> elixir_uuid: https://github.com/lyokato/riverside/pull/48 60 | - (2) formatter support: https://github.com/lyokato/riverside/pull/49 61 | 62 | ## [1.2.0] - 2018/08/12 63 | 64 | ### CHANGED 65 | 66 | - updated version of libraries on which riverside depends. 67 | - no longer use a ebus but a 'Registry' 68 | -------------------------------------------------------------------------------- /lib/riverside/auth_request.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.AuthRequest do 2 | alias Riverside.Util.CowboyUtil 3 | 4 | @type basic_credential :: {String.t(), String.t()} 5 | 6 | @type t :: %__MODULE__{ 7 | queries: map, 8 | headers: map, 9 | peer: Riverside.PeerAddress.t(), 10 | basic: basic_credential, 11 | bearer_token: String.t() 12 | } 13 | 14 | defstruct queries: %{}, 15 | headers: %{}, 16 | peer: nil, 17 | basic: {"", ""}, 18 | bearer_token: "" 19 | 20 | @spec new( 21 | cowboy_req :: :cowboy_req.req(), 22 | peer :: Riverside.PeerAddress.t() 23 | ) :: t 24 | 25 | def new(cowboy_req, peer) do 26 | queries = CowboyUtil.queries(cowboy_req) 27 | headers = CowboyUtil.headers(cowboy_req) 28 | 29 | %__MODULE__{ 30 | queries: queries, 31 | headers: headers, 32 | peer: peer, 33 | basic: basic(headers), 34 | bearer_token: bearer_token(headers) 35 | } 36 | end 37 | 38 | defp basic(headers) do 39 | case authorization_header(headers) do 40 | {"basic", value} -> 41 | case Base.decode64(value) do 42 | {:ok, b64decoded} -> 43 | case String.split(b64decoded, ":") do 44 | [username, password] -> {username, password} 45 | _other -> {"", ""} 46 | end 47 | 48 | _other -> 49 | {"", ""} 50 | end 51 | 52 | _other -> 53 | {"", ""} 54 | end 55 | end 56 | 57 | defp bearer_token(headers) do 58 | case authorization_header(headers) do 59 | {"bearer", value} -> value 60 | _other -> "" 61 | end 62 | end 63 | 64 | defp authorization_header(headers) do 65 | if headers["authorization"] == nil do 66 | {"", ""} 67 | else 68 | case String.split(headers["authorization"], " ") do 69 | [type, value] -> {String.downcase(type), value} 70 | _ -> {"", ""} 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/riverside/endpoint_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.EndpointSupervisor do 2 | use Supervisor 3 | alias Riverside.Config 4 | 5 | def start_link(opts) do 6 | Supervisor.start_link(__MODULE__, opts, name: __MODULE__) 7 | end 8 | 9 | def init(opts) do 10 | children(opts) 11 | |> Supervisor.init(strategy: :one_for_one) 12 | end 13 | 14 | def children(opts) do 15 | handler = Keyword.fetch!(opts, :handler) 16 | 17 | router = Keyword.get(opts, :router, Riverside.Router) 18 | 19 | scheme = 20 | if Config.get_tls(handler.__config__.tls) do 21 | :https 22 | else 23 | :http 24 | end 25 | 26 | [ 27 | { 28 | Plug.Cowboy, 29 | [ 30 | scheme: scheme, 31 | plug: router, 32 | options: cowboy_opts(router, handler) 33 | ] 34 | } 35 | ] 36 | end 37 | 38 | defp cowboy_opts(router, module) do 39 | Config.ensure_module_loaded(module) 40 | 41 | port = Config.get_port(module.__config__.port) 42 | extra_opts = Config.get_cowboy_opts(module.__config__.cowboy_opts) 43 | path = module.__config__.path 44 | idle_timeout = module.__config__.idle_timeout 45 | 46 | cowboy_opts = 47 | [ 48 | port: port, 49 | dispatch: dispatch_opts(module, router, path), 50 | protocol_options: [{:idle_timeout, idle_timeout}] 51 | ] ++ extra_opts 52 | 53 | cowboy_opts = 54 | if module.__config__.reuse_port do 55 | cowboy_opts ++ [{:raw, 1, 15, <<1, 0, 0, 0>>}] 56 | else 57 | cowboy_opts 58 | end 59 | 60 | if Config.get_tls(module.__config__.tls) do 61 | cowboy_opts ++ 62 | [ 63 | otp_app: module.__config__.otp_app, 64 | certfile: Config.get_tls_certfile(module.__config__.tls_certfile), 65 | keyfile: Config.get_tls_keyfile(module.__config__.tls_keyfile) 66 | ] 67 | else 68 | cowboy_opts 69 | end 70 | end 71 | 72 | defp dispatch_opts(module, router, path) do 73 | [ 74 | {:_, 75 | [ 76 | {path, Riverside.Connection, [handler: module]}, 77 | {:_, Plug.Cowboy.Handler, {router, []}} 78 | ]} 79 | ] 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/riverside/local_delivery.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.LocalDelivery do 2 | alias Riverside.Codec 3 | alias Riverside.Session 4 | 5 | defmodule Topic do 6 | def channel(channel_id) do 7 | "__channel__:#{channel_id}" 8 | end 9 | 10 | def user(user_id) do 11 | "__user__:#{user_id}" 12 | end 13 | 14 | def session(user_id, session_id) do 15 | "__session__:#{user_id}/#{session_id}" 16 | end 17 | end 18 | 19 | @type destination :: 20 | {:user, Session.user_id()} 21 | | {:session, Session.user_id(), String.t()} 22 | | {:channel, term} 23 | 24 | @spec deliver(destination, {Codec.frame_type(), any}) :: no_return 25 | def deliver({:user, user_id}, {frame_type, message}) do 26 | Topic.user(user_id) 27 | |> deliver_message(frame_type, message) 28 | end 29 | 30 | def deliver({:session, user_id, session_id}, {frame_type, message}) do 31 | Topic.session(user_id, session_id) 32 | |> deliver_message(frame_type, message) 33 | end 34 | 35 | def deliver({:channel, channel_id}, {frame_type, message}) do 36 | Topic.channel(channel_id) 37 | |> deliver_message(frame_type, message) 38 | end 39 | 40 | def deliver_message(topic, frame_type, message) do 41 | dispatch(topic, {:deliver, frame_type, message}) 42 | end 43 | 44 | def join_channel(channel_id) do 45 | Topic.channel(channel_id) |> sub() 46 | end 47 | 48 | def leave_channel(channel_id) do 49 | Topic.channel(channel_id) |> unsub() 50 | end 51 | 52 | def close(user_id, session_id) do 53 | Topic.session(user_id, session_id) 54 | |> dispatch(:stop) 55 | end 56 | 57 | defp dispatch(topic, message) do 58 | pub(topic, message) 59 | end 60 | 61 | def register(user_id, session_id) do 62 | Topic.user(user_id) |> sub() 63 | Topic.session(user_id, session_id) |> sub() 64 | end 65 | 66 | defp sub(topic) do 67 | Registry.register(Riverside.PubSub, topic, []) 68 | end 69 | 70 | defp unsub(topic) do 71 | Registry.unregister(Riverside.PubSub, topic) 72 | end 73 | 74 | defp pub(topic, message) do 75 | Registry.dispatch(Riverside.PubSub, topic, fn entries -> 76 | entries |> Enum.each(fn {pid, _item} -> send(pid, message) end) 77 | end) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/auth/query_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestAuthQueryHandler do 2 | require Logger 3 | use Riverside, otp_app: :riverside 4 | 5 | @impl Riverside 6 | def authenticate(req) do 7 | if req.queries["user"] != nil do 8 | username = req.queries["user"] 9 | 10 | if username == "valid_example" do 11 | {:ok, username, %{}} 12 | else 13 | error = auth_error_with_code(400) 14 | {:error, error} 15 | end 16 | else 17 | error = auth_error_with_code(400) 18 | {:error, error} 19 | end 20 | end 21 | 22 | @impl Riverside 23 | def handle_message(msg, session, state) do 24 | deliver_me(msg) 25 | {:ok, session, state} 26 | end 27 | end 28 | 29 | defmodule Riverside.Auth.QueryTest do 30 | use ExUnit.Case 31 | 32 | alias Riverside.Test.TestServer 33 | alias Riverside.Test.TestClient 34 | 35 | setup do 36 | Riverside.IO.Timestamp.Sandbox.start_link() 37 | Riverside.IO.Timestamp.Sandbox.mode(:real) 38 | 39 | Riverside.IO.Random.Sandbox.start_link() 40 | Riverside.IO.Random.Sandbox.mode(:real) 41 | 42 | Riverside.Stats.start_link() 43 | 44 | {:ok, pid} = TestServer.start(TestAuthQueryHandler, 3000, "/") 45 | 46 | ExUnit.Callbacks.on_exit(fn -> 47 | Riverside.Test.TestServer.stop(pid) 48 | end) 49 | 50 | :ok 51 | end 52 | 53 | test "authenticate with bad query" do 54 | {:error, {code, _desc}} = TestClient.connect("localhost", 3000, "/?user=invalid", []) 55 | assert code == 400 56 | end 57 | 58 | test "authenticate without query" do 59 | {:error, {code, _desc}} = TestClient.connect("localhost", 3000, "/", []) 60 | assert code == 400 61 | end 62 | 63 | test "authenticate with correct query" do 64 | {:ok, client} = 65 | TestClient.start_link(host: "localhost", port: 3000, path: "/?user=valid_example") 66 | 67 | TestClient.test_message(%{ 68 | sender: client, 69 | message: %{"content" => "Hello"}, 70 | receivers: [ 71 | %{ 72 | receiver: client, 73 | tests: [ 74 | fn msg -> 75 | assert Map.has_key?(msg, "content") 76 | assert msg["content"] == "Hello" 77 | end 78 | ] 79 | } 80 | ] 81 | }) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/auth/bearer_token_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestAuthBearerTokenHandler do 2 | require Logger 3 | use Riverside, otp_app: :riverside 4 | 5 | @impl Riverside 6 | def authenticate(req) do 7 | if req.bearer_token == "valid_example" do 8 | {:ok, 1, %{}} 9 | else 10 | error = 11 | auth_error_with_code(401) 12 | |> put_auth_error_bearer_header("example.org") 13 | 14 | {:error, error} 15 | end 16 | end 17 | 18 | @impl Riverside 19 | def handle_message(msg, session, state) do 20 | deliver_me(msg) 21 | {:ok, session, state} 22 | end 23 | end 24 | 25 | defmodule Riverside.Auth.BearerTokenTest do 26 | use ExUnit.Case 27 | 28 | alias Riverside.Test.TestServer 29 | alias Riverside.Test.TestClient 30 | 31 | setup do 32 | Riverside.IO.Timestamp.Sandbox.start_link() 33 | Riverside.IO.Timestamp.Sandbox.mode(:real) 34 | 35 | Riverside.IO.Random.Sandbox.start_link() 36 | Riverside.IO.Random.Sandbox.mode(:real) 37 | 38 | Riverside.Stats.start_link() 39 | 40 | {:ok, pid} = TestServer.start(TestAuthBearerTokenHandler, 3000, "/") 41 | 42 | ExUnit.Callbacks.on_exit(fn -> 43 | Riverside.Test.TestServer.stop(pid) 44 | end) 45 | 46 | :ok 47 | end 48 | 49 | test "authenticate with bad type authorization header" do 50 | {:error, {code, _desc}} = 51 | TestClient.connect("localhost", 3000, "/", [{:authorization, "Basic xxxx"}]) 52 | 53 | assert code == 401 54 | end 55 | 56 | test "authenticate with bad token" do 57 | {:error, {code, _desc}} = 58 | TestClient.connect("localhost", 3000, "/", [{:authorization, "Bearer bad_token"}]) 59 | 60 | assert code == 401 61 | end 62 | 63 | test "authenticate with correct token" do 64 | {:ok, client} = 65 | TestClient.start_link( 66 | host: "localhost", 67 | port: 3000, 68 | path: "/", 69 | headers: [{:authorization, "Bearer valid_example"}] 70 | ) 71 | 72 | TestClient.test_message(%{ 73 | sender: client, 74 | message: %{"content" => "Hello"}, 75 | receivers: [ 76 | %{ 77 | receiver: client, 78 | tests: [ 79 | fn msg -> 80 | assert Map.has_key?(msg, "content") 81 | assert msg["content"] == "Hello" 82 | end 83 | ] 84 | } 85 | ] 86 | }) 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/limitter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Riverside.LimitterTest do 2 | use ExUnit.Case 3 | 4 | alias Riverside.Session.TransmissionLimitter 5 | 6 | setup do 7 | Riverside.IO.Timestamp.Sandbox.start_link() 8 | 9 | Riverside.IO.Random.Sandbox.start_link() 10 | Riverside.IO.Random.Sandbox.mode(:real) 11 | 12 | :ok 13 | end 14 | 15 | test "enough capacity for step-count" do 16 | setup_timestamps(1..100) 17 | 18 | duration = 100 19 | capacity = 10 20 | limitter1 = TransmissionLimitter.new() 21 | 22 | result = step(limitter1, duration, capacity, 9) 23 | refute result == :error 24 | end 25 | 26 | test "not enough capacity for step-count" do 27 | setup_timestamps(1..100) 28 | 29 | duration = 100 30 | capacity = 10 31 | limitter1 = TransmissionLimitter.new() 32 | 33 | result = step(limitter1, duration, capacity, 10) 34 | assert result == :error 35 | end 36 | 37 | test "over capacity after duration" do 38 | setup_timestamps(1..100) 39 | 40 | duration = 100 41 | capacity = 10 42 | limitter1 = TransmissionLimitter.new() 43 | 44 | result = step(limitter1, duration, capacity, 9) 45 | refute result == :error 46 | 47 | setup_timestamps(200..300) 48 | result = step(limitter1, duration, capacity, 9) 49 | refute result == :error 50 | 51 | setup_timestamps(300..400) 52 | result = step(limitter1, duration, capacity, 9) 53 | refute result == :error 54 | end 55 | 56 | test "over capacity on second duration" do 57 | setup_timestamps(1..100) 58 | 59 | duration = 100 60 | capacity = 10 61 | limitter1 = TransmissionLimitter.new() 62 | 63 | result = step(limitter1, duration, capacity, 9) 64 | refute result == :error 65 | 66 | setup_timestamps(200..300) 67 | result = step(limitter1, duration, capacity, 10) 68 | assert result == :error 69 | end 70 | 71 | defp setup_timestamps(range) do 72 | range 73 | |> Enum.map(&(&1 + 1500_000_000)) 74 | |> Riverside.IO.Timestamp.Sandbox.set_milli_seconds() 75 | end 76 | 77 | defp step(limitter, _duration, _capacity, 0) do 78 | {:ok, limitter} 79 | end 80 | 81 | defp step(limitter, duration, capacity, rest) do 82 | case TransmissionLimitter.countup(limitter, duration, capacity) do 83 | {:ok, limitter2} -> 84 | step(limitter2, duration, capacity, rest - 1) 85 | 86 | _ -> 87 | :error 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/riverside/session.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Session do 2 | @abbreviation_header "Session" 3 | 4 | alias Riverside.Session.TransmissionLimitter 5 | 6 | @type user_id :: non_neg_integer | String.t() 7 | @type session_id :: String.t() 8 | 9 | @type t :: %__MODULE__{ 10 | user_id: user_id, 11 | id: String.t(), 12 | abbreviation: String.t(), 13 | transmission_limitter: TransmissionLimitter.t(), 14 | peer: Riverside.PeerAddress.t(), 15 | trapping_pids: MapSet.t() 16 | } 17 | 18 | defstruct user_id: 0, 19 | id: "", 20 | abbreviation: "", 21 | transmission_limitter: nil, 22 | peer: nil, 23 | trapping_pids: nil 24 | 25 | @spec new(user_id, session_id, Riverside.PeerAddress.t()) :: t 26 | def new(user_id, session_id, peer) do 27 | abbreviation = create_abbreviation(user_id, session_id) 28 | 29 | %__MODULE__{ 30 | user_id: user_id, 31 | id: session_id, 32 | abbreviation: abbreviation, 33 | transmission_limitter: TransmissionLimitter.new(), 34 | trapping_pids: MapSet.new(), 35 | peer: peer 36 | } 37 | end 38 | 39 | defp create_abbreviation(user_id, session_id) do 40 | "#{@abbreviation_header}:#{user_id}:#{String.slice(session_id, 0..5)}" 41 | end 42 | 43 | @spec should_delegate_exit?(t, pid) :: boolean 44 | def should_delegate_exit?(session, pid) do 45 | MapSet.member?(session.trapping_pids, pid) 46 | end 47 | 48 | @spec trap_exit(t, pid) :: t 49 | def trap_exit(%{trapping_pids: pids} = session, pid) do 50 | %{session | trapping_pids: MapSet.put(pids, pid)} 51 | end 52 | 53 | @spec forget_to_trap_exit(t, pid) :: t 54 | def forget_to_trap_exit(%{trapping_pids: pids} = session, pid) do 55 | %{session | trapping_pids: MapSet.delete(pids, pid)} 56 | end 57 | 58 | @spec countup_messages(t, keyword) :: 59 | {:ok, t} 60 | | {:error, :too_many_messages} 61 | def countup_messages(%{transmission_limitter: limitter} = session, opts) do 62 | duration = Keyword.fetch!(opts, :duration) 63 | capacity = Keyword.fetch!(opts, :capacity) 64 | 65 | case TransmissionLimitter.countup(limitter, duration, capacity) do 66 | {:ok, limitter} -> {:ok, %{session | transmission_limitter: limitter}} 67 | {:error, :too_many_messages} = error -> error 68 | end 69 | end 70 | 71 | def peer_address(%__MODULE__{peer: peer}) do 72 | "#{peer}" 73 | end 74 | end 75 | 76 | defimpl String.Chars, for: Riverside.Session do 77 | alias Riverside.Session 78 | 79 | def to_string(%Session{abbreviation: abbreviation}) do 80 | abbreviation 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/riverside/stats.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Stats do 2 | use GenServer 3 | 4 | defstruct current_connections: 0, 5 | total_connections: 0, 6 | incoming_messages: 0, 7 | outgoing_messages: 0, 8 | started_at: 0 9 | 10 | def current_state do 11 | GenServer.call(__MODULE__, :current_state) 12 | end 13 | 14 | def number_of_current_connections do 15 | GenServer.call(__MODULE__, :number_of_current_connections) 16 | end 17 | 18 | def number_of_total_connections do 19 | GenServer.call(__MODULE__, :number_of_total_connections) 20 | end 21 | 22 | def number_of_messages do 23 | GenServer.call(__MODULE__, :number_of_messages) 24 | end 25 | 26 | def countup_incoming_messages do 27 | GenServer.cast(__MODULE__, :countup_incoming_messages) 28 | end 29 | 30 | def countup_outgoing_messages do 31 | GenServer.cast(__MODULE__, :countup_outgoing_messages) 32 | end 33 | 34 | def countup_connections do 35 | GenServer.cast(__MODULE__, :countup_connections) 36 | end 37 | 38 | def countdown_connections do 39 | GenServer.cast(__MODULE__, :countdown_connections) 40 | end 41 | 42 | def start_link(_opts), do: start_link() 43 | 44 | def start_link() do 45 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 46 | end 47 | 48 | defp new() do 49 | %__MODULE__{ 50 | current_connections: 0, 51 | total_connections: 0, 52 | incoming_messages: 0, 53 | outgoing_messages: 0, 54 | started_at: Riverside.IO.Timestamp.seconds() 55 | } 56 | end 57 | 58 | def init(_args) do 59 | {:ok, new()} 60 | end 61 | 62 | def handle_call(:number_of_current_connections, _from, state) do 63 | {:reply, state.current_connections, state} 64 | end 65 | 66 | def handle_call(:current_state, _from, state) do 67 | {:reply, state, state} 68 | end 69 | 70 | def handle_call(:number_of_messages, _from, state) do 71 | {:reply, state.messages, state} 72 | end 73 | 74 | def handle_cast( 75 | :countup_connections, 76 | %{total_connections: total, current_connections: current} = state 77 | ) do 78 | {:noreply, %{state | current_connections: current + 1, total_connections: total + 1}} 79 | end 80 | 81 | def handle_cast(:countdown_connections, state) do 82 | {:noreply, %{state | current_connections: state.current_connections - 1}} 83 | end 84 | 85 | def handle_cast(:countup_incoming_messages, state) do 86 | {:noreply, %{state | incoming_messages: state.incoming_messages + 1}} 87 | end 88 | 89 | def handle_cast(:countup_outgoing_messages, state) do 90 | {:noreply, %{state | outgoing_messages: state.outgoing_messages + 1}} 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/direct_relay_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestDirectRelayHandler do 2 | require Logger 3 | use Riverside, otp_app: :riverside 4 | 5 | @impl Riverside 6 | def authenticate(req) do 7 | case req.bearer_token do 8 | "foo" -> 9 | {:ok, 1, %{}} 10 | 11 | "bar" -> 12 | {:ok, 1, %{}} 13 | 14 | _ -> 15 | error = 16 | auth_error_with_code(401) 17 | |> put_auth_error_bearer_header("example.org", "invalid_token") 18 | 19 | {:error, error} 20 | end 21 | end 22 | 23 | @impl Riverside 24 | def handle_message(incoming, session, state) do 25 | dest_user = incoming["to"] 26 | content = incoming["content"] 27 | 28 | outgoing = %{"from" => "#{session.user_id}/#{session.id}", "content" => content} 29 | 30 | deliver_user(dest_user, outgoing) 31 | 32 | {:ok, session, state} 33 | end 34 | end 35 | 36 | defmodule Riverside.DirectRelayTest do 37 | use ExUnit.Case 38 | 39 | alias Riverside.Test.TestServer 40 | alias Riverside.Test.TestClient 41 | 42 | setup do 43 | Riverside.IO.Timestamp.Sandbox.start_link() 44 | Riverside.IO.Timestamp.Sandbox.mode(:real) 45 | 46 | Riverside.IO.Random.Sandbox.start_link() 47 | Riverside.IO.Random.Sandbox.mode(:real) 48 | 49 | Riverside.Stats.start_link() 50 | 51 | {:ok, pid} = TestServer.start(TestDirectRelayHandler, 3000, "/") 52 | 53 | ExUnit.Callbacks.on_exit(fn -> 54 | Riverside.Test.TestServer.stop(pid) 55 | end) 56 | 57 | :ok 58 | end 59 | 60 | test "direct relay message" do 61 | {:ok, foo} = 62 | TestClient.start_link( 63 | host: "localhost", 64 | port: 3000, 65 | path: "/", 66 | headers: [{:authorization, "Bearer foo"}] 67 | ) 68 | 69 | {:ok, bar} = 70 | TestClient.start_link( 71 | host: "localhost", 72 | port: 3000, 73 | path: "/", 74 | headers: [{:authorization, "Bearer bar"}] 75 | ) 76 | 77 | TestClient.test_message(%{ 78 | sender: foo, 79 | message: %{"to" => "foo", "content" => "Hello"}, 80 | receivers: [ 81 | %{ 82 | receiver: bar, 83 | tests: [ 84 | fn msg -> 85 | assert Map.has_key?(msg, "content") 86 | [user_id, _session_id] = String.split(msg["from"], "/") 87 | assert user_id == "foo" 88 | assert msg["content"] == "Hello" 89 | end 90 | ] 91 | } 92 | ] 93 | }) 94 | 95 | TestClient.test_message(%{ 96 | sender: bar, 97 | message: %{"to" => "foo", "content" => "Hey"}, 98 | receivers: [ 99 | %{ 100 | receiver: foo, 101 | tests: [ 102 | fn msg -> 103 | assert Map.has_key?(msg, "content") 104 | [user_id, _session_id] = String.split(msg["from"], "/") 105 | assert user_id == "bar" 106 | assert msg["content"] == "Hey" 107 | end 108 | ] 109 | } 110 | ] 111 | }) 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /test/auth/basic_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestAuthBasicHandler do 2 | require Logger 3 | use Riverside, otp_app: :riverside 4 | 5 | @impl Riverside 6 | def authenticate(req) do 7 | {username, password} = req.basic 8 | 9 | if username == "valid_example" and password == "foobar" do 10 | {:ok, username, %{}} 11 | else 12 | error = 13 | auth_error_with_code(401) 14 | |> put_auth_error_basic_header("example.org") 15 | 16 | {:error, error} 17 | end 18 | end 19 | 20 | @impl Riverside 21 | def handle_message(msg, session, state) do 22 | deliver_me(msg) 23 | {:ok, session, state} 24 | end 25 | end 26 | 27 | defmodule Riverside.Auth.BasicTest do 28 | use ExUnit.Case 29 | 30 | alias Riverside.Test.TestServer 31 | alias Riverside.Test.TestClient 32 | 33 | setup do 34 | Riverside.IO.Timestamp.Sandbox.start_link() 35 | Riverside.IO.Timestamp.Sandbox.mode(:real) 36 | 37 | Riverside.IO.Random.Sandbox.start_link() 38 | Riverside.IO.Random.Sandbox.mode(:real) 39 | 40 | Riverside.Stats.start_link() 41 | 42 | {:ok, pid} = TestServer.start(TestAuthBasicHandler, 3000, "/") 43 | 44 | ExUnit.Callbacks.on_exit(fn -> 45 | Riverside.Test.TestServer.stop(pid) 46 | end) 47 | 48 | :ok 49 | end 50 | 51 | defp build_token(username, password) do 52 | Base.encode64("#{username}:#{password}") 53 | end 54 | 55 | test "authenticate with bad type authorization header" do 56 | {:error, {code, _desc}} = 57 | TestClient.connect("localhost", 3000, "/", [{:authorization, "Bearer xxxx"}]) 58 | 59 | assert code == 401 60 | end 61 | 62 | test "authenticate with bad token" do 63 | {:error, {code, _desc}} = 64 | TestClient.connect("localhost", 3000, "/", [{:authorization, "Basic bad_token"}]) 65 | 66 | assert code == 401 67 | end 68 | 69 | test "authenticate with bad username" do 70 | {:error, {code, _desc}} = 71 | TestClient.connect("localhost", 3000, "/", [ 72 | {:authorization, "Basic " <> build_token("invalid", "foobar")} 73 | ]) 74 | 75 | assert code == 401 76 | end 77 | 78 | test "authenticate with bad password" do 79 | {:error, {code, _desc}} = 80 | TestClient.connect("localhost", 3000, "/", [ 81 | {:authorization, "Basic " <> build_token("valid_example", "invalid")} 82 | ]) 83 | 84 | assert code == 401 85 | end 86 | 87 | test "authenticate with correct token" do 88 | {:ok, client} = 89 | TestClient.start_link( 90 | host: "localhost", 91 | port: 3000, 92 | path: "/", 93 | headers: [{:authorization, "Basic " <> build_token("valid_example", "foobar")}] 94 | ) 95 | 96 | TestClient.test_message(%{ 97 | sender: client, 98 | message: %{"content" => "Hello"}, 99 | receivers: [ 100 | %{ 101 | receiver: client, 102 | tests: [ 103 | fn msg -> 104 | assert Map.has_key?(msg, "content") 105 | assert msg["content"] == "Hello" 106 | end 107 | ] 108 | } 109 | ] 110 | }) 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/riverside/io/timestamp/sandbox.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.IO.Timestamp.Sandbox do 2 | @behaviour Riverside.IO.Timestamp.Behaviour 3 | 4 | require Logger 5 | 6 | use GenServer 7 | alias Riverside.IO.Timestamp.Real 8 | 9 | @type mode :: :fixture | :real 10 | 11 | defstruct stack: [], 12 | mode: :fixture 13 | 14 | def mode(mode) do 15 | GenServer.call(__MODULE__, {:set_mode, mode}) 16 | end 17 | 18 | @impl Riverside.IO.Timestamp.Behaviour 19 | def seconds() do 20 | {:ok, seconds} = GenServer.call(__MODULE__, :get_seconds) 21 | Logger.debug(" seconds/0 returns: #{seconds}") 22 | seconds 23 | end 24 | 25 | @impl Riverside.IO.Timestamp.Behaviour 26 | def milli_seconds() do 27 | {:ok, milli_seconds} = GenServer.call(__MODULE__, :get_milli_seconds) 28 | Logger.debug(" milli_seconds/0 returns: #{milli_seconds}") 29 | milli_seconds 30 | end 31 | 32 | def set_seconds(list) when is_list(list) do 33 | Logger.debug(" refreshes to #{inspect(list)}") 34 | GenServer.call(__MODULE__, {:set_seconds_list, list}) 35 | end 36 | 37 | def set_seconds(seconds) do 38 | set_seconds([seconds]) 39 | end 40 | 41 | def set_milli_seconds(list) when is_list(list) do 42 | Logger.debug(" refreshes to #{inspect(list)}") 43 | GenServer.call(__MODULE__, {:set_milli_seconds_list, list}) 44 | end 45 | 46 | def set_milli_seconds(milli_seconds) do 47 | set_milli_seconds([milli_seconds]) 48 | end 49 | 50 | def start_link(opts \\ []) do 51 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 52 | end 53 | 54 | @impl GenServer 55 | def init(opts) when is_list(opts) do 56 | {:ok, %__MODULE__{stack: opts, mode: :fixture}} 57 | end 58 | 59 | @impl GenServer 60 | def handle_call({:set_mode, mode}, _from, state) do 61 | {:reply, :ok, %{state | mode: mode}} 62 | end 63 | 64 | def handle_call(:get_seconds, _from, %{mode: :real} = state) do 65 | {:reply, {:ok, Real.seconds()}, state} 66 | end 67 | 68 | def handle_call(:get_seconds, _from, %{stack: stack} = state) do 69 | {ms, stack2} = shift_stack(stack) 70 | {:reply, {:ok, div(ms, 1_000)}, %{state | stack: stack2}} 71 | end 72 | 73 | def handle_call(:get_milli_seconds, _from, %{mode: :real} = state) do 74 | {:reply, {:ok, Real.milli_seconds()}, state} 75 | end 76 | 77 | def handle_call(:get_milli_seconds, _from, %{stack: stack} = state) do 78 | {ms, stack2} = shift_stack(stack) 79 | {:reply, {:ok, ms}, %{state | stack: stack2}} 80 | end 81 | 82 | def handle_call({:set_seconds_list, list}, _from, state) do 83 | stack = list |> Enum.map(&:erlang.*(&1, 1_000)) 84 | {:reply, :ok, %{state | stack: stack}} 85 | end 86 | 87 | def handle_call({:set_milli_seconds_list, stack}, _from, state) do 88 | {:reply, :ok, %{state | stack: stack}} 89 | end 90 | 91 | @impl GenServer 92 | def terminate(_reason, _state) do 93 | :ok 94 | end 95 | 96 | defp shift_stack([]) do 97 | raise " No more dummy timestamp data, set enough amount of them" 98 | end 99 | 100 | defp shift_stack([first | rest]) do 101 | {first, rest} 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/stats_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestStatsHandler do 2 | require Logger 3 | use Riverside, otp_app: :riverside 4 | 5 | @impl Riverside 6 | def authenticate(_req) do 7 | {:ok, 1, %{}} 8 | end 9 | 10 | @impl Riverside 11 | def handle_message(msg, session, state) do 12 | if msg["dont_deliver"] do 13 | {:ok, session, state} 14 | else 15 | deliver_me(msg) 16 | {:ok, session, state} 17 | end 18 | end 19 | end 20 | 21 | defmodule Riverside.StatsTest do 22 | use ExUnit.Case 23 | 24 | alias Riverside.Test.TestServer 25 | alias Riverside.Test.TestClient 26 | 27 | setup do 28 | Riverside.IO.Timestamp.Sandbox.start_link() 29 | Riverside.IO.Timestamp.Sandbox.mode(:real) 30 | 31 | Riverside.IO.Random.Sandbox.start_link() 32 | Riverside.IO.Random.Sandbox.mode(:real) 33 | 34 | Riverside.Stats.start_link() 35 | 36 | {:ok, pid} = TestServer.start(TestStatsHandler, 3000, "/") 37 | 38 | ExUnit.Callbacks.on_exit(fn -> 39 | Riverside.Test.TestServer.stop(pid) 40 | end) 41 | 42 | :ok 43 | end 44 | 45 | test "stats" do 46 | # check first state, all number should be zero 47 | stats1 = Riverside.Stats.current_state() 48 | assert stats1.total_connections == 0 49 | assert stats1.current_connections == 0 50 | assert stats1.incoming_messages == 0 51 | assert stats1.outgoing_messages == 0 52 | 53 | {:ok, client} = TestClient.start_link(host: "localhost", port: 3000, path: "/") 54 | 55 | # check if connection number incremented 56 | stats2 = Riverside.Stats.current_state() 57 | assert stats2.total_connections == 1 58 | assert stats2.current_connections == 1 59 | assert stats2.incoming_messages == 0 60 | 61 | TestClient.test_message(%{ 62 | sender: client, 63 | message: %{"content" => "Hello"}, 64 | receivers: [ 65 | %{ 66 | receiver: client, 67 | tests: [ 68 | fn msg -> 69 | assert Map.has_key?(msg, "content") 70 | assert msg["content"] == "Hello" 71 | end 72 | ] 73 | } 74 | ] 75 | }) 76 | 77 | :timer.sleep(50) 78 | 79 | # check if both incoming and outgoing message counts incremented 80 | stats2 = Riverside.Stats.current_state() 81 | assert stats2.total_connections == 1 82 | assert stats2.current_connections == 1 83 | assert stats2.incoming_messages == 1 84 | assert stats2.outgoing_messages == 1 85 | 86 | TestClient.test_message(%{ 87 | sender: client, 88 | message: %{"content" => "Hello", "dont_deliver" => true}, 89 | receivers: [ 90 | %{ 91 | receiver: client, 92 | tests: [ 93 | fn msg -> 94 | assert Map.has_key?(msg, "content") 95 | assert msg["content"] == "Hello" 96 | end 97 | ] 98 | } 99 | ] 100 | }) 101 | 102 | :timer.sleep(50) 103 | 104 | # check if only incoming message counts incremented 105 | stats3 = Riverside.Stats.current_state() 106 | assert stats3.total_connections == 1 107 | assert stats3.current_connections == 1 108 | assert stats3.incoming_messages == 2 109 | assert stats3.outgoing_messages == 1 110 | 111 | TestClient.stop(client) 112 | 113 | :timer.sleep(50) 114 | 115 | # check if total connection remains, but current connection is decremented 116 | stats4 = Riverside.Stats.current_state() 117 | assert stats4.total_connections == 1 118 | assert stats4.current_connections == 0 119 | assert stats4.incoming_messages == 2 120 | assert stats4.outgoing_messages == 1 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /example/hello_riverside/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, 3 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, 4 | "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, 5 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, 6 | "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, 7 | "msgpax": {:hex, :msgpax, "2.3.0", "14f52ad249a3f77b5e2d59f6143e6c18a6e74f34666989e22bac0a465f9835cc", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "65c36846a62ed5615baf7d7d47babb6541313a6c0b6d2ff19354bd518f52df7e"}, 8 | "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, 9 | "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, 10 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, 11 | "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, 12 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, 13 | "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"}, 14 | "socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm", "f82ea9833ef49dde272e6568ab8aac657a636acb4cf44a7de8a935acb8957c2e"}, 15 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, 16 | "the_end": {:hex, :the_end, "1.1.0", "cd11af29051d8823b2e4d0109aa0bf0f08a00ce60ca36080ad2014e11e7f9d52", [:mix], [], "hexpm", "e1bfadb140ca43d9010a0da1c159782697f96ef50c68dcd7c59cb9215a4779d7"}, 17 | } 18 | -------------------------------------------------------------------------------- /lib/riverside/io/random/sandbox.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.IO.Random.Sandbox do 2 | @behaviour Riverside.IO.Random.Behaviour 3 | 4 | require Logger 5 | alias Riverside.IO.Random.Real 6 | 7 | use GenServer 8 | 9 | @type mode :: :fixture | :real 10 | 11 | defstruct uuid: [], 12 | hex: [], 13 | bigint: [], 14 | mode: :fixture 15 | 16 | def mode(mode) do 17 | GenServer.call(__MODULE__, {:set_mode, mode}) 18 | end 19 | 20 | def hex(len) do 21 | {:ok, hex} = GenServer.call(__MODULE__, {:hex, len}) 22 | Logger.debug(" hex/1 returns: #{hex}") 23 | hex 24 | end 25 | 26 | def bigint() do 27 | {:ok, bigint} = GenServer.call(__MODULE__, :bigint) 28 | Logger.debug(" bigint/0 returns: #{bigint}") 29 | bigint 30 | end 31 | 32 | def uuid() do 33 | {:ok, uuid} = GenServer.call(__MODULE__, :uuid) 34 | Logger.debug(" uuid/0 returns: #{uuid}") 35 | uuid 36 | end 37 | 38 | def create_and_set_hex(len) do 39 | hex = Real.hex(len) 40 | set_hex(hex) 41 | hex 42 | end 43 | 44 | def set_hex(list) when is_list(list) do 45 | Logger.debug(" hex refreshes to #{inspect(list)}") 46 | GenServer.call(__MODULE__, {:set_hex_list, list}) 47 | end 48 | 49 | def set_hex(hex) do 50 | set_hex([hex]) 51 | end 52 | 53 | def create_and_set_bigint() do 54 | bigint = Real.bigint() 55 | set_bigint(bigint) 56 | bigint 57 | end 58 | 59 | def set_bigint(list) when is_list(list) do 60 | Logger.debug(" bigint refreshes to #{inspect(list)}") 61 | GenServer.call(__MODULE__, {:set_bigint_list, list}) 62 | end 63 | 64 | def set_bigint(bigint) do 65 | set_bigint([bigint]) 66 | end 67 | 68 | def create_and_set_uuid() do 69 | uuid = Real.uuid() 70 | set_uuid(uuid) 71 | uuid 72 | end 73 | 74 | def set_uuid(list) when is_list(list) do 75 | Logger.debug(" Random UUI refreshes to #{inspect(list)}") 76 | GenServer.call(__MODULE__, {:set_uuid_list, list}) 77 | end 78 | 79 | def set_uuid(uuid) do 80 | set_uuid([uuid]) 81 | end 82 | 83 | def start_link() do 84 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 85 | end 86 | 87 | def init(_args) do 88 | {:ok, %{hex: [], bigint: [], uuid: [], mode: :fixture}} 89 | end 90 | 91 | def handle_call({:set_mode, mode}, _from, state) do 92 | {:reply, :ok, %{state | mode: mode}} 93 | end 94 | 95 | def handle_call({:set_hex_list, list}, _from, state) do 96 | {:reply, :ok, %{state | hex: list}} 97 | end 98 | 99 | def handle_call({:set_bigint_list, list}, _from, state) do 100 | {:reply, :ok, %{state | bigint: list}} 101 | end 102 | 103 | def handle_call({:set_uuid_list, list}, _from, state) do 104 | {:reply, :ok, %{state | uuid: list}} 105 | end 106 | 107 | def handle_call({:hex, len}, _from, %{mode: :real} = state) do 108 | {:reply, {:ok, Real.hex(len)}, state} 109 | end 110 | 111 | def handle_call({:hex, _len}, _from, %{hex: stack} = state) do 112 | {hex, stack2} = shift_stack(stack) 113 | {:reply, {:ok, hex}, %{state | hex: stack2}} 114 | end 115 | 116 | def handle_call(:bigint, _from, %{mode: :real} = state) do 117 | {:reply, {:ok, Real.bigint()}, state} 118 | end 119 | 120 | def handle_call(:bigint, _from, %{bigint: stack} = state) do 121 | {bigint, stack2} = shift_stack(stack) 122 | {:reply, {:ok, bigint}, %{state | bigint: stack2}} 123 | end 124 | 125 | def handle_call(:uuid, _from, %{mode: :real} = state) do 126 | {:reply, {:ok, Real.uuid()}, state} 127 | end 128 | 129 | def handle_call(:uuid, _from, %{uuid: stack} = state) do 130 | {uuid, stack2} = shift_stack(stack) 131 | {:reply, {:ok, uuid}, %{state | uuid: stack2}} 132 | end 133 | 134 | def terminate(_reason, _state) do 135 | :ok 136 | end 137 | 138 | defp shift_stack([]) do 139 | raise " No more dummy data, set enough amount of them" 140 | end 141 | 142 | defp shift_stack([first | rest]) do 143 | {first, rest} 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/riverside/test/test_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Test.TestClient do 2 | require Logger 3 | 4 | use GenServer 5 | 6 | defstruct sock: nil, 7 | codec: nil 8 | 9 | def stop(pid) do 10 | GenServer.call(pid, :stop) 11 | end 12 | 13 | def send_message(pid, msg) do 14 | GenServer.cast(pid, {:send, msg}) 15 | end 16 | 17 | def test_message(%{sender: pid, message: message, receivers: receivers}, timeout \\ 1_000) do 18 | receivers 19 | |> Enum.each(fn %{receiver: receiver, tests: tests} -> 20 | wait_to_test(receiver, tests, timeout) 21 | end) 22 | 23 | send_message(pid, message) 24 | end 25 | 26 | def wait_to_test(pid, functions, timeout) do 27 | GenServer.call(pid, {:wait_to_receive, functions, timeout}, timeout + 1_000) 28 | end 29 | 30 | def start_link(opts) do 31 | GenServer.start_link(__MODULE__, opts) 32 | end 33 | 34 | def new(sock, codec) do 35 | %__MODULE__{sock: sock, codec: codec} 36 | end 37 | 38 | def connect(host, port, path, headers) do 39 | Socket.Web.connect(host, port, path: path, headers: headers) 40 | end 41 | 42 | @impl GenServer 43 | def init(opts) do 44 | host = Keyword.get(opts, :host, "localhost") 45 | port = Keyword.get(opts, :port, 8000) 46 | path = Keyword.get(opts, :path, "/") 47 | headers = Keyword.get(opts, :headers, []) 48 | codec = Keyword.get(opts, :codec, Riverside.Codec.JSON) 49 | 50 | case connect(host, port, path, headers) do 51 | {:ok, sock} -> 52 | start_receiver(sock) 53 | {:ok, new(sock, codec)} 54 | 55 | {:error, _reason} -> 56 | {:stop, :normal} 57 | end 58 | end 59 | 60 | @spec start_receiver(%Socket.Web{}) :: pid 61 | defp start_receiver(sock) do 62 | spawn_link(fn -> 63 | receiver_loop(self(), sock) 64 | end) 65 | end 66 | 67 | @spec receiver_loop(pid, %Socket.Web{}) :: no_return 68 | defp receiver_loop(parent, sock) do 69 | case receive_message(sock) do 70 | {:ok, type, data} -> 71 | send(parent, {:data, type, data}) 72 | receiver_loop(parent, sock) 73 | 74 | {:error, :unsupported_frame} -> 75 | receiver_loop(parent, sock) 76 | end 77 | end 78 | 79 | defp receive_message(sock) do 80 | case Socket.Web.recv!(sock) do 81 | {type, data} when type in [:text, :binary] -> 82 | {:ok, type, data} 83 | 84 | _other -> 85 | {:error, :unsupported_frame} 86 | end 87 | end 88 | 89 | defp decode_message(codec, type, packet) do 90 | if codec.frame_type === type do 91 | case codec.decode(packet) do 92 | {:ok, value} -> 93 | {:ok, value} 94 | 95 | {:error, reason} -> 96 | Logger.warn(" failed to decode received message: #{reason}") 97 | {:error, :bad_format} 98 | end 99 | else 100 | {:error, :unsupported_frame} 101 | end 102 | end 103 | 104 | defp wait_to_receive(timer, [], state) do 105 | :erlang.cancel_timer(timer) 106 | {:reply, :ok, state} 107 | end 108 | 109 | defp wait_to_receive(timer, [test | rest_tests], state) do 110 | receive do 111 | {:received, msg} -> 112 | case test.(msg) do 113 | :ok -> 114 | wait_to_receive(timer, rest_tests, state) 115 | 116 | :error -> 117 | {:reply, {:error, :failed}, state} 118 | end 119 | 120 | {:timeout, ^timer, :timeout} -> 121 | {:reply, {:error, :timeout}, state} 122 | end 123 | end 124 | 125 | @impl GenServer 126 | def handle_call({:wait_to_receive, tests, timeout}, _from, state) do 127 | timer = :erlang.start_timer(timeout, self(), :timeout) 128 | wait_to_receive(timer, tests, state) 129 | end 130 | 131 | def handle_call(:stop, _from, state) do 132 | Socket.Web.close(state.sock) 133 | {:stop, :normal, :ok, state} 134 | end 135 | 136 | @impl GenServer 137 | def handle_info({:data, type, data}, state) do 138 | case decode_message(state.codec, type, data) do 139 | {:ok, value} -> 140 | send(self(), {:receive, value}) 141 | {:noreply, state} 142 | 143 | {:error, _reason} -> 144 | {:noreply, state} 145 | end 146 | end 147 | 148 | @impl GenServer 149 | def handle_cast({:send, packet}, %{codec: codec} = state) do 150 | case codec.encode(packet) do 151 | {:ok, value} -> 152 | Socket.Web.send!(state.sock, {codec.frame_type, value}) 153 | {:noreply, state} 154 | 155 | {:error, reason} -> 156 | Logger.warn(" failed to format message: #{reason}") 157 | {:noreply, state} 158 | end 159 | end 160 | 161 | @impl GenServer 162 | def terminate(_reason, _state) do 163 | :ok 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /test/channel_broadcast_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestChannelBroadcastHandler do 2 | require Logger 3 | use Riverside, otp_app: :riverside 4 | 5 | @impl Riverside 6 | def authenticate(req) do 7 | channel = req.queries["channel"] 8 | 9 | case req.bearer_token do 10 | "foo" -> 11 | {:ok, 1, %{channel: channel}} 12 | 13 | "bar" -> 14 | {:ok, 2, %{channel: channel}} 15 | 16 | "buz" -> 17 | {:ok, 3, %{channel: channel}} 18 | 19 | _ -> 20 | error = 21 | auth_error_with_code(401) 22 | |> put_auth_error_bearer_header("example.org", "invalid_token") 23 | 24 | {:error, error} 25 | end 26 | end 27 | 28 | @impl Riverside 29 | def init(session, %{channel: channel} = state) do 30 | join_channel(channel) 31 | 32 | {:ok, session, state} 33 | end 34 | 35 | @impl Riverside 36 | def handle_message(incoming, session, %{channel: channel} = state) do 37 | content = incoming["content"] 38 | 39 | outgoing = %{"from" => "#{channel}/#{session.user_id}/#{session.id}", "content" => content} 40 | 41 | deliver_channel(channel, outgoing) 42 | 43 | {:ok, session, state} 44 | end 45 | end 46 | 47 | defmodule Riverside.ChannelBroadcastTest do 48 | use ExUnit.Case 49 | 50 | alias Riverside.Test.TestServer 51 | alias Riverside.Test.TestClient 52 | 53 | setup do 54 | Riverside.IO.Timestamp.Sandbox.start_link() 55 | Riverside.IO.Timestamp.Sandbox.mode(:real) 56 | 57 | Riverside.IO.Random.Sandbox.start_link() 58 | Riverside.IO.Random.Sandbox.mode(:real) 59 | 60 | Riverside.Stats.start_link() 61 | 62 | {:ok, pid} = TestServer.start(TestChannelBroadcastHandler, 3000, "/") 63 | 64 | ExUnit.Callbacks.on_exit(fn -> 65 | Riverside.Test.TestServer.stop(pid) 66 | end) 67 | 68 | :ok 69 | end 70 | 71 | test "broadcast in channel" do 72 | {:ok, foo1} = 73 | TestClient.start_link( 74 | host: "localhost", 75 | port: 3000, 76 | path: "/?channel=1", 77 | headers: [{:authorization, "Bearer foo"}] 78 | ) 79 | 80 | {:ok, bar} = 81 | TestClient.start_link( 82 | host: "localhost", 83 | port: 3000, 84 | path: "/?channel=1", 85 | headers: [{:authorization, "Bearer bar"}] 86 | ) 87 | 88 | {:ok, buz} = 89 | TestClient.start_link( 90 | host: "localhost", 91 | port: 3000, 92 | path: "/?channel=2", 93 | headers: [{:authorization, "Bearer buz"}] 94 | ) 95 | 96 | {:ok, foo2} = 97 | TestClient.start_link( 98 | host: "localhost", 99 | port: 3000, 100 | path: "/?channel=2", 101 | headers: [{:authorization, "Bearer foo"}] 102 | ) 103 | 104 | TestClient.test_message(%{ 105 | sender: foo1, 106 | message: %{"to" => "foo", "content" => "Hello"}, 107 | receivers: [ 108 | %{ 109 | receiver: bar, 110 | tests: [ 111 | fn msg -> 112 | assert Map.has_key?(msg, "content") 113 | [channel, user_id, _session_id] = String.split(msg["from"], "/") 114 | assert channel == "1" 115 | assert user_id == "foo" 116 | assert msg["content"] == "Hello" 117 | end 118 | ] 119 | }, 120 | %{ 121 | receiver: foo1, 122 | tests: [ 123 | fn msg -> 124 | assert Map.has_key?(msg, "content") 125 | [channel, user_id, _session_id] = String.split(msg["from"], "/") 126 | assert channel == "1" 127 | assert user_id == "foo" 128 | assert msg["content"] == "Hello" 129 | end 130 | ] 131 | } 132 | ] 133 | }) 134 | 135 | TestClient.test_message(%{ 136 | sender: buz, 137 | message: %{"to" => "foo", "content" => "Hey"}, 138 | receivers: [ 139 | %{ 140 | receiver: foo2, 141 | tests: [ 142 | fn msg -> 143 | assert Map.has_key?(msg, "content") 144 | [channel, user_id, _session_id] = String.split(msg["from"], "/") 145 | assert channel == "2" 146 | assert user_id == "bar" 147 | assert msg["content"] == "Hey" 148 | end 149 | ] 150 | }, 151 | %{ 152 | receiver: buz, 153 | tests: [ 154 | fn msg -> 155 | assert Map.has_key?(msg, "content") 156 | [channel, user_id, _session_id] = String.split(msg["from"], "/") 157 | assert channel == "2" 158 | assert user_id == "bar" 159 | assert msg["content"] == "Hey" 160 | end 161 | ] 162 | } 163 | ] 164 | }) 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /lib/riverside/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Config do 2 | @moduledoc ~S""" 3 | Helper for config data 4 | """ 5 | 6 | @type t :: %__MODULE__{ 7 | max_connections: non_neg_integer, 8 | codec: module, 9 | show_debug_logs: boolean, 10 | connection_max_age: non_neg_integer, 11 | port: non_neg_integer, 12 | path: String.t(), 13 | idle_timeout: non_neg_integer, 14 | reuse_port: boolean, 15 | tls: boolean, 16 | tls_certfile: String.t(), 17 | tls_keyfile: String.t(), 18 | transmission_limit: Keyword.t(), 19 | otp_app: atom, 20 | cowboy_opts: keyword() 21 | } 22 | 23 | defstruct max_connections: 0, 24 | codec: nil, 25 | show_debug_logs: false, 26 | connection_max_age: 0, 27 | port: 0, 28 | path: "", 29 | idle_timeout: 0, 30 | reuse_port: false, 31 | tls: false, 32 | tls_certfile: "", 33 | tls_keyfile: "", 34 | transmission_limit: [], 35 | otp_app: nil, 36 | cowboy_opts: [] 37 | 38 | @doc ~S""" 39 | Load handler's configuration. 40 | """ 41 | @spec load(module, any) :: any 42 | def load(handler, opts) do 43 | otp_app = Keyword.fetch!(opts, :otp_app) 44 | config = otp_app |> Application.get_env(handler, []) 45 | 46 | %__MODULE__{ 47 | max_connections: Keyword.get(config, :max_connections, 65536), 48 | codec: Keyword.get(config, :codec, Riverside.Codec.JSON), 49 | show_debug_logs: Keyword.get(config, :show_debug_logs, false), 50 | connection_max_age: Keyword.get(config, :connection_max_age, :infinity), 51 | port: Keyword.get(config, :port, 3000), 52 | path: Keyword.get(config, :path, "/"), 53 | idle_timeout: Keyword.get(config, :idle_timeout, 60_000), 54 | reuse_port: Keyword.get(config, :reuse_port, false), 55 | tls: Keyword.get(config, :tls, false), 56 | tls_certfile: Keyword.get(config, :tls_certfile, ""), 57 | tls_keyfile: Keyword.get(config, :tls_keyfile, ""), 58 | transmission_limit: transmission_limit(config), 59 | otp_app: otp_app, 60 | cowboy_opts: Keyword.get(config, :cowboy_opts, []) 61 | } 62 | end 63 | 64 | @type port_type :: pos_integer | {atom, String.t(), pos_integer} 65 | 66 | @doc ~S""" 67 | Get runtime port number from configuration 68 | """ 69 | @spec get_port(port_type) :: pos_integer 70 | def get_port(port) do 71 | case port do 72 | num when is_integer(num) -> 73 | num 74 | 75 | str when is_binary(str) -> 76 | String.to_integer(str) 77 | 78 | {:system, env, default} when is_binary(env) and is_integer(default) -> 79 | case System.get_env(env) || default do 80 | num when is_integer(num) -> num 81 | str when is_binary(str) -> String.to_integer(str) 82 | _other -> raise ArgumentError, "'port' value should be a number" 83 | end 84 | 85 | _other -> 86 | raise ArgumentError, 87 | "'port' config should be a positive-number or a tuple styleed value like {:system, 'ENV_NAME', 8080}." 88 | end 89 | end 90 | 91 | @doc ~S""" 92 | Get runtime TLS flag 93 | """ 94 | @spec get_tls(term) :: boolean 95 | def get_tls(tls_flag) do 96 | case tls_flag do 97 | flag when is_boolean(flag) -> 98 | flag 99 | 100 | str when is_binary(str) -> 101 | str == "true" 102 | 103 | {:system, env, default} when is_binary(env) and is_boolean(default) -> 104 | case System.get_env(env) || default do 105 | flag when is_boolean(flag) -> flag 106 | str when is_binary(str) -> str == "true" 107 | _other -> raise ArgumentError, "'tls' value should be a boolaen" 108 | end 109 | 110 | _other -> 111 | raise ArgumentError, 112 | "'tls' config should be a boolean or a tuple styleed value like {:system, 'ENV_NAME', false}." 113 | end 114 | end 115 | 116 | @doc ~S""" 117 | Get runtime TLS cert file 118 | """ 119 | @spec get_tls_certfile(term) :: String.t() 120 | def get_tls_certfile(path) do 121 | case path do 122 | str when is_binary(str) -> 123 | str 124 | 125 | {:system, env, default} when is_binary(env) and is_binary(default) -> 126 | case System.get_env(env) || default do 127 | str when is_binary(str) -> str 128 | _other -> raise ArgumentError, "'tls_certfile' value should be a string" 129 | end 130 | 131 | _other -> 132 | raise ArgumentError, 133 | "'tls_certfile' config should be a string or a tuple styleed value like {:system, 'ENV_NAME', '/path/to/default/certfile'}." 134 | end 135 | end 136 | 137 | @doc ~S""" 138 | Get runtime TLS key file 139 | """ 140 | @spec get_tls_keyfile(term) :: String.t() 141 | def get_tls_keyfile(path) do 142 | case path do 143 | str when is_binary(str) -> 144 | str 145 | 146 | {:system, env, default} when is_binary(env) and is_binary(default) -> 147 | case System.get_env(env) || default do 148 | str when is_binary(str) -> str 149 | _other -> raise ArgumentError, "'tls_keyfile' value should be a string" 150 | end 151 | 152 | _other -> 153 | raise ArgumentError, 154 | "'tls_keyfile' config should be a string or a tuple styleed value like {:system, 'ENV_NAME', '/path/to/default/keyfile'}." 155 | end 156 | end 157 | 158 | @doc ~S""" 159 | Get runtime cowboy options 160 | """ 161 | @spec get_cowboy_opts(term) :: keyword() 162 | def get_cowboy_opts(nil), do: [] 163 | def get_cowboy_opts(opts) when is_list(opts), do: opts 164 | 165 | def get_cowboy_opts(_) do 166 | raise ArgumentError, 167 | "'cowboy_opts' config should be a keyword list of cowboy options, see [Cowboy docs](https://ninenines.eu/docs/en/cowboy/2.5/manual/cowboy_http/)." 168 | end 169 | 170 | @doc ~S""" 171 | Pick the TransmissionLimitter's parameters 172 | from Handlers configuration. 173 | """ 174 | @spec transmission_limit(any) :: keyword 175 | def transmission_limit(config) do 176 | if Keyword.has_key?(config, :transmission_limit) do 177 | mc = Keyword.get(config, :transmission_limit, []) 178 | duration = Keyword.get(mc, :duration, 2_000) 179 | capacity = Keyword.get(mc, :capacity, 50) 180 | [duration: duration, capacity: capacity] 181 | else 182 | [duration: 2_000, capacity: 50] 183 | end 184 | end 185 | 186 | @doc ~S""" 187 | Ensure passed module is compiled already. 188 | Or else, this function raise an error. 189 | """ 190 | @spec ensure_module_loaded(module) :: :ok 191 | def ensure_module_loaded(module) do 192 | unless Code.ensure_loaded?(module) do 193 | raise ArgumentError, 194 | "#{module} not compiled, ensure the name is correct and it's included in project dependencies." 195 | end 196 | 197 | :ok 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "accept": {:hex, :accept, "0.3.3", "548ebb6fb2e8b0d170e75bb6123aea6ceecb0189bb1231eeadf52eac08384a97", [:rebar3], [], "hexpm", "9df23358b4d0c62d058fb84281aae5e7a850dcc923d4907d12b938b189e20208"}, 3 | "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, 4 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, 5 | "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, 6 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, 7 | "earmark_parser": {:hex, :earmark_parser, "1.4.16", "607709303e1d4e3e02f1444df0c821529af1c03b8578dfc81bb9cf64553d02b9", [:mix], [], "hexpm", "69fcf696168f5a274dd012e3e305027010658b2d1630cef68421d6baaeaccead"}, 8 | "ebus": {:hex, :erlbus, "0.2.1", "7d976888a8f8f3583200d31494d4629d9796d5790ecf14a5da6fbb9253106607", [:make, :rebar3], []}, 9 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.0", "ff26e938f95830b1db152cb6e594d711c10c02c6391236900ddd070a6b01271d", [:mix], [], "hexpm", "e4d6e26434471761ed45a3545239da87af7b70904dd4442a55f87d06b137c56b"}, 10 | "ex_doc": {:hex, :ex_doc, "0.25.5", "ac3c5425a80b4b7c4dfecdf51fa9c23a44877124dd8ca34ee45ff608b1c6deb9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "688cfa538cdc146bc4291607764a7f1fcfa4cce8009ecd62de03b27197528350"}, 11 | "gproc": {:hex, :gproc, "0.5.0", "2df2d886f8f8a7b81a4b04aa17972b5965bbc5bf0100ea6d8e8ac6a0e7389afe", [:rebar], []}, 12 | "graceful_stopper": {:git, "https://github.com/lyokato/graceful_stopper.git", "a7673a622a6ce2787ff56bd618593cf464b19768", [tag: "0.1.1"]}, 13 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 14 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 15 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 16 | "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, 17 | "msgpax": {:hex, :msgpax, "2.3.0", "14f52ad249a3f77b5e2d59f6143e6c18a6e74f34666989e22bac0a465f9835cc", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "65c36846a62ed5615baf7d7d47babb6541313a6c0b6d2ff19354bd518f52df7e"}, 18 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 19 | "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, 20 | "plug_cowboy": {:hex, :plug_cowboy, "2.5.0", "51c998f788c4e68fc9f947a5eba8c215fbb1d63a520f7604134cab0270ea6513", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5b2c8925a5e2587446f33810a58c01e66b3c345652eeec809b76ba007acde71a"}, 21 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, 22 | "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, 23 | "prometheus": {:hex, :prometheus, "4.2.0", "06c58bfdfe28d3168b926da614cb9a6d39593deebde648a5480e32dfa3c370e9", [:mix, :rebar3], [], "hexpm", "286536224ed4fdaca44d230d0b3c65e862e04971b8564443ef1f54ee07aa4328"}, 24 | "prometheus_ex": {:hex, :prometheus_ex, "3.0.2", "e1924ebc2983e6a04c9f69321f960c751b17254bcd09ba41d46e2c7e6bd88c3b", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "5a868092d7a668cdd1073e66d14cdd4761a6cfabcdf4ade236b26327ac838b21"}, 25 | "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, 26 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, 27 | "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"}, 28 | "socket": {:hex, :socket, "0.3.12", "4a6543815136503fee67eff0932da1742fad83f84c49130c854114153cc549a6", [:mix], [], "hexpm", "cc0a117a0ae025e60d9fbb4198ca73d64698b525fd942acffa0f711b7186f2f8"}, 29 | "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, 30 | "the_end": {:hex, :the_end, "1.1.0", "cd11af29051d8823b2e4d0109aa0bf0f08a00ce60ca36080ad2014e11e7f9d52", [:mix], [], "hexpm", "e1bfadb140ca43d9010a0da1c159782697f96ef50c68dcd7c59cb9215a4779d7"}, 31 | } 32 | -------------------------------------------------------------------------------- /lib/riverside/connection.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside.Connection do 2 | @behaviour :cowboy_websocket 3 | 4 | require Logger 5 | 6 | alias Riverside.AuthRequest 7 | alias Riverside.AuthError 8 | alias Riverside.ExceptionGuard 9 | alias Riverside.IO.Random 10 | alias Riverside.LocalDelivery 11 | alias Riverside.PeerAddress 12 | alias Riverside.Session 13 | alias Riverside.Stats 14 | alias Riverside.Util.CowboyUtil 15 | 16 | @type shutdown_reason :: :too_many_messages 17 | 18 | @type t :: %__MODULE__{ 19 | handler: module, 20 | session: Session.t(), 21 | shutdown_reason: shutdown_reason | nil, 22 | handler_state: any 23 | } 24 | 25 | defstruct handler: nil, 26 | session: nil, 27 | shutdown_reason: nil, 28 | handler_state: nil 29 | 30 | @spec new( 31 | handler :: module, 32 | user_id :: Session.user_id(), 33 | session_id :: Session.session_id(), 34 | peer :: PeerAddress.t(), 35 | handler_state :: any 36 | ) :: t 37 | 38 | def new(handler, user_id, session_id, peer, handler_state) do 39 | %__MODULE__{ 40 | handler: handler, 41 | session: Session.new(user_id, session_id, peer), 42 | shutdown_reason: nil, 43 | handler_state: handler_state 44 | } 45 | end 46 | 47 | def init(req, opts) do 48 | ExceptionGuard.guard( 49 | " init", 50 | fn -> {:ok, CowboyUtil.response(req, 500, %{})} end, 51 | fn -> 52 | peer = PeerAddress.gather(req) 53 | 54 | handler = Keyword.fetch!(opts, :handler) 55 | 56 | if handler.__config__.show_debug_logs do 57 | Logger.debug(" incoming new request: #{peer}") 58 | end 59 | 60 | if Stats.number_of_current_connections() >= 61 | handler.__config__.max_connections do 62 | Logger.warn( 63 | " connection number reached the limit." 64 | ) 65 | 66 | # :cow_http.status/1 doesn't support 508, so use 503 instead 67 | {:ok, CowboyUtil.response(req, 503, %{}), {:unset, handler.__config__.show_debug_logs}} 68 | else 69 | auth_req = AuthRequest.new(req, peer) 70 | 71 | case handler.__handle_authentication__(auth_req) do 72 | {:ok, user_id, handler_state} -> 73 | timeout = handler.__config__.idle_timeout 74 | session_id = Random.hex(20) 75 | state = new(handler, user_id, session_id, peer, handler_state) 76 | {:cowboy_websocket, req, state, %{idle_timeout: timeout}} 77 | 78 | {:ok, user_id, session_id, handler_state} -> 79 | timeout = handler.__config__.idle_timeout 80 | state = new(handler, user_id, session_id, peer, handler_state) 81 | {:cowboy_websocket, req, state, %{idle_timeout: timeout}} 82 | 83 | {:error, %AuthError{code: code, headers: headers}} -> 84 | {:ok, CowboyUtil.response(req, code, headers), 85 | {:unset, handler.__config__.show_debug_logs}} 86 | 87 | other -> 88 | if handler.__config__.show_debug_logs do 89 | Logger.debug( 90 | " failed to authenticate by reason: #{inspect(other)}, shutdown" 91 | ) 92 | end 93 | 94 | {:ok, CowboyUtil.response(req, 500, %{}), 95 | {:unset, handler.__config__.show_debug_logs}} 96 | end 97 | end 98 | end 99 | ) 100 | end 101 | 102 | def websocket_init(state) do 103 | ExceptionGuard.guard( 104 | "(#{state.session}) websocket_init", 105 | fn -> {:stop, state} end, 106 | fn -> 107 | if state.handler.__config__.show_debug_logs do 108 | Logger.debug("(#{state.session}) @init") 109 | end 110 | 111 | if Stats.number_of_current_connections() >= 112 | state.handler.__config__.max_connections do 113 | Logger.warn(" connection number is over limit") 114 | 115 | {:stop, state} 116 | else 117 | Process.flag(:trap_exit, true) 118 | 119 | send(self(), :post_init) 120 | 121 | Stats.countup_connections() 122 | 123 | LocalDelivery.register(state.session.user_id, state.session.id) 124 | 125 | {:ok, state} 126 | end 127 | end 128 | ) 129 | end 130 | 131 | def websocket_info(:post_init, state) do 132 | ExceptionGuard.guard( 133 | "(#{state.session}) websocket_info", 134 | fn -> {:stop, state} end, 135 | fn -> 136 | if state.handler.__config__.show_debug_logs do 137 | Logger.debug("(#{state.session}) @post_init") 138 | end 139 | 140 | case state.handler.init(state.session, state.handler_state) do 141 | {:ok, session2, handler_state2} -> 142 | if state.handler.__config__.connection_max_age != :infinity do 143 | Process.send_after(self(), :over_age, state.handler.__config__.connection_max_age) 144 | end 145 | 146 | state2 = %{state | session: session2, handler_state: handler_state2} 147 | {:ok, state2, :hibernate} 148 | 149 | {:error, reason} -> 150 | Logger.info( 151 | "(#{state.session}) failed to initialize: #{inspect(reason)}" 152 | ) 153 | 154 | {:stop, state} 155 | end 156 | end 157 | ) 158 | end 159 | 160 | def websocket_info(:over_age, state) do 161 | if state.handler.__config__.show_debug_logs do 162 | Logger.debug("(#{state.session}) @over_age") 163 | end 164 | 165 | {:stop, %{state | shutdown_reason: :over_age}} 166 | end 167 | 168 | def websocket_info(:stop, state) do 169 | if state.handler.__config__.show_debug_logs do 170 | Logger.debug("(#{state.session}) @stop") 171 | end 172 | 173 | {:stop, state} 174 | end 175 | 176 | def websocket_info({:deliver, type, msg}, state) do 177 | if state.handler.__config__.show_debug_logs do 178 | Logger.debug("(#{state.session}) @deliver") 179 | end 180 | 181 | Stats.countup_outgoing_messages() 182 | 183 | {:reply, {type, msg}, state, :hibernate} 184 | end 185 | 186 | def websocket_info({:EXIT, pid, reason}, %{session: session} = state) do 187 | ExceptionGuard.guard( 188 | "(#{session}) websocket_info", 189 | fn -> {:stop, state} end, 190 | fn -> 191 | if state.handler.__config__.show_debug_logs do 192 | Logger.debug( 193 | "(#{session}) @exit: #{inspect(pid)} -> #{inspect(self())}" 194 | ) 195 | end 196 | 197 | if Session.should_delegate_exit?(session, pid) do 198 | session2 = Session.forget_to_trap_exit(session, pid) 199 | 200 | state2 = %{state | session: session2} 201 | 202 | handler_info({:EXIT, pid, reason}, state2) 203 | else 204 | {:stop, state} 205 | end 206 | end 207 | ) 208 | end 209 | 210 | def websocket_info(event, state) do 211 | ExceptionGuard.guard( 212 | "(#{state.session}) websocket_info", 213 | fn -> {:stop, state} end, 214 | fn -> 215 | if state.handler.__config__.show_debug_logs do 216 | Logger.debug( 217 | "(#{state.session}) @info: #{inspect(event)}" 218 | ) 219 | end 220 | 221 | handler_info(event, state) 222 | end 223 | ) 224 | end 225 | 226 | defp handler_info(event, state) do 227 | case state.handler.handle_info(event, state.session, state.handler_state) do 228 | {:ok, session2, handler_state2} -> 229 | state2 = %{state | session: session2, handler_state: handler_state2} 230 | {:ok, state2} 231 | 232 | {:stop, reason, handler_state2} -> 233 | {:stop, %{state | shutdown_reason: reason, handler_state: handler_state2}} 234 | 235 | # TODO support reply? 236 | _other -> 237 | {:stop, state} 238 | end 239 | end 240 | 241 | def websocket_handle(:ping, state) do 242 | ExceptionGuard.guard( 243 | " websocket_handle", 244 | fn -> {:stop, state} end, 245 | fn -> 246 | if state.handler.__config__.show_debug_logs do 247 | Logger.debug("(#{state.session}) @ping") 248 | end 249 | 250 | handle_frame(:ping, nil, state) 251 | end 252 | ) 253 | end 254 | 255 | def websocket_handle({:binary, data}, state) do 256 | ExceptionGuard.guard( 257 | " websocket_handle", 258 | fn -> {:stop, state} end, 259 | fn -> 260 | if state.handler.__config__.show_debug_logs do 261 | Logger.debug("(#{state.session}) @binary") 262 | end 263 | 264 | handle_frame(:binary, data, state) 265 | end 266 | ) 267 | end 268 | 269 | def websocket_handle({:text, data}, state) do 270 | ExceptionGuard.guard( 271 | "(#{state.session}) websocket_handle", 272 | fn -> {:stop, state} end, 273 | fn -> 274 | if state.handler.__config__.show_debug_logs do 275 | Logger.debug("(#{state.session}) @text") 276 | end 277 | 278 | handle_frame(:text, data, state) 279 | end 280 | ) 281 | end 282 | 283 | def websocket_handle(event, state) do 284 | if state.handler.__config__.show_debug_logs do 285 | Logger.debug( 286 | "(#{state.session}) handle: unsupported event #{inspect(event)}" 287 | ) 288 | end 289 | 290 | {:ok, state} 291 | end 292 | 293 | def terminate(reason, _req, {:unset, show_debug_logs}) do 294 | if show_debug_logs do 295 | Logger.debug(" @terminate: #{inspect(reason)}") 296 | end 297 | 298 | :ok 299 | end 300 | 301 | def terminate(reason, _req, %{shutdown_reason: nil} = state) do 302 | ExceptionGuard.guard( 303 | "(#{state.session}) terminate", 304 | fn -> :ok end, 305 | fn -> 306 | if state.handler.__config__.show_debug_logs do 307 | Logger.debug( 308 | "(#{state.session}) @terminate: #{inspect(reason)}" 309 | ) 310 | end 311 | 312 | state.handler.terminate(reason, state.session, state.handler_state) 313 | 314 | Stats.countdown_connections() 315 | 316 | :ok 317 | end 318 | ) 319 | end 320 | 321 | def terminate(reason, _req, state) do 322 | ExceptionGuard.guard( 323 | "(#{state.session}) terminate", 324 | fn -> :ok end, 325 | fn -> 326 | if state.handler.__config__.show_debug_logs do 327 | Logger.debug( 328 | "(#{state.session}) @terminate: #{inspect(reason)}" 329 | ) 330 | end 331 | 332 | state.handler.terminate(state.shutdown_reason, state.session, state.handler_state) 333 | 334 | Stats.countdown_connections() 335 | 336 | :ok 337 | end 338 | ) 339 | end 340 | 341 | defp handle_frame(type, data, %{handler: handler, session: session} = state) do 342 | Stats.countup_incoming_messages() 343 | 344 | case Session.countup_messages(session, handler.__config__.transmission_limit) do 345 | {:ok, session2} -> 346 | state2 = %{state | session: session2} 347 | 348 | case handle_data(type, data, state2) do 349 | {:ok, session3, handler_state3} -> 350 | state3 = %{state2 | session: session3, handler_state: handler_state3} 351 | {:ok, state3, :hibernate} 352 | 353 | {:stop, reason, handler_state3} -> 354 | {:stop, %{state2 | shutdown_reason: reason, handler_state: handler_state3}} 355 | 356 | {:error, reason} -> 357 | if state.handler.__config__.show_debug_logs do 358 | Logger.debug( 359 | "(#{session2}) failed to handle frame_type #{inspect(type)}: #{inspect(reason)}" 360 | ) 361 | end 362 | 363 | {:ok, state2} 364 | end 365 | 366 | {:error, :too_many_messages} -> 367 | Logger.debug( 368 | "(#{session}) too many messages: #{Session.peer_address(session)}" 369 | ) 370 | 371 | {:stop, %{state | shutdown_reason: :too_many_messages}} 372 | end 373 | end 374 | 375 | defp handle_data(:text, data, state) do 376 | state.handler.__handle_data__(:text, data, state.session, state.handler_state) 377 | end 378 | 379 | defp handle_data(:binary, data, state) do 380 | state.handler.__handle_data__(:binary, data, state.session, state.handler_state) 381 | end 382 | 383 | defp handle_data(:ping, _data, state) do 384 | {:ok, state.session, state.handler_state} 385 | end 386 | end 387 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Riverside - Plain WebSocket Server Framework for Elixir 2 | 3 | ## Installation 4 | 5 | 6 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 7 | by adding `riverside` to your list of dependencies in `mix.exs`: 8 | 9 | ```elixir 10 | def deps do 11 | [ 12 | {:riverside, "~> 2.2.1"} 13 | ] 14 | end 15 | ``` 16 | 17 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 18 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 19 | be found at [https://hexdocs.pm/riverside](https://hexdocs.pm/riverside). 20 | 21 | ## Version v2 22 | 23 | This library had been updated only as a bugfix for a long time, but in the meantime, the version of Elixir, plug, cowboy, etc. had been upgraded, and the version of each library that riverside depends on had become outdated. 24 | 25 | The version of each library that riverside depends on became outdated. riverside-v2 is the result of upgrading the dependent libraries and Elixir versions to more recent ones, and fixing the problems that occurred with riverside at that time. 26 | Although the version has been increased, there are no additional features. 27 | 28 | The functionality is almost the same as v1, but the metrics-related features that existed in v1 have been removed in v2. 29 | This is because the libraries that were relied on for prometheus-related functions are now too old. 30 | 31 | We decided to take this opportunity to focus on the minimum functionality in v2. 32 | If you need statistics, please provide them yourself. 33 | 34 | ## Getting Started 35 | 36 | ### Handler 37 | 38 | At first, you need to prepare your own `Handler` module with `use Riverside` line. 39 | 40 | in `handle_message/3`, process messages sent by client. 41 | This doesn't depend on some protocol like Socket.io. 42 | So do client-side, you don't need to prepared some libraries. 43 | 44 | ```elixir 45 | defmodule MySocketHandler do 46 | 47 | # set 'otp_app' param like Ecto.Repo 48 | use Riverside, otp_app: :my_app 49 | 50 | @impl Riverside 51 | def handle_message(msg, session, state) do 52 | 53 | # `msg` is a 'TEXT' or 'BINARY' frame sent by client, 54 | # process it as you like 55 | deliver_me(msg) 56 | 57 | {:ok, session, state} 58 | 59 | end 60 | 61 | end 62 | ``` 63 | 64 | ### Application child_spec 65 | 66 | And in your `Application` module, set child spec for your supervisor. 67 | 68 | ```elixir 69 | defmodule MyApp do 70 | 71 | use Application 72 | 73 | def start(_type, _args) do 74 | [ 75 | # ... 76 | {Riverside, [handler: MySocketHandler]} 77 | ] 78 | |> Supervisor.start_link([ 79 | strategy: :one_for_one, 80 | name: MyApp.Supervisor 81 | ]) 82 | end 83 | 84 | end 85 | ``` 86 | 87 | ### Configuration 88 | 89 | ```elixir 90 | config :my_app, MySocketHandler, 91 | port: 3000, 92 | path: "/my_ws", 93 | max_connections: 10000, # don't accept connections if server already has this number of connections 94 | max_connection_age: :infinity, # force to disconnect a connection if the duration passed. if :infinity is set, do nothing. 95 | idle_timeout: 120_000, # disconnect if no event comes on a connection during this duration 96 | reuse_port: false, # TCP SO_REUSEPORT flag 97 | show_debug_logs: false, 98 | transmission_limit: [ 99 | capacity: 50, # if 50 frames are sent on a connection 100 | duration: 2000 # in 2 seconds, disconnect it. 101 | ], 102 | cowboy_opts: [ 103 | #... 104 | ] 105 | ``` 106 | 107 | I’ll show you detailed description below. 108 | But you will know most of them when you see them. 109 | 110 | ### Run 111 | 112 | Launch your application, then the WebSocket service is provided with an endpoint like the following. 113 | 114 | ``` 115 | ws://localhost:3000/my_ws 116 | ``` 117 | 118 | And at the same time, we can also access to 119 | 120 | ``` 121 | http://localhost:3000/health 122 | ``` 123 | 124 | If you send a HTTP GET request to this URL, it returns response with status code 200, and text content "OK". 125 | This is just for health check. 126 | 127 | This feature is defined in a Plug Router named `Riverside.Router`, and this is configured as default `router` param for child spec. So, you can defined your own Plug Router if you set as below. 128 | 129 | **In your Application module** 130 | 131 | ```elixir 132 | defmodule MyApp do 133 | 134 | use Application 135 | 136 | def start(_type, _args) do 137 | [ 138 | # ... 139 | {Riverside, [ 140 | handler: MySocketHandler, 141 | router: MyRouter, # Set your Plug Router here 142 | ]} 143 | ] 144 | |> Supervisor.start_link([ 145 | strategy: :one_for_one, 146 | name: MyApp.Spervisor 147 | ]) 148 | end 149 | 150 | end 151 | ``` 152 | 153 | ## Handler's Callbacks 154 | 155 | You can also define callback functions other than `handle_message/3`. 156 | 157 | For instance, there are functions named `init`, `terminate`, and `handle_info`. 158 | If you are accustomed to GenServer, you can easily imagine what they are, 159 | though their interface is little bit different. 160 | 161 | ```elixir 162 | defmodule MySocketHandler do 163 | 164 | use Riverside, otp_app: :my_app 165 | 166 | @impl Riverside 167 | def init(session, state) do 168 | # initialization 169 | {:ok, session, state} 170 | end 171 | 172 | @impl Riverside 173 | def handle_message(msg, session, state) do 174 | deliver_me(msg) 175 | {:ok, session, state} 176 | 177 | end 178 | 179 | @impl Riverside 180 | def handle_info(into, session, state) do 181 | # handle message sent to this process 182 | {:ok, session, state} 183 | end 184 | 185 | @impl Riverside 186 | def terminate(reason, session, state) do 187 |   # cleanup 188 | :ok 189 | end 190 | 191 | end 192 | ``` 193 | ## Authentication and Session 194 | 195 | Here, I'll describe `authenticate/1` callback function. 196 | 197 | ```elixir 198 | defmodule MySocketHandler do 199 | 200 | use Riverside, otp_app: :my_app 201 | 202 | @impl Riverside 203 | def authenticate(req) do 204 | {username, password} = req.basic 205 | case MyAuthenticator.authenticate(username, password) do 206 | 207 | {:ok, user_id} -> 208 | state = %{} 209 | {:ok, user_id, state} 210 | 211 | {:error, :invalid_password} -> 212 | error = auth_error_with_code(401) 213 | {:error, error} 214 | end 215 | end 216 | 217 | @impl Riverside 218 | def init(session, state) do 219 | {:ok, session, state} 220 | end 221 | 222 | @impl Riverside 223 | def handle_message(msg, session, state) do 224 | deliver_me(msg) 225 | {:ok, session, state} 226 | 227 | end 228 | 229 | @impl Riverside 230 | def handle_info(into, session, state) do 231 | {:ok, session, state} 232 | end 233 | 234 | @impl Riverside 235 | def terminate(reason, session, state) do 236 | :ok 237 | end 238 | 239 | end 240 | ``` 241 | 242 | The argument of `authenticate/1` is a struct of `Riverside.AuthRequest.t`. 243 | And it has **Map** members 244 | 245 | - queries: Map includes HTTP request's query params 246 | - headers: Map includes HTTP headers 247 | 248 | ```elixir 249 | 250 | # When client access with a URL such like ws://localhost:3000/my_ws?token=FOOBAR, 251 | # And you want to authenticate the `token` parameter ("FOOBAR", this time) 252 | 253 | @impl Riverside 254 | def authenticate(req) do 255 | # You can pick the parameter like as below 256 | token = req.queries["token"] 257 | # ... 258 | end 259 | ``` 260 | 261 | ```elixir 262 | # Or else you want to authenticate with `Authorization` HTTP header. 263 | 264 | @impl Riverside 265 | def authenticate(req) do 266 | # You can pick the header value like as below 267 | auth_header = req.headers["authorization"] 268 | # ... 269 | end 270 | 271 | ``` 272 | 273 | The fact is that, you don't need to parse **Authorization** header by yourself, if you want to do **Basic** 274 | or **Bearer** authentication. 275 | 276 | ```elixir 277 | 278 | # Pick up `username` and `password` from `Basic` Authorization header. 279 | # If it doesn't exist, `username` and `password` become empty strings. 280 | 281 | @impl Riverside 282 | def authenticate(req) do 283 | {username, password} = req.basic 284 | # ... 285 | end 286 | 287 | ``` 288 | 289 | ```elixir 290 | # Pick up token value from `Bearer` Authorization header 291 | # If it doesn't exist, `token` become empty string. 292 | 293 | @impl Riverside 294 | def authenticate(req) do 295 | token = req.bearer_token 296 | # ... 297 | end 298 | ``` 299 | 300 | ### Authentication failure 301 | 302 | If authentication failure, you need to return `{:error, Riverside.AuthError.t}`. 303 | You can build Riverside.AuthError struct with `auth_error_with_code/1`. 304 | Pass proper HTTP status code. 305 | 306 | ```elixir 307 | @impl Riverside 308 | def authenticate(req) do 309 | 310 | token = req.bearer_token 311 | 312 | case MyAuth.authenticate(token) do 313 | 314 | {:error, :invalid_token} -> 315 | error = auth_error_with_code(401) 316 | {:error, error} 317 | 318 | # _ -> ... 319 | 320 | end 321 | 322 | end 323 | ``` 324 | 325 | You can use `put_auth_error_header/2` to put response header 326 | 327 | ```elixir 328 | error = auth_erro_with_code(400) 329 | |> puth_auth_error_header("WWW-Authenticate", "Basic realm=\"example.org\"") 330 | ``` 331 | 332 | And two more shortcuts, `put_auth_error_basic_header` and `put_auth_error_bearer_header`. 333 | 334 | ```elixir 335 | error = auth_erro_with_code(401) 336 | |> puth_auth_error_basic_header("example.org") 337 | 338 | # This puts `WWW-Authenticate: Basic realm="example.org"` 339 | ``` 340 | 341 | ```elixir 342 | error = auth_erro_with_code(401) 343 | |> puth_auth_error_bearer_header("example.org") 344 | 345 | # This puts `WWW-Authenticate: Bearer realm="example.org"` 346 | ``` 347 | 348 | ```elixir 349 | error = auth_erro_with_code(400) 350 | |> puth_auth_error_bearer_header("example.org", "invalid_token") 351 | 352 | # This puts `WWW-Authenticate: Bearer realm="example.org", error="invalid_token"` 353 | ``` 354 | ### Successful authentication 355 | 356 | ```elixir 357 | @impl Riverside 358 | def authenticate(req) do 359 | 360 | token = req.bearer_token 361 | 362 | case MyAuth.authenticate(token) do 363 | 364 | {:ok, user_id} -> 365 | session_id = create_random_string() 366 | state = %{} 367 | {:ok, user_id, session_id, state} 368 | 369 | # _ -> ... 370 | 371 | end 372 | end 373 | ``` 374 | 375 | If authentication results in success, return `{:ok, user_id, session_id, state}`. 376 | You can put any data into `state`, same as you do in `init` in GenServer. 377 | `session_id` should be random string. You also can return `{:ok, user_id, state}`, and 378 | Then `session_id` will be generated automatically. 379 | 380 | And `init/3` will be called after successful auth response. 381 | 382 | ### session 383 | 384 | Now I can describe about the `session` parameter included for each callback functions. 385 | 386 | This is a `Riverside.Session.t` struct, and it includes some parameters like `user_id` and `session_id`. 387 | 388 | When you omit to define `authenticate/1`, both `user_id` and `session_id` will be set random value. 389 | 390 | ```elixir 391 | @impl Riverside 392 | def handle_message(msg, session, state) do 393 | # session.user_id 394 | # session.session_id 395 | end 396 | ``` 397 | 398 | ## Message and Delivery 399 | 400 | ### Message Format 401 | 402 | If a client sends a simple TEXT frame with JSON format like the following 403 | 404 | ```javascript 405 | { 406 | "to": 1111, 407 | "body": "Hello" 408 | } 409 | ``` 410 | 411 | You can handle this JSON message as a **Map**. 412 | 413 | ```elixir 414 | @impl Riverside 415 | def handle_message(incoming_message, session, state) do 416 | 417 | dest_user_id = incoming_message["to"] 418 | body = incoming_message["body"] 419 | 420 | outgoing_message = %{ 421 | "from" => "#{session.user_id}", 422 | "body" => body, 423 | } 424 | 425 | deliver_user(dest_user_id, outgoing_message) 426 | 427 | {:ok, session, state} 428 | end 429 | ``` 430 | 431 | Then the user who is set as destination(user_id == 1111, in this example) 432 | receives TEXT frame 433 | 434 | ```javascript 435 | { 436 | "from": 2222, 437 | "body": "Hello" 438 | } 439 | ``` 440 | 441 | This is because `Riverside.Codec.JSON` is set for `codec` config as default. 442 | 443 | ```elixir 444 | config :my_app, MySocketHandler, 445 | codec: Riverside.Codec.JSON 446 | ``` 447 | 448 | This codec decodes incoming message, and encodes outgoing message. 449 | 450 | If you want to accept TEXT frames but don't want encode/decode them. 451 | Should set `Riverside.Codec.RawText` 452 | 453 | ```elixir 454 | config :my_app, MySocketHandler, 455 | codec: Riverside.Codec.RawText 456 | ``` 457 | 458 | If you want to accept BINARY frames but don't want encode/decode them. 459 | Should set `Riverside.Codec.RawBinary` 460 | 461 | 462 | ```elixir 463 | config :my_app, MySocketHandler, 464 | codec: Riverside.Codec.RawBinary 465 | ``` 466 | 467 | #### Custom Codec 468 | 469 | The fact is that, JSON codec module is written with small amount of code. 470 | Take a look at the inside. 471 | 472 | ```elixir 473 | defmodule Riverside.Codec.JSON do 474 | 475 | @behaviour Riverside.Codec 476 | 477 | @impl Riverside.Codec 478 | def frame_type do 479 | :text 480 | end 481 | 482 | @impl Riverside.Codec 483 | def encode(msg) do 484 | case Poison.encode(msg) do 485 | 486 | {:ok, value} -> 487 | {:ok, value} 488 | 489 | {:error, _exception} -> 490 | {:error, :invalid_message} 491 | 492 | end 493 | end 494 | 495 | @impl Riverside.Codec 496 | def decode(data) do 497 | case Poison.decode(data) do 498 | 499 | {:ok, value} -> 500 | {:ok, value} 501 | 502 | {:error, _exception} -> 503 | {:error, :invalid_message} 504 | 505 | end 506 | end 507 | 508 | end 509 | ``` 510 | 511 | No explanation needed to write your own codec. 512 | It's too simple. 513 | 514 | ### Delivery 515 | 516 | There is a module named `Riverside.LocalDelivery`. 517 | With its `deliver/2` function, you can deliver messages to 518 | sessions connected to the server. 519 | 520 | ```elixir 521 | def handle_message(msg, session, state) do 522 | 523 | dest_user_id = msg["to"] 524 | body = msg["body"] 525 | 526 | outgoing = %{ 527 | from: session.user_id, 528 | body: body, 529 | } 530 | 531 |  Riverside.LocalDelivery.deliver( 532 | {:user, dest_user_id}, 533 | {:text, Poison.encode!(outgoing)} 534 | ) 535 | 536 | {:ok, session, state} 537 | end 538 | ``` 539 | 540 | First argument is a tuple which represents a **destination**, 541 | and second is a tuple which represents a **frame**. 542 | 543 | **frame** should be `{:text, body}` or `{:binary, body}`. choose proper one. 544 | 545 | OK, let's describe about 3 kinds of destination. 546 | 547 | #### **USER DESTINATION** 548 | 549 | ```elixir 550 | {:user, user_id} 551 | ``` 552 | 553 | Send message to all the connections for this user. 554 | 555 | Recent trend is `multi device` support. 556 | One single user may have a multi connections at the same time. 557 | 558 | #### **SESSION DESTINATION** 559 | 560 | ```elixir 561 | {:session, user_id, session_id} 562 | ``` 563 | 564 | Send message to a specific connection for this user. 565 | 566 | Sometime, this may be a very important feature. 567 | For instance, **WebRTC-signaling**, **end-to-end encryption**. 568 | 569 | #### **CHANNEL DESTINATION** 570 | 571 | ```elixir 572 | {:channel, channel_id} 573 | ``` 574 | 575 | Send message to all the members who is belonging to this channel. 576 | 577 | How to join or leave channels? See the example below. 578 | 579 | ```elixir 580 | def init(session, state) do 581 | Riverside.LocalDelivery.join_channel("my_channel") 582 | {:ok, session, state} 583 | end 584 | 585 | def handle_message(msg, session, state) do 586 | dest_channel_id = msg["to"] 587 | body = msg["body"] 588 | 589 | outgoing = %{ 590 | from: session.user_id, 591 | body: body, 592 | } 593 | 594 |  Riverside.LocalDelivery.deliver( 595 | {:channel, dest_channel_id}, 596 | {:text, Poison.encode!(outgoing)} 597 | ) 598 | {:ok, session, state} 599 | end 600 | 601 | def terminate(session, state) do 602 | Riverside.LocalDelivery.leave_channel("my_channel") 603 | :ok 604 | end 605 | ``` 606 | 607 | #### Shortcuts for delivery 608 | 609 | If you want to deliver messages from within your handler, 610 | You don't need to use `Riverside.LocalDelivery` directly. 611 | 612 | Here are handy functions. 613 | 614 | Let's replace LocalDelivery module to handy version. 615 | 616 | ```elixir 617 | def init(session, state) do 618 | join_channel("my_channel") 619 | {:ok, session, state} 620 | end 621 | 622 | def handle_message(msg, session, state) do 623 | dest_channel_id = msg["to"] 624 | body = msg["body"] 625 | 626 | outgoing = %{ 627 | from: session.user_id, 628 | body: body, 629 | } 630 | # same as LocalDelivery.deliver 631 | # deliver({:channel, dest_channel_id}, {:text, Poison.encode!(outgoing)}) 632 | 633 | # handy version, `codec` works on this way, so you don't need to encode by yourself. 634 |  deliver_channel(dest_channel_id, outgoing) 635 | 636 | # If you want to send message to `user` 637 | # deliver_user(dest_user_id, outgoing) 638 | 639 | # If you want to send message to `session` 640 | # deliver_session(dest_user_id, dest_user_session_id, outgoing) 641 | 642 | {:ok, session, state} 643 | end 644 | 645 | def terminate(session, state) do 646 | leave_channel("my_channel") 647 | :ok 648 | end 649 | ``` 650 | 651 | #### Echo Back 652 | 653 | To deliver message to sender's connection, you can write like following. 654 | 655 | ```elixir 656 | deliver_me(msg) 657 | ``` 658 | 659 | This is same as 660 | 661 | ```elixir 662 | deliver_session(session.user_id, session.session_id, msg) 663 | ``` 664 | 665 | #### Close 666 | 667 | Following like can deliver `close` message to specific connection. 668 | 669 | ```elixir 670 | Riverside.LocalDelivery.close(user_id, session_id) 671 | ``` 672 | 673 | or just `close` function. 674 | 675 | ```elixir 676 | close() 677 | ``` 678 | 679 | Example 680 | 681 | ```elixir 682 | def handle_message(msg, session, state) do 683 | 684 | if is_bad_message(msg) do 685 | close() 686 | else 687 | # ... 688 | end 689 | 690 | {:ok, session, state} 691 | end 692 | ``` 693 | 694 | ### Scalable Service 695 | 696 | `LocalDelivery` module and its handy shortcuts are just for **local**. 697 | This works only for communications in a single server. 698 | 699 | If you need to support more scalable service, consider other solutions. 700 | For example, Redis-PubSub, RabbitMQ, or gnatsd. 701 | 702 | Here is a example with https://github.com/lyokato/roulette 703 | (HashRing-ed gnatsd cluster client) 704 | 705 | ```elixir 706 | def init(session, state) do 707 | with {:ok, _} <- Roulette.sub("user:#{session.user_id}"), 708 | {:ok, _} <- Roulette.sub("session:#{session.user_id}/#{session.session_id}") do 709 | {:ok, session, state} 710 | else 711 | error -> 712 | Logger.wran "failed to setup subscription: #{inspect error}" 713 | {:error, :system_error} 714 | end 715 | end 716 | 717 | def handle_message(msg, session, state) do 718 | 719 | to = msg["to"] 720 | body = msg["body"] 721 | 722 | outgoing = %{ 723 | from: session.user_id, 724 | body: body, 725 | } 726 | 727 | case Roulette.pub("user:#{to}", Poison.encode!(outgoing)) do 728 | :ok -> {:ok, session, state} 729 | :error -> {:error, :system_error} 730 | end 731 | 732 | end 733 | 734 | def handle_info(:pubsub_message, topic, msg, pid}, session, state) do 735 | deliver_me(:text, msg) 736 | {:ok, session, state} 737 | end 738 | 739 | def terminate(session, state) do 740 | :ok 741 | end 742 | ``` 743 | 744 | ## Configurations 745 | 746 | ### child_spec 747 | 748 | ```elixir 749 | {Riverside, [ 750 | handler: MySocketHandler, 751 | router: MyRouter, 752 | ]} 753 | ``` 754 | 755 | |keyword|default value|description| 756 | |:--|:--|:--| 757 | |handler|--|Required. Set your own handler module.| 758 | |router|Riverside.Router|Plug.Router implementation module which provides endpoints other than **ws(s)://**| 759 | 760 | #### config file 761 | 762 | ```elixir 763 | config :my_app, MySocketHandler, 764 | port: 3000, 765 | path: "/my_ws", 766 | codec: Riverside.Codec.RawBinary, 767 | max_connections: 10000, 768 | max_connection_age: :infinity, 769 | show_debug_logs: false, 770 | idle_timeout: 120_000, 771 | reuse_port: false, 772 | transmission_limit: [ 773 | duration: 2000, 774 | capacity: 50 775 | ], 776 | tls: true, 777 | tls_certfile: "path/to/certfile", 778 | tls_keyfile: "path/to/keyfile" 779 | ``` 780 | 781 | |key|default value|description| 782 | |:--|:--|:--| 783 | |port|3000|Port number this http server listens.| 784 | |path|/|Path for WebSocket endpoint.| 785 | |max_connections|65536|maximum number of connections this server can keep. you also pay attention to a configuration for a number of OS's file descriptors| 786 | |max_connection_age|:infinity|Force to disconnect a connection if the duration(milliseconds) passed. Then `terminate/3` will be called with **:over_age** as a reason. if **:infinity** is set, do nothing.| 787 | |codec|Riverside.Codec.JSON|text/binary frame codec.| 788 | |show_debug_logs|false|If this flag is true. detailed debug logs will be shown.| 789 | |transmission_limit|duration:2000, capacity:50| if <:capacity> frames are sent on a connection in <:duration> milliseconds, disconnect it.Then `terminate/3` will be called with **:too_many_messages** as a reason.| 790 | |idle_timeout|60000|Disconnect if no event comes on a connection during this duration| 791 | |reuse_port|false|TCP **SO_REUSEPORT** flag| 792 | |tls|false|use TLS or not. If you set this flag, then you must also set the two parameters tls_certfile and tls_keyfile.| 793 | |tls_certfile|""|path to cert file for TLS| 794 | |tls_keyfile|""|path to private-key file for TLS| 795 | |cowboy_opts|[]| Set extra options for [cowboy](https://ninenines.eu/docs/en/cowboy/2.5/manual/cowboy_http/)| 796 | 797 | #### Dynamic Port Number 798 | 799 | You may set port number dinamically. 800 | 801 | You can set port number like following. 802 | 803 | 804 | ```elixir 805 | config :my_app, MySocketHandler, 806 | port: {:system, "MY_PORT", 3000} 807 | ``` 808 | 809 | Then, port number is picked from runtime environment variable "MY_PORT". 810 | if it doesn't exist, 3000 will be used. 811 | 812 | #### Dynamic TLS settings 813 | 814 | TLS-related settings can also be obtained from environment variables in the same way. 815 | 816 | ```elixir 817 | config :my_app, MySocketHandler, 818 | tls: {:system, "USE_TLS", true}, 819 | tls_certfile: {:system, "MY_TLS_CERT", "/default/path/to/cert"}, 820 | tls_keyfile: {:system, "MY_TLS_PRIVKEY", "/default/path/to/key"}, 821 | ``` 822 | 823 | ## LICENSE 824 | 825 | MIT-LICENSE 826 | 827 | ## Author 828 | 829 | Lyo Kaot 830 | -------------------------------------------------------------------------------- /lib/riverside.ex: -------------------------------------------------------------------------------- 1 | defmodule Riverside do 2 | @moduledoc ~S""" 3 | 4 | # Riverside - Plain WebSocket Server Framework for Elixir 5 | 6 | ## Getting Started 7 | 8 | ### Handler 9 | 10 | At first, you need to prepare your own `Handler` module with `use Riverside` line. 11 | 12 | in `handle_message/3`, process messages sent by client. 13 | This doesn't depend on some protocol like Socket.io. 14 | So do client-side, you don't need to prepared some libraries. 15 | 16 | ```elixir 17 | defmodule MySocketHandler do 18 | 19 | # set 'otp_app' param like Ecto.Repo 20 | use Riverside, otp_app: :my_app 21 | 22 | @impl Riverside 23 | def handle_message(msg, session, state) do 24 | 25 | # `msg` is a 'TEXT' or 'BINARY' frame sent by client, 26 | # process it as you like 27 | deliver_me(msg) 28 | 29 | {:ok, session, state} 30 | 31 | end 32 | 33 | end 34 | ``` 35 | 36 | ### Application child_spec 37 | 38 | And in your `Application` module, set child spec for your supervisor. 39 | 40 | ```elixir 41 | defmodule MyApp do 42 | 43 | use Application 44 | 45 | def start(_type, _args) do 46 | [ 47 | # ... 48 | {Riverside, [handler: MySocketHandler]} 49 | ] 50 | |> Supervisor.start_link([ 51 | strategy: :one_for_one, 52 | name: MyApp.Spervisor 53 | ]) 54 | end 55 | 56 | end 57 | ``` 58 | 59 | ### Configuration 60 | 61 | ```elixir 62 | config :my_app, MySocketHandler, 63 | port: 3000, 64 | path: "/my_ws", 65 | max_connections: 10000, # don't accept connections if server already has this number of connections 66 | max_connection_age: :infinity, # force to disconnect a connection if the duration passed. if :infinity is set, do nothing. 67 | idle_timeout: 120_000, # disconnect if no event comes on a connection during this duration 68 | reuse_port: false, # TCP SO_REUSEPORT flag 69 | show_debug_logs: false, 70 | transmission_limit: [ 71 | capacity: 50, # if 50 frames are sent on a connection 72 | duration: 2000 # in 2 seconds, disconnect it. 73 | ] 74 | ``` 75 | 76 | I’ll show you detailed description below. 77 | But you will know most of them when you see them. 78 | 79 | ### Run 80 | 81 | Launch your application, then the WebSocket service is provided with an endpoint like the following. 82 | 83 | ``` 84 | ws://localhost:3000/my_ws 85 | ``` 86 | 87 | And at the same time, we can also access to 88 | 89 | ``` 90 | http://localhost:3000/health 91 | ``` 92 | 93 | If you send a HTTP GET request to this URL, it returns response with status code 200, and text content "OK". 94 | This is just for health check. 95 | 96 | This feature is defined in a Plug Router named `Riverside.Router`, and this is configured as default `router` param for child spec. So, you can defined your own Plug Router if you set as below. 97 | 98 | **In your Application module** 99 | 100 | ```elixir 101 | defmodule MyApp do 102 | 103 | use Application 104 | 105 | def start(_type, _args) do 106 | [ 107 | # ... 108 | {Riverside, [ 109 | handler: MySocketHandler, 110 | router: MyRouter, # Set your Plug Router here 111 | ]} 112 | ] 113 | |> Supervisor.start_link([ 114 | strategy: :one_for_one, 115 | name: MyApp.Spervisor 116 | ]) 117 | end 118 | 119 | end 120 | ``` 121 | 122 | ## Handler's Callbacks 123 | 124 | You can also define callback functions other than `handle_message/3`. 125 | 126 | For instance, there are functions named `init`, `terminate`, and `handle_info`. 127 | If you are accustomed to GenServer, you can easily imagine what they are, 128 | though their interface is little bit different. 129 | 130 | ```elixir 131 | defmodule MySocketHandler do 132 | 133 | use Riverside, otp_app: :my_app 134 | 135 | @impl Riverside 136 | def init(session, state) do 137 | # initialization 138 | {:ok, session, state} 139 | end 140 | 141 | @impl Riverside 142 | def handle_message(msg, session, state) do 143 | deliver_me(msg) 144 | {:ok, session, state} 145 | 146 | end 147 | 148 | @impl Riverside 149 | def handle_info(into, session, state) do 150 | # handle message sent to this process 151 | {:ok, session, state} 152 | end 153 | 154 | @impl Riverside 155 | def terminate(reason, session, state) do 156 |   # cleanup 157 | :ok 158 | end 159 | 160 | end 161 | ``` 162 | ## Authentication and Session 163 | 164 | Here, I'll describe `authenticate/1` callback function. 165 | 166 | ```elixir 167 | defmodule MySocketHandler do 168 | 169 | use Riverside, otp_app: :my_app 170 | 171 | @impl Riverside 172 | def authenticate(req) do 173 | {username, password} = req.basic 174 | case MyAuthenticator.authenticate(username, password) do 175 | 176 | {:ok, user_id} -> 177 | state = %{} 178 | {:ok, user_id, state} 179 | 180 | {:error, :invalid_password} -> 181 | error = auth_error_with_code(401) 182 | {:error, error} 183 | end 184 | end 185 | 186 | @impl Riverside 187 | def init(session, state) do 188 | {:ok, session, state} 189 | end 190 | 191 | @impl Riverside 192 | def handle_message(msg, session, state) do 193 | deliver_me(msg) 194 | {:ok, session, state} 195 | 196 | end 197 | 198 | @impl Riverside 199 | def handle_info(into, session, state) do 200 | {:ok, session, state} 201 | end 202 | 203 | @impl Riverside 204 | def terminate(reason, session, state) do 205 | :ok 206 | end 207 | 208 | end 209 | ``` 210 | 211 | The argument of `authenticate/1` is a struct of `Riverside.AuthRequest.t`. 212 | And it has **Map** members 213 | 214 | - queries: Map includes HTTP request's query params 215 | - headers: Map includes HTTP headers 216 | 217 | ```elixir 218 | 219 | # When client access with a URL such like ws://localhost:3000/my_ws?token=FOOBAR, 220 | # And you want to authenticate the `token` parameter ("FOOBAR", this time) 221 | 222 | @impl Riverside 223 | def authenticate(req) do 224 | # You can pick the parameter like as below 225 | token = req.queries["token"] 226 | # ... 227 | end 228 | ``` 229 | 230 | ```elixir 231 | # Or else you want to authenticate with `Authorization` HTTP header. 232 | 233 | @impl Riverside 234 | def authenticate(req) do 235 | # You can pick the header value like as below 236 | auth_header = req.headers["authorization"] 237 | # ... 238 | end 239 | 240 | ``` 241 | 242 | The fact is that, you don't need to parse **Authorization** header by yourself, if you want to do **Basic** 243 | or **Bearer** authentication. 244 | 245 | ```elixir 246 | 247 | # Pick up `username` and `password` from `Basic` Authorization header. 248 | # If it doesn't exist, `username` and `password` become empty strings. 249 | 250 | @impl Riverside 251 | def authenticate(req) do 252 | {username, password} = req.basic 253 | # ... 254 | end 255 | 256 | ``` 257 | 258 | ```elixir 259 | # Pick up token value from `Bearer` Authorization header 260 | # If it doesn't exist, `token` become empty string. 261 | 262 | @impl Riverside 263 | def authenticate(req) do 264 | token = req.bearer_token 265 | # ... 266 | end 267 | ``` 268 | 269 | ### Authentication failure 270 | 271 | If authentication failure, you need to return `{:error, Riverside.AuthError.t}`. 272 | You can build Riverside.AuthError struct with `auth_error_with_code/1`. 273 | Pass proper HTTP status code. 274 | 275 | ```elixir 276 | @impl Riverside 277 | def authenticate(req) do 278 | 279 | token = req.bearer_token 280 | 281 | case MyAuth.authenticate(token) do 282 | 283 | {:error, :invalid_token} -> 284 | error = auth_error_with_code(401) 285 | {:error, error} 286 | 287 | # _ -> ... 288 | 289 | end 290 | 291 | end 292 | ``` 293 | 294 | You can use `put_auth_error_header/2` to put response header 295 | 296 | ```elixir 297 | error = auth_erro_with_code(400) 298 | |> puth_auth_error_header("WWW-Authenticate", "Basic realm=\"example.org\"") 299 | ``` 300 | 301 | And two more shortcuts, `put_auth_error_basic_header` and `put_auth_error_bearer_header`. 302 | 303 | ```elixir 304 | error = auth_erro_with_code(401) 305 | |> puth_auth_error_basic_header("example.org") 306 | 307 | # This puts `WWW-Authenticate: Basic realm="example.org"` 308 | ``` 309 | 310 | ```elixir 311 | error = auth_erro_with_code(401) 312 | |> puth_auth_error_bearer_header("example.org") 313 | 314 | # This puts `WWW-Authenticate: Bearer realm="example.org"` 315 | ``` 316 | 317 | ```elixir 318 | error = auth_erro_with_code(400) 319 | |> puth_auth_error_bearer_header("example.org", "invalid_token") 320 | 321 | # This puts `WWW-Authenticate: Bearer realm="example.org", error="invalid_token"` 322 | ``` 323 | ### Successful authentication 324 | 325 | ```elixir 326 | @impl Riverside 327 | def authenticate(req) do 328 | 329 | token = req.bearer_token 330 | 331 | case MyAuth.authenticate(token) do 332 | 333 | {:ok, user_id} -> 334 | session_id = create_random_string() 335 | state = %{} 336 | {:ok, user_id, session_id, state} 337 | 338 | # _ -> ... 339 | 340 | end 341 | end 342 | ``` 343 | 344 | If authentication results in success, return `{:ok, user_id, session_id, state}`. 345 | You can put any data into `state`, same as you do in `init` in GenServer. 346 | `session_id` should be random string. You also can return `{:ok, user_id, state}`, and 347 | Then `session_id` will be generated automatically. 348 | 349 | And `init/3` will be called after successful auth response. 350 | 351 | ### session 352 | 353 | Now I can describe about the `session` parameter included for each callback functions. 354 | 355 | This is a `Riverside.Session.t` struct, and it includes some parameters like `user_id` and `session_id`. 356 | 357 | When you omit to define `authenticate/1`, both `user_id` and `session_id` will be set random value. 358 | 359 | ```elixir 360 | @impl Riverside 361 | def handle_message(msg, session, state) do 362 | # session.user_id 363 | # session.session_id 364 | end 365 | ``` 366 | 367 | ## Message and Delivery 368 | 369 | ### Message Format 370 | 371 | If a client sends a simple TEXT frame with JSON format like the following 372 | 373 | ```javascript 374 | { 375 | "to": 1111, 376 | "body": "Hello" 377 | } 378 | ``` 379 | 380 | You can handle this JSON message as a **Map**. 381 | 382 | ```elixir 383 | @impl Riverside 384 | def handle_message(incoming_message, session, state) do 385 | 386 | dest_user_id = incoming_message["to"] 387 | body = incoming_message["body"] 388 | 389 | outgoing_message = %{ 390 | "from" => "#{session.user_id}", 391 | "body" => body, 392 | } 393 | 394 | deliver_user(dest_user_id, outgoing_message) 395 | 396 | {:ok, session, state} 397 | end 398 | ``` 399 | 400 | Then the user who is set as destination(user_id == 1111, in this example) 401 | receives TEXT frame 402 | 403 | ```javascript 404 | { 405 | "from": 2222, 406 | "body": "Hello" 407 | } 408 | ``` 409 | 410 | This is because `Riverside.Codec.JSON` is set for `codec` config as default. 411 | 412 | ```elixir 413 | config :my_app, MySocketHandler, 414 | codec: Riverside.Codec.JSON 415 | ``` 416 | 417 | This codec decodes incoming message, and encodes outgoing message. 418 | 419 | If you want to accept TEXT frames but don't want encode/decode them. 420 | Should set `Riverside.Codec.RawText` 421 | 422 | ```elixir 423 | config :my_app, MySocketHandler, 424 | codec: Riverside.Codec.RawText 425 | ``` 426 | 427 | If you want to accept BINARY frames but don't want encode/decode them. 428 | Should set `Riverside.Codec.RawBinary` 429 | 430 | 431 | ```elixir 432 | config :my_app, MySocketHandler, 433 | codec: Riverside.Codec.RawBinary 434 | ``` 435 | 436 | #### Custom Codec 437 | 438 | The fact is that, JSON codec module is written with small amount of code. 439 | Take a look at the inside. 440 | 441 | ```elixir 442 | defmodule Riverside.Codec.JSON do 443 | 444 | @behaviour Riverside.Codec 445 | 446 | @impl Riverside.Codec 447 | def frame_type do 448 | :text 449 | end 450 | 451 | @impl Riverside.Codec 452 | def encode(msg) do 453 | case Poison.encode(msg) do 454 | 455 | {:ok, value} -> 456 | {:ok, value} 457 | 458 | {:error, _exception} -> 459 | {:error, :invalid_message} 460 | 461 | end 462 | end 463 | 464 | @impl Riverside.Codec 465 | def decode(data) do 466 | case Poison.decode(data) do 467 | 468 | {:ok, value} -> 469 | {:ok, value} 470 | 471 | {:error, _exception} -> 472 | {:error, :invalid_message} 473 | 474 | end 475 | end 476 | 477 | end 478 | ``` 479 | 480 | No explanation needed to write your own codec. 481 | It's too simple. 482 | 483 | ### Delivery 484 | 485 | There is a module named `Riverside.LocalDelivery`. 486 | With its `deliver/2` function, you can deliver messages to 487 | sessions connected to the server. 488 | 489 | ```elixir 490 | def handle_message(msg, session, state) do 491 | 492 | dest_user_id = msg["to"] 493 | body = msg["body"] 494 | 495 | outgoing = %{ 496 | from: session.user_id, 497 | body: body, 498 | } 499 | 500 |  Riverside.LocalDelivery.deliver( 501 | {:user, dest_user_id}, 502 | {:text, Poison.encode!(outgoing)} 503 | ) 504 | 505 | {:ok, session, state} 506 | end 507 | ``` 508 | 509 | First argument is a tuple which represents a **destination**, 510 | and second is a tuple which represents a **frame**. 511 | 512 | **frame** should be `{:text, body}` or `{:binary, body}`. choose proper one. 513 | 514 | OK, let's describe about 3 kinds of destination. 515 | 516 | #### **USER DESTINATION** 517 | 518 | ```elixir 519 | {:user, user_id} 520 | ``` 521 | 522 | Send message to all the connections for this user. 523 | 524 | Recent trend is `multi device` support. 525 | One single user may have a multi connections at the same time. 526 | 527 | #### **SESSION DESTINATION** 528 | 529 | ```elixir 530 | {:session, user_id, session_id} 531 | ``` 532 | 533 | Send message to a specific connection for this user. 534 | 535 | Sometime, this may be a very important feature. 536 | For instance, **WebRTC-signaling**, **end-to-end encryption**. 537 | 538 | #### **CHANNEL DESTINATION** 539 | 540 | ```elixir 541 | {:channel, channel_id} 542 | ``` 543 | 544 | Send message to all the members who is belonging to this channel. 545 | 546 | How to join or leave channels? See the example below. 547 | 548 | ```elixir 549 | def init(session, state) do 550 | Riverside.LocalDelivery.join_channel("my_channel") 551 | {:ok, session, state} 552 | end 553 | 554 | def handle_message(msg, session, state) do 555 | dest_channel_id = msg["to"] 556 | body = msg["body"] 557 | 558 | outgoing = %{ 559 | from: session.user_id, 560 | body: body, 561 | } 562 | 563 |  Riverside.LocalDelivery.deliver( 564 | {:channel, dest_channel_id}, 565 | {:text, Poison.encode!(outgoing)} 566 | ) 567 | {:ok, session, state} 568 | end 569 | 570 | def terminate(session, state) do 571 | Riverside.LocalDelivery.leave_channel("my_channel") 572 | :ok 573 | end 574 | ``` 575 | 576 | #### Shortcuts for delivery 577 | 578 | If you want to deliver messages from within your handler, 579 | You don't need to use `Riverside.LocalDelivery` directly. 580 | 581 | Here are handy functions. 582 | 583 | Let's replace LocalDelivery module to handy version. 584 | 585 | ```elixir 586 | def init(session, state) do 587 | join_channel("my_channel") 588 | {:ok, session, state} 589 | end 590 | 591 | def handle_message(msg, session, state) do 592 | dest_channel_id = msg["to"] 593 | body = msg["body"] 594 | 595 | outgoing = %{ 596 | from: session.user_id, 597 | body: body, 598 | } 599 | # same as LocalDelivery.deliver 600 | # deliver({:channel, dest_channel_id}, {:text, Poison.encode!(outgoing)}) 601 | 602 | # handy version, `codec` works on this way, so you don't need to encode by yourself. 603 |  deliver_channel(dest_channel_id, outgoing) 604 | 605 | # If you want to send message to `user` 606 | # deliver_user(dest_user_id, outgoing) 607 | 608 | # If you want to send message to `session` 609 | # deliver_session(dest_user_id, dest_user_session_id, outgoing) 610 | 611 | {:ok, session, state} 612 | end 613 | 614 | def terminate(session, state) do 615 | leave_channel("my_channel") 616 | :ok 617 | end 618 | ``` 619 | 620 | #### Echo Back 621 | 622 | To deliver message to sender's connection, you can write like following. 623 | 624 | ```elixir 625 | deliver_me(msg) 626 | ``` 627 | 628 | This is same as 629 | 630 | ```elixir 631 | deliver_session(session.user_id, session.session_id, msg) 632 | ``` 633 | 634 | #### Close 635 | 636 | Following like can deliver `close` message to specific connection. 637 | 638 | ```elixir 639 | Riverside.LocalDelivery.close(user_id, session_id) 640 | ``` 641 | 642 | or just `close` function. 643 | 644 | ```elixir 645 | close() 646 | ``` 647 | 648 | Example 649 | 650 | ```elixir 651 | def handle_message(msg, session, state) do 652 | 653 | if is_bad_message(msg) do 654 | close() 655 | else 656 | # ... 657 | end 658 | 659 | {:ok, session, state} 660 | end 661 | ``` 662 | 663 | ### Scalable Service 664 | 665 | `LocalDelivery` module and its handy shortcuts are just for **local**. 666 | This works only for communications in a single server. 667 | 668 | If you need to support more scalable service, consider other solutions. 669 | For example, Redis-PubSub, RabbitMQ, or gnatsd. 670 | 671 | Here is a example with https://github.com/lyokato/roulette 672 | (HashRing-ed gnatsd cluster client) 673 | 674 | ```elixir 675 | def init(session, state) do 676 | with {:ok, _} <- Roulette.sub("user:#{session.user_id}"), 677 | {:ok, _} <- Roulette.sub("session:#{session.user_id}/#{session.session_id}") do 678 | {:ok, session, state} 679 | else 680 | error -> 681 | Logger.wran "failed to setup subscription: #{inspect error}" 682 | {:error, :system_error} 683 | end 684 | end 685 | 686 | def handle_message(msg, session, state) do 687 | 688 | to = msg["to"] 689 | body = msg["body"] 690 | 691 | outgoing = %{ 692 | from: session.user_id, 693 | body: body, 694 | } 695 | 696 | case Roulette.pub("user:#{to}", Poison.encode!(outgoing)) do 697 | :ok -> {:ok, session, state} 698 | :error -> {:error, :system_error} 699 | end 700 | 701 | end 702 | 703 | def handle_info(:pubsub_message, topic, msg, pid}, session, state) do 704 | deliver_me(:text, msg) 705 | {:ok, session, state} 706 | end 707 | 708 | def terminate(session, state) do 709 | :ok 710 | end 711 | ``` 712 | 713 | ## Configurations 714 | 715 | ### child_spec 716 | 717 | ```elixir 718 | {Riverside, [ 719 | handler: MySocketHandler, 720 | router: MyRouter, 721 | ]} 722 | ``` 723 | 724 | |keyword|default value|description| 725 | |:--|:--|:--| 726 | |handler|--|Required. Set your own handler module.| 727 | |router|Riverside.Router|Plug.Router implementation module which provides endpoints other than **ws(s)://**| 728 | 729 | #### config file 730 | 731 | ```elixir 732 | config :my_app, MySocketHandler, 733 | port: 3000, 734 | path: "/my_ws", 735 | codec: Riverside.Codec.RawBinary, 736 | max_connections: 10000, 737 | max_connection_age: :infinity, 738 | show_debug_logs: false, 739 | idle_timeout: 120_000, 740 | reuse_port: false, 741 | transmission_limit: [ 742 | duration: 2000, 743 | capacity: 50 744 | ] 745 | ``` 746 | 747 | |key|default value|description| 748 | |:--|:--|:--| 749 | |port|3000|Port number this http server listens.| 750 | |path|/|Path for WebSocket endpoint.| 751 | |max_connections|65536|maximum number of connections this server can keep. you also pay attention to a configuration for a number of OS's file descriptors| 752 | |max_connection_age|:infinity|Force to disconnect a connection if the duration(milliseconds) passed. Then `terminate/3` will be called with **:over_age** as a reason. if **:infinity** is set, do nothing.| 753 | |codec|Riverside.Codec.JSON|text/binary frame codec.| 754 | |show_debug_logs|false|If this flag is true. detailed debug logs will be shown.| 755 | |transmission_limit|duration:2000, capacity:50| if <:capacity> frames are sent on a connection in <:duration> milliseconds, disconnect it.Then `terminate/3` will be called with **:too_many_messages** as a reason.| 756 | |idle_timeout|60000|Disconnect if no event comes on a connection during this duration| 757 | |reuse_port|false|TCP **SO_REUSEPORT** flag| 758 | 759 | #### Dynamic Port Number 760 | 761 | You may set port number dinamically. 762 | 763 | You can set port number like following. 764 | 765 | 766 | ```elixir 767 | config :my_app, MySocketHandler, 768 | port: {:system, "MY_PORT", 3000} 769 | ``` 770 | 771 | Then, port number is picked from runtime environment variable "MY_PORT". 772 | if it doesn't exist, 3000 will be used. 773 | 774 | """ 775 | 776 | alias Riverside.AuthRequest 777 | alias Riverside.Session 778 | 779 | @type terminate_reason :: 780 | {:normal, :shutdown | :timeout} 781 | | {:remote, :closed} 782 | | {:remote, :cow_ws.close_code(), binary} 783 | | {:error, :badencoding | :badframe | :closed | :too_many_massages | :over_age | atom} 784 | 785 | @callback __handle_authentication__(req :: AuthRequest.t()) :: 786 | {:ok, Session.user_id(), any} 787 | | {:ok, Session.user_id(), Session.session_id(), any} 788 | | {:error, Riverside.AuthError.t()} 789 | 790 | @callback __config__() :: map 791 | 792 | @callback __handle_data__( 793 | frame_type :: Riverside.Codec.frame_type(), 794 | message :: binary, 795 | session :: Session.t(), 796 | state :: any 797 | ) :: 798 | {:ok, Session.t()} 799 | | {:error, :invalid_message | :unsupported} 800 | 801 | @callback authenticate(req :: AuthRequest.t()) :: 802 | {:ok, Session.user_id(), any} 803 | | {:ok, Session.user_id(), Session.session_id(), any} 804 | | {:error, Riverside.AuthError.t()} 805 | 806 | @callback init(session :: Session.t(), state :: any) :: 807 | {:ok, Session.t(), any} 808 | | {:error, any} 809 | 810 | @callback handle_message( 811 | message :: any, 812 | session :: Session.t(), 813 | state :: any 814 | ) :: {:ok, Session.t(), any} | {:stop, atom, any} 815 | 816 | @callback handle_info( 817 | info :: any, 818 | session :: Session.t(), 819 | state :: any 820 | ) :: {:ok, Session.t(), any} | {:stop, atom, any} 821 | 822 | @callback terminate( 823 | reason :: terminate_reason, 824 | session :: Session.t(), 825 | state :: any 826 | ) :: :ok 827 | 828 | defmacro __using__(opts \\ []) do 829 | quote location: :keep, bind_quoted: [opts: opts] do 830 | require Logger 831 | 832 | @behaviour Riverside 833 | 834 | @riverside_config Riverside.Config.load(__MODULE__, opts) 835 | 836 | import Riverside.LocalDelivery, 837 | only: [ 838 | join_channel: 1, 839 | leave_channel: 1 840 | ] 841 | 842 | import Riverside.AuthError, 843 | only: [ 844 | auth_error_with_code: 1, 845 | put_auth_error_header: 3, 846 | put_auth_error_basic_header: 2, 847 | put_auth_error_bearer_header: 2, 848 | put_auth_error_bearer_header: 3 849 | ] 850 | 851 | import Riverside.Session, only: [trap_exit: 2] 852 | 853 | @impl Riverside 854 | def __config__, do: @riverside_config 855 | 856 | @impl Riverside 857 | def __handle_authentication__(req) do 858 | authenticate(req) 859 | end 860 | 861 | @impl Riverside 862 | def __handle_data__(frame_type, data, session, state) do 863 | if @riverside_config.codec.frame_type() === frame_type do 864 | case @riverside_config.codec.decode(data) do 865 | {:ok, message} -> 866 | handle_message(message, session, state) 867 | 868 | {:error, _reason} -> 869 | {:error, :invalid_message} 870 | end 871 | else 872 | if @riverside_config.show_debug_logs do 873 | Logger.debug( 874 | "(#{session}) unsupported frame type: #{frame_type}" 875 | ) 876 | end 877 | 878 | {:error, :unsupported} 879 | end 880 | end 881 | 882 | @spec deliver(Riverside.LocalDelivery.destination(), any) :: :ok | :error 883 | def deliver(dest, {frame_type, message}) do 884 | Riverside.LocalDelivery.deliver(dest, {frame_type, message}) 885 | :ok 886 | end 887 | 888 | def deliver(dest, data) do 889 | case @riverside_config.codec.encode(data) do 890 | {:ok, value} -> 891 | deliver(dest, {@riverside_config.codec.frame_type(), value}) 892 | 893 | {:error, :invalid_message} -> 894 | :error 895 | end 896 | end 897 | 898 | @spec deliver_user( 899 | user_id :: Session.user_id(), 900 | data :: any 901 | ) :: :ok | :error 902 | 903 | def deliver_user(user_id, data) do 904 | deliver({:user, user_id}, data) 905 | end 906 | 907 | @spec deliver_session( 908 | user_id :: Session.user_id(), 909 | session_id :: String.t(), 910 | data :: any 911 | ) :: :ok | :error 912 | def deliver_session(user_id, session_id, data) do 913 | deliver({:session, user_id, session_id}, data) 914 | end 915 | 916 | @spec deliver_channel( 917 | channel_id :: any, 918 | data :: any 919 | ) :: :ok | :error 920 | 921 | def deliver_channel(channel_id, data) do 922 | deliver({:channel, channel_id}, data) 923 | end 924 | 925 | @spec deliver_me( 926 | frame_type :: Riverside.Codec.frame_type(), 927 | message :: binary 928 | ) :: :ok | :error 929 | 930 | def deliver_me(frame_type, message) do 931 | send(self(), {:deliver, frame_type, message}) 932 | :ok 933 | end 934 | 935 | @spec deliver_me(any) :: :ok | :error 936 | 937 | def deliver_me(data) do 938 | case @riverside_config.codec.encode(data) do 939 | {:ok, value} -> 940 | deliver_me(@riverside_config.codec.frame_type(), value) 941 | 942 | {:error, :invalid_message} -> 943 | :error 944 | end 945 | end 946 | 947 | @spec close() :: no_return 948 | def close(), do: send(self(), :stop) 949 | 950 | @impl Riverside 951 | def authenticate(req) do 952 | user_id = Riverside.IO.Random.bigint() 953 | session_id = Riverside.IO.Random.hex(20) 954 | {:ok, user_id, session_id, %{}} 955 | end 956 | 957 | @impl Riverside 958 | def init(session, state), do: {:ok, session, state} 959 | 960 | @impl Riverside 961 | def handle_info(event, session, state), do: {:ok, session, state} 962 | 963 | @impl Riverside 964 | def handle_message(_msg, session, state), do: {:ok, session, state} 965 | 966 | @impl Riverside 967 | def terminate(_reason, _session, _state), do: :ok 968 | 969 | defoverridable authenticate: 1, 970 | init: 2, 971 | handle_info: 3, 972 | handle_message: 3, 973 | terminate: 3 974 | end 975 | end 976 | 977 | def child_spec(args) do 978 | Riverside.Supervisor.child_spec(args) 979 | end 980 | end 981 | --------------------------------------------------------------------------------