fix(auth http): using ehttpc (#4021)

* fix(auth http): using ehttpc

* chore(ehttpc): update tag of ehttpc

* fix(config): update comment
This commit is contained in:
tigercl 2021-01-16 23:10:53 +08:00 committed by GitHub
parent a6de90c3f9
commit fd2e9f147b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 326 additions and 646 deletions

View File

@ -2,24 +2,29 @@
## HTTP Auth/ACL Plugin ## HTTP Auth/ACL Plugin
##-------------------------------------------------------------------- ##--------------------------------------------------------------------
##-------------------------------------------------------------------- ## HTTP URL API path for Auth Request
## Authentication request.
## HTTP URL API path for authentication request
## ##
## Value: URL ## Value: URL
## ##
## Examples: http://127.0.0.1:8991/mqtt/auth, https://[::1]:8991/mqtt/auth ## Examples: http://127.0.0.1:80/mqtt/auth, https://[::1]:80/mqtt/auth
auth.http.auth_req = http://127.0.0.1:8991/mqtt/auth auth.http.auth_req.url = http://127.0.0.1:80/mqtt/auth
## HTTP Request Method for Auth Request
##
## Value: post | get ## Value: post | get
auth.http.auth_req.method = post auth.http.auth_req.method = post
## It only works when method=post ## HTTP Request Headers for Auth Request, Content-Type header is configured by default.
## Value: json | x-www-form-urlencoded ## The possible values of the Content-Type header: application/x-www-form-urlencoded, application/json
auth.http.auth_req.content_type = x-www-form-urlencoded ##
## Examples: auth.http.auth_req.headers.accept = */*
auth.http.auth_req.headers.content-type = application/x-www-form-urlencoded
## Variables: ## Parameters used to construct the request body or query string parameters
## When the request method is GET, these parameters will be converted into query string parameters
## When the request method is POST, the final format is determined by content-type
##
## Available Variables:
## - %u: username ## - %u: username
## - %c: clientid ## - %c: clientid
## - %a: ipaddress ## - %a: ipaddress
@ -29,27 +34,32 @@ auth.http.auth_req.content_type = x-www-form-urlencoded
## - %C: common name of client TLS cert ## - %C: common name of client TLS cert
## - %d: subject of client TLS cert ## - %d: subject of client TLS cert
## ##
## Value: Params ## Value: <K1>=<V1>,<K2>=<V2>,...
auth.http.auth_req.params = clientid=%c,username=%u,password=%P auth.http.auth_req.params = clientid=%c,username=%u,password=%P
##-------------------------------------------------------------------- ## HTTP URL API path for SuperUser Request
## Superuser request.
## HTTP URL API path for Superuser request
## ##
## Value: URL ## Value: URL
## ##
## Examples: http://127.0.0.1:8991/mqtt/superuser, https://[::1]:8991/mqtt/superuser ## Examples: http://127.0.0.1:80/mqtt/superuser, https://[::1]:80/mqtt/superuser
#auth.http.super_req = http://127.0.0.1:8991/mqtt/superuser auth.http.super_req.url = http://127.0.0.1:80/mqtt/superuser
## HTTP Request Method for SuperUser Request
##
## Value: post | get ## Value: post | get
#auth.http.super_req.method = post auth.http.super_req.method = post
## It only works when method=pos ## HTTP Request Headers for SuperUser Request, Content-Type header is configured by default.
## Value: json | x-www-form-urlencoded ## The possible values of the Content-Type header: application/x-www-form-urlencoded, application/json
#auth.http.super_req.content_type = x-www-form-urlencoded ##
## Examples: auth.http.super_req.headers.accept = */*
auth.http.super_req.headers.content-type = application/x-www-form-urlencoded
## Variables: ## Parameters used to construct the request body or query string parameters
## When the request method is GET, these parameters will be converted into query string parameters
## When the request method is POST, the final format is determined by content-type
##
## Available Variables:
## - %u: username ## - %u: username
## - %c: clientid ## - %c: clientid
## - %a: ipaddress ## - %a: ipaddress
@ -59,42 +69,45 @@ auth.http.auth_req.params = clientid=%c,username=%u,password=%P
## - %C: common name of client TLS cert ## - %C: common name of client TLS cert
## - %d: subject of client TLS cert ## - %d: subject of client TLS cert
## ##
## Value: Params ## Value: <K1>=<V1>,<K2>=<V2>,...
#auth.http.super_req.params = clientid=%c,username=%u auth.http.super_req.params = clientid=%c,username=%u
##-------------------------------------------------------------------- ## HTTP URL API path for ACL Request
## ACL request.
## HTTP URL API path for ACL request
## ##
## Value: URL ## Value: URL
## ##
## Examples: http://127.0.0.1:8991/mqtt/acl, https://[::1]:8991/mqtt/acl ## Examples: http://127.0.0.1:80/mqtt/acl, https://[::1]:80/mqtt/acl
auth.http.acl_req = http://127.0.0.1:8991/mqtt/acl auth.http.acl_req.url = http://127.0.0.1:80/mqtt/acl
## HTTP Request Method for ACL Request
##
## Value: post | get ## Value: post | get
auth.http.acl_req.method = get auth.http.acl_req.method = post
## It only works when method=post ## HTTP Request Headers for ACL Request, Content-Type header is configured by default.
## Value: json | x-www-form-urlencoded ## The possible values of the Content-Type header: application/x-www-form-urlencoded, application/json
auth.http.acl_req.content_type = x-www-form-urlencoded ##
## Examples: auth.http.acl_req.headers.accept = */*
auth.http.acl_req.headers.content-type = application/x-www-form-urlencoded
## Variables: ## Parameters used to construct the request body or query string parameters
## - %A: 1 | 2, 1 = sub, 2 = pub ## When the request method is GET, these parameters will be converted into query string parameters
## When the request method is POST, the final format is determined by content-type
##
## Available Variables:
## - %u: username ## - %u: username
## - %c: clientid ## - %c: clientid
## - %a: ipaddress ## - %a: ipaddress
## - %r: protocol ## - %r: protocol
## - %m: mountpoint ## - %P: password
## - %t: topic ## - %p: sockport of server accepted
## - %C: common name of client TLS cert
## - %d: subject of client TLS cert
## ##
## Value: Params ## Value: <K1>=<V1>,<K2>=<V2>,...
auth.http.acl_req.params = access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t,mountpoint=%m auth.http.acl_req.params = access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t,mountpoint=%m
##------------------------------------------------------------------------------ ## Time-out time for the request.
## Http Reqeust options
## Time-out time for the http request, 0 is never timeout.
## ##
## Value: Duration ## Value: Duration
## -h: hour, e.g. '2h' for 2 hours ## -h: hour, e.g. '2h' for 2 hours
@ -102,37 +115,23 @@ auth.http.acl_req.params = access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t,
## -s: second, e.g. '30s' for 30 seconds ## -s: second, e.g. '30s' for 30 seconds
## ##
## Default: 5s ## Default: 5s
## auth.http.request.timeout = 5s auth.http.timeout = 5s
## Connection time-out time, used during the initial request ## Connection time-out time, used during the initial request,
## when the client is connecting to the server ## when the client is connecting to the server.
## ##
## Value: Duration ## Value: Duration
## -h: hour, e.g. '2h' for 2 hours
## -m: minute, e.g. '5m' for 5 minutes
## -s: second, e.g. '30s' for 30 seconds
## ##
## Default is same with the timeout option ## Default: 5s
## auth.http.request.connect_timeout = 0 auth.http.connect_timeout = 5s
## Re-send http reuqest times ## Connection process pool size
## ##
## Value: integer ## Value: Number
## auth.http.pool_size = 32
## Default: 3
auth.http.request.retry_times = 5
## The interval for re-sending the http request
##
## Value: Duration
##
## Default: 1s
auth.http.request.retry_interval = 1s
## The 'Exponential Backoff' mechanism for re-sending request. The actually
## re-send time interval is `interval * backoff ^ times`
##
## Value: float
##
## Default: 2.0
auth.http.request.retry_backoff = 2.0
##------------------------------------------------------------------------------ ##------------------------------------------------------------------------------
## SSL options ## SSL options
@ -152,11 +151,3 @@ auth.http.request.retry_backoff = 2.0
## ##
## Value: File ## Value: File
## auth.http.ssl.keyfile = {{ platform_etc_dir }}/certs/client-key.pem ## auth.http.ssl.keyfile = {{ platform_etc_dir }}/certs/client-key.pem
##--------------------------------------------------------------------
## HTTP Request Headers
##
## Example: auth.http.header.Accept-Encoding = *
##
## Value: String
## auth.http.header.Accept = */*

View File

@ -1,8 +1,6 @@
-define(APP, emqx_auth_http). -define(APP, emqx_auth_http).
-record(http_request, {method = post, path, headers, params, request_timeout}).
-record(auth_metrics, { -record(auth_metrics, {
success = 'client.auth.success', success = 'client.auth.success',
failure = 'client.auth.failure', failure = 'client.auth.failure',

View File

@ -1,6 +1,6 @@
%%-*- mode: erlang -*- %%-*- mode: erlang -*-
%% emqx_auth_http config mapping %% emqx_auth_http config mapping
{mapping, "auth.http.auth_req", "emqx_auth_http.auth_req", [ {mapping, "auth.http.auth_req.url", "emqx_auth_http.auth_req", [
{datatype, string} {datatype, string}
]}. ]}.
@ -9,9 +9,8 @@
{datatype, {enum, [post, get]}} {datatype, {enum, [post, get]}}
]}. ]}.
{mapping, "auth.http.auth_req.content_type", "emqx_auth_http.auth_req", [ {mapping, "auth.http.auth_req.headers.$field", "emqx_auth_http.auth_req", [
{default, 'x-www-form-urlencoded'}, {datatype, string}
{datatype, {enum, ['json', 'x-www-form-urlencoded']}}
]}. ]}.
{mapping, "auth.http.auth_req.params", "emqx_auth_http.auth_req", [ {mapping, "auth.http.auth_req.params", "emqx_auth_http.auth_req", [
@ -19,18 +18,19 @@
]}. ]}.
{translation, "emqx_auth_http.auth_req", fun(Conf) -> {translation, "emqx_auth_http.auth_req", fun(Conf) ->
case cuttlefish:conf_get("auth.http.auth_req", Conf) of case cuttlefish:conf_get("auth.http.auth_req.url", Conf, undefined) of
undefined -> cuttlefish:unset(); undefined -> cuttlefish:unset();
Url -> Url ->
Headers = cuttlefish_variable:filter_by_prefix("auth.http.auth_req.headers", Conf),
Params = cuttlefish:conf_get("auth.http.auth_req.params", Conf), Params = cuttlefish:conf_get("auth.http.auth_req.params", Conf),
[{url, Url}, [{url, Url},
{method, cuttlefish:conf_get("auth.http.auth_req.method", Conf)}, {method, cuttlefish:conf_get("auth.http.auth_req.method", Conf)},
{content_type, list_to_binary("application/" ++ atom_to_list(cuttlefish:conf_get("auth.http.auth_req.content_type", Conf)))}, {headers, [{K, V} || {[_, _, _, _, K], V} <- Headers]},
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}] {params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
end end
end}. end}.
{mapping, "auth.http.super_req", "emqx_auth_http.super_req", [ {mapping, "auth.http.super_req.url", "emqx_auth_http.super_req", [
{datatype, string} {datatype, string}
]}. ]}.
@ -39,9 +39,8 @@ end}.
{datatype, {enum, [post, get]}} {datatype, {enum, [post, get]}}
]}. ]}.
{mapping, "auth.http.super_req.content_type", "emqx_auth_http.super_req", [ {mapping, "auth.http.super_req.headers.$field", "emqx_auth_http.super_req", [
{default, 'x-www-form-urlencoded'}, {datatype, string}
{datatype, {enum, ['json', 'x-www-form-urlencoded']}}
]}. ]}.
{mapping, "auth.http.super_req.params", "emqx_auth_http.super_req", [ {mapping, "auth.http.super_req.params", "emqx_auth_http.super_req", [
@ -49,17 +48,19 @@ end}.
]}. ]}.
{translation, "emqx_auth_http.super_req", fun(Conf) -> {translation, "emqx_auth_http.super_req", fun(Conf) ->
case cuttlefish:conf_get("auth.http.super_req", Conf, undefined) of case cuttlefish:conf_get("auth.http.super_req.url", Conf, undefined) of
undefined -> cuttlefish:unset(); undefined -> cuttlefish:unset();
Url -> Params = cuttlefish:conf_get("auth.http.super_req.params", Conf), Url ->
[{url, Url}, {method, cuttlefish:conf_get("auth.http.super_req.method", Conf)}, Headers = cuttlefish_variable:filter_by_prefix("auth.http.super_req.headers", Conf),
{content_type, list_to_binary("application/" ++ atom_to_list(cuttlefish:conf_get("auth.http.super_req.content_type", Conf)))}, Params = cuttlefish:conf_get("auth.http.super_req.params", Conf),
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}] [{url, Url},
{method, cuttlefish:conf_get("auth.http.super_req.method", Conf)},
{headers, [{K, V} || {[_, _, _, _, K], V} <- Headers]},
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
end end
end}. end}.
{mapping, "auth.http.acl_req", "emqx_auth_http.acl_req", [ {mapping, "auth.http.acl_req.url", "emqx_auth_http.acl_req", [
{default, undefined},
{datatype, string} {datatype, string}
]}. ]}.
@ -68,9 +69,8 @@ end}.
{datatype, {enum, [post, get]}} {datatype, {enum, [post, get]}}
]}. ]}.
{mapping, "auth.http.acl_req.content_type", "emqx_auth_http.acl_req", [ {mapping, "auth.http.acl_req.headers.$field", "emqx_auth_http.acl_req", [
{default, 'x-www-form-urlencoded'}, {datatype, string}
{datatype, {enum, ['json', 'x-www-form-urlencoded']}}
]}. ]}.
{mapping, "auth.http.acl_req.params", "emqx_auth_http.acl_req", [ {mapping, "auth.http.acl_req.params", "emqx_auth_http.acl_req", [
@ -78,92 +78,41 @@ end}.
]}. ]}.
{translation, "emqx_auth_http.acl_req", fun(Conf) -> {translation, "emqx_auth_http.acl_req", fun(Conf) ->
case cuttlefish:conf_get("auth.http.acl_req", Conf, undefined) of case cuttlefish:conf_get("auth.http.acl_req.url", Conf, undefined) of
undefined -> cuttlefish:unset(); undefined -> cuttlefish:unset();
Url -> Params = cuttlefish:conf_get("auth.http.acl_req.params", Conf), Url ->
[{url, Url}, Headers = cuttlefish_variable:filter_by_prefix("auth.http.acl_req.headers", Conf),
{method, cuttlefish:conf_get("auth.http.acl_req.method", Conf)}, Params = cuttlefish:conf_get("auth.http.acl_req.params", Conf),
{content_type, list_to_binary("application/" ++ atom_to_list(cuttlefish:conf_get("auth.http.acl_req.content_type", Conf)))}, [{url, Url},
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}] {method, cuttlefish:conf_get("auth.http.acl_req.method", Conf)},
{headers, [{K, V} || {[_, _, _, _, K], V} <- Headers]},
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
end end
end}. end}.
{mapping, "auth.http.request.timeout", "emqx_auth_http.request_timeout", [ {mapping, "auth.http.timeout", "emqx_auth_http.timeout", [
{default, "5s"}, {default, "5s"},
{datatype, [integer, {duration, ms}]} {datatype, [integer, {duration, ms}]}
]}. ]}.
{mapping, "auth.http.pool_size", "emqx_auth_http.pool_opts", [ {mapping, "auth.http.connect_timeout", "emqx_auth_http.connect_timeout", [
{default, "5s"},
{datatype, [integer, {duration, ms}]}
]}.
{mapping, "auth.http.pool_size", "emqx_auth_http.pool_size", [
{default, 8}, {default, 8},
{datatype, integer} {datatype, integer}
]}. ]}.
{mapping, "auth.http.request.connect_timeout", "emqx_auth_http.pool_opts", [ {mapping, "auth.http.ssl.cacertfile", "emqx_auth_http.cacertfile", [
{default, "5s"},
{datatype, [integer, {duration, ms}]}
]}.
{mapping, "auth.http.ssl.cacertfile", "emqx_auth_http.pool_opts", [
{datatype, string} {datatype, string}
]}. ]}.
{mapping, "auth.http.ssl.certfile", "emqx_auth_http.pool_opts", [ {mapping, "auth.http.ssl.certfile", "emqx_auth_http.certfile", [
{datatype, string} {datatype, string}
]}. ]}.
{mapping, "auth.http.ssl.keyfile", "emqx_auth_http.pool_opts", [ {mapping, "auth.http.ssl.keyfile", "emqx_auth_http.keyfile", [
{datatype, string} {datatype, string}
]}. ]}.
{mapping, "auth.http.request.retry_times", "emqx_auth_http.pool_opts", [
{default, 5},
{datatype, integer}
]}.
{mapping, "auth.http.request.retry_interval", "emqx_auth_http.pool_opts", [
{default, "1s"},
{datatype, {duration, ms}}
]}.
{mapping, "auth.http.request.retry_backoff", "emqx_auth_http.pool_opts", [
{default, 2.0},
{datatype, float}
]}.
{translation, "emqx_auth_http.pool_opts", fun(Conf) ->
Filter = fun(L) -> [{K, V} || {K, V} <- L, V =/= undefined] end,
InfinityFun = fun(0) -> infinity;
(Duration) -> Duration
end,
SslOpts = Filter([{cacertfile, cuttlefish:conf_get("auth.http.ssl.cacertfile", Conf, undefined)},
{certfile, cuttlefish:conf_get("auth.http.ssl.certfile", Conf, undefined)},
{keyfile, cuttlefish:conf_get("auth.http.ssl.keyfile", Conf, undefined)}]),
Opts = [{pool_size, cuttlefish:conf_get("auth.http.pool_size", Conf)},
{connect_timeout, InfinityFun(cuttlefish:conf_get("auth.http.request.connect_timeout", Conf))},
{retry, cuttlefish:conf_get("auth.http.request.retry_times", Conf)},
{retry_timeout, cuttlefish:conf_get("auth.http.request.retry_interval", Conf)}],
case SslOpts of
[] -> Filter(Opts);
_ ->
TlsVers = ['tlsv1.2','tlsv1.1',tlsv1],
DefaultOpts = [{versions, TlsVers},
{ciphers, lists:foldl(
fun(TlsVer, Ciphers) ->
Ciphers ++ ssl:cipher_suites(all, TlsVer)
end, [], TlsVers)}],
Filter([{ssl, DefaultOpts ++ SslOpts} | Opts])
end
end}.
{mapping, "auth.http.header.$field", "emqx_auth_http.headers", [
{datatype, string}
]}.
{translation, "emqx_auth_http.headers", fun(Conf) ->
lists:map(
fun({["auth", "http", "header", Field], Value}) ->
{Field, Value}
end,
cuttlefish_variable:filter_by_prefix("auth.http.header", Conf))
end}.

View File

@ -1,6 +1,5 @@
{deps, {deps,
[{gun, {git, "https://github.com/emqx/gun", {tag, "1.3.4"}}}, [{ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.1"}}}
{gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
]}. ]}.
{edoc_opts, [{preprocess, true}]}. {edoc_opts, [{preprocess, true}]}.

View File

@ -42,22 +42,20 @@ register_metrics() ->
%% ACL callbacks %% ACL callbacks
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
check_acl(ClientInfo, PubSub, Topic, AclResult, State) -> check_acl(ClientInfo, PubSub, Topic, AclResult, Params) ->
return_with(fun inc_metrics/1, return_with(fun inc_metrics/1,
do_check_acl(ClientInfo, PubSub, Topic, AclResult, State)). do_check_acl(ClientInfo, PubSub, Topic, AclResult, Params)).
do_check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _Config) -> do_check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _Params) ->
ok; ok;
do_check_acl(ClientInfo, PubSub, Topic, _AclResult, #{acl_req := AclReq, do_check_acl(ClientInfo, PubSub, Topic, _AclResult, #{acl := ACLParams = #{path := Path}}) ->
pool_name := PoolName}) ->
ClientInfo1 = ClientInfo#{access => access(PubSub), topic => Topic}, ClientInfo1 = ClientInfo#{access => access(PubSub), topic => Topic},
case check_acl_request(PoolName, AclReq, ClientInfo1) of case check_acl_request(ACLParams, ClientInfo1) of
{ok, 200, <<"ignore">>} -> ok; {ok, 200, <<"ignore">>} -> ok;
{ok, 200, _Body} -> {stop, allow}; {ok, 200, _Body} -> {stop, allow};
{ok, _Code, _Body} -> {stop, deny}; {ok, _Code, _Body} -> {stop, deny};
{error, Error} -> {error, Error} ->
?LOG(error, "Request ACL path ~s, error: ~p", ?LOG(error, "Request ACL path ~s, error: ~p", [Path, Error]),
[AclReq#http_request.path, Error]),
ok ok
end. end.
@ -77,12 +75,13 @@ inc_metrics({stop, deny}) ->
return_with(Fun, Result) -> return_with(Fun, Result) ->
Fun(Result), Result. Fun(Result), Result.
check_acl_request(PoolName, #http_request{path = Path, check_acl_request(#{pool_name := PoolName,
method = Method, path := Path,
headers = Headers, method := Method,
params = Params, headers := Headers,
request_timeout = RequestTimeout}, ClientInfo) -> params := Params,
request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), RequestTimeout). timeout := Timeout}, ClientInfo) ->
request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), Timeout).
access(subscribe) -> 1; access(subscribe) -> 1;
access(publish) -> 2. access(publish) -> 2.

View File

@ -3,7 +3,7 @@
{vsn, "4.3.0"}, % strict semver, bump manually! {vsn, "4.3.0"}, % strict semver, bump manually!
{modules, []}, {modules, []},
{registered, [emqx_auth_http_sup]}, {registered, [emqx_auth_http_sup]},
{applications, [kernel,stdlib,gproc,gun]}, {applications, [kernel,stdlib,ehttpc]},
{mod, {emqx_auth_http_app, []}}, {mod, {emqx_auth_http_app, []}},
{env, []}, {env, []},
{licenses, ["Apache-2.0"]}, {licenses, ["Apache-2.0"]},

View File

@ -29,10 +29,6 @@
, feedvar/2 , feedvar/2
]). ]).
-type http_request() :: #http_request{method::'get' | 'post',params::[any()]}.
%-type http_opts() :: #{clientid:=_, peerhost:=_, protocol:=_, _=>_}.
%-type retry_opts() :: #{backoff:=_, interval:=_, times:=_, _=>_}.
%% Callbacks %% Callbacks
-export([ register_metrics/0 -export([ register_metrics/0
, check/3 , check/3
@ -43,28 +39,26 @@
register_metrics() -> register_metrics() ->
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
check(ClientInfo, AuthResult, #{auth_req := AuthReq, check(ClientInfo, AuthResult, #{auth := AuthParms = #{path := Path},
super_req := SuperReq, super := SuperParams}) ->
pool_name := PoolName}) -> case authenticate(AuthParms, ClientInfo) of
case authenticate(PoolName, AuthReq, ClientInfo) of
{ok, 200, <<"ignore">>} -> {ok, 200, <<"ignore">>} ->
emqx_metrics:inc(?AUTH_METRICS(ignore)), ok; emqx_metrics:inc(?AUTH_METRICS(ignore)), ok;
{ok, 200, Body} -> {ok, 200, Body} ->
emqx_metrics:inc(?AUTH_METRICS(success)), emqx_metrics:inc(?AUTH_METRICS(success)),
IsSuperuser = is_superuser(PoolName, SuperReq, ClientInfo), IsSuperuser = is_superuser(SuperParams, ClientInfo),
{stop, AuthResult#{is_superuser => IsSuperuser, {stop, AuthResult#{is_superuser => IsSuperuser,
auth_result => success, auth_result => success,
anonymous => false, anonymous => false,
mountpoint => mountpoint(Body, ClientInfo)}}; mountpoint => mountpoint(Body, ClientInfo)}};
{ok, Code, _Body} -> {ok, Code, _Body} ->
?LOG(error, "Deny connection from path: ~s, response http code: ~p", ?LOG(error, "Deny connection from path: ~s, response http code: ~p",
[AuthReq#http_request.path, Code]), [Path, Code]),
emqx_metrics:inc(?AUTH_METRICS(failure)), emqx_metrics:inc(?AUTH_METRICS(failure)),
{stop, AuthResult#{auth_result => http_to_connack_error(Code), {stop, AuthResult#{auth_result => http_to_connack_error(Code),
anonymous => false}}; anonymous => false}};
{error, Error} -> {error, Error} ->
?LOG(error, "Request auth path: ~s, error: ~p", ?LOG(error, "Request auth path: ~s, error: ~p", [Path, Error]),
[AuthReq#http_request.path, Error]),
emqx_metrics:inc(?AUTH_METRICS(failure)), emqx_metrics:inc(?AUTH_METRICS(failure)),
%%FIXME later: server_unavailable is not right. %%FIXME later: server_unavailable is not right.
{stop, AuthResult#{auth_result => server_unavailable, {stop, AuthResult#{auth_result => server_unavailable,
@ -77,22 +71,24 @@ description() -> "Authentication by HTTP API".
%% Requests %% Requests
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
authenticate(PoolName, #http_request{path = Path, authenticate(#{pool_name := PoolName,
method = Method, path := Path,
headers = Headers, method := Method,
params = Params, headers := Headers,
request_timeout = RequestTimeout}, ClientInfo) -> params := Params,
request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), RequestTimeout). timeout := Timeout}, ClientInfo) ->
request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), Timeout).
-spec(is_superuser(atom(), maybe(http_request()), emqx_types:client()) -> boolean()). -spec(is_superuser(maybe(map()), emqx_types:client()) -> boolean()).
is_superuser(_PoolName, undefined, _ClientInfo) -> is_superuser(undefined, _ClientInfo) ->
false; false;
is_superuser(PoolName, #http_request{path = Path, is_superuser(#{pool_name := PoolName,
method = Method, path := Path,
headers = Headers, method := Method,
params = Params, headers := Headers,
request_timeout = RequestTimeout}, ClientInfo) -> params := Params,
case request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), RequestTimeout) of timeout := Timeout}, ClientInfo) ->
case request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), Timeout) of
{ok, 200, _Body} -> true; {ok, 200, _Body} -> true;
{ok, _Code, _Body} -> false; {ok, _Code, _Body} -> false;
{error, Error} -> ?LOG(error, "Request superuser path ~s, error: ~p", [Path, Error]), {error, Error} -> ?LOG(error, "Request superuser path ~s, error: ~p", [Path, Error]),

View File

@ -25,139 +25,153 @@
-export([ start/2 -export([ start/2
, stop/1 , stop/1
]). ]).
-export([init/1]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Application Callbacks %% Application Callbacks
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
start(_StartType, _StartArgs) -> start(_StartType, _StartArgs) ->
case translate_env() of {ok, Sup} = emqx_auth_http_sup:start_link(),
ok -> translate_env(),
{ok, PoolOpts} = application:get_env(?APP, pool_opts), load_hooks(),
{ok, Sup} = emqx_http_client_sup:start_link(?APP, ssl(inet(PoolOpts))), {ok, Sup}.
_ = with_env(auth_req, fun load_auth_hook/1),
_ = with_env(acl_req, fun load_acl_hook/1),
{ok, Sup};
{error, Reason} ->
{error, Reason}
end.
load_auth_hook(AuthReq) ->
ok = emqx_auth_http:register_metrics(),
SuperReq = r(application:get_env(?APP, super_req, undefined)),
Params = #{auth_req => AuthReq,
super_req => SuperReq,
pool_name => ?APP},
emqx:hook('client.authenticate', {emqx_auth_http, check, [Params]}).
load_acl_hook(AclReq) ->
ok = emqx_acl_http:register_metrics(),
Params = #{acl_req => AclReq,
pool_name => ?APP},
emqx:hook('client.check_acl', {emqx_acl_http, check_acl, [Params]}).
stop(_State) -> stop(_State) ->
emqx:unhook('client.authenticate', {emqx_auth_http, check}), unload_hooks().
emqx:unhook('client.check_acl', {emqx_acl_http, check_acl}),
emqx_http_client_sup:stop_pool(?APP).
%%--------------------------------------------------------------------
%% Dummy supervisor
%%--------------------------------------------------------------------
init([]) ->
{ok, { {one_for_all, 10, 100}, []} }.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internel functions %% Internel functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
with_env(Par, Fun) ->
case application:get_env(?APP, Par) of
undefined -> ok;
{ok, Req} -> Fun(r(Req))
end.
r(undefined) ->
undefined;
r(Config) ->
Headers = application:get_env(?APP, headers, []),
Method = proplists:get_value(method, Config, post),
Path = proplists:get_value(path, Config),
NewHeaders = [{<<"content-type">>, proplists:get_value(content_type, Config, <<"application/x-www-form-urlencoded">>)} | Headers],
Params = proplists:get_value(params, Config),
{ok, RequestTimeout} = application:get_env(?APP, request_timeout),
#http_request{method = Method, path = Path, headers = NewHeaders, params = Params, request_timeout = RequestTimeout}.
inet(PoolOpts) ->
case proplists:get_value(host, PoolOpts) of
Host when tuple_size(Host) =:= 8 ->
TransOpts = proplists:get_value(transport_opts, PoolOpts, []),
NewPoolOpts = proplists:delete(transport_opts, PoolOpts),
[{transport_opts, [inet6 | TransOpts]} | NewPoolOpts];
_ ->
PoolOpts
end.
ssl(PoolOpts) ->
case proplists:get_value(ssl, PoolOpts, []) of
[] ->
PoolOpts;
SSLOpts ->
TransOpts = proplists:get_value(transport_opts, PoolOpts, []),
NewPoolOpts = proplists:delete(transport_opts, PoolOpts),
[{transport_opts, SSLOpts ++ TransOpts}, {transport, ssl} | NewPoolOpts]
end.
translate_env() -> translate_env() ->
URLs = lists:foldl(fun(Name, Acc) -> lists:foreach(fun translate_env/1, [auth_req, super_req, acl_req]).
case application:get_env(?APP, Name, []) of
[] -> Acc; translate_env(EnvName) ->
Env -> case application:get_env(?APP, EnvName) of
URL = proplists:get_value(url, Env), undefined -> ok;
#{host := Host0, {ok, Req} ->
port := Port, {ok, PoolSize} = application:get_env(?APP, pool_size),
path := Path} = uri_string:parse(URL), {ok, ConnectTimeout} = application:get_env(?APP, connect_timeout),
Host = get_addr(Host0), URL = proplists:get_value(url, Req),
[{Name, {Host, Port, path(Path)}} | Acc] #{host := Host0,
end path := Path0,
end, [], [acl_req, auth_req, super_req]), scheme := Scheme} = URIMap = uri_string:parse(add_default_scheme(URL)),
case same_host_and_port(URLs) of Port = maps:get(port, URIMap, case Scheme of
true -> "https" -> 443;
[begin _ -> 80
{ok, Req} = application:get_env(?APP, Name), end),
application:set_env(?APP, Name, [{path, Path} | Req]) Path = path(Path0),
end || {Name, {_, _, Path}} <- URLs], Host = case inet:parse_address(Host0) of
{_, {Host, Port, _}} = lists:last(URLs), {ok, {_,_,_,_} = Addr} -> Addr;
PoolOpts = application:get_env(?APP, pool_opts, []), {ok, {_,_,_,_,_,_,_,_} = Addr} -> Addr;
application:set_env(?APP, pool_opts, [{host, Host}, {port, Port} | PoolOpts]), {error, einval} -> Host0
ok; end,
false -> Inet = case Host of
{error, different_server} {_,_,_,_} -> inet;
{_,_,_,_,_,_,_,_} -> inet6;
_ ->
case inet:getaddr(Host, inet6) of
{error, _} -> inet;
{ok, _} -> inet6
end
end,
MoreOpts = case Scheme of
"http" ->
[{transport_opts, [Inet]}];
"https" ->
CACertFile = application:get_env(?APP, cafile, undefined),
CertFile = application:get_env(?APP, certfile, undefined),
KeyFile = application:get_env(?APP, keyfile, undefined),
TLSOpts = lists:filter(fun({_K, V}) when V =:= <<>> ->
false;
(_) ->
true
end, [{keyfile, KeyFile}, {certfile, CertFile}, {cacertfile, CACertFile}]),
TlsVers = ['tlsv1.2','tlsv1.1',tlsv1],
NTLSOpts = [{versions, TlsVers},
{ciphers, lists:foldl(fun(TlsVer, Ciphers) ->
Ciphers ++ ssl:cipher_suites(all, TlsVer)
end, [], TlsVers)} | TLSOpts],
[{transport, ssl}, {transport_opts, [Inet | NTLSOpts]}]
end,
PoolOpts = [{host, Host},
{port, Port},
{pool_size, PoolSize},
{pool_type, random},
{connect_timeout, ConnectTimeout},
{retry, 5},
{retry_timeout, 1000}] ++ MoreOpts,
Method = proplists:get_value(method, Req),
Headers = proplists:get_value(headers, Req),
NHeaders = ensure_content_type_header(Method, to_lower(Headers)),
NReq = lists:keydelete(headers, 1, Req),
{ok, Timeout} = application:get_env(?APP, timeout),
application:set_env(?APP, EnvName, [{path, Path},
{headers, NHeaders},
{timeout, Timeout},
{pool_name, list_to_atom("emqx_auth_http/" ++ atom_to_list(EnvName))},
{pool_opts, PoolOpts} | NReq])
end. end.
path("") -> "/"; load_hooks() ->
path(Path) -> Path. case application:get_env(?APP, auth_req) of
undefined -> ok;
same_host_and_port([_]) -> {ok, AuthReq} ->
true; ok = emqx_auth_http:register_metrics(),
same_host_and_port([{_, {Host, Port, _}}, {_, {Host, Port, _}}]) -> PoolOpts = proplists:get_value(pool_opts, AuthReq),
true; PoolName = proplists:get_value(pool_name, AuthReq),
same_host_and_port([{_, {Host, Port, _}}, URL = {_, {Host, Port, _}} | Rest]) -> ehttpc_sup:start_pool(PoolName, PoolOpts),
same_host_and_port([URL | Rest]); case application:get_env(?APP, super_req) of
same_host_and_port(_) -> undefined ->
false. emqx:hook('client.authenticate', {emqx_auth_http, check, [#{auth => maps:from_list(AuthReq),
super => undefined}]});
get_addr(Hostname) -> {ok, SuperReq} ->
case inet:parse_address(Hostname) of PoolOpts1 = proplists:get_value(pool_opts, SuperReq),
{ok, {_,_,_,_} = Addr} -> Addr; PoolName1 = proplists:get_value(pool_name, SuperReq),
{ok, {_,_,_,_,_,_,_,_} = Addr} -> Addr; ehttpc_sup:start_pool(PoolName1, PoolOpts1),
{error, einval} -> emqx:hook('client.authenticate', {emqx_auth_http, check, [#{auth => maps:from_list(AuthReq),
case inet:getaddr(Hostname, inet) of super => maps:from_list(SuperReq)}]})
{error, _} ->
{ok, Addr} = inet:getaddr(Hostname, inet6),
Addr;
{ok, Addr} -> Addr
end end
end. end,
case application:get_env(?APP, acl_req) of
undefined -> ok;
{ok, ACLReq} ->
ok = emqx_acl_http:register_metrics(),
PoolOpts2 = proplists:get_value(pool_opts, ACLReq),
PoolName2 = proplists:get_value(pool_name, ACLReq),
ehttpc_sup:start_pool(PoolName2, PoolOpts2),
emqx:hook('client.check_acl', {emqx_acl_http, check_acl, [#{acl => maps:from_list(ACLReq)}]})
end,
ok.
unload_hooks() ->
emqx:unhook('client.authenticate', {emqx_auth_http, check}),
emqx:unhook('client.check_acl', {emqx_acl_http, check_acl}),
ehttpc_sup:stop_pool('emqx_auth_http/auth_req'),
ehttpc_sup:stop_pool('emqx_auth_http/super_req'),
ehttpc_sup:stop_pool('emqx_auth_http/acl_req'),
ok.
to_lower(Headers) ->
[{string:to_lower(K), V} || {K, V} <- Headers].
ensure_content_type_header(Method, Headers)
when Method =:= post orelse Method =:= put ->
Headers;
ensure_content_type_header(_Method, Headers) ->
lists:keydelete("content-type", 1, Headers).
add_default_scheme(URL) when is_list(URL) ->
binary_to_list(add_default_scheme(list_to_binary(URL)));
add_default_scheme(<<"http://", _/binary>> = URL) ->
URL;
add_default_scheme(<<"https://", _/binary>> = URL) ->
URL;
add_default_scheme(URL) ->
<<"http://", URL/binary>>.
path("") ->
"/";
path(Path) ->
Path.

View File

@ -29,16 +29,16 @@
request(PoolName, get, Path, Headers, Params, Timeout) -> request(PoolName, get, Path, Headers, Params, Timeout) ->
NewPath = Path ++ "?" ++ binary_to_list(cow_qs:qs(bin_kw(Params))), NewPath = Path ++ "?" ++ binary_to_list(cow_qs:qs(bin_kw(Params))),
reply(emqx_http_client:request(get, PoolName, {NewPath, Headers}, Timeout)); reply(ehttpc:request(ehttpc_pool:pick_worker(PoolName), get, {NewPath, Headers}, Timeout));
request(PoolName, post, Path, Headers, Params, Timeout) -> request(PoolName, post, Path, Headers, Params, Timeout) ->
Body = case proplists:get_value(<<"content-type">>, Headers) of Body = case proplists:get_value("content-type", Headers) of
<<"application/x-www-form-urlencoded">> -> "application/x-www-form-urlencoded" ->
cow_qs:qs(bin_kw(Params)); cow_qs:qs(bin_kw(Params));
<<"application/json">> -> "application/json" ->
emqx_json:encode(bin_kw(Params)) emqx_json:encode(bin_kw(Params))
end, end,
reply(emqx_http_client:request(post, PoolName, {Path, Headers, Body}, Timeout)). reply(ehttpc:request(ehttpc_pool:pick_worker(PoolName), post, {Path, Headers, Body}, Timeout)).
reply({ok, StatusCode, _Headers}) -> reply({ok, StatusCode, _Headers}) ->
{ok, StatusCode, <<>>}; {ok, StatusCode, <<>>};
@ -80,6 +80,7 @@ feedvar(Params, ClientInfo = #{clientid := ClientId,
({Param, "%A"}) -> {Param, maps:get(access, ClientInfo, null)}; ({Param, "%A"}) -> {Param, maps:get(access, ClientInfo, null)};
({Param, "%t"}) -> {Param, maps:get(topic, ClientInfo, null)}; ({Param, "%t"}) -> {Param, maps:get(topic, ClientInfo, null)};
({Param, "%m"}) -> {Param, maps:get(mountpoint, ClientInfo, null)}; ({Param, "%m"}) -> {Param, maps:get(mountpoint, ClientInfo, null)};
({Param, "%k"}) -> {Param, emqx_json:encode(maps:get(ws_cookie, ClientInfo, null))};
({Param, Var}) -> {Param, Var} ({Param, Var}) -> {Param, Var}
end, Params). end, Params).

View File

@ -0,0 +1,29 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_auth_http_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
{ok, {{one_for_all, 0, 1}, []}}.

View File

@ -1,256 +0,0 @@
-module(emqx_http_client).
-behaviour(gen_server).
-include_lib("emqx/include/logger.hrl").
%% APIs
-export([ start_link/3
, request/3
, request/4
]).
%% gen_server callbacks
-export([ init/1
, handle_call/3
, handle_cast/2
, handle_info/2
, terminate/2
, code_change/3
]).
-record(state, {
pool :: ecpool:poo_name(),
id :: pos_integer(),
client :: pid() | undefined,
mref :: reference() | undefined,
host :: inet:hostname() | inet:ip_address(),
port :: inet:port_number(),
gun_opts :: proplists:proplist(),
gun_state :: down | up,
requests :: map()
}).
%%--------------------------------------------------------------------
%% APIs
%%--------------------------------------------------------------------
start_link(Pool, Id, Opts) ->
gen_server:start_link(?MODULE, [Pool, Id, Opts], []).
request(Method, Pool, Req) ->
request(Method, Pool, Req, 5000).
request(get, Pool, {Path, Headers}, Timeout) ->
call(pick(Pool), {get, {Path, Headers}, Timeout}, Timeout + 1000);
request(Method, Pool, {Path, Headers, Body}, Timeout) ->
call(pick(Pool), {Method, {Path, Headers, Body}, Timeout}, Timeout + 1000).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([Pool, Id, Opts]) ->
State = #state{pool = Pool,
id = Id,
client = undefined,
mref = undefined,
host = proplists:get_value(host, Opts),
port = proplists:get_value(port, Opts),
gun_opts = gun_opts(Opts),
gun_state = down,
requests = #{}},
true = gproc_pool:connect_worker(Pool, {Pool, Id}),
{ok, State}.
handle_call(Req = {_, _, _}, From, State = #state{client = undefined, gun_state = down}) ->
case open(State) of
{ok, NewState} ->
handle_call(Req, From, NewState);
{error, Reason} ->
{reply, {error, Reason}, State}
end;
handle_call(Req = {_, _, Timeout}, From, State = #state{client = Client, mref = MRef, gun_state = down}) when is_pid(Client) ->
case gun:await_up(Client, Timeout, MRef) of
{ok, _} ->
handle_call(Req, From, State#state{gun_state = up});
{error, timeout} ->
{reply, {error, timeout}, State};
{error, Reason} ->
true = erlang:demonitor(MRef, [flush]),
{reply, {error, Reason}, State#state{client = undefined, mref = undefined}}
end;
handle_call({Method, Request, Timeout}, From, State = #state{client = Client, requests = Requests, gun_state = up}) when is_pid(Client) ->
StreamRef = do_request(Client, Method, Request),
ExpirationTime = erlang:system_time(millisecond) + Timeout,
{noreply, State#state{requests = maps:put(StreamRef, {From, ExpirationTime, undefined}, Requests)}};
handle_call(Req, _From, State) ->
?LOG(error, "Unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast(Msg, State) ->
?LOG(error, "Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({gun_response, Client, StreamRef, IsFin, StatusCode, Headers}, State = #state{client = Client, requests = Requests}) ->
Now = erlang:system_time(millisecond),
case maps:take(StreamRef, Requests) of
error ->
?LOG(error, "Received 'gun_response' message from unknown stream ref: ~p", [StreamRef]),
{noreply, State};
{{_, ExpirationTime, _}, NRequests} when Now > ExpirationTime ->
gun:cancel(Client, StreamRef),
flush_stream(Client, StreamRef),
{noreply, State#state{requests = NRequests}};
{{From, ExpirationTime, undefined}, NRequests} ->
case IsFin of
fin ->
gen_server:reply(From, {ok, StatusCode, Headers}),
{noreply, State#state{requests = NRequests}};
nofin ->
{noreply, State#state{requests = NRequests#{StreamRef => {From, ExpirationTime, {StatusCode, Headers, <<>>}}}}}
end;
_ ->
?LOG(error, "Received 'gun_response' message does not match the state"),
{noreply, State}
end;
handle_info({gun_data, Client, StreamRef, IsFin, Data}, State = #state{client = Client, requests = Requests}) ->
Now = erlang:system_time(millisecond),
case maps:take(StreamRef, Requests) of
error ->
?LOG(error, "Received 'gun_data' message from unknown stream ref: ~p", [StreamRef]),
{noreply, State};
{{_, ExpirationTime, _}, NRequests} when Now > ExpirationTime ->
gun:cancel(Client, StreamRef),
flush_stream(Client, StreamRef),
{noreply, State#state{requests = NRequests}};
{{From, ExpirationTime, {StatusCode, Headers, Acc}}, NRequests} ->
case IsFin of
fin ->
gen_server:reply(From, {ok, StatusCode, Headers, <<Acc/binary, Data/binary>>}),
{noreply, State#state{requests = NRequests}};
nofin ->
{noreply, State#state{requests = NRequests#{StreamRef => {From, ExpirationTime, {StatusCode, Headers, <<Acc/binary, Data/binary>>}}}}}
end;
_ ->
?LOG(error, "Received 'gun_data' message does not match the state"),
{noreply, State}
end;
handle_info({gun_error, Client, StreamRef, Reason}, State = #state{client = Client, requests = Requests}) ->
Now = erlang:system_time(millisecond),
case maps:take(StreamRef, Requests) of
error ->
?LOG(error, "Received 'gun_error' message from unknown stream ref: ~p~n", [StreamRef]),
{noreply, State};
{{_, ExpirationTime, _}, NRequests} when Now > ExpirationTime ->
{noreply, State#state{requests = NRequests}};
{{From, _, _}, NRequests} ->
gen_server:reply(From, {error, Reason}),
{noreply, State#state{requests = NRequests}}
end;
handle_info({gun_up, Client, _}, State = #state{client = Client}) ->
{noreply, State#state{gun_state = up}};
handle_info({gun_down, Client, _, Reason, KilledStreams, _}, State = #state{client = Client, requests = Requests}) ->
Now = erlang:system_time(millisecond),
NRequests = lists:foldl(fun(StreamRef, Acc) ->
case maps:take(StreamRef, Acc) of
error -> Acc;
{{_, ExpirationTime, _}, NAcc} when Now > ExpirationTime ->
NAcc;
{{From, _, _}, NAcc} ->
gen_server:reply(From, {error, Reason}),
NAcc
end
end, Requests, KilledStreams),
{noreply, State#state{gun_state = down, requests = NRequests}};
handle_info({'DOWN', MRef, process, Client, Reason}, State = #state{mref = MRef, client = Client, requests = Requests}) ->
true = erlang:demonitor(MRef, [flush]),
Now = erlang:system_time(millisecond),
lists:foreach(fun({_, {_, ExpirationTime, _}}) when Now > ExpirationTime ->
ok;
({_, {From, _, _}}) ->
gen_server:reply(From, {error, Reason})
end, maps:to_list(Requests)),
case open(State#state{requests = #{}}) of
{ok, NewState} ->
{noreply, NewState};
{error, Reason} ->
{noreply, State#state{mref = undefined, client = undefined}}
end;
handle_info(Info, State) ->
?LOG(error, "Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #state{pool = Pool, id = Id}) ->
gproc_pool:disconnect_worker(Pool, {Pool, Id}),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
open(State = #state{host = Host, port = Port, gun_opts = GunOpts}) ->
case gun:open(Host, Port, GunOpts) of
{ok, ConnPid} when is_pid(ConnPid) ->
MRef = monitor(process, ConnPid),
{ok, State#state{mref = MRef, client = ConnPid}};
{error, Reason} ->
{error, Reason}
end.
gun_opts(Opts) ->
gun_opts(Opts, #{retry => 5,
retry_timeout => 1000,
connect_timeout => 5000,
protocols => [http],
http_opts => #{keepalive => infinity}}).
gun_opts([], Acc) ->
Acc;
gun_opts([{retry, Retry} | Opts], Acc) ->
gun_opts(Opts, Acc#{retry => Retry});
gun_opts([{retry_timeout, RetryTimeout} | Opts], Acc) ->
gun_opts(Opts, Acc#{retry_timeout => RetryTimeout});
gun_opts([{connect_timeout, ConnectTimeout} | Opts], Acc) ->
gun_opts(Opts, Acc#{connect_timeout => ConnectTimeout});
gun_opts([{transport, Transport} | Opts], Acc) ->
gun_opts(Opts, Acc#{transport => Transport});
gun_opts([{transport_opts, TransportOpts} | Opts], Acc) ->
gun_opts(Opts, Acc#{transport_opts => TransportOpts});
gun_opts([_ | Opts], Acc) ->
gun_opts(Opts, Acc).
call(ChannPid, Msg, Timeout) ->
gen_server:call(ChannPid, Msg, Timeout).
pick(Pool) ->
gproc_pool:pick_worker(Pool).
do_request(Client, get, {Path, Headers}) ->
gun:get(Client, Path, Headers);
do_request(Client, post, {Path, Headers, Body}) ->
gun:post(Client, Path, Headers, Body).
flush_stream(Client, StreamRef) ->
receive
{gun_response, Client, StreamRef, _, _, _} ->
flush_stream(Client, StreamRef);
{gun_data, Client, StreamRef, _, _} ->
flush_stream(Client, StreamRef);
{gun_error, Client, StreamRef, _} ->
flush_stream(Client, StreamRef)
after 0 ->
ok
end.

View File

@ -1,48 +0,0 @@
-module(emqx_http_client_sup).
-behaviour(supervisor).
-export([ start_link/2
, init/1
, stop_pool/1
]).
start_link(Pool, Opts) ->
supervisor:start_link(?MODULE, [Pool, Opts]).
init([Pool, Opts]) ->
PoolSize = pool_size(Opts),
ok = ensure_pool(Pool, random, [{size, PoolSize}]),
{ok, {{one_for_one, 10, 100}, [
begin
ensure_pool_worker(Pool, {Pool, I}, I),
#{id => {Pool, I},
start => {emqx_http_client, start_link, [Pool, I, Opts]},
restart => transient,
shutdown => 5000,
type => worker,
modules => [emqx_http_client]}
end || I <- lists:seq(1, PoolSize)]}}.
ensure_pool(Pool, Type, Opts) ->
try gproc_pool:new(Pool, Type, Opts)
catch
error:exists -> ok
end.
ensure_pool_worker(Pool, Name, Slot) ->
try gproc_pool:add_worker(Pool, Name, Slot)
catch
error:exists -> ok
end.
pool_size(Opts) ->
Schedulers = erlang:system_info(schedulers),
proplists:get_value(pool_size, Opts, Schedulers).
stop_pool(Name) ->
Workers = gproc_pool:defined_workers(Name),
_ = [gproc_pool:remove_worker(Name, WokerName) || {WokerName, _, _} <- Workers],
gproc_pool:delete(Name),
ok.

View File

@ -68,15 +68,15 @@ set_special_configs(emqx_auth_http, Schema, Inet) ->
AuthReq = #{method => post, AuthReq = #{method => post,
url => ServerAddr ++ "/mqtt/auth", url => ServerAddr ++ "/mqtt/auth",
content_type => <<"application/json">>, headers => [{"content-type", "application/json"}],
params => [{"clientid", "%c"}, {"username", "%u"}, {"password", "%P"}]}, params => [{"clientid", "%c"}, {"username", "%u"}, {"password", "%P"}]},
SuperReq = #{method => post, SuperReq = #{method => post,
url => ServerAddr ++ "/mqtt/superuser", url => ServerAddr ++ "/mqtt/superuser",
content_type => <<"application/json">>, headers => [{"content-type", "application/json"}],
params => [{"clientid", "%c"}, {"username", "%u"}]}, params => [{"clientid", "%c"}, {"username", "%u"}]},
AclReq = #{method => post, AclReq = #{method => post,
url => ServerAddr ++ "/mqtt/acl", url => ServerAddr ++ "/mqtt/acl",
content_type => <<"application/json">>, headers => [{"content-type", "application/json"}],
params => [{"access", "%A"}, {"username", "%u"}, {"clientid", "%c"}, {"ipaddr", "%a"}, {"topic", "%t"}, {"mountpoint", "%m"}]}, params => [{"access", "%A"}, {"username", "%u"}, {"clientid", "%c"}, {"ipaddr", "%a"}, {"topic", "%t"}, {"mountpoint", "%m"}]},
Schema =:= https andalso set_https_client_opts(), Schema =:= https andalso set_https_client_opts(),
@ -87,9 +87,10 @@ set_special_configs(emqx_auth_http, Schema, Inet) ->
%% @private %% @private
set_https_client_opts() -> set_https_client_opts() ->
TransportOpts = emqx_ct_helpers:client_ssl_twoway(), SSLOpt = emqx_ct_helpers:client_ssl_twoway(),
{ok, PoolOpts} = application:get_env(emqx_auth_http, pool_opts), application:set_env(emqx_auth_http, cafile, proplists:get_value(cacertfile, SSLOpt, undefined)),
application:set_env(emqx_auth_http, pool_opts, [{transport_opts, TransportOpts}, {transport, ssl} | PoolOpts]). application:set_env(emqx_auth_http, certfile, proplists:get_value(certfile, SSLOpt, undefined)),
application:set_env(emqx_auth_http, keyfile, proplists:get_value(keyfile, SSLOpt, undefined)).
%% @private %% @private
http_server(http, inet) -> "http://127.0.0.1:8991"; http_server(http, inet) -> "http://127.0.0.1:8991";
@ -171,3 +172,10 @@ t_comment_config(_) ->
?assertEqual(AuthCount - 1, length(emqx_hooks:lookup('client.authenticate'))), ?assertEqual(AuthCount - 1, length(emqx_hooks:lookup('client.authenticate'))),
?assertEqual(AclCount - 1, length(emqx_hooks:lookup('client.check_acl'))). ?assertEqual(AclCount - 1, length(emqx_hooks:lookup('client.check_acl'))).
t_feedvar(_) ->
Params = [{"cookie", "%k"}],
User0 = ?USER(<<"client1">>, <<"testuser">>, mqtt, {127,0,0,1}, external),
?assertEqual([{"cookie", <<"null">>}], emqx_auth_http_cli:feedvar(Params, User0)),
User1 = User0#{ws_cookie => [{<<"k">>, <<"v">>}]},
?assertEqual([{"cookie", <<"{\"k\":\"v\"}">>}], emqx_auth_http_cli:feedvar(Params, User1)).