Skip to content
Draft
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
26 changes: 25 additions & 1 deletion src/nova_router.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
routes/1,

%% Modulates the routes-table
add_routes/1,
add_routes/2
]).

Expand All @@ -52,8 +53,13 @@ compiled_apps() ->
-spec compile(Apps :: [atom() | {atom(), map()}]) -> host_tree().
compile(Apps) ->
UseStrict = application:get_env(nova, use_strict_routing, false),
Dispatch = compile(Apps, routing_tree:new(#{use_strict => UseStrict, convert_to_binary => true}), #{}),
StorageBackend = application:get_env(nova, dispatch_backend, persistent_term),

StoredDispatch = StorageBackend:get(nova_dispatch,
routing_tree:new(#{use_strict => UseStrict,
convert_to_binary => true})),
Dispatch = compile(Apps, StoredDispatch, #{}),
%% Write the updated dispatch to storage
StorageBackend:put(nova_dispatch, Dispatch),
Dispatch.

Expand Down Expand Up @@ -129,6 +135,24 @@ lookup_url(Host, Path, Method) ->
lookup_url(Host, Path, Method, Dispatch) ->
routing_tree:lookup(Host, Path, Method, Dispatch).


%%--------------------------------------------------------------------
%% @doc
%% Works the same way as add_routes/2 but with the exception that you
%% don't need to provide the routes explicitly. When using this it's
%% expected that there's a routing-module associated with the application.
%% Eg. for the application 'test' the corresponding router would then be
%% 'test_router'. Read more about routers in the official documentation.
%% @end
%%--------------------------------------------------------------------
-spec add_routes(App :: atom()) -> ok.
add_routes(App) ->
Router = erlang:list_to_atom(io_lib:format("~s_router", [App])),
Env = nova:get_environment(),
%% Call the router
Routes = Router:routes(Env),
add_routes(App, Routes).

%%--------------------------------------------------------------------
%% @doc
%% Add routes to the dispatch-table for the given app. The routes
Expand Down
261 changes: 174 additions & 87 deletions src/nova_sup.erl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
-behaviour(supervisor).

%% API
-export([start_link/0]).
-export([
start_link/0,
add_application/2,
remove_application/1,
get_started_applications/0
]).

%% Supervisor callbacks
-export([init/1]).
Expand All @@ -17,9 +22,20 @@
-include("../include/nova.hrl").

-define(SERVER, ?MODULE).
-define(NOVA_LISTENER, nova_listener).

-define(NOVA_LISTENER, fun(LApp, LPort) -> list_to_atom(atom_to_list(LApp) ++ integer_to_list(LPort)) end).
-define(NOVA_STD_PORT, 8080).
-define(NOVA_STD_SSL_PORT, 8443).
-define(NOVA_SUP_TABLE, nova_sup_table).
-define(COWBOY_LISTENERS, cowboy_listeners).


-record(nova_server, {
app :: atom(),
host :: inet:ip_address(),
port :: number(),
listener :: ranch:ref()
}).


%%%===================================================================
Expand All @@ -36,6 +52,54 @@
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).

%%--------------------------------------------------------------------
%% @doc
%% Add a Nova application. This can either be on the same cowboy server that
%% a previous application was started with, or a new one if the configuration
%% ie port is different.
%%
%% @end
%%--------------------------------------------------------------------
-spec add_application(App :: atom(), Configuration :: map()) -> {ok, App :: atom(),
Host :: inet:ip_address(), Port :: number()}
| {error, Reason :: any()}.
add_application(App, Configuration) ->
setup_cowboy(App, Configuration).

%%--------------------------------------------------------------------
%% @doc
%% Get all started Nova applications. This will return a list of
%% #nova_server{} records that contains the application name, host, port
%% and listener reference.
%%
%% @end
%%--------------------------------------------------------------------
-spec get_started_applications() -> [#{app => atom(), host => inet:ip_address(), port => number()}].
get_started_applications() ->
%% Fetch all started applications from the ETS table
Apps = ets:tab2list(?NOVA_SUP_TABLE),
[ #{app => App, host => Host, port => Port} ||
#nova_server{app = App, host = Host, port = Port} <- Apps ].

%%--------------------------------------------------------------------
%% @doc
%% Remove a Nova application. This will stop the cowboy listener so request
%% to that application will not be handled anymore.
%%
%% @end
%%--------------------------------------------------------------------
remove_application(App) ->
case ets:lookup(?NOVA_SUP_TABLE, App) of
[] ->
?LOG_ERROR(#{msg => <<"Application not found">>, app => App}),
{error, not_found};
[#nova_server{listener = Listener}] ->
?LOG_NOTICE(#{msg => <<"Stopping cowboy listener">>, app => App, listener => Listener}),
cowboy:stop_listener(Listener),
ets:delete(?NOVA_SUP_TABLE, App),
ok
end.

%%%===================================================================
%%% Supervisor callbacks
%%%===================================================================
Expand All @@ -51,19 +115,20 @@ start_link() ->
%% @end
%%--------------------------------------------------------------------
init([]) ->
%% Initialize the ETS table for application state
ets:new(?NOVA_SUP_TABLE, [named_table, protected, set]),

%% This is a bit ugly, but we need to do this anyhow(?)
SupFlags = #{strategy => one_for_one,
intensity => 1,
period => 5},

%% Bootstrap the environment
Environment = nova:get_environment(),

nova_pubsub:start(),

?LOG_NOTICE(#{msg => <<"Starting nova">>, environment => Environment}),

Configuration = application:get_env(nova, cowboy_configuration, #{}),

SessionManager = application:get_env(nova, session_manager, nova_session_ets),

Children = [
Expand All @@ -72,7 +137,7 @@ init([]) ->
child(nova_watcher, nova_watcher)
],

setup_cowboy(Configuration),
setup_cowboy(),


{ok, {SupFlags, Children}}.
Expand All @@ -97,8 +162,17 @@ child(Id, Type, Mod) ->
child(Id, Mod) ->
child(Id, worker, Mod).

setup_cowboy(Configuration) ->
case start_cowboy(Configuration) of

%%%-------------------------------------------------------------------
%%% Nova Cowboy setup
%%%-------------------------------------------------------------------
setup_cowboy() ->
CowboyConfiguration = application:get_env(nova, cowboy_configuration, #{}),
BootstrapApp = application:get_env(nova, bootstrap_application, undefined),
setup_cowboy(BootstrapApp, CowboyConfiguration).

setup_cowboy(BootstrapApp, Configuration) ->
case start_cowboy(BootstrapApp, Configuration) of
{ok, App, Host, Port} ->
Host0 = inet:ntoa(Host),
CowboyVersion = get_version(cowboy),
Expand All @@ -112,97 +186,110 @@ setup_cowboy(Configuration) ->
?LOG_ERROR(#{msg => <<"Cowboy could not start">>, reason => Error})
end.

-spec start_cowboy(Configuration :: map()) ->

-spec start_cowboy(BootstrapApp :: atom(), Configuration :: map()) ->
{ok, BootstrapApp :: atom(), Host :: string() | {integer(), integer(), integer(), integer()},
Port :: integer()} | {error, Reason :: any()}.
start_cowboy(Configuration) ->
Middlewares = [
nova_router, %% Lookup routes
nova_plugin_handler, %% Handle pre-request plugins
nova_security_handler, %% Handle security
nova_handler, %% Controller
nova_plugin_handler %% Handle post-request plugins
],
StreamH = [nova_stream_h,
cowboy_compress_h,
cowboy_stream_h],
StreamHandlers = maps:get(stream_handlers, Configuration, StreamH),
MiddlewareHandlers = maps:get(middleware_handlers, Configuration, Middlewares),
Options = maps:get(options, Configuration, #{compress => true}),

%% Build the options map
CowboyOptions1 = Options#{middlewares => MiddlewareHandlers,
stream_handlers => StreamHandlers},

BootstrapApp = application:get_env(nova, bootstrap_application, undefined),

%% Compile the routes
Dispatch =
case BootstrapApp of
undefined ->
?LOG_ERROR(#{msg => <<"You need to define bootstrap_application option in configuration">>}),
throw({error, no_nova_app_defined});
App ->
ExtraApps = application:get_env(App, nova_apps, []),
nova_router:compile([nova|[App|ExtraApps]])
end,

CowboyOptions2 =
case application:get_env(nova, use_persistent_term, true) of
true ->
CowboyOptions1;
_ ->
CowboyOptions1#{env => #{dispatch => Dispatch}}
end,

start_cowboy(BootstrapApp, Configuration) ->
%% Determine if we have an already started cowboy on the host/port configuration
Host = maps:get(ip, Configuration, { 0, 0, 0, 0}),

case maps:get(use_ssl, Configuration, false) of
false ->
Port = maps:get(port, Configuration, ?NOVA_STD_PORT),
case cowboy:start_clear(
?NOVA_LISTENER,
[{port, Port},
{ip, Host}],
CowboyOptions2) of
{ok, _Pid} ->
{ok, BootstrapApp, Host, Port};
Error ->
Error
end;
Port = maps:get(port, Configuration, ?NOVA_STD_PORT),

Listeners = nova:get_env(?COWBOY_LISTENERS, []),
AlreadyStarted = lists:any(fun({X, Y}) -> X == Host andalso Y == Port end, Listeners),

%% If yes we only need to add things to the dispatch
case AlreadyStarted of
true ->
%% A cowboy listener is already running on this host/port configuration - just add to the
%% dispatch.
logger:info(#{msg => <<"There's already a Cowboy listener running with the host/port config. Adding routes to dispatch.">>, host => Host, port => Port}),
ok;
_ ->
case maps:get(ca_cert, Configuration, undefined) of
undefined ->
Port = maps:get(ssl_port, Configuration, ?NOVA_STD_SSL_PORT),
SSLOptions = maps:get(ssl_options, Configuration, #{}),
TransportOpts = maps:put(port, Port, SSLOptions),
TransportOpts1 = maps:put(ip, Host, TransportOpts),

case cowboy:start_tls(
?NOVA_LISTENER, maps:to_list(TransportOpts1), CowboyOptions2) of
%% Cowboy configuration
Middlewares = [
nova_router, %% Lookup routes
nova_plugin_handler, %% Handle pre-request plugins
nova_security_handler, %% Handle security
nova_handler, %% Controller
nova_plugin_handler %% Handle post-request plugins
],
StreamH = [
nova_stream_h,
cowboy_compress_h,
cowboy_stream_h
],

%% Good debug message in case someone wants to double check which config they are running with
logger:debug(#{msg => <<"Configure cowboy">>, stream_handlers => StreamH, middlewares => Middlewares}),

StreamHandlers = maps:get(stream_handlers, Configuration, StreamH),
MiddlewareHandlers = maps:get(middleware_handlers, Configuration, Middlewares),
Options = maps:get(options, Configuration, #{compress => true}),

%% Build the options map
CowboyOptions1 = Options#{middlewares => MiddlewareHandlers,
stream_handlers => StreamHandlers},

%% Compile the routes
Dispatch =
case BootstrapApp of
undefined ->
?LOG_ERROR(#{msg => <<"You need to define bootstrap_application option in configuration">>}),
throw({error, no_nova_app_defined});
App ->
ExtraApps = application:get_env(App, nova_apps, []),
nova_router:compile([nova|[App|ExtraApps]])
end,

CowboyOptions2 =
case application:get_env(nova, use_persistent_term, true) of
true ->
CowboyOptions1;
_ ->
CowboyOptions1#{env => #{dispatch => Dispatch}}
end,

case maps:get(use_ssl, Configuration, false) of
false ->
case cowboy:start_clear(
?NOVA_LISTENER(BootstrapApp, Port),
[{port, Port},
{ip, Host}],
CowboyOptions2) of
{ok, _Pid} ->
?LOG_NOTICE(#{msg => <<"Nova starting SSL">>, port => Port}),
nova:set_env(?COWBOY_LISTENERS, [{Host, Port}|Listeners]),
ets:insert(?NOVA_SUP_TABLE, #nova_server{
app = BootstrapApp,
host = Host,
port = Port,
listener = ?NOVA_LISTENER(BootstrapApp, Port)
}),
{ok, BootstrapApp, Host, Port};
Error ->
?LOG_ERROR(#{msg => <<"Could not start cowboy with SSL">>, reason => Error}),
Error
end;
CACert ->
Cert = maps:get(cert, Configuration),
Port = maps:get(ssl_port, Configuration, ?NOVA_STD_SSL_PORT),
?LOG_DEPRECATED(<<"0.10.3">>, <<"Use of use_ssl is deprecated, use ssl instead">>),
_ ->
SSLPort = maps:get(ssl_port, Configuration, ?NOVA_STD_SSL_PORT),
SSLOptions = maps:get(ssl_options, Configuration, #{}),
TransportOpts = maps:put(port, SSLPort, SSLOptions),
TransportOpts1 = maps:put(ip, Host, TransportOpts),

case cowboy:start_tls(
?NOVA_LISTENER, [
{port, Port},
{ip, Host},
{certfile, Cert},
{cacertfile, CACert}
],
CowboyOptions2) of
?NOVA_LISTENER(BootstrapApp, SSLPort),
maps:to_list(TransportOpts1), CowboyOptions2) of
{ok, _Pid} ->
?LOG_NOTICE(#{msg => <<"Nova starting SSL">>, port => Port}),
{ok, BootstrapApp, Host, Port};
?LOG_NOTICE(#{msg => <<"Nova starting SSL">>, port => SSLPort}),
ets:insert(?NOVA_SUP_TABLE, #nova_server{
app = BootstrapApp,
host = Host,
port = SSLPort,
listener = ?NOVA_LISTENER(BootstrapApp, SSLPort)
}),
nova:set_env(?COWBOY_LISTENERS, [{Host, SSLPort}|Listeners]),
{ok, BootstrapApp, Host, SSLPort};
Error ->
?LOG_ERROR(#{msg => <<"Could not start cowboy with SSL">>, reason => Error}),
Error
end
end
Expand Down