From fb763ecebda3c4644f06e3719c975c074d7b2553 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 18 Jan 2023 15:49:57 +0100 Subject: [PATCH 1/7] feat: support HAProxy protocol for dashboard API --- .../i18n/emqx_dashboard_i18n.conf | 10 +++ apps/emqx_dashboard/src/emqx_dashboard.erl | 19 +++-- .../src/emqx_dashboard_schema.erl | 8 ++ .../test/emqx_dashboard_api_test_helpers.erl | 7 +- .../test/emqx_dashboard_haproxy_SUITE.erl | 83 +++++++++++++++++++ rebar.config | 2 +- 6 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf index 872cfdf26..cc0d0244b 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf @@ -92,6 +92,16 @@ Note: `sample_interval` should be a divisor of 60.""" zh: "IPv6 only" } } + proxy_header { + desc { + en: "Enable support for HAProxy header. Be aware once enabled regular HTTP requests can't be handled anymore." + zh: "[FIXME]" + } + label: { + en: "Enable support for HAProxy header" + zh: "[FIXME]" + } + } desc_dashboard { desc { en: "Configuration for EMQX dashboard." diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 36c7660cc..2afc8b362 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -92,8 +92,8 @@ start_listeners(Listeners) -> }, Res = lists:foldl( - fun({Name, Protocol, Bind, RanchOptions}, Acc) -> - Minirest = BaseMinirest#{protocol => Protocol}, + fun({Name, Protocol, Bind, RanchOptions, ProtoOpts}, Acc) -> + Minirest = BaseMinirest#{protocol => Protocol, protocol_options => ProtoOpts}, case minirest:start(Name, RanchOptions, Minirest) of {ok, _} -> ?ULOG("Listener ~ts on ~ts started.~n", [ @@ -125,7 +125,7 @@ stop_listeners(Listeners) -> ?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port}) end end - || {Name, _, Port, _} <- listeners(Listeners) + || {Name, _, Port, _, _} <- listeners(Listeners) ], ok. @@ -164,7 +164,13 @@ listeners(Listeners) -> maps:get(enable, Conf) andalso begin {Conf1, Bind} = ip_port(Conf), - {true, {listener_name(Protocol), Protocol, Bind, ranch_opts(Conf1)}} + {true, { + listener_name(Protocol), + Protocol, + Bind, + ranch_opts(Conf1), + proto_opts(Conf1) + }} end end, maps:to_list(Listeners) @@ -197,7 +203,7 @@ ranch_opts(Options) -> SocketOpts = maps:fold( fun filter_false/3, [], - maps:without([enable, inet6, ipv6_v6only | Keys], Options) + maps:without([enable, inet6, ipv6_v6only, proxy_header | Keys], Options) ), InetOpts = case Options of @@ -210,6 +216,9 @@ ranch_opts(Options) -> end, RanchOpts#{socket_opts => InetOpts ++ SocketOpts}. +proto_opts(Options) -> + maps:with([proxy_header], Options). + filter_false(_K, false, S) -> S; filter_false(K, V, S) -> [{K, V} | S]. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 6742032d5..a7a8c7828 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -160,6 +160,14 @@ common_listener_fields() -> default => false, desc => ?DESC(ipv6_v6only) } + )}, + {"proxy_header", + ?HOCON( + boolean(), + #{ + desc => ?DESC(proxy_header), + default => false + } )} ]. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl index 87a3654ac..8df130897 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl @@ -19,6 +19,7 @@ -export([ set_default_config/0, set_default_config/1, + set_default_config/2, request/2, request/3, request/4, @@ -36,6 +37,9 @@ set_default_config() -> set_default_config(<<"admin">>). set_default_config(DefaultUsername) -> + set_default_config(DefaultUsername, false). + +set_default_config(DefaultUsername, HAProxyEnabled) -> Config = #{ listeners => #{ http => #{ @@ -46,7 +50,8 @@ set_default_config(DefaultUsername) -> max_connections => 512, num_acceptors => 4, send_timeout => 5000, - backlog => 512 + backlog => 512, + proxy_header => HAProxyEnabled } }, default_username => DefaultUsername, diff --git a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl new file mode 100644 index 000000000..2fceeee6e --- /dev/null +++ b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl @@ -0,0 +1,83 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2023 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_dashboard_haproxy_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-import( + emqx_common_test_http, + [ + request_api/3 + ] +). + +-include_lib("eunit/include/eunit.hrl"). +-include("emqx_dashboard.hrl"). + +-define(HOST, "http://127.0.0.1:18083"). + +-define(BASE_PATH, "/"). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +end_suite() -> + end_suite([]). + +end_suite(Apps) -> + application:unload(emqx_management), + mnesia:clear_table(?ADMIN), + emqx_common_test_helpers:stop_apps(Apps ++ [emqx_dashboard]). + +init_per_suite(Config) -> + emqx_common_test_helpers:start_apps( + [emqx_management, emqx_dashboard], + fun set_special_configs/1 + ), + Config. + +end_per_suite(_Config) -> + emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_management]), + mria:stop(). + +set_special_configs(emqx_dashboard) -> + emqx_dashboard_api_test_helpers:set_default_config(<<"admin">>, true), + ok; +set_special_configs(_) -> + ok. + +disabled_t_status(_) -> + %% no easy way since httpc doesn't support emulating the haproxy protocol + {ok, 200, _Res} = http_get(["status"]), + ok. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ +http_get(Parts) -> + request_api(get, api_path(Parts), auth_header_()). + +auth_header_() -> + auth_header_(<<"admin">>, <<"public">>). + +auth_header_(Username, Password) -> + {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {"Authorization", "Bearer " ++ binary_to_list(Token)}. + +api_path(Parts) -> + ?HOST ++ filename:join([?BASE_PATH | Parts]). diff --git a/rebar.config b/rebar.config index bfdc3e4ee..a20bc7060 100644 --- a/rebar.config +++ b/rebar.config @@ -58,7 +58,7 @@ , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.7"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}} , {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.7"}}} - , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.7"}}} + , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.8"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.2"}}} , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.5"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} From 1c279546761630a5bdec23a31f36cafdf0a5ac77 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 18 Jan 2023 15:54:53 +0100 Subject: [PATCH 2/7] chore: add changelog --- changes/v5.0.15/feat-9802.en.md | 1 + changes/v5.0.15/feat-9802.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/feat-9802.en.md create mode 100644 changes/v5.0.15/feat-9802.zh.md diff --git a/changes/v5.0.15/feat-9802.en.md b/changes/v5.0.15/feat-9802.en.md new file mode 100644 index 000000000..ac314879a --- /dev/null +++ b/changes/v5.0.15/feat-9802.en.md @@ -0,0 +1 @@ +Support HAProxy protocol for dashboard API. diff --git a/changes/v5.0.15/feat-9802.zh.md b/changes/v5.0.15/feat-9802.zh.md new file mode 100644 index 000000000..97a926b1a --- /dev/null +++ b/changes/v5.0.15/feat-9802.zh.md @@ -0,0 +1 @@ +[FIXME] Support HAProxy protocol for dashboard API. From 62b01b35013e9ce3a53883cc0e4422659b4fb0a3 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 18 Jan 2023 16:31:14 +0100 Subject: [PATCH 3/7] fix: version mismatch between erlang and elixir --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index e124116c8..9fcc7b076 100644 --- a/mix.exs +++ b/mix.exs @@ -56,7 +56,7 @@ defmodule EMQXUmbrella.MixProject do {:ekka, github: "emqx/ekka", tag: "0.13.7", override: true}, {:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true}, {:grpc, github: "emqx/grpc-erl", tag: "0.6.7", override: true}, - {:minirest, github: "emqx/minirest", tag: "1.3.7", override: true}, + {:minirest, github: "emqx/minirest", tag: "1.3.8", override: true}, {:ecpool, github: "emqx/ecpool", tag: "0.5.2", override: true}, {:replayq, github: "emqx/replayq", tag: "0.3.5", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, From 6bde1173afecd84ef5dc644cc1bfec45d9800e69 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Thu, 19 Jan 2023 13:26:36 +0100 Subject: [PATCH 4/7] chore: add translations --- apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf | 6 +++--- changes/v5.0.15/feat-9802.zh.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf index cc0d0244b..8efacbdfb 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf @@ -94,12 +94,12 @@ Note: `sample_interval` should be a divisor of 60.""" } proxy_header { desc { - en: "Enable support for HAProxy header. Be aware once enabled regular HTTP requests can't be handled anymore." - zh: "[FIXME]" + en: "Enable support for `HAProxy` header. Be aware once enabled regular HTTP requests can't be handled anymore." + zh: "开启对 `HAProxy` 的支持,注意:一旦开启了这个功能,就无法再处理普通的 HTTP 请求了。" } label: { en: "Enable support for HAProxy header" - zh: "[FIXME]" + zh: "开启对 `HAProxy` 的支持" } } desc_dashboard { diff --git a/changes/v5.0.15/feat-9802.zh.md b/changes/v5.0.15/feat-9802.zh.md index 97a926b1a..9afcd8f17 100644 --- a/changes/v5.0.15/feat-9802.zh.md +++ b/changes/v5.0.15/feat-9802.zh.md @@ -1 +1 @@ -[FIXME] Support HAProxy protocol for dashboard API. +现在 dashboard 增加了对 `HAProxy` 协议的支持。 From 0b1483040a12056d86cf5b633ccdb7eec1ecafc6 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 30 Jan 2023 18:07:06 +0100 Subject: [PATCH 5/7] test: add actual test for haproxy --- .../test/emqx_dashboard_haproxy_SUITE.erl | 97 +++++++++++-------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl index 2fceeee6e..a05de339b 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl @@ -19,31 +19,12 @@ -compile(nowarn_export_all). -compile(export_all). --import( - emqx_common_test_http, - [ - request_api/3 - ] -). - -include_lib("eunit/include/eunit.hrl"). -include("emqx_dashboard.hrl"). --define(HOST, "http://127.0.0.1:18083"). - --define(BASE_PATH, "/"). - all() -> emqx_common_test_helpers:all(?MODULE). -end_suite() -> - end_suite([]). - -end_suite(Apps) -> - application:unload(emqx_management), - mnesia:clear_table(?ADMIN), - emqx_common_test_helpers:stop_apps(Apps ++ [emqx_dashboard]). - init_per_suite(Config) -> emqx_common_test_helpers:start_apps( [emqx_management, emqx_dashboard], @@ -51,33 +32,69 @@ init_per_suite(Config) -> ), Config. -end_per_suite(_Config) -> - emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_management]), - mria:stop(). - set_special_configs(emqx_dashboard) -> emqx_dashboard_api_test_helpers:set_default_config(<<"admin">>, true), ok; set_special_configs(_) -> ok. -disabled_t_status(_) -> - %% no easy way since httpc doesn't support emulating the haproxy protocol - {ok, 200, _Res} = http_get(["status"]), +end_per_suite(Config) -> + application:unload(emqx_management), + mnesia:clear_table(?ADMIN), + emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_management]), + mria:stop(), + Config. + +t_status(_Config) -> + ProxyInfo = #{ + version => 1, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 444, + dest_address => {192, 168, 0, 1}, + dest_port => 443 + }, + {ok, Socket} = gen_tcp:connect( + "localhost", + 18083, + [binary, {active, false}, {packet, raw}] + ), + ok = gen_tcp:send(Socket, ranch_proxy_header:header(ProxyInfo)), + {ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>), + ok = gen_tcp:send( + Socket, + "GET /status HTTP/1.1\r\n" + "Host: localhost\r\n" + "Authorization: Bearer " ++ binary_to_list(Token) ++ + "\r\n" + "\r\n" + ), + {_, 200, _, Rest0} = cow_http:parse_status_line(raw_recv_head(Socket)), + {Headers, Body0} = cow_http:parse_headers(Rest0), + {_, LenBin} = lists:keyfind(<<"content-length">>, 1, Headers), + Len = binary_to_integer(LenBin), + Body = + if + byte_size(Body0) =:= Len -> + Body0; + true -> + {ok, Body1} = gen_tcp:recv(Socket, Len - byte_size(Body0), 5000), + <> + end, + ?assertMatch({match, _}, re:run(Body, "Node .+ is started\nemqx is running")), ok. -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ -http_get(Parts) -> - request_api(get, api_path(Parts), auth_header_()). +raw_recv_head(Socket) -> + {ok, Data} = gen_tcp:recv(Socket, 0, 10000), + raw_recv_head(Socket, Data). -auth_header_() -> - auth_header_(<<"admin">>, <<"public">>). - -auth_header_(Username, Password) -> - {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), - {"Authorization", "Bearer " ++ binary_to_list(Token)}. - -api_path(Parts) -> - ?HOST ++ filename:join([?BASE_PATH | Parts]). +raw_recv_head(Socket, Buffer) -> + case binary:match(Buffer, <<"\r\n\r\n">>) of + nomatch -> + {ok, Data} = gen_tcp:recv(Socket, 0, 10000), + raw_recv_head(Socket, <>); + {_, _} -> + Buffer + end. From 3ce6cbcd6a7c280552e659e1a2d78f1c1cca924e Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 30 Jan 2023 18:09:10 +0100 Subject: [PATCH 6/7] chore: bump vsn --- apps/emqx_dashboard/src/emqx_dashboard.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard.app.src b/apps/emqx_dashboard/src/emqx_dashboard.app.src index 57d63247b..b6c95ca97 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.app.src +++ b/apps/emqx_dashboard/src/emqx_dashboard.app.src @@ -2,7 +2,7 @@ {application, emqx_dashboard, [ {description, "EMQX Web Dashboard"}, % strict semver, bump manually! - {vsn, "5.0.12"}, + {vsn, "5.0.13"}, {modules, []}, {registered, [emqx_dashboard_sup]}, {applications, [kernel, stdlib, mnesia, minirest, emqx]}, From 1bbae31268569e14c765594212455c070adbe4d4 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 31 Jan 2023 08:59:46 +0100 Subject: [PATCH 7/7] chore: mv for v5.0.16 release --- changes/{v5.0.15 => v5.0.16}/feat-9802.en.md | 0 changes/{v5.0.15 => v5.0.16}/feat-9802.zh.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename changes/{v5.0.15 => v5.0.16}/feat-9802.en.md (100%) rename changes/{v5.0.15 => v5.0.16}/feat-9802.zh.md (100%) diff --git a/changes/v5.0.15/feat-9802.en.md b/changes/v5.0.16/feat-9802.en.md similarity index 100% rename from changes/v5.0.15/feat-9802.en.md rename to changes/v5.0.16/feat-9802.en.md diff --git a/changes/v5.0.15/feat-9802.zh.md b/changes/v5.0.16/feat-9802.zh.md similarity index 100% rename from changes/v5.0.15/feat-9802.zh.md rename to changes/v5.0.16/feat-9802.zh.md