Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# nrte

An OTP application to send and receive real time events over HTTP connections.
An OTP library to send and receive real time events over HTTP connections.

## Status
[![GitHub branch checks state](https://github.com/nomasystems/nrte/actions/workflows/ci.yml/badge.svg)](https://github.com/nomasystems/nrte/actions/workflows/ci.yml)
Expand All @@ -19,6 +19,11 @@ In your `rebar.config` file, add the dependency:
]}.
```

And start listening to incoming connections:
```erl
nrte:start_listening(#{} = Opts).
```

Then, you can connect either via WebSocket or server-sent events to start receiving messages for the given topics.

For example, in javascript via the `WebSocket` interface:
Expand All @@ -38,24 +43,26 @@ curl -X POST -d 'my custom message' 'localhost:2080/message?topics=topic1'

## Configuration

`nrte` defaults to the following configuration values:
All the keys in the `nrte:start_listening/1` parameter are optional and default to the following values:
```erl
[
{auth_type, {always_allow, all}},
{data_template, {<<"{{topic}};{{message}}">>}},
{port, 2080},
{serve_priv_dir, false}
]
#{
auth_type => {always_allow, all}},
name => nrte_listener,
port => 2080,
serve_priv_dir => false
}
```

* `auth_type`: see [authentication](#authentication) for details.
* `data_template`: a template for sending the data through the http connections. Both `{{topic}}` and `{{message}}` are optional and will be replaced with the actual values.
* `name`: a unique name to be registered as the server.
* `port`: TCP port that serves the different endpoints.
* `serve_priv_dir`: whether to include the [priv dir](/priv/) in the server or not.

It's also possible to define a global `data_template` with `application:set_env(nrte, data_template, Template)`. Defaults to `{data_tempate, {<<"{{topic}};{{message}}">>}}` and any appearance of `{{topic}}` and `{{message}}` are replaced with the actual values.

## Authentication

By default nrte doesn't authenticate its users, but this can be changed by setting the `auth_type` configuration parameter to a tuple `{auth_mod, Mod}`. In that case, when a request arrives, `nrte` will call `Mod:nrte_auth(Headers)` and use the returned `nrte_auth_value()` to allow or deny access to the resource. See the [`nrte_auth` module](/src/nrte_auth.erl) for the behaviour to implement.
By default nrte doesn't authenticate its users, but this can be changed by setting the `auth_type` option to a tuple `{auth_mod, Mod}`. In that case, when a request arrives, `nrte` will call `Mod:nrte_auth(Headers)` and use the returned `nrte_auth_value()` to allow or deny access to the resource. See the [`nrte_auth` module](/src/nrte_auth.erl) for the behaviour to implement.

Some examples:
```erl
Expand Down
2 changes: 1 addition & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@
{cover_enabled, true}.
{cover_opts, [verbose]}.

{xref_ignores, []}.
{xref_ignores, [nrte]}.
5 changes: 2 additions & 3 deletions src/nrte.app.src
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{application, nrte, [
{description, "An OTP application to send and receive real time events over HTTP connections"},
{vsn, "0.1.0"},
{description, "An OTP libary to send and receive real time events over HTTP connections"},
{vsn, "1.0.0"},
{registered, []},
{applications, [kernel, stdlib, cowboy, ebus, njson]},
{mod, {nrte, []}},
{env, []}
]}.
42 changes: 29 additions & 13 deletions src/nrte.erl
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,52 @@
%% See the License for the specific language governing permissions and
%% limitations under the License
-module(nrte).
-behaviour(application).

%%% APPLICATION EXPORTS
-export([start/2, stop/1]).
%%% START/STOP SERVER EXPORTS
-export([start_listening/0, start_listening/1, stop_listening/0, stop_listening/1]).

%%% EXTERNAL EXPORTS
-export([publish/2, subscribe/1]).

%%% TYPES
-type opts() :: #{
auth_type => {always_allow, nrte_auth:nrte_auth_value()} | {auth_mod, atom()},
name => atom(),
port => pos_integer(),
serve_priv_dir => boolean()
}.

%%%-----------------------------------------------------------------------------
%%% APPLICATION EXPORTS
%%%-----------------------------------------------------------------------------
start(_, _) ->
-spec start_listening() -> {ok, pid()} | {error, any()}.
start_listening() ->
start_listening(#{}).

-spec start_listening(opts()) -> {ok, pid()} | {error, any()}.
start_listening(Opts) ->
BaseRoutes = [
{"/eventsource", nrte_eventsource, []},
{"/websocket", nrte_websocket, #{}},
{"/[...]", nrte_rest, []}
{"/eventsource", nrte_eventsource, Opts},
{"/websocket", nrte_websocket, Opts},
{"/[...]", nrte_rest, Opts}
],
Routes =
case nrte_conf:serve_priv_dir() of
case nrte_conf:serve_priv_dir(Opts) of
true -> [{"/priv/[...]", cowboy_static, {priv_dir, nrte, ""}} | BaseRoutes];
_ -> BaseRoutes
end,
Dispatch = cowboy_router:compile([{'_', Routes}]),
cowboy:start_clear(nrte_listener, [{port, nrte_conf:port()}], #{
cowboy:start_clear(nrte_conf:name(Opts), [{port, nrte_conf:port(Opts)}], #{
enable_connect_protocol => true, env => #{dispatch => Dispatch}
}),
nrte_sup:start_link().
}).

-spec stop_listening() -> ok.
stop_listening() ->
stop_listening(nrte_conf:name(#{})).

stop(_) ->
cowboy:stop_listener(nrte_listener),
-spec stop_listening(atom()) -> ok.
stop_listening(Name) ->
cowboy:stop_listener(Name),
ok.

%%%-----------------------------------------------------------------------------
Expand Down
7 changes: 4 additions & 3 deletions src/nrte_auth.erl
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,19 @@
-module(nrte_auth).

-type nrte_auth_value() :: unauthorized | all | none | [RegExp :: iodata()].
-export_type([nrte_auth_value/0]).
-callback nrte_auth(Headers :: #{binary() => binary()}) ->
nrte_auth_value() | #{allowed_publications | allowed_subscriptions => nrte_auth_value()}.

%%% EXTERNAL EXPORTS
-export([authorization/2]).
-export([authorization/3]).

%%%-----------------------------------------------------------------------------
%%% EXTERNAL EXPORTS
%%%-----------------------------------------------------------------------------
authorization(Req, Type) ->
authorization(Req, Opts, Type) ->
AuthValue =
case nrte_conf:auth_type() of
case nrte_conf:auth_type(Opts) of
{always_allow, Value} -> Value;
{auth_mod, Mod} -> Mod:nrte_auth(maps:get(headers, Req))
end,
Expand Down
22 changes: 13 additions & 9 deletions src/nrte_conf.erl
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,27 @@

%%% EXTERNAL EXPORTS
-export([
auth_type/0,
auth_type/1,
data_template/0,
port/0,
serve_priv_dir/0
name/1,
port/1,
serve_priv_dir/1
]).

%%%-----------------------------------------------------------------------------
%%% EXTERNAL EXPORTS
%%%-----------------------------------------------------------------------------
auth_type() ->
application:get_env(nrte, auth_type, {always_allow, all}).
auth_type(Opts) ->
maps:get(auth_type, Opts, {always_allow, all}).

data_template() ->
application:get_env(nrte, data_template, <<"{{topic}};{{message}}">>).

port() ->
application:get_env(nrte, port, 2080).
name(Opts) ->
maps:get(name, Opts, nrte_listener).

serve_priv_dir() ->
application:get_env(nrte, serve_priv_dir, false).
port(Opts) ->
maps:get(port, Opts, 2080).

serve_priv_dir(Opts) ->
maps:get(serve_priv_dir, Opts, false).
2 changes: 1 addition & 1 deletion src/nrte_eventsource.erl
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
%%% COWBOY LOOP EXPORTS
%%%-----------------------------------------------------------------------------
init(Req, Opts) ->
case nrte_auth:authorization(Req, subscribe) of
case nrte_auth:authorization(Req, Opts, subscribe) of
{authorized, TopicList} ->
nrte:subscribe(TopicList),
nrte_publications:subscription_init_link_terminate(<<"eventsource">>, TopicList),
Expand Down
2 changes: 1 addition & 1 deletion src/nrte_rest.erl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
%%% COWBOY HANDLER EXPORTS
%%%-----------------------------------------------------------------------------
init(Req, Opts) ->
case nrte_auth:authorization(Req, publish) of
case nrte_auth:authorization(Req, Opts, publish) of
{authorized, TopicList} ->
init_authorized(Req, Opts, TopicList);
{unauthorized, Req2} ->
Expand Down
33 changes: 0 additions & 33 deletions src/nrte_sup.erl

This file was deleted.

2 changes: 1 addition & 1 deletion src/nrte_websocket.erl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
%%% COWBOY WEBSOCKET EXPORTS
%%%-----------------------------------------------------------------------------
init(Req, Opts) ->
case nrte_auth:authorization(Req, subscribe) of
case nrte_auth:authorization(Req, Opts, subscribe) of
{authorized, TopicList} ->
{cowboy_websocket, Req, Opts#{topic_list => TopicList}};
{unauthorized, Req2} ->
Expand Down
37 changes: 23 additions & 14 deletions test/nrte_eventsource_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ suite() ->
%%% INIT SUITE EXPORTS
%%%-----------------------------------------------------------------------------
init_per_suite(Conf) ->
nct_util:setup_suite(Conf).
Conf2 = nct_util:setup_suite(Conf),
{ok, _} = nrte:start_listening(),
Conf2.

%%%-----------------------------------------------------------------------------
%%% END SUITE EXPORTS
%%%-----------------------------------------------------------------------------
end_per_suite(Conf) ->
ok = nrte:stop_listening(),
nct_util:teardown_suite(Conf).

%%%-----------------------------------------------------------------------------
Expand All @@ -60,28 +63,31 @@ connect_authorized_pattern() ->
[{userdata, [{doc, "Tests connecting to the event source with an authorized pattern"}]}].

connect_authorized_pattern(_Conf) ->
application:set_env(nrte, auth_type, {auth_mod, ?MODULE}),
{ok, {Pid, _StreamRef}} = connect_es("auth-allowed"),
application:unset_env(nrte, auth_type),
ok = gun:close(Pid).
{Name, Port} = {nrte_eventsource_connect_auth, 2081},
{ok, _} = nrte:start_listening(#{name => Name, port => Port, auth_type => {auth_mod, ?MODULE}}),
{ok, {Pid, _StreamRef}} = connect_es("auth-allowed", Port),
ok = gun:close(Pid),
ok = nrte:stop_listening(Name).

connect_forbidden_pattern() ->
[{userdata, [{doc, "Tests being forbidden from connecting to the event source"}]}].

connect_forbidden_pattern(_Conf) ->
application:set_env(nrte, auth_type, {auth_mod, ?MODULE}),
forbidden = connect_es("auth-allowed;noauth-noallowed"),
application:unset_env(nrte, auth_type),
ok.
{Name, Port} = {nrte_eventsource_connect_auth, 2081},
{ok, _} = nrte:start_listening(#{name => Name, port => Port, auth_type => {auth_mod, ?MODULE}}),
forbidden = connect_es("auth-allowed;noauth-noallowed", Port),
ok = nrte:stop_listening(Name).

connect_unauthorized() ->
[{userdata, [{doc, "Tests unauthorized connecting to the event source"}]}].

connect_unauthorized(_Conf) ->
application:set_env(nrte, auth_type, {always_allow, unauthorized}),
unauthorized = connect_es("topic1;topic2;topic3"),
application:unset_env(nrte, auth_type),
ok.
{Name, Port} = {nrte_eventsource_connect_auth, 2081},
{ok, _} = nrte:start_listening(#{
name => Name, port => Port, auth_type => {always_allow, unauthorized}
}),
unauthorized = connect_es("topic1;topic2;topic3", Port),
ok = nrte:stop_listening(Name).

receive_subscription_init() ->
[{userdata, [{doc, "Tests receiving the special subscription init message"}]}].
Expand Down Expand Up @@ -129,8 +135,11 @@ nrte_auth(_) ->
#{allowed_subscriptions => [<<"auth.*">>]}.

connect_es(Topics) ->
connect_es(Topics, 2080).

connect_es(Topics, Port) ->
Protocol = http2,
{ok, Pid} = gun:open("localhost", 2080, #{
{ok, Pid} = gun:open("localhost", Port, #{
protocols => [Protocol],
http2_opts => #{content_handlers => [gun_sse_h, gun_data_h]}
}),
Expand Down
Loading