From fae4c2ed651140cab2c1a734c46053940ea30860 Mon Sep 17 00:00:00 2001 From: Niclas Axelsson Date: Mon, 12 Aug 2024 16:35:01 +0200 Subject: [PATCH 1/9] Support for having a long-lived state per plugin --- src/nova_plugin.erl | 34 +++++- src/nova_plugin_handler.erl | 57 ++++++---- src/nova_plugin_manager.erl | 221 ++++++++++++++++++++++++++++++++++++ src/nova_router.erl | 3 + src/nova_sup.erl | 1 + 5 files changed, 294 insertions(+), 22 deletions(-) create mode 100644 src/nova_plugin_manager.erl diff --git a/src/nova_plugin.erl b/src/nova_plugin.erl index 1c1cb090..c028f2ce 100644 --- a/src/nova_plugin.erl +++ b/src/nova_plugin.erl @@ -30,7 +30,26 @@ -type request_type() :: pre_request | post_request. -export_type([request_type/0]). -%% Define the callback functions for HTTP-plugins +%% @doc +%% Start function for the plugin. This function is called when the plugin is started +%% and will return a state that will be passed to the other functions during +%% the life cycle of the plugin. The state can be any term. +%% @end +-callback init() -> State :: nova:state(). +-optional_callbacks([init/0]). + +%% @doc +%% Stop function for the plugin. This function is called when the application is stopped. +%% It takes a state as argument and should return ok. +%% @end +-callback stop(State :: nova:state()) -> ok. +-optional_callbacks([stop/1]). + +%% @doc +%% This function is called before the request is processed. It can modify the request +%% and the nova-state. It takes a state and a map of options as arguments and should return +%% either {ok, NewState}, {break, NewState}, {stop, NewState} or {error, Reason}. +%% @end -callback pre_request(State :: nova:state(), Options :: map()) -> {ok, State0 :: nova:state()} | {break, State0 :: nova:state()} | @@ -38,6 +57,12 @@ {error, Reason :: term()}. -optional_callbacks([pre_request/2]). +%% @doc +%% This function is called after the request is processed. It can modify the request. +%% It takes a state and a map of options as arguments and should return +%% either {ok, NewState}, {break, NewState}, {stop, NewState} or {error, Reason}. +%% The state is only used if there's another plugin invoked after this one. +%% @end -callback post_request(State :: nova:state(), Options :: map()) -> {ok, State0 :: nova:state()} | {break, State0 :: nova:state()} | @@ -45,8 +70,15 @@ {error, Reason :: term()}. -optional_callbacks([post_request/2]). +%% @doc +%% This function should return information about the plugin. The information is used +%% in the documentation and in the plugin-listing. +%% @end -callback plugin_info() -> {Title :: binary(), Version :: binary(), + Url :: binary(), Author :: binary(), Description :: binary(), + Requires :: [PluginName :: binary() | + {PluginName :: binary(), Version :: binary()}], Options :: [{Key :: atom(), OptionDescription :: binary()}]}. diff --git a/src/nova_plugin_handler.erl b/src/nova_plugin_handler.erl index 201762d8..8a641913 100644 --- a/src/nova_plugin_handler.erl +++ b/src/nova_plugin_handler.erl @@ -23,27 +23,35 @@ execute(Req, Env) -> run_plugins([], Callback, Req, Env) -> {ok, Req, Env#{plugin_state => Callback}}; run_plugins([{Module, Options}|Tl], Callback, Req, Env) -> - Args = case proplists:get_value(Callback, Module:module_info(exports)) of - 2 -> [Req, Options]; - 3 -> [Req, Env, Options]; - _ -> {throw, bad_callback} - end, - try erlang:apply(Module, Callback, Args) of - {ok, Req0} -> - run_plugins(Tl, Callback, Req0, Env); - {ok, Reply, Req0} -> - Req1 = handle_reply(Reply, Req0), - run_plugins(Tl, Callback, Req1, Env); - {break, Req0} -> - {ok, Req0}; - {break, Reply, Req0} -> - Req1 = handle_reply(Reply, Req0), - {ok, Req1}; - {stop, Req0} -> - {stop, Req0}; - {stop, Reply, Req0} -> - Req1 = handle_reply(Reply, Req0), - {stop, Req1} + %% Fetch state + State = + case nova_plugin_manager:get_state(Module) of + {ok, State0} -> + State0; + {error, _Reason} -> + undefined + end, + + try Module:Callback(Req, Env, Options, State) of + Result -> + set_state(Module, Result), + case Result of + {ok, Req0, _State0} -> + run_plugins(Tl, Callback, Req0, Env); + {ok, Reply, Req0, _State0} -> + Req1 = handle_reply(Reply, Req0), + run_plugins(Tl, Callback, Req1, Env); + {break, Req0, _State0} -> + {ok, Req0}; + {break, Reply, Req0, _State0} -> + Req1 = handle_reply(Reply, Req0), + {ok, Req1}; + {stop, Req0, _State0} -> + {stop, Req0}; + {stop, Reply, Req0, _State0} -> + Req1 = handle_reply(Reply, Req0), + {stop, Req1} + end catch Class:Reason:Stacktrace -> ?LOG_ERROR(#{msg => <<"Plugin crashed">>, class => Class, reason => Reason, stacktrace => Stacktrace}), @@ -63,3 +71,10 @@ handle_reply({reply, Status, Headers, Body}, Req) -> Req1#{resp_status_code => Status}; handle_reply(_, Req) -> Req. + + + +set_state(Module, Return) -> + ReturnList = erlang:tuple_to_list(Return), + State = lists:last(ReturnList), + nova_plugin_manager:set_state(Module, State). diff --git a/src/nova_plugin_manager.erl b/src/nova_plugin_manager.erl new file mode 100644 index 00000000..f6bc1fb3 --- /dev/null +++ b/src/nova_plugin_manager.erl @@ -0,0 +1,221 @@ +%%%------------------------------------------------------------------- +%%% @author Niclas Axelsson +%%% @doc +%%% Main manager for handling plugins +%%% @end +%%% Created : 10 Aug 2024 by Niclas Axelsson +%%%------------------------------------------------------------------- +-module(nova_plugin_manager). + +-behaviour(gen_server). + +%% API +-export([ + start_link/0, + add_plugin/1, + add_plugin/3, + get_state/1, + set_state/2 + ]). + +%% gen_server callbacks +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3, + format_status/2 + ]). + +-include_lib("kernel/include/logger.hrl"). + +-define(TABLE, '__nova_plugins__'). +-define(SERVER, ?MODULE). + +-record(state, {}). + +-record(plugin, { + module :: atom(), + name :: binary(), + version :: binary(), + state :: nova:state() + }). + +%%%=================================================================== +%%% API +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @doc +%% Starts the server +%% @end +%%-------------------------------------------------------------------- +-spec start_link() -> {ok, Pid :: pid()} | + {error, Error :: {already_started, pid()}} | + {error, Error :: term()} | + ignore. +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +add_plugin(Module) -> + ModuleInfo = Module:plugin_info(), + Title = element(1, ModuleInfo), + Version = element(2, ModuleInfo), + add_plugin(Module, Title, Version). + +add_plugin(Module, Name, Version) -> + gen_server:call(?SERVER, {add_plugin, Module, Name, Version}). + +get_state(Module) -> + case ets:lookup(?TABLE, Module) of + [#plugin{state = State}] -> + {ok, State}; + _ -> + {error, not_found} + end. + +set_state(Module, NewState) -> + gen_server:call(?SERVER, {set_state, Module, NewState}). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Initializes the server +%% @end +%%-------------------------------------------------------------------- +-spec init(Args :: term()) -> {ok, State :: term()} | + {ok, State :: term(), Timeout :: timeout()} | + {ok, State :: term(), hibernate} | + {stop, Reason :: term()} | + ignore. +init([]) -> + process_flag(trap_exit, true), + ets:new(?TABLE, [named_table, set, protected, {keypos, #plugin.module}]), + GlobalPlugins = application:get_env(nova, plugins, []), + [ add_plugin(Module) || Module <- GlobalPlugins ], + {ok, #state{}}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling call messages +%% @end +%%-------------------------------------------------------------------- +-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) -> + {reply, Reply :: term(), NewState :: term()} | + {reply, Reply :: term(), NewState :: term(), Timeout :: timeout()} | + {reply, Reply :: term(), NewState :: term(), hibernate} | + {noreply, NewState :: term()} | + {noreply, NewState :: term(), Timeout :: timeout()} | + {noreply, NewState :: term(), hibernate} | + {stop, Reason :: term(), Reply :: term(), NewState :: term()} | + {stop, Reason :: term(), NewState :: term()}. +handle_call({set_state, Module, NewState}, _From, State) -> + case ets:lookup(?TABLE, Module) of + [#plugin{}] -> + ets:insert(?TABLE, #plugin{module = Module, state = NewState}), + {reply, ok, State}; + [] -> + {reply, {error, not_found}, State} + end; +handle_call({add_plugin, Module, Name, Version}, _From, State) -> + case ets:lookup(?TABLE, Module) of + [#plugin{}] -> + {reply, ok, State}; + [] -> + PluginState = + case erlang:function_exported(Module, init, 0) of + true -> + ?LOG_INFO("Initializing plugin ~p", [Module]), + Module:init(); + _ -> + undefined + end, + ets:insert(?TABLE, #plugin{module = Module, name = Name, version = Version, state = PluginState}), + {reply, ok, State} + end; +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling cast messages +%% @end +%%-------------------------------------------------------------------- +-spec handle_cast(Request :: term(), State :: term()) -> + {noreply, NewState :: term()} | + {noreply, NewState :: term(), Timeout :: timeout()} | + {noreply, NewState :: term(), hibernate} | + {stop, Reason :: term(), NewState :: term()}. +handle_cast(_Request, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling all non call/cast messages +%% @end +%%-------------------------------------------------------------------- +-spec handle_info(Info :: timeout() | term(), State :: term()) -> + {noreply, NewState :: term()} | + {noreply, NewState :: term(), Timeout :: timeout()} | + {noreply, NewState :: term(), hibernate} | + {stop, Reason :: normal | term(), NewState :: term()}. +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates +%% with Reason. The return value is ignored. +%% @end +%%-------------------------------------------------------------------- +-spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(), + State :: term()) -> any(). +terminate(_Reason, _State) -> + %% Stop all plugins and clean state + Plugins = ets:tab2file(?TABLE), + [ Plugin:stop() || #plugin{module = Plugin} <- Plugins ], + ets:delete(?TABLE), + ok. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Convert process state when code is changed +%% @end +%%-------------------------------------------------------------------- +-spec code_change(OldVsn :: term() | {down, term()}, + State :: term(), + Extra :: term()) -> {ok, NewState :: term()} | + {error, Reason :: term()}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called for changing the form and appearance +%% of gen_server status when it is returned from sys:get_status/1,2 +%% or when it appears in termination error logs. +%% @end +%%-------------------------------------------------------------------- +-spec format_status(Opt :: normal | terminate, + Status :: list()) -> Status :: term(). +format_status(_Opt, Status) -> + Status. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/nova_router.erl b/src/nova_router.erl index 97199130..835d5140 100644 --- a/src/nova_router.erl +++ b/src/nova_router.erl @@ -203,6 +203,9 @@ compile_paths([RouteInfo|Tl], Dispatch, Options) -> SCallback end, + %% Add the plugins to the plugin-manager + [ nova_plugin_manager:add_plugin(Plugin) || {_, Plugin, _} <- Plugins ], + Value = #nova_handler_value{secure = Secure, app = App, plugins = normalize_plugins(Plugins), extra_state = maps:get(extra_state, RouteInfo, #{})}, diff --git a/src/nova_sup.erl b/src/nova_sup.erl index c8288a29..64bf92cd 100644 --- a/src/nova_sup.erl +++ b/src/nova_sup.erl @@ -65,6 +65,7 @@ init([]) -> Children = [ child(nova_handlers, nova_handlers), child(SessionManager, SessionManager), + child(nova_plugin_manager, nova_plugin_manager), child(nova_watcher, nova_watcher) ], From 37112d995b7075538de9c500a80399d63c100bb3 Mon Sep 17 00:00:00 2001 From: Niclas Axelsson Date: Tue, 13 Aug 2024 16:02:27 +0200 Subject: [PATCH 2/9] Update callbacks arity for plugins and use map instead of tuple for `plugin_info`-callback --- src/nova_plugin.erl | 46 +++++++++----- src/nova_plugin_manager.erl | 10 ++- src/plugins/nova_correlation_plugin.erl | 29 +++++---- src/plugins/nova_cors_plugin.erl | 61 +++++++++--------- src/plugins/nova_request_plugin.erl | 84 +++++++++++++------------ 5 files changed, 124 insertions(+), 106 deletions(-) diff --git a/src/nova_plugin.erl b/src/nova_plugin.erl index c028f2ce..042ab4b9 100644 --- a/src/nova_plugin.erl +++ b/src/nova_plugin.erl @@ -30,6 +30,10 @@ -type request_type() :: pre_request | post_request. -export_type([request_type/0]). +-type reply() :: {reply, Body :: binary()} | + {reply, Status :: integer(), Body :: binary()} | + {reply, Status :: integer(), Headers :: [{binary(), binary()}], Body :: binary()}. + %% @doc %% Start function for the plugin. This function is called when the plugin is started %% and will return a state that will be passed to the other functions during @@ -50,12 +54,16 @@ %% and the nova-state. It takes a state and a map of options as arguments and should return %% either {ok, NewState}, {break, NewState}, {stop, NewState} or {error, Reason}. %% @end --callback pre_request(State :: nova:state(), Options :: map()) -> - {ok, State0 :: nova:state()} | - {break, State0 :: nova:state()} | - {stop, State0 :: nova:state()} | +-callback pre_request(Req :: cowboy_req:req(), Env :: any(), + Options :: map(), PluginState :: any()) -> + {ok, Req0 :: cowboy_req:req(), NewState :: any()} | + {ok, Reply :: reply(), Req0 :: cowboy_req:req(), NewState :: any()} | + {break, Req0 :: cowboy_req:req(), NewState :: any()} | + {break, Reply :: reply(), Req0 :: cowboy_req:req(), NewState :: any()} | + {stop, Req0 :: cowboy_req:req(), NewState :: any()} | + {stop, Reply :: reply(), Req0 :: cowboy_req:req(), NewState :: any()} | {error, Reason :: term()}. --optional_callbacks([pre_request/2]). +-optional_callbacks([pre_request/4]). %% @doc %% This function is called after the request is processed. It can modify the request. @@ -63,22 +71,26 @@ %% either {ok, NewState}, {break, NewState}, {stop, NewState} or {error, Reason}. %% The state is only used if there's another plugin invoked after this one. %% @end --callback post_request(State :: nova:state(), Options :: map()) -> - {ok, State0 :: nova:state()} | - {break, State0 :: nova:state()} | - {stop, State0 :: nova:state()} | +-callback post_request(Req :: cowboy_req:req(), Env :: any(), + Options :: map(), PluginState :: any()) -> + {ok, Req0 :: cowboy_req:req(), NewState :: any()} | + {ok, Reply :: reply(), Req0 :: cowboy_req:req(), NewState :: any()} | + {break, Req0 :: cowboy_req:req(), NewState :: any()} | + {break, Reply :: reply(), Req0 :: cowboy_req:req(), NewState :: any()} | + {stop, Req0 :: cowboy_req:req(), NewState :: any()} | + {stop, Reply :: reply(), Req0 :: cowboy_req:req(), NewState :: any()} | {error, Reason :: term()}. --optional_callbacks([post_request/2]). +-optional_callbacks([post_request/4]). %% @doc %% This function should return information about the plugin. The information is used %% in the documentation and in the plugin-listing. %% @end --callback plugin_info() -> {Title :: binary(), - Version :: binary(), - Url :: binary(), - Author :: binary(), - Description :: binary(), - Requires :: [PluginName :: binary() | +-callback plugin_info() -> #{title := binary(), + version := binary(), + url := binary(), + authors := [binary()], + description := binary(), + requires => [PluginName :: binary() | {PluginName :: binary(), Version :: binary()}], - Options :: [{Key :: atom(), OptionDescription :: binary()}]}. + options => [{Key :: atom(), OptionDescription :: binary()}]}. diff --git a/src/nova_plugin_manager.erl b/src/nova_plugin_manager.erl index f6bc1fb3..3177238c 100644 --- a/src/nova_plugin_manager.erl +++ b/src/nova_plugin_manager.erl @@ -60,9 +60,7 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). add_plugin(Module) -> - ModuleInfo = Module:plugin_info(), - Title = element(1, ModuleInfo), - Version = element(2, ModuleInfo), + #{title := Title, version := Version} = Module:plugin_info(), add_plugin(Module, Title, Version). add_plugin(Module, Name, Version) -> @@ -118,8 +116,8 @@ init([]) -> {stop, Reason :: term(), NewState :: term()}. handle_call({set_state, Module, NewState}, _From, State) -> case ets:lookup(?TABLE, Module) of - [#plugin{}] -> - ets:insert(?TABLE, #plugin{module = Module, state = NewState}), + [#plugin{} = P] -> + ets:insert(?TABLE, P#plugin{state = NewState}), {reply, ok, State}; [] -> {reply, {error, not_found}, State} @@ -185,7 +183,7 @@ handle_info(_Info, State) -> State :: term()) -> any(). terminate(_Reason, _State) -> %% Stop all plugins and clean state - Plugins = ets:tab2file(?TABLE), + Plugins = ets:tab2list(?TABLE), [ Plugin:stop() || #plugin{module = Plugin} <- Plugins ], ets:delete(?TABLE), ok. diff --git a/src/plugins/nova_correlation_plugin.erl b/src/plugins/nova_correlation_plugin.erl index 7a383814..053de6c3 100644 --- a/src/plugins/nova_correlation_plugin.erl +++ b/src/plugins/nova_correlation_plugin.erl @@ -39,8 +39,8 @@ -module(nova_correlation_plugin). -behaviour(nova_plugin). --export([pre_request/2, - post_request/2, +-export([pre_request/4, + post_request/4, plugin_info/0]). -include_lib("kernel/include/logger.hrl"). @@ -52,25 +52,28 @@ %% @end %%-------------------------------------------------------------------- -pre_request(Req0, Opts) -> +pre_request(Req0, _Env, Opts, State) -> CorrId = get_correlation_id(Req0, Opts), %% Update the loggers metadata with correlation-id ok = update_logger_metadata(CorrId, Opts), Req1 = cowboy_req:set_resp_header(<<"X-Correlation-ID">>, CorrId, Req0), Req = Req1#{correlation_id => CorrId}, - {ok, Req}. + {ok, Req, State}. -post_request(Req, _) -> - {ok, Req}. +post_request(Req, _Env, _, State) -> + {ok, Req, State}. plugin_info() -> - { - <<"nova_correlation_plugin">>, - <<"0.2.0">>, - <<"Nova team >, - <<"Add X-Correlation-ID headers to response">>, - [] - }. + #{ + title => <<"nova_correlation_plugin">>, + version => <<"0.2.0">>, + url => <<"https://github.com/novaframework/nova">>, + authors => [<<"Nova team >], + description => <<"Add X-Correlation-ID headers to response">>, + options => [ + {logger_metadata_key, <<"Under which key should the UUID be put in logger metadata?">>} + ] + }. get_correlation_id(Req, #{ request_correlation_header := CorrelationHeader }) -> case cowboy_req:header(CorrelationHeader, Req) of diff --git a/src/plugins/nova_cors_plugin.erl b/src/plugins/nova_cors_plugin.erl index fa9f4537..d67fbaee 100644 --- a/src/plugins/nova_cors_plugin.erl +++ b/src/plugins/nova_cors_plugin.erl @@ -2,8 +2,8 @@ -behaviour(nova_plugin). -export([ - pre_request/2, - post_request/2, + pre_request/4, + post_request/4, plugin_info/0 ]). @@ -12,54 +12,55 @@ %% Pre-request callback %% @end %%-------------------------------------------------------------------- --spec pre_request(Req :: cowboy_req:req(), Options :: map()) -> - {ok, Req0 :: cowboy_req:req()}. -pre_request(Req, #{allow_origins := Origins}) -> +-spec pre_request(Req :: cowboy_req:req(), Env :: any(), Options :: map(), State :: any()) -> + {ok, Req0 :: cowboy_req:req(), NewState :: any()}. +pre_request(Req, _Env, #{allow_origins := Origins}, State) -> ReqWithOptions = add_cors_headers(Req, Origins), - continue(ReqWithOptions). + continue(ReqWithOptions, State). %%-------------------------------------------------------------------- %% @doc %% Post-request callback %% @end %%-------------------------------------------------------------------- --spec post_request(Req :: cowboy_req:req(), Options :: map()) -> - {ok, Req0 :: cowboy_req:req()}. -post_request(Req, _) -> - {ok, Req}. +-spec post_request(Req :: cowboy_req:req(), Env :: any(), Options :: map(), State :: any()) -> + {ok, Req0 :: cowboy_req:req(), NewState :: any()}. +post_request(Req, _Env, _Opts, State) -> + {ok, Req, State}. %%-------------------------------------------------------------------- %% @doc %% nova_plugin callback. Returns information about the plugin. %% @end %%-------------------------------------------------------------------- --spec plugin_info() -> {Title :: binary(), - Version :: binary(), - Author :: binary(), - Description :: binary(), - Options :: [{Key :: atom(), OptionDescription :: binary()}]}. +-spec plugin_info() -> #{title := binary(), + version := binary(), + url := binary(), + authors := [binary()], + description := binary(), + options := [{Key :: atom(), OptionDescription :: binary()}]}. plugin_info() -> - { - <<"nova_cors_plugin">>, - <<"0.2.0">>, - <<"Nova team >, - <<"Add CORS headers to request">>, - [ - { - allow_origins, - <<"Specifies which origins to insert into Access-Control-Allow-Origin">> - } - ]}. + #{ + title => <<"nova_cors_plugin">>, + version => <<"0.2.0">>, + url => <<"https://github.com/novaframework/nova">>, + authors => [<<"Nova team >], + description => <<"Add CORS headers to request">>, + options => [ + {allow_origins, <<"Specifies which origins to insert into Access-Control-Allow-Origin">>} + ] + }. %%%%%%%%%%%%%%%%%%%%%% %% Private functions %%%%%%%%%%%%%%%%%%%%%% -continue(#{method := <<"OPTIONS">>} = Req) -> +continue(#{method := <<"OPTIONS">>} = Req, State) -> Reply = cowboy_req:reply(200, Req), - {stop, Reply}; -continue(Req) -> - {ok, Req}. + {stop, Reply, State}; +continue(Req, State) -> + {ok, Req, State}. + add_cors_headers(Req, Origins) -> OriginsReq = cowboy_req:set_resp_header( <<"Access-Control-Allow-Origin">>, Origins, Req), diff --git a/src/plugins/nova_request_plugin.erl b/src/plugins/nova_request_plugin.erl index 7467d8be..e0707457 100644 --- a/src/plugins/nova_request_plugin.erl +++ b/src/plugins/nova_request_plugin.erl @@ -2,8 +2,8 @@ -behaviour(nova_plugin). -export([ - pre_request/2, - post_request/2, + pre_request/4, + post_request/4, plugin_info/0 ]). @@ -12,28 +12,29 @@ %% Pre-request callback %% @end %%-------------------------------------------------------------------- --spec pre_request(Req :: cowboy_req:req(), Options :: map()) -> - {ok, Req0 :: cowboy_req:req()}. -pre_request(Req, Options) -> +-spec pre_request(Req :: cowboy_req:req(), Env :: any(), Options :: map(), State :: any()) -> + {ok, Req0 :: cowboy_req:req(), NewState :: any()}. +pre_request(Req, _Env, Options, State) -> ListOptions = maps:to_list(Options), %% Read the body and put it into the Req object - BodyReq = case should_read_body(ListOptions) andalso cowboy_req:has_body(Req) of + BodyReq = case should_read_body(ListOptions) andalso + cowboy_req:has_body(Req) of true -> read_body(Req, <<>>); false -> Req#{body => <<>>} end, - modulate_state(BodyReq, ListOptions). + modulate_state(BodyReq, ListOptions, State). %%-------------------------------------------------------------------- %% @doc %% Post-request callback %% @end %%-------------------------------------------------------------------- --spec post_request(Req :: cowboy_req:req(), Options :: map()) -> - {ok, Req0 :: cowboy_req:req()}. -post_request(Req, _Options) -> - {ok, Req}. +-spec post_request(Req :: cowboy_req:req(), Env :: any(), Options :: map(), State :: any()) -> + {ok, Req0 :: cowboy_req:req(), NewState :: any()}. +post_request(Req, _Env, _Options, State) -> + {ok, Req, State}. %%-------------------------------------------------------------------- @@ -41,65 +42,68 @@ post_request(Req, _Options) -> %% nova_plugin callback. Returns information about the plugin. %% @end %%-------------------------------------------------------------------- --spec plugin_info() -> {Title :: binary(), - Version :: binary(), - Author :: binary(), - Description :: binary(), - Options :: [{Key :: atom(), OptionDescription :: binary()}]}. +-spec plugin_info() -> #{title := binary(), + version := binary(), + url := binary(), + authors := [binary()], + description := binary(), + options := [{Key :: atom(), OptionDescription :: binary()}]}. plugin_info() -> - {<<"Nova body plugin">>, - <<"0.0.1">>, - <<"Nova team >, - <<"This plugin modulates the body of a request.">>, - [ - {decode_json_body, <<"Decodes the body as JSON and puts it under `json`">>}, - {read_urlencoded_body, <<"Used to parse body as query-string and put them in state under `qs` key">>} - ]}. + #{title => <<"Nova body plugin">>, + version => <<"0.0.1">>, + url => <<"https://github.com/novaframework/nova">>, + authors => [<<"Nova team >], + description => <<"This plugin modulates the body of a request.">>, + options => [ + {decode_json_body, <<"Decodes the body as JSON and puts it under `json`">>}, + {read_urlencoded_body, <<"Used to parse body as query-string and put them in state under `qs` key">>} + ] + }. %%%%%%%%%%%%%%%%%%%%%% %% Private functions %%%%%%%%%%%%%%%%%%%%%% -modulate_state(Req, []) -> - {ok, Req}; +modulate_state(Req, [], State) -> + {ok, Req, State}; -modulate_state( Req = #{method := Method}, [{decode_json_body, true}|Tail]) when Method =:= <<"GET">>; Method =:= <<"DELETE">> -> - modulate_state(Req, Tail); -modulate_state(Req = #{headers := #{<<"content-type">> := <<"application/json", _/binary>>}, body := <<>>}, [{decode_json_body, true}|_Tl]) -> +modulate_state( Req = #{method := Method}, [{decode_json_body, true}|Tail], State) when Method =:= <<"GET">>; Method =:= <<"DELETE">> -> + modulate_state(Req, Tail, State); +modulate_state(Req = #{headers := #{<<"content-type">> := <<"application/json", _/binary>>}, body := <<>>}, [{decode_json_body, true}|_Tl], State) -> Req400 = cowboy_req:reply(400, Req), logger:warning(#{status_code => 400, msg => "Failed to decode json.", error => "No body to decode."}), - {stop, Req400}; -modulate_state(Req = #{headers := #{<<"content-type">> := <<"application/json", _/binary>>}, body := Body}, [{decode_json_body, true}|Tl]) -> + {stop, Req400, State}; +modulate_state(Req = #{headers := #{<<"content-type">> := <<"application/json", _/binary>>}, body := Body}, [{decode_json_body, true}|Tl], State) -> %% Decode the data JsonLib = nova:get_env(json_lib, thoas), case erlang:apply(JsonLib, decode, [Body]) of {ok, JSON} -> - modulate_state(Req#{json => JSON}, Tl); + modulate_state(Req#{json => JSON}, Tl, State); Error -> Req400 = cowboy_req:reply(400, Req), logger:warning(#{status_code => 400, msg => "Failed to decode json.", error => Error}), - {stop, Req400} + {stop, Req400, State} end; modulate_state(#{headers := #{<<"content-type">> := <<"application/x-www-form-urlencoded", _/binary>>}, body := Body} = Req, - [{read_urlencoded_body, true}|Tl]) -> + [{read_urlencoded_body, true}|Tl], State) -> Data = cow_qs:parse_qs(Body), %% First read in the body Params = maps:from_list(Data), - modulate_state(Req#{params => Params}, Tl); -modulate_state(Req, [{parse_qs, Type}|T1]) -> + modulate_state(Req#{params => Params}, Tl, State); +modulate_state(Req, [{parse_qs, Type}|T1], State) -> Qs = cowboy_req:parse_qs(Req), case Type of true -> MapQs = maps:from_list(Qs), - modulate_state(Req#{parsed_qs => MapQs}, T1); - list -> modulate_state(Req#{parsed_qs => Qs}, T1) + modulate_state(Req#{parsed_qs => MapQs}, T1, State); + list -> modulate_state(Req#{parsed_qs => Qs}, T1, State) end; -modulate_state(State, [_|Tl]) -> - modulate_state(State, Tl). +modulate_state(Req, [_|Tl], State) -> + modulate_state(Req, Tl, State). read_body(Req, Acc) -> case cowboy_req:read_body(Req) of From f58acdb67daf6081a77c7a0a8fc39a4531bb8226 Mon Sep 17 00:00:00 2001 From: Niclas Axelsson Date: Tue, 13 Aug 2024 16:13:32 +0200 Subject: [PATCH 3/9] Add documentation to the plugins --- guides/plugins.md | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/guides/plugins.md b/guides/plugins.md index c3421cbe..ba6743c8 100644 --- a/guides/plugins.md +++ b/guides/plugins.md @@ -5,32 +5,60 @@ These can be used to create access logs, insert CORS headers or similar. Plugins are used to handle things before and/or after a request. They are applied on all requests of a specified protocol. +A rule of thumb is that if you want to store a short-lived state (For the local request) then you could store that under the Env-object. If you want to store a long-lived state (For the global request) then you could store that under the State-object which is initialized in the optional `init/0` callback and (potentially) updated in each call to `pre_request/4` and `post_request/4`. The long-lived state is only applicable to the plugin-module while the local state belongs to the request and is distributed to each invoked plugin. + This is an example: ```erlang -module(correlation_id). -behaviour(nova_plugin). -export([ - pre_request/2, - post_request/2, + pre_request/4, + post_request/4, plugin_info/0 ]). -pre_request(Req, NovaState) -> +pre_request(Req, _Env, _Opts, State) -> UUID = uuid:uuid_to_string(uuid:get_v4()), - {ok, cowboy_req:set_resp_header(<<"x-correlation-id">>, UUID, Req), NovaState}. + {ok, cowboy_req:set_resp_header(<<"x-correlation-id">>, UUID, Req), State}. -post_request(Req, NovaState) -> - {ok, Req, NovaState}. +post_request(Req, _Env, _Opts, State) -> + {ok, Req, State}. plugin_info() -> - {<<"Correlation plugin">>, <<"1.0.0">>, <<"Niclas Axelsson ">>, - <<"Example plugin for nova">>}. + #{ + title => <<"nova_cors_plugin">>, + version => <<"0.2.0">>, + url => <<"https://github.com/novaframework/nova">>, + authors => [<<"Nova team >], + description => <<"Add CORS headers to request">>, + options => [ + ] + }. ``` This plugin injects a UUID into the headers. +## Optional callbacks + +There's two optional callbacks for a plugin that can be used for storing a global long-lived state. This can be useful if you want to keep track of something like a `pid` or similar. +The first callback is `init/0` and the second is `stop/1`. `init/0` is called when the plugin is loaded and `stop/1` is called when the plugin is unloaded (Usually when the nova-application is started/stopped). +Following example shows how we spawn a process and returns the pid in a map - this map will become our new state. When the plugin is stopped, `stop/1` will get invoked and we will send a stop message to the pid. + +```erlang +init() -> + %% Setup some pids + Pid = spawn(fun() -> ok end), + #{my_pid => Pid}. + +stop(State) -> + %% Stop the pids + Pid = maps:get(my_pid, State), + Pid ! stop, + ok. +``` + Adding a plugin Example: From 26a0fd5c11ed19df962a625678b1dd6021330127 Mon Sep 17 00:00:00 2001 From: Niclas Axelsson Date: Thu, 15 Aug 2024 23:10:38 +0200 Subject: [PATCH 4/9] Remove call to plugin-manager here since it have not yet been started --- src/nova_router.erl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/nova_router.erl b/src/nova_router.erl index 835d5140..97199130 100644 --- a/src/nova_router.erl +++ b/src/nova_router.erl @@ -203,9 +203,6 @@ compile_paths([RouteInfo|Tl], Dispatch, Options) -> SCallback end, - %% Add the plugins to the plugin-manager - [ nova_plugin_manager:add_plugin(Plugin) || {_, Plugin, _} <- Plugins ], - Value = #nova_handler_value{secure = Secure, app = App, plugins = normalize_plugins(Plugins), extra_state = maps:get(extra_state, RouteInfo, #{})}, From e04abd06fe34fa8d67fd813ef2f818affa30135a Mon Sep 17 00:00:00 2001 From: Niclas Axelsson Date: Thu, 15 Aug 2024 23:22:28 +0200 Subject: [PATCH 5/9] Make the plugin manager a bit more efficient --- src/nova_plugin_manager.erl | 57 ++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/nova_plugin_manager.erl b/src/nova_plugin_manager.erl index 3177238c..3d280b7a 100644 --- a/src/nova_plugin_manager.erl +++ b/src/nova_plugin_manager.erl @@ -64,18 +64,31 @@ add_plugin(Module) -> add_plugin(Module, Title, Version). add_plugin(Module, Name, Version) -> - gen_server:call(?SERVER, {add_plugin, Module, Name, Version}). + case ets:lookup(?TABLE, Module) of + [#plugin{}] -> + ?LOG_DEBUG("Plugin ~p already initialized.", [Module]), + ok; + [] -> + gen_server:cast(?SERVER, {add_plugin, Module, Name, Version}) + end. get_state(Module) -> case ets:lookup(?TABLE, Module) of [#plugin{state = State}] -> {ok, State}; _ -> + ?LOG_DEBUG("Plugin ~p not found. get_state/1 failed.", [Module]), {error, not_found} end. set_state(Module, NewState) -> - gen_server:call(?SERVER, {set_state, Module, NewState}). + case ets:lookup(?TABLE, Module) of + [#plugin{} = P] -> + gen_server:call(?SERVER, {set_state, Module, P, NewState}); + _ -> + ?LOG_DEBUG("Plugin ~p not found. set_state/2 failed.", [Module]), + {error, not_found} + end. %%%=================================================================== %%% gen_server callbacks @@ -114,30 +127,10 @@ init([]) -> {noreply, NewState :: term(), hibernate} | {stop, Reason :: term(), Reply :: term(), NewState :: term()} | {stop, Reason :: term(), NewState :: term()}. -handle_call({set_state, Module, NewState}, _From, State) -> - case ets:lookup(?TABLE, Module) of - [#plugin{} = P] -> - ets:insert(?TABLE, P#plugin{state = NewState}), - {reply, ok, State}; - [] -> - {reply, {error, not_found}, State} - end; -handle_call({add_plugin, Module, Name, Version}, _From, State) -> - case ets:lookup(?TABLE, Module) of - [#plugin{}] -> - {reply, ok, State}; - [] -> - PluginState = - case erlang:function_exported(Module, init, 0) of - true -> - ?LOG_INFO("Initializing plugin ~p", [Module]), - Module:init(); - _ -> - undefined - end, - ets:insert(?TABLE, #plugin{module = Module, name = Name, version = Version, state = PluginState}), - {reply, ok, State} - end; +handle_call({set_state, Module, P, NewState}, _From, State) -> + ?LOG_DEBUG("Set state for plugin ~p; NewState: ~p", [Module, NewState]), + ets:insert(?TABLE, P#plugin{state = NewState}), + {reply, ok, State}; handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. @@ -153,6 +146,18 @@ handle_call(_Request, _From, State) -> {noreply, NewState :: term(), Timeout :: timeout()} | {noreply, NewState :: term(), hibernate} | {stop, Reason :: term(), NewState :: term()}. +handle_cast({add_plugin, Module, Name, Version}, State) -> + PluginState = + case erlang:function_exported(Module, init, 0) of + true -> + ?LOG_INFO("Initializing plugin ~p", [Module]), + Module:init(); + _ -> + ?LOG_DEBUG("Plugin ~p has no init/0 function", [Module]), + undefined + end, + ets:insert(?TABLE, #plugin{module = Module, name = Name, version = Version, state = PluginState}), + {noreply, State}; handle_cast(_Request, State) -> {noreply, State}. From aa2ca5c38eace2bdac41e9761e2d3fe874604205 Mon Sep 17 00:00:00 2001 From: Niclas Axelsson Date: Tue, 20 Aug 2024 10:46:52 +0200 Subject: [PATCH 6/9] Add support for route-specific plugins --- src/nova_plugin_manager.erl | 5 ++++- src/nova_router.erl | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/nova_plugin_manager.erl b/src/nova_plugin_manager.erl index 3d280b7a..cfd244af 100644 --- a/src/nova_plugin_manager.erl +++ b/src/nova_plugin_manager.erl @@ -59,6 +59,8 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). +add_plugin({_Class, Module, _Opts}) -> + add_plugin(Module); add_plugin(Module) -> #{title := Title, version := Version} = Module:plugin_info(), add_plugin(Module, Title, Version). @@ -189,7 +191,8 @@ handle_info(_Info, State) -> terminate(_Reason, _State) -> %% Stop all plugins and clean state Plugins = ets:tab2list(?TABLE), - [ Plugin:stop() || #plugin{module = Plugin} <- Plugins ], + [ Plugin:stop() || #plugin{module = Plugin} <- Plugins, + erlang:function_exported(Plugin, stop, 0) ], ets:delete(?TABLE), ok. diff --git a/src/nova_router.erl b/src/nova_router.erl index 97199130..6074f127 100644 --- a/src/nova_router.erl +++ b/src/nova_router.erl @@ -14,7 +14,6 @@ %% API -export([ - compiled_apps/0, compile/1, lookup_url/1, lookup_url/2, @@ -27,7 +26,11 @@ routes/1, %% Modulates the routes-table - add_routes/2 + add_routes/2, + + %% Fetch information about the routing table + plugins/0, + compiled_apps/0 ]). -include_lib("routing_tree/include/routing_tree.hrl"). @@ -43,12 +46,17 @@ -define(NOVA_APPS, nova_apps). +-define(NOVA_PLUGINS, nova_plugins). -spec compiled_apps() -> [{App :: atom(), Prefix :: list()}]. compiled_apps() -> StorageBackend = application:get_env(nova, dispatch_backend, persistent_term), StorageBackend:get(?NOVA_APPS, []). +plugins() -> + StorageBackend = application:get_env(nova, dispatch_backend, persistent_term), + StorageBackend:get(?NOVA_PLUGINS, []). + -spec compile(Apps :: [atom() | {atom(), map()}]) -> host_tree(). compile(Apps) -> UseStrict = application:get_env(nova, use_strict_routing, false), @@ -178,7 +186,9 @@ compile([App|Tl], Dispatch, Options) -> %% Take out the prefix for the app and store it in the persistent store StorageBackend = application:get_env(nova, dispatch_backend, persistent_term), + CompiledApps = StorageBackend:get(?NOVA_APPS, []), + CompiledApps0 = [{App, maps:get(prefix, Options, "/")}|CompiledApps], StorageBackend:put(?NOVA_APPS, CompiledApps0), @@ -214,6 +224,8 @@ compile_paths([RouteInfo|Tl], Dispatch, Options) -> NovaEnv0 = [{App, #{prefix => Prefix}} | NovaEnv], nova:set_env(apps, NovaEnv0), + add_plugins(Plugins), + {ok, Dispatch1} = parse_url(Host, maps:get(routes, RouteInfo, []), Prefix, Value, Dispatch), Dispatch2 = compile(SubApps, Dispatch1, Options#{value => Value, prefix => Prefix}), @@ -387,6 +399,13 @@ insert(Host, Path, Combinator, Value, Tree) -> end. +add_plugins(Plugins) -> + Plugins0 = [ Plugin || {_, Plugin} <- Plugins], + StorageBackend = application:get_env(nova, dispatch_backend, persistent_term), + StoredPlugins = StorageBackend:get(?NOVA_PLUGINS, []), + Plugins1 = lists:umerge([Plugins0, StoredPlugins]), + StorageBackend:put(?NOVA_PLUGINS, Plugins1). + normalize_plugins(Plugins) -> NormalizedPlugins = normalize_plugins(Plugins, []), [{Type, lists:reverse(TypePlugins)} || {Type, TypePlugins} <- NormalizedPlugins]. From 2883f6f8789b0f29b3f493181b9ba86f9878ba9a Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Thu, 5 Sep 2024 20:40:14 +0200 Subject: [PATCH 7/9] fixed dialyzer and removed some styling from nova error --- src/nova_router.erl | 2 +- src/views/nova_error.dtl | 57 ---------------------------------------- 2 files changed, 1 insertion(+), 58 deletions(-) diff --git a/src/nova_router.erl b/src/nova_router.erl index 6074f127..7402fa5d 100644 --- a/src/nova_router.erl +++ b/src/nova_router.erl @@ -400,7 +400,7 @@ insert(Host, Path, Combinator, Value, Tree) -> add_plugins(Plugins) -> - Plugins0 = [ Plugin || {_, Plugin} <- Plugins], + Plugins0 = [ Plugin || {_, Plugin, _} <- Plugins], StorageBackend = application:get_env(nova, dispatch_backend, persistent_term), StoredPlugins = StorageBackend:get(?NOVA_PLUGINS, []), Plugins1 = lists:umerge([Plugins0, StoredPlugins]), diff --git a/src/views/nova_error.dtl b/src/views/nova_error.dtl index 2888fe18..c46f4388 100644 --- a/src/views/nova_error.dtl +++ b/src/views/nova_error.dtl @@ -8,57 +8,6 @@ overflow: hidden; } - #stars { - width: 1px; - height: 1px; - background: transparent; - box-shadow: 640px 1696px #FFF , 1160px 1058px #FFF , 1227px 156px #FFF , 1138px 1418px #FFF , 1727px 329px #FFF , 37px 279px #FFF , 1417px 1804px #FFF , 1837px 1261px #FFF , 856px 1149px #FFF , 43px 510px #FFF , 1140px 520px #FFF , 1356px 354px #FFF , 479px 1893px #FFF , 117px 1184px #FFF , 1637px 653px #FFF , 1079px 1938px #FFF , 987px 484px #FFF , 1574px 465px #FFF , 1961px 1762px #FFF , 738px 418px #FFF , 51px 1272px #FFF , 158px 859px #FFF , 1797px 503px #FFF , 83px 253px #FFF , 592px 1108px #FFF , 1832px 392px #FFF , 679px 1536px #FFF , 703px 1941px #FFF , 1932px 770px #FFF , 1755px 802px #FFF , 1643px 1784px #FFF , 1967px 163px #FFF , 1063px 1983px #FFF , 1509px 880px #FFF , 1891px 651px #FFF , 1832px 1644px #FFF , 1260px 638px #FFF , 453px 450px #FFF , 1977px 1782px #FFF , 208px 1856px #FFF , 1941px 1343px #FFF , 894px 872px #FFF , 936px 1444px #FFF , 503px 438px #FFF , 118px 1556px #FFF , 1591px 883px #FFF , 940px 1674px #FFF , 76px 1676px #FFF , 528px 938px #FFF , 772px 359px #FFF , 1934px 731px #FFF , 1787px 72px #FFF , 1445px 780px #FFF , 677px 1306px #FFF , 1021px 900px #FFF , 199px 343px #FFF , 1194px 1524px #FFF , 75px 1700px #FFF , 1323px 1400px #FFF , 712px 1806px #FFF , 136px 803px #FFF , 842px 299px #FFF , 138px 570px #FFF , 245px 975px #FFF , 1305px 1210px #FFF , 1615px 164px #FFF , 1710px 568px #FFF , 494px 397px #FFF , 598px 286px #FFF , 887px 1775px #FFF , 293px 1063px #FFF , 189px 49px #FFF , 1275px 199px #FFF , 693px 1163px #FFF , 1186px 322px #FFF , 527px 807px #FFF , 1412px 1246px #FFF , 1397px 843px #FFF , 1860px 480px #FFF , 847px 777px #FFF , 431px 917px #FFF , 487px 1662px #FFF , 1226px 1930px #FFF , 1109px 987px #FFF , 1110px 162px #FFF , 486px 1510px #FFF , 1364px 1018px #FFF , 255px 1488px #FFF , 1960px 1688px #FFF , 457px 1263px #FFF , 1202px 502px #FFF , 1206px 1974px #FFF , 1130px 1127px #FFF , 1164px 383px #FFF , 893px 1646px #FFF , 1575px 1292px #FFF , 600px 1489px #FFF , 348px 591px #FFF , 915px 1754px #FFF , 825px 757px #FFF , 379px 1727px #FFF , 573px 1729px #FFF , 1555px 1569px #FFF , 293px 512px #FFF , 1743px 470px #FFF , 1872px 1789px #FFF , 1535px 564px #FFF , 732px 92px #FFF , 1114px 777px #FFF , 1655px 1514px #FFF , 1781px 1308px #FFF , 1204px 1545px #FFF , 1068px 895px #FFF , 1956px 275px #FFF , 157px 1972px #FFF , 189px 254px #FFF , 1644px 65px #FFF , 324px 1851px #FFF , 54px 588px #FFF , 1183px 1600px #FFF , 1781px 1990px #FFF , 1403px 781px #FFF , 55px 102px #FFF , 335px 1225px #FFF , 934px 1000px #FFF , 642px 1489px #FFF , 430px 170px #FFF , 1708px 1737px #FFF , 563px 1802px #FFF , 702px 1114px #FFF , 1821px 487px #FFF , 1874px 267px #FFF , 466px 305px #FFF , 1622px 1459px #FFF , 50px 1693px #FFF , 1320px 64px #FFF , 1476px 1404px #FFF , 20px 415px #FFF , 1336px 1189px #FFF , 1812px 1978px #FFF , 1125px 1079px #FFF , 523px 47px #FFF , 515px 198px #FFF , 932px 1496px #FFF , 484px 519px #FFF , 1914px 1435px #FFF , 181px 1958px #FFF , 87px 103px #FFF , 911px 1200px #FFF , 709px 360px #FFF , 1588px 601px #FFF , 1964px 709px #FFF , 1348px 36px #FFF , 938px 397px #FFF , 617px 1508px #FFF , 28px 193px #FFF , 1102px 1507px #FFF , 350px 1562px #FFF , 1347px 1130px #FFF , 1747px 1912px #FFF , 41px 1624px #FFF , 1132px 950px #FFF , 651px 280px #FFF , 528px 686px #FFF , 796px 634px #FFF , 268px 172px #FFF , 316px 1330px #FFF , 282px 246px #FFF , 1148px 975px #FFF , 939px 1625px #FFF , 894px 1699px #FFF , 1467px 477px #FFF , 587px 1010px #FFF , 249px 1727px #FFF , 194px 862px #FFF , 1644px 1984px #FFF , 1787px 167px #FFF , 1027px 1014px #FFF , 880px 538px #FFF , 214px 329px #FFF , 666px 1285px #FFF , 821px 911px #FFF , 933px 1116px #FFF , 1962px 1543px #FFF , 626px 358px #FFF , 639px 1717px #FFF , 684px 517px #FFF , 377px 1249px #FFF , 1607px 282px #FFF , 1430px 64px #FFF , 734px 479px #FFF , 1483px 1205px #FFF , 725px 1645px #FFF , 369px 1102px #FFF , 504px 1061px #FFF , 1781px 159px #FFF , 158px 112px #FFF , 1457px 348px #FFF , 224px 1845px #FFF , 288px 1825px #FFF , 1265px 259px #FFF , 1834px 274px #FFF , 1125px 1175px #FFF , 1000px 413px #FFF , 589px 1735px #FFF , 1984px 1363px #FFF , 1430px 1122px #FFF , 1350px 1546px #FFF , 133px 1628px #FFF , 1099px 1705px #FFF , 1063px 806px #FFF , 993px 1810px #FFF , 657px 76px #FFF , 825px 388px #FFF , 1826px 1709px #FFF , 400px 1603px #FFF , 678px 1545px #FFF , 411px 1643px #FFF , 704px 354px #FFF , 1487px 1570px #FFF , 511px 1856px #FFF , 1311px 97px #FFF , 1666px 1888px #FFF , 1812px 921px #FFF , 1163px 1522px #FFF , 930px 1689px #FFF , 1061px 1627px #FFF , 1318px 1328px #FFF , 775px 1787px #FFF , 1557px 1977px #FFF , 854px 579px #FFF , 1114px 1666px #FFF , 867px 1184px #FFF , 739px 561px #FFF , 1138px 1189px #FFF , 1810px 1425px #FFF , 17px 642px #FFF , 805px 204px #FFF , 1207px 569px #FFF , 976px 797px #FFF , 1786px 657px #FFF , 1915px 478px #FFF , 1895px 1432px #FFF , 695px 39px #FFF , 445px 106px #FFF , 104px 285px #FFF , 734px 1319px #FFF , 1072px 1125px #FFF , 1749px 708px #FFF , 264px 1983px #FFF , 317px 814px #FFF , 45px 1978px #FFF , 398px 120px #FFF , 1477px 726px #FFF , 1655px 614px #FFF , 934px 1475px #FFF , 346px 1843px #FFF , 291px 454px #FFF , 889px 1563px #FFF , 1114px 417px #FFF , 1993px 1178px #FFF , 345px 259px #FFF , 271px 416px #FFF , 1858px 1431px #FFF , 1332px 1068px #FFF , 1080px 646px #FFF , 30px 1378px #FFF , 1476px 1174px #FFF , 585px 1452px #FFF , 855px 1934px #FFF , 1148px 1666px #FFF , 1250px 386px #FFF , 682px 760px #FFF , 691px 162px #FFF , 1149px 1211px #FFF , 1528px 384px #FFF , 1307px 334px #FFF , 322px 1427px #FFF , 1422px 1505px #FFF , 1479px 1870px #FFF , 764px 1000px #FFF , 27px 92px #FFF , 385px 326px #FFF , 3px 1911px #FFF , 1521px 546px #FFF , 438px 658px #FFF , 1605px 1637px #FFF , 326px 198px #FFF , 999px 716px #FFF , 1559px 927px #FFF , 1757px 1298px #FFF , 334px 1472px #FFF , 650px 817px #FFF , 1094px 116px #FFF , 1063px 1822px #FFF , 1480px 831px #FFF , 676px 1556px #FFF , 1619px 618px #FFF , 125px 211px #FFF , 553px 1554px #FFF , 1778px 863px #FFF , 1079px 1074px #FFF , 952px 462px #FFF , 707px 38px #FFF , 1125px 307px #FFF , 88px 1605px #FFF , 1388px 1217px #FFF , 776px 861px #FFF , 97px 1949px #FFF , 438px 250px #FFF , 731px 556px #FFF , 41px 652px #FFF , 544px 1111px #FFF , 1944px 1951px #FFF , 1421px 758px #FFF , 694px 1908px #FFF , 914px 400px #FFF , 745px 92px #FFF , 240px 1108px #FFF , 1428px 19px #FFF , 778px 1858px #FFF , 41px 587px #FFF , 1450px 765px #FFF , 1514px 700px #FFF , 1170px 827px #FFF , 420px 81px #FFF , 974px 576px #FFF , 1637px 426px #FFF , 699px 527px #FFF , 1360px 1251px #FFF , 1118px 1339px #FFF , 1542px 1298px #FFF , 1271px 1895px #FFF , 1976px 1893px #FFF , 529px 1743px #FFF , 1195px 597px #FFF , 1116px 594px #FFF , 686px 462px #FFF , 462px 1238px #FFF , 993px 122px #FFF , 1053px 871px #FFF , 1112px 1762px #FFF , 1217px 781px #FFF , 1591px 953px #FFF , 1516px 1984px #FFF , 1475px 1596px #FFF , 1740px 821px #FFF , 1438px 1074px #FFF , 655px 864px #FFF , 290px 766px #FFF , 1051px 827px #FFF , 1523px 1260px #FFF , 1070px 729px #FFF , 1425px 1298px #FFF , 624px 55px #FFF , 234px 275px #FFF , 1577px 384px #FFF , 802px 388px #FFF , 1327px 1695px #FFF , 1079px 798px #FFF , 806px 339px #FFF , 79px 1494px #FFF , 1585px 1431px #FFF , 642px 379px #FFF , 1668px 1502px #FFF , 9px 376px #FFF , 373px 683px #FFF , 1223px 1525px #FFF , 357px 1438px #FFF , 1916px 614px #FFF , 1586px 789px #FFF , 1964px 1973px #FFF , 1133px 1023px #FFF , 388px 931px #FFF , 1008px 1247px #FFF , 496px 1833px #FFF , 1260px 1118px #FFF , 1945px 256px #FFF , 812px 1017px #FFF , 536px 1524px #FFF , 1550px 1819px #FFF , 1774px 1333px #FFF , 1115px 1429px #FFF , 1396px 675px #FFF , 81px 1883px #FFF , 863px 984px #FFF , 422px 1462px #FFF , 641px 1426px #FFF , 1587px 833px #FFF , 1697px 1489px #FFF , 1043px 1566px #FFF , 197px 544px #FFF , 1098px 1442px #FFF , 1832px 1396px #FFF , 247px 1225px #FFF , 494px 766px #FFF , 437px 1162px #FFF , 1590px 1264px #FFF , 1672px 1977px #FFF , 1710px 136px #FFF , 1209px 936px #FFF , 1260px 264px #FFF , 117px 863px #FFF , 795px 1420px #FFF , 84px 81px #FFF , 1493px 378px #FFF , 1533px 1281px #FFF , 199px 1398px #FFF , 960px 1515px #FFF , 193px 266px #FFF , 694px 399px #FFF , 1298px 297px #FFF , 1139px 1379px #FFF , 568px 157px #FFF , 380px 877px #FFF , 1670px 515px #FFF , 1304px 80px #FFF , 602px 1160px #FFF , 991px 824px #FFF , 361px 572px #FFF , 1216px 709px #FFF , 1128px 198px #FFF , 191px 146px #FFF , 700px 1921px #FFF , 699px 1727px #FFF , 674px 986px #FFF , 166px 721px #FFF , 204px 265px #FFF , 856px 1329px #FFF , 1662px 1559px #FFF , 360px 908px #FFF , 1313px 1722px #FFF , 1780px 245px #FFF , 1433px 1242px #FFF , 1892px 1433px #FFF , 1323px 1805px #FFF , 295px 1555px #FFF , 564px 133px #FFF , 919px 1841px #FFF , 1352px 54px #FFF , 1885px 135px #FFF , 277px 1739px #FFF , 776px 1592px #FFF , 992px 1589px #FFF , 1073px 1027px #FFF , 842px 226px #FFF , 1193px 628px #FFF , 879px 1899px #FFF , 802px 1413px #FFF , 1417px 1335px #FFF , 348px 1318px #FFF , 724px 935px #FFF , 306px 559px #FFF , 1389px 1210px #FFF , 1085px 1449px #FFF , 1718px 1967px #FFF , 683px 1159px #FFF , 293px 1188px #FFF , 628px 476px #FFF , 652px 733px #FFF , 1936px 866px #FFF , 1903px 1011px #FFF , 977px 1294px #FFF , 346px 1401px #FFF , 864px 1013px #FFF , 1769px 107px #FFF , 1982px 1176px #FFF , 1867px 143px #FFF , 1997px 1582px #FFF , 1476px 593px #FFF , 128px 285px #FFF , 1140px 58px #FFF , 1026px 1972px #FFF , 1859px 1974px #FFF , 922px 64px #FFF , 1712px 582px #FFF , 1076px 638px #FFF , 362px 105px #FFF , 120px 83px #FFF , 610px 359px #FFF , 511px 456px #FFF , 1476px 602px #FFF , 269px 1775px #FFF , 272px 1625px #FFF , 162px 1864px #FFF , 1237px 817px #FFF , 1939px 507px #FFF , 1821px 1741px #FFF , 1603px 1234px #FFF , 562px 80px #FFF , 106px 880px #FFF , 1701px 586px #FFF , 1422px 908px #FFF , 1802px 1966px #FFF , 459px 1648px #FFF , 612px 1599px #FFF , 323px 1672px #FFF , 320px 716px #FFF , 807px 1441px #FFF , 1367px 1731px #FFF , 1501px 1379px #FFF , 1980px 1548px #FFF , 743px 747px #FFF , 1965px 951px #FFF , 1717px 697px #FFF , 1447px 1703px #FFF , 1100px 869px #FFF , 943px 1232px #FFF , 279px 1655px #FFF , 1428px 1289px #FFF , 365px 1825px #FFF , 1951px 526px #FFF , 1166px 1068px #FFF , 1802px 456px #FFF , 57px 381px #FFF , 837px 852px #FFF , 1114px 467px #FFF , 1007px 1832px #FFF , 174px 1221px #FFF , 195px 188px #FFF , 1823px 438px #FFF , 984px 917px #FFF , 1424px 530px #FFF , 272px 244px #FFF , 1386px 1239px #FFF , 4px 1442px #FFF , 1129px 1376px #FFF , 265px 1527px #FFF , 1003px 718px #FFF , 993px 75px #FFF , 1820px 1655px #FFF , 758px 282px #FFF , 934px 1108px #FFF , 1492px 1687px #FFF , 1245px 1344px #FFF , 110px 1810px #FFF , 1988px 1713px #FFF , 1386px 1883px #FFF , 1781px 1461px #FFF , 361px 563px #FFF , 474px 1150px #FFF , 185px 1218px #FFF , 793px 1513px #FFF , 1474px 1117px #FFF , 221px 295px #FFF , 1445px 1859px #FFF , 903px 972px #FFF , 1420px 139px #FFF , 1477px 1084px #FFF , 373px 1554px #FFF , 1355px 1247px #FFF , 1483px 48px #FFF , 158px 1684px #FFF , 145px 963px #FFF , 213px 841px #FFF , 634px 17px #FFF , 128px 1911px #FFF , 1711px 1125px #FFF , 906px 1253px #FFF , 1158px 433px #FFF , 1544px 1045px #FFF , 506px 1388px #FFF , 1261px 1555px #FFF , 876px 286px #FFF , 1042px 815px #FFF , 1870px 715px #FFF , 808px 1262px #FFF , 1777px 1838px #FFF , 470px 953px #FFF , 99px 469px #FFF , 1631px 896px #FFF , 676px 1877px #FFF , 805px 324px #FFF , 1966px 1784px #FFF , 893px 520px #FFF , 738px 1217px #FFF , 1064px 39px #FFF , 1284px 351px #FFF , 626px 1902px #FFF , 1272px 1574px #FFF , 62px 1290px #FFF , 43px 1931px #FFF , 449px 1603px #FFF , 442px 1841px #FFF , 933px 1952px #FFF , 1120px 311px #FFF , 1800px 1824px #FFF , 1736px 1738px #FFF , 1656px 3px #FFF , 1709px 359px #FFF , 559px 42px #FFF , 1404px 730px #FFF , 356px 179px #FFF , 1267px 1929px #FFF , 1629px 1547px #FFF , 115px 957px #FFF , 336px 1565px #FFF , 1727px 232px #FFF , 578px 1538px #FFF , 148px 161px #FFF , 704px 722px #FFF , 1331px 1899px #FFF , 571px 241px #FFF , 386px 810px #FFF , 934px 622px #FFF , 1006px 838px #FFF , 117px 1847px #FFF , 1850px 583px #FFF , 355px 258px #FFF , 1715px 144px #FFF , 1763px 1679px #FFF , 1072px 374px #FFF , 237px 1190px #FFF , 1034px 571px #FFF , 321px 1904px #FFF , 1732px 743px #FFF , 1352px 530px #FFF , 1260px 1611px #FFF , 1930px 192px #FFF , 1660px 611px #FFF , 163px 1627px #FFF , 311px 451px #FFF , 64px 180px #FFF , 193px 1528px #FFF , 1630px 687px #FFF , 1804px 577px #FFF , 786px 1702px #FFF , 1568px 127px #FFF , 817px 1712px #FFF , 1493px 647px #FFF , 266px 22px #FFF , 191px 1177px #FFF , 1408px 106px #FFF , 1257px 534px #FFF , 1040px 1282px #FFF , 1148px 1123px #FFF , 826px 1128px #FFF , 1406px 76px #FFF , 1835px 1071px #FFF , 1809px 1183px #FFF , 428px 866px #FFF , 1425px 1402px #FFF , 783px 896px #FFF , 987px 436px #FFF , 1452px 144px #FFF , 485px 1760px #FFF , 1678px 721px #FFF , 6px 1929px #FFF , 1629px 1544px #FFF , 1588px 21px #FFF , 1567px 485px #FFF , 442px 590px #FFF , 1232px 648px #FFF , 1397px 1949px #FFF , 1819px 651px #FFF , 467px 1636px #FFF , 1297px 1216px #FFF , 1743px 20px #FFF , 1264px 1242px #FFF , 116px 420px #FFF , 90px 721px #FFF , 470px 1939px #FFF , 816px 87px #FFF , 88px 1021px #FFF , 1593px 789px #FFF , 283px 36px #FFF , 775px 1722px #FFF , 1643px 1554px #FFF , 969px 389px #FFF , 467px 1442px #FFF , 1441px 1731px #FFF , 168px 1285px #FFF , 1553px 1056px #FFF , 1930px 777px #FFF , 1626px 915px #FFF , 1678px 1019px #FFF , 1739px 280px #FFF , 297px 1134px #FFF , 1477px 1896px #FFF , 1673px 1215px #FFF , 1629px 835px #FFF , 1024px 1875px #FFF , 1452px 437px #FFF , 850px 662px #FFF , 575px 257px #FFF , 1954px 1919px #FFF , 1985px 269px #FFF , 482px 701px #FFF , 1712px 339px #FFF , 1451px 665px #FFF , 1740px 16px #FFF , 1591px 13px #FFF , 1449px 30px #FFF , 18px 1715px #FFF , 370px 450px #FFF , 374px 917px #FFF , 1945px 1726px #FFF , 1086px 1134px #FFF , 323px 1135px #FFF , 1912px 1212px #FFF; - animation: animStar 50s linear infinite; - } - #stars:after { - content: " "; - position: absolute; - top: 2000px; - width: 1px; - height: 1px; - background: transparent; - box-shadow: 640px 1696px #FFF , 1160px 1058px #FFF , 1227px 156px #FFF , 1138px 1418px #FFF , 1727px 329px #FFF , 37px 279px #FFF , 1417px 1804px #FFF , 1837px 1261px #FFF , 856px 1149px #FFF , 43px 510px #FFF , 1140px 520px #FFF , 1356px 354px #FFF , 479px 1893px #FFF , 117px 1184px #FFF , 1637px 653px #FFF , 1079px 1938px #FFF , 987px 484px #FFF , 1574px 465px #FFF , 1961px 1762px #FFF , 738px 418px #FFF , 51px 1272px #FFF , 158px 859px #FFF , 1797px 503px #FFF , 83px 253px #FFF , 592px 1108px #FFF , 1832px 392px #FFF , 679px 1536px #FFF , 703px 1941px #FFF , 1932px 770px #FFF , 1755px 802px #FFF , 1643px 1784px #FFF , 1967px 163px #FFF , 1063px 1983px #FFF , 1509px 880px #FFF , 1891px 651px #FFF , 1832px 1644px #FFF , 1260px 638px #FFF , 453px 450px #FFF , 1977px 1782px #FFF , 208px 1856px #FFF , 1941px 1343px #FFF , 894px 872px #FFF , 936px 1444px #FFF , 503px 438px #FFF , 118px 1556px #FFF , 1591px 883px #FFF , 940px 1674px #FFF , 76px 1676px #FFF , 528px 938px #FFF , 772px 359px #FFF , 1934px 731px #FFF , 1787px 72px #FFF , 1445px 780px #FFF , 677px 1306px #FFF , 1021px 900px #FFF , 199px 343px #FFF , 1194px 1524px #FFF , 75px 1700px #FFF , 1323px 1400px #FFF , 712px 1806px #FFF , 136px 803px #FFF , 842px 299px #FFF , 138px 570px #FFF , 245px 975px #FFF , 1305px 1210px #FFF , 1615px 164px #FFF , 1710px 568px #FFF , 494px 397px #FFF , 598px 286px #FFF , 887px 1775px #FFF , 293px 1063px #FFF , 189px 49px #FFF , 1275px 199px #FFF , 693px 1163px #FFF , 1186px 322px #FFF , 527px 807px #FFF , 1412px 1246px #FFF , 1397px 843px #FFF , 1860px 480px #FFF , 847px 777px #FFF , 431px 917px #FFF , 487px 1662px #FFF , 1226px 1930px #FFF , 1109px 987px #FFF , 1110px 162px #FFF , 486px 1510px #FFF , 1364px 1018px #FFF , 255px 1488px #FFF , 1960px 1688px #FFF , 457px 1263px #FFF , 1202px 502px #FFF , 1206px 1974px #FFF , 1130px 1127px #FFF , 1164px 383px #FFF , 893px 1646px #FFF , 1575px 1292px #FFF , 600px 1489px #FFF , 348px 591px #FFF , 915px 1754px #FFF , 825px 757px #FFF , 379px 1727px #FFF , 573px 1729px #FFF , 1555px 1569px #FFF , 293px 512px #FFF , 1743px 470px #FFF , 1872px 1789px #FFF , 1535px 564px #FFF , 732px 92px #FFF , 1114px 777px #FFF , 1655px 1514px #FFF , 1781px 1308px #FFF , 1204px 1545px #FFF , 1068px 895px #FFF , 1956px 275px #FFF , 157px 1972px #FFF , 189px 254px #FFF , 1644px 65px #FFF , 324px 1851px #FFF , 54px 588px #FFF , 1183px 1600px #FFF , 1781px 1990px #FFF , 1403px 781px #FFF , 55px 102px #FFF , 335px 1225px #FFF , 934px 1000px #FFF , 642px 1489px #FFF , 430px 170px #FFF , 1708px 1737px #FFF , 563px 1802px #FFF , 702px 1114px #FFF , 1821px 487px #FFF , 1874px 267px #FFF , 466px 305px #FFF , 1622px 1459px #FFF , 50px 1693px #FFF , 1320px 64px #FFF , 1476px 1404px #FFF , 20px 415px #FFF , 1336px 1189px #FFF , 1812px 1978px #FFF , 1125px 1079px #FFF , 523px 47px #FFF , 515px 198px #FFF , 932px 1496px #FFF , 484px 519px #FFF , 1914px 1435px #FFF , 181px 1958px #FFF , 87px 103px #FFF , 911px 1200px #FFF , 709px 360px #FFF , 1588px 601px #FFF , 1964px 709px #FFF , 1348px 36px #FFF , 938px 397px #FFF , 617px 1508px #FFF , 28px 193px #FFF , 1102px 1507px #FFF , 350px 1562px #FFF , 1347px 1130px #FFF , 1747px 1912px #FFF , 41px 1624px #FFF , 1132px 950px #FFF , 651px 280px #FFF , 528px 686px #FFF , 796px 634px #FFF , 268px 172px #FFF , 316px 1330px #FFF , 282px 246px #FFF , 1148px 975px #FFF , 939px 1625px #FFF , 894px 1699px #FFF , 1467px 477px #FFF , 587px 1010px #FFF , 249px 1727px #FFF , 194px 862px #FFF , 1644px 1984px #FFF , 1787px 167px #FFF , 1027px 1014px #FFF , 880px 538px #FFF , 214px 329px #FFF , 666px 1285px #FFF , 821px 911px #FFF , 933px 1116px #FFF , 1962px 1543px #FFF , 626px 358px #FFF , 639px 1717px #FFF , 684px 517px #FFF , 377px 1249px #FFF , 1607px 282px #FFF , 1430px 64px #FFF , 734px 479px #FFF , 1483px 1205px #FFF , 725px 1645px #FFF , 369px 1102px #FFF , 504px 1061px #FFF , 1781px 159px #FFF , 158px 112px #FFF , 1457px 348px #FFF , 224px 1845px #FFF , 288px 1825px #FFF , 1265px 259px #FFF , 1834px 274px #FFF , 1125px 1175px #FFF , 1000px 413px #FFF , 589px 1735px #FFF , 1984px 1363px #FFF , 1430px 1122px #FFF , 1350px 1546px #FFF , 133px 1628px #FFF , 1099px 1705px #FFF , 1063px 806px #FFF , 993px 1810px #FFF , 657px 76px #FFF , 825px 388px #FFF , 1826px 1709px #FFF , 400px 1603px #FFF , 678px 1545px #FFF , 411px 1643px #FFF , 704px 354px #FFF , 1487px 1570px #FFF , 511px 1856px #FFF , 1311px 97px #FFF , 1666px 1888px #FFF , 1812px 921px #FFF , 1163px 1522px #FFF , 930px 1689px #FFF , 1061px 1627px #FFF , 1318px 1328px #FFF , 775px 1787px #FFF , 1557px 1977px #FFF , 854px 579px #FFF , 1114px 1666px #FFF , 867px 1184px #FFF , 739px 561px #FFF , 1138px 1189px #FFF , 1810px 1425px #FFF , 17px 642px #FFF , 805px 204px #FFF , 1207px 569px #FFF , 976px 797px #FFF , 1786px 657px #FFF , 1915px 478px #FFF , 1895px 1432px #FFF , 695px 39px #FFF , 445px 106px #FFF , 104px 285px #FFF , 734px 1319px #FFF , 1072px 1125px #FFF , 1749px 708px #FFF , 264px 1983px #FFF , 317px 814px #FFF , 45px 1978px #FFF , 398px 120px #FFF , 1477px 726px #FFF , 1655px 614px #FFF , 934px 1475px #FFF , 346px 1843px #FFF , 291px 454px #FFF , 889px 1563px #FFF , 1114px 417px #FFF , 1993px 1178px #FFF , 345px 259px #FFF , 271px 416px #FFF , 1858px 1431px #FFF , 1332px 1068px #FFF , 1080px 646px #FFF , 30px 1378px #FFF , 1476px 1174px #FFF , 585px 1452px #FFF , 855px 1934px #FFF , 1148px 1666px #FFF , 1250px 386px #FFF , 682px 760px #FFF , 691px 162px #FFF , 1149px 1211px #FFF , 1528px 384px #FFF , 1307px 334px #FFF , 322px 1427px #FFF , 1422px 1505px #FFF , 1479px 1870px #FFF , 764px 1000px #FFF , 27px 92px #FFF , 385px 326px #FFF , 3px 1911px #FFF , 1521px 546px #FFF , 438px 658px #FFF , 1605px 1637px #FFF , 326px 198px #FFF , 999px 716px #FFF , 1559px 927px #FFF , 1757px 1298px #FFF , 334px 1472px #FFF , 650px 817px #FFF , 1094px 116px #FFF , 1063px 1822px #FFF , 1480px 831px #FFF , 676px 1556px #FFF , 1619px 618px #FFF , 125px 211px #FFF , 553px 1554px #FFF , 1778px 863px #FFF , 1079px 1074px #FFF , 952px 462px #FFF , 707px 38px #FFF , 1125px 307px #FFF , 88px 1605px #FFF , 1388px 1217px #FFF , 776px 861px #FFF , 97px 1949px #FFF , 438px 250px #FFF , 731px 556px #FFF , 41px 652px #FFF , 544px 1111px #FFF , 1944px 1951px #FFF , 1421px 758px #FFF , 694px 1908px #FFF , 914px 400px #FFF , 745px 92px #FFF , 240px 1108px #FFF , 1428px 19px #FFF , 778px 1858px #FFF , 41px 587px #FFF , 1450px 765px #FFF , 1514px 700px #FFF , 1170px 827px #FFF , 420px 81px #FFF , 974px 576px #FFF , 1637px 426px #FFF , 699px 527px #FFF , 1360px 1251px #FFF , 1118px 1339px #FFF , 1542px 1298px #FFF , 1271px 1895px #FFF , 1976px 1893px #FFF , 529px 1743px #FFF , 1195px 597px #FFF , 1116px 594px #FFF , 686px 462px #FFF , 462px 1238px #FFF , 993px 122px #FFF , 1053px 871px #FFF , 1112px 1762px #FFF , 1217px 781px #FFF , 1591px 953px #FFF , 1516px 1984px #FFF , 1475px 1596px #FFF , 1740px 821px #FFF , 1438px 1074px #FFF , 655px 864px #FFF , 290px 766px #FFF , 1051px 827px #FFF , 1523px 1260px #FFF , 1070px 729px #FFF , 1425px 1298px #FFF , 624px 55px #FFF , 234px 275px #FFF , 1577px 384px #FFF , 802px 388px #FFF , 1327px 1695px #FFF , 1079px 798px #FFF , 806px 339px #FFF , 79px 1494px #FFF , 1585px 1431px #FFF , 642px 379px #FFF , 1668px 1502px #FFF , 9px 376px #FFF , 373px 683px #FFF , 1223px 1525px #FFF , 357px 1438px #FFF , 1916px 614px #FFF , 1586px 789px #FFF , 1964px 1973px #FFF , 1133px 1023px #FFF , 388px 931px #FFF , 1008px 1247px #FFF , 496px 1833px #FFF , 1260px 1118px #FFF , 1945px 256px #FFF , 812px 1017px #FFF , 536px 1524px #FFF , 1550px 1819px #FFF , 1774px 1333px #FFF , 1115px 1429px #FFF , 1396px 675px #FFF , 81px 1883px #FFF , 863px 984px #FFF , 422px 1462px #FFF , 641px 1426px #FFF , 1587px 833px #FFF , 1697px 1489px #FFF , 1043px 1566px #FFF , 197px 544px #FFF , 1098px 1442px #FFF , 1832px 1396px #FFF , 247px 1225px #FFF , 494px 766px #FFF , 437px 1162px #FFF , 1590px 1264px #FFF , 1672px 1977px #FFF , 1710px 136px #FFF , 1209px 936px #FFF , 1260px 264px #FFF , 117px 863px #FFF , 795px 1420px #FFF , 84px 81px #FFF , 1493px 378px #FFF , 1533px 1281px #FFF , 199px 1398px #FFF , 960px 1515px #FFF , 193px 266px #FFF , 694px 399px #FFF , 1298px 297px #FFF , 1139px 1379px #FFF , 568px 157px #FFF , 380px 877px #FFF , 1670px 515px #FFF , 1304px 80px #FFF , 602px 1160px #FFF , 991px 824px #FFF , 361px 572px #FFF , 1216px 709px #FFF , 1128px 198px #FFF , 191px 146px #FFF , 700px 1921px #FFF , 699px 1727px #FFF , 674px 986px #FFF , 166px 721px #FFF , 204px 265px #FFF , 856px 1329px #FFF , 1662px 1559px #FFF , 360px 908px #FFF , 1313px 1722px #FFF , 1780px 245px #FFF , 1433px 1242px #FFF , 1892px 1433px #FFF , 1323px 1805px #FFF , 295px 1555px #FFF , 564px 133px #FFF , 919px 1841px #FFF , 1352px 54px #FFF , 1885px 135px #FFF , 277px 1739px #FFF , 776px 1592px #FFF , 992px 1589px #FFF , 1073px 1027px #FFF , 842px 226px #FFF , 1193px 628px #FFF , 879px 1899px #FFF , 802px 1413px #FFF , 1417px 1335px #FFF , 348px 1318px #FFF , 724px 935px #FFF , 306px 559px #FFF , 1389px 1210px #FFF , 1085px 1449px #FFF , 1718px 1967px #FFF , 683px 1159px #FFF , 293px 1188px #FFF , 628px 476px #FFF , 652px 733px #FFF , 1936px 866px #FFF , 1903px 1011px #FFF , 977px 1294px #FFF , 346px 1401px #FFF , 864px 1013px #FFF , 1769px 107px #FFF , 1982px 1176px #FFF , 1867px 143px #FFF , 1997px 1582px #FFF , 1476px 593px #FFF , 128px 285px #FFF , 1140px 58px #FFF , 1026px 1972px #FFF , 1859px 1974px #FFF , 922px 64px #FFF , 1712px 582px #FFF , 1076px 638px #FFF , 362px 105px #FFF , 120px 83px #FFF , 610px 359px #FFF , 511px 456px #FFF , 1476px 602px #FFF , 269px 1775px #FFF , 272px 1625px #FFF , 162px 1864px #FFF , 1237px 817px #FFF , 1939px 507px #FFF , 1821px 1741px #FFF , 1603px 1234px #FFF , 562px 80px #FFF , 106px 880px #FFF , 1701px 586px #FFF , 1422px 908px #FFF , 1802px 1966px #FFF , 459px 1648px #FFF , 612px 1599px #FFF , 323px 1672px #FFF , 320px 716px #FFF , 807px 1441px #FFF , 1367px 1731px #FFF , 1501px 1379px #FFF , 1980px 1548px #FFF , 743px 747px #FFF , 1965px 951px #FFF , 1717px 697px #FFF , 1447px 1703px #FFF , 1100px 869px #FFF , 943px 1232px #FFF , 279px 1655px #FFF , 1428px 1289px #FFF , 365px 1825px #FFF , 1951px 526px #FFF , 1166px 1068px #FFF , 1802px 456px #FFF , 57px 381px #FFF , 837px 852px #FFF , 1114px 467px #FFF , 1007px 1832px #FFF , 174px 1221px #FFF , 195px 188px #FFF , 1823px 438px #FFF , 984px 917px #FFF , 1424px 530px #FFF , 272px 244px #FFF , 1386px 1239px #FFF , 4px 1442px #FFF , 1129px 1376px #FFF , 265px 1527px #FFF , 1003px 718px #FFF , 993px 75px #FFF , 1820px 1655px #FFF , 758px 282px #FFF , 934px 1108px #FFF , 1492px 1687px #FFF , 1245px 1344px #FFF , 110px 1810px #FFF , 1988px 1713px #FFF , 1386px 1883px #FFF , 1781px 1461px #FFF , 361px 563px #FFF , 474px 1150px #FFF , 185px 1218px #FFF , 793px 1513px #FFF , 1474px 1117px #FFF , 221px 295px #FFF , 1445px 1859px #FFF , 903px 972px #FFF , 1420px 139px #FFF , 1477px 1084px #FFF , 373px 1554px #FFF , 1355px 1247px #FFF , 1483px 48px #FFF , 158px 1684px #FFF , 145px 963px #FFF , 213px 841px #FFF , 634px 17px #FFF , 128px 1911px #FFF , 1711px 1125px #FFF , 906px 1253px #FFF , 1158px 433px #FFF , 1544px 1045px #FFF , 506px 1388px #FFF , 1261px 1555px #FFF , 876px 286px #FFF , 1042px 815px #FFF , 1870px 715px #FFF , 808px 1262px #FFF , 1777px 1838px #FFF , 470px 953px #FFF , 99px 469px #FFF , 1631px 896px #FFF , 676px 1877px #FFF , 805px 324px #FFF , 1966px 1784px #FFF , 893px 520px #FFF , 738px 1217px #FFF , 1064px 39px #FFF , 1284px 351px #FFF , 626px 1902px #FFF , 1272px 1574px #FFF , 62px 1290px #FFF , 43px 1931px #FFF , 449px 1603px #FFF , 442px 1841px #FFF , 933px 1952px #FFF , 1120px 311px #FFF , 1800px 1824px #FFF , 1736px 1738px #FFF , 1656px 3px #FFF , 1709px 359px #FFF , 559px 42px #FFF , 1404px 730px #FFF , 356px 179px #FFF , 1267px 1929px #FFF , 1629px 1547px #FFF , 115px 957px #FFF , 336px 1565px #FFF , 1727px 232px #FFF , 578px 1538px #FFF , 148px 161px #FFF , 704px 722px #FFF , 1331px 1899px #FFF , 571px 241px #FFF , 386px 810px #FFF , 934px 622px #FFF , 1006px 838px #FFF , 117px 1847px #FFF , 1850px 583px #FFF , 355px 258px #FFF , 1715px 144px #FFF , 1763px 1679px #FFF , 1072px 374px #FFF , 237px 1190px #FFF , 1034px 571px #FFF , 321px 1904px #FFF , 1732px 743px #FFF , 1352px 530px #FFF , 1260px 1611px #FFF , 1930px 192px #FFF , 1660px 611px #FFF , 163px 1627px #FFF , 311px 451px #FFF , 64px 180px #FFF , 193px 1528px #FFF , 1630px 687px #FFF , 1804px 577px #FFF , 786px 1702px #FFF , 1568px 127px #FFF , 817px 1712px #FFF , 1493px 647px #FFF , 266px 22px #FFF , 191px 1177px #FFF , 1408px 106px #FFF , 1257px 534px #FFF , 1040px 1282px #FFF , 1148px 1123px #FFF , 826px 1128px #FFF , 1406px 76px #FFF , 1835px 1071px #FFF , 1809px 1183px #FFF , 428px 866px #FFF , 1425px 1402px #FFF , 783px 896px #FFF , 987px 436px #FFF , 1452px 144px #FFF , 485px 1760px #FFF , 1678px 721px #FFF , 6px 1929px #FFF , 1629px 1544px #FFF , 1588px 21px #FFF , 1567px 485px #FFF , 442px 590px #FFF , 1232px 648px #FFF , 1397px 1949px #FFF , 1819px 651px #FFF , 467px 1636px #FFF , 1297px 1216px #FFF , 1743px 20px #FFF , 1264px 1242px #FFF , 116px 420px #FFF , 90px 721px #FFF , 470px 1939px #FFF , 816px 87px #FFF , 88px 1021px #FFF , 1593px 789px #FFF , 283px 36px #FFF , 775px 1722px #FFF , 1643px 1554px #FFF , 969px 389px #FFF , 467px 1442px #FFF , 1441px 1731px #FFF , 168px 1285px #FFF , 1553px 1056px #FFF , 1930px 777px #FFF , 1626px 915px #FFF , 1678px 1019px #FFF , 1739px 280px #FFF , 297px 1134px #FFF , 1477px 1896px #FFF , 1673px 1215px #FFF , 1629px 835px #FFF , 1024px 1875px #FFF , 1452px 437px #FFF , 850px 662px #FFF , 575px 257px #FFF , 1954px 1919px #FFF , 1985px 269px #FFF , 482px 701px #FFF , 1712px 339px #FFF , 1451px 665px #FFF , 1740px 16px #FFF , 1591px 13px #FFF , 1449px 30px #FFF , 18px 1715px #FFF , 370px 450px #FFF , 374px 917px #FFF , 1945px 1726px #FFF , 1086px 1134px #FFF , 323px 1135px #FFF , 1912px 1212px #FFF; - } - - #stars2 { - width: 2px; - height: 2px; - background: transparent; - box-shadow: 1525px 16px #FFF , 800px 1234px #FFF , 772px 1261px #FFF , 1350px 1211px #FFF , 1179px 1382px #FFF , 1593px 1378px #FFF , 385px 1199px #FFF , 123px 1615px #FFF , 225px 1069px #FFF , 76px 1610px #FFF , 545px 465px #FFF , 1871px 1565px #FFF , 931px 1475px #FFF , 288px 484px #FFF , 1168px 1634px #FFF , 1132px 582px #FFF , 266px 551px #FFF , 958px 1243px #FFF , 604px 1196px #FFF , 1847px 1135px #FFF , 182px 1150px #FFF , 643px 207px #FFF , 195px 1950px #FFF , 901px 1346px #FFF , 1121px 378px #FFF , 1859px 845px #FFF , 718px 1223px #FFF , 1014px 638px #FFF , 1525px 534px #FFF , 903px 1689px #FFF , 798px 66px #FFF , 1499px 806px #FFF , 503px 467px #FFF , 996px 739px #FFF , 1234px 1701px #FFF , 613px 1915px #FFF , 1070px 969px #FFF , 959px 1300px #FFF , 480px 527px #FFF , 302px 1658px #FFF , 284px 1466px #FFF , 1265px 1031px #FFF , 1903px 683px #FFF , 601px 56px #FFF , 1133px 1681px #FFF , 849px 177px #FFF , 872px 102px #FFF , 1577px 1281px #FFF , 1356px 1958px #FFF , 1553px 1340px #FFF , 262px 987px #FFF , 787px 1684px #FFF , 154px 194px #FFF , 172px 1278px #FFF , 1384px 148px #FFF , 400px 582px #FFF , 1998px 1227px #FFF , 1730px 162px #FFF , 688px 31px #FFF , 983px 1472px #FFF , 1576px 324px #FFF , 183px 1006px #FFF , 158px 801px #FFF , 1955px 1115px #FFF , 1702px 1695px #FFF , 917px 1701px #FFF , 745px 1862px #FFF , 1129px 1315px #FFF , 890px 446px #FFF , 992px 1626px #FFF , 1017px 1228px #FFF , 188px 574px #FFF , 1649px 939px #FFF , 899px 568px #FFF , 353px 1003px #FFF , 1367px 1696px #FFF , 1532px 753px #FFF , 699px 1190px #FFF , 753px 1566px #FFF , 69px 159px #FFF , 19px 830px #FFF , 570px 351px #FFF , 364px 1947px #FFF , 1042px 198px #FFF , 668px 184px #FFF , 1551px 152px #FFF , 796px 1433px #FFF , 1043px 917px #FFF , 1449px 946px #FFF , 1617px 934px #FFF , 32px 657px #FFF , 491px 1236px #FFF , 658px 1052px #FFF , 715px 1937px #FFF , 452px 284px #FFF , 1521px 35px #FFF , 228px 733px #FFF , 1652px 372px #FFF , 1976px 633px #FFF , 1700px 1554px #FFF , 237px 1762px #FFF , 1929px 1487px #FFF , 1242px 1234px #FFF , 1794px 1405px #FFF , 456px 709px #FFF , 862px 1221px #FFF , 1674px 28px #FFF , 1255px 332px #FFF , 1841px 170px #FFF , 1075px 193px #FFF , 149px 962px #FFF , 741px 1520px #FFF , 1379px 544px #FFF , 1091px 1231px #FFF , 1364px 1157px #FFF , 1879px 720px #FFF , 462px 13px #FFF , 420px 305px #FFF , 574px 835px #FFF , 575px 426px #FFF , 1208px 816px #FFF , 743px 1122px #FFF , 1847px 230px #FFF , 450px 1644px #FFF , 1688px 1409px #FFF , 453px 1959px #FFF , 1661px 917px #FFF , 1236px 1313px #FFF , 98px 299px #FFF , 1372px 676px #FFF , 1209px 320px #FFF , 1523px 371px #FFF , 1947px 1930px #FFF , 36px 1499px #FFF , 66px 1418px #FFF , 581px 1642px #FFF , 1225px 1664px #FFF , 1764px 566px #FFF , 656px 1009px #FFF , 1155px 641px #FFF , 1275px 1534px #FFF , 1822px 1593px #FFF , 1798px 1283px #FFF , 994px 1852px #FFF , 1933px 31px #FFF , 1247px 1459px #FFF , 1148px 669px #FFF , 455px 262px #FFF , 1319px 1419px #FFF , 1637px 1307px #FFF , 147px 1499px #FFF , 1372px 1796px #FFF , 1973px 1616px #FFF , 1491px 251px #FFF , 72px 158px #FFF , 1519px 878px #FFF , 13px 955px #FFF , 1120px 1225px #FFF , 767px 1873px #FFF , 475px 1980px #FFF , 221px 1803px #FFF , 654px 623px #FFF , 982px 285px #FFF , 1856px 1165px #FFF , 1533px 944px #FFF , 937px 1457px #FFF , 1935px 664px #FFF , 1793px 292px #FFF , 858px 1983px #FFF , 390px 398px #FFF , 1313px 1297px #FFF , 725px 1096px #FFF , 1469px 1658px #FFF , 648px 1141px #FFF , 953px 255px #FFF , 286px 346px #FFF , 32px 1662px #FFF , 1272px 316px #FFF , 1252px 710px #FFF , 1337px 1505px #FFF , 565px 1464px #FFF , 1116px 506px #FFF , 1593px 163px #FFF , 214px 1048px #FFF , 1195px 1475px #FFF , 1384px 1765px #FFF , 706px 988px #FFF , 283px 1279px #FFF , 737px 1052px #FFF , 381px 1420px #FFF , 1630px 610px #FFF , 938px 693px #FFF , 1721px 1096px #FFF , 1210px 341px #FFF , 906px 1940px #FFF , 1007px 650px #FFF , 7px 1048px #FFF , 180px 1353px #FFF , 1375px 872px #FFF , 230px 952px #FFF; - animation: animStar 100s linear infinite; - } - #stars2:after { - content: " "; - position: absolute; - top: 2000px; - width: 2px; - height: 2px; - background: transparent; - box-shadow: 1525px 16px #FFF , 800px 1234px #FFF , 772px 1261px #FFF , 1350px 1211px #FFF , 1179px 1382px #FFF , 1593px 1378px #FFF , 385px 1199px #FFF , 123px 1615px #FFF , 225px 1069px #FFF , 76px 1610px #FFF , 545px 465px #FFF , 1871px 1565px #FFF , 931px 1475px #FFF , 288px 484px #FFF , 1168px 1634px #FFF , 1132px 582px #FFF , 266px 551px #FFF , 958px 1243px #FFF , 604px 1196px #FFF , 1847px 1135px #FFF , 182px 1150px #FFF , 643px 207px #FFF , 195px 1950px #FFF , 901px 1346px #FFF , 1121px 378px #FFF , 1859px 845px #FFF , 718px 1223px #FFF , 1014px 638px #FFF , 1525px 534px #FFF , 903px 1689px #FFF , 798px 66px #FFF , 1499px 806px #FFF , 503px 467px #FFF , 996px 739px #FFF , 1234px 1701px #FFF , 613px 1915px #FFF , 1070px 969px #FFF , 959px 1300px #FFF , 480px 527px #FFF , 302px 1658px #FFF , 284px 1466px #FFF , 1265px 1031px #FFF , 1903px 683px #FFF , 601px 56px #FFF , 1133px 1681px #FFF , 849px 177px #FFF , 872px 102px #FFF , 1577px 1281px #FFF , 1356px 1958px #FFF , 1553px 1340px #FFF , 262px 987px #FFF , 787px 1684px #FFF , 154px 194px #FFF , 172px 1278px #FFF , 1384px 148px #FFF , 400px 582px #FFF , 1998px 1227px #FFF , 1730px 162px #FFF , 688px 31px #FFF , 983px 1472px #FFF , 1576px 324px #FFF , 183px 1006px #FFF , 158px 801px #FFF , 1955px 1115px #FFF , 1702px 1695px #FFF , 917px 1701px #FFF , 745px 1862px #FFF , 1129px 1315px #FFF , 890px 446px #FFF , 992px 1626px #FFF , 1017px 1228px #FFF , 188px 574px #FFF , 1649px 939px #FFF , 899px 568px #FFF , 353px 1003px #FFF , 1367px 1696px #FFF , 1532px 753px #FFF , 699px 1190px #FFF , 753px 1566px #FFF , 69px 159px #FFF , 19px 830px #FFF , 570px 351px #FFF , 364px 1947px #FFF , 1042px 198px #FFF , 668px 184px #FFF , 1551px 152px #FFF , 796px 1433px #FFF , 1043px 917px #FFF , 1449px 946px #FFF , 1617px 934px #FFF , 32px 657px #FFF , 491px 1236px #FFF , 658px 1052px #FFF , 715px 1937px #FFF , 452px 284px #FFF , 1521px 35px #FFF , 228px 733px #FFF , 1652px 372px #FFF , 1976px 633px #FFF , 1700px 1554px #FFF , 237px 1762px #FFF , 1929px 1487px #FFF , 1242px 1234px #FFF , 1794px 1405px #FFF , 456px 709px #FFF , 862px 1221px #FFF , 1674px 28px #FFF , 1255px 332px #FFF , 1841px 170px #FFF , 1075px 193px #FFF , 149px 962px #FFF , 741px 1520px #FFF , 1379px 544px #FFF , 1091px 1231px #FFF , 1364px 1157px #FFF , 1879px 720px #FFF , 462px 13px #FFF , 420px 305px #FFF , 574px 835px #FFF , 575px 426px #FFF , 1208px 816px #FFF , 743px 1122px #FFF , 1847px 230px #FFF , 450px 1644px #FFF , 1688px 1409px #FFF , 453px 1959px #FFF , 1661px 917px #FFF , 1236px 1313px #FFF , 98px 299px #FFF , 1372px 676px #FFF , 1209px 320px #FFF , 1523px 371px #FFF , 1947px 1930px #FFF , 36px 1499px #FFF , 66px 1418px #FFF , 581px 1642px #FFF , 1225px 1664px #FFF , 1764px 566px #FFF , 656px 1009px #FFF , 1155px 641px #FFF , 1275px 1534px #FFF , 1822px 1593px #FFF , 1798px 1283px #FFF , 994px 1852px #FFF , 1933px 31px #FFF , 1247px 1459px #FFF , 1148px 669px #FFF , 455px 262px #FFF , 1319px 1419px #FFF , 1637px 1307px #FFF , 147px 1499px #FFF , 1372px 1796px #FFF , 1973px 1616px #FFF , 1491px 251px #FFF , 72px 158px #FFF , 1519px 878px #FFF , 13px 955px #FFF , 1120px 1225px #FFF , 767px 1873px #FFF , 475px 1980px #FFF , 221px 1803px #FFF , 654px 623px #FFF , 982px 285px #FFF , 1856px 1165px #FFF , 1533px 944px #FFF , 937px 1457px #FFF , 1935px 664px #FFF , 1793px 292px #FFF , 858px 1983px #FFF , 390px 398px #FFF , 1313px 1297px #FFF , 725px 1096px #FFF , 1469px 1658px #FFF , 648px 1141px #FFF , 953px 255px #FFF , 286px 346px #FFF , 32px 1662px #FFF , 1272px 316px #FFF , 1252px 710px #FFF , 1337px 1505px #FFF , 565px 1464px #FFF , 1116px 506px #FFF , 1593px 163px #FFF , 214px 1048px #FFF , 1195px 1475px #FFF , 1384px 1765px #FFF , 706px 988px #FFF , 283px 1279px #FFF , 737px 1052px #FFF , 381px 1420px #FFF , 1630px 610px #FFF , 938px 693px #FFF , 1721px 1096px #FFF , 1210px 341px #FFF , 906px 1940px #FFF , 1007px 650px #FFF , 7px 1048px #FFF , 180px 1353px #FFF , 1375px 872px #FFF , 230px 952px #FFF; - } - - #stars3 { - width: 3px; - height: 3px; - background: transparent; - box-shadow: 1408px 274px #FFF , 895px 1719px #FFF , 958px 1846px #FFF , 1383px 219px #FFF , 453px 847px #FFF , 1092px 1479px #FFF , 1866px 1571px #FFF , 1279px 258px #FFF , 718px 1253px #FFF , 1905px 420px #FFF , 1720px 1477px #FFF , 340px 1013px #FFF , 1525px 1634px #FFF , 1873px 1892px #FFF , 1191px 1165px #FFF , 855px 949px #FFF , 1714px 394px #FFF , 1010px 1655px #FFF , 1172px 1204px #FFF , 1968px 1227px #FFF , 1260px 1735px #FFF , 1221px 816px #FFF , 535px 1270px #FFF , 314px 1383px #FFF , 701px 1365px #FFF , 398px 119px #FFF , 1577px 1449px #FFF , 266px 943px #FFF , 1041px 433px #FFF , 520px 69px #FFF , 494px 958px #FFF , 1702px 9px #FFF , 906px 1403px #FFF , 149px 1620px #FFF , 353px 1383px #FFF , 1150px 143px #FFF , 980px 810px #FFF , 848px 450px #FFF , 1929px 348px #FFF , 1679px 1631px #FFF , 1717px 361px #FFF , 557px 1029px #FFF , 397px 58px #FFF , 1059px 1824px #FFF , 1322px 1394px #FFF , 71px 1738px #FFF , 237px 1273px #FFF , 1961px 1497px #FFF , 1502px 637px #FFF , 564px 1253px #FFF , 1281px 204px #FFF , 314px 1260px #FFF , 1148px 1699px #FFF , 1172px 643px #FFF , 1392px 969px #FFF , 827px 963px #FFF , 1416px 1868px #FFF , 62px 1369px #FFF , 1640px 565px #FFF , 1829px 685px #FFF , 1798px 9px #FFF , 1064px 1963px #FFF , 165px 1315px #FFF , 1251px 173px #FFF , 47px 168px #FFF , 945px 353px #FFF , 281px 1641px #FFF , 348px 40px #FFF , 276px 256px #FFF , 1967px 1945px #FFF , 268px 669px #FFF , 1208px 798px #FFF , 1554px 1138px #FFF , 1486px 263px #FFF , 1248px 1369px #FFF , 884px 430px #FFF , 1098px 1795px #FFF , 439px 1180px #FFF , 43px 1602px #FFF , 1515px 1661px #FFF , 800px 1695px #FFF , 353px 21px #FFF , 1794px 38px #FFF , 493px 928px #FFF , 242px 1324px #FFF , 466px 684px #FFF , 1933px 1896px #FFF , 218px 1123px #FFF , 577px 935px #FFF , 576px 72px #FFF , 669px 235px #FFF , 1607px 366px #FFF , 460px 1601px #FFF , 121px 867px #FFF , 876px 7px #FFF , 1208px 1621px #FFF , 295px 362px #FFF , 1683px 108px #FFF , 1247px 849px #FFF , 1468px 732px #FFF; - animation: animStar 150s linear infinite; - } - #stars3:after { - content: " "; - position: absolute; - top: 2000px; - width: 3px; - height: 3px; - background: transparent; - box-shadow: 1408px 274px #FFF , 895px 1719px #FFF , 958px 1846px #FFF , 1383px 219px #FFF , 453px 847px #FFF , 1092px 1479px #FFF , 1866px 1571px #FFF , 1279px 258px #FFF , 718px 1253px #FFF , 1905px 420px #FFF , 1720px 1477px #FFF , 340px 1013px #FFF , 1525px 1634px #FFF , 1873px 1892px #FFF , 1191px 1165px #FFF , 855px 949px #FFF , 1714px 394px #FFF , 1010px 1655px #FFF , 1172px 1204px #FFF , 1968px 1227px #FFF , 1260px 1735px #FFF , 1221px 816px #FFF , 535px 1270px #FFF , 314px 1383px #FFF , 701px 1365px #FFF , 398px 119px #FFF , 1577px 1449px #FFF , 266px 943px #FFF , 1041px 433px #FFF , 520px 69px #FFF , 494px 958px #FFF , 1702px 9px #FFF , 906px 1403px #FFF , 149px 1620px #FFF , 353px 1383px #FFF , 1150px 143px #FFF , 980px 810px #FFF , 848px 450px #FFF , 1929px 348px #FFF , 1679px 1631px #FFF , 1717px 361px #FFF , 557px 1029px #FFF , 397px 58px #FFF , 1059px 1824px #FFF , 1322px 1394px #FFF , 71px 1738px #FFF , 237px 1273px #FFF , 1961px 1497px #FFF , 1502px 637px #FFF , 564px 1253px #FFF , 1281px 204px #FFF , 314px 1260px #FFF , 1148px 1699px #FFF , 1172px 643px #FFF , 1392px 969px #FFF , 827px 963px #FFF , 1416px 1868px #FFF , 62px 1369px #FFF , 1640px 565px #FFF , 1829px 685px #FFF , 1798px 9px #FFF , 1064px 1963px #FFF , 165px 1315px #FFF , 1251px 173px #FFF , 47px 168px #FFF , 945px 353px #FFF , 281px 1641px #FFF , 348px 40px #FFF , 276px 256px #FFF , 1967px 1945px #FFF , 268px 669px #FFF , 1208px 798px #FFF , 1554px 1138px #FFF , 1486px 263px #FFF , 1248px 1369px #FFF , 884px 430px #FFF , 1098px 1795px #FFF , 439px 1180px #FFF , 43px 1602px #FFF , 1515px 1661px #FFF , 800px 1695px #FFF , 353px 21px #FFF , 1794px 38px #FFF , 493px 928px #FFF , 242px 1324px #FFF , 466px 684px #FFF , 1933px 1896px #FFF , 218px 1123px #FFF , 577px 935px #FFF , 576px 72px #FFF , 669px 235px #FFF , 1607px 366px #FFF , 460px 1601px #FFF , 121px 867px #FFF , 876px 7px #FFF , 1208px 1621px #FFF , 295px 362px #FFF , 1683px 108px #FFF , 1247px 849px #FFF , 1468px 732px #FFF; - } - #title { left: 0; right: 0; @@ -106,12 +55,6 @@ - - - -
-
-
{% if logo %} {% endif %} From 6b32bc91238d5405ec64f45b7dce6bf4818fcc7a Mon Sep 17 00:00:00 2001 From: Niclas Axelsson Date: Wed, 2 Oct 2024 21:43:49 +0200 Subject: [PATCH 8/9] Use the function syntax internally for plugins --- src/nova_plugin_handler.erl | 18 ++++++++++-------- src/nova_plugin_manager.erl | 25 +++++++++++++++++++------ src/nova_router.erl | 2 +- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/nova_plugin_handler.erl b/src/nova_plugin_handler.erl index 8a641913..ae532f8f 100644 --- a/src/nova_plugin_handler.erl +++ b/src/nova_plugin_handler.erl @@ -22,25 +22,27 @@ execute(Req, Env) -> run_plugins([], Callback, Req, Env) -> {ok, Req, Env#{plugin_state => Callback}}; -run_plugins([{Module, Options}|Tl], Callback, Req, Env) -> +run_plugins([{Module, Options}|Tl], Callback, Req, Env) when is_atom(Module) -> + run_plugins([{fun Module:Callback/1, Options}|Tl], Callback, Req, Env); +run_plugins([{Callback, Options}|Tl], CallbackType, Req, Env) when is_function(Callback) -> %% Fetch state State = - case nova_plugin_manager:get_state(Module) of + case nova_plugin_manager:get_state(Callback) of {ok, State0} -> State0; {error, _Reason} -> undefined end, - try Module:Callback(Req, Env, Options, State) of + try Callback(Req, Env, Options, State) of Result -> - set_state(Module, Result), + set_state(Callback, Result), case Result of {ok, Req0, _State0} -> - run_plugins(Tl, Callback, Req0, Env); + run_plugins(Tl, CallbackType, Req0, Env); {ok, Reply, Req0, _State0} -> Req1 = handle_reply(Reply, Req0), - run_plugins(Tl, Callback, Req1, Env); + run_plugins(Tl, CallbackType, Req1, Env); {break, Req0, _State0} -> {ok, Req0}; {break, Reply, Req0, _State0} -> @@ -74,7 +76,7 @@ handle_reply(_, Req) -> -set_state(Module, Return) -> +set_state(Callback, Return) when is_function(Callback) -> ReturnList = erlang:tuple_to_list(Return), State = lists:last(ReturnList), - nova_plugin_manager:set_state(Module, State). + nova_plugin_manager:set_state(Callback, State). diff --git a/src/nova_plugin_manager.erl b/src/nova_plugin_manager.erl index cfd244af..9afd27dc 100644 --- a/src/nova_plugin_manager.erl +++ b/src/nova_plugin_manager.erl @@ -61,9 +61,13 @@ start_link() -> add_plugin({_Class, Module, _Opts}) -> add_plugin(Module); -add_plugin(Module) -> +add_plugin(Module) when is_atom(Module) -> #{title := Title, version := Version} = Module:plugin_info(), - add_plugin(Module, Title, Version). + add_plugin(Module, Title, Version); +add_plugin(Callback) when is_function(Callback) -> + Info = erlang:fun_info(Callback), + Module = proplists:get_value(module, Info), + add_plugin(Module). add_plugin(Module, Name, Version) -> case ets:lookup(?TABLE, Module) of @@ -74,23 +78,32 @@ add_plugin(Module, Name, Version) -> gen_server:cast(?SERVER, {add_plugin, Module, Name, Version}) end. -get_state(Module) -> +get_state(Module) when is_atom(Module) -> case ets:lookup(?TABLE, Module) of [#plugin{state = State}] -> {ok, State}; _ -> ?LOG_DEBUG("Plugin ~p not found. get_state/1 failed.", [Module]), {error, not_found} - end. + end; +get_state(Callback) when is_function(Callback) -> + Info = erlang:fun_info(Callback), + Module = proplists:get_value(module, Info), + get_state(Module). + -set_state(Module, NewState) -> +set_state(Module, NewState) when is_atom(Module) -> case ets:lookup(?TABLE, Module) of [#plugin{} = P] -> gen_server:call(?SERVER, {set_state, Module, P, NewState}); _ -> ?LOG_DEBUG("Plugin ~p not found. set_state/2 failed.", [Module]), {error, not_found} - end. + end; +set_state(Callback, NewState) when is_function(Callback) -> + Info = erlang:fun_info(Callback), + Module = proplists:get_value(module, Info), + set_state(Module, NewState). %%%=================================================================== %%% gen_server callbacks diff --git a/src/nova_router.erl b/src/nova_router.erl index 6074f127..eaa3523e 100644 --- a/src/nova_router.erl +++ b/src/nova_router.erl @@ -413,7 +413,7 @@ normalize_plugins(Plugins) -> normalize_plugins([], Ack) -> Ack; normalize_plugins([{Type, PluginName, Options}|Tl], Ack) -> ExistingPlugins = proplists:get_value(Type, Ack, []), - normalize_plugins(Tl, [{Type, [{PluginName, Options}|ExistingPlugins]}|proplists:delete(Type, Ack)]). + normalize_plugins(Tl, [{Type, [{fun PluginName:Type/4, Options}|ExistingPlugins]}|proplists:delete(Type, Ack)]). method_to_binary(get) -> <<"GET">>; method_to_binary(post) -> <<"POST">>; From c1f6db2933351f466afb7851075595c4cc594e51 Mon Sep 17 00:00:00 2001 From: Niclas Axelsson Date: Wed, 2 Oct 2024 21:54:22 +0200 Subject: [PATCH 9/9] Clean up the code a bit --- src/nova_plugin_manager.erl | 4 ++-- src/nova_router.erl | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/nova_plugin_manager.erl b/src/nova_plugin_manager.erl index 9afd27dc..b70f3146 100644 --- a/src/nova_plugin_manager.erl +++ b/src/nova_plugin_manager.erl @@ -123,8 +123,8 @@ set_state(Callback, NewState) when is_function(Callback) -> init([]) -> process_flag(trap_exit, true), ets:new(?TABLE, [named_table, set, protected, {keypos, #plugin.module}]), - GlobalPlugins = application:get_env(nova, plugins, []), - [ add_plugin(Module) || Module <- GlobalPlugins ], + Plugins = nova_router:plugins(), + [ add_plugin(Callback) || Callback <- Plugins ], {ok, #state{}}. %%-------------------------------------------------------------------- diff --git a/src/nova_router.erl b/src/nova_router.erl index 102501a9..8f776ade 100644 --- a/src/nova_router.erl +++ b/src/nova_router.erl @@ -224,8 +224,6 @@ compile_paths([RouteInfo|Tl], Dispatch, Options) -> NovaEnv0 = [{App, #{prefix => Prefix}} | NovaEnv], nova:set_env(apps, NovaEnv0), - add_plugins(Plugins), - {ok, Dispatch1} = parse_url(Host, maps:get(routes, RouteInfo, []), Prefix, Value, Dispatch), Dispatch2 = compile(SubApps, Dispatch1, Options#{value => Value, prefix => Prefix}), @@ -399,12 +397,16 @@ insert(Host, Path, Combinator, Value, Tree) -> end. -add_plugins(Plugins) -> - Plugins0 = [ Plugin || {_, Plugin, _} <- Plugins], +add_plugin(Plugin) -> StorageBackend = application:get_env(nova, dispatch_backend, persistent_term), StoredPlugins = StorageBackend:get(?NOVA_PLUGINS, []), - Plugins1 = lists:umerge([Plugins0, StoredPlugins]), - StorageBackend:put(?NOVA_PLUGINS, Plugins1). + Plugins1 = lists:umerge([[Plugin], StoredPlugins]), + case Plugins1 of + StoredPlugins -> + ok; + _ -> + StorageBackend:put(?NOVA_PLUGINS, Plugins1) + end. normalize_plugins(Plugins) -> NormalizedPlugins = normalize_plugins(Plugins, []), @@ -413,6 +415,7 @@ normalize_plugins(Plugins) -> normalize_plugins([], Ack) -> Ack; normalize_plugins([{Type, PluginName, Options}|Tl], Ack) -> ExistingPlugins = proplists:get_value(Type, Ack, []), + add_plugin(PluginName), normalize_plugins(Tl, [{Type, [{fun PluginName:Type/4, Options}|ExistingPlugins]}|proplists:delete(Type, Ack)]). method_to_binary(get) -> <<"GET">>;