feat: return 415 when UNSUPPORTED_MEDIA_TYPE

This commit is contained in:
zhongwencool 2024-05-21 09:18:07 +08:00
parent 63721bf1db
commit 5c759941d5
10 changed files with 154 additions and 31 deletions

View File

@ -86,5 +86,6 @@
{'SOURCE_ERROR', <<"Source error">>}, {'SOURCE_ERROR', <<"Source error">>},
{'UPDATE_FAILED', <<"Update failed">>}, {'UPDATE_FAILED', <<"Update failed">>},
{'REST_FAILED', <<"Reset source or config failed">>}, {'REST_FAILED', <<"Reset source or config failed">>},
{'CLIENT_NOT_RESPONSE', <<"Client not responding">>} {'CLIENT_NOT_RESPONSE', <<"Client not responding">>},
{'UNSUPPORTED_MEDIA_TYPE', <<"Unsupported media type">>}
]). ]).

View File

@ -82,7 +82,9 @@
namespace() -> "bridge". namespace() -> "bridge".
api_spec() -> api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}). emqx_dashboard_swagger:spec(?MODULE, #{
check_schema => fun emqx_dashboard_swagger:is_content_type_json/2
}).
paths() -> paths() ->
[ [

View File

@ -30,6 +30,7 @@
-export([base_path/0]). -export([base_path/0]).
-export([relative_uri/1, get_relative_uri/1]). -export([relative_uri/1, get_relative_uri/1]).
-export([compose_filters/2]). -export([compose_filters/2]).
-export([is_content_type_json/2, validate_content_type/3]).
-export([ -export([
filter_check_request/2, filter_check_request/2,
@ -152,6 +153,30 @@ spec(Module, Options) ->
), ),
{ApiSpec, components(lists:usort(AllRefs), Options)}. {ApiSpec, components(lists:usort(AllRefs), Options)}.
is_content_type_json(Params, Meta) ->
validate_content_type(Params, Meta, <<"application/json">>).
%% tip: Skip content-type check if body is empty.
validate_content_type(#{body := Body} = Params, #{method := Method}, Expect) when
(Method =:= put orelse
Method =:= post orelse
method =:= patch) andalso
Body =/= #{}
->
ExpectSize = byte_size(Expect),
case find_content_type(Params) of
<<Expect:ExpectSize/binary, _/binary>> ->
{ok, Params};
_ ->
{415, 'UNSUPPORTED_MEDIA_TYPE', <<"content-type:", Expect/binary, " Required">>}
end;
validate_content_type(Params, _Meta, _Expect) ->
{ok, Params}.
find_content_type(#{headers := #{<<"content-type">> := Type}}) -> Type;
find_content_type(#{headers := #{<<"Content-Type">> := Type}}) -> Type;
find_content_type(_Headers) -> error.
-spec namespace() -> hocon_schema:name(). -spec namespace() -> hocon_schema:name().
namespace() -> "public". namespace() -> "public".
@ -341,8 +366,12 @@ translate_req(Request, #{module := Module, path := Path, method := Method}, Chec
Params = maps:get(parameters, Spec, []), Params = maps:get(parameters, Spec, []),
Body = maps:get('requestBody', Spec, []), Body = maps:get('requestBody', Spec, []),
{Bindings, QueryStr} = check_parameters(Request, Params, Module), {Bindings, QueryStr} = check_parameters(Request, Params, Module),
NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)), case check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)) of
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}} {ok, NewBody} ->
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}};
Error ->
Error
end
catch catch
throw:HoconError -> throw:HoconError ->
Msg = hocon_error_msg(HoconError), Msg = hocon_error_msg(HoconError),
@ -523,16 +552,23 @@ unwrap_array_conv([HVal | _], _Opts) -> HVal;
unwrap_array_conv(SingleVal, _Opts) -> SingleVal. unwrap_array_conv(SingleVal, _Opts) -> SingleVal.
check_request_body(#{body := Body}, Schema, Module, CheckFun, true) -> check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
Type0 = hocon_schema:field_schema(Schema, type), %% the body was already being decoded
Type = %% if the content-type header specified application/json.
case Type0 of case is_binary(Body) of
?REF(StructName) -> ?R_REF(Module, StructName); false ->
_ -> Type0 Type0 = hocon_schema:field_schema(Schema, type),
end, Type =
NewSchema = ?INIT_SCHEMA#{roots => [{root, Type}]}, case Type0 of
Option = #{required => false}, ?REF(StructName) -> ?R_REF(Module, StructName);
#{<<"root">> := NewBody} = CheckFun(NewSchema, #{<<"root">> => Body}, Option), _ -> Type0
NewBody; end,
NewSchema = ?INIT_SCHEMA#{roots => [{root, Type}]},
Option = #{required => false},
#{<<"root">> := NewBody} = CheckFun(NewSchema, #{<<"root">> => Body}, Option),
{ok, NewBody};
true ->
{415, 'UNSUPPORTED_MEDIA_TYPE', <<"content-type:application/json Required">>}
end;
%% TODO not support nest object check yet, please use ref! %% TODO not support nest object check yet, please use ref!
%% 'requestBody' = [ {per_page, mk(integer(), #{}}, %% 'requestBody' = [ {per_page, mk(integer(), #{}},
%% {nest_object, [ %% {nest_object, [
@ -541,18 +577,19 @@ check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
%% ]} %% ]}
%% ] %% ]
check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list(Spec) -> check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list(Spec) ->
lists:foldl( {ok,
fun({Name, Type}, Acc) -> lists:foldl(
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]}, fun({Name, Type}, Acc) ->
maps:merge(Acc, CheckFun(Schema, Body, #{})) Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
end, maps:merge(Acc, CheckFun(Schema, Body, #{}))
#{}, end,
Spec #{},
); Spec
)};
%% requestBody => #{content => #{ 'application/octet-stream' => %% requestBody => #{content => #{ 'application/octet-stream' =>
%% #{schema => #{ type => string, format => binary}}} %% #{schema => #{ type => string, format => binary}}}
check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map(Spec) -> check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map(Spec) ->
Body. {ok, Body}.
%% tags, description, summary, security, deprecated %% tags, description, summary, security, deprecated
meta_to_spec(Meta, Module, Options) -> meta_to_spec(Meta, Module, Options) ->

View File

@ -1,6 +1,6 @@
{application, emqx_dashboard_sso, [ {application, emqx_dashboard_sso, [
{description, "EMQX Dashboard Single Sign-On"}, {description, "EMQX Dashboard Single Sign-On"},
{vsn, "0.1.4"}, {vsn, "0.1.5"},
{registered, [emqx_dashboard_sso_sup]}, {registered, [emqx_dashboard_sso_sup]},
{applications, [ {applications, [
kernel, kernel,

View File

@ -38,7 +38,12 @@
namespace() -> "dashboard_sso". namespace() -> "dashboard_sso".
api_spec() -> api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false, translate_body => false}). emqx_dashboard_swagger:spec(?MODULE, #{
translate_body => false,
check_schema => fun(Params, Meta) ->
emqx_dashboard_swagger:validate_content_type(Params, Meta, <<"application/xml">>)
end
}).
paths() -> paths() ->
[ [

View File

@ -28,7 +28,9 @@
namespace() -> "license_http_api". namespace() -> "license_http_api".
api_spec() -> api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}). emqx_dashboard_swagger:spec(?MODULE, #{
check_schema => fun emqx_dashboard_swagger:is_content_type_json/2
}).
paths() -> paths() ->
[ [

View File

@ -216,6 +216,45 @@ t_create_failed(_Config) ->
?assertEqual(BadRequest, create_banned(Expired)), ?assertEqual(BadRequest, create_banned(Expired)),
ok. ok.
%% validate check_schema is true with bad content_type
t_create_with_bad_content_type(_Config) ->
Now = erlang:system_time(second),
At = emqx_banned:to_rfc3339(Now),
Until = emqx_banned:to_rfc3339(Now + 3),
Who = <<"TestClient-"/utf8>>,
By = <<"banned suite 中"/utf8>>,
Reason = <<"test测试"/utf8>>,
Banned = #{
as => clientid,
who => Who,
by => By,
reason => Reason,
at => At,
until => Until
},
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
Path = emqx_mgmt_api_test_util:api_path(["banned"]),
{error, {
{"HTTP/1.1", 415, "Unsupported Media Type"},
_Headers,
MsgBin
}} =
emqx_mgmt_api_test_util:request_api(
post,
Path,
"",
AuthHeader,
Banned,
#{'content-type' => "application/xml", return_all => true}
),
?assertEqual(
#{
<<"code">> => <<"UNSUPPORTED_MEDIA_TYPE">>,
<<"message">> => <<"content-type:application/json Required">>
},
emqx_utils_json:decode(MsgBin)
).
t_delete(_Config) -> t_delete(_Config) ->
Now = erlang:system_time(second), Now = erlang:system_time(second),
At = emqx_banned:to_rfc3339(Now), At = emqx_banned:to_rfc3339(Now),

View File

@ -118,6 +118,7 @@ request_api(Method, Url, QueryParams, AuthOrHeaders, Body, Opts) when
(Method =:= put) orelse (Method =:= put) orelse
(Method =:= delete) (Method =:= delete)
-> ->
ContentType = maps:get('content-type', Opts, "application/json"),
NewUrl = NewUrl =
case QueryParams of case QueryParams of
"" -> Url; "" -> Url;
@ -125,9 +126,8 @@ request_api(Method, Url, QueryParams, AuthOrHeaders, Body, Opts) when
end, end,
do_request_api( do_request_api(
Method, Method,
{NewUrl, build_http_header(AuthOrHeaders), "application/json", {NewUrl, build_http_header(AuthOrHeaders), ContentType, emqx_utils_json:encode(Body)},
emqx_utils_json:encode(Body)}, maps:remove('content-type', Opts)
Opts
). ).
do_request_api(Method, Request, Opts) -> do_request_api(Method, Request, Opts) ->

View File

@ -137,7 +137,9 @@ end).
namespace() -> "rule". namespace() -> "rule".
api_spec() -> api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}). emqx_dashboard_swagger:spec(?MODULE, #{
check_schema => fun emqx_dashboard_swagger:is_content_type_json/2
}).
paths() -> paths() ->
[ [

View File

@ -60,8 +60,11 @@ maybe_json_decode(X) ->
end. end.
request(Method, Path, Params) -> request(Method, Path, Params) ->
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
Opts = #{return_all => true}, Opts = #{return_all => true},
request(Method, Path, Params, Opts).
request(Method, Path, Params, Opts) ->
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
case emqx_mgmt_api_test_util:request_api(Method, Path, "", AuthHeader, Params, Opts) of case emqx_mgmt_api_test_util:request_api(Method, Path, "", AuthHeader, Params, Opts) of
{ok, {Status, Headers, Body0}} -> {ok, {Status, Headers, Body0}} ->
Body = maybe_json_decode(Body0), Body = maybe_json_decode(Body0),
@ -386,6 +389,38 @@ t_rule_test_smoke(_Config) ->
?assertEqual([], FailedCases), ?assertEqual([], FailedCases),
ok. ok.
%% validate check_schema is function with bad content_type
t_rule_test_with_bad_content_type(_Config) ->
Params =
#{
<<"context">> =>
#{
<<"clientid">> => <<"c_emqx">>,
<<"event_type">> => <<"message_publish">>,
<<"payload">> => <<"{\"msg\": \"hello\"}">>,
<<"qos">> => 1,
<<"topic">> => <<"t/a">>,
<<"username">> => <<"u_emqx">>
},
<<"sql">> => <<"SELECT\n *\nFROM\n \"t/#\"">>
},
Method = post,
Path = emqx_mgmt_api_test_util:api_path(["rule_test"]),
Opts = #{return_all => true, 'content-type' => "application/xml"},
?assertMatch(
{error,
{
{"HTTP/1.1", 415, "Unsupported Media Type"},
_Headers,
#{
<<"code">> := <<"UNSUPPORTED_MEDIA_TYPE">>,
<<"message">> := <<"content-type:application/json Required">>
}
}},
request(Method, Path, Params, Opts)
),
ok.
do_t_rule_test_smoke(#{input := Input, expected := #{code := ExpectedCode}} = Case) -> do_t_rule_test_smoke(#{input := Input, expected := #{code := ExpectedCode}} = Case) ->
{_ErrOrOk, {{_, Code, _}, _, Body}} = sql_test_api(Input), {_ErrOrOk, {{_, Code, _}, _, Body}} = sql_test_api(Input),
case Code =:= ExpectedCode of case Code =:= ExpectedCode of