Merge remote-tracking branch 'origin/master' into 0324-merge-release-50-back-to-master

This commit is contained in:
Zaiming (Stone) Shi 2023-03-24 21:42:35 +01:00
commit b37f186142
249 changed files with 5163 additions and 814 deletions

View File

@ -18,7 +18,7 @@ services:
- /tmp/emqx-ci/emqx-shared-secret:/var/lib/secret - /tmp/emqx-ci/emqx-shared-secret:/var/lib/secret
kdc: kdc:
hostname: kdc.emqx.net hostname: kdc.emqx.net
image: ghcr.io/emqx/emqx-builder/5.0-28:1.13.4-24.3.4.2-2-ubuntu20.04 image: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu20.04
container_name: kdc.emqx.net container_name: kdc.emqx.net
expose: expose:
- 88 # kdc - 88 # kdc

View File

@ -0,0 +1,34 @@
version: '3.9'
services:
mqnamesrv:
image: apache/rocketmq:4.9.4
container_name: rocketmq_namesrv
# ports:
# - 9876:9876
volumes:
- ./rocketmq/logs:/opt/logs
- ./rocketmq/store:/opt/store
command: ./mqnamesrv
networks:
- emqx_bridge
mqbroker:
image: apache/rocketmq:4.9.4
container_name: rocketmq_broker
# ports:
# - 10909:10909
# - 10911:10911
volumes:
- ./rocketmq/logs:/opt/logs
- ./rocketmq/store:/opt/store
- ./rocketmq/conf/broker.conf:/etc/rocketmq/broker.conf
environment:
NAMESRV_ADDR: "rocketmq_namesrv:9876"
JAVA_OPTS: " -Duser.home=/opt"
JAVA_OPT_EXT: "-server -Xms1024m -Xmx1024m -Xmn1024m"
command: ./mqbroker -c /etc/rocketmq/broker.conf
depends_on:
- mqnamesrv
networks:
- emqx_bridge

View File

@ -22,6 +22,7 @@ services:
- 15433:5433 - 15433:5433
- 16041:6041 - 16041:6041
- 18000:8000 - 18000:8000
- 19876:9876
command: command:
- "-host=0.0.0.0" - "-host=0.0.0.0"
- "-config=/config/toxiproxy.json" - "-config=/config/toxiproxy.json"

View File

@ -3,7 +3,7 @@ version: '3.9'
services: services:
erlang: erlang:
container_name: erlang container_name: erlang
image: ${DOCKER_CT_RUNNER_IMAGE:-ghcr.io/emqx/emqx-builder/5.0-28:1.13.4-24.3.4.2-2-ubuntu20.04} image: ${DOCKER_CT_RUNNER_IMAGE:-ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu20.04}
env_file: env_file:
- conf.env - conf.env
environment: environment:

View File

@ -0,0 +1,22 @@
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=0
brokerIP1=rocketmq_broker
defaultTopicQueueNums=4
autoCreateTopicEnable=true
autoCreateSubscriptionGroup=true
listenPort=10911
deleteWhen=04
fileReservedTime=120
mapedFileSizeCommitLog=1073741824
mapedFileSizeConsumeQueue=300000
diskMaxUsedSpaceRatio=100
maxMessageSize=65536
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH

View File

@ -77,5 +77,11 @@
"listen": "0.0.0.0:9295", "listen": "0.0.0.0:9295",
"upstream": "kafka-1.emqx.net:9295", "upstream": "kafka-1.emqx.net:9295",
"enabled": true "enabled": true
},
{
"name": "rocketmq",
"listen": "0.0.0.0:9876",
"upstream": "rocketmq_namesrv:9876",
"enabled": true
} }
] ]

View File

@ -25,7 +25,7 @@ jobs:
prepare: prepare:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
# prepare source with any OTP version, no need for a matrix # prepare source with any OTP version, no need for a matrix
container: "ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-24.3.4.2-2-ubuntu22.04" container: "ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu22.04"
outputs: outputs:
PROFILE: ${{ steps.get_profile.outputs.PROFILE }} PROFILE: ${{ steps.get_profile.outputs.PROFILE }}
@ -121,9 +121,9 @@ jobs:
# NOTE: 'otp' and 'elixir' are to configure emqx-builder image # NOTE: 'otp' and 'elixir' are to configure emqx-builder image
# only support latest otp and elixir, not a matrix # only support latest otp and elixir, not a matrix
builder: builder:
- 5.0-32 # update to latest - 5.0-33 # update to latest
otp: otp:
- 24.3.4.2-2 # switch to 25 once ready to release 5.1 - 24.3.4.2-3 # switch to 25 once ready to release 5.1
elixir: elixir:
- 'no_elixir' - 'no_elixir'
- '1.13.4' # update to latest - '1.13.4' # update to latest

View File

@ -24,7 +24,7 @@ jobs:
prepare: prepare:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
if: (github.repository_owner == 'emqx' && github.event_name == 'schedule') || github.event_name != 'schedule' if: (github.repository_owner == 'emqx' && github.event_name == 'schedule') || github.event_name != 'schedule'
container: ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-24.3.4.2-2-ubuntu22.04 container: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu22.04
outputs: outputs:
BUILD_PROFILE: ${{ steps.get_profile.outputs.BUILD_PROFILE }} BUILD_PROFILE: ${{ steps.get_profile.outputs.BUILD_PROFILE }}
IS_EXACT_TAG: ${{ steps.get_profile.outputs.IS_EXACT_TAG }} IS_EXACT_TAG: ${{ steps.get_profile.outputs.IS_EXACT_TAG }}
@ -151,7 +151,7 @@ jobs:
profile: profile:
- ${{ needs.prepare.outputs.BUILD_PROFILE }} - ${{ needs.prepare.outputs.BUILD_PROFILE }}
otp: otp:
- 24.3.4.2-2 - 24.3.4.2-3
os: os:
- macos-11 - macos-11
- macos-12 - macos-12
@ -203,7 +203,7 @@ jobs:
profile: profile:
- ${{ needs.prepare.outputs.BUILD_PROFILE }} - ${{ needs.prepare.outputs.BUILD_PROFILE }}
otp: otp:
- 24.3.4.2-2 - 24.3.4.2-3
arch: arch:
- amd64 - amd64
- arm64 - arm64
@ -221,7 +221,7 @@ jobs:
- aws-arm64 - aws-arm64
- ubuntu-22.04 - ubuntu-22.04
builder: builder:
- 5.0-32 - 5.0-33
elixir: elixir:
- 1.13.4 - 1.13.4
exclude: exclude:
@ -231,19 +231,19 @@ jobs:
build_machine: aws-arm64 build_machine: aws-arm64
include: include:
- profile: emqx - profile: emqx
otp: 25.1.2-2 otp: 25.1.2-3
arch: amd64 arch: amd64
os: ubuntu22.04 os: ubuntu22.04
build_machine: ubuntu-22.04 build_machine: ubuntu-22.04
builder: 5.0-32 builder: 5.0-33
elixir: 1.13.4 elixir: 1.13.4
release_with: elixir release_with: elixir
- profile: emqx - profile: emqx
otp: 25.1.2-2 otp: 25.1.2-3
arch: amd64 arch: amd64
os: amzn2 os: amzn2
build_machine: ubuntu-22.04 build_machine: ubuntu-22.04
builder: 5.0-32 builder: 5.0-33
elixir: 1.13.4 elixir: 1.13.4
release_with: elixir release_with: elixir

View File

@ -30,12 +30,12 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
profile: profile:
- ["emqx", "24.3.4.2-2", "el7", "erlang"] - ["emqx", "24.3.4.2-3", "el7", "erlang"]
- ["emqx", "25.1.2-2", "ubuntu22.04", "elixir"] - ["emqx", "25.1.2-3", "ubuntu22.04", "elixir"]
- ["emqx-enterprise", "24.3.4.2-2", "amzn2", "erlang"] - ["emqx-enterprise", "24.3.4.2-3", "amzn2", "erlang"]
- ["emqx-enterprise", "25.1.2-2", "ubuntu20.04", "erlang"] - ["emqx-enterprise", "25.1.2-3", "ubuntu20.04", "erlang"]
builder: builder:
- 5.0-32 - 5.0-33
elixir: elixir:
- '1.13.4' - '1.13.4'
@ -132,7 +132,7 @@ jobs:
- emqx - emqx
- emqx-enterprise - emqx-enterprise
otp: otp:
- 24.3.4.2-2 - 24.3.4.2-3
os: os:
- macos-11 - macos-11
- macos-12-arm64 - macos-12-arm64

View File

@ -6,7 +6,7 @@ on:
jobs: jobs:
check_deps_integrity: check_deps_integrity:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
container: ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-25.1.2-2-ubuntu22.04 container: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-25.1.2-3-ubuntu22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@ -5,7 +5,7 @@ on: [pull_request]
jobs: jobs:
code_style_check: code_style_check:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
container: "ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-25.1.2-2-ubuntu22.04" container: "ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-25.1.2-3-ubuntu22.04"
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:

View File

@ -9,7 +9,7 @@ jobs:
elixir_apps_check: elixir_apps_check:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
# just use the latest builder # just use the latest builder
container: "ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-25.1.2-2-ubuntu22.04" container: "ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-25.1.2-3-ubuntu22.04"
strategy: strategy:
fail-fast: false fail-fast: false

View File

@ -8,7 +8,7 @@ on:
jobs: jobs:
elixir_deps_check: elixir_deps_check:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
container: ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-25.1.2-2-ubuntu22.04 container: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-25.1.2-3-ubuntu22.04
steps: steps:
- name: Checkout - name: Checkout

View File

@ -17,7 +17,7 @@ jobs:
profile: profile:
- emqx - emqx
- emqx-enterprise - emqx-enterprise
container: ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-25.1.2-2-ubuntu22.04 container: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-25.1.2-3-ubuntu22.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3

26
.github/workflows/geen_master.yaml vendored Normal file
View File

@ -0,0 +1,26 @@
---
name: Keep master green
on:
schedule:
# run hourly
- cron: "0 * * * *"
workflow_dispatch:
jobs:
rerun-failed-jobs:
runs-on: ubuntu-22.04
if: github.repository_owner == 'emqx'
permissions:
checks: read
actions: write
steps:
- uses: actions/checkout@v3
- name: run script
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python3 scripts/rerun-failed-checks.py

View File

@ -54,7 +54,7 @@ jobs:
OUTPUT_DIR=${{ steps.profile.outputs.s3dir }} OUTPUT_DIR=${{ steps.profile.outputs.s3dir }}
aws s3 cp --recursive s3://$BUCKET/$OUTPUT_DIR/${{ github.ref_name }} packages aws s3 cp --recursive s3://$BUCKET/$OUTPUT_DIR/${{ github.ref_name }} packages
cd packages cd packages
DEFAULT_BEAM_PLATFORM='otp24.3.4.2-2' DEFAULT_BEAM_PLATFORM='otp24.3.4.2-3'
# all packages including full-name and default-name are uploaded to s3 # all packages including full-name and default-name are uploaded to s3
# but we only upload default-name packages (and elixir) as github artifacts # but we only upload default-name packages (and elixir) as github artifacts
# so we rename (overwrite) non-default packages before uploading # so we rename (overwrite) non-default packages before uploading

View File

@ -12,10 +12,10 @@ jobs:
strategy: strategy:
matrix: matrix:
builder: builder:
- 5.0-32 - 5.0-33
otp: otp:
- 24.3.4.2-2 - 24.3.4.2-3
- 25.1.2-2 - 25.1.2-3
# no need to use more than 1 version of Elixir, since tests # no need to use more than 1 version of Elixir, since tests
# run using only Erlang code. This is needed just to specify # run using only Erlang code. This is needed just to specify
# the base image. # the base image.

View File

@ -17,7 +17,7 @@ jobs:
prepare: prepare:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
# prepare source with any OTP version, no need for a matrix # prepare source with any OTP version, no need for a matrix
container: ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-24.3.4.2-2-debian11 container: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-debian11
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -50,9 +50,9 @@ jobs:
os: os:
- ["debian11", "debian:11-slim"] - ["debian11", "debian:11-slim"]
builder: builder:
- 5.0-32 - 5.0-33
otp: otp:
- 24.3.4.2-2 - 24.3.4.2-3
elixir: elixir:
- 1.13.4 - 1.13.4
arch: arch:
@ -123,9 +123,9 @@ jobs:
os: os:
- ["debian11", "debian:11-slim"] - ["debian11", "debian:11-slim"]
builder: builder:
- 5.0-32 - 5.0-33
otp: otp:
- 24.3.4.2-2 - 24.3.4.2-3
elixir: elixir:
- 1.13.4 - 1.13.4
arch: arch:

View File

@ -15,7 +15,7 @@ concurrency:
jobs: jobs:
relup_test_plan: relup_test_plan:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
container: "ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-24.3.4.2-2-ubuntu22.04" container: "ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu22.04"
outputs: outputs:
CUR_EE_VSN: ${{ steps.find-versions.outputs.CUR_EE_VSN }} CUR_EE_VSN: ${{ steps.find-versions.outputs.CUR_EE_VSN }}
OLD_VERSIONS: ${{ steps.find-versions.outputs.OLD_VERSIONS }} OLD_VERSIONS: ${{ steps.find-versions.outputs.OLD_VERSIONS }}

View File

@ -31,13 +31,13 @@ jobs:
MATRIX="$(echo "${APPS}" | jq -c ' MATRIX="$(echo "${APPS}" | jq -c '
[ [
(.[] | select(.profile == "emqx") | . + { (.[] | select(.profile == "emqx") | . + {
builder: "5.0-32", builder: "5.0-33",
otp: "25.1.2-2", otp: "25.1.2-3",
elixir: "1.13.4" elixir: "1.13.4"
}), }),
(.[] | select(.profile == "emqx-enterprise") | . + { (.[] | select(.profile == "emqx-enterprise") | . + {
builder: "5.0-32", builder: "5.0-33",
otp: ["24.3.4.2-2", "25.1.2-2"][], otp: ["24.3.4.2-3", "25.1.2-3"][],
elixir: "1.13.4" elixir: "1.13.4"
}) })
] ]
@ -230,12 +230,12 @@ jobs:
- ct - ct
- ct_docker - ct_docker
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
container: "ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-24.3.4.2-2-ubuntu22.04" container: "ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu22.04"
steps: steps:
- uses: AutoModality/action-clean@v1 - uses: AutoModality/action-clean@v1
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: source-emqx-enterprise-24.3.4.2-2 name: source-emqx-enterprise-24.3.4.2-3
path: . path: .
- name: unzip source code - name: unzip source code
run: unzip -q source.zip run: unzip -q source.zip

View File

@ -1,2 +1,2 @@
erlang 24.3.4.2-2 erlang 24.3.4.2-3
elixir 1.13.4-otp-24 elixir 1.13.4-otp-24

View File

@ -152,6 +152,7 @@ $(PROFILES:%=clean-%):
.PHONY: clean-all .PHONY: clean-all
clean-all: clean-all:
@rm -f rebar.lock @rm -f rebar.lock
@rm -rf deps
@rm -rf _build @rm -rf _build
.PHONY: deps-all .PHONY: deps-all

View File

@ -1810,6 +1810,56 @@ server_ssl_opts_schema_ocsp_refresh_http_timeout {
} }
} }
server_ssl_opts_schema_enable_crl_check {
desc {
en: "Whether to enable CRL verification for this listener."
zh: "是否为该监听器启用 CRL 检查。"
}
label: {
en: "Enable CRL Check"
zh: "启用 CRL 检查"
}
}
crl_cache_refresh_http_timeout {
desc {
en: "The timeout for the HTTP request when fetching CRLs. This is"
" a global setting for all listeners."
zh: "获取 CRLs 时 HTTP 请求的超时。 该配置对所有启用 CRL 检查的监听器监听器有效。"
}
label: {
en: "CRL Cache Refresh HTTP Timeout"
zh: "CRL 缓存刷新 HTTP 超时"
}
}
crl_cache_refresh_interval {
desc {
en: "The period to refresh the CRLs from the servers. This is a global setting"
" for all URLs and listeners."
zh: "从服务器刷新CRL的周期。 该配置对所有 URL 和监听器有效。"
}
label: {
en: "CRL Cache Refresh Interval"
zh: "CRL 缓存刷新间隔"
}
}
crl_cache_capacity {
desc {
en: "The maximum number of CRL URLs that can be held in cache. If the cache is at"
" full capacity and a new URL must be fetched, then it'll evict the oldest"
" inserted URL in the cache."
zh: "缓存中可容纳的 CRL URL 的最大数量。"
" 如果缓存的容量已满,并且必须获取一个新的 URL"
"那么它将驱逐缓存中插入的最老的 URL。"
}
label: {
en: "CRL Cache Capacity"
zh: "CRL 缓存容量"
}
}
fields_listeners_tcp { fields_listeners_tcp {
desc { desc {
en: """TCP listeners.""" en: """TCP listeners."""

View File

@ -26,8 +26,8 @@
{gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}, {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}},
{jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}, {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}},
{cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}}, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}},
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}}, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.6"}}},
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.5"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.6"}}},
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}},
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.37.2"}}}, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.37.2"}}},
{emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}}, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}},
@ -59,4 +59,12 @@
{statistics, true} {statistics, true}
]}. ]}.
{project_plugins, [erlfmt]}. {project_plugins, [
{erlfmt, [
{files, [
"{src,include,test}/*.{hrl,erl,app.src}",
"rebar.config",
"rebar.config.script"
]}
]}
]}.

View File

@ -24,20 +24,20 @@ IsQuicSupp = fun() ->
end, end,
Bcrypt = {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {tag, "0.6.0"}}}, Bcrypt = {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {tag, "0.6.0"}}},
Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.113"}}}. Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.114"}}}.
Dialyzer = fun(Config) -> Dialyzer = fun(Config) ->
{dialyzer, OldDialyzerConfig} = lists:keyfind(dialyzer, 1, Config), {dialyzer, OldDialyzerConfig} = lists:keyfind(dialyzer, 1, Config),
{plt_extra_apps, OldExtra} = lists:keyfind(plt_extra_apps, 1, OldDialyzerConfig), {plt_extra_apps, OldExtra} = lists:keyfind(plt_extra_apps, 1, OldDialyzerConfig),
Extra = OldExtra ++ [quicer || IsQuicSupp()], Extra = OldExtra ++ [quicer || IsQuicSupp()],
NewDialyzerConfig = [{plt_extra_apps, Extra} | OldDialyzerConfig], NewDialyzerConfig = [{plt_extra_apps, Extra} | OldDialyzerConfig],
lists:keystore( lists:keystore(
dialyzer, dialyzer,
1, 1,
Config, Config,
{dialyzer, NewDialyzerConfig} {dialyzer, NewDialyzerConfig}
) )
end. end.
ExtraDeps = fun(C) -> ExtraDeps = fun(C) ->
{deps, Deps0} = lists:keyfind(deps, 1, C), {deps, Deps0} = lists:keyfind(deps, 1, C),

View File

@ -2128,17 +2128,23 @@ publish_will_msg(
ClientInfo = #{mountpoint := MountPoint}, ClientInfo = #{mountpoint := MountPoint},
Msg = #message{topic = Topic} Msg = #message{topic = Topic}
) -> ) ->
case emqx_access_control:authorize(ClientInfo, publish, Topic) of PublishingDisallowed = emqx_access_control:authorize(ClientInfo, publish, Topic) =/= allow,
allow -> ClientBanned = emqx_banned:check(ClientInfo),
NMsg = emqx_mountpoint:mount(MountPoint, Msg), case PublishingDisallowed orelse ClientBanned of
_ = emqx_broker:publish(NMsg), true ->
ok;
deny ->
?tp( ?tp(
warning, warning,
last_will_testament_publish_denied, last_will_testament_publish_denied,
#{topic => Topic} #{
topic => Topic,
client_banned => ClientBanned,
publishing_disallowed => PublishingDisallowed
}
), ),
ok;
false ->
NMsg = emqx_mountpoint:mount(MountPoint, Msg),
_ = emqx_broker:publish(NMsg),
ok ok
end. end.

View File

@ -0,0 +1,314 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-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.
%%
%% @doc EMQX CRL cache.
%%--------------------------------------------------------------------
-module(emqx_crl_cache).
%% API
-export([
start_link/0,
start_link/1,
register_der_crls/2,
refresh/1,
evict/1
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2
]).
%% internal exports
-export([http_get/2]).
-behaviour(gen_server).
-include("logger.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-define(HTTP_TIMEOUT, timer:seconds(15)).
-define(RETRY_TIMEOUT, 5_000).
-ifdef(TEST).
-define(MIN_REFRESH_PERIOD, timer:seconds(5)).
-else.
-define(MIN_REFRESH_PERIOD, timer:minutes(1)).
-endif.
-define(DEFAULT_REFRESH_INTERVAL, timer:minutes(15)).
-define(DEFAULT_CACHE_CAPACITY, 100).
-record(state, {
refresh_timers = #{} :: #{binary() => timer:tref()},
refresh_interval = timer:minutes(15) :: timer:time(),
http_timeout = ?HTTP_TIMEOUT :: timer:time(),
%% keeps track of URLs by insertion time
insertion_times = gb_trees:empty() :: gb_trees:tree(timer:time(), url()),
%% the set of cached URLs, for testing if an URL is already
%% registered.
cached_urls = sets:new([{version, 2}]) :: sets:set(url()),
cache_capacity = 100 :: pos_integer(),
%% for future use
extra = #{} :: map()
}).
-type url() :: uri_string:uri_string().
-type state() :: #state{}.
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
start_link() ->
Config = gather_config(),
start_link(Config).
start_link(Config = #{cache_capacity := _, refresh_interval := _, http_timeout := _}) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Config, []).
-spec refresh(url()) -> ok.
refresh(URL) ->
gen_server:cast(?MODULE, {refresh, URL}).
-spec evict(url()) -> ok.
evict(URL) ->
gen_server:cast(?MODULE, {evict, URL}).
%% Adds CRLs in DER format to the cache and register them for periodic
%% refresh.
-spec register_der_crls(url(), [public_key:der_encoded()]) -> ok.
register_der_crls(URL, CRLs) when is_list(CRLs) ->
gen_server:cast(?MODULE, {register_der_crls, URL, CRLs}).
%%--------------------------------------------------------------------
%% gen_server behaviour
%%--------------------------------------------------------------------
init(Config) ->
#{
cache_capacity := CacheCapacity,
refresh_interval := RefreshIntervalMS,
http_timeout := HTTPTimeoutMS
} = Config,
State = #state{
cache_capacity = CacheCapacity,
refresh_interval = RefreshIntervalMS,
http_timeout = HTTPTimeoutMS
},
{ok, State}.
handle_call(Call, _From, State) ->
{reply, {error, {bad_call, Call}}, State}.
handle_cast({evict, URL}, State0 = #state{refresh_timers = RefreshTimers0}) ->
emqx_ssl_crl_cache:delete(URL),
MTimer = maps:get(URL, RefreshTimers0, undefined),
emqx_misc:cancel_timer(MTimer),
RefreshTimers = maps:without([URL], RefreshTimers0),
State = State0#state{refresh_timers = RefreshTimers},
?tp(
crl_cache_evict,
#{url => URL}
),
{noreply, State};
handle_cast({register_der_crls, URL, CRLs}, State0) ->
handle_register_der_crls(State0, URL, CRLs);
handle_cast({refresh, URL}, State0) ->
case do_http_fetch_and_cache(URL, State0#state.http_timeout) of
{error, Error} ->
?tp(crl_refresh_failure, #{error => Error, url => URL}),
?SLOG(error, #{
msg => "failed_to_fetch_crl_response",
url => URL,
error => Error
}),
{noreply, ensure_timer(URL, State0, ?RETRY_TIMEOUT)};
{ok, _CRLs} ->
?SLOG(debug, #{
msg => "fetched_crl_response",
url => URL
}),
{noreply, ensure_timer(URL, State0)}
end;
handle_cast(_Cast, State) ->
{noreply, State}.
handle_info(
{timeout, TRef, {refresh, URL}},
State = #state{
refresh_timers = RefreshTimers,
http_timeout = HTTPTimeoutMS
}
) ->
case maps:get(URL, RefreshTimers, undefined) of
TRef ->
?tp(debug, crl_refresh_timer, #{url => URL}),
case do_http_fetch_and_cache(URL, HTTPTimeoutMS) of
{error, Error} ->
?SLOG(error, #{
msg => "failed_to_fetch_crl_response",
url => URL,
error => Error
}),
{noreply, ensure_timer(URL, State, ?RETRY_TIMEOUT)};
{ok, _CRLs} ->
?tp(debug, crl_refresh_timer_done, #{url => URL}),
{noreply, ensure_timer(URL, State)}
end;
_ ->
{noreply, State}
end;
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% internal functions
%%--------------------------------------------------------------------
http_get(URL, HTTPTimeout) ->
httpc:request(
get,
{URL, [{"connection", "close"}]},
[{timeout, HTTPTimeout}],
[{body_format, binary}]
).
do_http_fetch_and_cache(URL, HTTPTimeoutMS) ->
?tp(crl_http_fetch, #{crl_url => URL}),
Resp = ?MODULE:http_get(URL, HTTPTimeoutMS),
case Resp of
{ok, {{_, 200, _}, _, Body}} ->
case parse_crls(Body) of
error ->
{error, invalid_crl};
CRLs ->
%% Note: must ensure it's a string and not a
%% binary because that's what the ssl manager uses
%% when doing lookups.
emqx_ssl_crl_cache:insert(to_string(URL), {der, CRLs}),
?tp(crl_cache_insert, #{url => URL, crls => CRLs}),
{ok, CRLs}
end;
{ok, {{_, Code, _}, _, Body}} ->
{error, {bad_response, #{code => Code, body => Body}}};
{error, Error} ->
{error, {http_error, Error}}
end.
parse_crls(Bin) ->
try
[CRL || {'CertificateList', CRL, not_encrypted} <- public_key:pem_decode(Bin)]
catch
_:_ ->
error
end.
ensure_timer(URL, State = #state{refresh_interval = Timeout}) ->
ensure_timer(URL, State, Timeout).
ensure_timer(URL, State = #state{refresh_timers = RefreshTimers0}, Timeout) ->
?tp(crl_cache_ensure_timer, #{url => URL, timeout => Timeout}),
MTimer = maps:get(URL, RefreshTimers0, undefined),
emqx_misc:cancel_timer(MTimer),
RefreshTimers = RefreshTimers0#{
URL => emqx_misc:start_timer(
Timeout,
{refresh, URL}
)
},
State#state{refresh_timers = RefreshTimers}.
-spec gather_config() ->
#{
cache_capacity := pos_integer(),
refresh_interval := timer:time(),
http_timeout := timer:time()
}.
gather_config() ->
%% TODO: add a config handler to refresh the config when those
%% globals change?
CacheCapacity = emqx_config:get([crl_cache, capacity], ?DEFAULT_CACHE_CAPACITY),
RefreshIntervalMS0 = emqx_config:get([crl_cache, refresh_interval], ?DEFAULT_REFRESH_INTERVAL),
MinimumRefreshInverval = ?MIN_REFRESH_PERIOD,
RefreshIntervalMS = max(RefreshIntervalMS0, MinimumRefreshInverval),
HTTPTimeoutMS = emqx_config:get([crl_cache, http_timeout], ?HTTP_TIMEOUT),
#{
cache_capacity => CacheCapacity,
refresh_interval => RefreshIntervalMS,
http_timeout => HTTPTimeoutMS
}.
-spec handle_register_der_crls(state(), url(), [public_key:der_encoded()]) -> {noreply, state()}.
handle_register_der_crls(State0, URL0, CRLs) ->
#state{cached_urls = CachedURLs0} = State0,
URL = to_string(URL0),
case sets:is_element(URL, CachedURLs0) of
true ->
{noreply, State0};
false ->
emqx_ssl_crl_cache:insert(URL, {der, CRLs}),
?tp(debug, new_crl_url_inserted, #{url => URL}),
State1 = do_register_url(State0, URL),
State2 = handle_cache_overflow(State1),
State = ensure_timer(URL, State2),
{noreply, State}
end.
-spec do_register_url(state(), url()) -> state().
do_register_url(State0, URL) ->
#state{
cached_urls = CachedURLs0,
insertion_times = InsertionTimes0
} = State0,
Now = erlang:monotonic_time(),
CachedURLs = sets:add_element(URL, CachedURLs0),
InsertionTimes = gb_trees:enter(Now, URL, InsertionTimes0),
State0#state{
cached_urls = CachedURLs,
insertion_times = InsertionTimes
}.
-spec handle_cache_overflow(state()) -> state().
handle_cache_overflow(State0) ->
#state{
cached_urls = CachedURLs0,
insertion_times = InsertionTimes0,
cache_capacity = CacheCapacity,
refresh_timers = RefreshTimers0
} = State0,
case sets:size(CachedURLs0) > CacheCapacity of
false ->
State0;
true ->
{_Time, OldestURL, InsertionTimes} = gb_trees:take_smallest(InsertionTimes0),
emqx_ssl_crl_cache:delete(OldestURL),
MTimer = maps:get(OldestURL, RefreshTimers0, undefined),
emqx_misc:cancel_timer(MTimer),
RefreshTimers = maps:remove(OldestURL, RefreshTimers0),
CachedURLs = sets:del_element(OldestURL, CachedURLs0),
?tp(debug, crl_cache_overflow, #{oldest_url => OldestURL}),
State0#state{
insertion_times = InsertionTimes,
cached_urls = CachedURLs,
refresh_timers = RefreshTimers
}
end.
to_string(B) when is_binary(B) ->
binary_to_list(B);
to_string(L) when is_list(L) ->
L.

View File

@ -36,7 +36,8 @@ init([]) ->
child_spec(emqx_stats, worker), child_spec(emqx_stats, worker),
child_spec(emqx_metrics, worker), child_spec(emqx_metrics, worker),
child_spec(emqx_authn_authz_metrics_sup, supervisor), child_spec(emqx_authn_authz_metrics_sup, supervisor),
child_spec(emqx_ocsp_cache, worker) child_spec(emqx_ocsp_cache, worker),
child_spec(emqx_crl_cache, worker)
] ]
}}. }}.

View File

@ -487,7 +487,8 @@ esockd_opts(ListenerId, Type, Opts0) ->
tcp -> tcp ->
Opts3#{tcp_options => tcp_opts(Opts0)}; Opts3#{tcp_options => tcp_opts(Opts0)};
ssl -> ssl ->
OptsWithSNI = inject_sni_fun(ListenerId, Opts0), OptsWithCRL = inject_crl_config(Opts0),
OptsWithSNI = inject_sni_fun(ListenerId, OptsWithCRL),
SSLOpts = ssl_opts(OptsWithSNI), SSLOpts = ssl_opts(OptsWithSNI),
Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)} Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)}
end end
@ -794,3 +795,17 @@ inject_sni_fun(ListenerId, Conf = #{ssl_options := #{ocsp := #{enable_ocsp_stapl
emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf); emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf);
inject_sni_fun(_ListenerId, Conf) -> inject_sni_fun(_ListenerId, Conf) ->
Conf. Conf.
inject_crl_config(
Conf = #{ssl_options := #{enable_crl_check := true} = SSLOpts}
) ->
HTTPTimeout = emqx_config:get([crl_cache, http_timeout], timer:seconds(15)),
Conf#{
ssl_options := SSLOpts#{
%% `crl_check => true' doesn't work
crl_check => peer,
crl_cache => {emqx_ssl_crl_cache, {internal, [{http, HTTPTimeout}]}}
}
};
inject_crl_config(Conf) ->
Conf.

View File

@ -545,10 +545,23 @@ readable_error_msg(Error) ->
{ok, Msg} -> {ok, Msg} ->
Msg; Msg;
false -> false ->
iolist_to_binary(io_lib:format("~0p", [Error])) to_hr_error(Error)
end end
end. end.
to_hr_error(nxdomain) ->
<<"Could not resolve host">>;
to_hr_error(econnrefused) ->
<<"Connection refused">>;
to_hr_error({unauthorized_client, _}) ->
<<"Unauthorized client">>;
to_hr_error({not_authorized, _}) ->
<<"Not authorized">>;
to_hr_error({malformed_username_or_password, _}) ->
<<"Bad username or password">>;
to_hr_error(Error) ->
iolist_to_binary(io_lib:format("~0p", [Error])).
try_to_existing_atom(Convert, Data, Encoding) -> try_to_existing_atom(Convert, Data, Encoding) ->
try Convert(Data, Encoding) of try Convert(Data, Encoding) of
Atom -> Atom ->

View File

@ -226,6 +226,11 @@ roots(low) ->
sc( sc(
ref("trace"), ref("trace"),
#{} #{}
)},
{"crl_cache",
sc(
ref("crl_cache"),
#{hidden => true}
)} )}
]. ].
@ -794,6 +799,37 @@ fields("listeners") ->
} }
)} )}
]; ];
fields("crl_cache") ->
%% Note: we make the refresh interval and HTTP timeout global (not
%% per-listener) because multiple SSL listeners might point to the
%% same URL. If they had diverging timeout options, it would be
%% confusing.
[
{"refresh_interval",
sc(
duration(),
#{
default => <<"15m">>,
desc => ?DESC("crl_cache_refresh_interval")
}
)},
{"http_timeout",
sc(
duration(),
#{
default => <<"15s">>,
desc => ?DESC("crl_cache_refresh_http_timeout")
}
)},
{"capacity",
sc(
pos_integer(),
#{
default => 100,
desc => ?DESC("crl_cache_capacity")
}
)}
];
fields("mqtt_tcp_listener") -> fields("mqtt_tcp_listener") ->
mqtt_listener(1883) ++ mqtt_listener(1883) ++
[ [
@ -2065,6 +2101,8 @@ desc("shared_subscription_group") ->
"Per group dispatch strategy for shared subscription"; "Per group dispatch strategy for shared subscription";
desc("ocsp") -> desc("ocsp") ->
"Per listener OCSP Stapling configuration."; "Per listener OCSP Stapling configuration.";
desc("crl_cache") ->
"Global CRL cache options.";
desc(_) -> desc(_) ->
undefined. undefined.
@ -2264,13 +2302,22 @@ server_ssl_opts_schema(Defaults, IsRanchListener) ->
hidden => true, hidden => true,
validator => fun ocsp_inner_validator/1 validator => fun ocsp_inner_validator/1
} }
)},
{"enable_crl_check",
sc(
boolean(),
#{
default => false,
desc => ?DESC("server_ssl_opts_schema_enable_crl_check")
}
)} )}
] ]
]. ].
mqtt_ssl_listener_ssl_options_validator(Conf) -> mqtt_ssl_listener_ssl_options_validator(Conf) ->
Checks = [ Checks = [
fun ocsp_outer_validator/1 fun ocsp_outer_validator/1,
fun crl_outer_validator/1
], ],
case emqx_misc:pipeline(Checks, Conf, not_used) of case emqx_misc:pipeline(Checks, Conf, not_used) of
{ok, _, _} -> {ok, _, _} ->
@ -2305,6 +2352,18 @@ ocsp_inner_validator(#{<<"enable_ocsp_stapling">> := true} = Conf) ->
), ),
ok. ok.
crl_outer_validator(
#{<<"enable_crl_check">> := true} = SSLOpts
) ->
case maps:get(<<"verify">>, SSLOpts) of
verify_peer ->
ok;
_ ->
{error, "verify must be verify_peer when CRL check is enabled"}
end;
crl_outer_validator(_SSLOpts) ->
ok.
%% @doc Make schema for SSL client. %% @doc Make schema for SSL client.
-spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema(). -spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema().
client_ssl_opts_schema(Defaults) -> client_ssl_opts_schema(Defaults) ->

View File

@ -0,0 +1,237 @@
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2015-2022. 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.
%%
%% %CopyrightEnd%
%%--------------------------------------------------------------------
%% Copyright (c) 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.
%%--------------------------------------------------------------------
%----------------------------------------------------------------------
% Based on `otp/lib/ssl/src/ssl_crl_cache.erl'
%----------------------------------------------------------------------
%----------------------------------------------------------------------
%% Purpose: Simple default CRL cache
%%----------------------------------------------------------------------
-module(emqx_ssl_crl_cache).
-include_lib("ssl/src/ssl_internal.hrl").
-include_lib("public_key/include/public_key.hrl").
-behaviour(ssl_crl_cache_api).
-export_type([crl_src/0, uri/0]).
-type crl_src() :: {file, file:filename()} | {der, public_key:der_encoded()}.
-type uri() :: uri_string:uri_string().
-export([lookup/3, select/2, fresh_crl/2]).
-export([insert/1, insert/2, delete/1]).
%% Allow usage of OTP certificate record fields (camelCase).
-elvis([
{elvis_style, atom_naming_convention, #{
regex => "^([a-z][a-z0-9]*_?)([a-zA-Z0-9]*_?)*$",
enclosed_atoms => ".*"
}}
]).
%%====================================================================
%% Cache callback API
%%====================================================================
lookup(
#'DistributionPoint'{distributionPoint = {fullName, Names}},
_Issuer,
CRLDbInfo
) ->
get_crls(Names, CRLDbInfo);
lookup(_, _, _) ->
not_available.
select(GenNames, CRLDbHandle) when is_list(GenNames) ->
lists:flatmap(
fun
({directoryName, Issuer}) ->
select(Issuer, CRLDbHandle);
(_) ->
[]
end,
GenNames
);
select(Issuer, {{_Cache, Mapping}, _}) ->
case ssl_pkix_db:lookup(Issuer, Mapping) of
undefined ->
[];
CRLs ->
CRLs
end.
fresh_crl(#'DistributionPoint'{distributionPoint = {fullName, Names}}, CRL) ->
case get_crls(Names, undefined) of
not_available ->
CRL;
NewCRL ->
NewCRL
end.
%%====================================================================
%% API
%%====================================================================
insert(CRLs) ->
insert(?NO_DIST_POINT, CRLs).
insert(URI, {file, File}) when is_list(URI) ->
case file:read_file(File) of
{ok, PemBin} ->
PemEntries = public_key:pem_decode(PemBin),
CRLs = [
CRL
|| {'CertificateList', CRL, not_encrypted} <-
PemEntries
],
do_insert(URI, CRLs);
Error ->
Error
end;
insert(URI, {der, CRLs}) ->
do_insert(URI, CRLs).
delete({file, File}) ->
case file:read_file(File) of
{ok, PemBin} ->
PemEntries = public_key:pem_decode(PemBin),
CRLs = [
CRL
|| {'CertificateList', CRL, not_encrypted} <-
PemEntries
],
ssl_manager:delete_crls({?NO_DIST_POINT, CRLs});
Error ->
Error
end;
delete({der, CRLs}) ->
ssl_manager:delete_crls({?NO_DIST_POINT, CRLs});
delete(URI) ->
case uri_string:normalize(URI, [return_map]) of
#{scheme := "http", path := Path} ->
ssl_manager:delete_crls(string:trim(Path, leading, "/"));
_ ->
{error, {only_http_distribution_points_supported, URI}}
end.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
do_insert(URI, CRLs) ->
case uri_string:normalize(URI, [return_map]) of
#{scheme := "http", path := Path} ->
ssl_manager:insert_crls(string:trim(Path, leading, "/"), CRLs);
_ ->
{error, {only_http_distribution_points_supported, URI}}
end.
get_crls([], _) ->
not_available;
get_crls(
[{uniformResourceIdentifier, "http" ++ _ = URL} | Rest],
CRLDbInfo
) ->
case cache_lookup(URL, CRLDbInfo) of
[] ->
handle_http(URL, Rest, CRLDbInfo);
CRLs ->
CRLs
end;
get_crls([_ | Rest], CRLDbInfo) ->
%% unsupported CRL location
get_crls(Rest, CRLDbInfo).
http_lookup(URL, Rest, CRLDbInfo, Timeout) ->
case application:ensure_started(inets) of
ok ->
http_get(URL, Rest, CRLDbInfo, Timeout);
_ ->
get_crls(Rest, CRLDbInfo)
end.
http_get(URL, Rest, CRLDbInfo, Timeout) ->
case emqx_crl_cache:http_get(URL, Timeout) of
{ok, {_Status, _Headers, Body}} ->
case Body of
<<"-----BEGIN", _/binary>> ->
Pem = public_key:pem_decode(Body),
CRLs = lists:filtermap(
fun
({'CertificateList', CRL, not_encrypted}) ->
{true, CRL};
(_) ->
false
end,
Pem
),
emqx_crl_cache:register_der_crls(URL, CRLs),
CRLs;
_ ->
try public_key:der_decode('CertificateList', Body) of
_ ->
CRLs = [Body],
emqx_crl_cache:register_der_crls(URL, CRLs),
CRLs
catch
_:_ ->
get_crls(Rest, CRLDbInfo)
end
end;
{error, _Reason} ->
get_crls(Rest, CRLDbInfo)
end.
cache_lookup(_, undefined) ->
[];
cache_lookup(URL, {{Cache, _}, _}) ->
#{path := Path} = uri_string:normalize(URL, [return_map]),
case ssl_pkix_db:lookup(string:trim(Path, leading, "/"), Cache) of
undefined ->
[];
[CRLs] ->
CRLs
end.
handle_http(URI, Rest, {_, [{http, Timeout}]} = CRLDbInfo) ->
CRLs = http_lookup(URI, Rest, CRLDbInfo, Timeout),
%% Uncomment to improve performance, but need to
%% implement cache limit and or cleaning to prevent
%% DoS attack possibilities
%%insert(URI, {der, CRLs}),
CRLs;
handle_http(_, Rest, CRLDbInfo) ->
get_crls(Rest, CRLDbInfo).

View File

@ -16,7 +16,7 @@
-module(emqx_common_test_helpers). -module(emqx_common_test_helpers).
-include("emqx_authentication.hrl"). -include_lib("emqx/include/emqx_authentication.hrl").
-type special_config_handler() :: fun(). -type special_config_handler() :: fun().
@ -202,7 +202,6 @@ start_apps(Apps, SpecAppConfig, Opts) when is_function(SpecAppConfig) ->
%% Because, minirest, ekka etc.. application will scan these modules %% Because, minirest, ekka etc.. application will scan these modules
lists:foreach(fun load/1, [emqx | Apps]), lists:foreach(fun load/1, [emqx | Apps]),
ok = start_ekka(), ok = start_ekka(),
mnesia:clear_table(emqx_admin),
ok = emqx_ratelimiter_SUITE:load_conf(), ok = emqx_ratelimiter_SUITE:load_conf(),
lists:foreach(fun(App) -> start_app(App, SpecAppConfig, Opts) end, [emqx | Apps]). lists:foreach(fun(App) -> start_app(App, SpecAppConfig, Opts) end, [emqx | Apps]).
@ -262,12 +261,13 @@ app_schema(App) ->
end. end.
mustache_vars(App, Opts) -> mustache_vars(App, Opts) ->
ExtraMustacheVars = maps:get(extra_mustache_vars, Opts, []), ExtraMustacheVars = maps:get(extra_mustache_vars, Opts, #{}),
[ Defaults = #{
{platform_data_dir, app_path(App, "data")}, platform_data_dir => app_path(App, "data"),
{platform_etc_dir, app_path(App, "etc")}, platform_etc_dir => app_path(App, "etc"),
{platform_log_dir, app_path(App, "log")} platform_log_dir => app_path(App, "log")
] ++ ExtraMustacheVars. },
maps:merge(Defaults, ExtraMustacheVars).
render_config_file(ConfigFile, Vars0) -> render_config_file(ConfigFile, Vars0) ->
Temp = Temp =
@ -275,7 +275,7 @@ render_config_file(ConfigFile, Vars0) ->
{ok, T} -> T; {ok, T} -> T;
{error, Reason} -> error({failed_to_read_config_template, ConfigFile, Reason}) {error, Reason} -> error({failed_to_read_config_template, ConfigFile, Reason})
end, end,
Vars = [{atom_to_list(N), iolist_to_binary(V)} || {N, V} <- Vars0], Vars = [{atom_to_list(N), iolist_to_binary(V)} || {N, V} <- maps:to_list(Vars0)],
Targ = bbmustache:render(Temp, Vars), Targ = bbmustache:render(Temp, Vars),
NewName = ConfigFile ++ ".rendered", NewName = ConfigFile ++ ".rendered",
ok = file:write_file(NewName, Targ), ok = file:write_file(NewName, Targ),

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
-----BEGIN CERTIFICATE-----
MIIF+zCCA+OgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQK
DAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJvb3RD
QTAeFw0yMzAxMTIxMzA4MTZaFw0zMzAxMDkxMzA4MTZaMGsxCzAJBgNVBAYTAlNF
MRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcGA1UE
CwwQTXlJbnRlcm1lZGlhdGVDQTEZMBcGA1UEAwwQTXlJbnRlcm1lZGlhdGVDQTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQG7dMeU/y9HDNHzhydR0bm
wN9UGplqJOJPwqJRaZZcrn9umgJ9SU2il2ceEVxMDwzBWCRKJO5/H9A9k13SqsXM
2c2c9xXfIF1kb820lCm1Uow5hZ/auDjxliNk9kNJDigCRi3QoIs/dVeWzFsgEC2l
gxRqauN2eNFb6/yXY788YALHBsCRV2NFOFXxtPsvLXpD9Q/8EqYsSMuLARRdHVNU
ryaEF5lhShpcuz0TlIuTy2TiuXJUtJ+p7a4Z7friZ6JsrmQWsVQBj44F8TJRHWzW
C7vm9c+dzEX9eqbr5iPL+L4ctMW9Lz6ePcYfIXne6CElusRUf8G+xM1uwovF9bpV
+9IqY7tAu9G1iY9iNtJgNNDKOCcOGKcZCx6Cg1XYOEKReNnUMazvYeqRrrjV5WQ0
vOcD5zcBRNTXCddCLa7U0guXP9mQrfuk4NTH1Bt77JieTJ8cfDXHwtaKf6aGbmZP
wl1Xi/GuXNUP/xeog78RKyFwBmjt2JKwvWzMpfmH4mEkG9moh2alva+aEz6LIJuP
16g6s0Q6c793/OvUtpNcewHw4Vjn39LD9o6VLp854G4n8dVpUWSbWS+sXD1ZE69H
g/sMNMyq+09ufkbewY8xoCm/rQ1pqDZAVMWsstJEaYu7b/eb7R+RGOj1YECCV/Yp
EZPdDotbSNRkIi2d/a1NAgMBAAGjgaQwgaEwHQYDVR0OBBYEFExwhjsVUom6tQ+S
qq6xMUETvnPzMB8GA1UdIwQYMBaAFD90kfU5pc5l48THu0Ayj9SNpHuhMBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDsGA1UdHwQ0MDIwMKAuoCyG
Kmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUuY3JsLnBlbTANBgkq
hkiG9w0BAQsFAAOCAgEAK6NgdWQYtPNKQNBGjsgtgqTRh+k30iqSO6Y3yE1KGABO
EuQdVqkC2qUIbCB0M0qoV0ab50KNLfU6cbshggW4LDpcMpoQpI05fukNh1jm3ZuZ
0xsB7vlmlsv00tpqmfIl/zykPDynHKOmFh/hJP/KetMy4+wDv4/+xP31UdEj5XvG
HvMtuqOS23A+H6WPU7ol7KzKBnU2zz/xekvPbUD3JqV+ynP5bgbIZHAndd0o9T8e
NFX23Us4cTenU2/ZlOq694bRzGaK+n3Ksz995Nbtzv5fbUgqmf7Mcq4iHGRVtV11
MRyBrsXZp2vbF63c4hrf2Zd6SWRoaDKRhP2DMhajpH9zZASSTlfejg/ZRO2s+Clh
YrSTkeMAdnRt6i/q4QRcOTCfsX75RFM5v67njvTXsSaSTnAwaPi78tRtf+WSh0EP
VVPzy++BszBVlJ1VAf7soWZHCjZxZ8ZPqVTy5okoHwWQ09WmYe8GfulDh1oj0wbK
3FjN7bODWHJN+bFf5aQfK+tumYKoPG8RXL6QxpEzjFWjxhIMJHHMKfDWnAV1o1+7
/1/aDzq7MzEYBbrgQR7oE5ZHtyqhCf9LUgw0Kr7/8QWuNAdeDCJzjXRROU0hJczp
dOyfRlLbHmLLmGOnROlx6LsGNQ17zuz6SPi7ei8/ylhykawDOAGkM1+xFakmQhM=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFzzCCA7egAwIBAgIUYjc7hD7/UJ0/VPADfNfp/WpOwRowDQYJKoZIhvcNAQEL
BQAwbzELMAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJ
U3RvY2tob2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENB
MREwDwYDVQQDDAhNeVJvb3RDQTAeFw0yMzAxMTIxMzA4MTRaFw00MzAxMDcxMzA4
MTRaMG8xCzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAcM
CVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMREwDwYDVQQLDAhNeVJvb3RD
QTERMA8GA1UEAwwITXlSb290Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQCnBwSOYVJw47IoMHMXTVDtOYvUt3rqsurEhFcB4O8xmf2mmwr6m7s8A5Ft
AvAehg1GvnXT3t/KiyU7BK+acTwcErGyZwS2wvdB0lpHWSpOn/u5y+4ZETvQefcj
ZTdDOM9VN5nutpitgNb+1yL8sqSexfVbY7DnYYvFjOVBYoP/SGvM9jVjCad+0WL3
FhuD+L8QAxzCieX3n9UMymlFwINQuEc+TDjuNcEqt+0J5EgS1fwzxb2RCVL0TNv4
9a71hFGCNRj20AeZm99hbdufm7+0AFO7ocV5q43rLrWFUoBzqKPYIjga/cv/UdWZ
c5RLRXw3JDSrCqkf/mOlaEhNPlmWRF9MSus5Da3wuwgGCaVzmrf30rWR5aHHcscG
e+AOgJ4HayvBUQeb6ZlRXc0YlACiLToMKxuyxDyUcDfVEXpUIsDILF8dkiVQxEU3
j9g6qjXiqPVdNiwpqXfBKObj8vNCzORnoHYs8cCgib3RgDVWeqkDmlSwlZE7CvQh
U4Loj4l7813xxzYEKkVaT1JdXPWu42CG/b4Y/+f4V+3rkJkYzUwndX6kZNksIBai
phmtvKt+CTdP1eAbT+C9AWWF3PT31+BIhuT0u9tR8BVSkXdQB8dG4M/AAJcTo640
0mdYYOXT153gEKHJuUBm750ZTy+r6NjNvpw8VrMAakJwHqnIdQIDAQABo2MwYTAd
BgNVHQ4EFgQUP3SR9TmlzmXjxMe7QDKP1I2ke6EwHwYDVR0jBBgwFoAUP3SR9Tml
zmXjxMe7QDKP1I2ke6EwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw
DQYJKoZIhvcNAQELBQADggIBAFMFv4C+I0+xOAb9v6G/IOpfPBZ1ez31EXKJJBra
lulP4nRHQMeb310JS8BIeQ3dl+7+PkSxPABZSwc3jkxdSMvhc+Z4MQtTgos+Qsjs
gH7sTqwWeeQ0lHYxWmkXijrh5OPRZwTKzYQlkcn85BCUXl2KDuNEdiqPbDTao+lc
lA0/UAvC6NCyFKq/jqf4CmW5Kx6yG1v1LaE+IXn7cbIXj+DaehocVXi0wsXqj03Q
DDUHuLHZP+LBsg4e91/0Jy2ekNRTYJifSqr+9ufHl0ZX1pFDZyf396IgZ5CQZ0PJ
nRxZHlCfsxWxmxxdy3FQSE6YwXhdTjjoAa1ApZcKkkt1beJa6/oRLze/ux5x+5q+
4QczufHd6rjoKBi6BM3FgFQ8As5iNohHXlMHd/xITo1Go3CWw2j9TGH5vzksOElK
B0mcwwt2zwNEjvfytc+tI5jcfGN3tiT5fVHS8hw9dWKevypLL+55Ua9G8ZgDHasT
XFRJHgmnbyFcaAe26D2dSKmhC9u2mHBH+MaI8dj3e7wNBfpxNgp41aFIk+QTmiFW
VXFED6DHQ/Mxq93ACalHdYg18PlIYClbT6Pf2xXBnn33YPhn5xzoTZ+cDH/RpaQp
s0UUTSJT1UTXgtXPnZWQfvKlMjJEIiVFiLEC0sgZRlWuZDRAY0CdZJJxvQp59lqu
cbTm
-----END CERTIFICATE-----

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFdTCCA12gAwIBAgICEAUwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIzMDExODEyMzY1NloXDTMzMDQyNTEyMzY1NlowgYQxCzAJBgNVBAYTAlNFMRIw
EAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAcMCVN0b2NraG9sbTESMBAGA1UECgwJ
TXlPcmdOYW1lMRkwFwYDVQQLDBBNeUludGVybWVkaWF0ZUNBMR4wHAYDVQQDDBVj
bGllbnQtbm8tZGlzdC1wb2ludHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQCYQqNF7o20tEwyXphDgtwkZ628baYzQoCmmaufR+5SPQWdTN+GFeApv0dP
4y/ncZV24rgButMo73e4+wPsILwSGhaVIU0mMaCmexyC4W6INBkQsVB5FAd/YM0O
gdxS6A42h9HZTaAJ+4ftgFdOOHiP3lwicXeIYykAE7Y5ikxlnHgi8p1PTLowN4Q+
AjuXChRzmU16cUEAevZKkTVf7VCcK66aJsxBsxfykkGHhc6qLqmlMt6Te6DPCi/R
KP/kARTDWNEkp6qtpvzByYFYAKPSZxPuryajAC3RLuGNkVSB+PZ6NnZW6ASeTdra
Lwuiwsi5XPBeFb0147naQOBzSGG/AgMBAAGjggEHMIIBAzAJBgNVHRMEAjAAMBEG
CWCGSAGG+EIBAQQEAwIFoDBBBglghkgBhvhCAQ0ENBYyT3BlblNTTCBHZW5lcmF0
ZWQgQ2xpZW50IENlcnRpZmljYXRlIChubyBDUkwgaW5mbykwHQYDVR0OBBYEFBiV
sjDe46MixvftT/wej1mxGuN7MB8GA1UdIwQYMBaAFExwhjsVUom6tQ+Sqq6xMUET
vnPzMA4GA1UdDwEB/wQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
AwQwMQYIKwYBBQUHAQEEJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0
Ojk4NzcwDQYJKoZIhvcNAQELBQADggIBAKBEnKYVLFtZb3MI0oMJkrWBssVCq5ja
OYomZ61I13QLEeyPevTSWAcWFQ4zQDF/SWBsXjsrC+JIEjx2xac6XCpxcx3jDUgo
46u/hx2rT8tMKa60hW0V1Dk6w8ZHiCe94BlFLsWFKnn6dVzoJd2u3vgUaleh3uxF
hug8XY+wmHd36rO0kVe3DrsqdIdOfhMiJLDxU0cBA79vI5kCvqB8DIwCWtOzkA82
EPl3Iws5NPhuFAR9u0xOQu0akzmSJFcEGLZ4qfatHD/tZGRduyFvMKy5iIeMzuEs
2etm01tfLHqgKGOKp5LjPm7Aoac/GeVoTvctGF+wayvOuYE7inlGZToz3kQMMzHZ
ZGBBgOhXbR2y74QoFv6DUqmmTRbGfiLYyErA5r881ntgciQi02xrGjoAFntvKb+H
HNB22Qprz16OmdC9dJKF2RhO6Cketdhv65wFWw6xlhRMCWYPY3CI8tWkxS4A4yit
RZQZg3yaeHXMaCAu5HxuqAQXKGjz+7w7N6diwbT7o7CfKk8iHUrGfkQ5nCS0GZ1r
lU1vgKtdzVvJ6HmBrCRcdNqh/L/wdIltwI/52j+TKRtELM1qHuLAYmhcRBW+2wuH
ewaNA9KEgEk6JC+iR8uOBi0ZLkMIm47j+ZLJRJVUfgkVEEFjyiYSFfpwwcgT+/Aw
EczVZOdUEbDM
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCYQqNF7o20tEwy
XphDgtwkZ628baYzQoCmmaufR+5SPQWdTN+GFeApv0dP4y/ncZV24rgButMo73e4
+wPsILwSGhaVIU0mMaCmexyC4W6INBkQsVB5FAd/YM0OgdxS6A42h9HZTaAJ+4ft
gFdOOHiP3lwicXeIYykAE7Y5ikxlnHgi8p1PTLowN4Q+AjuXChRzmU16cUEAevZK
kTVf7VCcK66aJsxBsxfykkGHhc6qLqmlMt6Te6DPCi/RKP/kARTDWNEkp6qtpvzB
yYFYAKPSZxPuryajAC3RLuGNkVSB+PZ6NnZW6ASeTdraLwuiwsi5XPBeFb0147na
QOBzSGG/AgMBAAECggEACSMuozq+vFJ5pCgzIRIQXgruzTkTWU4rZFQijYuGjN7m
oFsFqwlTC45UHEI5FL2nR5wxiMEKfRFp8Or3gEsyni98nXSDKcCesH8A5gXbWUcv
HeZWOv3tuUI47B709vDAMZuTB2R2L0MuFB24n5QaACBLDTIcB05UHpIQRIG9NffH
MhxqFB2kuakp67VekYGZkBCNkqfL3VQZIGRpQC8SvpnRXELqZgI4MyJgvkK6myWj
Vtpwm8YiOQoJHJx4raoVfS2NWTsCwL0M0aXMMtmM2QfMP/xB9OifxnmDDBs7Tie8
0Wri845xLTCYthaU8B06rhoQdKXoqKmQMoF2doPm8QKBgQDN+0E0PtPkyxIho8pV
CsQnmif91EQQqWxOdkHbE96lT0UKu6ziBSbB4ClRHYil5c8p7INxRpj7pruOY3Kw
MAcacIMMBNhLBJL4R0hr/pwr18WOZxCIMcLHTaCfbVqL71TKp4/6C+GexZfaYJ46
IZEpLU5RPmD4f9MPIDDm6KcPxwKBgQC9O9TOor93g+A4sU54CGOqvVDrdi5TnGF8
YdimvUsT20gl2WGX5vq3OohzZi7U8FuxKHWpbgh2efqGLcFsRNFZ/T0ZXX4DDafN
Gzyu/DMVuFO4ccgFJNnl45w3/yFG40kL6yS8kss/iEYu550/uOZ1FjH+kJ0vjV6G
JD8q0PgOSQKBgG2i9cLcSia2nBEBwFlhoKS/ndeyWwRPWZGtykHUoqZ0ufgLiurG
+SkqqnM9eBVta8YR2Ki7fgQ8bApPDqWO+sjs6CPGlGXhqmSydG7fF7sSX1n7q8YC
Tn2M6RjSuOZQ3l37sFvUZSQAYmJfGPkyErTLI6uEu1KpnuqnJMBTR1DTAoGAIGQn
bx9oirqmHM4s0lsNRGKXgVZ/Y4x3G2VcQl5QhZuZY/ErxWaiL87zIF2zUnu6Fj8I
tPHCvRTwDxux6ih1dWPlm3vnX/psaK1q28ELtYIRwpanWEoQiktFqEghmBK7pDCh
3y15YOygptK6lfe+avhboml6nnMiZO+7aEbQzxECgYALuUM4fo1dQYmYuZIqZoFJ
TXGyzMkNGs61SMiD6mW6XgXj5h5T8Q0MdpmHkwsm+z9A/1of5cxkE6d8HCCz+dt5
tnY7OC0gYB1+gDld8MZgFgP6k0qklreLVhzEz11TbMldifa1EE4VjUDG/NeAEtbq
GbLaw0NhGJtRCgL9Bc7i7g==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFnDCCA4SgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIzMDExMjEzMDgxNloXDTMzMDQxOTEzMDgxNlowfTELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExFzAVBgNVBAMMDmNs
aWVudC1yZXZva2VkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+R6
PDtIxVlUoLYbDBbaVcxgoLjnWcvqL8wSqyWuqi/Y3cjuNYCziR9nR5dWajtkBjzJ
HyhgAr6gBVSRt4RRmDXoOcprK3GcpowAr65UAmC4hdH0af6FdKjKCnFw67byUg52
f7ueXZ6t/XuuKxlU/f2rjXVwmmnlhBi5EHDkXxvfgWXJekDfsPbW9j0kaCUWCpfj
rzGbfkXqrPkslO41PYlCbPxoiRItJjindFjcQySYvRq7A2uYMGsrxv4n3rzo5NGt
goBmnGj61ii9WOdopcFxKirhIB9zrxC4x0opRfIaF/n1ZXk6NOnaDxu1LTZ18wfC
ZB979ge6pleeKoPf7QIDAQABo4IBNjCCATIwCQYDVR0TBAIwADARBglghkgBhvhC
AQEEBAMCBaAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIENsaWVu
dCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUQeItXr3nc6CZ++G9UCoq1YlQ9oowHwYD
VR0jBBgwFoAUTHCGOxVSibq1D5KqrrExQRO+c/MwDgYDVR0PAQH/BAQDAgXgMB0G
A1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDA7BgNVHR8ENDAyMDCgLqAshipo
dHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW0wMQYIKwYB
BQUHAQEEJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJ
KoZIhvcNAQELBQADggIBAIFuhokODd54/1B2JiNyG6FMq/2z8B+UquC2iw3p2pyM
g/Jz4Ouvg6gGwUwmykEua06FRCxx5vJ5ahdhXvKst/zH/0qmYTFNMhNsDy76J/Ot
Ss+VwQ8ddpEG3EIUI9BQxB3xL7z7kRQzploQjakNcDWtDt1BmN05Iy2vz4lnYJky
Kss6ya9jEkNibHekhxJuchJ0fVGlVe74MO7RNDFG7+O3tMlxu0zH/LpW093V7BI2
snXNAwQBizvWTrDKWLDu5JsX8KKkrmDtFTs9gegnxDCOYdtG5GbbMq+H1SjWUJPV
wiXTF8/eE02s4Jzm7ZAxre4bRt/hAg7xTGmDQ1Hn+LzLn18I9LaW5ZWqSwwpgv+g
Z/jiLO9DJ/y525Cl7DLCpSFoDTWlQXouKhcgALcVay/cXCsZ3oFZCustburLiJi/
zgBeEk1gVpwljriJLeZifyfWtJx6yfgB/h6fid8XLsGRD+Yc8Tzs8J1LIgi+j4ZT
UzKX3B85Kht/dr43UDMtWOF3edkOMaJu7rcg5tTsK+LIyHtXvebKPVvvA9f27Dz/
4gmhAwwqS87Xv3FMVhZ03DNOJ6XAF+T6OTEqwYs+iK56IMSl1Jy+bCzo0j5jZVbl
XFwGxUHzM7pfM6PDx657oUxG1QwM/fIWA18F+kY/yigXxq6pYMeAiQsPanOThgHp
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCz5Ho8O0jFWVSg
thsMFtpVzGCguOdZy+ovzBKrJa6qL9jdyO41gLOJH2dHl1ZqO2QGPMkfKGACvqAF
VJG3hFGYNeg5ymsrcZymjACvrlQCYLiF0fRp/oV0qMoKcXDrtvJSDnZ/u55dnq39
e64rGVT9/auNdXCaaeWEGLkQcORfG9+BZcl6QN+w9tb2PSRoJRYKl+OvMZt+Reqs
+SyU7jU9iUJs/GiJEi0mOKd0WNxDJJi9GrsDa5gwayvG/ifevOjk0a2CgGacaPrW
KL1Y52ilwXEqKuEgH3OvELjHSilF8hoX+fVleTo06doPG7UtNnXzB8JkH3v2B7qm
V54qg9/tAgMBAAECggEAAml+HRgjZ+gEezot3yngSBW7NvR7v6e9DmKDXpGdB7Go
DANBdGyzG5PU9/AGy9pbgzzl6nnJXcgOD7w8TvRifrK8WCgHa1f05IPMj458GGMR
HlQ8HX647eFEgkLWo4Z6tdB1VM2geDtkNFmn8nJ+wgAYgIdSWPOyDOUi+B43ZbIN
eaLWkP2fiX9tcJp41cytW+ng2YIm4s90Nt4FJPNBNzOrhVm35jciId02MmEjCEnr
0YbK9uoMDC2YLg8vhRcjtsUHV2rREkwEAQj8nCWvWWheIwk943d6OicGAD/yebpV
PTjtlZlpIbrovfvuMcoTxJg3WS8LTg/+cNWAX5a3eQKBgQDcRY7nVSJusYyN0Bij
YWc9H47wU+YucaGT25xKe26w1pl6s4fmr1Sc3NcaN2iyUv4BuAvaQzymHe4g9deU
D9Ws/NCQ9EjHJJsklNyn2KCgkSp7oPKhPwyl64XfPdV2gr5AD6MILf7Rkyib5sSf
1WK8i25KatT7M4mCtrBVJYHNpQKBgQDREjwPIaQBPXouVpnHhSwRHfKD0B1a2koq
4VE6Fnf3ogkiGfV9kqXwIfPHL0tfotFraM3FFmld8RcxhKUPr4oj+K9KTxmMD9lm
9Hal0ANXYmHs5a1iHyoNmTpBGHALWLT9fCoeg+EIYabi2+P1c7cDIdUPkEzo4GmI
nCIpv7hGqQKBgEFUC+8GK+EinWoN1tDV+ZWCP5V9fJ43q1E7592bQBgIfZqLlnnP
dEvVn6Ix3sZMoPMHj9Ra7qjh5Zc28ooCLEBS9tSW7uLJM44k7FCHihQ1GaFy+aLj
HTA0aw7rutycKCq9uH+bjKDBgWDDj3tMAS2kOMCvcJ1UCquO3TtTlWzVAoGBAIDN
8yJ/X0NEVNnnkKZTbWq+QILk3LD0e20fk6Nt5Es0ENxpkczjZEglIsM8Z/trnAnI
b71UqWWu+tMPHYIka77tn1DwmpSnzxCW2+Ib3XMgsaP5fHBPMuFd3X3tSFo1NIxW
yrwyE5nOT7rELhUyTTYoydLk2/09BMedKY7/BtDBAoGAXeX1pX74K1i/uWyYKwYZ
sskRueSo9whDJuZWgNiUovArr57eA+oA+bKdFpiE419348bkFF8jNoGFQ6MXMedD
LqHAYIj+ZPIC4+rObHqO5EaIyblgutwx3citkQp7HXDBxojnOKA9mKQXj1vxCaL1
/1fFNJQCzEqwnKwnhI2MJ28=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFljCCA36gAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIzMDExMjEzMDgxNloXDTMzMDQxOTEzMDgxNlowdzELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExETAPBgNVBAMMCE15
Q2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvGuAShewEo8V
/+aWVO/MuUt92m8K0Ut4nC2gOvpjMjf8mhSSf6KfnxPklsFwP4fdyPOjOiXwCsf3
1QO5fjVr8to3iGTHhEyZpzRcRqmw1eYJC7iDh3BqtYLAT30R+Kq6Mk+f4tXB5Lp/
2jXgdi0wshWagCPgJO3CtiwGyE8XSa+Q6EBYwzgh3NFbgYdJma4x+S86Y/5WfmXP
zF//UipsFp4gFUqwGuj6kJrN9NnA1xCiuOxCyN4JuFNMfM/tkeh26jAp0OHhJGsT
s3YiUm9Dpt7Rs7o0so9ov9K+hgDFuQw9HZW3WIJI99M5a9QZ4ZEQqKpABtYBl/Nb
VPXcr+T3fQIDAQABo4IBNjCCATIwCQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMC
BaAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIENsaWVudCBDZXJ0
aWZpY2F0ZTAdBgNVHQ4EFgQUOIChBA5aZB0dPWEtALfMIfSopIIwHwYDVR0jBBgw
FoAUTHCGOxVSibq1D5KqrrExQRO+c/MwDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQW
MBQGCCsGAQUFBwMCBggrBgEFBQcDBDA7BgNVHR8ENDAyMDCgLqAshipodHRwOi8v
bG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW0wMQYIKwYBBQUHAQEE
JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJKoZIhvcN
AQELBQADggIBAE0qTL5WIWcxRPU9oTrzJ+oxMTp1JZ7oQdS+ZekLkQ8mP7T6C/Ew
6YftjvkopnHUvn842+PTRXSoEtlFiTccmA60eMAai2tn5asxWBsLIRC9FH3LzOgV
/jgyY7HXuh8XyDBCDD+Sj9QityO+accTHijYAbHPAVBwmZU8nO5D/HsxLjRrCfQf
qf4OQpX3l1ryOi19lqoRXRGwcoZ95dqq3YgTMlLiEqmerQZSR6iSPELw3bcwnAV1
hoYYzeKps3xhwszCTz2+WaSsUO2sQlcFEsZ9oHex/02UiM4a8W6hGFJl5eojErxH
7MqaSyhwwyX6yt8c75RlNcUThv+4+TLkUTbTnWgC9sFjYfd5KSfAdIMp3jYzw3zw
XEMTX5FaLaOCAfUDttPzn+oNezWZ2UyFTQXQE2CazpRdJoDd04qVg9WLpQxLYRP7
xSFEHulOPccdAYF2C45yNtJAZyWKfGaAZIxrgEXbMkcdDMlYphpRwpjS8SIBNZ31
KFE8BczKrg2qO0ywIjanPaRgrFVmeSvBKeU/YLQVx6fZMgOk6vtidLGZLyDXy0Ff
yaZSoj+on++RDz1IXb96Y8scuNlfcYI8QeoNjwiLtf80BV8SRJiG4e/jTvMf/z9L
kWrnDWvx4xkUmxFg4TK42dkNp7sEYBTlVVq9fjKE92ha7FGZRqsxOLNQ
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC8a4BKF7ASjxX/
5pZU78y5S33abwrRS3icLaA6+mMyN/yaFJJ/op+fE+SWwXA/h93I86M6JfAKx/fV
A7l+NWvy2jeIZMeETJmnNFxGqbDV5gkLuIOHcGq1gsBPfRH4qroyT5/i1cHkun/a
NeB2LTCyFZqAI+Ak7cK2LAbITxdJr5DoQFjDOCHc0VuBh0mZrjH5Lzpj/lZ+Zc/M
X/9SKmwWniAVSrAa6PqQms302cDXEKK47ELI3gm4U0x8z+2R6HbqMCnQ4eEkaxOz
diJSb0Om3tGzujSyj2i/0r6GAMW5DD0dlbdYgkj30zlr1BnhkRCoqkAG1gGX81tU
9dyv5Pd9AgMBAAECggEAAifx6dZKIeNkQ8OaNp5V2IKIPSqBOV4/h/xKMkUZXisV
eDmTCf8du0PR7hfLqrt9xYsGDv+6FQ1/8K231l8qR0tP/6CTl/0ynM4qqEAGeFXN
3h2LvM4liFbdjImechrcwcnVaNKg/DogT5zHUYSMtB/rokaG0VBO3IX/+SGz0aXi
LOLAx6SPaLOVX9GYUCiigTSEDwaQA+F3F6J2fR4u8PrXo+OQUqxjQ/fGXWp+4IfA
6djlpvzO2849/WPB1tL20iLXJlL2OL0UgQNtbKWTjexMe+wgCR5BzCwTyPsQvMwX
YOQrTOwgF3b6O+gLks5wSRT0ivq1sKgzA534+X4M+wKBgQDirPTLlrYobOO8KUpV
LOJU8x9leiRNU9CZWrW/mOw/BXGXikqNWvgL595vvADsjYciuRxSqEE7lClB8Pp9
20TMlES9orx7gdoQJCodpNV1BuBJhE9YtUiXzWAj+7m3D9LsXM1ewW/2A7Vvopj3
4zKY7uHAFlo3nXwLOfChG5/i9wKBgQDUy5fPFa58xmn7Elb6x4vmUDHg6P4pf75E
XHRQvNA8I7DTrpqfcsF1N4WuJ3Lm//RSpw7bnyqP20GoEfGHu/iCUPf29B7CuXhO
vvD+I8uPdn8EcKUBWV+V0xNQN/gCe0TzrEjAkZcO2Lq0j93R8HVl3BbowxgRvQV9
GmxQG/boKwKBgFeV8uSzsGEAaiKrZbBxrmaappgEUQCcES8gULfes/JJ/TFL2zCx
ZMTc7CMKZuUAbqXpFtuNbd9CiYqUPYXh8ryF0eXgeqnSa9ruzmMz7NLSPFnLyQkC
yzD0x2BABOuKLrrrxOMHJWbO2g1vq2GlJUjYjNw3BtcUf/iqg6MM1IPTAoGAWYWJ
SSqS7JVAcsrFYt1eIrdsNHVwr565OeM3X9v/Mr3FH1jeXeQWNSz1hU29Ticx7y+u
1YBBlKGmHoHl/bd7lb9ggjkzU7JZRa+YjSIb+i/cwc5t7IJf7xUMk/vnz4tyd5zs
Qm89gJZ2/Y1kwXSKvx53WNbyokvGKlpaZN1O418CgYACliGux77pe4bWeXSFFd9N
50ipxDLVghw1c5AiZn25GR5YHJZaV4R0wmFcHdZvogLKi0jDMPvU69PaiT8eX/A1
COkxv7jY1vtKlEtb+gugMjMN8wvb2va4kyFamjqnleiZlBSqIF/Y17wBoMvaWgZ0
bEPCN//ts5hBwgb1TwGrrg==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFljCCA36gAwIBAgICEAowDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIzMDMxNjIwMjAzMloXDTMzMDYyMTIwMjAzMlowdjELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEDAOBgNVBAMMB0Ns
aWVudDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDcDhlEvUIYc9uA
ocOBXt5thKrovs+8V0Eus/WrHMTKBk0Kw4X+7HBaRBoZj2sZpYfN63lVaO75kW4I
uJuorGj5PAXYWJj+4uAsCc95xAN/liCuHJnxE5togWVt8W+z0Zll98RIpiCohqiE
FLDL4X6FREL07GLgQZ/BFORvAwU+Gog05AFh43iZDnJl8MmrG2HBSRXtSZ6vQj9A
NrOSqz5eK4YIHEEsgwTWQmhtNwu3Y+GzrAPWCA4TeYrSRwIrnGh20fOWXkAMldS4
eRXmBztEsXMGqbe6oYO1QPYOlmoGO8EaaDPJ2sFIuM0zn98Alq3kCnRhM5Bi9RpJ
7IpudIopAgMBAAGjggE3MIIBMzAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIF
oDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgQ2xpZW50IENlcnRp
ZmljYXRlMB0GA1UdDgQWBBQoIuXq3wG6JEzAEj9wPe7am0OVgjAfBgNVHSMEGDAW
gBRMcIY7FVKJurUPkqqusTFBE75z8zAOBgNVHQ8BAf8EBAMCBeAwHQYDVR0lBBYw
FAYIKwYBBQUHAwIGCCsGAQUFBwMEMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9s
b2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUxLmNybC5wZW0wMQYIKwYBBQUHAQEE
JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJKoZIhvcN
AQELBQADggIBAHqKYcwkm3ODPD7Mqxq3bsswSXregWfc8tqfIBc5FZg2F+IzhxcJ
kINB0lmcNdLALK6ka0sDs1Nrj1KB96NcHUqE+WY/qPS1Yksr34yFatb1ddlKQ9HK
VRrIsi0ZfjBpHpvoQ0GsLeyRKm7iN/Fm5H9u8rw6RBu0Oe/l20FVSQIDzldYw51L
uV/E9No8ZhdQ2Dffujs8madI7b7I1NMXS+Z1pZ+gYrz6O60tDEprE+rYuYWypURr
fK+DnLLl+KQ+eekTPynw7LRpFzI/1cOMmd4BRnsBHCbCObfNp7WPasemZOEXGIlZ
CQwZS62DYOJE4u4Nz5pSF+JgXfr6X/Im6Y1SV900xVHfoL0GpFDI9k+0Y5ncHfSH
+V9HlRWB3zqQF+yla32XOpBbER0vFDH52gp8/o1ZGg7rr6KrP4QKxnqywNLiAPDX
txaAykZhON7uG8j+Lbjx5Ik91NRn9Fd5NH/vtT33a4uig2TP9EWd7EPcD2z8ONuD
yiK3S37XAnmSKKX4HcCpEb+LedtqQo/+sqWyWXkpKdpkUSozvcYS4J/ob3z9N2IE
qIH5I+Mty1I4EB4W89Pem8DHNq86Lt0Ea6TBtPTV8NwR5aG2vvLzb5lNdpANXYcp
nGr57mTWaHnQh+yqgy66J++k+WokWkAkwE989AvUfNoQ+Jr6cTH8nKo2
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcDhlEvUIYc9uA
ocOBXt5thKrovs+8V0Eus/WrHMTKBk0Kw4X+7HBaRBoZj2sZpYfN63lVaO75kW4I
uJuorGj5PAXYWJj+4uAsCc95xAN/liCuHJnxE5togWVt8W+z0Zll98RIpiCohqiE
FLDL4X6FREL07GLgQZ/BFORvAwU+Gog05AFh43iZDnJl8MmrG2HBSRXtSZ6vQj9A
NrOSqz5eK4YIHEEsgwTWQmhtNwu3Y+GzrAPWCA4TeYrSRwIrnGh20fOWXkAMldS4
eRXmBztEsXMGqbe6oYO1QPYOlmoGO8EaaDPJ2sFIuM0zn98Alq3kCnRhM5Bi9RpJ
7IpudIopAgMBAAECggEARcly2gnrXDXh9vlWN0EO6UyZpxZcay6AzX7k+k81WZyF
8lPvutjhCL9wR4rkPE3ys6tp31xX7W3hp4JkWynSYLhYYjQ20R7CWTUDR2qScXP7
CTyo1XuSXaIruKJI+o4OR/g7l46X7NpHtxuYtg/dQAZV9bbB5LzrHSCzEUGz9+17
jV//cBgLBiMdlbdLuQoGt4NQpBkNrauBVFq7Nq648uKkICmUo3Bzn/dfn3ehB+Zc
+580S+tawYd224j19tFQmd5oK8tfjqKuHenNGjp/gsRoY86N7qAtc3VIQ0yjE6ez
tgREo/ftCb8kGfwRJOAQIeeDamBv+FWNT6QzcOtbwQKBgQDzWhY9BUgI8JVzjYg0
oWfU90On81BtckKsEo//8MTlgwOD2PnUF0hTZF3RcSPYT+HybouTqHT8EOLBAzqy
1+koH06MnAc/Y2ipaAe2fGaVH3SuXAsV/b8VcWWl4Qx7tYJDhE7sKmdl3/+jHZ7A
hZQzgOQnxxCANBo3pwF9KboDbwKBgQDnfglSpgYdGzFpWp1hZnPl2RKIfX/4M2z2
s+hVN1tp+1VySPrBRUC3J6hKPQUzzeUzPICclHOnO+kP7jAos/rlJ9VcNdAQTbTL
7Ds9Em1KJTBphE038YbW3e93rydQpukXh50wRD9RI/3F3A/1rKhab92DXZZr6Wqu
JouhNV8f5wKBgQCLQ3XQi/Iyc4QDse5NuETUgoCsX7kaOTZghOr1nFMByV08mfI2
5vAUES8DigzqYKS8eXjVEqWIDx3FOVThPmCG/ouUOkKHixs9P3SSgVSvaGX81l3d
wu4UlmWGbWkYbsJSYyhLTOUJTwxby7qrEIbEhrGK9gfCZo7OZHucpkF2bwKBgFhl
1qWK5JbExY+XnLWO6/7/b4ZTdkSPTrK+bJ/t7aiA41Yq7CZVjarjJ+6BcrUfkMCK
AArK3Yck55C/wgApCkvrdBwsKHGxWrLsWIqvuLAxl1UTwnD0eCsgwMsRRZAUzLnB
fZLq3MrdVZDywd1suzUdtpbta/11OtmZuoQq31JNAoGAIzmevuPaUZRqnjDpLXAm
Bo11q6CunhG5qZX4wifeZ9Fp5AaQu97F36igZ5/eDxFkDCrFRq6teMiNjRQZSLA3
5tMBkq6BtN2Ozzm/6D135c4BF14ODHqAMPUy4sXbw5PS/kRFs4fKAH/+LcAOPgyI
N/jJIY1LfM7PzrG2NdyscMU=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFljCCA36gAwIBAgICEAswDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIzMDMxNjIwMjAzMloXDTMzMDYyMTIwMjAzMlowdjELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEDAOBgNVBAMMB0Ns
aWVudDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFLcCjzNhfY6Sk
2nSdrB/6UPPeTCCH5NBBVLvu1hdlqLS4qEdq8+EjyMDZTtspRtYPkBfjpOrlBWUO
lKyxw2mZOjZ8iWvd4sJaAI/6KZl5X0Rdsg1RjzW03kUdLx9XJCyrYY0YFrT1dgJo
Ih56jk2SJX7wrz0NCJ05VPIdpaOF6CcziA+YhdVHcE6xyHagsYI0JdDWxFZrl9zT
LyhaDgBUN/yUQBnxKzxs8TMT4YVSi73ouh5IP9Xvs52hd6HO8ZGVr+YthQZKo95p
OlwFF+AQWxdDIKoPYUPFo8XMOXvOeQ9iUJarxrYSrelLXtGkaGLBolAvqo/YKE7j
rcJWjRGHAgMBAAGjggE3MIIBMzAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIF
oDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgQ2xpZW50IENlcnRp
ZmljYXRlMB0GA1UdDgQWBBTOo9YSgx1h5k/imP7nOfRfzQrRxjAfBgNVHSMEGDAW
gBRMcIY7FVKJurUPkqqusTFBE75z8zAOBgNVHQ8BAf8EBAMCBeAwHQYDVR0lBBYw
FAYIKwYBBQUHAwIGCCsGAQUFBwMEMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9s
b2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUyLmNybC5wZW0wMQYIKwYBBQUHAQEE
JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJKoZIhvcN
AQELBQADggIBAFo91lLqjPY67Wmj2yWxZuTTuUwXdXXUQxL6sEUUnfkECvRhNyBA
eCHkfVopNbXZ5tdLfsUvXF0ulaC76GCK/P7gHOG9D/RJX/85VzhuJcqa4dsEEifg
IiKIG7viYxSA6HFXuyzHvwNco3FqTBHbY46lKf1lWRVLhiAtcwcyPP34/RWcPfQi
6NZfLyitu5U7Z9XVN5wCp8sg0ayaO5Ib2ejIYuBCUddV1gV//tSDf+rKCgtAbm/X
K64Bf3GdaX3h6EhoqMZ+Z2f4XpKSXTabsWAU44xdVxisI82eo+NwT8KleE65GpOv
nPvr/dLq5fQ6VtHbRL3wWqhzB1VKVCtd8a6RE2k8HVWflU3qgwJ+woF19ed921eq
OZxc+KzjsGFyW1D2fPdgoZFmePadSstIME7qtCNEi7D3im01/1KKzE2m/nosrHeW
ePjY2YrXu0w47re/N2kBJL2xRbj+fAjBsfNn9RhvQsWheXG6mgg8w1ac6y72ZA2W
72pWoDkgXQMX5XBBj/zMnmwtrX9zTILFjNGFuWMPYgBRI0xOf2FoqqZ67cQ2yTW/
1T/6Mp0FSh4cIo/ENiNSdvlt3BIo84EyOm3iHHy28Iv5SiFjF0pkwtXlYYvjM3+R
BeWqlPsVCZXcVC1rPVDzfWZE219yghldY4I3QPJ7dlmszi8eI0HtzhTK
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFLcCjzNhfY6Sk
2nSdrB/6UPPeTCCH5NBBVLvu1hdlqLS4qEdq8+EjyMDZTtspRtYPkBfjpOrlBWUO
lKyxw2mZOjZ8iWvd4sJaAI/6KZl5X0Rdsg1RjzW03kUdLx9XJCyrYY0YFrT1dgJo
Ih56jk2SJX7wrz0NCJ05VPIdpaOF6CcziA+YhdVHcE6xyHagsYI0JdDWxFZrl9zT
LyhaDgBUN/yUQBnxKzxs8TMT4YVSi73ouh5IP9Xvs52hd6HO8ZGVr+YthQZKo95p
OlwFF+AQWxdDIKoPYUPFo8XMOXvOeQ9iUJarxrYSrelLXtGkaGLBolAvqo/YKE7j
rcJWjRGHAgMBAAECggEABJYUCcyJcnbagytBxfnaNQUuAp8AIypFG3kipq0l5Stk
gGaTJq5F4OTGS4ofRsqeu07IgBSAfqJcJH8toPkDQqfvs6ftO1Mso2UzakMOcP51
Ywxd91Kjm+LKOyHkHGDirPGnutUg/YpLLrrMvTk/bJHDZCM4i/WP1WTREVFjUgl7
4L6Y53x2Lk5shJJhv0MzTGaoZzQcW0EbhNH1AI6MBv5/CN5m/7/+HCPlHSNKnozl
o3PXD6l0XNfOY2Hi6MgS/Vd70s3VmDT9UCJNsDjdFpKNHmI7vr9FScOLN8EwbqWe
maFa0TPknmPDmVjEGMtgGlJWL7Sm0MpNW+WsEXcDPQKBgQDv3sp0nVML9pxdzX/w
rGebFaZaMYDWmV9w0V1uXYh4ZkpFmqrWkq/QSTGpwIP/x8WH9FBDUZqspLpPBNgG
ft1XhuY34y3hoCxOyRhQcR/1dY+lgCzuN4G4MG3seq/cAXhrmkPljma/iO8KzoRK
Pa+uaKFGHy1vWY2AmOhT20zr4wKBgQDScA3478TFHg9THlSFzqpzvVn5eAvmmrCQ
RMYIZKFWPortqzeJHdA5ShVF1XBBar1yNMid7E7FXqi/P8Oh+E6Nuc7JxyVIJWlV
mcBE1ceTKdZn7A0nuQIaU6hcn7xz/UHmxGur1ZcNQm3diFJ2CPn11lzZlkSZLSCN
V86nndA9DQKBgQCWsUxXPo7xsRhDBdseg/ECyPMdLoRWTTxcT+t2bmRR31FBsQ0q
iDTTkWgV0NAcXJCH/MB/ykB1vXceNVjRm9nKJwFyktI8MLglNsiDoM4HErgPrRqM
/WoNIL+uFNVuTa4tS1jkWjXKlmg2Tc9mJKK92xWWS/frQENZSraKF/eXKQKBgGR9
ni6CUTTQZgELOtGrHzql8ZFwAj7dH/PE48yeQW0t8KoOWTbhRc4V0pLGmhSjJFSl
YCgJ8JPP4EVz7bgrG1gSou04bFVHiEWYZnh4nhVopTp7Psz5TEfGK2AP5658Ajxx
D/m+xaNPVae0sawsHTGIbE57s8ZyBll41Pa2JfsBAoGBANtS7SOehkflSdry0eAZ
50Ec3CmY+fArC044hQCmXxol5SiTUnXf/OIQH8y+RZUjF4F3PbbrFNLm/J6wuUPw
XUIb4gAL75srakL8DXqyTYfO02aNrFEHhXzMs+GoAnRkFy16IAAFwpjbYSPanfed
PfHCTWz6Y0pGdh1hUJAFV/3v
-----END PRIVATE KEY-----

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFljCCA36gAwIBAgICEAwwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIzMDMxNjIwMjAzMloXDTMzMDYyMTIwMjAzMlowdjELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEDAOBgNVBAMMB0Ns
aWVudDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEOZ6fYNjZDNXX
eOyapHMOMeNeYM3b7vsWXAbiJIt4utVrTS0A+/G640t/U0g8F9jbKgbjEEPtgPJ7
GltjLWObfqDWKSO2D9/ei2+NauqgiN/HX+dQnSKHob0McXBXvLfrA4tn4braKrbg
p1fZB8bAECuT/bUhVBqWlzrUwDMpqjMJWDab48ixezb2gnc/ePE6wq/d3ecDb0/k
cYWQ0LX4JiQBgaTGhwczyoGfL1z2vx5kJqptK+r0Hc2jNCn6kFvoZUCYjCWgWNxZ
sQk7fObQQkUb/XQyqRaKJBWDyqsNcuK2gOg3LGeolAlgtMiEqGhHv77XdJnJug/w
3OiHpP/7AgMBAAGjggE3MIIBMzAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIF
oDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgQ2xpZW50IENlcnRp
ZmljYXRlMB0GA1UdDgQWBBRxZFdIkSg6zDZCakXmIest5a6dBzAfBgNVHSMEGDAW
gBRMcIY7FVKJurUPkqqusTFBE75z8zAOBgNVHQ8BAf8EBAMCBeAwHQYDVR0lBBYw
FAYIKwYBBQUHAwIGCCsGAQUFBwMEMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9s
b2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUzLmNybC5wZW0wMQYIKwYBBQUHAQEE
JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJKoZIhvcN
AQELBQADggIBAEntkhiPpQtModUF/ffnxruq+cqopPhIdMXhMD8gtU5e4e7o3EHX
lfZKIbxyw56v6dFPrl4TuHBiBudqIvBCsPtllWKixWvg6FV3CrEeTcg4shUIaJcD
pqv1qHLwS4pue6oau/lb8jv1GuzuBXoMFQwlmiOXO7xXqXjV2GdmkFJCDdB/0BW1
VHvh0DXgotaxITWKhCpSNB7F7LSvegRwZIAN6JXrLDpue7tgqLqBB1EzpmS6ALbn
uZDdikOs/tGAFB3un/3Gl7jEPL8UGOoSj/H9PUT5AFHrHJDH72+QSXu09agz8RWJ
V939njYFCAxQ8Jt2mOK8BJQDJgPtLfIIb1iYicQV13Eypt8uIUYvp0i0Wq8WxPbq
rOEvQYpcGUsreS5XqZ7y68hgq6ePiR18Fnc3GyTV5o6qT3W7IOvPArTzNV5fFCwM
lx8xSEm+ebJrJPphp6Uc/h8evohvAN8R/Z7FSo9OL6V+F3ywPqWTXaqiIiRc9PS0
0vxsYZ96EeZY5HzjN6LzHxmkv4KYM5I1qmXlviQlaU+sotp3tzegADlM4K78nUFh
HuXamecEcS73eAgjk+FGqJ9E25B0TLlQMcP6tCKdaUIGn6ZsF5wT87GzqT99wL/5
foHCYIkyG7ZmAQmoaKBd4q6xqVOUHovmsPza69FuSrsBxoRR39PtAnrY
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEOZ6fYNjZDNXX
eOyapHMOMeNeYM3b7vsWXAbiJIt4utVrTS0A+/G640t/U0g8F9jbKgbjEEPtgPJ7
GltjLWObfqDWKSO2D9/ei2+NauqgiN/HX+dQnSKHob0McXBXvLfrA4tn4braKrbg
p1fZB8bAECuT/bUhVBqWlzrUwDMpqjMJWDab48ixezb2gnc/ePE6wq/d3ecDb0/k
cYWQ0LX4JiQBgaTGhwczyoGfL1z2vx5kJqptK+r0Hc2jNCn6kFvoZUCYjCWgWNxZ
sQk7fObQQkUb/XQyqRaKJBWDyqsNcuK2gOg3LGeolAlgtMiEqGhHv77XdJnJug/w
3OiHpP/7AgMBAAECggEADSe89sig5E63SKAlFXcGw0H2XgqIzDP/TGMnqPvNoYhX
eSXUgxDhBptpB9e9a4RaKwaFxxPjlSXEdYFX9O22YSN1RMMl6Q8Zl9g3edhcDR6W
b7Qbx2x8qj6Rjibnlh8JiFPiaDjN2wUeSDBss/9D98NkKiJ9Ue2YCYmJAOA3B3w9
2t4Co5+3YrxkdzkvibTQCUSEwHFeB1Nim21126fknMPxyrf+AezRBRc8JNAHqzWb
4QEeMnmIJDOzc3Oh7+P85tNyejOeRm9T7X3EQ0jKXgLYe+HUzXclBQ66b9x9Nc9b
tNn6XkMlLlsQ3f149Th6PtHksH3hM+GF8bMuCp9yxQKBgQDGk0PYPkLqTD8jHjJW
s8wBNhozigZPGaynxdTsD7L6UtDdOl1sSW/jFOj9UIs1duBce9dP1IjFc0jY+Kin
lMLv3qCtk5ZjxRglOoLipR9hdClcM69rDoRZdoQK8KYa+QHcOTSazIp3fnw4gWSX
nscelMfd1rtVP0dOGTuqE/73/QKBgQD8+F5WAi2IOVPHnBxAAOP+6XTs9Ntn1sDi
L5wNgm+QA28aJJ4KRAwdXIc3IFPlHxZI77c2K1L9dKDu9X4UcyZIZYDvGVLuOOt5
twaRaGuJW03cjbgOWC7rGyfzfZ49YlCZi2YuxERclBkbqgWD9hfa8twUfKNguF2Y
AyiOhohtVwKBgQCJB8zUp7pzhqQ3LrpcHHzWBSi1kjTiVvxPVnSlZfwDRCz/zSv0
8wRz9tUFIZS/E0ama4tcenTblL+bgpSX+E9BSiclQOiR9su/vQ3fK0Vpccis6LnP
rdflCKT8C68Eg/slppBHloijBzTfpWLuQlJ0JwV5b5ocrKsfGMiUiHH1XQKBgQDg
RnakfEPP7TtY0g+9ssxwOJxAZImM0zmojpsk4wpzvIeovuQap9+xvFHoztFyZhBE
07oz3U8zhE4V7TI9gSVktBEOaf47U914yIqbKd+FJJywODkBBq96I1ZVKn67X0mk
B5GtTrZo+agU/bTsHKdjp0L1KtdSLcJUviAb1Cxp+wKBgDrGqS01CCgxSUwMaZe4
8HFWp/oMSyVDG9lTSC3uP/VL76zNFI55T3X06Q87hDN3gCJGUOmHzDZ/oCOgM4/S
SU55M4lXeSEdFe84tMXJKOv5JXTkulzBYzATJ5J8DeS/4YZxMKyPDLXX8wgwmU+l
i6Imd3qCPhh5eI3z9eSNDX+6
-----END PRIVATE KEY-----

View File

@ -0,0 +1,20 @@
-----BEGIN X509 CRL-----
MIIDPDCCASQCAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0UxEjAQBgNV
BAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQLDBBNeUlu
dGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBFw0yMjA3MjAy
MDIzNTNaFw0zMjEwMjUyMDIzNTNaMBUwEwICEAIXDTIyMDYxMzEyNDIwNVqgbjBs
MB8GA1UdIwQYMBaAFCuv1TkzC1fSgTfzE1m1u5pRCJsVMDwGA1UdHAQ1MDOgLqAs
hipodHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW2EAf8w
CwYDVR0UBAQCAhADMA0GCSqGSIb3DQEBCwUAA4ICAQBbWdqRFsIrG6coL6ln1RL+
uhgW9l3XMmjNlyiYHHNzOgnlBok9xu9UdaVCOKC6GEthWSzSlBY1AZugje57DQQd
RkIJol9am94lKMTjF/qhzFLiSjho8fwZGDGyES5YeZXkLqNMOf6m/drKaI3iofWf
l63qU9jY8dnSrVDkwgCguUL2FTx60v5H9NPxSctQ3VDxDvDj0sTAcHFknQcZbfvY
ZWpOYNS0FAJlQPVK9wUoDxI0LhrWDq5h/T1jcGO34fPT8RUA5HRtFVUevqSuOLWx
WTfTx5oDeMZPJTvHWUcg4yMElHty4tEvtkFxLSYbZqj7qTU+mi/LAN3UKBH/gBEN
y2OsJvFhVRgHf+zPYegf3WzBSoeaXNAJZ4UnRo34P9AL3Mrh+4OOUP++oYRKjWno
pYtAmTrIwEYoLyisEhhZ6aD92f/Op3dIYsxwhHt0n0lKrbTmUfiJUAe7kUZ4PMn4
Gg/OHlbEDaDxW1dCymjyRGl+3/8kjy7bkYUXCf7w6JBeL2Hw2dFp1Gh13NRjre93
PYlSOvI6QNisYGscfuYPwefXogVrNjf/ttCksMa51tUk+ylw7ZMZqQjcPPSzmwKc
5CqpnQHfolvRuN0xIVZiAn5V6/MdHm7ocrXxOkzWQyaoNODTq4js8h8eYXgAkt1w
p1PTEFBucGud7uBDE6Ub6A==
-----END X509 CRL-----

View File

@ -0,0 +1,12 @@
crl_cache.refresh_interval = {{ refresh_interval }}
crl_cache.http_timeout = 17s
crl_cache.capacity = {{ cache_capacity }}
listeners.ssl.default {
ssl_options {
keyfile = "{{ test_data_dir }}/server.key.pem"
certfile = "{{ test_data_dir }}/server.cert.pem"
cacertfile = "{{ test_data_dir }}/ca-chain.cert.pem"
verify = verify_peer
enable_crl_check = true
}
}

View File

@ -0,0 +1,67 @@
-module(emqx_crl_cache_http_server).
-behaviour(gen_server).
-compile([nowarn_export_all, export_all]).
set_crl(CRLPem) ->
ets:insert(?MODULE, {crl, CRLPem}).
%%--------------------------------------------------------------------
%% `gen_server' APIs
%%--------------------------------------------------------------------
start_link(Parent, BasePort, CRLPem, Opts) ->
process_flag(trap_exit, true),
stop_http(),
timer:sleep(100),
gen_server:start_link(?MODULE, {Parent, BasePort, CRLPem, Opts}, []).
init({Parent, BasePort, CRLPem, Opts}) ->
Tab = ets:new(?MODULE, [named_table, ordered_set, public]),
ets:insert(Tab, {crl, CRLPem}),
ok = start_http(Parent, [{port, BasePort} | Opts]),
Parent ! {self(), ready},
{ok, #{parent => Parent}}.
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
stop_http().
stop(Pid) ->
ok = gen_server:stop(Pid).
%%--------------------------------------------------------------------
%% Callbacks
%%--------------------------------------------------------------------
start_http(Parent, Opts) ->
{ok, _Pid1} = cowboy:start_clear(http, Opts, #{
env => #{dispatch => compile_router(Parent)}
}),
ok.
stop_http() ->
cowboy:stop_listener(http),
ok.
compile_router(Parent) ->
{ok, _} = application:ensure_all_started(cowboy),
cowboy_router:compile([
{'_', [{'_', ?MODULE, #{parent => Parent}}]}
]).
init(Req, #{parent := Parent} = State) ->
%% assert
<<"GET">> = cowboy_req:method(Req),
[{crl, CRLPem}] = ets:lookup(?MODULE, crl),
Parent ! {http_get, iolist_to_binary(cowboy_req:uri(Req))},
Reply = reply(Req, CRLPem),
{ok, Reply, State}.
reply(Req, CRLPem) ->
cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, CRLPem, Req).

View File

@ -0,0 +1,12 @@
node.name = test@127.0.0.1
node.cookie = emqxsecretcookie
node.data_dir = "{{ test_priv_dir }}"
listeners.ssl.default {
ssl_options {
keyfile = "{{ test_data_dir }}/server.key.pem"
certfile = "{{ test_data_dir }}/server.cert.pem"
cacertfile = "{{ test_data_dir }}/ca-chain.cert.pem"
verify = verify_peer
enable_crl_check = false
}
}

View File

@ -0,0 +1,19 @@
-----BEGIN X509 CRL-----
MIIDJTCCAQ0CAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0UxEjAQBgNV
BAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQLDBBNeUlu
dGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBFw0yMzAxMTIx
MzA4MTZaFw0zMzAxMDkxMzA4MTZaoG4wbDAfBgNVHSMEGDAWgBRMcIY7FVKJurUP
kqqusTFBE75z8zA8BgNVHRwENTAzoC6gLIYqaHR0cDovL2xvY2FsaG9zdDo5ODc4
L2ludGVybWVkaWF0ZS5jcmwucGVthAH/MAsGA1UdFAQEAgIQADANBgkqhkiG9w0B
AQsFAAOCAgEAJGOZuqZL4m7zUaRyBrxeT6Tqo+XKz7HeD5zvO4BTNX+0E0CRyki4
HhIGbxjv2NKWoaUv0HYbGAiZdO4TaPu3w3tm4+pGEDBclBj2KTdbB+4Hlzv956gD
KXZ//ziNwx1SCoxxkxB+TALxReN0shE7Mof9GlB5HPskhLorZgg/pmgJtIykEpsq
QAjJo4aq+f2/L+9dzRM205fVFegtsHvgEVNKz6iK6skt+kDhj/ks9BKsnfCDIGr+
XnPYwS9yDnnhFdoJ40AQQDtomxggAjfgcSnqtHCxZwKJohuztbSWUgD/4yxzlrwP
Dk1cT/Ajjjqb2dXVOfTLK1VB2168uuouArxZ7KYbXwBjHduYWGGkA6FfkNJO/jpF
SL9qhX3oxcRF3hDhWigN1ZRD7NpDKwVal3Y9tmvO5bWhb5VF+3qv0HGeSGp6V0dp
sjwhIj+78bkUrcXxrivACLAXgSTGonx1uXD+T4P4NCt148dgRAbgd8sUXK5FcgU2
cdBl8Kv2ZUjEaod5gUzDtf22VGSoO9lHvfHdpG9o2H3wC7s4tyLTidNrduIguJff
IIgc44Y252iV0sOmZ5S0jjTRiF1YUUPy9qA/6bOnr2LohbwbNZv9tDlNj8cdhxUz
cKiS+c7Qsz+YCcrp19QRiJoQae/gUqz7kmUZQgyPmDd+ArE0V+kDZEE=
-----END X509 CRL-----

View File

@ -0,0 +1,19 @@
-----BEGIN X509 CRL-----
MIIC/TCB5gIBATANBgkqhkiG9w0BAQsFADBrMQswCQYDVQQGEwJTRTESMBAGA1UE
CAwJU3RvY2tob2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxGTAXBgNVBAsMEE15SW50
ZXJtZWRpYXRlQ0ExGTAXBgNVBAMMEE15SW50ZXJtZWRpYXRlQ0EXDTIzMDExODEz
Mjc1M1oXDTMzMDExNTEzMjc1M1owFTATAgIQAhcNMjMwMTEyMTMwODE2WqAwMC4w
HwYDVR0jBBgwFoAUTHCGOxVSibq1D5KqrrExQRO+c/MwCwYDVR0UBAQCAhACMA0G
CSqGSIb3DQEBCwUAA4ICAQCxoRYDc5MaBpDI+HQUX60+obFeZJdBkPO2wMb6HBQq
e0lZM2ukS+4n5oGhRelsvmEz0qKvnYS6ISpuFzv4Qy6Vaun/KwIYAdXsEQVwDHsu
Br4m1V01igjFnujowwR/7F9oPnZOmBaBdiyYbjgGV0YMF7sOfl4UO2MqI2GSGqVk
63wELT1AXjx31JVoyATQOQkq1A5HKFYLEbFmdF/8lNfbxSCBY2tuJ+uWVQtzjM0y
i+/owz5ez1BZ/Swx8akYhuvs8DVVTbjXydidVSrxt/QEf3+oJCzTA9qFqt4MH7gL
6BAglCGtRiYTHqeMHrwddaHF2hzR61lHJlkMCL61yhVuL8WsEJ/AxVX0W3MfQ4Cw
x/A6xIkgqtu+HtQnPyDcJxyaFHtFC+U67nSbEQySFvHfMw42DGdIGojKQCeUer9W
ECFC8OATQwN2h//f8QkY7D0H3k/brrNYDfdFIcCti9iZiFrrPFxO7NbOTfkeKCt3
7IwYduRc8DWKmS8c7j2re1KkdYnfE1sfwbn3trImkcET5tvDlVCZ1glnBQzk82PS
HvKmSjD2pZI7upfLkoMgMhYyYJhYk7Mw2o4JXuddYGKmmw3bJyHkG/Ot5NAKjb7g
k1QCeWzxO1xXm8PNDDFWMn351twUGDQ/cwrUw0ODeUZpfL0BtTn4YnfCLLTvZDxo
Vg==
-----END X509 CRL-----

View File

@ -0,0 +1,20 @@
-----BEGIN X509 CRL-----
MIIDPDCCASQCAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0UxEjAQBgNV
BAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQLDBBNeUlu
dGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBFw0yMzAxMTIx
MzA4MTZaFw0zMzAxMDkxMzA4MTZaMBUwEwICEAIXDTIzMDExMjEzMDgxNlqgbjBs
MB8GA1UdIwQYMBaAFExwhjsVUom6tQ+Sqq6xMUETvnPzMDwGA1UdHAQ1MDOgLqAs
hipodHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW2EAf8w
CwYDVR0UBAQCAhABMA0GCSqGSIb3DQEBCwUAA4ICAQCPadbaehEqLv4pwqF8em8T
CW8TOQ4Vjz02uiVk9Bo0za1dQqQmwCBA6UE1BcOh+aWzQxBRz56NeUcfhgDxTntG
xLs896N9MHIG6UxpqJH8cH+DXKHsQjvvCjXtiObmBQR1RiG5C1vEMkfzTt/WSrq5
7blowLDs4NP6YbtqXEyyUkF7DQSUEUuIDWPQdx1f++nSpVaHWW4xpoO4umesaJco
FuxaXQnZpTHHQfqUJVIL2Mmzvez9thgfKTV3vgkYrGiSLW2m2+Tfga30pUc0qaVI
RrBVORVbcu9m1sV0aJyk96b2T/+i2FRR/np4TOcLgckBpHKeK2FH69lHFr0W/71w
CErNTxahoh82Yi8POenu+S1m2sDnrF1FMf+ZG/i2wr0nW6/+zVGQsEOw77Spbmei
dbEchu3iWF1XEO/n4zVBzl6a1o2RyVg+1pItYd5C5bPwcrfZnBrm4WECPxO+6rbW
2/wz9Iku4XznTLqLEpXLAtenAdo73mLGC7riviX7mhcxfN2UjNfLuVGHmG8XwIsM
Lgpr6DKaxHwpHgW3wA3SGJrY5dj0TvGWaoInrNt1cOMnIpoxRNy5+ko71Ubx3yrV
RhbUMggd1GG1ct9uZn82v74RYF6J8Xcxn9vDFJu5LLT5kvfy414kdJeTXKqfKXA/
atdUgFa0otoccn5FzyUuzg==
-----END X509 CRL-----

View File

@ -0,0 +1,20 @@
-----BEGIN X509 CRL-----
MIIDPDCCASQCAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0UxEjAQBgNV
BAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQLDBBNeUlu
dGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBFw0yMjA3MjAy
MDIzNTNaFw0zMjEwMjUyMDIzNTNaMBUwEwICEAIXDTIyMDYxMzEyNDIwNVqgbjBs
MB8GA1UdIwQYMBaAFCuv1TkzC1fSgTfzE1m1u5pRCJsVMDwGA1UdHAQ1MDOgLqAs
hipodHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW2EAf8w
CwYDVR0UBAQCAhADMA0GCSqGSIb3DQEBCwUAA4ICAQBbWdqRFsIrG6coL6ln1RL+
uhgW9l3XMmjNlyiYHHNzOgnlBok9xu9UdaVCOKC6GEthWSzSlBY1AZugje57DQQd
RkIJol9am94lKMTjF/qhzFLiSjho8fwZGDGyES5YeZXkLqNMOf6m/drKaI3iofWf
l63qU9jY8dnSrVDkwgCguUL2FTx60v5H9NPxSctQ3VDxDvDj0sTAcHFknQcZbfvY
ZWpOYNS0FAJlQPVK9wUoDxI0LhrWDq5h/T1jcGO34fPT8RUA5HRtFVUevqSuOLWx
WTfTx5oDeMZPJTvHWUcg4yMElHty4tEvtkFxLSYbZqj7qTU+mi/LAN3UKBH/gBEN
y2OsJvFhVRgHf+zPYegf3WzBSoeaXNAJZ4UnRo34P9AL3Mrh+4OOUP++oYRKjWno
pYtAmTrIwEYoLyisEhhZ6aD92f/Op3dIYsxwhHt0n0lKrbTmUfiJUAe7kUZ4PMn4
Gg/OHlbEDaDxW1dCymjyRGl+3/8kjy7bkYUXCf7w6JBeL2Hw2dFp1Gh13NRjre93
PYlSOvI6QNisYGscfuYPwefXogVrNjf/ttCksMa51tUk+ylw7ZMZqQjcPPSzmwKc
5CqpnQHfolvRuN0xIVZiAn5V6/MdHm7ocrXxOkzWQyaoNODTq4js8h8eYXgAkt1w
p1PTEFBucGud7uBDE6Ub6A==
-----END X509 CRL-----

View File

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGCTCCA/GgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIzMDExMjEzMDgxNloXDTMzMDQxOTEzMDgxNloweDELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEjAQBgNVBAMMCWxv
Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKdU9FaA/n0Z
TXkd10XA9l+UV9xKR65ZTy2ApCFlw2gGWLiUh96a6hX+GQZFUV7ECIDDf+7nC85o
xo1Xyf0rHGABQ0uHlhqSemc12F9APIzRLlQkhtV4vMBBbGQFekje4F9bhY9JQtGd
XJGmwsR+XWo6SUY7K5l9FuSSSRXC0kSYYQfSTPR/LrF6efdHf+ZN4huP7lM2qIFd
afX+qBOI1/Y2LtITo2TaU/hXyKh9wEiuynoq0RZ2KkYQll5cKD9fSD+pW3Xm0XWX
TQy4RZEe3WoYEQsklNw3NC92ocA/PQB9BGNO1fKhzDn6kW2HxDxruDKOuO/meGek
ApCayu3e/I0CAwEAAaOCAagwggGkMAkGA1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQD
AgZAMDMGCWCGSAGG+EIBDQQmFiRPcGVuU1NMIEdlbmVyYXRlZCBTZXJ2ZXIgQ2Vy
dGlmaWNhdGUwHQYDVR0OBBYEFGy5LQPzIelruJl7mL0mtUXM57XhMIGaBgNVHSME
gZIwgY+AFExwhjsVUom6tQ+Sqq6xMUETvnPzoXOkcTBvMQswCQYDVQQGEwJTRTES
MBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlTdG9ja2hvbG0xEjAQBgNVBAoM
CU15T3JnTmFtZTERMA8GA1UECwwITXlSb290Q0ExETAPBgNVBAMMCE15Um9vdENB
ggIQADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwOwYDVR0f
BDQwMjAwoC6gLIYqaHR0cDovL2xvY2FsaG9zdDo5ODc4L2ludGVybWVkaWF0ZS5j
cmwucGVtMDEGCCsGAQUFBwEBBCUwIzAhBggrBgEFBQcwAYYVaHR0cDovL2xvY2Fs
aG9zdDo5ODc3MA0GCSqGSIb3DQEBCwUAA4ICAQCX3EQgiCVqLhnCNd0pmptxXPxo
l1KyZkpdrFa/NgSqRhkuZSAkszwBDDS/gzkHFKEUhmqs6/UZwN4+Rr3LzrHonBiN
aQ6GeNNXZ/3xAQfUCwjjGmz9Sgw6kaX19Gnk2CjI6xP7T+O5UmsMI9hHUepC9nWa
XX2a0hsO/KOVu5ZZckI16Ek/jxs2/HEN0epYdvjKFAaVmzZZ5PATNjrPQXvPmq2r
x++La+3bXZsrH8P2FhPpM5t/IxKKW/Tlpgz92c2jVSIHF5khSA/MFDC+dk80OFmm
v4ZTPIMuZ//Q+wo0f9P48rsL9D27qS7CA+8pn9wu+cfnBDSt7JD5Yipa1gHz71fy
YTa9qRxIAPpzW2v7TFZE8eSKFUY9ipCeM2BbdmCQGmq4+v36b5TZoyjH4k0UVWGo
Gclos2cic5Vxi8E6hb7b7yZpjEfn/5lbCiGMfAnI6aoOyrWg6keaRA33kaLUEZiK
OgFNbPkjiTV0ZQyLXf7uK9YFhpVzJ0dv0CFNse8rZb7A7PLn8VrV/ZFnJ9rPoawn
t7ZGxC0d5BRSEyEeEgsQdxuY4m8OkE18zwhCkt2Qs3uosOWlIrYmqSEa0i/sPSQP
jiwB4nEdBrf8ZygzuYjT5T9YRSwhVox4spS/Av8Ells5JnkuKAhCVv9gHxYwbj0c
CzyLJgE1z9Tq63m+gQ==
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCnVPRWgP59GU15
HddFwPZflFfcSkeuWU8tgKQhZcNoBli4lIfemuoV/hkGRVFexAiAw3/u5wvOaMaN
V8n9KxxgAUNLh5YaknpnNdhfQDyM0S5UJIbVeLzAQWxkBXpI3uBfW4WPSULRnVyR
psLEfl1qOklGOyuZfRbkkkkVwtJEmGEH0kz0fy6xenn3R3/mTeIbj+5TNqiBXWn1
/qgTiNf2Ni7SE6Nk2lP4V8iofcBIrsp6KtEWdipGEJZeXCg/X0g/qVt15tF1l00M
uEWRHt1qGBELJJTcNzQvdqHAPz0AfQRjTtXyocw5+pFth8Q8a7gyjrjv5nhnpAKQ
msrt3vyNAgMBAAECggEABnWvIQ/Fw0qQxRYz00uJt1LguW5cqgxklBsdOvTUwFVO
Y4HIZP2R/9tZV/ahF4l10pK5g52DxSoiUB6Ne6qIY+RolqfbUZdKBmX7vmGadM02
fqUSV3dbwghEiO/1Mo74FnZQB6IKZFEw26aWakN+k7VAUufB3SEJGzXSgHaO63ru
dFGSiYI8U+q+YnhUJjCnmI12fycNfy451TdUQtGZb6pNmm5HRUF6hpAV8Le9LojP
Ql9eacPpsrzU15X5ElCQZ/f9iNh1bplcISuhrULgKUKOvAVrBlEK67uRVy6g98xA
c/rgNLkbL/jZEsAc3/vHAyFgd3lABfwpBGLHej3QgQKBgQDFNYmfBNQr89HC5Zc+
M6jXcAT/R+0GNczBTfC4iyNemwqsumSSRelNZ748UefKuS3F6Mvb2CBqE2LbB61G
hrnCffG2pARjZ491SefRwghhWWVGLP1p8KliLgOGBehA1REgJb+XULncjuHZuh4O
LVn3HVnWGxeBGg+yKa6Z4YQi3QKBgQDZN0O8ZcZY74lRJ0UjscD9mJ1yHlsssZag
njkX/f0GR/iVpfaIxQNC3gvWUy2LsU0He9sidcB0cfej0j/qZObQyFsCB0+utOgy
+hX7gokV2pes27WICbNWE2lJL4QZRJgvf82OaEy57kfDrm+eK1XaSZTZ10P82C9u
gAmMnontcQKBgGu29lhY9tqa7jOZ26Yp6Uri8JfO3XPK5u+edqEVvlfqL0Zw+IW8
kdWpmIqx4f0kcA/tO4v03J+TvycLZmVjKQtGZ0PvCkaRRhY2K9yyMomZnmtaH4BB
5wKtR1do2pauyg/ZDnDDswD5OfsGYWw08TK8YVlEqu3lIjWZ9rguKVIxAoGAZYUk
zVqr10ks3pcCA2rCjkPT4lA5wKvHgI4ylPoKVfMxRY/pp4acvZXV5ne9o7pcDBFh
G7v5FPNnEFPlt4EtN4tMragJH9hBZgHoYEJkG6islweg0lHmVWaBIMlqbfzXO+v5
gINSyNuLAvP2CvCqEXmubhnkFrpbgMOqsuQuBqECgYB3ss2PDhBF+5qoWgqymFof
1ovRPuQ9sPjWBn5IrCdoYITDnbBzBZERx7GLs6A/PUlWgST7jkb1PY/TxYSUfXzJ
SNd47q0mCQ+IUdqUbHgpK9b1ncwLMsnexpYZdHJWRLgnUhOx7OMjJc/4iLCAFCoN
3KJ7/V1keo7GBHOwnsFcCA==
-----END PRIVATE KEY-----

View File

@ -76,7 +76,7 @@ init_per_testcase(t_openssl_client, Config) ->
[], [],
Handler, Handler,
#{ #{
extra_mustache_vars => [{test_data_dir, DataDir}], extra_mustache_vars => #{test_data_dir => DataDir},
conf_file_path => ConfFilePath conf_file_path => ConfFilePath
} }
), ),

View File

@ -26,6 +26,8 @@
-include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
-import(emqx_common_test_helpers, [on_exit/1]).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
@ -65,6 +67,7 @@ end_per_suite(_Config) ->
init_per_testcase(TestCase, Config) when init_per_testcase(TestCase, Config) when
TestCase =:= t_subscribe_deny_disconnect_publishes_last_will_testament; TestCase =:= t_subscribe_deny_disconnect_publishes_last_will_testament;
TestCase =:= t_publish_last_will_testament_banned_client_connecting;
TestCase =:= t_publish_deny_disconnect_publishes_last_will_testament TestCase =:= t_publish_deny_disconnect_publishes_last_will_testament
-> ->
{ok, _} = emqx_authz:update(?CMD_REPLACE, []), {ok, _} = emqx_authz:update(?CMD_REPLACE, []),
@ -76,11 +79,15 @@ init_per_testcase(_, Config) ->
end_per_testcase(TestCase, _Config) when end_per_testcase(TestCase, _Config) when
TestCase =:= t_subscribe_deny_disconnect_publishes_last_will_testament; TestCase =:= t_subscribe_deny_disconnect_publishes_last_will_testament;
TestCase =:= t_publish_last_will_testament_banned_client_connecting;
TestCase =:= t_publish_deny_disconnect_publishes_last_will_testament TestCase =:= t_publish_deny_disconnect_publishes_last_will_testament
-> ->
{ok, _} = emqx:update_config([authorization, deny_action], ignore), {ok, _} = emqx:update_config([authorization, deny_action], ignore),
{ok, _} = emqx_authz:update(?CMD_REPLACE, []),
emqx_common_test_helpers:call_janitor(),
ok; ok;
end_per_testcase(_TestCase, _Config) -> end_per_testcase(_TestCase, _Config) ->
emqx_common_test_helpers:call_janitor(),
ok. ok.
set_special_configs(emqx_authz) -> set_special_configs(emqx_authz) ->
@ -396,5 +403,63 @@ t_publish_last_will_testament_denied_topic(_Config) ->
ok. ok.
%% client is allowed by ACL to publish to its LWT topic, is connected,
%% and then gets banned and kicked out while connected. Should not
%% publish LWT.
t_publish_last_will_testament_banned_client_connecting(_Config) ->
{ok, _} = emqx_authz:update(?CMD_REPLACE, [?SOURCE7]),
Username = <<"some_client">>,
ClientId = <<"some_clientid">>,
LWTPayload = <<"should not be published">>,
LWTTopic = <<"some_client/lwt">>,
ok = emqx:subscribe(<<"some_client/lwt">>),
{ok, C} = emqtt:start_link([
{clientid, ClientId},
{username, Username},
{will_topic, LWTTopic},
{will_payload, LWTPayload}
]),
?assertMatch({ok, _}, emqtt:connect(C)),
%% Now we ban the client while it is connected.
Now = erlang:system_time(second),
Who = {username, Username},
emqx_banned:create(#{
who => Who,
by => <<"test">>,
reason => <<"test">>,
at => Now,
until => Now + 120
}),
on_exit(fun() -> emqx_banned:delete(Who) end),
%% Now kick it as we do in the ban API.
process_flag(trap_exit, true),
?check_trace(
begin
ok = emqx_cm:kick_session(ClientId),
receive
{deliver, LWTTopic, #message{payload = LWTPayload}} ->
error(lwt_should_not_be_published_to_forbidden_topic)
after 2_000 -> ok
end,
ok
end,
fun(Trace) ->
?assertMatch(
[
#{
client_banned := true,
publishing_disallowed := false
}
],
?of_kind(last_will_testament_publish_denied, Trace)
),
ok
end
),
ok = snabbkaffe:stop(),
ok.
stop_apps(Apps) -> stop_apps(Apps) ->
lists:foreach(fun application:stop/1, Apps). lists:foreach(fun application:stop/1, Apps).

View File

@ -54,6 +54,17 @@ emqx_bridge_schema {
} }
} }
desc_status_reason {
desc {
en: "This is the reason given in case a bridge is failing to connect."
zh: "桥接连接失败的原因。"
}
label: {
en: "Failure reason"
zh: "失败原因"
}
}
desc_node_status { desc_node_status {
desc { desc {
en: """The status of the bridge for each node. en: """The status of the bridge for each node.

View File

@ -67,7 +67,8 @@
T == timescale; T == timescale;
T == matrix; T == matrix;
T == tdengine; T == tdengine;
T == dynamo T == dynamo;
T == rocketmq
). ).
load() -> load() ->

View File

@ -46,18 +46,33 @@
-export([lookup_from_local_node/2]). -export([lookup_from_local_node/2]).
-define(BAD_REQUEST(Reason), {400, error_msg('BAD_REQUEST', Reason)}). %% [TODO] Move those to a commonly shared header file
-define(ERROR_MSG(CODE, REASON), #{code => CODE, message => emqx_misc:readable_error_msg(REASON)}).
-define(OK(CONTENT), {200, CONTENT}).
-define(NO_CONTENT, 204).
-define(BAD_REQUEST(CODE, REASON), {400, ?ERROR_MSG(CODE, REASON)}).
-define(BAD_REQUEST(REASON), ?BAD_REQUEST('BAD_REQUEST', REASON)).
-define(NOT_FOUND(REASON), {404, ?ERROR_MSG('NOT_FOUND', REASON)}).
-define(INTERNAL_ERROR(REASON), {500, ?ERROR_MSG('INTERNAL_ERROR', REASON)}).
-define(NOT_IMPLEMENTED, 501).
-define(SERVICE_UNAVAILABLE(REASON), {503, ?ERROR_MSG('SERVICE_UNAVAILABLE', REASON)}).
%% End TODO
-define(BRIDGE_NOT_ENABLED, -define(BRIDGE_NOT_ENABLED,
?BAD_REQUEST(<<"Forbidden operation, bridge not enabled">>) ?BAD_REQUEST(<<"Forbidden operation, bridge not enabled">>)
). ).
-define(NOT_FOUND(Reason), {404, error_msg('NOT_FOUND', Reason)}). -define(BRIDGE_NOT_FOUND(BRIDGE_TYPE, BRIDGE_NAME),
-define(BRIDGE_NOT_FOUND(BridgeType, BridgeName),
?NOT_FOUND( ?NOT_FOUND(
<<"Bridge lookup failed: bridge named '", (BridgeName)/binary, "' of type ", <<"Bridge lookup failed: bridge named '", (BRIDGE_NAME)/binary, "' of type ",
(bin(BridgeType))/binary, " does not exist.">> (bin(BRIDGE_TYPE))/binary, " does not exist.">>
) )
). ).
@ -284,7 +299,7 @@ schema("/bridges") ->
'operationId' => '/bridges', 'operationId' => '/bridges',
get => #{ get => #{
tags => [<<"bridges">>], tags => [<<"bridges">>],
summary => <<"List Bridges">>, summary => <<"List bridges">>,
description => ?DESC("desc_api1"), description => ?DESC("desc_api1"),
responses => #{ responses => #{
200 => emqx_dashboard_swagger:schema_with_example( 200 => emqx_dashboard_swagger:schema_with_example(
@ -295,7 +310,7 @@ schema("/bridges") ->
}, },
post => #{ post => #{
tags => [<<"bridges">>], tags => [<<"bridges">>],
summary => <<"Create Bridge">>, summary => <<"Create bridge">>,
description => ?DESC("desc_api2"), description => ?DESC("desc_api2"),
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
emqx_bridge_schema:post_request(), emqx_bridge_schema:post_request(),
@ -312,7 +327,7 @@ schema("/bridges/:id") ->
'operationId' => '/bridges/:id', 'operationId' => '/bridges/:id',
get => #{ get => #{
tags => [<<"bridges">>], tags => [<<"bridges">>],
summary => <<"Get Bridge">>, summary => <<"Get bridge">>,
description => ?DESC("desc_api3"), description => ?DESC("desc_api3"),
parameters => [param_path_id()], parameters => [param_path_id()],
responses => #{ responses => #{
@ -322,7 +337,7 @@ schema("/bridges/:id") ->
}, },
put => #{ put => #{
tags => [<<"bridges">>], tags => [<<"bridges">>],
summary => <<"Update Bridge">>, summary => <<"Update bridge">>,
description => ?DESC("desc_api4"), description => ?DESC("desc_api4"),
parameters => [param_path_id()], parameters => [param_path_id()],
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
@ -337,7 +352,7 @@ schema("/bridges/:id") ->
}, },
delete => #{ delete => #{
tags => [<<"bridges">>], tags => [<<"bridges">>],
summary => <<"Delete Bridge">>, summary => <<"Delete bridge">>,
description => ?DESC("desc_api5"), description => ?DESC("desc_api5"),
parameters => [param_path_id()], parameters => [param_path_id()],
responses => #{ responses => #{
@ -356,7 +371,7 @@ schema("/bridges/:id/metrics") ->
'operationId' => '/bridges/:id/metrics', 'operationId' => '/bridges/:id/metrics',
get => #{ get => #{
tags => [<<"bridges">>], tags => [<<"bridges">>],
summary => <<"Get Bridge Metrics">>, summary => <<"Get bridge metrics">>,
description => ?DESC("desc_bridge_metrics"), description => ?DESC("desc_bridge_metrics"),
parameters => [param_path_id()], parameters => [param_path_id()],
responses => #{ responses => #{
@ -370,7 +385,7 @@ schema("/bridges/:id/metrics/reset") ->
'operationId' => '/bridges/:id/metrics/reset', 'operationId' => '/bridges/:id/metrics/reset',
put => #{ put => #{
tags => [<<"bridges">>], tags => [<<"bridges">>],
summary => <<"Reset Bridge Metrics">>, summary => <<"Reset bridge metrics">>,
description => ?DESC("desc_api6"), description => ?DESC("desc_api6"),
parameters => [param_path_id()], parameters => [param_path_id()],
responses => #{ responses => #{
@ -385,7 +400,7 @@ schema("/bridges/:id/enable/:enable") ->
put => put =>
#{ #{
tags => [<<"bridges">>], tags => [<<"bridges">>],
summary => <<"Enable or Disable Bridge">>, summary => <<"Enable or disable bridge">>,
desc => ?DESC("desc_enable_bridge"), desc => ?DESC("desc_enable_bridge"),
parameters => [param_path_id(), param_path_enable()], parameters => [param_path_id(), param_path_enable()],
responses => responses =>
@ -401,7 +416,7 @@ schema("/bridges/:id/:operation") ->
'operationId' => '/bridges/:id/:operation', 'operationId' => '/bridges/:id/:operation',
post => #{ post => #{
tags => [<<"bridges">>], tags => [<<"bridges">>],
summary => <<"Stop or Restart Bridge">>, summary => <<"Stop or restart bridge">>,
description => ?DESC("desc_api7"), description => ?DESC("desc_api7"),
parameters => [ parameters => [
param_path_id(), param_path_id(),
@ -423,7 +438,7 @@ schema("/nodes/:node/bridges/:id/:operation") ->
'operationId' => '/nodes/:node/bridges/:id/:operation', 'operationId' => '/nodes/:node/bridges/:id/:operation',
post => #{ post => #{
tags => [<<"bridges">>], tags => [<<"bridges">>],
summary => <<"Stop/Restart Bridge">>, summary => <<"Stop/restart bridge">>,
description => ?DESC("desc_api8"), description => ?DESC("desc_api8"),
parameters => [ parameters => [
param_path_node(), param_path_node(),
@ -463,11 +478,10 @@ schema("/bridges_probe") ->
'/bridges'(post, #{body := #{<<"type">> := BridgeType, <<"name">> := BridgeName} = Conf0}) -> '/bridges'(post, #{body := #{<<"type">> := BridgeType, <<"name">> := BridgeName} = Conf0}) ->
case emqx_bridge:lookup(BridgeType, BridgeName) of case emqx_bridge:lookup(BridgeType, BridgeName) of
{ok, _} -> {ok, _} ->
{400, error_msg('ALREADY_EXISTS', <<"bridge already exists">>)}; ?BAD_REQUEST('ALREADY_EXISTS', <<"bridge already exists">>);
{error, not_found} -> {error, not_found} ->
Conf = filter_out_request_body(Conf0), Conf = filter_out_request_body(Conf0),
{ok, _} = emqx_bridge:create(BridgeType, BridgeName, Conf), create_bridge(BridgeType, BridgeName, Conf)
lookup_from_all_nodes(BridgeType, BridgeName, 201)
end; end;
'/bridges'(get, _Params) -> '/bridges'(get, _Params) ->
Nodes = mria:running_nodes(), Nodes = mria:running_nodes(),
@ -478,9 +492,9 @@ schema("/bridges_probe") ->
[format_resource(Data, Node) || Data <- Bridges] [format_resource(Data, Node) || Data <- Bridges]
|| {Node, Bridges} <- lists:zip(Nodes, NodeBridges) || {Node, Bridges} <- lists:zip(Nodes, NodeBridges)
], ],
{200, zip_bridges(AllBridges)}; ?OK(zip_bridges(AllBridges));
{error, Reason} -> {error, Reason} ->
{500, error_msg('INTERNAL_ERROR', Reason)} ?INTERNAL_ERROR(Reason)
end. end.
'/bridges/:id'(get, #{bindings := #{id := Id}}) -> '/bridges/:id'(get, #{bindings := #{id := Id}}) ->
@ -493,8 +507,7 @@ schema("/bridges_probe") ->
{ok, _} -> {ok, _} ->
RawConf = emqx:get_raw_config([bridges, BridgeType, BridgeName], #{}), RawConf = emqx:get_raw_config([bridges, BridgeType, BridgeName], #{}),
Conf = deobfuscate(Conf1, RawConf), Conf = deobfuscate(Conf1, RawConf),
{ok, _} = emqx_bridge:create(BridgeType, BridgeName, Conf), update_bridge(BridgeType, BridgeName, Conf);
lookup_from_all_nodes(BridgeType, BridgeName, 200);
{error, not_found} -> {error, not_found} ->
?BRIDGE_NOT_FOUND(BridgeType, BridgeName) ?BRIDGE_NOT_FOUND(BridgeType, BridgeName)
end end
@ -512,16 +525,16 @@ schema("/bridges_probe") ->
end, end,
case emqx_bridge:check_deps_and_remove(BridgeType, BridgeName, AlsoDeleteActs) of case emqx_bridge:check_deps_and_remove(BridgeType, BridgeName, AlsoDeleteActs) of
{ok, _} -> {ok, _} ->
204; ?NO_CONTENT;
{error, {rules_deps_on_this_bridge, RuleIds}} -> {error, {rules_deps_on_this_bridge, RuleIds}} ->
?BAD_REQUEST( ?BAD_REQUEST(
{<<"Cannot delete bridge while active rules are defined for this bridge">>, {<<"Cannot delete bridge while active rules are defined for this bridge">>,
RuleIds} RuleIds}
); );
{error, timeout} -> {error, timeout} ->
{503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; ?SERVICE_UNAVAILABLE(<<"request timeout">>);
{error, Reason} -> {error, Reason} ->
{500, error_msg('INTERNAL_ERROR', Reason)} ?INTERNAL_ERROR(Reason)
end; end;
{error, not_found} -> {error, not_found} ->
?BRIDGE_NOT_FOUND(BridgeType, BridgeName) ?BRIDGE_NOT_FOUND(BridgeType, BridgeName)
@ -538,7 +551,7 @@ schema("/bridges_probe") ->
ok = emqx_bridge_resource:reset_metrics( ok = emqx_bridge_resource:reset_metrics(
emqx_bridge_resource:resource_id(BridgeType, BridgeName) emqx_bridge_resource:resource_id(BridgeType, BridgeName)
), ),
{204} ?NO_CONTENT
end end
). ).
@ -549,9 +562,9 @@ schema("/bridges_probe") ->
Params1 = maybe_deobfuscate_bridge_probe(Params), Params1 = maybe_deobfuscate_bridge_probe(Params),
case emqx_bridge_resource:create_dry_run(ConnType, maps:remove(<<"type">>, Params1)) of case emqx_bridge_resource:create_dry_run(ConnType, maps:remove(<<"type">>, Params1)) of
ok -> ok ->
204; ?NO_CONTENT;
{error, Reason} when not is_tuple(Reason); element(1, Reason) =/= 'exit' -> {error, Reason} when not is_tuple(Reason); element(1, Reason) =/= 'exit' ->
{400, error_msg('TEST_FAILED', to_hr_reason(Reason))} ?BAD_REQUEST('TEST_FAILED', Reason)
end; end;
BadRequest -> BadRequest ->
BadRequest BadRequest
@ -585,7 +598,7 @@ do_lookup_from_all_nodes(BridgeType, BridgeName, SuccCode, FormatFun) ->
{ok, [{error, not_found} | _]} -> {ok, [{error, not_found} | _]} ->
?BRIDGE_NOT_FOUND(BridgeType, BridgeName); ?BRIDGE_NOT_FOUND(BridgeType, BridgeName);
{error, Reason} -> {error, Reason} ->
{500, error_msg('INTERNAL_ERROR', Reason)} ?INTERNAL_ERROR(Reason)
end. end.
lookup_from_local_node(BridgeType, BridgeName) -> lookup_from_local_node(BridgeType, BridgeName) ->
@ -594,6 +607,20 @@ lookup_from_local_node(BridgeType, BridgeName) ->
Error -> Error Error -> Error
end. end.
create_bridge(BridgeType, BridgeName, Conf) ->
create_or_update_bridge(BridgeType, BridgeName, Conf, 201).
update_bridge(BridgeType, BridgeName, Conf) ->
create_or_update_bridge(BridgeType, BridgeName, Conf, 200).
create_or_update_bridge(BridgeType, BridgeName, Conf, HttpStatusCode) ->
case emqx_bridge:create(BridgeType, BridgeName, Conf) of
{ok, _} ->
lookup_from_all_nodes(BridgeType, BridgeName, HttpStatusCode);
{error, #{kind := validation_error} = Reason} ->
?BAD_REQUEST(map_to_json(Reason))
end.
'/bridges/:id/enable/:enable'(put, #{bindings := #{id := Id, enable := Enable}}) -> '/bridges/:id/enable/:enable'(put, #{bindings := #{id := Id, enable := Enable}}) ->
?TRY_PARSE_ID( ?TRY_PARSE_ID(
Id, Id,
@ -603,15 +630,15 @@ lookup_from_local_node(BridgeType, BridgeName) ->
OperFunc -> OperFunc ->
case emqx_bridge:disable_enable(OperFunc, BridgeType, BridgeName) of case emqx_bridge:disable_enable(OperFunc, BridgeType, BridgeName) of
{ok, _} -> {ok, _} ->
204; ?NO_CONTENT;
{error, {pre_config_update, _, bridge_not_found}} -> {error, {pre_config_update, _, bridge_not_found}} ->
?BRIDGE_NOT_FOUND(BridgeType, BridgeName); ?BRIDGE_NOT_FOUND(BridgeType, BridgeName);
{error, {_, _, timeout}} -> {error, {_, _, timeout}} ->
{503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; ?SERVICE_UNAVAILABLE(<<"request timeout">>);
{error, timeout} -> {error, timeout} ->
{503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; ?SERVICE_UNAVAILABLE(<<"request timeout">>);
{error, Reason} -> {error, Reason} ->
{500, error_msg('INTERNAL_ERROR', Reason)} ?INTERNAL_ERROR(Reason)
end end
end end
). ).
@ -731,7 +758,7 @@ pick_bridges_by_id(Type, Name, BridgesAllNodes) ->
format_bridge_info([FirstBridge | _] = Bridges) -> format_bridge_info([FirstBridge | _] = Bridges) ->
Res = maps:without([node, metrics], FirstBridge), Res = maps:without([node, metrics], FirstBridge),
NodeStatus = collect_status(Bridges), NodeStatus = node_status(Bridges),
redact(Res#{ redact(Res#{
status => aggregate_status(NodeStatus), status => aggregate_status(NodeStatus),
node_status => NodeStatus node_status => NodeStatus
@ -744,8 +771,8 @@ format_bridge_metrics(Bridges) ->
node_metrics => NodeMetrics node_metrics => NodeMetrics
}. }.
collect_status(Bridges) -> node_status(Bridges) ->
[maps:with([node, status], B) || B <- Bridges]. [maps:with([node, status, status_reason], B) || B <- Bridges].
aggregate_status(AllStatus) -> aggregate_status(AllStatus) ->
Head = fun([A | _]) -> A end, Head = fun([A | _]) -> A end,
@ -816,52 +843,63 @@ format_resource(
) )
). ).
format_resource_data(#{status := Status, metrics := Metrics}) -> format_resource_data(ResData) ->
#{status => Status, metrics => format_metrics(Metrics)}; maps:fold(fun format_resource_data/3, #{}, maps:with([status, metrics, error], ResData)).
format_resource_data(#{status := Status}) ->
#{status => Status}.
format_metrics(#{ format_resource_data(error, undefined, Result) ->
counters := #{ Result;
'dropped' := Dropped, format_resource_data(error, Error, Result) ->
'dropped.other' := DroppedOther, Result#{status_reason => emqx_misc:readable_error_msg(Error)};
'dropped.expired' := DroppedExpired, format_resource_data(
'dropped.queue_full' := DroppedQueueFull, metrics,
'dropped.resource_not_found' := DroppedResourceNotFound, #{
'dropped.resource_stopped' := DroppedResourceStopped, counters := #{
'matched' := Matched, 'dropped' := Dropped,
'retried' := Retried, 'dropped.other' := DroppedOther,
'late_reply' := LateReply, 'dropped.expired' := DroppedExpired,
'failed' := SentFailed, 'dropped.queue_full' := DroppedQueueFull,
'success' := SentSucc, 'dropped.resource_not_found' := DroppedResourceNotFound,
'received' := Rcvd 'dropped.resource_stopped' := DroppedResourceStopped,
'matched' := Matched,
'retried' := Retried,
'late_reply' := LateReply,
'failed' := SentFailed,
'success' := SentSucc,
'received' := Rcvd
},
gauges := Gauges,
rate := #{
matched := #{current := Rate, last5m := Rate5m, max := RateMax}
}
}, },
gauges := Gauges, Result
rate := #{ ) ->
matched := #{current := Rate, last5m := Rate5m, max := RateMax}
}
}) ->
Queued = maps:get('queuing', Gauges, 0), Queued = maps:get('queuing', Gauges, 0),
SentInflight = maps:get('inflight', Gauges, 0), SentInflight = maps:get('inflight', Gauges, 0),
?METRICS( Result#{
Dropped, metrics =>
DroppedOther, ?METRICS(
DroppedExpired, Dropped,
DroppedQueueFull, DroppedOther,
DroppedResourceNotFound, DroppedExpired,
DroppedResourceStopped, DroppedQueueFull,
Matched, DroppedResourceNotFound,
Queued, DroppedResourceStopped,
Retried, Matched,
LateReply, Queued,
SentFailed, Retried,
SentInflight, LateReply,
SentSucc, SentFailed,
Rate, SentInflight,
Rate5m, SentSucc,
RateMax, Rate,
Rcvd Rate5m,
). RateMax,
Rcvd
)
};
format_resource_data(K, V, Result) ->
Result#{K => V}.
fill_defaults(Type, RawConf) -> fill_defaults(Type, RawConf) ->
PackedConf = pack_bridge_conf(Type, RawConf), PackedConf = pack_bridge_conf(Type, RawConf),
@ -903,6 +941,7 @@ filter_out_request_body(Conf) ->
<<"type">>, <<"type">>,
<<"name">>, <<"name">>,
<<"status">>, <<"status">>,
<<"status_reason">>,
<<"node_status">>, <<"node_status">>,
<<"node_metrics">>, <<"node_metrics">>,
<<"metrics">>, <<"metrics">>,
@ -910,9 +949,6 @@ filter_out_request_body(Conf) ->
], ],
maps:without(ExtraConfs, Conf). maps:without(ExtraConfs, Conf).
error_msg(Code, Msg) ->
#{code => Code, message => emqx_misc:readable_error_msg(Msg)}.
bin(S) when is_list(S) -> bin(S) when is_list(S) ->
list_to_binary(S); list_to_binary(S);
bin(S) when is_atom(S) -> bin(S) when is_atom(S) ->
@ -923,30 +959,31 @@ bin(S) when is_binary(S) ->
call_operation(NodeOrAll, OperFunc, Args = [_Nodes, BridgeType, BridgeName]) -> call_operation(NodeOrAll, OperFunc, Args = [_Nodes, BridgeType, BridgeName]) ->
case is_ok(do_bpapi_call(NodeOrAll, OperFunc, Args)) of case is_ok(do_bpapi_call(NodeOrAll, OperFunc, Args)) of
Ok when Ok =:= ok; is_tuple(Ok), element(1, Ok) =:= ok -> Ok when Ok =:= ok; is_tuple(Ok), element(1, Ok) =:= ok ->
204; ?NO_CONTENT;
{error, not_implemented} -> {error, not_implemented} ->
%% Should only happen if we call `start` on a node that is %% Should only happen if we call `start` on a node that is
%% still on an older bpapi version that doesn't support it. %% still on an older bpapi version that doesn't support it.
maybe_try_restart(NodeOrAll, OperFunc, Args); maybe_try_restart(NodeOrAll, OperFunc, Args);
{error, timeout} -> {error, timeout} ->
{503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; ?SERVICE_UNAVAILABLE(<<"Request timeout">>);
{error, {start_pool_failed, Name, Reason}} -> {error, {start_pool_failed, Name, Reason}} ->
{503, ?SERVICE_UNAVAILABLE(
error_msg( bin(io_lib:format("Failed to start ~p pool for reason ~p", [Name, Reason]))
'SERVICE_UNAVAILABLE', );
bin(
io_lib:format(
"failed to start ~p pool for reason ~p",
[Name, Reason]
)
)
)};
{error, not_found} -> {error, not_found} ->
?BRIDGE_NOT_FOUND(BridgeType, BridgeName); BridgeId = emqx_bridge_resource:bridge_id(BridgeType, BridgeName),
?SLOG(warning, #{
msg => "bridge_inconsistent_in_cluster_for_call_operation",
reason => not_found,
type => BridgeType,
name => BridgeName,
bridge => BridgeId
}),
?SERVICE_UNAVAILABLE(<<"Bridge not found on remote node: ", BridgeId/binary>>);
{error, {node_not_found, Node}} -> {error, {node_not_found, Node}} ->
?NOT_FOUND(<<"Node not found: ", (atom_to_binary(Node))/binary>>); ?NOT_FOUND(<<"Node not found: ", (atom_to_binary(Node))/binary>>);
{error, Reason} when not is_tuple(Reason); element(1, Reason) =/= 'exit' -> {error, Reason} when not is_tuple(Reason); element(1, Reason) =/= 'exit' ->
?BAD_REQUEST(to_hr_reason(Reason)) ?BAD_REQUEST(Reason)
end. end.
maybe_try_restart(all, start_bridges_to_all_nodes, Args) -> maybe_try_restart(all, start_bridges_to_all_nodes, Args) ->
@ -954,7 +991,7 @@ maybe_try_restart(all, start_bridges_to_all_nodes, Args) ->
maybe_try_restart(Node, start_bridge_to_node, Args) -> maybe_try_restart(Node, start_bridge_to_node, Args) ->
call_operation(Node, restart_bridge_to_node, Args); call_operation(Node, restart_bridge_to_node, Args);
maybe_try_restart(_, _, _) -> maybe_try_restart(_, _, _) ->
501. ?NOT_IMPLEMENTED.
do_bpapi_call(all, Call, Args) -> do_bpapi_call(all, Call, Args) ->
maybe_unwrap( maybe_unwrap(
@ -985,19 +1022,6 @@ supported_versions(start_bridge_to_node) -> [2, 3];
supported_versions(start_bridges_to_all_nodes) -> [2, 3]; supported_versions(start_bridges_to_all_nodes) -> [2, 3];
supported_versions(_Call) -> [1, 2, 3]. supported_versions(_Call) -> [1, 2, 3].
to_hr_reason(nxdomain) ->
<<"Host not found">>;
to_hr_reason(econnrefused) ->
<<"Connection refused">>;
to_hr_reason({unauthorized_client, _}) ->
<<"Unauthorized client">>;
to_hr_reason({not_authorized, _}) ->
<<"Not authorized">>;
to_hr_reason({malformed_username_or_password, _}) ->
<<"Malformed username or password">>;
to_hr_reason(Reason) ->
Reason.
redact(Term) -> redact(Term) ->
emqx_misc:redact(Term). emqx_misc:redact(Term).
@ -1021,3 +1045,8 @@ deobfuscate(NewConf, OldConf) ->
#{}, #{},
NewConf NewConf
). ).
map_to_json(M) ->
emqx_json:encode(
emqx_map_lib:jsonable_map(M, fun(K, V) -> {K, emqx_map_lib:binary_string(V)} end)
).

View File

@ -106,6 +106,12 @@ common_bridge_fields() ->
status_fields() -> status_fields() ->
[ [
{"status", mk(status(), #{desc => ?DESC("desc_status")})}, {"status", mk(status(), #{desc => ?DESC("desc_status")})},
{"status_reason",
mk(binary(), #{
required => false,
desc => ?DESC("desc_status_reason"),
example => <<"Connection refused">>
})},
{"node_status", {"node_status",
mk( mk(
hoconsc:array(ref(?MODULE, "node_status")), hoconsc:array(ref(?MODULE, "node_status")),
@ -190,7 +196,13 @@ fields("node_metrics") ->
fields("node_status") -> fields("node_status") ->
[ [
node_name(), node_name(),
{"status", mk(status(), #{})} {"status", mk(status(), #{})},
{"status_reason",
mk(binary(), #{
required => false,
desc => ?DESC("desc_status_reason"),
example => <<"Connection refused">>
})}
]. ].
desc(bridges) -> desc(bridges) ->

View File

@ -23,7 +23,7 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-define(CONF_DEFAULT, <<"bridges: {}">>). -define(CONF_DEFAULT, <<"bridges: {}">>).
-define(BRIDGE_TYPE, <<"webhook">>). -define(BRIDGE_TYPE_HTTP, <<"webhook">>).
-define(BRIDGE_NAME, (atom_to_binary(?FUNCTION_NAME))). -define(BRIDGE_NAME, (atom_to_binary(?FUNCTION_NAME))).
-define(URL(PORT, PATH), -define(URL(PORT, PATH),
list_to_binary( list_to_binary(
@ -48,7 +48,7 @@
}). }).
-define(MQTT_BRIDGE(SERVER), ?MQTT_BRIDGE(SERVER, <<"mqtt_egress_test_bridge">>)). -define(MQTT_BRIDGE(SERVER), ?MQTT_BRIDGE(SERVER, <<"mqtt_egress_test_bridge">>)).
-define(HTTP_BRIDGE(URL, TYPE, NAME), ?BRIDGE(NAME, TYPE)#{ -define(HTTP_BRIDGE(URL, NAME), ?BRIDGE(NAME, ?BRIDGE_TYPE_HTTP)#{
<<"url">> => URL, <<"url">> => URL,
<<"local_topic">> => <<"emqx_webhook/#">>, <<"local_topic">> => <<"emqx_webhook/#">>,
<<"method">> => <<"post">>, <<"method">> => <<"post">>,
@ -57,6 +57,7 @@
<<"content-type">> => <<"application/json">> <<"content-type">> => <<"application/json">>
} }
}). }).
-define(HTTP_BRIDGE(URL), ?HTTP_BRIDGE(URL, ?BRIDGE_NAME)).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
@ -97,6 +98,20 @@ init_per_testcase(t_old_bpapi_vsn, Config) ->
meck:expect(emqx_bpapi, supported_version, 1, 1), meck:expect(emqx_bpapi, supported_version, 1, 1),
meck:expect(emqx_bpapi, supported_version, 2, 1), meck:expect(emqx_bpapi, supported_version, 2, 1),
init_per_testcase(common, Config); init_per_testcase(common, Config);
init_per_testcase(StartStop, Config) when
StartStop == t_start_stop_bridges_cluster;
StartStop == t_start_stop_bridges_node
->
meck:new(emqx_bridge_resource, [passthrough]),
meck:expect(
emqx_bridge_resource,
stop,
fun
(_, <<"bridge_not_found">>) -> {error, not_found};
(Type, Name) -> meck:passthrough([Type, Name])
end
),
init_per_testcase(common, Config);
init_per_testcase(_, Config) -> init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
{Port, Sock, Acceptor} = start_http_server(fun handle_fun_200_ok/2), {Port, Sock, Acceptor} = start_http_server(fun handle_fun_200_ok/2),
@ -108,6 +123,12 @@ end_per_testcase(t_broken_bpapi_vsn, Config) ->
end_per_testcase(t_old_bpapi_vsn, Config) -> end_per_testcase(t_old_bpapi_vsn, Config) ->
meck:unload([emqx_bpapi]), meck:unload([emqx_bpapi]),
end_per_testcase(common, Config); end_per_testcase(common, Config);
end_per_testcase(StartStop, Config) when
StartStop == t_start_stop_bridges_cluster;
StartStop == t_start_stop_bridges_node
->
meck:unload([emqx_bridge_resource]),
end_per_testcase(common, Config);
end_per_testcase(_, Config) -> end_per_testcase(_, Config) ->
Sock = ?config(sock, Config), Sock = ?config(sock, Config),
Acceptor = ?config(acceptor, Config), Acceptor = ?config(acceptor, Config),
@ -206,12 +227,12 @@ t_http_crud_apis(Config) ->
{ok, 201, Bridge} = request( {ok, 201, Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL1, Name)
), ),
%ct:pal("---bridge: ~p", [Bridge]), %ct:pal("---bridge: ~p", [Bridge]),
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE_HTTP,
<<"name">> := Name, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := _, <<"status">> := _,
@ -219,7 +240,7 @@ t_http_crud_apis(Config) ->
<<"url">> := URL1 <<"url">> := URL1
} = emqx_json:decode(Bridge, [return_maps]), } = emqx_json:decode(Bridge, [return_maps]),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name),
%% send an message to emqx and the message should be forwarded to the HTTP server %% send an message to emqx and the message should be forwarded to the HTTP server
Body = <<"my msg">>, Body = <<"my msg">>,
emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)), emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)),
@ -243,11 +264,11 @@ t_http_crud_apis(Config) ->
{ok, 200, Bridge2} = request( {ok, 200, Bridge2} = request(
put, put,
uri(["bridges", BridgeID]), uri(["bridges", BridgeID]),
?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL2, Name)
), ),
?assertMatch( ?assertMatch(
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE_HTTP,
<<"name">> := Name, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := _, <<"status">> := _,
@ -262,7 +283,7 @@ t_http_crud_apis(Config) ->
?assertMatch( ?assertMatch(
[ [
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE_HTTP,
<<"name">> := Name, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := _, <<"status">> := _,
@ -277,7 +298,7 @@ t_http_crud_apis(Config) ->
{ok, 200, Bridge3Str} = request(get, uri(["bridges", BridgeID]), []), {ok, 200, Bridge3Str} = request(get, uri(["bridges", BridgeID]), []),
?assertMatch( ?assertMatch(
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE_HTTP,
<<"name">> := Name, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := _, <<"status">> := _,
@ -301,6 +322,33 @@ t_http_crud_apis(Config) ->
end end
), ),
%% Test bad updates
{ok, 400, PutFail1} = request(
put,
uri(["bridges", BridgeID]),
maps:remove(<<"url">>, ?HTTP_BRIDGE(URL2, Name))
),
?assertMatch(
#{<<"reason">> := <<"required_field">>},
emqx_json:decode(maps:get(<<"message">>, emqx_json:decode(PutFail1, [return_maps])), [
return_maps
])
),
{ok, 400, PutFail2} = request(
put,
uri(["bridges", BridgeID]),
maps:put(<<"curl">>, URL2, maps:remove(<<"url">>, ?HTTP_BRIDGE(URL2, Name)))
),
?assertMatch(
#{
<<"reason">> := <<"unknown_fields">>,
<<"unknown">> := <<"curl">>
},
emqx_json:decode(maps:get(<<"message">>, emqx_json:decode(PutFail2, [return_maps])), [
return_maps
])
),
%% delete the bridge %% delete the bridge
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
@ -309,7 +357,7 @@ t_http_crud_apis(Config) ->
{ok, 404, ErrMsg2} = request( {ok, 404, ErrMsg2} = request(
put, put,
uri(["bridges", BridgeID]), uri(["bridges", BridgeID]),
?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL2, Name)
), ),
?assertMatch( ?assertMatch(
#{ #{
@ -338,6 +386,37 @@ t_http_crud_apis(Config) ->
}, },
emqx_json:decode(ErrMsg3, [return_maps]) emqx_json:decode(ErrMsg3, [return_maps])
), ),
%% Create non working bridge
BrokenURL = ?URL(Port + 1, "/foo"),
{ok, 201, BrokenBridge} = request(
post,
uri(["bridges"]),
?HTTP_BRIDGE(BrokenURL, Name)
),
#{
<<"type">> := ?BRIDGE_TYPE_HTTP,
<<"name">> := Name,
<<"enable">> := true,
<<"status">> := <<"disconnected">>,
<<"status_reason">> := <<"Connection refused">>,
<<"node_status">> := [
#{<<"status">> := <<"disconnected">>, <<"status_reason">> := <<"Connection refused">>}
| _
],
<<"url">> := BrokenURL
} = emqx_json:decode(BrokenBridge, [return_maps]),
{ok, 200, FixedBridgeResponse} = request(put, uri(["bridges", BridgeID]), ?HTTP_BRIDGE(URL1)),
#{
<<"status">> := <<"connected">>,
<<"node_status">> := [FixedNodeStatus = #{<<"status">> := <<"connected">>} | _]
} = FixedBridge = emqx_json:decode(FixedBridgeResponse, [return_maps]),
?assert(not maps:is_key(<<"status_reason">>, FixedBridge)),
?assert(not maps:is_key(<<"status_reason">>, FixedNodeStatus)),
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
%% Try create bridge with bad characters as name
{ok, 400, _} = request(post, uri(["bridges"]), ?HTTP_BRIDGE(URL1, <<"隋达"/utf8>>)),
ok. ok.
t_http_bridges_local_topic(Config) -> t_http_bridges_local_topic(Config) ->
@ -354,16 +433,16 @@ t_http_bridges_local_topic(Config) ->
{ok, 201, _} = request( {ok, 201, _} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name1) ?HTTP_BRIDGE(URL1, Name1)
), ),
%% and we create another one without local_topic %% and we create another one without local_topic
{ok, 201, _} = request( {ok, 201, _} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
maps:remove(<<"local_topic">>, ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name2)) maps:remove(<<"local_topic">>, ?HTTP_BRIDGE(URL1, Name2))
), ),
BridgeID1 = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name1), BridgeID1 = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name1),
BridgeID2 = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name2), BridgeID2 = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name2),
%% Send an message to emqx and the message should be forwarded to the HTTP server. %% Send an message to emqx and the message should be forwarded to the HTTP server.
%% This is to verify we can have 2 bridges with and without local_topic fields %% This is to verify we can have 2 bridges with and without local_topic fields
%% at the same time. %% at the same time.
@ -398,11 +477,11 @@ t_check_dependent_actions_on_delete(Config) ->
%% POST /bridges/ will create a bridge %% POST /bridges/ will create a bridge
URL1 = ?URL(Port, "path1"), URL1 = ?URL(Port, "path1"),
Name = <<"t_http_crud_apis">>, Name = <<"t_http_crud_apis">>,
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name),
{ok, 201, _} = request( {ok, 201, _} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL1, Name)
), ),
{ok, 201, Rule} = request( {ok, 201, Rule} = request(
post, post,
@ -436,11 +515,11 @@ t_cascade_delete_actions(Config) ->
%% POST /bridges/ will create a bridge %% POST /bridges/ will create a bridge
URL1 = ?URL(Port, "path1"), URL1 = ?URL(Port, "path1"),
Name = <<"t_http_crud_apis">>, Name = <<"t_http_crud_apis">>,
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name),
{ok, 201, _} = request( {ok, 201, _} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL1, Name)
), ),
{ok, 201, Rule} = request( {ok, 201, Rule} = request(
post, post,
@ -470,7 +549,7 @@ t_cascade_delete_actions(Config) ->
{ok, 201, _} = request( {ok, 201, _} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL1, Name)
), ),
{ok, 201, _} = request( {ok, 201, _} = request(
post, post,
@ -494,9 +573,9 @@ t_broken_bpapi_vsn(Config) ->
{ok, 201, _Bridge} = request( {ok, 201, _Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL1, Name)
), ),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name),
%% still works since we redirect to 'restart' %% still works since we redirect to 'restart'
{ok, 501, <<>>} = request(post, operation_path(cluster, start, BridgeID), <<"">>), {ok, 501, <<>>} = request(post, operation_path(cluster, start, BridgeID), <<"">>),
{ok, 501, <<>>} = request(post, operation_path(node, start, BridgeID), <<"">>), {ok, 501, <<>>} = request(post, operation_path(node, start, BridgeID), <<"">>),
@ -509,9 +588,9 @@ t_old_bpapi_vsn(Config) ->
{ok, 201, _Bridge} = request( {ok, 201, _Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL1, Name)
), ),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name),
{ok, 204, <<>>} = request(post, operation_path(cluster, stop, BridgeID), <<"">>), {ok, 204, <<>>} = request(post, operation_path(cluster, stop, BridgeID), <<"">>),
{ok, 204, <<>>} = request(post, operation_path(node, stop, BridgeID), <<"">>), {ok, 204, <<>>} = request(post, operation_path(node, stop, BridgeID), <<"">>),
%% still works since we redirect to 'restart' %% still works since we redirect to 'restart'
@ -549,18 +628,18 @@ do_start_stop_bridges(Type, Config) ->
{ok, 201, Bridge} = request( {ok, 201, Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL1, Name)
), ),
%ct:pal("the bridge ==== ~p", [Bridge]), %ct:pal("the bridge ==== ~p", [Bridge]),
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE_HTTP,
<<"name">> := Name, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := <<"connected">>, <<"status">> := <<"connected">>,
<<"node_status">> := [_ | _], <<"node_status">> := [_ | _],
<<"url">> := URL1 <<"url">> := URL1
} = emqx_json:decode(Bridge, [return_maps]), } = emqx_json:decode(Bridge, [return_maps]),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name),
%% stop it %% stop it
{ok, 204, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>), {ok, 204, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>),
{ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []), {ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []),
@ -595,6 +674,16 @@ do_start_stop_bridges(Type, Config) ->
%% Looks ok but doesn't exist %% Looks ok but doesn't exist
{ok, 404, _} = request(post, operation_path(Type, start, <<"webhook:cptn_hook">>), <<"">>), {ok, 404, _} = request(post, operation_path(Type, start, <<"webhook:cptn_hook">>), <<"">>),
%%
{ok, 201, _Bridge} = request(
post,
uri(["bridges"]),
?HTTP_BRIDGE(URL1, <<"bridge_not_found">>)
),
{ok, 503, _} = request(
post, operation_path(Type, stop, <<"webhook:bridge_not_found">>), <<"">>
),
%% Create broken bridge %% Create broken bridge
{ListenPort, Sock} = listen_on_random_port(), {ListenPort, Sock} = listen_on_random_port(),
%% Connecting to this endpoint should always timeout %% Connecting to this endpoint should always timeout
@ -631,18 +720,18 @@ t_enable_disable_bridges(Config) ->
{ok, 201, Bridge} = request( {ok, 201, Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL1, Name)
), ),
%ct:pal("the bridge ==== ~p", [Bridge]), %ct:pal("the bridge ==== ~p", [Bridge]),
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE_HTTP,
<<"name">> := Name, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := <<"connected">>, <<"status">> := <<"connected">>,
<<"node_status">> := [_ | _], <<"node_status">> := [_ | _],
<<"url">> := URL1 <<"url">> := URL1
} = emqx_json:decode(Bridge, [return_maps]), } = emqx_json:decode(Bridge, [return_maps]),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name),
%% disable it %% disable it
{ok, 204, <<>>} = request(put, enable_path(false, BridgeID), <<"">>), {ok, 204, <<>>} = request(put, enable_path(false, BridgeID), <<"">>),
{ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []), {ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []),
@ -688,18 +777,18 @@ t_reset_bridges(Config) ->
{ok, 201, Bridge} = request( {ok, 201, Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL1, Name)
), ),
%ct:pal("the bridge ==== ~p", [Bridge]), %ct:pal("the bridge ==== ~p", [Bridge]),
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE_HTTP,
<<"name">> := Name, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := <<"connected">>, <<"status">> := <<"connected">>,
<<"node_status">> := [_ | _], <<"node_status">> := [_ | _],
<<"url">> := URL1 <<"url">> := URL1
} = emqx_json:decode(Bridge, [return_maps]), } = emqx_json:decode(Bridge, [return_maps]),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name),
{ok, 204, <<>>} = request(put, uri(["bridges", BridgeID, "metrics/reset"]), []), {ok, 204, <<>>} = request(put, uri(["bridges", BridgeID, "metrics/reset"]), []),
%% delete the bridge %% delete the bridge
@ -746,20 +835,20 @@ t_bridges_probe(Config) ->
{ok, 204, <<>>} = request( {ok, 204, <<>>} = request(
post, post,
uri(["bridges_probe"]), uri(["bridges_probe"]),
?HTTP_BRIDGE(URL, ?BRIDGE_TYPE, ?BRIDGE_NAME) ?HTTP_BRIDGE(URL)
), ),
%% second time with same name is ok since no real bridge created %% second time with same name is ok since no real bridge created
{ok, 204, <<>>} = request( {ok, 204, <<>>} = request(
post, post,
uri(["bridges_probe"]), uri(["bridges_probe"]),
?HTTP_BRIDGE(URL, ?BRIDGE_TYPE, ?BRIDGE_NAME) ?HTTP_BRIDGE(URL)
), ),
{ok, 400, NxDomain} = request( {ok, 400, NxDomain} = request(
post, post,
uri(["bridges_probe"]), uri(["bridges_probe"]),
?HTTP_BRIDGE(<<"http://203.0.113.3:1234/foo">>, ?BRIDGE_TYPE, ?BRIDGE_NAME) ?HTTP_BRIDGE(<<"http://203.0.113.3:1234/foo">>)
), ),
?assertMatch( ?assertMatch(
#{ #{
@ -788,7 +877,7 @@ t_bridges_probe(Config) ->
emqx_json:decode(ConnRefused, [return_maps]) emqx_json:decode(ConnRefused, [return_maps])
), ),
{ok, 400, HostNotFound} = request( {ok, 400, CouldNotResolveHost} = request(
post, post,
uri(["bridges_probe"]), uri(["bridges_probe"]),
?MQTT_BRIDGE(<<"nohost:2883">>) ?MQTT_BRIDGE(<<"nohost:2883">>)
@ -796,9 +885,9 @@ t_bridges_probe(Config) ->
?assertMatch( ?assertMatch(
#{ #{
<<"code">> := <<"TEST_FAILED">>, <<"code">> := <<"TEST_FAILED">>,
<<"message">> := <<"Host not found">> <<"message">> := <<"Could not resolve host">>
}, },
emqx_json:decode(HostNotFound, [return_maps]) emqx_json:decode(CouldNotResolveHost, [return_maps])
), ),
AuthnConfig = #{ AuthnConfig = #{
@ -842,7 +931,7 @@ t_bridges_probe(Config) ->
?assertMatch( ?assertMatch(
#{ #{
<<"code">> := <<"TEST_FAILED">>, <<"code">> := <<"TEST_FAILED">>,
<<"message">> := <<"Malformed username or password">> <<"message">> := <<"Bad username or password">>
}, },
emqx_json:decode(Malformed, [return_maps]) emqx_json:decode(Malformed, [return_maps])
), ),
@ -880,13 +969,13 @@ t_metrics(Config) ->
{ok, 201, Bridge} = request( {ok, 201, Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ?HTTP_BRIDGE(URL1, Name)
), ),
%ct:pal("---bridge: ~p", [Bridge]), %ct:pal("---bridge: ~p", [Bridge]),
Decoded = emqx_json:decode(Bridge, [return_maps]), Decoded = emqx_json:decode(Bridge, [return_maps]),
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE_HTTP,
<<"name">> := Name, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := _, <<"status">> := _,
@ -898,7 +987,7 @@ t_metrics(Config) ->
?assertNot(maps:is_key(<<"metrics">>, Decoded)), ?assertNot(maps:is_key(<<"metrics">>, Decoded)),
?assertNot(maps:is_key(<<"node_metrics">>, Decoded)), ?assertNot(maps:is_key(<<"node_metrics">>, Decoded)),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name),
%% check for empty bridge metrics %% check for empty bridge metrics
{ok, 200, Bridge1Str} = request(get, uri(["bridges", BridgeID, "metrics"]), []), {ok, 200, Bridge1Str} = request(get, uri(["bridges", BridgeID, "metrics"]), []),
@ -963,7 +1052,7 @@ t_inconsistent_webhook_request_timeouts(Config) ->
Name = ?BRIDGE_NAME, Name = ?BRIDGE_NAME,
BadBridgeParams = BadBridgeParams =
emqx_map_lib:deep_merge( emqx_map_lib:deep_merge(
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name), ?HTTP_BRIDGE(URL1, Name),
#{ #{
<<"request_timeout">> => <<"1s">>, <<"request_timeout">> => <<"1s">>,
<<"resource_opts">> => #{<<"request_timeout">> => <<"2s">>} <<"resource_opts">> => #{<<"request_timeout">> => <<"2s">>}

View File

@ -1,4 +1,41 @@
emqx_ctl # emqx_ctl
=====
Backend module for `emqx_ctl` command. This application accepts dynamic `emqx ctl` command registrations so plugins can add their own commands.
Please note that the 'proxy' command `emqx_ctl` is considered deprecated, going forward, please use `emqx ctl` instead.
## Add a new command
To add a new command, the application must implement a callback function to handle the command, and register the command with `emqx_ctl:register_command/2` API.
### Register
To add a new command which can be executed from `emqx ctl`, the application must call `emqx_ctl:register_command/2` API to register the command.
For example, to add a new command `myplugin` which is to be executed as `emqx ctl myplugin`, the application must call `emqx_ctl:register_command/2` API as follows:
```erlang
emqx_ctl:register_command(mypluin, {myplugin_cli, cmd}).
```
### Callback
The callback function must be exported by the application and must have the following signature:
```erlang
cmd([Arg1, Arg2, ...]) -> ok.
```
It must also implement a special clause to handle the `usage` argument:
```erlang
cmd([usage]) -> "myplugin [arg1] [arg2] ...";
```
### Utility
The `emqx_ctl` application provides some utility functions which help to format the output of the command.
For example `emqx_ctl:print/2` and `emqx_ctl:usage/1`.
## Reference
[emqx_management_cli](../emqx_management/src/emqx_mgmt_cli.erl) can be taken as a reference for how to implement a command.

View File

@ -74,7 +74,7 @@ schema("/login") ->
post => #{ post => #{
tags => [<<"dashboard">>], tags => [<<"dashboard">>],
desc => ?DESC(login_api), desc => ?DESC(login_api),
summary => <<"Dashboard Auth">>, summary => <<"Dashboard authentication">>,
'requestBody' => fields([username, password]), 'requestBody' => fields([username, password]),
responses => #{ responses => #{
200 => fields([token, version, license]), 200 => fields([token, version, license]),

View File

@ -457,7 +457,18 @@ trans_description(Spec, Hocon) ->
Spec; Spec;
Desc -> Desc ->
Desc1 = binary:replace(Desc, [<<"\n">>], <<"<br/>">>, [global]), Desc1 = binary:replace(Desc, [<<"\n">>], <<"<br/>">>, [global]),
Spec#{description => Desc1} maybe_add_summary_from_label(Spec#{description => Desc1}, Hocon)
end.
maybe_add_summary_from_label(Spec, Hocon) ->
Label =
case desc_struct(Hocon) of
?DESC(_, _) = Struct -> get_i18n(<<"label">>, Struct, undefined);
_ -> undefined
end,
case Label of
undefined -> Spec;
_ -> Spec#{summary => Label}
end. end.
get_i18n(Key, Struct, Default) -> get_i18n(Key, Struct, Default) ->

View File

@ -56,14 +56,6 @@
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). 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) -> init_per_suite(Config) ->
emqx_common_test_helpers:start_apps( emqx_common_test_helpers:start_apps(
[emqx_management, emqx_dashboard], [emqx_management, emqx_dashboard],
@ -72,6 +64,7 @@ init_per_suite(Config) ->
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->
mnesia:clear_table(?ADMIN),
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_management]), emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_management]),
mria:stop(). mria:stop().

View File

@ -33,10 +33,12 @@ all() ->
init_per_suite(Config) -> init_per_suite(Config) ->
application:load(emqx_dashboard), application:load(emqx_dashboard),
mria:start(), mria:start(),
mnesia:clear_table(?ADMIN),
emqx_common_test_helpers:start_apps([emqx_dashboard], fun set_special_configs/1), emqx_common_test_helpers:start_apps([emqx_dashboard], fun set_special_configs/1),
Config. Config.
end_per_suite(Config) -> end_per_suite(Config) ->
mnesia:clear_table(?ADMIN),
emqx_common_test_helpers:stop_apps([emqx_dashboard]), emqx_common_test_helpers:stop_apps([emqx_dashboard]),
Config. Config.

View File

@ -49,6 +49,10 @@ NOTE: The position should be \"front | rear | before:{name} | after:{name}"""
zh: """移动 Exhook 服务器顺序。 zh: """移动 Exhook 服务器顺序。
注意: 移动的参数只能是front | rear | before:{name} | after:{name}""" 注意: 移动的参数只能是front | rear | before:{name} | after:{name}"""
} }
label {
en: "Change order of execution for registered Exhook server"
zh: "改变已注册的Exhook服务器的执行顺序"
}
} }
move_position { move_position {

View File

@ -180,7 +180,7 @@ schema("/gateways") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(list_gateway), desc => ?DESC(list_gateway),
summary => <<"List All Gateways">>, summary => <<"List all gateways">>,
parameters => params_gateway_status_in_qs(), parameters => params_gateway_status_in_qs(),
responses => responses =>
#{ #{
@ -201,7 +201,7 @@ schema("/gateways/:name") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(get_gateway), desc => ?DESC(get_gateway),
summary => <<"Get the Gateway">>, summary => <<"Get gateway">>,
parameters => params_gateway_name_in_path(), parameters => params_gateway_name_in_path(),
responses => responses =>
#{ #{
@ -608,7 +608,7 @@ examples_gateway_confs() ->
#{ #{
stomp_gateway => stomp_gateway =>
#{ #{
summary => <<"A simple STOMP gateway configs">>, summary => <<"A simple STOMP gateway config">>,
value => value =>
#{ #{
enable => true, enable => true,
@ -636,7 +636,7 @@ examples_gateway_confs() ->
}, },
mqttsn_gateway => mqttsn_gateway =>
#{ #{
summary => <<"A simple MQTT-SN gateway configs">>, summary => <<"A simple MQTT-SN gateway config">>,
value => value =>
#{ #{
enable => true, enable => true,
@ -672,7 +672,7 @@ examples_gateway_confs() ->
}, },
coap_gateway => coap_gateway =>
#{ #{
summary => <<"A simple CoAP gateway configs">>, summary => <<"A simple CoAP gateway config">>,
value => value =>
#{ #{
enable => true, enable => true,
@ -699,7 +699,7 @@ examples_gateway_confs() ->
}, },
lwm2m_gateway => lwm2m_gateway =>
#{ #{
summary => <<"A simple LwM2M gateway configs">>, summary => <<"A simple LwM2M gateway config">>,
value => value =>
#{ #{
enable => true, enable => true,
@ -735,7 +735,7 @@ examples_gateway_confs() ->
}, },
exproto_gateway => exproto_gateway =>
#{ #{
summary => <<"A simple ExProto gateway configs">>, summary => <<"A simple ExProto gateway config">>,
value => value =>
#{ #{
enable => true, enable => true,
@ -765,7 +765,7 @@ examples_update_gateway_confs() ->
#{ #{
stomp_gateway => stomp_gateway =>
#{ #{
summary => <<"A simple STOMP gateway configs">>, summary => <<"A simple STOMP gateway config">>,
value => value =>
#{ #{
enable => true, enable => true,
@ -782,7 +782,7 @@ examples_update_gateway_confs() ->
}, },
mqttsn_gateway => mqttsn_gateway =>
#{ #{
summary => <<"A simple MQTT-SN gateway configs">>, summary => <<"A simple MQTT-SN gateway config">>,
value => value =>
#{ #{
enable => true, enable => true,
@ -803,7 +803,7 @@ examples_update_gateway_confs() ->
}, },
coap_gateway => coap_gateway =>
#{ #{
summary => <<"A simple CoAP gateway configs">>, summary => <<"A simple CoAP gateway config">>,
value => value =>
#{ #{
enable => true, enable => true,
@ -819,7 +819,7 @@ examples_update_gateway_confs() ->
}, },
lwm2m_gateway => lwm2m_gateway =>
#{ #{
summary => <<"A simple LwM2M gateway configs">>, summary => <<"A simple LwM2M gateway config">>,
value => value =>
#{ #{
enable => true, enable => true,
@ -844,7 +844,7 @@ examples_update_gateway_confs() ->
}, },
exproto_gateway => exproto_gateway =>
#{ #{
summary => <<"A simple ExProto gateway configs">>, summary => <<"A simple ExProto gateway config">>,
value => value =>
#{ #{
enable => true, enable => true,

View File

@ -185,13 +185,13 @@ schema("/gateways/:name/authentication") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(get_authn), desc => ?DESC(get_authn),
summary => <<"Get Authenticator Configuration">>, summary => <<"Get authenticator configuration">>,
parameters => params_gateway_name_in_path(), parameters => params_gateway_name_in_path(),
responses => responses =>
?STANDARD_RESP( ?STANDARD_RESP(
#{ #{
200 => schema_authn(), 200 => schema_authn(),
204 => <<"Authenticator doesn't initiated">> 204 => <<"Authenticator not initialized">>
} }
) )
}, },
@ -199,7 +199,7 @@ schema("/gateways/:name/authentication") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(update_authn), desc => ?DESC(update_authn),
summary => <<"Update Authenticator Configuration">>, summary => <<"Update authenticator configuration">>,
parameters => params_gateway_name_in_path(), parameters => params_gateway_name_in_path(),
'requestBody' => schema_authn(), 'requestBody' => schema_authn(),
responses => responses =>
@ -209,7 +209,7 @@ schema("/gateways/:name/authentication") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(add_authn), desc => ?DESC(add_authn),
summary => <<"Create an Authenticator for a Gateway">>, summary => <<"Create authenticator for gateway">>,
parameters => params_gateway_name_in_path(), parameters => params_gateway_name_in_path(),
'requestBody' => schema_authn(), 'requestBody' => schema_authn(),
responses => responses =>
@ -219,7 +219,7 @@ schema("/gateways/:name/authentication") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(delete_authn), desc => ?DESC(delete_authn),
summary => <<"Delete the Gateway Authenticator">>, summary => <<"Delete gateway authenticator">>,
parameters => params_gateway_name_in_path(), parameters => params_gateway_name_in_path(),
responses => responses =>
?STANDARD_RESP(#{204 => <<"Deleted">>}) ?STANDARD_RESP(#{204 => <<"Deleted">>})
@ -232,7 +232,7 @@ schema("/gateways/:name/authentication/users") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(list_users), desc => ?DESC(list_users),
summary => <<"List users for a Gateway Authenticator">>, summary => <<"List users for gateway authenticator">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_paging_in_qs() ++ params_paging_in_qs() ++
params_fuzzy_in_qs(), params_fuzzy_in_qs(),
@ -250,7 +250,7 @@ schema("/gateways/:name/authentication/users") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(add_user), desc => ?DESC(add_user),
summary => <<"Add User for a Gateway Authenticator">>, summary => <<"Add user for gateway authenticator">>,
parameters => params_gateway_name_in_path(), parameters => params_gateway_name_in_path(),
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
ref(emqx_authn_api, request_user_create), ref(emqx_authn_api, request_user_create),
@ -274,7 +274,7 @@ schema("/gateways/:name/authentication/users/:uid") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(get_user), desc => ?DESC(get_user),
summary => <<"Get User Info for a Gateway Authenticator">>, summary => <<"Get user info for gateway authenticator">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_userid_in_path(), params_userid_in_path(),
responses => responses =>
@ -291,7 +291,7 @@ schema("/gateways/:name/authentication/users/:uid") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(update_user), desc => ?DESC(update_user),
summary => <<"Update User Info for a Gateway Authenticator">>, summary => <<"Update user info for gateway authenticator">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_userid_in_path(), params_userid_in_path(),
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
@ -312,7 +312,7 @@ schema("/gateways/:name/authentication/users/:uid") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(delete_user), desc => ?DESC(delete_user),
summary => <<"Delete User for a Gateway Authenticator">>, summary => <<"Delete user for gateway authenticator">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_userid_in_path(), params_userid_in_path(),
responses => responses =>

View File

@ -126,7 +126,7 @@ schema("/gateways/:name/authentication/import_users") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(emqx_gateway_api_authn, import_users), desc => ?DESC(emqx_gateway_api_authn, import_users),
summary => <<"Import Users">>, summary => <<"Import users">>,
parameters => params_gateway_name_in_path(), parameters => params_gateway_name_in_path(),
'requestBody' => emqx_dashboard_swagger:file_schema(filename), 'requestBody' => emqx_dashboard_swagger:file_schema(filename),
responses => responses =>
@ -140,7 +140,7 @@ schema("/gateways/:name/listeners/:id/authentication/import_users") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(emqx_gateway_api_listeners, import_users), desc => ?DESC(emqx_gateway_api_listeners, import_users),
summary => <<"Import Users">>, summary => <<"Import users">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path(), params_listener_id_in_path(),
'requestBody' => emqx_dashboard_swagger:file_schema(filename), 'requestBody' => emqx_dashboard_swagger:file_schema(filename),

View File

@ -460,7 +460,7 @@ schema("/gateways/:name/clients") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(list_clients), desc => ?DESC(list_clients),
summary => <<"List Gateway's Clients">>, summary => <<"List gateway's clients">>,
parameters => params_client_query(), parameters => params_client_query(),
responses => responses =>
?STANDARD_RESP(#{ ?STANDARD_RESP(#{
@ -478,7 +478,7 @@ schema("/gateways/:name/clients/:clientid") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(get_client), desc => ?DESC(get_client),
summary => <<"Get Client Info">>, summary => <<"Get client info">>,
parameters => params_client_insta(), parameters => params_client_insta(),
responses => responses =>
?STANDARD_RESP(#{200 => schema_client()}) ?STANDARD_RESP(#{200 => schema_client()})
@ -487,7 +487,7 @@ schema("/gateways/:name/clients/:clientid") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(kick_client), desc => ?DESC(kick_client),
summary => <<"Kick out Client">>, summary => <<"Kick out client">>,
parameters => params_client_insta(), parameters => params_client_insta(),
responses => responses =>
?STANDARD_RESP(#{204 => <<"Kicked">>}) ?STANDARD_RESP(#{204 => <<"Kicked">>})
@ -500,7 +500,7 @@ schema("/gateways/:name/clients/:clientid/subscriptions") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(list_subscriptions), desc => ?DESC(list_subscriptions),
summary => <<"List Client's Subscription">>, summary => <<"List client's subscription">>,
parameters => params_client_insta(), parameters => params_client_insta(),
responses => responses =>
?STANDARD_RESP( ?STANDARD_RESP(
@ -516,7 +516,7 @@ schema("/gateways/:name/clients/:clientid/subscriptions") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(add_subscription), desc => ?DESC(add_subscription),
summary => <<"Add Subscription for Client">>, summary => <<"Add subscription for client">>,
parameters => params_client_insta(), parameters => params_client_insta(),
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
ref(subscription), ref(subscription),
@ -540,7 +540,7 @@ schema("/gateways/:name/clients/:clientid/subscriptions/:topic") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(delete_subscription), desc => ?DESC(delete_subscription),
summary => <<"Delete Client's Subscription">>, summary => <<"Delete client's subscription">>,
parameters => params_topic_name_in_path() ++ params_client_insta(), parameters => params_topic_name_in_path() ++ params_client_insta(),
responses => responses =>
?STANDARD_RESP(#{204 => <<"Unsubscribed">>}) ?STANDARD_RESP(#{204 => <<"Unsubscribed">>})
@ -1020,12 +1020,12 @@ examples_client_list() ->
#{ #{
general_client_list => general_client_list =>
#{ #{
summary => <<"General Client List">>, summary => <<"General client list">>,
value => [example_general_client()] value => [example_general_client()]
}, },
lwm2m_client_list => lwm2m_client_list =>
#{ #{
summary => <<"LwM2M Client List">>, summary => <<"LwM2M client list">>,
value => [example_lwm2m_client()] value => [example_lwm2m_client()]
} }
}. }.
@ -1034,12 +1034,12 @@ examples_client() ->
#{ #{
general_client => general_client =>
#{ #{
summary => <<"General Client Info">>, summary => <<"General client info">>,
value => example_general_client() value => example_general_client()
}, },
lwm2m_client => lwm2m_client =>
#{ #{
summary => <<"LwM2M Client Info">>, summary => <<"LwM2M client info">>,
value => example_lwm2m_client() value => example_lwm2m_client()
} }
}. }.
@ -1048,12 +1048,12 @@ examples_subscription_list() ->
#{ #{
general_subscription_list => general_subscription_list =>
#{ #{
summary => <<"A General Subscription List">>, summary => <<"A general subscription list">>,
value => [example_general_subscription()] value => [example_general_subscription()]
}, },
stomp_subscription_list => stomp_subscription_list =>
#{ #{
summary => <<"The Stomp Subscription List">>, summary => <<"The STOMP subscription list">>,
value => [example_stomp_subscription] value => [example_stomp_subscription]
} }
}. }.
@ -1062,12 +1062,12 @@ examples_subscription() ->
#{ #{
general_subscription => general_subscription =>
#{ #{
summary => <<"A General Subscription">>, summary => <<"A general subscription">>,
value => example_general_subscription() value => example_general_subscription()
}, },
stomp_subscription => stomp_subscription =>
#{ #{
summary => <<"A Stomp Subscription">>, summary => <<"A STOMP subscription">>,
value => example_stomp_subscription() value => example_stomp_subscription()
} }
}. }.

View File

@ -362,7 +362,7 @@ schema("/gateways/:name/listeners") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(list_listeners), desc => ?DESC(list_listeners),
summary => <<"List All Listeners">>, summary => <<"List all listeners">>,
parameters => params_gateway_name_in_path(), parameters => params_gateway_name_in_path(),
responses => responses =>
?STANDARD_RESP( ?STANDARD_RESP(
@ -378,7 +378,7 @@ schema("/gateways/:name/listeners") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(add_listener), desc => ?DESC(add_listener),
summary => <<"Add a Listener">>, summary => <<"Add listener">>,
parameters => params_gateway_name_in_path(), parameters => params_gateway_name_in_path(),
%% XXX: How to distinguish the different listener supported by %% XXX: How to distinguish the different listener supported by
%% different types of gateways? %% different types of gateways?
@ -404,7 +404,7 @@ schema("/gateways/:name/listeners/:id") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(get_listener), desc => ?DESC(get_listener),
summary => <<"Get the Listener Configs">>, summary => <<"Get listener config">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path(), params_listener_id_in_path(),
responses => responses =>
@ -421,7 +421,7 @@ schema("/gateways/:name/listeners/:id") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(delete_listener), desc => ?DESC(delete_listener),
summary => <<"Delete the Listener">>, summary => <<"Delete listener">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path(), params_listener_id_in_path(),
responses => responses =>
@ -431,7 +431,7 @@ schema("/gateways/:name/listeners/:id") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(update_listener), desc => ?DESC(update_listener),
summary => <<"Update the Listener Configs">>, summary => <<"Update listener config">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path(), params_listener_id_in_path(),
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
@ -456,7 +456,7 @@ schema("/gateways/:name/listeners/:id/authentication") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(get_listener_authn), desc => ?DESC(get_listener_authn),
summary => <<"Get the Listener's Authenticator">>, summary => <<"Get the listener's authenticator">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path(), params_listener_id_in_path(),
responses => responses =>
@ -471,7 +471,7 @@ schema("/gateways/:name/listeners/:id/authentication") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(add_listener_authn), desc => ?DESC(add_listener_authn),
summary => <<"Create an Authenticator for a Listener">>, summary => <<"Create authenticator for listener">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path(), params_listener_id_in_path(),
'requestBody' => schema_authn(), 'requestBody' => schema_authn(),
@ -482,7 +482,7 @@ schema("/gateways/:name/listeners/:id/authentication") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(update_listener_authn), desc => ?DESC(update_listener_authn),
summary => <<"Update the Listener Authenticator configs">>, summary => <<"Update config of authenticator for listener">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path(), params_listener_id_in_path(),
'requestBody' => schema_authn(), 'requestBody' => schema_authn(),
@ -493,7 +493,7 @@ schema("/gateways/:name/listeners/:id/authentication") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(delete_listener_authn), desc => ?DESC(delete_listener_authn),
summary => <<"Delete the Listener's Authenticator">>, summary => <<"Delete the listener's authenticator">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path(), params_listener_id_in_path(),
responses => responses =>
@ -507,7 +507,7 @@ schema("/gateways/:name/listeners/:id/authentication/users") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(list_users), desc => ?DESC(list_users),
summary => <<"List Authenticator's Users">>, summary => <<"List authenticator's users">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path() ++ params_listener_id_in_path() ++
params_paging_in_qs(), params_paging_in_qs(),
@ -525,7 +525,7 @@ schema("/gateways/:name/listeners/:id/authentication/users") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(add_user), desc => ?DESC(add_user),
summary => <<"Add User for an Authenticator">>, summary => <<"Add user for an authenticator">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path(), params_listener_id_in_path(),
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
@ -550,7 +550,7 @@ schema("/gateways/:name/listeners/:id/authentication/users/:uid") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(get_user), desc => ?DESC(get_user),
summary => <<"Get User Info">>, summary => <<"Get user info">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path() ++ params_listener_id_in_path() ++
params_userid_in_path(), params_userid_in_path(),
@ -568,7 +568,7 @@ schema("/gateways/:name/listeners/:id/authentication/users/:uid") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(update_user), desc => ?DESC(update_user),
summary => <<"Update User Info">>, summary => <<"Update user info">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path() ++ params_listener_id_in_path() ++
params_userid_in_path(), params_userid_in_path(),
@ -590,7 +590,7 @@ schema("/gateways/:name/listeners/:id/authentication/users/:uid") ->
#{ #{
tags => ?TAGS, tags => ?TAGS,
desc => ?DESC(delete_user), desc => ?DESC(delete_user),
summary => <<"Delete User">>, summary => <<"Delete user">>,
parameters => params_gateway_name_in_path() ++ parameters => params_gateway_name_in_path() ++
params_listener_id_in_path() ++ params_listener_id_in_path() ++
params_userid_in_path(), params_userid_in_path(),
@ -712,7 +712,7 @@ examples_listener() ->
#{ #{
tcp_listener => tcp_listener =>
#{ #{
summary => <<"A simple tcp listener example">>, summary => <<"A simple TCP listener example">>,
value => value =>
#{ #{
name => <<"tcp-def">>, name => <<"tcp-def">>,
@ -738,7 +738,7 @@ examples_listener() ->
}, },
ssl_listener => ssl_listener =>
#{ #{
summary => <<"A simple ssl listener example">>, summary => <<"A simple SSL listener example">>,
value => value =>
#{ #{
name => <<"ssl-def">>, name => <<"ssl-def">>,
@ -771,7 +771,7 @@ examples_listener() ->
}, },
udp_listener => udp_listener =>
#{ #{
summary => <<"A simple udp listener example">>, summary => <<"A simple UDP listener example">>,
value => value =>
#{ #{
name => <<"udp-def">>, name => <<"udp-def">>,
@ -789,7 +789,7 @@ examples_listener() ->
}, },
dtls_listener => dtls_listener =>
#{ #{
summary => <<"A simple dtls listener example">>, summary => <<"A simple DTLS listener example">>,
value => value =>
#{ #{
name => <<"dtls-def">>, name => <<"dtls-def">>,
@ -817,7 +817,7 @@ examples_listener() ->
}, },
dtls_listener_with_psk_ciphers => dtls_listener_with_psk_ciphers =>
#{ #{
summary => <<"A dtls listener with PSK example">>, summary => <<"A DTLS listener with PSK example">>,
value => value =>
#{ #{
name => <<"dtls-psk">>, name => <<"dtls-psk">>,
@ -845,7 +845,7 @@ examples_listener() ->
}, },
lisetner_with_authn => lisetner_with_authn =>
#{ #{
summary => <<"A tcp listener with authentication example">>, summary => <<"A TCP listener with authentication example">>,
value => value =>
#{ #{
name => <<"tcp-with-authn">>, name => <<"tcp-with-authn">>,

View File

@ -2,8 +2,7 @@
emqx_mgmt_api_publish { emqx_mgmt_api_publish {
publish_api { publish_api {
desc { desc {
en: """Publish one message.<br/> en: """Possible HTTP status response codes are:<br/>
Possible HTTP status response codes are:<br/>
<code>200</code>: The message is delivered to at least one subscriber;<br/> <code>200</code>: The message is delivered to at least one subscriber;<br/>
<code>202</code>: No matched subscribers;<br/> <code>202</code>: No matched subscribers;<br/>
<code>400</code>: Message is invalid. for example bad topic name, or QoS is out of range;<br/> <code>400</code>: Message is invalid. for example bad topic name, or QoS is out of range;<br/>
@ -16,11 +15,14 @@ Possible HTTP status response codes are:<br/>
400: 消息编码错误,如非法主题,或 QoS 超出范围等。<br/> 400: 消息编码错误,如非法主题,或 QoS 超出范围等。<br/>
503: 服务重启等过程中导致转发失败。""" 503: 服务重启等过程中导致转发失败。"""
} }
label {
en: "Publish a message"
zh: "发布一条信息"
}
} }
publish_bulk_api { publish_bulk_api {
desc { desc {
en: """Publish a batch of messages.<br/> en: """Possible HTTP response status code are:<br/>
Possible HTTP response status code are:<br/>
200: All messages are delivered to at least one subscriber;<br/> 200: All messages are delivered to at least one subscriber;<br/>
202: At least one message was not delivered to any subscriber;<br/> 202: At least one message was not delivered to any subscriber;<br/>
400: At least one message is invalid. For example bad topic name, or QoS is out of range;<br/> 400: At least one message is invalid. For example bad topic name, or QoS is out of range;<br/>
@ -41,6 +43,10 @@ result of each individual message in the batch."""
<code>/publish</code> 是一样的。 <code>/publish</code> 是一样的。
如果所有的消息都是合法的,那么 HTTP 返回的内容是一个 JSON 数组,每个元素代表了该消息转发的状态。""" 如果所有的消息都是合法的,那么 HTTP 返回的内容是一个 JSON 数组,每个元素代表了该消息转发的状态。"""
} }
label {
en: "Publish a batch of messages"
zh: "发布一批信息"
}
} }
topic_name { topic_name {

View File

@ -22,6 +22,10 @@ emqx_mgmt_api_status {
"GET `/status`端点(没有`/api/...`前缀)也是这个端点的一个别名,工作方式相同。" "GET `/status`端点(没有`/api/...`前缀)也是这个端点的一个别名,工作方式相同。"
" 这个别名从v5.0.0开始就有了。" " 这个别名从v5.0.0开始就有了。"
} }
label {
en: "Service health check"
zh: "服务健康检查"
}
} }
get_status_response200 { get_status_response200 {

View File

@ -119,6 +119,7 @@ schema("/configs_reset/:rootname") ->
"- For a config entry that has default value, this resets it to the default value;\n" "- For a config entry that has default value, this resets it to the default value;\n"
"- For a config entry that has no default value, an error 400 will be returned" "- For a config entry that has no default value, an error 400 will be returned"
>>, >>,
summary => <<"Reset config entry">>,
%% We only return "200" rather than the new configs that has been changed, as %% We only return "200" rather than the new configs that has been changed, as
%% the schema of the changed configs is depends on the request parameter %% the schema of the changed configs is depends on the request parameter
%% `conf_path`, it cannot be defined here. %% `conf_path`, it cannot be defined here.

View File

@ -48,6 +48,9 @@
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$"). -define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$").
-define(TAGS, [<<"Plugins">>]). -define(TAGS, [<<"Plugins">>]).
%% Plugin NameVsn must follow the pattern <app_name>-<vsn>,
%% app_name must be a snake_case (no '-' allowed).
-define(VSN_WILDCARD, "-*.tar.gz").
namespace() -> "plugins". namespace() -> "plugins".
@ -68,10 +71,10 @@ schema("/plugins") ->
#{ #{
'operationId' => list_plugins, 'operationId' => list_plugins,
get => #{ get => #{
summary => <<"List all installed plugins">>,
description => description =>
"List all install plugins.<br/>"
"Plugins are launched in top-down order.<br/>" "Plugins are launched in top-down order.<br/>"
"Using `POST /plugins/{name}/move` to change the boot order.", "Use `POST /plugins/{name}/move` to change the boot order.",
tags => ?TAGS, tags => ?TAGS,
responses => #{ responses => #{
200 => hoconsc:array(hoconsc:ref(plugin)) 200 => hoconsc:array(hoconsc:ref(plugin))
@ -82,8 +85,9 @@ schema("/plugins/install") ->
#{ #{
'operationId' => upload_install, 'operationId' => upload_install,
post => #{ post => #{
summary => <<"Install a new plugin">>,
description => description =>
"Install a plugin(plugin-vsn.tar.gz)." "Upload a plugin tarball (plugin-vsn.tar.gz)."
"Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) " "Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) "
"to develop plugin.", "to develop plugin.",
tags => ?TAGS, tags => ?TAGS,
@ -112,7 +116,8 @@ schema("/plugins/:name") ->
#{ #{
'operationId' => plugin, 'operationId' => plugin,
get => #{ get => #{
description => "Describe a plugin according `release.json` and `README.md`.", summary => <<"Get a plugin description">>,
description => "Describs plugin according to its `release.json` and `README.md`.",
tags => ?TAGS, tags => ?TAGS,
parameters => [hoconsc:ref(name)], parameters => [hoconsc:ref(name)],
responses => #{ responses => #{
@ -121,7 +126,8 @@ schema("/plugins/:name") ->
} }
}, },
delete => #{ delete => #{
description => "Uninstall a plugin package.", summary => <<"Delete a plugin">>,
description => "Uninstalls a previously uploaded plugin package.",
tags => ?TAGS, tags => ?TAGS,
parameters => [hoconsc:ref(name)], parameters => [hoconsc:ref(name)],
responses => #{ responses => #{
@ -134,6 +140,7 @@ schema("/plugins/:name/:action") ->
#{ #{
'operationId' => update_plugin, 'operationId' => update_plugin,
put => #{ put => #{
summary => <<"Trigger action on an installed plugin">>,
description => description =>
"start/stop a installed plugin.<br/>" "start/stop a installed plugin.<br/>"
"- **start**: start the plugin.<br/>" "- **start**: start the plugin.<br/>"
@ -153,6 +160,7 @@ schema("/plugins/:name/move") ->
#{ #{
'operationId' => update_boot_order, 'operationId' => update_boot_order,
post => #{ post => #{
summary => <<"Move plugin within plugin hiearchy">>,
description => "Setting the boot order of plugins.", description => "Setting the boot order of plugins.",
tags => ?TAGS, tags => ?TAGS,
parameters => [hoconsc:ref(name)], parameters => [hoconsc:ref(name)],
@ -329,7 +337,7 @@ upload_install(post, #{body := #{<<"plugin">> := Plugin}}) when is_map(Plugin) -
case emqx_plugins:parse_name_vsn(FileName) of case emqx_plugins:parse_name_vsn(FileName) of
{ok, AppName, _Vsn} -> {ok, AppName, _Vsn} ->
AppDir = filename:join(emqx_plugins:install_dir(), AppName), AppDir = filename:join(emqx_plugins:install_dir(), AppName),
case filelib:wildcard(AppDir ++ "*.tar.gz") of case filelib:wildcard(AppDir ++ ?VSN_WILDCARD) of
[] -> [] ->
do_install_package(FileName, Bin); do_install_package(FileName, Bin);
OtherVsn -> OtherVsn ->

View File

@ -50,6 +50,7 @@ schema("/publish") ->
#{ #{
'operationId' => publish, 'operationId' => publish,
post => #{ post => #{
summary => <<"Publish a message">>,
description => ?DESC(publish_api), description => ?DESC(publish_api),
tags => [<<"Publish">>], tags => [<<"Publish">>],
'requestBody' => hoconsc:mk(hoconsc:ref(?MODULE, publish_message)), 'requestBody' => hoconsc:mk(hoconsc:ref(?MODULE, publish_message)),
@ -65,6 +66,7 @@ schema("/publish/bulk") ->
#{ #{
'operationId' => publish_batch, 'operationId' => publish_batch,
post => #{ post => #{
summary => <<"Publish a batch of messages">>,
description => ?DESC(publish_bulk_api), description => ?DESC(publish_bulk_api),
tags => [<<"Publish">>], tags => [<<"Publish">>],
'requestBody' => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, publish_message)), #{}), 'requestBody' => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, publish_message)), #{}),

View File

@ -20,6 +20,7 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-define(EMQX_PLUGIN_TEMPLATE_NAME, "emqx_plugin_template").
-define(EMQX_PLUGIN_TEMPLATE_VSN, "5.0.0"). -define(EMQX_PLUGIN_TEMPLATE_VSN, "5.0.0").
-define(PACKAGE_SUFFIX, ".tar.gz"). -define(PACKAGE_SUFFIX, ".tar.gz").
@ -89,6 +90,27 @@ t_plugins(Config) ->
{ok, []} = uninstall_plugin(NameVsn), {ok, []} = uninstall_plugin(NameVsn),
ok. ok.
t_install_plugin_matching_exisiting_name(Config) ->
DemoShDir = proplists:get_value(demo_sh_dir, Config),
PackagePath = get_demo_plugin_package(DemoShDir),
NameVsn = filename:basename(PackagePath, ?PACKAGE_SUFFIX),
ok = emqx_plugins:ensure_uninstalled(NameVsn),
ok = emqx_plugins:delete_package(NameVsn),
NameVsn1 = ?EMQX_PLUGIN_TEMPLATE_NAME ++ "_a" ++ "-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN,
PackagePath1 = create_renamed_package(PackagePath, NameVsn1),
NameVsn1 = filename:basename(PackagePath1, ?PACKAGE_SUFFIX),
ok = emqx_plugins:ensure_uninstalled(NameVsn1),
ok = emqx_plugins:delete_package(NameVsn1),
%% First, install plugin "emqx_plugin_template_a", then:
%% "emqx_plugin_template" which matches the beginning
%% of the previously installed plugin name
ok = install_plugin(PackagePath1),
ok = install_plugin(PackagePath),
{ok, _} = describe_plugins(NameVsn),
{ok, _} = describe_plugins(NameVsn1),
{ok, _} = uninstall_plugin(NameVsn),
{ok, _} = uninstall_plugin(NameVsn1).
t_bad_plugin(Config) -> t_bad_plugin(Config) ->
DemoShDir = proplists:get_value(demo_sh_dir, Config), DemoShDir = proplists:get_value(demo_sh_dir, Config),
PackagePathOrig = get_demo_plugin_package(DemoShDir), PackagePathOrig = get_demo_plugin_package(DemoShDir),
@ -160,9 +182,31 @@ uninstall_plugin(Name) ->
get_demo_plugin_package(Dir) -> get_demo_plugin_package(Dir) ->
#{package := Pkg} = emqx_plugins_SUITE:get_demo_plugin_package(), #{package := Pkg} = emqx_plugins_SUITE:get_demo_plugin_package(),
FileName = "emqx_plugin_template-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX, FileName = ?EMQX_PLUGIN_TEMPLATE_NAME ++ "-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX,
PluginPath = "./" ++ FileName, PluginPath = "./" ++ FileName,
Pkg = filename:join([Dir, FileName]), Pkg = filename:join([Dir, FileName]),
_ = os:cmd("cp " ++ Pkg ++ " " ++ PluginPath), _ = os:cmd("cp " ++ Pkg ++ " " ++ PluginPath),
true = filelib:is_regular(PluginPath), true = filelib:is_regular(PluginPath),
PluginPath. PluginPath.
create_renamed_package(PackagePath, NewNameVsn) ->
{ok, Content} = erl_tar:extract(PackagePath, [compressed, memory]),
{ok, NewName, _Vsn} = emqx_plugins:parse_name_vsn(NewNameVsn),
NewNameB = atom_to_binary(NewName, utf8),
Content1 = lists:map(
fun({F, B}) ->
[_ | PathPart] = filename:split(F),
B1 = update_release_json(PathPart, B, NewNameB),
{filename:join([NewNameVsn | PathPart]), B1}
end,
Content
),
NewPackagePath = filename:join(filename:dirname(PackagePath), NewNameVsn ++ ?PACKAGE_SUFFIX),
ok = erl_tar:create(NewPackagePath, Content1, [compressed]),
NewPackagePath.
update_release_json(["release.json"], FileContent, NewName) ->
ContentMap = emqx_json:decode(FileContent, [return_maps]),
emqx_json:encode(ContentMap#{<<"name">> => NewName});
update_release_json(_FileName, FileContent, _NewName) ->
FileContent.

View File

@ -1,7 +1,7 @@
emqx_topic_metrics_api { emqx_topic_metrics_api {
get_topic_metrics_api { get_topic_metrics_api {
desc { desc {
en: """List Topic metrics""" en: """List topic metrics"""
zh: """获取主题监控数据""" zh: """获取主题监控数据"""
} }
} }
@ -15,21 +15,21 @@ emqx_topic_metrics_api {
post_topic_metrics_api { post_topic_metrics_api {
desc { desc {
en: """Create Topic metrics""" en: """Create topic metrics"""
zh: """创建主题监控数据""" zh: """创建主题监控数据"""
} }
} }
gat_topic_metrics_data_api { gat_topic_metrics_data_api {
desc { desc {
en: """Get Topic metrics""" en: """Get topic metrics"""
zh: """获取主题监控数据""" zh: """获取主题监控数据"""
} }
} }
delete_topic_metrics_data_api { delete_topic_metrics_data_api {
desc { desc {
en: """Delete Topic metrics""" en: """Delete topic metrics"""
zh: """删除主题监控数据""" zh: """删除主题监控数据"""
} }
} }
@ -43,7 +43,7 @@ emqx_topic_metrics_api {
topic_metrics_api_response400 { topic_metrics_api_response400 {
desc { desc {
en: """Bad Request. Already exists or bad topic name""" en: """Bad request. Already exists or bad topic name"""
zh: """错误请求。已存在或错误的主题名称""" zh: """错误请求。已存在或错误的主题名称"""
} }
} }

View File

@ -45,6 +45,17 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise
} }
} }
resume_interval {
desc {
en: """The interval at which the buffer worker attempts to resend failed requests in the inflight window."""
zh: """在发送失败后尝试重传飞行窗口中的请求的时间间隔。"""
}
label {
en: """Resume Interval"""
zh: """重试时间间隔"""
}
}
start_after_created { start_after_created {
desc { desc {
en: """Whether start the resource right after created.""" en: """Whether start the resource right after created."""

View File

@ -41,6 +41,7 @@
callback_mode := callback_mode(), callback_mode := callback_mode(),
query_mode := query_mode(), query_mode := query_mode(),
config := resource_config(), config := resource_config(),
error := term(),
state := resource_state(), state := resource_state(),
status := resource_status(), status := resource_status(),
metrics => emqx_metrics_worker:metrics() metrics => emqx_metrics_worker:metrics()

View File

@ -265,7 +265,7 @@ query(ResId, Request, Opts) ->
IsBufferSupported = is_buffer_supported(Module), IsBufferSupported = is_buffer_supported(Module),
case {IsBufferSupported, QM} of case {IsBufferSupported, QM} of
{true, _} -> {true, _} ->
%% only Kafka so far %% only Kafka producer so far
Opts1 = Opts#{is_buffer_supported => true}, Opts1 = Opts#{is_buffer_supported => true},
emqx_resource_buffer_worker:simple_async_query(ResId, Request, Opts1); emqx_resource_buffer_worker:simple_async_query(ResId, Request, Opts1);
{false, sync} -> {false, sync} ->

View File

@ -88,6 +88,8 @@
-type queue_query() :: ?QUERY(reply_fun(), request(), HasBeenSent :: boolean(), expire_at()). -type queue_query() :: ?QUERY(reply_fun(), request(), HasBeenSent :: boolean(), expire_at()).
-type request() :: term(). -type request() :: term().
-type request_from() :: undefined | gen_statem:from(). -type request_from() :: undefined | gen_statem:from().
-type request_timeout() :: infinity | timer:time().
-type health_check_interval() :: timer:time().
-type state() :: blocked | running. -type state() :: blocked | running.
-type inflight_key() :: integer(). -type inflight_key() :: integer().
-type data() :: #{ -type data() :: #{
@ -140,7 +142,7 @@ simple_sync_query(Id, Request) ->
QueryOpts = simple_query_opts(), QueryOpts = simple_query_opts(),
emqx_resource_metrics:matched_inc(Id), emqx_resource_metrics:matched_inc(Id),
Ref = make_request_ref(), Ref = make_request_ref(),
Result = call_query(sync, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), Result = call_query(force_sync, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts),
_ = handle_query_result(Id, Result, _HasBeenSent = false), _ = handle_query_result(Id, Result, _HasBeenSent = false),
Result. Result.
@ -152,7 +154,7 @@ simple_async_query(Id, Request, QueryOpts0) ->
QueryOpts = maps:merge(simple_query_opts(), QueryOpts0), QueryOpts = maps:merge(simple_query_opts(), QueryOpts0),
emqx_resource_metrics:matched_inc(Id), emqx_resource_metrics:matched_inc(Id),
Ref = make_request_ref(), Ref = make_request_ref(),
Result = call_query(async, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), Result = call_query(async_if_possible, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts),
_ = handle_query_result(Id, Result, _HasBeenSent = false), _ = handle_query_result(Id, Result, _HasBeenSent = false),
Result. Result.
@ -199,6 +201,8 @@ init({Id, Index, Opts}) ->
RequestTimeout = maps:get(request_timeout, Opts, ?DEFAULT_REQUEST_TIMEOUT), RequestTimeout = maps:get(request_timeout, Opts, ?DEFAULT_REQUEST_TIMEOUT),
BatchTime0 = maps:get(batch_time, Opts, ?DEFAULT_BATCH_TIME), BatchTime0 = maps:get(batch_time, Opts, ?DEFAULT_BATCH_TIME),
BatchTime = adjust_batch_time(Id, RequestTimeout, BatchTime0), BatchTime = adjust_batch_time(Id, RequestTimeout, BatchTime0),
DefaultResumeInterval = default_resume_interval(RequestTimeout, HealthCheckInterval),
ResumeInterval = maps:get(resume_interval, Opts, DefaultResumeInterval),
Data = #{ Data = #{
id => Id, id => Id,
index => Index, index => Index,
@ -207,7 +211,7 @@ init({Id, Index, Opts}) ->
batch_size => BatchSize, batch_size => BatchSize,
batch_time => BatchTime, batch_time => BatchTime,
queue => Queue, queue => Queue,
resume_interval => maps:get(resume_interval, Opts, HealthCheckInterval), resume_interval => ResumeInterval,
tref => undefined tref => undefined
}, },
?tp(buffer_worker_init, #{id => Id, index => Index}), ?tp(buffer_worker_init, #{id => Id, index => Index}),
@ -377,7 +381,7 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) ->
} = Data0, } = Data0,
?tp(buffer_worker_retry_inflight, #{query_or_batch => QueryOrBatch, ref => Ref}), ?tp(buffer_worker_retry_inflight, #{query_or_batch => QueryOrBatch, ref => Ref}),
QueryOpts = #{simple_query => false}, QueryOpts = #{simple_query => false},
Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), Result = call_query(force_sync, Id, Index, Ref, QueryOrBatch, QueryOpts),
ReplyResult = ReplyResult =
case QueryOrBatch of case QueryOrBatch of
?QUERY(ReplyTo, _, HasBeenSent, _ExpireAt) -> ?QUERY(ReplyTo, _, HasBeenSent, _ExpireAt) ->
@ -566,7 +570,7 @@ do_flush(
%% unwrap when not batching (i.e., batch size == 1) %% unwrap when not batching (i.e., batch size == 1)
[?QUERY(ReplyTo, _, HasBeenSent, _ExpireAt) = Request] = Batch, [?QUERY(ReplyTo, _, HasBeenSent, _ExpireAt) = Request] = Batch,
QueryOpts = #{inflight_tid => InflightTID, simple_query => false}, QueryOpts = #{inflight_tid => InflightTID, simple_query => false},
Result = call_query(configured, Id, Index, Ref, Request, QueryOpts), Result = call_query(async_if_possible, Id, Index, Ref, Request, QueryOpts),
Reply = ?REPLY(ReplyTo, HasBeenSent, Result), Reply = ?REPLY(ReplyTo, HasBeenSent, Result),
case reply_caller(Id, Reply, QueryOpts) of case reply_caller(Id, Reply, QueryOpts) of
%% Failed; remove the request from the queue, as we cannot pop %% Failed; remove the request from the queue, as we cannot pop
@ -651,7 +655,7 @@ do_flush(#{queue := Q1} = Data0, #{
inflight_tid := InflightTID inflight_tid := InflightTID
} = Data0, } = Data0,
QueryOpts = #{inflight_tid => InflightTID, simple_query => false}, QueryOpts = #{inflight_tid => InflightTID, simple_query => false},
Result = call_query(configured, Id, Index, Ref, Batch, QueryOpts), Result = call_query(async_if_possible, Id, Index, Ref, Batch, QueryOpts),
case batch_reply_caller(Id, Result, Batch, QueryOpts) of case batch_reply_caller(Id, Result, Batch, QueryOpts) of
%% Failed; remove the request from the queue, as we cannot pop %% Failed; remove the request from the queue, as we cannot pop
%% from it again, but we'll retry it using the inflight table. %% from it again, but we'll retry it using the inflight table.
@ -883,17 +887,13 @@ handle_async_worker_down(Data0, Pid) ->
mark_inflight_items_as_retriable(Data, WorkerMRef), mark_inflight_items_as_retriable(Data, WorkerMRef),
{keep_state, Data}. {keep_state, Data}.
call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> -spec call_query(force_sync | async_if_possible, _, _, _, _, _) -> _.
?tp(call_query_enter, #{id => Id, query => Query, query_mode => QM0}), call_query(QM, Id, Index, Ref, Query, QueryOpts) ->
?tp(call_query_enter, #{id => Id, query => Query, query_mode => QM}),
case emqx_resource_manager:lookup_cached(Id) of case emqx_resource_manager:lookup_cached(Id) of
{ok, _Group, #{status := stopped}} -> {ok, _Group, #{status := stopped}} ->
?RESOURCE_ERROR(stopped, "resource stopped or disabled"); ?RESOURCE_ERROR(stopped, "resource stopped or disabled");
{ok, _Group, Resource} -> {ok, _Group, Resource} ->
QM =
case QM0 =:= configured of
true -> maps:get(query_mode, Resource);
false -> QM0
end,
do_call_query(QM, Id, Index, Ref, Query, QueryOpts, Resource); do_call_query(QM, Id, Index, Ref, Query, QueryOpts, Resource);
{error, not_found} -> {error, not_found} ->
?RESOURCE_ERROR(not_found, "resource not found") ?RESOURCE_ERROR(not_found, "resource not found")
@ -1511,9 +1511,9 @@ inc_sent_success(Id, _HasBeenSent = true) ->
inc_sent_success(Id, _HasBeenSent) -> inc_sent_success(Id, _HasBeenSent) ->
emqx_resource_metrics:success_inc(Id). emqx_resource_metrics:success_inc(Id).
call_mode(sync, _) -> sync; call_mode(force_sync, _) -> sync;
call_mode(async, always_sync) -> sync; call_mode(async_if_possible, always_sync) -> sync;
call_mode(async, async_if_possible) -> async. call_mode(async_if_possible, async_if_possible) -> async.
assert_ok_result(ok) -> assert_ok_result(ok) ->
true; true;
@ -1679,6 +1679,17 @@ adjust_batch_time(Id, RequestTimeout, BatchTime0) ->
end, end,
BatchTime. BatchTime.
%% The request timeout should be greater than the resume interval, as
%% it defines how often the buffer worker tries to unblock. If request
%% timeout is <= resume interval and the buffer worker is ever
%% blocked, than all queued requests will basically fail without being
%% attempted.
-spec default_resume_interval(request_timeout(), health_check_interval()) -> timer:time().
default_resume_interval(_RequestTimeout = infinity, HealthCheckInterval) ->
max(1, HealthCheckInterval);
default_resume_interval(RequestTimeout, HealthCheckInterval) ->
max(1, min(HealthCheckInterval, RequestTimeout div 3)).
-ifdef(TEST). -ifdef(TEST).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
adjust_batch_time_test_() -> adjust_batch_time_test_() ->

View File

@ -388,6 +388,7 @@ handle_event(state_timeout, health_check, connecting, Data) ->
handle_event(enter, _OldState, connected = State, Data) -> handle_event(enter, _OldState, connected = State, Data) ->
ok = log_state_consistency(State, Data), ok = log_state_consistency(State, Data),
_ = emqx_alarm:deactivate(Data#data.id), _ = emqx_alarm:deactivate(Data#data.id),
?tp(resource_connected_enter, #{}),
{keep_state_and_data, health_check_actions(Data)}; {keep_state_and_data, health_check_actions(Data)};
handle_event(state_timeout, health_check, connected, Data) -> handle_event(state_timeout, health_check, connected, Data) ->
handle_connected_health_check(Data); handle_connected_health_check(Data);
@ -522,7 +523,7 @@ start_resource(Data, From) ->
id => Data#data.id, id => Data#data.id,
reason => Reason reason => Reason
}), }),
_ = maybe_alarm(disconnected, Data#data.id), _ = maybe_alarm(disconnected, Data#data.id, Data#data.error),
%% Keep track of the error reason why the connection did not work %% Keep track of the error reason why the connection did not work
%% so that the Reason can be returned when the verification call is made. %% so that the Reason can be returned when the verification call is made.
UpdatedData = Data#data{status = disconnected, error = Reason}, UpdatedData = Data#data{status = disconnected, error = Reason},
@ -597,7 +598,7 @@ with_health_check(Data, Func) ->
ResId = Data#data.id, ResId = Data#data.id,
HCRes = emqx_resource:call_health_check(Data#data.manager_id, Data#data.mod, Data#data.state), HCRes = emqx_resource:call_health_check(Data#data.manager_id, Data#data.mod, Data#data.state),
{Status, NewState, Err} = parse_health_check_result(HCRes, Data), {Status, NewState, Err} = parse_health_check_result(HCRes, Data),
_ = maybe_alarm(Status, ResId), _ = maybe_alarm(Status, ResId, Err),
ok = maybe_resume_resource_workers(ResId, Status), ok = maybe_resume_resource_workers(ResId, Status),
UpdatedData = Data#data{ UpdatedData = Data#data{
state = NewState, status = Status, error = Err state = NewState, status = Status, error = Err
@ -616,15 +617,20 @@ update_state(Data, _DataWas) ->
health_check_interval(Opts) -> health_check_interval(Opts) ->
maps:get(health_check_interval, Opts, ?HEALTHCHECK_INTERVAL). maps:get(health_check_interval, Opts, ?HEALTHCHECK_INTERVAL).
maybe_alarm(connected, _ResId) -> maybe_alarm(connected, _ResId, _Error) ->
ok; ok;
maybe_alarm(_Status, <<?TEST_ID_PREFIX, _/binary>>) -> maybe_alarm(_Status, <<?TEST_ID_PREFIX, _/binary>>, _Error) ->
ok; ok;
maybe_alarm(_Status, ResId) -> maybe_alarm(_Status, ResId, Error) ->
HrError =
case Error of
undefined -> <<"Unknown reason">>;
_Else -> emqx_misc:readable_error_msg(Error)
end,
emqx_alarm:activate( emqx_alarm:activate(
ResId, ResId,
#{resource_id => ResId, reason => resource_down}, #{resource_id => ResId, reason => resource_down},
<<"resource down: ", ResId/binary>> <<"resource down: ", HrError/binary>>
). ).
maybe_resume_resource_workers(ResId, connected) -> maybe_resume_resource_workers(ResId, connected) ->
@ -666,6 +672,7 @@ maybe_reply(Actions, From, Reply) ->
data_record_to_external_map(Data) -> data_record_to_external_map(Data) ->
#{ #{
id => Data#data.id, id => Data#data.id,
error => Data#data.error,
mod => Data#data.mod, mod => Data#data.mod,
callback_mode => Data#data.callback_mode, callback_mode => Data#data.callback_mode,
query_mode => Data#data.query_mode, query_mode => Data#data.query_mode,

View File

@ -55,6 +55,7 @@ fields("creation_opts") ->
[ [
{worker_pool_size, fun worker_pool_size/1}, {worker_pool_size, fun worker_pool_size/1},
{health_check_interval, fun health_check_interval/1}, {health_check_interval, fun health_check_interval/1},
{resume_interval, fun resume_interval/1},
{start_after_created, fun start_after_created/1}, {start_after_created, fun start_after_created/1},
{start_timeout, fun start_timeout/1}, {start_timeout, fun start_timeout/1},
{auto_restart_interval, fun auto_restart_interval/1}, {auto_restart_interval, fun auto_restart_interval/1},
@ -81,6 +82,12 @@ worker_pool_size(default) -> ?WORKER_POOL_SIZE;
worker_pool_size(required) -> false; worker_pool_size(required) -> false;
worker_pool_size(_) -> undefined. worker_pool_size(_) -> undefined.
resume_interval(type) -> emqx_schema:duration_ms();
resume_interval(hidden) -> true;
resume_interval(desc) -> ?DESC("resume_interval");
resume_interval(required) -> false;
resume_interval(_) -> undefined.
health_check_interval(type) -> emqx_schema:duration_ms(); health_check_interval(type) -> emqx_schema:duration_ms();
health_check_interval(desc) -> ?DESC("health_check_interval"); health_check_interval(desc) -> ?DESC("health_check_interval");
health_check_interval(default) -> ?HEALTHCHECK_INTERVAL_RAW; health_check_interval(default) -> ?HEALTHCHECK_INTERVAL_RAW;

View File

@ -146,6 +146,12 @@ on_query(_InstId, {sleep_before_reply, For}, #{pid := Pid}) ->
{error, timeout} {error, timeout}
end. end.
on_query_async(_InstId, block, ReplyFun, #{pid := Pid}) ->
Pid ! {block, ReplyFun},
{ok, Pid};
on_query_async(_InstId, resume, ReplyFun, #{pid := Pid}) ->
Pid ! {resume, ReplyFun},
{ok, Pid};
on_query_async(_InstId, {inc_counter, N}, ReplyFun, #{pid := Pid}) -> on_query_async(_InstId, {inc_counter, N}, ReplyFun, #{pid := Pid}) ->
Pid ! {inc, N, ReplyFun}, Pid ! {inc, N, ReplyFun},
{ok, Pid}; {ok, Pid};
@ -274,6 +280,10 @@ counter_loop(
block -> block ->
ct:pal("counter recv: ~p", [block]), ct:pal("counter recv: ~p", [block]),
State#{status => blocked}; State#{status => blocked};
{block, ReplyFun} ->
ct:pal("counter recv: ~p", [block]),
apply_reply(ReplyFun, ok),
State#{status => blocked};
{block_now, ReplyFun} -> {block_now, ReplyFun} ->
ct:pal("counter recv: ~p", [block_now]), ct:pal("counter recv: ~p", [block_now]),
apply_reply( apply_reply(
@ -284,6 +294,11 @@ counter_loop(
{messages, Msgs} = erlang:process_info(self(), messages), {messages, Msgs} = erlang:process_info(self(), messages),
ct:pal("counter recv: ~p, buffered msgs: ~p", [resume, length(Msgs)]), ct:pal("counter recv: ~p, buffered msgs: ~p", [resume, length(Msgs)]),
State#{status => running}; State#{status => running};
{resume, ReplyFun} ->
{messages, Msgs} = erlang:process_info(self(), messages),
ct:pal("counter recv: ~p, buffered msgs: ~p", [resume, length(Msgs)]),
apply_reply(ReplyFun, ok),
State#{status => running};
{inc, N, ReplyFun} when Status == running -> {inc, N, ReplyFun} when Status == running ->
%ct:pal("async counter recv: ~p", [{inc, N}]), %ct:pal("async counter recv: ~p", [{inc, N}]),
apply_reply(ReplyFun, ok), apply_reply(ReplyFun, ok),

View File

@ -2561,6 +2561,84 @@ do_t_recursive_flush() ->
), ),
ok. ok.
t_call_mode_uncoupled_from_query_mode(_Config) ->
DefaultOpts = #{
batch_size => 1,
batch_time => 5,
worker_pool_size => 1
},
?check_trace(
begin
%% We check that we can call the buffer workers with async
%% calls, even if the underlying connector itself only
%% supports sync calls.
emqx_connector_demo:set_callback_mode(always_sync),
{ok, _} = emqx_resource:create(
?ID,
?DEFAULT_RESOURCE_GROUP,
?TEST_RESOURCE,
#{name => test_resource},
DefaultOpts#{query_mode => async}
),
?tp_span(
async_query_sync_driver,
#{},
?assertMatch(
{ok, {ok, _}},
?wait_async_action(
emqx_resource:query(?ID, {inc_counter, 1}),
#{?snk_kind := buffer_worker_flush_ack},
500
)
)
),
?assertEqual(ok, emqx_resource:remove_local(?ID)),
%% And we check the converse: a connector that allows async
%% calls can be called synchronously, but the underlying
%% call should be async.
emqx_connector_demo:set_callback_mode(async_if_possible),
{ok, _} = emqx_resource:create(
?ID,
?DEFAULT_RESOURCE_GROUP,
?TEST_RESOURCE,
#{name => test_resource},
DefaultOpts#{query_mode => sync}
),
?tp_span(
sync_query_async_driver,
#{},
?assertEqual(ok, emqx_resource:query(?ID, {inc_counter, 2}))
),
?assertEqual(ok, emqx_resource:remove_local(?ID)),
?tp(sync_query_async_driver, #{}),
ok
end,
fun(Trace0) ->
Trace1 = trace_between_span(Trace0, async_query_sync_driver),
ct:pal("async query calling sync driver\n ~p", [Trace1]),
?assert(
?strict_causality(
#{?snk_kind := async_query, request := {inc_counter, 1}},
#{?snk_kind := call_query, call_mode := sync},
Trace1
)
),
Trace2 = trace_between_span(Trace0, sync_query_async_driver),
ct:pal("sync query calling async driver\n ~p", [Trace2]),
?assert(
?strict_causality(
#{?snk_kind := sync_query, request := {inc_counter, 2}},
#{?snk_kind := call_query_async},
Trace2
)
),
ok
end
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -2742,3 +2820,8 @@ assert_async_retry_fail_then_succeed_inflight(Trace) ->
) )
), ),
ok. ok.
trace_between_span(Trace0, Marker) ->
{Trace1, [_ | _]} = ?split_trace_at(#{?snk_kind := Marker, ?snk_span := {complete, _}}, Trace0),
{[_ | _], [_ | Trace2]} = ?split_trace_at(#{?snk_kind := Marker, ?snk_span := start}, Trace1),
Trace2.

View File

@ -1,23 +1,46 @@
# emqx-rule-engine # Emqx Rule Engine
IoT Rule Engine The rule engine's goal is to provide a simple and flexible way to transform and
reroute the messages coming to the EMQX broker. For example, one message
containing measurements from multiple sensors of different types can be
transformed into multiple messages.
## Concepts
A rule is quite simple. A rule describes which messages it affects by
specifying a topic filter and a set of conditions that need to be met. If a
message matches the topic filter and all the conditions are met, the rule is
triggered. The rule can then transform the message and route it to a different
topic, or send it to another service (defined by an EMQX bridge). The rule
engine's message data transformation is designed to work well with structured data
such as JSON, avro, and protobuf.
A rule consists of the three parts **MATCH**, **TRANSFORM** and **ACTIONS** that are
described below:
* **MATCH** - The rule's trigger condition. The rule is triggered when a message
arrives that matches the topic filter and all the specified conditions are met.
* **TRANSFORM** - The rule's data transformation. The rule can select data from the
incoming message and transform it into a new message.
* **ACTIONS** - The rule's action(s). The rule can have one or more actions. The
actions are executed when the rule is triggered. The actions can be to route
the message to a different topic, or send it to another service (defined by
an EMQX bridge).
## Concept
```
iot rule "Rule Name"
when
match TopicFilters and Conditions
select
para1 = val1
para2 = val2
then
take action(#{para2 => val1, #para2 => val2})
```
## Architecture ## Architecture
The following diagram shows how the rule engine is integrated with the EMQX
message broker. Incoming messages are checked against the rules, and if a rule
matches, it is triggered with the message as input. The rule can then transform
or split the message and/or route it to a different topic, or send it to another
service (defined by an EMQX bridge).
``` ```
|-----------------| |-----------------|
Pub ---->| Message Routing |----> Sub Pub ---->| Message Routing |----> Sub
@ -28,11 +51,33 @@ iot rule "Rule Name"
| Rule Engine | | Rule Engine |
|-----------------| |-----------------|
| | | |
Backends Services Bridges Services Bridges (defined by EMQX bridges)
``` ```
## SQL for Rule query statement ## Domain Specific Language for Rules
The **MATCH** and **TRANSFORM** parts of the rule are specified using a domain
specific language that looks similar to SQL. The following is an example of a
rule engine statement. The `from "topic/a"` part specifies the topic filter
(only messages to the topic `topic/a` will be considered). The `where t > 50`
part specifies the condition that needs to be met for the rule to be triggered.
The `select id, time, temperature as t` part specifies the data transformation
(the selected fields will remain in the transformed message payload). The `as
t` part specifies that the `temperature` field name is changed to `t` in the
output message. The name `t` can also be used in the where part of the rule as
an alias for `t`.
``` ```
select id, time, temperature as t from "topic/a" where t > 50; select id, time, temperature as t from "topic/a" where t > 50
``` ```
This just scratches the surface of what is possible with the rule engine. The
full documentation is available at [EMQX Rule
Engine](https://www.emqx.io/docs/en/v5.0/data-integration/rules.html). For
example, there are many built-in functions that can be used in the rule engine
language to help in doing transformations and matching. One of the [built-in
functions allows you to run JQ
queries](https://www.emqx.io/docs/en/v5.0/data-integration/rule-sql-jq.html)
which allows you to do complex transformations of the message.

View File

@ -180,7 +180,7 @@ schema("/rules") ->
ref(emqx_dashboard_swagger, page), ref(emqx_dashboard_swagger, page),
ref(emqx_dashboard_swagger, limit) ref(emqx_dashboard_swagger, limit)
], ],
summary => <<"List Rules">>, summary => <<"List rules">>,
responses => #{ responses => #{
200 => 200 =>
[ [
@ -193,7 +193,7 @@ schema("/rules") ->
post => #{ post => #{
tags => [<<"rules">>], tags => [<<"rules">>],
description => ?DESC("api2"), description => ?DESC("api2"),
summary => <<"Create a Rule">>, summary => <<"Create a rule">>,
'requestBody' => rule_creation_schema(), 'requestBody' => rule_creation_schema(),
responses => #{ responses => #{
400 => error_schema('BAD_REQUEST', "Invalid Parameters"), 400 => error_schema('BAD_REQUEST', "Invalid Parameters"),
@ -207,7 +207,7 @@ schema("/rule_events") ->
get => #{ get => #{
tags => [<<"rules">>], tags => [<<"rules">>],
description => ?DESC("api3"), description => ?DESC("api3"),
summary => <<"List Events">>, summary => <<"List rule events">>,
responses => #{ responses => #{
200 => mk(ref(emqx_rule_api_schema, "rule_events"), #{}) 200 => mk(ref(emqx_rule_api_schema, "rule_events"), #{})
} }
@ -219,7 +219,7 @@ schema("/rules/:id") ->
get => #{ get => #{
tags => [<<"rules">>], tags => [<<"rules">>],
description => ?DESC("api4"), description => ?DESC("api4"),
summary => <<"Get a Rule">>, summary => <<"Get rule">>,
parameters => param_path_id(), parameters => param_path_id(),
responses => #{ responses => #{
404 => error_schema('NOT_FOUND', "Rule not found"), 404 => error_schema('NOT_FOUND', "Rule not found"),
@ -229,7 +229,7 @@ schema("/rules/:id") ->
put => #{ put => #{
tags => [<<"rules">>], tags => [<<"rules">>],
description => ?DESC("api5"), description => ?DESC("api5"),
summary => <<"Update a Rule">>, summary => <<"Update rule">>,
parameters => param_path_id(), parameters => param_path_id(),
'requestBody' => rule_creation_schema(), 'requestBody' => rule_creation_schema(),
responses => #{ responses => #{
@ -240,7 +240,7 @@ schema("/rules/:id") ->
delete => #{ delete => #{
tags => [<<"rules">>], tags => [<<"rules">>],
description => ?DESC("api6"), description => ?DESC("api6"),
summary => <<"Delete a Rule">>, summary => <<"Delete rule">>,
parameters => param_path_id(), parameters => param_path_id(),
responses => #{ responses => #{
204 => <<"Delete rule successfully">> 204 => <<"Delete rule successfully">>
@ -253,7 +253,7 @@ schema("/rules/:id/metrics") ->
get => #{ get => #{
tags => [<<"rules">>], tags => [<<"rules">>],
description => ?DESC("api4_1"), description => ?DESC("api4_1"),
summary => <<"Get a Rule's Metrics">>, summary => <<"Get rule metrics">>,
parameters => param_path_id(), parameters => param_path_id(),
responses => #{ responses => #{
404 => error_schema('NOT_FOUND', "Rule not found"), 404 => error_schema('NOT_FOUND', "Rule not found"),
@ -267,7 +267,7 @@ schema("/rules/:id/metrics/reset") ->
put => #{ put => #{
tags => [<<"rules">>], tags => [<<"rules">>],
description => ?DESC("api7"), description => ?DESC("api7"),
summary => <<"Reset a Rule Metrics">>, summary => <<"Reset rule metrics">>,
parameters => param_path_id(), parameters => param_path_id(),
responses => #{ responses => #{
404 => error_schema('NOT_FOUND', "Rule not found"), 404 => error_schema('NOT_FOUND', "Rule not found"),
@ -281,7 +281,7 @@ schema("/rule_test") ->
post => #{ post => #{
tags => [<<"rules">>], tags => [<<"rules">>],
description => ?DESC("api8"), description => ?DESC("api8"),
summary => <<"Test a Rule">>, summary => <<"Test a rule">>,
'requestBody' => rule_test_schema(), 'requestBody' => rule_test_schema(),
responses => #{ responses => #{
400 => error_schema('BAD_REQUEST', "Invalid Parameters"), 400 => error_schema('BAD_REQUEST', "Invalid Parameters"),

25
build
View File

@ -147,7 +147,7 @@ make_rel() {
make_elixir_rel() { make_elixir_rel() {
./scripts/pre-compile.sh "$PROFILE" ./scripts/pre-compile.sh "$PROFILE"
export_release_vars "$PROFILE" export_elixir_release_vars "$PROFILE"
# for some reason, this has to be run outside "do"... # for some reason, this has to be run outside "do"...
mix local.rebar --if-missing --force mix local.rebar --if-missing --force
# shellcheck disable=SC1010 # shellcheck disable=SC1010
@ -362,7 +362,7 @@ function join {
# used to control the Elixir Mix Release output # used to control the Elixir Mix Release output
# see docstring in `mix.exs` # see docstring in `mix.exs`
export_release_vars() { export_elixir_release_vars() {
local profile="$1" local profile="$1"
case "$profile" in case "$profile" in
emqx|emqx-enterprise) emqx|emqx-enterprise)
@ -376,27 +376,6 @@ export_release_vars() {
exit 1 exit 1
esac esac
export MIX_ENV="$profile" export MIX_ENV="$profile"
local erl_opts=()
case "$(is_enterprise "$profile")" in
'yes')
erl_opts+=( "{d, 'EMQX_RELEASE_EDITION', ee}" )
;;
'no')
erl_opts+=( "{d, 'EMQX_RELEASE_EDITION', ce}" )
;;
esac
# At this time, Mix provides no easy way to pass `erl_opts' to
# dependencies. The workaround is to set this variable before
# compiling the project, so that `emqx_release.erl' picks up
# `emqx_vsn' as if it was compiled by rebar3.
erl_opts+=( "{compile_info,[{emqx_vsn,\"${PKG_VSN}\"}]}" )
erl_opts+=( "{d,snk_kind,msg}" )
ERL_COMPILER_OPTIONS="[$(join , "${erl_opts[@]}")]"
export ERL_COMPILER_OPTIONS
} }
log "building artifact=$ARTIFACT for profile=$PROFILE" log "building artifact=$ARTIFACT for profile=$PROFILE"

View File

@ -1 +0,0 @@
Add low level tuning settings for QUIC listeners.

View File

@ -1 +0,0 @@
为 QUIC 监听器添加更多底层调优选项。

View File

@ -1 +0,0 @@
Start releasing Rocky Linux 9 (compatible with Enterprise Linux 9) and MacOS 12 packages

View File

@ -1 +0,0 @@
Errors returned by rule engine API are formatted in a more human readable way rather than dumping the raw error including the stacktrace.

View File

@ -1 +0,0 @@
规则引擎 API 返回用户可读的错误信息而不是原始的栈追踪信息。

Some files were not shown because too many files have changed in this diff Show More