Skip to content

Commit

Permalink
HTTP controller API
Browse files Browse the repository at this point in the history
  • Loading branch information
vk committed Oct 6, 2021
1 parent 87f941a commit 2a790f9
Show file tree
Hide file tree
Showing 7 changed files with 1,481 additions and 5 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ ergw.config
data.ergw*
log.ergw*
apps/ergw_core/priv*
data
ergw@*
23 changes: 18 additions & 5 deletions apps/ergw/src/ergw_config.erl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
-compile({no_auto_import,[put/2]}).

%% API
-export([load/0, apply/1, serialize_config/1]).
-export([load/0, apply/1, serialize_config/1, load_part_of_config/1]).
-export([ergw_core_init/2]).

-ifdef(TEST).
-export([config_meta/0,
Expand Down Expand Up @@ -45,6 +46,16 @@
%%% API
%%%===================================================================

load_part_of_config(Config) when is_map(Config) ->
load_typespecs(),
do([error_m ||
load_schemas(),
validate_config_with_schema(Config),
return(coerce_config(Config))
]);
load_part_of_config(Config) ->
erlang:error(badarg, [Config]).

load() ->
load_typespecs(),
do([error_m ||
Expand Down Expand Up @@ -129,8 +140,9 @@ ergw_aaa_init(apps, #{apps := Apps0}) ->
Apps = ergw_aaa_config:validate_options(fun ergw_aaa_config:validate_app/2, Apps0, []),
maps:map(fun ergw_aaa:add_application/2, Apps),
Apps;
ergw_aaa_init(_, _) ->
ok.
ergw_aaa_init(K, _) ->
?LOG(warning, "The key ~p is missed in config of erGW-AAA", [K]),
{error, unhandled}.

ergw_sbi_client_init(Opts) ->
ergw_sbi_client_config:validate_options(fun ergw_sbi_client_config:validate_option/2, Opts).
Expand Down Expand Up @@ -185,8 +197,9 @@ ergw_core_init(proxy_map, #{proxy_map := Map}) ->
ok = ergw_core:setopts(proxy_map, Map);
ergw_core_init(http_api, #{http_api := Opts}) ->
ergw_http_api:init(Opts);
ergw_core_init(_K, _) ->
ok.
ergw_core_init(K, _) ->
?LOG(warning, "The key ~p is missed in config of erGW", [K]),
{error, unhandled}.

ergw_charging_init(rules, #{rules := Rules}) ->
maps:map(fun ergw_core:add_charging_rule/2, Rules);
Expand Down
2 changes: 2 additions & 0 deletions apps/ergw/src/ergw_http_api.erl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ start_http_listener(#{enabled := true} = Opts) ->
{"/status/[...]", http_status_handler, []},
%% 5G SBI APIs
{"/sbi/nbsf-management/v1/pcfBindings", sbi_nbsf_handler, []},
%% HTTP controller
{"/api/v1/controller", http_controller_handler, []},
%% serves static files for swagger UI
{"/api/v1/spec/ui", swagger_ui_handler, []},
{"/api/v1/spec/ui/[...]", cowboy_static, {priv_dir, ergw_core, "static"}}]}
Expand Down
163 changes: 163 additions & 0 deletions apps/ergw/src/http_controller_handler.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
%% Copyright 2021, Travelping GmbH <info@travelping.com>

%% This program is free software; you can redistribute it and/or
%% modify it under the terms of the GNU General Public License
%% as published by the Free Software Foundation; either version
%% 2 of the License, or (at your option) any later version.

-module(http_controller_handler).

-behavior(cowboy_rest).

-export([init/2, content_types_provided/2, handle_request_json/2,
allowed_methods/2, delete_resource/2, content_types_accepted/2]).

-ignore_xref([handle_request_json/2]).

-include_lib("kernel/include/logger.hrl").

-define(POST, <<"POST">>).
-define(HTTP_200, 200).
-define(HTTP_400, 400).
-define(CONTENT_TYPE_PROBLEM_JSON, #{<<"content-type">> => "application/problem+json"}).

init(Req0, State) ->
case cowboy_req:version(Req0) of
'HTTP/2' ->
{cowboy_rest, Req0, State};
_ ->
Body = jsx:encode(#{
title => <<"HTTP/2 is mandatory.">>,
status => ?HTTP_400,
cause => <<"UNSUPPORTED_HTTP_VERSION">>
}),
Req = cowboy_req:reply(?HTTP_400, ?CONTENT_TYPE_PROBLEM_JSON, Body, Req0),
{ok, Req, done}
end.

allowed_methods(Req, State) ->
{[?POST], Req, State}.

content_types_provided(Req, State) ->
{[{<<"application/json">>, handle_request_json}], Req, State}.

content_types_accepted(Req, State) ->
{[{'*', handle_request_json}], Req, State}.

delete_resource(Req, State) ->
Path = cowboy_req:path(Req),
Method = cowboy_req:method(Req),
handle_request(Method, Path, Req, State).

handle_request_json(Req, State) ->
Path = cowboy_req:path(Req),
Method = cowboy_req:method(Req),
handle_request(Method, Path, Req, State).

%%%===================================================================
%%% Handler of request
%%%===================================================================

handle_request(?POST, <<"/api/v1/controller">>, Req0, State) ->
{ok, Body, Req} = read_body(Req0),
case validate_json_req(Body) of
{ok, Response} ->
reply(?HTTP_200, Req, jsx:encode(Response), State);
{error, invalid_json} ->
Response = rfc7807(<<"Invalid JSON">>, <<"INVALID_JSON">>, []),
reply(?HTTP_400, Req, Response, State);
{error, InvalidParams} ->
Response = rfc7807(<<"Invalid JSON params">>, <<"INVALID_JSON_PARAM">>, InvalidParams),
reply(?HTTP_400, Req, Response, State)
end.

%%%===================================================================
%%% Helper functions
%%%===================================================================

validate_json_req(JsonBin) ->
case json_to_map(JsonBin) of
{ok, Map} ->
apply_config(Map);
Error ->
Error
end.

% Jesse errors: https://github.com/for-GET/jesse/blob/1.5.6/src/jesse_error.erl#L40
apply_config(Map) ->
case catch ergw_config:load_part_of_config(Map) of
{ok, Config} ->
_ = maps:fold(fun add_config_part/3, #{}, Config),
% @TODO for response collect all keys of config what was successfully applied
{ok, #{type => <<"success">>}};
{error, [_|_] = Error} = Reason ->
?LOG(warning, "~p", [Reason]),
Params = build_error_params(Error),
{error, Params};
Error ->
?LOG(warning, "Unhandled error ~p~nfor config ~p", [Error, Map]),
{error, []}
end.

build_error_params(Data) ->
build_error_params(Data, []).

build_error_params([], Acc) ->
Acc;
build_error_params([{Type, Schema, Error, Data, Path}|T], Acc) ->
I = #{
type => atom_to_binary(Type),
schema => Schema,
error => atom_to_binary(Error),
data => Data,
path => Path
},
build_error_params(T, [I|Acc]).

add_config_part(K, V, Acc) ->
Result = ergw_config:ergw_core_init(K, #{K => V}),
?LOG(info, "The ~p added with result ~p", [K, Result]),
maps:merge(Acc, Result).

json_to_map(JsonBin) ->
case catch jsx:decode(JsonBin) of
#{} = Map ->
{ok, Map};
_ ->
{error, invalid_json}
end.

read_body(Req) ->
read_body(Req, <<>>).

read_body(Req0, Acc) ->
case cowboy_req:read_body(Req0) of
{ok, Data, Req} ->
{ok, <<Acc/binary, Data/binary>>, Req};
{more, Data, Req} ->
read_body(Req, <<Acc/binary, Data/binary>>)
end.

reply(StatusCode, Req0, Body, State) ->
Req = case StatusCode of
?HTTP_200 ->
cowboy_req:reply(StatusCode, #{}, Body, Req0);
?HTTP_400 ->
cowboy_req:reply(StatusCode, ?CONTENT_TYPE_PROBLEM_JSON, Body, Req0)
end,
{stop, Req, State}.

%% https://datatracker.ietf.org/doc/html/rfc7807
rfc7807(Title, Cause, []) ->
jsx:encode(#{
title => Title,
status => ?HTTP_400,
cause => Cause
});
rfc7807(Title, Cause, InvalidParams) ->
jsx:encode(#{
title => Title,
status => ?HTTP_400,
cause => Cause,
'invalid-params' => InvalidParams
}).

0 comments on commit 2a790f9

Please sign in to comment.