diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index d14974679..47994c9ad 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -39,7 +39,7 @@ emqx_test(){ unzip -q "${PACKAGE_PATH}/${packagename}" export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 \ EMQX_MQTT__MAX_TOPIC_ALIAS=10 - sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins + # sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins echo "running ${packagename} start" if ! "${PACKAGE_PATH}"/emqx/bin/emqx start; then @@ -115,7 +115,7 @@ emqx_test(){ running_test(){ export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 \ EMQX_MQTT__MAX_TOPIC_ALIAS=10 - sed -i '/emqx_telemetry/d' /var/lib/emqx/loaded_plugins + # sed -i '/emqx_telemetry/d' /var/lib/emqx/loaded_plugins if ! emqx start; then cat /var/log/emqx/erlang.log.1 || true diff --git a/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml b/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml index 6bc8e67e2..81d48aba7 100644 --- a/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml +++ b/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml @@ -38,7 +38,7 @@ services: - -c - | sed -i "s 127.0.0.1 $$(ip route show |grep "link" |awk '{print $$1}') g" /opt/emqx/etc/acl.conf - sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins + # sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins /opt/emqx/bin/emqx foreground healthcheck: test: ["CMD", "/opt/emqx/bin/emqx_ctl", "status"] @@ -62,7 +62,7 @@ services: - -c - | sed -i "s 127.0.0.1 $$(ip route show |grep "link" |awk '{print $$1}') g" /opt/emqx/etc/acl.conf - sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins + # sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins /opt/emqx/bin/emqx foreground healthcheck: test: ["CMD", "/opt/emqx/bin/emqx", "ping"] diff --git a/.ci/docker-compose-file/docker-compose-mongo-replicaset-tcp.yaml b/.ci/docker-compose-file/docker-compose-mongo-replicaset-tcp.yaml new file mode 100644 index 000000000..f83fe0932 --- /dev/null +++ b/.ci/docker-compose-file/docker-compose-mongo-replicaset-tcp.yaml @@ -0,0 +1,81 @@ +version: "3" + +services: + mongo1: + hostname: mongo1 + container_name: mongo1 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27011:27017 + restart: always + command: + --ipv6 + --bind_ip_all + --replSet rs0 + + mongo2: + hostname: mongo2 + container_name: mongo2 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27012:27017 + restart: always + command: + --ipv6 + --bind_ip_all + --replSet rs0 + + mongo3: + hostname: mongo3 + container_name: mongo3 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27013:27017 + restart: always + command: + --ipv6 + --bind_ip_all + --replSet rs0 + + mongo_client: + image: mongo:${MONGO_TAG} + container_name: mongo_client + networks: + - emqx_bridge + depends_on: + - mongo1 + - mongo2 + - mongo3 + command: + - /bin/bash + - -c + - | + while ! mongo --host mongo1 --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + while ! mongo --host mongo2 --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + while ! mongo --host mongo3 --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + mongo --host mongo1 --eval "rs.initiate( { _id : 'rs0', members: [ { _id : 0, host : 'mongo1:27017' }, { _id : 1, host : 'mongo2:27017' }, { _id : 2, host : 'mongo3:27017' } ] })" --quiet + mongo --host mongo1 --eval "rs.status()" --quiet diff --git a/.ci/docker-compose-file/docker-compose-mongo-replicaset-tls.yaml b/.ci/docker-compose-file/docker-compose-mongo-replicaset-tls.yaml new file mode 100644 index 000000000..be8f7ea21 --- /dev/null +++ b/.ci/docker-compose-file/docker-compose-mongo-replicaset-tls.yaml @@ -0,0 +1,98 @@ +version: "3" + +services: + mongo1: + hostname: mongo1 + container_name: mongo1 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27011:27017 + restart: always + volumes: + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem + command: + - /bin/bash + - -c + - | + cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem + mongod --ipv6 --bind_ip_all --tlsMode requireTLS --tlsCertificateKeyFile /etc/certs/mongodb.pem --replSet rs0 + + mongo2: + hostname: mongo2 + container_name: mongo2 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27012:27017 + restart: always + volumes: + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem + command: + - /bin/bash + - -c + - | + cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem + mongod --ipv6 --bind_ip_all --tlsMode requireTLS --tlsCertificateKeyFile /etc/certs/mongodb.pem --replSet rs0 + + mongo3: + hostname: mongo3 + container_name: mongo3 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27013:27017 + restart: always + volumes: + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem + command: + - /bin/bash + - -c + - | + cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem + mongod --ipv6 --bind_ip_all --tlsMode requireTLS --tlsCertificateKeyFile /etc/certs/mongodb.pem --replSet rs0 + + mongo_client: + image: mongo:${MONGO_TAG} + container_name: mongo_client + networks: + - emqx_bridge + depends_on: + - mongo1 + - mongo2 + - mongo3 + volumes: + - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/cacert.pem + command: + - /bin/bash + - -c + - | + while ! mongo --host mongo1 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + while ! mongo --host mongo2 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + while ! mongo --host mongo3 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + mongo --host mongo1 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval "rs.initiate( { _id : 'rs0', members: [ { _id : 0, host : 'mongo1:27017' }, { _id : 1, host : 'mongo2:27017' }, { _id : 2, host : 'mongo3:27017' } ] })" --quiet + mongo --host mongo1 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval "rs.status()" --quiet diff --git a/.ci/docker-compose-file/docker-compose-mongo-tcp.yaml b/.ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml similarity index 88% rename from .ci/docker-compose-file/docker-compose-mongo-tcp.yaml rename to .ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml index dee2daff6..494b42ce4 100644 --- a/.ci/docker-compose-file/docker-compose-mongo-tcp.yaml +++ b/.ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml @@ -9,6 +9,8 @@ services: MONGO_INITDB_DATABASE: mqtt networks: - emqx_bridge + ports: + - "27017:27017" command: --ipv6 --bind_ip_all diff --git a/.ci/docker-compose-file/docker-compose-mongo-single-tls.yaml b/.ci/docker-compose-file/docker-compose-mongo-single-tls.yaml new file mode 100644 index 000000000..c4f162783 --- /dev/null +++ b/.ci/docker-compose-file/docker-compose-mongo-single-tls.yaml @@ -0,0 +1,23 @@ +version: '3.9' + +services: + mongo_server: + container_name: mongo + image: mongo:${MONGO_TAG} + restart: always + environment: + MONGO_INITDB_DATABASE: mqtt + volumes: + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem + networks: + - emqx_bridge + ports: + - "27017:27017" + command: + - /bin/bash + - -c + - | + cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem + mongod --ipv6 --bind_ip_all --sslMode requireSSL --sslPEMKeyFile /etc/certs/mongodb.pem + diff --git a/.ci/docker-compose-file/docker-compose-mongo-tls.yaml b/.ci/docker-compose-file/docker-compose-mongo-tls.yaml deleted file mode 100644 index a09bc803d..000000000 --- a/.ci/docker-compose-file/docker-compose-mongo-tls.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: '3.9' - -services: - mongo_server: - container_name: mongo - image: mongo:${MONGO_TAG} - restart: always - environment: - MONGO_INITDB_DATABASE: mqtt - volumes: - - ../../apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/mongodb.pem/:/etc/certs/mongodb.pem - networks: - - emqx_bridge - command: - --ipv6 - --bind_ip_all - --sslMode requireSSL - --sslPEMKeyFile /etc/certs/mongodb.pem diff --git a/.ci/docker-compose-file/docker-compose-mysql-tls.yaml b/.ci/docker-compose-file/docker-compose-mysql-tls.yaml index c4d5bd500..17dfdcc8e 100644 --- a/.ci/docker-compose-file/docker-compose-mysql-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-mysql-tls.yaml @@ -11,9 +11,11 @@ services: MYSQL_USER: ssluser MYSQL_PASSWORD: public volumes: - - ../../apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/ca.pem:/etc/certs/ca-cert.pem - - ../../apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-cert.pem:/etc/certs/server-cert.pem - - ../../apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-key.pem:/etc/certs/server-key.pem + - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca-cert.pem + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/server-cert.pem + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/server-key.pem + ports: + - "3306:3306" networks: - emqx_bridge command: diff --git a/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml b/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml index 78b655946..c5cefd9e6 100644 --- a/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml @@ -5,7 +5,9 @@ services: container_name: redis image: redis:${REDIS_TAG} volumes: - - ../../apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs:/tls + - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key - ./redis/:/data/conf command: bash -c "/bin/bash /data/conf/redis.sh --node cluster --tls-enabled && tail -f /var/log/redis-server.log" networks: diff --git a/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml b/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml index 7c7f46ce2..045570d5c 100644 --- a/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml @@ -5,7 +5,9 @@ services: container_name: redis image: redis:${REDIS_TAG} volumes: - - ../../apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs:/tls + - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key - ./redis/:/data/conf command: bash -c "/bin/bash /data/conf/redis.sh --node sentinel --tls-enabled && tail -f /var/log/redis-server.log" networks: diff --git a/.ci/docker-compose-file/docker-compose-redis-single-tls.yaml b/.ci/docker-compose-file/docker-compose-redis-single-tls.yaml index 814a0f1cb..bb6c3ff15 100644 --- a/.ci/docker-compose-file/docker-compose-redis-single-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-redis-single-tls.yaml @@ -5,15 +5,17 @@ services: container_name: redis image: redis:${REDIS_TAG} volumes: - - ../../apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs:/tls + - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key command: - redis-server - "--bind 0.0.0.0 ::" - --requirepass public - --tls-port 6380 - - --tls-cert-file /tls/redis.crt - - --tls-key-file /tls/redis.key - - --tls-ca-cert-file /tls/ca.crt + - --tls-cert-file /etc/certs/redis.crt + - --tls-key-file /etc/certs/redis.key + - --tls-ca-cert-file /etc/certs/ca.crt restart: always networks: - emqx_bridge diff --git a/.ci/docker-compose-file/pgsql/Dockerfile b/.ci/docker-compose-file/pgsql/Dockerfile index e4c973258..db2cd59fe 100644 --- a/.ci/docker-compose-file/pgsql/Dockerfile +++ b/.ci/docker-compose-file/pgsql/Dockerfile @@ -2,9 +2,9 @@ ARG BUILD_FROM=postgres:11 FROM ${BUILD_FROM} ARG POSTGRES_USER=postgres COPY --chown=$POSTGRES_USER .ci/docker-compose-file/pgsql/pg_hba.conf /var/lib/postgresql/pg_hba.conf -COPY --chown=$POSTGRES_USER apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-key.pem /var/lib/postgresql/server.key -COPY --chown=$POSTGRES_USER apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-cert.pem /var/lib/postgresql/server.crt -COPY --chown=$POSTGRES_USER apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/ca.pem /var/lib/postgresql/root.crt +COPY --chown=$POSTGRES_USER apps/emqx/etc/certs/key.pem /var/lib/postgresql/server.key +COPY --chown=$POSTGRES_USER apps/emqx/etc/certs/cert.pem /var/lib/postgresql/server.crt +COPY --chown=$POSTGRES_USER apps/emqx/etc/certs/cacert.pem /var/lib/postgresql/root.crt RUN chmod 600 /var/lib/postgresql/pg_hba.conf RUN chmod 600 /var/lib/postgresql/server.key RUN chmod 600 /var/lib/postgresql/server.crt diff --git a/.ci/docker-compose-file/redis/redis-tls.conf b/.ci/docker-compose-file/redis/redis-tls.conf index 325c200c3..e304c814f 100644 --- a/.ci/docker-compose-file/redis/redis-tls.conf +++ b/.ci/docker-compose-file/redis/redis-tls.conf @@ -1,9 +1,9 @@ daemonize yes bind 0.0.0.0 :: logfile /var/log/redis-server.log -tls-cert-file /tls/redis.crt -tls-key-file /tls/redis.key -tls-ca-cert-file /tls/ca.crt +tls-cert-file /etc/certs/redis.crt +tls-key-file /etc/certs/redis.key +tls-ca-cert-file /etc/certs/ca.crt tls-replication yes tls-cluster yes protected-mode no diff --git a/.ci/docker-compose-file/redis/redis.sh b/.ci/docker-compose-file/redis/redis.sh index 272a5b443..6cc7ce98b 100755 --- a/.ci/docker-compose-file/redis/redis.sh +++ b/.ci/docker-compose-file/redis/redis.sh @@ -91,7 +91,7 @@ do fi if [ "${node}" = "cluster" ] ; then if $tls ; then - yes "yes" | redis-cli --cluster create "$LOCAL_IP:8000" "$LOCAL_IP:8001" "$LOCAL_IP:8002" --pass public --no-auth-warning --tls true --cacert /tls/ca.crt --cert /tls/redis.crt --key /tls/redis.key; + yes "yes" | redis-cli --cluster create "$LOCAL_IP:8000" "$LOCAL_IP:8001" "$LOCAL_IP:8002" --pass public --no-auth-warning --tls true --cacert /etc/certs/ca.crt --cert /etc/certs/redis.crt --key /etc/certs/redis.key; else yes "yes" | redis-cli --cluster create "$LOCAL_IP:7000" "$LOCAL_IP:7001" "$LOCAL_IP:7002" --pass public --no-auth-warning; fi @@ -107,9 +107,9 @@ EOF cat >>/_sentinel.conf< log/emqx.log.1 ?SH-PROMPT @@ -120,6 +124,10 @@ """ ?SH-PROMPT + !./bin/emqx_ctl plugins list | grep emqx_management + ?Plugin\(emqx_management.*active=true\) + ?SH-PROMPT + [shell bench] ???publish complete ??SH-PROMPT: diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 0258866dd..96d193913 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -3,7 +3,6 @@ name: Bug Report about: Create a report to help us improve title: '' labels: Support -assignees: tigercl --- diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 67b1dfa82..0519e5699 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -3,7 +3,6 @@ name: Feature Request about: Suggest an idea for this project title: '' labels: Feature -assignees: tigercl --- diff --git a/.github/ISSUE_TEMPLATE/support-needed.md b/.github/ISSUE_TEMPLATE/support-needed.md index 80b494077..18b47bfb5 100644 --- a/.github/ISSUE_TEMPLATE/support-needed.md +++ b/.github/ISSUE_TEMPLATE/support-needed.md @@ -3,7 +3,6 @@ name: Support Needed about: Asking a question about usages, docs or anything you're insterested in title: '' labels: Support -assignees: tigercl --- diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 2090f722e..99ea45d29 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -83,6 +83,7 @@ jobs: - name: build env: PYTHON: python + DIAGNOSTIC: 1 run: | $env:PATH = "${{ steps.install_erlang.outputs.erlpath }}\bin;$env:PATH" @@ -168,19 +169,21 @@ jobs: - name: build run: | . $HOME/.kerl/${{ matrix.erl_otp }}/activate - make -C source ensure-rebar3 - sudo cp source/rebar3 /usr/local/bin/rebar3 - make -C source ${{ matrix.profile }}-zip + cd source + make ensure-rebar3 + sudo cp rebar3 /usr/local/bin/rebar3 + rm -rf _build/${{ matrix.profile }}/lib + make ${{ matrix.profile }}-zip - name: test run: | cd source pkg_name=$(basename _packages/${{ matrix.profile }}/${{ matrix.profile }}-*.zip) unzip -q _packages/${{ matrix.profile }}/$pkg_name - gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins + # gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' for i in {1..10}; do - if curl -fs 127.0.0.1:18083 > /dev/null; then + if curl -fs 127.0.0.1:8081/status > /dev/null; then ready='yes' break fi @@ -339,13 +342,6 @@ jobs: - [amd64, x86_64] - [arm64v8, aarch64] - [arm32v7, arm] - - [i386, i386] - - [s390x, s390x] - exclude: - - profile: emqx-ee - arch: [i386, i386] - - profile: emqx-ee - arch: [s390x, s390x] steps: - uses: actions/download-artifact@v2 diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 6c9bbf04a..30768e023 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -38,6 +38,11 @@ jobs: run: make ${EMQX_NAME}-zip - name: build deb/rpm packages run: make ${EMQX_NAME}-pkg + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: rebar3.crashdump + path: ./rebar3.crashdump - name: pakcages test run: | export CODE_PATH=$GITHUB_WORKSPACE @@ -94,15 +99,20 @@ jobs: make ensure-rebar3 sudo cp rebar3 /usr/local/bin/rebar3 make ${EMQX_NAME}-zip + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: rebar3.crashdump + path: ./rebar3.crashdump - name: test run: | pkg_name=$(basename _packages/${EMQX_NAME}/emqx-*.zip) unzip -q _packages/${EMQX_NAME}/$pkg_name - gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins + # gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' for i in {1..10}; do - if curl -fs 127.0.0.1:18083 > /dev/null; then + if curl -fs 127.0.0.1:8081/status > /dev/null; then ready='yes' break fi diff --git a/.github/workflows/run_cts_tests.yaml b/.github/workflows/run_cts_tests.yaml deleted file mode 100644 index 487d8fae7..000000000 --- a/.github/workflows/run_cts_tests.yaml +++ /dev/null @@ -1,407 +0,0 @@ -name: Compatibility Test Suite - -on: - push: - tags: - - v* - - e* - pull_request: - -jobs: - ldap: - runs-on: ubuntu-20.04 - - strategy: - fail-fast: false - matrix: - ldap_tag: - - 2.4.50 - network_type: - - ipv4 - - ipv6 - - steps: - - uses: actions/checkout@v1 - - name: docker compose up - env: - LDAP_TAG: ${{ matrix.ldap_tag }} - run: | - docker-compose \ - -f .ci/docker-compose-file/docker-compose-ldap-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose.yaml \ - up -d --build - - name: setup - if: matrix.network_type == 'ipv4' - run: | - echo EMQX_AUTH__LDAP__SERVERS=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ldap) >> "$GITHUB_ENV" - - name: setup - if: matrix.network_type == 'ipv6' - run: | - echo EMQX_AUTH__LDAP__SERVERS=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}' ldap) >> "$GITHUB_ENV" - - name: set git token - run: | - if make emqx-ee --dry-run > /dev/null 2>&1; then - docker exec -i erlang bash -c "echo \"https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com\" > /root/.git-credentials && git config --global credential.helper store" - fi - - name: run test cases - run: | - export CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_ - export HOCON_ENV_OVERRIDE_PREFIX=EMQX_ - printenv > .env - docker exec -i erlang sh -c "make ensure-rebar3" - docker exec -i erlang sh -c "./rebar3 eunit --application=emqx_auth_ldap" - docker exec --env-file .env -i erlang sh -c "make apps/emqx_auth_ldap-ct" - - uses: actions/upload-artifact@v1 - if: failure() - with: - name: logs_ldap${{ matrix.ldap_tag }}_${{ matrix.network_type }} - path: _build/test/logs - - mongo: - runs-on: ubuntu-20.04 - - strategy: - fail-fast: false - matrix: - mongo_tag: - - 3 - - 4 - network_type: - - ipv4 - - ipv6 - connect_type: - - tls - - tcp - - steps: - - uses: actions/checkout@v1 - - name: docker-compose up - run: | - docker-compose \ - -f .ci/docker-compose-file/docker-compose-mongo-${{ matrix.connect_type }}.yaml \ - -f .ci/docker-compose-file/docker-compose.yaml \ - up -d --build - - name: setup - env: - MONGO_TAG: ${{ matrix.mongo_tag }} - if: matrix.connect_type == 'tls' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__MONGO__SSL__ENABLE=on - EMQX_AUTH__MONGO__SSL__CACERTFILE=/emqx/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca.pem - EMQX_AUTH__MONGO__SSL__CERTFILE=/emqx/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-cert.pem - EMQX_AUTH__MONGO__SSL__KEYFILE=/emqx/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-key.pem - EMQX_AUTH__MONGO__SSL__VERIFY=true - EMQX_AUTH__MONGO__SSL__SERVER_NAME_INDICATION=disable - EOF - - name: setup - env: - MONGO_TAG: ${{ matrix.mongo_tag }} - if: matrix.connect_type == 'tcp' - run: | - echo EMQX_AUTH__MONGO__SSL__ENABLE=off >> "$GITHUB_ENV" - - name: setup - if: matrix.network_type == 'ipv4' - run: | - echo "EMQX_AUTH__MONGO__SERVER=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mongo):27017" >> "$GITHUB_ENV" - - name: setup - if: matrix.network_type == 'ipv6' - run: | - echo "EMQX_AUTH__MONGO__SERVER=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}' mongo):27017" >> "$GITHUB_ENV" - - name: set git token - run: | - if make emqx-ee --dry-run > /dev/null 2>&1; then - docker exec -i erlang bash -c "echo \"https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com\" > /root/.git-credentials && git config --global credential.helper store" - fi - - name: run test cases - run: | - export CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_ - export HOCON_ENV_OVERRIDE_PREFIX=EMQX_ - printenv > .env - docker exec -i erlang sh -c "make ensure-rebar3" - docker exec -i erlang sh -c "./rebar3 eunit --application=emqx_auth_mongo" - docker exec --env-file .env -i erlang sh -c "make apps/emqx_auth_mongo-ct" - - uses: actions/upload-artifact@v1 - if: failure() - with: - name: logs_mongo${{ matrix.mongo_tag }}_${{ matrix.network_type }}_${{ matrix.connect_type }} - path: _build/test/logs - - mysql: - runs-on: ubuntu-20.04 - - strategy: - fail-fast: false - matrix: - mysql_tag: - - 5.7 - - 8 - network_type: - - ipv4 - - ipv6 - connect_type: - - tls - - tcp - - steps: - - uses: actions/checkout@v1 - - name: docker-compose up - timeout-minutes: 5 - run: | - docker-compose \ - -f .ci/docker-compose-file/docker-compose-mysql-${{ matrix.connect_type }}.yaml \ - -f .ci/docker-compose-file/docker-compose.yaml \ - up -d --build - while [ $(docker ps -a --filter name=client --filter exited=0 | wc -l) \ - != $(docker ps -a --filter name=client | wc -l) ]; do - sleep 5 - done - - name: setup - env: - MYSQL_TAG: ${{ matrix.mysql_tag }} - if: matrix.connect_type == 'tls' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__MYSQL__SSL__ENABLE=on - EMQX_AUTH__MYSQL__USERNAME=ssluser - EMQX_AUTH__MYSQL__PASSWORD=public - EMQX_AUTH__MYSQL__DATABASE=mqtt - EMQX_AUTH__MYSQL__SSL__CACERTFILE=/emqx/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/ca.pem - EMQX_AUTH__MYSQL__SSL__CERTFILE=/emqx/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/client-cert.pem - EMQX_AUTH__MYSQL__SSL__KEYFILE=/emqx/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/client-key.pem - EMQX_AUTH__MYSQL__SSL__VERIFY=true - EMQX_AUTH__MYSQL__SSL__SERVER_NAME_INDICATION=disable - EOF - - name: setup - env: - MYSQL_TAG: ${{ matrix.mysql_tag }} - if: matrix.connect_type == 'tcp' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__MYSQL__USERNAME=root - EMQX_AUTH__MYSQL__PASSWORD=public - EMQX_AUTH__MYSQL__DATABASE=mqtt - EMQX_AUTH__MYSQL__SSL__ENABLE=off - EOF - - name: setup - if: matrix.network_type == 'ipv4' - run: | - echo "EMQX_AUTH__MYSQL__SERVER=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mysql):3306" >> "$GITHUB_ENV" - - name: setup - if: matrix.network_type == 'ipv6' - run: | - echo "EMQX_AUTH__MYSQL__SERVER=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}' mysql):3306" >> "$GITHUB_ENV" - - name: set git token - run: | - if make emqx-ee --dry-run > /dev/null 2>&1; then - docker exec -i erlang bash -c "echo \"https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com\" > /root/.git-credentials && git config --global credential.helper store" - fi - - name: run test cases - run: | - export CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_ - export HOCON_ENV_OVERRIDE_PREFIX=EMQX_ - printenv > .env - docker exec -i erlang sh -c "make ensure-rebar3" - docker exec -i erlang sh -c "./rebar3 eunit --application=emqx_auth_mysql" - docker exec --env-file .env -i erlang sh -c "make apps/emqx_auth_mysql-ct" - - uses: actions/upload-artifact@v1 - if: failure() - with: - name: logs_mysql${{ matrix.mysql_tag }}_${{ matrix.network_type }}_${{ matrix.connect_type }} - path: _build/test/logs - - pgsql: - runs-on: ubuntu-20.04 - - strategy: - fail-fast: false - matrix: - pgsql_tag: - - 9 - - 10 - - 11 - - 12 - - 13 - network_type: - - ipv4 - - ipv6 - connect_type: - - tls - - tcp - steps: - - uses: actions/checkout@v1 - - name: docker-compose up - run: | - docker-compose \ - -f .ci/docker-compose-file/docker-compose-pgsql-${{ matrix.connect_type }}.yaml \ - -f .ci/docker-compose-file/docker-compose.yaml \ - up -d --build - - name: setup - env: - PGSQL_TAG: ${{ matrix.pgsql_tag }} - if: matrix.connect_type == 'tls' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__PGSQL__SSL__ENABLE=on - EMQX_AUTH__PGSQL__SSL__CACERTFILE=/emqx/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/ca.pem - EMQX_AUTH__PGSQL__SSL__CERTFILE=/emqx/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/client-cert.pem - EMQX_AUTH__PGSQL__SSL__KEYFILE=/emqx/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/client-key.pem - EMQX_AUTH__PGSQL__SSL__VERIFY=true - EMQX_AUTH__PGSQL__SSL__SERVER_NAME_INDICATION=disable - EOF - - name: setup - env: - PGSQL_TAG: ${{ matrix.pgsql_tag }} - if: matrix.connect_type == 'tcp' - run: | - echo EMQX_AUTH__PGSQL__SSL__ENABLE=off >> "$GITHUB_ENV" - - name: setup - if: matrix.network_type == 'ipv4' - run: | - echo "EMQX_AUTH__PGSQL__SERVER=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' pgsql):5432" >> "$GITHUB_ENV" - - name: setup - if: matrix.network_type == 'ipv6' - run: | - echo "EMQX_AUTH__PGSQL__SERVER=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}' pgsql):5432" >> "$GITHUB_ENV" - - name: set git token - run: | - if make emqx-ee --dry-run > /dev/null 2>&1; then - docker exec -i erlang bash -c "echo \"https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com\" > /root/.git-credentials && git config --global credential.helper store" - fi - - name: run test cases - run: | - export EMQX_AUTH__PGSQL__USERNAME=root \ - EMQX_AUTH__PGSQL__PASSWORD=public \ - EMQX_AUTH__PGSQL__DATABASE=mqtt \ - CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_ \ - HOCON_ENV_OVERRIDE_PREFIX=EMQX_ - printenv > .env - docker exec -i erlang sh -c "make ensure-rebar3" - docker exec -i erlang sh -c "./rebar3 eunit --application=emqx_auth_pgsql" - docker exec --env-file .env -i erlang sh -c "make apps/emqx_auth_pgsql-ct" - - uses: actions/upload-artifact@v1 - if: failure() - with: - name: logs_pgsql${{ matrix.pgsql_tag }}_${{ matrix.network_type }}_${{ matrix.connect_type }} - path: _build/test/logs - - redis: - runs-on: ubuntu-20.04 - - strategy: - fail-fast: false - matrix: - redis_tag: - - 5 - - 6 - network_type: - - ipv4 - - ipv6 - connect_type: - - tls - - tcp - node_type: - - single - - sentinel - - cluster - exclude: - - redis_tag: 5 - connect_type: tls - - steps: - - uses: actions/checkout@v1 - - name: docker-compose up - run: | - docker-compose \ - -f .ci/docker-compose-file/docker-compose-redis-${{ matrix.node_type }}-${{ matrix.connect_type }}.yaml \ - -f .ci/docker-compose-file/docker-compose.yaml \ - up -d --build - - name: setup - env: - REDIS_TAG: ${{ matrix.redis_tag }} - if: matrix.connect_type == 'tls' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__REDIS__SSL__ENABLE=on - EMQX_AUTH__REDIS__SSL__CACERTFILE=/emqx/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/ca.crt - EMQX_AUTH__REDIS__SSL__CERTFILE=/emqx/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.crt - EMQX_AUTH__REDIS__SSL__KEYFILE=/emqx/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.key - EMQX_AUTH__REDIS__SSL__VERIFY=true - EMQX_AUTH__REDIS__SSL__SERVER_NAME_INDICATION=disable - EOF - - name: setup - env: - REDIS_TAG: ${{ matrix.redis_tag }} - if: matrix.connect_type == 'tcp' - run: | - echo EMQX_AUTH__REDIS__SSL__ENABLE=off >> "$GITHUB_ENV" - - name: get server address - run: | - ipv4_address=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' redis) - ipv6_address=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}' redis) - cat <<-EOF >> "$GITHUB_ENV" - redis_ipv4_address=$ipv4_address - redis_ipv6_address=$ipv6_address - EOF - - name: setup - if: matrix.node_type == 'single' && matrix.connect_type == 'tcp' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__REDIS__TYPE=single - EMQX_AUTH__REDIS__SERVER=${redis_${{ matrix.network_type }}_address}:6379 - EOF - - name: setup - if: matrix.node_type == 'single' && matrix.connect_type == 'tls' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__REDIS__TYPE=single - EMQX_AUTH__REDIS__SERVER=${redis_${{ matrix.network_type }}_address}:6380 - EOF - - name: setup - if: matrix.node_type == 'sentinel' && matrix.connect_type == 'tcp' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__REDIS__TYPE=sentinel - EMQX_AUTH__REDIS__SERVER=${redis_${{ matrix.network_type }}_address}:26379 - EMQX_AUTH__REDIS__SENTINEL=mymaster - EOF - - name: setup - if: matrix.node_type == 'sentinel' && matrix.connect_type == 'tls' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__REDIS__TYPE=sentinel - EMQX_AUTH__REDIS__SERVER=${redis_${{ matrix.network_type }}_address}:26380 - EMQX_AUTH__REDIS__SENTINEL=mymaster - EOF - - name: setup - if: matrix.node_type == 'cluster' && matrix.connect_type == 'tcp' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__REDIS__TYPE=cluster - EMQX_AUTH__REDIS__SERVER=${redis_${{ matrix.network_type }}_address}:7000 - EOF - - name: setup - if: matrix.node_type == 'cluster' && matrix.connect_type == 'tls' - run: | - cat <<-EOF >> "$GITHUB_ENV" - EMQX_AUTH__REDIS__TYPE=cluster - EMQX_AUTH__REDIS__SERVER=${redis_${{ matrix.network_type }}_address}:8000 - EOF - - name: set git token - run: | - if make emqx-ee --dry-run > /dev/null 2>&1; then - docker exec -i erlang bash -c "echo \"https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com\" > /root/.git-credentials && git config --global credential.helper store" - fi - - name: run test cases - run: | - export CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_ - export EMQX_AUTH__REIDS__PASSWORD=public - printenv > .env - docker exec -i erlang sh -c "make ensure-rebar3" - docker exec -i erlang sh -c "./rebar3 eunit --application=emqx_auth_redis" - docker exec --env-file .env -i erlang sh -c "make apps/emqx_auth_redis-ct" - - uses: actions/upload-artifact@v1 - if: failure() - with: - name: logs_redis${{ matrix.redis_tag }}_${{ matrix.node_type }}_${{ matrix.network_type }}_${{ matrix.connect_type }} - path: _build/test/logs diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index 035c0d0e3..280173bf7 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -48,13 +48,13 @@ jobs: echo "['$(date -u +"%Y-%m-%dT%H:%M:%SZ")']:waiting emqx"; sleep 5; done - - name: verify EMQX_LOADED_PLUGINS override working - run: | - expected="{emqx_sn, true}." - output=$(docker exec -i node1.emqx.io bash -c "cat data/loaded_plugins" | tail -n1) - if [ "$expected" != "$output" ]; then - exit 1 - fi + # - name: verify EMQX_LOADED_PLUGINS override working + # run: | + # expected="{emqx_sn, true}." + # output=$(docker exec -i node1.emqx.io bash -c "cat data/loaded_plugins" | tail -n1) + # if [ "$expected" != "$output" ]; then + # exit 1 + # fi - name: make paho tests run: | if ! docker exec -i python /scripts/pytest.sh; then @@ -131,11 +131,27 @@ jobs: echo "waiting emqx started"; sleep 10; done - - name: get pods log + - name: get emqx-0 pods log if: failure() env: KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: kubectl describe pods emqx-0 + run: | + kubectl describe pods emqx-0 + kubectl logs emqx-0 + - name: get emqx-1 pods log + if: failure() + env: + KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" + run: | + kubectl describe pods emqx-1 + kubectl logs emqx-1 + - name: get emqx-2 pods log + if: failure() + env: + KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" + run: | + kubectl describe pods emqx-2 + kubectl logs emqx-2 - uses: actions/checkout@v2 with: repository: emqx/paho.mqtt.testing diff --git a/.github/workflows/run_test_cases.yaml b/.github/workflows/run_test_cases.yaml index 3fbe88c40..812058111 100644 --- a/.github/workflows/run_test_cases.yaml +++ b/.github/workflows/run_test_cases.yaml @@ -56,64 +56,21 @@ jobs: - name: docker compose up if: env.EDITION == 'opensource' env: - MYSQL_TAG: 8 - REDIS_TAG: 6 - MONGO_TAG: 4 - PGSQL_TAG: 13 - LDAP_TAG: 2.4.50 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | docker-compose \ -f .ci/docker-compose-file/docker-compose.yaml \ - -f .ci/docker-compose-file/docker-compose-ldap-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-mongo-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-mysql-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-pgsql-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-redis-single-tcp.yaml \ up -d --build - name: docker compose up if: env.EDITION == 'enterprise' env: - MYSQL_TAG: 8 - REDIS_TAG: 6 - MONGO_TAG: 4 - PGSQL_TAG: 13 - LDAP_TAG: 2.4.50 - OPENTSDB_TAG: latest - INFLUXDB_TAG: 1.7.6 - DYNAMODB_TAG: 1.11.477 - TIMESCALE_TAG: latest-pg11 - CASSANDRA_TAG: 3.11.6 - RABBITMQ_TAG: 3.7 - KAFKA_TAG: 2.5.0 - PULSAR_TAG: 2.3.2 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 20 run: | docker-compose \ -f .ci/docker-compose-file/docker-compose.yaml \ - -f .ci/docker-compose-file/docker-compose-ldap-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-mongo-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-mysql-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-pgsql-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-redis-single-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-enterprise.yaml \ - -f .ci/docker-compose-file/docker-compose-enterprise-cassandra-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-enterprise-dynamodb-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-enterprise-influxdb-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-enterprise-kafka-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-enterprise-opentsdb-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-enterprise-pulsar-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-enterprise-rabbit-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-enterprise-timescale-tcp.yaml \ - -f .ci/docker-compose-file/docker-compose-enterprise-mysql-client.yaml \ - -f .ci/docker-compose-file/docker-compose-enterprise-pgsql-and-timescale-client.yaml \ up -d --build - docker exec -i erlang bash -c "echo \"https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com\" > /root/.git-credentials && git config --global credential.helper store" - while [ $(docker ps -a --filter name=client --filter exited=0 | wc -l) \ - != $(docker ps -a --filter name=client | wc -l) ]; do - sleep 5 - done - name: run eunit run: | docker exec -i erlang bash -c "make eunit" diff --git a/.gitignore b/.gitignore index dd8e9b82e..e7b31b394 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,6 @@ emqx_dialyzer_*_plt */emqx_dashboard/priv/www dist.zip scripts/git-token -etc/*.seg +apps/*/etc/*.all _upgrade_base/ TAGS diff --git a/Makefile b/Makefile index cc8cdb0db..c81b9d5a3 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,8 @@ coveralls: $(REBAR) @ENABLE_COVER_COMPILE=1 $(REBAR) as test coveralls send .PHONY: $(REL_PROFILES) -$(REL_PROFILES:%=%): $(REBAR) get-dashboard + +$(REL_PROFILES:%=%): $(REBAR) get-dashboard conf-segs @$(REBAR) as $(@) do compile,release ## Not calling rebar3 clean because @@ -111,7 +112,7 @@ xref: $(REBAR) dialyzer: $(REBAR) @$(REBAR) as check dialyzer -COMMON_DEPS := $(REBAR) get-dashboard $(CONF_SEGS) +COMMON_DEPS := $(REBAR) get-dashboard conf-segs ## rel target is to create release package without relup .PHONY: $(REL_PROFILES:%=%-rel) $(PKG_PROFILES:%=%-rel) @@ -152,3 +153,6 @@ quickrun: ./_build/$(PROFILE)/rel/emqx/bin/emqx console include docker.mk + +conf-segs: + @scripts/merge-config.escript diff --git a/README-CN.md b/README-CN.md index 7d2888327..67f1b0ff5 100644 --- a/README-CN.md +++ b/README-CN.md @@ -101,7 +101,7 @@ make dialyzer ##### 要分析特定的应用程序,(用逗号分隔的应用程序列表) ``` -DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer +DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer ``` ## 社区 @@ -145,7 +145,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html) -[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf) +[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf) ## 开源许可 diff --git a/README-JP.md b/README-JP.md index b7afe8195..a3e1f5130 100644 --- a/README-JP.md +++ b/README-JP.md @@ -95,7 +95,7 @@ make dialyzer ##### 特定のアプリケーションのみ解析する(アプリケーション名をコンマ区切りで入力) ``` -DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer +DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer ``` ## コミュニティ @@ -125,7 +125,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html) -[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf) +[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf) ## License diff --git a/README-RU.md b/README-RU.md index cddaba4a5..45f253f5b 100644 --- a/README-RU.md +++ b/README-RU.md @@ -104,7 +104,7 @@ make dialyzer ##### Статический анализ части приложений (список через запятую) ``` -DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer +DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer ``` ## Сообщество @@ -135,7 +135,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html) -[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf) +[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf) ## Лицензия diff --git a/README.md b/README.md index 8d8ed8731..0f86fd188 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ make dialyzer ##### To Analyse specific apps, (list of comma separated apps) ``` -DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer +DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer ``` ## Community @@ -134,7 +134,7 @@ You can read the mqtt protocol via the following links: [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html) -[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf) +[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf) ## License diff --git a/apps/emqx/etc/acl.conf b/apps/emqx/etc/acl.conf deleted file mode 100644 index af2fb0dd1..000000000 --- a/apps/emqx/etc/acl.conf +++ /dev/null @@ -1,26 +0,0 @@ -%%-------------------------------------------------------------------- -%% [ACL](https://docs.emqx.io/broker/v3/en/config.html) -%% -%% -type(who() :: all | binary() | -%% {ipaddr, esockd_access:cidr()} | -%% {client, binary()} | -%% {user, binary()}). -%% -%% -type(access() :: subscribe | publish | pubsub). -%% -%% -type(topic() :: binary()). -%% -%% -type(rule() :: {allow, all} | -%% {allow, who(), access(), list(topic())} | -%% {deny, all} | -%% {deny, who(), access(), list(topic())}). -%%-------------------------------------------------------------------- - -{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. - -{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. - -{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. - -{allow, all}. - diff --git a/apps/emqx/etc/acl.conf.paho b/apps/emqx/etc/acl.conf.paho deleted file mode 100644 index 5beec4347..000000000 --- a/apps/emqx/etc/acl.conf.paho +++ /dev/null @@ -1,14 +0,0 @@ -%%-------------------------------------------------------------------- -%% For paho interoperability test cases -%%-------------------------------------------------------------------- - -{deny, {client, "myclientid"}, subscribe, ["test/nosubscribe"]}. - -{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. - -{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. - -{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. - -{allow, all}. - diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 4e3ff60c3..f5495881d 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -307,6 +307,14 @@ cluster { ## Default: default namespace: default } + + db_backend: mnesia + + rlog: { + # role: core + # core_nodes: [] + } + } ##================================================================== @@ -1259,8 +1267,9 @@ zones.default { ## ## @doc zones..listeners..type ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket + ## - tcp: MQTT over TCP + ## - ws: MQTT over Websocket + ## - quic: MQTT over QUIC ## Required: true type: tcp @@ -1390,8 +1399,9 @@ zones.default { ## ## @doc zones..listeners..type ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket + ## - tcp: MQTT over TCP + ## - ws: MQTT over Websocket + ## - quic: MQTT over QUIC ## Required: true type: tcp @@ -1520,6 +1530,51 @@ zones.default { tcp.buffer: 4KB } + listeners.mqtt_quic: + #${example_common_ssl_options} # common options can be written in a separate config entry and reference it from here. + { + + ## The type of the listener. + ## + ## @doc zones..listeners..type + ## ValueType: tcp | ws + ## - tcp: MQTT over TCP + ## - ws: MQTT over Websocket + ## - quic: MQTT over QUIC + ## Required: true + type: quic + + ## The IP address and port that the listener will bind. + ## + ## @doc zones..listeners..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 14567, 127.0.0.1:14567, ::1:14567 + bind: "0.0.0.0:14567" + + ## The size of the acceptor pool for this listener. + ## + ## @doc zones..listeners..acceptors + ## ValueType: Number + ## Default: 16 + acceptors: 16 + + ## Maximum number of concurrent connections. + ## + ## @doc zones..listeners..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections: 1024000 + + ## SSL options + ## See ${example_common_ssl_options} for more information + ssl.enable: false + #ssl.versions: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] + #ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" + #ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" + #ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + } + listeners.mqtt_ws: #${example_common_tcp_options} ${example_common_websocket_options} # common options can be written in a separate config entry and reference it from here. { @@ -1528,8 +1583,9 @@ zones.default { ## ## @doc zones..listeners..type ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket + ## - tcp: MQTT over TCP + ## - ws: MQTT over Websocket + ## - quic: MQTT over QUIC ## Required: true type: ws @@ -1662,8 +1718,9 @@ zones.default { ## ## @doc zones..listeners..type ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket + ## - tcp: MQTT over TCP + ## - ws: MQTT over Websocket + ## - quic: MQTT over QUIC ## Required: true type: ws diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index ba72a47b5..a11c30cb4 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -23,6 +23,10 @@ -define(Otherwise, true). +-define(COMMON_SHARD, emqx_common_shard). +-define(SHARED_SUB_SHARD, emqx_shared_sub_shard). +-define(MOD_DELAYED_SHARD, emqx_delayed_shard). + %%-------------------------------------------------------------------- %% Banner %%-------------------------------------------------------------------- @@ -86,6 +90,9 @@ -define(ROUTE_SHARD, route_shard). + +-define(RULE_ENGINE_SHARD, emqx_rule_engine_shard). + -record(route, { topic :: binary(), dest :: node() | {binary(), node()} @@ -101,8 +108,7 @@ descr :: string(), vendor :: string() | undefined, active = false :: boolean(), - info = #{} :: map(), - type :: atom() + info = #{} :: map() }). %%-------------------------------------------------------------------- @@ -134,4 +140,3 @@ }). -endif. - diff --git a/apps/emqx/include/emqx_mqtt.hrl b/apps/emqx/include/emqx_mqtt.hrl index e6e9bffe5..5dd9a317c 100644 --- a/apps/emqx/include/emqx_mqtt.hrl +++ b/apps/emqx/include/emqx_mqtt.hrl @@ -30,11 +30,13 @@ %% MQTT Protocol Version and Names %%-------------------------------------------------------------------- +-define(MQTT_SN_PROTO_V1, 1). -define(MQTT_PROTO_V3, 3). -define(MQTT_PROTO_V4, 4). -define(MQTT_PROTO_V5, 5). -define(PROTOCOL_NAMES, [ + {?MQTT_SN_PROTO_V1, <<"MQTT-SN">>}, %% XXX:Compatible with emqx-sn plug-in {?MQTT_PROTO_V3, <<"MQIsdp">>}, {?MQTT_PROTO_V4, <<"MQTT">>}, {?MQTT_PROTO_V5, <<"MQTT">>}]). diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index e69b07558..6bacf7052 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -29,7 +29,7 @@ -ifndef(EMQX_ENTERPRISE). --define(EMQX_RELEASE, {opensource, "5.0-pre"}). +-define(EMQX_RELEASE, {opensource, "5.0-alpha.1"}). -else. diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 7493e79ed..5f85e6dc2 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -20,6 +20,7 @@ , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} + , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.5"}}} ]}. {plugins, [rebar3_proper]}. @@ -30,7 +31,7 @@ [ meck , {bbmustache,"1.10.0"} , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers", {branch,"hocon"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.3.1"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.1"}}} ]}, {extra_src_dirs, [{"test",[recursive]}]} ]} diff --git a/apps/emqx/src/emqx.app.src b/apps/emqx/src/emqx.app.src index e909702ae..d9efbe82a 100644 --- a/apps/emqx/src/emqx.app.src +++ b/apps/emqx/src/emqx.app.src @@ -4,7 +4,7 @@ {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, - {applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon]}, + {applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon,quicer,jiffy]}, {mod, {emqx_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, diff --git a/apps/emqx/src/emqx.appup.src b/apps/emqx/src/emqx.appup.src deleted file mode 100644 index b51a7f3b7..000000000 --- a/apps/emqx/src/emqx.appup.src +++ /dev/null @@ -1,69 +0,0 @@ -%% -*- mode: erlang -*- -{VSN, - [{"4.3.2", - [{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}]}, - {"4.3.1", - [{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_frame,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]}, - {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, - {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, - {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, - {"4.3.0", - [{load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_frame,brutal_purge,soft_purge,[]}, - {load_module,emqx_trie,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]}, - {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, - {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_metrics,brutal_purge,soft_purge,[]}, - {apply,{emqx_metrics,upgrade_retained_delayed_counter_type,[]}}, - {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, - {<<".*">>,[]}], - [{"4.3.2", - [{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}]}, - {"4.3.1", - [{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_frame,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]}, - {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, - {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, - {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, - {"4.3.0", - [{load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, - {load_module,emqx_frame,brutal_purge,soft_purge,[]}, - {load_module,emqx_trie,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]}, - {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, - {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_metrics,brutal_purge,soft_purge,[]}, - {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, - {<<".*">>,[]}]}. diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index 9c42e96a3..82688017a 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -227,7 +227,6 @@ shutdown() -> shutdown(Reason) -> ?LOG(critical, "emqx shutdown for ~s", [Reason]), _ = emqx_alarm_handler:unload(), - _ = emqx_plugins:unload(), lists:foreach(fun application:stop/1 , lists:reverse(default_started_applications()) ). @@ -237,10 +236,10 @@ reboot() -> -ifdef(EMQX_ENTERPRISE). default_started_applications() -> - [gproc, esockd, ranch, cowboy, ekka, emqx]. + [gproc, esockd, ranch, cowboy, ekka, quicer, emqx]. -else. default_started_applications() -> - [gproc, esockd, ranch, cowboy, ekka, emqx, emqx_modules]. + [gproc, esockd, ranch, cowboy, ekka, quicer, emqx] ++ emqx_feature(). -endif. %%-------------------------------------------------------------------- @@ -253,3 +252,16 @@ reload_config(ConfFile) -> [application:set_env(App, Par, Val) || {Par, Val} <- Vals] end, Conf). + +emqx_feature() -> + [ emqx_resource + , emqx_authn + , emqx_authz + , emqx_gateway + , emqx_data_bridge + , emqx_rule_engine + , emqx_bridge_mqtt + , emqx_modules + , emqx_management + , emqx_retainer + , emqx_statsd]. diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 4aa8eb505..41e74018e 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -20,7 +20,7 @@ -export([authenticate/1]). --export([ check_acl/3 +-export([ authorize/3 ]). -type(result() :: #{auth_result := emqx_types:auth_result(), @@ -37,25 +37,25 @@ authenticate(ClientInfo = #{zone := Zone, listener := Listener}) -> return_auth_result(run_hooks('client.authenticate', [ClientInfo], AuthResult)). %% @doc Check ACL --spec(check_acl(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) +-spec(authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny). -check_acl(ClientInfo = #{zone := Zone, listener := Listener}, PubSub, Topic) -> +authorize(ClientInfo = #{zone := Zone, listener := Listener}, PubSub, Topic) -> case emqx_acl_cache:is_enabled(Zone, Listener) of - true -> check_acl_cache(ClientInfo, PubSub, Topic); - false -> do_check_acl(ClientInfo, PubSub, Topic) + true -> check_authorization_cache(ClientInfo, PubSub, Topic); + false -> do_authorize(ClientInfo, PubSub, Topic) end. -check_acl_cache(ClientInfo = #{zone := Zone, listener := Listener}, PubSub, Topic) -> +check_authorization_cache(ClientInfo = #{zone := Zone, listener := Listener}, PubSub, Topic) -> case emqx_acl_cache:get_acl_cache(Zone, Listener, PubSub, Topic) of not_found -> - AclResult = do_check_acl(ClientInfo, PubSub, Topic), + AclResult = do_authorize(ClientInfo, PubSub, Topic), emqx_acl_cache:put_acl_cache(Zone, Listener, PubSub, Topic, AclResult), AclResult; AclResult -> AclResult end. -do_check_acl(ClientInfo, PubSub, Topic) -> - case run_hooks('client.check_acl', [ClientInfo, PubSub, Topic], allow) of +do_authorize(ClientInfo, PubSub, Topic) -> + case run_hooks('client.authorize', [ClientInfo, PubSub, Topic], allow) of allow -> allow; _Other -> deny end. diff --git a/apps/emqx/src/emqx_access_rule.erl b/apps/emqx/src/emqx_access_rule.erl deleted file mode 100644 index 5a607dd16..000000000 --- a/apps/emqx/src/emqx_access_rule.erl +++ /dev/null @@ -1,152 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_access_rule). - -%% APIs --export([ match/3 - , compile/1 - ]). - --export_type([rule/0]). - --type(acl_result() :: allow | deny). - --type(who() :: all | binary() | - {client, binary()} | - {user, binary()} | - {ipaddr, esockd_cidr:cidr_string()}). - --type(access() :: subscribe | publish | pubsub). - --type(rule() :: {acl_result(), all} | - {acl_result(), who(), access(), list(emqx_topic:topic())}). - --define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). --define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= pubsub))). - -%% @doc Compile Access Rule. -compile({A, all}) when ?ALLOW_DENY(A) -> - {A, all}; - -compile({A, Who, Access, Topic}) when ?ALLOW_DENY(A), ?PUBSUB(Access), is_binary(Topic) -> - {A, compile(who, Who), Access, [compile(topic, Topic)]}; - -compile({A, Who, Access, TopicFilters}) when ?ALLOW_DENY(A), ?PUBSUB(Access) -> - {A, compile(who, Who), Access, [compile(topic, Topic) || Topic <- TopicFilters]}. - -compile(who, all) -> - all; -compile(who, {ipaddr, CIDR}) -> - {ipaddr, esockd_cidr:parse(CIDR, true)}; -compile(who, {client, all}) -> - {client, all}; -compile(who, {client, ClientId}) -> - {client, bin(ClientId)}; -compile(who, {user, all}) -> - {user, all}; -compile(who, {user, Username}) -> - {user, bin(Username)}; -compile(who, {'and', Conds}) when is_list(Conds) -> - {'and', [compile(who, Cond) || Cond <- Conds]}; -compile(who, {'or', Conds}) when is_list(Conds) -> - {'or', [compile(who, Cond) || Cond <- Conds]}; - -compile(topic, {eq, Topic}) -> - {eq, emqx_topic:words(bin(Topic))}; -compile(topic, Topic) -> - Words = emqx_topic:words(bin(Topic)), - case pattern(Words) of - true -> {pattern, Words}; - false -> Words - end. - -pattern(Words) -> - lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words). - -bin(L) when is_list(L) -> - list_to_binary(L); -bin(B) when is_binary(B) -> - B. - -%% @doc Match access rule --spec(match(emqx_types:clientinfo(), emqx_types:topic(), rule()) - -> {matched, allow} | {matched, deny} | nomatch). -match(_ClientInfo, _Topic, {AllowDeny, all}) when ?ALLOW_DENY(AllowDeny) -> - {matched, AllowDeny}; -match(ClientInfo, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) - when ?ALLOW_DENY(AllowDeny) -> - case match_who(ClientInfo, Who) - andalso match_topics(ClientInfo, Topic, TopicFilters) of - true -> {matched, AllowDeny}; - false -> nomatch - end. - -match_who(_ClientInfo, all) -> - true; -match_who(_ClientInfo, {user, all}) -> - true; -match_who(_ClientInfo, {client, all}) -> - true; -match_who(#{clientid := ClientId}, {client, ClientId}) -> - true; -match_who(#{username := Username}, {user, Username}) -> - true; -match_who(#{peerhost := undefined}, {ipaddr, _Tup}) -> - false; -match_who(#{peerhost := IP}, {ipaddr, CIDR}) -> - esockd_cidr:match(IP, CIDR); -match_who(ClientInfo, {'and', Conds}) when is_list(Conds) -> - lists:foldl(fun(Who, Allow) -> - match_who(ClientInfo, Who) andalso Allow - end, true, Conds); -match_who(ClientInfo, {'or', Conds}) when is_list(Conds) -> - lists:foldl(fun(Who, Allow) -> - match_who(ClientInfo, Who) orelse Allow - end, false, Conds); -match_who(_ClientInfo, _Who) -> - false. - -match_topics(_ClientInfo, _Topic, []) -> - false; -match_topics(ClientInfo, Topic, [{pattern, PatternFilter}|Filters]) -> - TopicFilter = feed_var(ClientInfo, PatternFilter), - match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(ClientInfo, Topic, Filters); -match_topics(ClientInfo, Topic, [TopicFilter|Filters]) -> - match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(ClientInfo, Topic, Filters). - -match_topic(Topic, {eq, TopicFilter}) -> - Topic == TopicFilter; -match_topic(Topic, TopicFilter) -> - emqx_topic:match(Topic, TopicFilter). - -feed_var(ClientInfo, Pattern) -> - feed_var(ClientInfo, Pattern, []). -feed_var(_ClientInfo, [], Acc) -> - lists:reverse(Acc); -feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) -> - feed_var(ClientInfo, Words, [<<"%c">>|Acc]); -feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) -> - feed_var(ClientInfo, Words, [ClientId |Acc]); -feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) -> - feed_var(ClientInfo, Words, [<<"%u">>|Acc]); -feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) -> - feed_var(ClientInfo, Words, [Username|Acc]); -feed_var(ClientInfo, [W|Words], Acc) -> - feed_var(ClientInfo, Words, [W|Acc]). - diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 9fa4b3c3a..5b2fa6f40 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -82,6 +82,9 @@ -define(DEACTIVATED_ALARM, emqx_deactivated_alarm). +-rlog_shard({?COMMON_SHARD, ?ACTIVATED_ALARM}). +-rlog_shard({?COMMON_SHARD, ?DEACTIVATED_ALARM}). + -ifdef(TEST). -compile(export_all). -compile(nowarn_export_all). @@ -167,7 +170,7 @@ handle_call({activate_alarm, Name, Details}, _From, State) -> details = Details, message = normalize_message(Name, Details), activate_at = erlang:system_time(microsecond)}, - mnesia:dirty_write(?ACTIVATED_ALARM, Alarm), + ekka_mnesia:dirty_write(?ACTIVATED_ALARM, Alarm), do_actions(activate, Alarm, emqx_config:get([alarm, actions])), {reply, ok, State} end; @@ -186,9 +189,14 @@ handle_call(delete_all_deactivated_alarms, _From, State) -> {reply, ok, State}; handle_call({get_alarms, all}, _From, State) -> - Alarms = [normalize(Alarm) || - Alarm <- ets:tab2list(?ACTIVATED_ALARM) - ++ ets:tab2list(?DEACTIVATED_ALARM)], + {atomic, Alarms} = + ekka_mnesia:ro_transaction( + ?COMMON_SHARD, + fun() -> + [normalize(Alarm) || + Alarm <- ets:tab2list(?ACTIVATED_ALARM) + ++ ets:tab2list(?DEACTIVATED_ALARM)] + end), {reply, Alarms, State}; handle_call({get_alarms, activated}, _From, State) -> @@ -235,7 +243,7 @@ deactivate_alarm(Details, #activated_alarm{activate_at = ActivateAt, name = Name case mnesia:dirty_first(?DEACTIVATED_ALARM) of '$end_of_table' -> ok; ActivateAt2 -> - mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivateAt2) + ekka_mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivateAt2) end; false -> ok end, @@ -244,8 +252,8 @@ deactivate_alarm(Details, #activated_alarm{activate_at = ActivateAt, name = Name DeActAlarm = make_deactivated_alarm(ActivateAt, Name, Details, normalize_message(Name, Details), erlang:system_time(microsecond)), - mnesia:dirty_write(?DEACTIVATED_ALARM, HistoryAlarm), - mnesia:dirty_delete(?ACTIVATED_ALARM, Name), + ekka_mnesia:dirty_write(?DEACTIVATED_ALARM, HistoryAlarm), + ekka_mnesia:dirty_delete(?ACTIVATED_ALARM, Name), do_actions(deactivate, DeActAlarm, emqx_config:get([alarm, actions])). make_deactivated_alarm(ActivateAt, Name, Details, Message, DeActivateAt) -> @@ -262,7 +270,7 @@ deactivate_all_alarms() -> details = Details, message = Message, activate_at = ActivateAt}) -> - mnesia:dirty_write(?DEACTIVATED_ALARM, + ekka_mnesia:dirty_write(?DEACTIVATED_ALARM, #deactivated_alarm{ activate_at = ActivateAt, name = Name, @@ -274,7 +282,7 @@ deactivate_all_alarms() -> %% Delete all records from the given table, ignore result. clear_table(TableName) -> - case mnesia:clear_table(TableName) of + case ekka_mnesia:clear_table(TableName) of {aborted, Reason} -> ?LOG(warning, "Faile to clear table ~p reason: ~p", [TableName, Reason]); @@ -294,7 +302,7 @@ delete_expired_deactivated_alarms('$end_of_table', _Checkpoint) -> delete_expired_deactivated_alarms(ActivatedAt, Checkpoint) -> case ActivatedAt =< Checkpoint of true -> - mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivatedAt), + ekka_mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivatedAt), NActivatedAt = mnesia:dirty_next(?DEACTIVATED_ALARM, ActivatedAt), delete_expired_deactivated_alarms(NActivatedAt, Checkpoint); false -> diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index 234f42645..666a704f3 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -28,7 +28,12 @@ -define(APP, emqx). --define(EMQX_SHARDS, [route_shard]). +-define(EMQX_SHARDS, [ ?ROUTE_SHARD + , ?COMMON_SHARD + , ?SHARED_SUB_SHARD + , ?RULE_ENGINE_SHARD + , ?MOD_DELAYED_SHARD + ]). -include("emqx_release.hrl"). @@ -46,7 +51,7 @@ start(_Type, _Args) -> ok = ekka_rlog:wait_for_shards(?EMQX_SHARDS, infinity), {ok, Sup} = emqx_sup:start_link(), ok = start_autocluster(), - ok = emqx_plugins:init(), + % ok = emqx_plugins:init(), _ = emqx_plugins:load(), _ = start_ce_modules(), emqx_boot:is_enabled(listeners) andalso (ok = emqx_listeners:start()), diff --git a/apps/emqx/src/emqx_banned.erl b/apps/emqx/src/emqx_banned.erl index 762a2b61b..16804d329 100644 --- a/apps/emqx/src/emqx_banned.erl +++ b/apps/emqx/src/emqx_banned.erl @@ -51,6 +51,8 @@ -define(BANNED_TAB, ?MODULE). +-rlog_shard({?COMMON_SHARD, ?BANNED_TAB}). + %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -96,19 +98,19 @@ create(#{who := Who, reason := Reason, at := At, until := Until}) -> - mnesia:dirty_write(?BANNED_TAB, #banned{who = Who, - by = By, - reason = Reason, - at = At, - until = Until}); + ekka_mnesia:dirty_write(?BANNED_TAB, #banned{who = Who, + by = By, + reason = Reason, + at = At, + until = Until}); create(Banned) when is_record(Banned, banned) -> - mnesia:dirty_write(?BANNED_TAB, Banned). + ekka_mnesia:dirty_write(?BANNED_TAB, Banned). -spec(delete({clientid, emqx_types:clientid()} | {username, emqx_types:username()} | {peerhost, emqx_types:peerhost()}) -> ok). delete(Who) -> - mnesia:dirty_delete(?BANNED_TAB, Who). + ekka_mnesia:dirty_delete(?BANNED_TAB, Who). info(InfoKey) -> mnesia:table_info(?BANNED_TAB, InfoKey). @@ -129,7 +131,7 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) -> - mnesia:async_dirty(fun expire_banned_items/1, [erlang:system_time(second)]), + ekka_mnesia:transaction(?COMMON_SHARD, fun expire_banned_items/1, [erlang:system_time(second)]), {noreply, ensure_expiry_timer(State), hibernate}; handle_info(Info, State) -> @@ -160,4 +162,3 @@ expire_banned_items(Now) -> mnesia:delete_object(?BANNED_TAB, B, sticky_write); (_, _Acc) -> ok end, ok, ?BANNED_TAB). - diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 4c9ee7682..600e8bed0 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1420,7 +1420,7 @@ check_pub_alias(_Packet, _Channel) -> ok. check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, #channel{clientinfo = ClientInfo}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_acl(ClientInfo, publish, Topic) of + emqx_access_control:authorize(ClientInfo, publish, Topic) of false -> ok; allow -> ok; deny -> {error, ?RC_NOT_AUTHORIZED} @@ -1454,7 +1454,7 @@ check_sub_acls([], _Channel, Acc) -> check_sub_acl(TopicFilter, #channel{clientinfo = ClientInfo}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_acl(ClientInfo, subscribe, TopicFilter) of + emqx_access_control:authorize(ClientInfo, subscribe, TopicFilter) of false -> allow; Result -> Result end. diff --git a/apps/emqx/src/emqx_cm.erl b/apps/emqx/src/emqx_cm.erl index b15a2ff79..6eb375aba 100644 --- a/apps/emqx/src/emqx_cm.erl +++ b/apps/emqx/src/emqx_cm.erl @@ -294,6 +294,9 @@ do_discard_session(ClientId, Pid) -> _ : {noproc, _} -> % emqx_connection: gen_server:call ?tp(debug, "session_already_gone", #{pid => Pid}), ok; + _ : {'EXIT', {noproc, _}} -> % rpc_call/3 + ?tp(debug, "session_already_gone", #{pid => Pid}), + ok; _ : {{shutdown, _}, _} -> ?tp(debug, "session_already_shutdown", #{pid => Pid}), ok; diff --git a/apps/emqx/src/emqx_cm_registry.erl b/apps/emqx/src/emqx_cm_registry.erl index d8095b445..30035eca5 100644 --- a/apps/emqx/src/emqx_cm_registry.erl +++ b/apps/emqx/src/emqx_cm_registry.erl @@ -48,7 +48,9 @@ -define(TAB, emqx_channel_registry). -define(LOCK, {?MODULE, cleanup_down}). --rlog_shard({?ROUTE_SHARD, ?TAB}). +-define(CM_SHARD, emqx_cm_shard). + +-rlog_shard({?CM_SHARD, ?TAB}). -record(channel, {chid, pid}). @@ -111,6 +113,7 @@ init([]) -> {storage_properties, [{ets, [{read_concurrency, true}, {write_concurrency, true}]}]}]), ok = ekka_mnesia:copy_table(?TAB, ram_copies), + ok = ekka_rlog:wait_for_shards([?CM_SHARD], infinity), ok = ekka:monitor(membership), {ok, #{}}. @@ -125,7 +128,7 @@ handle_cast(Msg, State) -> handle_info({membership, {mnesia, down, Node}}, State) -> global:trans({?LOCK, self()}, fun() -> - ekka_mnesia:transaction(?ROUTE_SHARD, fun cleanup_channels/1, [Node]) + ekka_mnesia:transaction(?CM_SHARD, fun cleanup_channels/1, [Node]) end), {noreply, State}; diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 91b115e9d..9d5428fdd 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -120,4 +120,3 @@ put_raw(Config) -> -spec put_raw(emqx_map_lib:config_key_path(), term()) -> ok. put_raw(KeyPath, Config) -> put_raw(emqx_map_lib:deep_put(KeyPath, get_raw(), Config)). - diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 0df7cccf1..59c735afc 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -422,6 +422,13 @@ handle_msg({Inet, _Sock, Data}, State) when Inet == tcp; Inet == ssl -> ok = emqx_metrics:inc('bytes.received', Oct), parse_incoming(Data, State); +handle_msg({quic, Data, _Sock, _, _, _}, State) -> + ?LOG(debug, "RECV ~0p", [Data]), + Oct = iolist_size(Data), + inc_counter(incoming_bytes, Oct), + ok = emqx_metrics:inc('bytes.received', Oct), + parse_incoming(Data, State); + handle_msg({incoming, Packet = ?CONNECT_PACKET(ConnPkt)}, State = #state{idle_timer = IdleTimer}) -> ok = emqx_misc:cancel_timer(IdleTimer), @@ -446,7 +453,7 @@ handle_msg({Closed, _Sock}, State) handle_info({sock_closed, Closed}, close_socket(State)); handle_msg({Passive, _Sock}, State) - when Passive == tcp_passive; Passive == ssl_passive -> + when Passive == tcp_passive; Passive == ssl_passive; Passive =:= quic_passive -> %% In Stats Pubs = emqx_pd:reset_counter(incoming_pubs), Bytes = emqx_pd:reset_counter(incoming_bytes), @@ -739,6 +746,15 @@ handle_info({sock_error, Reason}, State) -> end, handle_info({sock_closed, Reason}, close_socket(State)); +handle_info({quic, peer_send_shutdown, _Stream}, State) -> + handle_info({sock_closed, force}, close_socket(State)); + +handle_info({quic, closed, _Channel, ReasonFlag}, State) -> + handle_info({sock_closed, ReasonFlag}, State); + +handle_info({quic, closed, _Stream}, State) -> + handle_info({sock_closed, force}, State); + handle_info(Info, State) -> with_channel(handle_info, [Info], State). diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 1d2592fff..636acc1bc 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -79,7 +79,28 @@ do_start_listener(ZoneName, ListenerName, #{type := ws, bind := ListenOn} = Opts cowboy:start_clear(Id, RanchOpts, WsOpts); true -> cowboy:start_tls(Id, RanchOpts, WsOpts) - end. + end; + +%% Start MQTT/QUIC listener +do_start_listener(ZoneName, ListenerName, #{type := quic, bind := ListenOn} = Opts) -> + %% @fixme unsure why we need reopen lib and reopen config. + quicer_nif:open_lib(), + quicer_nif:reg_open(), + SSLOpts = ssl_opts(Opts), + DefAcceptors = erlang:system_info(schedulers_online) * 8, + ListenOpts = [ {cert, maps:get(certfile, SSLOpts, undefined)} + , {key, maps:get(keyfile, SSLOpts, undefined)} + , {alpn, ["mqtt"]} + , {conn_acceptors, maps:get(acceptors, Opts, DefAcceptors)} + , {idle_timeout_ms, emqx_config:get_listener_conf(ZoneName, ListenerName, + [mqtt, idle_timeout])} + ], + ConnectionOpts = #{conn_callback => emqx_quic_connection + , peer_unidi_stream_count => 1 + , peer_bidi_stream_count => 10 + }, + StreamOpts = [], + quicer:start_listener('mqtt:quic', ListenOn, {ListenOpts, ConnectionOpts, StreamOpts}). esockd_opts(Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), diff --git a/apps/emqx/src/emqx_metrics.erl b/apps/emqx/src/emqx_metrics.erl index c3ce14d83..cd0039791 100644 --- a/apps/emqx/src/emqx_metrics.erl +++ b/apps/emqx/src/emqx_metrics.erl @@ -172,7 +172,7 @@ {counter, 'client.connected'}, {counter, 'client.authenticate'}, {counter, 'client.auth.anonymous'}, - {counter, 'client.check_acl'}, + {counter, 'client.authorize'}, {counter, 'client.subscribe'}, {counter, 'client.unsubscribe'}, {counter, 'client.disconnected'} @@ -563,7 +563,7 @@ reserved_idx('client.connected') -> 202; reserved_idx('client.authenticate') -> 203; reserved_idx('client.enhanced_authenticate') -> 204; reserved_idx('client.auth.anonymous') -> 205; -reserved_idx('client.check_acl') -> 206; +reserved_idx('client.authorize') -> 206; reserved_idx('client.subscribe') -> 207; reserved_idx('client.unsubscribe') -> 208; reserved_idx('client.disconnected') -> 209; diff --git a/apps/emqx/src/emqx_plugins.erl b/apps/emqx/src/emqx_plugins.erl index 2f39edcb4..ae324c71d 100644 --- a/apps/emqx/src/emqx_plugins.erl +++ b/apps/emqx/src/emqx_plugins.erl @@ -21,8 +21,6 @@ -logger_header("[Plugins]"). --export([init/0]). - -export([ load/0 , load/1 , unload/0 @@ -39,35 +37,14 @@ -compile(nowarn_export_all). -endif. --dialyzer({no_match, [ plugin_loaded/2 - , plugin_unloaded/2 - ]}). - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- -%% @doc Init plugins' config --spec(init() -> ok). -init() -> - case emqx:get_env(plugins_etc_dir) of - undefined -> ok; - PluginsEtc -> - CfgFiles = [filename:join(PluginsEtc, File) || - File <- filelib:wildcard("*.config", PluginsEtc)], - lists:foreach(fun init_config/1, CfgFiles) - end. - %% @doc Load all plugins when the broker started. -spec(load() -> ok | ignore | {error, term()}). load() -> - ok = load_ext_plugins(emqx:get_env(expand_plugins_dir)), - case emqx:get_env(plugins_loaded_file) of - undefined -> ignore; %% No plugins available - File -> - _ = ensure_file(File), - with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end) - end. + ok = load_ext_plugins(emqx:get_env(expand_plugins_dir)). %% @doc Load a Plugin -spec(load(atom()) -> ok | {error, term()}). @@ -80,17 +57,13 @@ load(PluginName) when is_atom(PluginName) -> ?LOG(notice, "Plugin ~s is already started", [PluginName]), {error, already_started}; {_, false} -> - load_plugin(PluginName, true) + load_plugin(PluginName) end. %% @doc Unload all plugins before broker stopped. --spec(unload() -> list() | {error, term()}). +-spec(unload() -> ok). unload() -> - case emqx:get_env(plugins_loaded_file) of - undefined -> ignore; - File -> - with_loaded_file(File, fun stop_plugins/1) - end. + stop_plugins(list()). %% @doc UnLoad a Plugin -spec(unload(atom()) -> ok | {error, term()}). @@ -103,7 +76,7 @@ unload(PluginName) when is_atom(PluginName) -> ?LOG(error, "Plugin ~s is not started", [PluginName]), {error, not_started}; {_, _} -> - unload_plugin(PluginName, true) + unload_plugin(PluginName) end. reload(PluginName) when is_atom(PluginName)-> @@ -124,8 +97,8 @@ reload(PluginName) when is_atom(PluginName)-> -spec(list() -> [emqx_types:plugin()]). list() -> StartedApps = names(started_app), - lists:map(fun({Name, _, [Type| _]}) -> - Plugin = plugin(Name, Type), + lists:map(fun({Name, _, _}) -> + Plugin = plugin(Name), case lists:member(Name, StartedApps) of true -> Plugin#plugin{active = true}; false -> Plugin @@ -142,12 +115,6 @@ find_plugin(Name, Plugins) -> %% Internal functions %%-------------------------------------------------------------------- -init_config(CfgFile) -> - {ok, [AppsEnv]} = file:consult(CfgFile), - lists:foreach(fun({App, Envs}) -> - [application:set_env(App, Par, Val) || {Par, Val} <- Envs] - end, AppsEnv). - %% load external plugins which are placed in etc/plugins dir load_ext_plugins(undefined) -> ok; load_ext_plugins(Dir) -> @@ -171,7 +138,15 @@ load_ext_plugin(PluginDir) -> ?LOG(alert, "plugin_app_file_not_found: ~s", [AppFile]), error({plugin_app_file_not_found, AppFile}) end, - load_plugin_app(AppName, Ebin). + ok = load_plugin_app(AppName, Ebin). + % try + % ok = generate_configs(AppName, PluginDir) + % catch + % throw : {conf_file_not_found, ConfFile} -> + % %% this is maybe a dependency of an external plugin + % ?LOG(debug, "config_load_error_ignored for app=~p, path=~s", [AppName, ConfFile]), + % ok + % end. load_plugin_app(AppName, Ebin) -> _ = code:add_patha(Ebin), @@ -189,56 +164,24 @@ load_plugin_app(AppName, Ebin) -> {error, {already_loaded, _}} -> ok end. -ensure_file(File) -> - case filelib:is_file(File) of false -> write_loaded([]); true -> ok end. - -with_loaded_file(File, SuccFun) -> - case read_loaded(File) of - {ok, Names0} -> - Names = filter_plugins(Names0), - SuccFun(Names); - {error, Error} -> - ?LOG(alert, "Failed to read: ~p, error: ~p", [File, Error]), - {error, Error} - end. - -filter_plugins(Names) -> - lists:filtermap(fun(Name1) when is_atom(Name1) -> {true, Name1}; - ({Name1, true}) -> {true, Name1}; - ({_Name1, false}) -> false - end, Names). - -load_plugins(Names, Persistent) -> - Plugins = list(), - NotFound = Names -- names(Plugins), - case NotFound of - [] -> ok; - NotFound -> ?LOG(alert, "cannot_find_plugins: ~p", [NotFound]) - end, - NeedToLoad = Names -- NotFound -- names(started_app), - lists:foreach(fun(Name) -> - Plugin = find_plugin(Name, Plugins), - load_plugin(Plugin#plugin.name, Persistent) - end, NeedToLoad). - %% Stop plugins -stop_plugins(Names) -> - _ = [stop_app(App) || App <- Names], +stop_plugins(Plugins) -> + _ = [stop_app(Plugin#plugin.name) || Plugin <- Plugins], ok. -plugin(AppName, Type) -> +plugin(AppName) -> case application:get_all_key(AppName) of {ok, Attrs} -> Descr = proplists:get_value(description, Attrs, ""), - #plugin{name = AppName, descr = Descr, type = plugin_type(Type)}; + #plugin{name = AppName, descr = Descr}; undefined -> error({plugin_not_found, AppName}) end. -load_plugin(Name, Persistent) -> +load_plugin(Name) -> try case load_app(Name) of ok -> - start_app(Name, fun(App) -> plugin_loaded(App, Persistent) end); + start_app(Name); {error, Error0} -> {error, Error0} end @@ -257,22 +200,21 @@ load_app(App) -> {error, Error} end. -start_app(App, SuccFun) -> +start_app(App) -> case application:ensure_all_started(App) of {ok, Started} -> ?LOG(info, "Started plugins: ~p", [Started]), ?LOG(info, "Load plugin ~s successfully", [App]), - _ = SuccFun(App), ok; {error, {ErrApp, Reason}} -> ?LOG(error, "Load plugin ~s failed, cannot start plugin ~s for ~0p", [App, ErrApp, Reason]), {error, {ErrApp, Reason}} end. -unload_plugin(App, Persistent) -> +unload_plugin(App) -> case stop_app(App) of ok -> - _ = plugin_unloaded(App, Persistent), ok; + ok; {error, Reason} -> {error, Reason} end. @@ -296,60 +238,5 @@ names(started_app) -> names(Plugins) -> [Name || #plugin{name = Name} <- Plugins]. -plugin_loaded(_Name, false) -> - ok; -plugin_loaded(Name, true) -> - case read_loaded() of - {ok, Names} -> - case lists:member(Name, Names) of - false -> - %% write file if plugin is loaded - write_loaded(lists:append(Names, [{Name, true}])); - true -> - ignore - end; - {error, Error} -> - ?LOG(error, "Cannot read loaded plugins: ~p", [Error]) - end. - -plugin_unloaded(_Name, false) -> - ok; -plugin_unloaded(Name, true) -> - case read_loaded() of - {ok, Names0} -> - Names = filter_plugins(Names0), - case lists:member(Name, Names) of - true -> - write_loaded(lists:delete(Name, Names)); - false -> - ?LOG(error, "Cannot find ~s in loaded_file", [Name]) - end; - {error, Error} -> - ?LOG(error, "Cannot read loaded_plugins: ~p", [Error]) - end. - -read_loaded() -> - case emqx:get_env(plugins_loaded_file) of - undefined -> {error, not_found}; - File -> read_loaded(File) - end. - -read_loaded(File) -> file:consult(File). - -write_loaded(AppNames) -> - FilePath = emqx:get_env(plugins_loaded_file), - case file:write_file(FilePath, [io_lib:format("~p.~n", [Name]) || Name <- AppNames]) of - ok -> ok; - {error, Error} -> - ?LOG(error, "Write File ~p Error: ~p", [FilePath, Error]), - {error, Error} - end. - -plugin_type(auth) -> auth; -plugin_type(protocol) -> protocol; -plugin_type(backend) -> backend; -plugin_type(bridge) -> bridge; -plugin_type(_) -> feature. - funlog(Key, Value) -> ?LOG(info, "~s = ~p", [string:join(Key, "."), Value]). diff --git a/apps/emqx_authentication/src/emqx_authentication_app.erl b/apps/emqx/src/emqx_quic_connection.erl similarity index 70% rename from apps/emqx_authentication/src/emqx_authentication_app.erl rename to apps/emqx/src/emqx_quic_connection.erl index 2d395def7..b83522c6e 100644 --- a/apps/emqx_authentication/src/emqx_authentication_app.erl +++ b/apps/emqx/src/emqx_quic_connection.erl @@ -14,21 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_app). +-module(emqx_quic_connection). --behaviour(application). - --emqx_plugin(?MODULE). - -%% Application callbacks --export([ start/2 - , stop/1 +%% Callbacks +-export([ new_conn/2 ]). -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_authentication_sup:start_link(), - ok = emqx_authentication:register_service_types(), - {ok, Sup}. - -stop(_State) -> - ok. +new_conn(Conn, {_L, COpts, _S}) when is_map(COpts) -> + new_conn(Conn, maps:to_list(COpts)); +new_conn(Conn, COpts) -> + emqx_connection:start_link(emqx_quic_stream, Conn, COpts). diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl new file mode 100644 index 000000000..236c11ad3 --- /dev/null +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -0,0 +1,92 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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. +%%-------------------------------------------------------------------- + +%% MQTT/QUIC Stream +-module(emqx_quic_stream). + +%% emqx transport Callbacks +-export([ type/1 + , wait/1 + , getstat/2 + , fast_close/1 + , ensure_ok_or_exit/2 + , async_send/3 + , setopts/2 + , getopts/2 + , peername/1 + , sockname/1 + , peercert/1 + ]). + +wait(Conn) -> + quicer:accept_stream(Conn, []). + +type(_) -> + quic. + +peername(S) -> + quicer:peername(S). + +sockname(S) -> + quicer:sockname(S). + +peercert(_S) -> + nossl. + +getstat(Socket, Stats) -> + case quicer:getstat(Socket, Stats) of + {error, _} -> {error, closed}; + Res -> Res + end. + +setopts(Socket, Opts) -> + lists:foreach(fun({Opt, V}) when is_atom(Opt) -> + quicer:setopt(Socket, Opt, V); + (Opt) when is_atom(Opt) -> + quicer:setopt(Socket, Opt, true) + end, Opts), + ok. + +getopts(_Socket, _Opts) -> + %% @todo + {ok, [{high_watermark, 0}, + {high_msgq_watermark, 0}, + {sndbuf, 0}, + {recbuf, 0}, + {buffer,80000}]}. + +fast_close(Stream) -> + %% Stream might be closed already. + _ = quicer:async_close_stream(Stream), + ok. + +-spec(ensure_ok_or_exit(atom(), list(term())) -> term()). +ensure_ok_or_exit(Fun, Args = [Sock|_]) when is_atom(Fun), is_list(Args) -> + case erlang:apply(?MODULE, Fun, Args) of + {error, Reason} when Reason =:= enotconn; Reason =:= closed -> + fast_close(Sock), + exit(normal); + {error, Reason} -> + fast_close(Sock), + exit({shutdown, Reason}); + Result -> Result + end. + +async_send(Stream, Data, Options) when is_list(Data) -> + async_send(Stream, iolist_to_binary(Data), Options); +async_send(Stream, Data, _Options) when is_binary(Data) -> + {ok, _Len} = quicer:send(Stream, Data), + ok. diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 0d6c8f3de..8d65a8ebe 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -59,8 +59,8 @@ -export([includes/0]). structs() -> ["cluster", "node", "rpc", "log", "lager", - "zones", "listeners", "module", "broker", - "plugins", "sysmon", "alarm", "telemetry"] + "zones", "listeners", "broker", + "plugins", "sysmon", "alarm"] ++ includes(). -ifdef(TEST). @@ -69,6 +69,14 @@ includes() ->[]. includes() -> [ "emqx_data_bridge" , "emqx_telemetry" + , "emqx_retainer" + , "emqx_statsd" + , "emqx_authn" + , "emqx_authz" + , "emqx_bridge_mqtt" + , "emqx_modules" + , "emqx_management" + , "emqx_gateway" ]. -endif. @@ -345,6 +353,7 @@ fields("listeners") -> [ {"$name", hoconsc:union( [ hoconsc:ref("mqtt_tcp_listener") , hoconsc:ref("mqtt_ws_listener") + , hoconsc:ref("mqtt_quic_listener") ])} ]; @@ -361,6 +370,10 @@ fields("mqtt_ws_listener") -> , {"websocket", ref("ws_opts")} ] ++ mqtt_listener(); +fields("mqtt_quic_listener") -> + [ {"type", t(quic)} + ] ++ base_listener(); + fields("ws_opts") -> [ {"mqtt_path", t(string(), undefined, "/mqtt")} , {"mqtt_piggyback", t(union(single, multiple), undefined, multiple)} @@ -409,12 +422,6 @@ fields("deflate_opts") -> , {"client_max_window_bits", t(range(8, 15), undefined, 15)} ]; -fields("module") -> - [ {"loaded_file", t(string(), "emqx.modules_loaded_file", undefined)} - , {"presence", ref("presence")} - , {"subscription", ref("subscription")} - , {"rewrite", ref("rewrite")} - ]; fields("presence") -> [ {"qos", t(range(0, 2), undefined, 1)}]; @@ -440,9 +447,7 @@ fields("rule") -> [ {"$id", t(string())}]; fields("plugins") -> - [ {"etc_dir", t(string(), "emqx.plugins_etc_dir", undefined)} - , {"loaded_file", t(string(), "emqx.plugins_loaded_file", undefined)} - , {"expand_plugins_dir", t(string(), "emqx.expand_plugins_dir", undefined)} + [ {"expand_plugins_dir", t(string(), "emqx.expand_plugins_dir", undefined)} ]; fields("broker") -> @@ -492,24 +497,22 @@ fields("alarm") -> , {"validity_period", t(duration_s(), undefined, "24h")} ]; -fields("telemetry") -> - [ {"enabled", t(boolean(), undefined, false)} - , {"url", t(string(), undefined, "https://telemetry-emqx-io.bigpar.vercel.app/api/telemetry")} - , {"report_interval", t(duration_s(), undefined, "7d")} - ]; - fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), Mod:fields(ExtraField). mqtt_listener() -> + base_listener() ++ + [ {"access_rules", t(hoconsc:array(string()))} + , {"proxy_protocol", t(boolean(), undefined, false)} + , {"proxy_protocol_timeout", t(duration())} + ]. + +base_listener() -> [ {"bind", t(union(ip_port(), integer()))} , {"acceptors", t(integer(), undefined, 16)} , {"max_connections", maybe_infinity(integer(), infinity)} , {"rate_limit", ref("rate_limit")} - , {"access_rules", t(hoconsc:array(string()))} - , {"proxy_protocol", t(boolean(), undefined, false)} - , {"proxy_protocol_timeout", t(duration())} ]. translations() -> ["kernel"]. diff --git a/apps/emqx/src/emqx_shared_sub.erl b/apps/emqx/src/emqx_shared_sub.erl index 4707f63db..c002653ba 100644 --- a/apps/emqx/src/emqx_shared_sub.erl +++ b/apps/emqx/src/emqx_shared_sub.erl @@ -77,6 +77,8 @@ -define(NACK(Reason), {shared_sub_nack, Reason}). -define(NO_ACK, no_ack). +-rlog_shard({?SHARED_SUB_SHARD, ?TAB}). + -record(state, {pmon}). -record(emqx_shared_subscription, {group, topic, subpid}). @@ -297,7 +299,7 @@ subscribers(Group, Topic) -> init([]) -> {ok, _} = mnesia:subscribe({table, ?TAB, simple}), - {atomic, PMon} = mnesia:transaction(fun init_monitors/0), + {atomic, PMon} = ekka_mnesia:transaction(?SHARED_SUB_SHARD, fun init_monitors/0), ok = emqx_tables:new(?SHARED_SUBS, [protected, bag]), ok = emqx_tables:new(?ALIVE_SUBS, [protected, set, {read_concurrency, true}]), {ok, update_stats(#state{pmon = PMon})}. @@ -309,7 +311,7 @@ init_monitors() -> end, emqx_pmon:new(), ?TAB). handle_call({subscribe, Group, Topic, SubPid}, _From, State = #state{pmon = PMon}) -> - mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)), + ekka_mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)), case ets:member(?SHARED_SUBS, {Group, Topic}) of true -> ok; false -> ok = emqx_router:do_add_route(Topic, {Group, node()}) @@ -319,7 +321,7 @@ handle_call({subscribe, Group, Topic, SubPid}, _From, State = #state{pmon = PMon {reply, ok, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; handle_call({unsubscribe, Group, Topic, SubPid}, _From, State) -> - mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)), + ekka_mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)), true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}), delete_route_if_needed({Group, Topic}), {reply, ok, State}; @@ -336,9 +338,13 @@ handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = P #emqx_shared_subscription{subpid = SubPid} = NewRecord, {noreply, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; -handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) -> - #emqx_shared_subscription{subpid = SubPid} = OldRecord, - {noreply, update_stats(State#state{pmon = emqx_pmon:demonitor(SubPid, PMon)})}; +%% The subscriber may have subscribed multiple topics, so we need to keep monitoring the PID until +%% it `unsubscribed` the last topic. +%% The trick is we don't demonitor the subscriber here, and (after a long time) it will eventually +%% be disconnected. +% handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) -> +% #emqx_shared_subscription{subpid = SubPid} = OldRecord, +% {noreply, update_stats(State#state{pmon = emqx_pmon:demonitor(SubPid, PMon)})}; handle_info({mnesia_table_event, _Event}, State) -> {noreply, State}; @@ -348,8 +354,7 @@ handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMo cleanup_down(SubPid), {noreply, update_stats(State#state{pmon = emqx_pmon:erase(SubPid, PMon)})}; -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), +handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> @@ -370,7 +375,7 @@ cleanup_down(SubPid) -> ?IS_LOCAL_PID(SubPid) orelse ets:delete(?ALIVE_SUBS, SubPid), lists:foreach( fun(Record = #emqx_shared_subscription{topic = Topic, group = Group}) -> - ok = mnesia:dirty_delete_object(?TAB, Record), + ok = ekka_mnesia:dirty_delete_object(?TAB, Record), true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}), delete_route_if_needed({Group, Topic}) end, mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})). diff --git a/apps/emqx/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl index cd432b9fc..50a1cfea1 100644 --- a/apps/emqx/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -403,7 +403,10 @@ websocket_close(Reason, State) -> terminate(Reason, _Req, #state{channel = Channel}) -> ?LOG(debug, "Terminated due to ~p", [Reason]), - emqx_channel:terminate(Reason, Channel). + emqx_channel:terminate(Reason, Channel); + +terminate(_Reason, _Req, _UnExpectedState) -> + ok. %%-------------------------------------------------------------------- %% Handle call diff --git a/apps/emqx/test/emqx_SUITE_data/loaded_modules b/apps/emqx/test/emqx_SUITE_data/loaded_modules index 49effa0c5..f31e47900 100644 --- a/apps/emqx/test/emqx_SUITE_data/loaded_modules +++ b/apps/emqx/test/emqx_SUITE_data/loaded_modules @@ -1,2 +1 @@ -{emqx_mod_acl_internal, true}. -{emqx_mod_presence, true}. \ No newline at end of file +{emqx_mod_presence, true}. diff --git a/apps/emqx/test/emqx_access_SUITE_data/acl.conf b/apps/emqx/test/emqx_access_SUITE_data/acl.conf deleted file mode 100644 index e5730b4c5..000000000 --- a/apps/emqx/test/emqx_access_SUITE_data/acl.conf +++ /dev/null @@ -1,15 +0,0 @@ -{allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}. - -{allow, {user, "testuser"}, subscribe, ["a/b/c", "d/e/f/#"]}. - -{allow, {user, "admin"}, pubsub, ["a/b/c", "d/e/f/#"]}. - -{allow, {client, "testClient"}, subscribe, ["testTopics/testClient"]}. - -{allow, all, subscribe, ["clients/%c"]}. - -{allow, all, pubsub, ["users/%u/#"]}. - -{deny, all, subscribe, ["$SYS/#", "#"]}. - -{deny, all}. diff --git a/apps/emqx/test/emqx_access_SUITE_data/acl_deny_action.conf b/apps/emqx/test/emqx_access_SUITE_data/acl_deny_action.conf deleted file mode 100644 index c7f933ce7..000000000 --- a/apps/emqx/test/emqx_access_SUITE_data/acl_deny_action.conf +++ /dev/null @@ -1,3 +0,0 @@ -{deny, {user, "emqx"}, pubsub, ["acl_deny_action"]}. -{deny, {user, "pub_deny"}, publish, ["pub_deny"]}. -{allow, all}. \ No newline at end of file diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index e4a888d14..b356402fb 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -38,16 +38,9 @@ t_authenticate(_) -> emqx_zone:set_env(zone, allow_anonymous, true), ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). -t_check_acl(_) -> - emqx_zone:set_env(zone, acl_nomatch, deny), - application:set_env(emqx, enable_acl_cache, false), +t_authorize(_) -> Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), - ?assertEqual(deny, emqx_access_control:check_acl(clientinfo(), Publish, <<"t">>)), - - emqx_zone:set_env(zone, acl_nomatch, allow), - application:set_env(emqx, enable_acl_cache, true), - Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), - ?assertEqual(allow, emqx_access_control:check_acl(clientinfo(), Publish, <<"t">>)). + ?assertEqual(allow, emqx_access_control:authorize(clientinfo(), Publish, <<"t">>)). t_bypass_auth_plugins(_) -> ClientInfo = clientinfo(), diff --git a/apps/emqx/test/emqx_access_rule_SUITE.erl b/apps/emqx/test/emqx_access_rule_SUITE.erl deleted file mode 100644 index 93c84a958..000000000 --- a/apps/emqx/test/emqx_access_rule_SUITE.erl +++ /dev/null @@ -1,97 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2019-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_access_rule_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("eunit/include/eunit.hrl"). - -all() -> emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - emqx_ct_helpers:boot_modules([router, broker]), - emqx_ct_helpers:start_apps([]), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([]). - -t_compile(_) -> - Rule1 = {allow, all, pubsub, <<"%u">>}, - Compile1 = {allow, all, pubsub, [{pattern,[<<"%u">>]}]}, - - Rule2 = {allow, {ipaddr, "127.0.0.1"}, pubsub, <<"%c">>}, - Compile2 = {allow, {ipaddr, {{127,0,0,1}, {127,0,0,1}, 32}}, pubsub, [{pattern,[<<"%c">>]}]}, - - Rule3 = {allow, {'and', [{client, <<"testClient">>}, {user, <<"testUser">>}]}, pubsub, [<<"testTopics1">>, <<"testTopics2">>]}, - Compile3 = {allow, {'and', [{client, <<"testClient">>}, {user, <<"testUser">>}]}, pubsub, [[<<"testTopics1">>], [<<"testTopics2">>]]}, - - Rule4 = {allow, {'or', [{client, all}, {user, all}]}, pubsub, [ <<"testTopics1">>, <<"testTopics2">>]}, - Compile4 = {allow, {'or', [{client, all}, {user, all}]}, pubsub, [[<<"testTopics1">>], [<<"testTopics2">>]]}, - - ?assertEqual(Compile1, emqx_access_rule:compile(Rule1)), - ?assertEqual(Compile2, emqx_access_rule:compile(Rule2)), - ?assertEqual(Compile3, emqx_access_rule:compile(Rule3)), - ?assertEqual(Compile4, emqx_access_rule:compile(Rule4)). - -t_match(_) -> - ClientInfo1 = #{zone => external, - clientid => <<"testClient">>, - username => <<"TestUser">>, - peerhost => {127,0,0,1} - }, - ClientInfo2 = #{zone => external, - clientid => <<"testClient">>, - username => <<"TestUser">>, - peerhost => {192,168,0,10} - }, - ClientInfo3 = #{zone => external, - clientid => <<"testClient">>, - username => <<"TestUser">>, - peerhost => undefined - }, - ?assertEqual({matched, deny}, emqx_access_rule:match([], [], {deny, all})), - ?assertEqual({matched, allow}, emqx_access_rule:match([], [], {allow, all})), - ?assertEqual(nomatch, emqx_access_rule:match(ClientInfo1, <<"Test/Topic">>, - emqx_access_rule:compile({allow, {user, all}, pubsub, []}))), - ?assertEqual({matched, allow}, emqx_access_rule:match(ClientInfo1, <<"Test/Topic">>, - emqx_access_rule:compile({allow, {client, all}, pubsub, ["$SYS/#", "#"]}))), - ?assertEqual(nomatch, emqx_access_rule:match(ClientInfo3, <<"Test/Topic">>, - emqx_access_rule:compile({allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}))), - ?assertEqual({matched, allow}, emqx_access_rule:match(ClientInfo1, <<"Test/Topic">>, - emqx_access_rule:compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}))), - ?assertEqual({matched, allow}, emqx_access_rule:match(ClientInfo2, <<"Test/Topic">>, - emqx_access_rule:compile({allow, {ipaddr, "192.168.0.1/24"}, subscribe, ["$SYS/#", "#"]}))), - ?assertEqual({matched, allow}, emqx_access_rule:match(ClientInfo1, <<"d/e/f/x">>, - emqx_access_rule:compile({allow, {user, "TestUser"}, subscribe, ["a/b/c", "d/e/f/#"]}))), - ?assertEqual(nomatch, emqx_access_rule:match(ClientInfo1, <<"d/e/f/x">>, - emqx_access_rule:compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]}))), - ?assertEqual({matched, allow}, emqx_access_rule:match(ClientInfo1, <<"testTopics/testClient">>, - emqx_access_rule:compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]}))), - ?assertEqual({matched, allow}, emqx_access_rule:match(ClientInfo1, <<"clients/testClient">>, - emqx_access_rule:compile({allow, all, pubsub, ["clients/%c"]}))), - ?assertEqual({matched, allow}, emqx_access_rule:match(#{username => <<"user2">>}, <<"users/user2/abc/def">>, - emqx_access_rule:compile({allow, all, subscribe, ["users/%u/#"]}))), - ?assertEqual({matched, deny}, emqx_access_rule:match(ClientInfo1, <<"d/e/f">>, - emqx_access_rule:compile({deny, all, subscribe, ["$SYS/#", "#"]}))), - ?assertEqual(nomatch, emqx_access_rule:match(ClientInfo1, <<"Topic">>, - emqx_access_rule:compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, <<"Topic">>}))), - ?assertEqual({matched, allow}, emqx_access_rule:match(ClientInfo1, <<"Topic">>, - emqx_access_rule:compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"TestUser">>}]}, publish, <<"Topic">>}))), - ?assertEqual({matched, allow}, emqx_access_rule:match(ClientInfo1, <<"Topic">>, - emqx_access_rule:compile({allow, {'or', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, ["Topic"]}))). diff --git a/apps/emqx/test/emqx_acl_test_mod.erl b/apps/emqx/test/emqx_acl_test_mod.erl index da400f076..f88e0354b 100644 --- a/apps/emqx/test/emqx_acl_test_mod.erl +++ b/apps/emqx/test/emqx_acl_test_mod.erl @@ -18,14 +18,14 @@ %% ACL callbacks -export([ init/1 - , check_acl/2 + , authorize/2 , description/0 ]). init(AclOpts) -> {ok, AclOpts}. -check_acl({_User, _PubSub, _Topic}, _State) -> +authorize({_User, _PubSub, _Topic}, _State) -> allow. description() -> diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 9558dfd28..09ac7a683 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -37,7 +37,7 @@ init_per_suite(Config) -> ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), ok = meck:expect(emqx_access_control, authenticate, fun(_) -> {ok, #{auth_result => success}} end), - ok = meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> allow end), + ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), %% Broker Meck ok = meck:new(emqx_broker, [passthrough, no_history, no_link]), %% Hooks Meck diff --git a/apps/emqx/test/emqx_listeners_SUITE.erl b/apps/emqx/test/emqx_listeners_SUITE.erl index 53f388dfa..41b9126b0 100644 --- a/apps/emqx/test/emqx_listeners_SUITE.erl +++ b/apps/emqx/test/emqx_listeners_SUITE.erl @@ -28,6 +28,7 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> NewConfig = generate_config(), application:ensure_all_started(esockd), + application:ensure_all_started(quicer), application:ensure_all_started(cowboy), lists:foreach(fun set_app_env/1, NewConfig), Config. diff --git a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl index 8ce35b50c..2f3048277 100644 --- a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl @@ -23,6 +23,7 @@ -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). +-include_lib("common_test/include/ct.hrl"). -import(lists, [nth/2]). @@ -32,18 +33,37 @@ -define(WILD_TOPICS, [<<"TopicA/+">>, <<"+/C">>, <<"#">>, <<"/#">>, <<"/+">>, <<"+/+">>, <<"TopicA/#">>]). -all() -> emqx_ct:all(?MODULE). +all() -> + [ {group, tcp} + , {group, quic} + ]. + +groups() -> + TCs = emqx_ct:all(?MODULE), + [ {tcp, [], TCs} + , {quic, [], TCs} + ]. + +init_per_group(tcp, Config) -> + emqx_ct_helpers:start_apps([]), + [ {port, 1883}, {conn_fun, connect} | Config]; +init_per_group(quic, Config) -> + emqx_ct_helpers:start_apps([]), + [ {port, 14567}, {conn_fun, quic_connect} | Config]; +init_per_group(_, Config) -> + emqx_ct_helpers:stop_apps([]), + Config. + +end_per_group(_Group, _Config) -> + ok. init_per_suite(Config) -> - %% Meck emqtt - ok = meck:new(emqtt, [non_strict, passthrough, no_history, no_link]), %% Start Apps emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), Config. end_per_suite(_Config) -> - ok = meck:unload(emqtt), emqx_ct_helpers:stop_apps([]). init_per_testcase(TestCase, Config) -> @@ -97,9 +117,10 @@ waiting_client_process_exit(C) -> 1000 -> error({waiting_timeout, C}) end. -clean_retained(Topic) -> - {ok, Clean} = emqtt:start_link([{clean_start, true}]), - {ok, _} = emqtt:connect(Clean), +clean_retained(Topic, Config) -> + ConnFun = ?config(conn_fun, Config), + {ok, Clean} = emqtt:start_link([{clean_start, true} | Config]), + {ok, _} = emqtt:ConnFun(Clean), {ok, _} = emqtt:publish(Clean, Topic, #{}, <<"">>, [{qos, ?QOS_1}, {retain, true}]), ok = emqtt:disconnect(Clean). @@ -107,11 +128,12 @@ clean_retained(Topic) -> %% Test Cases %%-------------------------------------------------------------------- -t_basic_test(_) -> +t_basic_test(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), ct:print("Basic test starting"), - {ok, C} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(C), + {ok, C} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(C), {ok, _, [1]} = emqtt:subscribe(C, Topic, qos1), {ok, _, [2]} = emqtt:subscribe(C, Topic, qos2), {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), @@ -124,16 +146,17 @@ t_basic_test(_) -> %% Connection %%-------------------------------------------------------------------- -t_connect_clean_start(_) -> +t_connect_clean_start(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), {ok, Client1} = emqtt:start_link([{clientid, <<"t_connect_clean_start">>}, - {proto_ver, v5},{clean_start, true}]), - {ok, _} = emqtt:connect(Client1), + {proto_ver, v5},{clean_start, true} | Config]), + {ok, _} = emqtt:ConnFun(Client1), ?assertEqual(0, client_info(session_present, Client1)), %% [MQTT-3.1.2-4] ok = emqtt:pause(Client1), {ok, Client2} = emqtt:start_link([{clientid, <<"t_connect_clean_start">>}, - {proto_ver, v5},{clean_start, false}]), - {ok, _} = emqtt:connect(Client2), + {proto_ver, v5},{clean_start, false} | Config]), + {ok, _} = emqtt:ConnFun(Client2), ?assertEqual(1, client_info(session_present, Client2)), %% [MQTT-3.1.2-5] ?assertEqual(142, receive_disconnect_reasoncode()), waiting_client_process_exit(Client1), @@ -142,32 +165,32 @@ t_connect_clean_start(_) -> waiting_client_process_exit(Client2), {ok, Client3} = emqtt:start_link([{clientid, <<"new_client">>}, - {proto_ver, v5},{clean_start, false}]), - {ok, _} = emqtt:connect(Client3), + {proto_ver, v5},{clean_start, false} | Config]), + {ok, _} = emqtt:ConnFun(Client3), ?assertEqual(0, client_info(session_present, Client3)), %% [MQTT-3.1.2-6] ok = emqtt:disconnect(Client3), waiting_client_process_exit(Client3), process_flag(trap_exit, false). -t_connect_will_message(_) -> +t_connect_will_message(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Payload = "will message", - {ok, Client1} = emqtt:start_link([ - {proto_ver, v5}, - {clean_start, true}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, Payload} - ]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([ {proto_ver, v5}, + {clean_start, true}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, Payload} | Config + ]), + {ok, _} = emqtt:ConnFun(Client1), [ClientPid] = emqx_cm:lookup_channels(client_info(clientid, Client1)), Info = emqx_connection:info(sys:get_state(ClientPid)), ?assertNotEqual(undefined, maps:find(will_msg, Info)), %% [MQTT-3.1.2-7] - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, _, [2]} = emqtt:subscribe(Client2, Topic, qos2), ok = emqtt:disconnect(Client1, 4), %% [MQTT-3.14.2-1] @@ -178,27 +201,32 @@ t_connect_will_message(_) -> ?assertEqual({ok, 0}, maps:find(qos, Msg)), ok = emqtt:disconnect(Client2), - {ok, Client3} = emqtt:start_link([ - {proto_ver, v5}, - {clean_start, true}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, Payload} - ]), - {ok, _} = emqtt:connect(Client3), + {ok, Client3} = emqtt:start_link([ {proto_ver, v5}, + {clean_start, true}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, Payload} | Config + ]), + {ok, _} = emqtt:ConnFun(Client3), - {ok, Client4} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client4), + {ok, Client4} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client4), {ok, _, [2]} = emqtt:subscribe(Client4, Topic, qos2), ok = emqtt:disconnect(Client3), ?assertEqual(0, length(receive_messages(1))), %% [MQTT-3.1.2-10] ok = emqtt:disconnect(Client4). -t_batch_subscribe(_) -> - {ok, Client} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"batch_test">>}]), - {ok, _} = emqtt:connect(Client), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, acl_nomatch, deny), +t_batch_subscribe(init, Config) -> + ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_access_control, authorize, fun(_, _, _) -> deny end), + Config; +t_batch_subscribe('end', _Config) -> + meck:unload(emqx_access_control). + +t_batch_subscribe(Config) -> + ConnFun = ?config(conn_fun, Config), + {ok, Client} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"batch_test">>} | Config]), + {ok, _} = emqtt:ConnFun(Client), {ok, _, [?RC_NOT_AUTHORIZED, ?RC_NOT_AUTHORIZED, ?RC_NOT_AUTHORIZED]} = emqtt:subscribe(Client, [{<<"t1">>, qos1}, @@ -209,25 +237,25 @@ t_batch_subscribe(_) -> ?RC_NO_SUBSCRIPTION_EXISTED]} = emqtt:unsubscribe(Client, [<<"t1">>, <<"t2">>, <<"t3">>]), - application:set_env(emqx, acl_nomatch, allow), emqtt:disconnect(Client). -t_connect_will_retain(_) -> +t_connect_will_retain(Config) -> + ConnFun = ?config(conn_fun, Config), + process_flag(trap_exit, true), Topic = nth(1, ?TOPICS), Payload = "will message", - {ok, Client1} = emqtt:start_link([ - {proto_ver, v5}, - {clean_start, true}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, Payload}, - {will_retain, false} - ]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([ {proto_ver, v5}, + {clean_start, true}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, Payload}, + {will_retain, false} | Config + ]), + {ok, _} = emqtt:ConnFun(Client1), - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, _, [2]} = emqtt:subscribe(Client2, #{}, [{Topic, [{rap, true}, {qos, 2}]}]), ok = emqtt:disconnect(Client1, 4), @@ -235,27 +263,26 @@ t_connect_will_retain(_) -> ?assertEqual({ok, false}, maps:find(retain, Msg1)), %% [MQTT-3.1.2-14] ok = emqtt:disconnect(Client2), - {ok, Client3} = emqtt:start_link([ - {proto_ver, v5}, - {clean_start, true}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, Payload}, - {will_retain, true} - ]), - {ok, _} = emqtt:connect(Client3), + {ok, Client3} = emqtt:start_link([ {proto_ver, v5}, + {clean_start, true}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, Payload}, + {will_retain, true} | Config + ]), + {ok, _} = emqtt:ConnFun(Client3), - {ok, Client4} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client4), + {ok, Client4} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client4), {ok, _, [2]} = emqtt:subscribe(Client4, #{}, [{Topic, [{rap, true}, {qos, 2}]}]), ok = emqtt:disconnect(Client3, 4), [Msg2 | _ ] = receive_messages(1), ?assertEqual({ok, true}, maps:find(retain, Msg2)), %% [MQTT-3.1.2-15] ok = emqtt:disconnect(Client4), - clean_retained(Topic). + clean_retained(Topic, Config). -t_connect_idle_timeout(_) -> +t_connect_idle_timeout(_Config) -> IdleTimeout = 2000, emqx_zone:set_env(external, idle_timeout, IdleTimeout), @@ -263,25 +290,30 @@ t_connect_idle_timeout(_) -> timer:sleep(IdleTimeout), ?assertMatch({error, closed}, emqtt_sock:recv(Sock,1024)). -t_connect_limit_timeout(_) -> +t_connect_limit_timeout(init, Config) -> ok = meck:new(proplists, [non_strict, passthrough, no_history, no_link, unstick]), meck:expect(proplists, get_value, fun(active_n, _Options, _Default) -> 1; (Arg1, ARg2, Arg3) -> meck:passthrough([Arg1, ARg2, Arg3]) end), + Config; +t_connect_limit_timeout('end', _Config) -> + catch meck:unload(proplists). +t_connect_limit_timeout(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), emqx_zone:set_env(external, publish_limit, {3, 5}), - {ok, Client} = emqtt:start_link([{proto_ver, v5},{keepalive, 60}]), - {ok, _} = emqtt:connect(Client), + {ok, Client} = emqtt:start_link([{proto_ver, v5},{keepalive, 60} | Config]), + {ok, _} = emqtt:ConnFun(Client), [ClientPid] = emqx_cm:lookup_channels(client_info(clientid, Client)), ?assertEqual(undefined, emqx_connection:info(limit_timer, sys:get_state(ClientPid))), Payload = <<"t_shared_subscriptions_client_terminates_when_qos_eq_2">>, - ok = emqtt:publish(Client, Topic, Payload, 0), - ok = emqtt:publish(Client, Topic, Payload, 0), - ok = emqtt:publish(Client, Topic, Payload, 0), - timer:sleep(200), + {ok, 2} = emqtt:publish(Client, Topic, Payload, 1), + {ok, 3} = emqtt:publish(Client, Topic, Payload, 1), + {ok, 4} = emqtt:publish(Client, Topic, Payload, 1), + timer:sleep(250), ?assert(is_reference(emqx_connection:info(limit_timer, sys:get_state(ClientPid)))), ok = emqtt:disconnect(Client), @@ -301,9 +333,10 @@ t_connect_emit_stats_timeout('end', Config) -> ok. t_connect_emit_stats_timeout(Config) -> + ConnFun = ?config(conn_fun, Config), {_, IdleTimeout} = lists:keyfind(idle_timeout, 1, Config), - {ok, Client} = emqtt:start_link([{proto_ver, v5},{keepalive, 60}]), - {ok, _} = emqtt:connect(Client), + {ok, Client} = emqtt:start_link([{proto_ver, v5},{keepalive, 60} | Config]), + {ok, _} = emqtt:ConnFun(Client), [ClientPid] = emqx_cm:lookup_channels(client_info(clientid, Client)), ?assert(is_reference(emqx_connection:info(stats_timer, sys:get_state(ClientPid)))), ?block_until(#{?snk_kind := cancel_stats_timer}, IdleTimeout * 2, _BackInTime = 0), @@ -311,15 +344,16 @@ t_connect_emit_stats_timeout(Config) -> ok = emqtt:disconnect(Client). %% [MQTT-3.1.2-22] -t_connect_keepalive_timeout(_) -> +t_connect_keepalive_timeout(Config) -> + ConnFun = ?config(conn_fun, Config), %% Prevent the emqtt client bringing us down on the disconnect. process_flag(trap_exit, true), Keepalive = 2, {ok, Client} = emqtt:start_link([{proto_ver, v5}, - {keepalive, Keepalive}]), - {ok, _} = emqtt:connect(Client), + {keepalive, Keepalive} | Config]), + {ok, _} = emqtt:ConnFun(Client), emqtt:pause(Client), receive {disconnected, ReasonCode, _Channel} -> ?assertEqual(141, ReasonCode) @@ -328,30 +362,30 @@ t_connect_keepalive_timeout(_) -> end. %% [MQTT-3.1.2-23] -t_connect_session_expiry_interval(_) -> +t_connect_session_expiry_interval(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Payload = "test message", - {ok, Client1} = emqtt:start_link([ - {clientid, <<"t_connect_session_expiry_interval">>}, - {proto_ver, v5}, - {properties, #{'Session-Expiry-Interval' => 7200}} + {ok, Client1} = emqtt:start_link([ {clientid, <<"t_connect_session_expiry_interval">>}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 7200}} + | Config ]), - {ok, _} = emqtt:connect(Client1), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Topic, qos2), ok = emqtt:disconnect(Client1), - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, 2} = emqtt:publish(Client2, Topic, Payload, 2), ok = emqtt:disconnect(Client2), - {ok, Client3} = emqtt:start_link([ - {clientid, <<"t_connect_session_expiry_interval">>}, - {proto_ver, v5}, - {clean_start, false} + {ok, Client3} = emqtt:start_link([ {clientid, <<"t_connect_session_expiry_interval">>}, + {proto_ver, v5}, + {clean_start, false} | Config ]), - {ok, _} = emqtt:connect(Client3), + {ok, _} = emqtt:ConnFun(Client3), [Msg | _ ] = receive_messages(1), ?assertEqual({ok, iolist_to_binary(Topic)}, maps:find(topic, Msg)), ?assertEqual({ok, iolist_to_binary(Payload)}, maps:find(payload, Msg)), @@ -360,13 +394,13 @@ t_connect_session_expiry_interval(_) -> %% [MQTT-3.1.3-9] %% !!!REFACTOR NEED: -%t_connect_will_delay_interval(_) -> +%t_connect_will_delay_interval(Config) -> % process_flag(trap_exit, true), % Topic = nth(1, ?TOPICS), % Payload = "will message", % -% {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), -% {ok, _} = emqtt:connect(Client1), +% {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), +% {ok, _} = emqtt:ConnFun(Client1), % {ok, _, [2]} = emqtt:subscribe(Client1, Topic, qos2), % % {ok, Client2} = emqtt:start_link([ @@ -379,9 +413,9 @@ t_connect_session_expiry_interval(_) -> % {will_payload, Payload}, % {will_props, #{'Will-Delay-Interval' => 3}}, % {properties, #{'Session-Expiry-Interval' => 7200}}, -% {keepalive, 2} +% {keepalive, 2} | Config % ]), -% {ok, _} = emqtt:connect(Client2), +% {ok, _} = emqtt:ConnFun(Client2), % timer:sleep(50), % erlang:exit(Client2, kill), % timer:sleep(2000), @@ -399,9 +433,9 @@ t_connect_session_expiry_interval(_) -> % {will_payload, Payload}, % {will_props, #{'Will-Delay-Interval' => 7200}}, % {properties, #{'Session-Expiry-Interval' => 3}}, -% {keepalive, 2} +% {keepalive, 2} | Config % ]), -% {ok, _} = emqtt:connect(Client3), +% {ok, _} = emqtt:ConnFun(Client3), % timer:sleep(50), % erlang:exit(Client3, kill), % @@ -418,18 +452,17 @@ t_connect_session_expiry_interval(_) -> % process_flag(trap_exit, false). %% [MQTT-3.1.4-3] -t_connect_duplicate_clientid(_) -> +t_connect_duplicate_clientid(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), - {ok, Client1} = emqtt:start_link([ - {clientid, <<"t_connect_duplicate_clientid">>}, - {proto_ver, v5} - ]), - {ok, _} = emqtt:connect(Client1), - {ok, Client2} = emqtt:start_link([ - {clientid, <<"t_connect_duplicate_clientid">>}, - {proto_ver, v5} - ]), - {ok, _} = emqtt:connect(Client2), + {ok, Client1} = emqtt:start_link([ {clientid, <<"t_connect_duplicate_clientid">>}, + {proto_ver, v5} | Config + ]), + {ok, _} = emqtt:ConnFun(Client1), + {ok, Client2} = emqtt:start_link([ {clientid, <<"t_connect_duplicate_clientid">>}, + {proto_ver, v5} | Config + ]), + {ok, _} = emqtt:ConnFun(Client2), ?assertEqual(142, receive_disconnect_reasoncode()), waiting_client_process_exit(Client1), @@ -441,28 +474,33 @@ t_connect_duplicate_clientid(_) -> %% Connack %%-------------------------------------------------------------------- -t_connack_session_present(_) -> - {ok, Client1} = emqtt:start_link([ - {clientid, <<"t_connect_duplicate_clientid">>}, - {proto_ver, v5}, - {properties, #{'Session-Expiry-Interval' => 7200}}, - {clean_start, true} - ]), - {ok, _} = emqtt:connect(Client1), +t_connack_session_present(Config) -> + ConnFun = ?config(conn_fun, Config), + {ok, Client1} = emqtt:start_link([ {clientid, <<"t_connect_duplicate_clientid">>}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 7200}}, + {clean_start, true} | Config + ]), + {ok, _} = emqtt:ConnFun(Client1), ?assertEqual(0, client_info(session_present, Client1)), %% [MQTT-3.2.2-2] ok = emqtt:disconnect(Client1), - {ok, Client2} = emqtt:start_link([ - {clientid, <<"t_connect_duplicate_clientid">>}, - {proto_ver, v5}, - {properties, #{'Session-Expiry-Interval' => 7200}}, - {clean_start, false} - ]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([ {clientid, <<"t_connect_duplicate_clientid">>}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 7200}}, + {clean_start, false} | Config + ]), + {ok, _} = emqtt:ConnFun(Client2), ?assertEqual(1, client_info(session_present, Client2)), %% [[MQTT-3.2.2-3]] ok = emqtt:disconnect(Client2). -t_connack_max_qos_allowed(_) -> +t_connack_max_qos_allowed(init, Config) -> + Config; +t_connack_max_qos_allowed('end', _Config) -> + emqx_zone:set_env(external, max_qos_allowed, 2), + ok. +t_connack_max_qos_allowed(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), Topic = nth(1, ?TOPICS), @@ -471,8 +509,8 @@ t_connack_max_qos_allowed(_) -> persistent_term:erase({emqx_zone, external, '$mqtt_caps'}), persistent_term:erase({emqx_zone, external, '$mqtt_pub_caps'}), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, Connack1} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, Connack1} = emqtt:ConnFun(Client1), ?assertEqual(0, maps:get('Maximum-QoS', Connack1)), %% [MQTT-3.2.2-9] {ok, _, [0]} = emqtt:subscribe(Client1, Topic, 0), %% [MQTT-3.2.2-10] @@ -483,14 +521,13 @@ t_connack_max_qos_allowed(_) -> ?assertEqual(155, receive_disconnect_reasoncode()), %% [MQTT-3.2.2-11] waiting_client_process_exit(Client1), - {ok, Client2} = emqtt:start_link([ - {proto_ver, v5}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, <<"Unsupported Qos">>}, - {will_qos, 2} - ]), - {error, Connack2} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([ {proto_ver, v5}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, <<"Unsupported Qos">>}, + {will_qos, 2} | Config + ]), + {error, Connack2} = emqtt:ConnFun(Client2), ?assertMatch({qos_not_supported, _}, Connack2), %% [MQTT-3.2.2-12] waiting_client_process_exit(Client2), @@ -499,8 +536,8 @@ t_connack_max_qos_allowed(_) -> persistent_term:erase({emqx_zone, external, '$mqtt_caps'}), persistent_term:erase({emqx_zone, external, '$mqtt_pub_caps'}), - {ok, Client3} = emqtt:start_link([{proto_ver, v5}]), - {ok, Connack3} = emqtt:connect(Client3), + {ok, Client3} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, Connack3} = emqtt:ConnFun(Client3), ?assertEqual(1, maps:get('Maximum-QoS', Connack3)), %% [MQTT-3.2.2-9] {ok, _, [0]} = emqtt:subscribe(Client3, Topic, 0), %% [MQTT-3.2.2-10] @@ -511,14 +548,13 @@ t_connack_max_qos_allowed(_) -> ?assertEqual(155, receive_disconnect_reasoncode()), %% [MQTT-3.2.2-11] waiting_client_process_exit(Client3), - {ok, Client4} = emqtt:start_link([ - {proto_ver, v5}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, <<"Unsupported Qos">>}, - {will_qos, 2} - ]), - {error, Connack4} = emqtt:connect(Client4), + {ok, Client4} = emqtt:start_link([ {proto_ver, v5}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, <<"Unsupported Qos">>}, + {will_qos, 2} | Config + ]), + {error, Connack4} = emqtt:ConnFun(Client4), ?assertMatch({qos_not_supported, _}, Connack4), %% [MQTT-3.2.2-12] waiting_client_process_exit(Client4), @@ -527,17 +563,18 @@ t_connack_max_qos_allowed(_) -> persistent_term:erase({emqx_zone, external, '$mqtt_caps'}), persistent_term:erase({emqx_zone, external, '$mqtt_pub_caps'}), - {ok, Client5} = emqtt:start_link([{proto_ver, v5}]), - {ok, Connack5} = emqtt:connect(Client5), + {ok, Client5} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, Connack5} = emqtt:ConnFun(Client5), ?assertEqual(undefined, maps:get('Maximum-QoS', Connack5, undefined)), %% [MQTT-3.2.2-9] ok = emqtt:disconnect(Client5), waiting_client_process_exit(Client5), process_flag(trap_exit, false). -t_connack_assigned_clienid(_) -> - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), +t_connack_assigned_clienid(Config) -> + ConnFun = ?config(conn_fun, Config), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), ?assert(is_binary(client_info(clientid, Client1))), %% [MQTT-3.2.2-16] ok = emqtt:disconnect(Client1). @@ -545,11 +582,12 @@ t_connack_assigned_clienid(_) -> %% Publish %%-------------------------------------------------------------------- -t_publish_rap(_) -> +t_publish_rap(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, #{}, [{Topic, [{rap, true}, {qos, 2}]}]), {ok, _} = emqtt:publish(Client1, Topic, #{}, <<"retained message">>, [{qos, ?QOS_1}, {retain, true}]), @@ -557,8 +595,8 @@ t_publish_rap(_) -> ?assertEqual(true, maps:get(retain, Msg1)), %% [MQTT-3.3.1-12] ok = emqtt:disconnect(Client1), - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, _, [2]} = emqtt:subscribe(Client2, #{}, [{Topic, [{rap, false}, {qos, 2}]}]), {ok, _} = emqtt:publish(Client2, Topic, #{}, <<"retained message">>, [{qos, ?QOS_1}, {retain, true}]), @@ -566,44 +604,47 @@ t_publish_rap(_) -> ?assertEqual(false, maps:get(retain, Msg2)), %% [MQTT-3.3.1-13] ok = emqtt:disconnect(Client2), - clean_retained(Topic). + clean_retained(Topic, Config). -t_publish_wildtopic(_) -> +t_publish_wildtopic(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), Topic = nth(1, ?WILD_TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), ok = emqtt:publish(Client1, Topic, <<"error topic">>), ?assertEqual(144, receive_disconnect_reasoncode()), waiting_client_process_exit(Client1), process_flag(trap_exit, false). -t_publish_payload_format_indicator(_) -> +t_publish_payload_format_indicator(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Properties = #{'Payload-Format-Indicator' => 233}, - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Topic, qos2), ok = emqtt:publish(Client1, Topic, Properties, <<"Payload Format Indicator">>, [{qos, ?QOS_0}]), [Msg1 | _] = receive_messages(1), ?assertEqual(Properties, maps:get(properties, Msg1)), %% [MQTT-3.3.2-6] ok = emqtt:disconnect(Client1). -t_publish_topic_alias(_) -> +t_publish_topic_alias(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), Topic = nth(1, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), ok = emqtt:publish(Client1, Topic, #{'Topic-Alias' => 0}, <<"Topic-Alias">>, [{qos, ?QOS_0}]), ?assertEqual(148, receive_disconnect_reasoncode()), %% [MQTT-3.3.2-8] waiting_client_process_exit(Client1), - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, _, [2]} = emqtt:subscribe(Client2, Topic, qos2), ok = emqtt:publish(Client2, Topic, #{'Topic-Alias' => 233}, <<"Topic-Alias">>, [{qos, ?QOS_0}]), @@ -615,12 +656,13 @@ t_publish_topic_alias(_) -> process_flag(trap_exit, false). -t_publish_response_topic(_) -> +t_publish_response_topic(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), Topic = nth(1, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), ok = emqtt:publish(Client1, Topic, #{'Response-Topic' => nth(1, ?WILD_TOPICS)}, <<"Response-Topic">>, [{qos, ?QOS_0}]), ?assertEqual(130, receive_disconnect_reasoncode()), %% [MQTT-3.3.2-14] @@ -628,7 +670,8 @@ t_publish_response_topic(_) -> process_flag(trap_exit, false). -t_publish_properties(_) -> +t_publish_properties(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Properties = #{ 'Response-Topic' => Topic, %% [MQTT-3.3.2-15] @@ -637,20 +680,21 @@ t_publish_properties(_) -> 'Content-Type' => <<"2333">> %% [MQTT-3.3.2-20] }, - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Topic, qos2), ok = emqtt:publish(Client1, Topic, Properties, <<"Publish Properties">>, [{qos, ?QOS_0}]), [Msg1 | _] = receive_messages(1), ?assertEqual(Properties, maps:get(properties, Msg1)), %% [MQTT-3.3.2-16] ok = emqtt:disconnect(Client1). -t_publish_overlapping_subscriptions(_) -> +t_publish_overlapping_subscriptions(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Properties = #{'Subscription-Identifier' => 2333}, - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [1]} = emqtt:subscribe(Client1, Properties, nth(1, ?WILD_TOPICS), qos1), {ok, _, [0]} = emqtt:subscribe(Client1, Properties, nth(3, ?WILD_TOPICS), qos0), {ok, _} = emqtt:publish(Client1, Topic, #{}, @@ -665,13 +709,15 @@ t_publish_overlapping_subscriptions(_) -> %% Subsctibe %%-------------------------------------------------------------------- -t_subscribe_topic_alias(_) -> +t_subscribe_topic_alias(Config) -> + ConnFun = ?config(conn_fun, Config), Topic1 = nth(1, ?TOPICS), Topic2 = nth(2, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}, - {properties, #{'Topic-Alias-Maximum' => 1}} + {ok, Client1} = emqtt:start_link([ {proto_ver, v5}, + {properties, #{'Topic-Alias-Maximum' => 1}} + | Config ]), - {ok, _} = emqtt:connect(Client1), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Topic1, qos2), {ok, _, [2]} = emqtt:subscribe(Client1, Topic2, qos2), @@ -692,27 +738,29 @@ t_subscribe_topic_alias(_) -> ok = emqtt:disconnect(Client1). -t_subscribe_no_local(_) -> +t_subscribe_no_local(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, #{}, [{Topic, [{nl, true}, {qos, 2}]}]), - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, _, [2]} = emqtt:subscribe(Client2, #{}, [{Topic, [{nl, true}, {qos, 2}]}]), ok = emqtt:publish(Client1, Topic, <<"t_subscribe_no_local">>, 0), ?assertEqual(1, length(receive_messages(2))), %% [MQTT-3.8.3-3] ok = emqtt:disconnect(Client1). -t_subscribe_actions(_) -> +t_subscribe_actions(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Properties = #{'Subscription-Identifier' => 2333}, - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Properties, Topic, qos2), {ok, _, [1]} = emqtt:subscribe(Client1, Properties, Topic, qos1), {ok, _} = emqtt:publish(Client1, Topic, <<"t_subscribe_actions">>, 2), @@ -726,12 +774,13 @@ t_subscribe_actions(_) -> %% Unsubsctibe Unsuback %%-------------------------------------------------------------------- -t_unscbsctibe(_) -> +t_unscbsctibe(Config) -> + ConnFun = ?config(conn_fun, Config), Topic1 = nth(1, ?TOPICS), Topic2 = nth(2, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Topic1, qos2), {ok, _, [0]} = emqtt:unsubscribe(Client1, Topic1), %% [MQTT-3.10.4-4] {ok, _, [17]} = emqtt:unsubscribe(Client1, <<"noExistTopic">>), %% [MQTT-3.10.4-5] @@ -745,9 +794,10 @@ t_unscbsctibe(_) -> %% Pingreq %%-------------------------------------------------------------------- -t_pingreq(_) -> - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), +t_pingreq(Config) -> + ConnFun = ?config(conn_fun, Config), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), pong = emqtt:ping(Client1), %% [MQTT-3.12.4-1] ok = emqtt:disconnect(Client1). @@ -755,7 +805,14 @@ t_pingreq(_) -> %% Shared Subscriptions %%-------------------------------------------------------------------- -t_shared_subscriptions_client_terminates_when_qos_eq_2(_) -> +t_shared_subscriptions_client_terminates_when_qos_eq_2(init, Config) -> + ok = meck:new(emqtt, [non_strict, passthrough, no_history, no_link]), + Config; +t_shared_subscriptions_client_terminates_when_qos_eq_2('end', _Config) -> + catch meck:unload(emqtt). + +t_shared_subscriptions_client_terminates_when_qos_eq_2(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), application:set_env(emqx, shared_dispatch_ack_enabled, true), @@ -766,32 +823,33 @@ t_shared_subscriptions_client_terminates_when_qos_eq_2(_) -> meck:expect(emqtt, connected, fun(cast, ?PUBLISH_PACKET(?QOS_2, _PacketId), _State) -> ok = counters:add(CRef, 1, 1), - {stop, {shutdown, for_testiong}}; + {stop, {shutdown, for_testing}}; (Arg1, ARg2, Arg3) -> meck:passthrough([Arg1, ARg2, Arg3]) end), - {ok, Sub1} = emqtt:start_link([{proto_ver, v5}, + {ok, Sub1} = emqtt:start_link([ {proto_ver, v5}, {clientid, <<"sub_client_1">>}, - {keepalive, 5}]), - {ok, _} = emqtt:connect(Sub1), + {keepalive, 5} | Config + ]), + {ok, _} = emqtt:ConnFun(Sub1), {ok, _, [2]} = emqtt:subscribe(Sub1, SharedTopic, qos2), {ok, Sub2} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"sub_client_2">>}, - {keepalive, 5}]), - {ok, _} = emqtt:connect(Sub2), + {keepalive, 5} | Config]), + {ok, _} = emqtt:ConnFun(Sub2), {ok, _, [2]} = emqtt:subscribe(Sub2, SharedTopic, qos2), - {ok, Pub} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"pub_client">>}]), - {ok, _} = emqtt:connect(Pub), + {ok, Pub} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"pub_client">>} | Config]), + {ok, _} = emqtt:ConnFun(Pub), {ok, _} = emqtt:publish(Pub, Topic, <<"t_shared_subscriptions_client_terminates_when_qos_eq_2">>, 2), receive - {'EXIT', _,{shutdown, for_testiong}} -> + {'EXIT', _,{shutdown, for_testing}} -> ok after 1000 -> - error("disconnected timeout") + ct:fail("disconnected timeout") end, ?assertEqual(1, counters:get(CRef, 1)), diff --git a/apps/emqx/test/emqx_plugins_SUITE.erl b/apps/emqx/test/emqx_plugins_SUITE.erl index 6a76cb9d2..a7dd91602 100644 --- a/apps/emqx/test/emqx_plugins_SUITE.erl +++ b/apps/emqx/test/emqx_plugins_SUITE.erl @@ -63,64 +63,28 @@ t_load(_) -> ?assertEqual({error, not_started}, emqx_plugins:unload(emqx_hocon_plugin)), application:set_env(emqx, expand_plugins_dir, undefined), - application:set_env(emqx, plugins_loaded_file, undefined), - ?assertEqual(ignore, emqx_plugins:load()), - ?assertEqual(ignore, emqx_plugins:unload()). - - -t_init_config(_) -> - ConfFile = "emqx_mini_plugin.config", - Data = "[{emqx_mini_plugin,[{mininame ,test}]}].", - file:write_file(ConfFile, list_to_binary(Data)), - ?assertEqual(ok, emqx_plugins:init_config(ConfFile)), - file:delete(ConfFile), - ?assertEqual({ok,test}, application:get_env(emqx_mini_plugin, mininame)). + application:set_env(emqx, plugins_loaded_file, undefined). t_load_ext_plugin(_) -> ?assertError({plugin_app_file_not_found, _}, emqx_plugins:load_ext_plugin("./not_existed_path/")). t_list(_) -> - ?assertMatch([{plugin, _, _, _, _, _, _, _} | _ ], emqx_plugins:list()). + ?assertMatch([{plugin, _, _, _, _, _, _} | _ ], emqx_plugins:list()). t_find_plugin(_) -> - ?assertMatch({plugin, emqx_mini_plugin, _, _, _, _, _, _}, emqx_plugins:find_plugin(emqx_mini_plugin)), - ?assertMatch({plugin, emqx_hocon_plugin, _, _, _, _, _, _}, emqx_plugins:find_plugin(emqx_hocon_plugin)). - -t_plugin_type(_) -> - ?assertEqual(auth, emqx_plugins:plugin_type(auth)), - ?assertEqual(protocol, emqx_plugins:plugin_type(protocol)), - ?assertEqual(backend, emqx_plugins:plugin_type(backend)), - ?assertEqual(bridge, emqx_plugins:plugin_type(bridge)), - ?assertEqual(feature, emqx_plugins:plugin_type(undefined)). - -t_with_loaded_file(_) -> - ?assertMatch({error, _}, emqx_plugins:with_loaded_file("./not_existed_path/", fun(_) -> ok end)). - -t_plugin_loaded(_) -> - ?assertEqual(ok, emqx_plugins:plugin_loaded(emqx_mini_plugin, false)), - ?assertEqual(ok, emqx_plugins:plugin_loaded(emqx_mini_plugin, true)), - ?assertEqual(ok, emqx_plugins:plugin_loaded(emqx_hocon_plugin, false)), - ?assertEqual(ok, emqx_plugins:plugin_loaded(emqx_hocon_plugin, true)). - -t_plugin_unloaded(_) -> - ?assertEqual(ok, emqx_plugins:plugin_unloaded(emqx_mini_plugin, false)), - ?assertEqual(ok, emqx_plugins:plugin_unloaded(emqx_mini_plugin, true)), - ?assertEqual(ok, emqx_plugins:plugin_unloaded(emqx_hocon_plugin, false)), - ?assertEqual(ok, emqx_plugins:plugin_unloaded(emqx_hocon_plugin, true)). + ?assertMatch({plugin, emqx_mini_plugin, _, _, _, _, _}, emqx_plugins:find_plugin(emqx_mini_plugin)), + ?assertMatch({plugin, emqx_hocon_plugin, _, _, _, _, _}, emqx_plugins:find_plugin(emqx_hocon_plugin)). t_plugin(_) -> try - emqx_plugins:plugin(not_existed_plugin, undefined) + emqx_plugins:plugin(not_existed_plugin) catch _Error:Reason:_Stacktrace -> ?assertEqual({plugin_not_found,not_existed_plugin}, Reason) end, - ?assertMatch({plugin, emqx_mini_plugin, _, _, _, _, _, _}, emqx_plugins:plugin(emqx_mini_plugin, undefined)), - ?assertMatch({plugin, emqx_hocon_plugin, _, _, _, _, _, _}, emqx_plugins:plugin(emqx_hocon_plugin, undefined)). - -t_filter_plugins(_) -> - ?assertEqual([name1, name2], emqx_plugins:filter_plugins([name1, {name2,true}, {name3, false}])). + ?assertMatch({plugin, emqx_mini_plugin, _, _, _, _, _}, emqx_plugins:plugin(emqx_mini_plugin)), + ?assertMatch({plugin, emqx_hocon_plugin, _, _, _, _, _}, emqx_plugins:plugin(emqx_hocon_plugin)). t_load_plugin(_) -> ok = meck:new(application, [unstick, non_strict, passthrough, no_history]), @@ -133,9 +97,9 @@ t_load_plugin(_) -> ok = meck:new(emqx_plugins, [unstick, non_strict, passthrough, no_history]), ok = meck:expect(emqx_plugins, generate_configs, fun(_) -> ok end), ok = meck:expect(emqx_plugins, apply_configs, fun(_) -> ok end), - ?assertMatch({error, _}, emqx_plugins:load_plugin(already_loaded_app, true)), - ?assertMatch(ok, emqx_plugins:load_plugin(normal, true)), - ?assertMatch({error,_}, emqx_plugins:load_plugin(error_app, true)), + ?assertMatch({error, _}, emqx_plugins:load_plugin(already_loaded_app)), + ?assertMatch(ok, emqx_plugins:load_plugin(normal)), + ?assertMatch({error,_}, emqx_plugins:load_plugin(error_app)), ok = meck:unload(emqx_plugins), ok = meck:unload(application). @@ -146,8 +110,8 @@ t_unload_plugin(_) -> (error_app) -> {error, error}; (_) -> ok end), - ?assertEqual(ok, emqx_plugins:unload_plugin(not_started_app, true)), - ?assertEqual(ok, emqx_plugins:unload_plugin(normal, true)), - ?assertEqual({error,error}, emqx_plugins:unload_plugin(error_app, true)), + ?assertEqual(ok, emqx_plugins:unload_plugin(not_started_app)), + ?assertEqual(ok, emqx_plugins:unload_plugin(normal)), + ?assertEqual({error,error}, emqx_plugins:unload_plugin(error_app)), ok = meck:unload(application). diff --git a/apps/emqx/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl index 6db831972..93c192b86 100644 --- a/apps/emqx/test/emqx_ws_connection_SUITE.erl +++ b/apps/emqx/test/emqx_ws_connection_SUITE.erl @@ -64,7 +64,7 @@ init_per_testcase(TestCase, Config) when end), %% Mock emqx_access_control ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), - ok = meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> allow end), + ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), %% Mock emqx_hooks ok = meck:new(emqx_hooks, [passthrough, no_history, no_link]), ok = meck:expect(emqx_hooks, run, fun(_Hook, _Args) -> ok end), diff --git a/apps/emqx_auth_http/.gitignore b/apps/emqx_auth_http/.gitignore deleted file mode 100644 index 557a3a337..000000000 --- a/apps/emqx_auth_http/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -.eunit -deps -*.o -*.beam -*.plt -erl_crash.dump -ebin -rel/example_project -.concrete/DEV_MODE -.rebar -.erlang.mk/ -emqx_auth_http.d -data -ct.cover.spec -cover/ -ct.coverdata -eunit.coverdata -logs/ -erlang.mk -_build/ -rebar.lock -rebar3.crashdump -etc/emqx_auth_http.conf.rendered -.rebar3/ -*.swp diff --git a/apps/emqx_auth_http/README.md b/apps/emqx_auth_http/README.md deleted file mode 100644 index ed743334a..000000000 --- a/apps/emqx_auth_http/README.md +++ /dev/null @@ -1,100 +0,0 @@ -emqx_auth_http -============== - -EMQ X HTTP Auth/ACL Plugin - -Build ------ - -``` -make && make tests -``` - -Configure the Plugin --------------------- - -File: etc/emqx_auth_http.conf - -``` -##-------------------------------------------------------------------- -## Authentication request. -## -## Variables: -## - %u: username -## - %c: clientid -## - %a: ipaddress -## - %r: protocol -## - %P: password -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -## Value: URL -auth.http.auth_req = http://127.0.0.1:8080/mqtt/auth -## Value: post | get | put -auth.http.auth_req.method = post -## Value: Params -auth.http.auth_req.params = clientid=%c,username=%u,password=%P - -##-------------------------------------------------------------------- -## Superuser request. -## -## Variables: -## - %u: username -## - %c: clientid -## - %a: ipaddress -## - %r: protocol -## - %P: password -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -## Value: URL -auth.http.super_req = http://127.0.0.1:8080/mqtt/superuser -## Value: post | get | put -auth.http.super_req.method = post -## Value: Params -auth.http.super_req.params = clientid=%c,username=%u - -##-------------------------------------------------------------------- -## ACL request. -## -## Variables: -## - %A: 1 | 2, 1 = sub, 2 = pub -## - %u: username -## - %c: clientid -## - %a: ipaddress -## - %r: protocol -## - %m: mountpoint -## - %t: topic -## -## Value: URL -auth.http.acl_req = http://127.0.0.1:8080/mqtt/acl -## Value: post | get | put -auth.http.acl_req.method = get -## Value: Params -auth.http.acl_req.params = access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t -``` - -Load the Plugin ---------------- - -``` -./bin/emqx_ctl plugins load emqx_auth_http -``` - -HTTP API --------- - -200 if ok - -4xx if unauthorized - -License -------- - -Apache License Version 2.0 - -Author ------- - -EMQ X Team. - diff --git a/apps/emqx_auth_http/etc/emqx_auth_http.conf b/apps/emqx_auth_http/etc/emqx_auth_http.conf deleted file mode 100644 index 56e2055c0..000000000 --- a/apps/emqx_auth_http/etc/emqx_auth_http.conf +++ /dev/null @@ -1,172 +0,0 @@ -##-------------------------------------------------------------------- -## HTTP Auth/ACL Plugin -##-------------------------------------------------------------------- - -## HTTP URL API path for Auth Request -## -## Value: URL -## -## Examples: http://127.0.0.1:80/mqtt/auth, https://[::1]:80/mqtt/auth -auth.http.auth_req.url = "http://127.0.0.1:80/mqtt/auth" - -## HTTP Request Method for Auth Request -## -## Value: post | get -auth.http.auth_req.method = post - -## HTTP Request Headers for Auth Request, Content-Type header is configured by default. -## The possible values of the Content-Type header: application/x-www-form-urlencoded, application/json -## -## Examples: auth.http.auth_req.headers.accept = */* - -auth.http.auth_req.headers.content_type = "application/x-www-form-urlencoded" - -## Parameters used to construct the request body or query string parameters -## When the request method is GET, these parameters will be converted into query string parameters -## When the request method is POST, the final format is determined by content-type -## -## Available Variables: -## - %u: username -## - %c: clientid -## - %a: ipaddress -## - %r: protocol -## - %P: password -## - %p: sockport of server accepted -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -## Value: =,=,... -auth.http.auth_req.params = "clientid=%c,username=%u,password=%P" - -## HTTP URL API path for SuperUser Request -## -## Value: URL -## -## Examples: http://127.0.0.1:80/mqtt/superuser, https://[::1]:80/mqtt/superuser -auth.http.super_req.url = "http://127.0.0.1:80/mqtt/superuser" - -## HTTP Request Method for SuperUser Request -## -## Value: post | get -auth.http.super_req.method = post - -## HTTP Request Headers for SuperUser Request, Content-Type header is configured by default. -## The possible values of the Content-Type header: application/x-www-form-urlencoded, application/json -## -## Examples: auth.http.super_req.headers.accept = */* -auth.http.super_req.headers.content-type = "application/x-www-form-urlencoded" - -## Parameters used to construct the request body or query string parameters -## When the request method is GET, these parameters will be converted into query string parameters -## When the request method is POST, the final format is determined by content-type -## -## Available Variables: -## - %u: username -## - %c: clientid -## - %a: ipaddress -## - %r: protocol -## - %P: password -## - %p: sockport of server accepted -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -## Value: =,=,... -auth.http.super_req.params = "clientid=%c,username=%u" - -## HTTP URL API path for ACL Request -## Comment out this config to disable ACL checks -## -## Value: URL -## -## Examples: http://127.0.0.1:80/mqtt/acl, https://[::1]:80/mqtt/acl -auth.http.acl_req.url = "http://127.0.0.1:80/mqtt/acl" - -## HTTP Request Method for ACL Request -## -## Value: post | get -auth.http.acl_req.method = post - -## HTTP Request Headers for ACL Request, Content-Type header is configured by default. -## The possible values of the Content-Type header: application/x-www-form-urlencoded, application/json -## -## Examples: auth.http.acl_req.headers.accept = */* -auth.http.acl_req.headers.content-type = "application/x-www-form-urlencoded" - -## Parameters used to construct the request body or query string parameters -## When the request method is GET, these parameters will be converted into query string parameters -## When the request method is POST, the final format is determined by content-type -## -## Available Variables: -## - %A: access (1 - subscribe, 2 - publish) -## - %u: username -## - %c: clientid -## - %a: ipaddress -## - %r: protocol -## - %P: password -## - %p: sockport of server accepted -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## - %t: topic -## -## Value: =,=,... -auth.http.acl_req.params = "access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t,mountpoint=%m" - -## Time-out time for the request. -## -## Value: Duration -## -h: hour, e.g. '2h' for 2 hours -## -m: minute, e.g. '5m' for 5 minutes -## -s: second, e.g. '30s' for 30 seconds -## -## Default: 5s -auth.http.timeout = 5s - -## Connection time-out time, used during the initial request, -## when the client is connecting to the server. -## -## Value: Duration -## -h: hour, e.g. '2h' for 2 hours -## -m: minute, e.g. '5m' for 5 minutes -## -s: second, e.g. '30s' for 30 seconds -## -## Default: 5s -auth.http.connect_timeout = 5s - -## Connection process pool size -## -## Value: Number -auth.http.pool_size = 32 - -##------------------------------------------------------------------------------ -## SSL options - -## Path to the file containing PEM-encoded CA certificates. The CA certificates -## are used during server authentication and when building the client certificate chain. -## -## Value: File -## auth.http.ssl.cacertfile = "{{ platform_etc_dir }}/certs/ca.pem" - -## The path to a file containing the client's certificate. -## -## Value: File -## auth.http.ssl.certfile = "{{ platform_etc_dir }}/certs/client-cert.pem" - -## Path to a file containing the client's private PEM-encoded key. -## -## Value: File -## auth.http.ssl.keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" - -## In mode verify_none the default behavior is to allow all x509-path -## validation errors. -## -## Value: true | false -## auth.http.ssl.verify = false - -## If not specified, the server's names returned in server's certificate is validated against -## what's provided `auth.http.auth_req.url` config's host part. -## Setting to 'disable' will make EMQ X ignore unmatched server names. -## If set with a host name, the server's names returned in server's certificate is validated -## against this value. -## -## Value: String | disable -## auth.http.ssl.server_name_indication = disable diff --git a/apps/emqx_auth_http/include/emqx_auth_http.hrl b/apps/emqx_auth_http/include/emqx_auth_http.hrl deleted file mode 100644 index 9c1216357..000000000 --- a/apps/emqx_auth_http/include/emqx_auth_http.hrl +++ /dev/null @@ -1,23 +0,0 @@ - --define(APP, emqx_auth_http). - --record(auth_metrics, { - success = 'client.auth.success', - failure = 'client.auth.failure', - ignore = 'client.auth.ignore' - }). - --record(acl_metrics, { - allow = 'client.acl.allow', - deny = 'client.acl.deny', - ignore = 'client.acl.ignore' - }). - --define(METRICS(Type), tl(tuple_to_list(#Type{}))). --define(METRICS(Type, K), #Type{}#Type.K). - --define(AUTH_METRICS, ?METRICS(auth_metrics)). --define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). - --define(ACL_METRICS, ?METRICS(acl_metrics)). --define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). diff --git a/apps/emqx_auth_http/priv/emqx_auth_http.schema b/apps/emqx_auth_http/priv/emqx_auth_http.schema deleted file mode 100644 index b248c7dc7..000000000 --- a/apps/emqx_auth_http/priv/emqx_auth_http.schema +++ /dev/null @@ -1,131 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_auth_http config mapping -{mapping, "auth.http.auth_req.url", "emqx_auth_http.auth_req", [ - {datatype, string} -]}. - -{mapping, "auth.http.auth_req.method", "emqx_auth_http.auth_req", [ - {default, post}, - {datatype, {enum, [post, get]}} -]}. - -{mapping, "auth.http.auth_req.headers.$field", "emqx_auth_http.auth_req", [ - {datatype, string} -]}. - -{mapping, "auth.http.auth_req.params", "emqx_auth_http.auth_req", [ - {datatype, string} -]}. - -{translation, "emqx_auth_http.auth_req", fun(Conf) -> - case cuttlefish:conf_get("auth.http.auth_req.url", Conf, undefined) of - undefined -> cuttlefish:unset(); - Url -> - Headers = cuttlefish_variable:filter_by_prefix("auth.http.auth_req.headers", Conf), - Params = cuttlefish:conf_get("auth.http.auth_req.params", Conf), - [{url, Url}, - {method, cuttlefish:conf_get("auth.http.auth_req.method", Conf)}, - {headers, [{K, V} || {[_, _, _, _, K], V} <- Headers]}, - {params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}] - end -end}. - -{mapping, "auth.http.super_req.url", "emqx_auth_http.super_req", [ - {datatype, string} -]}. - -{mapping, "auth.http.super_req.method", "emqx_auth_http.super_req", [ - {default, post}, - {datatype, {enum, [post, get]}} -]}. - -{mapping, "auth.http.super_req.headers.$field", "emqx_auth_http.super_req", [ - {datatype, string} -]}. - -{mapping, "auth.http.super_req.params", "emqx_auth_http.super_req", [ - {datatype, string} -]}. - -{translation, "emqx_auth_http.super_req", fun(Conf) -> - case cuttlefish:conf_get("auth.http.super_req.url", Conf, undefined) of - undefined -> cuttlefish:unset(); - Url -> - Headers = cuttlefish_variable:filter_by_prefix("auth.http.super_req.headers", Conf), - Params = cuttlefish:conf_get("auth.http.super_req.params", Conf), - [{url, Url}, - {method, cuttlefish:conf_get("auth.http.super_req.method", Conf)}, - {headers, [{K, V} || {[_, _, _, _, K], V} <- Headers]}, - {params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}] - end -end}. - -%% @doc URL for ACL checks. Example: http://127.0.0.1:80/mqtt/acl -%% ACL checks are disabled for this plugin if this config is -%% commented out from the config file, or when the overriding -%% environment variable is set to empty string. -{mapping, "auth.http.acl_req.url", "emqx_auth_http.acl_req", [ - {datatype, string} -]}. - -{mapping, "auth.http.acl_req.method", "emqx_auth_http.acl_req", [ - {default, post}, - {datatype, {enum, [post, get]}} -]}. - -{mapping, "auth.http.acl_req.headers.$field", "emqx_auth_http.acl_req", [ - {datatype, string} -]}. - -{mapping, "auth.http.acl_req.params", "emqx_auth_http.acl_req", [ - {datatype, string} -]}. - -{translation, "emqx_auth_http.acl_req", fun(Conf) -> - case cuttlefish:conf_get("auth.http.acl_req.url", Conf, undefined) of - undefined -> cuttlefish:unset(); - Url -> - Headers = cuttlefish_variable:filter_by_prefix("auth.http.acl_req.headers", Conf), - Params = cuttlefish:conf_get("auth.http.acl_req.params", Conf), - [{url, Url}, - {method, cuttlefish:conf_get("auth.http.acl_req.method", Conf)}, - {headers, [{K, V} || {[_, _, _, _, K], V} <- Headers]}, - {params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}] - end -end}. - -{mapping, "auth.http.timeout", "emqx_auth_http.timeout", [ - {default, "5s"}, - {datatype, [integer, {duration, ms}]} -]}. - -{mapping, "auth.http.connect_timeout", "emqx_auth_http.connect_timeout", [ - {default, "5s"}, - {datatype, [integer, {duration, ms}]} -]}. - -{mapping, "auth.http.pool_size", "emqx_auth_http.pool_size", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "auth.http.ssl.cacertfile", "emqx_auth_http.cacertfile", [ - {datatype, string} -]}. - -{mapping, "auth.http.ssl.certfile", "emqx_auth_http.certfile", [ - {datatype, string} -]}. - -{mapping, "auth.http.ssl.keyfile", "emqx_auth_http.keyfile", [ - {datatype, string} -]}. - -{mapping, "auth.http.ssl.verify", "emqx_auth_http.verify", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "auth.http.ssl.server_name_indication", "emqx_auth_http.server_name_indication", [ - {datatype, string} -]}. diff --git a/apps/emqx_auth_http/rebar.config b/apps/emqx_auth_http/rebar.config deleted file mode 100644 index 01c0f4209..000000000 --- a/apps/emqx_auth_http/rebar.config +++ /dev/null @@ -1,26 +0,0 @@ -{deps, []}. - -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - {parse_transform}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. - -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. - -{profiles, - [{test, - [{deps, - [ - {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "v1.2.2"}}} - ]} - ]} - ]}. diff --git a/apps/emqx_auth_http/src/emqx_acl_http.erl b/apps/emqx_auth_http/src/emqx_acl_http.erl deleted file mode 100644 index aa98759b0..000000000 --- a/apps/emqx_auth_http/src/emqx_acl_http.erl +++ /dev/null @@ -1,88 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_acl_http). - --include("emqx_auth_http.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --logger_header("[ACL http]"). - --import(emqx_auth_http_cli, - [ request/6 - , feedvar/2 - ]). - -%% ACL callbacks --export([ register_metrics/0 - , check_acl/5 - , description/0 - ]). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). - -%%-------------------------------------------------------------------- -%% ACL callbacks -%%-------------------------------------------------------------------- - -check_acl(ClientInfo, PubSub, Topic, AclResult, Params) -> - return_with(fun inc_metrics/1, - do_check_acl(ClientInfo, PubSub, Topic, AclResult, Params)). - -do_check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _Params) -> - ok; -do_check_acl(ClientInfo, PubSub, Topic, _AclResult, #{acl := ACLParams = #{path := Path}}) -> - ClientInfo1 = ClientInfo#{access => access(PubSub), topic => Topic}, - case check_acl_request(ACLParams, ClientInfo1) of - {ok, 200, <<"ignore">>} -> ok; - {ok, 200, _Body} -> {stop, allow}; - {ok, _Code, _Body} -> {stop, deny}; - {error, Error} -> - ?LOG(error, "Request ACL path ~s, error: ~p", [Path, Error]), - ok - end. - -description() -> "ACL with HTTP API". - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -inc_metrics(ok) -> - emqx_metrics:inc(?ACL_METRICS(ignore)); -inc_metrics({stop, allow}) -> - emqx_metrics:inc(?ACL_METRICS(allow)); -inc_metrics({stop, deny}) -> - emqx_metrics:inc(?ACL_METRICS(deny)). - -return_with(Fun, Result) -> - Fun(Result), Result. - -check_acl_request(#{pool_name := PoolName, - path := Path, - method := Method, - headers := Headers, - params := Params, - timeout := Timeout}, ClientInfo) -> - request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), Timeout). - -access(subscribe) -> 1; -access(publish) -> 2. - diff --git a/apps/emqx_auth_http/src/emqx_auth_http.app.src b/apps/emqx_auth_http/src/emqx_auth_http.app.src deleted file mode 100644 index b2c3221e6..000000000 --- a/apps/emqx_auth_http/src/emqx_auth_http.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_auth_http, - [{description, "EMQ X Authentication/ACL with HTTP API"}, - {vsn, "4.3.0"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_auth_http_sup]}, - {applications, [kernel,stdlib,ehttpc]}, - {mod, {emqx_auth_http_app, []}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-auth-http"} - ]} - ]}. diff --git a/apps/emqx_auth_http/src/emqx_auth_http.erl b/apps/emqx_auth_http/src/emqx_auth_http.erl deleted file mode 100644 index aba0a5d8d..000000000 --- a/apps/emqx_auth_http/src/emqx_auth_http.erl +++ /dev/null @@ -1,112 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_http). - --include("emqx_auth_http.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). --include_lib("emqx/include/types.hrl"). - --logger_header("[Auth http]"). - --import(emqx_auth_http_cli, - [ request/6 - , feedvar/2 - ]). - -%% Callbacks --export([ register_metrics/0 - , check/3 - , description/0 - ]). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). - -check(ClientInfo, AuthResult, #{auth := AuthParms = #{path := Path}, - super := SuperParams}) -> - case authenticate(AuthParms, ClientInfo) of - {ok, 200, <<"ignore">>} -> - emqx_metrics:inc(?AUTH_METRICS(ignore)), ok; - {ok, 200, Body} -> - emqx_metrics:inc(?AUTH_METRICS(success)), - IsSuperuser = is_superuser(SuperParams, ClientInfo), - {stop, AuthResult#{is_superuser => IsSuperuser, - auth_result => success, - anonymous => false, - mountpoint => mountpoint(Body, ClientInfo)}}; - {ok, Code, _Body} -> - ?LOG(error, "Deny connection from path: ~s, response http code: ~p", - [Path, Code]), - emqx_metrics:inc(?AUTH_METRICS(failure)), - {stop, AuthResult#{auth_result => http_to_connack_error(Code), - anonymous => false}}; - {error, Error} -> - ?LOG(error, "Request auth path: ~s, error: ~p", [Path, Error]), - emqx_metrics:inc(?AUTH_METRICS(failure)), - %%FIXME later: server_unavailable is not right. - {stop, AuthResult#{auth_result => server_unavailable, - anonymous => false}} - end. - -description() -> "Authentication by HTTP API". - -%%-------------------------------------------------------------------- -%% Requests -%%-------------------------------------------------------------------- - -authenticate(#{pool_name := PoolName, - path := Path, - method := Method, - headers := Headers, - params := Params, - timeout := Timeout}, ClientInfo) -> - request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), Timeout). - --spec(is_superuser(maybe(map()), emqx_types:client()) -> boolean()). -is_superuser(undefined, _ClientInfo) -> - false; -is_superuser(#{pool_name := PoolName, - path := Path, - method := Method, - headers := Headers, - params := Params, - timeout := Timeout}, ClientInfo) -> - case request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), Timeout) of - {ok, 200, _Body} -> true; - {ok, _Code, _Body} -> false; - {error, Error} -> ?LOG(error, "Request superuser path ~s, error: ~p", [Path, Error]), - false - end. - -mountpoint(Body, #{mountpoint := Mountpoint}) -> - case emqx_json:safe_decode(Body, [return_maps]) of - {error, _} -> Mountpoint; - {ok, Json} when is_map(Json) -> - maps:get(<<"mountpoint">>, Json, Mountpoint); - {ok, _NotMap} -> Mountpoint - end. - -http_to_connack_error(400) -> bad_username_or_password; -http_to_connack_error(401) -> bad_username_or_password; -http_to_connack_error(403) -> not_authorized; -http_to_connack_error(429) -> banned; -http_to_connack_error(503) -> server_unavailable; -http_to_connack_error(504) -> server_busy; -http_to_connack_error(_) -> server_unavailable. diff --git a/apps/emqx_auth_http/src/emqx_auth_http_app.erl b/apps/emqx_auth_http/src/emqx_auth_http_app.erl deleted file mode 100644 index 7d4781f7c..000000000 --- a/apps/emqx_auth_http/src/emqx_auth_http_app.erl +++ /dev/null @@ -1,158 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_http_app). - --behaviour(application). - --emqx_plugin(auth). - --include("emqx_auth_http.hrl"). - --export([ start/2 - , stop/1 - ]). - -%%-------------------------------------------------------------------- -%% Application Callbacks -%%-------------------------------------------------------------------- - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_auth_http_sup:start_link(), - translate_env(), - load_hooks(), - {ok, Sup}. - -stop(_State) -> - unload_hooks(). - -%%-------------------------------------------------------------------- -%% Internel functions -%%-------------------------------------------------------------------- - -translate_env() -> - lists:foreach(fun translate_env/1, [auth_req, super_req, acl_req]). - -translate_env(EnvName) -> - case application:get_env(?APP, EnvName) of - undefined -> ok; - {ok, Req} -> - {ok, PoolSize} = application:get_env(?APP, pool_size), - {ok, ConnectTimeout} = application:get_env(?APP, connect_timeout), - URL = proplists:get_value(url, Req), - {ok, #{host := Host, - path := Path0, - port := Port, - scheme := Scheme}} = emqx_http_lib:uri_parse(URL), - Path = path(Path0), - MoreOpts = case Scheme of - http -> - [{transport_opts, emqx_misc:ipv6_probe([])}]; - https -> - CACertFile = application:get_env(?APP, cacertfile, undefined), - CertFile = application:get_env(?APP, certfile, undefined), - KeyFile = application:get_env(?APP, keyfile, undefined), - Verify = case application:get_env(?APP, verify, fasle) of - true -> verify_peer; - false -> verify_none - end, - SNI = case application:get_env(?APP, server_name_indication, undefined) of - "disable" -> disable; - SNI0 -> SNI0 - end, - TLSOpts = lists:filter( - fun({_, V}) -> - V =/= <<>> andalso V =/= undefined - end, [{keyfile, KeyFile}, - {certfile, CertFile}, - {cacertfile, CACertFile}, - {verify, Verify}, - {server_name_indication, SNI}]), - NTLSOpts = [ {versions, emqx_tls_lib:default_versions()} - , {ciphers, emqx_tls_lib:default_ciphers()} - | TLSOpts - ], - [{transport, ssl}, {transport_opts, emqx_misc:ipv6_probe(NTLSOpts)}] - end, - PoolOpts = [{host, Host}, - {port, Port}, - {pool_size, PoolSize}, - {pool_type, random}, - {connect_timeout, ConnectTimeout}, - {retry, 5}, - {retry_timeout, 1000}] ++ MoreOpts, - Method = proplists:get_value(method, Req), - Headers = proplists:get_value(headers, Req), - NHeaders = ensure_content_type_header(Method, emqx_http_lib:normalise_headers(Headers)), - NReq = lists:keydelete(headers, 1, Req), - {ok, Timeout} = application:get_env(?APP, timeout), - application:set_env(?APP, EnvName, [{path, Path}, - {headers, NHeaders}, - {timeout, Timeout}, - {pool_name, list_to_atom("emqx_auth_http/" ++ atom_to_list(EnvName))}, - {pool_opts, PoolOpts} | NReq]) - end. - -load_hooks() -> - case application:get_env(?APP, auth_req) of - undefined -> ok; - {ok, AuthReq} -> - ok = emqx_auth_http:register_metrics(), - PoolOpts = proplists:get_value(pool_opts, AuthReq), - PoolName = proplists:get_value(pool_name, AuthReq), - {ok, _} = ehttpc_sup:start_pool(PoolName, PoolOpts), - case application:get_env(?APP, super_req) of - undefined -> - emqx_hooks:put('client.authenticate', {emqx_auth_http, check, [#{auth => maps:from_list(AuthReq), - super => undefined}]}); - {ok, SuperReq} -> - PoolOpts1 = proplists:get_value(pool_opts, SuperReq), - PoolName1 = proplists:get_value(pool_name, SuperReq), - {ok, _} = ehttpc_sup:start_pool(PoolName1, PoolOpts1), - emqx_hooks:put('client.authenticate', {emqx_auth_http, check, [#{auth => maps:from_list(AuthReq), - super => maps:from_list(SuperReq)}]}) - end - end, - case application:get_env(?APP, acl_req) of - undefined -> ok; - {ok, ACLReq} -> - ok = emqx_acl_http:register_metrics(), - PoolOpts2 = proplists:get_value(pool_opts, ACLReq), - PoolName2 = proplists:get_value(pool_name, ACLReq), - {ok, _} = ehttpc_sup:start_pool(PoolName2, PoolOpts2), - emqx_hooks:put('client.check_acl', {emqx_acl_http, check_acl, [#{acl => maps:from_list(ACLReq)}]}) - end, - ok. - -unload_hooks() -> - emqx:unhook('client.authenticate', {emqx_auth_http, check}), - emqx:unhook('client.check_acl', {emqx_acl_http, check_acl}), - _ = ehttpc_sup:stop_pool('emqx_auth_http/auth_req'), - _ = ehttpc_sup:stop_pool('emqx_auth_http/super_req'), - _ = ehttpc_sup:stop_pool('emqx_auth_http/acl_req'), - ok. - -ensure_content_type_header(Method, Headers) - when Method =:= post orelse Method =:= put -> - Headers; -ensure_content_type_header(_Method, Headers) -> - lists:keydelete("content-type", 1, Headers). - -path("") -> - "/"; -path(Path) -> - Path. - diff --git a/apps/emqx_auth_http/src/emqx_auth_http_cli.erl b/apps/emqx_auth_http/src/emqx_auth_http_cli.erl deleted file mode 100644 index 979ac475d..000000000 --- a/apps/emqx_auth_http/src/emqx_auth_http_cli.erl +++ /dev/null @@ -1,92 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_http_cli). - --include("emqx_auth_http.hrl"). - --export([ request/6 - , feedvar/2 - , feedvar/3 - ]). - -%%-------------------------------------------------------------------- -%% HTTP Request -%%-------------------------------------------------------------------- - -request(PoolName, get, Path, Headers, Params, Timeout) -> - NewPath = Path ++ "?" ++ binary_to_list(cow_qs:qs(bin_kw(Params))), - reply(ehttpc:request(ehttpc_pool:pick_worker(PoolName), get, {NewPath, Headers}, Timeout)); - -request(PoolName, post, Path, Headers, Params, Timeout) -> - Body = case proplists:get_value("content-type", Headers) of - "application/x-www-form-urlencoded" -> - cow_qs:qs(bin_kw(Params)); - "application/json" -> - emqx_json:encode(bin_kw(Params)) - end, - reply(ehttpc:request(ehttpc_pool:pick_worker(PoolName), post, {Path, Headers, Body}, Timeout)). - -reply({ok, StatusCode, _Headers}) -> - {ok, StatusCode, <<>>}; -reply({ok, StatusCode, _Headers, Body}) -> - {ok, StatusCode, Body}; -reply({error, Reason}) -> - {error, Reason}. - -%% TODO: move this conversion to cuttlefish config and schema -bin_kw(KeywordList) when is_list(KeywordList) -> - [{bin(K), bin(V)} || {K, V} <- KeywordList]. - -bin(Atom) when is_atom(Atom) -> - list_to_binary(atom_to_list(Atom)); -bin(Int) when is_integer(Int) -> - integer_to_binary(Int); -bin(Float) when is_float(Float) -> - float_to_binary(Float, [{decimals, 12}, compact]); -bin(List) when is_list(List)-> - list_to_binary(List); -bin(Binary) when is_binary(Binary) -> - Binary. - -%%-------------------------------------------------------------------- -%% Feed Variables -%%-------------------------------------------------------------------- - -feedvar(Params, ClientInfo = #{clientid := ClientId, - protocol := Protocol, - peerhost := Peerhost}) -> - lists:map(fun({Param, "%u"}) -> {Param, maps:get(username, ClientInfo, null)}; - ({Param, "%c"}) -> {Param, ClientId}; - ({Param, "%r"}) -> {Param, Protocol}; - ({Param, "%a"}) -> {Param, inet:ntoa(Peerhost)}; - ({Param, "%P"}) -> {Param, maps:get(password, ClientInfo, null)}; - ({Param, "%p"}) -> {Param, maps:get(sockport, ClientInfo, null)}; - ({Param, "%C"}) -> {Param, maps:get(cn, ClientInfo, null)}; - ({Param, "%d"}) -> {Param, maps:get(dn, ClientInfo, null)}; - ({Param, "%A"}) -> {Param, maps:get(access, ClientInfo, null)}; - ({Param, "%t"}) -> {Param, maps:get(topic, ClientInfo, null)}; - ({Param, "%m"}) -> {Param, maps:get(mountpoint, ClientInfo, null)}; - ({Param, Var}) -> {Param, Var} - end, Params). - -feedvar(Params, Var, Val) -> - lists:map(fun({Param, Var0}) when Var0 == Var -> - {Param, Val}; - ({Param, Var0}) -> - {Param, Var0} - end, Params). - diff --git a/apps/emqx_auth_http/test/emqx_auth_http_SUITE.erl b/apps/emqx_auth_http/test/emqx_auth_http_SUITE.erl deleted file mode 100644 index ef692e886..000000000 --- a/apps/emqx_auth_http/test/emqx_auth_http_SUITE.erl +++ /dev/null @@ -1,257 +0,0 @@ -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. - --module(emqx_auth_http_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("emqx/include/emqx.hrl"). --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). - --define(APP, emqx_auth_http). - --define(USER(ClientId, Username, Protocol, Peerhost, Zone), - #{clientid => ClientId, username => Username, protocol => Protocol, - peerhost => Peerhost, zone => Zone}). - --define(USER(ClientId, Username, Protocol, Peerhost, Zone, Mountpoint), - #{clientid => ClientId, username => Username, protocol => Protocol, - peerhost => Peerhost, zone => Zone, mountpoint => Mountpoint}). - -%%-------------------------------------------------------------------- -%% Setups -%%-------------------------------------------------------------------- - -all() -> - [ - {group, http_inet}, - {group, http_inet6}, - {group, https_inet}, - {group, https_inet6}, - pub_sub_no_acl, - no_hook_if_config_unset - ]. - -groups() -> - Cases = emqx_ct:all(?MODULE), - [{Name, Cases} || Name <- [http_inet, http_inet6, https_inet, https_inet6]]. - -init_per_group(GrpName, Cfg) -> - [Scheme, Inet] = [list_to_atom(X) || X <- string:tokens(atom_to_list(GrpName), "_")], - ok = setup(Scheme, Inet), - Cfg. - -end_per_group(_GrpName, _Cfg) -> - teardown(). - -init_per_testcase(pub_sub_no_acl, Cfg) -> - Scheme = http, - Inet = inet, - http_auth_server:start(Scheme, Inet), - Fun = fun(App) -> set_special_configs(App, Scheme, Inet, no_acl) end, - emqx_ct_helpers:start_apps([emqx_auth_http], Fun), - ?assert(is_hooked('client.authenticate')), - ?assertNot(is_hooked('client.check_acl')), - Cfg; -init_per_testcase(no_hook_if_config_unset, Cfg) -> - setup(http, inet), - Cfg; -init_per_testcase(_, Cfg) -> - %% init per group - Cfg. - -end_per_testcase(pub_sub_no_acl, _Cfg) -> - teardown(); -end_per_testcase(no_hook_if_config_unset, _Cfg) -> - teardown(); -end_per_testcase(_, _Cfg) -> - %% teardown per group - ok. - -setup(Scheme, Inet) -> - http_auth_server:start(Scheme, Inet), - Fun = fun(App) -> set_special_configs(App, Scheme, Inet, normal) end, - emqx_ct_helpers:start_apps([emqx_auth_http], Fun), - ?assert(is_hooked('client.authenticate')), - ?assert(is_hooked('client.check_acl')). - -teardown() -> - http_auth_server:stop(), - application:stop(emqx_auth_http), - ?assertNot(is_hooked('client.authenticate')), - ?assertNot(is_hooked('client.check_acl')), - emqx_ct_helpers:stop_apps([emqx]). - -set_special_configs(emqx, _Scheme, _Inet, _AuthConfig) -> - application:set_env(emqx, allow_anonymous, true), - application:set_env(emqx, enable_acl_cache, false), - LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)); - -set_special_configs(emqx_auth_http, Scheme, Inet, PluginConfig) -> - [application:unset_env(?APP, Par) || Par <- [acl_req, auth_req]], - ServerAddr = http_server(Scheme, Inet), - - AuthReq = #{method => get, - url => ServerAddr ++ "/mqtt/auth", - headers => [{"content-type", "application/json"}], - params => [{"clientid", "%c"}, {"username", "%u"}, {"password", "%P"}]}, - SuperReq = #{method => post, - url => ServerAddr ++ "/mqtt/superuser", - headers => [{"content-type", "application/json"}], - params => [{"clientid", "%c"}, {"username", "%u"}]}, - AclReq = #{method => post, - url => ServerAddr ++ "/mqtt/acl", - headers => [{"content-type", "application/json"}], - params => [{"access", "%A"}, {"username", "%u"}, {"clientid", "%c"}, {"ipaddr", "%a"}, {"topic", "%t"}, {"mountpoint", "%m"}]}, - - Scheme =:= https andalso set_https_client_opts(), - - application:set_env(emqx_auth_http, auth_req, maps:to_list(AuthReq)), - application:set_env(emqx_auth_http, super_req, maps:to_list(SuperReq)), - case PluginConfig of - normal -> ok = application:set_env(emqx_auth_http, acl_req, maps:to_list(AclReq)); - no_acl -> ok - end. - -%% @private -set_https_client_opts() -> - SSLOpt = emqx_ct_helpers:client_ssl_twoway(), - application:set_env(emqx_auth_http, cacertfile, proplists:get_value(cacertfile, SSLOpt, undefined)), - application:set_env(emqx_auth_http, certfile, proplists:get_value(certfile, SSLOpt, undefined)), - application:set_env(emqx_auth_http, keyfile, proplists:get_value(keyfile, SSLOpt, undefined)), - application:set_env(emqx_auth_http, verify, true), - application:set_env(emqx_auth_http, server_name_indication, "disable"). - -%% @private -http_server(http, inet) -> "http://127.0.0.1:8991"; % ipv4 -http_server(http, inet6) -> "http://localhost:8991"; % test hostname resolution -http_server(https, inet) -> "https://localhost:8991"; % test hostname resolution -http_server(https, inet6) -> "https://[::1]:8991". % ipv6 - -%%------------------------------------------------------------------------------ -%% Testcases -%%------------------------------------------------------------------------------ - -t_check_acl(Cfg) when is_list(Cfg) -> - SuperUser = ?USER(<<"superclient">>, <<"superuser">>, mqtt, {127,0,0,1}, external), - deny = emqx_access_control:check_acl(SuperUser, subscribe, <<"users/testuser/1">>), - deny = emqx_access_control:check_acl(SuperUser, publish, <<"anytopic">>), - - User1 = ?USER(<<"client1">>, <<"testuser">>, mqtt, {127,0,0,1}, external), - UnIpUser1 = ?USER(<<"client1">>, <<"testuser">>, mqtt, {192,168,0,4}, external), - UnClientIdUser1 = ?USER(<<"unkonwc">>, <<"testuser">>, mqtt, {127,0,0,1}, external), - UnnameUser1= ?USER(<<"client1">>, <<"unuser">>, mqtt, {127,0,0,1}, external), - allow = emqx_access_control:check_acl(User1, subscribe, <<"users/testuser/1">>), - deny = emqx_access_control:check_acl(User1, publish, <<"users/testuser/1">>), - deny = emqx_access_control:check_acl(UnIpUser1, subscribe, <<"users/testuser/1">>), - deny = emqx_access_control:check_acl(UnClientIdUser1, subscribe, <<"users/testuser/1">>), - deny = emqx_access_control:check_acl(UnnameUser1, subscribe, <<"$SYS/testuser/1">>), - - User2 = ?USER(<<"client2">>, <<"xyz">>, mqtt, {127,0,0,1}, external), - UserC = ?USER(<<"client2">>, <<"xyz">>, mqtt, {192,168,1,3}, external), - allow = emqx_access_control:check_acl(UserC, publish, <<"a/b/c">>), - deny = emqx_access_control:check_acl(User2, publish, <<"a/b/c">>), - deny = emqx_access_control:check_acl(User2, subscribe, <<"$SYS/testuser/1">>). - -t_check_auth(Cfg) when is_list(Cfg) -> - User1 = ?USER(<<"client1">>, <<"testuser1">>, mqtt, {127,0,0,1}, external, undefined), - User2 = ?USER(<<"client2">>, <<"testuser2">>, mqtt, {127,0,0,1}, exteneral, undefined), - User3 = ?USER(<<"client3">>, undefined, mqtt, {127,0,0,1}, exteneral, undefined), - - {ok, #{auth_result := success, - anonymous := false, - is_superuser := false}} = emqx_access_control:authenticate(User1#{password => <<"pass1">>}), - {error, bad_username_or_password} = emqx_access_control:authenticate(User1#{password => <<"pass">>}), - {error, bad_username_or_password} = emqx_access_control:authenticate(User1#{password => <<>>}), - - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(User2#{password => <<"pass2">>}), - {error, bad_username_or_password} = emqx_access_control:authenticate(User2#{password => <<>>}), - {error, bad_username_or_password} = emqx_access_control:authenticate(User2#{password => <<"errorpwd">>}), - - {error, bad_username_or_password} = emqx_access_control:authenticate(User3#{password => <<"pwd">>}). - -pub_sub_no_acl(Cfg) when is_list(Cfg) -> - {ok, T1} = emqtt:start_link([{host, "localhost"}, - {clientid, <<"client1">>}, - {username, <<"testuser1">>}, - {password, <<"pass1">>}]), - {ok, _} = emqtt:connect(T1), - emqtt:publish(T1, <<"topic">>, <<"body">>, [{qos, 0}, {retain, true}]), - timer:sleep(1000), - {ok, T2} = emqtt:start_link([{host, "localhost"}, - {clientid, <<"client2">>}, - {username, <<"testuser2">>}, - {password, <<"pass2">>}]), - {ok, _} = emqtt:connect(T2), - emqtt:subscribe(T2, <<"topic">>), - receive - {publish, _Topic, Payload} -> - ?assertEqual(<<"body">>, Payload) - after 1000 -> false end, - emqtt:disconnect(T1), - emqtt:disconnect(T2). - -t_pub_sub(Cfg) when is_list(Cfg) -> - {ok, T1} = emqtt:start_link([{host, "localhost"}, - {clientid, <<"client1">>}, - {username, <<"testuser1">>}, - {password, <<"pass1">>}]), - {ok, _} = emqtt:connect(T1), - emqtt:publish(T1, <<"topic">>, <<"body">>, [{qos, 0}, {retain, true}]), - timer:sleep(1000), - {ok, T2} = emqtt:start_link([{host, "localhost"}, - {clientid, <<"client2">>}, - {username, <<"testuser2">>}, - {password, <<"pass2">>}]), - {ok, _} = emqtt:connect(T2), - emqtt:subscribe(T2, <<"topic">>), - receive - {publish, _Topic, Payload} -> - ?assertEqual(<<"body">>, Payload) - after 1000 -> false end, - emqtt:disconnect(T1), - emqtt:disconnect(T2). - -no_hook_if_config_unset(Cfg) when is_list(Cfg) -> - ?assert(is_hooked('client.authenticate')), - ?assert(is_hooked('client.check_acl')), - application:stop(?APP), - [application:unset_env(?APP, Par) || Par <- [acl_req, auth_req]], - application:start(?APP), - ?assertEqual([], emqx_hooks:lookup('client.authenticate')), - ?assertNot(is_hooked('client.authenticate')), - ?assertNot(is_hooked('client.check_acl')). - -is_hooked(HookName) -> - Callbacks = emqx_hooks:lookup(HookName), - F = fun(Callback) -> - case emqx_hooks:callback_action(Callback) of - {emqx_auth_http, check, _} -> - 'client.authenticate' = HookName, % assert - true; - {emqx_acl_http, check_acl, _} -> - 'client.check_acl' = HookName, % assert - true; - _ -> - false - end - end, - case lists:filter(F, Callbacks) of - [_] -> true; - [] -> false - end. diff --git a/apps/emqx_auth_http/test/http_auth_server.erl b/apps/emqx_auth_http/test/http_auth_server.erl deleted file mode 100644 index 54c4d38b3..000000000 --- a/apps/emqx_auth_http/test/http_auth_server.erl +++ /dev/null @@ -1,152 +0,0 @@ --module(http_auth_server). - --export([ start/2 - , stop/0 - ]). - --define(SUPERUSER, [[{"username", "superuser"}, {"clientid", "superclient"}]]). - --define(ACL, [[{<<"username">>, <<"testuser">>}, - {<<"clientid">>, <<"client1">>}, - {<<"access">>, <<"1">>}, - {<<"topic">>, <<"users/testuser/1">>}, - {<<"ipaddr">>, <<"127.0.0.1">>}, - {<<"mountpoint">>, <<"null">>}], - [{<<"username">>, <<"xyz">>}, - {<<"clientid">>, <<"client2">>}, - {<<"access">>, <<"2">>}, - {<<"topic">>, <<"a/b/c">>}, - {<<"ipaddr">>, <<"192.168.1.3">>}, - {<<"mountpoint">>, <<"null">>}], - [{<<"username">>, <<"testuser1">>}, - {<<"clientid">>, <<"client1">>}, - {<<"access">>, <<"2">>}, - {<<"topic">>, <<"topic">>}, - {<<"ipaddr">>, <<"127.0.0.1">>}, - {<<"mountpoint">>, <<"null">>}], - [{<<"username">>, <<"testuser2">>}, - {<<"clientid">>, <<"client2">>}, - {<<"access">>, <<"1">>}, - {<<"topic">>, <<"topic">>}, - {<<"ipaddr">>, <<"127.0.0.1">>}, - {<<"mountpoint">>, <<"null">>}]]). - --define(AUTH, [[{<<"clientid">>, <<"client1">>}, - {<<"username">>, <<"testuser1">>}, - {<<"password">>, <<"pass1">>}], - [{<<"clientid">>, <<"client2">>}, - {<<"username">>, <<"testuser2">>}, - {<<"password">>, <<"pass2">>}]]). - -%%------------------------------------------------------------------------------ -%% REST Interface -%%------------------------------------------------------------------------------ - --rest_api(#{ name => auth - , method => 'GET' - , path => "/mqtt/auth" - , func => authenticate - , descr => "Authenticate user access permission" - }). - --rest_api(#{ name => is_superuser - , method => 'GET' - , path => "/mqtt/superuser" - , func => is_superuser - , descr => "Is super user" - }). - --rest_api(#{ name => acl - , method => 'GET' - , path => "/mqtt/acl" - , func => check_acl - , descr => "Check acl" - }). - --rest_api(#{ name => auth - , method => 'POST' - , path => "/mqtt/auth" - , func => authenticate - , descr => "Authenticate user access permission" - }). - --rest_api(#{ name => is_superuser - , method => 'POST' - , path => "/mqtt/superuser" - , func => is_superuser - , descr => "Is super user" - }). - --rest_api(#{ name => acl - , method => 'POST' - , path => "/mqtt/acl" - , func => check_acl - , descr => "Check acl" - }). - --export([ authenticate/2 - , is_superuser/2 - , check_acl/2 - ]). - -authenticate(_Binding, Params) -> - return(check(Params, ?AUTH)). - -is_superuser(_Binding, Params) -> - return(check(Params, ?SUPERUSER)). - -check_acl(_Binding, Params) -> - return(check(Params, ?ACL)). - -return(allow) -> {200, <<"allow">>}; -return(deny) -> {400, <<"deny">>}. - -start(http, Inet) -> - application:ensure_all_started(minirest), - Handlers = [{"/", minirest:handler(#{modules => [?MODULE]})}], - Dispatch = [{"/[...]", minirest, Handlers}], - minirest:start_http(http_auth_server, #{socket_opts => [Inet, {port, 8991}]}, Dispatch); - -start(https, Inet) -> - application:ensure_all_started(minirest), - Handlers = [{"/", minirest:handler(#{modules => [?MODULE]})}], - Dispatch = [{"/[...]", minirest, Handlers}], - minirest:start_https(http_auth_server, #{socket_opts => [Inet, {port, 8991} | certopts()]}, Dispatch). - -%% @private -certopts() -> - Certfile = filename:join(["etc", "certs", "cert.pem"]), - Keyfile = filename:join(["etc", "certs", "key.pem"]), - CaCert = filename:join(["etc", "certs", "cacert.pem"]), - [{verify, verify_peer}, - {certfile, emqx_ct_helpers:deps_path(emqx, Certfile)}, - {keyfile, emqx_ct_helpers:deps_path(emqx, Keyfile)}, - {cacertfile, emqx_ct_helpers:deps_path(emqx, CaCert)}] ++ emqx_ct_helpers:client_ssl(). - -stop() -> - minirest:stop_http(http_auth_server). - --spec check(HttpReqParams :: list(), DefinedConf :: list()) -> allow | deny. -check(_Params, []) -> - %ct:pal("check auth_result: deny~n"), - deny; -check(Params, [ConfRecord|T]) -> - % ct:pal("Params: ~p, ConfRecord:~p ~n", [Params, ConfRecord]), - case match_config(Params, ConfRecord) of - not_match -> - check(Params, T); - matched -> allow - end. - -match_config([], _ConfigColumn) -> - %ct:pal("match_config auth_result: matched~n"), - matched; - -match_config([Param|T], ConfigColumn) -> - %ct:pal("Param: ~p, ConfigColumn:~p ~n", [Param, ConfigColumn]), - case lists:member(Param, ConfigColumn) of - true -> - match_config(T, ConfigColumn); - false -> - not_match - end. diff --git a/apps/emqx_auth_jwt/.gitignore b/apps/emqx_auth_jwt/.gitignore deleted file mode 100644 index 62e4fbb25..000000000 --- a/apps/emqx_auth_jwt/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -.eunit -deps -*.o -*.beam -*.plt -erl_crash.dump -ebin -rel/example_project -.concrete/DEV_MODE -.rebar -.erlang.mk/ -emqx_auth_jwt.d -data/ -.DS_Store -cover/ -ct.coverdata -eunit.coverdata -logs/ -test/ct.cover.spec -emq_auth_jwt.d -erlang.mk -_build/ -rebar.lock -rebar3.crashdump -etc/emqx_auth_jwt.conf.rendered -.rebar3/ -*.swp -Mnesia.nonode@nohost/ diff --git a/apps/emqx_auth_jwt/README.md b/apps/emqx_auth_jwt/README.md deleted file mode 100644 index 9675ae87c..000000000 --- a/apps/emqx_auth_jwt/README.md +++ /dev/null @@ -1,90 +0,0 @@ - -# emqx-auth-jwt - -EMQ X JWT Authentication Plugin - -Build ------ - -``` -make && make tests -``` - -Configure the Plugin --------------------- - -File: etc/plugins/emqx_auth_jwt.conf - -``` -## HMAC Hash Secret. -## -## Value: String -auth.jwt.secret = emqxsecret - -## From where the JWT string can be got -## -## Value: username | password -## Default: password -auth.jwt.from = password - -## RSA or ECDSA public key file. -## -## Value: File -## auth.jwt.pubkey = etc/certs/jwt_public_key.pem - -## Enable to verify claims fields -## -## Value: on | off -auth.jwt.verify_claims = off - -## The checklist of claims to validate -## -## Value: String -## auth.jwt.verify_claims.$name = expected -## -## Variables: -## - %u: username -## - %c: clientid -# auth.jwt.verify_claims.username = %u -``` - -Load the Plugin ---------------- - -``` -./bin/emqx_ctl plugins load emqx_auth_jwt -``` - -Example -------- - -``` -mosquitto_pub -t 'pub' -m 'hello' -i test -u test -P eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYm9iIiwiYWdlIjoyOX0.bIV_ZQ8D5nQi0LT8AVkpM4Pd6wmlbpR9S8nOLJAsA8o -``` - -Algorithms ----------- - -The JWT spec supports several algorithms for cryptographic signing. This plugin currently supports: - -* HS256 - HMAC using SHA-256 hash algorithm -* HS384 - HMAC using SHA-384 hash algorithm -* HS512 - HMAC using SHA-512 hash algorithm - -* RS256 - RSA with the SHA-256 hash algorithm -* RS384 - RSA with the SHA-384 hash algorithm -* RS512 - RSA with the SHA-512 hash algorithm - -* ES256 - ECDSA using the P-256 curve -* ES384 - ECDSA using the P-384 curve -* ES512 - ECDSA using the P-512 curve - -License -------- - -Apache License Version 2.0 - -Author ------- - -EMQ X Team. diff --git a/apps/emqx_auth_jwt/TODO.md b/apps/emqx_auth_jwt/TODO.md deleted file mode 100644 index dfd730e0a..000000000 --- a/apps/emqx_auth_jwt/TODO.md +++ /dev/null @@ -1,2 +0,0 @@ -1. Notice for the [Critical vulnerabilities in JSON Web Token](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/) - diff --git a/apps/emqx_auth_jwt/doc/hmac-vs-ecdsa-for-jwt.txt b/apps/emqx_auth_jwt/doc/hmac-vs-ecdsa-for-jwt.txt deleted file mode 100644 index 88fa5ebde..000000000 --- a/apps/emqx_auth_jwt/doc/hmac-vs-ecdsa-for-jwt.txt +++ /dev/null @@ -1,3 +0,0 @@ - -https://crypto.stackexchange.com/questions/30657/hmac-vs-ecdsa-for-jwt - diff --git a/apps/emqx_auth_jwt/etc/emqx_auth_jwt.conf b/apps/emqx_auth_jwt/etc/emqx_auth_jwt.conf deleted file mode 100644 index e0e6d48dd..000000000 --- a/apps/emqx_auth_jwt/etc/emqx_auth_jwt.conf +++ /dev/null @@ -1,49 +0,0 @@ -##-------------------------------------------------------------------- -## JWT Auth Plugin -##-------------------------------------------------------------------- - -## HMAC Hash Secret. -## -## Value: String -auth.jwt.secret = emqxsecret - -## RSA or ECDSA public key file. -## -## Value: File -#auth.jwt.pubkey = "etc/certs/jwt_public_key.pem" - -## The JWKs server address -## -## see: http://self-issued.info/docs/draft-ietf-jose-json-web-key.html -## -#auth.jwt.jwks.endpoint = "https://127.0.0.1:8080/jwks" - -## The JWKs refresh interval -## -## Value: Duration -#auth.jwt.jwks.refresh_interval = 5m - -## From where the JWT string can be got -## -## Value: username | password -## Default: password -auth.jwt.from = password - -## Enable to verify claims fields -## -## Value: on | off -auth.jwt.verify_claims.enable = off - -## The checklist of claims to validate -## -## Configuration format: auth.jwt.verify_claims.$name = $expected -## - $name: the name of the field in the JWT payload to be verified -## - $expected: the expected value -## -## The available placeholders for $expected: -## - %u: username -## - %c: clientid -## -## For example, to verify that the username in the JWT payload is the same -## as the client (MQTT protocol) username -#auth.jwt.verify_claims.username = "%u" \ No newline at end of file diff --git a/apps/emqx_auth_jwt/priv/emqx_auth_jwt.schema b/apps/emqx_auth_jwt/priv/emqx_auth_jwt.schema deleted file mode 100644 index 10b2daa5e..000000000 --- a/apps/emqx_auth_jwt/priv/emqx_auth_jwt.schema +++ /dev/null @@ -1,49 +0,0 @@ -%%-*- mode: erlang -*- - -{mapping, "auth.jwt.secret", "emqx_auth_jwt.secret", [ - {datatype, string} -]}. - -{mapping, "auth.jwt.jwks.endpoint", "emqx_auth_jwt.jwks", [ - {datatype, string} -]}. - -{mapping, "auth.jwt.jwks.refresh_interval", "emqx_auth_jwt.refresh_interval", [ - {datatype, {duration, ms}} -]}. - -{mapping, "auth.jwt.from", "emqx_auth_jwt.from", [ - {default, password}, - {datatype, atom} -]}. - -{mapping, "auth.jwt.pubkey", "emqx_auth_jwt.pubkey", [ - {datatype, string} -]}. - -{mapping, "auth.jwt.signature_format", "emqx_auth_jwt.jwerl_opts", [ - {default, "der"}, - {datatype, {enum, [raw, der]}} -]}. - -{mapping, "auth.jwt.verify_claims.enable", "emqx_auth_jwt.verify_claims", [ - {default, off}, - {datatype, flag} -]}. - -{mapping, "auth.jwt.verify_claims.$name", "emqx_auth_jwt.verify_claims", [ - {datatype, string} -]}. - -{translation, "emqx_auth_jwt.verify_claims", fun(Conf) -> - case cuttlefish:conf_get("auth.jwt.verify_claims.enable", Conf) of - false -> cuttlefish:unset(); - true -> - lists:foldr( - fun({["auth","jwt","verify_claims", Name], Value}, Acc) -> - [{list_to_atom(Name), list_to_binary(Value)} | Acc]; - ({["auth","jwt","verify_claims"], _Value}, Acc) -> - Acc - end, [], cuttlefish_variable:filter_by_prefix("auth.jwt.verify_claims", Conf)) - end -end}. diff --git a/apps/emqx_auth_jwt/rebar.config b/apps/emqx_auth_jwt/rebar.config deleted file mode 100644 index 3ec554950..000000000 --- a/apps/emqx_auth_jwt/rebar.config +++ /dev/null @@ -1,25 +0,0 @@ -{deps, - [ - {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} - ]}. - -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - {parse_transform}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. - -{profiles, - [{test, - [{deps, []} - ]} - ]}. diff --git a/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src b/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src deleted file mode 100644 index 7d784e3b2..000000000 --- a/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_auth_jwt, - [{description, "EMQ X Authentication with JWT"}, - {vsn, "4.4.0"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_auth_jwt_sup]}, - {applications, [kernel,stdlib,jose]}, - {mod, {emqx_auth_jwt_app, []}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-auth-jwt"} - ]} - ]}. diff --git a/apps/emqx_auth_jwt/src/emqx_auth_jwt.appup.src b/apps/emqx_auth_jwt/src/emqx_auth_jwt.appup.src deleted file mode 100644 index b9831bb6f..000000000 --- a/apps/emqx_auth_jwt/src/emqx_auth_jwt.appup.src +++ /dev/null @@ -1,15 +0,0 @@ -%% -*-: erlang -*- -{VSN, - [ - {"4.3.0", [ - {load_module, emqx_auth_jwt_svr, brutal_purge, soft_purge, []} - ]}, - {<<".*">>, []} - ], - [ - {"4.3.0", [ - {load_module, emqx_auth_jwt_svr, brutal_purge, soft_purge, []} - ]}, - {<<".*">>, []} - ] -}. diff --git a/apps/emqx_auth_jwt/src/emqx_auth_jwt.erl b/apps/emqx_auth_jwt/src/emqx_auth_jwt.erl deleted file mode 100644 index ba37eac2b..000000000 --- a/apps/emqx_auth_jwt/src/emqx_auth_jwt.erl +++ /dev/null @@ -1,99 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_jwt). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --logger_header("[JWT]"). - --export([ register_metrics/0 - , check/3 - , description/0 - ]). - --record(auth_metrics, { - success = 'client.auth.success', - failure = 'client.auth.failure', - ignore = 'client.auth.ignore' - }). - --define(METRICS(Type), tl(tuple_to_list(#Type{}))). --define(METRICS(Type, K), #Type{}#Type.K). - --define(AUTH_METRICS, ?METRICS(auth_metrics)). --define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). - -%%-------------------------------------------------------------------- -%% Authentication callbacks -%%-------------------------------------------------------------------- - -check(ClientInfo, AuthResult, #{pid := Pid, - from := From, - checklists := Checklists}) -> - case maps:find(From, ClientInfo) of - error -> - ok = emqx_metrics:inc(?AUTH_METRICS(ignore)); - {ok, undefined} -> - ok = emqx_metrics:inc(?AUTH_METRICS(ignore)); - {ok, Token} -> - case emqx_auth_jwt_svr:verify(Pid, Token) of - {error, not_found} -> - ok = emqx_metrics:inc(?AUTH_METRICS(ignore)); - {error, not_token} -> - ok = emqx_metrics:inc(?AUTH_METRICS(ignore)); - {error, Reason} -> - ok = emqx_metrics:inc(?AUTH_METRICS(failure)), - {stop, AuthResult#{auth_result => Reason, anonymous => false}}; - {ok, Claims} -> - {stop, maps:merge(AuthResult, verify_claims(Checklists, Claims, ClientInfo))} - end - end. - -description() -> "Authentication with JWT". - -%%------------------------------------------------------------------------------ -%% Verify Claims -%%-------------------------------------------------------------------- - -verify_claims(Checklists, Claims, ClientInfo) -> - case do_verify_claims(feedvar(Checklists, ClientInfo), Claims) of - {error, Reason} -> - ok = emqx_metrics:inc(?AUTH_METRICS(failure)), - #{auth_result => Reason, anonymous => false}; - ok -> - ok = emqx_metrics:inc(?AUTH_METRICS(success)), - #{auth_result => success, anonymous => false, jwt_claims => Claims} - end. - -do_verify_claims([], _Claims) -> - ok; -do_verify_claims([{Key, Expected} | L], Claims) -> - case maps:get(Key, Claims, undefined) =:= Expected of - true -> do_verify_claims(L, Claims); - false -> {error, {verify_claim_failed, Key}} - end. - -feedvar(Checklists, #{username := Username, clientid := ClientId}) -> - lists:map(fun({K, <<"%u">>}) -> {K, Username}; - ({K, <<"%c">>}) -> {K, ClientId}; - ({K, Expected}) -> {K, Expected} - end, Checklists). diff --git a/apps/emqx_auth_jwt/src/emqx_auth_jwt_app.erl b/apps/emqx_auth_jwt/src/emqx_auth_jwt_app.erl deleted file mode 100644 index e501b0af4..000000000 --- a/apps/emqx_auth_jwt/src/emqx_auth_jwt_app.erl +++ /dev/null @@ -1,81 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_jwt_app). - --behaviour(application). - --behaviour(supervisor). - --emqx_plugin(auth). - --export([start/2, stop/1]). - --export([init/1]). - --define(APP, emqx_auth_jwt). - -start(_Type, _Args) -> - {ok, Sup} = supervisor:start_link({local, ?MODULE}, ?MODULE, []), - - {ok, Pid} = start_auth_server(jwks_svr_options()), - ok = emqx_auth_jwt:register_metrics(), - AuthEnv0 = auth_env(), - AuthEnv1 = AuthEnv0#{pid => Pid}, - - _ = emqx:hook('client.authenticate', {emqx_auth_jwt, check, [AuthEnv1]}), - {ok, Sup, AuthEnv1}. - -stop(AuthEnv) -> - emqx:unhook('client.authenticate', {emqx_auth_jwt, check, [AuthEnv]}). - -%%-------------------------------------------------------------------- -%% Dummy supervisor -%%-------------------------------------------------------------------- - -init([]) -> - {ok, {{one_for_all, 1, 10}, []}}. - -start_auth_server(Options) -> - Spec = #{id => jwt_svr, - start => {emqx_auth_jwt_svr, start_link, [Options]}, - restart => permanent, - shutdown => brutal_kill, - type => worker, - modules => [emqx_auth_jwt_svr]}, - supervisor:start_child(?MODULE, Spec). - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -auth_env() -> - Checklists = [{atom_to_binary(K, utf8), V} - || {K, V} <- env(verify_claims, [])], - #{ from => env(from, password) - , checklists => Checklists - }. - -jwks_svr_options() -> - [{K, V} || {K, V} - <- [{secret, env(secret, undefined)}, - {pubkey, env(pubkey, undefined)}, - {jwks_addr, env(jwks, undefined)}, - {interval, env(refresh_interval, undefined)}], - V /= undefined]. - -env(Key, Default) -> - application:get_env(?APP, Key, Default). diff --git a/apps/emqx_auth_jwt/src/emqx_auth_jwt_svr.erl b/apps/emqx_auth_jwt/src/emqx_auth_jwt_svr.erl deleted file mode 100644 index b9d19bf57..000000000 --- a/apps/emqx_auth_jwt/src/emqx_auth_jwt_svr.erl +++ /dev/null @@ -1,224 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_jwt_svr). - --behaviour(gen_server). - --include_lib("emqx/include/logger.hrl"). --include_lib("jose/include/jose_jwk.hrl"). - --logger_header("[JWT-SVR]"). - -%% APIs --export([start_link/1]). - --export([verify/2]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --type options() :: [option()]. --type option() :: {secret, list()} - | {pubkey, list()} - | {jwks_addr, list()} - | {interval, pos_integer()}. - --define(INTERVAL, 300000). - --record(state, {static, remote, addr, tref, intv}). - -%%-------------------------------------------------------------------- -%% APIs -%%-------------------------------------------------------------------- - --spec start_link(options()) -> gen_server:start_ret(). -start_link(Options) -> - gen_server:start_link(?MODULE, [Options], []). - --spec verify(pid(), binary()) - -> {error, term()} - | {ok, Payload :: map()}. -verify(S, JwsCompacted) when is_binary(JwsCompacted) -> - case catch jose_jws:peek(JwsCompacted) of - {'EXIT', _} -> {error, not_token}; - _ -> gen_server:call(S, {verify, JwsCompacted}) - end. - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init([Options]) -> - ok = jose:json_module(jiffy), - {Static, Remote} = do_init_jwks(Options), - Intv = proplists:get_value(interval, Options, ?INTERVAL), - {ok, reset_timer( - #state{ - static = Static, - remote = Remote, - addr = proplists:get_value(jwks_addr, Options), - intv = Intv})}. - -%% @private -do_init_jwks(Options) -> - K2J = fun(K, F) -> - case proplists:get_value(K, Options) of - undefined -> undefined; - V -> - try F(V) of - {error, Reason} -> - ?LOG(warning, "Build ~p JWK ~p failed: {error, ~p}~n", - [K, V, Reason]), - undefined; - J -> J - catch T:R:_ -> - ?LOG(warning, "Build ~p JWK ~p failed: {~p, ~p}~n", - [K, V, T, R]), - undefined - end - end - end, - OctJwk = K2J(secret, fun(V) -> - jose_jwk:from_oct(list_to_binary(V)) - end), - PemJwk = K2J(pubkey, fun jose_jwk:from_pem_file/1), - Remote = K2J(jwks_addr, fun request_jwks/1), - {[J ||J <- [OctJwk, PemJwk], J /= undefined], Remote}. - -handle_call({verify, JwsCompacted}, _From, State) -> - handle_verify(JwsCompacted, State); - -handle_call(_Req, _From, State) -> - {reply, ok, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info({timeout, _TRef, refresh}, State = #state{addr = Addr}) -> - NState = try - State#state{remote = request_jwks(Addr)} - catch _:_ -> - State - end, - {noreply, reset_timer(NState)}; - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, State) -> - _ = cancel_timer(State), - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal funcs -%%-------------------------------------------------------------------- - -handle_verify(JwsCompacted, - State = #state{static = Static, remote = Remote}) -> - try - Jwks = case emqx_json:decode(jose_jws:peek_protected(JwsCompacted), [return_maps]) of - #{<<"kid">> := Kid} when Remote /= undefined -> - [J || J <- Remote, maps:get(<<"kid">>, J#jose_jwk.fields, undefined) =:= Kid]; - _ -> Static - end, - case Jwks of - [] -> {reply, {error, not_found}, State}; - _ -> - {reply, do_verify(JwsCompacted, Jwks), State} - end - catch - Class : Reason : Stk -> - ?LOG(error, "Handle JWK crashed: ~p, ~p, stacktrace: ~p~n", - [Class, Reason, Stk]), - {reply, {error, invalid_signature}, State} - end. - -request_jwks(Addr) -> - case httpc:request(get, {Addr, []}, [], [{body_format, binary}]) of - {error, Reason} -> - error(Reason); - {ok, {_Code, _Headers, Body}} -> - try - JwkSet = jose_jwk:from(emqx_json:decode(Body, [return_maps])), - {_, Jwks} = JwkSet#jose_jwk.keys, Jwks - catch _:_ -> - ?LOG(error, "Invalid jwks server response: ~p~n", [Body]), - error(badarg) - end - end. - -reset_timer(State = #state{addr = undefined}) -> - State; -reset_timer(State = #state{intv = Intv}) -> - State#state{tref = erlang:start_timer(Intv, self(), refresh)}. - -cancel_timer(State = #state{tref = undefined}) -> - State; -cancel_timer(State = #state{tref = TRef}) -> - _ = erlang:cancel_timer(TRef), - State#state{tref = undefined}. - -do_verify(_JwsCompated, []) -> - {error, invalid_signature}; -do_verify(JwsCompacted, [Jwk|More]) -> - case jose_jws:verify(Jwk, JwsCompacted) of - {true, Payload, _Jws} -> - Claims = emqx_json:decode(Payload, [return_maps]), - case check_claims(Claims) of - {false, <<"exp">>} -> - {error, {invalid_signature, expired}}; - NClaims -> - {ok, NClaims} - end; - {false, _, _} -> - do_verify(JwsCompacted, More) - end. - -check_claims(Claims) -> - Now = os:system_time(seconds), - Checker = [{<<"exp">>, fun(ExpireTime) -> - Now < ExpireTime - end}, - {<<"iat">>, fun(IssueAt) -> - IssueAt =< Now - end}, - {<<"nbf">>, fun(NotBefore) -> - NotBefore =< Now - end} - ], - do_check_claim(Checker, Claims). - -do_check_claim([], Claims) -> - Claims; -do_check_claim([{K, F}|More], Claims) -> - case maps:take(K, Claims) of - error -> do_check_claim(More, Claims); - {V, NClaims} -> - case F(V) of - true -> do_check_claim(More, NClaims); - _ -> {false, K} - end - end. diff --git a/apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl b/apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl deleted file mode 100644 index d4f562b6f..000000000 --- a/apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl +++ /dev/null @@ -1,166 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_jwt_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("emqx/include/emqx.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --define(APP, emqx_auth_jwt). - -all() -> - [{group, emqx_auth_jwt}]. - -groups() -> - [{emqx_auth_jwt, [sequence], [ t_check_auth - , t_check_claims - , t_check_claims_clientid - , t_check_claims_username - , t_check_claims_kid_in_header - ]} - ]. - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_auth_jwt], fun set_special_configs/1), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_auth_jwt]). - -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, acl_nomatch, deny), - application:set_env(emqx, enable_acl_cache, false), - LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]), - AclFilePath = filename:join(["test", "emqx_SUITE_data", "acl.conf"]), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)), - application:set_env(emqx, acl_file, - emqx_ct_helpers:deps_path(emqx, AclFilePath)); - -set_special_configs(emqx_auth_jwt) -> - application:set_env(emqx_auth_jwt, secret, "emqxsecret"), - application:set_env(emqx_auth_jwt, from, password); - -set_special_configs(_) -> - ok. - -sign(Payload, Header, Key) when is_map(Header) -> - Jwk = jose_jwk:from_oct(Key), - Jwt = emqx_json:encode(Payload), - {_, Token} = jose_jws:compact(jose_jwt:sign(Jwk, Header, Jwt)), - Token; - -sign(Payload, Alg, Key) -> - Jwk = jose_jwk:from_oct(Key), - Jwt = emqx_json:encode(Payload), - {_, Token} = jose_jws:compact(jose_jwt:sign(Jwk, #{<<"alg">> => Alg}, Jwt)), - Token. - -%%------------------------------------------------------------------------------ -%% Testcases -%%------------------------------------------------------------------------------ - -t_check_auth(_) -> - Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external}, - Jwt = sign([{clientid, <<"client1">>}, - {username, <<"plain">>}, - {exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>), - ct:pal("Jwt: ~p~n", [Jwt]), - - Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}), - ct:pal("Auth result: ~p~n", [Result0]), - ?assertMatch({ok, #{auth_result := success, jwt_claims := #{<<"clientid">> := <<"client1">>}}}, Result0), - - ct:sleep(3100), - Result1 = emqx_access_control:authenticate(Plain#{password => Jwt}), - ct:pal("Auth result after 1000ms: ~p~n", [Result1]), - ?assertMatch({error, _}, Result1), - - Jwt_Error = sign([{client_id, <<"client1">>}, - {username, <<"plain">>}], <<"HS256">>, <<"secret">>), - ct:pal("invalid jwt: ~p~n", [Jwt_Error]), - Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}), - ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]), - ?assertEqual({error, invalid_signature}, Result2), - ?assertMatch({error, _}, emqx_access_control:authenticate(Plain#{password => <<"asd">>})). - -t_check_claims(_) -> - application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]), - application:stop(emqx_auth_jwt), application:start(emqx_auth_jwt), - - Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external}, - Jwt = sign([{client_id, <<"client1">>}, - {username, <<"plain">>}, - {sub, value}, - {exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>), - Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}), - ct:pal("Auth result: ~p~n", [Result0]), - ?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0), - Jwt_Error = sign([{clientid, <<"client1">>}, - {username, <<"plain">>}], <<"HS256">>, <<"secret">>), - Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}), - ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]), - ?assertEqual({error, invalid_signature}, Result2). - -t_check_claims_clientid(_) -> - application:set_env(emqx_auth_jwt, verify_claims, [{clientid, <<"%c">>}]), - application:stop(emqx_auth_jwt), application:start(emqx_auth_jwt), - Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external}, - Jwt = sign([{clientid, <<"client23">>}, - {username, <<"plain">>}, - {exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>), - Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}), - ct:pal("Auth result: ~p~n", [Result0]), - ?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0), - Jwt_Error = sign([{clientid, <<"client1">>}, - {username, <<"plain">>}], <<"HS256">>, <<"secret">>), - Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}), - ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]), - ?assertEqual({error, invalid_signature}, Result2). - -t_check_claims_username(_) -> - application:set_env(emqx_auth_jwt, verify_claims, [{username, <<"%u">>}]), - application:stop(emqx_auth_jwt), application:start(emqx_auth_jwt), - - Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external}, - Jwt = sign([{client_id, <<"client23">>}, - {username, <<"plain">>}, - {exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>), - Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}), - ct:pal("Auth result: ~p~n", [Result0]), - ?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0), - Jwt_Error = sign([{clientid, <<"client1">>}, - {username, <<"plain">>}], <<"HS256">>, <<"secret">>), - Result3 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}), - ct:pal("Auth result for the invalid jwt: ~p~n", [Result3]), - ?assertEqual({error, invalid_signature}, Result3). - -t_check_claims_kid_in_header(_) -> - application:set_env(emqx_auth_jwt, verify_claims, []), - Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external}, - Jwt = sign([{clientid, <<"client23">>}, - {username, <<"plain">>}, - {exp, os:system_time(seconds) + 3}], - #{<<"alg">> => <<"HS256">>, - <<"kid">> => <<"a_kid_str">>}, <<"emqxsecret">>), - Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}), - ct:pal("Auth result: ~p~n", [Result0]), - ?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0). diff --git a/apps/emqx_auth_ldap/.gitignore b/apps/emqx_auth_ldap/.gitignore deleted file mode 100644 index eb8f0639f..000000000 --- a/apps/emqx_auth_ldap/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -.eunit -deps -*.o -*.beam -*.plt -erl_crash.dump -ebin -rel/example_project -.concrete/DEV_MODE -.rebar -.erlang.mk/ -emqx_auth_ldap.d -data/ -cover/ -ct.coverdata -eunit.coverdata -logs/ -test/ct.cover.spec -.DS_Store -_build/ -rebar.lock -erlang.mk -rebar3.crashdump -.rebar3/ -etc/emqx_auth_ldap.conf.rendered diff --git a/apps/emqx_auth_ldap/README.md b/apps/emqx_auth_ldap/README.md deleted file mode 100644 index c4d56c839..000000000 --- a/apps/emqx_auth_ldap/README.md +++ /dev/null @@ -1,96 +0,0 @@ -emqx_auth_ldap -============== - -EMQ X LDAP Authentication Plugin - -Build ------ - -``` -make -``` - -Load the Plugin ---------------- - -``` -# ./bin/emqx_ctl plugins load emqx_auth_ldap -``` - -Generate Password ---------------- - -``` -slappasswd -h '{ssha}' -s public -``` - -Configuration Open LDAP ------------------------ - -vim /etc/openldap/slapd.conf - -``` -include /etc/openldap/schema/core.schema -include /etc/openldap/schema/cosine.schema -include /etc/openldap/schema/inetorgperson.schema -include /etc/openldap/schema/ppolicy.schema -include /etc/openldap/schema/emqx.schema - -database bdb -suffix "dc=emqx,dc=io" -rootdn "cn=root,dc=emqx,dc=io" -rootpw {SSHA}eoF7NhNrejVYYyGHqnt+MdKNBh4r1w3W - -directory /etc/openldap/data -``` - -If the ldap launched with error below: -``` -Unrecognized database type (bdb) -5c4a72b9 slapd.conf: line 7: failed init (bdb) -slapadd: bad configuration file! -``` - -Insert lines to the slapd.conf -``` -modulepath /usr/lib/ldap -moduleload back_bdb.la -``` - -Import EMQX User Data ----------------------- - -Use ldapadd -``` -# ldapadd -x -D "cn=root,dc=emqx,dc=io" -w public -f emqx.com.ldif -``` - -Use slapadd -``` -# sudo slapadd -l schema/emqx.io.ldif -f slapd.conf -``` - -Launch slapd -``` -# sudo slapd -d 3 -``` - -Test ------ -After configure slapd correctly and launch slapd successfully. -You could execute - -``` bash -# make tests -``` - -License -------- - -Apache License Version 2.0 - -Author ------- - -EMQ X Team. - diff --git a/apps/emqx_auth_ldap/emqx.io.ldif b/apps/emqx_auth_ldap/emqx.io.ldif deleted file mode 100644 index f9833cd88..000000000 --- a/apps/emqx_auth_ldap/emqx.io.ldif +++ /dev/null @@ -1,135 +0,0 @@ -## create emqx.io - -dn:dc=emqx,dc=io -objectclass: top -objectclass: dcobject -objectclass: organization -dc:emqx -o:emqx,Inc. - -# create testdevice.emqx.io -dn:ou=testdevice,dc=emqx,dc=io -objectClass: top -objectclass:organizationalUnit -ou:testdevice - -# create user admin -dn:uid=admin,ou=testdevice,dc=emqx,dc=io -objectClass: top -objectClass: simpleSecurityObject -objectClass: account -userPassword:: e1NIQX1XNnBoNU1tNVB6OEdnaVVMYlBnekczN21qOWc9 -uid: admin - -## create user=mqttuser0001, -# password=mqttuser0001, -# passhash={SHA}mlb3fat40MKBTXUVZwCKmL73R/0= -# base64passhash=e1NIQX1tbGIzZmF0NDBNS0JUWFVWWndDS21MNzNSLzA9 -dn:uid=mqttuser0001,ou=testdevice,dc=emqx,dc=io -objectClass: top -objectClass: mqttUser -objectClass: mqttDevice -objectClass: mqttSecurity -uid: mqttuser0001 -isEnabled: TRUE -mqttAccountName: user1 -mqttPublishTopic: mqttuser0001/pub/1 -mqttPublishTopic: mqttuser0001/pub/+ -mqttPublishTopic: mqttuser0001/pub/# -mqttSubscriptionTopic: mqttuser0001/sub/1 -mqttSubscriptionTopic: mqttuser0001/sub/+ -mqttSubscriptionTopic: mqttuser0001/sub/# -mqttPubSubTopic: mqttuser0001/pubsub/1 -mqttPubSubTopic: mqttuser0001/pubsub/+ -mqttPubSubTopic: mqttuser0001/pubsub/# -userPassword:: e1NIQX1tbGIzZmF0NDBNS0JUWFVWWndDS21MNzNSLzA9 - -## create user=mqttuser0002 -# password=mqttuser0002, -# passhash={SSHA}n9XdtoG4Q/TQ3TQF4Y+khJbMBH4qXj4M -# base64passhash=e1NTSEF9bjlYZHRvRzRRL1RRM1RRRjRZK2toSmJNQkg0cVhqNE0= -dn:uid=mqttuser0002,ou=testdevice,dc=emqx,dc=io -objectClass: top -objectClass: mqttUser -objectClass: mqttDevice -objectClass: mqttSecurity -uid: mqttuser0002 -isEnabled: TRUE -mqttAccountName: user2 -mqttPublishTopic: mqttuser0002/pub/1 -mqttPublishTopic: mqttuser0002/pub/+ -mqttPublishTopic: mqttuser0002/pub/# -mqttSubscriptionTopic: mqttuser0002/sub/1 -mqttSubscriptionTopic: mqttuser0002/sub/+ -mqttSubscriptionTopic: mqttuser0002/sub/# -mqttPubSubTopic: mqttuser0002/pubsub/1 -mqttPubSubTopic: mqttuser0002/pubsub/+ -mqttPubSubTopic: mqttuser0002/pubsub/# -userPassword:: e1NTSEF9bjlYZHRvRzRRL1RRM1RRRjRZK2toSmJNQkg0cVhqNE0= - -## create user mqttuser0003 -# password=mqttuser0003, -# passhash={MD5}ybsPGoaK3nDyiQvveiCOIw== -# base64passhash=e01ENX15YnNQR29hSzNuRHlpUXZ2ZWlDT0l3PT0= -dn:uid=mqttuser0003,ou=testdevice,dc=emqx,dc=io -objectClass: top -objectClass: mqttUser -objectClass: mqttDevice -objectClass: mqttSecurity -uid: mqttuser0003 -isEnabled: TRUE -mqttPublishTopic: mqttuser0003/pub/1 -mqttPublishTopic: mqttuser0003/pub/+ -mqttPublishTopic: mqttuser0003/pub/# -mqttSubscriptionTopic: mqttuser0003/sub/1 -mqttSubscriptionTopic: mqttuser0003/sub/+ -mqttSubscriptionTopic: mqttuser0003/sub/# -mqttPubSubTopic: mqttuser0003/pubsub/1 -mqttPubSubTopic: mqttuser0003/pubsub/+ -mqttPubSubTopic: mqttuser0003/pubsub/# -userPassword:: e01ENX15YnNQR29hSzNuRHlpUXZ2ZWlDT0l3PT0= - -## create user mqttuser0004 -# password=mqttuser0004, -# passhash={MD5}2Br6pPDSEDIEvUlu9+s+MA== -# base64passhash=e01ENX0yQnI2cFBEU0VESUV2VWx1OStzK01BPT0= -dn:uid=mqttuser0004,ou=testdevice,dc=emqx,dc=io -objectClass: top -objectClass: mqttUser -objectClass: mqttDevice -objectClass: mqttSecurity -uid: mqttuser0004 -isEnabled: TRUE -mqttPublishTopic: mqttuser0004/pub/1 -mqttPublishTopic: mqttuser0004/pub/+ -mqttPublishTopic: mqttuser0004/pub/# -mqttSubscriptionTopic: mqttuser0004/sub/1 -mqttSubscriptionTopic: mqttuser0004/sub/+ -mqttSubscriptionTopic: mqttuser0004/sub/# -mqttPubSubTopic: mqttuser0004/pubsub/1 -mqttPubSubTopic: mqttuser0004/pubsub/+ -mqttPubSubTopic: mqttuser0004/pubsub/# -userPassword: {MD5}2Br6pPDSEDIEvUlu9+s+MA== - -## create user mqttuser0005 -# password=mqttuser0005, -# passhash={SHA}jKnxeEDGR14kE8AR7yuVFOelhz4= -# base64passhash=e1NIQX1qS254ZUVER1IxNGtFOEFSN3l1VkZPZWxoejQ9 -objectClass: top -dn:uid=mqttuser0005,ou=testdevice,dc=emqx,dc=io -objectClass: mqttUser -objectClass: mqttDevice -objectClass: mqttSecurity -uid: mqttuser0005 -isEnabled: TRUE -mqttPublishTopic: mqttuser0005/pub/1 -mqttPublishTopic: mqttuser0005/pub/+ -mqttPublishTopic: mqttuser0005/pub/# -mqttSubscriptionTopic: mqttuser0005/sub/1 -mqttSubscriptionTopic: mqttuser0005/sub/+ -mqttSubscriptionTopic: mqttuser0005/sub/# -mqttPubSubTopic: mqttuser0005/pubsub/1 -mqttPubSubTopic: mqttuser0005/pubsub/+ -mqttPubSubTopic: mqttuser0005/pubsub/# -userPassword: {SHA}jKnxeEDGR14kE8AR7yuVFOelhz4= - diff --git a/apps/emqx_auth_ldap/emqx.schema b/apps/emqx_auth_ldap/emqx.schema deleted file mode 100644 index 55f92269b..000000000 --- a/apps/emqx_auth_ldap/emqx.schema +++ /dev/null @@ -1,46 +0,0 @@ -# -# Preliminary Apple OS X Native LDAP Schema -# This file is subject to change. -# -attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.1.3 NAME 'isEnabled' - EQUALITY booleanMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 - SINGLE-VALUE - USAGE userApplications ) - -attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.1 NAME ( 'mqttPublishTopic' 'mpt' ) - EQUALITY caseIgnoreMatch - SUBSTR caseIgnoreSubstringsMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 - USAGE userApplications ) -attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.2 NAME ( 'mqttSubscriptionTopic' 'mst' ) - EQUALITY caseIgnoreMatch - SUBSTR caseIgnoreSubstringsMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 - USAGE userApplications ) -attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.3 NAME ( 'mqttPubSubTopic' 'mpst' ) - EQUALITY caseIgnoreMatch - SUBSTR caseIgnoreSubstringsMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 - USAGE userApplications ) -attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.4 NAME ( 'mqttAccountName' 'man' ) - EQUALITY caseIgnoreMatch - SUBSTR caseIgnoreSubstringsMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 - USAGE userApplications ) - - -objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4 NAME 'mqttUser' - AUXILIARY - MAY ( mqttPublishTopic $ mqttSubscriptionTopic $ mqttPubSubTopic $ mqttAccountName) ) - -objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.2 NAME 'mqttDevice' - SUP top - STRUCTURAL - MUST ( uid ) - MAY ( isEnabled ) ) - -objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.3 NAME 'mqttSecurity' - SUP top - AUXILIARY - MAY ( userPassword $ userPKCS12 $ pwdAttribute $ pwdLockout ) ) diff --git a/apps/emqx_auth_ldap/etc/emqx_auth_ldap.conf b/apps/emqx_auth_ldap/etc/emqx_auth_ldap.conf deleted file mode 100644 index b457229e3..000000000 --- a/apps/emqx_auth_ldap/etc/emqx_auth_ldap.conf +++ /dev/null @@ -1,76 +0,0 @@ -##-------------------------------------------------------------------- -## LDAP Auth Plugin -##-------------------------------------------------------------------- - -## LDAP server list, seperated by ','. -## -## Value: String -auth.ldap.servers = "127.0.0.1" - -## LDAP server port. -## -## Value: Port -auth.ldap.port = 389 - -## LDAP pool size -## -## Value: String -auth.ldap.pool = 8 - -## LDAP Bind DN. -## -## Value: DN -auth.ldap.bind_dn = "cn=root,dc=emqx,dc=io" - -## LDAP Bind Password. -## -## Value: String -auth.ldap.bind_password = public - -## LDAP query timeout. -## -## Value: Number -auth.ldap.timeout = 30s - -## Device DN. -## -## Variables: -## -## Value: DN -auth.ldap.device_dn = "ou=device,dc=emqx,dc=io" - -## Specified ObjectClass -## -## Variables: -## -## Value: string -auth.ldap.match_objectclass = mqttUser - -## attributetype for username -## -## Variables: -## -## Value: string -auth.ldap.username.attributetype = uid - -## attributetype for password -## -## Variables: -## -## Value: string -auth.ldap.password.attributetype = userPassword - -## Whether to enable SSL. -## -## Value: true | false -auth.ldap.ssl.enable = false - -#auth.ldap.ssl.certfile = "etc/certs/cert.pem" - -#auth.ldap.ssl.keyfile = "etc/certs/key.pem" - -#auth.ldap.ssl.cacertfile = "etc/certs/cacert.pem" - -#auth.ldap.ssl.verify = "verify_peer" - -#auth.ldap.ssl.server_name_indication = your_server_name diff --git a/apps/emqx_auth_ldap/include/emqx_auth_ldap.hrl b/apps/emqx_auth_ldap/include/emqx_auth_ldap.hrl deleted file mode 100644 index 8950c0ec8..000000000 --- a/apps/emqx_auth_ldap/include/emqx_auth_ldap.hrl +++ /dev/null @@ -1,23 +0,0 @@ - --define(APP, emqx_auth_ldap). - --record(auth_metrics, { - success = 'client.auth.success', - failure = 'client.auth.failure', - ignore = 'client.auth.ignore' - }). - --record(acl_metrics, { - allow = 'client.acl.allow', - deny = 'client.acl.deny', - ignore = 'client.acl.ignore' - }). - --define(METRICS(Type), tl(tuple_to_list(#Type{}))). --define(METRICS(Type, K), #Type{}#Type.K). - --define(AUTH_METRICS, ?METRICS(auth_metrics)). --define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). - --define(ACL_METRICS, ?METRICS(acl_metrics)). --define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). diff --git a/apps/emqx_auth_ldap/priv/emqx_auth_ldap.schema b/apps/emqx_auth_ldap/priv/emqx_auth_ldap.schema deleted file mode 100644 index f9c3bf16b..000000000 --- a/apps/emqx_auth_ldap/priv/emqx_auth_ldap.schema +++ /dev/null @@ -1,174 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_auth_ldap config mapping - -{mapping, "auth.ldap.servers", "emqx_auth_ldap.ldap", [ - {default, "127.0.0.1"}, - {datatype, string} -]}. - -{mapping, "auth.ldap.port", "emqx_auth_ldap.ldap", [ - {default, 389}, - {datatype, integer} -]}. - -{mapping, "auth.ldap.pool", "emqx_auth_ldap.ldap", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "auth.ldap.bind_dn", "emqx_auth_ldap.ldap", [ - {datatype, string}, - {default, "cn=root,dc=emqx,dc=io"} -]}. - -{mapping, "auth.ldap.bind_password", "emqx_auth_ldap.ldap", [ - {datatype, string}, - {default, "public"} -]}. - -{mapping, "auth.ldap.timeout", "emqx_auth_ldap.ldap", [ - {default, "30s"}, - {datatype, {duration, ms}} -]}. - -{mapping, "auth.ldap.ssl.enable", "emqx_auth_ldap.ldap", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "auth.ldap.ssl.certfile", "emqx_auth_ldap.ldap", [ - {datatype, string} -]}. - -{mapping, "auth.ldap.ssl.keyfile", "emqx_auth_ldap.ldap", [ - {datatype, string} -]}. - -{mapping, "auth.ldap.ssl.cacertfile", "emqx_auth_ldap.ldap", [ - {datatype, string} -]}. - -{mapping, "auth.ldap.ssl.verify", "emqx_auth_ldap.ldap", [ - {default, verify_none}, - {datatype, {enum, [verify_none, verify_peer]}} -]}. - -{mapping, "auth.ldap.ssl.server_name_indication", "emqx_auth_ldap.ldap", [ - {datatype, string} -]}. - -{translation, "emqx_auth_ldap.ldap", fun(Conf) -> - A2N = fun(A) -> case inet:parse_address(A) of {ok, N} -> N; _ -> A end end, - Servers = [A2N(A) || A <- string:tokens(cuttlefish:conf_get("auth.ldap.servers", Conf), ",")], - Port = cuttlefish:conf_get("auth.ldap.port", Conf), - Pool = cuttlefish:conf_get("auth.ldap.pool", Conf), - BindDN = cuttlefish:conf_get("auth.ldap.bind_dn", Conf), - BindPassword = cuttlefish:conf_get("auth.ldap.bind_password", Conf), - Timeout = cuttlefish:conf_get("auth.ldap.timeout", Conf), - Filter = fun(Ls) -> [E || E = {_, V} <- Ls, V /= undefined]end, - SslOpts = fun() -> - [{certfile, cuttlefish:conf_get("auth.ldap.ssl.certfile", Conf)}, - {keyfile, cuttlefish:conf_get("auth.ldap.ssl.keyfile", Conf)}, - {cacertfile, cuttlefish:conf_get("auth.ldap.ssl.cacertfile", Conf, undefined)}, - {verify, cuttlefish:conf_get("auth.ldap.ssl.verify", Conf, undefined)}, - {server_name_indication, case cuttlefish:conf_get("auth.ldap.ssl.server_name_indication", Conf, undefined) of - "disable" -> disable; - SNI -> SNI - end}] - end, - Opts = [{servers, Servers}, - {port, Port}, - {timeout, Timeout}, - {bind_dn, BindDN}, - {bind_password, BindPassword}, - {pool, Pool}, - {auto_reconnect, 2}], - case cuttlefish:conf_get("auth.ldap.ssl.enable", Conf) of - true -> [{ssl, true}, {sslopts, Filter(SslOpts())}|Opts]; - false -> [{ssl, false}|Opts] - end -end}. - -{mapping, "auth.ldap.device_dn", "emqx_auth_ldap.device_dn", [ - {default, "ou=device,dc=emqx,dc=io"}, - {datatype, string} -]}. - -{mapping, "auth.ldap.match_objectclass", "emqx_auth_ldap.match_objectclass", [ - {default, "mqttUser"}, - {datatype, string} -]}. - -{mapping, "auth.ldap.custom_base_dn", "emqx_auth_ldap.custom_base_dn", [ - {default, "${username_attr}=${user},${device_dn}"}, - {datatype, string} -]}. - -%% auth.ldap.filters.1.key = "objectClass" -%% auth.ldap.filters.1.value = "mqttUser" -%% auth.ldap.filters.1.op = "and" -%% auth.ldap.filters.2.key = "uiAttr" -%% auth.ldap.filters.2.value "someAttr" -%% auth.ldap.filters.2.op = "or" -%% auth.ldap.filters.3.key = "someKey" -%% auth.ldap.filters.3.value = "someValue" -%% The configuratation structure sent to the application: -%% [{"objectClass","mqttUser"},"and",{"uiAttr","someAttr"},"or",{"someKey","someAttr"}] -%% The resulting LDAP filter would look like this: -%% ==> "|(&(objectClass=Class)(uiAttr=someAttr)(someKey=someValue))" -{translation, "emqx_auth_ldap.filters", -fun(Conf) -> - Settings = cuttlefish_variable:filter_by_prefix("auth.ldap.filters", Conf), - Keys = [{Num, {key, V}} || {["auth","ldap","filters", Num, "key"], V} <- Settings], - Values = [{Num, {value, V}} || {["auth","ldap","filters", Num, "value"], V} <- Settings], - Ops = [{Num, {op, V}} || {["auth","ldap","filters", Num, "op"], V} <- Settings], - RawFilters = Keys ++ Values ++ Ops, - Filters = - lists:foldl( - fun({Num,{T,V}}, Acc)-> - maps:update_with(Num, - fun(F)-> - maps:put(T,V,F) - end, - #{T=>V}, Acc) - end, #{}, RawFilters), - Order=lists:usort(maps:keys(Filters)), - lists:reverse( - lists:foldl( - fun(F,Acc)-> - case F of - #{key:=K, op:=Op, value:=V} -> [Op,{K,V}|Acc]; - #{key:=K, value:=V} -> [{K,V}|Acc] - end - end, - [], - lists:map(fun(K) -> maps:get(K, Filters) end, Order))) -end}. - -{mapping, "auth.ldap.filters.$num.key", "emqx_auth_ldap.filters", [ - {datatype, string} -]}. - -{mapping, "auth.ldap.filters.$num.value", "emqx_auth_ldap.filters", [ - {datatype, string} -]}. - -{mapping, "auth.ldap.filters.$num.op", "emqx_auth_ldap.filters", [ - {datatype, {enum, [ "or", "and" ] } } -]}. - - -{mapping, "auth.ldap.bind_as_user", "emqx_auth_ldap.bind_as_user", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "auth.ldap.username.attributetype", "emqx_auth_ldap.username_attr", [ - {default, "uid"}, - {datatype, string} -]}. - -{mapping, "auth.ldap.password.attributetype", "emqx_auth_ldap.password_attr", [ - {default, "userPassword"}, - {datatype, string} -]}. diff --git a/apps/emqx_auth_ldap/rebar.config b/apps/emqx_auth_ldap/rebar.config deleted file mode 100644 index 811468a7b..000000000 --- a/apps/emqx_auth_ldap/rebar.config +++ /dev/null @@ -1,25 +0,0 @@ -{deps, - [{eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}} - ]}. - -{profiles, - [{test, - [{deps, []} - ]} - ]}. - -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - {parse_transform}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. - diff --git a/apps/emqx_auth_ldap/src/emqx_acl_ldap.erl b/apps/emqx_auth_ldap/src/emqx_acl_ldap.erl deleted file mode 100644 index 25287052c..000000000 --- a/apps/emqx_auth_ldap/src/emqx_acl_ldap.erl +++ /dev/null @@ -1,98 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_acl_ldap). - --include("emqx_auth_ldap.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("eldap/include/eldap.hrl"). --include_lib("emqx/include/logger.hrl"). - --export([ register_metrics/0 - , check_acl/5 - , description/0 - ]). - --import(proplists, [get_value/2]). - --import(emqx_auth_ldap_cli, [search/4]). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). - -check_acl(ClientInfo, PubSub, Topic, NoMatchAction, State) -> - case do_check_acl(ClientInfo, PubSub, Topic, NoMatchAction, State) of - ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok; - {stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow}; - {stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny} - end. - -do_check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _NoMatchAction, _State) -> - ok; - -do_check_acl(#{username := Username}, PubSub, Topic, _NoMatchAction, - #{device_dn := DeviceDn, - match_objectclass := ObjectClass, - username_attr := UidAttr, - custom_base_dn := CustomBaseDN, - pool := Pool} = Config) -> - - Filters = maps:get(filters, Config, []), - - ReplaceRules = [{"${username_attr}", UidAttr}, - {"${user}", binary_to_list(Username)}, - {"${device_dn}", DeviceDn}], - - Filter = emqx_auth_ldap:prepare_filter(Filters, UidAttr, ObjectClass, ReplaceRules), - - Attribute = case PubSub of - publish -> "mqttPublishTopic"; - subscribe -> "mqttSubscriptionTopic" - end, - Attribute1 = "mqttPubSubTopic", - ?LOG(debug, "[LDAP] search dn:~p filter:~p, attribute:~p", - [DeviceDn, Filter, Attribute]), - - BaseDN = emqx_auth_ldap:replace_vars(CustomBaseDN, ReplaceRules), - - case search(Pool, BaseDN, Filter, [Attribute, Attribute1]) of - {error, noSuchObject} -> - ok; - {ok, #eldap_search_result{entries = []}} -> - ok; - {ok, #eldap_search_result{entries = [Entry]}} -> - Topics = get_value(Attribute, Entry#eldap_entry.attributes) - ++ get_value(Attribute1, Entry#eldap_entry.attributes), - match(Topic, Topics); - Error -> - ?LOG(error, "[LDAP] search error:~p", [Error]), - {stop, deny} - end. - -match(_Topic, []) -> - ok; - -match(Topic, [Filter | Topics]) -> - case emqx_topic:match(Topic, list_to_binary(Filter)) of - true -> {stop, allow}; - false -> match(Topic, Topics) - end. - -description() -> - "ACL with LDAP". - diff --git a/apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src b/apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src deleted file mode 100644 index 1b76c32c8..000000000 --- a/apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_auth_ldap, - [{description, "EMQ X Authentication/ACL with LDAP"}, - {vsn, "4.4.0"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_auth_ldap_sup]}, - {applications, [kernel,stdlib,eldap2,ecpool]}, - {mod, {emqx_auth_ldap_app,[]}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-auth-ldap"} - ]} - ]}. diff --git a/apps/emqx_auth_ldap/src/emqx_auth_ldap.erl b/apps/emqx_auth_ldap/src/emqx_auth_ldap.erl deleted file mode 100644 index 9163362c7..000000000 --- a/apps/emqx_auth_ldap/src/emqx_auth_ldap.erl +++ /dev/null @@ -1,210 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_ldap). - --include("emqx_auth_ldap.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("eldap/include/eldap.hrl"). --include_lib("emqx/include/logger.hrl"). - --import(proplists, [get_value/2]). - --import(emqx_auth_ldap_cli, [search/3]). - --export([ register_metrics/0 - , check/3 - , description/0 - , prepare_filter/4 - , replace_vars/2 - ]). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). - -check(ClientInfo = #{username := Username, password := Password}, AuthResult, - State = #{password_attr := PasswdAttr, bind_as_user := BindAsUserRequired, pool := Pool}) -> - CheckResult = - case lookup_user(Username, State) of - undefined -> {error, not_found}; - {error, Error} -> {error, Error}; - Entry -> - PasswordString = binary_to_list(Password), - ObjectName = Entry#eldap_entry.object_name, - Attributes = Entry#eldap_entry.attributes, - case BindAsUserRequired of - true -> - emqx_auth_ldap_cli:post_bind(Pool, ObjectName, PasswordString); - false -> - case get_value(PasswdAttr, Attributes) of - undefined -> - logger:error("LDAP Search State: ~p, uid: ~p, result:~p", - [State, Username, Attributes]), - {error, not_found}; - [Passhash1] -> - format_password(Passhash1, Password, ClientInfo) - end - end - end, - case CheckResult of - ok -> - ok = emqx_metrics:inc(?AUTH_METRICS(success)), - {stop, AuthResult#{auth_result => success, anonymous => false}}; - {error, not_found} -> - emqx_metrics:inc(?AUTH_METRICS(ignore)); - {error, ResultCode} -> - ok = emqx_metrics:inc(?AUTH_METRICS(failure)), - ?LOG(error, "[LDAP] Auth from ldap failed: ~p", [ResultCode]), - {stop, AuthResult#{auth_result => ResultCode, anonymous => false}} - end. - -lookup_user(Username, #{username_attr := UidAttr, - match_objectclass := ObjectClass, - device_dn := DeviceDn, - custom_base_dn := CustomBaseDN, pool := Pool} = Config) -> - - Filters = maps:get(filters, Config, []), - - ReplaceRules = [{"${username_attr}", UidAttr}, - {"${user}", binary_to_list(Username)}, - {"${device_dn}", DeviceDn}], - - Filter = prepare_filter(Filters, UidAttr, ObjectClass, ReplaceRules), - - %% auth.ldap.custom_base_dn = "${username_attr}=${user},${device_dn}" - BaseDN = replace_vars(CustomBaseDN, ReplaceRules), - - case search(Pool, BaseDN, Filter) of - %% This clause seems to be impossible to match. `eldap2:search/2` does - %% not validates the result, so if it returns "successfully" from the - %% LDAP server, it always returns `{ok, #eldap_search_result{}}`. - {error, noSuchObject} -> - undefined; - %% In case no user was found by the search, but the search was completed - %% without error we get an empty `entries` list. - {ok, #eldap_search_result{entries = []}} -> - undefined; - {ok, #eldap_search_result{entries = [Entry]}} -> - Attributes = Entry#eldap_entry.attributes, - case get_value("isEnabled", Attributes) of - undefined -> - Entry; - [Val] -> - case list_to_atom(string:to_lower(Val)) of - true -> Entry; - false -> {error, username_disabled} - end - end; - {error, Error} -> - ?LOG(error, "[LDAP] Search dn: ~p, filter: ~p, fail:~p", [DeviceDn, Filter, Error]), - {error, username_or_password_error} - end. - -check_pass(Password, Password, _ClientInfo) -> ok; -check_pass(_, _, _) -> {error, bad_username_or_password}. - -format_password(Passhash, Password, ClientInfo) -> - case do_format_password(Passhash, Password) of - {error, Error2} -> - {error, Error2}; - {Passhash1, Password1} -> - check_pass(Passhash1, Password1, ClientInfo) - end. - -do_format_password(Passhash, Password) -> - Base64PasshashHandler = - handle_passhash(fun(HashType, Passhash1, Password1) -> - Passhash2 = binary_to_list(base64:decode(Passhash1)), - resolve_passhash(HashType, Passhash2, Password1) - end, - fun(_Passhash, _Password) -> - {error, password_error} - end), - PasshashHandler = handle_passhash(fun resolve_passhash/3, Base64PasshashHandler), - PasshashHandler(Passhash, Password). - -resolve_passhash(HashType, Passhash, Password) -> - [_, Passhash1] = string:tokens(Passhash, "}"), - do_resolve(HashType, Passhash1, Password). - -handle_passhash(HandleMatch, HandleNoMatch) -> - fun(Passhash, Password) -> - case re:run(Passhash, "(?<={)[^{}]+(?=})", [{capture, all, list}, global]) of - {match, [[HashType]]} -> - HandleMatch(list_to_atom(string:to_lower(HashType)), Passhash, Password); - _ -> - HandleNoMatch(Passhash, Password) - end - end. - -do_resolve(ssha, Passhash, Password) -> - D64 = base64:decode(Passhash), - {HashedData, Salt} = lists:split(20, binary_to_list(D64)), - NewHash = crypto:hash(sha, <>), - {list_to_binary(HashedData), NewHash}; -do_resolve(HashType, Passhash, Password) -> - Password1 = base64:encode(crypto:hash(HashType, Password)), - {list_to_binary(Passhash), Password1}. - -description() -> "LDAP Authentication Plugin". - -prepare_filter(Filters, _UidAttr, ObjectClass, ReplaceRules) -> - SubFilters = - lists:map(fun({K, V}) -> - {replace_vars(K, ReplaceRules), replace_vars(V, ReplaceRules)}; - (Op) -> - Op - end, Filters), - case SubFilters of - [] -> eldap2:equalityMatch("objectClass", ObjectClass); - _List -> compile_filters(SubFilters, []) - end. - - -compile_filters([{Key, Value}], []) -> - compile_equal(Key, Value); -compile_filters([{K1, V1}, "and", {K2, V2} | Rest], []) -> - compile_filters( - Rest, - eldap2:'and'([compile_equal(K1, V1), - compile_equal(K2, V2)])); -compile_filters([{K1, V1}, "or", {K2, V2} | Rest], []) -> - compile_filters( - Rest, - eldap2:'or'([compile_equal(K1, V1), - compile_equal(K2, V2)])); -compile_filters(["and", {K, V} | Rest], PartialFilter) -> - compile_filters( - Rest, - eldap2:'and'([PartialFilter, - compile_equal(K, V)])); -compile_filters(["or", {K, V} | Rest], PartialFilter) -> - compile_filters( - Rest, - eldap2:'or'([PartialFilter, - compile_equal(K, V)])); -compile_filters([], Filter) -> - Filter. - -compile_equal(Key, Value) -> - eldap2:equalityMatch(Key, Value). - -replace_vars(CustomBaseDN, ReplaceRules) -> - lists:foldl(fun({Pattern, Substitute}, DN) -> - lists:flatten(string:replace(DN, Pattern, Substitute)) - end, CustomBaseDN, ReplaceRules). diff --git a/apps/emqx_auth_ldap/src/emqx_auth_ldap_app.erl b/apps/emqx_auth_ldap/src/emqx_auth_ldap_app.erl deleted file mode 100644 index ed43c8d26..000000000 --- a/apps/emqx_auth_ldap/src/emqx_auth_ldap_app.erl +++ /dev/null @@ -1,78 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_ldap_app). - --behaviour(application). - --emqx_plugin(auth). - --include("emqx_auth_ldap.hrl"). - -%% Application callbacks --export([ start/2 - , prep_stop/1 - , stop/1 - ]). - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_auth_ldap_sup:start_link(), - _ = if_enabled([device_dn, match_objectclass, - username_attr, password_attr, - filters, custom_base_dn, bind_as_user], - fun load_auth_hook/1), - _ = if_enabled([device_dn, match_objectclass, - username_attr, password_attr, - filters, custom_base_dn, bind_as_user], - fun load_acl_hook/1), - {ok, Sup}. - -prep_stop(State) -> - emqx:unhook('client.authenticate',{emqx_auth_ldap, check}), - emqx:unhook('client.check_acl', {emqx_acl_ldap, check_acl}), - State. - -stop(_State) -> - ok. - -load_auth_hook(DeviceDn) -> - ok = emqx_auth_ldap:register_metrics(), - Params = maps:from_list(DeviceDn), - emqx:hook('client.authenticate', {emqx_auth_ldap, check, [Params#{pool => ?APP}]}). - -load_acl_hook(DeviceDn) -> - ok = emqx_acl_ldap:register_metrics(), - Params = maps:from_list(DeviceDn), - emqx:hook('client.check_acl', {emqx_acl_ldap, check_acl, [Params#{pool => ?APP}]}). - -if_enabled(Cfgs, Fun) -> - case get_env(Cfgs) of - {ok, []} -> ok; - {ok, InitArgs} -> Fun(InitArgs) - end. - -get_env(Cfgs) -> - get_env(Cfgs, []). - -get_env([Cfg | LeftCfgs], ENVS) -> - case application:get_env(?APP, Cfg) of - {ok, ENV} -> - get_env(LeftCfgs, [{Cfg, ENV} | ENVS]); - undefined -> - get_env(LeftCfgs, ENVS) - end; -get_env([], ENVS) -> - {ok, ENVS}. diff --git a/apps/emqx_auth_ldap/src/emqx_auth_ldap_cli.erl b/apps/emqx_auth_ldap/src/emqx_auth_ldap_cli.erl deleted file mode 100644 index 412754664..000000000 --- a/apps/emqx_auth_ldap/src/emqx_auth_ldap_cli.erl +++ /dev/null @@ -1,150 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_ldap_cli). - --behaviour(ecpool_worker). - --include("emqx_auth_ldap.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - -%% ecpool callback --export([connect/1]). - --export([ search/3 - , search/4 - , post_bind/3 - , init_args/1 - ]). - --import(proplists, - [ get_value/2 - , get_value/3 - ]). - -%%-------------------------------------------------------------------- -%% LDAP Connect/Search -%%-------------------------------------------------------------------- - -connect(Opts) -> - Servers = get_value(servers, Opts, ["localhost"]), - Port = get_value(port, Opts, 389), - Timeout = get_value(timeout, Opts, 30), - BindDn = get_value(bind_dn, Opts), - BindPassword = get_value(bind_password, Opts), - LdapOpts = case get_value(ssl, Opts, false)of - true -> - SslOpts = get_value(sslopts, Opts), - [{port, Port}, {timeout, Timeout}, {sslopts, SslOpts}]; - false -> - [{port, Port}, {timeout, Timeout}] - end, - ?LOG(debug, "[LDAP] Connecting to OpenLDAP server: ~p, Opts:~p ...", [Servers, LdapOpts]), - - case eldap2:open(Servers, LdapOpts) of - {ok, LDAP} -> - try eldap2:simple_bind(LDAP, BindDn, BindPassword) of - ok -> {ok, LDAP}; - {error, Error} -> - ?LOG(error, "[LDAP] Can't authenticated to OpenLDAP server: ~p", [Error]), - {error, Error} - catch - error:Reason -> - ?LOG(error, "[LDAP] Can't authenticated to OpenLDAP server: ~p", [Reason]), - {error, Reason} - end; - {error, Reason} -> - ?LOG(error, "[LDAP] Can't connect to OpenLDAP server: ~p", [Reason]), - {error, Reason} - end. - -search(Pool, Base, Filter) -> - ecpool:with_client(Pool, - fun(C) -> - case application:get_env(?APP, bind_as_user) of - {ok, true} -> - {ok, Opts} = application:get_env(?APP, ldap), - BindDn = get_value(bind_dn, Opts), - BindPassword = get_value(bind_password, Opts), - try eldap2:simple_bind(C, BindDn, BindPassword) of - ok -> - eldap2:search(C, [{base, Base}, - {filter, Filter}, - {deref, eldap2:derefFindingBaseObj()}]); - {error, Error} -> - {error, Error} - catch - error:Reason -> {error, Reason} - end; - {ok, false} -> - eldap2:search(C, [{base, Base}, - {filter, Filter}, - {deref, eldap2:derefFindingBaseObj()}]) - end - end). - -search(Pool, Base, Filter, Attributes) -> - ecpool:with_client(Pool, - fun(C) -> - case application:get_env(?APP, bind_as_user) of - {ok, true} -> - {ok, Opts} = application:get_env(?APP, ldap), - BindDn = get_value(bind_dn, Opts), - BindPassword = get_value(bind_password, Opts), - try eldap2:simple_bind(C, BindDn, BindPassword) of - ok -> - eldap2:search(C, [{base, Base}, - {filter, Filter}, - {attributes, Attributes}, - {deref, eldap2:derefFindingBaseObj()}]); - {error, Error} -> - {error, Error} - catch - error:Reason -> {error, Reason} - end; - {ok, false} -> - eldap2:search(C, [{base, Base}, - {filter, Filter}, - {attributes, Attributes}, - {deref, eldap2:derefFindingBaseObj()}]) - end - end). - -post_bind(Pool, BindDn, BindPassword) -> - ecpool:with_client(Pool, - fun(C) -> - try eldap2:simple_bind(C, BindDn, BindPassword) of - ok -> ok; - {error, Error} -> - {error, Error} - catch - error:Reason -> {error, Reason} - end - end). - - -init_args(ENVS) -> - DeviceDn = get_value(device_dn, ENVS), - ObjectClass = get_value(match_objectclass, ENVS), - UidAttr = get_value(username_attr, ENVS), - PasswdAttr = get_value(password_attr, ENVS), - {ok, #{device_dn => DeviceDn, - match_objectclass => ObjectClass, - username_attr => UidAttr, - password_attr => PasswdAttr}}. - diff --git a/apps/emqx_auth_ldap/test/certs/cacert.pem b/apps/emqx_auth_ldap/test/certs/cacert.pem deleted file mode 100644 index 604fd2362..000000000 --- a/apps/emqx_auth_ldap/test/certs/cacert.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDUTCCAjmgAwIBAgIJAPPYCjTmxdt/MA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV -BAYTAkNOMREwDwYDVQQIDAhoYW5nemhvdTEMMAoGA1UECgwDRU1RMQ8wDQYDVQQD -DAZSb290Q0EwHhcNMjAwNTA4MDgwNjUyWhcNMzAwNTA2MDgwNjUyWjA/MQswCQYD -VQQGEwJDTjERMA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UE -AwwGUm9vdENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzcgVLex1 -EZ9ON64EX8v+wcSjzOZpiEOsAOuSXOEN3wb8FKUxCdsGrsJYB7a5VM/Jot25Mod2 -juS3OBMg6r85k2TWjdxUoUs+HiUB/pP/ARaaW6VntpAEokpij/przWMPgJnBF3Ur -MjtbLayH9hGmpQrI5c2vmHQ2reRZnSFbY+2b8SXZ+3lZZgz9+BaQYWdQWfaUWEHZ -uDaNiViVO0OT8DRjCuiDp3yYDj3iLWbTA/gDL6Tf5XuHuEwcOQUrd+h0hyIphO8D -tsrsHZ14j4AWYLk1CPA6pq1HIUvEl2rANx2lVUNv+nt64K/Mr3RnVQd9s8bK+TXQ -KGHd2Lv/PALYuwIDAQABo1AwTjAdBgNVHQ4EFgQUGBmW+iDzxctWAWxmhgdlE8Pj -EbQwHwYDVR0jBBgwFoAUGBmW+iDzxctWAWxmhgdlE8PjEbQwDAYDVR0TBAUwAwEB -/zANBgkqhkiG9w0BAQsFAAOCAQEAGbhRUjpIred4cFAFJ7bbYD9hKu/yzWPWkMRa -ErlCKHmuYsYk+5d16JQhJaFy6MGXfLgo3KV2itl0d+OWNH0U9ULXcglTxy6+njo5 -CFqdUBPwN1jxhzo9yteDMKF4+AHIxbvCAJa17qcwUKR5MKNvv09C6pvQDJLzid7y -E2dkgSuggik3oa0427KvctFf8uhOV94RvEDyqvT5+pgNYZ2Yfga9pD/jjpoHEUlo -88IGU8/wJCx3Ds2yc8+oBg/ynxG8f/HmCC1ET6EHHoe2jlo8FpU/SgGtghS1YL30 -IWxNsPrUP+XsZpBJy/mvOhE5QXo6Y35zDqqj8tI7AGmAWu22jg== ------END CERTIFICATE----- diff --git a/apps/emqx_auth_ldap/test/certs/cert.pem b/apps/emqx_auth_ldap/test/certs/cert.pem deleted file mode 100644 index 092390b1d..000000000 --- a/apps/emqx_auth_ldap/test/certs/cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIBAjANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJDTjER -MA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UEAwwGUm9vdENB -MB4XDTIwMDUwODA4MDcwNVoXDTMwMDUwNjA4MDcwNVowPzELMAkGA1UEBhMCQ04x -ETAPBgNVBAgMCGhhbmd6aG91MQwwCgYDVQQKDANFTVExDzANBgNVBAMMBlNlcnZl -cjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALNeWT3pE+QFfiRJzKmn -AMUrWo3K2j/Tm3+Xnl6WLz67/0rcYrJbbKvS3uyRP/stXyXEKw9CepyQ1ViBVFkW -Aoy8qQEOWFDsZc/5UzhXUnb6LXr3qTkFEjNmhj+7uzv/lbBxlUG1NlYzSeOB6/RT -8zH/lhOeKhLnWYPXdXKsa1FL6ij4X8DeDO1kY7fvAGmBn/THh1uTpDizM4YmeI+7 -4dmayA5xXvARte5h4Vu5SIze7iC057N+vymToMk2Jgk+ZZFpyXrnq+yo6RaD3ANc -lrc4FbeUQZ5a5s5Sxgs9a0Y3WMG+7c5VnVXcbjBRz/aq2NtOnQQjikKKQA8GF080 -BQkCAwEAAaMaMBgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQEL -BQADggEBAJefnMZpaRDHQSNUIEL3iwGXE9c6PmIsQVE2ustr+CakBp3TZ4l0enLt -iGMfEVFju69cO4oyokWv+hl5eCMkHBf14Kv51vj448jowYnF1zmzn7SEzm5Uzlsa -sqjtAprnLyof69WtLU1j5rYWBuFX86yOTwRAFNjm9fvhAcrEONBsQtqipBWkMROp -iUYMkRqbKcQMdwxov+lHBYKq9zbWRoqLROAn54SRqgQk6c15JdEfgOOjShbsOkIH -UhqcwRkQic7n1zwHVGVDgNIZVgmJ2IdIWBlPEC7oLrRrBD/X1iEEXtKab6p5o22n -KB5mN+iQaE+Oe2cpGKZJiJRdM+IqDDQ= ------END CERTIFICATE----- diff --git a/apps/emqx_auth_ldap/test/certs/client-cert.pem b/apps/emqx_auth_ldap/test/certs/client-cert.pem deleted file mode 100644 index 09d855221..000000000 --- a/apps/emqx_auth_ldap/test/certs/client-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIBATANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJDTjER -MA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UEAwwGUm9vdENB -MB4XDTIwMDUwODA4MDY1N1oXDTMwMDUwNjA4MDY1N1owPzELMAkGA1UEBhMCQ04x -ETAPBgNVBAgMCGhhbmd6aG91MQwwCgYDVQQKDANFTVExDzANBgNVBAMMBkNsaWVu -dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMy4hoksKcZBDbY680u6 -TS25U51nuB1FBcGMlF9B/t057wPOlxF/OcmbxY5MwepS41JDGPgulE1V7fpsXkiW -1LUimYV/tsqBfymIe0mlY7oORahKji7zKQ2UBIVFhdlvQxunlIDnw6F9popUgyHt -dMhtlgZK8oqRwHxO5dbfoukYd6J/r+etS5q26sgVkf3C6dt0Td7B25H9qW+f7oLV -PbcHYCa+i73u9670nrpXsC+Qc7Mygwa2Kq/jwU+ftyLQnOeW07DuzOwsziC/fQZa -nbxR+8U9FNftgRcC3uP/JMKYUqsiRAuaDokARZxVTV5hUElfpO6z6/NItSDvvh3i -eikCAwEAAaMaMBgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQEL -BQADggEBABchYxKo0YMma7g1qDswJXsR5s56Czx/I+B41YcpMBMTrRqpUC0nHtLk -M7/tZp592u/tT8gzEnQjZLKBAhFeZaR3aaKyknLqwiPqJIgg0pgsBGITrAK3Pv4z -5/YvAJJKgTe5UdeTz6U4lvNEux/4juZ4pmqH4qSFJTOzQS7LmgSmNIdd072rwXBd -UzcSHzsJgEMb88u/LDLjj1pQ7AtZ4Tta8JZTvcgBFmjB0QUi6fgkHY6oGat/W4kR -jSRUBlMUbM/drr2PVzRc2dwbFIl3X+ZE6n5Sl3ZwRAC/s92JU6CPMRW02muVu6xl -goraNgPISnrbpR6KjxLZkVembXzjNNc= ------END CERTIFICATE----- diff --git a/apps/emqx_auth_ldap/test/certs/client-key.pem b/apps/emqx_auth_ldap/test/certs/client-key.pem deleted file mode 100644 index 2b3f30cf6..000000000 --- a/apps/emqx_auth_ldap/test/certs/client-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAzLiGiSwpxkENtjrzS7pNLblTnWe4HUUFwYyUX0H+3TnvA86X -EX85yZvFjkzB6lLjUkMY+C6UTVXt+mxeSJbUtSKZhX+2yoF/KYh7SaVjug5FqEqO -LvMpDZQEhUWF2W9DG6eUgOfDoX2milSDIe10yG2WBkryipHAfE7l1t+i6Rh3on+v -561LmrbqyBWR/cLp23RN3sHbkf2pb5/ugtU9twdgJr6Lve73rvSeulewL5BzszKD -BrYqr+PBT5+3ItCc55bTsO7M7CzOIL99BlqdvFH7xT0U1+2BFwLe4/8kwphSqyJE -C5oOiQBFnFVNXmFQSV+k7rPr80i1IO++HeJ6KQIDAQABAoIBAGWgvPjfuaU3qizq -uti/FY07USz0zkuJdkANH6LiSjlchzDmn8wJ0pApCjuIE0PV/g9aS8z4opp5q/gD -UBLM/a8mC/xf2EhTXOMrY7i9p/I3H5FZ4ZehEqIw9sWKK9YzC6dw26HabB2BGOnW -5nozPSQ6cp2RGzJ7BIkxSZwPzPnVTgy3OAuPOiJytvK+hGLhsNaT+Y9bNDvplVT2 -ZwYTV8GlHZC+4b2wNROILm0O86v96O+Qd8nn3fXjGHbMsAnONBq10bZS16L4fvkH -5G+W/1PeSXmtZFppdRRDxIW+DWcXK0D48WRliuxcV4eOOxI+a9N2ZJZZiNLQZGwg -w3A8+mECgYEA8HuJFrlRvdoBe2U/EwUtG74dcyy30L4yEBnN5QscXmEEikhaQCfX -Wm6EieMcIB/5I5TQmSw0cmBMeZjSXYoFdoI16/X6yMMuATdxpvhOZGdUGXxhAH+x -xoTUavWZnEqW3fkUU71kT5E2f2i+0zoatFESXHeslJyz85aAYpP92H0CgYEA2e5A -Yozt5eaA1Gyhd8SeptkEU4xPirNUnVQHStpMWUb1kzTNXrPmNWccQ7JpfpG6DcYl -zUF6p6mlzY+zkMiyPQjwEJlhiHM2NlL1QS7td0R8ewgsFoyn8WsBI4RejWrEG9td -EDniuIw+pBFkcWthnTLHwECHdzgquToyTMjrBB0CgYEA28tdGbrZXhcyAZEhHAZA -Gzog+pKlkpEzeonLKIuGKzCrEKRecIK5jrqyQsCjhS0T7ZRnL4g6i0s+umiV5M5w -fcc292pEA1h45L3DD6OlKplSQVTv55/OYS4oY3YEJtf5mfm8vWi9lQeY8sxOlQpn -O+VZTdBHmTC8PGeTAgZXHZUCgYA6Tyv88lYowB7SN2qQgBQu8jvdGtqhcs/99GCr -H3N0I69LPsKAR0QeH8OJPXBKhDUywESXAaEOwS5yrLNP1tMRz5Vj65YUCzeDG3kx -gpvY4IMp7ArX0bSRvJ6mYSFnVxy3k174G3TVCfksrtagHioVBGQ7xUg5ltafjrms -n8l55QKBgQDVzU8tQvBVqY8/1lnw11Vj4fkE/drZHJ5UkdC1eenOfSWhlSLfUJ8j -ds7vEWpRPPoVuPZYeR1y78cyxKe1GBx6Wa2lF5c7xjmiu0xbRnrxYeLolce9/ntp -asClqpnHT8/VJYTD7Kqj0fouTTZf0zkig/y+2XERppd8k+pSKjUCPQ== ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_ldap/test/certs/key.pem b/apps/emqx_auth_ldap/test/certs/key.pem deleted file mode 100644 index 6c338216e..000000000 --- a/apps/emqx_auth_ldap/test/certs/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAs15ZPekT5AV+JEnMqacAxStajcraP9Obf5eeXpYvPrv/Stxi -sltsq9Le7JE/+y1fJcQrD0J6nJDVWIFUWRYCjLypAQ5YUOxlz/lTOFdSdvotevep -OQUSM2aGP7u7O/+VsHGVQbU2VjNJ44Hr9FPzMf+WE54qEudZg9d1cqxrUUvqKPhf -wN4M7WRjt+8AaYGf9MeHW5OkOLMzhiZ4j7vh2ZrIDnFe8BG17mHhW7lIjN7uILTn -s36/KZOgyTYmCT5lkWnJeuer7KjpFoPcA1yWtzgVt5RBnlrmzlLGCz1rRjdYwb7t -zlWdVdxuMFHP9qrY206dBCOKQopADwYXTzQFCQIDAQABAoIBAQCuvCbr7Pd3lvI/ -n7VFQG+7pHRe1VKwAxDkx2t8cYos7y/QWcm8Ptwqtw58HzPZGWYrgGMCRpzzkRSF -V9g3wP1S5Scu5C6dBu5YIGc157tqNGXB+SpdZddJQ4Nc6yGHXYERllT04ffBGc3N -WG/oYS/1cSteiSIrsDy/91FvGRCi7FPxH3wIgHssY/tw69s1Cfvaq5lr2NTFzxIG -xCvpJKEdSfVfS9I7LYiymVjst3IOR/w76/ZFY9cRa8ZtmQSWWsm0TUpRC1jdcbkm -ZoJptYWlP+gSwx/fpMYftrkJFGOJhHJHQhwxT5X/ajAISeqjjwkWSEJLwnHQd11C -Zy2+29lBAoGBANlEAIK4VxCqyPXNKfoOOi5dS64NfvyH4A1v2+KaHWc7lqaqPN49 -ezfN2n3X+KWx4cviDD914Yc2JQ1vVJjSaHci7yivocDo2OfZDmjBqzaMp/y+rX1R -/f3MmiTqMa468rjaxI9RRZu7vDgpTR+za1+OBCgMzjvAng8dJuN/5gjlAoGBANNY -uYPKtearBmkqdrSV7eTUe49Nhr0XotLaVBH37TCW0Xv9wjO2xmbm5Ga/DCtPIsBb -yPeYwX9FjoasuadUD7hRvbFu6dBa0HGLmkXRJZTcD7MEX2Lhu4BuC72yDLLFd0r+ -Ep9WP7F5iJyagYqIZtz+4uf7gBvUDdmvXz3sGr1VAoGAdXTD6eeKeiI6PlhKBztF -zOb3EQOO0SsLv3fnodu7ZaHbUgLaoTMPuB17r2jgrYM7FKQCBxTNdfGZmmfDjlLB -0xZ5wL8ibU30ZXL8zTlWPElST9sto4B+FYVVF/vcG9sWeUUb2ncPcJ/Po3UAktDG -jYQTTyuNGtSJHpad/YOZctkCgYBtWRaC7bq3of0rJGFOhdQT9SwItN/lrfj8hyHA -OjpqTV4NfPmhsAtu6j96OZaeQc+FHvgXwt06cE6Rt4RG4uNPRluTFgO7XYFDfitP -vCppnoIw6S5BBvHwPP+uIhUX2bsi/dm8vu8tb+gSvo4PkwtFhEr6I9HglBKmcmog -q6waEQKBgHyecFBeM6Ls11Cd64vborwJPAuxIW7HBAFj/BS99oeG4TjBx4Sz2dFd -rzUibJt4ndnHIvCN8JQkjNG14i9hJln+H3mRss8fbZ9vQdqG+2vOWADYSzzsNI55 -RFY7JjluKcVkp/zCDeUxTU3O6sS+v6/3VE11Cob6OYQx3lN5wrZ3 ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_ldap/test/emqx_auth_ldap_SUITE.erl b/apps/emqx_auth_ldap/test/emqx_auth_ldap_SUITE.erl deleted file mode 100644 index 52bed9cf4..000000000 --- a/apps/emqx_auth_ldap/test/emqx_auth_ldap_SUITE.erl +++ /dev/null @@ -1,152 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_ldap_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("emqx/include/emqx.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --define(PID, emqx_auth_ldap). - --define(APP, emqx_auth_ldap). - --define(DeviceDN, "ou=test_device,dc=emqx,dc=io"). - --define(AuthDN, "ou=test_auth,dc=emqx,dc=io"). - -%%-------------------------------------------------------------------- -%% Setups -%%-------------------------------------------------------------------- - -all() -> - [{group, nossl}, {group, ssl}]. - -groups() -> - Cases = emqx_ct:all(?MODULE), - [{nossl, Cases}, {ssl, Cases}]. - -init_per_group(GrpName, Cfg) -> - Fun = fun(App) -> set_special_configs(GrpName, App) end, - emqx_ct_helpers:start_apps([emqx_auth_ldap], Fun), - Cfg. - -end_per_group(_GrpName, _Cfg) -> - emqx_ct_helpers:stop_apps([emqx_auth_ldap]). - -%%-------------------------------------------------------------------- -%% Cases -%%-------------------------------------------------------------------- - -t_check_auth(_) -> - MqttUser1 = #{clientid => <<"mqttuser1">>, - username => <<"mqttuser0001">>, - password => <<"mqttuser0001">>, - zone => external}, - MqttUser2 = #{clientid => <<"mqttuser2">>, - username => <<"mqttuser0002">>, - password => <<"mqttuser0002">>, - zone => external}, - MqttUser3 = #{clientid => <<"mqttuser3">>, - username => <<"mqttuser0003">>, - password => <<"mqttuser0003">>, - zone => external}, - MqttUser4 = #{clientid => <<"mqttuser4">>, - username => <<"mqttuser0004">>, - password => <<"mqttuser0004">>, - zone => external}, - MqttUser5 = #{clientid => <<"mqttuser5">>, - username => <<"mqttuser0005">>, - password => <<"mqttuser0005">>, - zone => external}, - NonExistUser1 = #{clientid => <<"mqttuser6">>, - username => <<"mqttuser0006">>, - password => <<"mqttuser0006">>, - zone => external}, - NonExistUser2 = #{clientid => <<"mqttuser7">>, - username => <<"mqttuser0005">>, - password => <<"mqttuser0006">>, - zone => external}, - ct:log("MqttUser: ~p", [emqx_access_control:authenticate(MqttUser1)]), - ?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser1)), - ?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser2)), - ?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser3)), - ?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser4)), - ?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser5)), - ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(NonExistUser1)), - ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(NonExistUser2)). - -t_check_acl(_) -> - MqttUser = #{clientid => <<"mqttuser1">>, username => <<"mqttuser0001">>, zone => external}, - NoMqttUser = #{clientid => <<"mqttuser2">>, username => <<"mqttuser0007">>, zone => external}, - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/1">>), - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/+">>), - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/#">>), - - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/1">>), - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/+">>), - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/#">>), - - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/1">>), - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/+">>), - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/#">>), - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/1">>), - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/+">>), - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/#">>), - - deny = emqx_access_control:check_acl(NoMqttUser, publish, <<"mqttuser0001/req/mqttuser0001/+">>), - deny = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/req/mqttuser0002/+">>), - deny = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/req/+/mqttuser0002">>), - ok. - -%%-------------------------------------------------------------------- -%% Helpers -%%-------------------------------------------------------------------- - -set_special_configs(_, emqx) -> - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, acl_nomatch, deny), - AclFilePath = filename:join(["test", "emqx_SUITE_data", "acl.conf"]), - application:set_env(emqx, acl_file, - emqx_ct_helpers:deps_path(emqx, AclFilePath)), - LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)); - -set_special_configs(Ssl, emqx_auth_ldap) -> - case Ssl == ssl of - true -> - LdapOpts = application:get_env(emqx_auth_ldap, ldap, []), - Path = emqx_ct_helpers:deps_path(emqx_auth_ldap, "test/certs/"), - SslOpts = [{verify, verify_peer}, - {fail_if_no_peer_cert, true}, - {server_name_indication, disable}, - {keyfile, Path ++ "/client-key.pem"}, - {certfile, Path ++ "/client-cert.pem"}, - {cacertfile, Path ++ "/cacert.pem"}], - LdapOpts1 = lists:keystore(ssl, 1, LdapOpts, {ssl, true}), - LdapOpts2 = lists:keystore(sslopts, 1, LdapOpts1, {sslopts, SslOpts}), - LdapOpts3 = lists:keystore(port, 1, LdapOpts2, {port, 636}), - application:set_env(emqx_auth_ldap, ldap, LdapOpts3); - _ -> - ok - end, - application:set_env(emqx_auth_ldap, device_dn, "ou=testdevice, dc=emqx, dc=io"). - diff --git a/apps/emqx_auth_ldap/test/emqx_auth_ldap_bind_as_user_SUITE.erl b/apps/emqx_auth_ldap/test/emqx_auth_ldap_bind_as_user_SUITE.erl deleted file mode 100644 index 24c03fdaf..000000000 --- a/apps/emqx_auth_ldap/test/emqx_auth_ldap_bind_as_user_SUITE.erl +++ /dev/null @@ -1,112 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_ldap_bind_as_user_SUITE). - --compile(nowarn_export_all). --compile(export_all). - --include_lib("emqx/include/emqx.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --define(PID, emqx_auth_ldap). - --define(APP, emqx_auth_ldap). - --define(DeviceDN, "ou=test_device,dc=emqx,dc=io"). - --define(AuthDN, "ou=test_auth,dc=emqx,dc=io"). - -all() -> - [check_auth, - check_acl]. - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_auth_ldap], fun set_special_configs/1), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_auth_ldap]). - -check_auth(_) -> - MqttUser1 = #{clientid => <<"mqttuser1">>, - username => <<"user1">>, - password => <<"mqttuser0001">>, - zone => external}, - MqttUser2 = #{clientid => <<"mqttuser2">>, - username => <<"user2">>, - password => <<"mqttuser0002">>, - zone => external}, - NonExistUser1 = #{clientid => <<"mqttuser3">>, - username => <<"user3">>, - password => <<"mqttuser0003">>, - zone => external}, - ct:log("MqttUser: ~p", [emqx_access_control:authenticate(MqttUser1)]), - ?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser1)), - ?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser2)), - ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(NonExistUser1)). - -check_acl(_) -> - MqttUser = #{clientid => <<"mqttuser1">>, username => <<"user1">>, zone => external}, - NoMqttUser = #{clientid => <<"mqttuser2">>, username => <<"user7">>, zone => external}, - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/1">>), - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/+">>), - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/#">>), - - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/1">>), - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/+">>), - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/#">>), - - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/1">>), - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/+">>), - allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/#">>), - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/1">>), - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/+">>), - allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/#">>), - - deny = emqx_access_control:check_acl(NoMqttUser, publish, <<"mqttuser0001/req/mqttuser0001/+">>), - deny = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/req/mqttuser0002/+">>), - deny = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/req/+/mqttuser0002">>), - ok. - -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, acl_nomatch, deny), - AclFilePath = filename:join(["test", "emqx_SUITE_data", "acl.conf"]), - application:set_env(emqx, acl_file, - emqx_ct_helpers:deps_path(emqx, AclFilePath)), - LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)); - -set_special_configs(emqx_auth_ldap) -> - application:set_env(emqx_auth_ldap, bind_as_user, true), - application:set_env(emqx_auth_ldap, device_dn, "ou=testdevice, dc=emqx, dc=io"), - application:set_env(emqx_auth_ldap, custom_base_dn, "${device_dn}"), - %% auth.ldap.filters.1.key = mqttAccountName - %% auth.ldap.filters.1.value = ${user} - %% auth.ldap.filters.1.op = and - %% auth.ldap.filters.2.key = objectClass - %% auth.ldap.filters.1.value = mqttUser - application:set_env(emqx_auth_ldap, filters, [{"mqttAccountName", "${user}"}, - "and", - {"objectClass", "mqttUser"}]); - -set_special_configs(_App) -> - ok. - diff --git a/apps/emqx_auth_mnesia/.gitignore b/apps/emqx_auth_mnesia/.gitignore deleted file mode 100644 index a4d9fea0a..000000000 --- a/apps/emqx_auth_mnesia/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -.eunit -deps -*.o -*.beam -*.plt -erl_crash.dump -ebin -rel/example_project -.concrete/DEV_MODE -.rebar -.erlang.mk/ -emqx_auth_mnesia.d -data/ -_build/ -.DS_Store -cover/ -ct.coverdata -eunit.coverdata -logs/ -test/ct.cover.spec -rebar.lock -rebar3.crashdump -erlang.mk -.*.swp -.rebar3/ -etc/emqx_auth_mnesia.conf.rendered diff --git a/apps/emqx_auth_mnesia/README.md b/apps/emqx_auth_mnesia/README.md deleted file mode 100644 index 8b4c145a8..000000000 --- a/apps/emqx_auth_mnesia/README.md +++ /dev/null @@ -1,2 +0,0 @@ -emqx_auth_mnesia -=============== diff --git a/apps/emqx_auth_mnesia/etc/emqx_auth_mnesia.conf b/apps/emqx_auth_mnesia/etc/emqx_auth_mnesia.conf deleted file mode 100644 index 758df1a9c..000000000 --- a/apps/emqx_auth_mnesia/etc/emqx_auth_mnesia.conf +++ /dev/null @@ -1,30 +0,0 @@ -## Password hash. -## -## Value: plain | md5 | sha | sha256 | sha512 -auth.mnesia.password_hash = sha256 - -##-------------------------------------------------------------------- -## ClientId Authentication -##-------------------------------------------------------------------- - -## Examples -##auth.client.1.clientid = id -##auth.client.1.password = passwd -##auth.client.2.clientid = "dev:devid" -##auth.client.2.password = passwd2 -##auth.client.3.clientid = "app:appid" -##auth.client.3.password = passwd3 -##auth.client.4.clientid = "client~!@#$%^&*()_+" -##auth.client.4.password = "passwd~!@#$%^&*()_+" - -##-------------------------------------------------------------------- -## Username Authentication -##-------------------------------------------------------------------- - -## Examples: -##auth.user.1.username = admin -##auth.user.1.password = public -##auth.user.2.username = feng@emqtt.io -##auth.user.2.password = public -##auth.user.3.username = "name~!@#$%^&*()_+" -##auth.user.3.password = "pwsswd~!@#$%^&*()_+" diff --git a/apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl b/apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl deleted file mode 100644 index 034bd4f30..000000000 --- a/apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl +++ /dev/null @@ -1,38 +0,0 @@ --define(APP, emqx_auth_mnesia). - --type(login():: {clientid, binary()} - | {username, binary()}). - --record(emqx_user, { - login :: login(), - password :: binary(), - created_at :: integer() - }). - --record(emqx_acl, { - filter:: {login() | all, emqx_topic:topic()}, - action :: pub | sub | pubsub, - access :: allow | deny, - created_at :: integer() - }). - --record(auth_metrics, { - success = 'client.auth.success', - failure = 'client.auth.failure', - ignore = 'client.auth.ignore' - }). - --record(acl_metrics, { - allow = 'client.acl.allow', - deny = 'client.acl.deny', - ignore = 'client.acl.ignore' - }). - --define(METRICS(Type), tl(tuple_to_list(#Type{}))). --define(METRICS(Type, K), #Type{}#Type.K). - --define(AUTH_METRICS, ?METRICS(auth_metrics)). --define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). - --define(ACL_METRICS, ?METRICS(acl_metrics)). --define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). diff --git a/apps/emqx_auth_mnesia/priv/emqx_auth_mnesia.schema b/apps/emqx_auth_mnesia/priv/emqx_auth_mnesia.schema deleted file mode 100644 index 87d6bf47f..000000000 --- a/apps/emqx_auth_mnesia/priv/emqx_auth_mnesia.schema +++ /dev/null @@ -1,43 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_auth_mnesia config mapping - -{mapping, "auth.mnesia.password_hash", "emqx_auth_mnesia.password_hash", [ - {default, sha256}, - {datatype, {enum, [plain, md5, sha, sha256, sha512]}} -]}. - -{mapping, "auth.client.$id.clientid", "emqx_auth_mnesia.clientid_list", [ - {datatype, string} -]}. - -{mapping, "auth.client.$id.password", "emqx_auth_mnesia.clientid_list", [ - {datatype, string} -]}. - -{translation, "emqx_auth_mnesia.clientid_list", fun(Conf) -> - ClientList = cuttlefish_variable:filter_by_prefix("auth.client", Conf), - lists:foldl( - fun({["auth", "client", Id, "clientid"], ClientId}, AccIn) -> - [{ClientId, cuttlefish:conf_get("auth.client." ++ Id ++ ".password", Conf)} | AccIn]; - (_, AccIn) -> - AccIn - end, [], ClientList) -end}. - -{mapping, "auth.user.$id.username", "emqx_auth_mnesia.username_list", [ - {datatype, string} -]}. - -{mapping, "auth.user.$id.password", "emqx_auth_mnesia.username_list", [ - {datatype, string} -]}. - -{translation, "emqx_auth_mnesia.username_list", fun(Conf) -> - Userlist = cuttlefish_variable:filter_by_prefix("auth.user", Conf), - lists:foldl( - fun({["auth", "user", Id, "username"], Username}, AccIn) -> - [{Username, cuttlefish:conf_get("auth.user." ++ Id ++ ".password", Conf)} | AccIn]; - (_, AccIn) -> - AccIn - end, [], Userlist) -end}. diff --git a/apps/emqx_auth_mnesia/rebar.config b/apps/emqx_auth_mnesia/rebar.config deleted file mode 100644 index 4c695ec69..000000000 --- a/apps/emqx_auth_mnesia/rebar.config +++ /dev/null @@ -1,17 +0,0 @@ -{deps, - [ ]}. - -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - {parse_transform}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. - diff --git a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia.erl b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia.erl deleted file mode 100644 index c21955182..000000000 --- a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia.erl +++ /dev/null @@ -1,104 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_acl_mnesia). - --include("emqx_auth_mnesia.hrl"). - --include_lib("stdlib/include/ms_transform.hrl"). - --define(TABLE, emqx_acl). - -%% ACL Callbacks --export([ init/0 - , register_metrics/0 - , check_acl/5 - , description/0 - ]). - -init() -> - ok = ekka_mnesia:create_table(emqx_acl, [ - {type, bag}, - {disc_copies, [node()]}, - {attributes, record_info(fields, emqx_acl)}, - {storage_properties, [{ets, [{read_concurrency, true}]}]}]), - ok = ekka_mnesia:copy_table(emqx_acl, disc_copies). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). - -check_acl(ClientInfo = #{ clientid := Clientid }, PubSub, Topic, _NoMatchAction, _Params) -> - Username = maps:get(username, ClientInfo, undefined), - - Acls = case Username of - undefined -> - emqx_acl_mnesia_cli:lookup_acl({clientid, Clientid}) ++ - emqx_acl_mnesia_cli:lookup_acl(all); - _ -> - emqx_acl_mnesia_cli:lookup_acl({clientid, Clientid}) ++ - emqx_acl_mnesia_cli:lookup_acl({username, Username}) ++ - emqx_acl_mnesia_cli:lookup_acl(all) - end, - - case match(ClientInfo, PubSub, Topic, Acls) of - allow -> - emqx_metrics:inc(?ACL_METRICS(allow)), - {stop, allow}; - deny -> - emqx_metrics:inc(?ACL_METRICS(deny)), - {stop, deny}; - _ -> - emqx_metrics:inc(?ACL_METRICS(ignore)), - ok - end. - -description() -> "Acl with Mnesia". - -%%-------------------------------------------------------------------- -%% Internal functions -%%------------------------------------------------------------------- - -match(_ClientInfo, _PubSub, _Topic, []) -> - nomatch; -match(ClientInfo, PubSub, Topic, [ {_, ACLTopic, Action, Access, _} | Acls]) -> - case match_actions(PubSub, Action) andalso match_topic(ClientInfo, Topic, ACLTopic) of - true -> Access; - false -> match(ClientInfo, PubSub, Topic, Acls) - end. - -match_topic(ClientInfo, Topic, ACLTopic) when is_binary(Topic) -> - emqx_topic:match(Topic, feed_var(ClientInfo, ACLTopic)). - -match_actions(_, pubsub) -> true; -match_actions(subscribe, sub) -> true; -match_actions(publish, pub) -> true; -match_actions(_, _) -> false. - -feed_var(ClientInfo, Pattern) -> - feed_var(ClientInfo, emqx_topic:words(Pattern), []). -feed_var(_ClientInfo, [], Acc) -> - emqx_topic:join(lists:reverse(Acc)); -feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) -> - feed_var(ClientInfo, Words, [<<"%c">>|Acc]); -feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) -> - feed_var(ClientInfo, Words, [ClientId |Acc]); -feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) -> - feed_var(ClientInfo, Words, [<<"%u">>|Acc]); -feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) -> - feed_var(ClientInfo, Words, [Username|Acc]); -feed_var(ClientInfo, [W|Words], Acc) -> - feed_var(ClientInfo, Words, [W|Acc]). diff --git a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl deleted file mode 100644 index 63e8fedd1..000000000 --- a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl +++ /dev/null @@ -1,226 +0,0 @@ -%c%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_acl_mnesia_api). - --include("emqx_auth_mnesia.hrl"). - --include_lib("stdlib/include/ms_transform.hrl"). - --import(proplists, [ get_value/2 - , get_value/3 - ]). - --import(minirest, [return/1]). - --rest_api(#{name => list_clientid, - method => 'GET', - path => "/acl/clientid", - func => list_clientid, - descr => "List available mnesia in the cluster" - }). - --rest_api(#{name => list_username, - method => 'GET', - path => "/acl/username", - func => list_username, - descr => "List available mnesia in the cluster" - }). - --rest_api(#{name => list_all, - method => 'GET', - path => "/acl/$all", - func => list_all, - descr => "List available mnesia in the cluster" - }). - --rest_api(#{name => lookup_clientid, - method => 'GET', - path => "/acl/clientid/:bin:clientid", - func => lookup, - descr => "Lookup mnesia in the cluster" - }). - --rest_api(#{name => lookup_username, - method => 'GET', - path => "/acl/username/:bin:username", - func => lookup, - descr => "Lookup mnesia in the cluster" - }). - --rest_api(#{name => add, - method => 'POST', - path => "/acl", - func => add, - descr => "Add mnesia in the cluster" - }). - --rest_api(#{name => delete_clientid, - method => 'DELETE', - path => "/acl/clientid/:bin:clientid/topic/:bin:topic", - func => delete, - descr => "Delete mnesia in the cluster" - }). - --rest_api(#{name => delete_username, - method => 'DELETE', - path => "/acl/username/:bin:username/topic/:bin:topic", - func => delete, - descr => "Delete mnesia in the cluster" - }). - --rest_api(#{name => delete_all, - method => 'DELETE', - path => "/acl/$all/topic/:bin:topic", - func => delete, - descr => "Delete mnesia in the cluster" - }). - - --export([ list_clientid/2 - , list_username/2 - , list_all/2 - , lookup/2 - , add/2 - , delete/2 - ]). - -list_clientid(_Bindings, Params) -> - MatchSpec = ets:fun2ms( - fun({emqx_acl, {{clientid, Clientid}, Topic}, Action, Access, CreatedAt}) -> {{clientid,Clientid}, Topic, Action,Access, CreatedAt} end), - return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, MatchSpec, Params, fun emqx_acl_mnesia_cli:comparing/2, fun format/1)}). - -list_username(_Bindings, Params) -> - MatchSpec = ets:fun2ms( - fun({emqx_acl, {{username, Username}, Topic}, Action, Access, CreatedAt}) -> {{username, Username}, Topic, Action,Access, CreatedAt} end), - return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, MatchSpec, Params, fun emqx_acl_mnesia_cli:comparing/2, fun format/1)}). - -list_all(_Bindings, Params) -> - MatchSpec = ets:fun2ms( - fun({emqx_acl, {all, Topic}, Action, Access, CreatedAt}) -> {all, Topic, Action,Access, CreatedAt}end - ), - return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, MatchSpec, Params, fun emqx_acl_mnesia_cli:comparing/2, fun format/1)}). - - -lookup(#{clientid := Clientid}, _Params) -> - return({ok, format(emqx_acl_mnesia_cli:lookup_acl({clientid, urldecode(Clientid)}))}); -lookup(#{username := Username}, _Params) -> - return({ok, format(emqx_acl_mnesia_cli:lookup_acl({username, urldecode(Username)}))}). - -add(_Bindings, Params) -> - [ P | _] = Params, - case is_list(P) of - true -> return(do_add(Params, [])); - false -> - Re = do_add(Params), - case Re of - #{result := ok} -> return({ok, Re}); - #{result := <<"ok">>} -> return({ok, Re}); - _ -> return({error, {add, Re}}) - end - end. - -do_add([ Params | ParamsN ], ReList) -> - do_add(ParamsN, [do_add(Params) | ReList]); - -do_add([], ReList) -> - {ok, ReList}. - -do_add(Params) -> - Clientid = get_value(<<"clientid">>, Params, undefined), - Username = get_value(<<"username">>, Params, undefined), - Login = case {Clientid, Username} of - {undefined, undefined} -> all; - {_, undefined} -> {clientid, urldecode(Clientid)}; - {undefined, _} -> {username, urldecode(Username)} - end, - Topic = urldecode(get_value(<<"topic">>, Params)), - Action = urldecode(get_value(<<"action">>, Params)), - Access = urldecode(get_value(<<"access">>, Params)), - Re = case validate([login, topic, action, access], [Login, Topic, Action, Access]) of - ok -> - emqx_acl_mnesia_cli:add_acl(Login, Topic, erlang:binary_to_atom(Action, utf8), erlang:binary_to_atom(Access, utf8)); - Err -> Err - end, - maps:merge(#{topic => Topic, - action => Action, - access => Access, - result => format_msg(Re) - }, case Login of - all -> #{all => '$all'}; - _ -> maps:from_list([Login]) - end). - -delete(#{clientid := Clientid, topic := Topic}, _) -> - return(emqx_acl_mnesia_cli:remove_acl({clientid, urldecode(Clientid)}, urldecode(Topic))); -delete(#{username := Username, topic := Topic}, _) -> - return(emqx_acl_mnesia_cli:remove_acl({username, urldecode(Username)}, urldecode(Topic))); -delete(#{topic := Topic}, _) -> - return(emqx_acl_mnesia_cli:remove_acl(all, urldecode(Topic))). - -%%------------------------------------------------------------------------------ -%% Interval Funcs -%%------------------------------------------------------------------------------ -format({{clientid, Clientid}, Topic, Action, Access, _CreatedAt}) -> - #{clientid => Clientid, topic => Topic, action => Action, access => Access}; -format({{username, Username}, Topic, Action, Access, _CreatedAt}) -> - #{username => Username, topic => Topic, action => Action, access => Access}; -format({all, Topic, Action, Access, _CreatedAt}) -> - #{all => '$all', topic => Topic, action => Action, access => Access}; -format(List) when is_list(List) -> - format(List, []). - -format([L | List], Relist) -> - format(List, [format(L) | Relist]); -format([], ReList) -> lists:reverse(ReList). - -validate([], []) -> - ok; -validate([K|Keys], [V|Values]) -> - case do_validation(K, V) of - false -> {error, K}; - true -> validate(Keys, Values) - end. -do_validation(login, all) -> - true; -do_validation(login, {clientid, V}) when is_binary(V) - andalso byte_size(V) > 0 -> - true; -do_validation(login, {username, V}) when is_binary(V) - andalso byte_size(V) > 0 -> - true; -do_validation(topic, V) when is_binary(V) - andalso byte_size(V) > 0 -> - true; -do_validation(action, V) when is_binary(V) -> - case V =:= <<"pub">> orelse V =:= <<"sub">> orelse V =:= <<"pubsub">> of - true -> true; - false -> false - end; -do_validation(access, V) when V =:= <<"allow">> orelse V =:= <<"deny">> -> - true; -do_validation(_, _) -> - false. - -format_msg(Message) - when is_atom(Message); - is_binary(Message) -> Message; - -format_msg(Message) when is_tuple(Message) -> - iolist_to_binary(io_lib:format("~p", [Message])). - -urldecode(S) -> - emqx_http_lib:uri_decode(S). diff --git a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl deleted file mode 100644 index 302a81637..000000000 --- a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl +++ /dev/null @@ -1,270 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_acl_mnesia_cli). - --include("emqx_auth_mnesia.hrl"). --include_lib("emqx/include/logger.hrl"). --include_lib("stdlib/include/ms_transform.hrl"). --define(TABLE, emqx_acl). - -%% Acl APIs --export([ add_acl/4 - , lookup_acl/1 - , all_acls/0 - , all_acls/1 - , remove_acl/2 - ]). - --export([cli/1]). --export([comparing/2]). -%%-------------------------------------------------------------------- -%% Acl API -%%-------------------------------------------------------------------- - -%% @doc Add Acls --spec(add_acl(login() | all, emqx_topic:topic(), pub | sub | pubsub, allow | deny) -> - ok | {error, any()}). -add_acl(Login, Topic, Action, Access) -> - Filter = {Login, Topic}, - Acl = #?TABLE{ - filter = Filter, - action = Action, - access = Access, - created_at = erlang:system_time(millisecond) - }, - ret(mnesia:transaction( - fun() -> - OldRecords = mnesia:wread({?TABLE, Filter}), - case Action of - pubsub -> - update_permission(pub, Acl, OldRecords), - update_permission(sub, Acl, OldRecords); - _ -> - update_permission(Action, Acl, OldRecords) - end - end)). - -%% @doc Lookup acl by login --spec(lookup_acl(login() | all) -> list()). -lookup_acl(undefined) -> []; -lookup_acl(Login) -> - MatchSpec = ets:fun2ms(fun({?TABLE, {Filter, ACLTopic}, Action, Access, CreatedAt}) - when Filter =:= Login -> - {Filter, ACLTopic, Action, Access, CreatedAt} - end), - lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)). - -%% @doc Remove acl --spec(remove_acl(login() | all, emqx_topic:topic()) -> ok | {error, any()}). -remove_acl(Login, Topic) -> - ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, {Login, Topic}}])). - -%% @doc All logins --spec(all_acls() -> list()). -all_acls() -> - all_acls(clientid) ++ - all_acls(username) ++ - all_acls(all). - -all_acls(clientid) -> - MatchSpec = ets:fun2ms( - fun({?TABLE, {{clientid, Clientid}, Topic}, Action, Access, CreatedAt}) -> - {{clientid, Clientid}, Topic, Action, Access, CreatedAt} - end), - lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)); -all_acls(username) -> - MatchSpec = ets:fun2ms( - fun({?TABLE, {{username, Username}, Topic}, Action, Access, CreatedAt}) -> - {{username, Username}, Topic, Action, Access, CreatedAt} - end), - lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)); -all_acls(all) -> - MatchSpec = ets:fun2ms( - fun({?TABLE, {all, Topic}, Action, Access, CreatedAt}) -> - {all, Topic, Action, Access, CreatedAt} - end - ), - lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)). - -%%-------------------------------------------------------------------- -%% ACL Cli -%%-------------------------------------------------------------------- - -cli(["list"]) -> - [print_acl(Acl) || Acl <- all_acls()]; - -cli(["list", "clientid"]) -> - [print_acl(Acl) || Acl <- all_acls(clientid)]; - -cli(["list", "username"]) -> - [print_acl(Acl) || Acl <- all_acls(username)]; - -cli(["list", "_all"]) -> - [print_acl(Acl) || Acl <- all_acls(all)]; - -cli(["add", "clientid", Clientid, Topic, Action, Access]) -> - case validate(action, Action) andalso validate(access, Access) of - true -> - case add_acl( - {clientid, iolist_to_binary(Clientid)}, - iolist_to_binary(Topic), - list_to_existing_atom(Action), - list_to_existing_atom(Access) - ) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - _ -> - emqx_ctl:print("Error: Input is illegal~n") - end; - -cli(["add", "username", Username, Topic, Action, Access]) -> - case validate(action, Action) andalso validate(access, Access) of - true -> - case add_acl( - {username, iolist_to_binary(Username)}, - iolist_to_binary(Topic), - list_to_existing_atom(Action), - list_to_existing_atom(Access) - ) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - _ -> - emqx_ctl:print("Error: Input is illegal~n") - end; - -cli(["add", "_all", Topic, Action, Access]) -> - case validate(action, Action) andalso validate(access, Access) of - true -> - case add_acl( - all, - iolist_to_binary(Topic), - list_to_existing_atom(Action), - list_to_existing_atom(Access) - ) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - _ -> - emqx_ctl:print("Error: Input is illegal~n") - end; - -cli(["show", "clientid", Clientid]) -> - [print_acl(Acl) || Acl <- lookup_acl({clientid, iolist_to_binary(Clientid)})]; - -cli(["show", "username", Username]) -> - [print_acl(Acl) || Acl <- lookup_acl({username, iolist_to_binary(Username)})]; - -cli(["del", "clientid", Clientid, Topic])-> - cli(["delete", "clientid", Clientid, Topic]); - -cli(["delete", "clientid", Clientid, Topic])-> - case remove_acl({clientid, iolist_to_binary(Clientid)}, iolist_to_binary(Topic)) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -cli(["del", "username", Username, Topic])-> - cli(["delete", "username", Username, Topic]); - -cli(["delete", "username", Username, Topic])-> - case remove_acl({username, iolist_to_binary(Username)}, iolist_to_binary(Topic)) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -cli(["del", "_all", Topic])-> - cli(["delete", "_all", Topic]); - -cli(["delete", "_all", Topic])-> - case remove_acl(all, iolist_to_binary(Topic)) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -cli(_) -> - emqx_ctl:usage([ {"acl list clientid", "List clientid acls"} - , {"acl list username", "List username acls"} - , {"acl list _all", "List $all acls"} - , {"acl show clientid ", "Lookup clientid acl detail"} - , {"acl show username ", "Lookup username acl detail"} - , {"acl aad clientid ", "Add clientid acl"} - , {"acl add Username ", "Add username acl"} - , {"acl add _all ", "Add $all acl"} - , {"acl delete clientid ", "Delete clientid acl"} - , {"acl delete username ", "Delete username acl"} - , {"acl delete _all ", "Delete $all acl"} - ]). - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -comparing({_, _, _, _, CreatedAt1}, - {_, _, _, _, CreatedAt2}) -> - CreatedAt1 >= CreatedAt2. - -ret({atomic, ok}) -> ok; -ret({aborted, Error}) -> {error, Error}. - -validate(action, "pub") -> true; -validate(action, "sub") -> true; -validate(action, "pubsub") -> true; -validate(access, "allow") -> true; -validate(access, "deny") -> true; -validate(_, _) -> false. - -print_acl({{clientid, Clientid}, Topic, Action, Access, _}) -> - emqx_ctl:print( - "Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n", - [Clientid, Topic, Action, Access] - ); -print_acl({{username, Username}, Topic, Action, Access, _}) -> - emqx_ctl:print( - "Acl(username = ~p topic = ~p action = ~p access = ~p)~n", - [Username, Topic, Action, Access] - ); -print_acl({all, Topic, Action, Access, _}) -> - emqx_ctl:print( - "Acl($all topic = ~p action = ~p access = ~p)~n", - [Topic, Action, Access] - ). - -update_permission(Action, Acl0, OldRecords) -> - Acl = Acl0 #?TABLE{action = Action}, - maybe_delete_shadowed_records(Action, OldRecords), - mnesia:write(Acl). - -maybe_delete_shadowed_records(_, []) -> - ok; -maybe_delete_shadowed_records(Action1, [Rec = #emqx_acl{action = Action2} | Rest]) -> - if Action1 =:= Action2 -> - ok = mnesia:delete_object(Rec); - Action2 =:= pubsub -> - %% Perform migration from the old data format on the - %% fly. This is needed only for the enterprise version, - %% delete this branch on 5.0 - mnesia:delete_object(Rec), - mnesia:write(Rec#?TABLE{action = other_action(Action1)}); - true -> - ok - end, - maybe_delete_shadowed_records(Action1, Rest). - -other_action(pub) -> sub; -other_action(sub) -> pub. diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src deleted file mode 100644 index 8ff574ab5..000000000 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_auth_mnesia, - [{description, "EMQ X Authentication with Mnesia"}, - {vsn, "4.3.1"}, % strict semver, bump manually - {modules, []}, - {registered, []}, - {applications, [kernel,stdlib,mnesia]}, - {mod, {emqx_auth_mnesia_app,[]}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-auth-mnesia"} - ]} - ]}. diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src deleted file mode 100644 index 5b26c962c..000000000 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src +++ /dev/null @@ -1,13 +0,0 @@ -%% -*-: erlang -*- -{VSN, - [ - {"4.3.0", [ - {restart_application, emqx_auth_mnesia} - ]} - ], - [ - {"4.3.0", [ - {restart_application, emqx_auth_mnesia} - ]} - ] -}. diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.erl b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.erl deleted file mode 100644 index 905bcaaf0..000000000 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.erl +++ /dev/null @@ -1,109 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mnesia). - --include("emqx_auth_mnesia.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). --include_lib("emqx/include/types.hrl"). - --include_lib("stdlib/include/ms_transform.hrl"). - --define(TABLE, emqx_user). -%% Auth callbacks --export([ init/1 - , register_metrics/0 - , check/3 - , description/0 - ]). - -init(#{clientid_list := ClientidList, username_list := UsernameList}) -> - ok = ekka_mnesia:create_table(?TABLE, [ - {disc_copies, [node()]}, - {attributes, record_info(fields, emqx_user)}, - {storage_properties, [{ets, [{read_concurrency, true}]}]}]), - _ = [ add_default_user({{clientid, iolist_to_binary(Clientid)}, iolist_to_binary(Password)}) - || {Clientid, Password} <- ClientidList], - _ = [ add_default_user({{username, iolist_to_binary(Username)}, iolist_to_binary(Password)}) - || {Username, Password} <- UsernameList], - ok = ekka_mnesia:copy_table(?TABLE, disc_copies). - -%% @private -add_default_user({Login, Password}) when is_tuple(Login) -> - emqx_auth_mnesia_cli:add_user(Login, Password). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). - -check(ClientInfo = #{ clientid := Clientid - , password := NPassword - }, AuthResult, #{hash_type := HashType}) -> - Username = maps:get(username, ClientInfo, undefined), - MatchSpec = ets:fun2ms(fun({?TABLE, {clientid, X}, Password, InterTime}) when X =:= Clientid-> Password; - ({?TABLE, {username, X}, Password, InterTime}) when X =:= Username andalso X =/= undefined -> Password - end), - case ets:select(?TABLE, MatchSpec) of - [] -> - emqx_metrics:inc(?AUTH_METRICS(ignore)), - ok; - List -> - case match_password(NPassword, HashType, List) of - false -> - ?LOG(error, "[Mnesia] Auth from mnesia failed: ~p", [ClientInfo]), - emqx_metrics:inc(?AUTH_METRICS(failure)), - {stop, AuthResult#{anonymous => false, auth_result => password_error}}; - _ -> - emqx_metrics:inc(?AUTH_METRICS(success)), - {stop, AuthResult#{anonymous => false, auth_result => success}} - end - end. - -description() -> "Authentication with Mnesia". - -match_password(Password, HashType, HashList) -> - lists:any( - fun(Secret) -> - case is_salt_hash(Secret, HashType) of - true -> - <> = Secret, - Hash =:= hash(Password, Salt, HashType); - _ -> - Secret =:= hash(Password, HashType) - end - end, HashList). - -hash(undefined, HashType) -> - hash(<<>>, HashType); -hash(Password, HashType) -> - emqx_passwd:hash(HashType, Password). - -hash(undefined, SaltBin, HashType) -> - hash(<<>>, SaltBin, HashType); -hash(Password, SaltBin, HashType) -> - emqx_passwd:hash(HashType, <>). - -is_salt_hash(_, plain) -> - true; -is_salt_hash(Secret, HashType) -> - not (byte_size(Secret) == len(HashType)). - -len(md5) -> 32; -len(sha) -> 40; -len(sha256) -> 64; -len(sha512) -> 128. diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl deleted file mode 100644 index 07ff3bdf5..000000000 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl +++ /dev/null @@ -1,305 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mnesia_api). - --include_lib("stdlib/include/qlc.hrl"). --include_lib("stdlib/include/ms_transform.hrl"). - --define(TABLE, emqx_user). - --import(proplists, [get_value/2]). --import(minirest, [return/1]). --export([paginate/5]). - --export([ list_clientid/2 - , lookup_clientid/2 - , add_clientid/2 - , update_clientid/2 - , delete_clientid/2 - ]). - --rest_api(#{name => list_clientid, - method => 'GET', - path => "/auth_clientid", - func => list_clientid, - descr => "List available clientid in the cluster" - }). - --rest_api(#{name => lookup_clientid, - method => 'GET', - path => "/auth_clientid/:bin:clientid", - func => lookup_clientid, - descr => "Lookup clientid in the cluster" - }). - --rest_api(#{name => add_clientid, - method => 'POST', - path => "/auth_clientid", - func => add_clientid, - descr => "Add clientid in the cluster" - }). - --rest_api(#{name => update_clientid, - method => 'PUT', - path => "/auth_clientid/:bin:clientid", - func => update_clientid, - descr => "Update clientid in the cluster" - }). - --rest_api(#{name => delete_clientid, - method => 'DELETE', - path => "/auth_clientid/:bin:clientid", - func => delete_clientid, - descr => "Delete clientid in the cluster" - }). - --export([ list_username/2 - , lookup_username/2 - , add_username/2 - , update_username/2 - , delete_username/2 - ]). - --rest_api(#{name => list_username, - method => 'GET', - path => "/auth_username", - func => list_username, - descr => "List available username in the cluster" - }). - --rest_api(#{name => lookup_username, - method => 'GET', - path => "/auth_username/:bin:username", - func => lookup_username, - descr => "Lookup username in the cluster" - }). - --rest_api(#{name => add_username, - method => 'POST', - path => "/auth_username", - func => add_username, - descr => "Add username in the cluster" - }). - --rest_api(#{name => update_username, - method => 'PUT', - path => "/auth_username/:bin:username", - func => update_username, - descr => "Update username in the cluster" - }). - --rest_api(#{name => delete_username, - method => 'DELETE', - path => "/auth_username/:bin:username", - func => delete_username, - descr => "Delete username in the cluster" - }). - -%%------------------------------------------------------------------------------ -%% Auth Clientid Api -%%------------------------------------------------------------------------------ - -list_clientid(_Bindings, Params) -> - MatchSpec = ets:fun2ms(fun({?TABLE, {clientid, Clientid}, Password, CreatedAt}) -> {?TABLE, {clientid, Clientid}, Password, CreatedAt} end), - return({ok, paginate(?TABLE, MatchSpec, Params, fun emqx_auth_mnesia_cli:comparing/2, fun({?TABLE, {clientid, X}, _, _}) -> #{clientid => X} end)}). - -lookup_clientid(#{clientid := Clientid}, _Params) -> - return({ok, format(emqx_auth_mnesia_cli:lookup_user({clientid, urldecode(Clientid)}))}). - -add_clientid(_Bindings, Params) -> - [ P | _] = Params, - case is_list(P) of - true -> return(do_add_clientid(Params, [])); - false -> - Re = do_add_clientid(Params), - case Re of - ok -> return(ok); - {error, Error} -> return({error, format_msg(Error)}) - end - end. - -do_add_clientid([ Params | ParamsN ], ReList ) -> - Clientid = urldecode(get_value(<<"clientid">>, Params)), - do_add_clientid(ParamsN, [{Clientid, format_msg(do_add_clientid(Params))} | ReList]); - -do_add_clientid([], ReList) -> - {ok, ReList}. - -do_add_clientid(Params) -> - Clientid = urldecode(get_value(<<"clientid">>, Params)), - Password = urldecode(get_value(<<"password">>, Params)), - Login = {clientid, Clientid}, - case validate([login, password], [Login, Password]) of - ok -> - emqx_auth_mnesia_cli:add_user(Login, Password); - Err -> Err - end. - -update_clientid(#{clientid := Clientid}, Params) -> - Password = get_value(<<"password">>, Params), - case validate([password], [Password]) of - ok -> return(emqx_auth_mnesia_cli:update_user({clientid, urldecode(Clientid)}, urldecode(Password))); - Err -> return(Err) - end. - -delete_clientid(#{clientid := Clientid}, _) -> - return(emqx_auth_mnesia_cli:remove_user({clientid, urldecode(Clientid)})). - -%%------------------------------------------------------------------------------ -%% Auth Username Api -%%------------------------------------------------------------------------------ - -list_username(_Bindings, Params) -> - MatchSpec = ets:fun2ms(fun({?TABLE, {username, Username}, Password, CreatedAt}) -> {?TABLE, {username, Username}, Password, CreatedAt} end), - return({ok, paginate(?TABLE, MatchSpec, Params, fun emqx_auth_mnesia_cli:comparing/2, fun({?TABLE, {username, X}, _, _}) -> #{username => X} end)}). - -lookup_username(#{username := Username}, _Params) -> - return({ok, format(emqx_auth_mnesia_cli:lookup_user({username, urldecode(Username)}))}). - -add_username(_Bindings, Params) -> - [ P | _] = Params, - case is_list(P) of - true -> return(do_add_username(Params, [])); - false -> - case do_add_username(Params) of - ok -> return(ok); - {error, Error} -> return({error, format_msg(Error)}) - end - end. - -do_add_username([ Params | ParamsN ], ReList ) -> - Username = urldecode(get_value(<<"username">>, Params)), - do_add_username(ParamsN, [{Username, format_msg(do_add_username(Params))} | ReList]); - -do_add_username([], ReList) -> - {ok, ReList}. - -do_add_username(Params) -> - Username = urldecode(get_value(<<"username">>, Params)), - Password = urldecode(get_value(<<"password">>, Params)), - Login = {username, Username}, - case validate([login, password], [Login, Password]) of - ok -> - emqx_auth_mnesia_cli:add_user(Login, Password); - Err -> Err - end. - -update_username(#{username := Username}, Params) -> - Password = get_value(<<"password">>, Params), - case validate([password], [Password]) of - ok -> return(emqx_auth_mnesia_cli:update_user({username, urldecode(Username)}, urldecode(Password))); - Err -> return(Err) - end. - -delete_username(#{username := Username}, _) -> - return(emqx_auth_mnesia_cli:remove_user({username, urldecode(Username)})). - -%%------------------------------------------------------------------------------ -%% Paging Query -%%------------------------------------------------------------------------------ - -paginate(Tables, MatchSpec, Params, ComparingFun, RowFun) -> - Qh = query_handle(Tables, MatchSpec), - Count = count(Tables, MatchSpec), - Page = page(Params), - Limit = limit(Params), - Cursor = qlc:cursor(Qh), - case Page > 1 of - true -> - _ = qlc:next_answers(Cursor, (Page - 1) * Limit), - ok; - false -> ok - end, - Rows = qlc:next_answers(Cursor, Limit), - qlc:delete_cursor(Cursor), - #{meta => #{page => Page, limit => Limit, count => Count}, - data => [RowFun(Row) || Row <- lists:sort(ComparingFun, Rows)]}. - -query_handle(Table, MatchSpec) when is_atom(Table) -> - Options = {traverse, {select, MatchSpec}}, - qlc:q([R|| R <- ets:table(Table, Options)]); -query_handle([Table], MatchSpec) when is_atom(Table) -> - Options = {traverse, {select, MatchSpec}}, - qlc:q([R|| R <- ets:table(Table, Options)]); -query_handle(Tables, MatchSpec) -> - Options = {traverse, {select, MatchSpec}}, - qlc:append([qlc:q([E || E <- ets:table(T, Options)]) || T <- Tables]). - -count(Table, MatchSpec) when is_atom(Table) -> - [{MatchPattern, Where, _Re}] = MatchSpec, - NMatchSpec = [{MatchPattern, Where, [true]}], - ets:select_count(Table, NMatchSpec); -count([Table], MatchSpec) when is_atom(Table) -> - [{MatchPattern, Where, _Re}] = MatchSpec, - NMatchSpec = [{MatchPattern, Where, [true]}], - ets:select_count(Table, NMatchSpec); -count(Tables, MatchSpec) -> - lists:sum([count(T, MatchSpec) || T <- Tables]). - -page(Params) -> - binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)). - -limit(Params) -> - case proplists:get_value(<<"_limit">>, Params) of - undefined -> 10; - Size -> binary_to_integer(Size) - end. - -%%------------------------------------------------------------------------------ -%% Interval Funcs -%%------------------------------------------------------------------------------ - -format([{?TABLE, {clientid, ClientId}, Password, _InterTime}]) -> - #{clientid => ClientId, - password => Password}; - -format([{?TABLE, {username, Username}, Password, _InterTime}]) -> - #{username => Username, - password => Password}; - -format([]) -> - #{}. - -validate([], []) -> - ok; -validate([K|Keys], [V|Values]) -> - case do_validation(K, V) of - false -> {error, K}; - true -> validate(Keys, Values) - end. - -do_validation(login, {clientid, V}) when is_binary(V) - andalso byte_size(V) > 0 -> - true; -do_validation(login, {username, V}) when is_binary(V) - andalso byte_size(V) > 0 -> - true; -do_validation(password, V) when is_binary(V) - andalso byte_size(V) > 0 -> - true; -do_validation(_, _) -> - false. - -format_msg(Message) - when is_atom(Message); - is_binary(Message) -> Message; - -format_msg(Message) when is_tuple(Message) -> - iolist_to_binary(io_lib:format("~p", [Message])). - -urldecode(S) -> - emqx_http_lib:uri_decode(S). diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_app.erl b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_app.erl deleted file mode 100644 index 91f4bdf4f..000000000 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_app.erl +++ /dev/null @@ -1,68 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mnesia_app). - --behaviour(application). - --emqx_plugin(auth). - --include("emqx_auth_mnesia.hrl"). - -%% Application callbacks --export([ start/2 - , prep_stop/1 - , stop/1 - ]). - -%%-------------------------------------------------------------------- -%% Application callbacks -%%-------------------------------------------------------------------- - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_auth_mnesia_sup:start_link(), - emqx_ctl:register_command(clientid, {emqx_auth_mnesia_cli, auth_clientid_cli}, []), - emqx_ctl:register_command(user, {emqx_auth_mnesia_cli, auth_username_cli}, []), - emqx_ctl:register_command(acl, {emqx_acl_mnesia_cli, cli}, []), - _ = load_auth_hook(), - _ = load_acl_hook(), - {ok, Sup}. - -prep_stop(State) -> - emqx:unhook('client.authenticate', {emqx_auth_mnesia, check}), - emqx:unhook('client.check_acl', {emqx_acl_mnesia, check_acl}), - emqx_ctl:unregister_command(clientid), - emqx_ctl:unregister_command(user), - emqx_ctl:unregister_command(acl), - State. - -stop(_State) -> - ok. - -load_auth_hook() -> - ClientidList = application:get_env(?APP, clientid_list, []), - UsernameList = application:get_env(?APP, username_list, []), - ok = emqx_auth_mnesia:init(#{clientid_list => ClientidList, username_list => UsernameList}), - ok = emqx_auth_mnesia:register_metrics(), - Params = #{ - hash_type => application:get_env(emqx_auth_mnesia, password_hash, sha256) - }, - emqx:hook('client.authenticate', {emqx_auth_mnesia, check, [Params]}). - -load_acl_hook() -> - ok = emqx_acl_mnesia:init(), - ok = emqx_acl_mnesia:register_metrics(), - emqx:hook('client.check_acl', {emqx_acl_mnesia, check_acl, [#{}]}). diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_cli.erl b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_cli.erl deleted file mode 100644 index d89e6836c..000000000 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_cli.erl +++ /dev/null @@ -1,194 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mnesia_cli). - --include("emqx_auth_mnesia.hrl"). --include_lib("emqx/include/logger.hrl"). --include_lib("stdlib/include/ms_transform.hrl"). --define(TABLE, emqx_user). -%% Auth APIs --export([ add_user/2 - , update_user/2 - , remove_user/1 - , lookup_user/1 - , all_users/0 - , all_users/1 - ]). -%% Cli --export([ auth_clientid_cli/1 - , auth_username_cli/1 - ]). - -%% Helper --export([comparing/2]). - -%%-------------------------------------------------------------------- -%% Auth APIs -%%-------------------------------------------------------------------- - -%% @doc Add User --spec(add_user(tuple(), binary()) -> ok | {error, any()}). -add_user(Login, Password) -> - User = #emqx_user{ - login = Login, - password = encrypted_data(Password), - created_at = erlang:system_time(millisecond) - }, - ret(mnesia:transaction(fun insert_user/1, [User])). - -insert_user(User = #emqx_user{login = Login}) -> - case mnesia:read(?TABLE, Login) of - [] -> mnesia:write(User); - [_|_] -> mnesia:abort(existed) - end. - -%% @doc Update User --spec(update_user(tuple(), binary()) -> ok | {error, any()}). -update_user(Login, NewPassword) -> - ret(mnesia:transaction(fun do_update_user/2, [Login, encrypted_data(NewPassword)])). - -do_update_user(Login, NewPassword) -> - case mnesia:read(?TABLE, Login) of - [#emqx_user{} = User] -> - mnesia:write(User#emqx_user{password = NewPassword}); - [] -> mnesia:abort(noexisted) - end. - -%% @doc Lookup user by login --spec(lookup_user(tuple()) -> list()). -lookup_user(undefined) -> []; -lookup_user(Login) -> - Re = mnesia:dirty_read(?TABLE, Login), - lists:sort(fun comparing/2, Re). - -%% @doc Remove user --spec(remove_user(tuple()) -> ok | {error, any()}). -remove_user(Login) -> - ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, Login}])). - -%% @doc All logins --spec(all_users() -> list()). -all_users() -> mnesia:dirty_all_keys(?TABLE). - -all_users(clientid) -> - MatchSpec = ets:fun2ms( - fun({?TABLE, {clientid, Clientid}, Password, CreatedAt}) -> - {?TABLE, {clientid, Clientid}, Password, CreatedAt} - end), - lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)); -all_users(username) -> - MatchSpec = ets:fun2ms( - fun({?TABLE, {username, Username}, Password, CreatedAt}) -> - {?TABLE, {username, Username}, Password, CreatedAt} - end), - lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)). - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -comparing({?TABLE, _, _, CreatedAt1}, - {?TABLE, _, _, CreatedAt2}) -> - CreatedAt1 >= CreatedAt2. - -ret({atomic, ok}) -> ok; -ret({aborted, Error}) -> {error, Error}. - -encrypted_data(Password) -> - HashType = application:get_env(emqx_auth_mnesia, password_hash, sha256), - SaltBin = salt(), - <>. - -hash(undefined, SaltBin, HashType) -> - hash(<<>>, SaltBin, HashType); -hash(Password, SaltBin, HashType) -> - emqx_passwd:hash(HashType, <>). - -salt() -> - {_AlgHandler, _AlgState} = rand:seed(exsplus, erlang:timestamp()), - Salt = rand:uniform(16#ffffffff), <>. - -%%-------------------------------------------------------------------- -%% Auth Clientid Cli -%%-------------------------------------------------------------------- - -auth_clientid_cli(["list"]) -> - [emqx_ctl:print("~s~n", [ClientId]) - || {?TABLE, {clientid, ClientId}, _Password, _CreatedAt} <- all_users(clientid) - ]; - -auth_clientid_cli(["add", ClientId, Password]) -> - case add_user({clientid, iolist_to_binary(ClientId)}, iolist_to_binary(Password)) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -auth_clientid_cli(["update", ClientId, NewPassword]) -> - case update_user({clientid, iolist_to_binary(ClientId)}, iolist_to_binary(NewPassword)) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -auth_clientid_cli(["del", ClientId]) -> - auth_clientid_cli(["delete", ClientId]); - -auth_clientid_cli(["delete", ClientId]) -> - case remove_user({clientid, iolist_to_binary(ClientId)}) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -auth_clientid_cli(_) -> - emqx_ctl:usage([{"clientid list", "List clientid auth rules"}, - {"clientid add ", "Add clientid auth rule"}, - {"clientid update ", "Update clientid auth rule"}, - {"clientid delete ", "Delete clientid auth rule"}]). - -%%-------------------------------------------------------------------- -%% Auth Username Cli -%%-------------------------------------------------------------------- - -auth_username_cli(["list"]) -> - [emqx_ctl:print("~s~n", [Username]) - || {?TABLE, {username, Username}, _Password, _CreatedAt} <- all_users(username) - ]; - -auth_username_cli(["add", Username, Password]) -> - case add_user({username, iolist_to_binary(Username)}, iolist_to_binary(Password)) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -auth_username_cli(["update", Username, NewPassword]) -> - case update_user({username, iolist_to_binary(Username)}, iolist_to_binary(NewPassword)) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; -auth_username_cli(["del", Username]) -> - auth_username_cli(["delete", Username]); - -auth_username_cli(["delete", Username]) -> - case remove_user({username, iolist_to_binary(Username)}) of - ok -> emqx_ctl:print("ok~n"); - {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -auth_username_cli(_) -> - emqx_ctl:usage([{"user list", "List username auth rules"}, - {"user add ", "Add username auth rule"}, - {"user update ", "Update username auth rule"}, - {"user delete ", "Delete username auth rule"}]). diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_sup.erl b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_sup.erl deleted file mode 100644 index 3784eaaf6..000000000 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_sup.erl +++ /dev/null @@ -1,36 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mnesia_sup). - --behaviour(supervisor). - --include("emqx_auth_mnesia.hrl"). - --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%%-------------------------------------------------------------------- -%% Supervisor callbacks -%%-------------------------------------------------------------------- - -init([]) -> - {ok, {{one_for_one, 10, 100}, []}}. \ No newline at end of file diff --git a/apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl b/apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl deleted file mode 100644 index f7994387b..000000000 --- a/apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl +++ /dev/null @@ -1,293 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_acl_mnesia_SUITE). - --compile(nowarn_export_all). --compile(export_all). - --include("emqx_auth_mnesia.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --import(emqx_ct_http, [ request_api/3 - , request_api/5 - , get_http_data/1 - , create_default_app/0 - , delete_default_app/0 - , default_auth_header/0 - ]). - --define(HOST, "http://127.0.0.1:8081/"). --define(API_VERSION, "v4"). --define(BASE_PATH, "api"). - -all() -> - emqx_ct:all(?MODULE). - -groups() -> - []. - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_modules, emqx_management, emqx_auth_mnesia], fun set_special_configs/1), - create_default_app(), - Config. - -end_per_suite(_Config) -> - delete_default_app(), - emqx_ct_helpers:stop_apps([emqx_modules, emqx_management, emqx_auth_mnesia]). - -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, true), - application:set_env(emqx, enable_acl_cache, false), - LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)); - -set_special_configs(_App) -> - ok. - -%%------------------------------------------------------------------------------ -%% Testcases -%%------------------------------------------------------------------------------ - -t_management(_Config) -> - clean_all_acls(), - ?assertEqual("Acl with Mnesia", emqx_acl_mnesia:description()), - ?assertEqual([], emqx_acl_mnesia_cli:all_acls()), - - ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>, sub, allow), - ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/+">>, pub, deny), - ok = emqx_acl_mnesia_cli:add_acl({username, <<"test_username">>}, <<"topic/%u">>, sub, deny), - ok = emqx_acl_mnesia_cli:add_acl({username, <<"test_username">>}, <<"topic/+">>, pub, allow), - ok = emqx_acl_mnesia_cli:add_acl(all, <<"#">>, pubsub, deny), - %% Sleeps below are needed to hide the race condition between - %% mnesia and ets dirty select in check_acl, that make this test - %% flaky - timer:sleep(100), - - ?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({clientid, <<"test_clientid">>}))), - ?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({username, <<"test_username">>}))), - ?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl(all))), - ?assertEqual(6, length(emqx_acl_mnesia_cli:all_acls())), - - User1 = #{zone => external, clientid => <<"test_clientid">>}, - User2 = #{zone => external, clientid => <<"no_exist">>, username => <<"test_username">>}, - User3 = #{zone => external, clientid => <<"test_clientid">>, username => <<"test_username">>}, - allow = emqx_access_control:check_acl(User1, subscribe, <<"topic/test_clientid">>), - deny = emqx_access_control:check_acl(User1, publish, <<"topic/A">>), - deny = emqx_access_control:check_acl(User2, subscribe, <<"topic/test_username">>), - allow = emqx_access_control:check_acl(User2, publish, <<"topic/A">>), - allow = emqx_access_control:check_acl(User3, subscribe, <<"topic/test_clientid">>), - deny = emqx_access_control:check_acl(User3, subscribe, <<"topic/test_username">>), - deny = emqx_access_control:check_acl(User3, publish, <<"topic/A">>), - deny = emqx_access_control:check_acl(User3, subscribe, <<"topic/A/B">>), - deny = emqx_access_control:check_acl(User3, publish, <<"topic/A/B">>), - - %% Test merging of pubsub capability: - ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pubsub, deny), - timer:sleep(100), - deny = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>), - deny = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>), - ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pub, allow), - timer:sleep(100), - deny = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>), - allow = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>), - ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pubsub, allow), - timer:sleep(100), - allow = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>), - allow = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>), - ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, sub, deny), - timer:sleep(100), - deny = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>), - allow = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>), - ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pub, deny), - timer:sleep(100), - deny = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>), - deny = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>), - - %% Test implicit migration of pubsub to pub and sub: - ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>), - ok = mnesia:dirty_write(#emqx_acl{ - filter = {{clientid, <<"test_clientid">>}, <<"topic/mix">>}, - action = pubsub, - access = allow, - created_at = erlang:system_time(millisecond) - }), - timer:sleep(100), - allow = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>), - allow = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>), - ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pub, deny), - timer:sleep(100), - allow = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>), - deny = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>), - ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, sub, deny), - timer:sleep(100), - deny = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>), - deny = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>), - - ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>), - ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/+">>), - ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>), - ok = emqx_acl_mnesia_cli:remove_acl({username, <<"test_username">>}, <<"topic/%u">>), - ok = emqx_acl_mnesia_cli:remove_acl({username, <<"test_username">>}, <<"topic/+">>), - ok = emqx_acl_mnesia_cli:remove_acl(all, <<"#">>), - timer:sleep(100), - - ?assertEqual([], emqx_acl_mnesia_cli:all_acls()). - -t_acl_cli(_Config) -> - meck:new(emqx_ctl, [non_strict, passthrough]), - meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end), - meck:expect(emqx_ctl, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end), - meck:expect(emqx_ctl, usage, fun(Usages) -> emqx_ctl:format_usage(Usages) end), - meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end), - - clean_all_acls(), - - ?assertEqual(0, length(emqx_acl_mnesia_cli:cli(["list"]))), - - emqx_acl_mnesia_cli:cli(["add", "clientid", "test_clientid", "topic/A", "pub", "deny"]), - emqx_acl_mnesia_cli:cli(["add", "clientid", "test_clientid", "topic/A", "pub", "allow"]), - R1 = emqx_ctl:format("Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n", - [<<"test_clientid">>, <<"topic/A">>, pub, allow]), - ?assertEqual([R1], emqx_acl_mnesia_cli:cli(["show", "clientid", "test_clientid"])), - ?assertEqual([R1], emqx_acl_mnesia_cli:cli(["list", "clientid"])), - - emqx_acl_mnesia_cli:cli(["add", "username", "test_username", "topic/B", "sub", "deny"]), - R2 = emqx_ctl:format("Acl(username = ~p topic = ~p action = ~p access = ~p)~n", - [<<"test_username">>, <<"topic/B">>, sub, deny]), - ?assertEqual([R2], emqx_acl_mnesia_cli:cli(["show", "username", "test_username"])), - ?assertEqual([R2], emqx_acl_mnesia_cli:cli(["list", "username"])), - - emqx_acl_mnesia_cli:cli(["add", "_all", "#", "pub", "allow"]), - emqx_acl_mnesia_cli:cli(["add", "_all", "#", "pubsub", "deny"]), - ?assertMatch(["", - "Acl($all topic = <<\"#\">> action = pub access = deny)", - "Acl($all topic = <<\"#\">> action = sub access = deny)"], - lists:sort(string:split(emqx_acl_mnesia_cli:cli(["list", "_all"]), "\n", all)) - ), - ?assertEqual(4, length(emqx_acl_mnesia_cli:cli(["list"]))), - - emqx_acl_mnesia_cli:cli(["del", "clientid", "test_clientid", "topic/A"]), - emqx_acl_mnesia_cli:cli(["del", "username", "test_username", "topic/B"]), - emqx_acl_mnesia_cli:cli(["del", "_all", "#"]), - ?assertEqual(0, length(emqx_acl_mnesia_cli:cli(["list"]))), - - meck:unload(emqx_ctl). - -t_rest_api(_Config) -> - clean_all_acls(), - - Params1 = [#{<<"clientid">> => <<"test_clientid">>, - <<"topic">> => <<"topic/A">>, - <<"action">> => <<"pub">>, - <<"access">> => <<"allow">> - }, - #{<<"clientid">> => <<"test_clientid">>, - <<"topic">> => <<"topic/B">>, - <<"action">> => <<"sub">>, - <<"access">> => <<"allow">> - }, - #{<<"clientid">> => <<"test_clientid">>, - <<"topic">> => <<"topic/C">>, - <<"action">> => <<"pubsub">>, - <<"access">> => <<"deny">> - }], - {ok, _} = request_http_rest_add([], Params1), - {ok, Re1} = request_http_rest_list(["clientid", "test_clientid"]), - ?assertMatch(4, length(get_http_data(Re1))), - {ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/A"]), - {ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/B"]), - {ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/C"]), - {ok, Res1} = request_http_rest_list(["clientid"]), - ?assertMatch([], get_http_data(Res1)), - - Params2 = [#{<<"username">> => <<"test_username">>, - <<"topic">> => <<"topic/A">>, - <<"action">> => <<"pub">>, - <<"access">> => <<"allow">> - }, - #{<<"username">> => <<"test_username">>, - <<"topic">> => <<"topic/B">>, - <<"action">> => <<"sub">>, - <<"access">> => <<"allow">> - }, - #{<<"username">> => <<"test_username">>, - <<"topic">> => <<"topic/C">>, - <<"action">> => <<"pubsub">>, - <<"access">> => <<"deny">> - }], - {ok, _} = request_http_rest_add([], Params2), - {ok, Re2} = request_http_rest_list(["username", "test_username"]), - ?assertMatch(4, length(get_http_data(Re2))), - {ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/A"]), - {ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/B"]), - {ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/C"]), - {ok, Res2} = request_http_rest_list(["username"]), - ?assertMatch([], get_http_data(Res2)), - - Params3 = [#{<<"topic">> => <<"topic/A">>, - <<"action">> => <<"pub">>, - <<"access">> => <<"allow">> - }, - #{<<"topic">> => <<"topic/B">>, - <<"action">> => <<"sub">>, - <<"access">> => <<"allow">> - }, - #{<<"topic">> => <<"topic/C">>, - <<"action">> => <<"pubsub">>, - <<"access">> => <<"deny">> - }], - {ok, _} = request_http_rest_add([], Params3), - {ok, Re3} = request_http_rest_list(["$all"]), - ?assertMatch(4, length(get_http_data(Re3))), - {ok, _} = request_http_rest_delete(["$all", "topic", "topic/A"]), - {ok, _} = request_http_rest_delete(["$all", "topic", "topic/B"]), - {ok, _} = request_http_rest_delete(["$all", "topic", "topic/C"]), - {ok, Res3} = request_http_rest_list(["$all"]), - ?assertMatch([], get_http_data(Res3)). - -%%------------------------------------------------------------------------------ -%% Helpers -%%------------------------------------------------------------------------------ - -clean_all_acls() -> - [ mnesia:dirty_delete({emqx_acl, Login}) - || Login <- mnesia:dirty_all_keys(emqx_acl)]. - -%%-------------------------------------------------------------------- -%% HTTP Request -%%-------------------------------------------------------------------- - -request_http_rest_list(Path) -> - request_api(get, uri(Path), default_auth_header()). - -request_http_rest_lookup(Path) -> - request_api(get, uri(Path), default_auth_header()). - -request_http_rest_add(Path, Params) -> - request_api(post, uri(Path), [], default_auth_header(), Params). - -request_http_rest_delete(Path) -> - request_api(delete, uri(Path), default_auth_header()). - -uri() -> uri([]). -uri(Parts) when is_list(Parts) -> - NParts = [b2l(E) || E <- Parts], - ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "acl"| NParts]). - -b2l(B) -> binary_to_list(emqx_http_lib:uri_encode(iolist_to_binary(B))). diff --git a/apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl b/apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl deleted file mode 100644 index c5c0eb727..000000000 --- a/apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl +++ /dev/null @@ -1,318 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mnesia_SUITE). - --compile(nowarn_export_all). --compile(export_all). - --include("emqx_auth_mnesia.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --import(emqx_ct_http, [ request_api/3 - , request_api/5 - , get_http_data/1 - , create_default_app/0 - , delete_default_app/0 - , default_auth_header/0 - ]). - --define(HOST, "http://127.0.0.1:8081/"). --define(API_VERSION, "v4"). --define(BASE_PATH, "api"). - --define(TABLE, emqx_user). --define(CLIENTID, <<"clientid_for_ct">>). --define(USERNAME, <<"username_for_ct">>). --define(PASSWORD, <<"password">>). --define(NPASSWORD, <<"new_password">>). - -all() -> - emqx_ct:all(?MODULE). - -groups() -> - []. - -init_per_suite(Config) -> - ok = emqx_ct_helpers:start_apps([emqx_management, emqx_auth_mnesia], fun set_special_configs/1), - create_default_app(), - Config. - -end_per_suite(_Config) -> - delete_default_app(), - emqx_ct_helpers:stop_apps([emqx_management, emqx_auth_mnesia]). - -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, true), - application:set_env(emqx, enable_acl_cache, false), - LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)); - -set_special_configs(_App) -> - ok. - -%%------------------------------------------------------------------------------ -%% Testcases -%%------------------------------------------------------------------------------ - -t_management(_Config) -> - clean_all_users(), - - ok = emqx_auth_mnesia_cli:add_user({username, ?USERNAME}, ?PASSWORD), - {error, existed} = emqx_auth_mnesia_cli:add_user({username, ?USERNAME}, ?PASSWORD), - ?assertMatch([{?TABLE, {username, ?USERNAME}, _, _}], - emqx_auth_mnesia_cli:all_users(username) - ), - - ok = emqx_auth_mnesia_cli:add_user({clientid, ?CLIENTID}, ?PASSWORD), - {error, existed} = emqx_auth_mnesia_cli:add_user({clientid, ?CLIENTID}, ?PASSWORD), - ?assertMatch([{?TABLE, {clientid, ?CLIENTID}, _, _}], - emqx_auth_mnesia_cli:all_users(clientid) - ), - - ?assertEqual(2, length(emqx_auth_mnesia_cli:all_users())), - - ok = emqx_auth_mnesia_cli:update_user({username, ?USERNAME}, ?NPASSWORD), - {error, noexisted} = emqx_auth_mnesia_cli:update_user( - {username, <<"no_existed_user">>}, ?PASSWORD - ), - - ok = emqx_auth_mnesia_cli:update_user({clientid, ?CLIENTID}, ?NPASSWORD), - {error, noexisted} = emqx_auth_mnesia_cli:update_user( - {clientid, <<"no_existed_user">>}, ?PASSWORD - ), - - ?assertMatch([{?TABLE, {username, ?USERNAME}, _, _}], - emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME}) - ), - ?assertMatch([{?TABLE, {clientid, ?CLIENTID}, _, _}], - emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID}) - ), - - User1 = #{username => ?USERNAME, - clientid => undefined, - password => ?NPASSWORD, - zone => external}, - - {ok, #{auth_result := success, - anonymous := false}} = emqx_access_control:authenticate(User1), - - {error, password_error} = emqx_access_control:authenticate( - User1#{password => <<"error_password">>} - ), - - ok = emqx_auth_mnesia_cli:remove_user({username, ?USERNAME}), - {ok, #{auth_result := success, - anonymous := true }} = emqx_access_control:authenticate(User1), - - User2 = #{clientid => ?CLIENTID, - password => ?NPASSWORD, - zone => external}, - - {ok, #{auth_result := success, - anonymous := false}} = emqx_access_control:authenticate(User2), - - {error, password_error} = emqx_access_control:authenticate( - User2#{password => <<"error_password">>} - ), - - ok = emqx_auth_mnesia_cli:remove_user({clientid, ?CLIENTID}), - {ok, #{auth_result := success, - anonymous := true }} = emqx_access_control:authenticate(User2), - - [] = emqx_auth_mnesia_cli:all_users(). - -t_auth_clientid_cli(_) -> - clean_all_users(), - - HashType = application:get_env(emqx_auth_mnesia, password_hash, sha256), - - emqx_auth_mnesia_cli:auth_clientid_cli(["add", ?CLIENTID, ?PASSWORD]), - [{_, {clientid, ?CLIENTID}, - <>, - _}] = emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID}), - ?assertEqual(Hash, emqx_passwd:hash(HashType, <>)), - - emqx_auth_mnesia_cli:auth_clientid_cli(["update", ?CLIENTID, ?NPASSWORD]), - [{_, {clientid, ?CLIENTID}, - <>, - _}] = emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID}), - ?assertEqual(Hash1, emqx_passwd:hash(HashType, <>)), - - emqx_auth_mnesia_cli:auth_clientid_cli(["del", ?CLIENTID]), - ?assertEqual([], emqx_auth_mnesia_cli:lookup_user(?CLIENTID)), - - emqx_auth_mnesia_cli:auth_clientid_cli(["add", "user1", "pass1"]), - emqx_auth_mnesia_cli:auth_clientid_cli(["add", "user2", "pass2"]), - ?assertEqual(2, length(emqx_auth_mnesia_cli:auth_clientid_cli(["list"]))), - - emqx_auth_mnesia_cli:auth_clientid_cli(usage). - -t_auth_username_cli(_) -> - clean_all_users(), - - HashType = application:get_env(emqx_auth_mnesia, password_hash, sha256), - - emqx_auth_mnesia_cli:auth_username_cli(["add", ?USERNAME, ?PASSWORD]), - [{_, {username, ?USERNAME}, - <>, - _}] = emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME}), - ?assertEqual(Hash, emqx_passwd:hash(HashType, <>)), - - emqx_auth_mnesia_cli:auth_username_cli(["update", ?USERNAME, ?NPASSWORD]), - [{_, {username, ?USERNAME}, - <>, - _}] = emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME}), - ?assertEqual(Hash1, emqx_passwd:hash(HashType, <>)), - - emqx_auth_mnesia_cli:auth_username_cli(["del", ?USERNAME]), - ?assertEqual([], emqx_auth_mnesia_cli:lookup_user(?USERNAME)), - - emqx_auth_mnesia_cli:auth_username_cli(["add", "user1", "pass1"]), - emqx_auth_mnesia_cli:auth_username_cli(["add", "user2", "pass2"]), - ?assertEqual(2, length(emqx_auth_mnesia_cli:auth_username_cli(["list"]))), - - emqx_auth_mnesia_cli:auth_username_cli(usage). - - -t_clientid_rest_api(_Config) -> - clean_all_users(), - - {ok, Result1} = request_http_rest_list(["auth_clientid"]), - [] = get_http_data(Result1), - - Params1 = #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD}, - {ok, _} = request_http_rest_add(["auth_clientid"], Params1), - - Path = ["auth_clientid/" ++ binary_to_list(?CLIENTID)], - Params2 = #{<<"clientid">> => ?CLIENTID, <<"password">> => ?NPASSWORD}, - {ok, _} = request_http_rest_update(Path, Params2), - - {ok, Result2} = request_http_rest_lookup(Path), - ?assertMatch(#{<<"clientid">> := ?CLIENTID}, get_http_data(Result2)), - - Params3 = [ #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD} - , #{<<"clientid">> => <<"clientid1">>, <<"password">> => ?PASSWORD} - , #{<<"clientid">> => <<"clientid2">>, <<"password">> => ?PASSWORD} - ], - {ok, Result3} = request_http_rest_add(["auth_clientid"], Params3), - ?assertMatch(#{ ?CLIENTID := <<"{error,existed}">> - , <<"clientid1">> := <<"ok">> - , <<"clientid2">> := <<"ok">> - }, get_http_data(Result3)), - - {ok, Result4} = request_http_rest_list(["auth_clientid"]), - ?assertEqual(3, length(get_http_data(Result4))), - - {ok, _} = request_http_rest_delete(Path), - {ok, Result5} = request_http_rest_lookup(Path), - ?assertMatch(#{}, get_http_data(Result5)). - -t_username_rest_api(_Config) -> - clean_all_users(), - - {ok, Result1} = request_http_rest_list(["auth_username"]), - [] = get_http_data(Result1), - - Params1 = #{<<"username">> => ?USERNAME, <<"password">> => ?PASSWORD}, - {ok, _} = request_http_rest_add(["auth_username"], Params1), - - Path = ["auth_username/" ++ binary_to_list(?USERNAME)], - Params2 = #{<<"username">> => ?USERNAME, <<"password">> => ?NPASSWORD}, - {ok, _} = request_http_rest_update(Path, Params2), - - {ok, Result2} = request_http_rest_lookup(Path), - ?assertMatch(#{<<"username">> := ?USERNAME}, get_http_data(Result2)), - - Params3 = [ #{<<"username">> => ?USERNAME, <<"password">> => ?PASSWORD} - , #{<<"username">> => <<"username1">>, <<"password">> => ?PASSWORD} - , #{<<"username">> => <<"username2">>, <<"password">> => ?PASSWORD} - ], - {ok, Result3} = request_http_rest_add(["auth_username"], Params3), - ?assertMatch(#{ ?USERNAME := <<"{error,existed}">> - , <<"username1">> := <<"ok">> - , <<"username2">> := <<"ok">> - }, get_http_data(Result3)), - - {ok, Result4} = request_http_rest_list(["auth_username"]), - ?assertEqual(3, length(get_http_data(Result4))), - - {ok, _} = request_http_rest_delete(Path), - {ok, Result5} = request_http_rest_lookup([Path]), - ?assertMatch(#{}, get_http_data(Result5)). - -t_password_hash(_) -> - clean_all_users(), - {ok, Default} = application:get_env(emqx_auth_mnesia, password_hash), - application:set_env(emqx_auth_mnesia, password_hash, plain), - - %% change the password_hash to 'plain' - application:stop(emqx_auth_mnesia), - ok = application:start(emqx_auth_mnesia), - - Params = #{<<"username">> => ?USERNAME, <<"password">> => ?PASSWORD}, - {ok, _} = request_http_rest_add(["auth_username"], Params), - - %% check - User = #{username => ?USERNAME, - clientid => undefined, - password => ?PASSWORD, - zone => external}, - {ok, #{auth_result := success, - anonymous := false}} = emqx_access_control:authenticate(User), - - application:set_env(emqx_auth_mnesia, password_hash, Default), - application:stop(emqx_auth_mnesia), - ok = application:start(emqx_auth_mnesia). - -%%------------------------------------------------------------------------------ -%% Helpers -%%------------------------------------------------------------------------------ - -clean_all_users() -> - [ mnesia:dirty_delete({emqx_user, Login}) - || Login <- mnesia:dirty_all_keys(emqx_user)]. - -%%-------------------------------------------------------------------- -%% HTTP Request -%%-------------------------------------------------------------------- - -request_http_rest_list(Path) -> - request_api(get, uri(Path), default_auth_header()). - -request_http_rest_lookup(Path) -> - request_api(get, uri([Path]), default_auth_header()). - -request_http_rest_add(Path, Params) -> - request_api(post, uri(Path), [], default_auth_header(), Params). - -request_http_rest_update(Path, Params) -> - request_api(put, uri([Path]), [], default_auth_header(), Params). - -request_http_rest_delete(Login) -> - request_api(delete, uri([Login]), default_auth_header()). - -uri() -> uri([]). -uri(Parts) when is_list(Parts) -> - NParts = [b2l(E) || E <- Parts], - ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]). - -%% @private -b2l(B) when is_binary(B) -> - binary_to_list(B); -b2l(L) when is_list(L) -> - L. diff --git a/apps/emqx_auth_mongo/.gitignore b/apps/emqx_auth_mongo/.gitignore deleted file mode 100644 index a6635ffa0..000000000 --- a/apps/emqx_auth_mongo/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -.eunit -deps -*.o -*.beam -*.plt -erl_crash.dump -ebin -rel/example_project -.concrete/DEV_MODE -.rebar -.DS_Store -.erlang.mk/ -emqx_auth_mongo.d -ct.coverdata -logs/ -test/ct.cover.spec -data/ -cover/ -eunit.coverdata -_build/ -rebar.lock -erlang.mk -etc/emqx_auth_mongo.conf.rendered -.rebar3 diff --git a/apps/emqx_auth_mongo/CHANGES b/apps/emqx_auth_mongo/CHANGES deleted file mode 100644 index 4bddd63a9..000000000 --- a/apps/emqx_auth_mongo/CHANGES +++ /dev/null @@ -1,31 +0,0 @@ - -2.0.7 (2017-01-20) ------------------- - -Tag 2.0.7 - use `cuttlefish:unset()` for commented ACL/super config - -2.0.1 (2016-11-30) ------------------- - -Tag 2.0.1 - -2.0-beta.1 (2016-08-24) ------------------------ - -gen_conf - -1.1.3-beta (2016-08-19) ------------------------ - -Bump version to 1.1.3 - -1.1.2-beta (2016-06-30) ------------------------ - -Bump version to 1.1.2 - -1.1-beta (2016-05-28) ---------------------- - -First public release - diff --git a/apps/emqx_auth_mongo/README.md b/apps/emqx_auth_mongo/README.md deleted file mode 100644 index 3bacfca5b..000000000 --- a/apps/emqx_auth_mongo/README.md +++ /dev/null @@ -1,192 +0,0 @@ -emqx_auth_mongo -=============== - -EMQ X Authentication/ACL with MongoDB - -Build the Plugin ----------------- - -``` -make & make tests -``` - -Configuration -------------- - -File: etc/emqx_auth_mongo.conf - -``` -## MongoDB Topology Type. -## -## Value: single | unknown | sharded | rs -auth.mongo.type = single - -## Sets the set name if type is rs. -## -## Value: String -## auth.mongo.rs_set_name = - -## MongoDB server list. -## -## Value: String -## -## Examples: 127.0.0.1:27017,127.0.0.2:27017... -auth.mongo.server = 127.0.0.1:27017 - -## MongoDB pool size -## -## Value: Number -auth.mongo.pool = 8 - -## MongoDB login user. -## -## Value: String -## auth.mongo.login = - -## MongoDB password. -## -## Value: String -## auth.mongo.password = - -## MongoDB AuthSource -## -## Value: String -## Default: mqtt -## auth.mongo.auth_source = admin - -## MongoDB database -## -## Value: String -auth.mongo.database = mqtt - -## MongoDB write mode. -## -## Value: unsafe | safe -## auth.mongo.w_mode = - -## Mongo read mode. -## -## Value: master | slave_ok -## auth.mongo.r_mode = - -## MongoDB topology options. -auth.mongo.topology.pool_size = 1 -auth.mongo.topology.max_overflow = 0 -## auth.mongo.topology.overflow_ttl = 1000 -## auth.mongo.topology.overflow_check_period = 1000 -## auth.mongo.topology.local_threshold_ms = 1000 -## auth.mongo.topology.connect_timeout_ms = 20000 -## auth.mongo.topology.socket_timeout_ms = 100 -## auth.mongo.topology.server_selection_timeout_ms = 30000 -## auth.mongo.topology.wait_queue_timeout_ms = 1000 -## auth.mongo.topology.heartbeat_frequency_ms = 10000 -## auth.mongo.topology.min_heartbeat_frequency_ms = 1000 - -## Authentication query. -auth.mongo.auth_query.collection = mqtt_user - -auth.mongo.auth_query.password_field = password - -## Password hash. -## -## Value: plain | md5 | sha | sha256 | bcrypt -auth.mongo.auth_query.password_hash = sha256 - -## sha256 with salt suffix -## auth.mongo.auth_query.password_hash = sha256,salt - -## sha256 with salt prefix -## auth.mongo.auth_query.password_hash = salt,sha256 - -## bcrypt with salt prefix -## auth.mongo.auth_query.password_hash = salt,bcrypt - -## pbkdf2 with macfun iterations dklen -## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512 -## auth.mongo.auth_query.password_hash = pbkdf2,sha256,1000,20 - -auth.mongo.auth_query.selector = username=%u - -## Enable superuser query. -auth.mongo.super_query = on - -auth.mongo.super_query.collection = mqtt_user - -auth.mongo.super_query.super_field = is_superuser - -auth.mongo.super_query.selector = username=%u - -## Enable ACL query. -auth.mongo.acl_query = on - -auth.mongo.acl_query.collection = mqtt_acl - -auth.mongo.acl_query.selector = username=%u -``` - -Load the Plugin ---------------- - -``` -./bin/emqx_ctl plugins load emqx_auth_mongo -``` - -MongoDB Database ----------------- - -``` -use mqtt -db.createCollection("mqtt_user") -db.createCollection("mqtt_acl") -db.mqtt_user.ensureIndex({"username":1}) -``` - -mqtt_user Collection --------------------- - -``` -{ - username: "user", - password: "password hash", - salt: "password salt", - is_superuser: boolean (true, false), - created: "datetime" -} -``` - -For example: -``` -db.mqtt_user.insert({username: "test", password: "password hash", salt: "password salt", is_superuser: false}) -db.mqtt_user.insert({username: "root", is_superuser: true}) -``` - -mqtt_acl Collection -------------------- - -``` -{ - username: "username", - clientid: "clientid", - publish: ["topic1", "topic2", ...], - subscribe: ["subtop1", "subtop2", ...], - pubsub: ["topic/#", "topic1", ...] -} -``` - -For example: - -``` -db.mqtt_acl.insert({username: "test", publish: ["t/1", "t/2"], subscribe: ["user/%u", "client/%c"]}) -db.mqtt_acl.insert({username: "admin", pubsub: ["#"]}) -``` - -License -------- - -Apache License Version 2.0 - -Author ------- - -EMQ X Team. - diff --git a/apps/emqx_auth_mongo/etc/emqx_auth_mongo.conf b/apps/emqx_auth_mongo/etc/emqx_auth_mongo.conf deleted file mode 100644 index c59c80643..000000000 --- a/apps/emqx_auth_mongo/etc/emqx_auth_mongo.conf +++ /dev/null @@ -1,187 +0,0 @@ -##-------------------------------------------------------------------- -## MongoDB Auth/ACL Plugin -##-------------------------------------------------------------------- - -## MongoDB Topology Type. -## -## Value: single | unknown | sharded | rs -auth.mongo.type = single - -## The set name if type is rs. -## -## Value: String -## auth.mongo.rs_set_name = - -## MongoDB server list. -## -## Value: String -## -## Examples: "127.0.0.1:27017,127.0.0.2:27017,..." -auth.mongo.server = "127.0.0.1:27017" - -## MongoDB pool size -## -## Value: Number -auth.mongo.pool = 8 - -## MongoDB login user. -## -## Value: String -# auth.mongo.username = - -## MongoDB password. -## -## Value: String -## auth.mongo.password = - -## MongoDB AuthSource -## -## Value: String -## Default: mqtt -## auth.mongo.auth_source = admin - -## MongoDB database -## -## Value: String -auth.mongo.database = mqtt - -## MongoDB query timeout -## -## Value: Duration -## auth.mongo.query_timeout = 5s - -## Whether to enable SSL connection. -## -## Value: on | off -## auth.mongo.ssl.enable = off - -## SSL keyfile. -## -## Value: File -## auth.mongo.ssl.keyfile = - -## SSL certfile. -## -## Value: File -## auth.mongo.ssl.certfile = - -## SSL cacertfile. -## -## Value: File -## auth.mongo.ssl.cacertfile = - -## In mode verify_none the default behavior is to allow all x509-path -## validation errors. -## -## Value: true | false -## auth.mongo.ssl.verify = false - -## If not specified, the server's names returned in server's certificate is validated against -## what's provided `auth.mongo.server` config's host part. -## Setting to 'disable' will make EMQ X ignore unmatched server names. -## If set with a host name, the server's names returned in server's certificate is validated -## against this value. -## -## Value: String | disable -## auth.mongo.ssl.server_name_indication = disable - -## MongoDB write mode. -## -## Value: unsafe | safe -## auth.mongo.w_mode = - -## Mongo read mode. -## -## Value: master | slave_ok -## auth.mongo.r_mode = - -## MongoDB topology options. -auth.mongo.topology.pool_size = 1 -auth.mongo.topology.max_overflow = 0 -## auth.mongo.topology.overflow_ttl = 1000 -## auth.mongo.topology.overflow_check_period = 1000 -## auth.mongo.topology.local_threshold_ms = 1000 -## auth.mongo.topology.connect_timeout_ms = 20000 -## auth.mongo.topology.socket_timeout_ms = 100 -## auth.mongo.topology.server_selection_timeout_ms = 30000 -## auth.mongo.topology.wait_queue_timeout_ms = 1000 -## auth.mongo.topology.heartbeat_frequency_ms = 10000 -## auth.mongo.topology.min_heartbeat_frequency_ms = 1000 - -## ------------------------------------------------- -## Auth Query -## ------------------------------------------------- -## Password hash. -## -## Value: plain | md5 | sha | sha256 | bcrypt -auth.mongo.auth_query.password_hash = sha256 - -## sha256 with salt suffix -## auth.mongo.auth_query.password_hash = "sha256,salt" - -## sha256 with salt prefix -## auth.mongo.auth_query.password_hash = "salt,sha256" - -## bcrypt with salt prefix -## auth.mongo.auth_query.password_hash = "salt,bcrypt" - -## pbkdf2 with macfun iterations dklen -## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512 -## auth.mongo.auth_query.password_hash = "pbkdf2,sha256,1000,20" - -## Authentication query. -auth.mongo.auth_query.collection = mqtt_user - -## Password mainly fields -## -## Value: password | password,salt -auth.mongo.auth_query.password_field = password - -## Authentication Selector. -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -## auth.mongo.auth_query.selector = {Field}={Placeholder} -auth.mongo.auth_query.selector = "username=%u" - -## ------------------------------------------------- -## Super User Query -## ------------------------------------------------- -auth.mongo.super_query.collection = mqtt_user -auth.mongo.super_query.super_field = is_superuser -#auth.mongo.super_query.selector.1 = username=%u, clientid=%c -auth.mongo.super_query.selector = "username=%u" - -## ACL Selector. -## -## Multiple selectors could be combined with '$or' -## when query acl from mongo. -## -## e.g. -## -## With following 2 selectors configured: -## -## auth.mongo.acl_query.selector.1 = "username=%u" -## auth.mongo.acl_query.selector.2 = "username=$all" -## -## And if a client connected using username 'ilyas', -## then the following mongo command will be used to -## retrieve acl entries: -## -## db.mqtt_acl.find({$or: [{username: "ilyas"}, {username: "$all"}]}); -## -## Variables: -## - %u: username -## - %c: clientid -## -## Examples: -## -## auth.mongo.acl_query.selector.1 = "username=%u,clientid=%c" -## auth.mongo.acl_query.selector.2 = "username=$all" -## auth.mongo.acl_query.selector.3 = "clientid=$all" -auth.mongo.acl_query.collection = mqtt_acl -auth.mongo.acl_query.selector = "username=%u" diff --git a/apps/emqx_auth_mongo/include/emqx_auth_mongo.hrl b/apps/emqx_auth_mongo/include/emqx_auth_mongo.hrl deleted file mode 100644 index 97ecf9973..000000000 --- a/apps/emqx_auth_mongo/include/emqx_auth_mongo.hrl +++ /dev/null @@ -1,37 +0,0 @@ - --define(APP, emqx_auth_mongo). - --define(DEFAULT_SELECTORS, [{<<"username">>, <<"%u">>}]). - --record(superquery, {collection = <<"mqtt_user">>, - field = <<"is_superuser">>, - selector = {<<"username">>, <<"%u">>}}). - --record(authquery, {collection = <<"mqtt_user">>, - field = <<"password">>, - hash = sha256, - selector = {<<"username">>, <<"%u">>}}). - --record(aclquery, {collection = <<"mqtt_acl">>, - selector = {<<"username">>, <<"%u">>}}). - --record(auth_metrics, { - success = 'client.auth.success', - failure = 'client.auth.failure', - ignore = 'client.auth.ignore' - }). - --record(acl_metrics, { - allow = 'client.acl.allow', - deny = 'client.acl.deny', - ignore = 'client.acl.ignore' - }). - --define(METRICS(Type), tl(tuple_to_list(#Type{}))). --define(METRICS(Type, K), #Type{}#Type.K). - --define(AUTH_METRICS, ?METRICS(auth_metrics)). --define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). - --define(ACL_METRICS, ?METRICS(acl_metrics)). --define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). diff --git a/apps/emqx_auth_mongo/priv/emqx_auth_mongo.schema b/apps/emqx_auth_mongo/priv/emqx_auth_mongo.schema deleted file mode 100644 index cd8c03015..000000000 --- a/apps/emqx_auth_mongo/priv/emqx_auth_mongo.schema +++ /dev/null @@ -1,341 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_auth_mongo config mapping - -{mapping, "auth.mongo.type", "emqx_auth_mongo.server", [ - {default, single}, - {datatype, {enum, [single, unknown, sharded, rs]}} -]}. - -{mapping, "auth.mongo.rs_set_name", "emqx_auth_mongo.server", [ - {default, "mqtt"}, - {datatype, string} -]}. - -{mapping, "auth.mongo.server", "emqx_auth_mongo.server", [ - {default, "127.0.0.1:27017"}, - {datatype, string} -]}. - -{mapping, "auth.mongo.pool", "emqx_auth_mongo.server", [ - {default, 8}, - {datatype, integer} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.mongo.login", "emqx_auth_mongo.server", [ - {datatype, string} -]}. - -{mapping, "auth.mongo.username", "emqx_auth_mongo.server", [ - {datatype, string} -]}. - -{mapping, "auth.mongo.password", "emqx_auth_mongo.server", [ - {default, ""}, - {datatype, string} -]}. - -{mapping, "auth.mongo.database", "emqx_auth_mongo.server", [ - {default, "mqtt"}, - {datatype, string} -]}. - -{mapping, "auth.mongo.auth_source", "emqx_auth_mongo.server", [ - {default, "mqtt"}, - {datatype, string} -]}. - -{mapping, "auth.mongo.ssl.enable", "emqx_auth_mongo.server", [ - {default, off}, - {datatype, {enum, [on, off, true, false]}} %% FIXME: ture/false is compatible with 4.0-4.2 version format, plan to delete in 5.0 -]}. - -{mapping, "auth.mongo.ssl.keyfile", "emqx_auth_mongo.server", [ - {datatype, string} -]}. - -{mapping, "auth.mongo.ssl.certfile", "emqx_auth_mongo.server", [ - {datatype, string} -]}. - -{mapping, "auth.mongo.ssl.cacertfile", "emqx_auth_mongo.server", [ - {datatype, string} -]}. - -{mapping, "auth.mongo.ssl.verify", "emqx_auth_mongo.server", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "auth.mongo.ssl.server_name_indication", "emqx_auth_mongo.server", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.mongo.ssl_opts.keyfile", "emqx_auth_mongo.server", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.mongo.ssl_opts.certfile", "emqx_auth_mongo.server", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.mongo.ssl_opts.cacertfile", "emqx_auth_mongo.server", [ - {datatype, string} -]}. - -{mapping, "auth.mongo.w_mode", "emqx_auth_mongo.server", [ - {default, undef}, - {datatype, {enum, [safe, unsafe, undef]}} -]}. - -{mapping, "auth.mongo.r_mode", "emqx_auth_mongo.server", [ - {default, undef}, - {datatype, {enum, [master, slave_ok, undef]}} -]}. - -{mapping, "auth.mongo.topology.$name", "emqx_auth_mongo.server", [ - {datatype, integer} -]}. - -{translation, "emqx_auth_mongo.server", fun(Conf) -> - H = cuttlefish:conf_get("auth.mongo.server", Conf), - Hosts = string:tokens(H, ","), - Type0 = cuttlefish:conf_get("auth.mongo.type", Conf), - Pool = cuttlefish:conf_get("auth.mongo.pool", Conf), - %% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 - Login = cuttlefish:conf_get("auth.mongo.username", Conf, - cuttlefish:conf_get("auth.mongo.login", Conf, "") - ), - Passwd = cuttlefish:conf_get("auth.mongo.password", Conf), - DB = cuttlefish:conf_get("auth.mongo.database", Conf), - AuthSrc = cuttlefish:conf_get("auth.mongo.auth_source", Conf), - R = cuttlefish:conf_get("auth.mongo.w_mode", Conf), - W = cuttlefish:conf_get("auth.mongo.r_mode", Conf), - Login0 = case Login =:= [] of - true -> []; - false -> [{login, list_to_binary(Login)}] - end, - Passwd0 = case Passwd =:= [] of - true -> []; - false -> [{password, list_to_binary(Passwd)}] - end, - W0 = case W =:= undef of - true -> []; - false -> [{w_mode, W}] - end, - R0 = case R =:= undef of - true -> []; - false -> [{r_mode, R}] - end, - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - SslOpts = fun(Prefix) -> - Verify = case cuttlefish:conf_get(Prefix ++ ".verify", Conf, false) of - true -> verify_peer; - false -> verify_none - end, - Filter([{verify, Verify}, - {server_name_indication, case cuttlefish:conf_get(Prefix ++ ".server_name_indication", Conf, undefined) of - "disable" -> disable; - SNI -> SNI - end}, - {keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)}, - {certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)}, - {cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)}]) - end, - - %% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 - GenSsl = case cuttlefish:conf_get("auth.mongo.ssl.cacertfile", Conf, undefined) of - undefined -> [{ssl, true}, {ssl_opts, SslOpts("auth.mongo.ssl_opts")}]; - _ -> [{ssl, true}, {ssl_opts, SslOpts("auth.mongo.ssl")}] - end, - - %% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 - Ssl = case cuttlefish:conf_get("auth.mongo.ssl.enable", Conf) of - on -> GenSsl; - off -> []; - true -> [{ssl, true}, {ssl_opts, SslOpts("auth.mongo.ssl_opts")}]; - false -> [] - end, - - WorkerOptions = [{database, list_to_binary(DB)}, {auth_source, list_to_binary(AuthSrc)}] - ++ Login0 ++ Passwd0 ++ W0 ++ R0 ++ Ssl, - - Vars = cuttlefish_variable:fuzzy_matches(["auth", "mongo", "topology", "$name"], Conf), - Options = lists:map(fun({_, Name}) -> - Name2 = case Name of - "local_threshold_ms" -> "localThresholdMS"; - "connect_timeout_ms" -> "connectTimeoutMS"; - "socket_timeout_ms" -> "socketTimeoutMS"; - "server_selection_timeout_ms" -> "serverSelectionTimeoutMS"; - "wait_queue_timeout_ms" -> "waitQueueTimeoutMS"; - "heartbeat_frequency_ms" -> "heartbeatFrequencyMS"; - "min_heartbeat_frequency_ms" -> "minHeartbeatFrequencyMS"; - _ -> Name - end, - {list_to_atom(Name2), cuttlefish:conf_get("auth.mongo.topology."++Name, Conf)} - end, Vars), - - Type = case Type0 =:= rs of - true -> {Type0, list_to_binary(cuttlefish:conf_get("auth.mongo.rs_set_name", Conf))}; - false -> Type0 - end, - [{type, Type}, - {hosts, Hosts}, - {options, Options}, - {worker_options, WorkerOptions}, - {auto_reconnect, 1}, - {pool_size, Pool}] -end}. - -%% The mongodb operation timeout is specified by the value of `cursor_timeout` from application config, -%% or `infinity` if `cursor_timeout` not specified -{mapping, "auth.mongo.query_timeout", "mongodb.cursor_timeout", [ - {datatype, string} -]}. - -{translation, "mongodb.cursor_timeout", fun(Conf) -> - case cuttlefish:conf_get("auth.mongo.query_timeout", Conf, undefined) of - undefined -> infinity; - Duration -> - case cuttlefish_duration:parse(Duration, ms) of - {error, Reason} -> error(Reason); - Ms when is_integer(Ms) -> Ms - end - end -end}. - -{mapping, "auth.mongo.auth_query.collection", "emqx_auth_mongo.auth_query", [ - {default, "mqtt_user"}, - {datatype, string} -]}. - -{mapping, "auth.mongo.auth_query.password_field", "emqx_auth_mongo.auth_query", [ - {default, "password"}, - {datatype, string} -]}. - -{mapping, "auth.mongo.auth_query.password_hash", "emqx_auth_mongo.auth_query", [ - {datatype, string} -]}. - -{mapping, "auth.mongo.auth_query.selector", "emqx_auth_mongo.auth_query", [ - {default, ""}, - {datatype, string} -]}. - -{translation, "emqx_auth_mongo.auth_query", fun(Conf) -> - case cuttlefish:conf_get("auth.mongo.auth_query.collection", Conf) of - undefined -> cuttlefish:unset(); - Collection -> - PasswordField = cuttlefish:conf_get("auth.mongo.auth_query.password_field", Conf), - PasswordHash = cuttlefish:conf_get("auth.mongo.auth_query.password_hash", Conf), - SelectorStr = cuttlefish:conf_get("auth.mongo.auth_query.selector", Conf), - SelectorList = - lists:map(fun(Selector) -> - case string:tokens(Selector, "=") of - [Field, Val] -> {list_to_binary(Field), list_to_binary(Val)}; - _ -> {<<"username">>, <<"%u">>} - end - end, string:tokens(SelectorStr, ", ")), - - PasswordFields = [list_to_binary(Field) || Field <- string:tokens(PasswordField, ",")], - HashValue = - case string:tokens(PasswordHash, ",") of - [Hash] -> list_to_atom(Hash); - [Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)}; - [Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), list_to_integer(Iterations), list_to_integer(Dklen)}; - _ -> plain - end, - [{collection, Collection}, - {password_field, PasswordFields}, - {password_hash, HashValue}, - {selector, SelectorList}] - end -end}. - -{mapping, "auth.mongo.super_query", "emqx_auth_mongo.super_query", [ - {default, off}, - {datatype, flag} -]}. - -{mapping, "auth.mongo.super_query.collection", "emqx_auth_mongo.super_query", [ - {default, "mqtt_user"}, - {datatype, string} -]}. - -{mapping, "auth.mongo.super_query.super_field", "emqx_auth_mongo.super_query", [ - {default, "is_superuser"}, - {datatype, string} -]}. - -{mapping, "auth.mongo.super_query.selector", "emqx_auth_mongo.super_query", [ - {default, ""}, - {datatype, string} -]}. - -{translation, "emqx_auth_mongo.super_query", fun(Conf) -> - case cuttlefish:conf_get("auth.mongo.super_query.collection", Conf) of - undefined -> cuttlefish:unset(); - Collection -> - SuperField = cuttlefish:conf_get("auth.mongo.super_query.super_field", Conf), - SelectorStr = cuttlefish:conf_get("auth.mongo.super_query.selector", Conf), - SelectorList = - lists:map(fun(Selector) -> - case string:tokens(Selector, "=") of - [Field, Val] -> {list_to_binary(Field), list_to_binary(Val)}; - _ -> {<<"username">>, <<"%u">>} - end - end, string:tokens(SelectorStr, ", ")), - [{collection, Collection}, {super_field, SuperField}, {selector, SelectorList}] - end -end}. - -{mapping, "auth.mongo.acl_query", "emqx_auth_mongo.acl_query", [ - {default, off}, - {datatype, flag} -]}. - -{mapping, "auth.mongo.acl_query.collection", "emqx_auth_mongo.acl_query", [ - {default, "mqtt_user"}, - {datatype, string} -]}. - -{mapping, "auth.mongo.acl_query.selector", "emqx_auth_mongo.acl_query", [ - {default, ""}, - {datatype, string} -]}. -{mapping, "auth.mongo.acl_query.selector.$id", "emqx_auth_mongo.acl_query", [ - {default, ""}, - {datatype, string} -]}. - -{translation, "emqx_auth_mongo.acl_query", fun(Conf) -> - case cuttlefish:conf_get("auth.mongo.acl_query.collection", Conf) of - undefined -> cuttlefish:unset(); - Collection -> - SelectorStrList = - lists:map( - fun - ({["auth","mongo","acl_query","selector"], ConfEntry}) -> - ConfEntry; - ({["auth","mongo","acl_query","selector", _], ConfEntry}) -> - ConfEntry - end, - cuttlefish_variable:filter_by_prefix("auth.mongo.acl_query.selector", Conf)), - SelectorListList = - lists:map( - fun(SelectorStr) -> - lists:map(fun(Selector) -> - case string:tokens(Selector, "=") of - [Field, Val] -> {list_to_binary(Field), list_to_binary(Val)}; - _ -> {<<"username">>, <<"%u">>} - end - end, string:tokens(SelectorStr, ", ")) - end, - SelectorStrList), - [{collection, Collection}, {selector, SelectorListList}] - end -end}. diff --git a/apps/emqx_auth_mongo/rebar.config b/apps/emqx_auth_mongo/rebar.config deleted file mode 100644 index f44e69543..000000000 --- a/apps/emqx_auth_mongo/rebar.config +++ /dev/null @@ -1,32 +0,0 @@ -{deps, - %% NOTE: mind poolboy version when updating mongodb-erlang version - [{mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.7"}}}, - %% mongodb-erlang uses a special fork https://github.com/comtihon/poolboy.git - %% (which has overflow_ttl feature added). - %% However, it references `{branch, "master}` (commit 9c06a9a on 2021-04-07). - %% By accident, We have always been using the upstream fork due to - %% eredis_cluster's dependency getting resolved earlier. - %% Here we pin 1.5.2 to avoid surprises in the future. - {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}} - ]}. - -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - compressed, - {parse_transform} - ]}. -{overrides, [{add, [{erl_opts, [compressed]}]}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions - ]}. - -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. - diff --git a/apps/emqx_auth_mongo/src/emqx_acl_mongo.erl b/apps/emqx_auth_mongo/src/emqx_acl_mongo.erl deleted file mode 100644 index 653600395..000000000 --- a/apps/emqx_auth_mongo/src/emqx_acl_mongo.erl +++ /dev/null @@ -1,91 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_acl_mongo). - --include("emqx_auth_mongo.hrl"). --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - -%% ACL callbacks --export([ register_metrics/0 - , check_acl/5 - , description/0 - ]). --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). - -check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _State) -> - ok; - -check_acl(ClientInfo, PubSub, Topic, _AclResult, Env = #{aclquery := AclQuery}) -> - #aclquery{collection = Coll, selector = SelectorList} = AclQuery, - Pool = maps:get(pool, Env, ?APP), - SelectorMapList = - lists:map(fun(Selector) -> - maps:from_list(emqx_auth_mongo:replvars(Selector, ClientInfo)) - end, SelectorList), - case emqx_auth_mongo:query_multi(Pool, Coll, SelectorMapList) of - [] -> ok; - Rows -> - try match(ClientInfo, Topic, topics(PubSub, Rows)) of - matched -> emqx_metrics:inc(?ACL_METRICS(allow)), - {stop, allow}; - nomatch -> emqx_metrics:inc(?ACL_METRICS(deny)), - {stop, deny} - catch - _Err:Reason-> - ?LOG(error, "[MongoDB] Check mongo ~p ACL failed, got ACL config: ~p, error: :~p", - [PubSub, Rows, Reason]), - emqx_metrics:inc(?ACL_METRICS(ignore)), - ignore - end - end. - - -match(_ClientInfo, _Topic, []) -> - nomatch; -match(ClientInfo, Topic, [TopicFilter|More]) -> - case emqx_topic:match(Topic, feedvar(ClientInfo, TopicFilter)) of - true -> matched; - false -> match(ClientInfo, Topic, More) - end. - -topics(publish, Rows) -> - lists:foldl(fun(Row, Acc) -> - Topics = maps:get(<<"publish">>, Row, []) ++ maps:get(<<"pubsub">>, Row, []), - lists:umerge(Acc, Topics) - end, [], Rows); - -topics(subscribe, Rows) -> - lists:foldl(fun(Row, Acc) -> - Topics = maps:get(<<"subscribe">>, Row, []) ++ maps:get(<<"pubsub">>, Row, []), - lists:umerge(Acc, Topics) - end, [], Rows). - -feedvar(#{clientid := ClientId, username := Username}, Str) -> - lists:foldl(fun({Var, Val}, Acc) -> - feedvar(Acc, Var, Val) - end, Str, [{"%u", Username}, {"%c", ClientId}]). - -feedvar(Str, _Var, undefined) -> - Str; -feedvar(Str, Var, Val) -> - re:replace(Str, Var, Val, [global, {return, binary}]). - -description() -> "ACL with MongoDB". - diff --git a/apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src b/apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src deleted file mode 100644 index ab0b4ff56..000000000 --- a/apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_auth_mongo, - [{description, "EMQ X Authentication/ACL with MongoDB"}, - {vsn, "4.4.0"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_auth_mongo_sup]}, - {applications, [kernel,stdlib,mongodb,ecpool]}, - {mod, {emqx_auth_mongo_app,[]}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-auth-mongo"} - ]} - ]}. diff --git a/apps/emqx_auth_mongo/src/emqx_auth_mongo.erl b/apps/emqx_auth_mongo/src/emqx_auth_mongo.erl deleted file mode 100644 index cd1d21b42..000000000 --- a/apps/emqx_auth_mongo/src/emqx_auth_mongo.erl +++ /dev/null @@ -1,138 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mongo). - --behaviour(ecpool_worker). - --include("emqx_auth_mongo.hrl"). --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). --include_lib("emqx/include/types.hrl"). - --export([ register_metrics/0 - , check/3 - , description/0 - ]). - --export([ replvar/2 - , replvars/2 - , connect/1 - , query/3 - , query_multi/3 - ]). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). - -check(ClientInfo = #{password := Password}, AuthResult, - Env = #{authquery := AuthQuery, superquery := SuperQuery}) -> - #authquery{collection = Collection, field = Fields, - hash = HashType, selector = Selector} = AuthQuery, - Pool = maps:get(pool, Env, ?APP), - case query(Pool, Collection, maps:from_list(replvars(Selector, ClientInfo))) of - undefined -> emqx_metrics:inc(?AUTH_METRICS(ignore)); - {error, Reason} -> - ?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]), - ok = emqx_metrics:inc(?AUTH_METRICS(failure)), - {stop, AuthResult#{auth_result => not_authorized, anonymous => false}}; - UserMap -> - Result = case [maps:get(Field, UserMap, undefined) || Field <- Fields] of - [undefined] -> {error, password_error}; - [PassHash] -> - check_pass({PassHash, Password}, HashType); - [PassHash, Salt|_] -> - check_pass({PassHash, Salt, Password}, HashType) - end, - case Result of - ok -> - ok = emqx_metrics:inc(?AUTH_METRICS(success)), - {stop, AuthResult#{is_superuser => is_superuser(Pool, SuperQuery, ClientInfo), - anonymous => false, - auth_result => success}}; - {error, Error} -> - ?LOG(error, "[MongoDB] check auth fail: ~p", [Error]), - ok = emqx_metrics:inc(?AUTH_METRICS(failure)), - {stop, AuthResult#{auth_result => Error, anonymous => false}} - end - end. - -check_pass(Password, HashType) -> - case emqx_passwd:check_pass(Password, HashType) of - ok -> ok; - {error, _Reason} -> {error, not_authorized} - end. - -description() -> "Authentication with MongoDB". - -%%-------------------------------------------------------------------- -%% Is Superuser? -%%-------------------------------------------------------------------- -is_superuser(_Pool, undefined, _ClientInfo) -> - false; -is_superuser(Pool, #superquery{collection = Coll, field = Field, selector = Selector}, ClientInfo) -> - case query(Pool, Coll, maps:from_list(replvars(Selector, ClientInfo))) of - undefined -> false; - {error, Reason} -> - ?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]), - false; - Row -> - case maps:get(Field, Row, false) of - true -> true; - _False -> false - end - end. - -replvars(VarList, ClientInfo) -> - lists:map(fun(Var) -> replvar(Var, ClientInfo) end, VarList). - -replvar({Field, <<"%u">>}, #{username := Username}) -> - {Field, Username}; -replvar({Field, <<"%c">>}, #{clientid := ClientId}) -> - {Field, ClientId}; -replvar({Field, <<"%C">>}, #{cn := CN}) -> - {Field, CN}; -replvar({Field, <<"%d">>}, #{dn := DN}) -> - {Field, DN}; -replvar(Selector, _ClientInfo) -> - Selector. - -%%-------------------------------------------------------------------- -%% MongoDB Connect/Query -%%-------------------------------------------------------------------- - -connect(Opts) -> - Type = proplists:get_value(type, Opts, single), - Hosts = proplists:get_value(hosts, Opts, []), - Options = proplists:get_value(options, Opts, []), - WorkerOptions = proplists:get_value(worker_options, Opts, []), - mongo_api:connect(Type, Hosts, Options, WorkerOptions). - -query(Pool, Collection, Selector) -> - ecpool:with_client(Pool, fun(Conn) -> mongo_api:find_one(Conn, Collection, Selector, #{}) end). - -query_multi(Pool, Collection, SelectorList) -> - lists:reverse(lists:flatten(lists:foldl(fun(Selector, Acc1) -> - Batch = ecpool:with_client(Pool, fun(Conn) -> - case mongo_api:find(Conn, Collection, Selector, #{}) of - [] -> []; - {ok, Cursor} -> - mc_cursor:foldl(fun(O, Acc2) -> [O|Acc2] end, [], Cursor, 1000) - end - end), - [Batch|Acc1] - end, [], SelectorList))). diff --git a/apps/emqx_auth_mongo/src/emqx_auth_mongo_app.erl b/apps/emqx_auth_mongo/src/emqx_auth_mongo_app.erl deleted file mode 100644 index b8d9431c2..000000000 --- a/apps/emqx_auth_mongo/src/emqx_auth_mongo_app.erl +++ /dev/null @@ -1,88 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mongo_app). - --behaviour(application). - --emqx_plugin(auth). - --include("emqx_auth_mongo.hrl"). - --import(proplists, [get_value/3]). - -%% Application callbacks --export([ start/2 - , prep_stop/1 - , stop/1 - ]). - -%%-------------------------------------------------------------------- -%% Application callbacks -%%-------------------------------------------------------------------- - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_auth_mongo_sup:start_link(), - with_env(auth_query, fun reg_authmod/1), - with_env(acl_query, fun reg_aclmod/1), - {ok, Sup}. - -prep_stop(State) -> - ok = emqx:unhook('client.authenticate', {emqx_auth_mongo, check}), - ok = emqx:unhook('client.check_acl', {emqx_acl_mongo, check_acl}), - State. - -stop(_State) -> - ok. - -reg_authmod(AuthQuery) -> - emqx_auth_mongo:register_metrics(), - SuperQuery = r(super_query, application:get_env(?APP, super_query, undefined)), - ok = emqx:hook('client.authenticate', {emqx_auth_mongo, check, - [#{authquery => AuthQuery, superquery => SuperQuery, pool => ?APP}] - }). - -reg_aclmod(AclQuery) -> - emqx_acl_mongo:register_metrics(), - ok = emqx:hook('client.check_acl', {emqx_acl_mongo, check_acl, [#{aclquery => AclQuery, pool => ?APP}]}). - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -with_env(Name, Fun) -> - case application:get_env(?APP, Name) of - undefined -> ok; - {ok, Config} -> Fun(r(Name, Config)) - end. - -r(super_query, undefined) -> - undefined; -r(super_query, Config) -> - #superquery{collection = list_to_binary(get_value(collection, Config, "mqtt_user")), - field = list_to_binary(get_value(super_field, Config, "is_superuser")), - selector = get_value(selector, Config, ?DEFAULT_SELECTORS)}; - -r(auth_query, Config) -> - #authquery{collection = list_to_binary(get_value(collection, Config, "mqtt_user")), - field = get_value(password_field, Config, [<<"password">>]), - hash = get_value(password_hash, Config, sha256), - selector = get_value(selector, Config, ?DEFAULT_SELECTORS)}; - -r(acl_query, Config) -> - #aclquery{collection = list_to_binary(get_value(collection, Config, "mqtt_acl")), - selector = get_value(selector, Config, [?DEFAULT_SELECTORS])}. - diff --git a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE.erl b/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE.erl deleted file mode 100644 index a988e87bb..000000000 --- a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE.erl +++ /dev/null @@ -1,169 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mongo_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("emqx/include/emqx.hrl"). --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). - --define(APP, emqx_auth_mongo). - --define(POOL(App), ecpool_worker:client(gproc_pool:pick_worker({ecpool, App}))). - --define(MONGO_CL_ACL, <<"mqtt_acl">>). --define(MONGO_CL_USER, <<"mqtt_user">>). - --define(INIT_ACL, [{<<"username">>, <<"testuser">>, <<"clientid">>, <<"null">>, <<"subscribe">>, [<<"#">>]}, - {<<"username">>, <<"dashboard">>, <<"clientid">>, <<"null">>, <<"pubsub">>, [<<"$SYS/#">>]}, - {<<"username">>, <<"user3">>, <<"clientid">>, <<"null">>, <<"publish">>, [<<"a/b/c">>]}]). - --define(INIT_AUTH, [{<<"username">>, <<"plain">>, <<"password">>, <<"plain">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, true}, - {<<"username">>, <<"md5">>, <<"password">>, <<"1bc29b36f623ba82aaf6724fd3b16718">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false}, - {<<"username">>, <<"sha">>, <<"password">>, <<"d8f4590320e1343a915b6394170650a8f35d6926">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false}, - {<<"username">>, <<"sha256">>, <<"password">>, <<"5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false}, - {<<"username">>, <<"pbkdf2_password">>, <<"password">>, <<"cdedb5281bb2f801565a1122b2563515">>, <<"salt">>, <<"ATHENA.MIT.EDUraeburn">>, <<"is_superuser">>, false}, - {<<"username">>, <<"bcrypt_foo">>, <<"password">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.HbUIOdlQI0iS22Q5rd5z.JVVYH6sfm6">>, <<"salt">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.">>, <<"is_superuser">>, false} - ]). - -%%-------------------------------------------------------------------- -%% Setups -%%-------------------------------------------------------------------- - -all() -> - emqx_ct:all(?MODULE). - -init_per_suite(Cfg) -> - emqx_ct_helpers:start_apps([emqx_auth_mongo], fun set_special_confs/1), - init_mongo_data(), - Cfg. - -end_per_suite(_Cfg) -> - deinit_mongo_data(), - emqx_ct_helpers:stop_apps([emqx_auth_mongo]). - -set_special_confs(emqx) -> - application:set_env(emqx, acl_nomatch, deny), - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, enable_acl_cache, false); -set_special_confs(_App) -> - ok. - -init_mongo_data() -> - %% Users - {ok, Connection} = ?POOL(?APP), - mongo_api:delete(Connection, ?MONGO_CL_USER, {}), - ?assertMatch({{true, _}, _}, mongo_api:insert(Connection, ?MONGO_CL_USER, ?INIT_AUTH)), - %% ACLs - mongo_api:delete(Connection, ?MONGO_CL_ACL, {}), - ?assertMatch({{true, _}, _}, mongo_api:insert(Connection, ?MONGO_CL_ACL, ?INIT_ACL)). - -deinit_mongo_data() -> - {ok, Connection} = ?POOL(?APP), - mongo_api:delete(Connection, ?MONGO_CL_USER, {}), - mongo_api:delete(Connection, ?MONGO_CL_ACL, {}). - -%%-------------------------------------------------------------------- -%% Test cases -%%-------------------------------------------------------------------- - -t_check_auth(_) -> - Plain = #{zone => external, clientid => <<"client1">>, username => <<"plain">>}, - Plain1 = #{zone => external, clientid => <<"client1">>, username => <<"plain2">>}, - Md5 = #{zone => external, clientid => <<"md5">>, username => <<"md5">>}, - Sha = #{zone => external, clientid => <<"sha">>, username => <<"sha">>}, - Sha256 = #{zone => external, clientid => <<"sha256">>, username => <<"sha256">>}, - Pbkdf2 = #{zone => external, clientid => <<"pbkdf2_password">>, username => <<"pbkdf2_password">>}, - Bcrypt = #{zone => external, clientid => <<"bcrypt_foo">>, username => <<"bcrypt_foo">>}, - User1 = #{zone => external, clientid => <<"bcrypt_foo">>, username => <<"user">>}, - reload({auth_query, [{password_hash, plain}]}), - %% With exactly username/password, connection success - {ok, #{is_superuser := true}} = emqx_access_control:authenticate(Plain#{password => <<"plain">>}), - %% With exactly username and wrong password, connection fail - {error, _} = emqx_access_control:authenticate(Plain#{password => <<"error_pwd">>}), - %% With wrong username and wrong password, emqx_auth_mongo auth fail, then allow anonymous authentication - {error, _} = emqx_access_control:authenticate(Plain1#{password => <<"error_pwd">>}), - %% With wrong username and exactly password, emqx_auth_mongo auth fail, then allow anonymous authentication - {error, _} = emqx_access_control:authenticate(Plain1#{password => <<"plain">>}), - reload({auth_query, [{password_hash, md5}]}), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Md5#{password => <<"md5">>}), - reload({auth_query, [{password_hash, sha}]}), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Sha#{password => <<"sha">>}), - reload({auth_query, [{password_hash, sha256}]}), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Sha256#{password => <<"sha256">>}), - %%pbkdf2 sha - reload({auth_query, [{password_hash, {pbkdf2, sha, 1, 16}}, {password_field, [<<"password">>, <<"salt">>]}]}), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Pbkdf2#{password => <<"password">>}), - reload({auth_query, [{password_hash, {salt, bcrypt}}]}), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Bcrypt#{password => <<"foo">>}), - {error, _} = emqx_access_control:authenticate(User1#{password => <<"foo">>}). - -t_check_acl(_) -> - {ok, Connection} = ?POOL(?APP), - User1 = #{zone => external, clientid => <<"client1">>, username => <<"testuser">>}, - User2 = #{zone => external, clientid => <<"client2">>, username => <<"dashboard">>}, - User3 = #{zone => external, clientid => <<"client2">>, username => <<"user3">>}, - User4 = #{zone => external, clientid => <<"$$client2">>, username => <<"$$user3">>}, - 3 = mongo_api:count(Connection, ?MONGO_CL_ACL, {}, 17), - %% ct log output - allow = emqx_access_control:check_acl(User1, subscribe, <<"users/testuser/1">>), - deny = emqx_access_control:check_acl(User1, subscribe, <<"$SYS/testuser/1">>), - deny = emqx_access_control:check_acl(User2, subscribe, <<"a/b/c">>), - allow = emqx_access_control:check_acl(User2, subscribe, <<"$SYS/testuser/1">>), - allow = emqx_access_control:check_acl(User3, publish, <<"a/b/c">>), - deny = emqx_access_control:check_acl(User3, publish, <<"c">>), - deny = emqx_access_control:check_acl(User4, publish, <<"a/b/c">>). - -t_acl_super(_) -> - reload({auth_query, [{password_hash, plain}, {password_field, [<<"password">>]}]}), - {ok, C} = emqtt:start_link([{clientid, <<"simpleClient">>}, - {username, <<"plain">>}, - {password, <<"plain">>}]), - {ok, _} = emqtt:connect(C), - timer:sleep(10), - emqtt:subscribe(C, <<"TopicA">>, qos2), - timer:sleep(1000), - emqtt:publish(C, <<"TopicA">>, <<"Payload">>, qos2), - timer:sleep(1000), - receive - {publish, #{payload := Payload}} -> - ?assertEqual(<<"Payload">>, Payload) - after - 1000 -> - ct:fail({receive_timeout, <<"Payload">>}), - ok - end, - emqtt:disconnect(C). - -%%-------------------------------------------------------------------- -%% Utils -%%-------------------------------------------------------------------- - -reload({Par, Vals}) when is_list(Vals) -> - application:stop(?APP), - {ok, TupleVals} = application:get_env(?APP, Par), - NewVals = - lists:filtermap(fun({K, V}) -> - case lists:keymember(K, 1, Vals) of - false ->{true, {K, V}}; - _ -> false - end - end, TupleVals), - application:set_env(?APP, Par, lists:append(NewVals, Vals)), - application:start(?APP). diff --git a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca-key.pem b/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca-key.pem deleted file mode 100644 index e9717011e..000000000 --- a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA0kGUBi9NDp65jgdxKfizIfuSr2wpwb44yM9SuP4oUQSULOA2 -4iFpLR/c5FAYHU81y9Vx91dQjdZfffaBZuv2zVvteXUkol8Nez7boKbo2E41MTew -8edtNKZAQVvnaHAC2NCZxjchCzUCDEoUUcl+cIERZ8R48FBqK5iTVcMRIx1akwus -+dhBqP0ykA5TGOWZkJrLM9aUXSPQha9+wXlOpkvu0Ur2nkX8PPJnifWao9UShSar -ll1IqPZNCSlZMwcFYcQNBCpdvITUUYlHvMRQV64bUpOxUGDuJkQL3dLKBlNuBRlJ -BcjBAKw7rFnwwHZcMmQ9tan/dZzpzwjo/T0XjwIDAQABAoIBAQCSHvUqnzDkWjcG -l/Fzg92qXlYBCCC0/ugj1sHcwvVt6Mq5rVE3MpUPwTcYjPlVVTlD4aEEjm/zQuq2 -ddxUlOS+r4aIhHrjRT/vSS4FpjnoKeIZxGR6maVxk6DQS3i1QjMYT1CvSpzyVvKH -a+xXMrtmoKxh+085ZAmFJtIuJhUA2yEa4zggCxWnvz8ecLClUPfVDPhdLBHc3KmL -CRpHEC6L/wanvDPRdkkzfKyaJuIJlTDaCg63AY5sDkTW2I57iI/nJ3haSeidfQKz -39EfbnM1A/YprIakafjAu3frBIsjBVcxwGihZmL/YriTHjOggJF841kT5zFkkv2L -/530Wk6xAoGBAOqZLZ4DIi/zLndEOz1mRbUfjc7GQUdYplBnBwJ22VdS0P4TOXnd -UbJth2MA92NM7ocTYVFl4TVIZY/Y+Prxk7KQdHWzR7JPpKfx9OEVgtSqV0vF9eGI -rKp79Y1T4Mvc3UcQCXX6TP7nHLihEzpS8odm2LW4txrOiLsn4Fq/IWrLAoGBAOVv -6U4tm3lImotUupKLZPKEBYwruo9qRysoug9FiorP4TjaBVOfltiiHbAQD6aGfVtN -SZpZZtrs17wL7Xl4db5asgMcZd+8Hkfo5siR7AuGW9FZloOjDcXb5wCh9EvjJ74J -Cjw7RqyVymq9t7IP6wnVwj5Ck48YhlOZCz/mzlnNAoGAWq7NYFgLvgc9feLFF23S -IjpJQZWHJEITP98jaYNxbfzYRm49+GphqxwFinKULjFNvq7yHlnIXSVYBOu1CqOZ -GRwXuGuNmlKI7lZr9xmukfAqgGLMMdr4C4qRF4lFyufcLRz42z7exmWlx4ST/yaT -E13hBRWayeTuG5JFei6Jh1MCgYEAqmX4LyC+JFBgvvQZcLboLRkSCa18bADxhENG -FAuAvmFvksqRRC71WETmqZj0Fqgxt7pp3KFjO1rFSprNLvbg85PmO1s+6fCLyLpX -lESTu2d5D71qhK93jigooxalGitFm+SY3mzjq0/AOpBWOn+J/w7rqVPGxXLgaHv0 -l+vx+00CgYBOvo9/ImjwYii2jFl+sHEoCzlvpITi2temRlT2j6ulSjCLJgjwEFw9 -8e+vvfQumQOsutakUVyURrkMGNDiNlIv8kv5YLCCkrwN22E6Ghyi69MJUvHQXkc/ -QZhjn/luyfpB5f/BeHFS2bkkxAXo+cfG45ApY3Qfz6/7o+H+vDa6/A== ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca.pem b/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca.pem deleted file mode 100644 index 00b31d8a4..000000000 --- a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDAzCCAeugAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowPDE6MDgGA1UEAwwxTXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DQV9DZXJ0aWZpY2F0ZTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANJBlAYvTQ6euY4HcSn4syH7kq9s -KcG+OMjPUrj+KFEElCzgNuIhaS0f3ORQGB1PNcvVcfdXUI3WX332gWbr9s1b7Xl1 -JKJfDXs+26Cm6NhONTE3sPHnbTSmQEFb52hwAtjQmcY3IQs1AgxKFFHJfnCBEWfE -ePBQaiuYk1XDESMdWpMLrPnYQaj9MpAOUxjlmZCayzPWlF0j0IWvfsF5TqZL7tFK -9p5F/DzyZ4n1mqPVEoUmq5ZdSKj2TQkpWTMHBWHEDQQqXbyE1FGJR7zEUFeuG1KT -sVBg7iZEC93SygZTbgUZSQXIwQCsO6xZ8MB2XDJkPbWp/3Wc6c8I6P09F48CAwEA -AaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADKz6bIpP5anp -GgLB0jkclRWuMlS4qqIt4itSsMXPJ/ezpHwECixmgW2TIQl6S1woRkUeMxhT2/Ay -Sn/7aKxuzRagyE5NEGOvrOuAP5RO2ZdNJ/X3/Rh533fK1sOTEEbSsWUvW6iSkZef -rsfZBVP32xBhRWkKRdLeLB4W99ADMa0IrTmZPCXHSSE2V4e1o6zWLXcOZeH1Qh8N -SkelBweR+8r1Fbvy1r3s7eH7DCbYoGEDVLQGOLvzHKBisQHmoDnnF5E9g1eeNRdg -o+vhOKfYCOzeNREJIqS42PHcGhdNRk90ycigPmfUJclz1mDHoMjKR2S5oosTpr65 -tNPx3CL7GA== ------END CERTIFICATE----- diff --git a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-cert.pem b/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-cert.pem deleted file mode 100644 index aad1404ca..000000000 --- a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIBAzANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0N1oXDTMwMDYwOTAzMzg0N1owQDE+MDwGA1UEAww1TXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DbGllbnRfQ2VydGlmaWNhdGUw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVYSWpOvCTupz82fc85Opv -EQ7rkB8X2oOMyBCpkyHKBIr1ZQgRDWBp9UVOASq3GnSElm6+T3Kb1QbOffa8GIlw -sjAueKdq5L2eSkmPIEQ7eoO5kEW+4V866hE1LeL/PmHg2lGP0iqZiJYtElhHNQO8 -3y9I7cm3xWMAA3SSWikVtpJRn3qIp2QSrH+tK+/HHbE5QwtPxdir4ULSCSOaM5Yh -Wi5Oto88TZqe1v7SXC864JVvO4LuS7TuSreCdWZyPXTJFBFeCEWSAxonKZrqHbBe -CwKML6/0NuzjaQ51c2tzmVI6xpHj3nnu4cSRx6Jf9WBm+35vm0wk4pohX3ptdzeV -AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAByQ5zSNeFUH -Aw7JlpZHtHaSEeiiyBHke20ziQ07BK1yi/ms2HAWwQkpZv149sjNuIRH8pkTmkZn -g8PDzSefjLbC9AsWpWV0XNV22T/cdobqLqMBDDZ2+5bsV+jTrOigWd9/AHVZ93PP -IJN8HJn6rtvo2l1bh/CdsX14uVSdofXnuWGabNTydqtMvmCerZsdf6qKqLL+PYwm -RDpgWiRUY7KPBSSlKm/9lJzA+bOe4dHeJzxWFVCJcbpoiTFs1je1V8kKQaHtuW39 -ifX6LTKUMlwEECCbDKM8Yq2tm8NjkjCcnFDtKg8zKGPUu+jrFMN5otiC3wnKcP7r -O9EkaPcgYH8= ------END CERTIFICATE----- diff --git a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-key.pem b/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-key.pem deleted file mode 100644 index 6789d0291..000000000 --- a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA1WElqTrwk7qc/Nn3POTqbxEO65AfF9qDjMgQqZMhygSK9WUI -EQ1gafVFTgEqtxp0hJZuvk9ym9UGzn32vBiJcLIwLninauS9nkpJjyBEO3qDuZBF -vuFfOuoRNS3i/z5h4NpRj9IqmYiWLRJYRzUDvN8vSO3Jt8VjAAN0klopFbaSUZ96 -iKdkEqx/rSvvxx2xOUMLT8XYq+FC0gkjmjOWIVouTraPPE2antb+0lwvOuCVbzuC -7ku07kq3gnVmcj10yRQRXghFkgMaJyma6h2wXgsCjC+v9Dbs42kOdXNrc5lSOsaR -49557uHEkceiX/VgZvt+b5tMJOKaIV96bXc3lQIDAQABAoIBAF7yjXmSOn7h6P0y -WCuGiTLG2mbDiLJqj2LTm2Z5i+2Cu/qZ7E76Ls63TxF4v3MemH5vGfQhEhR5ZD/6 -GRJ1sKKvB3WGRqjwA9gtojHH39S/nWGy6vYW/vMOOH37XyjIr3EIdIaUtFQBTSHd -Kd71niYrAbVn6fyWHolhADwnVmTMOl5OOAhCdEF4GN3b5aIhIu8BJ7EUzTtHBJIj -CAEfjZFjDs1y1cIgGFJkuIQxMfCpq5recU2qwip7YO6fk//WEjOPu7kSf5IEswL8 -jg1dea9rGBV6KaD2xsgsC6Ll6Sb4BbsrHMfflG3K2Lk3RdVqqTFp1Fn1PTLQE/1S -S/SZPYECgYEA9qYcHKHd0+Q5Ty5wgpxKGa4UCWkpwvfvyv4bh8qlmxueB+l2AIdo -ZvkM8gTPagPQ3WypAyC2b9iQu70uOJo1NizTtKnpjDdN1YpDjISJuS/P0x73gZwy -gmoM5AzMtN4D6IbxXtXnPaYICvwLKU80ouEN5ZPM4/ODLUu6gsp0v2UCgYEA3Xgi -zMC4JF0vEKEaK0H6QstaoXUmw/lToZGH3TEojBIkb/2LrHUclygtONh9kJSFb89/ -jbmRRLAOrx3HZKCNGUmF4H9k5OQyAIv6OGBinvLGqcbqnyNlI+Le8zxySYwKMlEj -EMrBCLmSyi0CGFrbZ3mlj/oCET/ql9rNvcK+DHECgYAEx5dH3sMjtgp+RFId1dWB -xePRgt4yTwewkVgLO5wV82UOljGZNQaK6Eyd7AXw8f38LHzh+KJQbIvxd2sL4cEi -OaAoohpKg0/Y0YMZl//rPMf0OWdmdZZs/I0fZjgZUSwWN3c59T8z7KG/RL8an9RP -S7kvN7wCttdV61/D5RR6GQKBgDxCe/WKWpBKaovzydMLWLTj7/0Oi0W3iXHkzzr4 -LTgvl4qBSofaNbVLUUKuZTv5rXUG2IYPf99YqCYtzBstNDc1MiAriaBeFtzfOW4t -i6gEFtoLLbuvPc3N5Sv5vn8Ug5G9UfU3td5R4AbyyCcoUZqOFuZd+EIJSiOXfXOs -kVmBAoGBAIU9aPAqhU5LX902oq8KsrpdySONqv5mtoStvl3wo95WIqXNEsFY60wO -q02jKQmJJ2MqhkJm2EoF2Mq8+40EZ5sz8LdgeQ/M0yQ9lAhPi4rftwhpe55Ma9dk -SE9X1c/DMCBEaIjJqVXdy0/EeArwpb8sHkguVVAZUWxzD+phm1gs ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/mongodb.pem b/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/mongodb.pem deleted file mode 100644 index 1fe94891a..000000000 --- a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/mongodb.pem +++ /dev/null @@ -1,46 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua -NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z -G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL -JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB -4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy -TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9 -AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6 -zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI -hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F -sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD -3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR -+DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC -dN/klu446fI= ------END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj -U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho -XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT -29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX -NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv -f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn -WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP -PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV -4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS -VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk -Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb -SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq -EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx -VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH -cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0 -ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h -J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ -h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K -eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq -dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD -PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes -Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2 -/UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH -PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd -JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/private_key.pem b/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/private_key.pem deleted file mode 100644 index 8fbf6bdec..000000000 --- a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/private_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA1zVmMhPqpSPMmYkKh5wwlRD5XuS8YWJKEM6tjFx61VK8qxHE -YngkC2KnL5EuKAjQZIF3tJskwt0hAat047CCCZxrkNEpbVvSnvnk+A/8bg/Ww1n3 -qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XAPy7ZjzecAF9SDV6WSiPeAxUX2+hN -dId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGwcnkKCUikiBqr2ijSIgvRtBfZ9fBG -jFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtPnBtTytmqTy9V/BRgsVKUoksm6wsx -kUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h+wIDAQABAoIBAEQcrHmRACTADdNS -IjkFYALt2l8EOfMAbryfDSJtapr1kqz59JPNvmq0EIHnixo0n/APYdmReLML1ZR3 -tYkSpjVwgkLVUC1CcIjMQoGYXaZf8PLnGJHZk45RR8m6hsTV0mQ5bfBaeVa2jbma -OzJMjcnxg/3l9cPQZ2G/3AUfEPccMxOXp1KRz3mUQcGnKJGtDbN/kfmntcwYoxaE -Zg4RoeKAoMpK1SSHAiJKe7TnztINJ7uygR9XSzNd6auY8A3vomSIjpYO7XL+lh7L -izm4Ir3Gb/eCYBvWgQyQa2KCJgK/sQyEs3a09ngofSEUhQJQYhgZDwUj+fDDOGqj -hCZOA8ECgYEA+ZWuHdcUQ3ygYhLds2QcogUlIsx7C8n/Gk/FUrqqXJrTkuO0Eqqa -B47lCITvmn2zm0ODfSFIARgKEUEDLS/biZYv7SUTrFqBLcet+aGI7Dpv91CgB75R -tNzcIf8VxoiP0jPqdbh9mLbbxGi5Uc4p9TVXRljC4hkswaouebWee0sCgYEA3L2E -YB3kiHrhPI9LHS5Px9C1w+NOu5wP5snxrDGEgaFCvL6zgY6PflacppgnmTXl8D1x -im0IDKSw5dP3FFonSVXReq3CXDql7UnhfTCiLDahV7bLxTH42FofcBpDN3ERdOal -58RwQh6VrLkzQRVoObo+hbGlFiwwSAfQC509FhECgYBsRSBpVXo25IN2yBRg09cP -+gdoFyhxrsj5kw1YnB13WrrZh+oABv4WtUhp77E5ZbpaamlKCPwBbXpAjeFg4tfr -0bksuN7V79UGFQ9FsWuCfr8/nDwv38H2IbFlFhFONMOfPmJBey0Q6JJhm8R41mSh -OOiJXcv85UrjIH5U0hLUDQKBgQDVLOU5WcUJlPoOXSgiT0ZW5xWSzuOLRUUKEf6l -19BqzAzCcLy0orOrRAPW01xylt2v6/bJw1Ahva7k1ZZo/kOwjANYoZPxM+ZoSZBN -MXl8j2mzZuJVV1RFxItV3NcLJNPB/Lk+IbRz9kt/2f9InF7iWR3mSU/wIM6j0X+2 -p6yFsQKBgQCM/ldWb511lA+SNkqXB2P6WXAgAM/7+jwsNHX2ia2Ikufm4SUEKMSv -mti/nZkHDHsrHU4wb/2cOAywMELzv9EHzdcoenjBQP65OAc/1qWJs+LnBcCXfqKk -aHjEZW6+brkHdRGLLY3YAHlt/AUL+RsKPJfN72i/FSpmu+52G36eeQ== ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/public_key.pem b/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/public_key.pem deleted file mode 100644 index f9772b533..000000000 --- a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/public_key.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1zVmMhPqpSPMmYkKh5ww -lRD5XuS8YWJKEM6tjFx61VK8qxHEYngkC2KnL5EuKAjQZIF3tJskwt0hAat047CC -CZxrkNEpbVvSnvnk+A/8bg/Ww1n3qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XA -Py7ZjzecAF9SDV6WSiPeAxUX2+hNdId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGw -cnkKCUikiBqr2ijSIgvRtBfZ9fBGjFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtP -nBtTytmqTy9V/BRgsVKUoksm6wsxkUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h -+wIDAQAB ------END PUBLIC KEY----- diff --git a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/server-cert.pem b/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/server-cert.pem deleted file mode 100644 index a2f9688df..000000000 --- a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/server-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua -NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z -G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL -JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB -4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy -TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9 -AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6 -zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI -hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F -sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD -3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR -+DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC -dN/klu446fI= ------END CERTIFICATE----- diff --git a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/server-key.pem b/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/server-key.pem deleted file mode 100644 index a1dfd5f78..000000000 --- a/apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/server-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj -U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho -XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT -29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX -NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv -f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn -WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP -PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV -4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS -VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk -Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb -SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq -EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx -VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH -cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0 -ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h -J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ -h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K -eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq -dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD -PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes -Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2 -/UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH -PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd -JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_mysql/.gitignore b/apps/emqx_auth_mysql/.gitignore deleted file mode 100644 index bc6fa0f2f..000000000 --- a/apps/emqx_auth_mysql/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -.eunit -deps -*.so -.iml -.idea -*.o -*.beam -*.plt -erl_crash.dump -ebin -rel/example_project -.concrete/DEV_MODE -.rebar -.erlang.mk/ -emqx_auth_mysql.d -ct.coverdata -logs/ -test/ct.cover.spec -test/*.beam -cover/ -eunit.coverdata -data -.placeholder -_build/ -rebar.lock -erlang.mk -rebar3.crashdump -etc/emqx_auth_mysql.conf.rendered -.rebar3/ -*.swp -.DS_Store diff --git a/apps/emqx_auth_mysql/README.md b/apps/emqx_auth_mysql/README.md deleted file mode 100644 index e55a2103f..000000000 --- a/apps/emqx_auth_mysql/README.md +++ /dev/null @@ -1,167 +0,0 @@ -emqx_auth_mysql -=============== - -Authentication, ACL with MySQL Database. - -Notice: changed mysql driver to [mysql-otp](https://github.com/mysql-otp/mysql-otp). - -Features ---------- - -- Full *Authentication*, *Superuser*, *ACL* support -- IPv4, IPv6 and TLS support -- Connection pool by [ecpool](https://github.com/emqx/ecpool) -- Completely cover MySQL 5.7, MySQL 8 in our tests - -Build Plugin -------------- - -make && make tests - -Configure Plugin ----------------- - -File: etc/emqx_auth_mysql.conf - -``` -## MySQL server address. -## -## Value: Port | IP:Port -## -## Examples: 3306, 127.0.0.1:3306, localhost:3306 -auth.mysql.server = 127.0.0.1:3306 - -## MySQL pool size. -## -## Value: Number -auth.mysql.pool = 8 - -## MySQL username. -## -## Value: String -## auth.mysql.username = - -## MySQL Password. -## -## Value: String -## auth.mysql.password = - -## MySQL database. -## -## Value: String -auth.mysql.database = mqtt - -## Variables: %u = username, %c = clientid - -## Authentication query. -## -## Note that column names should be 'password' and 'salt' (if used). -## In case column names differ in your DB - please use aliases, -## e.g. "my_column_name as password". -## -## Value: SQL -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -auth.mysql.auth_query = select password from mqtt_user where username = '%u' limit 1 -## auth.mysql.auth_query = select password_hash as password from mqtt_user where username = '%u' limit 1 - -## Password hash. -## -## Value: plain | md5 | sha | sha256 | bcrypt -auth.mysql.password_hash = sha256 - -## sha256 with salt prefix -## auth.mysql.password_hash = salt,sha256 - -## bcrypt with salt only prefix -## auth.mysql.password_hash = salt,bcrypt - -## sha256 with salt suffix -## auth.mysql.password_hash = sha256,salt - -## pbkdf2 with macfun iterations dklen -## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512 -## auth.mysql.password_hash = pbkdf2,sha256,1000,20 - -## Superuser query. -## -## Value: SQL -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -auth.mysql.super_query = select is_superuser from mqtt_user where username = '%u' limit 1 - -## ACL query. -## -## Value: SQL -## -## Variables: -## - %a: ipaddr -## - %u: username -## - %c: clientid -## Note: You can add the 'ORDER BY' statement to control the rules match order -auth.mysql.acl_query = select allow, ipaddr, username, clientid, access, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c' - -``` - -Import mqtt.sql ---------------- - -Import mqtt.sql into your database. - -Load Plugin ------------ - -./bin/emqx_ctl plugins load emqx_auth_mysql - -Auth Table ----------- - -Notice: This is a demo table. You could authenticate with any user table. - -```sql -CREATE TABLE `mqtt_user` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `username` varchar(100) DEFAULT NULL, - `password` varchar(100) DEFAULT NULL, - `salt` varchar(35) DEFAULT NULL, - `is_superuser` tinyint(1) DEFAULT 0, - `created` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `mqtt_username` (`username`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -``` - -ACL Table ----------- - -```sql -CREATE TABLE `mqtt_acl` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `allow` int(1) DEFAULT NULL COMMENT '0: deny, 1: allow', - `ipaddr` varchar(60) DEFAULT NULL COMMENT 'IpAddress', - `username` varchar(100) DEFAULT NULL COMMENT 'Username', - `clientid` varchar(100) DEFAULT NULL COMMENT 'ClientId', - `access` int(2) NOT NULL COMMENT '1: subscribe, 2: publish, 3: pubsub', - `topic` varchar(100) NOT NULL DEFAULT '' COMMENT 'Topic Filter', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -``` - -License -------- - -Apache License Version 2.0 - -Author ------- - -EMQ X Team. diff --git a/apps/emqx_auth_mysql/etc/emqx_auth_mysql.conf b/apps/emqx_auth_mysql/etc/emqx_auth_mysql.conf deleted file mode 100644 index 1c3d40059..000000000 --- a/apps/emqx_auth_mysql/etc/emqx_auth_mysql.conf +++ /dev/null @@ -1,131 +0,0 @@ -##-------------------------------------------------------------------- -## MySQL Auth/ACL Plugin -##-------------------------------------------------------------------- - -## MySQL server address. -## -## Value: Port | IP:Port -## -## Examples: 3306, 127.0.0.1:3306, localhost:3306 -auth.mysql.server = "127.0.0.1:3306" - -## MySQL pool size. -## -## Value: Number -auth.mysql.pool = 8 - -## MySQL username. -## -## Value: String -#auth.mysql.username = - -## MySQL password. -## -## Value: String -#auth.mysql.password = - -## MySQL database. -## -## Value: String -auth.mysql.database = mqtt - -## MySQL query timeout -## -## Value: Duration -## auth.mysql.query_timeout = 5s - -## Variables: %u = username, %c = clientid - -## Authentication query. -## -## Note that column names should be 'password' and 'salt' (if used). -## In case column names differ in your DB - please use aliases, -## e.g. "my_column_name as password". -## -## Value: SQL -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -auth.mysql.auth_query = "select password from mqtt_user where username = '%u' limit 1" -## auth.mysql.auth_query = select password_hash as password from mqtt_user where username = '%u' limit 1 - -## Password hash. -## -## Value: plain | md5 | sha | sha256 | bcrypt -auth.mysql.password_hash = sha256 - -## sha256 with salt prefix -## auth.mysql.password_hash = "salt,sha256" - -## bcrypt with salt only prefix -## auth.mysql.password_hash = "salt,bcrypt" - -## sha256 with salt suffix -## auth.mysql.password_hash = "sha256,salt" - -## pbkdf2 with macfun iterations dklen -## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512 -## auth.mysql.password_hash = "pbkdf2,sha256,1000,20" - -## Superuser query. -## -## Value: SQL -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -auth.mysql.super_query = "select is_superuser from mqtt_user where username = '%u' limit 1" - -## ACL query. -## -## Value: SQL -## -## Variables: -## - %a: ipaddr -## - %u: username -## - %c: clientid -## -## Note: You can add the 'ORDER BY' statement to control the rules match order -auth.mysql.acl_query = "select allow, ipaddr, username, clientid, access, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'" - -## Mysql ssl configuration. -## -## Value: on | off -## auth.mysql.ssl.enable = off - -## CA certificate. -## -## Value: File -#auth.mysql.ssl.cacertfile = /path/to/ca.pem - -## Client ssl certificate. -## -## Value: File -#auth.mysql.ssl.certfile = /path/to/your/clientcert.pem - -## Client ssl keyfile. -## -## Value: File -#auth.mysql.ssl.keyfile = /path/to/your/clientkey.pem - -## In mode verify_none the default behavior is to allow all x509-path -## validation errors. -## -## Value: true | false -#auth.mysql.ssl.verify = false - -## If not specified, the server's names returned in server's certificate is validated against -## what's provided `auth.mysql.server` config's host part. -## Setting to 'disable' will make EMQ X ignore unmatched server names. -## If set with a host name, the server's names returned in server's certificate is validated -## against this value. -## -## Value: String | disable -## auth.mysql.ssl.server_name_indication = disable diff --git a/apps/emqx_auth_mysql/include/emqx_auth_mysql.hrl b/apps/emqx_auth_mysql/include/emqx_auth_mysql.hrl deleted file mode 100644 index fca431e81..000000000 --- a/apps/emqx_auth_mysql/include/emqx_auth_mysql.hrl +++ /dev/null @@ -1,23 +0,0 @@ - --define(APP, emqx_auth_mysql). - --record(auth_metrics, { - success = 'client.auth.success', - failure = 'client.auth.failure', - ignore = 'client.auth.ignore' - }). - --record(acl_metrics, { - allow = 'client.acl.allow', - deny = 'client.acl.deny', - ignore = 'client.acl.ignore' - }). - --define(METRICS(Type), tl(tuple_to_list(#Type{}))). --define(METRICS(Type, K), #Type{}#Type.K). - --define(AUTH_METRICS, ?METRICS(auth_metrics)). --define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). - --define(ACL_METRICS, ?METRICS(acl_metrics)). --define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). diff --git a/apps/emqx_auth_mysql/mqtt.sql b/apps/emqx_auth_mysql/mqtt.sql deleted file mode 100644 index 9635bee58..000000000 --- a/apps/emqx_auth_mysql/mqtt.sql +++ /dev/null @@ -1,41 +0,0 @@ - -DROP TABLE IF EXISTS `mqtt_acl`; - -CREATE TABLE `mqtt_acl` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `allow` int(1) DEFAULT NULL COMMENT '0: deny, 1: allow', - `ipaddr` varchar(60) DEFAULT NULL COMMENT 'IpAddress', - `username` varchar(100) DEFAULT NULL COMMENT 'Username', - `clientid` varchar(100) DEFAULT NULL COMMENT 'ClientId', - `access` int(2) NOT NULL COMMENT '1: subscribe, 2: publish, 3: pubsub', - `topic` varchar(100) NOT NULL DEFAULT '' COMMENT 'Topic Filter', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -LOCK TABLES `mqtt_acl` WRITE; - -INSERT INTO `mqtt_acl` (`id`, `allow`, `ipaddr`, `username`, `clientid`, `access`, `topic`) -VALUES - (1,1,NULL,'$all',NULL,2,'#'), - (2,0,NULL,'$all',NULL,1,'$SYS/#'), - (3,0,NULL,'$all',NULL,1,'eq #'), - (4,1,'127.0.0.1',NULL,NULL,2,'$SYS/#'), - (5,1,'127.0.0.1',NULL,NULL,2,'#'), - (6,1,NULL,'dashboard',NULL,1,'$SYS/#'); - -UNLOCK TABLES; - - -DROP TABLE IF EXISTS `mqtt_user`; - -CREATE TABLE `mqtt_user` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `username` varchar(100) DEFAULT NULL, - `password` varchar(100) DEFAULT NULL, - `salt` varchar(35) DEFAULT NULL, - `is_superuser` tinyint(1) DEFAULT 0, - `created` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `mqtt_username` (`username`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; - diff --git a/apps/emqx_auth_mysql/priv/emqx_auth_mysql.schema b/apps/emqx_auth_mysql/priv/emqx_auth_mysql.schema deleted file mode 100644 index fba25f41f..000000000 --- a/apps/emqx_auth_mysql/priv/emqx_auth_mysql.schema +++ /dev/null @@ -1,156 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_auth_mysql config mapping -{mapping, "auth.mysql.server", "emqx_auth_mysql.server", [ - {default, {"127.0.0.1", 3306}}, - {datatype, [integer, ip, string]} -]}. - -{mapping, "auth.mysql.pool", "emqx_auth_mysql.server", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "auth.mysql.username", "emqx_auth_mysql.server", [ - {default, ""}, - {datatype, string} -]}. - -{mapping, "auth.mysql.password", "emqx_auth_mysql.server", [ - {default, ""}, - {datatype, string} -]}. - -{mapping, "auth.mysql.database", "emqx_auth_mysql.server", [ - {default, "mqtt"}, - {datatype, string} -]}. - -{mapping, "auth.mysql.query_timeout", "emqx_auth_mysql.server", [ - {default, ""}, - {datatype, string} -]}. - -{mapping, "auth.mysql.ssl.enable", "emqx_auth_mysql.server", [ - {default, off}, - {datatype, flag} -]}. - -{mapping, "auth.mysql.ssl.cafile", "emqx_auth_mysql.server", [ - {datatype, string} -]}. - -{mapping, "auth.mysql.ssl.cacertfile", "emqx_auth_mysql.server", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.mysql.ssl.certfile", "emqx_auth_mysql.server", [ - {datatype, string} -]}. - -{mapping, "auth.mysql.ssl.keyfile", "emqx_auth_mysql.server", [ - {datatype, string} -]}. - -{mapping, "auth.mysql.ssl.verify", "emqx_auth_mysql.server", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "auth.mysql.ssl.server_name_indication", "emqx_auth_mysql.server", [ - {datatype, string} -]}. - -{translation, "emqx_auth_mysql.server", fun(Conf) -> - {MyHost, MyPort} = - case cuttlefish:conf_get("auth.mysql.server", Conf) of - {Ip, Port} -> {Ip, Port}; - S -> case string:tokens(S, ":") of - [Domain] -> {Domain, 3306}; - [Domain, Port] -> {Domain, list_to_integer(Port)} - end - end, - Pool = cuttlefish:conf_get("auth.mysql.pool", Conf), - Username = cuttlefish:conf_get("auth.mysql.username", Conf), - Passwd = cuttlefish:conf_get("auth.mysql.password", Conf), - DB = cuttlefish:conf_get("auth.mysql.database", Conf), - Timeout = case cuttlefish:conf_get("auth.mysql.query_timeout", Conf) of - "" -> 300000; - Duration -> - case cuttlefish_duration:parse(Duration, ms) of - {error, Reason} -> error(Reason); - Ms when is_integer(Ms) -> Ms - end - end, - Options = [{pool_size, Pool}, - {auto_reconnect, 1}, - {host, MyHost}, - {port, MyPort}, - {user, Username}, - {password, Passwd}, - {database, DB}, - {encoding, utf8}, - {query_timeout, Timeout}, - {keep_alive, true}], - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - Options1 = - case cuttlefish:conf_get("auth.mysql.ssl.enable", Conf) of - true -> - %% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 - CA = cuttlefish:conf_get( - "auth.mysql.ssl.cacertfile", Conf, - cuttlefish:conf_get("auth.mysql.ssl.cafile", Conf, undefined) - ), - Cert = cuttlefish:conf_get("auth.mysql.ssl.certfile", Conf, undefined), - Key = cuttlefish:conf_get("auth.mysql.ssl.keyfile", Conf, undefined), - Verify = case cuttlefish:conf_get("auth.mysql.ssl.verify", Conf, false) of - true -> verify_peer; - false -> verify_none - end, - SNI = case cuttlefish:conf_get("auth.mysql.ssl.server_name_indication", Conf, undefined) of - "disable" -> disable; - SNI0 -> SNI0 - end, - Options ++ [{ssl, Filter([{server_name_indication, SNI}, - {cacertfile, CA}, - {certfile, Cert}, - {keyfile, Key}, - {verify, Verify} - ]) - }]; - _ -> - Options - end, - case inet:parse_address(MyHost) of - {ok, IpAddr} when tuple_size(IpAddr) =:= 8 -> - [{tcp_options, [inet6]} | Options1]; - _ -> - Options1 - end -end}. - -{mapping, "auth.mysql.auth_query", "emqx_auth_mysql.auth_query", [ - {datatype, string} -]}. - -{mapping, "auth.mysql.password_hash", "emqx_auth_mysql.password_hash", [ - {datatype, string} -]}. - -{mapping, "auth.mysql.super_query", "emqx_auth_mysql.super_query", [ - {datatype, string} -]}. - -{mapping, "auth.mysql.acl_query", "emqx_auth_mysql.acl_query", [ - {datatype, string} -]}. - -{translation, "emqx_auth_mysql.password_hash", fun(Conf) -> - HashValue = cuttlefish:conf_get("auth.mysql.password_hash", Conf), - case string:tokens(HashValue, ",") of - [Hash] -> list_to_atom(Hash); - [Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)}; - [Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), list_to_integer(Iterations), list_to_integer(Dklen)}; - _ -> plain - end -end}. diff --git a/apps/emqx_auth_mysql/src/emqx_acl_mysql.erl b/apps/emqx_auth_mysql/src/emqx_acl_mysql.erl deleted file mode 100644 index ef4acea94..000000000 --- a/apps/emqx_auth_mysql/src/emqx_acl_mysql.erl +++ /dev/null @@ -1,119 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_acl_mysql). - --include("emqx_auth_mysql.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - -%% ACL Callbacks --export([ register_metrics/0 - , check_acl/5 - , description/0 - ]). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). - -check_acl(ClientInfo, PubSub, Topic, NoMatchAction, #{pool := Pool} = State) -> - case do_check_acl(Pool, ClientInfo, PubSub, Topic, NoMatchAction, State) of - ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok; - {stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow}; - {stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny} - end. - -do_check_acl(_Pool, #{username := <<$$, _/binary>>}, _PubSub, _Topic, _NoMatchAction, _State) -> - ok; -do_check_acl(Pool, ClientInfo, PubSub, Topic, _NoMatchAction, #{acl_query := {AclSql, AclParams}}) -> - case emqx_auth_mysql_cli:query(Pool, AclSql, AclParams, ClientInfo) of - {ok, _Columns, []} -> ok; - {ok, _Columns, Rows} -> - Rules = filter(PubSub, compile(Rows)), - case match(ClientInfo, Topic, Rules) of - {matched, allow} -> {stop, allow}; - {matched, deny} -> {stop, deny}; - nomatch -> ok - end; - {error, Reason} -> - ?LOG(error, "[MySQL] do_check_acl error: ~p~n", [Reason]), - ok - end. - -match(_ClientInfo, _Topic, []) -> - nomatch; - -match(ClientInfo, Topic, [Rule|Rules]) -> - case emqx_access_rule:match(ClientInfo, Topic, Rule) of - nomatch -> - match(ClientInfo, Topic, Rules); - {matched, AllowDeny} -> - {matched, AllowDeny} - end. - -filter(PubSub, Rules) -> - [Term || Term = {_, _, Access, _} <- Rules, - Access =:= PubSub orelse Access =:= pubsub]. - -compile(Rows) -> - compile(Rows, []). -compile([], Acc) -> - Acc; -compile([[Allow, IpAddr, Username, ClientId, Access, Topic]|T], Acc) -> - Who = who(IpAddr, Username, ClientId), - Term = {allow(Allow), Who, access(Access), [topic(Topic)]}, - compile(T, [emqx_access_rule:compile(Term) | Acc]). - -who(_, <<"$all">>, _) -> - all; -who(null, null, null) -> - throw(undefined_who); -who(CIDR, Username, ClientId) -> - Cols = [{ipaddr, b2l(CIDR)}, {user, Username}, {client, ClientId}], - case [{C, V} || {C, V} <- Cols, not empty(V)] of - [Who] -> Who; - Conds -> {'and', Conds} - end. - -allow(1) -> allow; -allow(0) -> deny; -allow(<<"1">>) -> allow; -allow(<<"0">>) -> deny. - -access(1) -> subscribe; -access(2) -> publish; -access(3) -> pubsub; -access(<<"1">>) -> subscribe; -access(<<"2">>) -> publish; -access(<<"3">>) -> pubsub. - -topic(<<"eq ", Topic/binary>>) -> - {eq, Topic}; -topic(Topic) -> - Topic. - -description() -> - "ACL with Mysql". - -b2l(null) -> null; -b2l(B) -> binary_to_list(B). - -empty(null) -> true; -empty("") -> true; -empty(<<>>) -> true; -empty(_) -> false. diff --git a/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src b/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src deleted file mode 100644 index 8a0d116cc..000000000 --- a/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_auth_mysql, - [{description, "EMQ X Authentication/ACL with MySQL"}, - {vsn, "4.4.0"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_auth_mysql_sup]}, - {applications, [kernel,stdlib,mysql,ecpool]}, - {mod, {emqx_auth_mysql_app,[]}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-auth-mysql"} - ]} - ]}. diff --git a/apps/emqx_auth_mysql/src/emqx_auth_mysql.erl b/apps/emqx_auth_mysql/src/emqx_auth_mysql.erl deleted file mode 100644 index eba7ef081..000000000 --- a/apps/emqx_auth_mysql/src/emqx_auth_mysql.erl +++ /dev/null @@ -1,91 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mysql). - --include("emqx_auth_mysql.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). --include_lib("emqx/include/types.hrl"). - --export([ register_metrics/0 - , check/3 - , description/0 - ]). - --define(EMPTY(Username), (Username =:= undefined orelse Username =:= <<>>)). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). - -check(ClientInfo = #{password := Password}, AuthResult, - #{auth_query := {AuthSql, AuthParams}, - super_query := SuperQuery, - hash_type := HashType, - pool := Pool}) -> - CheckPass = case emqx_auth_mysql_cli:query(Pool, AuthSql, AuthParams, ClientInfo) of - {ok, [<<"password">>], [[PassHash]]} -> - check_pass({PassHash, Password}, HashType); - {ok, [<<"password">>, <<"salt">>], [[PassHash, Salt]]} -> - check_pass({PassHash, Salt, Password}, HashType); - {ok, _Columns, []} -> - {error, not_found}; - {error, Reason} -> - ?LOG(error, "[MySQL] query '~p' failed: ~p", [AuthSql, Reason]), - {error, Reason} - end, - case CheckPass of - ok -> - emqx_metrics:inc(?AUTH_METRICS(success)), - {stop, AuthResult#{is_superuser => is_superuser(Pool, SuperQuery, ClientInfo), - anonymous => false, - auth_result => success}}; - {error, not_found} -> - emqx_metrics:inc(?AUTH_METRICS(ignore)), ok; - {error, ResultCode} -> - ?LOG(error, "[MySQL] Auth from mysql failed: ~p", [ResultCode]), - emqx_metrics:inc(?AUTH_METRICS(failure)), - {stop, AuthResult#{auth_result => ResultCode, anonymous => false}} - end. - -%%-------------------------------------------------------------------- -%% Is Superuser? -%%-------------------------------------------------------------------- - --spec(is_superuser(atom(), maybe({string(), list()}), emqx_types:client()) -> boolean()). -is_superuser(_Pool, undefined, _ClientInfo) -> false; -is_superuser(Pool, {SuperSql, Params}, ClientInfo) -> - case emqx_auth_mysql_cli:query(Pool, SuperSql, Params, ClientInfo) of - {ok, [_Super], [[1]]} -> - true; - {ok, [_Super], [[_False]]} -> - false; - {ok, [_Super], []} -> - false; - {error, _Error} -> - false - end. - -check_pass(Password, HashType) -> - case emqx_passwd:check_pass(Password, HashType) of - ok -> ok; - {error, _Reason} -> {error, not_authorized} - end. - -description() -> "Authentication with MySQL". - diff --git a/apps/emqx_auth_mysql/src/emqx_auth_mysql_app.erl b/apps/emqx_auth_mysql/src/emqx_auth_mysql_app.erl deleted file mode 100644 index ec1e25e6b..000000000 --- a/apps/emqx_auth_mysql/src/emqx_auth_mysql_app.erl +++ /dev/null @@ -1,74 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mysql_app). - --behaviour(application). - --emqx_plugin(auth). - --include("emqx_auth_mysql.hrl"). - --import(emqx_auth_mysql_cli, [parse_query/1]). - -%% Application callbacks --export([ start/2 - , prep_stop/1 - , stop/1 - ]). - -%%-------------------------------------------------------------------- -%% Application callbacks -%%-------------------------------------------------------------------- - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_auth_mysql_sup:start_link(), - _ = if_enabled(auth_query, fun load_auth_hook/1), - _ = if_enabled(acl_query, fun load_acl_hook/1), - - {ok, Sup}. - -prep_stop(State) -> - emqx:unhook('client.authenticate', {emqx_auth_mysql, check}), - emqx:unhook('client.check_acl', {emqx_acl_mysql, check_acl}), - State. - -stop(_State) -> - ok. - -load_auth_hook(AuthQuery) -> - ok = emqx_auth_mysql:register_metrics(), - SuperQuery = parse_query(application:get_env(?APP, super_query, undefined)), - {ok, HashType} = application:get_env(?APP, password_hash), - Params = #{auth_query => AuthQuery, - super_query => SuperQuery, - hash_type => HashType, - pool => ?APP}, - emqx:hook('client.authenticate', {emqx_auth_mysql, check, [Params]}). - -load_acl_hook(AclQuery) -> - ok = emqx_acl_mysql:register_metrics(), - emqx:hook('client.check_acl', {emqx_acl_mysql, check_acl, [#{acl_query => AclQuery, pool =>?APP}]}). - -%%-------------------------------------------------------------------- -%% Internal function -%%-------------------------------------------------------------------- - -if_enabled(Cfg, Fun) -> - case application:get_env(?APP, Cfg) of - {ok, Query} -> Fun(parse_query(Query)); - undefined -> ok - end. diff --git a/apps/emqx_auth_mysql/src/emqx_auth_mysql_cli.erl b/apps/emqx_auth_mysql/src/emqx_auth_mysql_cli.erl deleted file mode 100644 index cf3be3426..000000000 --- a/apps/emqx_auth_mysql/src/emqx_auth_mysql_cli.erl +++ /dev/null @@ -1,91 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mysql_cli). - --behaviour(ecpool_worker). - --include("emqx_auth_mysql.hrl"). --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --export([ parse_query/1 - , connect/1 - , query/4 - ]). - -%%-------------------------------------------------------------------- -%% Avoid SQL Injection: Parse SQL to Parameter Query. -%%-------------------------------------------------------------------- - -parse_query(undefined) -> - undefined; -parse_query(Sql) -> - case re:run(Sql, "'%[ucCad]'", [global, {capture, all, list}]) of - {match, Variables} -> - Params = [Var || [Var] <- Variables], - {re:replace(Sql, "'%[ucCad]'", "?", [global, {return, list}]), Params}; - nomatch -> - {Sql, []} - end. - -%%-------------------------------------------------------------------- -%% MySQL Connect/Query -%%-------------------------------------------------------------------- - -connect(Options) -> - case mysql:start_link(Options) of - {ok, Pid} -> {ok, Pid}; - ignore -> {error, ignore}; - {error, Reason = {{_, {error, econnrefused}}, _}} -> - ?LOG(error, "[MySQL] Can't connect to MySQL server: Connection refused."), - {error, Reason}; - {error, Reason = {ErrorCode, _, Error}} -> - ?LOG(error, "[MySQL] Can't connect to MySQL server: ~p - ~p", [ErrorCode, Error]), - {error, Reason}; - {error, Reason} -> - ?LOG(error, "[MySQL] Can't connect to MySQL server: ~p", [Reason]), - {error, Reason} - end. - -query(Pool, Sql, Params, ClientInfo) -> - ecpool:with_client(Pool, fun(C) -> mysql:query(C, Sql, replvar(Params, ClientInfo)) end). - -replvar(Params, ClientInfo) -> - replvar(Params, ClientInfo, []). - -replvar([], _ClientInfo, Acc) -> - lists:reverse(Acc); - -replvar(["'%u'" | Params], ClientInfo, Acc) -> - replvar(Params, ClientInfo, [safe_get(username, ClientInfo) | Acc]); -replvar(["'%c'" | Params], ClientInfo = #{clientid := ClientId}, Acc) -> - replvar(Params, ClientInfo, [ClientId | Acc]); -replvar(["'%a'" | Params], ClientInfo = #{peerhost := IpAddr}, Acc) -> - replvar(Params, ClientInfo, [inet_parse:ntoa(IpAddr) | Acc]); -replvar(["'%C'" | Params], ClientInfo, Acc) -> - replvar(Params, ClientInfo, [safe_get(cn, ClientInfo)| Acc]); -replvar(["'%d'" | Params], ClientInfo, Acc) -> - replvar(Params, ClientInfo, [safe_get(dn, ClientInfo)| Acc]); -replvar([Param | Params], ClientInfo, Acc) -> - replvar(Params, ClientInfo, [Param | Acc]). - -safe_get(K, ClientInfo) -> - bin(maps:get(K, ClientInfo, "undefined")). - -bin(A) when is_atom(A) -> atom_to_binary(A, utf8); -bin(B) when is_binary(B) -> B; -bin(X) -> X. diff --git a/apps/emqx_auth_mysql/src/emqx_auth_mysql_sup.erl b/apps/emqx_auth_mysql/src/emqx_auth_mysql_sup.erl deleted file mode 100644 index 70f4987a3..000000000 --- a/apps/emqx_auth_mysql/src/emqx_auth_mysql_sup.erl +++ /dev/null @@ -1,40 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mysql_sup). - --behaviour(supervisor). - --include("emqx_auth_mysql.hrl"). - --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%%-------------------------------------------------------------------- -%% Supervisor callbacks -%%-------------------------------------------------------------------- - -init([]) -> - %% MySQL Connection Pool. - {ok, Server} = application:get_env(?APP, server), - PoolSpec = ecpool:pool_spec(?APP, ?APP, emqx_auth_mysql_cli, Server), - {ok, {{one_for_one, 10, 100}, [PoolSpec]}}. - diff --git a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE.erl b/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE.erl deleted file mode 100644 index 0ae81435d..000000000 --- a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE.erl +++ /dev/null @@ -1,235 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_mysql_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --define(APP, emqx_auth_mysql). - --include_lib("emqx/include/emqx.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --define(DROP_ACL_TABLE, <<"DROP TABLE IF EXISTS mqtt_acl">>). - --define(CREATE_ACL_TABLE, <<"CREATE TABLE mqtt_acl (" - " id int(11) unsigned NOT NULL AUTO_INCREMENT," - " allow int(1) DEFAULT NULL COMMENT '0: deny, 1: allow'," - " ipaddr varchar(60) DEFAULT NULL COMMENT 'IpAddress'," - " username varchar(100) DEFAULT NULL COMMENT 'Username'," - " clientid varchar(100) DEFAULT NULL COMMENT 'ClientId'," - " access int(2) NOT NULL COMMENT '1: subscribe, 2: publish, 3: pubsub'," - " topic varchar(100) NOT NULL DEFAULT '' COMMENT 'Topic Filter'," - " PRIMARY KEY (`id`)" - ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4">>). - --define(INIT_ACL, <<"INSERT INTO mqtt_acl (id, allow, ipaddr, username, clientid, access, topic)" - "VALUES (1,1,'127.0.0.1','u1','c1',1,'t1')," - "(2,0,'127.0.0.1','u2','c2',1,'t1')," - "(3,1,'10.10.0.110','u1','c1',1,'t1')," - "(4,1,'127.0.0.1','u3','c3',3,'t1')">>). - --define(DROP_AUTH_TABLE, <<"DROP TABLE IF EXISTS `mqtt_user`">>). - --define(CREATE_AUTH_TABLE, <<"CREATE TABLE `mqtt_user` (" - "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," - "`username` varchar(100) DEFAULT NULL," - "`password` varchar(100) DEFAULT NULL," - "`salt` varchar(100) DEFAULT NULL," - "`is_superuser` tinyint(1) DEFAULT 0," - "`created` datetime DEFAULT NULL," - "PRIMARY KEY (`id`)," - "UNIQUE KEY `mqtt_username` (`username`)" - ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4">>). - --define(INIT_AUTH, <<"INSERT INTO mqtt_user (id, is_superuser, username, password, salt)" - "VALUES (1, 1, 'plain', 'plain', 'salt')," - "(2, 0, 'md5', '1bc29b36f623ba82aaf6724fd3b16718', 'salt')," - "(3, 0, 'sha', 'd8f4590320e1343a915b6394170650a8f35d6926', 'salt')," - "(4, 0, 'sha256', '5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e', 'salt')," - "(5, 0, 'pbkdf2_password', 'cdedb5281bb2f801565a1122b2563515', 'ATHENA.MIT.EDUraeburn')," - "(6, 0, 'bcrypt_foo', '$2a$12$sSS8Eg.ovVzaHzi1nUHYK.HbUIOdlQI0iS22Q5rd5z.JVVYH6sfm6', '$2a$12$sSS8Eg.ovVzaHzi1nUHYK.')," - "(7, 0, 'bcrypt', '$2y$16$rEVsDarhgHYB0TGnDFJzyu5f.T.Ha9iXMTk9J36NCMWWM7O16qyaK', 'salt')," - "(8, 0, 'bcrypt_wrong', '$2y$16$rEVsDarhgHYB0TGnDFJzyu', 'salt')">>). - -%%-------------------------------------------------------------------- -%% Setups -%%-------------------------------------------------------------------- - -all() -> - emqx_ct:all(?MODULE). - -init_per_suite(Cfg) -> - emqx_ct_helpers:start_apps([emqx_auth_mysql], fun set_special_configs/1), - init_mysql_data(), - Cfg. - -end_per_suite(_) -> - deinit_mysql_data(), - emqx_ct_helpers:stop_apps([emqx_auth_mysql]), - ok. - -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, acl_nomatch, deny), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins")); - -set_special_configs(_App) -> - ok. - -init_mysql_data() -> - {ok, Pid} = ecpool_worker:client(gproc_pool:pick_worker({ecpool, ?APP})), - %% Users - ok = mysql:query(Pid, ?DROP_AUTH_TABLE), - ok = mysql:query(Pid, ?CREATE_AUTH_TABLE), - ok = mysql:query(Pid, ?INIT_AUTH), - - %% ACLs - ok = mysql:query(Pid, ?DROP_ACL_TABLE), - ok = mysql:query(Pid, ?CREATE_ACL_TABLE), - ok = mysql:query(Pid, ?INIT_ACL). - -deinit_mysql_data() -> - {ok, Pid} = ecpool_worker:client(gproc_pool:pick_worker({ecpool, ?APP})), - ok = mysql:query(Pid, ?DROP_AUTH_TABLE), - ok = mysql:query(Pid, ?DROP_ACL_TABLE). - -%%-------------------------------------------------------------------- -%% Test cases -%%-------------------------------------------------------------------- - -t_check_acl(_) -> - User0 = #{zone => external,peerhost => {127,0,0,1}}, - deny = emqx_access_control:check_acl(User0, subscribe, <<"t1">>), - User1 = #{zone => external, clientid => <<"c1">>, username => <<"u1">>, peerhost => {127,0,0,1}}, - User2 = #{zone => external, clientid => <<"c2">>, username => <<"u2">>, peerhost => {127,0,0,1}}, - allow = emqx_access_control:check_acl(User1, subscribe, <<"t1">>), - deny = emqx_access_control:check_acl(User2, subscribe, <<"t1">>), - - User3 = #{zone => external, peerhost => {10,10,0,110}, clientid => <<"c1">>, username => <<"u1">>}, - User4 = #{zone => external, peerhost => {10,10,10,110}, clientid => <<"c1">>, username => <<"u1">>}, - allow = emqx_access_control:check_acl(User3, subscribe, <<"t1">>), - allow = emqx_access_control:check_acl(User3, subscribe, <<"t1">>), - deny = emqx_access_control:check_acl(User3, subscribe, <<"t2">>),%% nomatch -> ignore -> emqx acl - deny = emqx_access_control:check_acl(User4, subscribe, <<"t1">>),%% nomatch -> ignore -> emqx acl - User5 = #{zone => external, peerhost => {127,0,0,1}, clientid => <<"c3">>, username => <<"u3">>}, - allow = emqx_access_control:check_acl(User5, subscribe, <<"t1">>), - allow = emqx_access_control:check_acl(User5, publish, <<"t1">>). - -t_acl_super(_Config) -> - reload([{password_hash, plain}, - {auth_query, "select password from mqtt_user where username = '%u' limit 1"}]), - {ok, C} = emqtt:start_link([{host, "localhost"}, - {clientid, <<"simpleClient">>}, - {username, <<"plain">>}, - {password, <<"plain">>}]), - {ok, _} = emqtt:connect(C), - timer:sleep(10), - emqtt:subscribe(C, <<"TopicA">>, qos2), - timer:sleep(1000), - emqtt:publish(C, <<"TopicA">>, <<"Payload">>, qos2), - timer:sleep(1000), - receive - {publish, #{payload := Payload}} -> - ?assertEqual(<<"Payload">>, Payload) - after - 1000 -> - ct:fail({receive_timeout, <<"Payload">>}), - ok - end, - emqtt:disconnect(C). - -t_check_auth(_) -> - Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external}, - Md5 = #{clientid => <<"md5">>, username => <<"md5">>, zone => external}, - Sha = #{clientid => <<"sha">>, username => <<"sha">>, zone => external}, - Sha256 = #{clientid => <<"sha256">>, username => <<"sha256">>, zone => external}, - Pbkdf2 = #{clientid => <<"pbkdf2_password">>, username => <<"pbkdf2_password">>, zone => external}, - BcryptFoo = #{clientid => <<"bcrypt_foo">>, username => <<"bcrypt_foo">>, zone => external}, - User1 = #{clientid => <<"bcrypt_foo">>, username => <<"user">>, zone => external}, - Bcrypt = #{clientid => <<"bcrypt">>, username => <<"bcrypt">>, zone => external}, - BcryptWrong = #{clientid => <<"bcrypt_wrong">>, username => <<"bcrypt_wrong">>, zone => external}, - reload([{password_hash, plain}]), - {ok,#{is_superuser := true}} = - emqx_access_control:authenticate(Plain#{password => <<"plain">>}), - reload([{password_hash, md5}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(Md5#{password => <<"md5">>}), - reload([{password_hash, sha}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(Sha#{password => <<"sha">>}), - reload([{password_hash, sha256}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(Sha256#{password => <<"sha256">>}), - reload([{password_hash, bcrypt}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(Bcrypt#{password => <<"password">>}), - {error, not_authorized} = - emqx_access_control:authenticate(BcryptWrong#{password => <<"password">>}), - %%pbkdf2 sha - reload([{password_hash, {pbkdf2, sha, 1, 16}}, - {auth_query, "select password, salt from mqtt_user where username = '%u' limit 1"}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(Pbkdf2#{password => <<"password">>}), - reload([{password_hash, {salt, bcrypt}}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(BcryptFoo#{password => <<"foo">>}), - {error, _} = emqx_access_control:authenticate(User1#{password => <<"foo">>}), - {error, not_authorized} = emqx_access_control:authenticate(Bcrypt#{password => <<"password">>}). - -t_comment_config(_) -> - application:stop(?APP), - [application:unset_env(?APP, Par) || Par <- [acl_query, auth_query]], - application:start(?APP). - -t_placeholders(_) -> - ClientA = #{username => <<"plain">>, clientid => <<"plain">>, zone => external}, - - reload([{password_hash, plain}, - {auth_query, "select password from mqtt_user where username = '%u' and 'a_cn_val' = '%C' limit 1"}]), - {error, not_authorized} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>}), - {error, not_authorized} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>, cn => undefined}), - {ok, _} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>, cn => <<"a_cn_val">>}), - - reload([{auth_query, "select password from mqtt_user where username = '%c' and 'a_dn_val' = '%d' limit 1"}]), - {error, not_authorized} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>}), - {error, not_authorized} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>, dn => undefined}), - {ok, _} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>, dn => <<"a_dn_val">>}), - - reload([{auth_query, "select password from mqtt_user where username = '%u' and '192.168.1.5' = '%a' limit 1"}]), - {error, not_authorized} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>}), - {ok, _} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>, peerhost => {192,168,1,5}}). - -%%-------------------------------------------------------------------- -%% Internal funcs -%%-------------------------------------------------------------------- - -reload(Config) when is_list(Config) -> - application:stop(?APP), - [application:set_env(?APP, K, V) || {K, V} <- Config], - application:start(?APP). diff --git a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/ca-key.pem b/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/ca-key.pem deleted file mode 100644 index e9717011e..000000000 --- a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/ca-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA0kGUBi9NDp65jgdxKfizIfuSr2wpwb44yM9SuP4oUQSULOA2 -4iFpLR/c5FAYHU81y9Vx91dQjdZfffaBZuv2zVvteXUkol8Nez7boKbo2E41MTew -8edtNKZAQVvnaHAC2NCZxjchCzUCDEoUUcl+cIERZ8R48FBqK5iTVcMRIx1akwus -+dhBqP0ykA5TGOWZkJrLM9aUXSPQha9+wXlOpkvu0Ur2nkX8PPJnifWao9UShSar -ll1IqPZNCSlZMwcFYcQNBCpdvITUUYlHvMRQV64bUpOxUGDuJkQL3dLKBlNuBRlJ -BcjBAKw7rFnwwHZcMmQ9tan/dZzpzwjo/T0XjwIDAQABAoIBAQCSHvUqnzDkWjcG -l/Fzg92qXlYBCCC0/ugj1sHcwvVt6Mq5rVE3MpUPwTcYjPlVVTlD4aEEjm/zQuq2 -ddxUlOS+r4aIhHrjRT/vSS4FpjnoKeIZxGR6maVxk6DQS3i1QjMYT1CvSpzyVvKH -a+xXMrtmoKxh+085ZAmFJtIuJhUA2yEa4zggCxWnvz8ecLClUPfVDPhdLBHc3KmL -CRpHEC6L/wanvDPRdkkzfKyaJuIJlTDaCg63AY5sDkTW2I57iI/nJ3haSeidfQKz -39EfbnM1A/YprIakafjAu3frBIsjBVcxwGihZmL/YriTHjOggJF841kT5zFkkv2L -/530Wk6xAoGBAOqZLZ4DIi/zLndEOz1mRbUfjc7GQUdYplBnBwJ22VdS0P4TOXnd -UbJth2MA92NM7ocTYVFl4TVIZY/Y+Prxk7KQdHWzR7JPpKfx9OEVgtSqV0vF9eGI -rKp79Y1T4Mvc3UcQCXX6TP7nHLihEzpS8odm2LW4txrOiLsn4Fq/IWrLAoGBAOVv -6U4tm3lImotUupKLZPKEBYwruo9qRysoug9FiorP4TjaBVOfltiiHbAQD6aGfVtN -SZpZZtrs17wL7Xl4db5asgMcZd+8Hkfo5siR7AuGW9FZloOjDcXb5wCh9EvjJ74J -Cjw7RqyVymq9t7IP6wnVwj5Ck48YhlOZCz/mzlnNAoGAWq7NYFgLvgc9feLFF23S -IjpJQZWHJEITP98jaYNxbfzYRm49+GphqxwFinKULjFNvq7yHlnIXSVYBOu1CqOZ -GRwXuGuNmlKI7lZr9xmukfAqgGLMMdr4C4qRF4lFyufcLRz42z7exmWlx4ST/yaT -E13hBRWayeTuG5JFei6Jh1MCgYEAqmX4LyC+JFBgvvQZcLboLRkSCa18bADxhENG -FAuAvmFvksqRRC71WETmqZj0Fqgxt7pp3KFjO1rFSprNLvbg85PmO1s+6fCLyLpX -lESTu2d5D71qhK93jigooxalGitFm+SY3mzjq0/AOpBWOn+J/w7rqVPGxXLgaHv0 -l+vx+00CgYBOvo9/ImjwYii2jFl+sHEoCzlvpITi2temRlT2j6ulSjCLJgjwEFw9 -8e+vvfQumQOsutakUVyURrkMGNDiNlIv8kv5YLCCkrwN22E6Ghyi69MJUvHQXkc/ -QZhjn/luyfpB5f/BeHFS2bkkxAXo+cfG45ApY3Qfz6/7o+H+vDa6/A== ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/ca.pem b/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/ca.pem deleted file mode 100644 index 00b31d8a4..000000000 --- a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/ca.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDAzCCAeugAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowPDE6MDgGA1UEAwwxTXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DQV9DZXJ0aWZpY2F0ZTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANJBlAYvTQ6euY4HcSn4syH7kq9s -KcG+OMjPUrj+KFEElCzgNuIhaS0f3ORQGB1PNcvVcfdXUI3WX332gWbr9s1b7Xl1 -JKJfDXs+26Cm6NhONTE3sPHnbTSmQEFb52hwAtjQmcY3IQs1AgxKFFHJfnCBEWfE -ePBQaiuYk1XDESMdWpMLrPnYQaj9MpAOUxjlmZCayzPWlF0j0IWvfsF5TqZL7tFK -9p5F/DzyZ4n1mqPVEoUmq5ZdSKj2TQkpWTMHBWHEDQQqXbyE1FGJR7zEUFeuG1KT -sVBg7iZEC93SygZTbgUZSQXIwQCsO6xZ8MB2XDJkPbWp/3Wc6c8I6P09F48CAwEA -AaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADKz6bIpP5anp -GgLB0jkclRWuMlS4qqIt4itSsMXPJ/ezpHwECixmgW2TIQl6S1woRkUeMxhT2/Ay -Sn/7aKxuzRagyE5NEGOvrOuAP5RO2ZdNJ/X3/Rh533fK1sOTEEbSsWUvW6iSkZef -rsfZBVP32xBhRWkKRdLeLB4W99ADMa0IrTmZPCXHSSE2V4e1o6zWLXcOZeH1Qh8N -SkelBweR+8r1Fbvy1r3s7eH7DCbYoGEDVLQGOLvzHKBisQHmoDnnF5E9g1eeNRdg -o+vhOKfYCOzeNREJIqS42PHcGhdNRk90ycigPmfUJclz1mDHoMjKR2S5oosTpr65 -tNPx3CL7GA== ------END CERTIFICATE----- diff --git a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/client-cert.pem b/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/client-cert.pem deleted file mode 100644 index aad1404ca..000000000 --- a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/client-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIBAzANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0N1oXDTMwMDYwOTAzMzg0N1owQDE+MDwGA1UEAww1TXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DbGllbnRfQ2VydGlmaWNhdGUw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVYSWpOvCTupz82fc85Opv -EQ7rkB8X2oOMyBCpkyHKBIr1ZQgRDWBp9UVOASq3GnSElm6+T3Kb1QbOffa8GIlw -sjAueKdq5L2eSkmPIEQ7eoO5kEW+4V866hE1LeL/PmHg2lGP0iqZiJYtElhHNQO8 -3y9I7cm3xWMAA3SSWikVtpJRn3qIp2QSrH+tK+/HHbE5QwtPxdir4ULSCSOaM5Yh -Wi5Oto88TZqe1v7SXC864JVvO4LuS7TuSreCdWZyPXTJFBFeCEWSAxonKZrqHbBe -CwKML6/0NuzjaQ51c2tzmVI6xpHj3nnu4cSRx6Jf9WBm+35vm0wk4pohX3ptdzeV -AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAByQ5zSNeFUH -Aw7JlpZHtHaSEeiiyBHke20ziQ07BK1yi/ms2HAWwQkpZv149sjNuIRH8pkTmkZn -g8PDzSefjLbC9AsWpWV0XNV22T/cdobqLqMBDDZ2+5bsV+jTrOigWd9/AHVZ93PP -IJN8HJn6rtvo2l1bh/CdsX14uVSdofXnuWGabNTydqtMvmCerZsdf6qKqLL+PYwm -RDpgWiRUY7KPBSSlKm/9lJzA+bOe4dHeJzxWFVCJcbpoiTFs1je1V8kKQaHtuW39 -ifX6LTKUMlwEECCbDKM8Yq2tm8NjkjCcnFDtKg8zKGPUu+jrFMN5otiC3wnKcP7r -O9EkaPcgYH8= ------END CERTIFICATE----- diff --git a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/client-key.pem b/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/client-key.pem deleted file mode 100644 index 6789d0291..000000000 --- a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/client-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA1WElqTrwk7qc/Nn3POTqbxEO65AfF9qDjMgQqZMhygSK9WUI -EQ1gafVFTgEqtxp0hJZuvk9ym9UGzn32vBiJcLIwLninauS9nkpJjyBEO3qDuZBF -vuFfOuoRNS3i/z5h4NpRj9IqmYiWLRJYRzUDvN8vSO3Jt8VjAAN0klopFbaSUZ96 -iKdkEqx/rSvvxx2xOUMLT8XYq+FC0gkjmjOWIVouTraPPE2antb+0lwvOuCVbzuC -7ku07kq3gnVmcj10yRQRXghFkgMaJyma6h2wXgsCjC+v9Dbs42kOdXNrc5lSOsaR -49557uHEkceiX/VgZvt+b5tMJOKaIV96bXc3lQIDAQABAoIBAF7yjXmSOn7h6P0y -WCuGiTLG2mbDiLJqj2LTm2Z5i+2Cu/qZ7E76Ls63TxF4v3MemH5vGfQhEhR5ZD/6 -GRJ1sKKvB3WGRqjwA9gtojHH39S/nWGy6vYW/vMOOH37XyjIr3EIdIaUtFQBTSHd -Kd71niYrAbVn6fyWHolhADwnVmTMOl5OOAhCdEF4GN3b5aIhIu8BJ7EUzTtHBJIj -CAEfjZFjDs1y1cIgGFJkuIQxMfCpq5recU2qwip7YO6fk//WEjOPu7kSf5IEswL8 -jg1dea9rGBV6KaD2xsgsC6Ll6Sb4BbsrHMfflG3K2Lk3RdVqqTFp1Fn1PTLQE/1S -S/SZPYECgYEA9qYcHKHd0+Q5Ty5wgpxKGa4UCWkpwvfvyv4bh8qlmxueB+l2AIdo -ZvkM8gTPagPQ3WypAyC2b9iQu70uOJo1NizTtKnpjDdN1YpDjISJuS/P0x73gZwy -gmoM5AzMtN4D6IbxXtXnPaYICvwLKU80ouEN5ZPM4/ODLUu6gsp0v2UCgYEA3Xgi -zMC4JF0vEKEaK0H6QstaoXUmw/lToZGH3TEojBIkb/2LrHUclygtONh9kJSFb89/ -jbmRRLAOrx3HZKCNGUmF4H9k5OQyAIv6OGBinvLGqcbqnyNlI+Le8zxySYwKMlEj -EMrBCLmSyi0CGFrbZ3mlj/oCET/ql9rNvcK+DHECgYAEx5dH3sMjtgp+RFId1dWB -xePRgt4yTwewkVgLO5wV82UOljGZNQaK6Eyd7AXw8f38LHzh+KJQbIvxd2sL4cEi -OaAoohpKg0/Y0YMZl//rPMf0OWdmdZZs/I0fZjgZUSwWN3c59T8z7KG/RL8an9RP -S7kvN7wCttdV61/D5RR6GQKBgDxCe/WKWpBKaovzydMLWLTj7/0Oi0W3iXHkzzr4 -LTgvl4qBSofaNbVLUUKuZTv5rXUG2IYPf99YqCYtzBstNDc1MiAriaBeFtzfOW4t -i6gEFtoLLbuvPc3N5Sv5vn8Ug5G9UfU3td5R4AbyyCcoUZqOFuZd+EIJSiOXfXOs -kVmBAoGBAIU9aPAqhU5LX902oq8KsrpdySONqv5mtoStvl3wo95WIqXNEsFY60wO -q02jKQmJJ2MqhkJm2EoF2Mq8+40EZ5sz8LdgeQ/M0yQ9lAhPi4rftwhpe55Ma9dk -SE9X1c/DMCBEaIjJqVXdy0/EeArwpb8sHkguVVAZUWxzD+phm1gs ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/private_key.pem b/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/private_key.pem deleted file mode 100644 index 8fbf6bdec..000000000 --- a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/private_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA1zVmMhPqpSPMmYkKh5wwlRD5XuS8YWJKEM6tjFx61VK8qxHE -YngkC2KnL5EuKAjQZIF3tJskwt0hAat047CCCZxrkNEpbVvSnvnk+A/8bg/Ww1n3 -qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XAPy7ZjzecAF9SDV6WSiPeAxUX2+hN -dId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGwcnkKCUikiBqr2ijSIgvRtBfZ9fBG -jFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtPnBtTytmqTy9V/BRgsVKUoksm6wsx -kUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h+wIDAQABAoIBAEQcrHmRACTADdNS -IjkFYALt2l8EOfMAbryfDSJtapr1kqz59JPNvmq0EIHnixo0n/APYdmReLML1ZR3 -tYkSpjVwgkLVUC1CcIjMQoGYXaZf8PLnGJHZk45RR8m6hsTV0mQ5bfBaeVa2jbma -OzJMjcnxg/3l9cPQZ2G/3AUfEPccMxOXp1KRz3mUQcGnKJGtDbN/kfmntcwYoxaE -Zg4RoeKAoMpK1SSHAiJKe7TnztINJ7uygR9XSzNd6auY8A3vomSIjpYO7XL+lh7L -izm4Ir3Gb/eCYBvWgQyQa2KCJgK/sQyEs3a09ngofSEUhQJQYhgZDwUj+fDDOGqj -hCZOA8ECgYEA+ZWuHdcUQ3ygYhLds2QcogUlIsx7C8n/Gk/FUrqqXJrTkuO0Eqqa -B47lCITvmn2zm0ODfSFIARgKEUEDLS/biZYv7SUTrFqBLcet+aGI7Dpv91CgB75R -tNzcIf8VxoiP0jPqdbh9mLbbxGi5Uc4p9TVXRljC4hkswaouebWee0sCgYEA3L2E -YB3kiHrhPI9LHS5Px9C1w+NOu5wP5snxrDGEgaFCvL6zgY6PflacppgnmTXl8D1x -im0IDKSw5dP3FFonSVXReq3CXDql7UnhfTCiLDahV7bLxTH42FofcBpDN3ERdOal -58RwQh6VrLkzQRVoObo+hbGlFiwwSAfQC509FhECgYBsRSBpVXo25IN2yBRg09cP -+gdoFyhxrsj5kw1YnB13WrrZh+oABv4WtUhp77E5ZbpaamlKCPwBbXpAjeFg4tfr -0bksuN7V79UGFQ9FsWuCfr8/nDwv38H2IbFlFhFONMOfPmJBey0Q6JJhm8R41mSh -OOiJXcv85UrjIH5U0hLUDQKBgQDVLOU5WcUJlPoOXSgiT0ZW5xWSzuOLRUUKEf6l -19BqzAzCcLy0orOrRAPW01xylt2v6/bJw1Ahva7k1ZZo/kOwjANYoZPxM+ZoSZBN -MXl8j2mzZuJVV1RFxItV3NcLJNPB/Lk+IbRz9kt/2f9InF7iWR3mSU/wIM6j0X+2 -p6yFsQKBgQCM/ldWb511lA+SNkqXB2P6WXAgAM/7+jwsNHX2ia2Ikufm4SUEKMSv -mti/nZkHDHsrHU4wb/2cOAywMELzv9EHzdcoenjBQP65OAc/1qWJs+LnBcCXfqKk -aHjEZW6+brkHdRGLLY3YAHlt/AUL+RsKPJfN72i/FSpmu+52G36eeQ== ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/public_key.pem b/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/public_key.pem deleted file mode 100644 index f9772b533..000000000 --- a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/public_key.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1zVmMhPqpSPMmYkKh5ww -lRD5XuS8YWJKEM6tjFx61VK8qxHEYngkC2KnL5EuKAjQZIF3tJskwt0hAat047CC -CZxrkNEpbVvSnvnk+A/8bg/Ww1n3qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XA -Py7ZjzecAF9SDV6WSiPeAxUX2+hNdId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGw -cnkKCUikiBqr2ijSIgvRtBfZ9fBGjFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtP -nBtTytmqTy9V/BRgsVKUoksm6wsxkUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h -+wIDAQAB ------END PUBLIC KEY----- diff --git a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-cert.pem b/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-cert.pem deleted file mode 100644 index a2f9688df..000000000 --- a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua -NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z -G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL -JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB -4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy -TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9 -AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6 -zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI -hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F -sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD -3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR -+DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC -dN/klu446fI= ------END CERTIFICATE----- diff --git a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-key.pem b/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-key.pem deleted file mode 100644 index a1dfd5f78..000000000 --- a/apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj -U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho -XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT -29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX -NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv -f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn -WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP -PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV -4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS -VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk -Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb -SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq -EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx -VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH -cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0 -ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h -J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ -h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K -eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq -dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD -PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes -Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2 -/UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH -PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd -JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_pgsql/.gitignore b/apps/emqx_auth_pgsql/.gitignore deleted file mode 100644 index 672d34c0c..000000000 --- a/apps/emqx_auth_pgsql/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -ebin -.rebar -.eunit -.DS_Store -.erlang.mk/ -deps/ -emqx_auth_pgsql.d -ct.coverdata -logs/ -test/ct.cover.spec -test/*.beam -data/ -.DS_Store -cover/ -eunit.coverdata -_build/ -rebar.lock -erlang.mk -*.conf.rendered -.rebar3/ diff --git a/apps/emqx_auth_pgsql/README.md b/apps/emqx_auth_pgsql/README.md deleted file mode 100644 index a8f5d723f..000000000 --- a/apps/emqx_auth_pgsql/README.md +++ /dev/null @@ -1,183 +0,0 @@ -emqx_auth_pgsql -=============== - -Authentication/ACL with PostgreSQL Database. - -Build Plugin ------------- - -make && make tests - -Configuration -------------- - -File: etc/emqx_auth_pgsql.conf - -``` -## PostgreSQL server address. -## -## Value: Port | IP:Port -## -## Examples: 5432, 127.0.0.1:5432, localhost:5432 -auth.pgsql.server = 127.0.0.1:5432 - -## PostgreSQL pool size. -## -## Value: Number -auth.pgsql.pool = 8 - -## PostgreSQL username. -## -## Value: String -auth.pgsql.username = root - -## PostgreSQL password. -## -## Value: String -## auth.pgsql.password = - -## PostgreSQL database. -## -## Value: String -auth.pgsql.database = mqtt - -## PostgreSQL database encoding. -## -## Value: String -auth.pgsql.encoding = utf8 - -## Whether to enable SSL connection. -## -## Value: true | false -auth.pgsql.ssl.enable = false - -## SSL keyfile. -## -## Value: File -## auth.pgsql.ssl_opts.keyfile = - -## SSL certfile. -## -## Value: File -## auth.pgsql.ssl_opts.certfile = - -## SSL cacertfile. -## -## Value: File -## auth.pgsql.ssl_opts.cacertfile = - -## Authentication query. -## -## Value: SQL -## -## Variables: -## - %u: username -## - %c: clientid -## -auth.pgsql.auth_query = select password from mqtt_user where username = '%u' limit 1 - -## Password hash. -## -## Value: plain | md5 | sha | sha256 | bcrypt -auth.pgsql.password_hash = sha256 - -## sha256 with salt prefix -## auth.pgsql.password_hash = salt,sha256 - -## sha256 with salt suffix -## auth.pgsql.password_hash = sha256,salt - -## bcrypt with salt prefix -## auth.pgsql.password_hash = salt,bcrypt - -## pbkdf2 with macfun iterations dklen -## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512 -## auth.pgsql.password_hash = pbkdf2,sha256,1000,20 - -## Superuser query. -## -## Value: SQL -## -## Variables: -## - %u: username -## - %c: clientid -auth.pgsql.super_query = select is_superuser from mqtt_user where username = '%u' limit 1 - -## ACL query. Comment this query, the ACL will be disabled. -## -## Value: SQL -## -## Variables: -## - %a: ipaddress -## - %u: username -## - %c: clientid -auth.pgsql.acl_query = select allow, ipaddr, username, clientid, access, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c' -``` - -Load Plugin ------------ - -./bin/emqx_ctl plugins load emqx_auth_pgsql - -Auth Table ----------- - -Notice: This is a demo table. You could authenticate with any user table. - -```sql -CREATE TABLE mqtt_user ( - id SERIAL primary key, - is_superuser boolean, - username character varying(100), - password character varying(100), - salt character varying(40) -) -``` - -ACL Table ---------- - -```sql -CREATE TABLE mqtt_acl ( - id SERIAL primary key, - allow integer, - ipaddr character varying(60), - username character varying(100), - clientid character varying(100), - access integer, - topic character varying(100) -) - -INSERT INTO mqtt_acl (id, allow, ipaddr, username, clientid, access, topic) -VALUES - (1,1,NULL,'$all',NULL,2,'#'), - (2,0,NULL,'$all',NULL,1,'$SYS/#'), - (3,0,NULL,'$all',NULL,1,'eq #'), - (5,1,'127.0.0.1',NULL,NULL,2,'$SYS/#'), - (6,1,'127.0.0.1',NULL,NULL,2,'#'), - (7,1,NULL,'dashboard',NULL,1,'$SYS/#'); -``` -**allow:** Client's permission to access a topic. '0' means that the client does not have permission to access the topic, '1' means that the client have permission to access the topic. - -**ipaddr:** Client IP address. For all ip addresses it can be '$all' or 'NULL'. - -**username:** Client username. For all users it can be '$all' or 'NULL'. - -**clientid:** Client id. For all client ids it can be '$all' or 'NULL'. - -**access:** Operations that the client can perform. '1' means that the client can subscribe to a topic, '2' means that the client can publish to a topic, '3' means that the client can subscribe and can publish to a topic. - -**topic:** Topic name. Topic wildcards are supported. - -**Notice that only one value allowed for ipaddr, username and clientid fields.** - -License -------- - -Apache License Version 2.0 - -Author ------- - -EMQ X Team. - diff --git a/apps/emqx_auth_pgsql/etc/emqx_auth_pgsql.conf b/apps/emqx_auth_pgsql/etc/emqx_auth_pgsql.conf deleted file mode 100644 index 6f7018210..000000000 --- a/apps/emqx_auth_pgsql/etc/emqx_auth_pgsql.conf +++ /dev/null @@ -1,132 +0,0 @@ -##-------------------------------------------------------------------- -## PostgreSQL Auth/ACL Plugin -##-------------------------------------------------------------------- - -## PostgreSQL server address. -## -## Value: Port | IP:Port -## -## Examples: 5432, "127.0.0.1:5432", "localhost:5432" -auth.pgsql.server = "127.0.0.1:5432" - -## PostgreSQL pool size. -## -## Value: Number -auth.pgsql.pool = 8 - -## PostgreSQL username. -## -## Value: String -auth.pgsql.username = root - -## PostgreSQL password. -## -## Value: String -#auth.pgsql.password = - -## PostgreSQL database. -## -## Value: String -auth.pgsql.database = mqtt - -## PostgreSQL database encoding. -## -## Value: String -auth.pgsql.encoding = utf8 - -## Whether to enable SSL connection. -## -## Value: on | off -auth.pgsql.ssl.enable = off - -## TLS version. -## -## Available enum values: -## tlsv1.3,tlsv1.2,tlsv1.1,tlsv1 -## -## Value: String, seperated by ',' -#auth.pgsql.ssl.tls_versions = tlsv1.3,tlsv1.2,tlsv1.1 - -## SSL keyfile. -## -## Value: File -#auth.pgsql.ssl.keyfile = - -## SSL certfile. -## -## Value: File -#auth.pgsql.ssl.certfile = - -## SSL cacertfile. -## -## Value: File -#auth.pgsql.ssl.cacertfile = - -## In mode verify_none the default behavior is to allow all x509-path -## validation errors. -## -## Value: true | false -#auth.pgsql.ssl.verify = false - -## If not specified, the server's names returned in server's certificate is validated against -## what's provided `auth.pgsql.server` config's host part. -## Setting to 'disable' will make EMQ X ignore unmatched server names. -## If set with a host name, the server's names returned in server's certificate is validated -## against this value. -## -## Value: String | disable -## auth.pgsql.ssl.server_name_indication = disable - -## Authentication query. -## -## Value: SQL -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -auth.pgsql.auth_query = "select password from mqtt_user where username = '%u' limit 1" - -## Password hash. -## -## Value: plain | md5 | sha | sha256 | bcrypt -auth.pgsql.password_hash = sha256 - -## sha256 with salt prefix -## auth.pgsql.password_hash = "salt,sha256" - -## sha256 with salt suffix -## auth.pgsql.password_hash = "sha256,salt" - -## bcrypt with salt prefix -## auth.pgsql.password_hash = "salt,bcrypt" - -## pbkdf2 with macfun iterations dklen -## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512 -## auth.pgsql.password_hash = "pbkdf2,sha256,1000,20" - -## Superuser query. -## -## Value: SQL -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -auth.pgsql.super_query = "select is_superuser from mqtt_user where username = '%u' limit 1" - -## ACL query. Comment this query, the ACL will be disabled. -## -## Value: SQL -## -## Variables: -## - %a: ipaddress -## - %u: username -## - %c: clientid -## -## Note: You can add the 'ORDER BY' statement to control the rules match order -auth.pgsql.acl_query = "select allow, ipaddr, username, clientid, access, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'" diff --git a/apps/emqx_auth_pgsql/include/emqx_auth_pgsql.hrl b/apps/emqx_auth_pgsql/include/emqx_auth_pgsql.hrl deleted file mode 100644 index b86692752..000000000 --- a/apps/emqx_auth_pgsql/include/emqx_auth_pgsql.hrl +++ /dev/null @@ -1,23 +0,0 @@ --define(APP, emqx_auth_pgsql). - --record(auth_metrics, { - success = 'client.auth.success', - failure = 'client.auth.failure', - ignore = 'client.auth.ignore' - }). - --record(acl_metrics, { - allow = 'client.acl.allow', - deny = 'client.acl.deny', - ignore = 'client.acl.ignore' - }). - --define(METRICS(Type), tl(tuple_to_list(#Type{}))). --define(METRICS(Type, K), #Type{}#Type.K). - --define(AUTH_METRICS, ?METRICS(auth_metrics)). --define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). - --define(ACL_METRICS, ?METRICS(acl_metrics)). --define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). - diff --git a/apps/emqx_auth_pgsql/mqtt.sql b/apps/emqx_auth_pgsql/mqtt.sql deleted file mode 100644 index 933b0058a..000000000 --- a/apps/emqx_auth_pgsql/mqtt.sql +++ /dev/null @@ -1,28 +0,0 @@ - -CREATE TABLE mqtt_user ( - id SERIAL primary key, - is_superuser boolean, - username character varying(100), - password character varying(100), - salt character varying(40) -); - -CREATE TABLE mqtt_acl ( - id SERIAL primary key, - allow integer, - ipaddr character varying(60), - username character varying(100), - clientid character varying(100), - access integer, - topic character varying(100) -); - -INSERT INTO mqtt_acl (id, allow, ipaddr, username, clientid, access, topic) -VALUES - (1,1,NULL,'$all',NULL,2,'#'), - (2,0,NULL,'$all',NULL,1,'$SYS/#'), - (3,0,NULL,'$all',NULL,1,'eq #'), - (5,1,'127.0.0.1',NULL,NULL,2,'$SYS/#'), - (6,1,'127.0.0.1',NULL,NULL,2,'#'), - (7,1,NULL,'dashboard',NULL,1,'$SYS/#'); - diff --git a/apps/emqx_auth_pgsql/priv/emqx_auth_pgsql.schema b/apps/emqx_auth_pgsql/priv/emqx_auth_pgsql.schema deleted file mode 100644 index 2be9b0670..000000000 --- a/apps/emqx_auth_pgsql/priv/emqx_auth_pgsql.schema +++ /dev/null @@ -1,184 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_auth_pgsl config mapping - -{mapping, "auth.pgsql.server", "emqx_auth_pgsql.server", [ - {default, {"127.0.0.1", 5432}}, - {datatype, [integer, ip, string]} -]}. - -{mapping, "auth.pgsql.pool", "emqx_auth_pgsql.server", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "auth.pgsql.database", "emqx_auth_pgsql.server", [ - {datatype, string} -]}. - -{mapping, "auth.pgsql.username", "emqx_auth_pgsql.server", [ - {default, ""}, - {datatype, string} -]}. - -{mapping, "auth.pgsql.password", "emqx_auth_pgsql.server", [ - {default, ""}, - {datatype, string} -]}. - -{mapping, "auth.pgsql.encoding", "emqx_auth_pgsql.server", [ - {default, utf8}, - {datatype, atom} -]}. - -{mapping, "auth.pgsql.ssl.enable", "emqx_auth_pgsql.server", [ - {default, off}, - {datatype, {enum, [on, off, true, false]}} %% FIXME: true/fasle is compatible with 4.0-4.2 version format, plan to delete in 5.0 -]}. - -{mapping, "auth.pgsql.ssl.tls_versions", "emqx_auth_pgsql.server", [ - {default, "tlsv1.3,tlsv1.2,tlsv1.1"}, - {datatype, string} -]}. - -{mapping, "auth.pgsql.ssl.keyfile", "emqx_auth_pgsql.server", [ - {datatype, string} -]}. - -{mapping, "auth.pgsql.ssl.certfile", "emqx_auth_pgsql.server", [ - {datatype, string} -]}. - -{mapping, "auth.pgsql.ssl.cacertfile", "emqx_auth_pgsql.server", [ - {datatype, string} -]}. - -{mapping, "auth.pgsql.ssl.verify", "emqx_auth_pgsql.server", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "auth.pgsql.ssl.server_name_indication", "emqx_auth_pgsql.server", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.pgsql.ssl_opts.keyfile", "emqx_auth_pgsql.server", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.pgsql.ssl_opts.certfile", "emqx_auth_pgsql.server", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.pgsql.ssl_opts.cacertfile", "emqx_auth_pgsql.server", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.pgsql.ssl_opts.tls_versions", "emqx_auth_pgsql.server", [ - {default, "tlsv1.2"}, - {datatype, string} -]}. - -{translation, "emqx_auth_pgsql.server", fun(Conf) -> - {PgHost, PgPort} = - case cuttlefish:conf_get("auth.pgsql.server", Conf) of - {Ip, Port} -> {Ip, Port}; - S -> case string:tokens(S, ":") of - [Domain] -> {Domain, 5432}; - [Domain, Port] -> {Domain, list_to_integer(Port)} - end - end, - Pool = cuttlefish:conf_get("auth.pgsql.pool", Conf), - Username = cuttlefish:conf_get("auth.pgsql.username", Conf), - Passwd = cuttlefish:conf_get("auth.pgsql.password", Conf, ""), - DB = cuttlefish:conf_get("auth.pgsql.database", Conf), - Encoding = cuttlefish:conf_get("auth.pgsql.encoding", Conf), - - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - SslOpts = fun(Prefix) -> - Verify = case cuttlefish:conf_get(Prefix ++ ".verify", Conf, false) of - true -> verify_peer; - false -> verify_none - end, - Filter([{keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)}, - {certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)}, - {cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)}, - {verify, Verify}, - {server_name_indication, case cuttlefish:conf_get(Prefix ++ ".server_name_indication", Conf, undefined) of - "disable" -> disable; - SNI -> SNI - end}, - {versions, [list_to_existing_atom(Value) - || Value <- string:tokens(cuttlefish:conf_get(Prefix ++ ".tls_versions", Conf), " ,")]}]) - end, - - %% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 - GenSsl = case cuttlefish:conf_get("auth.pgsql.ssl.cacertfile", Conf, undefined) of - undefined -> [{ssl, true}, {ssl_opts, SslOpts("auth.pgsql.ssl_opts")}]; - _ -> [{ssl, true}, {ssl_opts, SslOpts("auth.pgsql.ssl")}] - end, - - %% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 - Ssl = case cuttlefish:conf_get("auth.pgsql.ssl.enable", Conf) of - on -> GenSsl; - off -> []; - true -> [{ssl, true}, {ssl_opts, SslOpts("auth.pgsql.ssl_opts")}]; - false -> [] - end, - - TempHost = case inet:parse_address(PgHost) of - {ok, IpAddr} -> - IpAddr; - _ -> - PgHost - end, - [{pool_size, Pool}, - {auto_reconnect, 1}, - {host, TempHost}, - {port, PgPort}, - {username, Username}, - {password, Passwd}, - {database, DB}, - {encoding, Encoding}] ++ Ssl -end}. - -{mapping, "auth.pgsql.auth_query", "emqx_auth_pgsql.auth_query", [ - {datatype, string} -]}. - -{mapping, "auth.pgsql.password_hash", "emqx_auth_pgsql.password_hash", [ - {datatype, string} -]}. - -{mapping, "auth.pgsql.pbkdf2_macfun", "emqx_auth_pgsql.pbkdf2_macfun", [ - {datatype, atom} -]}. - -{mapping, "auth.pgsql.pbkdf2_iterations", "emqx_auth_pgsql.pbkdf2_iterations", [ - {datatype, integer} -]}. - -{mapping, "auth.pgsql.pbkdf2_dklen", "emqx_auth_pgsql.pbkdf2_dklen", [ - {datatype, integer} -]}. - -{mapping, "auth.pgsql.super_query", "emqx_auth_pgsql.super_query", [ - {datatype, string} -]}. - -{mapping, "auth.pgsql.acl_query", "emqx_auth_pgsql.acl_query", [ - {datatype, string} -]}. - -{translation, "emqx_auth_pgsql.password_hash", fun(Conf) -> - HashValue = cuttlefish:conf_get("auth.pgsql.password_hash", Conf), - case string:tokens(HashValue, ",") of - [Hash] -> list_to_atom(Hash); - [Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)}; - [Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), list_to_integer(Iterations), list_to_integer(Dklen)}; - _ -> plain - end -end}. diff --git a/apps/emqx_auth_pgsql/rebar.config b/apps/emqx_auth_pgsql/rebar.config deleted file mode 100644 index 3155bbef3..000000000 --- a/apps/emqx_auth_pgsql/rebar.config +++ /dev/null @@ -1,21 +0,0 @@ -{deps, - [{epgsql, {git, "https://github.com/epgsql/epgsql", {tag, "4.4.0"}}} - ]}. - -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - compressed - ]}. -{overrides, [{add, [{erl_opts, [compressed]}]}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions - ]}. - -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. diff --git a/apps/emqx_auth_pgsql/src/emqx_acl_pgsql.erl b/apps/emqx_auth_pgsql/src/emqx_acl_pgsql.erl deleted file mode 100644 index 099ce4438..000000000 --- a/apps/emqx_auth_pgsql/src/emqx_acl_pgsql.erl +++ /dev/null @@ -1,117 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_acl_pgsql). - --include("emqx_auth_pgsql.hrl"). --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - -%% ACL callbacks --export([ register_metrics/0 - , check_acl/5 - , description/0 - ]). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). - -check_acl(ClientInfo, PubSub, Topic, NoMatchAction, #{pool := Pool} = State) -> - case do_check_acl(Pool, ClientInfo, PubSub, Topic, NoMatchAction, State) of - ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok; - {stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow}; - {stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny} - end. - -do_check_acl(_Pool, #{username := <<$$, _/binary>>}, _PubSub, _Topic, _NoMatchAction, _State) -> - ok; -do_check_acl(Pool, ClientInfo, PubSub, Topic, _NoMatchAction, #{acl_query := {AclSql, AclParams}}) -> - case emqx_auth_pgsql_cli:equery(Pool, AclSql, AclParams, ClientInfo) of - {ok, _, []} -> ok; - {ok, _, Rows} -> - Rules = filter(PubSub, compile(Rows)), - case match(ClientInfo, Topic, Rules) of - {matched, allow} -> {stop, allow}; - {matched, deny} -> {stop, deny}; - nomatch -> ok - end; - {error, Reason} -> - ?LOG(error, "[Postgres] do_check_acl error: ~p~n", [Reason]), - ok - end. - -match(_ClientInfo, _Topic, []) -> - nomatch; - -match(ClientInfo, Topic, [Rule|Rules]) -> - case emqx_access_rule:match(ClientInfo, Topic, Rule) of - nomatch -> match(ClientInfo, Topic, Rules); - {matched, AllowDeny} -> {matched, AllowDeny} - end. - -filter(PubSub, Rules) -> - [Term || Term = {_, _, Access, _} <- Rules, - Access =:= PubSub orelse Access =:= pubsub]. - -compile(Rows) -> - compile(Rows, []). -compile([], Acc) -> - Acc; -compile([{Allow, IpAddr, Username, ClientId, Access, Topic}|T], Acc) -> - Who = who(IpAddr, Username, ClientId), - Term = {allow(Allow), Who, access(Access), [topic(Topic)]}, - compile(T, [emqx_access_rule:compile(Term) | Acc]). - -who(_, <<"$all">>, _) -> - all; -who(null, null, null) -> - throw(undefined_who); -who(CIDR, Username, ClientId) -> - Cols = [{ipaddr, b2l(CIDR)}, {user, Username}, {client, ClientId}], - case [{C, V} || {C, V} <- Cols, not empty(V)] of - [Who] -> Who; - Conds -> {'and', Conds} - end. - -allow(1) -> allow; -allow(0) -> deny; -allow(<<"1">>) -> allow; -allow(<<"0">>) -> deny. - -access(1) -> subscribe; -access(2) -> publish; -access(3) -> pubsub; -access(<<"1">>) -> subscribe; -access(<<"2">>) -> publish; -access(<<"3">>) -> pubsub. - -topic(<<"eq ", Topic/binary>>) -> - {eq, Topic}; -topic(Topic) -> - Topic. - -description() -> - "ACL with Postgres". - -b2l(null) -> null; -b2l(B) -> binary_to_list(B). - -empty(null) -> true; -empty("") -> true; -empty(<<>>) -> true; -empty(_) -> false. - diff --git a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql.app.src b/apps/emqx_auth_pgsql/src/emqx_auth_pgsql.app.src deleted file mode 100644 index e97487e21..000000000 --- a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_auth_pgsql, - [{description, "EMQ X Authentication/ACL with PostgreSQL"}, - {vsn, "4.4.0"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_auth_pgsql_sup]}, - {applications, [kernel,stdlib,epgsql,ecpool]}, - {mod, {emqx_auth_pgsql_app,[]}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-auth-pgsql"} - ]} - ]}. diff --git a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql.erl b/apps/emqx_auth_pgsql/src/emqx_auth_pgsql.erl deleted file mode 100644 index f8f365cd7..000000000 --- a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql.erl +++ /dev/null @@ -1,91 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_pgsql). - --include("emqx_auth_pgsql.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --export([ register_metrics/0 - , check/3 - , description/0 - ]). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). - -%%-------------------------------------------------------------------- -%% Auth Module Callbacks -%%-------------------------------------------------------------------- - -check(ClientInfo = #{password := Password}, AuthResult, - #{auth_query := {AuthSql, AuthParams}, - super_query := SuperQuery, - hash_type := HashType, - pool := Pool}) -> - CheckPass = case emqx_auth_pgsql_cli:equery(Pool, AuthSql, AuthParams, ClientInfo) of - {ok, _, [Record]} -> - check_pass(erlang:append_element(Record, Password), HashType); - {ok, _, []} -> - {error, not_found}; - {error, Reason} -> - ?LOG(error, "[Postgres] query '~p' failed: ~p", [AuthSql, Reason]), - {error, not_found} - end, - case CheckPass of - ok -> - emqx_metrics:inc(?AUTH_METRICS(success)), - {stop, AuthResult#{is_superuser => is_superuser(Pool, SuperQuery, ClientInfo), - anonymous => false, - auth_result => success}}; - {error, not_found} -> - emqx_metrics:inc(?AUTH_METRICS(ignore)), ok; - {error, ResultCode} -> - ?LOG(error, "[Postgres] Auth from pgsql failed: ~p", [ResultCode]), - emqx_metrics:inc(?AUTH_METRICS(failure)), - {stop, AuthResult#{auth_result => ResultCode, anonymous => false}} - end. - -%%-------------------------------------------------------------------- -%% Is Superuser? -%%-------------------------------------------------------------------- - --spec(is_superuser(atom(),undefined | {string(), list()}, emqx_types:client()) -> boolean()). -is_superuser(_Pool, undefined, _Client) -> - false; -is_superuser(Pool, {SuperSql, Params}, ClientInfo) -> - case emqx_auth_pgsql_cli:equery(Pool, SuperSql, Params, ClientInfo) of - {ok, [_Super], [{true}]} -> - true; - {ok, [_Super], [_False]} -> - false; - {ok, [_Super], []} -> - false; - {error, _Error} -> - false - end. - -check_pass(Password, HashType) -> - case emqx_passwd:check_pass(Password, HashType) of - ok -> ok; - {error, _Reason} -> {error, not_authorized} - end. - -description() -> "Authentication with PostgreSQL". - diff --git a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql_app.erl b/apps/emqx_auth_pgsql/src/emqx_auth_pgsql_app.erl deleted file mode 100644 index 571fd0c4b..000000000 --- a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql_app.erl +++ /dev/null @@ -1,63 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_pgsql_app). - --behaviour(application). - --emqx_plugin(auth). - --include("emqx_auth_pgsql.hrl"). - --import(emqx_auth_pgsql_cli, [parse_query/2]). - -%% Application callbacks --export([ start/2 - , stop/1 - ]). - -%%-------------------------------------------------------------------- -%% Application callbacks -%%-------------------------------------------------------------------- - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_auth_pgsql_sup:start_link(), - if_enabled(auth_query, fun(AuthQuery) -> - SuperQuery = parse_query(super_query, application:get_env(?APP, super_query, undefined)), - {ok, HashType} = application:get_env(?APP, password_hash), - AuthEnv = #{auth_query => AuthQuery, - super_query => SuperQuery, - hash_type => HashType, - pool => ?APP}, - ok = emqx_auth_pgsql:register_metrics(), - ok = emqx:hook('client.authenticate', {emqx_auth_pgsql, check, [AuthEnv]}) - end), - if_enabled(acl_query, fun(AclQuery) -> - ok = emqx_acl_pgsql:register_metrics(), - ok = emqx:hook('client.check_acl', {emqx_acl_pgsql, check_acl, [#{acl_query => AclQuery, pool => ?APP}]}) - end), - {ok, Sup}. - -stop(_State) -> - ok = emqx:unhook('client.authenticate', {emqx_auth_pgsql, check}), - ok = emqx:unhook('client.check_acl', {emqx_acl_pgsql, check_acl}). - -if_enabled(Par, Fun) -> - case application:get_env(?APP, Par) of - {ok, Query} -> Fun(parse_query(Par, Query)); - undefined -> ok - end. - diff --git a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql_cli.erl b/apps/emqx_auth_pgsql/src/emqx_auth_pgsql_cli.erl deleted file mode 100644 index 7dde566a2..000000000 --- a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql_cli.erl +++ /dev/null @@ -1,150 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_pgsql_cli). - --behaviour(ecpool_worker). - --include("emqx_auth_pgsql.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --export([connect/1]). --export([parse_query/2]). --export([ equery/4 - , equery/3 - ]). - --type client_info() :: #{username := _, - clientid := _, - peerhost := _, - _ => _}. - -%%-------------------------------------------------------------------- -%% Avoid SQL Injection: Parse SQL to Parameter Query. -%%-------------------------------------------------------------------- - -parse_query(_Par, undefined) -> - undefined; -parse_query(Par, Sql) -> - case re:run(Sql, "'%[ucCad]'", [global, {capture, all, list}]) of - {match, Variables} -> - Params = [Var || [Var] <- Variables], - {atom_to_list(Par), Params}; - nomatch -> - {atom_to_list(Par), []} - end. - -pgvar(Sql, Params) -> - Vars = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Params))], - lists:foldl(fun({Param, Var}, S) -> - re:replace(S, Param, Var, [{return, list}]) - end, Sql, lists:zip(Params, Vars)). - -%%-------------------------------------------------------------------- -%% PostgreSQL Connect/Query -%%-------------------------------------------------------------------- - -%% Due to a bug in epgsql the caluse for `econnrefused` is not recognised by -%% dialyzer, result in this error: -%% The pattern {'error', Reason = 'econnrefused'} can never match the type ... -%% https://github.com/epgsql/epgsql/issues/246 --dialyzer([{nowarn_function, [connect/1]}]). -connect(Opts) -> - Host = proplists:get_value(host, Opts), - Username = proplists:get_value(username, Opts), - Password = proplists:get_value(password, Opts), - case epgsql:connect(Host, Username, Password, conn_opts(Opts)) of - {ok, C} -> - conn_post(C), - {ok, C}; - {error, Reason = econnrefused} -> - ?LOG(error, "[Postgres] Can't connect to Postgres server: Connection refused."), - {error, Reason}; - {error, Reason = invalid_authorization_specification} -> - ?LOG(error, "[Postgres] Can't connect to Postgres server: Invalid authorization specification."), - {error, Reason}; - {error, Reason = invalid_password} -> - ?LOG(error, "[Postgres] Can't connect to Postgres server: Invalid password."), - {error, Reason}; - {error, Reason} -> - ?LOG(error, "[Postgres] Can't connect to Postgres server: ~p", [Reason]), - {error, Reason} - end. - -conn_post(Connection) -> - lists:foreach(fun(Par) -> - Sql0 = application:get_env(?APP, Par, undefined), - case parse_query(Par, Sql0) of - undefined -> ok; - {_, Params} -> - Sql = pgvar(Sql0, Params), - epgsql:parse(Connection, atom_to_list(Par), Sql, []) - end - end, [auth_query, acl_query, super_query]). - -conn_opts(Opts) -> - conn_opts(Opts, []). -conn_opts([], Acc) -> - Acc; -conn_opts([Opt = {database, _}|Opts], Acc) -> - conn_opts(Opts, [Opt|Acc]); -conn_opts([Opt = {ssl, _}|Opts], Acc) -> - conn_opts(Opts, [Opt|Acc]); -conn_opts([Opt = {port, _}|Opts], Acc) -> - conn_opts(Opts, [Opt|Acc]); -conn_opts([Opt = {timeout, _}|Opts], Acc) -> - conn_opts(Opts, [Opt|Acc]); -conn_opts([Opt = {ssl_opts, _}|Opts], Acc) -> - conn_opts(Opts, [Opt|Acc]); -conn_opts([_Opt|Opts], Acc) -> - conn_opts(Opts, Acc). - --spec(equery(atom(), string() | epgsql:statement(), Parameters::[any()]) -> {ok, ColumnsDescription :: [any()], RowsValues :: [any()]} | {error, any()} ). -equery(Pool, Sql, Params) -> - ecpool:with_client(Pool, fun(C) -> epgsql:prepared_query(C, Sql, Params) end). - --spec(equery(atom(), string() | epgsql:statement(), Parameters::[any()], client_info()) -> {ok, ColumnsDescription :: [any()], RowsValues :: [any()]} | {error, any()} ). -equery(Pool, Sql, Params, ClientInfo) -> - ecpool:with_client(Pool, fun(C) -> epgsql:prepared_query(C, Sql, replvar(Params, ClientInfo)) end). - -replvar(Params, ClientInfo) -> - replvar(Params, ClientInfo, []). - -replvar([], _ClientInfo, Acc) -> - lists:reverse(Acc); - -replvar(["'%u'" | Params], ClientInfo = #{username := Username}, Acc) -> - replvar(Params, ClientInfo, [Username | Acc]); -replvar(["'%c'" | Params], ClientInfo = #{clientid := ClientId}, Acc) -> - replvar(Params, ClientInfo, [ClientId | Acc]); -replvar(["'%a'" | Params], ClientInfo = #{peerhost := IpAddr}, Acc) -> - replvar(Params, ClientInfo, [inet_parse:ntoa(IpAddr) | Acc]); -replvar(["'%C'" | Params], ClientInfo, Acc) -> - replvar(Params, ClientInfo, [safe_get(cn, ClientInfo)| Acc]); -replvar(["'%d'" | Params], ClientInfo, Acc) -> - replvar(Params, ClientInfo, [safe_get(dn, ClientInfo)| Acc]); -replvar([Param | Params], ClientInfo, Acc) -> - replvar(Params, ClientInfo, [Param | Acc]). - -safe_get(K, ClientInfo) -> - bin(maps:get(K, ClientInfo, undefined)). - -bin(A) when is_atom(A) -> atom_to_binary(A, utf8); -bin(B) when is_binary(B) -> B; -bin(X) -> X. - diff --git a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql_sup.erl b/apps/emqx_auth_pgsql/src/emqx_auth_pgsql_sup.erl deleted file mode 100644 index 21d005dc2..000000000 --- a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql_sup.erl +++ /dev/null @@ -1,37 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_pgsql_sup). - --behaviour(supervisor). - --include("emqx_auth_pgsql.hrl"). - -%% API --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - %% PgSQL Connection Pool - {ok, Opts} = application:get_env(?APP, server), - PoolSpec = ecpool:pool_spec(?APP, ?APP, emqx_auth_pgsql_cli, Opts), - {ok, {{one_for_one, 10, 100}, [PoolSpec]}}. - diff --git a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE.erl b/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE.erl deleted file mode 100644 index 6c4cd2eb3..000000000 --- a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE.erl +++ /dev/null @@ -1,221 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_pgsql_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --define(POOL, emqx_auth_pgsql). - --define(APP, emqx_auth_pgsql). - --include_lib("emqx/include/emqx.hrl"). - --include_lib("eunit/include/eunit.hrl"). - --include_lib("common_test/include/ct.hrl"). - -%%setp1 init table --define(DROP_ACL_TABLE, "DROP TABLE IF EXISTS mqtt_acl"). - --define(CREATE_ACL_TABLE, "CREATE TABLE mqtt_acl ( - id SERIAL primary key, - allow integer, - ipaddr character varying(60), - username character varying(100), - clientid character varying(100), - access integer, - topic character varying(100))"). - --define(INIT_ACL, "INSERT INTO mqtt_acl (id, allow, ipaddr, username, clientid, access, topic) - VALUES - (1,1,'127.0.0.1','u1','c1',1,'t1'), - (2,0,'127.0.0.1','u2','c2',1,'t1'), - (3,1,'10.10.0.110','u1','c1',1,'t1'), - (4,1,'127.0.0.1','u3','c3',3,'t1')"). - --define(DROP_AUTH_TABLE, "DROP TABLE IF EXISTS mqtt_user"). - --define(CREATE_AUTH_TABLE, "CREATE TABLE mqtt_user ( - id SERIAL primary key, - is_superuser boolean, - username character varying(100), - password character varying(100), - salt character varying(40))"). - --define(INIT_AUTH, "INSERT INTO mqtt_user (id, is_superuser, username, password, salt) - VALUES - (1, true, 'plain', 'plain', 'salt'), - (2, false, 'md5', '1bc29b36f623ba82aaf6724fd3b16718', 'salt'), - (3, false, 'sha', 'd8f4590320e1343a915b6394170650a8f35d6926', 'salt'), - (4, false, 'sha256', '5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e', 'salt'), - (5, false, 'pbkdf2_password', 'cdedb5281bb2f801565a1122b2563515', 'ATHENA.MIT.EDUraeburn'), - (6, false, 'bcrypt_foo', '$2a$12$sSS8Eg.ovVzaHzi1nUHYK.HbUIOdlQI0iS22Q5rd5z.JVVYH6sfm6', '$2a$12$sSS8Eg.ovVzaHzi1nUHYK.'), - (7, false, 'bcrypt', '$2y$16$rEVsDarhgHYB0TGnDFJzyu5f.T.Ha9iXMTk9J36NCMWWM7O16qyaK', 'salt')"). - -all() -> - emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_auth_pgsql]), - drop_acl(), - drop_auth(), - init_auth(), - init_acl(), - set_special_configs(), - Config. - -end_per_suite(Config) -> - emqx_ct_helpers:stop_apps([emqx_auth_pgsql]), - Config. - -set_special_configs() -> - application:set_env(emqx, acl_nomatch, deny), - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, enable_acl_cache, false). - -t_comment_config(_) -> - AuthCount = length(emqx_hooks:lookup('client.authenticate')), - AclCount = length(emqx_hooks:lookup('client.check_acl')), - application:stop(?APP), - [application:unset_env(?APP, Par) || Par <- [acl_query, auth_query]], - application:start(?APP), - ?assertEqual([], emqx_hooks:lookup('client.authenticate')), - ?assertEqual(AuthCount - 1, length(emqx_hooks:lookup('client.authenticate'))), - ?assertEqual(AclCount - 1, length(emqx_hooks:lookup('client.check_acl'))). - -t_placeholders(_) -> - ClientA = #{username => <<"plain">>, clientid => <<"plain">>, zone => external}, - reload([{password_hash, plain}, - {auth_query, "select password from mqtt_user where username = '%u' and 'a_cn_val' = '%C' limit 1"}]), - {error, not_authorized} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>}), - {error, not_authorized} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>, cn => undefined}), - {ok, _} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>, cn => <<"a_cn_val">>}), - - reload([{auth_query, "select password from mqtt_user where username = '%c' and 'a_dn_val' = '%d' limit 1"}]), - {error, not_authorized} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>}), - {error, not_authorized} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>, dn => undefined}), - {ok, _} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>, dn => <<"a_dn_val">>}), - - reload([{auth_query, "select password from mqtt_user where username = '%u' and '192.168.1.5' = '%a' limit 1"}]), - {error, not_authorized} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>}), - {ok, _} = - emqx_access_control:authenticate(ClientA#{password => <<"plain">>, peerhost => {192,168,1,5}}). - -t_check_auth(_) -> - Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external}, - Md5 = #{clientid => <<"md5">>, username => <<"md5">>, zone => external}, - Sha = #{clientid => <<"sha">>, username => <<"sha">>, zone => external}, - Sha256 = #{clientid => <<"sha256">>, username => <<"sha256">>, zone => external}, - Pbkdf2 = #{clientid => <<"pbkdf2_password">>, username => <<"pbkdf2_password">>, zone => external}, - BcryptFoo = #{clientid => <<"bcrypt_foo">>, username => <<"bcrypt_foo">>, zone => external}, - User1 = #{clientid => <<"bcrypt_foo">>, username => <<"user">>, zone => external}, - Bcrypt = #{clientid => <<"bcrypt">>, username => <<"bcrypt">>, zone => external}, - BcryptWrong = #{clientid => <<"bcrypt_wrong">>, username => <<"bcrypt_wrong">>, zone => external}, - reload([{password_hash, plain}]), - {ok,#{is_superuser := true}} = - emqx_access_control:authenticate(Plain#{password => <<"plain">>}), - reload([{password_hash, md5}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(Md5#{password => <<"md5">>}), - reload([{password_hash, sha}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(Sha#{password => <<"sha">>}), - reload([{password_hash, sha256}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(Sha256#{password => <<"sha256">>}), - reload([{password_hash, bcrypt}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(Bcrypt#{password => <<"password">>}), - {error, not_authorized} = - emqx_access_control:authenticate(BcryptWrong#{password => <<"password">>}), - %%pbkdf2 sha - reload([{password_hash, {pbkdf2, sha, 1, 16}}, - {auth_query, "select password, salt from mqtt_user where username = '%u' limit 1"}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(Pbkdf2#{password => <<"password">>}), - reload([{password_hash, {salt, bcrypt}}]), - {ok,#{is_superuser := false}} = - emqx_access_control:authenticate(BcryptFoo#{password => <<"foo">>}), - {error, _} = emqx_access_control:authenticate(User1#{password => <<"foo">>}), - {error, not_authorized} = emqx_access_control:authenticate(Bcrypt#{password => <<"password">>}). - -t_check_acl(_) -> - User1 = #{zone => external, peerhost => {127,0,0,1}, clientid => <<"c1">>, username => <<"u1">>}, - User2 = #{zone => external, peerhost => {127,0,0,1}, clientid => <<"c2">>, username => <<"u2">>}, - allow = emqx_access_control:check_acl(User1, subscribe, <<"t1">>), - deny = emqx_access_control:check_acl(User2, subscribe, <<"t1">>), - User3 = #{zone => external, peerhost => {10,10,0,110}, clientid => <<"c1">>, username => <<"u1">>}, - User4 = #{zone => external, peerhost => {10,10,10,110}, clientid => <<"c1">>, username => <<"u1">>}, - allow = emqx_access_control:check_acl(User3, subscribe, <<"t1">>), - allow = emqx_access_control:check_acl(User3, subscribe, <<"t1">>), - deny = emqx_access_control:check_acl(User3, subscribe, <<"t2">>),%% nomatch -> ignore -> emqx acl - deny = emqx_access_control:check_acl(User4, subscribe, <<"t1">>),%% nomatch -> ignore -> emqx acl - User5 = #{zone => external, peerhost => {127,0,0,1}, clientid => <<"c3">>, username => <<"u3">>}, - allow = emqx_access_control:check_acl(User5, subscribe, <<"t1">>), - allow = emqx_access_control:check_acl(User5, publish, <<"t1">>). - -t_acl_super(_) -> - reload([{password_hash, plain}, {auth_query, "select password from mqtt_user where username = '%u' limit 1"}]), - {ok, C} = emqtt:start_link([{host, "localhost"}, {clientid, <<"simpleClient">>}, - {username, <<"plain">>}, {password, <<"plain">>}]), - {ok, _} = emqtt:connect(C), - timer:sleep(10), - emqtt:subscribe(C, <<"TopicA">>, qos2), - emqtt:publish(C, <<"TopicA">>, <<"Payload">>, qos2), - timer:sleep(1000), - receive - {publish, #{payload := Payload}} -> - ?assertEqual(<<"Payload">>, Payload) - after - 1000 -> - ct:fail({receive_timeout, <<"Payload">>}), - ok - end, - emqtt:disconnect(C). - -reload(Config) when is_list(Config) -> - application:stop(?APP), - [application:set_env(?APP, K, V) || {K, V} <- Config], - application:start(?APP). - -init_acl() -> - {ok, Pid} = ecpool_worker:client(gproc_pool:pick_worker({ecpool, ?POOL})), - {ok, [], []} = epgsql:squery(Pid, ?DROP_ACL_TABLE), - {ok, [], []} = epgsql:squery(Pid, ?CREATE_ACL_TABLE), - {ok, _} = epgsql:equery(Pid, ?INIT_ACL). - -drop_acl() -> - {ok, Pid} = ecpool_worker:client(gproc_pool:pick_worker({ecpool, ?POOL})), - {ok, [], []}= epgsql:squery(Pid, ?DROP_ACL_TABLE). - -init_auth() -> - {ok, Pid} = ecpool_worker:client(gproc_pool:pick_worker({ecpool, ?POOL})), - {ok, [], []} = epgsql:squery(Pid, ?DROP_AUTH_TABLE), - {ok, [], []} = epgsql:squery(Pid, ?CREATE_AUTH_TABLE), - {ok, _} = epgsql:equery(Pid, ?INIT_AUTH). - -drop_auth() -> - {ok, Pid} = ecpool_worker:client(gproc_pool:pick_worker({ecpool, ?POOL})), - {ok, [], []} = epgsql:squery(Pid, ?DROP_AUTH_TABLE). diff --git a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/ca-key.pem b/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/ca-key.pem deleted file mode 100644 index e9717011e..000000000 --- a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/ca-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA0kGUBi9NDp65jgdxKfizIfuSr2wpwb44yM9SuP4oUQSULOA2 -4iFpLR/c5FAYHU81y9Vx91dQjdZfffaBZuv2zVvteXUkol8Nez7boKbo2E41MTew -8edtNKZAQVvnaHAC2NCZxjchCzUCDEoUUcl+cIERZ8R48FBqK5iTVcMRIx1akwus -+dhBqP0ykA5TGOWZkJrLM9aUXSPQha9+wXlOpkvu0Ur2nkX8PPJnifWao9UShSar -ll1IqPZNCSlZMwcFYcQNBCpdvITUUYlHvMRQV64bUpOxUGDuJkQL3dLKBlNuBRlJ -BcjBAKw7rFnwwHZcMmQ9tan/dZzpzwjo/T0XjwIDAQABAoIBAQCSHvUqnzDkWjcG -l/Fzg92qXlYBCCC0/ugj1sHcwvVt6Mq5rVE3MpUPwTcYjPlVVTlD4aEEjm/zQuq2 -ddxUlOS+r4aIhHrjRT/vSS4FpjnoKeIZxGR6maVxk6DQS3i1QjMYT1CvSpzyVvKH -a+xXMrtmoKxh+085ZAmFJtIuJhUA2yEa4zggCxWnvz8ecLClUPfVDPhdLBHc3KmL -CRpHEC6L/wanvDPRdkkzfKyaJuIJlTDaCg63AY5sDkTW2I57iI/nJ3haSeidfQKz -39EfbnM1A/YprIakafjAu3frBIsjBVcxwGihZmL/YriTHjOggJF841kT5zFkkv2L -/530Wk6xAoGBAOqZLZ4DIi/zLndEOz1mRbUfjc7GQUdYplBnBwJ22VdS0P4TOXnd -UbJth2MA92NM7ocTYVFl4TVIZY/Y+Prxk7KQdHWzR7JPpKfx9OEVgtSqV0vF9eGI -rKp79Y1T4Mvc3UcQCXX6TP7nHLihEzpS8odm2LW4txrOiLsn4Fq/IWrLAoGBAOVv -6U4tm3lImotUupKLZPKEBYwruo9qRysoug9FiorP4TjaBVOfltiiHbAQD6aGfVtN -SZpZZtrs17wL7Xl4db5asgMcZd+8Hkfo5siR7AuGW9FZloOjDcXb5wCh9EvjJ74J -Cjw7RqyVymq9t7IP6wnVwj5Ck48YhlOZCz/mzlnNAoGAWq7NYFgLvgc9feLFF23S -IjpJQZWHJEITP98jaYNxbfzYRm49+GphqxwFinKULjFNvq7yHlnIXSVYBOu1CqOZ -GRwXuGuNmlKI7lZr9xmukfAqgGLMMdr4C4qRF4lFyufcLRz42z7exmWlx4ST/yaT -E13hBRWayeTuG5JFei6Jh1MCgYEAqmX4LyC+JFBgvvQZcLboLRkSCa18bADxhENG -FAuAvmFvksqRRC71WETmqZj0Fqgxt7pp3KFjO1rFSprNLvbg85PmO1s+6fCLyLpX -lESTu2d5D71qhK93jigooxalGitFm+SY3mzjq0/AOpBWOn+J/w7rqVPGxXLgaHv0 -l+vx+00CgYBOvo9/ImjwYii2jFl+sHEoCzlvpITi2temRlT2j6ulSjCLJgjwEFw9 -8e+vvfQumQOsutakUVyURrkMGNDiNlIv8kv5YLCCkrwN22E6Ghyi69MJUvHQXkc/ -QZhjn/luyfpB5f/BeHFS2bkkxAXo+cfG45ApY3Qfz6/7o+H+vDa6/A== ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/ca.pem b/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/ca.pem deleted file mode 100644 index 00b31d8a4..000000000 --- a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/ca.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDAzCCAeugAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowPDE6MDgGA1UEAwwxTXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DQV9DZXJ0aWZpY2F0ZTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANJBlAYvTQ6euY4HcSn4syH7kq9s -KcG+OMjPUrj+KFEElCzgNuIhaS0f3ORQGB1PNcvVcfdXUI3WX332gWbr9s1b7Xl1 -JKJfDXs+26Cm6NhONTE3sPHnbTSmQEFb52hwAtjQmcY3IQs1AgxKFFHJfnCBEWfE -ePBQaiuYk1XDESMdWpMLrPnYQaj9MpAOUxjlmZCayzPWlF0j0IWvfsF5TqZL7tFK -9p5F/DzyZ4n1mqPVEoUmq5ZdSKj2TQkpWTMHBWHEDQQqXbyE1FGJR7zEUFeuG1KT -sVBg7iZEC93SygZTbgUZSQXIwQCsO6xZ8MB2XDJkPbWp/3Wc6c8I6P09F48CAwEA -AaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADKz6bIpP5anp -GgLB0jkclRWuMlS4qqIt4itSsMXPJ/ezpHwECixmgW2TIQl6S1woRkUeMxhT2/Ay -Sn/7aKxuzRagyE5NEGOvrOuAP5RO2ZdNJ/X3/Rh533fK1sOTEEbSsWUvW6iSkZef -rsfZBVP32xBhRWkKRdLeLB4W99ADMa0IrTmZPCXHSSE2V4e1o6zWLXcOZeH1Qh8N -SkelBweR+8r1Fbvy1r3s7eH7DCbYoGEDVLQGOLvzHKBisQHmoDnnF5E9g1eeNRdg -o+vhOKfYCOzeNREJIqS42PHcGhdNRk90ycigPmfUJclz1mDHoMjKR2S5oosTpr65 -tNPx3CL7GA== ------END CERTIFICATE----- diff --git a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/client-cert.pem b/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/client-cert.pem deleted file mode 100644 index aad1404ca..000000000 --- a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/client-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIBAzANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0N1oXDTMwMDYwOTAzMzg0N1owQDE+MDwGA1UEAww1TXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DbGllbnRfQ2VydGlmaWNhdGUw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVYSWpOvCTupz82fc85Opv -EQ7rkB8X2oOMyBCpkyHKBIr1ZQgRDWBp9UVOASq3GnSElm6+T3Kb1QbOffa8GIlw -sjAueKdq5L2eSkmPIEQ7eoO5kEW+4V866hE1LeL/PmHg2lGP0iqZiJYtElhHNQO8 -3y9I7cm3xWMAA3SSWikVtpJRn3qIp2QSrH+tK+/HHbE5QwtPxdir4ULSCSOaM5Yh -Wi5Oto88TZqe1v7SXC864JVvO4LuS7TuSreCdWZyPXTJFBFeCEWSAxonKZrqHbBe -CwKML6/0NuzjaQ51c2tzmVI6xpHj3nnu4cSRx6Jf9WBm+35vm0wk4pohX3ptdzeV -AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAByQ5zSNeFUH -Aw7JlpZHtHaSEeiiyBHke20ziQ07BK1yi/ms2HAWwQkpZv149sjNuIRH8pkTmkZn -g8PDzSefjLbC9AsWpWV0XNV22T/cdobqLqMBDDZ2+5bsV+jTrOigWd9/AHVZ93PP -IJN8HJn6rtvo2l1bh/CdsX14uVSdofXnuWGabNTydqtMvmCerZsdf6qKqLL+PYwm -RDpgWiRUY7KPBSSlKm/9lJzA+bOe4dHeJzxWFVCJcbpoiTFs1je1V8kKQaHtuW39 -ifX6LTKUMlwEECCbDKM8Yq2tm8NjkjCcnFDtKg8zKGPUu+jrFMN5otiC3wnKcP7r -O9EkaPcgYH8= ------END CERTIFICATE----- diff --git a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/client-key.pem b/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/client-key.pem deleted file mode 100644 index 6789d0291..000000000 --- a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/client-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA1WElqTrwk7qc/Nn3POTqbxEO65AfF9qDjMgQqZMhygSK9WUI -EQ1gafVFTgEqtxp0hJZuvk9ym9UGzn32vBiJcLIwLninauS9nkpJjyBEO3qDuZBF -vuFfOuoRNS3i/z5h4NpRj9IqmYiWLRJYRzUDvN8vSO3Jt8VjAAN0klopFbaSUZ96 -iKdkEqx/rSvvxx2xOUMLT8XYq+FC0gkjmjOWIVouTraPPE2antb+0lwvOuCVbzuC -7ku07kq3gnVmcj10yRQRXghFkgMaJyma6h2wXgsCjC+v9Dbs42kOdXNrc5lSOsaR -49557uHEkceiX/VgZvt+b5tMJOKaIV96bXc3lQIDAQABAoIBAF7yjXmSOn7h6P0y -WCuGiTLG2mbDiLJqj2LTm2Z5i+2Cu/qZ7E76Ls63TxF4v3MemH5vGfQhEhR5ZD/6 -GRJ1sKKvB3WGRqjwA9gtojHH39S/nWGy6vYW/vMOOH37XyjIr3EIdIaUtFQBTSHd -Kd71niYrAbVn6fyWHolhADwnVmTMOl5OOAhCdEF4GN3b5aIhIu8BJ7EUzTtHBJIj -CAEfjZFjDs1y1cIgGFJkuIQxMfCpq5recU2qwip7YO6fk//WEjOPu7kSf5IEswL8 -jg1dea9rGBV6KaD2xsgsC6Ll6Sb4BbsrHMfflG3K2Lk3RdVqqTFp1Fn1PTLQE/1S -S/SZPYECgYEA9qYcHKHd0+Q5Ty5wgpxKGa4UCWkpwvfvyv4bh8qlmxueB+l2AIdo -ZvkM8gTPagPQ3WypAyC2b9iQu70uOJo1NizTtKnpjDdN1YpDjISJuS/P0x73gZwy -gmoM5AzMtN4D6IbxXtXnPaYICvwLKU80ouEN5ZPM4/ODLUu6gsp0v2UCgYEA3Xgi -zMC4JF0vEKEaK0H6QstaoXUmw/lToZGH3TEojBIkb/2LrHUclygtONh9kJSFb89/ -jbmRRLAOrx3HZKCNGUmF4H9k5OQyAIv6OGBinvLGqcbqnyNlI+Le8zxySYwKMlEj -EMrBCLmSyi0CGFrbZ3mlj/oCET/ql9rNvcK+DHECgYAEx5dH3sMjtgp+RFId1dWB -xePRgt4yTwewkVgLO5wV82UOljGZNQaK6Eyd7AXw8f38LHzh+KJQbIvxd2sL4cEi -OaAoohpKg0/Y0YMZl//rPMf0OWdmdZZs/I0fZjgZUSwWN3c59T8z7KG/RL8an9RP -S7kvN7wCttdV61/D5RR6GQKBgDxCe/WKWpBKaovzydMLWLTj7/0Oi0W3iXHkzzr4 -LTgvl4qBSofaNbVLUUKuZTv5rXUG2IYPf99YqCYtzBstNDc1MiAriaBeFtzfOW4t -i6gEFtoLLbuvPc3N5Sv5vn8Ug5G9UfU3td5R4AbyyCcoUZqOFuZd+EIJSiOXfXOs -kVmBAoGBAIU9aPAqhU5LX902oq8KsrpdySONqv5mtoStvl3wo95WIqXNEsFY60wO -q02jKQmJJ2MqhkJm2EoF2Mq8+40EZ5sz8LdgeQ/M0yQ9lAhPi4rftwhpe55Ma9dk -SE9X1c/DMCBEaIjJqVXdy0/EeArwpb8sHkguVVAZUWxzD+phm1gs ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/private_key.pem b/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/private_key.pem deleted file mode 100644 index 8fbf6bdec..000000000 --- a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/private_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA1zVmMhPqpSPMmYkKh5wwlRD5XuS8YWJKEM6tjFx61VK8qxHE -YngkC2KnL5EuKAjQZIF3tJskwt0hAat047CCCZxrkNEpbVvSnvnk+A/8bg/Ww1n3 -qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XAPy7ZjzecAF9SDV6WSiPeAxUX2+hN -dId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGwcnkKCUikiBqr2ijSIgvRtBfZ9fBG -jFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtPnBtTytmqTy9V/BRgsVKUoksm6wsx -kUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h+wIDAQABAoIBAEQcrHmRACTADdNS -IjkFYALt2l8EOfMAbryfDSJtapr1kqz59JPNvmq0EIHnixo0n/APYdmReLML1ZR3 -tYkSpjVwgkLVUC1CcIjMQoGYXaZf8PLnGJHZk45RR8m6hsTV0mQ5bfBaeVa2jbma -OzJMjcnxg/3l9cPQZ2G/3AUfEPccMxOXp1KRz3mUQcGnKJGtDbN/kfmntcwYoxaE -Zg4RoeKAoMpK1SSHAiJKe7TnztINJ7uygR9XSzNd6auY8A3vomSIjpYO7XL+lh7L -izm4Ir3Gb/eCYBvWgQyQa2KCJgK/sQyEs3a09ngofSEUhQJQYhgZDwUj+fDDOGqj -hCZOA8ECgYEA+ZWuHdcUQ3ygYhLds2QcogUlIsx7C8n/Gk/FUrqqXJrTkuO0Eqqa -B47lCITvmn2zm0ODfSFIARgKEUEDLS/biZYv7SUTrFqBLcet+aGI7Dpv91CgB75R -tNzcIf8VxoiP0jPqdbh9mLbbxGi5Uc4p9TVXRljC4hkswaouebWee0sCgYEA3L2E -YB3kiHrhPI9LHS5Px9C1w+NOu5wP5snxrDGEgaFCvL6zgY6PflacppgnmTXl8D1x -im0IDKSw5dP3FFonSVXReq3CXDql7UnhfTCiLDahV7bLxTH42FofcBpDN3ERdOal -58RwQh6VrLkzQRVoObo+hbGlFiwwSAfQC509FhECgYBsRSBpVXo25IN2yBRg09cP -+gdoFyhxrsj5kw1YnB13WrrZh+oABv4WtUhp77E5ZbpaamlKCPwBbXpAjeFg4tfr -0bksuN7V79UGFQ9FsWuCfr8/nDwv38H2IbFlFhFONMOfPmJBey0Q6JJhm8R41mSh -OOiJXcv85UrjIH5U0hLUDQKBgQDVLOU5WcUJlPoOXSgiT0ZW5xWSzuOLRUUKEf6l -19BqzAzCcLy0orOrRAPW01xylt2v6/bJw1Ahva7k1ZZo/kOwjANYoZPxM+ZoSZBN -MXl8j2mzZuJVV1RFxItV3NcLJNPB/Lk+IbRz9kt/2f9InF7iWR3mSU/wIM6j0X+2 -p6yFsQKBgQCM/ldWb511lA+SNkqXB2P6WXAgAM/7+jwsNHX2ia2Ikufm4SUEKMSv -mti/nZkHDHsrHU4wb/2cOAywMELzv9EHzdcoenjBQP65OAc/1qWJs+LnBcCXfqKk -aHjEZW6+brkHdRGLLY3YAHlt/AUL+RsKPJfN72i/FSpmu+52G36eeQ== ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/public_key.pem b/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/public_key.pem deleted file mode 100644 index f9772b533..000000000 --- a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/public_key.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1zVmMhPqpSPMmYkKh5ww -lRD5XuS8YWJKEM6tjFx61VK8qxHEYngkC2KnL5EuKAjQZIF3tJskwt0hAat047CC -CZxrkNEpbVvSnvnk+A/8bg/Ww1n3qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XA -Py7ZjzecAF9SDV6WSiPeAxUX2+hNdId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGw -cnkKCUikiBqr2ijSIgvRtBfZ9fBGjFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtP -nBtTytmqTy9V/BRgsVKUoksm6wsxkUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h -+wIDAQAB ------END PUBLIC KEY----- diff --git a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-cert.pem b/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-cert.pem deleted file mode 100644 index a2f9688df..000000000 --- a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua -NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z -G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL -JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB -4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy -TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9 -AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6 -zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI -hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F -sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD -3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR -+DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC -dN/klu446fI= ------END CERTIFICATE----- diff --git a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-key.pem b/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-key.pem deleted file mode 100644 index a1dfd5f78..000000000 --- a/apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj -U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho -XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT -29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX -NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv -f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn -WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP -PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV -4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS -VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk -Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb -SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq -EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx -VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH -cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0 -ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h -J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ -h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K -eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq -dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD -PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes -Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2 -/UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH -PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd -JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_redis/.gitignore b/apps/emqx_auth_redis/.gitignore deleted file mode 100644 index 71ecbe89a..000000000 --- a/apps/emqx_auth_redis/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -.rebar/ -.eunit/ -.erlang.mk/ -emqttd_auth_redis.d -deps/ -ct.coverdata -logs/ -test/ct.cover.spec -ebin/ -*.o -*.beam -*.plt -erl_crash.dump -data -emqx_auth_redis.d -cover/ -eunit.coverdata -_build/ -rebar.lock -erlang.mk -*.conf.rendered -.rebar3/ -*.swp -rebar.lock -/.idea/ -.DS_Store -/.ci/redis/nodes.*.conf -/.ci/redis/*.log \ No newline at end of file diff --git a/apps/emqx_auth_redis/README.md b/apps/emqx_auth_redis/README.md deleted file mode 100644 index 9aa851f88..000000000 --- a/apps/emqx_auth_redis/README.md +++ /dev/null @@ -1,171 +0,0 @@ -emqx_auth_redis -=============== - -EMQ X Redis Authentication/ACL Plugin - -Features ---------- - -- Full *Authentication*, *Superuser*, *ACL* support -- IPv4, IPv6 support -- Connection pool by [ecpool](https://github.com/emqx/ecpool) -- Support `single`, `sentinel`, `cluster` deployment structures of Redis -- Completely cover Redis 5, Redis 6 in our tests - - -Build Plugin ------------- - -``` -make && make tests -``` - -Configure Plugin ----------------- - -File: etc/emqx_auth_redis.conf - -``` -## Redis server address. -## -## Value: Port | IP:Port -## -## Redis Server: 6379, 127.0.0.1:6379, localhost:6379, Redis Sentinel: 127.0.0.1:26379 -auth.redis.server = 127.0.0.1:6379 - -## redis sentinel cluster name -## auth.redis.sentinel = mymaster - -## Redis pool size. -## -## Value: Number -auth.redis.pool = 8 - -## Redis database no. -## -## Value: Number -auth.redis.database = 0 - -## Redis password. -## -## Value: String -## auth.redis.password = - -## Authentication query command. -## -## Value: Redis cmd -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -## Examples: -## - HGET mqtt_user:%u password -## - HMGET mqtt_user:%u password -## - HMGET mqtt_user:%u password salt -auth.redis.auth_cmd = HMGET mqtt_user:%u password - -## Password hash. -## -## Value: plain | md5 | sha | sha256 | bcrypt -auth.redis.password_hash = plain - -## sha256 with salt prefix -## auth.redis.password_hash = salt,sha256 - -## sha256 with salt suffix -## auth.redis.password_hash = sha256,salt - -## bcrypt with salt prefix -## auth.redis.password_hash = salt,bcrypt - -## pbkdf2 with macfun iterations dklen -## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512 -## auth.redis.password_hash = pbkdf2,sha256,1000,20 - -## Superuser query command. -## -## Value: Redis cmd -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -auth.redis.super_cmd = HGET mqtt_user:%u is_superuser - -## ACL query command. -## -## Value: Redis cmd -## -## Variables: -## - %u: username -## - %c: clientid -auth.redis.acl_cmd = HGETALL mqtt_acl:%u -``` - -SuperUser ---------- - -``` -HSET mqtt_user: is_superuser 1 -``` - -User Hash with Password Salt ----------------------------- - -Set a 'user' hash with 'password' 'salt' field, for example: - -``` -HMSET mqtt_user: password "password" salt "salt" -``` - -User Set with Password ------------------------ - -Set a 'user' Set with 'password' field for example: - -``` -HSET mqtt_user: password "password" -``` - -ACL Rule Hash -------------- - -The plugin uses a redis hash to store ACL rules: - -``` -HSET mqtt_acl: topic1 1 -HSET mqtt_acl: topic2 2 -HSET mqtt_acl: topic3 3 -``` - -NOTE: 1: subscribe, 2: publish, 3: pubsub - -Subscription Hash ------------------ - -NOTICE: Move to emqx_backend_redis... - -The plugin could store the static subscriptions into a redis Hash: - -``` -HSET mqtt_sub: topic1 0 -HSET mqtt_sub: topic2 1 -HSET mqtt_sub: topic3 2 -``` - -Load Plugin ------------ - -``` -./bin/emqx_ctl plugins load emqx_auth_redis -``` - -Author ------- - -EMQ X Team. - diff --git a/apps/emqx_auth_redis/etc/emqx_auth_redis.conf b/apps/emqx_auth_redis/etc/emqx_auth_redis.conf deleted file mode 100644 index 62a6e4fe1..000000000 --- a/apps/emqx_auth_redis/etc/emqx_auth_redis.conf +++ /dev/null @@ -1,131 +0,0 @@ -##-------------------------------------------------------------------- -## Redis Auth/ACL Plugin -##-------------------------------------------------------------------- -## Redis Server cluster type -## single Single redis server -## sentinel Redis cluster through sentinel -## cluster Redis through cluster -auth.redis.type = single - -## Redis server address. -## -## Value: Port | IP:Port -## -## Single Redis Server: 127.0.0.1:6379, localhost:6379 -## Redis Sentinel: "127.0.0.1:26379,127.0.0.2:26379,127.0.0.3:26379" -## Redis Cluster: "127.0.0.1:6379,127.0.0.2:6379,127.0.0.3:6379" -auth.redis.server = "127.0.0.1:6379" - -## Redis sentinel cluster name. -## -## Value: String -## auth.redis.sentinel = mymaster - -## Redis pool size. -## -## Value: Number -auth.redis.pool = 8 - -## Redis database no. -## -## Value: Number -auth.redis.database = 0 - -## Redis password. -## -## Value: String -## auth.redis.password = - -## Redis query timeout -## -## Value: Duration -## auth.redis.query_timeout = 5s - -## Authentication query command. -## -## Value: Redis cmd -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -## -## Examples: -## - "HGET mqtt_user:%u password" -## - "HMGET mqtt_user:%u password" -## - "HMGET mqtt_user:%u password salt" -auth.redis.auth_cmd = "HMGET mqtt_user:%u password" - -## Password hash. -## -## Value: plain | md5 | sha | sha256 | bcrypt -auth.redis.password_hash = plain - -## sha256 with salt prefix -## auth.redis.password_hash = "salt,sha256" - -## sha256 with salt suffix -## auth.redis.password_hash = "sha256,salt" - -## bcrypt with salt prefix -## auth.redis.password_hash = "salt,bcrypt" - -## pbkdf2 with macfun iterations dklen -## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512 -## auth.redis.password_hash = "pbkdf2,sha256,1000,20" - -## Superuser query command. -## -## Value: Redis cmd -## -## Variables: -## - %u: username -## - %c: clientid -## - %C: common name of client TLS cert -## - %d: subject of client TLS cert -auth.redis.super_cmd = "HGET mqtt_user:%u is_superuser" - -## ACL query command. -## -## Value: Redis cmd -## -## Variables: -## - %u: username -## - %c: clientid -auth.redis.acl_cmd = "HGETALL mqtt_acl:%u" - -## Redis ssl configuration. -## -## Value: on | off -# auth.redis.ssl.enable = off - -## CA certificate. -## -## Value: File -#auth.redis.ssl.cacertfile = path/to/your/cafile.pem - -## Client ssl certificate. -## -## Value: File -# auth.redis.ssl.certfile = path/to/your/certfile - -## Client ssl keyfile. -## -## Value: File -# auth.redis.ssl.keyfile = path/to/your/keyfile - -## In mode verify_none the default behavior is to allow all x509-path -## validation errors. -## -## Value: true | false -#auth.redis.ssl.verify = false - -## If not specified, the server's names returned in server's certificate is validated against -## what's provided `auth.redis.server` config's host part. -## Setting to 'disable' will make EMQ X ignore unmatched server names. -## If set with a host name, the server's names returned in server's certificate is validated -## against this value. -## -## Value: String | disable -## auth.redis.ssl.server_name_indication = disable \ No newline at end of file diff --git a/apps/emqx_auth_redis/include/emqx_auth_redis.hrl b/apps/emqx_auth_redis/include/emqx_auth_redis.hrl deleted file mode 100644 index 204d8ef70..000000000 --- a/apps/emqx_auth_redis/include/emqx_auth_redis.hrl +++ /dev/null @@ -1,23 +0,0 @@ - --define(APP, emqx_auth_redis). - --record(auth_metrics, { - success = 'client.auth.success', - failure = 'client.auth.failure', - ignore = 'client.auth.ignore' - }). - --record(acl_metrics, { - allow = 'client.acl.allow', - deny = 'client.acl.deny', - ignore = 'client.acl.ignore' - }). - --define(METRICS(Type), tl(tuple_to_list(#Type{}))). --define(METRICS(Type, K), #Type{}#Type.K). - --define(AUTH_METRICS, ?METRICS(auth_metrics)). --define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). - --define(ACL_METRICS, ?METRICS(acl_metrics)). --define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). diff --git a/apps/emqx_auth_redis/priv/emqx_auth_redis.schema b/apps/emqx_auth_redis/priv/emqx_auth_redis.schema deleted file mode 100644 index db758d8c0..000000000 --- a/apps/emqx_auth_redis/priv/emqx_auth_redis.schema +++ /dev/null @@ -1,184 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_auth_redis config mapping - -{mapping, "auth.redis.type", "emqx_auth_redis.server", [ - {default, single}, - {datatype, {enum, [single, sentinel, cluster]}} -]}. - -{mapping, "auth.redis.server", "emqx_auth_redis.server", [ - {default, "127.0.0.1:6379"}, - {datatype, [string]} -]}. - -{mapping, "auth.redis.sentinel", "emqx_auth_redis.server", [ - {default, ""}, - {datatype, string}, - hidden -]}. - -{mapping, "auth.redis.pool", "emqx_auth_redis.server", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "auth.redis.database", "emqx_auth_redis.server", [ - {default, 0}, - {datatype, integer} -]}. - -{mapping, "auth.redis.password", "emqx_auth_redis.server", [ - {default, ""}, - {datatype, string}, - hidden -]}. - -{mapping, "auth.redis.ssl.enable", "emqx_auth_redis.options", [ - {default, off}, - {datatype, flag} -]}. - -{mapping, "auth.redis.ssl.cacertfile", "emqx_auth_redis.options", [ - {datatype, string} -]}. - -{mapping, "auth.redis.ssl.certfile", "emqx_auth_redis.options", [ - {datatype, string} -]}. - -{mapping, "auth.redis.ssl.keyfile", "emqx_auth_redis.options", [ - {datatype, string} -]}. - -{mapping, "auth.redis.ssl.verify", "emqx_auth_redis.options", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "auth.redis.ssl.server_name_indication", "emqx_auth_redis.options", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.redis.cafile", "emqx_auth_redis.options", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.redis.certfile", "emqx_auth_redis.options", [ - {datatype, string} -]}. - -%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 -{mapping, "auth.redis.keyfile", "emqx_auth_redis.options", [ - {datatype, string} -]}. - -{translation, "emqx_auth_redis.options", fun(Conf) -> - Ssl = cuttlefish:conf_get("auth.redis.ssl.enable", Conf, false), - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - case Ssl of - true -> - %% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0 - CA = cuttlefish:conf_get( - "auth.redis.ssl.cacertfile", Conf, - cuttlefish:conf_get("auth.redis.cafile", Conf, undefined) - ), - Cert = cuttlefish:conf_get( - "auth.redis.ssl.certfile", Conf, - cuttlefish:conf_get("auth.redis.certfile", Conf, undefined) - ), - Key = cuttlefish:conf_get( - "auth.redis.ssl.keyfile", Conf, - cuttlefish:conf_get("auth.redis.keyfile", Conf, undefined) - ), - Verify = case cuttlefish:conf_get("auth.redis.ssl.verify", Conf, false) of - true -> verify_peer; - false -> verify_none - end, - SNI = case cuttlefish:conf_get("auth.redis.ssl.server_name_indication", Conf, undefined) of - "disable" -> disable; - SNI0 -> SNI0 - end, - [{options, [{ssl_options, - Filter([{cacertfile, CA}, - {certfile, Cert}, - {keyfile, Key}, - {verify, Verify}, - {server_name_indication, SNI} - ]) - }]}]; - _ -> [{options, []}] - end -end}. - -{translation, "emqx_auth_redis.server", fun(Conf) -> - Fun = fun(S) -> - case string:split(S, ":", trailing) of - [Domain] -> {Domain, 6379}; - [Domain, Port] -> {Domain, list_to_integer(Port)} - end - end, - Servers = cuttlefish:conf_get("auth.redis.server", Conf), - Type = cuttlefish:conf_get("auth.redis.type", Conf), - Server = case Type of - single -> - {Host, Port} = Fun(Servers), - [{host, Host}, {port, Port}]; - _ -> - S = string:tokens(Servers, ","), - [{servers, [Fun(S1) || S1 <- S]}] - end, - Pool = cuttlefish:conf_get("auth.redis.pool", Conf), - Passwd = cuttlefish:conf_get("auth.redis.password", Conf), - DB = cuttlefish:conf_get("auth.redis.database", Conf), - Sentinel = cuttlefish:conf_get("auth.redis.sentinel", Conf), - [{type, Type}, - {pool_size, Pool}, - {auto_reconnect, 1}, - {database, DB}, - {password, Passwd}, - {sentinel, Sentinel}] ++ Server -end}. - -{mapping, "auth.redis.query_timeout", "emqx_auth_redis.query_timeout", [ - {default, ""}, - {datatype, string} -]}. - -{translation, "emqx_auth_redis.query_timeout", fun(Conf) -> - case cuttlefish:conf_get("auth.redis.query_timeout", Conf) of - "" -> infinity; - Duration -> - case cuttlefish_duration:parse(Duration, ms) of - {error, Reason} -> error(Reason); - Ms when is_integer(Ms) -> Ms - end - end -end}. - -{mapping, "auth.redis.auth_cmd", "emqx_auth_redis.auth_cmd", [ - {datatype, string} -]}. - -{mapping, "auth.redis.password_hash", "emqx_auth_redis.password_hash", [ - {datatype, string} -]}. - -{mapping, "auth.redis.super_cmd", "emqx_auth_redis.super_cmd", [ - {datatype, string} -]}. - -{mapping, "auth.redis.acl_cmd", "emqx_auth_redis.acl_cmd", [ - {datatype, string} -]}. - -{translation, "emqx_auth_redis.password_hash", fun(Conf) -> - HashValue = cuttlefish:conf_get("auth.redis.password_hash", Conf), - case string:tokens(HashValue, ",") of - [Hash] -> list_to_atom(Hash); - [Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)}; - [Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), list_to_integer(Iterations), list_to_integer(Dklen)}; - _ -> plain - end -end}. diff --git a/apps/emqx_auth_redis/rebar.config b/apps/emqx_auth_redis/rebar.config deleted file mode 100644 index 750f07809..000000000 --- a/apps/emqx_auth_redis/rebar.config +++ /dev/null @@ -1,24 +0,0 @@ -{deps, - %% NOTE: mind poolboy version when updating eredis_cluster version - %% poolboy version may clash with emqx_auth_mongo - [ - {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}} - ]}. - -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - compressed - ]}. -{overrides, [{add, [{erl_opts, [compressed]}]}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions - ]}. - -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. diff --git a/apps/emqx_auth_redis/src/emqx_acl_redis.erl b/apps/emqx_auth_redis/src/emqx_acl_redis.erl deleted file mode 100644 index 47f5acbba..000000000 --- a/apps/emqx_auth_redis/src/emqx_acl_redis.erl +++ /dev/null @@ -1,86 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_acl_redis). - --include("emqx_auth_redis.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --export([ register_metrics/0 - , check_acl/5 - , description/0 - ]). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). - -check_acl(ClientInfo, PubSub, Topic, AclResult, Config) -> - case do_check_acl(ClientInfo, PubSub, Topic, AclResult, Config) of - ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok; - {stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow}; - {stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny} - end. - -do_check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _Config) -> - ok; -do_check_acl(ClientInfo, PubSub, Topic, _AclResult, - #{acl_cmd := AclCmd, timeout := Timeout, type := Type, pool := Pool}) -> - case emqx_auth_redis_cli:q(Pool, Type, AclCmd, ClientInfo, Timeout) of - {ok, []} -> ok; - {ok, Rules} -> - case match(ClientInfo, PubSub, Topic, Rules) of - allow -> {stop, allow}; - nomatch -> {stop, deny} - end; - {error, Reason} -> - ?LOG(error, "[Redis] do_check_acl error: ~p", [Reason]), - ok - end. - -match(_ClientInfo, _PubSub, _Topic, []) -> - nomatch; -match(ClientInfo, PubSub, Topic, [Filter, Access | Rules]) -> - case {match_topic(Topic, feed_var(ClientInfo, Filter)), - match_access(PubSub, b2i(Access))} of - {true, true} -> allow; - {_, _} -> match(ClientInfo, PubSub, Topic, Rules) - end. - -match_topic(Topic, Filter) -> - emqx_topic:match(Topic, Filter). - -match_access(subscribe, Access) -> - (1 band Access) > 0; -match_access(publish, Access) -> - (2 band Access) > 0. - -feed_var(#{clientid := ClientId, username := Username}, Str) -> - lists:foldl(fun({Var, Val}, Acc) -> - feed_var(Acc, Var, Val) - end, Str, [{"%u", Username}, {"%c", ClientId}]). - -feed_var(Str, _Var, undefined) -> - Str; -feed_var(Str, Var, Val) -> - re:replace(Str, Var, Val, [global, {return, binary}]). - -b2i(Bin) -> list_to_integer(binary_to_list(Bin)). - -description() -> "Redis ACL Module". - diff --git a/apps/emqx_auth_redis/src/emqx_auth_redis.app.src b/apps/emqx_auth_redis/src/emqx_auth_redis.app.src deleted file mode 100644 index 419131566..000000000 --- a/apps/emqx_auth_redis/src/emqx_auth_redis.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_auth_redis, - [{description, "EMQ X Authentication/ACL with Redis"}, - {vsn, "4.4.0"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_auth_redis_sup]}, - {applications, [kernel,stdlib,eredis,eredis_cluster,ecpool]}, - {mod, {emqx_auth_redis_app, []}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-auth-redis"} - ]} - ]}. diff --git a/apps/emqx_auth_redis/src/emqx_auth_redis.erl b/apps/emqx_auth_redis/src/emqx_auth_redis.erl deleted file mode 100644 index 318a8b23a..000000000 --- a/apps/emqx_auth_redis/src/emqx_auth_redis.erl +++ /dev/null @@ -1,85 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_redis). - --include("emqx_auth_redis.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --export([ register_metrics/0 - , check/3 - , description/0 - ]). - --spec(register_metrics() -> ok). -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). - -check(ClientInfo = #{password := Password}, AuthResult, - #{auth_cmd := AuthCmd, - super_cmd := SuperCmd, - hash_type := HashType, - timeout := Timeout, - type := Type, - pool := Pool}) -> - CheckPass = case emqx_auth_redis_cli:q(Pool, Type, AuthCmd, ClientInfo, Timeout) of - {ok, PassHash} when is_binary(PassHash) -> - check_pass({PassHash, Password}, HashType); - {ok, [undefined|_]} -> - {error, not_found}; - {ok, [PassHash]} -> - check_pass({PassHash, Password}, HashType); - {ok, [PassHash, Salt|_]} -> - check_pass({PassHash, Salt, Password}, HashType); - {error, Reason} -> - ?LOG(error, "[Redis] Command: ~p failed: ~p", [AuthCmd, Reason]), - {error, not_found} - end, - case CheckPass of - ok -> - ok = emqx_metrics:inc(?AUTH_METRICS(success)), - IsSuperuser = is_superuser(Pool, Type, SuperCmd, ClientInfo, Timeout), - {stop, AuthResult#{is_superuser => IsSuperuser, - anonymous => false, - auth_result => success}}; - {error, not_found} -> - ok = emqx_metrics:inc(?AUTH_METRICS(ignore)); - {error, ResultCode} -> - ok = emqx_metrics:inc(?AUTH_METRICS(failure)), - ?LOG(error, "[Redis] Auth from redis failed: ~p", [ResultCode]), - {stop, AuthResult#{auth_result => ResultCode, anonymous => false}} - end. - -description() -> "Authentication with Redis". - --spec(is_superuser(atom(), atom(), undefined|list(), emqx_types:client(), timeout()) -> boolean()). -is_superuser(_Pool, _Type, undefined, _ClientInfo, _Timeout) -> false; -is_superuser(Pool, Type, SuperCmd, ClientInfo, Timeout) -> - case emqx_auth_redis_cli:q(Pool, Type, SuperCmd, ClientInfo, Timeout) of - {ok, undefined} -> false; - {ok, <<"1">>} -> true; - {ok, _Other} -> false; - {error, _Error} -> false - end. - -check_pass(Password, HashType) -> - case emqx_passwd:check_pass(Password, HashType) of - ok -> ok; - {error, _Reason} -> {error, not_authorized} - end. - diff --git a/apps/emqx_auth_redis/src/emqx_auth_redis_app.erl b/apps/emqx_auth_redis/src/emqx_auth_redis_app.erl deleted file mode 100644 index 3f0b6ce26..000000000 --- a/apps/emqx_auth_redis/src/emqx_auth_redis_app.erl +++ /dev/null @@ -1,70 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_redis_app). - --behaviour(application). - --emqx_plugin(auth). - --include("emqx_auth_redis.hrl"). - --export([ start/2 - , stop/1 - ]). - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_auth_redis_sup:start_link(), - _ = if_cmd_enabled(auth_cmd, fun load_auth_hook/1), - _ = if_cmd_enabled(acl_cmd, fun load_acl_hook/1), - {ok, Sup}. - -stop(_State) -> - emqx:unhook('client.authenticate', {emqx_auth_redis, check}), - emqx:unhook('client.check_acl', {emqx_acl_redis, check_acl}), - %% Ensure stop cluster pool if the server type is cluster - eredis_cluster:stop_pool(?APP). - -load_auth_hook(AuthCmd) -> - SuperCmd = application:get_env(?APP, super_cmd, undefined), - {ok, HashType} = application:get_env(?APP, password_hash), - {ok, Timeout} = application:get_env(?APP, query_timeout), - Type = proplists:get_value(type, application:get_env(?APP, server, [])), - Config = #{auth_cmd => AuthCmd, - super_cmd => SuperCmd, - hash_type => HashType, - timeout => Timeout, - type => Type, - pool => ?APP}, - ok = emqx_auth_redis:register_metrics(), - emqx:hook('client.authenticate', {emqx_auth_redis, check, [Config]}). - -load_acl_hook(AclCmd) -> - {ok, Timeout} = application:get_env(?APP, query_timeout), - Type = proplists:get_value(type, application:get_env(?APP, server, [])), - Config = #{acl_cmd => AclCmd, - timeout => Timeout, - type => Type, - pool => ?APP}, - ok = emqx_acl_redis:register_metrics(), - emqx:hook('client.check_acl', {emqx_acl_redis, check_acl, [Config]}). - -if_cmd_enabled(Par, Fun) -> - case application:get_env(?APP, Par) of - {ok, Cmd} -> Fun(Cmd); - undefined -> ok - end. - diff --git a/apps/emqx_auth_redis/src/emqx_auth_redis_cli.erl b/apps/emqx_auth_redis/src/emqx_auth_redis_cli.erl deleted file mode 100644 index 52ac39a7b..000000000 --- a/apps/emqx_auth_redis/src/emqx_auth_redis_cli.erl +++ /dev/null @@ -1,89 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_redis_cli). - --behaviour(ecpool_worker). - --include("emqx_auth_redis.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --import(proplists, [get_value/2, get_value/3]). - --export([ connect/1 - , q/5 - ]). - -%%-------------------------------------------------------------------- -%% Redis Connect/Query -%%-------------------------------------------------------------------- - -connect(Opts) -> - Sentinel = get_value(sentinel, Opts), - Host = case Sentinel =:= "" of - true -> get_value(host, Opts); - false -> - _ = eredis_sentinel:start_link(get_value(servers, Opts), get_value(options, Opts, [])), - "sentinel:" ++ Sentinel - end, - case eredis:start_link(Host, - get_value(port, Opts, 6379), - get_value(database, Opts, 0), - get_value(password, Opts, ""), - 3000, - 5000, - get_value(options, Opts, [])) of - {ok, Pid} -> {ok, Pid}; - {error, Reason = {connection_error, _}} -> - ?LOG(error, "[Redis] Can't connect to Redis server: Connection refused."), - {error, Reason}; - {error, Reason = {authentication_error, _}} -> - ?LOG(error, "[Redis] Can't connect to Redis server: Authentication failed."), - {error, Reason}; - {error, Reason} -> - ?LOG(error, "[Redis] Can't connect to Redis server: ~p", [Reason]), - {error, Reason} - end. - -%% Redis Query. --spec(q(atom(), atom(), string(), emqx_types:credentials(), timeout()) - -> {ok, undefined | binary() | list()} | {error, atom() | binary()}). -q(Pool, Type, CmdStr, Credentials, Timeout) -> - Cmd = string:tokens(replvar(CmdStr, Credentials), " "), - case Type of - cluster -> eredis_cluster:q(Pool, Cmd); - _ -> ecpool:with_client(Pool, fun(C) -> eredis:q(C, Cmd, Timeout) end) - end. - -replvar(Cmd, Credentials = #{cn := CN}) -> - replvar(repl(Cmd, "%C", CN), maps:remove(cn, Credentials)); -replvar(Cmd, Credentials = #{dn := DN}) -> - replvar(repl(Cmd, "%d", DN), maps:remove(dn, Credentials)); -replvar(Cmd, Credentials = #{clientid := ClientId}) -> - replvar(repl(Cmd, "%c", ClientId), maps:remove(clientid, Credentials)); -replvar(Cmd, Credentials = #{username := Username}) -> - replvar(repl(Cmd, "%u", Username), maps:remove(username, Credentials)); -replvar(Cmd, _) -> - Cmd. - -repl(S, _Var, undefined) -> - S; -repl(S, Var, Val) -> - NVal = re:replace(Val, "&", "\\\\&", [global, {return, list}]), - re:replace(S, Var, NVal, [{return, list}]). - diff --git a/apps/emqx_auth_redis/src/emqx_auth_redis_sup.erl b/apps/emqx_auth_redis/src/emqx_auth_redis_sup.erl deleted file mode 100644 index ef81eef86..000000000 --- a/apps/emqx_auth_redis/src/emqx_auth_redis_sup.erl +++ /dev/null @@ -1,43 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_redis_sup). - --behaviour(supervisor). - --include("emqx_auth_redis.hrl"). - --export([start_link/0]). - --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - {ok, Server} = application:get_env(?APP, server), - {ok, {{one_for_one, 10, 100}, pool_spec(Server)}}. - -pool_spec(Server) -> - Options = application:get_env(?APP, options, []), - case proplists:get_value(type, Server) of - cluster -> - {ok, _} = eredis_cluster:start_pool(?APP, Server ++ Options), - []; - _ -> - [ecpool:pool_spec(?APP, ?APP, emqx_auth_redis_cli, Server ++ Options)] - end. - diff --git a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE.erl b/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE.erl deleted file mode 100644 index 24d3b20bd..000000000 --- a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE.erl +++ /dev/null @@ -1,186 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_auth_redis_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("emqx/include/emqx.hrl"). - --include_lib("common_test/include/ct.hrl"). - --include_lib("eunit/include/eunit.hrl"). - --define(APP, emqx_auth_redis). - --define(POOL(App), ecpool_worker:client(gproc_pool:pick_worker({ecpool, App}))). - --define(INIT_ACL, [{"mqtt_acl:test1", "topic1", "2"}, - {"mqtt_acl:test2", "topic2", "1"}, - {"mqtt_acl:test3", "topic3", "3"}]). - --define(INIT_AUTH, [{"mqtt_user:plain", ["password", "plain", "salt", "salt", "is_superuser", "1"]}, - {"mqtt_user:special&symbol", ["password", "plain", "salt", "salt", "is_superuser", "0"]}, - {"mqtt_user:md5", ["password", "1bc29b36f623ba82aaf6724fd3b16718", "salt", "salt", "is_superuser", "0"]}, - {"mqtt_user:sha", ["password", "d8f4590320e1343a915b6394170650a8f35d6926", "salt", "salt", "is_superuser", "0"]}, - {"mqtt_user:sha256", ["password", "5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e", "salt", "salt", "is_superuser", "0"]}, - {"mqtt_user:pbkdf2_password", ["password", "cdedb5281bb2f801565a1122b2563515", "salt", "ATHENA.MIT.EDUraeburn", "is_superuser", "0"]}, - {"mqtt_user:bcrypt_foo", ["password", "$2a$12$sSS8Eg.ovVzaHzi1nUHYK.HbUIOdlQI0iS22Q5rd5z.JVVYH6sfm6", "salt", "$2a$12$sSS8Eg.ovVzaHzi1nUHYK.", "is_superuser", "0"]}, - {"mqtt_user:bcrypt", ["password", "$2y$16$rEVsDarhgHYB0TGnDFJzyu5f.T.Ha9iXMTk9J36NCMWWM7O16qyaK", "salt", "salt", "is_superuser", "0"]}]). - -%%-------------------------------------------------------------------- -%% Setups -%%-------------------------------------------------------------------- - -all() -> - emqx_ct:all(?MODULE). - -init_per_suite(Cfg) -> - emqx_ct_helpers:start_apps([emqx_auth_redis], fun set_special_configs/1), - init_redis_rows(), - Cfg. - -end_per_suite(_Cfg) -> - deinit_redis_rows(), - emqx_ct_helpers:stop_apps([emqx_auth_redis]). - -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, acl_nomatch, deny), - application:set_env(emqx, enable_acl_cache, false); -set_special_configs(_App) -> - ok. - -init_redis_rows() -> - %% Users - [q(["HMSET", Key|FiledValue]) || {Key, FiledValue} <- ?INIT_AUTH], - %% ACLs - Result = [q(["HSET", Key, Filed, Value]) || {Key, Filed, Value} <- ?INIT_ACL], - ct:pal("redis init result: ~p~n", [Result]). - -deinit_redis_rows() -> - AuthKeys = [Key || {Key, _Filed, _Value} <- ?INIT_AUTH], - AclKeys = [Key || {Key, _Value} <- ?INIT_ACL], - q(["DEL" | AuthKeys]), - q(["DEL" | AclKeys]). - -%%-------------------------------------------------------------------- -%% Cases -%%-------------------------------------------------------------------- - -t_check_auth(_) -> - Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external}, - SpecialSymbol = #{clientid => <<"special_symbol">>, username => <<"special&symbol">>, zone => external}, - Md5 = #{clientid => <<"md5">>, username => <<"md5">>, zone => external}, - Sha = #{clientid => <<"sha">>, username => <<"sha">>, zone => external}, - Sha256 = #{clientid => <<"sha256">>, username => <<"sha256">>, zone => external}, - Pbkdf2 = #{clientid => <<"pbkdf2_password">>, username => <<"pbkdf2_password">>, zone => external}, - BcryptFoo = #{clientid => <<"bcrypt_foo">>, username => <<"bcrypt_foo">>, zone => external}, - User1 = #{clientid => <<"bcrypt_foo">>, username => <<"user">>, zone => external}, - User3 = #{clientid => <<"client3">>, zone => external}, - Bcrypt = #{clientid => <<"bcrypt">>, username => <<"bcrypt">>, zone => external}, - {error, _} = emqx_access_control:authenticate(User3#{password => <<>>}), - reload([{password_hash, plain}]), - {ok, #{is_superuser := true}} = emqx_access_control:authenticate(Plain#{password => <<"plain">>}), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(SpecialSymbol#{password => <<"plain">>}), - reload([{password_hash, md5}]), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Md5#{password => <<"md5">>}), - reload([{password_hash, sha}]), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Sha#{password => <<"sha">>}), - reload([{password_hash, sha256}]), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Sha256#{password => <<"sha256">>}), - reload([{password_hash, bcrypt}]), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Bcrypt#{password => <<"password">>}), - %%pbkdf2 sha - reload([{password_hash, {pbkdf2, sha, 1, 16}}, {auth_cmd, "HMGET mqtt_user:%u password salt"}]), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Pbkdf2#{password => <<"password">>}), - reload([{password_hash, {salt, bcrypt}}]), - {ok, #{is_superuser := false}} = emqx_access_control:authenticate(BcryptFoo#{password => <<"foo">>}), - {error,_} = emqx_access_control:authenticate(User1#{password => <<"foo">>}), - {error, _} = emqx_access_control:authenticate(Bcrypt#{password => <<"password">>}). - -t_check_auth_hget(_) -> - q(["HSET", "mqtt_user:hset", "password", "hset"]), - q(["HSET", "mqtt_user:hset", "is_superuser", "1"]), - reload([{password_hash, plain}, {auth_cmd, "HGET mqtt_user:%u password"}]), - Hset = #{clientid => <<"hset">>, username => <<"hset">>, zone => external}, - {ok, #{is_superuser := true}} = emqx_access_control:authenticate(Hset#{password => <<"hset">>}). - -t_check_acl(_) -> - User1 = #{zone => external, clientid => <<"client1">>, username => <<"test1">>}, - User2 = #{zone => external, clientid => <<"client2">>, username => <<"test2">>}, - User3 = #{zone => external, clientid => <<"client3">>, username => <<"test3">>}, - User4 = #{zone => external, clientid => <<"client4">>, username => <<"$$user4">>}, - deny = emqx_access_control:check_acl(User1, subscribe, <<"topic1">>), - allow = emqx_access_control:check_acl(User1, publish, <<"topic1">>), - - deny = emqx_access_control:check_acl(User2, publish, <<"topic2">>), - allow = emqx_access_control:check_acl(User2, subscribe, <<"topic2">>), - allow = emqx_access_control:check_acl(User3, publish, <<"topic3">>), - allow = emqx_access_control:check_acl(User3, subscribe, <<"topic3">>), - deny = emqx_access_control:check_acl(User4, publish, <<"a/b/c">>). - -t_acl_super(_) -> - reload([{password_hash, plain}]), - {ok, C} = emqtt:start_link([{host, "localhost"}, - {clientid, <<"simpleClient">>}, - {username, <<"plain">>}, - {password, <<"plain">>}]), - {ok, _} = emqtt:connect(C), - timer:sleep(10), - emqtt:subscribe(C, <<"TopicA">>, qos2), - timer:sleep(1000), - emqtt:publish(C, <<"TopicA">>, <<"Payload">>, qos2), - timer:sleep(1000), - receive - {publish, #{payload := Payload}} -> - ?assertEqual(<<"Payload">>, Payload) - after - 1000 -> - ct:fail({receive_timeout, <<"Payload">>}), - ok - end, - emqtt:disconnect(C). - -t_check_cluster_connection(_) -> - ?assertMatch({error, _Reason}, reload([{server, [{type,cluster}, - {pool_size,8}, - {auto_reconnect,1}, - {database,0}, - {password,[]}, - {sentinel,[]}, - {servers,[{"wrong",6379},{"wrong",6380},{"wrong",6381}]}]}])). - - -%%-------------------------------------------------------------------- -%% Internal funcs -%%-------------------------------------------------------------------- - -reload(Config) when is_list(Config) -> - application:stop(?APP), - [application:set_env(?APP, K, V) || {K, V} <- Config], - application:start(?APP). - -q(Cmd) -> - {ok, Server} = application:get_env(?APP, server), - case proplists:get_value(type, Server) of - cluster -> - eredis_cluster:q(emqx_auth_redis, Cmd); - _ -> - {ok, Connection} = ?POOL(?APP), - eredis:q(Connection, Cmd) - end. diff --git a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/ca.crt b/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/ca.crt deleted file mode 100644 index b46bef4e5..000000000 --- a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/ca.crt +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIE5jCCAs4CCQCc1DzEYETfKTANBgkqhkiG9w0BAQsFADA1MRMwEQYDVQQKDApS -ZWRpcyBUZXN0MR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjAx -MDI5MDEzNDE2WhcNMzAxMDI3MDEzNDE2WjA1MRMwEQYDVQQKDApSZWRpcyBUZXN0 -MR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQC/RxC/zQ6+ThI2l+LT5tpuvljE7CPca5erahTjv1Pq -mbmHYIVlige9jvZKR/AaaHuhNRT6C4PDpD98TgrhSLSgMMFImoFMSnmFEOVave3O -y1qV9vtoHLMB9hO+t7P98KRi1sCoMdPIE/o5uEGSd4YgWbk3NllAV6me108UniWU -yZMCSEKmV9OpfQ+YfHFolESV92ajdViDbtRBjfDNwD7qb8zgigxIJvBzEnWF4RZl -4+KIiyoJ55AQ3omdEi0QwiRRRONFtB6kRSqjGS8genGnycX1ZNPRB8JeG3ESuFj9 -1WQUD0EMBXFB5agHoZjvtFwxOkUkA4XbcnpKddHGKRt4BAbm+YcizJaT7mRytGWZ -UoTrDWz8/Cc0BlwAfPEk6ogU/sLSZpdxjxwprCNB89UOI+q7ng7CYiFnxY9HHZeg -GCJxYfvpKM/eOT9mSLUug8EGITd0j2cusflO4Q243clPyRbTSSr39Pcpy8rfKApF -HkUuGIpa/qgAbez+lPlIydzpbrTgrnHvL1P6fCYTnHkcgSn8glBIKv3vh4zQd6df -JvcLv3WEka9+lyoCvJ0QH+/ITqrToyWa8g9fR3ajTlyMANesKxQejo80zCwk/0ns -SFKRIJc6vfnUJ12Vdxpmm1LeoJZnCYODNUeeksL1ahHCBGq4M8UJ+ycUM6N4ndWE -6QIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQAg0BaIi7lzrNb7xC42c+GJVrBq8Qf8 -7CBzP8SXYGUavQIYNRrtH8UgTOwaju9vOn3zoY8L59N6e+Icyt+Oh1FENcQMCZ2l -SP79iaY9A/dRV56p6NqNd3VWH+EuRGbQVatLdhJf3l5+W1z3Dum1YXmIn26acawF -GZVqLalgvLqPzPHHWEqz9RnmcvTu3w9YVb4NgbmY4byCb6mB2avt0iWQrY/fZSMe -FvRXurr0jwyIXBncqnXu97sCeccNc+fo3qZC1xxH9iXOIzrRg0ud7VGMTKcNLTTc -GqnbjNT8BC96Qp2Bs8J+JGZa3mT/usKBq2TT/3q6oKevuc23u/a5s1rztnqZgIe5 -RzfevJ79xdva6DMSq/8Yyd3I8hrs3oZKJbAce6ux01RsrCcY2O7gi4dAMoEGumxW -CS9XLchNy7QxQ+J2AKBZXd6AZjvTvloDGz/yC5EbdK/MnLz8oApK5Z8U/huEilFa -AymVWQWpmlX2KxW0nkCperlb7lcbPS+ZuH0+Zd9HOvqr9cpYMrwpF54q4vnzUQkR -Hsxoapv/FBsVoxtcOqrcxwGpYWCsV0VBnv9+1fzzZ83aK7CHDIeGVuKPyjkhHzLy -v7Ljuqg400wH0WB9pyEdK+O3F+xO3zJgf4o0JptOKOFBVVSkZWTrqlDjjbcnXBmh -dwgj2xYeigqHJA== ------END CERTIFICATE----- diff --git a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/ca.key b/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/ca.key deleted file mode 100644 index b615a8c1e..000000000 --- a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/ca.key +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIJKAIBAAKCAgEAv0cQv80Ovk4SNpfi0+babr5YxOwj3GuXq2oU479T6pm5h2CF -ZYoHvY72SkfwGmh7oTUU+guDw6Q/fE4K4Ui0oDDBSJqBTEp5hRDlWr3tzstalfb7 -aByzAfYTvrez/fCkYtbAqDHTyBP6ObhBkneGIFm5NzZZQFepntdPFJ4llMmTAkhC -plfTqX0PmHxxaJRElfdmo3VYg27UQY3wzcA+6m/M4IoMSCbwcxJ1heEWZePiiIsq -CeeQEN6JnRItEMIkUUTjRbQepEUqoxkvIHpxp8nF9WTT0QfCXhtxErhY/dVkFA9B -DAVxQeWoB6GY77RcMTpFJAOF23J6SnXRxikbeAQG5vmHIsyWk+5kcrRlmVKE6w1s -/PwnNAZcAHzxJOqIFP7C0maXcY8cKawjQfPVDiPqu54OwmIhZ8WPRx2XoBgicWH7 -6SjP3jk/Zki1LoPBBiE3dI9nLrH5TuENuN3JT8kW00kq9/T3KcvK3ygKRR5FLhiK -Wv6oAG3s/pT5SMnc6W604K5x7y9T+nwmE5x5HIEp/IJQSCr974eM0HenXyb3C791 -hJGvfpcqArydEB/vyE6q06MlmvIPX0d2o05cjADXrCsUHo6PNMwsJP9J7EhSkSCX -Or351CddlXcaZptS3qCWZwmDgzVHnpLC9WoRwgRquDPFCfsnFDOjeJ3VhOkCAwEA -AQKCAgBF1jSPUtcnNGoB9MKki40FEgpnG7CcMcxWkYy++oQxC59phhwuTo807pWN -2WYYvj0lRrQ59ypMrBNh1zyxtFH+is6HK6I5sJddtiWHVAEXl7ejOWHhSVkyRh4/ -a+MTvGDIlZAR2N9yFZkuqc+HIoyeEyREvFsp2tfbXtFIvdUK1e4Oz0NGaJqnLzoa -epUNkdTYzFN1Ksr+ceCdbq2U8bQG9HrhIIYLcewol3zBPMVoviNfpy/aHenDvvyP -lKtPixKneXdhY7osT/SZSACk4w/MKydTyVRs5WBZ67sFErmrM9YuXMNrGDGZ1bfb -0Wx9WGSwtI258G9XCB0OQqYsq6WTMaEei+z8l0iarZi1l2bz2F89J+IBYM8RqSsa -E30F8AtEG32QJUfK3F6k6N4uLx6JZduJgLyzsSh6q51ghAJ8kD1vUkEeXffCzynp -hzwRHUw5O1jNLEBdKYHpSyszlFX6qbzR1YXypzZs/aehZi5d89eBKN8X/Fnbi9a2 -Q0MqpZ5J/1hH7zadJFibNyuOCP4CNO3Hm18PjyEFRrCMbSF293kY9GoiOlQiwNAT -MqrsyLYgHPCXKXpG/R60lyHEfWKO9sOjyh+mSbv3QfNZS32Fweuo0R/vGYmkmtGn -wpn2IeSmX8ychdQrSemJjwzjUl/EUN0lGRAlHEt4ZDf52vHZ4QKCAQEA9k7b2vpU -g3S3GRCMzhl8GKZloNSbnR/ZHE8b9PVNahp0bQcZj+1yimF35p27VZR662ZBoKLy -/MLyPT+ZyynykwypcTVA5U9CABpSlyZMeezLnFlBXeHHMeoXcBZfFqHeXSsNYbhW -OStf4BGwKf7m/V0P/QL9mNsA/iq2uugC1gHoyp422YUIQQvKkBiFyMl34Zp2URsX -yIwb9aVyg2GogKDtbDPIwW89l3BCBiwalvjR6UotbXh3PQgYbsv62rTJ8AN+E+XH -eSQPnmPR6EXtX2nDuov996qlbja+JQE3SAls4EXLbrLyjSlObjcvkA59r+kZgNIY -g88hv7e9ublfqwKCAQEAxs3d9Zmjh9ByCT6jOUVnRMqw0+lVyVOs0kp7nOCb4HKM -CnupZuJQHQVQt7VhgD7FrALmYwkpt2e/WllN9bPFHRJcsw+SylOqyPil9G4DO5XZ -YPvk6PeQ/c0cbREKhsYNXqj5fWdq5pRd8rE72rK82mhdGQtAAN7NOEW5fo5tqHDK -D079SZmpcgd8Wz9luNpnZRpNhO3ccKV5yf0S1LZOZBbG9t875OVNhxlQY5wwIBXv -8ab13zcFKG21tWvLzz80vgkMIp0A9xh0XznIRnH3NnBZB80Yubg4sIaWvX7bqZ4X -EE9HGeiamw6c6Sm/Lvh/H659ri95l7C9TgAfAA7puwKCAQEAgJ/N0BzJ5ZwdwckS -vs4wL+81QzfDy9nF1zK4tsMjGjWWdxkuECs/lWQw6Q2VtqtDRYqw2uI9YiGrvrBn -7+CH/KKwGZ5ltVoebU9Rsf0eEs3FxnAV4qD1FOvaMX59SaReKulAo7dPz6sG9kxG -YqfqmITwxH+7TwePDSvhINnoITn+B1F38z+1f8JYlcc4lhIfuIChKNmtId2I/E7Z -7iIhjIp9cfPY8qrUzzCgSfjeKdjmRZ2m+3PdUNHZcIK1DWE700r/nARylqBuR5h5 -FYLu4tSokdJpXdyPZ27O/SQValkBslzAT57Da1QW0RegjuoCWMqxtsQAaVTRmvyo -50QW4QKCAQBLJFbn1MmFtRjVO7KwG/Z7fu01O7WsIg9pcLOmSRNB06nw8GrIM3Q6 -c97dgRY4RgGrEXGJL1ZwNyuRd73Kx8cSRPV6zMEb7mHYEnuPluFr7Si7ypnsIF7S -P2umIdHLvSIijFW4u5UhUCTubWUFNZfCKb4+kA0CBzSkN150Yls6Vl9ZR+7emdD9 -A61SQ/Ur2IlKIpX4T3uJrFILMbejZMDefel4OEpIKw+Rp9TFwaxDBGer/AJk+0Pc -0xLiXrsrO2WxCnRmxNcvjjO2Jn33em4JSo+sLi5RTDtJJaXmPAPE6bcn9/8U4OFH -CE/wpVHY7B4ImIhyhQk9d5Ul3U/aUsivAoIBAG8zk3CnFAnii1ENxhPtE8GCinvs -NdsluVtvUgMcA8gNzvqLHLQCoIy/b1wkqxPVsdTq1gZ7+FX9D9LzW9JxrWeEZqVV -jrUQIbls6HZei7i5x0tPwh1shOZiijgY24I6HDX9QRKZ7H7lLw0HfJI9YBI1Hl8E -naOtCuzFiaYEPfbGQACL8/UuoOZaD31JQda2EGYysGRxxJ2ZNPJIphCrwRb/nQBG -7WwCSCzu0peFNhZPVvkWHaFN73Uv/MmgFkp8RZzw9TEENB05wluCZB1TYJAOe65n -HnRWSDvWYR4lzMtq5WASFLC0WrFTiJKRCuKPljjoTptbXsJKyskW/t/+XPM= ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/ca.txt b/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/ca.txt deleted file mode 100644 index cf4e2aba5..000000000 --- a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/ca.txt +++ /dev/null @@ -1 +0,0 @@ -BFFAA2A065DFA6FC diff --git a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.crt b/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.crt deleted file mode 100644 index 5eefadf62..000000000 --- a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.crt +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID1zCCAb8CCQC/+qKgZd+m/DANBgkqhkiG9w0BAQsFADA1MRMwEQYDVQQKDApS -ZWRpcyBUZXN0MR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjAx -MDI5MDEzNDE2WhcNMjExMDI5MDEzNDE2WjAmMRMwEQYDVQQKDApSZWRpcyBUZXN0 -MQ8wDQYDVQQDDAZTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQDSs3bQ9sYi2AhFuHU75Ryk1HHSgfzA6pQAJilmJdTy0s5vyiWe1HQJaWkMcS5V -GVzGMK+c+OBqtXtDDninL3betg1YPMjSCOjPMOTC1H9K7+effwf7Iwpnw9Zro8mb -TEmMslIYhhcDedzT9Owli4QAgbgTn4l1BYuKX9CLrrKFtnr21miKu3ydViy9q7T1 -pib3eigvAyk7X2fadHFArGEttsXrD6cetPPkSF/1OLWNlqzUKXzhSyrBXzO44Kks -fwR/EpTiES9g4dNOL2wvKS/YE1fNKhiCENrNxTXQo1l0yOdm2+MeyOeHFzRuS0b/ -+uGDFOPPi04KXeO6dQ5olBCPAgMBAAEwDQYJKoZIhvcNAQELBQADggIBADn0E2vG -iQWe8/I7VbBdPhPNupVNcLvew10eIHxY2g5vSruCSVRQTgk8itVMRmDQxbb7gdDW -jnCRbxykxbLjM9iCRljnOCsIcTi7qO7JRl8niV8dtEpPOs9lZxEdNXjIV1iZoWf3 -arBbPQSyQZvTQHG6qbFnyCdMMyyXGGvEPGQDaBiKH+Ko1qeAbCi0zupChYvxmtZ8 -hSTPlMFezDT9bKoNY0pkJSELfokEPU/Pn6Lz/NVbdzmCMjVa/xmF3s31g+DGhz95 -4AyOnCr6o0aydPVVV3pB/BCezNXPUxpp53BG0w/K2f2DnKYCvGvJbqDAaJ8bG/J1 -EFSOmwobdwVxJz3KNubmo1qJ6xOl/YT7yyqPRQRM1SY8nZW+YcoJSZjOe8wJVlob -d0bOwN1C3HQwomyMWes187bEQP6Y36HuEbR1fK8yIOzGsGDKRFAFwQwMgw2M91lr -EJIP5NRD3OZRuiYDiVfVhDZDaNahrAMZUcPCgeCAwc4YG6Gp2sDtdorOl4kIJYWE -BbBZ0Jplq9+g6ciu5ChjAW8iFl0Ae5U24MxPGXnrxiRF4WWxLeZMVLXLDvlPqReD -CHII5ifyvGEt5+RhqtZC/L+HimL+5wQgOlntqhUdLb6yWRz7YW37PFMnUXU3MXe9 -uY7m73ZLluXiLojcZxU2+cx89u5FOJxrYtrj ------END CERTIFICATE----- diff --git a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.dh b/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.dh deleted file mode 100644 index f7dd0569d..000000000 --- a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.dh +++ /dev/null @@ -1,8 +0,0 @@ ------BEGIN DH PARAMETERS----- -MIIBCAKCAQEAo2dgOzTnLK7c8AjkiTXxdmo2MJsyzTlNXUDxLfl2hgwic6benyQ3 -9iL95wKjYg2YpMhzbwux50D+9XeVkRatf1pRi/N9H911f90MO6penzUx/dxfOepN -qoGK/T9xO8e6aFCYOoQjJaZzQYC0HixJVadZd7wRlHkZ3siNKUU5QK68KaN3JE3J -R3yZ9A7MU/TVdwZyVIyoWF2+WJMQW+qaezoqiuVKZXXzzoqbj14ZrtPRmO26vMV/ -bmMuHwPsk9dL7tKnTWEOrs6NVHIQW+RxJuRE9wGa0qqzHAzysEQ8q9QYPRvGo5y+ -XRWosl1bHG4+EmvXsCCs35bcbKToi3NFWwIBAg== ------END DH PARAMETERS----- diff --git a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.key b/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.key deleted file mode 100644 index b76303f14..000000000 --- a/apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs/redis.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpgIBAAKCAQEA0rN20PbGItgIRbh1O+UcpNRx0oH8wOqUACYpZiXU8tLOb8ol -ntR0CWlpDHEuVRlcxjCvnPjgarV7Qw54py923rYNWDzI0gjozzDkwtR/Su/nn38H -+yMKZ8PWa6PJm0xJjLJSGIYXA3nc0/TsJYuEAIG4E5+JdQWLil/Qi66yhbZ69tZo -irt8nVYsvau09aYm93ooLwMpO19n2nRxQKxhLbbF6w+nHrTz5Ehf9Ti1jZas1Cl8 -4UsqwV8zuOCpLH8EfxKU4hEvYOHTTi9sLykv2BNXzSoYghDazcU10KNZdMjnZtvj -Hsjnhxc0bktG//rhgxTjz4tOCl3junUOaJQQjwIDAQABAoIBAQCP7CJ27nm9B0/v -P+ZkeUWtmaf+IOhjZlieGXMh4SmqjDCSz8QO0BRK8YPeCdmaK27huhPa521ztm9y -CIqFuLg7vKM06KBMR+Wu0TkRlFE3ANR4cC8lbnQHGRB4CjMGL3/16UCGm+FQcIdV -CPHdW4VZS0JPtSQRmS4N4RD0uOocxqGcVbCRqnJoNp1zyXhookgHfZsC3b3cgzC7 -qvI9F1oY4Yg4b9Lw5sNi3JXWtFth8JFOPyImRcE0ngcGZK4iWjiufNKWVeTmSmVy -njMZfj8xKSpfqO3sOTbJMdrH1v5pMrAR/Ed748HheXuL15Ur9n88683hMMATZInn -YzIqNSrBAoGBAO94YBB1hN+jSKw+2FbAhuuM0gWHREmLQuaF2vjeVXL3r6YofFmf -+oJNgoOWXsv4KO2MgKDv4qrz7RohhhQpOFm5PpapSH/di7u6KsbJLYSxv/TEqQFE -NPyGywwNDIkn1wPlnX3LXp26puj2Gtn21Z0trUrpgsDM99BaTBbqTR2xAoGBAOE+ -tw0GHD/6CRPfoBIgVilS/sUJ5VJYTTKo/y6ozovCAq4bt5LkYmAOy6q8paHb58Oc -J890+LEPhelM/ZJDDz9oQFfq5LvuzgNfzDRyIhgDSpghtFrdDxQZP1X1lSdh+MFW -gx0k9h8VuIPksBsIgcmUtyCYitxLFep/0tAA/GI/AoGBAMxexEVntjWSScROgh1P -hBXlAZycO4g0ZK0OEboRLYXHos1AghePM6Ee+0LIAzE6IdvR7DjtYVoagQCrGZ19 -LE1Ojf7QjEIr1kQpdrZeHQ3BERyY9c9R4ZKeiw1G2ar4KEV4Ifeop6AfGrF4z6Oz -R80znVBwhxl6FAhp98QaxCORAoGBAInkc/nEKN09u/rvpzYRl83aol+MDFjZ+ACw -lvBApZnHnw5pp3uE13jI9gRDUv8A+iS1X2XQzULQJwHJgV7eMOJ3dxSbl4Y5zuMf -7YqZ6KdctHjoAVqzBD0gq7Z7DuG6R6hMxx27d/VVvcz43preHV6D7YxF9pSgXv1d -XXi7ccbPAoGBAIeLzCYd+JGufHwbq7oNvSyXJjGMjsAQuErUQ0xXwo7VAyOere2P -Dwk67wq6vsmn38EAs7IkXDgIoTD9z69DNtcjr/3fARYfmDSWyHscRwyUaJ15WQcZ -TCXAPf70Vf0KGBpRkgD+Qnq+lMZ3dr1uINGdalI4AWsXje0dPKpd+W8U ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_authentication/etc/emqx_authentication.conf b/apps/emqx_authentication/etc/emqx_authentication.conf deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/emqx_authentication/include/emqx_authentication.hrl b/apps/emqx_authentication/include/emqx_authentication.hrl deleted file mode 100644 index 09d3c5fc4..000000000 --- a/apps/emqx_authentication/include/emqx_authentication.hrl +++ /dev/null @@ -1,41 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --define(APP, emqx_authentication). - --type(service_type_name() :: atom()). --type(service_name() :: binary()). --type(chain_id() :: binary()). - --record(service_type, - { name :: service_type_name() - , provider :: module() - , params_spec :: #{atom() => term()} - }). - --record(service, - { name :: service_name() - , type :: service_type_name() - , provider :: module() - , params :: map() - , state :: map() - }). - --record(chain, - { id :: chain_id() - , services :: [{service_name(), #service{}}] - , created_at :: integer() - }). diff --git a/apps/emqx_authentication/priv/emqx_authentication.schema b/apps/emqx_authentication/priv/emqx_authentication.schema deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl deleted file mode 100644 index ab7b8537c..000000000 --- a/apps/emqx_authentication/src/emqx_authentication.erl +++ /dev/null @@ -1,519 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authentication). - --include("emqx_authentication.hrl"). - --export([ enable/0 - , disable/0 - ]). - --export([authenticate/1]). - --export([register_service_types/0]). - --export([ create_chain/1 - , delete_chain/1 - , lookup_chain/1 - , list_chains/0 - , add_services/2 - , delete_services/2 - , update_service/3 - , lookup_service/2 - , list_services/1 - , move_service_to_the_front/2 - , move_service_to_the_end/2 - , move_service_to_the_nth/3 - ]). - --export([ import_users/3 - , add_user/3 - , delete_user/3 - , update_user/4 - , lookup_user/3 - , list_users/2 - ]). - --export([mnesia/1]). - --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). - --define(CHAIN_TAB, emqx_authentication_chain). --define(SERVICE_TYPE_TAB, emqx_authentication_service_type). - -%%------------------------------------------------------------------------------ -%% Mnesia bootstrap -%%------------------------------------------------------------------------------ - -%% @doc Create or replicate tables. --spec(mnesia(boot | copy) -> ok). -mnesia(boot) -> - %% Optimize storage - StoreProps = [{ets, [{read_concurrency, true}]}], - %% Chain table - ok = ekka_mnesia:create_table(?CHAIN_TAB, [ - {disc_copies, [node()]}, - {record_name, chain}, - {attributes, record_info(fields, chain)}, - {storage_properties, StoreProps}]), - %% Service type table - ok = ekka_mnesia:create_table(?SERVICE_TYPE_TAB, [ - {ram_copies, [node()]}, - {record_name, service_type}, - {attributes, record_info(fields, service_type)}, - {storage_properties, StoreProps}]); - -mnesia(copy) -> - %% Copy chain table - ok = ekka_mnesia:copy_table(?CHAIN_TAB, disc_copies), - %% Copy service type table - ok = ekka_mnesia:copy_table(?SERVICE_TYPE_TAB, ram_copies). - -enable() -> - case emqx:hook('client.authenticate', {emqx_authentication, authenticate, []}) of - ok -> ok; - {error, already_exists} -> ok - end. - -disable() -> - emqx:unhook('client.authenticate', {emqx_authentication, authenticate}), - ok. - -authenticate(#{chain_id := ChainID} = ClientInfo) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [#chain{services = []}] -> - {error, no_services}; - [#chain{services = Services}] -> - do_authenticate(Services, ClientInfo); - [] -> - {error, todo} - end. - -do_authenticate([], _) -> - {error, user_not_found}; -do_authenticate([{_, #service{provider = Provider, state = State}} | More], ClientInfo) -> - case Provider:authenticate(ClientInfo, State) of - ignore -> do_authenticate(More, ClientInfo); - ok -> ok; - {ok, NewClientInfo} -> {ok, NewClientInfo}; - {stop, Reason} -> {error, Reason} - end. - -register_service_types() -> - Attrs = find_attrs(?APP, service_type), - register_service_types(Attrs). - -register_service_types(Attrs) -> - register_service_types(Attrs, []). - -register_service_types([], Acc) -> - do_register_service_types(Acc); -register_service_types([{_App, Mod, #{name := Name, - params_spec := ParamsSpec}} | Types], Acc) -> - %% TODO: Temporary realization - ok = emqx_rule_validator:validate_spec(ParamsSpec), - ServiceType = #service_type{name = Name, - provider = Mod, - params_spec = ParamsSpec}, - register_service_types(Types, [ServiceType | Acc]). - -create_chain(#{id := ID}) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ID, write) of - [] -> - Chain = #chain{id = ID, - services = [], - created_at = erlang:system_time(millisecond)}, - mnesia:write(?CHAIN_TAB, Chain, write), - {ok, serialize_chain(Chain)}; - [_ | _] -> - {error, {already_exists, {chain, ID}}} - end - end). - -delete_chain(ID) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ID, write) of - [] -> - {error, {not_found, {chain, ID}}}; - [#chain{services = Services}] -> - ok = delete_services_(Services), - mnesia:delete(?CHAIN_TAB, ID, write) - end - end). - -lookup_chain(ID) -> - case mnesia:dirty_read(?CHAIN_TAB, ID) of - [] -> - {error, {not_found, {chain, ID}}}; - [Chain] -> - {ok, serialize_chain(Chain)} - end. - -list_chains() -> - Chains = ets:tab2list(?CHAIN_TAB), - {ok, [serialize_chain(Chain) || Chain <- Chains]}. - -add_services(ChainID, ServiceParams) -> - case validate_service_params(ServiceParams) of - {ok, NServiceParams} -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - Names = [Name || {Name, _} <- Services] ++ [Name || #{name := Name} <- NServiceParams], - case no_duplicate_names(Names) of - ok -> - case create_services(ChainID, NServiceParams) of - {ok, NServices} -> - NChain = Chain#chain{services = Services ++ NServices}, - ok = mnesia:write(?CHAIN_TAB, NChain, write), - {ok, serialize_services(NServices)}; - {error, Reason} -> - {error, Reason} - end; - {error, {duplicate, Name}} -> - {error, {already_exists, {service, Name}}} - end - end, - update_chain(ChainID, UpdateFun); - {error, Reason} -> - {error, Reason} - end. - -delete_services(ChainID, ServiceNames) -> - case no_duplicate_names(ServiceNames) of - ok -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case extract_services(ServiceNames, Services) of - {ok, Extracted, Rest} -> - ok = delete_services_(Extracted), - NChain = Chain#chain{services = Rest}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun); - {error, Reason} -> - {error, Reason} - end. - -update_service(ChainID, ServiceName, NewParams) -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case proplists:get_value(ServiceName, Services, undefined) of - undefined -> - {error, {not_found, {service, ServiceName}}}; - #service{type = Type, - provider = Provider, - params = OriginalParams, - state = State} = Service -> - Params = maps:merge(OriginalParams, NewParams), - {ok, #service_type{params_spec = ParamsSpec}} = find_service_type(Type), - NParams = emqx_rule_validator:validate_params(Params, ParamsSpec), - case Provider:update(ChainID, ServiceName, NParams, State) of - {ok, NState} -> - NService = Service#service{params = Params, - state = NState}, - NServices = lists:keyreplace(ServiceName, 1, Services, {ServiceName, NService}), - ok = mnesia:write(?CHAIN_TAB, Chain#chain{services = NServices}, write), - {ok, serialize_service({ServiceName, NService})}; - {error, Reason} -> - {error, Reason} - end - end - end, - update_chain(ChainID, UpdateFun). - -lookup_service(ChainID, ServiceName) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{services = Services}] -> - case lists:keytake(ServiceName, 1, Services) of - {value, Service, _} -> - {ok, serialize_service(Service)}; - false -> - {error, {not_found, {service, ServiceName}}} - end - end. - -list_services(ChainID) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{services = Services}] -> - {ok, serialize_services(Services)} - end. - -move_service_to_the_front(ChainID, ServiceName) -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case move_service_to_the_front_(ServiceName, Services) of - {ok, NServices} -> - NChain = Chain#chain{services = NServices}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun). - -move_service_to_the_end(ChainID, ServiceName) -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case move_service_to_the_end_(ServiceName, Services) of - {ok, NServices} -> - NChain = Chain#chain{services = NServices}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun). - -move_service_to_the_nth(ChainID, ServiceName, N) -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case move_service_to_the_nth_(ServiceName, Services, N) of - {ok, NServices} -> - NChain = Chain#chain{services = NServices}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun). - -import_users(ChainID, ServiceName, Filename) -> - call_service(ChainID, ServiceName, import_users, [Filename]). - -add_user(ChainID, ServiceName, UserInfo) -> - call_service(ChainID, ServiceName, add_user, [UserInfo]). - -delete_user(ChainID, ServiceName, UserID) -> - call_service(ChainID, ServiceName, delete_user, [UserID]). - -update_user(ChainID, ServiceName, UserID, NewUserInfo) -> - call_service(ChainID, ServiceName, update_user, [UserID, NewUserInfo]). - -lookup_user(ChainID, ServiceName, UserID) -> - call_service(ChainID, ServiceName, lookup_user, [UserID]). - -list_users(ChainID, ServiceName) -> - call_service(ChainID, ServiceName, list_users, []). - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -find_attrs(App, AttrName) -> - [{App, Mod, Attr} || {ok, Modules} <- [application:get_key(App, modules)], - Mod <- Modules, - {Name, Attrs} <- module_attributes(Mod), Name =:= AttrName, - Attr <- Attrs]. - -module_attributes(Module) -> - try Module:module_info(attributes) - catch - error:undef -> [] - end. - -do_register_service_types(ServiceTypes) -> - trans(fun lists:foreach/2, [fun insert_service_type/1, ServiceTypes]). - -insert_service_type(ServiceType) -> - mnesia:write(?SERVICE_TYPE_TAB, ServiceType, write). - -find_service_type(Name) -> - case mnesia:dirty_read(?SERVICE_TYPE_TAB, Name) of - [ServiceType] -> {ok, ServiceType}; - [] -> {error, not_found} - end. - -validate_service_params(ServiceParams) -> - case validate_service_names(ServiceParams) of - ok -> - validate_other_service_params(ServiceParams); - {error, Reason} -> - {error, Reason} - end. - -validate_service_names(ServiceParams) -> - Names = [Name || #{name := Name} <- ServiceParams], - no_duplicate_names(Names). - -validate_other_service_params(ServiceParams) -> - validate_other_service_params(ServiceParams, []). - -validate_other_service_params([], Acc) -> - {ok, lists:reverse(Acc)}; -validate_other_service_params([#{type := Type, params := Params} = ServiceParams | More], Acc) -> - case find_service_type(Type) of - {ok, #service_type{provider = Provider, params_spec = ParamsSpec}} -> - NParams = emqx_rule_validator:validate_params(Params, ParamsSpec), - validate_other_service_params(More, - [ServiceParams#{params => NParams, - original_params => Params, - provider => Provider} | Acc]); - {error, not_found} -> - {error, {not_found, {service_type, Type}}} - end. - -no_duplicate_names(Names) -> - no_duplicate_names(Names, #{}). - -no_duplicate_names([], _) -> - ok; -no_duplicate_names([Name | More], Acc) -> - case maps:is_key(Name, Acc) of - false -> no_duplicate_names(More, Acc#{Name => true}); - true -> {error, {duplicate, Name}} - end. - -create_services(ChainID, ServiceParams) -> - create_services(ChainID, ServiceParams, []). - -create_services(_ChainID, [], Acc) -> - {ok, lists:reverse(Acc)}; -create_services(ChainID, [#{name := Name, - type := Type, - provider := Provider, - params := Params, - original_params := OriginalParams} | More], Acc) -> - case Provider:create(ChainID, Name, Params) of - {ok, State} -> - Service = #service{name = Name, - type = Type, - provider = Provider, - params = OriginalParams, - state = State}, - create_services(ChainID, More, [{Name, Service} | Acc]); - {error, Reason} -> - delete_services_(Acc), - {error, Reason} - end. - -delete_services_([]) -> - ok; -delete_services_([{_, #service{provider = Provider, state = State}} | More]) -> - Provider:destroy(State), - delete_services_(More). - -extract_services(ServiceNames, Services) -> - extract_services(ServiceNames, Services, []). - -extract_services([], Rest, Extracted) -> - {ok, lists:reverse(Extracted), Rest}; -extract_services([ServiceName | More], Services, Acc) -> - case lists:keytake(ServiceName, 1, Services) of - {value, Extracted, Rest} -> - extract_services(More, Rest, [Extracted | Acc]); - false -> - {error, {not_found, {service, ServiceName}}} - end. - -move_service_to_the_front_(ServiceName, Services) -> - move_service_to_the_front_(ServiceName, Services, []). - -move_service_to_the_front_(ServiceName, [], _) -> - {error, {not_found, {service, ServiceName}}}; -move_service_to_the_front_(ServiceName, [{ServiceName, _} = Service | More], Passed) -> - {ok, [Service | (lists:reverse(Passed) ++ More)]}; -move_service_to_the_front_(ServiceName, [Service | More], Passed) -> - move_service_to_the_front_(ServiceName, More, [Service | Passed]). - -move_service_to_the_end_(ServiceName, Services) -> - move_service_to_the_end_(ServiceName, Services, []). - -move_service_to_the_end_(ServiceName, [], _) -> - {error, {not_found, {service, ServiceName}}}; -move_service_to_the_end_(ServiceName, [{ServiceName, _} = Service | More], Passed) -> - {ok, lists:reverse(Passed) ++ More ++ [Service]}; -move_service_to_the_end_(ServiceName, [Service | More], Passed) -> - move_service_to_the_end_(ServiceName, More, [Service | Passed]). - -move_service_to_the_nth_(ServiceName, Services, N) - when N =< length(Services) andalso N > 0 -> - move_service_to_the_nth_(ServiceName, Services, N, []); -move_service_to_the_nth_(_, _, _) -> - {error, out_of_range}. - -move_service_to_the_nth_(ServiceName, [], _, _) -> - {error, {not_found, {service, ServiceName}}}; -move_service_to_the_nth_(ServiceName, [{ServiceName, _} = Service | More], N, Passed) - when N =< length(Passed) -> - {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), - {ok, L1 ++ [Service] ++ L2 ++ More}; -move_service_to_the_nth_(ServiceName, [{ServiceName, _} = Service | More], N, Passed) -> - {L1, L2} = lists:split(N - length(Passed) - 1, More), - {ok, lists:reverse(Passed) ++ L1 ++ [Service] ++ L2}; -move_service_to_the_nth_(ServiceName, [Service | More], N, Passed) -> - move_service_to_the_nth_(ServiceName, More, N, [Service | Passed]). - -update_chain(ChainID, UpdateFun) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ChainID, write) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [Chain] -> - UpdateFun(Chain) - end - end). - -call_service(ChainID, ServiceName, Func, Args) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{services = Services}] -> - case proplists:get_value(ServiceName, Services, undefined) of - undefined -> - {error, {not_found, {service, ServiceName}}}; - #service{provider = Provider, - state = State} -> - case erlang:function_exported(Provider, Func, length(Args) + 1) of - true -> - erlang:apply(Provider, Func, Args ++ [State]); - false -> - {error, unsupported_feature} - end - end - end. - -serialize_chain(#chain{id = ID, - services = Services, - created_at = CreatedAt}) -> - #{id => ID, - services => serialize_services(Services), - created_at => CreatedAt}. - -serialize_services(Services) -> - [serialize_service(Service) || Service <- Services]. - -serialize_service({_, #service{name = Name, - type = Type, - params = Params}}) -> - #{name => Name, - type => Type, - params => Params}. - -trans(Fun) -> - trans(Fun, []). - -trans(Fun, Args) -> - case mnesia:transaction(Fun, Args) of - {atomic, Res} -> Res; - {aborted, Reason} -> {error, Reason} - end. diff --git a/apps/emqx_authentication/src/emqx_authentication_api.erl b/apps/emqx_authentication/src/emqx_authentication_api.erl deleted file mode 100644 index 74887a0b2..000000000 --- a/apps/emqx_authentication/src/emqx_authentication_api.erl +++ /dev/null @@ -1,407 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authentication_api). - --export([ create_chain/2 - , delete_chain/2 - , lookup_chain/2 - , list_chains/2 - , add_service/2 - , delete_service/2 - , update_service/2 - , lookup_service/2 - , list_services/2 - , move_service/2 - , import_users/2 - , add_user/2 - , delete_user/2 - , update_user/2 - , lookup_user/2 - , list_users/2 - ]). - --import(minirest, [return/1]). - --rest_api(#{name => create_chain, - method => 'POST', - path => "/authentication/chains", - func => create_chain, - descr => "Create a chain" - }). - --rest_api(#{name => delete_chain, - method => 'DELETE', - path => "/authentication/chains/:bin:id", - func => delete_chain, - descr => "Delete chain" - }). - --rest_api(#{name => lookup_chain, - method => 'GET', - path => "/authentication/chains/:bin:id", - func => lookup_chain, - descr => "Lookup chain" - }). - --rest_api(#{name => list_chains, - method => 'GET', - path => "/authentication/chains", - func => list_chains, - descr => "List all chains" - }). - --rest_api(#{name => add_service, - method => 'POST', - path => "/authentication/chains/:bin:id/services", - func => add_service, - descr => "Add service to chain" - }). - --rest_api(#{name => delete_service, - method => 'DELETE', - path => "/authentication/chains/:bin:id/services/:bin:service_name", - func => delete_service, - descr => "Delete service from chain" - }). - --rest_api(#{name => update_service, - method => 'PUT', - path => "/authentication/chains/:bin:id/services/:bin:service_name", - func => update_service, - descr => "Update service in chain" - }). - --rest_api(#{name => lookup_service, - method => 'GET', - path => "/authentication/chains/:bin:id/services/:bin:service_name", - func => lookup_service, - descr => "Lookup service in chain" - }). - --rest_api(#{name => list_services, - method => 'GET', - path => "/authentication/chains/:bin:id/services", - func => list_services, - descr => "List services in chain" - }). - --rest_api(#{name => move_service, - method => 'POST', - path => "/authentication/chains/:bin:id/services/:bin:service_name/position", - func => move_service, - descr => "Change the order of services" - }). - --rest_api(#{name => import_users, - method => 'POST', - path => "/authentication/chains/:bin:id/services/:bin:service_name/import-users", - func => import_users, - descr => "Import users" - }). - --rest_api(#{name => add_user, - method => 'POST', - path => "/authentication/chains/:bin:id/services/:bin:service_name/users", - func => add_user, - descr => "Add user" - }). - --rest_api(#{name => delete_user, - method => 'DELETE', - path => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id", - func => delete_user, - descr => "Delete user" - }). - --rest_api(#{name => update_user, - method => 'PUT', - path => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id", - func => update_user, - descr => "Update user" - }). - --rest_api(#{name => lookup_user, - method => 'GET', - path => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id", - func => lookup_user, - descr => "Lookup user" - }). - -%% TODO: Support pagination --rest_api(#{name => list_users, - method => 'GET', - path => "/authentication/chains/:bin:id/services/:bin:service_name/users", - func => list_users, - descr => "List all users" - }). - -create_chain(Binding, Params) -> - do_create_chain(uri_decode(Binding), maps:from_list(Params)). - -do_create_chain(_Binding, #{<<"id">> := ChainID}) -> - case emqx_authentication:create_chain(#{id => ChainID}) of - {ok, Chain} -> - return({ok, Chain}); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_create_chain(_Binding, _Params) -> - return(serialize_error({missing_parameter, id})). - -delete_chain(Binding, Params) -> - do_delete_chain(uri_decode(Binding), maps:from_list(Params)). - -do_delete_chain(#{id := ChainID}, _Params) -> - case emqx_authentication:delete_chain(ChainID) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -lookup_chain(Binding, Params) -> - do_lookup_chain(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_chain(#{id := ChainID}, _Params) -> - case emqx_authentication:lookup_chain(ChainID) of - {ok, Chain} -> - return({ok, Chain}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -list_chains(Binding, Params) -> - do_list_chains(uri_decode(Binding), maps:from_list(Params)). - -do_list_chains(_Binding, _Params) -> - {ok, Chains} = emqx_authentication:list_chains(), - return({ok, Chains}). - -add_service(Binding, Params) -> - do_add_service(uri_decode(Binding), maps:from_list(Params)). - -do_add_service(#{id := ChainID}, #{<<"name">> := Name, - <<"type">> := Type, - <<"params">> := Params}) -> - case emqx_authentication:add_services(ChainID, [#{name => Name, - type => binary_to_existing_atom(Type, utf8), - params => maps:from_list(Params)}]) of - {ok, Services} -> - return({ok, Services}); - {error, Reason} -> - return(serialize_error(Reason)) - end; -%% TODO: Check missed field in params -do_add_service(_Binding, Params) -> - Missed = get_missed_params(Params, [<<"name">>, <<"type">>, <<"params">>]), - return(serialize_error({missing_parameter, Missed})). - -delete_service(Binding, Params) -> - do_delete_service(uri_decode(Binding), maps:from_list(Params)). - -do_delete_service(#{id := ChainID, - service_name := ServiceName}, _Params) -> - case emqx_authentication:delete_services(ChainID, [ServiceName]) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -update_service(Binding, Params) -> - do_update_service(uri_decode(Binding), maps:from_list(Params)). - -%% TOOD: PUT method supports creation and update -do_update_service(#{id := ChainID, - service_name := ServiceName}, Params) -> - case emqx_authentication:update_service(ChainID, ServiceName, Params) of - {ok, Service} -> - return({ok, Service}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -lookup_service(Binding, Params) -> - do_lookup_service(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_service(#{id := ChainID, - service_name := ServiceName}, _Params) -> - case emqx_authentication:lookup_service(ChainID, ServiceName) of - {ok, Service} -> - return({ok, Service}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -list_services(Binding, Params) -> - do_list_services(uri_decode(Binding), maps:from_list(Params)). - -do_list_services(#{id := ChainID}, _Params) -> - case emqx_authentication:list_services(ChainID) of - {ok, Services} -> - return({ok, Services}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -move_service(Binding, Params) -> - do_move_service(uri_decode(Binding), maps:from_list(Params)). - -do_move_service(#{id := ChainID, - service_name := ServiceName}, #{<<"position">> := <<"the front">>}) -> - case emqx_authentication:move_service_to_the_front(ChainID, ServiceName) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_move_service(#{id := ChainID, - service_name := ServiceName}, #{<<"position">> := <<"the end">>}) -> - case emqx_authentication:move_service_to_the_end(ChainID, ServiceName) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_move_service(#{id := ChainID, - service_name := ServiceName}, #{<<"position">> := N}) when is_number(N) -> - case emqx_authentication:move_service_to_the_nth(ChainID, ServiceName, N) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_move_service(_Binding, _Params) -> - return(serialize_error({missing_parameter, <<"position">>})). - -import_users(Binding, Params) -> - do_import_users(uri_decode(Binding), maps:from_list(Params)). - -do_import_users(#{id := ChainID, service_name := ServiceName}, - #{<<"filename">> := Filename}) -> - case emqx_authentication:import_users(ChainID, ServiceName, Filename) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_import_users(_Binding, Params) -> - Missed = get_missed_params(Params, [<<"filename">>, <<"file_format">>]), - return(serialize_error({missing_parameter, Missed})). - -add_user(Binding, Params) -> - do_add_user(uri_decode(Binding), maps:from_list(Params)). - -do_add_user(#{id := ChainID, - service_name := ServiceName}, UserInfo) -> - case emqx_authentication:add_user(ChainID, ServiceName, UserInfo) of - {ok, User} -> - return({ok, User}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -delete_user(Binding, Params) -> - do_delete_user(uri_decode(Binding), maps:from_list(Params)). - -do_delete_user(#{id := ChainID, - service_name := ServiceName, - user_id := UserID}, _Params) -> - case emqx_authentication:delete_user(ChainID, ServiceName, UserID) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -update_user(Binding, Params) -> - do_update_user(uri_decode(Binding), maps:from_list(Params)). - -do_update_user(#{id := ChainID, - service_name := ServiceName, - user_id := UserID}, NewUserInfo) -> - case emqx_authentication:update_user(ChainID, ServiceName, UserID, NewUserInfo) of - {ok, User} -> - return({ok, User}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -lookup_user(Binding, Params) -> - do_lookup_user(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_user(#{id := ChainID, - service_name := ServiceName, - user_id := UserID}, _Params) -> - case emqx_authentication:lookup_user(ChainID, ServiceName, UserID) of - {ok, User} -> - return({ok, User}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -list_users(Binding, Params) -> - do_list_users(uri_decode(Binding), maps:from_list(Params)). - -do_list_users(#{id := ChainID, - service_name := ServiceName}, _Params) -> - case emqx_authentication:list_users(ChainID, ServiceName) of - {ok, Users} -> - return({ok, Users}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -uri_decode(Params) -> - maps:fold(fun(K, V, Acc) -> - Acc#{K => emqx_http_lib:uri_decode(V)} - end, #{}, Params). - -serialize_error({already_exists, {Type, ID}}) -> - {error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~s '~s' already exists", [serialize_type(Type), ID]))}; -serialize_error({not_found, {Type, ID}}) -> - {error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~s '~s' not found", [serialize_type(Type), ID]))}; -serialize_error({duplicate, Name}) -> - {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Service name '~s' is duplicated", [Name]))}; -serialize_error({missing_parameter, Names = [_ | Rest]}) -> - Format = ["~s," || _ <- Rest] ++ ["~s"], - NFormat = binary_to_list(iolist_to_binary(Format)), - {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names))}; -serialize_error({missing_parameter, Name}) -> - {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter '~s' that is mandatory for processing this request is not supplied.", [Name]))}; -serialize_error(_) -> - {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. - -serialize_type(service) -> - "Service"; -serialize_type(chain) -> - "Chain"; -serialize_type(service_type) -> - "Service type". - -get_missed_params(Actual, Expected) -> - Keys = lists:foldl(fun(Key, Acc) -> - case maps:is_key(Key, Actual) of - true -> Acc; - false -> [Key | Acc] - end - end, [], Expected), - lists:reverse(Keys). diff --git a/apps/emqx_authentication/src/emqx_authentication_jwt.erl b/apps/emqx_authentication/src/emqx_authentication_jwt.erl deleted file mode 100644 index 2b8024e1c..000000000 --- a/apps/emqx_authentication/src/emqx_authentication_jwt.erl +++ /dev/null @@ -1,409 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authentication_jwt). - --export([ create/3 - , update/4 - , authenticate/2 - , destroy/1 - ]). - --service_type(#{ - name => jwt, - params_spec => #{ - use_jwks => #{ - order => 1, - type => boolean - }, - jwks_endpoint => #{ - order => 2, - type => string - }, - refresh_interval => #{ - order => 3, - type => number - }, - algorithm => #{ - order => 3, - type => string, - enum => [<<"hmac-based">>, <<"public-key">>] - }, - secret => #{ - order => 4, - type => string - }, - secret_base64_encoded => #{ - order => 5, - type => boolean - }, - jwt_certfile => #{ - order => 6, - type => file - }, - cacertfile => #{ - order => 7, - type => file - }, - keyfile => #{ - order => 8, - type => file - }, - certfile => #{ - order => 9, - type => file - }, - verify => #{ - order => 10, - type => boolean - }, - server_name_indication => #{ - order => 11, - type => string - } - } -}). - --define(RULES, - #{ - use_jwks => [], - jwks_endpoint => [use_jwks], - refresh_interval => [use_jwks], - algorithm => [use_jwks], - secret => [algorithm], - secret_base64_encoded => [algorithm], - jwt_certfile => [algorithm], - cacertfile => [jwks_endpoint], - keyfile => [jwks_endpoint], - certfile => [jwks_endpoint], - verify => [jwks_endpoint], - server_name_indication => [jwks_endpoint], - verify_claims => [] - }). - -create(_ChainID, _ServiceName, Params) -> - try handle_options(Params) of - Opts -> - do_create(Opts) - catch - {error, Reason} -> - {error, Reason} - end. - -update(_ChainID, _ServiceName, Params, State) -> - try handle_options(Params) of - Opts -> - do_update(Opts, State) - catch - {error, Reason} -> - {error, Reason} - end. - -authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, - verify_claims := VerifyClaims0}) -> - JWKs = case erlang:is_pid(JWK) of - false -> - [JWK]; - true -> - {ok, JWKs0} = emqx_authentication_jwks_connector:get_jwks(JWK), - JWKs0 - end, - VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo), - case verify(JWT, JWKs, VerifyClaims) of - ok -> ok; - {error, invalid_signature} -> ignore; - {error, {claims, _}} -> {stop, bad_passowrd} - end. - -destroy(#{jwks_connector := undefined}) -> - ok; -destroy(#{jwks_connector := Connector}) -> - _ = emqx_authentication_jwks_connector:stop(Connector), - ok. - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -do_create(#{use_jwks := false, - algorithm := 'hmac-based', - secret := Secret0, - secret_base64_encoded := Base64Encoded} = Opts) -> - Secret = case Base64Encoded of - true -> - base64:decode(Secret0); - false -> - Secret0 - end, - JWK = jose_jwk:from_oct(Secret), - {ok, #{jwk => JWK, - verify_claims => maps:get(verify_claims, Opts)}}; - -do_create(#{use_jwks := false, - algorithm := 'public-key', - jwt_certfile := Certfile} = Opts) -> - JWK = jose_jwk:from_pem_file(Certfile), - {ok, #{jwk => JWK, - verify_claims => maps:get(verify_claims, Opts)}}; - -do_create(#{use_jwks := true} = Opts) -> - case emqx_authentication_jwks_connector:start_link(Opts) of - {ok, Connector} -> - {ok, #{jwk => Connector, - verify_claims => maps:get(verify_claims, Opts)}}; - {error, Reason} -> - {error, Reason} - end. - -do_update(Opts, #{jwk_connector := undefined}) -> - do_create(Opts); -do_update(#{use_jwks := false} = Opts, #{jwk_connector := Connector}) -> - _ = emqx_authentication_jwks_connector:stop(Connector), - do_create(Opts); -do_update(#{use_jwks := true} = Opts, #{jwk_connector := Connector} = State) -> - ok = emqx_authentication_jwks_connector:update(Connector, Opts), - {ok, State}. - -replace_placeholder(L, Variables) -> - replace_placeholder(L, Variables, []). - -replace_placeholder([], _Variables, Acc) -> - Acc; -replace_placeholder([{Name, {placeholder, PL}} | More], Variables, Acc) -> - Value = maps:get(PL, Variables), - replace_placeholder(More, Variables, [{Name, Value} | Acc]); -replace_placeholder([{Name, Value} | More], Variables, Acc) -> - replace_placeholder(More, Variables, [{Name, Value} | Acc]). - -verify(_JWS, [], _VerifyClaims) -> - {error, invalid_signature}; -verify(JWS, [JWK | More], VerifyClaims) -> - case jose_jws:verify(JWK, JWS) of - {true, Payload, _JWS} -> - Claims = emqx_json:decode(Payload, [return_maps]), - verify_claims(Claims, VerifyClaims); - {false, _, _} -> - verify(JWS, More, VerifyClaims) - end. - -verify_claims(Claims, VerifyClaims0) -> - Now = os:system_time(seconds), - VerifyClaims = [{<<"exp">>, fun(ExpireTime) -> - Now < ExpireTime - end}, - {<<"iat">>, fun(IssueAt) -> - IssueAt =< Now - end}, - {<<"nbf">>, fun(NotBefore) -> - NotBefore =< Now - end}] ++ VerifyClaims0, - do_verify_claims(Claims, VerifyClaims). - -do_verify_claims(_Claims, []) -> - ok; -do_verify_claims(Claims, [{Name, Fun} | More]) when is_function(Fun) -> - case maps:take(Name, Claims) of - error -> - do_verify_claims(Claims, More); - {Value, NClaims} -> - case Fun(Value) of - true -> - do_verify_claims(NClaims, More); - _ -> - {error, {claims, {Name, Value}}} - end - end; -do_verify_claims(Claims, [{Name, Value} | More]) -> - case maps:take(Name, Claims) of - error -> - do_verify_claims(Claims, More); - {Value, NClaims} -> - do_verify_claims(NClaims, More); - {Value0, _} -> - {error, {claims, {Name, Value0}}} - end. - -handle_options(Opts0) when is_map(Opts0) -> - Ks = maps:fold(fun(K, _, Acc) -> - [atom_to_binary(K, utf8) | Acc] - end, [], ?RULES), - Opts1 = maps:to_list(maps:with(Ks, Opts0)), - handle_options([{binary_to_existing_atom(K, utf8), V} || {K, V} <- Opts1]); - -handle_options(Opts0) when is_list(Opts0) -> - Opts1 = add_missing_options(Opts0), - process_options({Opts1, [], length(Opts1)}, #{}). - -add_missing_options(Opts) -> - AllOpts = maps:keys(?RULES), - Fun = fun(K, Acc) -> - case proplists:is_defined(K, Acc) of - true -> - Acc; - false -> - [{K, unbound} | Acc] - end - end, - lists:foldl(Fun, Opts, AllOpts). - -process_options({[], [], _}, OptsMap) -> - OptsMap; -process_options({[], Skipped, Counter}, OptsMap) - when length(Skipped) < Counter -> - process_options({Skipped, [], length(Skipped)}, OptsMap); -process_options({[], _Skipped, _Counter}, _OptsMap) -> - throw({error, faulty_configuration}); -process_options({[{K, V} = Opt | More], Skipped, Counter}, OptsMap0) -> - case check_dependencies(K, OptsMap0) of - true -> - OptsMap1 = handle_option(K, V, OptsMap0), - process_options({More, Skipped, Counter}, OptsMap1); - false -> - process_options({More, [Opt | Skipped], Counter}, OptsMap0) - end. - -%% TODO: This is not a particularly good implementation(K => needless), it needs to be improved -handle_option(use_jwks, true, OptsMap) -> - OptsMap#{use_jwks => true, - algorithm => needless}; -handle_option(use_jwks, false, OptsMap) -> - OptsMap#{use_jwks => false, - jwks_endpoint => needless}; -handle_option(jwks_endpoint = Opt, unbound, #{use_jwks := true}) -> - throw({error, {options, {Opt, unbound}}}); -handle_option(jwks_endpoint, Value, #{use_jwks := true} = OptsMap) - when Value =/= unbound -> - case emqx_http_lib:uri_parse(Value) of - {ok, #{scheme := http}} -> - OptsMap#{enable_ssl => false, - jwks_endpoint => Value}; - {ok, #{scheme := https}} -> - OptsMap#{enable_ssl => true, - jwks_endpoint => Value}; - {error, _Reason} -> - throw({error, {options, {jwks_endpoint, Value}}}) - end; -handle_option(refresh_interval = Opt, Value0, #{use_jwks := true} = OptsMap) -> - Value = validate_option(Opt, Value0), - OptsMap#{Opt => Value}; -handle_option(algorithm = Opt, Value0, #{use_jwks := false} = OptsMap) -> - Value = validate_option(Opt, Value0), - OptsMap#{Opt => Value}; -handle_option(secret = Opt, unbound, #{algorithm := 'hmac-based'}) -> - throw({error, {options, {Opt, unbound}}}); -handle_option(secret = Opt, Value, #{algorithm := 'hmac-based'} = OptsMap) -> - OptsMap#{Opt => Value}; -handle_option(secret_base64_encoded = Opt, Value0, #{algorithm := 'hmac-based'} = OptsMap) -> - Value = validate_option(Opt, Value0), - OptsMap#{Opt => Value}; -handle_option(jwt_certfile = Opt, unbound, #{algorithm := 'public-key'}) -> - throw({error, {options, {Opt, unbound}}}); -handle_option(jwt_certfile = Opt, Value, #{algorithm := 'public-key'} = OptsMap) -> - OptsMap#{Opt => Value}; -handle_option(verify = Opt, Value0, #{enable_ssl := true} = OptsMap) -> - Value = validate_option(Opt, Value0), - OptsMap#{Opt => Value}; -handle_option(cacertfile = Opt, Value, #{enable_ssl := true} = OptsMap) - when Value =/= unbound -> - OptsMap#{Opt => Value}; -handle_option(certfile, unbound, #{enable_ssl := true} = OptsMap) -> - OptsMap; -handle_option(certfile = Opt, Value, #{enable_ssl := true} = OptsMap) -> - OptsMap#{Opt => Value}; -handle_option(keyfile, unbound, #{enable_ssl := true} = OptsMap) -> - OptsMap; -handle_option(keyfile = Opt, Value, #{enable_ssl := true} = OptsMap) -> - OptsMap#{Opt => Value}; -handle_option(server_name_indication = Opt, Value0, #{enable_ssl := true} = OptsMap) -> - Value = validate_option(Opt, Value0), - OptsMap#{Opt => Value}; -handle_option(verify_claims = Opt, Value0, OptsMap) -> - Value = handle_verify_claims(Value0), - OptsMap#{Opt => Value}; -handle_option(_Opt, _Value, OptsMap) -> - OptsMap. - -validate_option(refresh_interval, unbound) -> - 300; -validate_option(refresh_interval, Value) when is_integer(Value) -> - Value; -validate_option(algorithm, <<"hmac-based">>) -> - 'hmac-based'; -validate_option(algorithm, <<"public-key">>) -> - 'public-key'; -validate_option(secret_base64_encoded, unbound) -> - false; -validate_option(secret_base64_encoded, Value) when is_boolean(Value) -> - Value; -validate_option(verify, unbound) -> - verify_none; -validate_option(verify, true) -> - verify_peer; -validate_option(verify, false) -> - verify_none; -validate_option(server_name_indication, unbound) -> - disable; -validate_option(server_name_indication, <<"disable">>) -> - disable; -validate_option(server_name_indication, Value) when is_list(Value) -> - Value; -validate_option(Opt, Value) -> - throw({error, {options, {Opt, Value}}}). - -handle_verify_claims(Opts0) -> - try handle_verify_claims(Opts0, []) - catch - error:_ -> - throw({error, {options, {verify_claims, Opts0}}}) - end. - -handle_verify_claims([], Acc) -> - Acc; -handle_verify_claims([{Name, Expected0} | More], Acc) - when is_binary(Name) andalso is_binary(Expected0) -> - Expected = handle_placeholder(Expected0), - handle_verify_claims(More, [{Name, Expected} | Acc]). - -handle_placeholder(Placeholder0) -> - case re:run(Placeholder0, "^\\$\\{[a-z0-9\\_]+\\}$", [{capture, all}]) of - {match, [{Offset, Length}]} -> - Placeholder1 = binary:part(Placeholder0, Offset + 2, Length - 3), - Placeholder2 = validate_placeholder(Placeholder1), - {placeholder, Placeholder2}; - nomatch -> - Placeholder0 - end. - -validate_placeholder(<<"clientid">>) -> - clientid; -validate_placeholder(<<"username">>) -> - username. - -check_dependencies(Opt, OptsMap) -> - case maps:get(Opt, ?RULES) of - [] -> - true; - Deps -> - option_already_defined(Opt, OptsMap) orelse - dependecies_already_defined(Deps, OptsMap) - end. - -option_already_defined(Opt, OptsMap) -> - maps:get(Opt, OptsMap, unbound) =/= unbound. - -dependecies_already_defined(Deps, OptsMap) -> - Fun = fun(Opt) -> option_already_defined(Opt, OptsMap) end, - lists:all(Fun, Deps). diff --git a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl deleted file mode 100644 index d110d940a..000000000 --- a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl +++ /dev/null @@ -1,191 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authentication_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). - --define(AUTH, emqx_authentication). - -all() -> - emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_authentication]), - Config. - -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authentication]), - ok. - -t_chain(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(#{id => ChainID})), - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), - ok. - -t_service(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)), - - ServiceName1 = <<"myservice1">>, - ServiceParams1 = #{name => ServiceName1, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ?assertEqual({ok, [ServiceParams1]}, ?AUTH:add_services(ChainID, [ServiceParams1])), - ?assertEqual({ok, ServiceParams1}, ?AUTH:lookup_service(ChainID, ServiceName1)), - ?assertEqual({ok, [ServiceParams1]}, ?AUTH:list_services(ChainID)), - ?assertEqual({error, {already_exists, {service, ServiceName1}}}, ?AUTH:add_services(ChainID, [ServiceParams1])), - - ServiceName2 = <<"myservice2">>, - ServiceParams2 = ServiceParams1#{name => ServiceName2}, - ?assertEqual({ok, [ServiceParams2]}, ?AUTH:add_services(ChainID, [ServiceParams2])), - ?assertMatch({ok, #{id := ChainID, services := [ServiceParams1, ServiceParams2]}}, ?AUTH:lookup_chain(ChainID)), - ?assertEqual({ok, ServiceParams2}, ?AUTH:lookup_service(ChainID, ServiceName2)), - ?assertEqual({ok, [ServiceParams1, ServiceParams2]}, ?AUTH:list_services(ChainID)), - - ?assertEqual(ok, ?AUTH:move_service_to_the_front(ChainID, ServiceName2)), - ?assertEqual({ok, [ServiceParams2, ServiceParams1]}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:move_service_to_the_end(ChainID, ServiceName2)), - ?assertEqual({ok, [ServiceParams1, ServiceParams2]}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 1)), - ?assertEqual({ok, [ServiceParams2, ServiceParams1]}, ?AUTH:list_services(ChainID)), - ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 3)), - ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 0)), - ?assertEqual(ok, ?AUTH:delete_services(ChainID, [ServiceName1, ServiceName2])), - ?assertEqual({ok, []}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ok. - -t_mnesia_service(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - - ServiceName = <<"myservice">>, - ServiceParams = #{name => ServiceName, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])), - - UserInfo = #{<<"user_id">> => <<"myuser">>, - <<"password">> => <<"mypass">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, ServiceName, UserInfo)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), - ClientInfo = #{chain_id => ChainID, - username => <<"myuser">>, - password => <<"mypass">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), - ClientInfo2 = ClientInfo#{username => <<"baduser">>}, - ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), - ClientInfo3 = ClientInfo#{password => <<"badpass">>}, - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), - UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, ServiceName, <<"myuser">>, UserInfo2)), - ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), - ?assertEqual(ok, ?AUTH:delete_user(ChainID, ServiceName, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), - - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, ServiceName, UserInfo)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_services(ChainID, [ServiceName])), - ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])), - ?assertMatch({error, not_found}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), - - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ?assertEqual([], ets:tab2list(mnesia_basic_auth)), - ok. - -t_import(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - - ServiceName = <<"myservice">>, - ServiceParams = #{name => ServiceName, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])), - - Dir = code:lib_dir(emqx_authentication, test), - ?assertEqual(ok, ?AUTH:import_users(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.json"]))), - ?assertEqual(ok, ?AUTH:import_users(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.csv"]))), - ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser1">>)), - ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser3">>)), - ClientInfo1 = #{chain_id => ChainID, - username => <<"myuser1">>, - password => <<"mypassword1">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), - ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, - password => <<"mypassword3">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ok. - -t_multi_mnesia_service(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - - ServiceName1 = <<"myservice1">>, - ServiceParams1 = #{name => ServiceName1, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ServiceName2 = <<"myservice2">>, - ServiceParams2 = #{name => ServiceName2, - type => mnesia, - params => #{ - user_id_type => <<"clientid">>, - password_hash_algorithm => <<"sha256">>}}, - ?assertEqual({ok, [ServiceParams1]}, ?AUTH:add_services(ChainID, [ServiceParams1])), - ?assertEqual({ok, [ServiceParams2]}, ?AUTH:add_services(ChainID, [ServiceParams2])), - - ?assertEqual({ok, #{user_id => <<"myuser">>}}, - ?AUTH:add_user(ChainID, ServiceName1, - #{<<"user_id">> => <<"myuser">>, - <<"password">> => <<"mypass1">>})), - ?assertEqual({ok, #{user_id => <<"myclient">>}}, - ?AUTH:add_user(ChainID, ServiceName2, - #{<<"user_id">> => <<"myclient">>, - <<"password">> => <<"mypass2">>})), - ClientInfo1 = #{chain_id => ChainID, - username => <<"myuser">>, - clientid => <<"myclient">>, - password => <<"mypass1">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), - ?assertEqual(ok, ?AUTH:move_service_to_the_front(ChainID, ServiceName2)), - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)), - ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ok. - - - diff --git a/apps/emqx_authentication/data/user-credentials.csv b/apps/emqx_authn/data/user-credentials.csv similarity index 100% rename from apps/emqx_authentication/data/user-credentials.csv rename to apps/emqx_authn/data/user-credentials.csv diff --git a/apps/emqx_authentication/data/user-credentials.json b/apps/emqx_authn/data/user-credentials.json similarity index 100% rename from apps/emqx_authentication/data/user-credentials.json rename to apps/emqx_authn/data/user-credentials.json diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf new file mode 100644 index 000000000..ecd49d5a5 --- /dev/null +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -0,0 +1,26 @@ +emqx_authn: { + chains: [ + # { + # id: "chain1" + # type: simple + # authenticators: [ + # { + # name: "authenticator1" + # type: built-in-database + # config: { + # user_id_type: clientid + # password_hash_algorithm: { + # name: sha256 + # } + # } + # } + # ] + # } + ] + bindings: [ + # { + # chain_id: "chain1" + # listeners: ["mqtt-tcp", "mqtt-ssl"] + # } + ] +} diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl new file mode 100644 index 000000000..46c1cf7ca --- /dev/null +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -0,0 +1,67 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-define(APP, emqx_authn). + +-type chain_id() :: binary(). +-type authn_type() :: simple | enhanced. +-type authenticator_name() :: binary(). +-type authenticator_type() :: mnesia | jwt | mysql | postgresql. +-type listener_id() :: binary(). + +-record(authenticator, + { name :: authenticator_name() + , type :: authenticator_type() + , provider :: module() + , config :: map() + , state :: map() + }). + +-record(chain, + { id :: chain_id() + , type :: authn_type() + , authenticators :: [{authenticator_name(), #authenticator{}}] + , created_at :: integer() + }). + +-record(binding, + { bound :: {listener_id(), authn_type()} + , chain_id :: chain_id() + }). + +-define(AUTH_SHARD, emqx_authn_shard). + +-define(CLUSTER_CALL(Module, Func, Args), ?CLUSTER_CALL(Module, Func, Args, ok)). + +-define(CLUSTER_CALL(Module, Func, Args, ResParttern), + fun() -> + case LocalResult = erlang:apply(Module, Func, Args) of + ResParttern -> + Nodes = nodes(), + {ResL, BadNodes} = rpc:multicall(Nodes, Module, Func, Args, 5000), + NResL = lists:zip(Nodes - BadNodes, ResL), + Errors = lists:filter(fun({_, ResParttern}) -> false; + (_) -> true + end, NResL), + OtherErrors = [{BadNode, node_does_not_exist} || BadNode <- BadNodes], + case Errors ++ OtherErrors of + [] -> LocalResult; + NErrors -> {error, NErrors} + end; + ErrorResult -> + {error, ErrorResult} + end + end()). diff --git a/apps/emqx_authentication/rebar.config b/apps/emqx_authn/rebar.config similarity index 94% rename from apps/emqx_authentication/rebar.config rename to apps/emqx_authn/rebar.config index 0a0af8c29..73696b033 100644 --- a/apps/emqx_authentication/rebar.config +++ b/apps/emqx_authn/rebar.config @@ -15,4 +15,4 @@ {cover_enabled, true}. {cover_opts, [verbose]}. -{cover_export_enabled, true}. \ No newline at end of file +{cover_export_enabled, true}. diff --git a/apps/emqx_authentication/src/emqx_authentication.app.src b/apps/emqx_authn/src/emqx_authn.app.src similarity index 63% rename from apps/emqx_authentication/src/emqx_authentication.app.src rename to apps/emqx_authn/src/emqx_authn.app.src index 4f55ca0a7..c997582ec 100644 --- a/apps/emqx_authentication/src/emqx_authentication.app.src +++ b/apps/emqx_authn/src/emqx_authn.app.src @@ -1,10 +1,10 @@ -{application, emqx_authentication, +{application, emqx_authn, [{description, "EMQ X Authentication"}, {vsn, "0.1.0"}, {modules, []}, - {registered, [emqx_authentication_sup, emqx_authentication_registry]}, + {registered, [emqx_authn_sup, emqx_authn_registry]}, {applications, [kernel,stdlib]}, - {mod, {emqx_authentication_app,[]}}, + {mod, {emqx_authn_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, {maintainers, ["EMQ X Team "]}, diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl new file mode 100644 index 000000000..24bdb21e7 --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -0,0 +1,490 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn). + +-include("emqx_authn.hrl"). + +-export([ enable/0 + , disable/0 + ]). + +-export([authenticate/1]). + +-export([ create_chain/1 + , delete_chain/1 + , lookup_chain/1 + , list_chains/0 + , bind/2 + , unbind/2 + , list_bindings/1 + , list_bound_chains/1 + , create_authenticator/2 + , delete_authenticator/2 + , update_authenticator/3 + , lookup_authenticator/2 + , list_authenticators/1 + , move_authenticator_to_the_front/2 + , move_authenticator_to_the_end/2 + , move_authenticator_to_the_nth/3 + ]). + +-export([ import_users/3 + , add_user/3 + , delete_user/3 + , update_user/4 + , lookup_user/3 + , list_users/2 + ]). + +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). + +-define(CHAIN_TAB, emqx_authn_chain). +-define(BINDING_TAB, emqx_authn_binding). + +-rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}). +-rlog_shard({?AUTH_SHARD, ?BINDING_TAB}). + +%%------------------------------------------------------------------------------ +%% Mnesia bootstrap +%%------------------------------------------------------------------------------ + +%% @doc Create or replicate tables. +-spec(mnesia(boot) -> ok). +mnesia(boot) -> + %% Optimize storage + StoreProps = [{ets, [{read_concurrency, true}]}], + %% Chain table + ok = ekka_mnesia:create_table(?CHAIN_TAB, [ + {ram_copies, [node()]}, + {record_name, chain}, + {local_content, true}, + {attributes, record_info(fields, chain)}, + {storage_properties, StoreProps}]), + %% Binding table + ok = ekka_mnesia:create_table(?BINDING_TAB, [ + {ram_copies, [node()]}, + {record_name, binding}, + {local_content, true}, + {attributes, record_info(fields, binding)}, + {storage_properties, StoreProps}]). + +enable() -> + case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of + ok -> ok; + {error, already_exists} -> ok + end. + +disable() -> + emqx:unhook('client.authenticate', {?MODULE, authenticate, []}), + ok. + +authenticate(#{listener_id := ListenerID} = ClientInfo) -> + case lookup_chain_by_listener(ListenerID, simple) of + {error, _} -> + {error, no_authenticators}; + {ok, ChainID} -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [#chain{authenticators = []}] -> + {error, no_authenticators}; + [#chain{authenticators = Authenticators}] -> + do_authenticate(Authenticators, ClientInfo); + [] -> + {error, no_authenticators} + end + end. + +do_authenticate([], _) -> + {error, user_not_found}; +do_authenticate([{_, #authenticator{provider = Provider, state = State}} | More], ClientInfo) -> + case Provider:authenticate(ClientInfo, State) of + ignore -> do_authenticate(More, ClientInfo); + ok -> ok; + {ok, NewClientInfo} -> {ok, NewClientInfo}; + {stop, Reason} -> {error, Reason} + end. + +create_chain(#{id := ID, + type := Type}) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ID, write) of + [] -> + Chain = #chain{id = ID, + type = Type, + authenticators = [], + created_at = erlang:system_time(millisecond)}, + mnesia:write(?CHAIN_TAB, Chain, write), + {ok, serialize_chain(Chain)}; + [_ | _] -> + {error, {already_exists, {chain, ID}}} + end + end). + +delete_chain(ID) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ID, write) of + [] -> + {error, {not_found, {chain, ID}}}; + [#chain{authenticators = Authenticators}] -> + _ = [do_delete_authenticator(Authenticator) || {_, Authenticator} <- Authenticators], + mnesia:delete(?CHAIN_TAB, ID, write) + end + end). + +lookup_chain(ID) -> + case mnesia:dirty_read(?CHAIN_TAB, ID) of + [] -> + {error, {not_found, {chain, ID}}}; + [Chain] -> + {ok, serialize_chain(Chain)} + end. + +list_chains() -> + Chains = ets:tab2list(?CHAIN_TAB), + {ok, [serialize_chain(Chain) || Chain <- Chains]}. + +bind(ChainID, Listeners) -> + %% TODO: ensure listener id is valid + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ChainID, write) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{type = AuthNType}] -> + Result = lists:foldl( + fun(ListenerID, Acc) -> + case mnesia:read(?BINDING_TAB, {ListenerID, AuthNType}, write) of + [] -> + Binding = #binding{bound = {ListenerID, AuthNType}, chain_id = ChainID}, + mnesia:write(?BINDING_TAB, Binding, write), + Acc; + _ -> + [ListenerID | Acc] + end + end, [], Listeners), + case Result of + [] -> ok; + Listeners0 -> {error, {already_bound, Listeners0}} + end + end + end). + +unbind(ChainID, Listeners) -> + trans( + fun() -> + Result = lists:foldl( + fun(ListenerID, Acc) -> + MatchSpec = [{{binding, {ListenerID, '_'}, ChainID}, [], ['$_']}], + case mnesia:select(?BINDING_TAB, MatchSpec, write) of + [] -> + [ListenerID | Acc]; + [#binding{bound = Bound}] -> + mnesia:delete(?BINDING_TAB, Bound, write), + Acc + end + end, [], Listeners), + case Result of + [] -> ok; + Listeners0 -> + {error, {not_found, Listeners0}} + end + end). + +list_bindings(ChainID) -> + trans( + fun() -> + MatchSpec = [{{binding, {'$1', '_'}, ChainID}, [], ['$1']}], + Listeners = mnesia:select(?BINDING_TAB, MatchSpec), + {ok, #{chain_id => ChainID, listeners => Listeners}} + end). + +list_bound_chains(ListenerID) -> + trans( + fun() -> + MatchSpec = [{{binding, {ListenerID, '_'}, '_'}, [], ['$_']}], + Bindings = mnesia:select(?BINDING_TAB, MatchSpec), + Chains = [{AuthNType, ChainID} || #binding{bound = {_, AuthNType}, + chain_id = ChainID} <- Bindings], + {ok, maps:from_list(Chains)} + end). + +create_authenticator(ChainID, #{name := Name, + type := Type, + config := Config}) -> + UpdateFun = + fun(Chain = #chain{type = AuthNType, authenticators = Authenticators}) -> + case lists:keymember(Name, 1, Authenticators) of + true -> + {error, {already_exists, {authenticator, Name}}}; + false -> + Provider = authenticator_provider(AuthNType, Type), + case Provider:create(ChainID, Name, Config) of + {ok, State} -> + Authenticator = #authenticator{name = Name, + type = Type, + provider = Provider, + config = Config, + state = State}, + NChain = Chain#chain{authenticators = Authenticators ++ [{Name, Authenticator}]}, + ok = mnesia:write(?CHAIN_TAB, NChain, write), + {ok, serialize_authenticator(Authenticator)}; + {error, Reason} -> + {error, Reason} + end + end + end, + update_chain(ChainID, UpdateFun). + +delete_authenticator(ChainID, AuthenticatorName) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case lists:keytake(AuthenticatorName, 1, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + {value, {_, Authenticator}, NAuthenticators} -> + _ = do_delete_authenticator(Authenticator), + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write) + end + end, + update_chain(ChainID, UpdateFun). + +update_authenticator(ChainID, AuthenticatorName, Config) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case proplists:get_value(AuthenticatorName, Authenticators, undefined) of + undefined -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + #authenticator{provider = Provider, + config = OriginalConfig, + state = State} = Authenticator -> + NewConfig = maps:merge(OriginalConfig, Config), + case Provider:update(ChainID, AuthenticatorName, NewConfig, State) of + {ok, NState} -> + NAuthenticator = Authenticator#authenticator{config = NewConfig, + state = NState}, + NAuthenticators = update_value(AuthenticatorName, NAuthenticator, Authenticators), + ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), + {ok, serialize_authenticator(NAuthenticator)}; + {error, Reason} -> + {error, Reason} + end + end + end, + update_chain(ChainID, UpdateFun). + +lookup_authenticator(ChainID, AuthenticatorName) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{authenticators = Authenticators}] -> + case proplists:get_value(AuthenticatorName, Authenticators, undefined) of + undefined -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + Authenticator -> + {ok, serialize_authenticator(Authenticator)} + end + end. + +list_authenticators(ChainID) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{authenticators = Authenticators}] -> + {ok, serialize_authenticators(Authenticators)} + end. + +move_authenticator_to_the_front(ChainID, AuthenticatorName) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case move_authenticator_to_the_front_(AuthenticatorName, Authenticators) of + {ok, NAuthenticators} -> + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +move_authenticator_to_the_end(ChainID, AuthenticatorName) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case move_authenticator_to_the_end_(AuthenticatorName, Authenticators) of + {ok, NAuthenticators} -> + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N) of + {ok, NAuthenticators} -> + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +import_users(ChainID, AuthenticatorName, Filename) -> + call_authenticator(ChainID, AuthenticatorName, import_users, [Filename]). + +add_user(ChainID, AuthenticatorName, UserInfo) -> + call_authenticator(ChainID, AuthenticatorName, add_user, [UserInfo]). + +delete_user(ChainID, AuthenticatorName, UserID) -> + call_authenticator(ChainID, AuthenticatorName, delete_user, [UserID]). + +update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) -> + call_authenticator(ChainID, AuthenticatorName, update_user, [UserID, NewUserInfo]). + +lookup_user(ChainID, AuthenticatorName, UserID) -> + call_authenticator(ChainID, AuthenticatorName, lookup_user, [UserID]). + +list_users(ChainID, AuthenticatorName) -> + call_authenticator(ChainID, AuthenticatorName, list_users, []). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +authenticator_provider(simple, 'built-in-database') -> emqx_authn_mnesia; +authenticator_provider(simple, jwt) -> emqx_authn_jwt; +authenticator_provider(simple, mysql) -> emqx_authn_mysql; +authenticator_provider(simple, postgresql) -> emqx_authn_pgsql. + +% authenticator_provider(enhanced, 'enhanced-built-in-database') -> emqx_enhanced_authn_mnesia. + +do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> + Provider:destroy(State). + +update_value(Key, Value, List) -> + lists:keyreplace(Key, 1, List, {Key, Value}). + +move_authenticator_to_the_front_(AuthenticatorName, Authenticators) -> + move_authenticator_to_the_front_(AuthenticatorName, Authenticators, []). + +move_authenticator_to_the_front_(AuthenticatorName, [], _) -> + {error, {not_found, {authenticator, AuthenticatorName}}}; +move_authenticator_to_the_front_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) -> + {ok, [Authenticator | (lists:reverse(Passed) ++ More)]}; +move_authenticator_to_the_front_(AuthenticatorName, [Authenticator | More], Passed) -> + move_authenticator_to_the_front_(AuthenticatorName, More, [Authenticator | Passed]). + +move_authenticator_to_the_end_(AuthenticatorName, Authenticators) -> + move_authenticator_to_the_end_(AuthenticatorName, Authenticators, []). + +move_authenticator_to_the_end_(AuthenticatorName, [], _) -> + {error, {not_found, {authenticator, AuthenticatorName}}}; +move_authenticator_to_the_end_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) -> + {ok, lists:reverse(Passed) ++ More ++ [Authenticator]}; +move_authenticator_to_the_end_(AuthenticatorName, [Authenticator | More], Passed) -> + move_authenticator_to_the_end_(AuthenticatorName, More, [Authenticator | Passed]). + +move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N) + when N =< length(Authenticators) andalso N > 0 -> + move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N, []); +move_authenticator_to_the_nth_(_, _, _) -> + {error, out_of_range}. + +move_authenticator_to_the_nth_(AuthenticatorName, [], _, _) -> + {error, {not_found, {authenticator, AuthenticatorName}}}; +move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) + when N =< length(Passed) -> + {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), + {ok, L1 ++ [Authenticator] ++ L2 ++ More}; +move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) -> + {L1, L2} = lists:split(N - length(Passed) - 1, More), + {ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2}; +move_authenticator_to_the_nth_(AuthenticatorName, [Authenticator | More], N, Passed) -> + move_authenticator_to_the_nth_(AuthenticatorName, More, N, [Authenticator | Passed]). + +update_chain(ChainID, UpdateFun) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ChainID, write) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [Chain] -> + UpdateFun(Chain) + end + end). + +lookup_chain_by_listener(ListenerID, AuthNType) -> + case mnesia:dirty_read(?BINDING_TAB, {ListenerID, AuthNType}) of + [] -> + {error, not_found}; + [#binding{chain_id = ChainID}] -> + {ok, ChainID} + end. + + +call_authenticator(ChainID, AuthenticatorName, Func, Args) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{authenticators = Authenticators}] -> + case proplists:get_value(AuthenticatorName, Authenticators, undefined) of + undefined -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + #authenticator{provider = Provider, state = State} -> + case erlang:function_exported(Provider, Func, length(Args) + 1) of + true -> + erlang:apply(Provider, Func, Args ++ [State]); + false -> + {error, unsupported_feature} + end + end + end. + +serialize_chain(#chain{id = ID, + type = Type, + authenticators = Authenticators, + created_at = CreatedAt}) -> + #{id => ID, + type => Type, + authenticators => serialize_authenticators(Authenticators), + created_at => CreatedAt}. + +% serialize_binding(#binding{bound = {ListenerID, _}, +% chain_id = ChainID}) -> +% #{listener_id => ListenerID, +% chain_id => ChainID}. + +serialize_authenticators(Authenticators) -> + [serialize_authenticator(Authenticator) || {_, Authenticator} <- Authenticators]. + +serialize_authenticator(#authenticator{name = Name, + type = Type, + config = Config}) -> + #{name => Name, + type => Type, + config => Config}. + +trans(Fun) -> + trans(Fun, []). + +trans(Fun, Args) -> + case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of + {atomic, Res} -> Res; + {aborted, Reason} -> {error, Reason} + end. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl new file mode 100644 index 000000000..ad9542958 --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -0,0 +1,544 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_api). + +-include("emqx_authn.hrl"). + +-export([ create_chain/2 + , delete_chain/2 + , lookup_chain/2 + , list_chains/2 + , bind/2 + , unbind/2 + , list_bindings/2 + , list_bound_chains/2 + , create_authenticator/2 + , delete_authenticator/2 + , update_authenticator/2 + , lookup_authenticator/2 + , list_authenticators/2 + , move_authenticator/2 + , import_users/2 + , add_user/2 + , delete_user/2 + , update_user/2 + , lookup_user/2 + , list_users/2 + ]). + +-import(minirest, [return/1]). + +-rest_api(#{name => create_chain, + method => 'POST', + path => "/authentication/chains", + func => create_chain, + descr => "Create a chain" + }). + +-rest_api(#{name => delete_chain, + method => 'DELETE', + path => "/authentication/chains/:bin:id", + func => delete_chain, + descr => "Delete chain" + }). + +-rest_api(#{name => lookup_chain, + method => 'GET', + path => "/authentication/chains/:bin:id", + func => lookup_chain, + descr => "Lookup chain" + }). + +-rest_api(#{name => list_chains, + method => 'GET', + path => "/authentication/chains", + func => list_chains, + descr => "List all chains" + }). + +-rest_api(#{name => bind, + method => 'POST', + path => "/authentication/chains/:bin:id/bindings/bulk", + func => bind, + descr => "Bind" + }). + +-rest_api(#{name => unbind, + method => 'DELETE', + path => "/authentication/chains/:bin:id/bindings/bulk", + func => unbind, + descr => "Unbind" + }). + +-rest_api(#{name => list_bindings, + method => 'GET', + path => "/authentication/chains/:bin:id/bindings", + func => list_bindings, + descr => "List bindings" + }). + +-rest_api(#{name => list_bound_chains, + method => 'GET', + path => "/authentication/listeners/:bin:listener_id/bound_chains", + func => list_bound_chains, + descr => "List bound chains" + }). + +-rest_api(#{name => create_authenticator, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators", + func => create_authenticator, + descr => "Create authenticator to chain" + }). + +-rest_api(#{name => delete_authenticator, + method => 'DELETE', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + func => delete_authenticator, + descr => "Delete authenticator from chain" + }). + +-rest_api(#{name => update_authenticator, + method => 'PUT', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + func => update_authenticator, + descr => "Update authenticator in chain" + }). + +-rest_api(#{name => lookup_authenticator, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + func => lookup_authenticator, + descr => "Lookup authenticator in chain" + }). + +-rest_api(#{name => list_authenticators, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators", + func => list_authenticators, + descr => "List authenticators in chain" + }). + +-rest_api(#{name => move_authenticator, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/position", + func => move_authenticator, + descr => "Change the order of authenticators" + }). + +-rest_api(#{name => import_users, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/import-users", + func => import_users, + descr => "Import users" + }). + +-rest_api(#{name => add_user, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users", + func => add_user, + descr => "Add user" + }). + +-rest_api(#{name => delete_user, + method => 'DELETE', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + func => delete_user, + descr => "Delete user" + }). + +-rest_api(#{name => update_user, + method => 'PUT', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + func => update_user, + descr => "Update user" + }). + +-rest_api(#{name => lookup_user, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + func => lookup_user, + descr => "Lookup user" + }). + +%% TODO: Support pagination +-rest_api(#{name => list_users, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users", + func => list_users, + descr => "List all users" + }). + +create_chain(Binding, Params) -> + do_create_chain(uri_decode(Binding), maps:from_list(Params)). + +do_create_chain(_Binding, Chain0) -> + Config = #{<<"authn">> => #{<<"chains">> => [Chain0#{<<"authenticators">> => []}], + <<"bindings">> => []}}, + #{authn := #{chains := [Chain1]}} + = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:create_chain(Chain1) of + {ok, Chain2} -> + return({ok, Chain2}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +delete_chain(Binding, Params) -> + do_delete_chain(uri_decode(Binding), maps:from_list(Params)). + +do_delete_chain(#{id := ChainID}, _Params) -> + case emqx_authn:delete_chain(ChainID) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +lookup_chain(Binding, Params) -> + do_lookup_chain(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_chain(#{id := ChainID}, _Params) -> + case emqx_authn:lookup_chain(ChainID) of + {ok, Chain} -> + return({ok, Chain}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +list_chains(Binding, Params) -> + do_list_chains(uri_decode(Binding), maps:from_list(Params)). + +do_list_chains(_Binding, _Params) -> + {ok, Chains} = emqx_authn:list_chains(), + return({ok, Chains}). + +bind(Binding, Params) -> + do_bind(uri_decode(Binding), lists_to_map(Params)). + +do_bind(#{id := ChainID}, #{<<"listeners">> := Listeners}) -> + % Config = #{<<"authn">> => #{<<"chains">> => [], + % <<"bindings">> => [#{<<"chain">> := ChainID, + % <<"listeners">> := Listeners}]}}, + % #{authn := #{bindings := [#{listeners := Listeners}]}} + % = hocon_schema:check_plain(emqx_authn_schema, Config, + % #{atom_key => true, nullable => true}), + case emqx_authn:bind(ChainID, Listeners) of + ok -> + return(ok); + {error, {alread_bound, Listeners}} -> + {ok, #{code => <<"ALREADY_EXISTS">>, + message => <<"ALREADY_BOUND">>, + detail => Listeners}}; + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_bind(_, _) -> + return(serialize_error({missing_parameter, <<"listeners">>})). + +unbind(Binding, Params) -> + do_unbind(uri_decode(Binding), lists_to_map(Params)). + +do_unbind(#{id := ChainID}, #{<<"listeners">> := Listeners0}) -> + case emqx_authn:unbind(ChainID, Listeners0) of + ok -> + return(ok); + {error, {not_found, Listeners1}} -> + {ok, #{code => <<"NOT_FOUND">>, + detail => Listeners1}}; + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_unbind(_, _) -> + return(serialize_error({missing_parameter, <<"listeners">>})). + +list_bindings(Binding, Params) -> + do_list_bindings(uri_decode(Binding), lists_to_map(Params)). + +do_list_bindings(#{id := ChainID}, _) -> + {ok, Binding} = emqx_authn:list_bindings(ChainID), + return({ok, Binding}). + +list_bound_chains(Binding, Params) -> + do_list_bound_chains(uri_decode(Binding), lists_to_map(Params)). + +do_list_bound_chains(#{listener_id := ListenerID}, _) -> + {ok, Chains} = emqx_authn:list_bound_chains(ListenerID), + return({ok, Chains}). + +create_authenticator(Binding, Params) -> + do_create_authenticator(uri_decode(Binding), lists_to_map(Params)). + +do_create_authenticator(#{id := ChainID}, Authenticator0) -> + case emqx_authn:lookup_chain(ChainID) of + {ok, #{type := Type}} -> + Chain = #{<<"id">> => ChainID, + <<"type">> => Type, + <<"authenticators">> => [Authenticator0]}, + Config = #{<<"authn">> => #{<<"chains">> => [Chain], + <<"bindings">> => []}}, + #{authn := #{chains := [#{authenticators := [Authenticator1]}]}} + = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:create_authenticator(ChainID, Authenticator1) of + {ok, Authenticator2} -> + return({ok, Authenticator2}); + {error, Reason} -> + return(serialize_error(Reason)) + end; + {error, Reason} -> + return(serialize_error(Reason)) + end. + +delete_authenticator(Binding, Params) -> + do_delete_authenticator(uri_decode(Binding), maps:from_list(Params)). + +do_delete_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, _Params) -> + case emqx_authn:delete_authenticator(ChainID, AuthenticatorName) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +%% TODO: Support incremental update +update_authenticator(Binding, Params) -> + do_update_authenticator(uri_decode(Binding), lists_to_map(Params)). + +%% TOOD: PUT method supports creation and update +do_update_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, AuthenticatorConfig0) -> + case emqx_authn:lookup_chain(ChainID) of + {ok, #{type := ChainType}} -> + case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of + {ok, #{type := Type}} -> + Authenticator = #{<<"name">> => AuthenticatorName, + <<"type">> => Type, + <<"config">> => AuthenticatorConfig0}, + Chain = #{<<"id">> => ChainID, + <<"type">> => ChainType, + <<"authenticators">> => [Authenticator]}, + Config = #{<<"authn">> => #{<<"chains">> => [Chain], + <<"bindings">> => []}}, + #{ + authn := #{ + chains := [#{ + authenticators := [#{ + config := AuthenticatorConfig1 + }] + }] + } + } = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:update_authenticator(ChainID, AuthenticatorName, AuthenticatorConfig1) of + {ok, NAuthenticator} -> + return({ok, NAuthenticator}); + {error, Reason} -> + return(serialize_error(Reason)) + end; + {error, Reason} -> + return(serialize_error(Reason)) + end; + {error, Reason} -> + return(serialize_error(Reason)) + end. + +lookup_authenticator(Binding, Params) -> + do_lookup_authenticator(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, _Params) -> + case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of + {ok, Authenticator} -> + return({ok, Authenticator}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +list_authenticators(Binding, Params) -> + do_list_authenticators(uri_decode(Binding), maps:from_list(Params)). + +do_list_authenticators(#{id := ChainID}, _Params) -> + case emqx_authn:list_authenticators(ChainID) of + {ok, Authenticators} -> + return({ok, Authenticators}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +move_authenticator(Binding, Params) -> + do_move_authenticator(uri_decode(Binding), maps:from_list(Params)). + +do_move_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the front">>}) -> + case emqx_authn:move_authenticator_to_the_front(ChainID, AuthenticatorName) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the end">>}) -> + case emqx_authn:move_authenticator_to_the_end(ChainID, AuthenticatorName) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, #{<<"position">> := N}) when is_number(N) -> + case emqx_authn:move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_authenticator(_Binding, _Params) -> + return(serialize_error({missing_parameter, <<"position">>})). + +import_users(Binding, Params) -> + do_import_users(uri_decode(Binding), maps:from_list(Params)). + +do_import_users(#{id := ChainID, authenticator_name := AuthenticatorName}, + #{<<"filename">> := Filename}) -> + case emqx_authn:import_users(ChainID, AuthenticatorName, Filename) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_import_users(_Binding, Params) -> + Missed = get_missed_params(Params, [<<"filename">>, <<"file_format">>]), + return(serialize_error({missing_parameter, Missed})). + +add_user(Binding, Params) -> + do_add_user(uri_decode(Binding), maps:from_list(Params)). + +do_add_user(#{id := ChainID, + authenticator_name := AuthenticatorName}, UserInfo) -> + case emqx_authn:add_user(ChainID, AuthenticatorName, UserInfo) of + {ok, User} -> + return({ok, User}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +delete_user(Binding, Params) -> + do_delete_user(uri_decode(Binding), maps:from_list(Params)). + +do_delete_user(#{id := ChainID, + authenticator_name := AuthenticatorName, + user_id := UserID}, _Params) -> + case emqx_authn:delete_user(ChainID, AuthenticatorName, UserID) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +update_user(Binding, Params) -> + do_update_user(uri_decode(Binding), maps:from_list(Params)). + +do_update_user(#{id := ChainID, + authenticator_name := AuthenticatorName, + user_id := UserID}, NewUserInfo) -> + case emqx_authn:update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) of + {ok, User} -> + return({ok, User}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +lookup_user(Binding, Params) -> + do_lookup_user(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_user(#{id := ChainID, + authenticator_name := AuthenticatorName, + user_id := UserID}, _Params) -> + case emqx_authn:lookup_user(ChainID, AuthenticatorName, UserID) of + {ok, User} -> + return({ok, User}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +list_users(Binding, Params) -> + do_list_users(uri_decode(Binding), maps:from_list(Params)). + +do_list_users(#{id := ChainID, + authenticator_name := AuthenticatorName}, _Params) -> + case emqx_authn:list_users(ChainID, AuthenticatorName) of + {ok, Users} -> + return({ok, Users}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +uri_decode(Params) -> + maps:fold(fun(K, V, Acc) -> + Acc#{K => emqx_http_lib:uri_decode(V)} + end, #{}, Params). + +lists_to_map(L) -> + lists_to_map(L, #{}). + +lists_to_map([], Acc) -> + Acc; +lists_to_map([{K, V} | More], Acc) when is_list(V) -> + NV = lists_to_map(V), + lists_to_map(More, Acc#{K => NV}); +lists_to_map([{K, V} | More], Acc) -> + lists_to_map(More, Acc#{K => V}); +lists_to_map([_ | _] = L, _) -> + L. + +serialize_error({already_exists, {Type, ID}}) -> + {error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~s '~s' already exists", [serialize_type(Type), ID]))}; +serialize_error({not_found, {Type, ID}}) -> + {error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~s '~s' not found", [serialize_type(Type), ID]))}; +serialize_error({duplicate, Name}) -> + {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Authenticator name '~s' is duplicated", [Name]))}; +serialize_error({missing_parameter, Names = [_ | Rest]}) -> + Format = ["~s," || _ <- Rest] ++ ["~s"], + NFormat = binary_to_list(iolist_to_binary(Format)), + {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names))}; +serialize_error({missing_parameter, Name}) -> + {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter '~s' that is mandatory for processing this request is not supplied.", [Name]))}; +serialize_error(_) -> + {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. + +serialize_type(authenticator) -> + "Authenticator"; +serialize_type(chain) -> + "Chain"; +serialize_type(authenticator_type) -> + "Authenticator type". + +get_missed_params(Actual, Expected) -> + Keys = lists:foldl(fun(Key, Acc) -> + case maps:is_key(Key, Actual) of + true -> Acc; + false -> [Key | Acc] + end + end, [], Expected), + lists:reverse(Keys). diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl new file mode 100644 index 000000000..033c760af --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -0,0 +1,77 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_app). + +-include("emqx_authn.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-behaviour(application). + +-emqx_plugin(?MODULE). + +%% Application callbacks +-export([ start/2 + , stop/1 + ]). + +start(_StartType, _StartArgs) -> + {ok, Sup} = emqx_authn_sup:start_link(), + ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), + initialize(), + {ok, Sup}. + +stop(_State) -> + ok. + +initialize() -> + #{chains := Chains, + bindings := Bindings} = emqx_config:get([authn], #{chains => [], bindings => []}), + initialize_chains(Chains), + initialize_bindings(Bindings). + +initialize_chains([]) -> + ok; +initialize_chains([#{id := ChainID, + type := Type, + authenticators := Authenticators} | More]) -> + case emqx_authn:create_chain(#{id => ChainID, + type => Type}) of + {ok, _} -> + initialize_authenticators(ChainID, Authenticators), + initialize_chains(More); + {error, Reason} -> + ?LOG(error, "Failed to create chain '~s': ~p", [ChainID, Reason]) + end. + +initialize_authenticators(_ChainID, []) -> + ok; +initialize_authenticators(ChainID, [#{name := Name} = Authenticator | More]) -> + case emqx_authn:create_authenticator(ChainID, Authenticator) of + {ok, _} -> + initialize_authenticators(ChainID, More); + {error, Reason} -> + ?LOG(error, "Failed to create authenticator '~s' in chain '~s': ~p", [Name, ChainID, Reason]) + end. + +initialize_bindings([]) -> + ok; +initialize_bindings([#{chain_id := ChainID, listeners := Listeners} | More]) -> + case emqx_authn:bind(Listeners, ChainID) of + ok -> initialize_bindings(More); + {error, Reason} -> + ?LOG(error, "Failed to bind: ~p", [Reason]) + end. diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl new file mode 100644 index 000000000..d9bf72910 --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -0,0 +1,114 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_schema). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([structs/0, fields/1]). + +-reflect_type([ chain_id/0 + , authenticator_name/0 + ]). + +structs() -> ["emqx_authn"]. + +fields("emqx_authn") -> + [ {chains, fun chains/1} + , {bindings, fun bindings/1}]; + +fields('simple-chain') -> + [ {id, fun chain_id/1} + , {type, {enum, [simple]}} + , {authenticators, fun simple_authenticators/1} + ]; + +% fields('enhanced-chain') -> +% [ {id, fun chain_id/1} +% , {type, {enum, [enhanced]}} +% , {authenticators, fun enhanced_authenticators/1} +% ]; + +fields(binding) -> + [ {chain_id, fun chain_id/1} + , {listeners, fun listeners/1} + ]; + +fields('built-in-database') -> + [ {name, fun authenticator_name/1} + , {type, {enum, ['built-in-database']}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_mnesia, config))} + ]; + +% fields('enhanced-built-in-database') -> +% [ {name, fun authenticator_name/1} +% , {type, {enum, ['built-in-database']}} +% , {config, hoconsc:t(hoconsc:ref(emqx_enhanced_authn_mnesia, config))} +% ]; + +fields(jwt) -> + [ {name, fun authenticator_name/1} + , {type, {enum, [jwt]}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_jwt, config))} + ]; + +fields(mysql) -> + [ {name, fun authenticator_name/1} + , {type, {enum, [mysql]}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_mysql, config))} + ]; + +fields(pgsql) -> + [ {name, fun authenticator_name/1} + , {type, {enum, [postgresql]}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_pgsql, config))} + ]. + +chains(type) -> hoconsc:array({union, [hoconsc:ref(?MODULE, 'simple-chain')]}); +chains(default) -> []; +chains(_) -> undefined. + +chain_id(type) -> chain_id(); +chain_id(nullable) -> false; +chain_id(_) -> undefined. + +simple_authenticators(type) -> + hoconsc:array({union, [ hoconsc:ref(?MODULE, 'built-in-database') + , hoconsc:ref(?MODULE, jwt) + , hoconsc:ref(?MODULE, mysql) + , hoconsc:ref(?MODULE, pgsql)]}); +simple_authenticators(default) -> []; +simple_authenticators(_) -> undefined. + +% enhanced_authenticators(type) -> +% hoconsc:array({union, [hoconsc:ref('enhanced-built-in-database')]}); +% enhanced_authenticators(default) -> []; +% enhanced_authenticators(_) -> undefined. + +authenticator_name(type) -> authenticator_name(); +authenticator_name(nullable) -> false; +authenticator_name(_) -> undefined. + +bindings(type) -> hoconsc:array(hoconsc:ref(?MODULE, binding)); +bindings(default) -> []; +bindings(_) -> undefined. + +listeners(type) -> hoconsc:array(binary()); +listeners(default) -> []; +listeners(_) -> undefined. diff --git a/apps/emqx_authentication/src/emqx_authentication_sup.erl b/apps/emqx_authn/src/emqx_authn_sup.erl similarity index 96% rename from apps/emqx_authentication/src/emqx_authentication_sup.erl rename to apps/emqx_authn/src/emqx_authn_sup.erl index 06e12ce6c..bb26af0ad 100644 --- a/apps/emqx_authentication/src/emqx_authentication_sup.erl +++ b/apps/emqx_authn/src/emqx_authn_sup.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_sup). +-module(emqx_authn_sup). -behaviour(supervisor). diff --git a/apps/emqx_authn/src/emqx_authn_utils.erl b/apps/emqx_authn/src/emqx_authn_utils.erl new file mode 100644 index 000000000..98e27e76c --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_utils.erl @@ -0,0 +1,55 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_utils). + +-export([ replace_placeholder/2 + ]). + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +replace_placeholder(PlaceHolders, Data) -> + replace_placeholder(PlaceHolders, Data, []). + +replace_placeholder([], _Data, Acc) -> + lists:reverse(Acc); +replace_placeholder([<<"${mqtt-username}">> | More], #{username := Username} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(Username) | Acc]); +replace_placeholder([<<"${mqtt-clientid}">> | More], #{clientid := ClientID} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(ClientID) | Acc]); +replace_placeholder([<<"${ip-address}">> | More], #{peerhost := IPAddress} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(IPAddress) | Acc]); +replace_placeholder([<<"${cert-subject}">> | More], #{dn := Subject} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(Subject) | Acc]); +replace_placeholder([<<"${cert-common-name}">> | More], #{cn := CommonName} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(CommonName) | Acc]); +replace_placeholder([_ | More], Data, Acc) -> + replace_placeholder(More, Data, [null | Acc]). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +convert_to_sql_param(undefined) -> + null; +convert_to_sql_param(V) -> + bin(V). + +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(L) when is_list(L) -> list_to_binary(L); +bin(X) -> X. diff --git a/apps/emqx_sasl/include/emqx_sasl.hrl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl similarity index 83% rename from apps/emqx_sasl/include/emqx_sasl.hrl rename to apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl index a1658f2b8..207e93495 100644 --- a/apps/emqx_sasl/include/emqx_sasl.hrl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2021 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. @@ -14,6 +14,4 @@ %% limitations under the License. %%-------------------------------------------------------------------- --define(APP, emqx_sasl). - --define(SCRAM_AUTH_TAB, scram_auth). \ No newline at end of file +-module(emqx_enhanced_authn_mnesia). diff --git a/apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl similarity index 87% rename from apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl rename to apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl index 9dafc9f5e..95e4b3d6d 100644 --- a/apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_jwks_connector). +-module(emqx_authn_jwks_connector). -behaviour(gen_server). @@ -125,23 +125,17 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -handle_options(Opts) -> - #{endpoint => proplists:get_value(jwks_endpoint, Opts), - refresh_interval => limit_refresh_interval(proplists:get_value(refresh_interval, Opts)), - ssl_opts => get_ssl_opts(Opts), +handle_options(#{endpoint := Endpoint, + refresh_interval := RefreshInterval0, + ssl_opts := SSLOpts}) -> + #{endpoint => Endpoint, + refresh_interval => limit_refresh_interval(RefreshInterval0), + ssl_opts => maps:to_list(SSLOpts), jwks => [], - request_id => undefined}. + request_id => undefined}; -get_ssl_opts(Opts) -> - case proplists:get_value(enable_ssl, Opts) of - false -> []; - true -> - maps:to_list(maps:with([cacertfile, - keyfile, - certfile, - verify, - server_name_indication], maps:from_list(Opts))) - end. +handle_options(#{enable_ssl := false} = Opts) -> + handle_options(Opts#{ssl_opts => #{}}). refresh_jwks(#{endpoint := Endpoint, ssl_opts := SSLOpts} = State) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl new file mode 100644 index 000000000..f737d5168 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -0,0 +1,343 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_jwt). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1 + , validations/0 + ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [config]. + +fields("") -> + [{config, {union, [ hoconsc:t('hmac-based') + , hoconsc:t('public-key') + , hoconsc:t('jwks') + , hoconsc:t('jwks-using-ssl') + ]}}]; + +fields(config) -> + [{union, [ hoconsc:t('hmac-based') + , hoconsc:t('public-key') + , hoconsc:t('jwks') + , hoconsc:t('jwks-using-ssl') + ]}]; + +fields('hmac-based') -> + [ {use_jwks, {enum, [false]}} + , {algorithm, {enum, ['hmac-based']}} + , {secret, fun secret/1} + , {secret_base64_encoded, fun secret_base64_encoded/1} + , {verify_claims, fun verify_claims/1} + ]; + +fields('public-key') -> + [ {use_jwks, {enum, [false]}} + , {algorithm, {enum, ['public-key']}} + , {certificate, fun certificate/1} + , {verify_claims, fun verify_claims/1} + ]; + +fields('jwks') -> + [ {enable_ssl, {enum, [false]}} + ] ++ jwks_fields(); + +fields('jwks-using-ssl') -> + [ {enable_ssl, {enum, [true]}} + , {ssl_opts, fun ssl_opts/1} + ] ++ jwks_fields(); + +fields(ssl_opts) -> + [ {cacertfile, fun cacertfile/1} + , {certfile, fun certfile/1} + , {keyfile, fun keyfile/1} + , {verify, fun verify/1} + , {server_name_indication, fun server_name_indication/1} + ]; + +fields(claim) -> + [ {"$name", fun expected_claim_value/1} ]. + +validations() -> + [ {check_verify_claims, fun check_verify_claims/1} ]. + +jwks_fields() -> + [ {use_jwks, {enum, [true]}} + , {endpoint, fun endpoint/1} + , {refresh_interval, fun refresh_interval/1} + , {verify_claims, fun verify_claims/1} + ]. + +secret(type) -> string(); +secret(_) -> undefined. + +secret_base64_encoded(type) -> boolean(); +secret_base64_encoded(defualt) -> false; +secret_base64_encoded(_) -> undefined. + +certificate(type) -> string(); +certificate(_) -> undefined. + +endpoint(type) -> string(); +endpoint(_) -> undefined. + +ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts)); +ssl_opts(default) -> []; +ssl_opts(_) -> undefined. + +refresh_interval(type) -> integer(); +refresh_interval(default) -> 300; +refresh_interval(validator) -> [fun(I) -> I > 0 end]; +refresh_interval(_) -> undefined. + +cacertfile(type) -> string(); +cacertfile(_) -> undefined. + +certfile(type) -> string(); +certfile(_) -> undefined. + +keyfile(type) -> string(); +keyfile(_) -> undefined. + +verify(type) -> boolean(); +verify(default) -> false; +verify(_) -> undefined. + +server_name_indication(type) -> string(); +server_name_indication(_) -> undefined. + +verify_claims(type) -> hoconsc:array(hoconsc:ref(claim)); +verify_claims(default) -> []; +verify_claims(_) -> undefined. + +expected_claim_value(type) -> string(); +expected_claim_value(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(_ChainID, _AuthenticatorName, Config) -> + create(Config). + +update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, #{jwk := Connector}) + when is_pid(Connector) -> + _ = emqx_authn_jwks_connector:stop(Connector), + create(Config); + +update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, _) -> + create(Config); + +update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Connector} = State) + when is_pid(Connector) -> + ok = emqx_authn_jwks_connector:update(Connector, Config), + case maps:get(verify_cliams, Config, undefined) of + undefined -> + {ok, State}; + VerifyClaims -> + {ok, State#{verify_claims => handle_verify_claims(VerifyClaims)}} + end; + +update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, _) -> + create(Config). + +authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, + verify_claims := VerifyClaims0}) -> + JWKs = case erlang:is_pid(JWK) of + false -> + [JWK]; + true -> + {ok, JWKs0} = emqx_authn_jwks_connector:get_jwks(JWK), + JWKs0 + end, + VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo), + case verify(JWT, JWKs, VerifyClaims) of + ok -> ok; + {error, invalid_signature} -> ignore; + {error, {claims, _}} -> {stop, bad_password} + end. + +destroy(#{jwk := Connector}) when is_pid(Connector) -> + _ = emqx_authn_jwks_connector:stop(Connector), + ok; +destroy(_) -> + ok. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +create(#{verify_claims := VerifyClaims} = Config) -> + create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}). + +create2(#{use_jwks := false, + algorithm := 'hmac-based', + secret := Secret0, + secret_base64_encoded := Base64Encoded, + verify_claims := VerifyClaims}) -> + Secret = case Base64Encoded of + true -> + base64:decode(Secret0); + false -> + Secret0 + end, + JWK = jose_jwk:from_oct(Secret), + {ok, #{jwk => JWK, + verify_claims => VerifyClaims}}; + +create2(#{use_jwks := false, + algorithm := 'public-key', + certificate := Certificate, + verify_claims := VerifyClaims}) -> + JWK = jose_jwk:from_pem_file(Certificate), + {ok, #{jwk => JWK, + verify_claims => VerifyClaims}}; + +create2(#{use_jwks := true, + verify_claims := VerifyClaims} = Config) -> + case emqx_authn_jwks_connector:start_link(Config) of + {ok, Connector} -> + {ok, #{jwk => Connector, + verify_claims => VerifyClaims}}; + {error, Reason} -> + {error, Reason} + end. + +replace_placeholder(L, Variables) -> + replace_placeholder(L, Variables, []). + +replace_placeholder([], _Variables, Acc) -> + Acc; +replace_placeholder([{Name, {placeholder, PL}} | More], Variables, Acc) -> + Value = maps:get(PL, Variables), + replace_placeholder(More, Variables, [{Name, Value} | Acc]); +replace_placeholder([{Name, Value} | More], Variables, Acc) -> + replace_placeholder(More, Variables, [{Name, Value} | Acc]). + +verify(_JWS, [], _VerifyClaims) -> + {error, invalid_signature}; +verify(JWS, [JWK | More], VerifyClaims) -> + try jose_jws:verify(JWK, JWS) of + {true, Payload, _JWS} -> + Claims = emqx_json:decode(Payload, [return_maps]), + verify_claims(Claims, VerifyClaims); + {false, _, _} -> + verify(JWS, More, VerifyClaims) + catch + _:_Reason:_Stacktrace -> + %% TODO: Add log + {error, invalid_signature} + end. + +verify_claims(Claims, VerifyClaims0) -> + Now = os:system_time(seconds), + VerifyClaims = [{<<"exp">>, fun(ExpireTime) -> + Now < ExpireTime + end}, + {<<"iat">>, fun(IssueAt) -> + IssueAt =< Now + end}, + {<<"nbf">>, fun(NotBefore) -> + NotBefore =< Now + end}] ++ VerifyClaims0, + do_verify_claims(Claims, VerifyClaims). + +do_verify_claims(_Claims, []) -> + ok; +do_verify_claims(Claims, [{Name, Fun} | More]) when is_function(Fun) -> + case maps:take(Name, Claims) of + error -> + do_verify_claims(Claims, More); + {Value, NClaims} -> + case Fun(Value) of + true -> + do_verify_claims(NClaims, More); + _ -> + {error, {claims, {Name, Value}}} + end + end; +do_verify_claims(Claims, [{Name, Value} | More]) -> + case maps:take(Name, Claims) of + error -> + {error, {missing_claim, Name}}; + {Value, NClaims} -> + do_verify_claims(NClaims, More); + {Value0, _} -> + {error, {claims, {Name, Value0}}} + end. + +check_verify_claims([]) -> + false; +check_verify_claims([{Name, Expected} | More]) -> + check_claim_name(Name) andalso + check_claim_expected(Expected) andalso + check_verify_claims(More). + +check_claim_name(exp) -> + false; +check_claim_name(iat) -> + false; +check_claim_name(nbf) -> + false; +check_claim_name(_) -> + true. + +check_claim_expected(Expected) -> + try handle_placeholder(Expected) of + _ -> true + catch + _:_ -> + false + end. + +handle_verify_claims(VerifyClaims) -> + handle_verify_claims(VerifyClaims, []). + +handle_verify_claims([], Acc) -> + Acc; +handle_verify_claims([{Name, Expected0} | More], Acc) -> + Expected = handle_placeholder(Expected0), + handle_verify_claims(More, [{Name, Expected} | Acc]). + +handle_placeholder(Placeholder0) -> + case re:run(Placeholder0, "^\\$\\{[a-z0-9\\-]+\\}$", [{capture, all}]) of + {match, [{Offset, Length}]} -> + Placeholder1 = binary:part(Placeholder0, Offset + 2, Length - 3), + Placeholder2 = validate_placeholder(Placeholder1), + {placeholder, Placeholder2}; + nomatch -> + Placeholder0 + end. + +validate_placeholder(<<"mqtt-clientid">>) -> + clientid; +validate_placeholder(<<"mqtt-username">>) -> + username. diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl similarity index 79% rename from apps/emqx_authentication/src/emqx_authentication_mnesia.erl rename to apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index 53dc4dd73..26b20c517 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -14,9 +14,14 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_mnesia). +-module(emqx_authn_mnesia). --include("emqx_authentication.hrl"). +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0, fields/1 ]). -export([ create/3 , update/4 @@ -32,29 +37,10 @@ , list_users/1 ]). -%% TODO: support bcrypt --service_type(#{ - name => mnesia, - params_spec => #{ - user_id_type => #{ - order => 1, - type => string, - enum => [<<"username">>, <<"clientid">>, <<"ip">>, <<"common name">>, <<"issuer">>], - default => <<"username">> - }, - password_hash_algorithm => #{ - order => 2, - type => string, - enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], - default => <<"sha256">> - }, - salt_rounds => #{ - order => 3, - type => number, - default => 10 - } - } -}). +-type user_id_type() :: clientid | username. + +-type user_group() :: {chain_id(), authenticator_name()}. +-type user_id() :: binary(). -record(user_info, { user_id :: {user_group(), user_id()} @@ -62,8 +48,7 @@ , salt :: binary() }). --type(user_group() :: {chain_id(), service_name()}). --type(user_id() :: binary()). +-reflect_type([ user_id_type/0 ]). -export([mnesia/1]). @@ -72,6 +57,7 @@ -define(TAB, mnesia_basic_auth). +-rlog_shard({?AUTH_SHARD, ?TAB}). %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -88,18 +74,61 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB, disc_copies). -create(ChainID, ServiceName, #{<<"user_id_type">> := Type, - <<"password_hash_algorithm">> := Algorithm, - <<"salt_rounds">> := SaltRounds}) -> - Algorithm =:= <<"bcrypt">> andalso ({ok, _} = application:ensure_all_started(bcrypt)), - State = #{user_group => {ChainID, ServiceName}, - user_id_type => binary_to_atom(Type, utf8), - password_hash_algorithm => binary_to_atom(Algorithm, utf8), +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [config]. + +fields(config) -> + [ {user_id_type, fun user_id_type/1} + , {password_hash_algorithm, fun password_hash_algorithm/1} + ]; + +fields(bcrypt) -> + [ {name, {enum, [bcrypt]}} + , {salt_rounds, fun salt_rounds/1} + ]; + +fields(other_algorithms) -> + [ {name, {enum, [plain, md5, sha, sha256, sha512]}} + ]. + +user_id_type(type) -> user_id_type(); +user_id_type(default) -> clientid; +user_id_type(_) -> undefined. + +password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]}; +password_hash_algorithm(default) -> sha256; +password_hash_algorithm(_) -> undefined. + +salt_rounds(type) -> integer(); +salt_rounds(default) -> 10; +salt_rounds(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, AuthenticatorName, #{user_id_type := Type, + password_hash_algorithm := #{name := bcrypt, + salt_rounds := SaltRounds}}) -> + {ok, _} = application:ensure_all_started(bcrypt), + State = #{user_group => {ChainID, AuthenticatorName}, + user_id_type => Type, + password_hash_algorithm => bcrypt, salt_rounds => SaltRounds}, + {ok, State}; + +create(ChainID, AuthenticatorName, #{user_id_type := Type, + password_hash_algorithm := #{name := Name}}) -> + State = #{user_group => {ChainID, AuthenticatorName}, + user_id_type => Type, + password_hash_algorithm => Name}, {ok, State}. -update(ChainID, ServiceName, Params, _State) -> - create(ChainID, ServiceName, Params). +update(ChainID, AuthenticatorName, Config, _State) -> + create(ChainID, AuthenticatorName, Config). authenticate(ClientInfo = #{password := Password}, #{user_group := UserGroup, @@ -109,7 +138,11 @@ authenticate(ClientInfo = #{password := Password}, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0}] -> + Salt = case Algorithm of + bcrypt -> PasswordHash; + _ -> Salt0 + end, case PasswordHash =:= hash(Algorithm, Password, Salt) of true -> ok; false -> {stop, bad_password} @@ -231,7 +264,7 @@ import(UserGroup, [#{<<"user_id">> := UserID, import(_UserGroup, [_ | _More]) -> {error, bad_format}. -%% Importing 5w users needs 1.7 seconds +%% Importing 5w users needs 1.7 seconds import(UserGroup, File, Seq) -> case file:read_line(File) of {ok, Line} -> @@ -330,7 +363,7 @@ trans(Fun) -> trans(Fun, []). trans(Fun, Args) -> - case mnesia:transaction(Fun, Args) of + case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of {atomic, Res} -> Res; {aborted, Reason} -> {error, Reason} end. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl new file mode 100644 index 000000000..3b5384d9c --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -0,0 +1,140 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_mysql). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0, fields/1 ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [config]. + +fields(config) -> + [ {password_hash_algorithm, fun password_hash_algorithm/1} + , {salt_position, {enum, [prefix, suffix]}} + , {query, fun query/1} + , {query_timeout, fun query_timeout/1} + ] ++ emqx_connector_schema_lib:relational_db_fields() + ++ emqx_connector_schema_lib:ssl_fields(). + +password_hash_algorithm(type) -> string(); +password_hash_algorithm(_) -> undefined. + +query(type) -> string(); +query(nullable) -> false; +query(_) -> undefined. + +query_timeout(type) -> integer(); +query_timeout(defualt) -> 5000; +query_timeout(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, ServiceName, #{query := Query0, + password_hash_algorithm := Algorithm} = Config) -> + {Query, PlaceHolders} = parse_query(Query0), + ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])), + State = #{query => Query, + placeholders => PlaceHolders, + password_hash_algorithm => Algorithm}, + case emqx_resource:create_local(ResourceID, emqx_connector_mysql, Config) of + {ok, _} -> + {ok, State#{resource_id => ResourceID}}; + {error, already_created} -> + {ok, State#{resource_id => ResourceID}}; + {error, Reason} -> + {error, Reason} + end. + +update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> + case emqx_resource:update_local(ResourceID, emqx_connector_mysql, Config, []) of + {ok, _} -> {ok, State}; + {error, Reason} -> {error, Reason} + end. + +authenticate(#{password := Password} = ClientInfo, + #{resource_id := ResourceID, + placeholders := PlaceHolders, + query := Query, + query_timeout := Timeout} = State) -> + Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo), + case emqx_resource:query(ResourceID, {sql, Query, Params, Timeout}) of + {ok, _Columns, []} -> ignore; + {ok, Columns, Rows} -> + %% TODO: Support superuser + Selected = maps:from_list(lists:zip(Columns, Rows)), + check_password(Password, Selected, State); + {error, _Reason} -> + ignore + end. + +destroy(#{resource_id := ResourceID}) -> + _ = emqx_resource:remove_local(ResourceID), + ok. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +check_password(undefined, _Algorithm, _Selected) -> + {stop, bad_password}; +check_password(Password, + #{password_hash := Hash}, + #{password_hash_algorithm := bcrypt}) -> + {ok, Hash0} = bcrypt:hashpw(Password, Hash), + case list_to_binary(Hash0) =:= Hash of + true -> ok; + false -> {stop, bad_password} + end; +check_password(Password, + #{password_hash := Hash} = Selected, + #{password_hash_algorithm := Algorithm, + salt_position := SaltPosition}) -> + Salt = maps:get(salt, Selected, <<>>), + Hash0 = case SaltPosition of + prefix -> emqx_passwd:hash(Algorithm, <>); + suffix -> emqx_passwd:hash(Algorithm, <>) + end, + case Hash0 =:= Hash of + true -> ok; + false -> {stop, bad_password} + end. + +%% TODO: Support prepare +parse_query(Query) -> + case re:run(Query, "\\$\\{[a-z0-9\\_]+\\}", [global, {capture, all, binary}]) of + {match, Captured} -> + PlaceHolders = [PlaceHolder || PlaceHolder <- Captured], + NQuery = re:replace(Query, "'\\$\\{[a-z0-9\\_]+\\}'", "?", [global, {return, binary}]), + {NQuery, PlaceHolders}; + nomatch -> + {Query, []} + end. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl new file mode 100644 index 000000000..c9046c606 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -0,0 +1,137 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_pgsql). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0, fields/1 ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [config]. + +fields(config) -> + [ {password_hash_algorithm, fun password_hash_algorithm/1} + , {salt_position, {enum, [prefix, suffix]}} + , {query, fun query/1} + ] ++ emqx_connector_schema_lib:relational_db_fields() + ++ emqx_connector_schema_lib:ssl_fields(). + +password_hash_algorithm(type) -> string(); +password_hash_algorithm(_) -> undefined. + +query(type) -> string(); +query(nullable) -> false; +query(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, ServiceName, #{query := Query0, + password_hash_algorithm := Algorithm} = Config) -> + {Query, PlaceHolders} = parse_query(Query0), + ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])), + State = #{query => Query, + placeholders => PlaceHolders, + password_hash_algorithm => Algorithm}, + case emqx_resource:create_local(ResourceID, emqx_connector_pgsql, Config) of + {ok, _} -> + {ok, State#{resource_id => ResourceID}}; + {error, already_created} -> + {ok, State#{resource_id => ResourceID}}; + {error, Reason} -> + {error, Reason} + end. + +update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> + case emqx_resource:update_local(ResourceID, emqx_connector_pgsql, Config, []) of + {ok, _} -> {ok, State}; + {error, Reason} -> {error, Reason} + end. + +authenticate(#{password := Password} = ClientInfo, + #{resource_id := ResourceID, + query := Query, + placeholders := PlaceHolders} = State) -> + Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo), + case emqx_resource:query(ResourceID, {sql, Query, Params}) of + {ok, _Columns, []} -> ignore; + {ok, Columns, Rows} -> + %% TODO: Support superuser + Selected = maps:from_list(lists:zip(Columns, Rows)), + check_password(Password, Selected, State); + {error, _Reason} -> + ignore + end. + +destroy(#{resource_id := ResourceID}) -> + _ = emqx_resource:remove_local(ResourceID), + ok. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +check_password(undefined, _Algorithm, _Selected) -> + {stop, bad_password}; +check_password(Password, + #{password_hash := Hash}, + #{password_hash_algorithm := bcrypt}) -> + {ok, Hash0} = bcrypt:hashpw(Password, Hash), + case list_to_binary(Hash0) =:= Hash of + true -> ok; + false -> {stop, bad_password} + end; +check_password(Password, + #{password_hash := Hash} = Selected, + #{password_hash_algorithm := Algorithm, + salt_position := SaltPosition}) -> + Salt = maps:get(salt, Selected, <<>>), + Hash0 = case SaltPosition of + prefix -> emqx_passwd:hash(Algorithm, <>); + suffix -> emqx_passwd:hash(Algorithm, <>) + end, + case Hash0 =:= Hash of + true -> ok; + false -> {stop, bad_password} + end. + +%% TODO: Support prepare +parse_query(Query) -> + case re:run(Query, "\\$\\{[a-z0-9\\_]+\\}", [global, {capture, all, binary}]) of + {match, Captured} -> + PlaceHolders = [PlaceHolder || PlaceHolder <- Captured], + Replacements = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Captured))], + NQuery = lists:foldl(fun({PlaceHolder, Replacement}, Query0) -> + re:replace(Query0, <<"'\\", PlaceHolder/binary, "'">>, Replacement, [{return, binary}]) + end, Query, lists:zip(PlaceHolders, Replacements)), + {NQuery, PlaceHolders}; + nomatch -> + {Query, []} + end. diff --git a/apps/emqx_authn/test/data/private_key.pem b/apps/emqx_authn/test/data/private_key.pem new file mode 100644 index 000000000..318eefe27 --- /dev/null +++ b/apps/emqx_authn/test/data/private_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgHA+aGk+D/0J9ZAj3tQIAvDTnRvZNF0IeaTmJcBooxsY6Ze8PGFS +QJ/+EJf1i1ffExeaIH99d3nwyBspNlihooGHvTVDeYsu15Htxpqig1L/+MphbZlF +ClDXtzV0+GeZGoqeloHAXku3Qzk+hMxROalFtH+8GNp+/j2yir1Z9E2xAgMBAAEC +gYBX7HsLncMWexux6nddbl0nWwyhyPZcvgvT4TjHTPAfhNdOtfQyZCUdbv5+mqip +j6O8BE7ar2TMz5FgvVrF+O97LkYHNmZk0q3xtZlCYXp4BQqD6Wq65H5U4fAomalK +xm7HsTCSVXx5CvnZK/JbkPw18QsgwrSHEFs+4Pf2noH+FQJBAL/bpPrkDOB476Iy +RGnuCckUN1pdCU+UINC8oOWGNwsG6EE5ywlIWRXHtp4VMksG6mCLNJwGUAv2zWIs +iEjZVfsCQQCVxOciTajTtYO5bPjkXoZoe4VKKXWMYv9AXXVCjq0ff/LjrnKJjbRm +aoKQGhzjKHk5rgd9+Ydl6FnJw5K4B9dDAkEAtaHfQpZ7ildzpf4ovpBYO0EkViwW +EHyPxI2PVTwHCC1126o3CYawr+nufSJcBqN5aAThvYRMa8cvEW5PZ4g52QJALF5L +tt7Yz/crEciVp1nVaaiGISVNHIzLX28QaOpJoVZPR2ILrnJbaifNjBEgU69O0maa +85fzo54E03/rvDcebwJAfTMgIyzFQK/ESnM43bUCI/Y5XAeKFBiN1YhCioNR4Hj7 +Lkw2RdrrPC9LV+gVJK0b7VUqR5odjdj7PN6SipuXNw== +-----END RSA PRIVATE KEY----- diff --git a/apps/emqx_authn/test/data/public_key.pem b/apps/emqx_authn/test/data/public_key.pem new file mode 100644 index 000000000..b0b151981 --- /dev/null +++ b/apps/emqx_authn/test/data/public_key.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHA+aGk+D/0J9ZAj3tQIAvDTnRvZ +NF0IeaTmJcBooxsY6Ze8PGFSQJ/+EJf1i1ffExeaIH99d3nwyBspNlihooGHvTVD +eYsu15Htxpqig1L/+MphbZlFClDXtzV0+GeZGoqeloHAXku3Qzk+hMxROalFtH+8 +GNp+/j2yir1Z9E2xAgMBAAE= +-----END PUBLIC KEY----- diff --git a/apps/emqx_authentication/test/data/user-credentials.csv b/apps/emqx_authn/test/data/user-credentials.csv similarity index 100% rename from apps/emqx_authentication/test/data/user-credentials.csv rename to apps/emqx_authn/test/data/user-credentials.csv diff --git a/apps/emqx_authentication/test/data/user-credentials.json b/apps/emqx_authn/test/data/user-credentials.json similarity index 100% rename from apps/emqx_authentication/test/data/user-credentials.json rename to apps/emqx_authn/test/data/user-credentials.json diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl new file mode 100644 index 000000000..17c08cc70 --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -0,0 +1,142 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(AUTH, emqx_authn). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + application:set_env(ekka, strict_mode, true), + emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')), + emqx_ct_helpers:stop_apps([emqx_authn]), + ok. + +set_special_configs(emqx_authn) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_authn, "test")), + Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + +t_chain(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(Chain)), + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), + ok. + +t_binding(_) -> + Listener1 = <<"listener1">>, + Listener2 = <<"listener2">>, + ChainID = <<"mychain">>, + + ?assertEqual({error, {not_found, {chain, ChainID}}}, ?AUTH:bind(ChainID, [Listener1])), + + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1])), + ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener2])), + ?assertEqual({error, {already_bound, [Listener1]}}, ?AUTH:bind(ChainID, [Listener1])), + {ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID), + ?assertEqual(2, length(Listeners)), + ?assertMatch({ok, #{simple := ChainID}}, ?AUTH:list_bound_chains(Listener1)), + + ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1])), + ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener2])), + ?assertEqual({error, {not_found, [Listener1]}}, ?AUTH:unbind(ChainID, [Listener1])), + + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_binding2(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + Listener1 = <<"listener1">>, + Listener2 = <<"listener2">>, + + ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1, Listener2])), + {ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID), + ?assertEqual(2, length(Listeners)), + ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1, Listener2])), + ?assertMatch({ok, #{listeners := []}}, ?AUTH:list_bindings(ChainID)), + + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), + + AuthenticatorName1 = <<"myauthenticator1">>, + AuthenticatorConfig1 = #{name => AuthenticatorName1, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName1)), + ?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), + + AuthenticatorName2 = <<"myauthenticator2">>, + AuthenticatorConfig2 = AuthenticatorConfig1#{name => AuthenticatorName2}, + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)), + ?assertMatch({ok, #{id := ChainID, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(ChainID)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)), + + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(ChainID, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 1)), + ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 3)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 0)), + ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName2)), + ?assertEqual({ok, []}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl new file mode 100644 index 000000000..27f34f936 --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -0,0 +1,182 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_jwt_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(AUTH, emqx_authn). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')), + emqx_ct_helpers:stop_apps([emqx_authn]), + ok. + +set_special_configs(emqx_authn) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_authn, "test")), + Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + +t_jwt_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName = <<"myauthenticator">>, + Config = #{use_jwks => false, + algorithm => 'hmac-based', + secret => <<"abcdef">>, + secret_base64_encoded => false, + verify_claims => []}, + AuthenticatorConfig = #{name => AuthenticatorName, + type => jwt, + config => Config}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + Payload = #{<<"username">> => <<"myuser">>}, + JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), + ClientInfo = #{listener_id => ListenerID, + username => <<"myuser">>, + password => JWS}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + + BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), + ClientInfo2 = ClientInfo#{password => BadJWS}, + ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + + %% secret_base64_encoded + Config2 = Config#{secret => base64:encode(<<"abcdef">>), + secret_base64_encoded => true}, + ?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config2)), + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + + Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, + ?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config3)), + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>})), + + %% Expiration + Payload3 = #{ <<"username">> => <<"myuser">> + , <<"exp">> => erlang:system_time(second) - 60}, + JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), + ClientInfo3 = ClientInfo#{password => JWS3}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + + Payload4 = #{ <<"username">> => <<"myuser">> + , <<"exp">> => erlang:system_time(second) + 60}, + JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), + ClientInfo4 = ClientInfo#{password => JWS4}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), + + %% Issued At + Payload5 = #{ <<"username">> => <<"myuser">> + , <<"iat">> => erlang:system_time(second) - 60}, + JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), + ClientInfo5 = ClientInfo#{password => JWS5}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo5)), + + Payload6 = #{ <<"username">> => <<"myuser">> + , <<"iat">> => erlang:system_time(second) + 60}, + JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), + ClientInfo6 = ClientInfo#{password => JWS6}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo6)), + + %% Not Before + Payload7 = #{ <<"username">> => <<"myuser">> + , <<"nbf">> => erlang:system_time(second) - 60}, + JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), + ClientInfo7 = ClientInfo#{password => JWS7}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo7)), + + Payload8 = #{ <<"username">> => <<"myuser">> + , <<"nbf">> => erlang:system_time(second) + 60}, + JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), + ClientInfo8 = ClientInfo#{password => JWS8}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo8)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_jwt_authenticator2(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + Dir = code:lib_dir(emqx_authn, test), + PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), + PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), + AuthenticatorName = <<"myauthenticator">>, + Config = #{use_jwks => false, + algorithm => 'public-key', + certificate => PublicKey, + verify_claims => []}, + AuthenticatorConfig = #{name => AuthenticatorName, + type => jwt, + config => Config}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + Payload = #{<<"username">> => <<"myuser">>}, + JWS = generate_jws('public-key', Payload, PrivateKey), + ClientInfo = #{listener_id => ListenerID, + username => <<"myuser">>, + password => JWS}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>})), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +generate_jws('hmac-based', Payload, Secret) -> + JWK = jose_jwk:from_oct(Secret), + Header = #{ <<"alg">> => <<"HS256">> + , <<"typ">> => <<"JWT">> + }, + Signed = jose_jwt:sign(JWK, Header, Payload), + {_, JWS} = jose_jws:compact(Signed), + JWS; +generate_jws('public-key', Payload, PrivateKey) -> + JWK = jose_jwk:from_pem_file(PrivateKey), + Header = #{ <<"alg">> => <<"RS256">> + , <<"typ">> => <<"JWT">> + }, + Signed = jose_jwt:sign(JWK, Header, Payload), + {_, JWS} = jose_jws:compact(Signed), + JWS. diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl new file mode 100644 index 000000000..abc7ad149 --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -0,0 +1,187 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_mnesia_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(AUTH, emqx_authn). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')), + emqx_ct_helpers:stop_apps([emqx_authn]), + ok. + +set_special_configs(emqx_authn) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_authn, "test")), + Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + +t_mnesia_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName = <<"myauthenticator">>, + AuthenticatorConfig = #{name => AuthenticatorName, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + UserInfo = #{<<"user_id">> => <<"myuser">>, + <<"password">> => <<"mypass">>}, + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + ClientInfo = #{listener_id => ListenerID, + username => <<"myuser">>, + password => <<"mypass">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ClientInfo2 = ClientInfo#{username => <<"baduser">>}, + ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + ClientInfo3 = ClientInfo#{password => <<"badpass">>}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + + UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, AuthenticatorName, <<"myuser">>, UserInfo2)), + ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), + + ?assertEqual(ok, ?AUTH:delete_user(ChainID, AuthenticatorName, <<"myuser">>)), + ?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName)), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + ?assertMatch({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual([], ets:tab2list(mnesia_basic_auth)), + ok. + +t_import(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName = <<"myauthenticator">>, + AuthenticatorConfig = #{name => AuthenticatorName, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + Dir = code:lib_dir(emqx_authn, test), + ?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))), + ?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))), + ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser1">>)), + ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser3">>)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + ClientInfo1 = #{listener_id => ListenerID, + username => <<"myuser1">>, + password => <<"mypassword1">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), + ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, + password => <<"mypassword3">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_multi_mnesia_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName1 = <<"myauthenticator1">>, + AuthenticatorConfig1 = #{name => AuthenticatorName1, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + AuthenticatorName2 = <<"myauthenticator2">>, + AuthenticatorConfig2 = #{name => AuthenticatorName2, + type => 'built-in-database', + config => #{ + user_id_type => clientid, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)), + + ?assertEqual({ok, #{user_id => <<"myuser">>}}, + ?AUTH:add_user(ChainID, AuthenticatorName1, + #{<<"user_id">> => <<"myuser">>, + <<"password">> => <<"mypass1">>})), + ?assertEqual({ok, #{user_id => <<"myclient">>}}, + ?AUTH:add_user(ChainID, AuthenticatorName2, + #{<<"user_id">> => <<"myclient">>, + <<"password">> => <<"mypass2">>})), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + ClientInfo1 = #{listener_id => ListenerID, + username => <<"myuser">>, + clientid => <<"myclient">>, + password => <<"mypass1">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)), + + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)), + ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. diff --git a/apps/emqx_authz/README.md b/apps/emqx_authz/README.md index 0fddac9b0..a8b4ca170 100644 --- a/apps/emqx_authz/README.md +++ b/apps/emqx_authz/README.md @@ -16,7 +16,12 @@ authz:{ username: root password: public auto_reconnect: true - ssl: false + ssl: { + enable: true + cacertfile: "etc/certs/cacert.pem" + certfile: "etc/certs/client-cert.pem" + keyfile: "etc/certs/client-key.pem" + } } sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or clientid = '%c'" }, @@ -29,7 +34,7 @@ authz:{ username: root password: public auto_reconnect: true - ssl: false + ssl: {enable: false} } sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'" }, @@ -41,7 +46,7 @@ authz:{ pool_size: 1 password: public auto_reconnect: true - ssl: false + ssl: {enable: false} } cmd: "HGETALL mqtt_acl:%u" }, @@ -126,5 +131,18 @@ Sample data in the default configuration: HSET mqtt_acl:emqx '$SYS/#' subscribe ``` -A rule of Redis ACL defines `publish`, `subscribe`, or `all `information. All lists in the rule are **allow** lists. +A rule of Redis AuthZ defines `publish`, `subscribe`, or `all `information. All lists in the rule are **allow** lists. +#### Mongo + +Create Example BSON documents +```sql +db.inventory.insertOne( + {username: "emqx", + clientid: "emqx", + ipaddress: "127.0.0.1", + permission: "allow", + action: "all", + topics: ["#"] + }) +``` diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index ec8f642b1..e91a68a63 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -1,4 +1,4 @@ -authz:{ +emqx_authz:{ rules: [ # { # type: mysql @@ -9,7 +9,12 @@ authz:{ # username: root # password: public # auto_reconnect: true - # ssl: false + # ssl: { + # enable: true + # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + # certfile: "{{ platform_etc_dir }}/certs/client-cert.pem" + # keyfile: "{{ platform_etc_dir }}/certs/client-key.pem" + # } # } # sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or clientid = '%c'" # }, @@ -22,7 +27,7 @@ authz:{ # username: root # password: public # auto_reconnect: true - # ssl: false + # ssl: {enable: false} # } # sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'" # }, @@ -34,10 +39,22 @@ authz:{ # pool_size: 1 # password: public # auto_reconnect: true - # ssl: false + # ssl: {enable: false} # } # cmd: "HGETALL mqtt_acl:%u" # }, + # { + # type: mongo + # config: { + # mongo_type: single + # servers: "127.0.0.1:27017" + # pool_size: 1 + # database: mqtt + # ssl: {enable: false} + # } + # collection: mqtt_acl + # find: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] } + # }, { permission: allow action: all diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl index ca595525d..76aa20688 100644 --- a/apps/emqx_authz/include/emqx_authz.hrl +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -1,4 +1,4 @@ --type(rule() :: #{binary() => any()}). +-type(rule() :: #{atom() => any()}). -type(rules() :: [rule()]). -define(APP, emqx_authz). @@ -6,14 +6,14 @@ -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). -define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= all))). --record(acl_metrics, { - allow = 'client.acl.allow', - deny = 'client.acl.deny', - ignore = 'client.acl.ignore' +-record(authz_metrics, { + allow = 'client.authorize.allow', + deny = 'client.authorize.deny', + ignore = 'client.authorize.ignore' }). -define(METRICS(Type), tl(tuple_to_list(#Type{}))). -define(METRICS(Type, K), #Type{}#Type.K). --define(ACL_METRICS, ?METRICS(acl_metrics)). --define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). +-define(AUTHZ_METRICS, ?METRICS(authz_metrics)). +-define(AUTHZ_METRICS(K), ?METRICS(authz_metrics, K)). diff --git a/apps/emqx_authz/src/emqx_authz.app.src b/apps/emqx_authz/src/emqx_authz.app.src index 10801eca1..f79e10f85 100644 --- a/apps/emqx_authz/src/emqx_authz.app.src +++ b/apps/emqx_authz/src/emqx_authz.app.src @@ -1,6 +1,6 @@ {application, emqx_authz, [{description, "An OTP application"}, - {vsn, "0.1.0"}, + {vsn, "0.1.1"}, {registered, []}, {mod, {emqx_authz_app, []}}, {applications, diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 24393a4b0..725d884e1 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -26,32 +26,29 @@ , compile/1 , lookup/0 , update/1 - , check_authz/5 + , authorize/5 , match/4 ]). -spec(register_metrics() -> ok). register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). + lists:foreach(fun emqx_metrics:ensure/1, ?AUTHZ_METRICS). init() -> ok = register_metrics(), - Conf = filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), - {ok, RawConf} = hocon:load(Conf), - #{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:check_plain(emqx_authz_schema, RawConf), - ok = application:set_env(?APP, rules, Rules), + Rules = emqx_config:get([emqx_authz, rules], []), NRules = [compile(Rule) || Rule <- Rules], - ok = emqx_hooks:add('client.check_acl', {?MODULE, check_authz, [NRules]}, -1). + ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). lookup() -> - application:get_env(?APP, rules, []). + emqx_config:get([emqx_authz, rules], []). update(Rules) -> - ok = application:set_env(?APP, rules, Rules), + emqx_config:put([emqx_authz], #{rules => Rules}), NRules = [compile(Rule) || Rule <- Rules], Action = find_action_in_hooks(), - ok = emqx_hooks:del('client.check_acl', Action), - ok = emqx_hooks:add('client.check_acl', {?MODULE, check_authz, [NRules]}, -1), + ok = emqx_hooks:del('client.authorize', Action), + ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1), ok = emqx_acl_cache:empty_acl_cache(). %%-------------------------------------------------------------------- @@ -59,77 +56,83 @@ update(Rules) -> %%-------------------------------------------------------------------- find_action_in_hooks() -> - Callbacks = emqx_hooks:lookup('client.check_acl'), - [Action] = [Action || {callback,{?MODULE, check_authz, _} = Action, _, _} <- Callbacks ], + Callbacks = emqx_hooks:lookup('client.authorize'), + [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], Action. -create_resource(#{<<"type">> := DB, - <<"config">> := Config +create_resource(#{type := DB, + config := Config } = Rule) -> ResourceID = iolist_to_binary([io_lib:format("~s_~s",[?APP, DB]), "_", integer_to_list(erlang:system_time())]), + NConfig = case DB of + redis -> #{config => Config }; + mongo -> #{config => Config }; + _ -> Config + end, case emqx_resource:check_and_create( ResourceID, list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), - #{<<"config">> => Config }) + NConfig) of {ok, _} -> - Rule#{<<"resource_id">> => ResourceID}; + Rule#{resource_id => ResourceID}; {error, already_created} -> - Rule#{<<"resource_id">> => ResourceID}; + Rule#{resource_id => ResourceID}; {error, Reason} -> error({load_config_error, Reason}) end. -spec(compile(rule()) -> rule()). -compile(#{<<"topics">> := Topics, - <<"action">> := Action, - <<"permission">> := Permission, - <<"principal">> := Principal +compile(#{topics := Topics, + action := Action, + permission := Permission, + principal := Principal } = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) -> NTopics = [compile_topic(Topic) || Topic <- Topics], - Rule#{<<"principal">> => compile_principal(Principal), - <<"topics">> => NTopics + Rule#{principal => compile_principal(Principal), + topics => NTopics }; -compile(#{<<"principal">> := Principal, - <<"type">> := redis - } = Rule) -> +compile(#{principal := Principal, + type := DB + } = Rule) when DB =:= redis; + DB =:= mongo -> NRule = create_resource(Rule), - NRule#{<<"principal">> => compile_principal(Principal)}; + NRule#{principal => compile_principal(Principal)}; -compile(#{<<"principal">> := Principal, - <<"type">> := DB, - <<"sql">> := SQL +compile(#{principal := Principal, + type := DB, + sql := SQL } = Rule) when DB =:= mysql; DB =:= pgsql -> Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])), NRule = create_resource(Rule), - NRule#{<<"principal">> => compile_principal(Principal), - <<"sql">> => Mod:parse_query(SQL) + NRule#{principal => compile_principal(Principal), + sql => Mod:parse_query(SQL) }. compile_principal(all) -> all; -compile_principal(#{<<"username">> := Username}) -> +compile_principal(#{username := Username}) -> {ok, MP} = re:compile(bin(Username)), - #{<<"username">> => MP}; -compile_principal(#{<<"clientid">> := Clientid}) -> + #{username => MP}; +compile_principal(#{clientid := Clientid}) -> {ok, MP} = re:compile(bin(Clientid)), - #{<<"clientid">> => MP}; -compile_principal(#{<<"ipaddress">> := IpAddress}) -> - #{<<"ipaddress">> => esockd_cidr:parse(b2l(IpAddress), true)}; -compile_principal(#{<<"and">> := Principals}) when is_list(Principals) -> - #{<<"and">> => [compile_principal(Principal) || Principal <- Principals]}; -compile_principal(#{<<"or">> := Principals}) when is_list(Principals) -> - #{<<"or">> => [compile_principal(Principal) || Principal <- Principals]}. + #{clientid => MP}; +compile_principal(#{ipaddress := IpAddress}) -> + #{ipaddress => esockd_cidr:parse(b2l(IpAddress), true)}; +compile_principal(#{'and' := Principals}) when is_list(Principals) -> + #{'and' => [compile_principal(Principal) || Principal <- Principals]}; +compile_principal(#{'or' := Principals}) when is_list(Principals) -> + #{'or' => [compile_principal(Principal) || Principal <- Principals]}. compile_topic(<<"eq ", Topic/binary>>) -> - compile_topic(#{<<"eq">> => Topic}); -compile_topic(#{<<"eq">> := Topic}) -> - #{<<"eq">> => emqx_topic:words(bin(Topic))}; + compile_topic(#{'eq' => Topic}); +compile_topic(#{'eq' := Topic}) -> + #{'eq' => emqx_topic:words(bin(Topic))}; compile_topic(Topic) when is_binary(Topic)-> Words = emqx_topic:words(bin(Topic)), case pattern(Words) of - true -> #{<<"pattern">> => Words}; + true -> #{pattern => Words}; false -> Words end. @@ -145,53 +148,53 @@ b2l(B) when is_list(B) -> B; b2l(B) when is_binary(B) -> binary_to_list(B). %%-------------------------------------------------------------------- -%% ACL callbacks +%% AuthZ callbacks %%-------------------------------------------------------------------- -%% @doc Check ACL --spec(check_authz(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules()) - -> {ok, allow} | {ok, deny} | deny). -check_authz(#{username := Username, +%% @doc Check AuthZ +-spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules()) + -> {stop, allow} | {ok, deny}). +authorize(#{username := Username, peerhost := IpAddress - } = Client, PubSub, Topic, DefaultResult, Rules) -> - case do_check_authz(Client, PubSub, Topic, Rules) of + } = Client, PubSub, Topic, _DefaultResult, Rules) -> + case do_authorize(Client, PubSub, Topic, Rules) of {matched, allow} -> - ?LOG(info, "Client succeeded authorizationa: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]), - emqx_metrics:inc(?ACL_METRICS(allow)), + ?LOG(info, "Client succeeded authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]), + emqx_metrics:inc(?AUTHZ_METRICS(allow)), {stop, allow}; {matched, deny} -> - ?LOG(info, "Client failed authorizationa: Username: ~p, IP: ~p, Topic: ~p, Permission: deny", [Username, IpAddress, Topic]), - emqx_metrics:inc(?ACL_METRICS(deny)), + ?LOG(info, "Client failed authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: deny", [Username, IpAddress, Topic]), + emqx_metrics:inc(?AUTHZ_METRICS(deny)), {stop, deny}; nomatch -> - ?LOG(info, "Client failed authorizationa: Username: ~p, IP: ~p, Topic: ~p, Reasion: ~p", [Username, IpAddress, Topic, "no-match rule"]), - DefaultResult + ?LOG(info, "Client failed authorization: Username: ~p, IP: ~p, Topic: ~p, Reasion: ~p", [Username, IpAddress, Topic, "no-match rule"]), + {stop, deny} end. -do_check_authz(Client, PubSub, Topic, - [Connector = #{<<"principal">> := Principal, - <<"type">> := DB} | Tail] ) -> +do_authorize(Client, PubSub, Topic, + [Connector = #{principal := Principal, + type := DB} | Tail] ) -> case match_principal(Client, Principal) of true -> Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_authz, DB])), - case Mod:check_authz(Client, PubSub, Topic, Connector) of - nomatch -> do_check_authz(Client, PubSub, Topic, Tail); + case Mod:authorize(Client, PubSub, Topic, Connector) of + nomatch -> do_authorize(Client, PubSub, Topic, Tail); Matched -> Matched end; - false -> do_check_authz(Client, PubSub, Topic, Tail) + false -> do_authorize(Client, PubSub, Topic, Tail) end; -do_check_authz(Client, PubSub, Topic, - [#{<<"permission">> := Permission} = Rule | Tail]) -> +do_authorize(Client, PubSub, Topic, + [#{permission := Permission} = Rule | Tail]) -> case match(Client, PubSub, Topic, Rule) of true -> {matched, Permission}; - false -> do_check_authz(Client, PubSub, Topic, Tail) + false -> do_authorize(Client, PubSub, Topic, Tail) end; -do_check_authz(_Client, _PubSub, _Topic, []) -> nomatch. +do_authorize(_Client, _PubSub, _Topic, []) -> nomatch. match(Client, PubSub, Topic, - #{<<"principal">> := Principal, - <<"topics">> := TopicFilters, - <<"action">> := Action + #{principal := Principal, + topics := TopicFilters, + action := Action }) -> match_action(PubSub, Action) andalso match_principal(Client, Principal) andalso @@ -203,27 +206,27 @@ match_action(_, all) -> true; match_action(_, _) -> false. match_principal(_, all) -> true; -match_principal(#{username := undefined}, #{<<"username">> := _MP}) -> +match_principal(#{username := undefined}, #{username := _MP}) -> false; -match_principal(#{username := Username}, #{<<"username">> := MP}) -> +match_principal(#{username := Username}, #{username := MP}) -> case re:run(Username, MP) of {match, _} -> true; _ -> false end; -match_principal(#{clientid := Clientid}, #{<<"clientid">> := MP}) -> +match_principal(#{clientid := Clientid}, #{clientid := MP}) -> case re:run(Clientid, MP) of {match, _} -> true; _ -> false end; -match_principal(#{peerhost := undefined}, #{<<"ipaddress">> := _CIDR}) -> +match_principal(#{peerhost := undefined}, #{ipaddress := _CIDR}) -> false; -match_principal(#{peerhost := IpAddress}, #{<<"ipaddress">> := CIDR}) -> +match_principal(#{peerhost := IpAddress}, #{ipaddress := CIDR}) -> esockd_cidr:match(IpAddress, CIDR); -match_principal(ClientInfo, #{<<"and">> := Principals}) when is_list(Principals) -> +match_principal(ClientInfo, #{'and' := Principals}) when is_list(Principals) -> lists:foldl(fun(Principal, Permission) -> match_principal(ClientInfo, Principal) andalso Permission end, true, Principals); -match_principal(ClientInfo, #{<<"or">> := Principals}) when is_list(Principals) -> +match_principal(ClientInfo, #{'or' := Principals}) when is_list(Principals) -> lists:foldl(fun(Principal, Permission) -> match_principal(ClientInfo, Principal) orelse Permission end, false, Principals); @@ -231,7 +234,7 @@ match_principal(_, _) -> false. match_topics(_ClientInfo, _Topic, []) -> false; -match_topics(ClientInfo, Topic, [#{<<"pattern">> := PatternFilter}|Filters]) -> +match_topics(ClientInfo, Topic, [#{pattern := PatternFilter}|Filters]) -> TopicFilter = feed_var(ClientInfo, PatternFilter), match_topic(emqx_topic:words(Topic), TopicFilter) orelse match_topics(ClientInfo, Topic, Filters); @@ -239,7 +242,7 @@ match_topics(ClientInfo, Topic, [TopicFilter|Filters]) -> match_topic(emqx_topic:words(Topic), TopicFilter) orelse match_topics(ClientInfo, Topic, Filters). -match_topic(Topic, #{<<"eq">> := TopicFilter}) -> +match_topic(Topic, #{'eq' := TopicFilter}) -> Topic == TopicFilter; match_topic(Topic, TopicFilter) -> emqx_topic:match(Topic, TopicFilter). diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index 6b4ed6c74..08ff0a7d7 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -74,10 +74,9 @@ push_authz(_Bindings, Params) -> %%------------------------------------------------------------------------------ get_rules(Params) -> - % #{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:check_plain(emqx_authz_schema, #{<<"authz">> => Params}), - {ok, Conf} = hocon:binary(jsx:encode(#{<<"authz">> => Params}), #{format => richmap}), - CheckConf = hocon_schema:check(emqx_authz_schema, Conf), - #{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:richmap_to_map(CheckConf), + {ok, Conf} = hocon:binary(jsx:encode(#{<<"emqx_authz">> => Params}), #{format => richmap}), + CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), + #{emqx_authz := #{rules := Rules}} = hocon_schema:richmap_to_map(CheckConf), Rules. %%-------------------------------------------------------------------- diff --git a/apps/emqx_authz/src/emqx_authz_mongo.erl b/apps/emqx_authz/src/emqx_authz_mongo.erl new file mode 100644 index 000000000..04af8f1ec --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_mongo.erl @@ -0,0 +1,105 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_mongo). + +-include("emqx_authz.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). + +%% AuthZ Callbacks +-export([ authorize/4 + , description/0 + ]). + +-ifdef(TEST). +-compile(export_all). +-compile(nowarn_export_all). +-endif. + +description() -> + "AuthZ with Mongo". + +authorize(Client, PubSub, Topic, + #{resource_id := ResourceID, + collection := Collection, + find := Find + }) -> + case emqx_resource:query(ResourceID, {find, Collection, replvar(Find, Client), #{}}) of + {error, Reason} -> + ?LOG(error, "[AuthZ] Query mongo error: ~p", [Reason]), + nomatch; + [] -> nomatch; + Rows -> + do_authorize(Client, PubSub, Topic, Rows) + end. + +do_authorize(_Client, _PubSub, _Topic, []) -> + nomatch; +do_authorize(Client, PubSub, Topic, [Rule | Tail]) -> + case match(Client, PubSub, Topic, Rule) of + {matched, Permission} -> {matched, Permission}; + nomatch -> do_authorize(Client, PubSub, Topic, Tail) + end. + +match(Client, PubSub, Topic, + #{<<"topics">> := Topics, + <<"permission">> := Permission, + <<"action">> := Action + }) -> + Rule = #{<<"principal">> => all, + <<"permission">> => Permission, + <<"topics">> => Topics, + <<"action">> => Action + }, + #{simple_rule := + #{permission := NPermission} = NRule + } = hocon_schema:check_plain( + emqx_authz_schema, + #{<<"simple_rule">> => Rule}, + #{atom_key => true}, + [simple_rule]), + case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of + true -> {matched, NPermission}; + false -> nomatch + end. + +replvar(Find, #{clientid := Clientid, + username := Username, + peerhost := IpAddress + }) -> + Fun = fun + _Fun(K, V, AccIn) when is_map(V) -> maps:put(K, maps:fold(_Fun, AccIn, V), AccIn); + _Fun(K, V, AccIn) when is_list(V) -> + maps:put(K, [ begin + [{K1, V1}] = maps:to_list(M), + _Fun(K1, V1, AccIn) + end || M <- V], + AccIn); + _Fun(K, V, AccIn) when is_binary(V) -> + V1 = re:replace(V, "%c", bin(Clientid), [global, {return, binary}]), + V2 = re:replace(V1, "%u", bin(Username), [global, {return, binary}]), + V3 = re:replace(V2, "%a", inet_parse:ntoa(IpAddress), [global, {return, binary}]), + maps:put(K, V3, AccIn); + _Fun(K, V, AccIn) -> maps:put(K, V, AccIn) + end, + maps:fold(Fun, #{}, Find). + +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(B) when is_binary(B) -> B; +bin(L) when is_list(L) -> list_to_binary(L); +bin(X) -> X. + diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index c1ab20125..4c769085d 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -20,10 +20,10 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). -%% ACL Callbacks +%% AuthZ Callbacks -export([ description/0 , parse_query/1 - , check_authz/4 + , authorize/4 ]). -ifdef(TEST). @@ -45,25 +45,25 @@ parse_query(Sql) -> {Sql, []} end. -check_authz(Client, PubSub, Topic, - #{<<"resource_id">> := ResourceID, - <<"sql">> := {SQL, Params} +authorize(Client, PubSub, Topic, + #{resource_id := ResourceID, + sql := {SQL, Params} }) -> case emqx_resource:query(ResourceID, {sql, SQL, replvar(Params, Client)}) of {ok, _Columns, []} -> nomatch; {ok, Columns, Rows} -> - do_check_authz(Client, PubSub, Topic, Columns, Rows); + do_authorize(Client, PubSub, Topic, Columns, Rows); {error, Reason} -> ?LOG(error, "[AuthZ] Query mysql error: ~p~n", [Reason]), nomatch end. -do_check_authz(_Client, _PubSub, _Topic, _Columns, []) -> +do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> nomatch; -do_check_authz(Client, PubSub, Topic, Columns, [Row | Tail]) -> +do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> case match(Client, PubSub, Topic, format_result(Columns, Row)) of {matched, Permission} -> {matched, Permission}; - nomatch -> do_check_authz(Client, PubSub, Topic, Columns, Tail) + nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail) end. format_result(Columns, Row) -> @@ -87,12 +87,12 @@ match(Client, PubSub, Topic, <<"action">> => Action, <<"permission">> => Permission }, - #{<<"simple_rule">> := - #{<<"permission">> := NPermission} = NRule + #{simple_rule := + #{permission := NPermission} = NRule } = hocon_schema:check_plain( emqx_authz_schema, #{<<"simple_rule">> => Rule}, - #{}, + #{atom_key => true}, [simple_rule]), case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of true -> {matched, NPermission}; diff --git a/apps/emqx_authz/src/emqx_authz_pgsql.erl b/apps/emqx_authz/src/emqx_authz_pgsql.erl index edea8102f..d74db36b2 100644 --- a/apps/emqx_authz/src/emqx_authz_pgsql.erl +++ b/apps/emqx_authz/src/emqx_authz_pgsql.erl @@ -20,10 +20,10 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). -%% ACL Callbacks +%% AuthZ Callbacks -export([ description/0 , parse_query/1 - , check_authz/4 + , authorize/4 ]). -ifdef(TEST). @@ -49,25 +49,25 @@ parse_query(Sql) -> {Sql, []} end. -check_authz(Client, PubSub, Topic, - #{<<"resource_id">> := ResourceID, - <<"sql">> := {SQL, Params} +authorize(Client, PubSub, Topic, + #{resource_id := ResourceID, + sql := {SQL, Params} }) -> case emqx_resource:query(ResourceID, {sql, SQL, replvar(Params, Client)}) of {ok, _Columns, []} -> nomatch; {ok, Columns, Rows} -> - do_check_authz(Client, PubSub, Topic, Columns, Rows); + do_authorize(Client, PubSub, Topic, Columns, Rows); {error, Reason} -> ?LOG(error, "[AuthZ] Query pgsql error: ~p~n", [Reason]), nomatch end. -do_check_authz(_Client, _PubSub, _Topic, _Columns, []) -> +do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> nomatch; -do_check_authz(Client, PubSub, Topic, Columns, [Row | Tail]) -> +do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> case match(Client, PubSub, Topic, format_result(Columns, Row)) of {matched, Permission} -> {matched, Permission}; - nomatch -> do_check_authz(Client, PubSub, Topic, Columns, Tail) + nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail) end. format_result(Columns, Row) -> @@ -91,12 +91,12 @@ match(Client, PubSub, Topic, <<"action">> => Action, <<"permission">> => Permission }, - #{<<"simple_rule">> := - #{<<"permission">> := NPermission} = NRule + #{simple_rule := + #{permission := NPermission} = NRule } = hocon_schema:check_plain( emqx_authz_schema, #{<<"simple_rule">> => Rule}, - #{}, + #{atom_key => true}, [simple_rule]), case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of true -> {matched, NPermission}; diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl index 7a85b26af..8d24b4534 100644 --- a/apps/emqx_authz/src/emqx_authz_redis.erl +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -20,8 +20,8 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). -%% ACL Callbacks --export([ check_authz/4 +%% AuthZ Callbacks +-export([ authorize/4 , description/0 ]). @@ -33,33 +33,33 @@ description() -> "AuthZ with redis". -check_authz(Client, PubSub, Topic, - #{<<"resource_id">> := ResourceID, - <<"cmd">> := CMD +authorize(Client, PubSub, Topic, + #{resource_id := ResourceID, + cmd := CMD }) -> NCMD = string:tokens(replvar(CMD, Client), " "), case emqx_resource:query(ResourceID, {cmd, NCMD}) of {ok, []} -> nomatch; {ok, Rows} -> - do_check_authz(Client, PubSub, Topic, Rows); + do_authorize(Client, PubSub, Topic, Rows); {error, Reason} -> ?LOG(error, "[AuthZ] Query redis error: ~p", [Reason]), nomatch end. -do_check_authz(_Client, _PubSub, _Topic, []) -> +do_authorize(_Client, _PubSub, _Topic, []) -> nomatch; -do_check_authz(Client, PubSub, Topic, [TopicFilter, Action | Tail]) -> - case match(Client, PubSub, Topic, +do_authorize(Client, PubSub, Topic, [TopicFilter, Action | Tail]) -> + case match(Client, PubSub, Topic, #{topics => TopicFilter, action => Action - }) + }) of {matched, Permission} -> {matched, Permission}; - nomatch -> do_check_authz(Client, PubSub, Topic, Tail) + nomatch -> do_authorize(Client, PubSub, Topic, Tail) end. -match(Client, PubSub, Topic, +match(Client, PubSub, Topic, #{topics := TopicFilter, action := Action }) -> @@ -68,11 +68,11 @@ match(Client, PubSub, Topic, <<"action">> => Action, <<"permission">> => allow }, - #{<<"simple_rule">> := NRule + #{simple_rule := NRule } = hocon_schema:check_plain( emqx_authz_schema, #{<<"simple_rule">> => Rule}, - #{}, + #{atom_key => true}, [simple_rule]), case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of true -> {matched, allow}; diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 04d1b2268..0b6a1d107 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -11,11 +11,18 @@ -export([structs/0, fields/1]). -structs() -> [authz]. +structs() -> ["emqx_authz"]. -fields(authz) -> +fields("emqx_authz") -> [ {rules, rules()} ]; +fields(mongo_connector) -> + [ {principal, principal()} + , {type, #{type => hoconsc:enum([mongo])}} + , {config, #{type => map()}} + , {collection, #{type => atom()}} + , {find, #{type => map()}} + ]; fields(redis_connector) -> [ {principal, principal()} , {type, #{type => hoconsc:enum([redis])}} @@ -27,7 +34,6 @@ fields(redis_connector) -> } , {cmd, query()} ]; - fields(sql_connector) -> [ {principal, principal() } , {type, #{type => hoconsc:enum([mysql, pgsql])}} @@ -39,7 +45,7 @@ fields(simple_rule) -> , {action, #{type => action()}} , {topics, #{type => union_array( [ binary() - , hoconsc:ref(eq_topic) + , hoconsc:ref(?MODULE, eq_topic) ] )}} , {principal, principal()} @@ -52,18 +58,18 @@ fields(ipaddress) -> [{ipaddress, #{type => string()}}]; fields(andlist) -> [{'and', #{type => union_array( - [ hoconsc:ref(username) - , hoconsc:ref(clientid) - , hoconsc:ref(ipaddress) + [ hoconsc:ref(?MODULE, username) + , hoconsc:ref(?MODULE, clientid) + , hoconsc:ref(?MODULE, ipaddress) ]) } } ]; fields(orlist) -> [{'or', #{type => union_array( - [ hoconsc:ref(username) - , hoconsc:ref(clientid) - , hoconsc:ref(ipaddress) + [ hoconsc:ref(?MODULE, username) + , hoconsc:ref(?MODULE, clientid) + , hoconsc:ref(?MODULE, ipaddress) ]) } } @@ -81,9 +87,10 @@ union_array(Item) when is_list(Item) -> rules() -> #{type => union_array( - [ hoconsc:ref(simple_rule) - , hoconsc:ref(sql_connector) - , hoconsc:ref(redis_connector) + [ hoconsc:ref(?MODULE, simple_rule) + , hoconsc:ref(?MODULE, sql_connector) + , hoconsc:ref(?MODULE, redis_connector) + , hoconsc:ref(?MODULE, mongo_connector) ]) }. @@ -91,11 +98,11 @@ principal() -> #{default => all, type => hoconsc:union( [ all - , hoconsc:ref(username) - , hoconsc:ref(clientid) - , hoconsc:ref(ipaddress) - , hoconsc:ref(andlist) - , hoconsc:ref(orlist) + , hoconsc:ref(?MODULE, username) + , hoconsc:ref(?MODULE, clientid) + , hoconsc:ref(?MODULE, ipaddress) + , hoconsc:ref(?MODULE, andlist) + , hoconsc:ref(?MODULE, orlist) ]) }. diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 88e250377..66b2e62de 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -39,46 +39,42 @@ end_per_suite(_Config) -> set_special_configs(emqx) -> application:set_env(emqx, allow_anonymous, true), application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, acl_nomatch, deny), ok; set_special_configs(emqx_authz) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => #{<<"rules">> => []}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), + emqx_config:put([emqx_authz], #{rules => []}), ok; set_special_configs(_App) -> ok. --define(RULE1, #{<<"principal">> => all, - <<"topics">> => [<<"#">>], - <<"action">> => all, - <<"permission">> => deny} +-define(RULE1, #{principal => all, + topics => [<<"#">>], + action => all, + permission => deny} ). --define(RULE2, #{<<"principal">> => - #{<<"ipaddress">> => <<"127.0.0.1">>}, - <<"topics">> => - [#{<<"eq">> => <<"#">>}, - #{<<"eq">> => <<"+">>} +-define(RULE2, #{principal => + #{ipaddress => <<"127.0.0.1">>}, + topics => + [#{eq => <<"#">>}, + #{eq => <<"+">>} ] , - <<"action">> => all, - <<"permission">> => allow} + action => all, + permission => allow} ). --define(RULE3,#{<<"principal">> => - #{<<"and">> => [#{<<"username">> => "^test?"}, - #{<<"clientid">> => "^test?"} +-define(RULE3,#{principal => + #{'and' => [#{username => "^test?"}, + #{clientid => "^test?"} ]}, - <<"topics">> => [<<"test">>], - <<"action">> => publish, - <<"permission">> => allow} + topics => [<<"test">>], + action => publish, + permission => allow} ). --define(RULE4,#{<<"principal">> => - #{<<"or">> => [#{<<"username">> => <<"^test">>}, - #{<<"clientid">> => <<"test?">>} - ]}, - <<"topics">> => [<<"%u">>,<<"%c">>], - <<"action">> => publish, - <<"permission">> => deny} +-define(RULE4,#{principal => + #{'or' => [#{username => <<"^test">>}, + #{clientid => <<"test?">>} + ]}, + topics => [<<"%u">>,<<"%c">>], + action => publish, + permission => deny} ). @@ -86,39 +82,39 @@ set_special_configs(_App) -> %% Testcases %%------------------------------------------------------------------------------ t_compile(_) -> - ?assertEqual(#{<<"permission">> => deny, - <<"action">> => all, - <<"principal">> => all, - <<"topics">> => [['#']] + ?assertEqual(#{permission => deny, + action => all, + principal => all, + topics => [['#']] },emqx_authz:compile(?RULE1)), - ?assertEqual(#{<<"permission">> => allow, - <<"action">> => all, - <<"principal">> => - #{<<"ipaddress">> => {{127,0,0,1},{127,0,0,1},32}}, - <<"topics">> => [#{<<"eq">> => ['#']}, - #{<<"eq">> => ['+']}] + ?assertEqual(#{permission => allow, + action => all, + principal => + #{ipaddress => {{127,0,0,1},{127,0,0,1},32}}, + topics => [#{eq => ['#']}, + #{eq => ['+']}] }, emqx_authz:compile(?RULE2)), ?assertMatch( - #{<<"permission">> := allow, - <<"action">> := publish, - <<"principal">> := - #{<<"and">> := [#{<<"username">> := {re_pattern, _, _, _, _}}, - #{<<"clientid">> := {re_pattern, _, _, _, _}} - ] + #{permission := allow, + action := publish, + principal := + #{'and' := [#{username := {re_pattern, _, _, _, _}}, + #{clientid := {re_pattern, _, _, _, _}} + ] }, - <<"topics">> := [[<<"test">>]] + topics := [[<<"test">>]] }, emqx_authz:compile(?RULE3)), ?assertMatch( - #{<<"permission">> := deny, - <<"action">> := publish, - <<"principal">> := - #{<<"or">> := [#{<<"username">> := {re_pattern, _, _, _, _}}, - #{<<"clientid">> := {re_pattern, _, _, _, _}} - ] + #{permission := deny, + action := publish, + principal := + #{'or' := [#{username := {re_pattern, _, _, _, _}}, + #{clientid := {re_pattern, _, _, _, _}} + ] }, - <<"topics">> := [#{<<"pattern">> := [<<"%u">>]}, - #{<<"pattern">> := [<<"%c">>]} - ] + topics := [#{pattern := [<<"%u">>]}, + #{pattern := [<<"%c">>]} + ] }, emqx_authz:compile(?RULE4)), ok. @@ -145,24 +141,24 @@ t_authz(_) -> Rules3 = [emqx_authz:compile(Rule) || Rule <- [?RULE3, ?RULE4]], Rules4 = [emqx_authz:compile(Rule) || Rule <- [?RULE4, ?RULE1]], - ?assertEqual(deny, - emqx_authz:check_authz(ClientInfo1, subscribe, <<"#">>, deny, [])), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo1, subscribe, <<"+">>, deny, Rules1)), + emqx_authz:authorize(ClientInfo1, subscribe, <<"#">>, deny, [])), + ?assertEqual({stop, deny}, + emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules1)), ?assertEqual({stop, allow}, - emqx_authz:check_authz(ClientInfo1, subscribe, <<"+">>, deny, Rules2)), + emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules2)), ?assertEqual({stop, allow}, - emqx_authz:check_authz(ClientInfo1, publish, <<"test">>, deny, Rules3)), + emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules3)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo1, publish, <<"test">>, deny, Rules4)), + emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules4)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo2, subscribe, <<"#">>, deny, Rules2)), + emqx_authz:authorize(ClientInfo2, subscribe, <<"#">>, deny, Rules2)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo3, publish, <<"test">>, deny, Rules3)), + emqx_authz:authorize(ClientInfo3, publish, <<"test">>, deny, Rules3)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo3, publish, <<"fake">>, deny, Rules4)), + emqx_authz:authorize(ClientInfo3, publish, <<"fake">>, deny, Rules4)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo4, publish, <<"test">>, deny, Rules3)), + emqx_authz:authorize(ClientInfo4, publish, <<"test">>, deny, Rules3)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo4, publish, <<"fake">>, deny, Rules4)), + emqx_authz:authorize(ClientInfo4, publish, <<"fake">>, deny, Rules4)), ok. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 4871e0d7f..24683cd5b 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -55,11 +55,13 @@ set_special_configs(emqx) -> application:set_env(emqx, enable_acl_cache, false), ok; set_special_configs(emqx_authz) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => #{<<"rules">> => []}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), + emqx_config:put([emqx_authz], #{rules => []}), + ok; +set_special_configs(emqx_management) -> + emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], + default_application_id => <<"admin">>, + default_application_secret => <<"public">>}), ok; set_special_configs(_App) -> diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl new file mode 100644 index 000000000..daf4d1722 --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -0,0 +1,112 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_mongo_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include("emqx_authz.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +all() -> + emqx_ct:all(?MODULE). + +groups() -> + []. + +init_per_suite(Config) -> + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ), + ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), + Config. + +end_per_suite(_Config) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), + emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), + meck:unload(emqx_resource). + +set_special_configs(emqx) -> + application:set_env(emqx, allow_anonymous, true), + application:set_env(emqx, enable_acl_cache, false), + application:set_env(emqx, acl_nomatch, deny), + application:set_env(emqx, plugins_loaded_file, + emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), + ok; +set_special_configs(emqx_authz) -> + Rules = [#{config =>#{}, + principal => all, + collection => <<"fake">>, + find => #{<<"a">> => <<"b">>}, + type => mongo} + ], + emqx_config:put([emqx_authz], #{rules => Rules}), + ok; +set_special_configs(_App) -> + ok. + +-define(RULE1,[#{<<"topics">> => [<<"#">>], + <<"permission">> => <<"deny">>, + <<"action">> => <<"all">>}]). +-define(RULE2,[#{<<"topics">> => [<<"eq #">>], + <<"permission">> => <<"allow">>, + <<"action">> => <<"all">>}]). +-define(RULE3,[#{<<"topics">> => [<<"test/%c">>], + <<"permission">> => <<"allow">>, + <<"action">> => <<"subscribe">>}]). +-define(RULE4,[#{<<"topics">> => [<<"test/%u">>], + <<"permission">> => <<"allow">>, + <<"action">> => <<"publish">>}]). + +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ + +t_authz(_) -> + ClientInfo1 = #{clientid => <<"test">>, + username => <<"test">>, + peerhost => {127,0,0,1} + }, + ClientInfo2 = #{clientid => <<"test_clientid">>, + username => <<"test_username">>, + peerhost => {192,168,0,10} + }, + ClientInfo3 = #{clientid => <<"test_clientid">>, + username => <<"fake_username">>, + peerhost => {127,0,0,1} + }, + + meck:expect(emqx_resource, query, fun(_, _) -> [] end), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"#">>)), % nomatch + + meck:expect(emqx_resource, query, fun(_, _) -> ?RULE1 ++ ?RULE2 end), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"+">>)), + + meck:expect(emqx_resource, query, fun(_, _) -> ?RULE2 ++ ?RULE1 end), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), + + meck:expect(emqx_resource, query, fun(_, _) -> ?RULE3 ++ ?RULE4 end), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_username">>)), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_username">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, subscribe, <<"test">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, publish, <<"test">>)), % nomatch + ok. + diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 4f2148522..edc35ca45 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -47,16 +47,12 @@ set_special_configs(emqx) -> emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), ok; set_special_configs(emqx_authz) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => - #{<<"rules">> => - [#{<<"config">> =>#{<<"meck">> => <<"fake">>}, - <<"principal">> => all, - <<"sql">> => <<"fake sql">>, - <<"type">> => mysql} - ]}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), + Rules = [#{config =>#{}, + principal => all, + sql => <<"fake">>, + type => mysql} + ], + emqx_config:put([emqx_authz], #{rules => Rules}), ok; set_special_configs(_App) -> ok. @@ -80,38 +76,35 @@ set_special_configs(_App) -> t_authz(_) -> ClientInfo1 = #{clientid => <<"test">>, username => <<"test">>, - peerhost => {127,0,0,1}, - zone => zone + peerhost => {127,0,0,1} }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, - peerhost => {192,168,0,10}, - zone => zone + peerhost => {192,168,0,10} }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, - peerhost => {127,0,0,1}, - zone => zone + peerhost => {127,0,0,1} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"#">>)), % nomatch meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"+">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_username">>)), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_username">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, subscribe, <<"test">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, publish, <<"test">>)), % nomatch + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_username">>)), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_username">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, subscribe, <<"test">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, publish, <<"test">>)), % nomatch ok. diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index dcc820a4c..d5f89bcad 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -47,16 +47,12 @@ set_special_configs(emqx) -> emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), ok; set_special_configs(emqx_authz) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => - #{<<"rules">> => - [#{<<"config">> =>#{<<"meck">> => <<"fake">>}, - <<"principal">> => all, - <<"sql">> => <<"fake sql">>, - <<"type">> => pgsql} - ]}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), + Rules = [#{config =>#{}, + principal => all, + sql => <<"fake">>, + type => pgsql} + ], + emqx_config:put([emqx_authz], #{rules => Rules}), ok; set_special_configs(_App) -> ok. @@ -80,38 +76,35 @@ set_special_configs(_App) -> t_authz(_) -> ClientInfo1 = #{clientid => <<"test">>, username => <<"test">>, - peerhost => {127,0,0,1}, - zone => zone + peerhost => {127,0,0,1} }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, - peerhost => {192,168,0,10}, - zone => zone + peerhost => {192,168,0,10} }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, - peerhost => {127,0,0,1}, - zone => zone + peerhost => {127,0,0,1} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"#">>)), % nomatch meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"+">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"+">>)), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_username">>)), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_username">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, subscribe, <<"test">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, publish, <<"test">>)), % nomatch + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_username">>)), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_username">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, subscribe, <<"test">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, publish, <<"test">>)), % nomatch ok. diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 3f8bea166..0d7ffa9d8 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -47,21 +47,12 @@ set_special_configs(emqx) -> emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), ok; set_special_configs(emqx_authz) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => - #{<<"rules">> => - [#{<<"config">> =>#{ - <<"server">> => <<"127.0.0.1:6379">>, - <<"password">> => <<"public">>, - <<"pool_size">> => 1, - <<"auto_reconnect">> => true - }, - <<"principal">> => all, - <<"cmd">> => <<"fake cmd">>, - <<"type">> => redis} - ]}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), + Rules = [#{config =>#{}, + principal => all, + cmd => <<"fake">>, + type => redis} + ], + emqx_config:put([emqx_authz], #{rules => Rules}), ok; set_special_configs(_App) -> ok. @@ -77,37 +68,36 @@ set_special_configs(_App) -> t_authz(_) -> ClientInfo = #{clientid => <<"clientid">>, username => <<"username">>, - peerhost => {127,0,0,1}, - zone => zone + peerhost => {127,0,0,1} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, []} end), % nomatch ?assertEqual(deny, - emqx_access_control:check_acl(ClientInfo, subscribe, <<"#">>)), + emqx_access_control:authorize(ClientInfo, subscribe, <<"#">>)), ?assertEqual(deny, - emqx_access_control:check_acl(ClientInfo, publish, <<"#">>)), + emqx_access_control:authorize(ClientInfo, publish, <<"#">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE1 ++ ?RULE2} end), % nomatch ?assertEqual(deny, - emqx_access_control:check_acl(ClientInfo, subscribe, <<"+">>)), + emqx_access_control:authorize(ClientInfo, subscribe, <<"+">>)), % nomatch ?assertEqual(deny, - emqx_access_control:check_acl(ClientInfo, subscribe, <<"test/username">>)), + emqx_access_control:authorize(ClientInfo, subscribe, <<"test/username">>)), ?assertEqual(allow, - emqx_access_control:check_acl(ClientInfo, publish, <<"test/clientid">>)), + emqx_access_control:authorize(ClientInfo, publish, <<"test/clientid">>)), ?assertEqual(allow, - emqx_access_control:check_acl(ClientInfo, publish, <<"test/clientid">>)), + emqx_access_control:authorize(ClientInfo, publish, <<"test/clientid">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE3} end), ?assertEqual(allow, - emqx_access_control:check_acl(ClientInfo, subscribe, <<"#">>)), + emqx_access_control:authorize(ClientInfo, subscribe, <<"#">>)), % nomatch ?assertEqual(deny, - emqx_access_control:check_acl(ClientInfo, publish, <<"#">>)), + emqx_access_control:authorize(ClientInfo, publish, <<"#">>)), ok. diff --git a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf index 3cc719e40..c34567ee4 100644 --- a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf +++ b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf @@ -2,173 +2,57 @@ ## Configuration for EMQ X MQTT Broker Bridge ##==================================================================== -##-------------------------------------------------------------------- -## Bridges to aws -##-------------------------------------------------------------------- - -## Bridge address: node name for local bridge, host:port for remote. -## -## Value: String -## Example: emqx@127.0.0.1, "127.0.0.1:1883" -bridge.mqtt.aws.address = "127.0.0.1:1883" - -## Protocol version of the bridge. -## -## Value: Enum -## - mqttv5 -## - mqttv4 -## - mqttv3 -bridge.mqtt.aws.proto_ver = mqttv4 - -## Start type of the bridge. -## -## Value: enum -## manual -## auto -bridge.mqtt.aws.start_type = manual - -## Whether to enable bridge mode for mqtt bridge -## -## This option is prepared for the mqtt broker which does not -## support bridge_mode such as the mqtt-plugin of the rabbitmq -## -## Value: boolean -#bridge.mqtt.aws.bridge_mode = false - -## The ClientId of a remote bridge. -## -## Placeholders: -## ${node}: Node name -## -## Value: String -bridge.mqtt.aws.clientid = bridge_aws - -## The Clean start flag of a remote bridge. -## -## Value: boolean -## Default: true -## -## NOTE: Some IoT platforms require clean_start -## must be set to 'true' -bridge.mqtt.aws.clean_start = true - -## The username for a remote bridge. -## -## Value: String -bridge.mqtt.aws.username = user - -## The password for a remote bridge. -## -## Value: String -bridge.mqtt.aws.password = passwd - -## Topics that need to be forward to AWS IoTHUB -## -## Value: String -## Example: "topic1/#,topic2/#" -bridge.mqtt.aws.forwards = "topic1/#,topic2/#" - -## Forward messages to the mountpoint of an AWS IoTHUB -## -## Value: String -bridge.mqtt.aws.forward_mountpoint = "bridge/aws/${node}/" - -## Need to subscribe to AWS topics -## -## Value: String -## bridge.mqtt.aws.subscription.1.topic = "cmd/topic1" - -## Need to subscribe to AWS topics QoS. -## -## Value: Number -## bridge.mqtt.aws.subscription.1.qos = 1 - -## A mountpoint that receives messages from AWS IoTHUB -## -## Value: String -## bridge.mqtt.aws.receive_mountpoint = "receive/aws/" - - -## Bribge to remote server via SSL. -## -## Value: on | off -bridge.mqtt.aws.ssl = off - -## PEM-encoded CA certificates of the bridge. -## -## Value: File -bridge.mqtt.aws.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" - -## Client SSL Certfile of the bridge. -## -## Value: File -bridge.mqtt.aws.certfile = "{{ platform_etc_dir }}/certs/client-cert.pem" - -## Client SSL Keyfile of the bridge. -## -## Value: File -bridge.mqtt.aws.keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" - -## SSL Ciphers used by the bridge. -## -## Value: String -bridge.mqtt.aws.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" - -## Ciphers for TLS PSK. -## Note that 'bridge.${BridgeName}.ciphers' and 'bridge.${BridgeName}.psk_ciphers' cannot -## be configured at the same time. -## See 'https://tools.ietf.org/html/rfc4279#section-2'. -#bridge.mqtt.aws.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" - -## Ping interval of a down bridge. -## -## Value: Duration -## Default: 10 seconds -bridge.mqtt.aws.keepalive = 60s - -## TLS versions used by the bridge. -## -## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier -## Value: String -bridge.mqtt.aws.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" - -## Bridge reconnect time. -## -## Value: Duration -## Default: 30 seconds -bridge.mqtt.aws.reconnect_interval = 30s - -## Retry interval for bridge QoS1 message delivering. -## -## Value: Duration -bridge.mqtt.aws.retry_interval = 20s - -## Publish messages in batches, only RPC Bridge supports -## -## Value: Integer -## default: 32 -bridge.mqtt.aws.batch_size = 32 - -## Inflight size. -## 0 means infinity (no limit on the inflight window) -## -## Value: Integer -bridge.mqtt.aws.max_inflight_size = 32 - -## Base directory for replayq to store messages on disk -## If this config entry is missing or set to undefined, -## replayq works in a mem-only manner. -## -## Value: String -bridge.mqtt.aws.queue.replayq_dir = "{{ platform_data_dir }}/replayq/emqx_aws_bridge/" - -## Replayq segment size -## -## Value: Bytesize -bridge.mqtt.aws.queue.replayq_seg_bytes = 10MB - -## Replayq max total size -## -## Value: Bytesize -bridge.mqtt.aws.queue.max_total_size = 5GB - +emqx_bridge_mqtt:{ + bridges:[ + # { + # name: "mqtt1" + # start_type: auto + # forwards: ["test/#"], + # forward_mountpoint: "" + # reconnect_interval: "30s" + # batch_size: 100 + # queue:{ + # replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/" + # replayq_seg_bytes: "100MB" + # replayq_offload_mode: false + # replayq_max_total_bytes: "1GB" + # }, + # config:{ + # conn_type: mqtt + # address: "127.0.0.1:1883" + # proto_ver: v4 + # bridge_mode: true + # clientid: "client1" + # clean_start: true + # username: "username1" + # password: "" + # keepalive: 300 + # subscriptions: [{ + # topic: "t/#" + # qos: 1 + # }] + # receive_mountpoint: "" + # retry_interval: "30s" + # max_inflight: 32 + # } + # }, + # { + # name: "rpc1" + # start_type: auto + # forwards: ["test/#"], + # forward_mountpoint: "" + # reconnect_interval: "30s" + # batch_size: 100 + # queue:{ + # replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/" + # replayq_seg_bytes: "100MB" + # replayq_offload_mode: false + # replayq_max_total_bytes: "1GB" + # }, + # config:{ + # conn_type: rpc + # node: "emqx@127.0.0.1" + # } + # } + ] +} diff --git a/apps/emqx_bridge_mqtt/priv/emqx_bridge_mqtt.schema b/apps/emqx_bridge_mqtt/priv/emqx_bridge_mqtt.schema deleted file mode 100644 index 3168bfc14..000000000 --- a/apps/emqx_bridge_mqtt/priv/emqx_bridge_mqtt.schema +++ /dev/null @@ -1,244 +0,0 @@ -%%-*- mode: erlang -*- -%%-------------------------------------------------------------------- -%% Bridges -%%-------------------------------------------------------------------- -{mapping, "bridge.mqtt.$name.address", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.proto_ver", "emqx_bridge_mqtt.bridges", [ - {datatype, {enum, [mqttv3, mqttv4, mqttv5]}} -]}. - -{mapping, "bridge.mqtt.$name.bridge_mode", "emqx_bridge_mqtt.bridges", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "bridge.mqtt.$name.start_type", "emqx_bridge_mqtt.bridges", [ - {datatype, {enum, [manual, auto]}}, - {default, auto} -]}. - -{mapping, "bridge.mqtt.$name.clientid", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.clean_start", "emqx_bridge_mqtt.bridges", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "bridge.mqtt.$name.username", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.password", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.forwards", "emqx_bridge_mqtt.bridges", [ - {datatype, string}, - {default, ""} -]}. - -{mapping, "bridge.mqtt.$name.forward_mountpoint", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.subscription.$id.topic", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.subscription.$id.qos", "emqx_bridge_mqtt.bridges", [ - {datatype, integer} -]}. - -{mapping, "bridge.mqtt.$name.receive_mountpoint", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.ssl", "emqx_bridge_mqtt.bridges", [ - {datatype, flag}, - {default, off} -]}. - -{mapping, "bridge.mqtt.$name.cacertfile", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.certfile", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.keyfile", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.ciphers", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.psk_ciphers", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.keepalive", "emqx_bridge_mqtt.bridges", [ - {default, "10s"}, - {datatype, {duration, s}} -]}. - -{mapping, "bridge.mqtt.$name.tls_versions", "emqx_bridge_mqtt.bridges", [ - {datatype, string}, - {default, "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1"} -]}. - -{mapping, "bridge.mqtt.$name.reconnect_interval", "emqx_bridge_mqtt.bridges", [ - {default, "30s"}, - {datatype, {duration, ms}} -]}. - -{mapping, "bridge.mqtt.$name.retry_interval", "emqx_bridge_mqtt.bridges", [ - {default, "20s"}, - {datatype, {duration, s}} -]}. - -{mapping, "bridge.mqtt.$name.max_inflight_size", "emqx_bridge_mqtt.bridges", [ - {default, 0}, - {datatype, integer} - ]}. - -{mapping, "bridge.mqtt.$name.batch_size", "emqx_bridge_mqtt.bridges", [ - {default, 0}, - {datatype, integer} -]}. - -{mapping, "bridge.mqtt.$name.queue.replayq_dir", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.queue.replayq_seg_bytes", "emqx_bridge_mqtt.bridges", [ - {datatype, bytesize} -]}. - -{mapping, "bridge.mqtt.$name.queue.max_total_size", "emqx_bridge_mqtt.bridges", [ - {datatype, bytesize} -]}. - -{translation, "emqx_bridge_mqtt.bridges", fun(Conf) -> - - MapPSKCiphers = fun(PSKCiphers) -> - lists:map( - fun("PSK-AES128-CBC-SHA") -> {psk, aes_128_cbc, sha}; - ("PSK-AES256-CBC-SHA") -> {psk, aes_256_cbc, sha}; - ("PSK-3DES-EDE-CBC-SHA") -> {psk, '3des_ede_cbc', sha}; - ("PSK-RC4-SHA") -> {psk, rc4_128, sha} - end, PSKCiphers) - end, - - Split = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, - - IsSsl = fun(cacertfile) -> true; - (certfile) -> true; - (keyfile) -> true; - (ciphers) -> true; - (psk_ciphers) -> true; - (tls_versions) -> true; - (_Opt) -> false - end, - - Parse = fun(tls_versions, Vers) -> - [{versions, [list_to_atom(S) || S <- Split(Vers)]}]; - (ciphers, Ciphers) -> - [{ciphers, Split(Ciphers)}]; - (psk_ciphers, Ciphers) -> - [{ciphers, MapPSKCiphers(Split(Ciphers))}, {user_lookup_fun, {fun emqx_psk:lookup/3, <<>>}}]; - (Opt, Val) -> - [{Opt, Val}] - end, - - Merge = fun(forwards, Val, Opts) -> - [{forwards, string:tokens(Val, ",")}|Opts]; - (Opt, Val, Opts) -> - case IsSsl(Opt) of - true -> - SslOpts = Parse(Opt, Val) ++ proplists:get_value(ssl_opts, Opts, []), - lists:ukeymerge(1, [{ssl_opts, SslOpts}], lists:usort(Opts)); - false -> - [{Opt, Val}|Opts] - end - end, - Queue = fun(Name) -> - Configs = cuttlefish_variable:filter_by_prefix("bridge.mqtt." ++ Name ++ ".queue", Conf), - - QOpts = [{list_to_atom(QOpt), QValue}|| {[_, _, _, "queue", QOpt], QValue} <- Configs], - maps:from_list(QOpts) - end, - Subscriptions = fun(Name) -> - Configs = cuttlefish_variable:filter_by_prefix("bridge.mqtt." ++ Name ++ ".subscription", Conf), - lists:zip([Topic || {_, Topic} <- lists:sort([{I, Topic} || {[_, _, _, "subscription", I, "topic"], Topic} <- Configs])], - [QoS || {_, QoS} <- lists:sort([{I, QoS} || {[_, _, _, "subscription", I, "qos"], QoS} <- Configs])]) - end, - IsNodeAddr = fun(Addr) -> - case string:tokens(Addr, "@") of - [_NodeName, _Hostname] -> true; - _ -> false - end - end, - ConnMod = fun(Name) -> - - [AddrConfig] = cuttlefish_variable:filter_by_prefix("bridge.mqtt." ++ Name ++ ".address", Conf), - {_, Addr} = AddrConfig, - - Subs = Subscriptions(Name), - case IsNodeAddr(Addr) of - true when Subs =/= [] -> - error({"subscriptions are not supported when bridging between emqx nodes", Name, Subs}); - true -> - emqx_bridge_rpc; - false -> - emqx_bridge_mqtt - end - end, - - %% to be backward compatible - Translate = - fun Tr(queue, Q, Cfg) -> - NewQ = maps:fold(Tr, #{}, Q), - Cfg#{queue => NewQ}; - Tr(address, Addr0, Cfg) -> - Addr = case IsNodeAddr(Addr0) of - true -> list_to_atom(Addr0); - false -> Addr0 - end, - Cfg#{address => Addr}; - Tr(reconnect_interval, Ms, Cfg) -> - Cfg#{reconnect_delay_ms => Ms}; - Tr(proto_ver, Ver, Cfg) -> - Cfg#{proto_ver => - case Ver of - mqttv3 -> v3; - mqttv4 -> v4; - mqttv5 -> v5; - _ -> v4 - end}; - Tr(max_inflight_size, Size, Cfg) -> - Cfg#{max_inflight => Size}; - Tr(Key, Value, Cfg) -> - Cfg#{Key => Value} - end, - C = lists:foldl( - fun({["bridge", "mqtt", Name, Opt], Val}, Acc) -> - %% e.g #{aws => [{OptKey, OptVal}]} - Init = [{list_to_atom(Opt), Val}, - {connect_module, ConnMod(Name)}, - {subscriptions, Subscriptions(Name)}, - {queue, Queue(Name)}], - maps:update_with(list_to_atom(Name), fun(Opts) -> Merge(list_to_atom(Opt), Val, Opts) end, Init, Acc); - (_, Acc) -> Acc - end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("bridge.mqtt", Conf))), - C1 = maps:map(fun(Bn, Bc) -> - maps:to_list(maps:fold(Translate, #{}, maps:from_list(Bc))) - end, C), - maps:to_list(C1) -end}. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_connect.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_connect.erl deleted file mode 100644 index ece6002a7..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_connect.erl +++ /dev/null @@ -1,74 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_connect). - --export([start/2]). - --export_type([config/0, connection/0]). - --optional_callbacks([ensure_subscribed/3, ensure_unsubscribed/2]). - -%% map fields depend on implementation --type(config() :: map()). --type(connection() :: term()). --type(batch() :: emqx_protal:batch()). --type(ack_ref() :: emqx_bridge_worker:ack_ref()). --type(topic() :: emqx_topic:topic()). --type(qos() :: emqx_mqtt_types:qos()). - --include_lib("emqx/include/logger.hrl"). - --logger_header("[Bridge Connect]"). - -%% establish the connection to remote node/cluster -%% protal worker (the caller process) should be expecting -%% a message {disconnected, conn_ref()} when disconnected. --callback start(config()) -> {ok, connection()} | {error, any()}. - -%% send to remote node/cluster -%% bridge worker (the caller process) should be expecting -%% a message {batch_ack, reference()} when batch is acknowledged by remote node/cluster --callback send(connection(), batch()) -> {ok, ack_ref()} | {ok, integer()} | {error, any()}. - -%% called when owner is shutting down. --callback stop(connection()) -> ok. - --callback ensure_subscribed(connection(), topic(), qos()) -> ok. - --callback ensure_unsubscribed(connection(), topic()) -> ok. - -start(Module, Config) -> - case Module:start(Config) of - {ok, Conn} -> - {ok, Conn}; - {error, Reason} -> - Config1 = obfuscate(Config), - ?LOG(error, "Failed to connect with module=~p\n" - "config=~p\nreason:~p", [Module, Config1, Reason]), - {error, Reason} - end. - -obfuscate(Map) -> - maps:fold(fun(K, V, Acc) -> - case is_sensitive(K) of - true -> [{K, '***'} | Acc]; - false -> [{K, V} | Acc] - end - end, [], Map). - -is_sensitive(password) -> true; -is_sensitive(_) -> false. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src index 83ce7a759..385c89965 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src @@ -1,6 +1,6 @@ {application, emqx_bridge_mqtt, [{description, "EMQ X Bridge to MQTT Broker"}, - {vsn, "4.3.1"}, % strict semver, bump manually! + {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, {applications, [kernel,stdlib,replayq,emqtt]}, diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src deleted file mode 100644 index 03e6119ae..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src +++ /dev/null @@ -1,16 +0,0 @@ -%% -*-: erlang -*- - -{VSN, - [ - {"4.3.0", [ - {load_module, emqx_bridge_worker, brutal_purge, soft_purge, []} - ]}, - {<<".*">>, []} - ], - [ - {"4.3.0", [ - {load_module, emqx_bridge_worker, brutal_purge, soft_purge, []} - ]}, - {<<".*">>, []} - ] -}. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl index d612af668..8d442463b 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl @@ -18,15 +18,11 @@ -module(emqx_bridge_mqtt). --behaviour(emqx_bridge_connect). - -%% behaviour callbacks -export([ start/1 , send/2 , stop/1 ]). -%% optional behaviour callbacks -export([ ensure_subscribed/3 , ensure_unsubscribed/2 ]). @@ -37,6 +33,9 @@ , handle_disconnected/2 ]). +-export([ check_subscriptions/1 + ]). + -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). @@ -49,26 +48,31 @@ %% emqx_bridge_connect callbacks %%-------------------------------------------------------------------- -start(Config = #{address := Address}) -> +start(Config) -> Parent = self(), + Address = maps:get(address, Config), Mountpoint = maps:get(receive_mountpoint, Config, undefined), + Subscriptions = maps:get(subscriptions, Config, []), + Subscriptions1 = check_subscriptions(Subscriptions), Handlers = make_hdlr(Parent, Mountpoint), {Host, Port} = case string:tokens(Address, ":") of [H] -> {H, 1883}; [H, P] -> {H, list_to_integer(P)} end, - ClientConfig = Config#{msg_handler => Handlers, - host => Host, - port => Port, - force_ping => true - }, - case emqtt:start_link(replvar(ClientConfig)) of + Config1 = Config#{ + msg_handler => Handlers, + host => Host, + port => Port, + force_ping => true, + proto_ver => maps:get(proto_ver, Config, v4) + }, + case emqtt:start_link(without_config(Config1)) of {ok, Pid} -> case emqtt:connect(Pid) of {ok, _} -> try - subscribe_remote_topics(Pid, maps:get(subscriptions, Config, [])), - {ok, #{client_pid => Pid}} + Subscriptions2 = subscribe_remote_topics(Pid, Subscriptions1), + {ok, #{client_pid => Pid, subscriptions => Subscriptions2}} catch throw : Reason -> ok = stop(#{client_pid => Pid}), @@ -86,25 +90,25 @@ stop(#{client_pid := Pid}) -> safe_stop(Pid, fun() -> emqtt:stop(Pid) end, 1000), ok. -ensure_subscribed(#{client_pid := Pid}, Topic, QoS) when is_pid(Pid) -> +ensure_subscribed(#{client_pid := Pid, subscriptions := Subs} = Conn, Topic, QoS) when is_pid(Pid) -> case emqtt:subscribe(Pid, Topic, QoS) of - {ok, _, _} -> ok; - Error -> Error + {ok, _, _} -> Conn#{subscriptions => [{Topic, QoS}|Subs]}; + Error -> {error, Error} end; ensure_subscribed(_Conn, _Topic, _QoS) -> %% return ok for now %% next re-connect should should call start with new topic added to config ok. -ensure_unsubscribed(#{client_pid := Pid}, Topic) when is_pid(Pid) -> +ensure_unsubscribed(#{client_pid := Pid, subscriptions := Subs} = Conn, Topic) when is_pid(Pid) -> case emqtt:unsubscribe(Pid, Topic) of - {ok, _, _} -> ok; - Error -> Error + {ok, _, _} -> Conn#{subscriptions => lists:keydelete(Topic, 1, Subs)}; + Error -> {error, Error} end; -ensure_unsubscribed(_, _) -> +ensure_unsubscribed(Conn, _) -> %% return ok for now %% next re-connect should should call start with this topic deleted from config - ok. + Conn. safe_stop(Pid, StopF, Timeout) -> MRef = monitor(process, Pid), @@ -169,36 +173,18 @@ make_hdlr(Parent, Mountpoint) -> }. subscribe_remote_topics(ClientPid, Subscriptions) -> - lists:foreach(fun({Topic, Qos}) -> - case emqtt:subscribe(ClientPid, Topic, Qos) of - {ok, _, _} -> ok; - Error -> throw(Error) - end - end, Subscriptions). + lists:map(fun({Topic, Qos}) -> + case emqtt:subscribe(ClientPid, Topic, Qos) of + {ok, _, _} -> {Topic, Qos}; + Error -> throw(Error) + end + end, Subscriptions). -%%-------------------------------------------------------------------- -%% Internal funcs -%%-------------------------------------------------------------------- +without_config(Config) -> + maps:without([conn_type, address, receive_mountpoint, subscriptions], Config). -replvar(Options) -> - replvar([clientid, max_inflight], Options). - -replvar([], Options) -> - Options; -replvar([Key|More], Options) -> - case maps:get(Key, Options, undefined) of - undefined -> - replvar(More, Options); - Val -> - replvar(More, maps:put(Key, feedvar(Key, Val, Options), Options)) - end. - -%% ${node} => node() -feedvar(clientid, ClientId, _) -> - iolist_to_binary(re:replace(ClientId, "\\${node}", atom_to_list(node()))); - -feedvar(max_inflight, 0, _) -> - infinity; - -feedvar(max_inflight, Size, _) -> - Size. +check_subscriptions(Subscriptions) -> + lists:map(fun(#{qos := QoS, topic := Topic}) -> + true = emqx_topic:validate({filter, Topic}), + {Topic, QoS} + end, Subscriptions). diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl new file mode 100644 index 000000000..8cc87ef64 --- /dev/null +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl @@ -0,0 +1,89 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_bridge_mqtt_schema). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1]). + +structs() -> ["emqx_bridge_mqtt"]. + +fields("emqx_bridge_mqtt") -> + [ {bridges, hoconsc:array(hoconsc:ref(?MODULE, "bridges"))} + ]; + +fields("bridges") -> + [ {name, emqx_schema:t(string(), undefined, true)} + , {start_type, fun start_type/1} + , {forwards, fun forwards/1} + , {forward_mountpoint, emqx_schema:t(string())} + , {reconnect_interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "30s")} + , {batch_size, emqx_schema:t(integer(), undefined, 100)} + , {queue, emqx_schema:t(hoconsc:ref(?MODULE, "queue"))} + , {config, hoconsc:union([hoconsc:ref(?MODULE, "mqtt"), hoconsc:ref(?MODULE, "rpc")])} + ]; + +fields("mqtt") -> + [ {conn_type, fun conn_type/1} + , {address, emqx_schema:t(string(), undefined, "127.0.0.1:1883")} + , {proto_ver, fun proto_ver/1} + , {bridge_mode, emqx_schema:t(boolean(), undefined, true)} + , {clientid, emqx_schema:t(string())} + , {username, emqx_schema:t(string())} + , {password, emqx_schema:t(string())} + , {clean_start, emqx_schema:t(boolean(), undefined, true)} + , {keepalive, emqx_schema:t(integer(), undefined, 300)} + , {subscriptions, hoconsc:array("subscriptions")} + , {receive_mountpoint, emqx_schema:t(string())} + , {retry_interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "30s")} + , {max_inflight, emqx_schema:t(integer(), undefined, 32)} + ]; + +fields("rpc") -> + [ {conn_type, fun conn_type/1} + , {node, emqx_schema:t(atom(), undefined, 'emqx@127.0.0.1')} + ]; + +fields("subscriptions") -> + [ {topic, #{type => binary(), nullable => false}} + , {qos, emqx_schema:t(integer(), undefined, 1)} + ]; + +fields("queue") -> + [ {replayq_dir, hoconsc:union([boolean(), string()])} + , {replayq_seg_bytes, emqx_schema:t(emqx_schema:bytesize(), undefined, "100MB")} + , {replayq_offload_mode, emqx_schema:t(boolean(), undefined, false)} + , {replayq_max_total_bytes, emqx_schema:t(emqx_schema:bytesize(), undefined, "1024MB")} + ]. + +conn_type(type) -> hoconsc:enum([mqtt, rpc]); +conn_type(_) -> undefined. + +proto_ver(type) -> hoconsc:enum([v3, v4, v5]); +proto_ver(default) -> v4; +proto_ver(_) -> undefined. + +start_type(type) -> hoconsc:enum([auto, manual]); +start_type(default) -> auto; +start_type(_) -> undefined. + +forwards(type) -> hoconsc:array(binary()); +forwards(default) -> []; +forwards(_) -> undefined. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl index 80a11c1c0..0075b4a1d 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl @@ -24,37 +24,33 @@ %% APIs -export([ start_link/0 - , start_link/1 ]). --export([ create_bridge/2 +-export([ create_bridge/1 , drop_bridge/1 , bridges/0 - , is_bridge_exist/1 ]). %% supervisor callbacks -export([init/1]). --define(SUP, ?MODULE). -define(WORKER_SUP, emqx_bridge_worker_sup). -start_link() -> start_link(?SUP). +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). -start_link(Name) -> - supervisor:start_link({local, Name}, ?MODULE, Name). - -init(?SUP) -> - BridgesConf = application:get_env(?APP, bridges, []), +init([]) -> + BridgesConf = emqx_config:get([?APP, bridges], []), BridgeSpec = lists:map(fun bridge_spec/1, BridgesConf), SupFlag = #{strategy => one_for_one, intensity => 100, period => 10}, {ok, {SupFlag, BridgeSpec}}. -bridge_spec({Name, Config}) -> +bridge_spec(Config) -> + Name = list_to_atom(maps:get(name, Config)), #{id => Name, - start => {emqx_bridge_worker, start_link, [Name, Config]}, + start => {emqx_bridge_worker, start_link, [Config]}, restart => permanent, shutdown => 5000, type => worker, @@ -62,22 +58,15 @@ bridge_spec({Name, Config}) -> -spec(bridges() -> [{node(), map()}]). bridges() -> - [{Name, emqx_bridge_worker:status(Pid)} || {Name, Pid, _, _} <- supervisor:which_children(?SUP)]. + [{Name, emqx_bridge_worker:status(Name)} || {Name, _Pid, _, _} <- supervisor:which_children(?MODULE)]. --spec(is_bridge_exist(atom() | pid()) -> boolean()). -is_bridge_exist(Id) -> - case supervisor:get_childspec(?SUP, Id) of - {ok, _ChildSpec} -> true; - {error, _Error} -> false - end. +create_bridge(Config) -> + supervisor:start_child(?MODULE, bridge_spec(Config)). -create_bridge(Id, Config) -> - supervisor:start_child(?SUP, bridge_spec({Id, Config})). - -drop_bridge(Id) -> - case supervisor:terminate_child(?SUP, Id) of +drop_bridge(Name) -> + case supervisor:terminate_child(?MODULE, Name) of ok -> - supervisor:delete_child(?SUP, Id); + supervisor:delete_child(?MODULE, Name); {error, Error} -> ?LOG(error, "Delete bridge failed, error : ~p", [Error]), {error, Error} diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl index 0cf4b5bc5..33511cc03 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl @@ -18,9 +18,6 @@ -module(emqx_bridge_rpc). --behaviour(emqx_bridge_connect). - -%% behaviour callbacks -export([ start/1 , send/2 , stop/1 @@ -33,17 +30,15 @@ -type ack_ref() :: emqx_bridge_worker:ack_ref(). -type batch() :: emqx_bridge_worker:batch(). --type node_or_tuple() :: atom() | {atom(), term()}. - -define(HEARTBEAT_INTERVAL, timer:seconds(1)). -define(RPC, emqx_rpc). -start(#{address := Remote}) -> - case poke(Remote) of +start(#{node := RemoteNode}) -> + case poke(RemoteNode) of ok -> - Pid = proc_lib:spawn_link(?MODULE, heartbeat, [self(), Remote]), - {ok, #{client_pid => Pid, address => Remote}}; + Pid = proc_lib:spawn_link(?MODULE, heartbeat, [self(), RemoteNode]), + {ok, #{client_pid => Pid, remote_node => RemoteNode}}; Error -> Error end. @@ -62,9 +57,9 @@ stop(#{client_pid := Pid}) when is_pid(Pid) -> ok. %% @doc Callback for `emqx_bridge_connect' behaviour --spec send(#{address := node_or_tuple(), _ => _}, batch()) -> {ok, ack_ref()} | {error, any()}. -send(#{address := Remote}, Batch) -> - case ?RPC:call(Remote, ?MODULE, handle_send, [Batch]) of +-spec send(#{remote_node := atom(), _ => _}, batch()) -> {ok, ack_ref()} | {error, any()}. +send(#{remote_node := RemoteNode}, Batch) -> + case ?RPC:call(RemoteNode, ?MODULE, handle_send, [Batch]) of ok -> Ref = make_ref(), self() ! {batch_ack, Ref}, @@ -93,8 +88,8 @@ heartbeat(Parent, RemoteNode) -> end end. -poke(Node) -> - case ?RPC:call(Node, erlang, node, []) of - Node -> ok; +poke(RemoteNode) -> + case ?RPC:call(RemoteNode, erlang, node, []) of + RemoteNode -> ok; {badrpc, Reason} -> {error, Reason} end. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl index e7414683c..dfef6973e 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl @@ -66,7 +66,6 @@ %% APIs -export([ start_link/1 - , start_link/2 , register_metrics/0 , stop/1 ]). @@ -86,7 +85,6 @@ %% management APIs -export([ ensure_started/1 , ensure_stopped/1 - , ensure_stopped/2 , status/1 ]). @@ -125,14 +123,13 @@ -define(DEFAULT_RECONNECT_DELAY_MS, timer:seconds(5)). -define(DEFAULT_SEG_BYTES, (1 bsl 20)). -define(DEFAULT_MAX_TOTAL_SIZE, (1 bsl 31)). --define(NO_BRIDGE_HANDLER, undefined). %% @doc Start a bridge worker. Supported configs: %% start_type: 'manual' (default) or 'auto', when manual, bridge will stay %% at 'idle' state until a manual call to start it. %% connect_module: The module which implements emqx_bridge_connect behaviour %% and work as message batch transport layer -%% reconnect_delay_ms: Delay in milli-seconds for the bridge worker to retry +%% reconnect_interval: Delay in milli-seconds for the bridge worker to retry %% in case of transportation failure. %% max_inflight: Max number of batches allowed to send-ahead before receiving %% confirmation from remote node/cluster @@ -148,128 +145,98 @@ %% %% Find more connection specific configs in the callback modules %% of emqx_bridge_connect behaviour. -start_link(Config) when is_list(Config) -> - start_link(maps:from_list(Config)); -start_link(Config) -> - gen_statem:start_link(?MODULE, Config, []). - -start_link(Name, Config) when is_list(Config) -> - start_link(Name, maps:from_list(Config)); -start_link(Name, Config) -> - Name1 = name(Name), - gen_statem:start_link({local, Name1}, ?MODULE, Config#{name => Name1}, []). +start_link(Opts) when is_list(Opts) -> + start_link(maps:from_list(Opts)); +start_link(Opts) -> + case maps:get(name, Opts, undefined) of + undefined -> + gen_statem:start_link(?MODULE, Opts, []); + Name -> + Name1 = name(Name), + gen_statem:start_link({local, Name1}, ?MODULE, Opts#{name => Name1}, []) + end. ensure_started(Name) -> gen_statem:call(name(Name), ensure_started). %% @doc Manually stop bridge worker. State idempotency ensured. -ensure_stopped(Id) -> - ensure_stopped(Id, 1000). - -ensure_stopped(Id, Timeout) -> - Pid = case id(Id) of - P when is_pid(P) -> P; - N -> whereis(N) - end, - case Pid of - undefined -> - ok; - _ -> - MRef = monitor(process, Pid), - unlink(Pid), - _ = gen_statem:call(id(Id), ensure_stopped, Timeout), - receive - {'DOWN', MRef, _, _, _} -> - ok - after - Timeout -> - exit(Pid, kill) - end - end. +ensure_stopped(Name) -> + gen_statem:call(name(Name), ensure_stopped, 5000). stop(Pid) -> gen_statem:stop(Pid). status(Pid) when is_pid(Pid) -> gen_statem:call(Pid, status); -status(Id) -> - gen_statem:call(name(Id), status). +status(Name) -> + gen_statem:call(name(Name), status). %% @doc Return all forwards (local subscriptions). -spec get_forwards(id()) -> [topic()]. -get_forwards(Id) -> gen_statem:call(id(Id), get_forwards, timer:seconds(1000)). +get_forwards(Name) -> gen_statem:call(name(Name), get_forwards, timer:seconds(1000)). %% @doc Return all subscriptions (subscription over mqtt connection to remote broker). -spec get_subscriptions(id()) -> [{emqx_topic:topic(), qos()}]. -get_subscriptions(Id) -> gen_statem:call(id(Id), get_subscriptions). +get_subscriptions(Name) -> gen_statem:call(name(Name), get_subscriptions). %% @doc Add a new forward (local topic subscription). -spec ensure_forward_present(id(), topic()) -> ok. -ensure_forward_present(Id, Topic) -> - gen_statem:call(id(Id), {ensure_present, forwards, topic(Topic)}). +ensure_forward_present(Name, Topic) -> + gen_statem:call(name(Name), {ensure_forward_present, topic(Topic)}). %% @doc Ensure a forward topic is deleted. -spec ensure_forward_absent(id(), topic()) -> ok. -ensure_forward_absent(Id, Topic) -> - gen_statem:call(id(Id), {ensure_absent, forwards, topic(Topic)}). +ensure_forward_absent(Name, Topic) -> + gen_statem:call(name(Name), {ensure_forward_absent, topic(Topic)}). %% @doc Ensure subscribed to remote topic. %% NOTE: only applicable when connection module is emqx_bridge_mqtt %% return `{error, no_remote_subscription_support}' otherwise. -spec ensure_subscription_present(id(), topic(), qos()) -> ok | {error, any()}. -ensure_subscription_present(Id, Topic, QoS) -> - gen_statem:call(id(Id), {ensure_present, subscriptions, {topic(Topic), QoS}}). +ensure_subscription_present(Name, Topic, QoS) -> + gen_statem:call(name(Name), {ensure_subscription_present, topic(Topic), QoS}). %% @doc Ensure unsubscribed from remote topic. %% NOTE: only applicable when connection module is emqx_bridge_mqtt -spec ensure_subscription_absent(id(), topic()) -> ok. -ensure_subscription_absent(Id, Topic) -> - gen_statem:call(id(Id), {ensure_absent, subscriptions, topic(Topic)}). +ensure_subscription_absent(Name, Topic) -> + gen_statem:call(name(Name), {ensure_subscription_absent, topic(Topic)}). callback_mode() -> [state_functions]. %% @doc Config should be a map(). -init(Config) -> +init(Opts) -> erlang:process_flag(trap_exit, true), - ConnectModule = maps:get(connect_module, Config), - Subscriptions = maps:get(subscriptions, Config, []), - Forwards = maps:get(forwards, Config, []), - Queue = open_replayq(Config), - State = init_opts(Config), - Topics = [iolist_to_binary(T) || T <- Forwards], - Subs = check_subscriptions(Subscriptions), - ConnectCfg = get_conn_cfg(Config), + ConnectOpts = maps:get(config, Opts), + ConnectModule = conn_type(maps:get(conn_type, ConnectOpts)), + Forwards = maps:get(forwards, Opts, []), + Queue = open_replayq(maps:get(queue, Opts, #{})), + State = init_opts(Opts), self() ! idle, {ok, idle, State#{connect_module => ConnectModule, - connect_cfg => ConnectCfg, - forwards => Topics, - subscriptions => Subs, + connect_opts => ConnectOpts, + forwards => Forwards, replayq => Queue }}. -init_opts(Config) -> - IfRecordMetrics = maps:get(if_record_metrics, Config, true), - ReconnDelayMs = maps:get(reconnect_delay_ms, Config, ?DEFAULT_RECONNECT_DELAY_MS), - StartType = maps:get(start_type, Config, manual), - BridgeHandler = maps:get(bridge_handler, Config, ?NO_BRIDGE_HANDLER), - Mountpoint = maps:get(forward_mountpoint, Config, undefined), - ReceiveMountpoint = maps:get(receive_mountpoint, Config, undefined), - MaxInflightSize = maps:get(max_inflight, Config, ?DEFAULT_BATCH_SIZE), - BatchSize = maps:get(batch_size, Config, ?DEFAULT_BATCH_SIZE), - Name = maps:get(name, Config, undefined), +init_opts(Opts) -> + IfRecordMetrics = maps:get(if_record_metrics, Opts, true), + ReconnDelayMs = maps:get(reconnect_interval, Opts, ?DEFAULT_RECONNECT_DELAY_MS), + StartType = maps:get(start_type, Opts, manual), + Mountpoint = maps:get(forward_mountpoint, Opts, undefined), + MaxInflightSize = maps:get(max_inflight, Opts, ?DEFAULT_BATCH_SIZE), + BatchSize = maps:get(batch_size, Opts, ?DEFAULT_BATCH_SIZE), + Name = maps:get(name, Opts, undefined), #{start_type => StartType, - reconnect_delay_ms => ReconnDelayMs, + reconnect_interval => ReconnDelayMs, batch_size => BatchSize, mountpoint => format_mountpoint(Mountpoint), - receive_mountpoint => ReceiveMountpoint, inflight => [], max_inflight => MaxInflightSize, connection => undefined, - bridge_handler => BridgeHandler, if_record_metrics => IfRecordMetrics, name => Name}. -open_replayq(Config) -> - QCfg = maps:get(queue, Config, #{}), +open_replayq(QCfg) -> Dir = maps:get(replayq_dir, QCfg, undefined), SegBytes = maps:get(replayq_seg_bytes, QCfg, ?DEFAULT_SEG_BYTES), MaxTotalSize = maps:get(max_total_size, QCfg, ?DEFAULT_MAX_TOTAL_SIZE), @@ -280,22 +247,6 @@ open_replayq(Config) -> replayq:open(QueueConfig#{sizer => fun emqx_bridge_msg:estimate_size/1, marshaller => fun ?MODULE:msg_marshaller/1}). -check_subscriptions(Subscriptions) -> - lists:map(fun({Topic, QoS}) -> - Topic1 = iolist_to_binary(Topic), - true = emqx_topic:validate({filter, Topic1}), - {Topic1, QoS} - end, Subscriptions). - -get_conn_cfg(Config) -> - maps:without([connect_module, - queue, - reconnect_delay_ms, - forwards, - mountpoint, - name - ], Config). - code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. @@ -321,14 +272,10 @@ idle(info, idle, #{start_type := auto} = State) -> idle(state_timeout, reconnect, State) -> connecting(State); -idle(info, {batch_ack, Ref}, State) -> - NewState = handle_batch_ack(State, Ref), - {keep_state, NewState}; - idle(Type, Content, State) -> common(idle, Type, Content, State). -connecting(#{reconnect_delay_ms := ReconnectDelayMs} = State) -> +connecting(#{reconnect_interval := ReconnectDelayMs} = State) -> case do_connect(State) of {ok, State1} -> {next_state, connected, State1, {state_timeout, 0, connected}}; @@ -348,7 +295,7 @@ connected(internal, maybe_send, State) -> {keep_state, NewState}; connected(info, {disconnected, Conn, Reason}, - #{connection := Connection, name := Name, reconnect_delay_ms := ReconnectDelayMs} = State) -> + #{connection := Connection, name := Name, reconnect_interval := ReconnectDelayMs} = State) -> ?tp(info, disconnected, #{name => Name, reason => Reason}), case Conn =:= maps:get(client_pid, Connection, undefined) of true -> @@ -365,19 +312,27 @@ connected(Type, Content, State) -> %% Common handlers common(StateName, {call, From}, status, _State) -> {keep_state_and_data, [{reply, From, StateName}]}; -common(_StateName, {call, From}, ensure_started, _State) -> - {keep_state_and_data, [{reply, From, connected}]}; -common(_StateName, {call, From}, ensure_stopped, _State) -> - {stop_and_reply, {shutdown, manual}, [{reply, From, ok}]}; +common(_StateName, {call, From}, ensure_stopped, #{connection := undefined} = _State) -> + {keep_state_and_data, [{reply, From, ok}]}; +common(_StateName, {call, From}, ensure_stopped, #{connection := Conn, + connect_module := ConnectModule} = State) -> + Reply = ConnectModule:stop(Conn), + {next_state, idle, State#{connection => undefined}, [{reply, From, Reply}]}; common(_StateName, {call, From}, get_forwards, #{forwards := Forwards}) -> {keep_state_and_data, [{reply, From, Forwards}]}; -common(_StateName, {call, From}, get_subscriptions, #{subscriptions := Subs}) -> - {keep_state_and_data, [{reply, From, Subs}]}; -common(_StateName, {call, From}, {ensure_present, What, Topic}, State) -> - {Result, NewState} = ensure_present(What, Topic, State), +common(_StateName, {call, From}, get_subscriptions, #{connection := Connection}) -> + {keep_state_and_data, [{reply, From, maps:get(subscriptions, Connection, [])}]}; +common(_StateName, {call, From}, {ensure_forward_present, Topic}, State) -> + {Result, NewState} = do_ensure_forward_present(Topic, State), {keep_state, NewState, [{reply, From, Result}]}; -common(_StateName, {call, From}, {ensure_absent, What, Topic}, State) -> - {Result, NewState} = ensure_absent(What, Topic, State), +common(_StateName, {call, From}, {ensure_subscription_present, Topic, QoS}, State) -> + {Result, NewState} = do_ensure_subscription_present(Topic, QoS, State), + {keep_state, NewState, [{reply, From, Result}]}; +common(_StateName, {call, From}, {ensure_forward_absent, Topic}, State) -> + {Result, NewState} = do_ensure_forward_absent(Topic, State), + {keep_state, NewState, [{reply, From, Result}]}; +common(_StateName, {call, From}, {ensure_subscription_absent, Topic}, State) -> + {Result, NewState} = do_ensure_subscription_absent(Topic, State), {keep_state, NewState, [{reply, From, Result}]}; common(_StateName, info, {deliver, _, Msg}, State = #{replayq := Q, if_record_metrics := IfRecordMetric}) -> @@ -395,77 +350,79 @@ common(StateName, Type, Content, #{name := Name} = State) -> [Name, Type, StateName, Content]), {keep_state, State}. -eval_bridge_handler(State = #{bridge_handler := ?NO_BRIDGE_HANDLER}, _Msg) -> - State; -eval_bridge_handler(State = #{bridge_handler := Handler}, Msg) -> - Handler(Msg), - State. - -ensure_present(Key, Topic, State) -> - Topics = maps:get(Key, State), - case is_topic_present(Topic, Topics) of +do_ensure_forward_present(Topic, #{forwards := Forwards, name := Name} = State) -> + case is_topic_present(Topic, Forwards) of true -> {ok, State}; false -> - R = do_ensure_present(Key, Topic, State), - {R, State#{Key := lists:usort([Topic | Topics])}} + R = subscribe_local_topic(Topic, Name), + {R, State#{forwards => [Topic | Forwards]}} end. -ensure_absent(Key, Topic, State) -> - Topics = maps:get(Key, State), - case is_topic_present(Topic, Topics) of +do_ensure_subscription_present(_Topic, _QoS, #{connection := undefined} = State) -> + {{error, no_connection}, State}; +do_ensure_subscription_present(_Topic, _QoS, #{connect_module := emqx_bridge_rpc} = State) -> + {{error, no_remote_subscription_support}, State}; +do_ensure_subscription_present(Topic, QoS, #{connect_module := ConnectModule, + connection := Conn} = State) -> + case is_topic_present(Topic, maps:get(subscriptions, Conn, [])) of true -> - R = do_ensure_absent(Key, Topic, State), - {R, State#{Key := ensure_topic_absent(Topic, Topics)}}; + {ok, State}; + false -> + case ConnectModule:ensure_subscribed(Conn, Topic, QoS) of + {error, Error} -> + {{error, Error}, State}; + Conn1 -> + {ok, State#{connection => Conn1}} + end + end. + +do_ensure_forward_absent(Topic, #{forwards := Forwards} = State) -> + case is_topic_present(Topic, Forwards) of + true -> + R = do_unsubscribe(Topic), + {R, State#{forwards => lists:delete(Topic, Forwards)}}; + false -> + {ok, State} + end. +do_ensure_subscription_absent(_Topic, #{connection := undefined} = State) -> + {{error, no_connection}, State}; +do_ensure_subscription_absent(_Topic, #{connect_module := emqx_bridge_rpc} = State) -> + {{error, no_remote_subscription_support}, State}; +do_ensure_subscription_absent(Topic, #{connect_module := ConnectModule, + connection := Conn} = State) -> + case is_topic_present(Topic, maps:get(subscriptions, Conn, [])) of + true -> + case ConnectModule:ensure_unsubscribed(Conn, Topic) of + {error, Error} -> + {{error, Error}, State}; + Conn1 -> + {ok, State#{connection => Conn1}} + end; false -> {ok, State} end. -ensure_topic_absent(_Topic, []) -> []; -ensure_topic_absent(Topic, [{_, _} | _] = L) -> lists:keydelete(Topic, 1, L); -ensure_topic_absent(Topic, L) -> lists:delete(Topic, L). - -is_topic_present({Topic, _QoS}, Topics) -> - is_topic_present(Topic, Topics); is_topic_present(Topic, Topics) -> lists:member(Topic, Topics) orelse false =/= lists:keyfind(Topic, 1, Topics). do_connect(#{forwards := Forwards, - subscriptions := Subs, connect_module := ConnectModule, - connect_cfg := ConnectCfg, + connect_opts := ConnectOpts, inflight := Inflight, name := Name} = State) -> ok = subscribe_local_topics(Forwards, Name), - case emqx_bridge_connect:start(ConnectModule, ConnectCfg#{subscriptions => Subs}) of + case ConnectModule:start(ConnectOpts) of {ok, Conn} -> - Res = eval_bridge_handler(State#{connection => Conn}, connected), ?tp(info, connected, #{name => Name, inflight => length(Inflight)}), - {ok, Res}; + {ok, State#{connection => Conn}}; {error, Reason} -> + ConnectOpts1 = obfuscate(ConnectOpts), + ?LOG(error, "Failed to connect with module=~p\n" + "config=~p\nreason:~p", [ConnectModule, ConnectOpts1, Reason]), {error, Reason, State} end. -do_ensure_present(forwards, Topic, #{name := Name}) -> - subscribe_local_topic(Topic, Name); -do_ensure_present(subscriptions, _Topic, #{connection := undefined}) -> - {error, no_connection}; -do_ensure_present(subscriptions, _Topic, #{connect_module := emqx_bridge_rpc}) -> - {error, no_remote_subscription_support}; -do_ensure_present(subscriptions, {Topic, QoS}, #{connect_module := ConnectModule, - connection := Conn}) -> - ConnectModule:ensure_subscribed(Conn, Topic, QoS). - -do_ensure_absent(forwards, Topic, _) -> - do_unsubscribe(Topic); -do_ensure_absent(subscriptions, _Topic, #{connection := undefined}) -> - {error, no_connection}; -do_ensure_absent(subscriptions, _Topic, #{connect_module := emqx_bridge_rpc}) -> - {error, no_remote_subscription_support}; -do_ensure_absent(subscriptions, Topic, #{connect_module := ConnectModule, - connection := Conn}) -> - ConnectModule:ensure_unsubscribed(Conn, Topic). - collect(Acc) -> receive {deliver, _, Msg} -> @@ -605,10 +562,9 @@ disconnect(#{connection := Conn, connect_module := Module } = State) when Conn =/= undefined -> Module:stop(Conn), - State0 = State#{connection => undefined}, - eval_bridge_handler(State0, disconnected); + State#{connection => undefined}; disconnect(State) -> - eval_bridge_handler(State, disconnected). + State. %% Called only when replayq needs to dump it to disk. msg_marshaller(Bin) when is_binary(Bin) -> emqx_bridge_msg:from_binary(Bin); @@ -621,9 +577,6 @@ format_mountpoint(Prefix) -> name(Id) -> list_to_atom(lists:concat([?MODULE, "_", Id])). -id(Pid) when is_pid(Pid) -> Pid; -id(Name) -> name(Name). - register_metrics() -> lists:foreach(fun emqx_metrics:ensure/1, ['bridge.mqtt.message_sent', @@ -639,3 +592,21 @@ bridges_metrics_inc(true, Metric, Value) -> emqx_metrics:inc(Metric, Value); bridges_metrics_inc(_IsRecordMetric, _Metric, _Value) -> ok. + +obfuscate(Map) -> + maps:fold(fun(K, V, Acc) -> + case is_sensitive(K) of + true -> [{K, '***'} | Acc]; + false -> [{K, V} | Acc] + end + end, [], Map). + +is_sensitive(password) -> true; +is_sensitive(_) -> false. + +conn_type(rpc) -> + emqx_bridge_rpc; +conn_type(mqtt) -> + emqx_bridge_mqtt; +conn_type(Mod) when is_atom(Mod) -> + Mod. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl index 830fb1fe0..5babe0ed9 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl +++ b/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl @@ -44,4 +44,4 @@ send_and_ack_test() -> ok = emqx_bridge_mqtt:stop(Conn) after meck:unload(emqtt) - end. \ No newline at end of file + end. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl index fdcc25d5f..cbd80ba3d 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl +++ b/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl @@ -30,8 +30,8 @@ send_and_ack_test() -> end), meck:new(emqx_bridge_worker, [passthrough, no_history]), try - {ok, #{client_pid := Pid, address := Node}} = emqx_bridge_rpc:start(#{address => node()}), - {ok, Ref} = emqx_bridge_rpc:send(#{address => Node}, []), + {ok, #{client_pid := Pid, remote_node := Node}} = emqx_bridge_rpc:start(#{node => node()}), + {ok, Ref} = emqx_bridge_rpc:send(#{remote_node => Node}, []), receive {batch_ack, Ref} -> ok diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl index d38663fcd..4c2fde6dd 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl +++ b/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl @@ -16,9 +16,6 @@ -module(emqx_bridge_stub_conn). --behaviour(emqx_bridge_connect). - -%% behaviour callbacks -export([ start/1 , send/2 , stop/1 diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl index 680756742..f3f5d5ceb 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl +++ b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl @@ -59,13 +59,12 @@ init_per_suite(Config) -> nonode@nohost -> net_kernel:start(['emqx@127.0.0.1', longnames]); _ -> ok end, - ok = application:set_env(gen_rpc, tcp_client_num, 1), - emqx_ct_helpers:start_apps([emqx_modules, emqx_bridge_mqtt]), + emqx_ct_helpers:start_apps([emqx_bridge_mqtt]), emqx_logger:set_log_level(error), [{log_level, error} | Config]. end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_bridge_mqtt, emqx_modules]). + emqx_ct_helpers:stop_apps([emqx_bridge_mqtt]). init_per_testcase(_TestCase, Config) -> ok = snabbkaffe:start_trace(), @@ -74,260 +73,290 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> ok = snabbkaffe:stop(). -t_mngr(Config) when is_list(Config) -> - Subs = [{<<"a">>, 1}, {<<"b">>, 2}], - Cfg = #{address => node(), - forwards => [<<"mngr">>], - connect_module => emqx_bridge_rpc, - mountpoint => <<"forwarded">>, - subscriptions => Subs, - start_type => auto}, - Name = ?FUNCTION_NAME, - {ok, Pid} = emqx_bridge_worker:start_link(Name, Cfg), - try - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr2")), - ?assertEqual([<<"mngr">>, <<"mngr2">>], emqx_bridge_worker:get_forwards(Pid)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr2")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr3")), - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Pid)), - ?assertEqual({error, no_remote_subscription_support}, - emqx_bridge_worker:ensure_subscription_present(Pid, <<"t">>, 0)), - ?assertEqual({error, no_remote_subscription_support}, - emqx_bridge_worker:ensure_subscription_absent(Pid, <<"t">>)), - ?assertEqual(Subs, emqx_bridge_worker:get_subscriptions(Pid)) - after - ok = emqx_bridge_worker:stop(Pid) - end. +t_rpc_mngr(_Config) -> + Name = "rpc_name", + Cfg = #{ + name => Name, + forwards => [<<"mngr">>], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + config => #{ + conn_type => rpc, + node => node() + } + }, + {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), + ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr")), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr2")), + ?assertEqual([<<"mngr2">>, <<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr2")), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr3")), + ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual({error, no_remote_subscription_support}, + emqx_bridge_worker:ensure_subscription_present(Name, <<"t">>, 0)), + ?assertEqual({error, no_remote_subscription_support}, + emqx_bridge_worker:ensure_subscription_absent(Name, <<"t">>)), + ok = emqx_bridge_worker:stop(Pid). + +t_mqtt_mngr(_Config) -> + Name = "mqtt_name", + Cfg = #{ + name => Name, + forwards => [<<"mngr">>], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + config => #{ + address => "127.0.0.1:1883", + conn_type => mqtt, + clientid => <<"client1">>, + keepalive => 300, + subscriptions => [#{topic => <<"t/#">>, qos => 1}] + } + }, + {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), + ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr")), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr2")), + ?assertEqual([<<"mngr2">>, <<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr2")), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr3")), + ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual(ok, emqx_bridge_worker:ensure_subscription_present(Name, <<"t">>, 0)), + ?assertEqual(ok, emqx_bridge_worker:ensure_subscription_absent(Name, <<"t">>)), + ?assertEqual([{<<"t/#">>,1}], emqx_bridge_worker:get_subscriptions(Name)), + ok = emqx_bridge_worker:stop(Pid). %% A loopback RPC to local node -t_rpc(Config) when is_list(Config) -> - Cfg = #{address => node(), - forwards => [<<"t_rpc/#">>], - connect_module => emqx_bridge_rpc, - forward_mountpoint => <<"forwarded">>, - start_type => auto}, - {ok, Pid} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), - ClientId = <<"ClientId">>, - try - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _Props} = emqtt:connect(ConnPid), - {ok, _Props, [1]} = emqtt:subscribe(ConnPid, {<<"forwarded/t_rpc/one">>, ?QOS_1}), - timer:sleep(100), - {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_rpc/one">>, <<"hello">>, ?QOS_1), - timer:sleep(100), - ?assertEqual(1, length(receive_messages(1))), - emqtt:disconnect(ConnPid) - after - ok = emqx_bridge_worker:stop(Pid) - end. +t_rpc(_Config) -> + Name = "rpc", + Cfg = #{ + name => Name, + forwards => [<<"t_rpc/#">>], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + config => #{ + conn_type => rpc, + node => node() + } + }, + {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), + {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), + {ok, _Props} = emqtt:connect(ConnPid), + {ok, _Props, [1]} = emqtt:subscribe(ConnPid, {<<"forwarded/t_rpc/one">>, ?QOS_1}), + timer:sleep(100), + {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_rpc/one">>, <<"hello">>, ?QOS_1), + timer:sleep(100), + ?assertEqual(1, length(receive_messages(1))), + emqtt:disconnect(ConnPid), + emqx_bridge_worker:stop(Pid). %% Full data loopback flow explained: %% mqtt-client ----> local-broker ---(local-subscription)---> %% bridge(export) --- (mqtt-connection)--> local-broker ---(remote-subscription) --> %% bridge(import) --> mqtt-client -t_mqtt(Config) when is_list(Config) -> +t_mqtt(_Config) -> SendToTopic = <<"t_mqtt/one">>, SendToTopic2 = <<"t_mqtt/two">>, SendToTopic3 = <<"t_mqtt/three">>, Mountpoint = <<"forwarded/${node}/">>, - Cfg = #{address => "127.0.0.1:1883", - forwards => [SendToTopic], - connect_module => emqx_bridge_mqtt, - forward_mountpoint => Mountpoint, - username => "user", - clean_start => true, - clientid => "bridge_aws", - keepalive => 60000, - password => "passwd", - proto_ver => mqttv4, - queue => #{replayq_dir => "data/t_mqtt/", - replayq_seg_bytes => 10000, - batch_bytes_limit => 1000, - batch_count_limit => 10 - }, - reconnect_delay_ms => 1000, - ssl => false, - %% Consume back to forwarded message for verification - %% NOTE: this is a indefenite loopback without mocking emqx_bridge_worker:import_batch/1 - subscriptions => [{SendToTopic2, _QoS = 1}], - receive_mountpoint => <<"receive/aws/">>, - start_type => auto}, - {ok, Pid} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), - ClientId = <<"client-1">>, - try - ?assertEqual([{SendToTopic2, 1}], emqx_bridge_worker:get_subscriptions(Pid)), - ok = emqx_bridge_worker:ensure_subscription_present(Pid, SendToTopic3, _QoS = 1), - ?assertEqual([{SendToTopic3, 1},{SendToTopic2, 1}], - emqx_bridge_worker:get_subscriptions(Pid)), - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _Props} = emqtt:connect(ConnPid), - emqtt:subscribe(ConnPid, <<"forwarded/+/t_mqtt/one">>, 1), - %% message from a different client, to avoid getting terminated by no-local - Max = 10, - Msgs = lists:seq(1, Max), - lists:foreach(fun(I) -> - {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic, integer_to_binary(I), ?QOS_1) - end, Msgs), - ?assertEqual(10, length(receive_messages(200))), + Name = "mqtt", + Cfg = #{ + name => Name, + forwards => [SendToTopic], + forward_mountpoint => Mountpoint, + start_type => auto, + config => #{ + address => "127.0.0.1:1883", + conn_type => mqtt, + clientid => <<"client1">>, + keepalive => 300, + subscriptions => [#{topic => SendToTopic2, qos => 1}], + receive_mountpoint => <<"receive/aws/">> + }, + queue => #{ + replayq_dir => "data/t_mqtt/", + replayq_seg_bytes => 10000, + batch_bytes_limit => 1000, + batch_count_limit => 10 + } + }, + {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), + ?assertEqual([{SendToTopic2, 1}], emqx_bridge_worker:get_subscriptions(Name)), + ok = emqx_bridge_worker:ensure_subscription_present(Name, SendToTopic3, _QoS = 1), + ?assertEqual([{SendToTopic3, 1},{SendToTopic2, 1}], + emqx_bridge_worker:get_subscriptions(Name)), + {ok, ConnPid} = emqtt:start_link([{clientid, <<"client-1">>}]), + {ok, _Props} = emqtt:connect(ConnPid), + emqtt:subscribe(ConnPid, <<"forwarded/+/t_mqtt/one">>, 1), + %% message from a different client, to avoid getting terminated by no-local + Max = 10, + Msgs = lists:seq(1, Max), + lists:foreach(fun(I) -> + {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic, integer_to_binary(I), ?QOS_1) + end, Msgs), + ?assertEqual(10, length(receive_messages(200))), - emqtt:subscribe(ConnPid, <<"receive/aws/t_mqtt/two">>, 1), - %% message from a different client, to avoid getting terminated by no-local - Max = 10, - Msgs = lists:seq(1, Max), - lists:foreach(fun(I) -> - {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic2, integer_to_binary(I), ?QOS_1) - end, Msgs), - ?assertEqual(10, length(receive_messages(200))), + emqtt:subscribe(ConnPid, <<"receive/aws/t_mqtt/two">>, 1), + %% message from a different client, to avoid getting terminated by no-local + Max = 10, + Msgs = lists:seq(1, Max), + lists:foreach(fun(I) -> + {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic2, integer_to_binary(I), ?QOS_1) + end, Msgs), + ?assertEqual(10, length(receive_messages(200))), - emqtt:disconnect(ConnPid) - after - ok = emqx_bridge_worker:stop(Pid) - end. + emqtt:disconnect(ConnPid), + ok = emqx_bridge_worker:stop(Pid). t_stub_normal(Config) when is_list(Config) -> - Cfg = #{forwards => [<<"t_stub_normal/#">>], - connect_module => emqx_bridge_stub_conn, - forward_mountpoint => <<"forwarded">>, - start_type => auto, + Name = "stub_normal", + Cfg = #{ + name => Name, + forwards => [<<"t_stub_normal/#">>], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + config => #{ + conn_type => emqx_bridge_stub_conn, client_pid => self() - }, - {ok, Pid} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), + } + }, + {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), receive {Pid, emqx_bridge_stub_conn, ready} -> ok after 5000 -> error(timeout) end, - ClientId = <<"ClientId">>, - try - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_stub_normal/one">>, <<"hello">>, ?QOS_1), - receive - {stub_message, WorkerPid, BatchRef, _Batch} -> - WorkerPid ! {batch_ack, BatchRef}, - ok - after - 5000 -> - error(timeout) - end, - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid) + {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), + {ok, _} = emqtt:connect(ConnPid), + {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_stub_normal/one">>, <<"hello">>, ?QOS_1), + receive + {stub_message, WorkerPid, BatchRef, _Batch} -> + WorkerPid ! {batch_ack, BatchRef}, + ok after - ok = emqx_bridge_worker:stop(Pid) - end. + 5000 -> + error(timeout) + end, + ?SNK_WAIT(inflight_drained), + ?SNK_WAIT(replayq_drained), + emqtt:disconnect(ConnPid), + ok = emqx_bridge_worker:stop(Pid). -t_stub_overflow(Config) when is_list(Config) -> +t_stub_overflow(_Config) -> Topic = <<"t_stub_overflow/one">>, MaxInflight = 20, - Cfg = #{forwards => [Topic], - connect_module => emqx_bridge_stub_conn, - forward_mountpoint => <<"forwarded">>, - start_type => auto, - client_pid => self(), - max_inflight => MaxInflight - }, - {ok, Worker} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), - ClientId = <<"ClientId">>, - try - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight * 2)), - ?SNK_WAIT(inflight_full), - Acks = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks), - Acks2 = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks2), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid) - after - ok = emqx_bridge_worker:stop(Worker) - end. + Name = "stub_overflow", + Cfg = #{ + name => Name, + forwards => [<<"t_stub_overflow/one">>], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + max_inflight => MaxInflight, + config => #{ + conn_type => emqx_bridge_stub_conn, + client_pid => self() + } + }, + {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), + {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), + {ok, _} = emqtt:connect(ConnPid), + lists:foreach( + fun(I) -> + Data = integer_to_binary(I), + _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) + end, lists:seq(1, MaxInflight * 2)), + ?SNK_WAIT(inflight_full), + Acks = stub_receive(MaxInflight), + lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks), + Acks2 = stub_receive(MaxInflight), + lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks2), + ?SNK_WAIT(inflight_drained), + ?SNK_WAIT(replayq_drained), + emqtt:disconnect(ConnPid), + ok = emqx_bridge_worker:stop(Worker). -t_stub_random_order(Config) when is_list(Config) -> +t_stub_random_order(_Config) -> Topic = <<"t_stub_random_order/a">>, MaxInflight = 10, - Cfg = #{forwards => [Topic], - connect_module => emqx_bridge_stub_conn, - forward_mountpoint => <<"forwarded">>, - start_type => auto, - client_pid => self(), - max_inflight => MaxInflight - }, - {ok, Worker} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), + Name = "stub_random_order", + Cfg = #{ + name => Name, + forwards => [Topic], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + max_inflight => MaxInflight, + config => #{ + conn_type => emqx_bridge_stub_conn, + client_pid => self() + } + }, + {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), ClientId = <<"ClientId">>, - try - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight)), - Acks = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, - lists:reverse(Acks)), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid) - after - ok = emqx_bridge_worker:stop(Worker) - end. + {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), + {ok, _} = emqtt:connect(ConnPid), + lists:foreach( + fun(I) -> + Data = integer_to_binary(I), + _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) + end, lists:seq(1, MaxInflight)), + Acks = stub_receive(MaxInflight), + lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, + lists:reverse(Acks)), + ?SNK_WAIT(inflight_drained), + ?SNK_WAIT(replayq_drained), + emqtt:disconnect(ConnPid), + ok = emqx_bridge_worker:stop(Worker). -t_stub_retry_inflight(Config) when is_list(Config) -> +t_stub_retry_inflight(_Config) -> Topic = <<"to_stub_retry_inflight/a">>, MaxInflight = 10, - Cfg = #{forwards => [Topic], - connect_module => emqx_bridge_stub_conn, - forward_mountpoint => <<"forwarded">>, - reconnect_delay_ms => 10, - start_type => auto, - client_pid => self(), - max_inflight => MaxInflight - }, - {ok, Worker} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), + Name = "stub_retry_inflight", + Cfg = #{ + name => Name, + forwards => [Topic], + forward_mountpoint => <<"forwarded">>, + reconnect_interval => 10, + start_type => auto, + max_inflight => MaxInflight, + config => #{ + conn_type => emqx_bridge_stub_conn, + client_pid => self() + } + }, + {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), ClientId = <<"ClientId2">>, - try - case ?block_until(#{?snk_kind := connected, inflight := 0}, 2000, 1000) of - {ok, #{inflight := 0}} -> ok; - Other -> ct:fail("~p", [Other]) - end, - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight)), - %% receive acks but do not ack - Acks1 = stub_receive(MaxInflight), - ?assertEqual(MaxInflight, length(Acks1)), - %% simulate a disconnect - Worker ! {disconnected, self(), test}, - ?SNK_WAIT(disconnected), - case ?block_until(#{?snk_kind := connected, inflight := MaxInflight}, 2000, 20) of - {ok, _} -> ok; - Error -> ct:fail("~p", [Error]) - end, - %% expect worker to retry inflight, so to receive acks again - Acks2 = stub_receive(MaxInflight), - ?assertEqual(MaxInflight, length(Acks2)), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, - lists:reverse(Acks2)), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid) - after - ok = emqx_bridge_worker:stop(Worker) - end. + case ?block_until(#{?snk_kind := connected, inflight := 0}, 2000, 1000) of + {ok, #{inflight := 0}} -> ok; + Other -> ct:fail("~p", [Other]) + end, + {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), + {ok, _} = emqtt:connect(ConnPid), + lists:foreach( + fun(I) -> + Data = integer_to_binary(I), + _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) + end, lists:seq(1, MaxInflight)), + %% receive acks but do not ack + Acks1 = stub_receive(MaxInflight), + ?assertEqual(MaxInflight, length(Acks1)), + %% simulate a disconnect + Worker ! {disconnected, self(), test}, + ?SNK_WAIT(disconnected), + case ?block_until(#{?snk_kind := connected, inflight := MaxInflight}, 2000, 20) of + {ok, _} -> ok; + Error -> ct:fail("~p", [Error]) + end, + %% expect worker to retry inflight, so to receive acks again + Acks2 = stub_receive(MaxInflight), + ?assertEqual(MaxInflight, length(Acks2)), + lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, + lists:reverse(Acks2)), + ?SNK_WAIT(inflight_drained), + ?SNK_WAIT(replayq_drained), + emqtt:disconnect(ConnPid), + ok = emqx_bridge_worker:stop(Worker). stub_receive(N) -> stub_receive(N, []). diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl index 69ff87356..ffa2e9ee5 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl +++ b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl @@ -15,7 +15,6 @@ %%-------------------------------------------------------------------- -module(emqx_bridge_worker_tests). --behaviour(emqx_bridge_connect). -include_lib("eunit/include/eunit.hrl"). -include_lib("emqx/include/emqx.hrl"). @@ -69,14 +68,14 @@ disturbance_test() -> emqx_bridge_worker:register_metrics(), Ref = make_ref(), TestPid = self(), - Config = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), - {ok, Pid} = emqx_bridge_worker:start_link(?BRIDGE_NAME, Config), - ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), + Config = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), + {ok, Pid} = emqx_bridge_worker:start_link(Config#{name => disturbance}), + ?assertEqual(Pid, whereis(emqx_bridge_worker_disturbance)), ?WAIT({connection_start_attempt, Ref}, 1000), Pid ! {disconnected, TestPid, test}, ?WAIT({connection_start_attempt, Ref}, 1000), emqx_metrics:stop(), - ok = emqx_bridge_worker:stop(?BRIDGE_REG_NAME). + ok = emqx_bridge_worker:stop(Pid). % % %% buffer should continue taking in messages when disconnected % buffer_when_disconnected_test_() -> @@ -113,22 +112,24 @@ manual_start_stop_test() -> emqx_bridge_worker:register_metrics(), Ref = make_ref(), TestPid = self(), + BridgeName = manual_start_stop, Config0 = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), Config = Config0#{start_type := manual}, - {ok, Pid} = emqx_bridge_worker:start_link(?BRIDGE_NAME, Config), + {ok, Pid} = emqx_bridge_worker:start_link(Config#{name => BridgeName}), %% call ensure_started again should yeld the same result - ok = emqx_bridge_worker:ensure_started(?BRIDGE_NAME), - ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), - emqx_bridge_worker:ensure_stopped(unknown), - emqx_bridge_worker:ensure_stopped(Pid), - emqx_bridge_worker:ensure_stopped(?BRIDGE_REG_NAME), - emqx_metrics:stop(). + ok = emqx_bridge_worker:ensure_started(BridgeName), + emqx_bridge_worker:ensure_stopped(BridgeName), + emqx_metrics:stop(), + ok = emqx_bridge_worker:stop(Pid). make_config(Ref, TestPid, Result) -> - #{test_pid => TestPid, - test_ref => Ref, - connect_module => ?MODULE, - reconnect_delay_ms => 50, - connect_result => Result, - start_type => auto - }. + #{ + start_type => auto, + reconnect_interval => 50, + config => #{ + test_pid => TestPid, + test_ref => Ref, + conn_type => ?MODULE, + connect_result => Result + } + }. diff --git a/apps/emqx_coap/src/emqx_coap.app.src b/apps/emqx_coap/src/emqx_coap.app.src index 2b5fcbb6a..bb6d0431f 100644 --- a/apps/emqx_coap/src/emqx_coap.app.src +++ b/apps/emqx_coap/src/emqx_coap.app.src @@ -1,6 +1,6 @@ {application, emqx_coap, [{description, "EMQ X CoAP Gateway"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, {applications, [kernel,stdlib,gen_coap]}, diff --git a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl index d465f9ca3..315cfbb5c 100644 --- a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl +++ b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl @@ -105,8 +105,8 @@ call(Pid, Msg, _) -> %%-------------------------------------------------------------------- init({ClientId, Username, Password, Channel}) -> - ?LOG(debug, "try to start adapter ClientId=~p, Username=~p, Password=~p, " - "Channel=~0p", [ClientId, Username, Password, Channel]), + ?LOG(debug, "try to start adapter ClientId=~p, Username=~p, " + "Channel=~0p", [ClientId, Username, Channel]), State0 = #state{peername = Channel, clientid = ClientId, username = Username, @@ -222,7 +222,7 @@ code_change(_OldVsn, State, _Extra) -> chann_subscribe(Topic, State = #state{clientid = ClientId}) -> ?LOG(debug, "subscribe Topic=~p", [Topic]), - case emqx_access_control:check_acl(clientinfo(State), subscribe, Topic) of + case emqx_access_control:authorize(clientinfo(State), subscribe, Topic) of allow -> emqx_broker:subscribe(Topic, ClientId, ?SUBOPTS), emqx_hooks:run('session.subscribed', [clientinfo(State), Topic, ?SUBOPTS]), @@ -241,7 +241,7 @@ chann_unsubscribe(Topic, State) -> chann_publish(Topic, Payload, State = #state{clientid = ClientId}) -> ?LOG(debug, "publish Topic=~p, Payload=~p", [Topic, Payload]), - case emqx_access_control:check_acl(clientinfo(State), publish, Topic) of + case emqx_access_control:authorize(clientinfo(State), publish, Topic) of allow -> _ = emqx_broker:publish( emqx_message:set_flag(retain, false, @@ -384,4 +384,3 @@ clientinfo(#state{peername = {PeerHost, _}, mountpoint => undefined, ws_cookie => undefined }. - diff --git a/apps/emqx_coap/test/emqx_coap_SUITE.erl b/apps/emqx_coap/test/emqx_coap_SUITE.erl index 440b80ebd..9618425a3 100644 --- a/apps/emqx_coap/test/emqx_coap_SUITE.erl +++ b/apps/emqx_coap/test/emqx_coap_SUITE.erl @@ -77,7 +77,7 @@ t_publish_acl_deny(_Config) -> emqx:subscribe(Topic), ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]), - ok = meck:expect(emqx_access_control, check_acl, 3, deny), + ok = meck:expect(emqx_access_control, authorize, 3, deny), Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), ?assertEqual({error,forbidden}, Reply), ok = meck:unload(emqx_access_control), @@ -114,7 +114,7 @@ t_observe_acl_deny(_Config) -> Topic = <<"abc">>, TopicStr = binary_to_list(Topic), Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]), - ok = meck:expect(emqx_access_control, check_acl, 3, deny), + ok = meck:expect(emqx_access_control, authorize, 3, deny), ?assertEqual({error,forbidden}, er_coap_observer:observe(Uri)), [] = emqx:subscribers(Topic), ok = meck:unload(emqx_access_control). @@ -266,11 +266,18 @@ t_kick_1(_Config) -> % mqtt connection kicked by coap with same client id t_acl(Config) -> - %% Update acl file and reload mod_acl_internal - Path = filename:join([testdir(proplists:get_value(data_dir, Config)), "deny.conf"]), - ok = file:write_file(Path, <<"{deny, {user, \"coap\"}, publish, [\"abc\"]}.">>), - OldPath = emqx:get_env(acl_file), - emqx_mod_acl_internal:reload([{acl_file, Path}]), + OldPath = emqx:get_env(plugins_etc_dir), + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_authz, "test")), + Conf = #{<<"authz">> => + #{<<"rules">> => + [#{<<"principal">> =>#{<<"username">> => <<"coap">>}, + <<"permission">> => deny, + <<"topics">> => [<<"abc">>], + <<"action">> => <<"publish">>} + ]}}, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), + application:ensure_all_started(emqx_authz), emqx:subscribe(<<"abc">>), URI = "coap://127.0.0.1/mqtt/adbc?c=client1&u=coap&p=secret", @@ -282,9 +289,10 @@ t_acl(Config) -> ok end, - application:set_env(emqx, acl_file, OldPath), - file:delete(Path), - emqx_mod_acl_internal:reload([{acl_file, OldPath}]). + ok = emqx_hooks:del('client.authorize', {emqx_authz, authorize}), + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), + application:set_env(emqx, plugins_etc_dir, OldPath), + application:stop(emqx_authz). t_stats(_) -> ok. diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index 53b5c63e8..b12bd3edb 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -4,7 +4,21 @@ ]}. {deps, [ - {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}} + {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}, + {eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}}, + {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}, + {epgsql, {git, "https://github.com/epgsql/epgsql", {tag, "4.4.0"}}}, + %% NOTE: mind poolboy version when updating mongodb-erlang version + {mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.7"}}}, + %% NOTE: mind poolboy version when updating eredis_cluster version + {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.6.7"}}}, + %% mongodb-erlang uses a special fork https://github.com/comtihon/poolboy.git + %% (which has overflow_ttl feature added). + %% However, it references `{branch, "master}` (commit 9c06a9a on 2021-04-07). + %% By accident, We have always been using the upstream fork due to + %% eredis_cluster's dependency getting resolved earlier. + %% Here we pin 1.5.2 to avoid surprises in the future. + {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}} ]}. {shell, [ diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 5ff7d0828..0b8717a0f 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/apps/emqx_connector/src/emqx_connector.app.src @@ -1,14 +1,18 @@ {application, emqx_connector, [{description, "An OTP application"}, - {vsn, "0.1.0"}, + {vsn, "0.1.1"}, {registered, []}, {mod, {emqx_connector_app, []}}, {applications, [kernel, stdlib, + ecpool, emqx_resource, eredis_cluster, - ecpool + eredis, + epgsql, + mysql, + mongodb ]}, {env,[]}, {modules, []}, diff --git a/apps/emqx_connector/src/emqx_connector_ldap.erl b/apps/emqx_connector/src/emqx_connector_ldap.erl index ca5ff2482..8c0504d53 100644 --- a/apps/emqx_connector/src/emqx_connector_ldap.erl +++ b/apps/emqx_connector/src/emqx_connector_ldap.erl @@ -38,7 +38,7 @@ structs() -> [""]. fields("") -> - redis_fields() ++ + ldap_fields() ++ emqx_connector_schema_lib:ssl_fields(). on_jsonify(Config) -> @@ -51,10 +51,17 @@ on_start(InstId, #{servers := Servers0, bind_password := BindPassword, timeout := Timeout, pool_size := PoolSize, - auto_reconnect := AutoReconn} = Config) -> - logger:info("starting redis connector: ~p, config: ~p", [InstId, Config]), + auto_reconnect := AutoReconn, + ssl := SSL} = Config) -> + logger:info("starting ldap connector: ~p, config: ~p", [InstId, Config]), Servers = [begin proplists:get_value(host, S) end || S <- Servers0], - SslOpts = init_ssl_opts(Config, InstId), + SslOpts = case maps:get(enable, SSL) of + true -> + [{ssl, true}, + {sslopts, emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)} + ]; + false -> [{ssl, false}] + end, Opts = [{servers, Servers}, {port, Port}, {bind_dn, BindDn}, @@ -68,14 +75,14 @@ on_start(InstId, #{servers := Servers0, {ok, #{poolname => PoolName}}. on_stop(InstId, #{poolname := PoolName}) -> - logger:info("stopping redis connector: ~p", [InstId]), + logger:info("stopping ldap connector: ~p", [InstId]), emqx_plugin_libs_pool:stop_pool(PoolName). on_query(InstId, {search, Base, Filter, Attributes}, AfterQuery, #{poolname := PoolName} = State) -> - logger:debug("redis connector ~p received request: ~p, at state: ~p", [InstId, {Base, Filter, Attributes}, State]), + logger:debug("ldap connector ~p received request: ~p, at state: ~p", [InstId, {Base, Filter, Attributes}, State]), case Result = ecpool:pick_and_do(PoolName, {?MODULE, search, [Base, Filter, Attributes]}, no_handover) of {error, Reason} -> - logger:debug("redis connector ~p do request failed, request: ~p, reason: ~p", [InstId, {Base, Filter, Attributes}, Reason]), + logger:debug("ldap connector ~p do request failed, request: ~p, reason: ~p", [InstId, {Base, Filter, Attributes}, Reason]), emqx_resource:query_failed(AfterQuery); _ -> emqx_resource:query_success(AfterQuery) @@ -116,14 +123,7 @@ connect(Opts) -> ok = eldap2:simple_bind(LDAP, BindDn, BindPassword), {ok, LDAP}. -init_ssl_opts(#{ssl := true} = Config, InstId) -> - [{ssl, true}, - {sslopts, emqx_plugin_libs_ssl:save_files_return_opts(Config, "connectors", InstId)} - ]; -init_ssl_opts(_Config, _InstId) -> - [{ssl, false}]. - -redis_fields() -> +ldap_fields() -> [ {servers, fun emqx_connector_schema_lib:servers/1} , {port, fun port/1} , {pool_size, fun emqx_connector_schema_lib:pool_size/1} diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 841cc0a2a..b8a3c0da0 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -19,6 +19,9 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). +-type server() :: string(). +-reflect_type([server/0]). + %% callbacks of behaviour emqx_resource -export([ on_start/2 , on_stop/2 @@ -36,38 +39,78 @@ structs() -> [""]. fields("") -> - mongodb_fields() ++ - mongodb_topology_fields() ++ - mongodb_rs_set_name_fields() ++ + [ {config, #{type => hoconsc:union( + [ hoconsc:ref(?MODULE, single) + , hoconsc:ref(?MODULE, rs) + , hoconsc:ref(?MODULE, sharded) + ])}} + ]; +fields(single) -> + [ {mongo_type, #{type => single, + default => single}} + , {server, fun server/1} + ] ++ mongo_fields(); +fields(rs) -> + [ {mongo_type, #{type => rs, + default => rs}} + , {servers, fun servers/1} + , {replicaset_name, fun emqx_connector_schema_lib:database/1} + ] ++ mongo_fields(); +fields(sharded) -> + [ {mongo_type, #{type => sharded, + default => sharded}} + , {servers, fun servers/1} + ] ++ mongo_fields(); +fields(topology) -> + [ {max_overflow, fun emqx_connector_schema_lib:pool_size/1} + , {overflow_ttl, fun duration/1} + , {overflow_check_period, fun duration/1} + , {local_threshold_ms, fun duration/1} + , {connect_timeout_ms, fun duration/1} + , {socket_timeout_ms, fun duration/1} + , {server_selection_timeout_ms, fun duration/1} + , {wait_queue_timeout_ms, fun duration/1} + , {heartbeat_frequency_ms, fun duration/1} + , {min_heartbeat_frequency_ms, fun duration/1} + ]. + +mongo_fields() -> + [ {pool_size, fun emqx_connector_schema_lib:pool_size/1} + , {username, fun emqx_connector_schema_lib:username/1} + , {password, fun emqx_connector_schema_lib:password/1} + , {authentication_database, #{type => binary(), + nullable => true}} + , {database, fun emqx_connector_schema_lib:database/1} + ] ++ emqx_connector_schema_lib:ssl_fields(). on_jsonify(Config) -> Config. %% =================================================================== -on_start(InstId, #{servers := Servers, - mongo_type := Type, - database := Database, - pool_size := PoolSize} = Config) -> +on_start(InstId, #{config := #{server := Server, + mongo_type := single} = Config}) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), - SslOpts = init_ssl_opts(Config, InstId), - Hosts = [string:trim(H) || H <- string:tokens(binary_to_list(Servers), ",")], - Opts = [{type, init_type(Type, Config)}, - {hosts, Hosts}, - {pool_size, PoolSize}, - {options, init_topology_options(maps:to_list(Config), [])}, - {worker_options, init_worker_options(maps:to_list(Config), SslOpts)}], + Opts = [{type, single}, + {hosts, [Server]} + ], + do_start(InstId, Opts, Config); - %% test the connection - TestOpts = [{database, Database}] ++ host_port(hd(Hosts)), - {ok, TestConn} = mc_worker_api:connect(TestOpts), +on_start(InstId, #{config := #{servers := Servers, + mongo_type := rs, + replicaset_name := RsName} = Config}) -> + logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), + Opts = [{type, {rs, RsName}}, + {hosts, Servers}], + do_start(InstId, Opts, Config); - PoolName = emqx_plugin_libs_pool:pool_name(InstId), - _ = emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ SslOpts), - {ok, #{pool => PoolName, - type => Type, - test_conn => TestConn, - test_opts => TestOpts}}. +on_start(InstId, #{config := #{servers := Servers, + mongo_type := sharded} = Config}) -> + logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), + Opts = [{type, sharded}, + {hosts, Servers} + ], + do_start(InstId, Opts, Config). on_stop(InstId, #{poolname := PoolName}) -> logger:info("stopping mongodb connector: ~p", [InstId]), @@ -75,23 +118,27 @@ on_stop(InstId, #{poolname := PoolName}) -> on_query(InstId, {Action, Collection, Selector, Docs}, AfterQuery, #{poolname := PoolName} = State) -> logger:debug("mongodb connector ~p received request: ~p, at state: ~p", [InstId, {Action, Collection, Selector, Docs}, State]), - case Result = ecpool:pick_and_do(PoolName, {?MODULE, mongo_query, [Action, Collection, Selector, Docs]}, no_handover) of + case ecpool:pick_and_do(PoolName, {?MODULE, mongo_query, [Action, Collection, Selector, Docs]}, no_handover) of {error, Reason} -> logger:debug("mongodb connector ~p do sql query failed, request: ~p, reason: ~p", [InstId, {Action, Collection, Selector, Docs}, Reason]), - emqx_resource:query_failed(AfterQuery); - _ -> - emqx_resource:query_success(AfterQuery) - end, - Result. + emqx_resource:query_failed(AfterQuery), + {error, Reason}; + {ok, Cursor} when is_pid(Cursor) -> + emqx_resource:query_success(AfterQuery), + mc_cursor:foldl(fun(O, Acc2) -> [O|Acc2] end, [], Cursor, 1000); + Result -> + emqx_resource:query_success(AfterQuery), + Result + end. -dialyzer({nowarn_function, [on_health_check/2]}). -on_health_check(_InstId, #{test_opts := TestOpts}) -> +on_health_check(_InstId, #{test_opts := TestOpts} = State) -> case mc_worker_api:connect(TestOpts) of {ok, TestConn} -> mc_worker_api:disconnect(TestConn), - {ok, true}; + {ok, State}; {error, _} -> - {ok, false} + {error, health_check_failed, State} end. %% =================================================================== @@ -109,10 +156,38 @@ mongo_query(Conn, find, Collection, Selector, Docs) -> mongo_query(_Conn, _Action, _Collection, _Selector, _Docs) -> ok. -init_type(rs, #{rs_set_name := Name}) -> - {rs, Name}; -init_type(Type, _Opts) -> - Type. +do_start(InstId, Opts0, Config = #{mongo_type := Type, + database := Database, + pool_size := PoolSize, + ssl := SSL}) -> + SslOpts = case maps:get(enable, SSL) of + true -> + [{ssl, true}, + {ssl_opts, emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)} + ]; + false -> [{ssl, false}] + end, + Opts = Opts0 ++ + [{pool_size, PoolSize}, + {options, init_topology_options(maps:to_list(Config), [])}, + {worker_options, init_worker_options(maps:to_list(Config), SslOpts)}], + %% test the connection + TestOpts = case maps:is_key(server, Config) of + true -> + Server = maps:get(server, Config), + host_port(Server); + false -> + Servers = maps:get(servers, Config), + host_port(erlang:hd(Servers)) + end ++ [{database, Database}], + {ok, TestConn} = mc_worker_api:connect(TestOpts), + + PoolName = emqx_plugin_libs_pool:pool_name(InstId), + _ = emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ SslOpts), + {ok, #{poolname => PoolName, + type => Type, + test_conn => TestConn, + test_opts => TestOpts}}. init_topology_options([{pool_size, Val}| R], Acc) -> init_topology_options(R, [{pool_size, Val}| Acc]); @@ -143,9 +218,9 @@ init_topology_options([], Acc) -> init_worker_options([{database, V} | R], Acc) -> init_worker_options(R, [{database, V} | Acc]); -init_worker_options([{auth_source, V} | R], Acc) -> +init_worker_options([{authentication_database, V} | R], Acc) -> init_worker_options(R, [{auth_source, V} | Acc]); -init_worker_options([{login, V} | R], Acc) -> +init_worker_options([{username, V} | R], Acc) -> init_worker_options(R, [{login, V} | Acc]); init_worker_options([{password, V} | R], Acc) -> init_worker_options(R, [{password, V} | Acc]); @@ -157,13 +232,6 @@ init_worker_options([_ | R], Acc) -> init_worker_options(R, Acc); init_worker_options([], Acc) -> Acc. -init_ssl_opts(#{ssl := true} = Config, InstId) -> - [{ssl, true}, - {ssl_opts, emqx_plugin_libs_ssl:save_files_return_opts(Config, "connectors", InstId)} - ]; -init_ssl_opts(_Config, _InstId) -> - [{ssl, false}]. - host_port(HostPort) -> case string:split(HostPort, ":") of [Host, Port] -> @@ -174,43 +242,14 @@ host_port(HostPort) -> [{host, Host1}] end. -mongodb_fields() -> - [ {mongo_type, fun mongo_type/1} - , {servers, fun servers/1} - , {pool_size, fun emqx_connector_schema_lib:pool_size/1} - , {login, fun emqx_connector_schema_lib:username/1} - , {password, fun emqx_connector_schema_lib:password/1} - , {auth_source, fun auth_source/1} - , {database, fun emqx_connector_schema_lib:database/1} - ]. +server(type) -> server(); +server(validator) -> [?REQUIRED("the field 'server' is required")]; +server(_) -> undefined. -mongodb_topology_fields() -> - [ {max_overflow, fun emqx_connector_schema_lib:pool_size/1} - , {overflow_ttl, fun duration/1} - , {overflow_check_period, fun duration/1} - , {local_threshold_ms, fun duration/1} - , {connect_timeout_ms, fun duration/1} - , {socket_timeout_ms, fun duration/1} - , {server_selection_timeout_ms, fun duration/1} - , {wait_queue_timeout_ms, fun duration/1} - , {heartbeat_frequency_ms, fun duration/1} - , {min_heartbeat_frequency_ms, fun duration/1} - ]. - -mongodb_rs_set_name_fields() -> - [ {rs_set_name, fun emqx_connector_schema_lib:database/1} - ]. - -auth_source(type) -> binary(); -auth_source(_) -> undefined. - -servers(type) -> binary(); +servers(type) -> hoconsc:array(server()); servers(validator) -> [?REQUIRED("the field 'servers' is required")]; servers(_) -> undefined. -mongo_type(type) -> hoconsc:enum([single, unknown, shared, rs]); -mongo_type(default) -> single; -mongo_type(_) -> undefined. - duration(type) -> emqx_schema:duration_ms(); +duration(nullable) -> true; duration(_) -> undefined. diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index afe249682..a606bb82d 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -51,14 +51,14 @@ on_start(InstId, #{server := {Host, Port}, username := User, password := Password, auto_reconnect := AutoReconn, - pool_size := PoolSize} = Config) -> + pool_size := PoolSize, + ssl := SSL } = Config) -> logger:info("starting mysql connector: ~p, config: ~p", [InstId, Config]), - SslOpts = case maps:get(ssl, Config) of + SslOpts = case maps:get(enable, SSL) of true -> [{ssl, [{server_name_indication, disable} | - emqx_plugin_libs_ssl:save_files_return_opts(Config, "connectors", InstId)]}]; - false -> - [] + emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)]}]; + false -> [] end, Options = [{host, Host}, {port, Port}, diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index 827a9606c..ddcc2a7c7 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -50,14 +50,14 @@ on_start(InstId, #{server := {Host, Port}, username := User, password := Password, auto_reconnect := AutoReconn, - pool_size := PoolSize} = Config) -> + pool_size := PoolSize, + ssl := SSL } = Config) -> logger:info("starting postgresql connector: ~p, config: ~p", [InstId, Config]), - SslOpts = case maps:get(ssl, Config) of + SslOpts = case maps:get(enable, SSL) of true -> - [{ssl_opts, [{server_name_indication, disable} | - emqx_plugin_libs_ssl:save_files_return_opts(Config, "connectors", InstId)]}]; - false -> - [] + [{ssl, [{server_name_indication, disable} | + emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)]}]; + false -> [] end, Options = [{host, Host}, {port, Port}, diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 1aa6263b6..0df12185d 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -45,9 +45,9 @@ structs() -> [""]. fields("") -> [ {config, #{type => hoconsc:union( - [ hoconsc:ref(cluster) - , hoconsc:ref(single) - , hoconsc:ref(sentinel) + [ hoconsc:ref(?MODULE, cluster) + , hoconsc:ref(?MODULE, single) + , hoconsc:ref(?MODULE, sentinel) ])} } ]; @@ -81,7 +81,8 @@ on_jsonify(Config) -> on_start(InstId, #{config :=#{redis_type := Type, database := Database, pool_size := PoolSize, - auto_reconnect := AutoReconn} = Config}) -> + auto_reconnect := AutoReconn, + ssl := SSL } = Config}) -> logger:info("starting redis connector: ~p, config: ~p", [InstId, Config]), Servers = case Type of single -> [{servers, [maps:get(server, Config)]}]; @@ -92,8 +93,13 @@ on_start(InstId, #{config :=#{redis_type := Type, {password, maps:get(password, Config, "")}, {auto_reconnect, reconn_interval(AutoReconn)} ] ++ Servers, - Options = init_ssl_opts(Config, InstId) ++ - [{sentinel, maps:get(sentinel, Config, undefined)}], + Options = case maps:get(enable, SSL) of + true -> + [{ssl, true}, + {ssl_options, emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)} + ]; + false -> [{ssl, false}] + end ++ [{sentinel, maps:get(sentinel, Config, undefined)}], PoolName = emqx_plugin_libs_pool:pool_name(InstId), case Type of cluster -> @@ -134,7 +140,7 @@ on_health_check(_InstId, #{type := cluster, poolname := PoolName} = State) -> eredis_cluster_pool_worker:is_connected(Pid) =:= true end, Workers) of true -> {ok, State}; - false -> {error, test_query_failed, State} + false -> {error, health_check_failed, State} end; on_health_check(_InstId, #{poolname := PoolName} = State) -> emqx_plugin_libs_pool:health_check(PoolName, fun ?MODULE:do_health_check/1, State). @@ -157,13 +163,6 @@ cmd(Conn, _Type, Command) -> connect(Opts) -> eredis:start_link(Opts). -init_ssl_opts(#{ssl := true} = Config, InstId) -> - [{ssl, true}, - {ssl_opts, emqx_plugin_libs_ssl:save_files_return_opts(Config, "connectors", InstId)} - ]; -init_ssl_opts(_Config, _InstId) -> - [{ssl, false}]. - redis_fields() -> [ {pool_size, fun emqx_connector_schema_lib:pool_size/1} , {password, fun emqx_connector_schema_lib:password/1} diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 03572d91d..743d37ae3 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -51,6 +51,31 @@ , servers/0 ]). +-export([structs/0, fields/1]). + +structs() -> [ssl_on, ssl_off]. + +fields(ssl_on) -> + [ {enable, #{type => true}} + , {cacertfile, fun cacertfile/1} + , {keyfile, fun keyfile/1} + , {certfile, fun certfile/1} + , {verify, fun verify/1} + ]; + +fields(ssl_off) -> + [ {enable, #{type => false}} ]. + +ssl_fields() -> + [ {ssl, #{type => hoconsc:union( + [ hoconsc:ref(?MODULE, ssl_on) + , hoconsc:ref(?MODULE, ssl_off) + ]), + default => hoconsc:ref(?MODULE, ssl_off) + } + } + ]. + relational_db_fields() -> [ {server, fun server/1} , {database, fun database/1} @@ -60,14 +85,6 @@ relational_db_fields() -> , {auto_reconnect, fun auto_reconnect/1} ]. -ssl_fields() -> - [ {ssl, fun ssl/1} - , {cacertfile, fun cacertfile/1} - , {keyfile, fun keyfile/1} - , {certfile, fun certfile/1} - , {verify, fun verify/1} - ]. - server(type) -> emqx_schema:ip_port(); server(validator) -> [?REQUIRED("the field 'server' is required")]; server(_) -> undefined. @@ -82,30 +99,26 @@ pool_size(validator) -> [?MIN(1), ?MAX(64)]; pool_size(_) -> undefined. username(type) -> binary(); -username(default) -> "root"; +username(nullable) -> true; username(_) -> undefined. password(type) -> binary(); -password(default) -> ""; +password(nullable) -> true; password(_) -> undefined. auto_reconnect(type) -> boolean(); auto_reconnect(default) -> true; auto_reconnect(_) -> undefined. -ssl(type) -> boolean(); -ssl(default) -> false; -ssl(_) -> undefined. - -cacertfile(type) -> binary(); +cacertfile(type) -> string(); cacertfile(default) -> ""; cacertfile(_) -> undefined. -keyfile(type) -> binary(); +keyfile(type) -> string(); keyfile(default) -> ""; keyfile(_) -> undefined. -certfile(type) -> binary(); +certfile(type) -> string(); certfile(default) -> ""; certfile(_) -> undefined. diff --git a/apps/emqx_dashboard/src/emqx_dashboard.appup.src b/apps/emqx_dashboard/src/emqx_dashboard.appup.src new file mode 100644 index 000000000..678bd3b22 --- /dev/null +++ b/apps/emqx_dashboard/src/emqx_dashboard.appup.src @@ -0,0 +1,16 @@ +%% -*- mode: erlang -*- +{VSN, + [ {"4.3.0", + %% load all plugins + %% NOTE: this depends on the fact that emqx_dashboard is always + %% the last application gets upgraded + [ {apply, {emqx_plugins, load, []}} + ]}, + {<<".*">>, []} + ], + [ {"4.3.0", + [ {apply, {emqx_plugins, load, []}} + ]}, + {<<".*">>, []} + ] +}. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index be77d474b..3ea8ab743 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -40,38 +40,38 @@ -define(OVERVIEWS, ['alarms/activated', 'alarms/deactivated', banned, brokers, stats, metrics, listeners, clients, subscriptions, routes, plugins]). all() -> - [{group, overview}, - {group, admins}, - {group, rest}, - {group, cli} - ]. - -groups() -> - [{overview, [sequence], [t_overview]}, - {admins, [sequence], [t_admins_add_delete]}, - {rest, [sequence], [t_rest_api]}, - {cli, [sequence], [t_cli]} - ]. + emqx_ct:all(?MODULE). init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_modules, emqx_management, emqx_dashboard]), + emqx_ct_helpers:start_apps([emqx_management, emqx_dashboard],fun set_special_configs/1), Config. end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_dashboard, emqx_management, emqx_modules]), + emqx_ct_helpers:stop_apps([emqx_dashboard, emqx_management]), ekka_mnesia:ensure_stopped(). +set_special_configs(emqx_management) -> + emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], + default_application_id => <<"admin">>, + default_application_secret => <<"public">>}), + ok; +set_special_configs(_) -> + ok. + t_overview(_) -> + mnesia:clear_table(mqtt_admin), + emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, <<"tag">>), [?assert(request_dashboard(get, api_path(erlang:atom_to_list(Overview)), auth_header_()))|| Overview <- ?OVERVIEWS]. t_admins_add_delete(_) -> + mnesia:clear_table(mqtt_admin), ok = emqx_dashboard_admin:add_user(<<"username">>, <<"password">>, <<"tag">>), ok = emqx_dashboard_admin:add_user(<<"username1">>, <<"password1">>, <<"tag1">>), Admins = emqx_dashboard_admin:all_users(), - ?assertEqual(3, length(Admins)), + ?assertEqual(2, length(Admins)), ok = emqx_dashboard_admin:remove_user(<<"username1">>), Users = emqx_dashboard_admin:all_users(), - ?assertEqual(2, length(Users)), + ?assertEqual(1, length(Users)), ok = emqx_dashboard_admin:change_password(<<"username">>, <<"password">>, <<"pwd">>), timer:sleep(10), ?assert(request_dashboard(get, api_path("brokers"), auth_header_("username", "pwd"))), @@ -80,6 +80,8 @@ t_admins_add_delete(_) -> ?assertNotEqual(true, request_dashboard(get, api_path("brokers"), auth_header_("username", "pwd"))). t_rest_api(_Config) -> + mnesia:clear_table(mqtt_admin), + emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, <<"administrator">>), {ok, Res0} = http_get("users"), ?assertEqual([#{<<"username">> => <<"admin">>, diff --git a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf b/apps/emqx_data_bridge/etc/emqx_data_bridge.conf index 696cadf05..c299b97a1 100644 --- a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf +++ b/apps/emqx_data_bridge/etc/emqx_data_bridge.conf @@ -2,125 +2,128 @@ ## EMQ X Bridge Plugin ##-------------------------------------------------------------------- -emqx_data_bridge.bridges: [ -# {name: "mysql_bridge_1" -# type: mysql -# config: { -# server: "192.168.0.172:3306" -# database: mqtt -# pool_size: 1 -# username: root -# password: public -# auto_reconnect: true -# ssl: false -# } -# } -# , {name: "pgsql_bridge_1" -# type: pgsql -# config: { -# server: "192.168.0.172:5432" -# database: mqtt -# pool_size: 1 -# username: root -# password: public -# auto_reconnect: true -# ssl: false -# } -# } -# , {name: "mongodb_bridge_single" -# type: mongo -# config: { -# servers: "192.168.0.172:27017" -# mongo_type: single -# pool_size: 1 -# login: root -# password: public -# auth_source: mqtt -# database: mqtt -# ssl: false -# } -# } -# ,{name: "mongodb_bridge_rs" -# type: mongo -# config: { -# servers: "127.0.0.1:27017" -# mongo_type: rs -# rs_set_name: rs_name -# pool_size: 1 -# login: root -# password: public -# auth_source: mqtt -# database: mqtt -# ssl: false -# } -# } -# ,{name: "mongodb_bridge_shared" -# type: mongo -# config: { -# servers: "127.0.0.1:27017" -# mongo_type: shared -# pool_size: 1 -# login: root -# password: public -# auth_source: mqtt -# database: mqtt -# ssl: false -# max_overflow: 1 -# overflow_ttl: -# overflow_check_period: 10s -# local_threshold_ms: 10s -# connect_timeout_ms: 10s -# socket_timeout_ms: 10s -# server_selection_timeout_ms: 10s -# wait_queue_timeout_ms: 10s -# heartbeat_frequency_ms: 10s -# min_heartbeat_frequency_ms: 10s -# } -# } -# , {name: "redis_bridge_single" -# type: redis -# config: { -# servers: "192.168.0.172:6379" -# redis_type: single -# pool_size: 1 -# database: 0 -# password: public -# auto_reconnect: true -# ssl: false -# } -# } -# ,{name: "redis_bridge_sentinel" -# type: redis -# config: { -# servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" -# redis_type: sentinel -# sentinel_name: mymaster -# pool_size: 1 -# database: 0 -# ssl: false -# } -# } -# ,{name: "redis_bridge_cluster" -# type: redis -# config: { -# servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" -# redis_type: cluster -# pool_size: 1 -# database: 0 -# password: "public" -# ssl: false -# } -# } -# , {name: "ldap_bridge_1" -# type: ldap -# config: { -# servers: "192.168.0.172" -# port: 389 -# bind_dn: "cn=root,dc=emqx,dc=io" -# bind_password: "public" -# timeout: 30s -# pool_size: 1 -# ssl: false -# } -# } -] +emqx_data_bridge:{ + bridges:[ + # {name: "mysql_bridge_1" + # type: mysql + # config: { + # server: "192.168.0.172:3306" + # database: mqtt + # pool_size: 1 + # username: root + # password: public + # auto_reconnect: true + # ssl: false + # } + # } + # , {name: "pgsql_bridge_1" + # type: pgsql + # config: { + # server: "192.168.0.172:5432" + # database: mqtt + # pool_size: 1 + # username: root + # password: public + # auto_reconnect: true + # ssl: false + # } + # } + # , {name: "mongodb_bridge_single" + # type: mongo + # config: { + # servers: "192.168.0.172:27017" + # mongo_type: single + # pool_size: 1 + # login: root + # password: public + # auth_source: mqtt + # database: mqtt + # ssl: false + # } + # } + # ,{name: "mongodb_bridge_rs" + # type: mongo + # config: { + # servers: "127.0.0.1:27017" + # mongo_type: rs + # rs_set_name: rs_name + # pool_size: 1 + # login: root + # password: public + # auth_source: mqtt + # database: mqtt + # ssl: false + # } + # } + # ,{name: "mongodb_bridge_shared" + # type: mongo + # config: { + # servers: "127.0.0.1:27017" + # mongo_type: shared + # pool_size: 1 + # login: root + # password: public + # auth_source: mqtt + # database: mqtt + # ssl: false + # max_overflow: 1 + # overflow_ttl: + # overflow_check_period: 10s + # local_threshold_ms: 10s + # connect_timeout_ms: 10s + # socket_timeout_ms: 10s + # server_selection_timeout_ms: 10s + # wait_queue_timeout_ms: 10s + # heartbeat_frequency_ms: 10s + # min_heartbeat_frequency_ms: 10s + # } + # } + # , {name: "redis_bridge_single" + # type: redis + # config: { + # servers: "192.168.0.172:6379" + # redis_type: single + # pool_size: 1 + # database: 0 + # password: public + # auto_reconnect: true + # ssl: false + # } + # } + # ,{name: "redis_bridge_sentinel" + # type: redis + # config: { + # servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" + # redis_type: sentinel + # sentinel_name: mymaster + # pool_size: 1 + # database: 0 + # ssl: false + # } + # } + # ,{name: "redis_bridge_cluster" + # type: redis + # config: { + # servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" + # redis_type: cluster + # pool_size: 1 + # database: 0 + # password: "public" + # ssl: false + # } + # } + # , {name: "ldap_bridge_1" + # type: ldap + # config: { + # servers: "192.168.0.172" + # port: 389 + # bind_dn: "cn=root,dc=emqx,dc=io" + # bind_password: "public" + # timeout: 30s + # pool_size: 1 + # ssl: false + # } + # } + + ] +} diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl index 1ba7f2fc5..b4749af0e 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl @@ -23,4 +23,4 @@ fields(mysql) -> ?BRIDGE_FIELDS(mysql); fields(pgsql) -> ?BRIDGE_FIELDS(pgsql); fields(mongo) -> ?BRIDGE_FIELDS(mongo); fields(redis) -> ?BRIDGE_FIELDS(redis); -fields(ldap) -> ?BRIDGE_FIELDS(ldap). \ No newline at end of file +fields(ldap) -> ?BRIDGE_FIELDS(ldap). diff --git a/apps/emqx_exhook/include/emqx_exhook.hrl b/apps/emqx_exhook/include/emqx_exhook.hrl index 7301fdcbb..64131735e 100644 --- a/apps/emqx_exhook/include/emqx_exhook.hrl +++ b/apps/emqx_exhook/include/emqx_exhook.hrl @@ -25,7 +25,7 @@ , {'client.connected', {emqx_exhook_handler, on_client_connected, []}} , {'client.disconnected', {emqx_exhook_handler, on_client_disconnected, []}} , {'client.authenticate', {emqx_exhook_handler, on_client_authenticate, []}} - , {'client.check_acl', {emqx_exhook_handler, on_client_check_acl, []}} + , {'client.authorize', {emqx_exhook_handler, on_client_authorize, []}} , {'client.subscribe', {emqx_exhook_handler, on_client_subscribe, []}} , {'client.unsubscribe', {emqx_exhook_handler, on_client_unsubscribe, []}} , {'session.created', {emqx_exhook_handler, on_session_created, []}} diff --git a/apps/emqx_exhook/priv/protos/exhook.proto b/apps/emqx_exhook/priv/protos/exhook.proto index 72ba26581..97a011352 100644 --- a/apps/emqx_exhook/priv/protos/exhook.proto +++ b/apps/emqx_exhook/priv/protos/exhook.proto @@ -40,7 +40,7 @@ service HookProvider { rpc OnClientAuthenticate(ClientAuthenticateRequest) returns (ValuedResponse) {}; - rpc OnClientCheckAcl(ClientCheckAclRequest) returns (ValuedResponse) {}; + rpc OnClientAuthorize(ClientAuthorizeRequest) returns (ValuedResponse) {}; rpc OnClientSubscribe(ClientSubscribeRequest) returns (EmptySuccess) {}; @@ -123,18 +123,18 @@ message ClientAuthenticateRequest { bool result = 2; } -message ClientCheckAclRequest { +message ClientAuthorizeRequest { ClientInfo clientinfo = 1; - enum AclReqType { + enum AuthzReqType { PUBLISH = 0; SUBSCRIBE = 1; } - AclReqType type = 2; + AuthzReqType type = 2; string topic = 3; @@ -253,7 +253,7 @@ message ValuedResponse { oneof value { - // Boolean result, used on the 'client.authenticate', 'client.check_acl' hooks + // Boolean result, used on the 'client.authenticate', 'client.authorize' hooks bool bool_result = 3; // Message result, used on the 'message.*' hooks @@ -279,7 +279,7 @@ message HookSpec { // Available value: // "client.connect", "client.connack" // "client.connected", "client.disconnected" - // "client.authenticate", "client.check_acl" + // "client.authenticate", "client.authorize" // "client.subscribe", "client.unsubscribe" // // "session.created", "session.subscribed" diff --git a/apps/emqx_exhook/src/emqx_exhook.app.src b/apps/emqx_exhook/src/emqx_exhook.app.src index 452d2a742..e703cfe5e 100644 --- a/apps/emqx_exhook/src/emqx_exhook.app.src +++ b/apps/emqx_exhook/src/emqx_exhook.app.src @@ -1,6 +1,6 @@ {application, emqx_exhook, [{description, "EMQ X Extension for Hook"}, - {vsn, "4.3.1"}, + {vsn, "4.3.2"}, {modules, []}, {registered, []}, {mod, {emqx_exhook_app, []}}, diff --git a/apps/emqx_exhook/src/emqx_exhook.appup.src b/apps/emqx_exhook/src/emqx_exhook.appup.src index 2811c1554..26e84d88f 100644 --- a/apps/emqx_exhook/src/emqx_exhook.appup.src +++ b/apps/emqx_exhook/src/emqx_exhook.appup.src @@ -1,14 +1,22 @@ %% -*-: erlang -*- {VSN, [ + {"4.3.1", [ + {load_module, emqx_exhook_server, brutal_purge, soft_purge, []} + ]}, {"4.3.0", [ - {load_module, emqx_exhook_pb, brutal_purge, soft_purge, []} + {load_module, emqx_exhook_pb, brutal_purge, soft_purge, []}, + {load_module, emqx_exhook_server, brutal_purge, soft_purge, []} ]}, {<<".*">>, []} ], [ + {"4.3.1", [ + {load_module, emqx_exhook_server, brutal_purge, soft_purge, []} + ]}, {"4.3.0", [ - {load_module, emqx_exhook_pb, brutal_purge, soft_purge, []} + {load_module, emqx_exhook_pb, brutal_purge, soft_purge, []}, + {load_module, emqx_exhook_server, brutal_purge, soft_purge, []} ]}, {<<".*">>, []} ] diff --git a/apps/emqx_exhook/src/emqx_exhook_app.erl b/apps/emqx_exhook/src/emqx_exhook_app.erl index 4e00340d8..2988be6d2 100644 --- a/apps/emqx_exhook/src/emqx_exhook_app.erl +++ b/apps/emqx_exhook/src/emqx_exhook_app.erl @@ -88,7 +88,7 @@ init_hooks_cnter() -> try _ = ets:new(?CNTER, [named_table, public]), ok catch - exit:badarg:_ -> + error:badarg:_ -> ok end. diff --git a/apps/emqx_exhook/src/emqx_exhook_handler.erl b/apps/emqx_exhook/src/emqx_exhook_handler.erl index f3964dc42..db653c52b 100644 --- a/apps/emqx_exhook/src/emqx_exhook_handler.erl +++ b/apps/emqx_exhook/src/emqx_exhook_handler.erl @@ -27,7 +27,7 @@ , on_client_connected/2 , on_client_disconnected/3 , on_client_authenticate/2 - , on_client_check_acl/4 + , on_client_authorize/4 , on_client_subscribe/3 , on_client_unsubscribe/3 ]). @@ -109,7 +109,7 @@ on_client_authenticate(ClientInfo, AuthResult) -> {ok, AuthResult} end. -on_client_check_acl(ClientInfo, PubSub, Topic, Result) -> +on_client_authorize(ClientInfo, PubSub, Topic, Result) -> Bool = Result == allow, Type = case PubSub of publish -> 'PUBLISH'; @@ -120,7 +120,7 @@ on_client_check_acl(ClientInfo, PubSub, Topic, Result) -> topic => Topic, result => Bool }, - case call_fold('client.check_acl', Req, + case call_fold('client.authorize', Req, fun merge_responsed_bool/2) of {StopOrOk, #{result := Result0}} when is_boolean(Result0) -> NResult = case Result0 of true -> allow; _ -> deny end, diff --git a/apps/emqx_exhook/src/emqx_exhook_server.erl b/apps/emqx_exhook/src/emqx_exhook_server.erl index 848a3f59d..a3b132065 100644 --- a/apps/emqx_exhook/src/emqx_exhook_server.erl +++ b/apps/emqx_exhook/src/emqx_exhook_server.erl @@ -58,7 +58,7 @@ | 'client.connected' | 'client.disconnected' | 'client.authenticate' - | 'client.check_acl' + | 'client.authorize' | 'client.subscribe' | 'client.unsubscribe' | 'session.created' @@ -122,7 +122,7 @@ channel_opts(Opts) -> Scheme = proplists:get_value(scheme, Opts), Host = proplists:get_value(host, Opts), Port = proplists:get_value(port, Opts), - SvrAddr = lists:flatten(io_lib:format("~s://~s:~w", [Scheme, Host, Port])), + SvrAddr = format_http_uri(Scheme, Host, Port), ClientOpts = case Scheme of https -> SslOpts = lists:keydelete(ssl, 1, proplists:get_value(ssl_options, Opts, [])), @@ -133,6 +133,13 @@ channel_opts(Opts) -> end, {SvrAddr, ClientOpts}. +format_http_uri(Scheme, Host0, Port) -> + Host = case is_tuple(Host0) of + true -> inet:ntoa(Host0); + _ -> Host0 + end, + lists:flatten(io_lib:format("~s://~s:~w", [Scheme, Host, Port])). + -spec unload(server()) -> ok. unload(#server{name = Name, hookspec = HookSpecs}) -> _ = do_deinit(Name), @@ -290,7 +297,7 @@ hk2func('client.connack') -> 'on_client_connack'; hk2func('client.connected') -> 'on_client_connected'; hk2func('client.disconnected') -> 'on_client_disconnected'; hk2func('client.authenticate') -> 'on_client_authenticate'; -hk2func('client.check_acl') -> 'on_client_check_acl'; +hk2func('client.authorize') -> 'on_client_authorize'; hk2func('client.subscribe') -> 'on_client_subscribe'; hk2func('client.unsubscribe') -> 'on_client_unsubscribe'; hk2func('session.created') -> 'on_session_created'; @@ -313,7 +320,7 @@ message_hooks() -> -compile({inline, [available_hooks/0]}). available_hooks() -> ['client.connect', 'client.connack', 'client.connected', - 'client.disconnected', 'client.authenticate', 'client.check_acl', + 'client.disconnected', 'client.authenticate', 'client.authorize', 'client.subscribe', 'client.unsubscribe', 'session.created', 'session.subscribed', 'session.unsubscribed', 'session.resumed', 'session.discarded', 'session.takeovered', diff --git a/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl b/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl index c2db04dd4..656788b5e 100644 --- a/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl +++ b/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl @@ -33,7 +33,7 @@ , on_client_connected/2 , on_client_disconnected/2 , on_client_authenticate/2 - , on_client_check_acl/2 + , on_client_authorize/2 , on_client_subscribe/2 , on_client_unsubscribe/2 , on_session_created/2 @@ -122,7 +122,7 @@ on_provider_loaded(Req, Md) -> #{name => <<"client.connected">>}, #{name => <<"client.disconnected">>}, #{name => <<"client.authenticate">>}, - #{name => <<"client.check_acl">>}, + #{name => <<"client.authorize">>}, #{name => <<"client.subscribe">>}, #{name => <<"client.unsubscribe">>}, #{name => <<"session.created">>}, @@ -197,10 +197,10 @@ on_client_authenticate(#{clientinfo := #{username := Username}} = Req, Md) -> {ok, #{type => 'IGNORE'}, Md} end. --spec on_client_check_acl(emqx_exhook_pb:client_check_acl_request(), grpc:metadata()) +-spec on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:metadata()) -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} | {error, grpc_cowboy_h:error_response()}. -on_client_check_acl(#{clientinfo := #{username := Username}} = Req, Md) -> +on_client_authorize(#{clientinfo := #{username := Username}} = Req, Md) -> ?MODULE:in({?FUNCTION_NAME, Req}), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %% some cases for testing diff --git a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl index 24f45c8b0..12f54eef6 100644 --- a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl +++ b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl @@ -109,14 +109,14 @@ prop_client_authenticate() -> true end). -prop_client_check_acl() -> +prop_client_authorize() -> ?ALL({ClientInfo0, PubSub, Topic, Result}, {clientinfo(), oneof([publish, subscribe]), topic(), oneof([allow, deny])}, begin ClientInfo = inject_magic_into(username, ClientInfo0), OutResult = emqx_hooks:run_fold( - 'client.check_acl', + 'client.authorize', [ClientInfo, PubSub, Topic], Result), ExpectedOutResult = case maps:get(username, ClientInfo) of @@ -127,7 +127,7 @@ prop_client_check_acl() -> end, ?assertEqual(ExpectedOutResult, OutResult), - {'on_client_check_acl', Resp} = emqx_exhook_demo_svr:take(), + {'on_client_authorize', Resp} = emqx_exhook_demo_svr:take(), Expected = #{result => aclresult_to_bool(Result), type => pubsub_to_enum(PubSub), diff --git a/apps/emqx_exproto/src/emqx_exproto_channel.erl b/apps/emqx_exproto/src/emqx_exproto_channel.erl index 229e6f930..d45f445ab 100644 --- a/apps/emqx_exproto/src/emqx_exproto_channel.erl +++ b/apps/emqx_exproto/src/emqx_exproto_channel.erl @@ -305,7 +305,7 @@ handle_call({subscribe, TopicFilter, Qos}, conn_state = connected, clientinfo = ClientInfo}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_acl(ClientInfo, subscribe, TopicFilter) of + emqx_access_control:authorize(ClientInfo, subscribe, TopicFilter) of deny -> {reply, {error, ?RESP_PERMISSION_DENY, <<"ACL deny">>}, Channel}; _ -> @@ -325,7 +325,7 @@ handle_call({publish, Topic, Qos, Payload}, = #{clientid := From, mountpoint := Mountpoint}}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_acl(ClientInfo, publish, Topic) of + emqx_access_control:authorize(ClientInfo, publish, Topic) of deny -> {reply, {error, ?RESP_PERMISSION_DENY, <<"ACL deny">>}, Channel}; _ -> diff --git a/apps/emqx_exproto/test/emqx_exproto_SUITE.erl b/apps/emqx_exproto/test/emqx_exproto_SUITE.erl index db64c7438..e38347e5e 100644 --- a/apps/emqx_exproto/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_exproto/test/emqx_exproto_SUITE.erl @@ -167,7 +167,7 @@ t_acl_deny(Cfg) -> Password = <<"123456">>, ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), - ok = meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> deny end), + ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> deny end), ConnBin = frame_connect(Client, Password), ConnAckBin = frame_connack(0), diff --git a/apps/emqx_gateway/.gitignore b/apps/emqx_gateway/.gitignore new file mode 100644 index 000000000..71ab0135c --- /dev/null +++ b/apps/emqx_gateway/.gitignore @@ -0,0 +1,20 @@ +.rebar3 +_* +.eunit +*.o +*.beam +*.plt +*.swp +*.swo +.erlang.cookie +ebin +log +erl_crash.dump +.rebar +logs +_build +.idea +*.iml +rebar3.crashdump +*~ +rebar.lock diff --git a/apps/emqx_gateway/LICENSE b/apps/emqx_gateway/LICENSE new file mode 100644 index 000000000..1f15def74 --- /dev/null +++ b/apps/emqx_gateway/LICENSE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021, JianBo He . + + 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. + diff --git a/apps/emqx_gateway/Makefile b/apps/emqx_gateway/Makefile new file mode 100644 index 000000000..b2a54f7dd --- /dev/null +++ b/apps/emqx_gateway/Makefile @@ -0,0 +1,28 @@ +## shallow clone for speed + +REBAR_GIT_CLONE_OPTIONS += --depth 1 +export REBAR_GIT_CLONE_OPTIONS + +REBAR = rebar3 +all: compile + +compile: + $(REBAR) compile + +clean: distclean + +ct: + $(REBAR) as test ct -v + +eunit: + $(REBAR) as test eunit + +xref: + $(REBAR) xref + +cover: + $(REBAR) cover + +distclean: + @rm -rf _build + @rm -f data/app.*.config data/vm.*.args rebar.lock diff --git a/apps/emqx_gateway/README.md b/apps/emqx_gateway/README.md new file mode 100644 index 000000000..0f95f286a --- /dev/null +++ b/apps/emqx_gateway/README.md @@ -0,0 +1,309 @@ +# emqx_gateway + +***This is a very early prototype application*** for Gateway in EMQ X Broker 5.0 + +## Concept + + EMQ X Gateway Managment + - Gateway-Registry (or Gateway Type) + - *Load + - *UnLoad + - *List + + - Gateway + - *Create + - *Delete + - *Update + - *Stop-And-Start + - *Hot-Upgrade + - *Satrt/Enable + - *Stop/Disable + - Listener + +## ROADMAP + +Gateway v0.1: Management support + +Gateway v0.2: Conn/Frame/Protocol Template + +### Compatible with EMQ X + +> Why we need to compatible + +1. Authentication +2. Hooks/Event system +3. Messages Mode & Rule Engine +4. Cluster registration +5. Metrics & Statistic + +> How to do it + +> + +### User Interface + +#### Configurations + +```hocon +gateway { + + ## ... some confs for top scope + .. + ## End. + + ## Gateway Instances + + lwm2m[.name] { + + ## variable support + mountpoint: lwm2m/%e/ + + lifetime_min: 1s + lifetime_max: 86400s + #qmode_time_window: 22 + #auto_observe: off + + #update_msg_publish_condition: contains_object_list + + xml_dir: {{ platform_etc_dir }}/lwm2m_xml + + clientinfo_override: { + username: ${register.opts.uname} + password: ${register.opts.passwd} + clientid: ${epn} + } + + #authenticator: allow_anonymous + authenticator: [ + { + type: auth-http + method: post + //?? how to generate clientinfo ?? + params: $client.credential + } + ] + + translator: { + downlink: "dn/#" + uplink: { + notify: "up/notify" + response: "up/resp" + register: "up/resp" + update: "up/reps" + } + } + + %% ?? listener.$type.name ?? + listener.udp[.name] { + listen_on: 0.0.0.0:5683 + max_connections: 1024000 + max_conn_rate: 1000 + ## ?? udp keepalive in socket level ??? + #keepalive: + ## ?? udp proxy-protocol in socket level ??? + #proxy_protocol: on + #proxy_timeout: 30s + recbuf: 2KB + sndbuf: 2KB + buffer: 2KB + tune_buffer: off + #access: allow all + read_packets: 20 + } + + listener.dtls[.name] { + listen_on: 0.0.0.0:5684 + ... + } + } + + ## The CoAP Gateway + coap[.name] { + + #enable_stats: on + + authenticator: [ + ... + ] + + listener.udp[.name] { + ... + } + + listener.dtls[.name] { + ... + } +} + + ## The Stomp Gateway + stomp[.name] { + + allow_anonymous: true + + default_user.login: guest + default_user.passcode: guest + + frame.max_headers: 10 + frame.max_header_length: 1024 + frame.max_body_length: 8192 + + listener.tcp[.name] { + ... + } + + listener.ssl[.name] { + ... + } + } + + exproto[.name] { + + proto_name: DL-648 + + authenticators: [...] + + adapter: { + type: grpc + options: { + listen_on: 9100 + } + } + + handler: { + type: grpc + options: { + url: + } + } + + listener.tcp[.name] { + ... + } + } + + ## ============================ Enterpise gateways + + ## The JT/T 808 Gateway + jtt808[.name] { + + idle_timeout: 30s + enable_stats: on + max_packet_size: 8192 + + clientinfo_override: { + clientid: $phone + username: xxx + password: xxx + } + + authenticator: [ + { + type: auth-http + method: post + params: $clientinfo.credential + } + ] + + translator: { + subscribe: [jt808/%c/dn] + publish: [jt808/%c/up] + } + + listener.tcp[.name] { + ... + } + + listener.ssl[.name] { + ... + } + } + + gbt32960[.name] { + + frame.max_length: 8192 + retx_interval: 8s + retx_max_times: 3 + message_queue_len: 10 + + authenticators: [...] + + translator: { + ## upstream + login: gbt32960/${vin}/upstream/vlogin + logout: gbt32960/${vin}/upstream/vlogout + informing: gbt32960/${vin}/upstream/info + reinforming: gbt32960/${vin}/upstream/reinfo + ## downstream + downstream: gbt32960/${vin}/dnstream + response: gbt32960/${vin}/upstream/response + } + + listener.tcp[.name] { + ... + } + + listener.ssl[.name] { + ... + } + } + + privtcp[.name] { + + max_packet_size: 65535 + idle_timeout: 15s + + enable_stats: on + + force_gc_policy: 1000|1MB + force_shutdown_policy: 8000|800MB + + translator: { + up_topic: tcp/%c/up + dn_topic: tcp/%c/dn + } + + listener.tcp[.name]: { + ... + } + } +} +``` + +#### CLI + +##### Gateway + +```bash +## List all started gateway and gateway-instance +emqx_ctl gateway list +emqx_ctl gateway lookup +emqx_ctl gateway stop +emqx_ctl gateway start + +emqx_ctl gateway-registry re-searching +emqx_ctl gateway-registry list + +emqx_ctl gateway-clients list +emqx_ctl gateway-clients show +emqx_ctl gateway-clients kick + +## Banned ?? +emqx_ctl gateway-banned + +## Metrics +emqx_ctl gateway-metrics [] +``` + +#### Mangement by HTTP-API/Dashboard/ + +#### How to integrate a protocol to your platform + +### Develop your protocol gateway + +There are 3 way to create your protocol gateway for EMQ X 5.0: + +1. Use Erlang to create a new emqx plugin to handle all of protocol packets (same as v5.0 before) + +2. Based on the emqx-gateway-impl-bhvr and emqx-gateway + +3. Use the gRPC Gateway diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf new file mode 100644 index 000000000..ab5b52143 --- /dev/null +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -0,0 +1,30 @@ +##-------------------------------------------------------------------- +## EMQ X Gateway configurations +##-------------------------------------------------------------------- + +## TODO: + +emqx_gateway: { + stomp.1: { + frame: { + max_headers: 10 + max_headers_length: 1024 + max_body_length: 8192 + } + + clientinfo_override: { + username: "${Packet.headers.login}" + password: "${Packet.headers.passcode}" + } + + authenticator: allow_anonymous + + listener.tcp.1: { + bind: 61613 + acceptors: 16 + max_connections: 1024000 + max_conn_rate: 1000 + active_n: 100 + } + } +} diff --git a/apps/emqx_auth_mongo/src/emqx_auth_mongo_sup.erl b/apps/emqx_gateway/include/emqx_gateway.hrl similarity index 54% rename from apps/emqx_auth_mongo/src/emqx_auth_mongo_sup.erl rename to apps/emqx_gateway/include/emqx_gateway.hrl index 3f27cb1dd..35fad7f23 100644 --- a/apps/emqx_auth_mongo/src/emqx_auth_mongo_sup.erl +++ b/apps/emqx_gateway/include/emqx_gateway.hrl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2017-2021 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. @@ -14,21 +14,22 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_auth_mongo_sup). +-ifndef(EMQX_GATEWAY_HRL). +-define(EMQX_GATEWAY_HRL, 1). --behaviour(supervisor). +-type instance_id() :: atom(). +-type gateway_type() :: atom(). --include("emqx_auth_mongo.hrl"). - --export([start_link/0]). - --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - {ok, PoolEnv} = application:get_env(?APP, server), - PoolSpec = ecpool:pool_spec(?APP, ?APP, ?APP, PoolEnv), - {ok, {{one_for_all, 10, 100}, [PoolSpec]}}. +%% @doc The Gateway Instace defination +-type instance() :: + #{ id := instance_id() + , type := gateway_type() + , name := binary() + , descr => binary() | undefined + %% Appears only in creating or detailed info + , rawconf => map() + %% Appears only in getting instance status/info + , status => stopped | running + }. +-endif. diff --git a/apps/emqx_gateway/rebar.config b/apps/emqx_gateway/rebar.config new file mode 100644 index 000000000..71fc61330 --- /dev/null +++ b/apps/emqx_gateway/rebar.config @@ -0,0 +1,7 @@ +{erl_opts, [debug_info]}. +{deps, []}. + +{shell, [ + % {config, "config/sys.config"}, + {apps, [emqx_gateway]} +]}. diff --git a/apps/emqx_lua_hook/include/emqx_lua_hook.hrl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl similarity index 79% rename from apps/emqx_lua_hook/include/emqx_lua_hook.hrl rename to apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index fb7010c2b..7392f0b20 100644 --- a/apps/emqx_lua_hook/include/emqx_lua_hook.hrl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2021 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. @@ -14,5 +14,9 @@ %% limitations under the License. %%-------------------------------------------------------------------- --define(LOG(Level, Format, Args), emqx_logger:Level("Lua Hook: " ++ Format, Args)). +%% @doc The behavior abstrat for TCP based gateway conn +%% +-module(emqx_gateway_conn). + +%% TODO: Gateway v0.2 diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl new file mode 100644 index 000000000..9726dad02 --- /dev/null +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl @@ -0,0 +1,49 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_impl). + +-include("include/emqx_gateway.hrl"). + +-type state() :: map(). +-type reason() :: any(). + +%% @doc +-callback init(Options :: list()) -> {error, reason()} | {ok, GwState :: state()}. + +%% @doc +-callback on_insta_create(Insta :: instance(), + Ctx :: emqx_gateway_ctx:context(), + GwState :: state() + ) + -> {error, reason()} + | {ok, [GwInstaPid :: pid()], GwInstaState :: state()} + | {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()}. + +%% @doc +-callback on_insta_update(NewInsta :: instance(), + OldInsta :: instance(), + GwInstaState :: state(), + GwState :: state()) + -> ok + | {ok, [GwInstaPid :: pid()], GwInstaState :: state()} + | {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()} + | {error, reason()}. + +%% @doc +-callback on_insta_destroy(Insta :: instance(), + GwInstaState :: state(), + GwState :: state()) -> ok. diff --git a/apps/emqx_gateway/src/emqx_gateway.app.src b/apps/emqx_gateway/src/emqx_gateway.app.src new file mode 100644 index 000000000..287d710eb --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway.app.src @@ -0,0 +1,11 @@ +{application, emqx_gateway, + [{description, "The Gateway management application"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, {emqx_gateway_app, []}}, + {applications, [kernel, stdlib]}, + {env, []}, + {modules, []}, + {licenses, ["Apache 2.0"]}, + {links, []} + ]}. diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl new file mode 100644 index 000000000..f6c12ad53 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -0,0 +1,84 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway). + +-include("include/emqx_gateway.hrl"). + +%% APIs +-export([ registered_gateway/0 + , create/4 + , remove/1 + , lookup/1 + , update/1 + , start/1 + , stop/1 + , list/0 + ]). + +-spec registered_gateway() -> + [{gateway_type(), emqx_gateway_registry:descriptor()}]. +registered_gateway() -> + emqx_gateway_registry:list(). + +%%-------------------------------------------------------------------- +%% Gateway Instace APIs + +-spec list() -> [instance()]. +list() -> + lists:append(lists:map( + fun({_, Insta}) -> Insta end, + emqx_gateway_sup:list_gateway_insta() + )). + +-spec create(gateway_type(), binary(), binary(), map()) + -> {ok, pid()} + | {error, any()}. +create(Type, Name, Descr, RawConf) -> + Insta = #{ id => clacu_insta_id(Type, Name) + , type => Type + , name => Name + , descr => Descr + , rawconf => RawConf + }, + emqx_gateway_sup:create_gateway_insta(Insta). + +-spec remove(instance_id()) -> ok | {error, any()}. +remove(InstaId) -> + emqx_gateway_sup:remove_gateway_insta(InstaId). + +-spec lookup(instance_id()) -> instance() | undefined. +lookup(InstaId) -> + emqx_gateway_sup:lookup_gateway_insta(InstaId). + +-spec update(instance()) -> ok | {error, any()}. +update(NewInsta) -> + emqx_gateway_sup:update_gateway_insta(NewInsta). + +-spec start(instance_id()) -> ok | {error, any()}. +start(InstaId) -> + emqx_gateway_sup:start_gateway_insta(InstaId). + +-spec stop(instance_id()) -> ok | {error, any()}. +stop(InstaId) -> + emqx_gateway_sup:stop_gateway_insta(InstaId). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +clacu_insta_id(Type, Name) when is_binary(Name) -> + list_to_atom(lists:concat([Type, "#", binary_to_list(Name)])). diff --git a/apps/emqx_gateway/src/emqx_gateway_app.erl b/apps/emqx_gateway/src/emqx_gateway_app.erl new file mode 100644 index 000000000..c99228f17 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -0,0 +1,91 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_app). + +-behaviour(application). + +-include_lib("emqx/include/logger.hrl"). + +-logger_header("[Gateway]"). + +-export([start/2, stop/1]). + +start(_StartType, _StartArgs) -> + {ok, Sup} = emqx_gateway_sup:start_link(), + emqx_gateway_cli:load(), + load_default_gateway_applications(), + create_gateway_by_default(), + {ok, Sup}. + +stop(_State) -> + emqx_gateway_cli:unload(), + ok. + +%%-------------------------------------------------------------------- +%% Internal funcs + +load_default_gateway_applications() -> + Apps = gateway_type_searching(), + ?LOG(info, "Starting the default gateway types: ~p", [Apps]), + lists:foreach(fun load/1, Apps). + +gateway_type_searching() -> + %% FIXME: Hardcoded apps + [emqx_stomp_impl]. + +load(Mod) -> + try + Mod:load(), + ?LOG(info, "Load ~s gateway application successfully!", [Mod]) + catch + Class : Reason -> + ?LOG(error, "Load ~s gateway application failed: {~p, ~p}", + [Mod, Class, Reason]) + end. + +create_gateway_by_default() -> + create_gateway_by_default(zipped_confs()). + +create_gateway_by_default([]) -> + ok; +create_gateway_by_default([{Type, Name, Confs}|More]) -> + case emqx_gateway_registry:lookup(Type) of + undefined -> + ?LOG(error, "Skip to start ~p#~p: not_registred_type", + [Type, Name]); + _ -> + case emqx_gateway:create(Type, + atom_to_binary(Name, utf8), + <<>>, + Confs) of + {ok, _} -> + ?LOG(debug, "Start ~p#~p successfully!", [Type, Name]); + {error, Reason} -> + ?LOG(error, "Start ~p#~p failed: ~0p", + [Type, Name, Reason]) + end + end, + create_gateway_by_default(More). + +zipped_confs() -> + All = maps:to_list(emqx_config:get([emqx_gateway])), + lists:append(lists:foldr( + fun({Type, Gws}, Acc) -> + {Names, Confs} = lists:unzip(maps:to_list(Gws)), + Types = [ Type || _ <- lists:seq(1, length(Names))], + [lists:zip3(Types, Names, Confs) | Acc] + end, [], All)). diff --git a/apps/emqx_gateway/src/emqx_gateway_cli.erl b/apps/emqx_gateway/src/emqx_gateway_cli.erl new file mode 100644 index 000000000..beb3e5eae --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_cli.erl @@ -0,0 +1,201 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The Command-Line-Interface module for Gateway Application +-module(emqx_gateway_cli). + +-export([ load/0 + , unload/0 + ]). + +-export([ gateway/1 + , 'gateway-registry'/1 + , 'gateway-clients'/1 + , 'gateway-metrics'/1 + %, 'gateway-banned'/1 + ]). + +-spec load() -> ok. +load() -> + Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)], + lists:foreach(fun(Cmd) -> emqx_ctl:register_command(Cmd, {?MODULE, Cmd}, []) end, Cmds). + +-spec unload() -> ok. +unload() -> + Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)], + lists:foreach(fun(Cmd) -> emqx_ctl:unregister_command(Cmd) end, Cmds). + +is_cmd(Fun) -> + not lists:member(Fun, [init, load, module_info]). + + +%%-------------------------------------------------------------------- +%% Cmds + +gateway(["list"]) -> + lists:foreach(fun(#{id := InstaId, name := Name, type := Type}) -> + %% FIXME: Get the real running status + emqx_ctl:print("Gateway(~s, name=~s, type=~s, status=running~n", + [InstaId, Name, Type]) + end, emqx_gateway:list()); + +gateway(["lookup", GatewayInstaId]) -> + case emqx_gateway:lookup(GatewayInstaId) of + undefined -> + emqx_ctl:print("undefined"); + Info -> + emqx_ctl:print("~p~n", [Info]) + end; + +gateway(["stop", GatewayInstaId]) -> + case emqx_gateway:stop(GatewayInstaId) of + ok -> + emqx_ctl:print("ok"); + {error, Reason} -> + emqx_ctl:print("Error: ~p~n", [Reason]) + end; + +gateway(["start", GatewayInstaId]) -> + case emqx_gateway:start(GatewayInstaId) of + ok -> + emqx_ctl:print("ok"); + {error, Reason} -> + emqx_ctl:print("Error: ~p~n", [Reason]) + end; + +gateway(_) -> + %% TODO: create/remove APIs + emqx_ctl:usage([ {"gateway list", + "List all created gateway instances"} + , {"gateway lookup ", + "Looup a gateway detailed informations"} + , {"gateway stop ", + "Stop a gateway instance and release all resources"} + , {"gateway start ", + "Start a gateway instance"} + ]). + +'gateway-registry'(["list"]) -> + lists:foreach( + fun({GwType, #{cbkmod := CbMod}}) -> + emqx_ctl:print("Registered Type: ~s, Callback Module: ~s~n", [GwType, CbMod]) + end, + emqx_gateway_registry:list()); + +'gateway-registry'(_) -> + emqx_ctl:usage([ {"gateway-registry list", + "List all registered gateway types"} + ]). + +'gateway-clients'(["list", Type]) -> + InfoTab = emqx_gateway_cm:tabname(info, Type), + dump(InfoTab, client); + +'gateway-clients'(["lookup", Type, ClientId]) -> + ChanTab = emqx_gateway_cm:tabname(chan, Type), + case ets:lookup(ChanTab, bin(ClientId)) of + [] -> emqx_ctl:print("Not Found.~n"); + [Chann] -> + InfoTab = emqx_gateway_cm:tabname(info, Type), + [ChannInfo] = ets:lookup(InfoTab, Chann), + print({client, ChannInfo}) + end; + +'gateway-clients'(["kick", Type, ClientId]) -> + case emqx_gateway_cm:kick_session(Type, bin(ClientId)) of + ok -> emqx_ctl:print("ok~n"); + _ -> emqx_ctl:print("Not Found.~n") + end; + +'gateway-clients'(_) -> + emqx_ctl:usage([ {"gateway-clients list ", + "List all clients for a type of gateway"} + , {"gateway-clients lookup ", + "Lookup the Client Info for specified client"} + , {"gateway-clients kick ", + "Kick out a client"} + ]). + +'gateway-metrics'([GatewayType]) -> + Tab = emqx_gateway_metrics:tabname(GatewayType), + case ets:info(Tab) of + undefined -> + emqx_ctl:print("Bad Gateway Tyep.~n"); + _ -> + lists:foreach( + fun({K, V}) -> + emqx_ctl:print("~-30s: ~w~n", [K, V]) + end, lists:sort(ets:tab2list(Tab))) + end; + +'gateway-metrics'(_) -> + emqx_ctl:usage([ {"gateway-metrics ", + "List all metrics for a type of gateway"} + ]). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +bin(S) -> iolist_to_binary(S). + +dump(Table, Tag) -> + dump(Table, Tag, ets:first(Table), []). + +dump(_Table, _, '$end_of_table', Result) -> + lists:reverse(Result); + +dump(Table, Tag, Key, Result) -> + PrintValue = [print({Tag, Record}) || Record <- ets:lookup(Table, Key)], + dump(Table, Tag, ets:next(Table, Key), [PrintValue | Result]). + +print({client, {_, Infos, Stats}}) -> + ClientInfo = maps:get(clientinfo, Infos, #{}), + ConnInfo = maps:get(conninfo, Infos, #{}), + _Session = maps:get(session, Infos, #{}), + SafeGet = fun(K, M) -> maps:get(K, M, undefined) end, + StatsGet = fun(K) -> proplists:get_value(K, Stats, 0) end, + + ConnectedAt = SafeGet(connected_at, ConnInfo), + InfoKeys = [clientid, username, peername, clean_start, keepalive, + subscriptions_cnt, send_msg, connected, created_at, connected_at], + Info = #{ clientid => SafeGet(clientid, ClientInfo), + username => SafeGet(username, ClientInfo), + peername => SafeGet(peername, ConnInfo), + clean_start => SafeGet(clean_start, ConnInfo), + keepalive => SafeGet(keepalive, ConnInfo), + subscriptions_cnt => StatsGet(subscriptions_cnt), + send_msg => StatsGet(send_msg), + connected => SafeGet(conn_state, ClientInfo) == connected, + created_at => ConnectedAt, + connected_at => ConnectedAt + }, + + emqx_ctl:print("Client(~s, username=~s, peername=~s, " + "clean_start=~s, keepalive=~w, " + "subscriptions=~w, delivered_msgs=~w, " + "connected=~s, created_at=~w, connected_at=~w)~n", + [format(K, maps:get(K, Info)) || K <- InfoKeys]). + +format(_, undefined) -> + undefined; + +format(peername, {IPAddr, Port}) -> + IPStr = emqx_mgmt_util:ntoa(IPAddr), + io_lib:format("~s:~p", [IPStr, Port]); + +format(_, Val) -> + Val. diff --git a/apps/emqx_gateway/src/emqx_gateway_cm.erl b/apps/emqx_gateway/src/emqx_gateway_cm.erl new file mode 100644 index 000000000..f8ca18c1a --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_cm.erl @@ -0,0 +1,447 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The gateway connection management +-module(emqx_gateway_cm). + +-behaviour(gen_server). + +-include("include/emqx_gateway.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-logger_header("[PGW-CM]"). + +%% APIs +-export([start_link/1]). + +-export([ open_session/5 + , kick_session/2 + , kick_session/3 + , register_channel/4 + , unregister_channel/2 + , insert_channel_info/4 + , set_chan_info/3 + , set_chan_info/4 + , get_chan_info/2 + , get_chan_info/3 + , set_chan_stats/3 + , set_chan_stats/4 + , get_chan_stats/2 + , get_chan_stats/3 + , connection_closed/2 + ]). + +%% Internal funcs for getting tabname by GatewayId +-export([cmtabs/1, tabname/2]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-record(state, { + type :: atom(), %% Gateway Id + locker :: pid(), %% ClientId Locker for CM + registry :: pid(), %% ClientId Registry server + chan_pmon :: emqx_pmon:pmon() + }). + +-define(T_TAKEOVER, 15000). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +%% XXX: Options for cm process +start_link(Options) -> + Type = proplists:get_value(type, Options), + gen_server:start_link({local, procname(Type)}, ?MODULE, Options, []). + +procname(Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_cm'])). + +-spec cmtabs(Type :: atom()) -> {ChanTab :: atom(), + ConnTab :: atom(), + ChannInfoTab :: atom()}. +cmtabs(Type) -> + { tabname(chan, Type) %% Client Tabname; Record: {ClientId, Pid} + , tabname(conn, Type) %% Client ConnMod; Recrod: {{ClientId, Pid}, ConnMod} + , tabname(info, Type) %% ClientInfo Tabname; Record: {{ClientId, Pid}, ClientInfo, ClientStats} + }. + +tabname(chan, Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_channel'])); +tabname(conn, Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_conn'])); +tabname(info, Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_info'])). + +lockername(Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_locker'])). + +-spec register_channel(atom(), binary(), pid(), emqx_types:conninfo()) -> ok. +register_channel(Type, ClientId, ChanPid, #{conn_mod := ConnMod}) when is_pid(ChanPid) -> + Chan = {ClientId, ChanPid}, + true = ets:insert(tabname(chan, Type), Chan), + true = ets:insert(tabname(conn, Type), {Chan, ConnMod}), + ok = emqx_gateway_cm_registry:register_channel(Type, Chan), + cast(procname(Type), {registered, Chan}). + +%% @doc Unregister a channel. +-spec unregister_channel(atom(), emqx_types:clientid()) -> ok. +unregister_channel(Type, ClientId) when is_binary(ClientId) -> + true = do_unregister_channel(Type, {ClientId, self()}, cmtabs(Type)), + ok. + +%% @doc Insert/Update the channel info and stats +-spec insert_channel_info(atom(), + emqx_types:clientid(), + emqx_types:infos(), + emqx_types:stats()) -> ok. +insert_channel_info(Type, ClientId, Info, Stats) -> + Chan = {ClientId, self()}, + true = ets:insert(tabname(info, Type), {Chan, Info, Stats}), + %%?tp(debug, insert_channel_info, #{client_id => ClientId}), + ok. + +%% @doc Get info of a channel. +-spec get_chan_info(gateway_type(), emqx_types:clientid()) + -> emqx_types:infos() | undefined. +get_chan_info(Type, ClientId) -> + with_channel(Type, ClientId, + fun(ChanPid) -> + get_chan_info(Type, ClientId, ChanPid) + end). + +-spec get_chan_info(gateway_type(), emqx_types:clientid(), pid()) + -> emqx_types:infos() | undefined. +get_chan_info(Type, ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try ets:lookup_element(tabname(info, Type), Chan, 2) + catch + error:badarg -> undefined + end; +get_chan_info(Type, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_info, [Type, ClientId, ChanPid]). + +%% @doc Update infos of the channel. +-spec set_chan_info(gateway_type(), + emqx_types:clientid(), + emqx_types:infos()) -> boolean(). +set_chan_info(Type, ClientId, Infos) -> + set_chan_info(Type, ClientId, self(), Infos). + +-spec set_chan_info(gateway_type(), + emqx_types:clientid(), + pid(), + emqx_types:infos()) -> boolean(). +set_chan_info(Type, ClientId, ChanPid, Infos) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try ets:update_element(tabname(info, Type), Chan, {2, Infos}) + catch + error:badarg -> false + end; +set_chan_info(Type, ClientId, ChanPid, Infos) -> + rpc_call(node(ChanPid), set_chan_info, [Type, ClientId, ChanPid, Infos]). + +%% @doc Get channel's stats. +-spec get_chan_stats(gateway_type(), emqx_types:clientid()) + -> emqx_types:stats() | undefined. +get_chan_stats(Type, ClientId) -> + with_channel(Type, ClientId, + fun(ChanPid) -> + get_chan_stats(Type, ClientId, ChanPid) + end). + +-spec get_chan_stats(gateway_type(), emqx_types:clientid(), pid()) + -> emqx_types:stats() | undefined. +get_chan_stats(Type, ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try ets:lookup_element(tabname(info, Type), Chan, 3) + catch + error:badarg -> undefined + end; +get_chan_stats(Type, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_stats, [Type, ClientId, ChanPid]). + +-spec set_chan_stats(gateway_type(), + emqx_types:clientid(), + emqx_types:stats()) -> boolean(). +set_chan_stats(Type, ClientId, Stats) -> + set_chan_stats(Type, ClientId, self(), Stats). + +-spec set_chan_stats(gateway_type(), + emqx_types:clientid(), + pid(), + emqx_types:stats()) -> boolean(). +set_chan_stats(Type, ClientId, ChanPid, Stats) when node(ChanPid) == node() -> + Chan = {ClientId, self()}, + try ets:update_element(tabname(info, Type), Chan, {3, Stats}) + catch + error:badarg -> false + end; +set_chan_stats(Type, ClientId, ChanPid, Stats) -> + rpc_call(node(ChanPid), set_chan_stats, [Type, ClientId, ChanPid, Stats]). + +-spec connection_closed(gateway_type(), emqx_types:clientid()) -> true. +connection_closed(Type, ClientId) -> + %% XXX: Why we need to delete conn_mod tab ??? + Chan = {ClientId, self()}, + ets:delete_object(tabname(conn, Type), Chan). + +-spec open_session(Type :: atom(), CleanStart :: boolean(), + ClientInfo :: emqx_types:clientinfo(), + ConnInfo :: emqx_types:conninfo(), + CreateSessionFun :: function()) + -> {ok, #{session := map(), + present := boolean(), + pendings => list() + }} + | {error, any()}. + +open_session(Type, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> + Self = self(), + ClientId = maps:get(clientid, ClientInfo), + Fun = fun(_) -> + ok = discard_session(Type, ClientId), + Session = create_session(Type, + ClientInfo, + ConnInfo, + CreateSessionFun + ), + register_channel(Type, ClientId, Self, ConnInfo), + {ok, #{session => Session, present => false}} + end, + locker_trans(Type, ClientId, Fun); + +open_session(_Type, false = _CleanStart, + _ClientInfo, _ConnInfo, _CreateSessionFun) -> + {error, not_supported_now}. + +%% @private +create_session(_Type, ClientInfo, ConnInfo, CreateSessionFun) -> + try + Session = emqx_gateway_utils:apply( + CreateSessionFun, + [ClientInfo, ConnInfo] + ), + %% TODO: v0.2 session metrics & hooks + %ok = emqx_metrics:inc('session.created'), + %ok = emqx_hooks:run('session.created', [ClientInfo, emqx_session:info(Session)]), + Session + catch + Class : Reason : Stk -> + ?LOG(error, "Failed to create a session: ~p, ~p " + "Stacktrace:~0p", [Class, Reason, Stk]), + throw(Reason) + end. + +%% @doc Discard all the sessions identified by the ClientId. +-spec discard_session(Type :: atom(), binary()) -> ok. +discard_session(Type, ClientId) when is_binary(ClientId) -> + case lookup_channels(Type, ClientId) of + [] -> ok; + ChanPids -> lists:foreach(fun(Pid) -> do_discard_session(Type, ClientId, Pid) end, ChanPids) + end. + +%% @private +do_discard_session(Type, ClientId, Pid) -> + try + discard_session(Type, ClientId, Pid) + catch + _ : noproc -> % emqx_ws_connection: call + %?tp(debug, "session_already_gone", #{pid => Pid}), + ok; + _ : {noproc, _} -> % emqx_connection: gen_server:call + %?tp(debug, "session_already_gone", #{pid => Pid}), + ok; + _ : {{shutdown, _}, _} -> + %?tp(debug, "session_already_shutdown", #{pid => Pid}), + ok; + _ : _Error : _St -> + %?tp(error, "failed_to_discard_session", + % #{pid => Pid, reason => Error, stacktrace=>St}) + ok + end. + +%% @private +discard_session(Type, ClientId, ChanPid) when node(ChanPid) == node() -> + case get_chann_conn_mod(Type, ClientId, ChanPid) of + undefined -> ok; + ConnMod when is_atom(ConnMod) -> + ConnMod:call(ChanPid, discard, ?T_TAKEOVER) + end; + +%% @private +discard_session(Type, ClientId, ChanPid) -> + rpc_call(node(ChanPid), discard_session, [Type, ClientId, ChanPid]). + +-spec kick_session(gateway_type(), emqx_types:clientid()) + -> {error, any()} + | ok. +kick_session(Type, ClientId) -> + case lookup_channels(Type, ClientId) of + [] -> {error, not_found}; + [ChanPid] -> + kick_session(Type, ClientId, ChanPid); + ChanPids -> + [ChanPid|StalePids] = lists:reverse(ChanPids), + ?LOG(error, "More than one channel found: ~p", [ChanPids]), + lists:foreach(fun(StalePid) -> + catch discard_session(Type, ClientId, StalePid) + end, StalePids), + kick_session(Type, ClientId, ChanPid) + end. + +kick_session(Type, ClientId, ChanPid) when node(ChanPid) == node() -> + case get_chan_info(Type, ClientId, ChanPid) of + #{conninfo := #{conn_mod := ConnMod}} -> + ConnMod:call(ChanPid, kick, ?T_TAKEOVER); + undefined -> + {error, not_found} + end; + +kick_session(Type, ClientId, ChanPid) -> + rpc_call(node(ChanPid), kick_session, [Type, ClientId, ChanPid]). + +with_channel(Type, ClientId, Fun) -> + case lookup_channels(Type, ClientId) of + [] -> undefined; + [Pid] -> Fun(Pid); + Pids -> Fun(lists:last(Pids)) + end. + +%% @doc Lookup channels. +-spec(lookup_channels(atom(), emqx_types:clientid()) -> list(pid())). +lookup_channels(Type, ClientId) -> + emqx_gateway_cm_registry:lookup_channels(Type, ClientId). + +get_chann_conn_mod(Type, ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try [ConnMod] = ets:lookup_element(tabname(conn, Type), Chan, 2), ConnMod + catch + error:badarg -> undefined + end; +get_chann_conn_mod(Type, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chann_conn_mod, [Type, ClientId, ChanPid]). + +%% Locker + +locker_trans(_Type, undefined, Fun) -> + Fun([]); +locker_trans(Type, ClientId, Fun) -> + Locker = lockername(Type), + case locker_lock(Locker, ClientId) of + {true, Nodes} -> + try Fun(Nodes) after locker_unlock(Locker, ClientId) end; + {false, _Nodes} -> + {error, client_id_unavailable} + end. + +locker_lock(Locker, ClientId) -> + ekka_locker:acquire(Locker, ClientId, quorum). + +locker_unlock(Locker, ClientId) -> + ekka_locker:release(Locker, ClientId, quorum). + +%% @private +rpc_call(Node, Fun, Args) -> + case rpc:call(Node, ?MODULE, Fun, Args) of + {badrpc, Reason} -> error(Reason); + Res -> Res + end. + +cast(Name, Msg) -> + gen_server:cast(Name, Msg). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init(Options) -> + Type = proplists:get_value(type, Options), + + TabOpts = [public, {write_concurrency, true}], + + {ChanTab, ConnTab, InfoTab} = cmtabs(Type), + ok = emqx_tables:new(ChanTab, [bag, {read_concurrency, true}|TabOpts]), + ok = emqx_tables:new(ConnTab, [bag | TabOpts]), + ok = emqx_tables:new(InfoTab, [set, compressed | TabOpts]), + + %% Start link cm-registry process + %% XXX: Should I hang it under a higher level supervisor? + {ok, Registry} = emqx_gateway_cm_registry:start_link(Type), + + %% Start locker process + {ok, Locker} = ekka_locker:start_link(lockername(Type)), + + %% Interval update stats + %% TODO: v0.2 + %ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0), + + {ok, #state{type = Type, + locker = Locker, + registry = Registry, + chan_pmon = emqx_pmon:new()}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast({registered, {ClientId, ChanPid}}, State = #state{chan_pmon = PMon}) -> + PMon1 = emqx_pmon:monitor(ChanPid, ClientId, PMon), + {noreply, State#state{chan_pmon = PMon1}}; + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({'DOWN', _MRef, process, Pid, _Reason}, + State = #state{type = Type, chan_pmon = PMon}) -> + ChanPids = [Pid | emqx_misc:drain_down(10000)], %% XXX: Fixed BATCH_SIZE + {Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon), + + CmTabs = cmtabs(Type), + ok = emqx_pool:async_submit(fun do_unregister_channel_task/3, [Items, Type, CmTabs]), + {noreply, State#state{chan_pmon = PMon1}}; + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +do_unregister_channel_task(Items, Type, CmTabs) -> + lists:foreach( + fun({ChanPid, ClientId}) -> + do_unregister_channel(Type, {ClientId, ChanPid}, CmTabs) + end, Items). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +do_unregister_channel(Type, Chan, {ChanTab, ConnTab, InfoTab}) -> + ok = emqx_gateway_cm_registry:unregister_channel(Type, Chan), + true = ets:delete(ConnTab, Chan), + true = ets:delete(InfoTab, Chan), + ets:delete_object(ChanTab, Chan). diff --git a/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl b/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl new file mode 100644 index 000000000..4275fdf3e --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl @@ -0,0 +1,141 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The gateway connection registry +-module(emqx_gateway_cm_registry). + +-behaviour(gen_server). + +-logger_header("[PGW-CM-Registy]"). + +-export([start_link/1]). + +%% XXX: needless +%-export([is_enabled/0]). + +-export([ register_channel/2 + , unregister_channel/2 + ]). + +-export([lookup_channels/2]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-define(LOCK, {?MODULE, cleanup_down}). + +-record(channel, {chid, pid}). + +%% @doc Start the global channel registry. +-spec(start_link(atom()) -> gen_server:startlink_ret()). +start_link(Type) -> + gen_server:start_link(?MODULE, [Type], []). + +-spec tabname(atom()) -> atom(). +tabname(Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_registry'])). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +%% @doc Register a global channel. +-spec register_channel(atom(), binary() | {binary(), pid()}) -> ok. +register_channel(Type, ClientId) when is_binary(ClientId) -> + register_channel(Type, {ClientId, self()}); + +register_channel(Type, {ClientId, ChanPid}) when is_binary(ClientId), is_pid(ChanPid) -> + ekka_mnesia:dirty_write(tabname(Type), record(ClientId, ChanPid)). + +%% @doc Unregister a global channel. +-spec unregister_channel(atom(), binary() | {binary(), pid()}) -> ok. +unregister_channel(Type, ClientId) when is_binary(ClientId) -> + unregister_channel(Type, {ClientId, self()}); + +unregister_channel(Type, {ClientId, ChanPid}) when is_binary(ClientId), is_pid(ChanPid) -> + ekka_mnesia:dirty_delete_object(tabname(Type), record(ClientId, ChanPid)). + +%% @doc Lookup the global channels. +-spec lookup_channels(atom(), binary()) -> list(pid()). +lookup_channels(Type, ClientId) -> + [ChanPid || #channel{pid = ChanPid} <- mnesia:dirty_read(tabname(Type), ClientId)]. + +record(ClientId, ChanPid) -> + #channel{chid = ClientId, pid = ChanPid}. + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Type]) -> + Tab = tabname(Type), + ok = ekka_mnesia:create_table(Tab, [ + {type, bag}, + {ram_copies, [node()]}, + {record_name, channel}, + {attributes, record_info(fields, channel)}, + {storage_properties, [{ets, [{read_concurrency, true}, + {write_concurrency, true}]}]}]), + ok = ekka_mnesia:copy_table(Tab, ram_copies), + %%ok = ekka_rlog:wait_for_shards([?CM_SHARD], infinity), + ok = ekka:monitor(membership), + {ok, #{type => Type}}. + +handle_call(Req, _From, State) -> + logger:error("Unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Msg, State) -> + logger:error("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({membership, {mnesia, down, Node}}, State = #{type := Type}) -> + Tab = tabname(Type), + global:trans({?LOCK, self()}, + fun() -> + %% FIXME: The shard name should be fixed later + ekka_mnesia:transaction(?MODULE, fun cleanup_channels/2, [Node, Tab]) + end), + {noreply, State}; + +handle_info({membership, _Event}, State) -> + {noreply, State}; + +handle_info(Info, State) -> + logger:error("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +cleanup_channels(Node, Tab) -> + Pat = [{#channel{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}], + lists:foreach(fun(Chan) -> + mnesia:delete_object(Tab, Chan, write) + end, mnesia:select(Tab, Pat, write)). diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl new file mode 100644 index 000000000..5dadf2aca --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -0,0 +1,148 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The gateway instance context +-module(emqx_gateway_ctx). + +-include("include/emqx_gateway.hrl"). + +-logger_header(["PGW-Ctx"]). + +%% @doc The running context for a Connection/Channel process. +%% +%% The `Context` encapsulates a complex structure of contextual information. +%% It is convenient to use it directly in Channel/Connection to read +%% configuration, register devices and other common operations. +%% +-type context() :: + #{ %% Gateway Instance ID + instid := instance_id() + %% Gateway ID + , type := gateway_type() + %% Autenticator + , auth := allow_anonymous | emqx_authentication:chain_id() + %% The ConnectionManager PID + , cm := pid() + }. + +%% Authentication circle +-export([ authenticate/2 + , open_session/5 + , insert_channel_info/4 + , set_chan_info/3 + , set_chan_stats/3 + , connection_closed/2 + ]). + +%% Message circle +-export([ authorize/4 + % Needless for pub/sub + %, publish/3 + %, subscribe/4 + ]). + +%% Metrics & Stats +-export([ metrics_inc/2 + , metrics_inc/3 + ]). + +%%-------------------------------------------------------------------- +%% Authentication circle + +%% @doc Authenticate whether the client has access to the Broker. +-spec authenticate(context(), emqx_types:clientinfo()) + -> {ok, emqx_types:clientinfo()} + | {error, any()}. +authenticate(_Ctx = #{auth := allow_anonymous}, ClientInfo) -> + {ok, ClientInfo#{anonymous => true}}; +authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) -> + ClientInfo = ClientInfo0#{ + zone => undefined, + chain_id => ChainId + }, + case emqx_access_control:authenticate(ClientInfo) of + {ok, AuthResult} -> + {ok, mountpoint(maps:merge(ClientInfo, AuthResult))}; + {error, Reason} -> + {error, Reason} + end. + +%% @doc Register the session to the cluster. +%% +%% This function should be called after the client has authenticated +%% successfully so that the client can be managed in the cluster. +-spec open_session(context(), boolean(), emqx_types:clientinfo(), + emqx_types:conninfo(), function()) + -> {ok, #{session := any(), + present := boolean(), + pendings => list() + }} + | {error, any()}. +open_session(Ctx, false, ClientInfo, ConnInfo, CreateSessionFun) -> + logger:warning("clean_start=false is not supported now, " + "fallback to clean_start mode"), + open_session(Ctx, true, ClientInfo, ConnInfo, CreateSessionFun); + +open_session(_Ctx = #{type := Type}, + CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> + emqx_gateway_cm:open_session(Type, CleanStart, + ClientInfo, ConnInfo, CreateSessionFun). + +-spec insert_channel_info(context(), + emqx_types:clientid(), + emqx_types:infos(), + emqx_types:stats()) -> ok. +insert_channel_info(_Ctx = #{type := Type}, ClientId, Infos, Stats) -> + emqx_gateway_cm:insert_channel_info(Type, ClientId, Infos, Stats). + +%% @doc Set the Channel Info to the ConnectionManager for this client +-spec set_chan_info(context(), + emqx_types:clientid(), + emqx_types:infos()) -> boolean(). +set_chan_info(_Ctx = #{type := Type}, ClientId, Infos) -> + emqx_gateway_cm:set_chan_info(Type, ClientId, Infos). + +-spec set_chan_stats(context(), + emqx_types:clientid(), + emqx_types:stats()) -> boolean(). +set_chan_stats(_Ctx = #{type := Type}, ClientId, Stats) -> + emqx_gateway_cm:set_chan_stats(Type, ClientId, Stats). + +-spec connection_closed(context(), emqx_types:clientid()) -> boolean(). +connection_closed(_Ctx = #{type := Type}, ClientId) -> + emqx_gateway_cm:connection_closed(Type, ClientId). + +-spec authorize(context(), emqx_types:clientinfo(), + emqx_types:pubsub(), emqx_types:topic()) + -> allow | deny. +authorize(_Ctx, ClientInfo, PubSub, Topic) -> + emqx_access_control:authorize(ClientInfo, PubSub, Topic). + +metrics_inc(_Ctx = #{type := Type}, Name) -> + emqx_gateway_metrics:inc(Type, Name). + +metrics_inc(_Ctx = #{type := Type}, Name, Oct) -> + emqx_gateway_metrics:inc(Type, Name, Oct). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +mountpoint(ClientInfo = #{mountpoint := undefined}) -> + ClientInfo; +mountpoint(ClientInfo = #{mountpoint := MountPoint}) -> + MountPoint1 = emqx_mountpoint:replvar(MountPoint, ClientInfo), + ClientInfo#{mountpoint := MountPoint1}. diff --git a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl new file mode 100644 index 000000000..21ad30c0d --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl @@ -0,0 +1,135 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The Gateway Top supervisor. +-module(emqx_gateway_gw_sup). + +-behaviour(supervisor). + +-include("include/emqx_gateway.hrl"). + +-export([start_link/1]). + +-export([ create_insta/3 + , remove_insta/2 + , update_insta/2 + , start_insta/2 + , stop_insta/2 + , list_insta/1 + ]). + +%% Supervisor callbacks +-export([init/1]). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link(Type) -> + supervisor:start_link({local, Type}, ?MODULE, [Type]). + +-spec create_insta(pid(), instance(), map()) -> {ok, GwInstaPid :: pid()} | {error, any()}. +create_insta(Sup, Insta = #{id := InstaId}, GwDscrptr) -> + case emqx_gateway_utils:find_sup_child(Sup, InstaId) of + {ok, _GwInstaPid} -> {error, alredy_existed}; + false -> + %% XXX: More instances options to it? + %% + Ctx = ctx(Sup, InstaId), + %% + ChildSpec = emqx_gateway_utils:childspec( + InstaId, + worker, + emqx_gateway_insta_sup, + [Insta, Ctx, GwDscrptr] + ), + emqx_gateway_utils:supervisor_ret( + supervisor:start_child(Sup, ChildSpec) + ) + end. + +-spec remove_insta(pid(), InstaId :: atom()) -> ok | {error, any()}. +remove_insta(Sup, InstaId) -> + case emqx_gateway_utils:find_sup_child(Sup, InstaId) of + false -> ok; + {ok, _GwInstaPid} -> + ok = supervisor:terminate_child(Sup, InstaId), + ok = supervisor:delete_child(Sup, InstaId) + end. + +-spec update_insta(pid(), NewInsta :: instance()) -> ok | {error, any()}. +update_insta(Sup, NewInsta = #{id := InstaId}) -> + case emqx_gateway_utils:find_sup_child(Sup, InstaId) of + false -> {error, not_found}; + {ok, GwInstaPid} -> + emqx_gateway_insta_sup:update(GwInstaPid, NewInsta) + end. + +-spec start_insta(pid(), atom()) -> ok | {error, any()}. +start_insta(Sup, InstaId) -> + case emqx_gateway_utils:find_sup_child(Sup, InstaId) of + false -> {error, not_found}; + {ok, GwInstaPid} -> + emqx_gateway_insta_sup:enable(GwInstaPid) + end. + +-spec stop_insta(pid(), atom()) -> ok | {error, any()}. +stop_insta(Sup, InstaId) -> + case emqx_gateway_utils:find_sup_child(Sup, InstaId) of + false -> {error, not_found}; + {ok, GwInstaPid} -> + emqx_gateway_insta_sup:disable(GwInstaPid) + end. + +-spec list_insta(pid()) -> [instance()]. +list_insta(Sup) -> + lists:filtermap( + fun({InstaId, GwInstaPid, _Type, _Mods}) -> + is_gateway_insta_id(InstaId) + andalso {true, emqx_gateway_insta_sup:info(GwInstaPid)} + end, supervisor:which_children(Sup)). + +%% Supervisor callback + +%% @doc Initialize Top Supervisor for a Protocol +init([Type]) -> + SupFlags = #{ strategy => one_for_one + , intensity => 10 + , period => 60 + }, + CmOpts = [{type, Type}], + CM = emqx_gateway_utils:childspec(worker, emqx_gateway_cm, [CmOpts]), + Metrics = emqx_gateway_utils:childspec(worker, emqx_gateway_metrics, [Type]), + {ok, {SupFlags, [CM, Metrics]}}. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +ctx(Sup, InstaId) -> + {_, Type} = erlang:process_info(Sup, registered_name), + {ok, CM} = emqx_gateway_utils:find_sup_child(Sup, emqx_gateway_cm), + #{ instid => InstaId + , type => Type + , cm => CM + }. + +is_gateway_insta_id(emqx_gateway_cm) -> + false; +is_gateway_insta_id(emqx_gateway_metrics) -> + false; +is_gateway_insta_id(_Id) -> + true. diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl new file mode 100644 index 000000000..9f21f0e05 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -0,0 +1,312 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The gateway instance management +-module(emqx_gateway_insta_sup). + +-behaviour(gen_server). + +-include("include/emqx_gateway.hrl"). + +-logger_header("[PGW-Insta-Sup]"). + +%% APIs +-export([ start_link/3 + , info/1 + , disable/1 + , enable/1 + , update/2 + ]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-record(state, { + insta :: instance(), + ctx :: emqx_gateway_ctx:context(), + status :: stopped | running, + child_pids :: [pid()], + insta_state :: emqx_gateway_impl:state() | undefined + }). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link(Insta, Ctx, GwDscrptr) -> + gen_server:start_link( + {local, ?MODULE}, + ?MODULE, + [Insta, Ctx, GwDscrptr], + [] + ). + +-spec info(pid()) -> instance(). +info(Pid) -> + gen_server:call(Pid, info). + +%% @doc Stop instance +-spec disable(pid()) -> ok | {error, any()}. +disable(Pid) -> + call(Pid, disable). + +%% @doc Start instance +-spec enable(pid()) -> ok | {error, any()}. +enable(Pid) -> + call(Pid, enable). + +%% @doc Update the gateway instance configurations +-spec update(pid(), instance()) -> ok | {error, any()}. +update(Pid, NewInsta) -> + call(Pid, {update, NewInsta}). + +call(Pid, Req) -> + gen_server:call(Pid, Req, 5000). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Insta, Ctx0, _GwDscrptr]) -> + process_flag(trap_exit, true), + #{rawconf := RawConf} = Insta, + Ctx = do_init_context(RawConf, Ctx0), + State = #state{ + insta = Insta, + ctx = Ctx, + child_pids = [], + status = stopped + }, + case cb_insta_create(State) of + {error, _Reason} -> + do_deinit_context(Ctx), + %% XXX: Return Reason?? + {stop, create_gateway_instance_failed}; + {ok, NState} -> + {ok, NState} + end. + +do_init_context(RawConf, Ctx) -> + Auth = case maps:get(authenticator, RawConf, allow_anonymous) of + allow_anonymous -> allow_anonymous; + Funcs when is_list(Funcs) -> + create_authenticator_for_gateway_insta(Funcs) + end, + Ctx#{auth => Auth}. + +do_deinit_context(Ctx) -> + cleanup_authenticator_for_gateway_insta(maps:get(auth, Ctx)), + ok. + +handle_call(info, _From, State = #state{insta = Insta}) -> + {reply, Insta, State}; + +handle_call(disable, _From, State = #state{status = Status}) -> + case Status of + running -> + case cb_insta_destroy(State) of + {ok, NState} -> + {reply, ok, NState}; + {error, Reason} -> + {reply, {error, Reason}, State} + end; + _ -> + {reply, {error, already_stopped}, State} + end; + +handle_call(enable, _From, State = #state{status = Status}) -> + case Status of + stopped -> + case cb_insta_create(State) of + {error, Reason} -> + {reply, {error, Reason}, State}; + {ok, NState} -> + {reply, ok, NState} + end; + _ -> + {reply, {error, already_started}, State} + end; + +%% Stopped -> update +handle_call({update, NewInsta}, _From, State = #state{insta = Insta, + status = stopped}) -> + case maps:get(id, NewInsta, undefined) == maps:get(id, Insta, undefined) of + true -> + {reply, ok, State#state{insta = NewInsta}}; + false -> + {reply, {error, bad_instan_id}, State} + end; + +%% Running -> update +handle_call({update, NewInsta}, _From, State = #state{insta = Insta, + status = running}) -> + case maps:get(id, NewInsta, undefined) == maps:get(id, Insta, undefined) of + true -> + case cb_insta_update(NewInsta, State) of + {ok, NState} -> + {reply, ok, NState}; + {error, Reason} -> + {reply, {error, Reason}, State} + end; + false -> + {reply, {error, bad_instan_id}, State} + end; + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({'EXIT', Pid, Reason}, State = #state{child_pids = Pids}) -> + case lists:member(Pid, Pids) of + true -> + logger:error("Child process ~p exited: ~0p.", [Pid, Reason]), + case Pids -- [Pid]of + [] -> + logger:error("All child process exited!"), + {noreply, State#state{status = stopped, + child_pids = [], + insta_state = undefined}}; + RemainPids -> + {noreply, State#state{child_pids = RemainPids}} + end; + _ -> + logger:error("Unknown process exited ~p:~0p", [Pid, Reason]), + {noreply, State} + end; + +handle_info(Info, State) -> + logger:warning("Unexcepted info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, State = #state{ctx = Ctx, child_pids = Pids}) -> + %% Cleanup instances + %% Step1. Destory instance + Pids /= [] andalso (_ = cb_insta_destroy(State)), + %% Step2. Delete authenticator resources + _ = do_deinit_context(Ctx), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +create_authenticator_for_gateway_insta(_Funcs) -> + todo. + +cleanup_authenticator_for_gateway_insta(allow_anonymouse) -> + ok; +cleanup_authenticator_for_gateway_insta(_ChainId) -> + todo. + +cb_insta_destroy(State = #state{insta = Insta = #{type := Type}, + insta_state = InstaState}) -> + try + #{cbkmod := CbMod, + state := GwState} = emqx_gateway_registry:lookup(Type), + CbMod:on_insta_destroy(Insta, InstaState, GwState), + {ok, State#state{child_pids = [], + insta_state = undefined, + status = stopped}} + catch + Class : Reason : Stk -> + logger:error("Destroy instance (~0p, ~0p, _) crashed: " + "{~p, ~p}, stacktrace: ~0p", + [Insta, InstaState, + Class, Reason, Stk]), + {error, {Class, Reason, Stk}} + end. + +cb_insta_create(State = #state{insta = Insta = #{type := Type}, + ctx = Ctx}) -> + try + #{cbkmod := CbMod, + state := GwState} = emqx_gateway_registry:lookup(Type), + case CbMod:on_insta_create(Insta, Ctx, GwState) of + {error, Reason} -> throw({callback_return_error, Reason}); + {ok, InstaPidOrSpecs, InstaState} -> + ChildPids = start_child_process(InstaPidOrSpecs), + {ok, State#state{ + status = running, + child_pids = ChildPids, + insta_state = InstaState + }} + end + catch + Class : Reason1 : Stk -> + logger:error("Create instance (~0p, ~0p, _) crashed: " + "{~p, ~p}, stacktrace: ~0p", + [Insta, Ctx, + Class, Reason1, Stk]), + {error, {Class, Reason1, Stk}} + end. + +cb_insta_update(NewInsta, + State = #state{insta = Insta = #{type := Type}, + ctx = Ctx, + insta_state = GwInstaState}) -> + try + #{cbkmod := CbMod, + state := GwState} = emqx_gateway_registry:lookup(Type), + case CbMod:on_insta_update(NewInsta, Insta, GwInstaState, GwState) of + {error, Reason} -> throw({callback_return_error, Reason}); + {ok, InstaPidOrSpecs, InstaState} -> + %% XXX: Hot-upgrade ??? + ChildPids = start_child_process(InstaPidOrSpecs), + {ok, State#state{ + status = running, + child_pids = ChildPids, + insta_state = InstaState + }} + end + catch + Class : Reason1 : Stk -> + logger:error("Update instance (~0p, ~0p, ~0p, _) crashed: " + "{~p, ~p}, stacktrace: ~0p", + [NewInsta, Insta, Ctx, + Class, Reason1, Stk]), + {error, {Class, Reason1, Stk}} + end. + +start_child_process([Indictor|_] = InstaPidOrSpecs) -> + case erlang:is_pid(Indictor) of + true -> + InstaPidOrSpecs; + _ -> + do_start_child_process(InstaPidOrSpecs) + end. + +do_start_child_process(ChildSpecs) when is_list(ChildSpecs) -> + lists:map(fun do_start_child_process/1, ChildSpecs); + +do_start_child_process(_ChildSpec = #{start := {M, F, A}}) -> + case erlang:apply(M, F, A) of + {ok, Pid} -> + Pid; + {error, Reason} -> + throw({start_child_process, Reason}) + end. diff --git a/apps/emqx_gateway/src/emqx_gateway_metrics.erl b/apps/emqx_gateway/src/emqx_gateway_metrics.erl new file mode 100644 index 000000000..04b711d0a --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_metrics.erl @@ -0,0 +1,101 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_metrics). + +-behaviour(gen_server). + +-include("include/emqx_gateway.hrl"). + +-logger_header("[PGW-Metrics]"). + +%% APIs +-export([start_link/1]). + +-export([ inc/2 + , inc/3 + , dec/2 + , dec/3 + ]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-export([tabname/1]). + +-record(state, {}). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link(Type) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [Type], []). + +-spec inc(gateway_type(), atom()) -> ok. +inc(Type, Name) -> + inc(Type, Name, 1). + +-spec inc(gateway_type(), atom(), integer()) -> ok. +inc(Type, Name, Oct) -> + ets:update_counter(tabname(Type), Name, {2, Oct}, {Name, 0}), + ok. + +-spec dec(gateway_type(), atom()) -> ok. +dec(Type, Name) -> + inc(Type, Name, -1). + +-spec dec(gateway_type(), atom(), non_neg_integer()) -> ok. +dec(Type, Name, Oct) -> + inc(Type, Name, -Oct). + +tabname(Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_metrics'])). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Type]) -> + TabOpts = [public, {write_concurrency, true}], + ok = emqx_tables:new(tabname(Type), [set|TabOpts]), + {ok, #state{}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/emqx_gateway_registry.erl b/apps/emqx_gateway/src/emqx_gateway_registry.erl new file mode 100644 index 000000000..a100636cf --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_registry.erl @@ -0,0 +1,163 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The Registry Centre of Gateway Type +-module(emqx_gateway_registry). + +-include("include/emqx_gateway.hrl"). + +-logger_header("[PGW-Registry]"). + +-behavior(gen_server). + +%% APIs for Impl. +-export([ load/3 + , unload/1 + ]). + +-export([ list/0 + , lookup/1 + ]). + +%% APIs +-export([start_link/0]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-record(state, { + loaded = #{} :: #{ gateway_type() => descriptor() } + }). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +%%-------------------------------------------------------------------- +%% Mgmt +%%-------------------------------------------------------------------- + +-type registry_options() :: [registry_option()]. + +-type registry_option() :: {cbkmod, atom()}. + +-type gateway_options() :: list(). + +-type descriptor() :: #{ cbkmod := atom() + , rgopts := registry_options() + , gwopts := gateway_options() + , state => any() + }. + +-spec load(gateway_type(), registry_options(), gateway_options()) -> ok | {error, any()}. +load(Type, RgOpts, GwOpts) -> + CbMod = proplists:get_value(cbkmod, RgOpts, Type), + Dscrptr = #{ cbkmod => CbMod + , rgopts => RgOpts + , gwopts => GwOpts + }, + call({load, Type, Dscrptr}). + +-spec unload(gateway_type()) -> ok | {error, any()}. +unload(Type) -> + %% TODO: Checking ALL INSTACE HAS STOPPED + call({unload, Type}). + +%% TODO: +%unload(Type, Force) -> +% call({unload, Type, Froce}). + +%% @doc Return all registered protocol gateway implementation +-spec list() -> [{gateway_type(), descriptor()}]. +list() -> + call(all). + +-spec lookup(gateway_type()) -> undefined | descriptor(). +lookup(Type) -> + call({lookup, Type}). + +call(Req) -> + gen_server:call(?MODULE, Req, 5000). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([]) -> + %% TODO: Metrics ??? + process_flag(trap_exit, true), + {ok, #state{loaded = #{}}}. + +handle_call({load, Type, Dscrptr}, _From, State = #state{loaded = Gateways}) -> + case maps:get(Type, Gateways, notfound) of + notfound -> + try + GwOpts = maps:get(gwopts, Dscrptr), + CbMod = maps:get(cbkmod, Dscrptr), + {ok, GwState} = CbMod:init(GwOpts), + NDscrptr = maps:put(state, GwState, Dscrptr), + NGateways = maps:put(Type, NDscrptr, Gateways), + {reply, ok, State#state{loaded = NGateways}} + catch + Class : Reason : Stk -> + logger:error("Load ~s crashed {~p, ~p}; stacktrace: ~0p", + [Type, Class, Reason, Stk]), + {reply, {error, {Class, Reason}}, State} + end; + _ -> + {reply, {error, already_existed}, State} + end; + +handle_call({unload, Type}, _From, State = #state{loaded = Gateways}) -> + case maps:get(Type, Gateways, undefined) of + undefined -> + {reply, ok, State}; + _ -> + emqx_gateway_sup:stop_all_suptree(Type), + {reply, ok, State#state{loaded = maps:remove(Type, Gateways)}} + end; + +handle_call(all, _From, State = #state{loaded = Gateways}) -> + {reply, maps:to_list(Gateways), State}; + +handle_call({lookup, Type}, _From, State = #state{loaded = Gateways}) -> + Reply = maps:get(Type, Gateways, undefined), + {reply, Reply, State}; + +handle_call(Req, _From, State) -> + logger:error("Unexpected call: ~0p", [Req]), + {reply, ok, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl new file mode 100644 index 000000000..fe6f605da --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -0,0 +1,175 @@ +-module(emqx_gateway_schema). + +-dialyzer(no_return). +-dialyzer(no_match). +-dialyzer(no_contracts). +-dialyzer(no_unused). +-dialyzer(no_fail_call). + +-include_lib("typerefl/include/types.hrl"). + +-type duration() :: integer(). +-type bytesize() :: integer(). +-type comma_separated_list() :: list(). +-type ip_port() :: tuple(). + +-typerefl_from_string({duration/0, emqx_schema, to_duration}). +-typerefl_from_string({bytesize/0, emqx_schema, to_bytesize}). +-typerefl_from_string({comma_separated_list/0, emqx_schema, to_comma_separated_list}). +-typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}). + +-behaviour(hocon_schema). + +-reflect_type([ duration/0 + , bytesize/0 + , comma_separated_list/0 + , ip_port/0 + ]). + +-export([structs/0 , fields/1]). +-export([t/1, t/3, t/4, ref/1]). + +structs() -> ["emqx_gateway"]. + +fields("emqx_gateway") -> + [{stomp, t(ref(stomp))}]; + +fields(stomp) -> + [{"$id", t(ref(stomp_structs))}]; + +fields(stomp_structs) -> + [ {frame, t(ref(stomp_frame))} + , {clientinfo_override, t(ref(clientinfo_override))} + , {authenticator, t(union([allow_anonymous]))} + , {listener, t(ref(listener))} + ]; + +fields(stomp_frame) -> + [ {max_headers, t(integer(), undefined, 10)} + , {max_headers_length, t(integer(), undefined, 1024)} + , {max_body_length, t(integer(), undefined, 8192)} + ]; + +fields(clientinfo_override) -> + [ {username, t(string())} + , {password, t(string())} + , {clientid, t(string())} + ]; + +fields(listener) -> + [ {tcp, t(ref(tcp_listener))} + , {ssl, t(ref(ssl_listener))} + ]; + +fields(tcp_listener) -> + [ {"$name", t(ref(tcp_listener_settings))}]; + +fields(ssl_listener) -> + [ {"$name", t(ref(ssl_listener_settings))}]; + +fields(listener_settings) -> + %[ {"bind", t(union(ip_port(), integer()))} + [ {bind, t(integer())} + , {acceptors, t(integer(), undefined, 8)} + , {max_connections, t(integer(), undefined, 1024)} + , {max_conn_rate, t(integer())} + , {active_n, t(integer(), undefined, 100)} + %, {zone, t(string())} + %, {rate_limit, t(comma_separated_list())} + , {access, t(ref(access))} + , {proxy_protocol, t(boolean())} + , {proxy_protocol_timeout, t(duration())} + , {backlog, t(integer(), undefined, 1024)} + , {send_timeout, t(duration(), undefined, "15s")} + , {send_timeout_close, t(boolean(), undefined, true)} + , {recbuf, t(bytesize())} + , {sndbuf, t(bytesize())} + , {buffer, t(bytesize())} + , {high_watermark, t(bytesize(), undefined, "1MB")} + , {tune_buffer, t(boolean())} + , {nodelay, t(boolean())} + , {reuseaddr, t(boolean())} + ]; + +fields(tcp_listener_settings) -> + [ + %% some special confs for tcp listener + ] ++ fields(listener_settings); + +fields(ssl_listener_settings) -> + [ + %% some special confs for ssl listener + ] ++ + ssl(undefined, #{handshake_timeout => "15s" + , depth => 10 + , reuse_sessions => true}) ++ fields(listener_settings); + +fields(access) -> + [ {"$id", #{type => string(), + nullable => true}}]; + +fields(ExtraField) -> + Mod = list_to_atom(ExtraField++"_schema"), + Mod:fields(ExtraField). + +%translations() -> []. +% +%translations(_) -> []. + +%%-------------------------------------------------------------------- +%% Helpers + +%% types + +t(Type) -> #{type => Type}. + +t(Type, Mapping, Default) -> + hoconsc:t(Type, #{mapping => Mapping, default => Default}). + +t(Type, Mapping, Default, OverrideEnv) -> + hoconsc:t(Type, #{ mapping => Mapping + , default => Default + , override_env => OverrideEnv + }). + +ref(Field) -> + hoconsc:ref(?MODULE, Field). + +%% utils + +%% generate a ssl field. +%% ssl("emqx", #{"verify" => verify_peer}) will return +%% [ {"cacertfile", t(string(), "emqx.cacertfile", undefined)} +%% , {"certfile", t(string(), "emqx.certfile", undefined)} +%% , {"keyfile", t(string(), "emqx.keyfile", undefined)} +%% , {"verify", t(union(verify_peer, verify_none), "emqx.verify", verify_peer)} +%% , {"server_name_indication", "emqx.server_name_indication", undefined)} +%% ... +ssl(Mapping, Defaults) -> + M = fun (Field) -> + case (Mapping) of + undefined -> undefined; + _ -> Mapping ++ "." ++ Field + end end, + D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, + [ {"enable", t(boolean(), M("enable"), D("enable"))} + , {"cacertfile", t(string(), M("cacertfile"), D("cacertfile"))} + , {"certfile", t(string(), M("certfile"), D("certfile"))} + , {"keyfile", t(string(), M("keyfile"), D("keyfile"))} + , {"verify", t(union(verify_peer, verify_none), M("verify"), D("verify"))} + , {"fail_if_no_peer_cert", t(boolean(), M("fail_if_no_peer_cert"), D("fail_if_no_peer_cert"))} + , {"secure_renegotiate", t(boolean(), M("secure_renegotiate"), D("secure_renegotiate"))} + , {"reuse_sessions", t(boolean(), M("reuse_sessions"), D("reuse_sessions"))} + , {"honor_cipher_order", t(boolean(), M("honor_cipher_order"), D("honor_cipher_order"))} + , {"handshake_timeout", t(duration(), M("handshake_timeout"), D("handshake_timeout"))} + , {"depth", t(integer(), M("depth"), D("depth"))} + , {"password", hoconsc:t(string(), #{mapping => M("key_password"), + default => D("key_password"), + sensitive => true + })} + , {"dhfile", t(string(), M("dhfile"), D("dhfile"))} + , {"server_name_indication", t(union(disable, string()), M("server_name_indication"), + D("server_name_indication"))} + , {"tls_versions", t(comma_separated_list(), M("tls_versions"), D("tls_versions"))} + , {"ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))} + , {"psk_ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))}]. diff --git a/apps/emqx_gateway/src/emqx_gateway_sup.erl b/apps/emqx_gateway/src/emqx_gateway_sup.erl new file mode 100644 index 000000000..d56b27e52 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_sup.erl @@ -0,0 +1,194 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_sup). + +-behaviour(supervisor). + +-include("include/emqx_gateway.hrl"). + +-export([start_link/0]). + +%% Gateway Instance APIs +-export([ create_gateway_insta/1 + , remove_gateway_insta/1 + , lookup_gateway_insta/1 + , update_gateway_insta/1 + , start_gateway_insta/1 + , stop_gateway_insta/1 + , list_gateway_insta/1 + , list_gateway_insta/0 + ]). + +%% Gateway APs +-export([ list_started_gateway/0 + , stop_all_suptree/1 + ]). + +%% supervisor callbacks +-export([init/1]). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +-spec create_gateway_insta(instance()) -> {ok, pid()} | {error, any()}. +create_gateway_insta(Insta = #{type := Type}) -> + case emqx_gateway_registry:lookup(Type) of + undefined -> {error, {unknown_gateway_id, Type}}; + GwDscrptr -> + {ok, GwSup} = ensure_gateway_suptree_ready(gatewayid(Type)), + emqx_gateway_gw_sup:create_insta(GwSup, Insta, GwDscrptr) + end. + +-spec remove_gateway_insta(instance_id()) -> ok | {error, any()}. +remove_gateway_insta(InstaId) -> + case search_gateway_insta_proc(InstaId) of + {ok, {GwSup, _}} -> + emqx_gateway_gw_sup:remove_insta(GwSup, InstaId); + _ -> + ok + end. + +-spec lookup_gateway_insta(instance_id()) -> instance() | undefined. +lookup_gateway_insta(InstaId) -> + case search_gateway_insta_proc(InstaId) of + {ok, {_, GwInstaPid}} -> + emqx_gateway_insta_sup:info(GwInstaPid); + _ -> + undefined + end. + +-spec update_gateway_insta(instance()) + -> ok + | {error, any()}. +update_gateway_insta(NewInsta = #{type := Type}) -> + case emqx_gateway_utils:find_sup_child(?MODULE, gatewayid(Type)) of + {ok, GwSup} -> + emqx_gateway_gw_sup:update_insta(GwSup, NewInsta); + _ -> {error, not_found} + end. + +start_gateway_insta(InstaId) -> + case search_gateway_insta_proc(InstaId) of + {ok, {GwSup, _}} -> + emqx_gateway_gw_sup:start_insta(GwSup, InstaId); + _ -> {error, not_found} + end. + +-spec stop_gateway_insta(instance_id()) -> ok | {error, any()}. +stop_gateway_insta(InstaId) -> + case search_gateway_insta_proc(InstaId) of + {ok, {GwSup, _}} -> + emqx_gateway_gw_sup:stop_insta(GwSup, InstaId); + _ -> {error, not_found} + end. + +-spec list_gateway_insta(gateway_type()) -> {ok, [instance()]} | {error, any()}. +list_gateway_insta(Type) -> + case emqx_gateway_utils:find_sup_child(?MODULE, gatewayid(Type)) of + {ok, GwSup} -> + {ok, emqx_gateway_gw_sup:list_insta(GwSup)}; + _ -> {error, not_found} + end. + +-spec list_gateway_insta() -> [{gateway_type(), instance()}]. +list_gateway_insta() -> + lists:map( + fun(SupId) -> + Instas = emqx_gateway_gw_sup:list_insta(SupId), + {SupId, Instas} + end, list_started_gateway()). + +-spec list_started_gateway() -> [gateway_type()]. +list_started_gateway() -> + started_gateway_type(). + +-spec stop_all_suptree(atom()) -> ok. +stop_all_suptree(Type) -> + case lists:keyfind(Type, 1, supervisor:which_children(?MODULE)) of + false -> ok; + _ -> + _ = supervisor:terminate_child(?MODULE, Type), + _ = supervisor:delete_child(?MODULE, Type), + ok + end. + +%% Supervisor callback + +init([]) -> + SupFlags = #{ strategy => one_for_one + , intensity => 10 + , period => 60 + }, + ChildSpecs = [ emqx_gateway_utils:childspec(worker, emqx_gateway_registry) + ], + {ok, {SupFlags, ChildSpecs}}. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +gatewayid(Type) -> + list_to_atom(lists:concat([Type])). + +ensure_gateway_suptree_ready(Type) -> + case lists:keyfind(Type, 1, supervisor:which_children(?MODULE)) of + false -> + ChildSpec = emqx_gateway_utils:childspec( + Type, + supervisor, + emqx_gateway_gw_sup, + [Type] + ), + emqx_gateway_utils:supervisor_ret( + supervisor:start_child(?MODULE, ChildSpec) + ); + {_Id, Pid, _Type, _Mods} -> + {ok, Pid} + end. + +search_gateway_insta_proc(InstaId) -> + search_gateway_insta_proc(InstaId, started_gateway_pid()). + +search_gateway_insta_proc(_InstaId, []) -> + {error, not_found}; +search_gateway_insta_proc(InstaId, [SupPid|More]) -> + case emqx_gateway_utils:find_sup_child(SupPid, InstaId) of + {ok, InstaPid} -> {ok, {SupPid, InstaPid}}; + _ -> + search_gateway_insta_proc(InstaId, More) + end. + +started_gateway_type() -> + lists:filtermap( + fun({Id, _, _, _}) -> + is_a_gateway_id(Id) andalso {true, Id} + end, supervisor:which_children(?MODULE)). + +started_gateway_pid() -> + lists:filtermap( + fun({Id, Pid, _, _}) -> + is_a_gateway_id(Id) andalso {true, Pid} + end, supervisor:which_children(?MODULE)). + +is_a_gateway_id(Id) -> + Id /= emqx_gateway_registry. + + diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl new file mode 100644 index 000000000..184c3ff87 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -0,0 +1,163 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 Utils funcs for emqx-gateway +-module(emqx_gateway_utils). + +-export([ childspec/2 + , childspec/3 + , childspec/4 + , supervisor_ret/1 + , find_sup_child/2 + ]). + +-export([ apply/2 + ]). + +-export([ normalize_rawconf/1 + ]). + +%% Common Envs +-export([ active_n/1 + , ratelimit/1 + , frame_options/1 + , init_gc_state/1 + , stats_timer/1 + , idle_timeout/1 + , oom_policy/1 + ]). + +-define(ACTIVE_N, 100). +-define(DEFAULT_IDLE_TIMEOUT, 30000). + +-spec childspec(supervisor:worker(), Mod :: atom()) + -> supervisor:child_spec(). +childspec(Type, Mod) -> + childspec(Mod, Type, Mod, []). + +-spec childspec(supervisor:worker(), Mod :: atom(), Args :: list()) + -> supervisor:child_spec(). +childspec(Type, Mod, Args) -> + childspec(Mod, Type, Mod, Args). + +-spec childspec(atom(), supervisor:worker(), Mod :: atom(), Args :: list()) + -> supervisor:child_spec(). +childspec(Id, Type, Mod, Args) -> + #{ id => Id + , start => {Mod, start_link, Args} + , type => Type + }. + +-spec supervisor_ret(supervisor:startchild_ret()) + -> {ok, pid()} + | {error, supervisor:startchild_err()}. +supervisor_ret({ok, Pid, _Info}) -> {ok, Pid}; +supervisor_ret(Ret) -> Ret. + +-spec find_sup_child(Sup :: pid() | atom(), ChildId :: supervisor:child_id()) + -> false + | {ok, pid()}. +find_sup_child(Sup, ChildId) -> + case lists:keyfind(ChildId, 1, supervisor:which_children(Sup)) of + false -> false; + {_Id, Pid, _Type, _Mods} -> {ok, Pid} + end. + +apply({M, F, A}, A2) when is_atom(M), + is_atom(M), + is_list(A), + is_list(A2) -> + erlang:apply(M, F, A ++ A2); +apply({F, A}, A2) when is_function(F), + is_list(A), + is_list(A2) -> + erlang:apply(F, A ++ A2); +apply(F, A2) when is_function(F), + is_list(A2) -> + erlang:apply(F, A2). + +-type listener() :: #{}. + +-type rawconf() :: + #{ clientinfo_override => #{} + , authenticators := #{} + , listeners => listener() + , atom() => any() + }. + +-spec normalize_rawconf(rawconf()) + -> list({ Type :: udp | tcp | ssl | dtls + , ListenOn :: esockd:listen_on() + , SocketOpts :: esockd:option() + , Cfg :: map() + }). +normalize_rawconf(RawConf = #{listener := LisMap}) -> + Cfg0 = maps:without([listener], RawConf), + lists:append(maps:fold(fun(Type, Liss, AccIn1) -> + Listeners = + maps:fold(fun(_Name, Confs, AccIn2) -> + ListenOn = maps:get(bind, Confs), + SocketOpts = esockd:parse_opt(maps:to_list(Confs)), + RemainCfgs = maps:without( + [bind] ++ proplists:get_keys(SocketOpts), + Confs), + Cfg = maps:merge(Cfg0, RemainCfgs), + [{Type, ListenOn, SocketOpts, Cfg}|AccIn2] + end, [], Liss), + [Listeners|AccIn1] + end, [], LisMap)). + +%%-------------------------------------------------------------------- +%% Envs + +active_n(Options) -> + maps:get( + active_n, + maps:get(listener, Options, #{active_n => ?ACTIVE_N}), + ?ACTIVE_N + ). + +-spec idle_timeout(map()) -> pos_integer(). +idle_timeout(Options) -> + maps:get(idle_timeout, Options, ?DEFAULT_IDLE_TIMEOUT). + +-spec ratelimit(map()) -> esockd_rate_limit:config() | undefined. +ratelimit(Options) -> + maps:get(ratelimit, Options, undefined). + +-spec frame_options(map()) -> map(). +frame_options(Options) -> + maps:get(frame, Options, #{}). + +-spec init_gc_state(map()) -> emqx_gc:gc_state() | undefined. +init_gc_state(Options) -> + emqx_misc:maybe_apply(fun emqx_gc:init/1, force_gc_policy(Options)). + +-spec force_gc_policy(map()) -> emqx_gc:opts() | undefined. +force_gc_policy(Options) -> + maps:get(force_gc_policy, Options, undefined). + +-spec oom_policy(map()) -> emqx_types:oom_policy(). +oom_policy(Options) -> + maps:get(force_shutdown_policy, Options). + +-spec stats_timer(map()) -> undefined | disabled. +stats_timer(Options) -> + case enable_stats(Options) of true -> undefined; false -> disabled end. + +-spec enable_stats(map()) -> boolean(). +enable_stats(Options) -> + maps:get(enable_stats, Options, true). diff --git a/apps/emqx_stomp/README.md b/apps/emqx_gateway/src/stomp/README.md similarity index 90% rename from apps/emqx_stomp/README.md rename to apps/emqx_gateway/src/stomp/README.md index ec841b1e6..c5736a755 100644 --- a/apps/emqx_stomp/README.md +++ b/apps/emqx_gateway/src/stomp/README.md @@ -1,13 +1,12 @@ -emqx-stomp -========== +# emqx-stomp + The plugin adds STOMP 1.0/1.1/1.2 protocol supports to the EMQ X broker. The STOMP clients could PubSub to the MQTT clients. -Configuration -------------- +## Configuration etc/emqx_stomp.conf @@ -58,20 +57,17 @@ stomp.frame.max_header_length = 1024 stomp.frame.max_body_length = 8192 ``` -Load the Plugin ---------------- +## Load the Plugin ``` ./bin/emqx_ctl plugins load emqx_stomp ``` -License -------- +## License Apache License Version 2.0 -Author ------- +## Author EMQ X Team. diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl new file mode 100644 index 000000000..322baa120 --- /dev/null +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -0,0 +1,978 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_stomp_channel). + +-include("src/stomp/include/emqx_stomp.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-logger_header("[Stomp-Proto]"). + +-import(proplists, [get_value/2, get_value/3]). + +%% API +-export([ info/1 + , info/2 + , stats/1 + ]). + +-export([ init/2 + , handle_in/2 + , handle_out/3 + , handle_deliver/2 + , handle_timeout/3 + , terminate/2 + , set_conn_state/2 + ]). + +-export([ handle_call/2 + , handle_info/2 + ]). + +%% for trans callback +-export([ handle_recv_send_frame/2 + , handle_recv_ack_frame/2 + , handle_recv_nack_frame/2 + ]). + +-record(channel, { + %% Context + ctx :: emqx_gateway_ctx:context(), + %% Stomp Connection Info + conninfo :: emqx_types:conninfo(), + %% Stomp Client Info + clientinfo :: emqx_types:clientinfo(), + %% ClientInfo override specs + clientinfo_override :: map(), + %% Connection Channel + conn_state :: conn_state(), + %% Heartbeat + heartbeat :: emqx_stomp_heartbeat:heartbeat(), + %% Subscriptions + subscriptions = [], + %% Timer + timers :: #{atom() => disable | undefined | reference()}, + %% Transaction + transaction :: #{binary() => list()} + }). + +-type(channel() :: #channel{}). + +-type(conn_state() :: idle | connecting | connected | disconnected). + +-type(reply() :: {outgoing, stomp_frame()} + | {outgoing, [stomp_frame()]} + | {event, conn_state()|updated} + | {close, Reason :: atom()}). + +-type(replies() :: emqx_stomp_frame:packet() | reply() | [reply()]). + +-define(TIMER_TABLE, #{ + incoming_timer => incoming, + outgoing_timer => outgoing, + clean_trans_timer => clean_trans + }). + +-define(TRANS_TIMEOUT, 60000). + +-define(DEFAULT_OVERRIDE, + #{ clientid => <<"">> %% Generate clientid by default + , username => <<"${Packet.headers.login}">> + , password => <<"${Packet.headers.passcode}">> + }). + +-define(INFO_KEYS, [conninfo, conn_state, clientinfo, session, will_msg]). + +-dialyzer({nowarn_function, [init/2,enrich_conninfo/2,ensure_connected/1, + process_connect/1,handle_in/2,handle_info/2, + ensure_disconnected/2,reverse_heartbeats/1, + negotiate_version/2]}). + +%%-------------------------------------------------------------------- +%% Init the channel +%%-------------------------------------------------------------------- + +%% @doc Init protocol +init(ConnInfo = #{peername := {PeerHost, _}, + sockname := {_, SockPort}}, Option) -> + Peercert = maps:get(peercert, ConnInfo, undefined), + Mountpoint = maps:get(mountpoint, Option, undefined), + ClientInfo = setting_peercert_infos( + Peercert, + #{ zone => undefined + , protocol => stomp + , peerhost => PeerHost + , sockport => SockPort + , clientid => undefined + , username => undefined + , is_bridge => false + , is_superuser => false + , mountpoint => Mountpoint + } + ), + + Ctx = maps:get(ctx, Option), + Override = maps:merge(?DEFAULT_OVERRIDE, + maps:get(clientinfo_override, Option, #{}) + ), + #channel{ ctx = Ctx + , conninfo = ConnInfo + , clientinfo = ClientInfo + , clientinfo_override = Override + , timers = #{} + , transaction = #{} + }. + +setting_peercert_infos(NoSSL, ClientInfo) + when NoSSL =:= nossl; + NoSSL =:= undefined -> + ClientInfo; +setting_peercert_infos(Peercert, ClientInfo) -> + {DN, CN} = {esockd_peercert:subject(Peercert), + esockd_peercert:common_name(Peercert)}, + ClientInfo#{dn => DN, cn => CN}. + +-spec info(channel()) -> emqx_types:infos(). +info(Channel) -> + maps:from_list(info(?INFO_KEYS, Channel)). + +-spec(info(list(atom())|atom(), channel()) -> term()). +info(Keys, Channel) when is_list(Keys) -> + [{Key, info(Key, Channel)} || Key <- Keys]; + +info(conninfo, #channel{conninfo = ConnInfo}) -> + ConnInfo; +info(conn_state, #channel{conn_state = ConnState}) -> + ConnState; +info(clientinfo, #channel{clientinfo = ClientInfo}) -> + ClientInfo; +info(session, _) -> + #{}; +info(will_msg, _) -> + undefined; +info(clientid, #channel{clientinfo = #{clientid := ClientId}}) -> + ClientId; +info(ctx, #channel{ctx = Ctx}) -> + Ctx. + +stats(_Channel) -> + []. + +set_conn_state(ConnState, Channel) -> + Channel#channel{conn_state = ConnState}. + +enrich_conninfo(_Packet, + Channel = #channel{conninfo = ConnInfo}) -> + %% XXX: How enrich more infos? + NConnInfo = ConnInfo#{ proto_name => <<"STOMP">> + , proto_ver => undefined + , clean_start => true + , keepalive => 0 + , expiry_interval => 0 + }, + {ok, Channel#channel{conninfo = NConnInfo}}. + +run_conn_hooks(Packet, Channel = #channel{ctx = Ctx, + conninfo = ConnInfo}) -> + %% XXX: Assign headers of Packet to ConnProps + ConnProps = #{}, + case run_hooks(Ctx, 'client.connect', [ConnInfo], ConnProps) of + Error = {error, _Reason} -> Error; + _NConnProps -> + {ok, Packet, Channel} + end. + +negotiate_version(#stomp_frame{headers = Headers}, + Channel = #channel{conninfo = ConnInfo}) -> + %% XXX: + case do_negotiate_version(header(<<"accept-version">>, Headers)) of + {ok, Version} -> + {ok, Channel#channel{conninfo = ConnInfo#{proto_ver => Version}}}; + {error, Reason}-> + {error, Reason} + end. + +enrich_clientinfo(Packet, + Channel = #channel{ + conninfo = ConnInfo, + clientinfo = ClientInfo0, + clientinfo_override = Override}) -> + ClientInfo = write_clientinfo( + feedvar(Override, Packet, ConnInfo, ClientInfo0), + ClientInfo0 + ), + {ok, NPacket, NClientInfo} = emqx_misc:pipeline( + [ fun maybe_assign_clientid/2 + , fun parse_heartbeat/2 + %% FIXME: CALL After authentication successfully + , fun fix_mountpoint/2 + ], Packet, ClientInfo + ), + {ok, NPacket, Channel#channel{clientinfo = NClientInfo}}. + +feedvar(Override, Packet, ConnInfo, ClientInfo) -> + Envs = #{ 'ConnInfo' => ConnInfo + , 'ClientInfo' => ClientInfo + , 'Packet' => connect_packet_to_map(Packet) + }, + maps:map(fun(_K, V) -> + Tokens = emqx_rule_utils:preproc_tmpl(V), + emqx_rule_utils:proc_tmpl(Tokens, Envs) + end, Override). + +connect_packet_to_map(#stomp_frame{headers = Headers}) -> + #{headers => maps:from_list(Headers)}. + +write_clientinfo(Override, ClientInfo) -> + Override1 = maps:with([username, password, clientid], Override), + maps:merge(ClientInfo, Override1). + +maybe_assign_clientid(_Packet, ClientInfo = #{clientid := ClientId}) + when ClientId == undefined; + ClientId == <<>> -> + {ok, ClientInfo#{clientid => emqx_guid:to_base62(emqx_guid:gen())}}; + +maybe_assign_clientid(_Packet, ClientInfo) -> + {ok, ClientInfo}. + +parse_heartbeat(#stomp_frame{headers = Headers}, ClientInfo) -> + Heartbeat0 = header(<<"heart-beat">>, Headers, <<"0,0">>), + CxCy = re:split(Heartbeat0, <<",">>, [{return, list}]), + Heartbeat = list_to_tuple([list_to_integer(S) || S <- CxCy]), + {ok, ClientInfo#{heartbeat => Heartbeat}}. + +fix_mountpoint(_Packet, #{mountpoint := undefined}) -> ok; +fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) -> + %% TODO: Enrich the varibale replacement???? + %% i.e: ${ClientInfo.auth_result.productKey} + Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo), + {ok, ClientInfo#{mountpoint := Mountpoint1}}. + +set_log_meta(_Packet, #channel{clientinfo = #{clientid := ClientId}}) -> + emqx_logger:set_metadata_clientid(ClientId), + ok. + +auth_connect(_Packet, Channel = #channel{ctx = Ctx, + clientinfo = ClientInfo}) -> + #{clientid := ClientId, + username := Username} = ClientInfo, + case emqx_gateway_ctx:authenticate(Ctx, ClientInfo) of + {ok, NClientInfo} -> + {ok, Channel#channel{clientinfo = NClientInfo}}; + {error, Reason} -> + ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", + [ClientId, Username, Reason]), + {error, Reason} + end. + +ensure_connected(Channel = #channel{ + ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo}) -> + NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)}, + ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]), + Channel#channel{conninfo = NConnInfo, + conn_state = connected + }. + +process_connect(Channel = #channel{ + ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo + }) -> + SessFun = fun(_,_) -> #{} end, + case emqx_gateway_ctx:open_session( + Ctx, + true, + ClientInfo, + ConnInfo, + SessFun + ) of + {ok, _Sess} -> %% The stomp protocol doesn't have session + #{proto_ver := Version} = ConnInfo, + #{heartbeat := Heartbeat} = ClientInfo, + Headers = [{<<"version">>, Version}, + {<<"heart-beat">>, reverse_heartbeats(Heartbeat)}], + handle_out(connected, Headers, Channel); + {error, Reason} -> + ?LOG(error, "Failed to open session du to ~p", [Reason]), + Headers = [{<<"version">>, <<"1.0,1.1,1.2">>}, + {<<"content-type">>, <<"text/plain">>}], + handle_out(connerr, {Headers, undefined, <<"Not Authenticated">>}, Channel) + end. + +%%-------------------------------------------------------------------- +%% Handle incoming packet +%%-------------------------------------------------------------------- + +-spec handle_in(emqx_types:packet(), channel()) + -> {ok, channel()} + | {ok, replies(), channel()} + | {shutdown, Reason :: term(), channel()} + | {shutdown, Reason :: term(), replies(), channel()}. + +handle_in(Frame = ?PACKET(?CMD_STOMP), Channel) -> + handle_in(Frame#stomp_frame{command = <<"CONNECT">>}, Channel); + +handle_in(?PACKET(?CMD_CONNECT), + Channel = #channel{conn_state = connected}) -> + {error, unexpected_connect, Channel}; + +handle_in(Packet = ?PACKET(?CMD_CONNECT), Channel) -> + case emqx_misc:pipeline( + [ fun enrich_conninfo/2 + , fun run_conn_hooks/2 + , fun negotiate_version/2 + , fun enrich_clientinfo/2 + , fun set_log_meta/2 + %% TODO: How to implement the banned in the gateway instance? + %, fun check_banned/2 + , fun auth_connect/2 + ], Packet, Channel#channel{conn_state = connecting}) of + {ok, _NPacket, NChannel} -> + process_connect(ensure_connected(NChannel)); + {error, ReasonCode, NChannel} -> + ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), + handle_out(connerr, {[], undefined, ErrMsg}, NChannel) + end; + +handle_in(Frame = ?PACKET(?CMD_SEND, Headers), + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo + }) -> + Topic = header(<<"destination">>, Headers), + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic) of + deny -> + handle_out(error, {receipt_id(Headers), "ACL Deny"}, Channel); + allow -> + case header(<<"transaction">>, Headers) of + undefined -> + handle_recv_send_frame(Frame, Channel); + TxId -> + add_action(TxId, {fun ?MODULE:handle_recv_send_frame/2, [Frame]}, receipt_id(Headers), Channel) + end + end; + +handle_in(?PACKET(?CMD_SUBSCRIBE, Headers), + Channel = #channel{ + ctx = Ctx, + subscriptions = Subs, + clientinfo = ClientInfo = #{mountpoint := Mountpoint} + }) -> + SubId = header(<<"id">>, Headers), + Topic = header(<<"destination">>, Headers), + Ack = header(<<"ack">>, Headers, <<"auto">>), + + MountedTopic = emqx_mountpoint:mount(Mountpoint, Topic), + + case lists:keyfind(SubId, 1, Subs) of + {SubId, MountedTopic, Ack} -> + maybe_outgoing_receipt(receipt_id(Headers), Channel); + {SubId, _OtherTopic, _OtherAck} -> + %% FIXME: + ?LOG(error, "Conflicts with subscribed topics ~s, id: ~s", + [_OtherTopic, SubId]), + ErrMsg = "Conflict subscribe id ", + handle_out(error, {receipt_id(Headers), ErrMsg}, Channel); + false -> + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, Topic) of + deny -> + handle_out(error, {receipt_id(Headers), "ACL Deny"}, Channel); + allow -> + _ = emqx_broker:subscribe(MountedTopic), + maybe_outgoing_receipt( + receipt_id(Headers), + Channel#channel{subscriptions = [{SubId, MountedTopic, Ack} | Subs]} + ) + end + end; + +handle_in(?PACKET(?CMD_UNSUBSCRIBE, Headers), + Channel = #channel{subscriptions = Subs}) -> + SubId = header(<<"id">>, Headers), + {ok, NChannel} = case lists:keyfind(SubId, 1, Subs) of + {SubId, Topic, _Ack} -> + ok = emqx_broker:unsubscribe(Topic), + {ok, Channel#channel{subscriptions = lists:keydelete(SubId, 1, Subs)}}; + false -> + {ok, Channel} + end, + handle_out(receipt, receipt_id(Headers), NChannel); + +%% XXX: How to ack a frame ??? +handle_in(Frame = ?PACKET(?CMD_ACK, Headers), Channel) -> + case header(<<"transaction">>, Headers) of + undefined -> handle_recv_ack_frame(Frame, Channel); + TxId -> add_action(TxId, {fun ?MODULE:handle_recv_ack_frame/2, [Frame]}, receipt_id(Headers), Channel) + end; + +%% NACK +%% id:12345 +%% transaction:tx1 +%% +%% ^@ +handle_in(Frame = ?PACKET(?CMD_NACK, Headers), Channel) -> + case header(<<"transaction">>, Headers) of + undefined -> handle_recv_nack_frame(Frame, Channel); + TxId -> add_action(TxId, {fun ?MODULE:handle_recv_nack_frame/2, [Frame]}, receipt_id(Headers), Channel) + end; + +%% The transaction header is REQUIRED, and the transaction identifier +%% will be used for SEND, COMMIT, ABORT, ACK, and NACK frames to bind +%% them to the named transaction. +%% +%% BEGIN +%% transaction:tx1 +%% +%% ^@ +handle_in(?PACKET(?CMD_BEGIN, Headers), + Channel = #channel{transaction = Trans}) -> + TxId = header(<<"transaction">>, Headers), + case maps:get(TxId, Trans, undefined) of + undefined -> + StartedAt = erlang:system_time(millisecond), + NChannel = ensure_clean_trans_timer( + Channel#channel{ + transaction = Trans#{TxId => {StartedAt, []}}} + ), + handle_out(receipt, receipt_id(Headers), NChannel); + _ -> + ErrMsg = ["Transaction ", TxId, " already started"], + handle_out(error, {receipt_id(Headers), ErrMsg}, Channel) + end; + +%% COMMIT +%% transaction:tx1 +%% +%% ^@ +handle_in(?PACKET(?CMD_COMMIT, Headers), Channel) -> + with_transaction(Headers, Channel, fun(TxId, Actions) -> + Chann0 = remove_trans(TxId, Channel), + case trans_pipeline(lists:reverse(Actions), [], Chann0) of + {ok, Outgoings, Chann1} -> + maybe_outgoing_receipt(receipt_id(Headers), Outgoings, Chann1); + {error, Reason} -> + %% FIXME: atomic for transaction ?? + ErrMsg = io_lib:format("Execute transaction ~s falied: ~0p", + [TxId, Reason] + ), + handle_out(error, {receipt_id(Headers), ErrMsg}, Chann0) + end + end); + +%% ABORT +%% transaction:tx1 +%% +%% ^@ +handle_in(?PACKET(?CMD_ABORT, Headers), + Channel = #channel{transaction = Trans}) -> + with_transaction(Headers, Channel, fun(Id, _Actions) -> + NChannel = Channel#channel{transaction = maps:remove(Id, Trans)}, + handle_out(receipt, receipt_id(Headers), NChannel) + end); + +handle_in(?PACKET(?CMD_DISCONNECT, Headers), Channel) -> + shutdown_with_recepit(normal, receipt_id(Headers), Channel); + +handle_in({frame_error, Reason}, Channel = #channel{conn_state = _ConnState}) -> + ?LOG(error, "Unexpected frame error: ~p", [Reason]), + shutdown(Reason, Channel). + +with_transaction(Headers, Channel = #channel{transaction = Trans}, Fun) -> + Id = header(<<"transaction">>, Headers), + ReceiptId = receipt_id(Headers), + case maps:get(Id, Trans, undefined) of + {_, Actions} -> + Fun(Id, Actions); + _ -> + ErrMsg = ["Transaction ", Id, " not found"], + handle_out(error, {ReceiptId, ErrMsg}, Channel) + end. + +remove_trans(Id, Channel = #channel{transaction = Trans}) -> + Channel#channel{transaction = maps:remove(Id, Trans)}. + +trans_pipeline([], Outgoings, Channel) -> + {ok, Outgoings, Channel}; + +trans_pipeline([{Func, Args}|More], Outgoings, Channel) -> + case erlang:apply(Func, Args ++ [Channel]) of + {ok, NChannel} -> + trans_pipeline(More, Outgoings, NChannel); + {ok, Outgoings1, NChannel} -> + trans_pipeline(More, Outgoings ++ Outgoings1, NChannel); + {error, Reason} -> + {error, Reason, Channel} + end. + +%%-------------------------------------------------------------------- +%% Handle outgoing packet +%%-------------------------------------------------------------------- + +-spec(handle_out(atom(), term(), channel()) + -> {ok, channel()} + | {ok, replies(), channel()} + | {shutdown, Reason :: term(), channel()} + | {shutdown, Reason :: term(), replies(), channel()}). + +handle_out(connerr, {Headers, ReceiptId, ErrMsg}, Channel) -> + Frame = error_frame(Headers, ReceiptId, ErrMsg), + shutdown(ErrMsg, Frame, Channel); + +handle_out(error, {ReceiptId, ErrMsg}, Channel) -> + Frame = error_frame(ReceiptId, ErrMsg), + {ok, Frame, Channel}; + +handle_out(connected, Headers, Channel = #channel{ + ctx = Ctx, + conninfo = ConnInfo + }) -> + %% XXX: connection_accepted is not defined by stomp protocol + _ = run_hooks(Ctx, 'client.connack', [ConnInfo, connection_accepted, []]), + Replies = [{outgoing, connected_frame(Headers)}, + {event, connected} + ], + {ok, Replies, ensure_heartbeart_timer(Channel)}; + +handle_out(receipt, undefined, Channel) -> + {ok, Channel}; +handle_out(receipt, ReceiptId, Channel) -> + Frame = receipt_frame(ReceiptId), + {ok, Frame, Channel}. + +%%-------------------------------------------------------------------- +%% Handle call +%%-------------------------------------------------------------------- + +-spec(handle_call(Req :: term(), channel()) + -> {reply, Reply :: term(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), emqx_types:packet(), channel()}). +handle_call(kick, Channel) -> + NChannel = ensure_disconnected(kicked, Channel), + shutdown_and_reply(kicked, ok, NChannel); + +handle_call(discard, Channel) -> + shutdown_and_reply(discarded, ok, Channel); + +%% XXX: No Session Takeover +%handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> +% reply(Session, Channel#channel{takeover = true}); +% +%handle_call({takeover, 'end'}, Channel = #channel{session = Session, +% pendings = Pendings}) -> +% ok = emqx_session:takeover(Session), +% %% TODO: Should not drain deliver here (side effect) +% Delivers = emqx_misc:drain_deliver(), +% AllPendings = lists:append(Delivers, Pendings), +% shutdown_and_reply(takeovered, AllPendings, Channel); + +handle_call(list_acl_cache, Channel) -> + {reply, emqx_acl_cache:list_acl_cache(), Channel}; + +%% XXX: No Quota Now +% handle_call({quota, Policy}, Channel) -> +% Zone = info(zone, Channel), +% Quota = emqx_limiter:init(Zone, Policy), +% reply(ok, Channel#channel{quota = Quota}); + +handle_call(Req, Channel) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + reply(ignored, Channel). + +%%-------------------------------------------------------------------- +%% Handle Info +%%-------------------------------------------------------------------- + +-spec(handle_info(Info :: term(), channel()) + -> ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}). + +%% XXX: Received from the emqx-management ??? +%handle_info({subscribe, TopicFilters}, Channel ) -> +% {_, NChannel} = lists:foldl( +% fun({TopicFilter, SubOpts}, {_, ChannelAcc}) -> +% do_subscribe(TopicFilter, SubOpts, ChannelAcc) +% end, {[], Channel}, parse_topic_filters(TopicFilters)), +% {ok, NChannel}; +% +%handle_info({unsubscribe, TopicFilters}, Channel) -> +% {_RC, NChannel} = process_unsubscribe(TopicFilters, #{}, Channel), +% {ok, NChannel}; + +handle_info({sock_closed, Reason}, + Channel = #channel{conn_state = idle}) -> + shutdown(Reason, Channel); + +handle_info({sock_closed, Reason}, + Channel = #channel{conn_state = connecting}) -> + shutdown(Reason, Channel); + +handle_info({sock_closed, Reason}, + Channel = #channel{conn_state = connected, + clientinfo = _ClientInfo}) -> + %% XXX: Flapping detect ??? + %% How to get the flapping detect policy ??? + %emqx_zone:enable_flapping_detect(Zone) + % andalso emqx_flapping:detect(ClientInfo), + NChannel = ensure_disconnected(Reason, Channel), + %% XXX: Session keepper detect here + shutdown(Reason, NChannel); + +handle_info({sock_closed, Reason}, + Channel = #channel{conn_state = disconnected}) -> + ?LOG(error, "Unexpected sock_closed: ~p", [Reason]), + {ok, Channel}; + +handle_info(clean_acl_cache, Channel) -> + ok = emqx_acl_cache:empty_acl_cache(), + {ok, Channel}; + +handle_info(Info, Channel) -> + ?LOG(error, "Unexpected info: ~p", [Info]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Ensure disconnected + +ensure_disconnected(Reason, Channel = #channel{ + ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo}) -> + NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)}, + ok = run_hooks(Ctx, 'client.disconnected', + [ClientInfo, Reason, NConnInfo]), + Channel#channel{conninfo = NConnInfo, conn_state = disconnected}. + +%%-------------------------------------------------------------------- +%% Handle Delivers from broker to client +%%-------------------------------------------------------------------- + +-spec(handle_deliver(list(emqx_types:deliver()), channel()) + -> {ok, channel()} + | {ok, replies(), channel()}). + +handle_deliver(Delivers, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo, + subscriptions = Subs + }) -> + + %% TODO: Re-deliver ??? + %% Shared-subscription support ??? + + Frames0 = lists:foldl(fun({_, _, Message}, Acc) -> + Topic0 = emqx_message:topic(Message), + case lists:keyfind(Topic0, 2, Subs) of + {Id, Topic, Ack} -> + %% XXX: refactor later + metrics_inc('messages.delivered', Channel), + NMessage = run_hooks_without_metrics( + Ctx, + 'message.delivered', + [ClientInfo], + Message + ), + Topic = emqx_message:topic(NMessage), + Headers = emqx_message:get_headers(NMessage), + Payload = emqx_message:payload(NMessage), + Headers0 = [{<<"subscription">>, Id}, + {<<"message-id">>, next_msgid()}, + {<<"destination">>, Topic}, + {<<"content-type">>, <<"text/plain">>}], + Headers1 = case Ack of + _ when Ack =:= <<"client">>; + Ack =:= <<"client-individual">> -> + Headers0 ++ [{<<"ack">>, next_ackid()}]; + _ -> + Headers0 + end, + Frame = #stomp_frame{command = <<"MESSAGE">>, + headers = Headers1 ++ maps:get(stomp_headers, Headers, []), + body = Payload + }, + [Frame|Acc]; + false -> + ?LOG(error, "Dropped message ~0p due to not found " + "subscription id for ~s", + [Message, emqx_message:topic(Message)]), + metrics_inc('delivery.dropped', Channel), + metrics_inc('delivery.dropped.no_subid', Channel), + Acc + end + end, [], Delivers), + {ok, [{outgoing, lists:reverse(Frames0)}], Channel}. + +%%-------------------------------------------------------------------- +%% Handle timeout +%%-------------------------------------------------------------------- + +-spec(handle_timeout(reference(), Msg :: term(), channel()) + -> {ok, channel()} + | {ok, replies(), channel()} + | {shutdown, Reason :: term(), channel()}). + +handle_timeout(_TRef, {incoming, NewVal}, + Channel = #channel{heartbeat = HrtBt}) -> + case emqx_stomp_heartbeat:check(incoming, NewVal, HrtBt) of + {error, timeout} -> + shutdown(heartbeat_timeout, Channel); + {ok, NHrtBt} -> + {ok, reset_timer(incoming_timer, + Channel#channel{heartbeat = NHrtBt} + )} + end; + +handle_timeout(_TRef, {outgoing, NewVal}, + Channel = #channel{heartbeat = HrtBt}) -> + case emqx_stomp_heartbeat:check(outgoing, NewVal, HrtBt) of + {error, timeout} -> + NHrtBt = emqx_stomp_heartbeat:reset(outgoing, NewVal, HrtBt), + NChannel = Channel#channel{heartbeat = NHrtBt}, + {ok, emqx_stomp_frame:make(heartbeat), + reset_timer(outgoing_timer, NChannel)}; + {ok, NHrtBt} -> + {ok, reset_timer(outgoing_timer, + Channel#channel{heartbeat = NHrtBt} + )} + end; + +handle_timeout(_TRef, clean_trans, Channel = #channel{transaction = Trans}) -> + Now = erlang:system_time(millisecond), + NTrans = maps:filter(fun(_, {StartedAt, _}) -> + StartedAt + ?TRANS_TIMEOUT < Now + end, Trans), + {ok, ensure_clean_trans_timer(Channel#channel{transaction = NTrans})}. + +%%-------------------------------------------------------------------- +%% Terminate +%%-------------------------------------------------------------------- + +terminate(_Reason, _Channel) -> + ok. + +reply(Reply, Channel) -> + {reply, Reply, Channel}. + +shutdown(Reason, Channel) -> + {shutdown, Reason, Channel}. + +shutdown_with_recepit(Reason, ReceiptId, Channel) -> + case ReceiptId of + undefined -> + {shutdown, Reason, Channel}; + _ -> + {shutdown, Reason, receipt_frame(ReceiptId), Channel} + end. + +shutdown(Reason, AckFrame, Channel) -> + {shutdown, Reason, AckFrame, Channel}. + +shutdown_and_reply(Reason, Reply, Channel) -> + {shutdown, Reason, Reply, Channel}. + +do_negotiate_version(undefined) -> + {ok, <<"1.0">>}; + +do_negotiate_version(Accepts) -> + do_negotiate_version( + ?STOMP_VER, + lists:reverse(lists:sort(binary:split(Accepts, <<",">>, [global]))) + ). + +do_negotiate_version(Ver, []) -> + {error, <<"Supported protocol versions < ", Ver/binary>>}; +do_negotiate_version(Ver, [AcceptVer|_]) when Ver >= AcceptVer -> + {ok, AcceptVer}; +do_negotiate_version(Ver, [_|T]) -> + do_negotiate_version(Ver, T). + +header(Name, Headers) -> + get_value(Name, Headers). +header(Name, Headers, Val) -> + get_value(Name, Headers, Val). + +connected_frame(Headers) -> + emqx_stomp_frame:make(<<"CONNECTED">>, Headers). + +receipt_frame(ReceiptId) -> + emqx_stomp_frame:make(<<"RECEIPT">>, [{<<"receipt-id">>, ReceiptId}]). + +error_frame(ReceiptId, Msg) -> + error_frame([{<<"content-type">>, <<"text/plain">>}], ReceiptId, Msg). + +error_frame(Headers, undefined, Msg) -> + emqx_stomp_frame:make(<<"ERROR">>, Headers, Msg); +error_frame(Headers, ReceiptId, Msg) -> + emqx_stomp_frame:make(<<"ERROR">>, [{<<"receipt-id">>, ReceiptId} | Headers], Msg). + +next_msgid() -> + MsgId = case get(msgid) of + undefined -> 1; + I -> I + end, + put(msgid, MsgId + 1), + MsgId. + +next_ackid() -> + AckId = case get(ackid) of + undefined -> 1; + I -> I + end, + put(ackid, AckId + 1), + AckId. + +frame2message(?PACKET(?CMD_SEND, Headers, Body), + #channel{ + conninfo = #{proto_ver := ProtoVer}, + clientinfo = #{ + protocol := Protocol, + clientid := ClientId, + username := Username, + peerhost := PeerHost, + mountpoint := Mountpoint + }}) -> + Topic = header(<<"destination">>, Headers), + Msg = emqx_message:make(ClientId, Topic, Body), + StompHeaders = lists:foldl( + fun(Key, Headers0) -> + proplists:delete(Key, Headers0) + end, Headers, + [<<"destination">>, + <<"content-length">>, + <<"content-type">>, + <<"transaction">>, + <<"receipt">> + ]), + %% Pass-through of custom headers on the sending side + NMsg = emqx_message:set_headers(#{proto_ver => ProtoVer, + protocol => Protocol, + username => Username, + peerhost => PeerHost, + stomp_headers => StompHeaders + }, Msg), + emqx_mountpoint:mount(Mountpoint, NMsg). + +receipt_id(Headers) -> + header(<<"receipt">>, Headers). + +%%-------------------------------------------------------------------- +%% Trans + +add_action(TxId, Action, ReceiptId, Channel = #channel{transaction = Trans}) -> + case maps:get(TxId, Trans, undefined) of + {_StartedAt, Actions} -> + NTrans = Trans#{TxId => {_StartedAt, [Action|Actions]}}, + {ok, Channel#channel{transaction = NTrans}}; + _ -> + {ok, error_frame(ReceiptId, ["Transaction ", TxId, " not found"]), Channel} + end. + +%%-------------------------------------------------------------------- +%% Transaction Handle + +handle_recv_send_frame(Frame = ?PACKET(?CMD_SEND, Headers), Channel) -> + Msg = frame2message(Frame, Channel), + _ = emqx_broker:publish(Msg), + maybe_outgoing_receipt(receipt_id(Headers), Channel). + +handle_recv_ack_frame(?PACKET(?CMD_ACK, Headers), Channel) -> + maybe_outgoing_receipt(receipt_id(Headers), Channel). + +handle_recv_nack_frame(?PACKET(?CMD_NACK, Headers), Channel) -> + maybe_outgoing_receipt(receipt_id(Headers), Channel). + +maybe_outgoing_receipt(undefined, Channel) -> + {ok, [], Channel}; +maybe_outgoing_receipt(ReceiptId, Channel) -> + {ok, [{outgoing, receipt_frame(ReceiptId)}], Channel}. + +maybe_outgoing_receipt(undefined, Outgoings, Channel) -> + {ok, Outgoings, Channel}; +maybe_outgoing_receipt(ReceiptId, Outgoings, Channel) -> + {ok, lists:reverse([receipt_frame(ReceiptId)|Outgoings]), Channel}. + +ensure_clean_trans_timer(Channel = #channel{transaction = Trans}) -> + case maps:size(Trans) of + 0 -> Channel; + _ -> ensure_timer(clean_trans_timer, Channel) + end. + +%%-------------------------------------------------------------------- +%% Heartbeat + +reverse_heartbeats({Cx, Cy}) -> + iolist_to_binary(io_lib:format("~w,~w", [Cy, Cx])). + +ensure_heartbeart_timer(Channel = #channel{clientinfo = ClientInfo}) -> + Heartbeat = maps:get(heartbeat, ClientInfo), + ensure_timer( + [incoming_timer, outgoing_timer], + Channel#channel{heartbeat = emqx_stomp_heartbeat:init(Heartbeat)}). + +%%-------------------------------------------------------------------- +%% Timer + +ensure_timer([Name], Channel) -> + ensure_timer(Name, Channel); +ensure_timer([Name | Rest], Channel) -> + ensure_timer(Rest, ensure_timer(Name, Channel)); + +ensure_timer(Name, Channel = #channel{timers = Timers}) -> + TRef = maps:get(Name, Timers, undefined), + Time = interval(Name, Channel), + case TRef == undefined andalso is_integer(Time) andalso Time > 0 of + true -> ensure_timer(Name, Time, Channel); + false -> Channel %% Timer disabled or exists + end. + +ensure_timer(Name, Time, Channel = #channel{timers = Timers}) -> + Msg = maps:get(Name, ?TIMER_TABLE), + TRef = emqx_misc:start_timer(Time, Msg), + Channel#channel{timers = Timers#{Name => TRef}}. + +reset_timer(Name, Channel) -> + ensure_timer(Name, clean_timer(Name, Channel)). + +clean_timer(Name, Channel = #channel{timers = Timers}) -> + Channel#channel{timers = maps:remove(Name, Timers)}. + +interval(incoming_timer, #channel{heartbeat = HrtBt}) -> + emqx_stomp_heartbeat:interval(incoming, HrtBt); +interval(outgoing_timer, #channel{heartbeat = HrtBt}) -> + emqx_stomp_heartbeat:interval(outgoing, HrtBt); +interval(clean_trans_timer, _) -> + ?TRANS_TIMEOUT. + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +run_hooks(Ctx, Name, Args) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name), + emqx_hooks:run(Name, Args). + +run_hooks(Ctx, Name, Args, Acc) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name), + emqx_hooks:run_fold(Name, Args, Acc). + +run_hooks_without_metrics(_Ctx, Name, Args, Acc) -> + emqx_hooks:run_fold(Name, Args, Acc). + +metrics_inc(Name, #channel{ctx = Ctx}) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_connection.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_connection.erl new file mode 100644 index 000000000..a4b87fcd4 --- /dev/null +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_connection.erl @@ -0,0 +1,908 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_stomp_connection). + +-include("src/stomp/include/emqx_stomp.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +-logger_header("[Stomp-Conn]"). + +%% API +-export([ start_link/3 + , stop/1 + ]). + +-export([ info/1 + , stats/1 + ]). + +-export([ async_set_keepalive/3 + , async_set_keepalive/4 + , async_set_socket_options/2 + ]). + +-export([ call/2 + , call/3 + , cast/2 + ]). + +%% Callback +-export([init/4]). + +%% Sys callbacks +-export([ system_continue/3 + , system_terminate/4 + , system_code_change/4 + , system_get_state/1 + ]). + +%% Internal callback +-export([wakeup_from_hib/2, recvloop/2, get_state/1]). + +%% Export for CT +-export([set_field/3]). + +-import(emqx_misc, + [ maybe_apply/2 + ]). + +-record(state, { + %% TCP/TLS Transport + transport :: esockd:transport(), + %% TCP/TLS Socket + socket :: esockd:socket(), + %% Peername of the connection + peername :: emqx_types:peername(), + %% Sockname of the connection + sockname :: emqx_types:peername(), + %% Sock State + sockstate :: emqx_types:sockstate(), + %% The {active, N} option + active_n :: pos_integer(), + %% Limiter + limiter :: emqx_limiter:limiter() | undefined, + %% Limit Timer + limit_timer :: reference() | undefined, + %% Parse State + parse_state :: emqx_stomp_frame:parse_state(), + %% Serialize options + serialize :: emqx_stomp_frame:serialize_opts(), + %% Channel State + channel :: emqx_stomp_channel:channel(), + %% GC State + gc_state :: emqx_gc:gc_state() | undefined, + %% Stats Timer + stats_timer :: disabled | reference(), + %% Idle Timeout + idle_timeout :: integer(), + %% Idle Timer + idle_timer :: reference() | undefined + }). + +-type(state() :: #state{}). + +-define(ACTIVE_N, 100). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). +-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). +-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). + +-define(ENABLED(X), (X =/= undefined)). + +%-define(ALARM_TCP_CONGEST(Channel), +% list_to_binary(io_lib:format("mqtt_conn/congested/~s/~s", +% [emqx_stomp_channel:info(clientid, Channel), +% emqx_stomp_channel:info(username, Channel)]))). +%-define(ALARM_CONN_INFO_KEYS, [ +% socktype, sockname, peername, +% clientid, username, proto_name, proto_ver, connected_at +%]). +%-define(ALARM_SOCK_STATS_KEYS, [send_pend, recv_cnt, recv_oct, send_cnt, send_oct]). +%-define(ALARM_SOCK_OPTS_KEYS, [high_watermark, high_msgq_watermark, sndbuf, recbuf, buffer]). + +-dialyzer({no_match, [info/2]}). +-dialyzer({nowarn_function, [ init/4 + , init_state/3 + , run_loop/2 + , system_terminate/4 + , system_code_change/4 + ]}). + +-dialyzer({nowarn_function, [ensure_stats_timer/2,cancel_stats_timer/1, + terminate/2,handle_call/3,handle_timeout/3, + parse_incoming/3,serialize_and_inc_stats_fun/1, + check_oom/1,inc_incoming_stats/1, + inc_outgoing_stats/1]}). + +-spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist()) + -> {ok, pid()}). +start_link(Transport, Socket, Options) -> + Args = [self(), Transport, Socket, Options], + CPid = proc_lib:spawn_link(?MODULE, init, Args), + {ok, CPid}. + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +%% @doc Get infos of the connection/channel. +-spec(info(pid()|state()) -> emqx_types:infos()). +info(CPid) when is_pid(CPid) -> + call(CPid, info); +info(State = #state{channel = Channel}) -> + ChanInfo = emqx_stomp_channel:info(Channel), + SockInfo = maps:from_list( + info(?INFO_KEYS, State)), + ChanInfo#{sockinfo => SockInfo}. + +info(Keys, State) when is_list(Keys) -> + [{Key, info(Key, State)} || Key <- Keys]; +info(socktype, #state{transport = Transport, socket = Socket}) -> + Transport:type(Socket); +info(peername, #state{peername = Peername}) -> + Peername; +info(sockname, #state{sockname = Sockname}) -> + Sockname; +info(sockstate, #state{sockstate = SockSt}) -> + SockSt; +info(active_n, #state{active_n = ActiveN}) -> + ActiveN; +info(stats_timer, #state{stats_timer = StatsTimer}) -> + StatsTimer; +info(limit_timer, #state{limit_timer = LimitTimer}) -> + LimitTimer; +info(limiter, #state{limiter = Limiter}) -> + maybe_apply(fun emqx_limiter:info/1, Limiter). + +%% @doc Get stats of the connection/channel. +-spec(stats(pid()|state()) -> emqx_types:stats()). +stats(CPid) when is_pid(CPid) -> + call(CPid, stats); +stats(#state{transport = Transport, + socket = Socket, + channel = Channel}) -> + SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of + {ok, Ss} -> Ss; + {error, _} -> [] + end, + ConnStats = emqx_pd:get_counters(?CONN_STATS), + ChanStats = emqx_stomp_channel:stats(Channel), + ProcStats = emqx_misc:proc_stats(), + lists:append([SockStats, ConnStats, ChanStats, ProcStats]). + +%% @doc Set TCP keepalive socket options to override system defaults. +%% Idle: The number of seconds a connection needs to be idle before +%% TCP begins sending out keep-alive probes (Linux default 7200). +%% Interval: The number of seconds between TCP keep-alive probes +%% (Linux default 75). +%% Probes: The maximum number of TCP keep-alive probes to send before +%% giving up and killing the connection if no response is +%% obtained from the other end (Linux default 9). +%% +%% NOTE: This API sets TCP socket options, which has nothing to do with +%% the MQTT layer's keepalive (PINGREQ and PINGRESP). +async_set_keepalive(Idle, Interval, Probes) -> + async_set_keepalive(self(), Idle, Interval, Probes). + +async_set_keepalive(Pid, Idle, Interval, Probes) -> + Options = [ {keepalive, true} + , {raw, 6, 4, <>} + , {raw, 6, 5, <>} + , {raw, 6, 6, <>} + ], + async_set_socket_options(Pid, Options). + +%% @doc Set custom socket options. +%% This API is made async because the call might be originated from +%% a hookpoint callback (otherwise deadlock). +%% If failed to set, the error message is logged. +async_set_socket_options(Pid, Options) -> + cast(Pid, {async_set_socket_options, Options}). + +cast(Pid, Req) -> + gen_server:cast(Pid, Req). + +call(Pid, Req) -> + call(Pid, Req, infinity). +call(Pid, Req, Timeout) -> + gen_server:call(Pid, Req, Timeout). + +stop(Pid) -> + gen_server:stop(Pid). + +%%-------------------------------------------------------------------- +%% callbacks +%%-------------------------------------------------------------------- + +init(Parent, Transport, RawSocket, Options) -> + case Transport:wait(RawSocket) of + {ok, Socket} -> + run_loop(Parent, init_state(Transport, Socket, Options)); + {error, Reason} -> + ok = Transport:fast_close(RawSocket), + exit_on_sock_error(Reason) + end. + +init_state(Transport, Socket, Options) -> + {ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]), + {ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]), + Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]), + ConnInfo = #{socktype => Transport:type(Socket), + peername => Peername, + sockname => Sockname, + peercert => Peercert, + conn_mod => ?MODULE + }, + ActiveN = emqx_gateway_utils:active_n(Options), + %% TODO: RateLimit ? How ? + Limiter = undefined, + %RateLimit = emqx_gateway_utils:ratelimit(Options), + %%Limiter = emqx_limiter:init(Zone, RateLimit), + FrameOpts = emqx_gateway_utils:frame_options(Options), + ParseState = emqx_stomp_frame:initial_parse_state(FrameOpts), + Serialize = emqx_stomp_frame:serialize_opts(), + Channel = emqx_stomp_channel:init(ConnInfo, Options), + GcState = emqx_gateway_utils:init_gc_state(Options), + StatsTimer = emqx_gateway_utils:stats_timer(Options), + IdleTimeout = emqx_gateway_utils:idle_timeout(Options), + IdleTimer = emqx_misc:start_timer(IdleTimeout, idle_timeout), + #state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + sockstate = idle, + active_n = ActiveN, + limiter = Limiter, + parse_state = ParseState, + serialize = Serialize, + channel = Channel, + gc_state = GcState, + stats_timer = StatsTimer, + idle_timeout = IdleTimeout, + idle_timer = IdleTimer + }. + +run_loop(Parent, State = #state{transport = Transport, + socket = Socket, + peername = Peername, + channel = _Channel}) -> + emqx_logger:set_metadata_peername(esockd:format(Peername)), + % TODO: How yo get oom_policy ??? + %emqx_misc:tune_heap_size(emqx_gateway_utils:oom_policy( + % emqx_stomp_channel:info(zone, Channel))), + case activate_socket(State) of + {ok, NState} -> hibernate(Parent, NState); + {error, Reason} -> + ok = Transport:fast_close(Socket), + exit_on_sock_error(Reason) + end. + +-spec exit_on_sock_error(any()) -> no_return(). +exit_on_sock_error(Reason) when Reason =:= einval; + Reason =:= enotconn; + Reason =:= closed -> + erlang:exit(normal); +exit_on_sock_error(timeout) -> + erlang:exit({shutdown, ssl_upgrade_timeout}); +exit_on_sock_error(Reason) -> + erlang:exit({shutdown, Reason}). + +%%-------------------------------------------------------------------- +%% Recv Loop + +recvloop(Parent, State = #state{idle_timeout = IdleTimeout}) -> + receive + Msg -> + handle_recv(Msg, Parent, State) + after + IdleTimeout + 100 -> + hibernate(Parent, cancel_stats_timer(State)) + end. + +handle_recv({system, From, Request}, Parent, State) -> + sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State); +handle_recv({'EXIT', Parent, Reason}, Parent, State) -> + %% FIXME: it's not trapping exit, should never receive an EXIT + terminate(Reason, State); +handle_recv(Msg, Parent, State = #state{idle_timeout = IdleTimeout}) -> + case process_msg([Msg], ensure_stats_timer(IdleTimeout, State)) of + {ok, NewState} -> + ?MODULE:recvloop(Parent, NewState); + {stop, Reason, NewSate} -> + terminate(Reason, NewSate) + end. + +hibernate(Parent, State) -> + proc_lib:hibernate(?MODULE, wakeup_from_hib, [Parent, State]). + +%% Maybe do something here later. +wakeup_from_hib(Parent, State) -> + ?MODULE:recvloop(Parent, State). + +%%-------------------------------------------------------------------- +%% Ensure/cancel stats timer + +ensure_stats_timer(Timeout, State = #state{stats_timer = undefined}) -> + State#state{stats_timer = emqx_misc:start_timer(Timeout, emit_stats)}; +ensure_stats_timer(_Timeout, State) -> State. + +cancel_stats_timer(State = #state{stats_timer = TRef}) + when is_reference(TRef) -> + ?tp(debug, cancel_stats_timer, #{}), + ok = emqx_misc:cancel_timer(TRef), + State#state{stats_timer = undefined}; +cancel_stats_timer(State) -> State. + +%%-------------------------------------------------------------------- +%% Process next Msg + +process_msg([], State) -> + {ok, State}; +process_msg([Msg|More], State) -> + try + case handle_msg(Msg, State) of + ok -> + process_msg(More, State); + {ok, NState} -> + process_msg(More, NState); + {ok, Msgs, NState} -> + process_msg(append_msg(More, Msgs), NState); + {stop, Reason, NState} -> + {stop, Reason, NState} + end + catch + exit : normal -> + {stop, normal, State}; + exit : shutdown -> + {stop, shutdown, State}; + exit : {shutdown, _} = Shutdown -> + {stop, Shutdown, State}; + Exception : Context : Stack -> + {stop, #{exception => Exception, + context => Context, + stacktrace => Stack}, State} + end. + +append_msg([], Msgs) when is_list(Msgs) -> + Msgs; +append_msg([], Msg) -> [Msg]; +append_msg(Q, Msgs) when is_list(Msgs) -> + lists:append(Q, Msgs); +append_msg(Q, Msg) -> + lists:append(Q, [Msg]). + +%%-------------------------------------------------------------------- +%% Handle a Msg + +handle_msg({'$gen_call', From, Req}, State) -> + case handle_call(From, Req, State) of + {reply, Reply, NState} -> + gen_server:reply(From, Reply), + {ok, NState}; + {stop, Reason, Reply, NState} -> + gen_server:reply(From, Reply), + stop(Reason, NState) + end; +handle_msg({'$gen_cast', Req}, State) -> + NewState = handle_cast(Req, State), + {ok, NewState}; + +handle_msg({Inet, _Sock, Data}, State = #state{channel = Channel}) + when Inet == tcp; + Inet == ssl -> + ?LOG(debug, "RECV ~0p", [Data]), + Oct = iolist_size(Data), + inc_counter(incoming_bytes, Oct), + Ctx = emqx_stomp_channel:info(ctx, Channel), + ok = emqx_gateway_ctx:metrics_inc(Ctx, 'bytes.received', Oct), + parse_incoming(Data, State); + +handle_msg({incoming, Packet}, State = #state{idle_timer = undefined}) -> + handle_incoming(Packet, State); + +handle_msg({incoming, Packet}, + State = #state{idle_timer = IdleTimer}) -> + ok = emqx_misc:cancel_timer(IdleTimer), + %% XXX: Serialize with inpunt packets + %%Serialize = emqx_stomp_frame:serialize_opts(), + NState = State#state{idle_timer = undefined}, + handle_incoming(Packet, NState); + +handle_msg({outgoing, Packets}, State) -> + handle_outgoing(Packets, State); + +handle_msg({Error, _Sock, Reason}, State) + when Error == tcp_error; Error == ssl_error -> + handle_info({sock_error, Reason}, State); + +handle_msg({Closed, _Sock}, State) + when Closed == tcp_closed; Closed == ssl_closed -> + handle_info({sock_closed, Closed}, close_socket(State)); + +handle_msg({Passive, _Sock}, State) + when Passive == tcp_passive; Passive == ssl_passive -> + %% In Stats + Pubs = emqx_pd:reset_counter(incoming_pubs), + Bytes = emqx_pd:reset_counter(incoming_bytes), + InStats = #{cnt => Pubs, oct => Bytes}, + %% Ensure Rate Limit + NState = ensure_rate_limit(InStats, State), + %% Run GC and Check OOM + NState1 = check_oom(run_gc(InStats, NState)), + handle_info(activate_socket, NState1); + +handle_msg(Deliver = {deliver, _Topic, _Msg}, + #state{active_n = ActiveN} = State) -> + Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], + with_channel(handle_deliver, [Delivers], State); + +%% Something sent +handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) -> + case emqx_pd:get_counter(outgoing_pubs) > ActiveN of + true -> + Pubs = emqx_pd:reset_counter(outgoing_pubs), + Bytes = emqx_pd:reset_counter(outgoing_bytes), + OutStats = #{cnt => Pubs, oct => Bytes}, + {ok, run_gc(OutStats, State)}; + %% FIXME: check oom ??? + %%{ok, check_oom(run_gc(OutStats, State))}; + false -> ok + end; + +handle_msg({inet_reply, _Sock, {error, Reason}}, State) -> + handle_info({sock_error, Reason}, State); + +handle_msg({connack, ConnAck}, State) -> + handle_outgoing(ConnAck, State); + +handle_msg({close, Reason}, State) -> + ?LOG(debug, "Force to close the socket due to ~p", [Reason]), + handle_info({sock_closed, Reason}, close_socket(State)); + +handle_msg({event, connected}, State = #state{channel = Channel}) -> + Ctx = emqx_stomp_channel:info(ctx, Channel), + ClientId = emqx_stomp_channel:info(clientid, Channel), + emqx_gateway_ctx:insert_channel_info( + Ctx, + ClientId, + info(State), + stats(State) + ); + +handle_msg({event, disconnected}, State = #state{channel = Channel}) -> + Ctx = emqx_stomp_channel:info(ctx, Channel), + ClientId = emqx_stomp_channel:info(clientid, Channel), + emqx_gateway_ctx:set_chan_info(Ctx, ClientId, info(State)), + emqx_gateway_ctx:connection_closed(Ctx, ClientId), + {ok, State}; + +handle_msg({event, _Other}, State = #state{channel = Channel}) -> + Ctx = emqx_stomp_channel:info(ctx, Channel), + ClientId = emqx_stomp_channel:info(clientid, Channel), + emqx_gateway_ctx:set_chan_info(Ctx, ClientId, info(State)), + emqx_gateway_ctx:set_chan_stats(Ctx, ClientId, stats(State)), + {ok, State}; + +handle_msg({timeout, TRef, TMsg}, State) -> + handle_timeout(TRef, TMsg, State); + +handle_msg(Shutdown = {shutdown, _Reason}, State) -> + stop(Shutdown, State); + +handle_msg(Msg, State) -> + handle_info(Msg, State). + +%%-------------------------------------------------------------------- +%% Terminate + +-spec terminate(any(), state()) -> no_return(). +terminate(Reason, State = #state{channel = Channel, transport = _Transport, + socket = _Socket}) -> + try + Channel1 = emqx_stomp_channel:set_conn_state(disconnected, Channel), + %emqx_congestion:cancel_alarms(Socket, Transport, Channel1), + emqx_stomp_channel:terminate(Reason, Channel1), + close_socket_ok(State) + catch + E : C : S -> + ?tp(warning, unclean_terminate, #{exception => E, context => C, stacktrace => S}) + end, + ?tp(info, terminate, #{reason => Reason}), + maybe_raise_excption(Reason). + +%% close socket, discard new state, always return ok. +close_socket_ok(State) -> + _ = close_socket(State), + ok. + +%% tell truth about the original exception +maybe_raise_excption(#{exception := Exception, + context := Context, + stacktrace := Stacktrace + }) -> + erlang:raise(Exception, Context, Stacktrace); +maybe_raise_excption(Reason) -> + exit(Reason). + +%%-------------------------------------------------------------------- +%% Sys callbacks + +system_continue(Parent, _Debug, State) -> + ?MODULE:recvloop(Parent, State). + +system_terminate(Reason, _Parent, _Debug, State) -> + terminate(Reason, State). + +system_code_change(State, _Mod, _OldVsn, _Extra) -> + {ok, State}. + +system_get_state(State) -> {ok, State}. + +%%-------------------------------------------------------------------- +%% Handle call + +handle_call(_From, info, State) -> + {reply, info(State), State}; + +handle_call(_From, stats, State) -> + {reply, stats(State), State}; + +%% TODO: How to set ratelimit ??? +%%handle_call(_From, {ratelimit, Policy}, State = #state{channel = Channel}) -> +%% Zone = emqx_stomp_channel:info(zone, Channel), +%% Limiter = emqx_limiter:init(Zone, Policy), +%% {reply, ok, State#state{limiter = Limiter}}; + +handle_call(_From, Req, State = #state{channel = Channel}) -> + case emqx_stomp_channel:handle_call(Req, Channel) of + {reply, Reply, NChannel} -> + {reply, Reply, State#state{channel = NChannel}}; + {shutdown, Reason, Reply, NChannel} -> + shutdown(Reason, Reply, State#state{channel = NChannel}); + {shutdown, Reason, Reply, OutPacket, NChannel} -> + NState = State#state{channel = NChannel}, + ok = handle_outgoing(OutPacket, NState), + shutdown(Reason, Reply, NState) + end. + +%%-------------------------------------------------------------------- +%% Handle timeout + +handle_timeout(_TRef, idle_timeout, State) -> + shutdown(idle_timeout, State); + +handle_timeout(_TRef, limit_timeout, State) -> + NState = State#state{sockstate = idle, + limit_timer = undefined + }, + handle_info(activate_socket, NState); + +handle_timeout(_TRef, emit_stats, State = #state{channel = Channel, + transport = _Transport, + socket = _Socket}) -> + %emqx_congestion:maybe_alarm_conn_congestion(Socket, Transport, Channel), + Ctx = emqx_stomp_channel:info(ctx, Channel), + ClientId = emqx_stomp_channel:info(clientid, Channel), + emqx_gateway_ctx:set_chan_stats(Ctx, ClientId, stats(State)), + {ok, State#state{stats_timer = undefined}}; + +%% Abstraction ??? +%handle_timeout(TRef, keepalive, State = #state{transport = Transport, +% socket = Socket, +% channel = Channel})-> +% case emqx_stomp_channel:info(conn_state, Channel) of +% disconnected -> {ok, State}; +% _ -> +% case Transport:getstat(Socket, [recv_oct]) of +% {ok, [{recv_oct, RecvOct}]} -> +% handle_timeout(TRef, {keepalive, RecvOct}, State); +% {error, Reason} -> +% handle_info({sock_error, Reason}, State) +% end +% end; + +handle_timeout(TRef, TMsg, State = #state{transport = Transport, + socket = Socket, + channel = Channel + }) + when TMsg =:= incoming; + TMsg =:= outgoing -> + Stat = case TMsg of incoming -> recv_oct; _ -> send_oct end, + case emqx_stomp_channel:info(conn_state, Channel) of + disconnected -> {ok, State}; + _ -> + case Transport:getstat(Socket, [Stat]) of + {ok, [{recv_oct, RecvOct}]} -> + handle_timeout(TRef, {incoming, RecvOct}, State); + {ok, [{send_oct, SendOct}]} -> + handle_timeout(TRef, {outgoing, SendOct}, State); + {error, Reason} -> + handle_info({sock_error, Reason}, State) + end + end; + +handle_timeout(TRef, Msg, State) -> + with_channel(handle_timeout, [TRef, Msg], State). + +%%-------------------------------------------------------------------- +%% Parse incoming data + +parse_incoming(Data, State) -> + {Packets, NState} = parse_incoming(Data, [], State), + {ok, next_incoming_msgs(Packets), NState}. + +parse_incoming(<<>>, Packets, State) -> + {Packets, State}; + +parse_incoming(Data, Packets, State = #state{parse_state = ParseState}) -> + try emqx_stomp_frame:parse(Data, ParseState) of + {more, NParseState} -> + {Packets, State#state{parse_state = NParseState}}; + {ok, Packet, Rest, NParseState} -> + NState = State#state{parse_state = NParseState}, + parse_incoming(Rest, [Packet|Packets], NState) + catch + error:Reason:Stk -> + ?LOG(error, "~nParse failed for ~0p~n~0p~nFrame data:~0p", + [Reason, Stk, Data]), + {[{frame_error, Reason}|Packets], State} + end. + +next_incoming_msgs([Packet]) -> + {incoming, Packet}; +next_incoming_msgs(Packets) -> + [{incoming, Packet} || Packet <- lists:reverse(Packets)]. + +%%-------------------------------------------------------------------- +%% Handle incoming packet + +handle_incoming(Packet, State) when is_record(Packet, stomp_frame) -> + ok = inc_incoming_stats(Packet), + ?LOG(debug, "RECV ~s", [emqx_stomp_frame:format(Packet)]), + with_channel(handle_in, [Packet], State); + +handle_incoming(FrameError, State) -> + with_channel(handle_in, [FrameError], State). + +%%-------------------------------------------------------------------- +%% With Channel + +with_channel(Fun, Args, State = #state{channel = Channel}) -> + case erlang:apply(emqx_stomp_channel, Fun, Args ++ [Channel]) of + ok -> {ok, State}; + {ok, NChannel} -> + {ok, State#state{channel = NChannel}}; + {ok, Replies, NChannel} -> + {ok, next_msgs(Replies), State#state{channel = NChannel}}; + {shutdown, Reason, NChannel} -> + shutdown(Reason, State#state{channel = NChannel}); + {shutdown, Reason, Packet, NChannel} -> + NState = State#state{channel = NChannel}, + ok = handle_outgoing(Packet, NState), + shutdown(Reason, NState) + end. + +%%-------------------------------------------------------------------- +%% Handle outgoing packets + +handle_outgoing(Packets, State) when is_list(Packets) -> + send(lists:map(serialize_and_inc_stats_fun(State), Packets), State); + +handle_outgoing(Packet, State) -> + send((serialize_and_inc_stats_fun(State))(Packet), State). + +serialize_and_inc_stats_fun(#state{serialize = Serialize, channel = Channel}) -> + Ctx = emqx_stomp_channel:info(ctx, Channel), + fun(Packet) -> + case emqx_stomp_frame:serialize_pkt(Packet, Serialize) of + <<>> -> ?LOG(warning, "~s is discarded due to the frame is too large!", + [emqx_stomp_frame:format(Packet)]), + ok = emqx_gateway_ctx:metrics_inc(Ctx, 'delivery.dropped.too_large'), + ok = emqx_gateway_ctx:metrics_inc(Ctx, 'delivery.dropped'), + <<>>; + Data -> ?LOG(debug, "SEND ~s", [emqx_stomp_frame:format(Packet)]), + ok = inc_outgoing_stats(Packet), + Data + end + end. + +%%-------------------------------------------------------------------- +%% Send data + +-spec(send(iodata(), state()) -> ok). +send(IoData, #state{transport = Transport, socket = Socket, channel = Channel}) -> + Ctx = emqx_stomp_channel:info(ctx, Channel), + Oct = iolist_size(IoData), + ok = emqx_gateway_ctx:metrics_inc(Ctx, 'bytes.sent', Oct), + inc_counter(outgoing_bytes, Oct), + %emqx_congestion:maybe_alarm_conn_congestion(Socket, Transport, Channel), + case Transport:async_send(Socket, IoData, [nosuspend]) of + ok -> ok; + Error = {error, _Reason} -> + %% Send an inet_reply to postpone handling the error + self() ! {inet_reply, Socket, Error}, + ok + end. + +%%-------------------------------------------------------------------- +%% Handle Info + +handle_info(activate_socket, State = #state{sockstate = OldSst}) -> + case activate_socket(State) of + {ok, NState = #state{sockstate = NewSst}} -> + case OldSst =/= NewSst of + true -> {ok, {event, NewSst}, NState}; + false -> {ok, NState} + end; + {error, Reason} -> + handle_info({sock_error, Reason}, State) + end; + +handle_info({sock_error, Reason}, State) -> + case Reason =/= closed andalso Reason =/= einval of + true -> ?LOG(warning, "socket_error: ~p", [Reason]); + false -> ok + end, + handle_info({sock_closed, Reason}, close_socket(State)); + +handle_info(Info, State) -> + with_channel(handle_info, [Info], State). + +%%-------------------------------------------------------------------- +%% Handle Info + +handle_cast({async_set_socket_options, Opts}, + State = #state{transport = Transport, + socket = Socket + }) -> + case Transport:setopts(Socket, Opts) of + ok -> ?tp(info, "custom_socket_options_successfully", #{opts => Opts}); + Err -> ?tp(error, "failed_to_set_custom_socket_optionn", #{reason => Err}) + end, + State; +handle_cast(Req, State) -> + ?tp(error, "received_unknown_cast", #{cast => Req}), + State. + +%%-------------------------------------------------------------------- +%% Ensure rate limit + +ensure_rate_limit(Stats, State = #state{limiter = Limiter}) -> + case ?ENABLED(Limiter) andalso emqx_limiter:check(Stats, Limiter) of + false -> State; + {ok, Limiter1} -> + State#state{limiter = Limiter1}; + {pause, Time, Limiter1} -> + ?LOG(warning, "Pause ~pms due to rate limit", [Time]), + TRef = emqx_misc:start_timer(Time, limit_timeout), + State#state{sockstate = blocked, + limiter = Limiter1, + limit_timer = TRef + } + end. + +%%-------------------------------------------------------------------- +%% Run GC and Check OOM + +run_gc(Stats, State = #state{gc_state = GcSt}) -> + case ?ENABLED(GcSt) andalso emqx_gc:run(Stats, GcSt) of + false -> State; + {_IsGC, GcSt1} -> + State#state{gc_state = GcSt1} + end. + +check_oom(State = #state{channel = Channel}) -> + Zone = emqx_stomp_channel:info(zone, Channel), + OomPolicy = emqx_gateway_utils:oom_policy(Zone), + ?tp(debug, check_oom, #{policy => OomPolicy}), + case ?ENABLED(OomPolicy) andalso emqx_misc:check_oom(OomPolicy) of + {shutdown, Reason} -> + %% triggers terminate/2 callback immediately + erlang:exit({shutdown, Reason}); + _Other -> + ok + end, + State. + +%%-------------------------------------------------------------------- +%% Activate Socket + +-compile({inline, [activate_socket/1]}). +activate_socket(State = #state{sockstate = closed}) -> + {ok, State}; +activate_socket(State = #state{sockstate = blocked}) -> + {ok, State}; +activate_socket(State = #state{transport = Transport, + socket = Socket, + active_n = N}) -> + case Transport:setopts(Socket, [{active, N}]) of + ok -> {ok, State#state{sockstate = running}}; + Error -> Error + end. + +%%-------------------------------------------------------------------- +%% Close Socket + +close_socket(State = #state{sockstate = closed}) -> State; +close_socket(State = #state{transport = Transport, socket = Socket}) -> + ok = Transport:fast_close(Socket), + State#state{sockstate = closed}. + +%%-------------------------------------------------------------------- +%% Inc incoming/outgoing stats + +%% XXX: Other packet type? +inc_incoming_stats(Packet = ?PACKET(Type)) -> + inc_counter(recv_pkt, 1), + case Type =:= ?CMD_SEND of + true -> + inc_counter(recv_msg, 1), + inc_counter(incoming_pubs, 1); + false -> + ok + end, + emqx_metrics:inc_recv(Packet). + +inc_outgoing_stats(Packet = ?PACKET(Type)) -> + inc_counter(send_pkt, 1), + case Type =:= ?CMD_MESSAGE of + true -> + inc_counter(send_msg, 1), + inc_counter(outgoing_pubs, 1); + false -> + ok + end, + emqx_metrics:inc_sent(Packet). + +%%-------------------------------------------------------------------- +%% Helper functions + +next_msgs(Packet) when is_record(Packet, stomp_frame) -> + {outgoing, Packet}; +next_msgs(Event) when is_tuple(Event) -> + Event; +next_msgs(More) when is_list(More) -> + More. + +shutdown(Reason, State) -> + stop({shutdown, Reason}, State). + +shutdown(Reason, Reply, State) -> + stop({shutdown, Reason}, Reply, State). + +stop(Reason, State) -> + {stop, Reason, State}. + +stop(Reason, Reply, State) -> + {stop, Reason, Reply, State}. + +inc_counter(Key, Inc) -> + _ = emqx_pd:inc_counter(Key, Inc), + ok. + +%%-------------------------------------------------------------------- +%% For CT tests +%%-------------------------------------------------------------------- + +set_field(Name, Value, State) -> + Pos = emqx_misc:index_of(Name, record_info(fields, state)), + setelement(Pos+1, State, Value). + +get_state(Pid) -> + State = sys:get_state(Pid), + maps:from_list(lists:zip(record_info(fields, state), + tl(tuple_to_list(State)))). diff --git a/apps/emqx_stomp/src/emqx_stomp_frame.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_frame.erl similarity index 70% rename from apps/emqx_stomp/src/emqx_stomp_frame.erl rename to apps/emqx_gateway/src/stomp/emqx_stomp_frame.erl index fa9cb63a8..4db8a1f5f 100644 --- a/apps/emqx_stomp/src/emqx_stomp_frame.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_frame.erl @@ -68,14 +68,16 @@ -module(emqx_stomp_frame). --include("emqx_stomp.hrl"). +-include("src/stomp/include/emqx_stomp.hrl"). --export([ init_parer_state/1 +-export([ initial_parse_state/1 , parse/2 - , serialize/1 + , serialize_opts/0 + , serialize_pkt/2 ]). --export([ make/2 +-export([ make/1 + , make/2 , make/3 , format/1 ]). @@ -96,28 +98,33 @@ -record(frame_limit, {max_header_num, max_header_length, max_body_length}). --type(result() :: {ok, stomp_frame(), binary()} - | {more, parser()} - | {error, any()}). +-type(parse_result() :: {ok, stomp_frame(), binary()} + | {more, parse_state()}). --type(parser() :: #{phase := none | command | headers | hdname | hdvalue | body, - pre => binary(), - state := #parser_state{}}). +-type(parse_state() :: + #{phase := none | command | headers | hdname | hdvalue | body, + pre => binary(), + state := #parser_state{} + }). + +-dialyzer({nowarn_function, [serialize_pkt/2,make/1]}). %% @doc Initialize a parser --spec init_parer_state([proplists:property()]) -> parser(). -init_parer_state(Opts) -> +-spec initial_parse_state(map()) -> parse_state(). +initial_parse_state(Opts) -> #{phase => none, state => #parser_state{limit = limit(Opts)}}. limit(Opts) -> - #frame_limit{max_header_num = g(max_header_num, Opts, ?MAX_HEADER_NUM), - max_header_length = g(max_header_length, Opts, ?MAX_BODY_LENGTH), - max_body_length = g(max_body_length, Opts, ?MAX_BODY_LENGTH)}. + #frame_limit{ + max_header_num = g(max_header_num, Opts, ?MAX_HEADER_NUM), + max_header_length = g(max_header_length, Opts, ?MAX_BODY_LENGTH), + max_body_length = g(max_body_length, Opts, ?MAX_BODY_LENGTH) + }. g(Key, Opts, Val) -> - proplists:get_value(Key, Opts, Val). + maps:get(Key, Opts, Val). --spec parse(binary(), parser()) -> result(). +-spec parse(binary(), parse_state()) -> parse_result(). parse(<<>>, Parser) -> {more, Parser}; @@ -131,11 +138,14 @@ parse(<>, #{phase := Phase, state := State}) -> parse(<>, Parser) -> {more, Parser#{pre => <>}}; parse(<>, _Parser) -> - {error, linefeed_expected}; + error(linefeed_expected); -parse(<>, Parser = #{phase := Phase}) when Phase =:= hdname; Phase =:= hdvalue -> +parse(<>, Parser = #{phase := Phase}) when Phase =:= hdname; + Phase =:= hdvalue -> {more, Parser#{pre => <>}}; -parse(<>, #{phase := Phase, state := State}) when Phase =:= hdname; Phase =:= hdvalue -> +parse(<>, + #{phase := Phase, state := State}) when Phase =:= hdname; + Phase =:= hdvalue -> parse(Phase, Rest, acc(unescape(Ch), State)); parse(Bytes, #{phase := none, state := State}) -> @@ -153,14 +163,19 @@ parse(headers, Bin, State) -> parse(hdname, Bin, State); parse(hdname, <>, _State) -> - {error, unexpected_linefeed}; + error(unexpected_linefeed); parse(hdname, <>, State = #parser_state{acc = Acc}) -> parse(hdvalue, Rest, State#parser_state{hdname = Acc, acc = <<>>}); parse(hdname, <>, State) -> parse(hdname, Rest, acc(Ch, State)); -parse(hdvalue, <>, State = #parser_state{headers = Headers, hdname = Name, acc = Acc}) -> - parse(headers, Rest, State#parser_state{headers = add_header(Name, Acc, Headers), hdname = undefined, acc = <<>>}); +parse(hdvalue, <>, + State = #parser_state{headers = Headers, hdname = Name, acc = Acc}) -> + NState = State#parser_state{headers = add_header(Name, Acc, Headers), + hdname = undefined, + acc = <<>> + }, + parse(headers, Rest, NState); parse(hdvalue, <>, State) -> parse(hdvalue, Rest, acc(Ch, State)). @@ -170,15 +185,19 @@ parse(body, <<>>, State, Length) -> parse(body, Bin, State, none) -> case binary:split(Bin, <>) of [Chunk, Rest] -> - {ok, new_frame(acc(Chunk, State)), Rest}; + {ok, new_frame(acc(Chunk, State)), Rest, new_state(State)}; [Chunk] -> - {more, #{phase => body, length => none, state => acc(Chunk, State)}} + {more, #{phase => body, + length => none, + state => acc(Chunk, State)}} end; parse(body, Bin, State, Len) when byte_size(Bin) >= (Len+1) -> <> = Bin, - {ok, new_frame(acc(Chunk, State)), Rest}; + {ok, new_frame(acc(Chunk, State)), Rest, new_state(State)}; parse(body, Bin, State, Len) -> - {more, #{phase => body, length => Len - byte_size(Bin), state => acc(Bin, State)}}. + {more, #{phase => body, + length => Len - byte_size(Bin), + state => acc(Bin, State)}}. add_header(Name, Value, Headers) -> case lists:keyfind(Name, 1, Headers) of @@ -208,20 +227,33 @@ unescape($r) -> ?CR; unescape($n) -> ?LF; unescape($c) -> ?COLON; unescape($\\) -> ?BSL; -unescape(_Ch) -> {error, cannnot_unescape}. +unescape(_Ch) -> error(cannnot_unescape). -serialize(#stomp_frame{command = Cmd, headers = Headers, body = Body}) -> +%%-------------------------------------------------------------------- +%% Serialize funcs +%%-------------------------------------------------------------------- + +serialize_opts() -> + #{}. + +serialize_pkt(#stomp_frame{command = heartbeat}, _SerializeOpts) -> + <<$\n>>; + +serialize_pkt(#stomp_frame{command = Cmd, headers = Headers, body = Body}, + _SerializeOpts) -> Headers1 = lists:keydelete(<<"content-length">>, 1, Headers), Headers2 = case iolist_size(Body) of 0 -> Headers1; Len -> Headers1 ++ [{<<"content-length">>, Len}] end, - [Cmd, ?LF, [serialize(header, Header) || Header <- Headers2], ?LF, Body, 0]. + [Cmd, + ?LF, [serialize_pkt(header, Header) || Header <- Headers2], + ?LF, Body, 0]; -serialize(header, {Name, Val}) when is_integer(Val) -> +serialize_pkt(header, {Name, Val}) when is_integer(Val) -> [escape(Name), ?COLON, integer_to_list(Val), ?LF]; -serialize(header, {Name, Val}) -> +serialize_pkt(header, {Name, Val}) -> [escape(Name), ?COLON, escape(Val), ?LF]. escape(Bin) when is_binary(Bin) -> @@ -232,8 +264,18 @@ escape(?BSL) -> <>; escape(?COLON) -> <>; escape(Ch) -> <>. +new_state(#parser_state{limit = Limit}) -> + #{phase => none, state => #parser_state{limit = Limit}}. + +%%-------------------------------------------------------------------- +%% ??? +%%-------------------------------------------------------------------- %% @doc Make a frame + +make(heartbeat) -> + #stomp_frame{command = heartbeat}. + make(<<"CONNECTED">>, Headers) -> #stomp_frame{command = <<"CONNECTED">>, headers = [{<<"server">>, ?STOMP_SERVER} | Headers]}; @@ -245,5 +287,4 @@ make(Command, Headers, Body) -> #stomp_frame{command = Command, headers = Headers, body = Body}. %% @doc Format a frame -format(Frame) -> serialize(Frame). - +format(Frame) -> serialize_pkt(Frame, #{}). diff --git a/apps/emqx_stomp/src/emqx_stomp_heartbeat.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_heartbeat.erl similarity index 89% rename from apps/emqx_stomp/src/emqx_stomp_heartbeat.erl rename to apps/emqx_gateway/src/stomp/emqx_stomp_heartbeat.erl index 145359e53..99a1508e1 100644 --- a/apps/emqx_stomp/src/emqx_stomp_heartbeat.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_heartbeat.erl @@ -17,10 +17,11 @@ %% @doc Stomp heartbeat. -module(emqx_stomp_heartbeat). --include("emqx_stomp.hrl"). +-include("src/stomp/include/emqx_stomp.hrl"). -export([ init/1 , check/3 + , reset/3 , info/1 , interval/2 ]). @@ -33,7 +34,6 @@ outgoing => #heartbeater{} }. - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- @@ -77,6 +77,15 @@ check(NewVal, HrtBter = #heartbeater{statval = OldVal, true -> {error, timeout} end. +-spec reset(name(), pos_integer(), heartbeat()) + -> heartbeat(). +reset(Name, NewVal, HrtBt) -> + HrtBter = maps:get(Name, HrtBt), + HrtBt#{Name => reset(NewVal, HrtBter)}. + +reset(NewVal, HrtBter) -> + HrtBter#heartbeater{statval = NewVal, repeat = 1}. + -spec info(heartbeat()) -> map(). info(HrtBt) -> maps:map(fun(_, #heartbeater{interval = Intv, diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl new file mode 100644 index 000000000..e6e62565a --- /dev/null +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -0,0 +1,153 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_stomp_impl). + +-include_lib("emqx_gateway/include/emqx_gateway.hrl"). + +-behavior(emqx_gateway_impl). + +%% APIs +-export([ load/0 + , unload/0 + ]). + +-export([ init/1 + , on_insta_create/3 + , on_insta_update/4 + , on_insta_destroy/3 + ]). + +-define(TCP_OPTS, [binary, {packet, raw}, {reuseaddr, true}, {nodelay, true}]). + +-dialyzer({nowarn_function, [load/0]}). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +load() -> + RegistryOptions = [ {cbkmod, ?MODULE} + , {schema, emqx_stomp_schema} + ], + + YourOptions = [param1, param2], + emqx_gateway_registry:load(stomp, RegistryOptions, YourOptions). + +unload() -> + emqx_gateway_registry:unload(stomp). + +init([param1, param2]) -> + GwState = #{}, + {ok, GwState}. + +%%-------------------------------------------------------------------- +%% emqx_gateway_registry callbacks +%%-------------------------------------------------------------------- + +on_insta_create(_Insta = #{ id := InstaId, + rawconf := RawConf + }, Ctx, _GwState) -> + %% Step1. Fold the rawconfs to listeners + Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + %% Step2. Start listeners or escokd:specs + ListenerPids = lists:map(fun(Lis) -> + start_listener(InstaId, Ctx, Lis) + end, Listeners), + %% FIXME: How to throw an exception to interrupt the restart logic ? + %% FIXME: Assign ctx to InstaState + {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. + +%% @private +on_insta_update(NewInsta, OldInstace, GwInstaState = #{ctx := Ctx}, GwState) -> + InstaId = maps:get(id, NewInsta), + try + %% XXX: 1. How hot-upgrade the changes ??? + %% XXX: 2. Check the New confs first before destroy old instance ??? + on_insta_destroy(OldInstace, GwInstaState, GwState), + on_insta_create(NewInsta, Ctx, GwState) + catch + Class : Reason : Stk -> + logger:error("Failed to update stomp instance ~s; " + "reason: {~0p, ~0p} stacktrace: ~0p", + [InstaId, Class, Reason, Stk]), + {error, {Class, Reason}} + end. + +on_insta_destroy(_Insta = #{ id := InstaId, + rawconf := RawConf + }, _GwInstaState, _GwState) -> + Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + lists:foreach(fun(Lis) -> + stop_listener(InstaId, Lis) + end, Listeners). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> + case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + {ok, Pid} -> + io:format("Start stomp ~s:~s listener on ~s successfully.~n", + [InstaId, Type, format(ListenOn)]), + Pid; + {error, Reason} -> + io:format(standard_error, + "Failed to start stomp ~s:~s listener on ~s: ~0p~n", + [InstaId, Type, format(ListenOn), Reason]), + throw({badconf, Reason}) + end. + +start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(InstaId, Type), + esockd:open(Name, ListenOn, merge_default(SocketOpts), + {emqx_stomp_connection, start_link, [Cfg#{ctx => Ctx}]}). + +name(InstaId, Type) -> + list_to_atom(lists:concat([InstaId, ":", Type])). + +merge_default(Options) -> + case lists:keytake(tcp_options, 1, Options) of + {value, {tcp_options, TcpOpts}, Options1} -> + [{tcp_options, emqx_misc:merge_opts(?TCP_OPTS, TcpOpts)} | Options1]; + false -> + [{tcp_options, ?TCP_OPTS} | Options] + end. + +format(Port) when is_integer(Port) -> + io_lib:format("0.0.0.0:~w", [Port]); +format({Addr, Port}) when is_list(Addr) -> + io_lib:format("~s:~w", [Addr, Port]); +format({Addr, Port}) when is_tuple(Addr) -> + io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). + +stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), + case StopRet of + ok -> io:format("Stop stomp ~s:~s listener on ~s successfully.~n", + [InstaId, Type, format(ListenOn)]); + {error, Reason} -> + io:format(standard_error, + "Failed to stop stomp ~s:~s listener on ~s: ~0p~n", + [InstaId, Type, format(ListenOn), Reason] + ) + end, + StopRet. + +stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(InstaId, Type), + esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/stomp/include/emqx_stomp.hrl b/apps/emqx_gateway/src/stomp/include/emqx_stomp.hrl new file mode 100644 index 000000000..cffcb1bdf --- /dev/null +++ b/apps/emqx_gateway/src/stomp/include/emqx_stomp.hrl @@ -0,0 +1,92 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 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. +%%-------------------------------------------------------------------- + +-ifndef(EMQX_STOMP_HRL). +-define(EMQX_STOMP_HRL, true). + +-define(STOMP_VER, <<"1.2">>). + +-define(STOMP_SERVER, <<"emqx-stomp/1.2">>). + +%%-------------------------------------------------------------------- +%% STOMP Frame +%%-------------------------------------------------------------------- + +%% client command +-define(CMD_STOMP, <<"STOMP">>). +-define(CMD_CONNECT, <<"CONNECT">>). +-define(CMD_SEND, <<"SEND">>). +-define(CMD_SUBSCRIBE, <<"SUBSCRIBE">>). +-define(CMD_UNSUBSCRIBE, <<"UNSUBSCRIBE">>). +-define(CMD_BEGIN, <<"BEGIN">>). +-define(CMD_COMMIT, <<"COMMIT">>). +-define(CMD_ABORT, <<"ABORT">>). +-define(CMD_ACK, <<"ACK">>). +-define(CMD_NACK, <<"NACK">>). +-define(CMD_DISCONNECT, <<"DISCONNECT">>). + +%% server command +-define(CMD_CONNECTED, <<"CONNECTED">>). +-define(CMD_MESSAGE, <<"MESSAGE">>). +-define(CMD_RECEIPT, <<"RECEIPT">>). +-define(CMD_ERROR, <<"ERROR">>). + +-type client_command() :: binary(). +%-type client_command() :: ?CMD_SEND | ?CMD_SUBSCRIBE | ?CMD_UNSUBSCRIBE +% | ?CMD_BEGIN | ?CMD_COMMIT | ?CMD_ABORT | ?CMD_ACK +% | ?CMD_NACK | ?CMD_DISCONNECT | ?CMD_CONNECT +% | ?CMD_STOMP. +% +-type server_command() :: binary(). +%-type server_command() :: ?CMD_CONNECTED | ?CMD_MESSAGE | ?CMD_RECEIPT +% | ?CMD_ERROR. + +-record(stomp_frame, { + command :: client_command() | server_command(), + headers = [], + body = <<>> :: iodata()} + ). + +-type stomp_frame() :: #stomp_frame{}. + +-define(PACKET(CMD), #stomp_frame{command = CMD}). + +-define(PACKET(CMD, Headers), #stomp_frame{command = CMD, headers = Headers}). + +-define(PACKET(CMD, Headers, Body), #stomp_frame{command = CMD, + headers = Headers, + body = Body + }). + +%%-------------------------------------------------------------------- +%% Frame Size Limits +%% +%% To prevent malicious clients from exploiting memory allocation in a server, +%% servers MAY place maximum limits on: +%% +%% the number of frame headers allowed in a single frame +%% the maximum length of header lines +%% the maximum size of a frame body +%% +%% If these limits are exceeded the server SHOULD send the client an ERROR frame +%% and then close the connection. +%%-------------------------------------------------------------------- + +-define(MAX_HEADER_NUM, 10). +-define(MAX_HEADER_LENGTH, 1024). +-define(MAX_BODY_LENGTH, 65536). + +-endif. diff --git a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl new file mode 100644 index 000000000..cfc9399bb --- /dev/null +++ b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl @@ -0,0 +1,87 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_registry_SUITE). + +-include_lib("eunit/include/eunit.hrl"). + +-compile(export_all). +-compile(nowarn_export_all). + +all() -> emqx_ct:all(?MODULE). + +%%-------------------------------------------------------------------- +%% Setups +%%-------------------------------------------------------------------- + +init_per_suite(Cfg) -> + emqx_ct_helpers:start_apps([emqx_gateway], fun set_special_configs/1), + Cfg. + +end_per_suite(_Cfg) -> + emqx_ct_helpers:stop_apps([emqx_gateway]), + ok. + +set_special_configs(emqx_gateway) -> + emqx_config:put( + [emqx_gateway], + #{stomp => + #{'1' => + #{authenticator => allow_anonymous, + clientinfo_override => + #{password => "${Packet.headers.passcode}", + username => "${Packet.headers.login}"}, + frame => + #{max_body_length => 8192, + max_headers => 10, + max_headers_length => 1024}, + listener => + #{tcp => + #{'1' => + #{acceptors => 16,active_n => 100,backlog => 1024, + bind => 61613,high_watermark => 1048576, + max_conn_rate => 1000,max_connections => 1024000, + send_timeout => 15000,send_timeout_close => true}}}}}}), + ok; +set_special_configs(_) -> + ok. + +%%-------------------------------------------------------------------- +%% Test cases +%%-------------------------------------------------------------------- + +t_load_unload(_) -> + OldCnt = length(emqx_gateway_registry:list()), + RgOpts = [{cbkmod, ?MODULE}], + GwOpts = [paramsin], + ok = emqx_gateway_registry:load(test, RgOpts, GwOpts), + ?assertEqual(OldCnt+1, length(emqx_gateway_registry:list())), + + #{cbkmod := ?MODULE, + rgopts := RgOpts, + gwopts := GwOpts, + state := #{gwstate := 1}} = emqx_gateway_registry:lookup(test), + + {error, already_existed} = emqx_gateway_registry:load(test, [{cbkmod, ?MODULE}], GwOpts), + + ok = emqx_gateway_registry:unload(test), + undefined = emqx_gateway_registry:lookup(test), + OldCnt = length(emqx_gateway_registry:list()), + ok. + +init([paramsin]) -> + {ok, _GwState = #{gwstate => 1}}. + diff --git a/apps/emqx_stomp/test/emqx_stomp_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl similarity index 82% rename from apps/emqx_stomp/test/emqx_stomp_SUITE.erl rename to apps/emqx_gateway/test/emqx_stomp_SUITE.erl index 9a5d9698e..cc2b0db54 100644 --- a/apps/emqx_stomp/test/emqx_stomp_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl @@ -16,7 +16,7 @@ -module(emqx_stomp_SUITE). --include_lib("emqx_stomp/include/emqx_stomp.hrl"). +-include_lib("emqx_gateway/src/stomp/include/emqx_stomp.hrl"). -compile(export_all). -compile(nowarn_export_all). @@ -29,12 +29,37 @@ all() -> emqx_ct:all(?MODULE). %% Setups %%-------------------------------------------------------------------- -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_stomp]), - Config. +init_per_suite(Cfg) -> + emqx_ct_helpers:start_apps([emqx_gateway], fun set_special_configs/1), + Cfg. -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_stomp]). +end_per_suite(_Cfg) -> + emqx_ct_helpers:stop_apps([emqx_gateway]), + ok. + +set_special_configs(emqx_gateway) -> + emqx_config:put( + [emqx_gateway], + #{stomp => + #{'1' => + #{authenticator => allow_anonymous, + clientinfo_override => + #{password => "${Packet.headers.passcode}", + username => "${Packet.headers.login}"}, + frame => + #{max_body_length => 8192, + max_headers => 10, + max_headers_length => 1024}, + listener => + #{tcp => + #{'1' => + #{acceptors => 16,active_n => 100,backlog => 1024, + bind => 61613,high_watermark => 1048576, + max_conn_rate => 1000,max_connections => 1024000, + send_timeout => 15000,send_timeout_close => true}}}}}}), + ok; +set_special_configs(_) -> + ok. %%-------------------------------------------------------------------- %% Test Cases @@ -52,7 +77,7 @@ t_connect(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, Frame = #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), <<"2000,1000">> = proplists:get_value(<<"heart-beat">>, Frame#stomp_frame.headers), gen_tcp:send(Sock, serialize(<<"DISCONNECT">>, @@ -61,22 +86,23 @@ t_connect(_) -> {ok, Data1} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"RECEIPT">>, headers = [{<<"receipt-id">>, <<"12345">>}], - body = _}, _} = parse(Data1) + body = _}, _, _} = parse(Data1) end), %% Connect will be failed, because of bad login or passcode - with_connection(fun(Sock) -> - gen_tcp:send(Sock, serialize(<<"CONNECT">>, - [{<<"accept-version">>, ?STOMP_VER}, - {<<"host">>, <<"127.0.0.1:61613">>}, - {<<"login">>, <<"admin">>}, - {<<"passcode">>, <<"admin">>}, - {<<"heart-beat">>, <<"1000,2000">>}])), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, #stomp_frame{command = <<"ERROR">>, - headers = _, - body = <<"Login or passcode error!">>}, _} = parse(Data) - end), + %% FIXME: Waiting for authentication works + %with_connection(fun(Sock) -> + % gen_tcp:send(Sock, serialize(<<"CONNECT">>, + % [{<<"accept-version">>, ?STOMP_VER}, + % {<<"host">>, <<"127.0.0.1:61613">>}, + % {<<"login">>, <<"admin">>}, + % {<<"passcode">>, <<"admin">>}, + % {<<"heart-beat">>, <<"1000,2000">>}])), + % {ok, Data} = gen_tcp:recv(Sock, 0), + % {ok, #stomp_frame{command = <<"ERROR">>, + % headers = _, + % body = <<"Login or passcode error!">>}, _, _} = parse(Data) + % end), %% Connect will be failed, because of bad version with_connection(fun(Sock) -> @@ -89,7 +115,7 @@ t_connect(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"ERROR">>, headers = _, - body = <<"Supported protocol versions < 1.2">>}, _} = parse(Data) + body = <<"Login Failed: Supported protocol versions < 1.2">>}, _, _} = parse(Data) end). t_heartbeat(_) -> @@ -104,7 +130,7 @@ t_heartbeat(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), {ok, ?HEARTBEAT} = gen_tcp:recv(Sock, 0), %% Server will close the connection because never receive the heart beat from client @@ -122,7 +148,7 @@ t_subscribe(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), %% Subscribe gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>, @@ -139,7 +165,7 @@ t_subscribe(_) -> {ok, Data1} = gen_tcp:recv(Sock, 0, 1000), {ok, Frame = #stomp_frame{command = <<"MESSAGE">>, headers = _, - body = <<"hello">>}, _} = parse(Data1), + body = <<"hello">>}, _, _} = parse(Data1), lists:foreach(fun({Key, Val}) -> Val = proplists:get_value(Key, Frame#stomp_frame.headers) end, [{<<"destination">>, <<"/queue/foo">>}, @@ -155,7 +181,7 @@ t_subscribe(_) -> {ok, #stomp_frame{command = <<"RECEIPT">>, headers = [{<<"receipt-id">>, <<"12345">>}], - body = _}, _} = parse(Data2), + body = _}, _, _} = parse(Data2), gen_tcp:send(Sock, serialize(<<"SEND">>, [{<<"destination">>, <<"/queue/foo">>}], @@ -175,7 +201,7 @@ t_transaction(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), %% Subscribe gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>, @@ -208,12 +234,12 @@ t_transaction(_) -> {ok, #stomp_frame{command = <<"MESSAGE">>, headers = _, - body = <<"hello">>}, Rest1} = parse(Data1), + body = <<"hello">>}, Rest1, _} = parse(Data1), %{ok, Data2} = gen_tcp:recv(Sock, 0, 500), {ok, #stomp_frame{command = <<"MESSAGE">>, headers = _, - body = <<"hello again">>}, _Rest2} = parse(Rest1), + body = <<"hello again">>}, _Rest2, _} = parse(Rest1), %% Transaction: tx2 gen_tcp:send(Sock, serialize(<<"BEGIN">>, @@ -236,7 +262,7 @@ t_transaction(_) -> {ok, Data3} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"RECEIPT">>, headers = [{<<"receipt-id">>, <<"12345">>}], - body = _}, _} = parse(Data3) + body = _}, _, _} = parse(Data3) end). t_receipt_in_error(_) -> @@ -250,7 +276,7 @@ t_receipt_in_error(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), gen_tcp:send(Sock, serialize(<<"ABORT">>, [{<<"transaction">>, <<"tx1">>}, @@ -259,7 +285,7 @@ t_receipt_in_error(_) -> {ok, Data1} = gen_tcp:recv(Sock, 0), {ok, Frame = #stomp_frame{command = <<"ERROR">>, headers = _, - body = <<"Transaction tx1 not found">>}, _} = parse(Data1), + body = <<"Transaction tx1 not found">>}, _, _} = parse(Data1), <<"12345">> = proplists:get_value(<<"receipt-id">>, Frame#stomp_frame.headers) end). @@ -275,7 +301,7 @@ t_ack(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), %% Subscribe gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>, @@ -290,7 +316,7 @@ t_ack(_) -> {ok, Data1} = gen_tcp:recv(Sock, 0), {ok, Frame = #stomp_frame{command = <<"MESSAGE">>, headers = _, - body = <<"ack test">>}, _} = parse(Data1), + body = <<"ack test">>}, _, _} = parse(Data1), AckId = proplists:get_value(<<"ack">>, Frame#stomp_frame.headers), @@ -301,7 +327,7 @@ t_ack(_) -> {ok, Data2} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"RECEIPT">>, headers = [{<<"receipt-id">>, <<"12345">>}], - body = _}, _} = parse(Data2), + body = _}, _, _} = parse(Data2), gen_tcp:send(Sock, serialize(<<"SEND">>, [{<<"destination">>, <<"/queue/foo">>}], @@ -310,7 +336,7 @@ t_ack(_) -> {ok, Data3} = gen_tcp:recv(Sock, 0), {ok, Frame1 = #stomp_frame{command = <<"MESSAGE">>, headers = _, - body = <<"nack test">>}, _} = parse(Data3), + body = <<"nack test">>}, _, _} = parse(Data3), AckId1 = proplists:get_value(<<"ack">>, Frame1#stomp_frame.headers), @@ -321,9 +347,16 @@ t_ack(_) -> {ok, Data4} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"RECEIPT">>, headers = [{<<"receipt-id">>, <<"12345">>}], - body = _}, _} = parse(Data4) + body = _}, _, _} = parse(Data4) end). +%% TODO: Mountpoint, AuthChain, ACL + Mountpoint, ClientInfoOverride, +%% Listeners, Metrics, Stats, ClientInfo +%% +%% TODO: Start/Stop, List Instace +%% +%% TODO: RateLimit, OOM, + with_connection(DoFun) -> {ok, Sock} = gen_tcp:connect({127, 0, 0, 1}, 61613, @@ -336,14 +369,15 @@ with_connection(DoFun) -> end. serialize(Command, Headers) -> - emqx_stomp_frame:serialize(emqx_stomp_frame:make(Command, Headers)). + emqx_stomp_frame:serialize_pkt(emqx_stomp_frame:make(Command, Headers), #{}). serialize(Command, Headers, Body) -> - emqx_stomp_frame:serialize(emqx_stomp_frame:make(Command, Headers, Body)). + emqx_stomp_frame:serialize_pkt(emqx_stomp_frame:make(Command, Headers, Body), #{}). parse(Data) -> - ProtoEnv = [{max_headers, 10}, - {max_header_length, 1024}, - {max_body_length, 8192}], - Parser = emqx_stomp_frame:init_parer_state(ProtoEnv), + ProtoEnv = #{max_headers => 10, + max_header_length => 1024, + max_body_length => 8192 + }, + Parser = emqx_stomp_frame:initial_parse_state(ProtoEnv), emqx_stomp_frame:parse(Data, Parser). diff --git a/apps/emqx_stomp/test/emqx_stomp_heartbeat_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_heartbeat_SUITE.erl similarity index 100% rename from apps/emqx_stomp/test/emqx_stomp_heartbeat_SUITE.erl rename to apps/emqx_gateway/test/emqx_stomp_heartbeat_SUITE.erl diff --git a/apps/emqx_lua_hook/.gitignore b/apps/emqx_lua_hook/.gitignore deleted file mode 100644 index af616ea1a..000000000 --- a/apps/emqx_lua_hook/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -deps/ -ebin/ -_rel/ -.erlang.mk/ -*.d -data/ -*.iml -.idea/ -logs/ -*.beam -.DS_Store -erlang.mk -_build/ -rebar.lock -rebar3.crashdump -bbmustache/ -*.conf.rendered -.rebar3 -*.swp diff --git a/apps/emqx_lua_hook/README.md b/apps/emqx_lua_hook/README.md deleted file mode 100644 index a3f65a094..000000000 --- a/apps/emqx_lua_hook/README.md +++ /dev/null @@ -1,338 +0,0 @@ - -# emqx-lua-hook - -This plugin makes it possible to write hooks in lua scripts. - -Lua virtual machine is implemented by [luerl](https://github.com/rvirding/luerl) which supports Lua 5.2. Following features may not work properly: -* label and goto -* tail-call optimisation in return -* only limited standard libraries -* proper handling of `__metatable` - -For the supported functions, please refer to luerl's [project page](https://github.com/rvirding/luerl). - -Lua scripts are stored in 'data/scripts' directory, and will be loaded automatically. If a script is changed during runtime, it should be reloaded to take effect. - -Each lua script could export several functions binding with emqx hooks, triggered by message publish, topic subscribe, client connect, etc. Different lua scripts may export same type function, binding with a same event. But their order being triggered is not guaranteed. - -To start this plugin, run following command: - -```shell -bin/emqx_ctl plugins load emqx_lua_hook -``` - - -## NOTE - -* Since lua VM is run on erlang VM, its performance is poor. Please do NOT write long or complicated lua scripts which may degrade entire system. -* It's hard to debug lua script in emqx environment. Recommended to unit test your lua script in your host first. If everything is OK, deploy it to emqx 'data/scripts' directory. -* Global variable will lost its value for each call. Do NOT use global variable in lua scripts. - - -# Example - -Suppose your emqx is installed in /emqx, and the lua script directory should be /emqx/data/scripts. - -Make a new file called "test.lua" and put following code into this file: - -```lua -function on_message_publish(clientid, username, topic, payload, qos, retain) - return topic, "hello", qos, retain -end - -function register_hook() - return "on_message_publish" -end -``` - -Execute following command to start emq-lua-hook and load scripts in 'data/scripts' directory. - -``` -/emqx/bin/emqx_ctl plugins load emqx_lua_hook -``` - -Now let's take a look at what will happend. - -- Start a mqtt client, such as mqtt.fx. -- Subscribe a topic="a/b". -- Send a message, topic="a/b", payload="123" -- Subscriber will get a message with topic="a/b" and payload="hello". test.lua modifies the payload. - -If there are "test1.lua", "test2.lua" and "test3.lua" in /emqx/data/scripts, all these files will be loaded once emq-lua-hook get started. - -If test2.lua has been changed, restart emq-lua-hook to reload all scripts, or execute following command to reload test2.lua only: - -``` -/emqx/bin/emqx_ctl luahook reload test2.lua -``` - - -# Hook API - -You can find all example codes in the `examples.lua` file. - -## on_client_connected - -```lua -function on_client_connected(clientId, userName, returncode) - return 0 -end -``` -This API is called after a mqtt client has establish a connection with broker. - -### Input -* clientid : a string, mqtt client id. -* username : a string mqtt username -* returncode : a string, has following values - - success : Connection accepted - - Others is failed reason - -### Output -Needless - -## on_client_disconnected - -```lua -function on_client_disconnected(clientId, username, error) - return -end -``` -This API is called after a mqtt client has disconnected. - -### Input -* clientId : a string, mqtt client id. -* username : a string mqtt username -* error : a string, denote the disconnection reason. - -### Output -Needless - -## on_client_subscribe - -```lua -function on_client_subscribe(clientId, username, topic) - -- do your job here - if some_condition then - return new_topic - else - return false - end -end -``` -This API is called before mqtt engine process client's subscribe command. It is possible to change topic or cancel it. - -### Input -* clientid : a string, mqtt client id. -* username : a string mqtt username -* topic : a string, mqtt message's topic - -### Output -* new_topic : a string, change mqtt message's topic -* false : cancel subscription - - -## on_client_unsubscribe - -```lua - function on_client_unsubscribe(clientId, username, topic) - -- do your job here - if some_condition then - return new_topic - else - return false - end -end -``` -This API is called before mqtt engine process client's unsubscribe command. It is possible to change topic or cancel it. - -### Input -* clientid : a string, mqtt client id. -* username : a string mqtt username -* topic : a string, mqtt message's topic - -### Output -* new_topic : a string, change mqtt message's topic -* false : cancel unsubscription - - -## on_session_subscribed - -```lua -function on_session_subscribed(ClientId, Username, Topic) - return -end -``` -This API is called after a subscription has been done. - -### Input -* clientid : a string, mqtt client id. -* username : a string mqtt username -* topic : a string, mqtt's topic filter. - -### Output -Needless - - -## on_session_unsubscribed - -```lua -function on_session_unsubscribed(clientid, username, topic) - return -end -``` -This API is called after a unsubscription has been done. - -### Input -* clientid : a string, mqtt client id. -* username : a string mqtt username -* topic : a string, mqtt's topic filter. - -### Output -Needless - -## on_message_delivered - -```lua -function on_message_delivered(clientid, username, topic, payload, qos, retain) - -- do your job here - return topic, payload, qos, retain -end -``` -This API is called after a message has been pushed to mqtt clients. - -### Input -* clientId : a string, mqtt client id. -* username : a string mqtt username -* topic : a string, mqtt message's topic -* payload : a string, mqtt message's payload -* qos : a number, mqtt message's QOS (0, 1, 2) -* retain : a boolean, mqtt message's retain flag - -### Output -Needless - -## on_message_acked - -```lua -function on_message_acked(clientId, username, topic, payload, qos, retain) - return -end -``` -This API is called after a message has been acknowledged. - -### Input -* clientId : a string, mqtt client id. -* username : a string mqtt username -* topic : a string, mqtt message's topic -* payload : a string, mqtt message's payload -* qos : a number, mqtt message's QOS (0, 1, 2) -* retain : a boolean, mqtt message's retain flag - -### Output -Needless - -## on_message_publish - -```lua -function on_message_publish(clientid, username, topic, payload, qos, retain) - -- do your job here - if some_condition then - return new_topic, new_payload, new_qos, new_retain - else - return false - end -end -``` -This API is called before publishing message into mqtt engine. It's possible to change message or cancel publish in this API. - -### Input -* clientid : a string, mqtt client id of publisher. -* username : a string, mqtt username of publisher -* topic : a string, mqtt message's topic -* payload : a string, mqtt message's payload -* qos : a number, mqtt message's QOS (0, 1, 2) -* retain : a boolean, mqtt message's retain flag - -### Output -* new_topic : a string, change mqtt message's topic -* new_payload : a string, change mqtt message's payload -* new_qos : a number, change mqtt message's QOS -* new_retain : a boolean, change mqtt message's retain flag -* false : cancel publishing this mqtt message - -## register_hook - -```lua -function register_hook() - return "hook_name" -end - --- Or register multiple callbacks - -function register_hook() - return "hook_name1", "hook_name2", ... , "hook_nameX" -end -``` - -This API exports hook(s) implemented in its lua script. - -### Output -* hook_name must be a string, which is equal to the hook API(s) implemented. Possible values: - - "on_client_connected" - - "on_client_disconnected" - - "on_client_subscribe" - - "on_client_unsubscribe" - - "on_session_subscribed" - - "on_session_unsubscribed" - - "on_message_delivered" - - "on_message_acked" - - "on_message_publish" - -# management command - -## load - -```shell -emqx_ctl luahook load script_name -``` -This command will load lua file "script_name" in 'data/scripts' directory, into emqx hook. - -## unload - -```shell -emqx_ctl luahook unload script_name -``` -This command will unload lua file "script_name" out of emqx hook. - -## reload - -```shell -emqx_ctl luahook reload script_name -``` -This command will reload lua file "script_name" in 'data/scripts'. It is useful if a lua script has been modified and apply it immediately. - -## enable - -```shell -emqx_ctl luahook enable script_name -``` -This command will rename lua file "script_name.x" to "script_name", and load it immediately. - -## disable - -```shell -emqx_ctl luahook disable script_name -``` -This command will unload this script, and rename lua file "script_name" to "script_name.x", which will not be loaded during next boot. - - -License -------- - -Apache License Version 2.0 - -Author ------- - -EMQ X Team. - diff --git a/apps/emqx_lua_hook/etc/emqx_lua_hook.conf b/apps/emqx_lua_hook/etc/emqx_lua_hook.conf deleted file mode 100644 index f0256afae..000000000 --- a/apps/emqx_lua_hook/etc/emqx_lua_hook.conf +++ /dev/null @@ -1,4 +0,0 @@ -##-------------------------------------------------------------------- -## EMQ X Lua Hook -##-------------------------------------------------------------------- - diff --git a/apps/emqx_lua_hook/examples.lua b/apps/emqx_lua_hook/examples.lua deleted file mode 100644 index bc36eb771..000000000 --- a/apps/emqx_lua_hook/examples.lua +++ /dev/null @@ -1,71 +0,0 @@ --- --- Given all funcation names needed register to system --- -function register_hook() - return "on_client_connected", - "on_client_disconnected", - "on_client_subscribe", - "on_client_unsubscribe", - "on_session_subscribed", - "on_session_unsubscribed", - "on_message_delivered", - "on_message_acked", - "on_message_publish" -end - ----------------------------------------------------------------------- --- Callback Functions - -function on_client_connected(clientid, username, returncode) - print("Lua: on_client_connected - " .. clientid) - -- do your job here - return -end - -function on_client_disconnected(clientid, username, reason) - print("Lua: on_client_disconnected - " .. clientid) - -- do your job here - return -end - -function on_client_subscribe(clientid, username, topic) - print("Lua: on_client_subscribe - " .. clientid) - -- do your job here - return topic -end - -function on_client_unsubscribe(clientid, username, topic) - print("Lua: on_client_unsubscribe - " .. clientid) - -- do your job here - return topic -end - -function on_session_subscribed(clientid, username, topic) - print("Lua: on_session_subscribed - " .. clientid) - -- do your job here - return -end - -function on_session_unsubscribed(clientid, username, topic) - print("Lua: on_session_unsubscribed - " .. clientid) - -- do your job here - return -end - -function on_message_delivered(clientid, username, topic, payload, qos, retain) - print("Lua: on_message_delivered - " .. clientid) - -- do your job here - return topic, payload, qos, retain -end - -function on_message_acked(clientid, username, topic, payload, qos, retain) - print("Lua: on_message_acked- " .. clientid) - -- do your job here - return -end - -function on_message_publish(clientid, username, topic, payload, qos, retain) - print("Lua: on_message_publish - " .. clientid) - -- do your job here - return topic, payload, qos, retain -end diff --git a/apps/emqx_lua_hook/priv/emqx_lua_hook.schema b/apps/emqx_lua_hook/priv/emqx_lua_hook.schema deleted file mode 100644 index 8b1378917..000000000 --- a/apps/emqx_lua_hook/priv/emqx_lua_hook.schema +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/emqx_lua_hook/rebar.config b/apps/emqx_lua_hook/rebar.config deleted file mode 100644 index 97a06a77c..000000000 --- a/apps/emqx_lua_hook/rebar.config +++ /dev/null @@ -1,21 +0,0 @@ -{deps, - [{luerl, {git, "https://github.com/emqx/luerl", {tag, "v0.3.1"}}} - ]}. - -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - compressed, - {parse_transform} - ]}. -{overrides, [{add, [{erl_opts, [compressed]}]}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. diff --git a/apps/emqx_lua_hook/src/emqx_lua_hook.app.src b/apps/emqx_lua_hook/src/emqx_lua_hook.app.src deleted file mode 100644 index 627c8e29d..000000000 --- a/apps/emqx_lua_hook/src/emqx_lua_hook.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_lua_hook, - [{description, "EMQ X Lua Hooks"}, - {vsn, "4.3.0"}, % strict semver, bump manually! - {modules, []}, - {registered, []}, - {applications, [kernel,stdlib]}, - {mod, {emqx_lua_hook_app,[]}}, - {env,[]}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-lua-hook"} - ]} - ]}. diff --git a/apps/emqx_lua_hook/src/emqx_lua_hook.erl b/apps/emqx_lua_hook/src/emqx_lua_hook.erl deleted file mode 100644 index 6e7810827..000000000 --- a/apps/emqx_lua_hook/src/emqx_lua_hook.erl +++ /dev/null @@ -1,199 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lua_hook). - --behaviour(gen_server). - --include("emqx_lua_hook.hrl"). --include_lib("luerl/src/luerl.hrl"). - --export([ start_link/0 - , stop/0 - ]). - --export([ load_scripts/0 - , unload_scripts/0 - , load_script/1 - , unload_script/1 - ]). - --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --export([lua_dir/0]). - --define(SERVER, ?MODULE). - --record(state, {loaded_scripts = []}). - -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, {}, []). - -stop() -> - gen_server:call(?SERVER, stop). - -load_scripts() -> - gen_server:call(?SERVER, load_scripts). - -unload_scripts() -> - gen_server:call(?SERVER, unload_scrips). - -load_script(ScriptName) -> - gen_server:call(?SERVER, {load_script, ScriptName}). - -unload_script(ScriptName) -> - gen_server:call(?SERVER, {unload_script, ScriptName}). - -lua_dir() -> - filename:join([emqx:get_env(data_dir, "data"), "scripts"]). - -%%----------------------------------------------------------------------------- -%% gen_server callbacks -%%----------------------------------------------------------------------------- - -init({}) -> - {ok, #state{}}. - -handle_call(stop, _From, State) -> - {stop, normal, ok, State}; - -handle_call(load_scripts, _From, State) -> - {reply, ok, State#state{loaded_scripts = do_loadall()}, hibernate}; - -handle_call(unload_scrips, _From, State=#state{loaded_scripts = Scripts}) -> - do_unloadall(Scripts), - {reply, ok, State#state{loaded_scripts = []}, hibernate}; - -handle_call({load_script, ScriptName}, _From, State=#state{loaded_scripts = Scripts}) -> - {Ret, NewScripts} = case do_load(ScriptName) of - error -> {error, Scripts}; - {ScriptName, LuaState} -> - case lists:member({ScriptName, LuaState}, Scripts) of - true -> {ok, Scripts}; - false -> {ok, lists:append([{ScriptName, LuaState}], Scripts)} - end - end, - {reply, Ret, State#state{loaded_scripts = NewScripts}, hibernate}; - -handle_call({unload_script, ScriptName}, _From, State=#state{loaded_scripts = Scripts}) -> - case proplists:get_all_values(ScriptName, Scripts) of - [] -> - {reply, ok, State, hibernate}; - LuaStates -> - lists:foreach(fun(LuaState) -> - % Unload first! If this gen_server has been crashed, loaded_scripts will be empty - do_unload({ScriptName, LuaState}) - end, LuaStates), - NewScripts = proplists:delete(ScriptName, Scripts), - {reply, ok, State#state{loaded_scripts = NewScripts}, hibernate} - end; - -handle_call(Request, From, State) -> - ?LOG(error, "Unknown Request=~p from ~p", [Request, From]), - {reply, ignored, State, hibernate}. - -handle_cast(Msg, State) -> - ?LOG(error, "unexpected cast: ~p", [Msg]), - {noreply, State, hibernate}. - -handle_info(Info, State) -> - ?LOG(error, "unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, #state{loaded_scripts = Scripts}) -> - do_unloadall(Scripts), - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%% ------------------------------------------------------------------ -%% Internal Function Definitions -%% ------------------------------------------------------------------ - -do_loadall() -> - FileList = filelib:wildcard(filename:join([lua_dir(), "*.lua"])), - List = [do_load(X) || X <- FileList], - [X || X <- List, is_tuple(X)]. - -do_load(FileName) -> - case catch luerl:dofile(FileName) of - {'EXIT', St00} -> - ?LOG(error, "Failed to load lua script ~p due to error ~p", [FileName, St00]), - error; - {_Ret, St0=#luerl{}} -> - case catch luerl:call_function([register_hook], [], St0) of - {'EXIT', St1} -> - ?LOG(error, "Failed to execute register_hook function in lua script ~p, which has syntax error, St1=~p", [FileName, St1]), - error; - {Ret1, St1} -> - ?LOG(debug, "Register lua script ~p", [FileName]), - _ = do_register_hooks(Ret1, FileName, St1), - {FileName, St1}; - Other -> - ?LOG(error, "Failed to load lua script ~p, register_hook() raise exception ~p", [FileName, Other]), - error - end; - Exception -> - ?LOG(error, "Failed to load lua script ~p with error ~p", [FileName, Exception]), - error - end. - -do_register(<<"on_message_publish">>, ScriptName, St) -> - emqx_lua_script:register_on_message_publish(ScriptName, St); -do_register(<<"on_message_delivered">>, ScriptName, St) -> - emqx_lua_script:register_on_message_delivered(ScriptName, St); -do_register(<<"on_message_acked">>, ScriptName, St) -> - emqx_lua_script:register_on_message_acked(ScriptName, St); -do_register(<<"on_client_connected">>, ScriptName, St) -> - emqx_lua_script:register_on_client_connected(ScriptName, St); -do_register(<<"on_client_subscribe">>, ScriptName, St) -> - emqx_lua_script:register_on_client_subscribe(ScriptName, St); -do_register(<<"on_client_unsubscribe">>, ScriptName, St) -> - emqx_lua_script:register_on_client_unsubscribe(ScriptName, St); -do_register(<<"on_client_disconnected">>, ScriptName, St) -> - emqx_lua_script:register_on_client_disconnected(ScriptName, St); -do_register(<<"on_session_subscribed">>, ScriptName, St) -> - emqx_lua_script:register_on_session_subscribed(ScriptName, St); -do_register(<<"on_client_authenticate">>, ScriptName, St) -> - emqx_lua_script:register_on_client_authenticate(ScriptName, St); -do_register(<<"on_client_check_acl">>, ScriptName, St) -> - emqx_lua_script:register_on_client_check_acl(ScriptName, St); -do_register(Hook, ScriptName, _St) -> - ?LOG(error, "Discard unknown hook ~p ScriptName=~p", [Hook, ScriptName]). - -do_register_hooks([], _ScriptName, _St) -> - ok; -do_register_hooks([H|T], ScriptName, St) -> - _ = do_register(H, ScriptName, St), - do_register_hooks(T, ScriptName, St); -do_register_hooks(Hook = <<$o, $n, _Rest/binary>>, ScriptName, St) -> - do_register(Hook, ScriptName, St); -do_register_hooks(Hook, ScriptName, _St) -> - ?LOG(error, "Discard unknown hook type ~p from ~p", [Hook, ScriptName]). - -do_unloadall(Scripts) -> - lists:foreach(fun do_unload/1, Scripts). - -do_unload(Script) -> - emqx_lua_script:unregister_hooks(Script), - ok. diff --git a/apps/emqx_lua_hook/src/emqx_lua_hook_app.erl b/apps/emqx_lua_hook/src/emqx_lua_hook_app.erl deleted file mode 100644 index 6b0ec3574..000000000 --- a/apps/emqx_lua_hook/src/emqx_lua_hook_app.erl +++ /dev/null @@ -1,40 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lua_hook_app). - --behaviour(application). - --emqx_plugin(?MODULE). - --export([ start/2 - , stop/1 - , prep_stop/1 - ]). - -start(_Type, _Args) -> - {ok, Sup} = emqx_lua_hook_sup:start_link(), - emqx_lua_hook:load_scripts(), - emqx_lua_hook_cli:load(), - {ok, Sup}. - -prep_stop(State) -> - emqx_lua_hook:unload_scripts(), - emqx_lua_hook_cli:unload(), - State. - -stop(_State) -> - ok. diff --git a/apps/emqx_lua_hook/src/emqx_lua_hook_cli.erl b/apps/emqx_lua_hook/src/emqx_lua_hook_cli.erl deleted file mode 100644 index 83c6fc5ef..000000000 --- a/apps/emqx_lua_hook/src/emqx_lua_hook_cli.erl +++ /dev/null @@ -1,88 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lua_hook_cli). - --export([ load/0 - , cmd/1 - , unload/0 - ]). - --include("emqx_lua_hook.hrl"). --include_lib("luerl/src/luerl.hrl"). - --define(PRINT(Format, Args), io:format(Format, Args)). --define(PRINT_CMD(Cmd, Descr), io:format("~-48s# ~s~n", [Cmd, Descr])). --define(USAGE(CmdList), [?PRINT_CMD(Cmd, Descr) || {Cmd, Descr} <- CmdList]). - -load() -> - emqx_ctl:register_command(luahook, {?MODULE, cmd}, []). - -unload() -> - emqx_ctl:unregister_command(luahook). - -cmd(["load", Script]) -> - case emqx_lua_hook:load_script(fullname(Script)) of - ok -> emqx_ctl:print("Load ~p successfully~n", [Script]); - error -> emqx_ctl:print("Load ~p error~n", [Script]) - end; - -cmd(["reload", Script]) -> - FullName = fullname(Script), - emqx_lua_hook:unload_script(FullName), - case emqx_lua_hook:load_script(FullName) of - ok -> emqx_ctl:print("Reload ~p successfully~n", [Script]); - error -> emqx_ctl:print("Reload ~p error~n", [Script]) - end; - -cmd(["unload", Script]) -> - emqx_lua_hook:unload_script(fullname(Script)), - emqx_ctl:print("Unload ~p successfully~n", [Script]); - -cmd(["enable", Script]) -> - FullName = fullname(Script), - case file:rename(fullnamedisable(Script), FullName) of - ok -> case emqx_lua_hook:load_script(FullName) of - ok -> - emqx_ctl:print("Enable ~p successfully~n", [Script]); - error -> - emqx_ctl:print("Fail to enable ~p~n", [Script]) - end; - {error, Reason} -> - emqx_ctl:print("Fail to enable ~p due to ~p~n", [Script, Reason]) - end; - -cmd(["disable", Script]) -> - FullName = fullname(Script), - emqx_lua_hook:unload_script(FullName), - case file:rename(FullName, fullnamedisable(Script)) of - ok -> - emqx_ctl:print("Disable ~p successfully~n", [Script]); - {error, Reason} -> - emqx_ctl:print("Fail to disable ~p due to ~p~n", [Script, Reason]) - end; - -cmd(_) -> - emqx_ctl:usage([{"luahook load