diff --git a/Makefile b/Makefile index 2930a4926..da3810271 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PROJECT = cowboy PROJECT_DESCRIPTION = Small, fast, modern HTTP server. -PROJECT_VERSION = 2.9.0 +PROJECT_VERSION = 2.9.1 PROJECT_REGISTERED = cowboy_clock # Options. diff --git a/src/cowboy.erl b/src/cowboy.erl index c4be25bf9..17b56a133 100644 --- a/src/cowboy.erl +++ b/src/cowboy.erl @@ -19,6 +19,9 @@ -export([stop_listener/1]). -export([set_env/3]). +-export([add_routes/2]). +-export([remove_routes/2]). + %% Internal. -export([log/2]). -export([log/4]). @@ -70,6 +73,18 @@ ensure_connection_type(TransOpts) -> stop_listener(Ref) -> ranch:stop_listener(Ref). +-spec add_routes(ranch:ref(), cowboy_router:dispatch_rules()) -> ok. +add_routes(Ref, NewDispatch) -> + Opts = #{env := Env0 = #{dispatch := Dispatch}} = ranch:get_protocol_options(Ref), + Env1 = Env0#{dispatch => add_routes_(NewDispatch, Dispatch)}, + ok = ranch:set_protocol_options(Ref, Opts#{env => Env1}). + +-spec remove_routes(ranch:ref(), cowboy_router:dispatch_rules()) -> ok. +remove_routes(Ref, NewDispatch) -> + Opts = #{env := Env0 = #{dispatch := Dispatch}} = ranch:get_protocol_options(Ref), + Env1 = Env0#{dispatch => remove_routes_(NewDispatch, Dispatch)}, + ok = ranch:set_protocol_options(Ref, Opts#{env => Env1}). + -spec set_env(ranch:ref(), atom(), any()) -> ok. set_env(Ref, Name, Value) -> Opts = ranch:get_protocol_options(Ref), @@ -103,3 +118,43 @@ log(Level, Format, Args, _) -> debug -> info_msg end, error_logger:Function(Format, Args). + +add_routes_([], Dispatch) -> + Dispatch; +add_routes_(Dispatch, []) -> + Dispatch; +add_routes_([Route0 = {HostMatch, CowboyFields, Paths}|Tail], Dispatch0) -> + {Route1, Dispatch1} = + case lists:keytake(HostMatch, 1, Dispatch0) of + {value, {HostMatch, ExistingCowboyFields, ExistingPaths}, Dispatch} -> + Route = {HostMatch, lists:flatten(CowboyFields, ExistingCowboyFields), lists:flatten(Paths, ExistingPaths)}, + {Route, Dispatch}; + false -> + {Route0, Dispatch0} + end, + [Route1 | add_routes_(Tail, Dispatch1)]. + +remove_routes_([], Dispatch) -> + Dispatch; +remove_routes_(_Dispatch, []) -> + []; +remove_routes_([Route0 = {HostMatch, CowboyFields, Paths}|Tail], Dispatch0) -> + {Route1, Dispatch1} = + case lists:keytake(HostMatch, 1, Dispatch0) of + {value, {HostMatch, ExistingCowboyFields, ExistingPaths}, Dispatch} -> + case ExistingPaths -- Paths of + [] -> + {undefined, Dispatch}; + NewPaths -> + Route = {HostMatch, ExistingCowboyFields -- CowboyFields, NewPaths}, + {Route, Dispatch} + end; + false -> + {Route0, Dispatch0} + end, + case Route1 of + undefined -> + add_routes_(Tail, Dispatch1); + _ -> + [Route1 | add_routes_(Tail, Dispatch1)] + end. \ No newline at end of file diff --git a/test/misc_SUITE.erl b/test/misc_SUITE.erl index 624563649..e65dfac17 100644 --- a/test/misc_SUITE.erl +++ b/test/misc_SUITE.erl @@ -21,14 +21,15 @@ -import(cowboy_test, [gun_open/1]). all() -> - [{group, app}, {group, set_env}|cowboy_test:common_all()]. + [{group, app}, {group, set_env}, {group, routes}|cowboy_test:common_all()]. groups() -> Common = ct_helper:all(?MODULE) -- [restart_gracefully, set_env, set_env_missing], [ {app, [], [restart_gracefully]}, - {set_env, [parallel], [set_env, set_env_missing]} + {set_env, [parallel], [set_env, set_env_missing]}, + {routes, [parallel], [add_route, remove_route]} |cowboy_test:common_groups(Common)]. init_per_group(Name=app, Config) -> @@ -37,11 +38,15 @@ init_per_group(Name=app, Config) -> }, Config); init_per_group(set_env, Config) -> Config; +init_per_group(routes, Config) -> + Config; init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(set_env, _) -> ok; +end_per_group(routes, _) -> + ok; end_per_group(Name, _) -> cowboy:stop_listener(Name). @@ -119,3 +124,65 @@ set_env_missing(Config0) -> after cowboy:stop_listener(?FUNCTION_NAME) end. + +add_route(Config0) -> + doc("Live add a route."), + Dispatch = + cowboy_router:compile([{'_', [ + {"/", hello_h, []} + ]}]), + Config = cowboy_test:init_http(?FUNCTION_NAME, #{ + env => #{ + dispatch => Dispatch + } + }, Config0), + try + ConnPid1 = gun_open(Config), + Ref1 = gun:get(ConnPid1, "/"), + {response, _, 200, _} = gun:await(ConnPid1, Ref1), + ConnPid2 = gun_open(Config), + Ref2 = gun:get(ConnPid2, "/new"), + {response, _, 404, _} = gun:await(ConnPid2, Ref2), + cowboy:add_routes(?FUNCTION_NAME, cowboy_router:compile([{'_', [ + {"/new", hello_h, []} + ]}])), + %% Only new connections get the updated environment. + ConnPid3 = gun_open(Config), + Ref3 = gun:get(ConnPid3, "/new"), + {response, _, 200, _} = gun:await(ConnPid3, Ref3) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +remove_route(Config0) -> + doc("Live remove a route."), + Dispatch = + cowboy_router:compile([{'_', [ + {"/", hello_h, []}, + {"/new", hello_h, []} + ]}]), + Config = cowboy_test:init_http(?FUNCTION_NAME, #{ + env => #{ + dispatch => Dispatch + } + }, Config0), + try + ConnPid1 = gun_open(Config), + Ref1 = gun:get(ConnPid1, "/"), + {response, _, 200, _} = gun:await(ConnPid1, Ref1), + ConnPid2 = gun_open(Config), + Ref2 = gun:get(ConnPid2, "/new"), + {response, _, 200, _} = gun:await(ConnPid2, Ref2), + cowboy:remove_routes(?FUNCTION_NAME, cowboy_router:compile([{'_', [ + {"/new", hello_h, []} + ]}])), + %% Only new connections get the updated environment. + ConnPid3 = gun_open(Config), + Ref3 = gun:get(ConnPid3, "/"), + {response, _, 200, _} = gun:await(ConnPid3, Ref3), + ConnPid4 = gun_open(Config), + Ref4 = gun:get(ConnPid4, "/new"), + {response, _, 404, _} = gun:await(ConnPid4, Ref4) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. \ No newline at end of file