diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 602ec96d..ad4d990b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,12 +21,18 @@ jobs: services: rabbitmq: image: rabbitmq:3.13-rc-management - options: --hostname test-node + options: --hostname test-node --name test-node env: RABBITMQ_DEFAULT_USER: "test-user" RABBITMQ_DEFAULT_PASS: "test-password" + volumes: + # these directories will be empty until checkout, but they will be + # populated by the time we restart the service + - ${{ github.workspace }}/conf:/etc/rabbitmq + - ${{ github.workspace }}/certs:/certs ports: - 5552:5552 + - 5551:5551 - 5672:5672 - 15672:15672 - 1883:1883 @@ -41,17 +47,32 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: "npm" - - name: Enable RabbitMQ Plugins - run: docker exec $(docker ps --filter ancestor=rabbitmq:3.13-rc-management -q) rabbitmq-plugins enable rabbitmq_stream rabbitmq_stream_management + - name: Generate certificates + env: + CN: test-node + run: | + git clone https://github.com/rabbitmq/tls-gen tls-gen + cd tls-gen/basic + make + cd ../.. + cp -a tls-gen/basic/result certs/ + sudo chown -R 999:999 certs + sudo mv certs/server_test-node_certificate.pem certs/server_rabbitmq_certificate.pem + sudo mv certs/server_test-node_key.pem certs/server_rabbitmq_key.pem - name: Restart RabbitMQ - run: docker restart $(docker ps --filter ancestor=rabbitmq:3.13-rc-management -q) - - name: Wait for rabbit instance restart - run: sleep 10 + run: | + docker restart test-node + sleep 2 + docker exec test-node rabbitmqctl await_startup - name: Create SuperStream - run: docker exec $(docker ps --filter ancestor=rabbitmq:3.13-rc-management -q) rabbitmq-streams add_super_stream super-stream-test --partitions 2 + run: docker exec test-node rabbitmq-streams add_super_stream super-stream-test --partitions 2 - run: npm ci - run: npm run check - run: npm run build --if-present + - run: | + docker exec test-node rabbitmqctl add_user 'O=client,CN=test-node' '' + docker exec test-node rabbitmqctl clear_password 'O=client,CN=test-node' + docker exec test-node rabbitmqctl set_permissions 'O=client,CN=test-node' '.*' '.*' '.*' - run: npm test env: RABBITMQ_USER: "test-user" diff --git a/.gitignore b/.gitignore index 45c37b23..e9f34dd1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist/ node_modules/ performance_test/node_modules .envrc +tls-gen/ diff --git a/Makefile b/Makefile index 0af87249..3fc32311 100644 --- a/Makefile +++ b/Makefile @@ -6,4 +6,16 @@ rabbitmq-cluster: cd cluster; docker build -t haproxy-rabbitmq-cluster . cd cluster; chmod 755 -R tls-gen cd cluster; docker compose down - cd cluster; docker compose up -d \ No newline at end of file + cd cluster; docker compose up -d + +rabbitmq-test: + rm -rf tls-gen; + git clone https://github.com/rabbitmq/tls-gen tls-gen; cd tls-gen/basic; CN=rabbitmq make + chmod 755 -R tls-gen + docker compose down + docker compose up -d + sleep 5 + docker exec rabbitmq-stream rabbitmqctl await_startup + docker exec rabbitmq-stream rabbitmqctl add_user 'O=client,CN=rabbitmq' '' + docker exec rabbitmq-stream rabbitmqctl clear_password 'O=client,CN=rabbitmq' + docker exec rabbitmq-stream rabbitmqctl set_permissions 'O=client,CN=rabbitmq' '.*' '.*' '.*' diff --git a/README.md b/README.md index ec13b7ca..167df73f 100644 --- a/README.md +++ b/README.md @@ -374,7 +374,7 @@ npm i run the docker-compose to launch a rabbit instance already stream enabled ```shell -docker-compose up -d +make rabbitmq-test ``` add this line to your host file (on linux `/etc/hosts`) to correctly resolve rabbitmq @@ -400,8 +400,8 @@ npm run build Test: ```shell -docker-compose up -d -npm run test +make rabbitmq-test +RABBIT_MQ_TEST_NODES=rabbitmq:5552 npm run test ``` Check everything: diff --git a/conf/enabled_plugins b/conf/enabled_plugins index 6ba3f5fe..3c2cc22b 100644 --- a/conf/enabled_plugins +++ b/conf/enabled_plugins @@ -1 +1 @@ -[rabbitmq_management,rabbitmq_prometheus,rabbitmq_stream_management]. +[rabbitmq_management,rabbitmq_prometheus,rabbitmq_stream_management,rabbitmq_auth_mechanism_ssl]. diff --git a/conf/rabbitmq.conf b/conf/rabbitmq.conf new file mode 100755 index 00000000..3b510140 --- /dev/null +++ b/conf/rabbitmq.conf @@ -0,0 +1,15 @@ +loopback_users.guest = false + +ssl_options.cacertfile = /certs/ca_certificate.pem +ssl_options.certfile = /certs/server_rabbitmq_certificate.pem +ssl_options.keyfile = /certs/server_rabbitmq_key.pem +listeners.ssl.default = 5671 +listeners.tcp.default = 5672 +stream.listeners.tcp.default = 5552 +stream.listeners.ssl.default = 5551 +auth_mechanisms.1 = PLAIN +auth_mechanisms.2 = EXTERNAL +ssl_options.verify = verify_peer +ssl_options.fail_if_no_peer_cert = false +log.file.level = debug +log.console = true diff --git a/docker-compose.yaml b/docker-compose.yaml index 0ac0dbae..c0f2ece1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: "2" - services: rabbitmq-stream: image: rabbitmq:3.13-rc-management @@ -8,10 +6,13 @@ services: hostname: "rabbitmq" ports: - "15672:15672" + - "5671:5671" - "5672:5672" + - "5551:5551" - "5552:5552" environment: RABBITMQ_DEFAULT_USER: "rabbit" RABBITMQ_DEFAULT_PASS: "rabbit" volumes: - - ./conf/enabled_plugins:/etc/rabbitmq/enabled_plugins + - ./conf/:/etc/rabbitmq/ + - "./tls-gen/basic/result/:/certs" diff --git a/src/client.ts b/src/client.ts index 6e582f07..89493fe8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -707,6 +707,7 @@ export interface ClientParams { port: number username: string password: string + mechanism?: "PLAIN" | "EXTERNAL" vhost: string frameMax?: number heartbeat?: number diff --git a/src/connection.ts b/src/connection.ts index a1a838cf..19180f46 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -146,7 +146,11 @@ export class Connection { this.logger.info(`Connected to RabbitMQ ${this.params.hostname}:${this.params.port}`) this.peerProperties = (await this.exchangeProperties()).properties this.filteringEnabled = lt(coerce(this.rabbitManagementVersion)!, REQUIRED_MANAGEMENT_VERSION) ? false : true - await this.auth({ username: this.params.username, password: this.params.password }) + await this.auth({ + username: this.params.username, + password: this.params.password, + mechanism: this.params.mechanism ?? "PLAIN", + }) const { heartbeat } = await this.tune(this.params.heartbeat ?? 0) await this.open({ virtualHost: this.params.vhost }) if (!this.heartbeat.started) this.heartbeat.start(heartbeat) @@ -436,19 +440,17 @@ export class Connection { return this.setupCompleted } - private async auth(params: { username: string; password: string }) { + private async auth(params: { username: string; password: string; mechanism: string }) { this.logger.debug(`Start authentication process ...`) this.logger.debug(`Start SASL handshake ...`) const handshakeResponse = await this.sendAndWait(new SaslHandshakeRequest()) this.logger.debug(`Mechanisms: ${handshakeResponse.mechanisms}`) - if (!handshakeResponse.mechanisms.find((m) => m === "PLAIN")) { - throw new Error(`Unable to find PLAIN mechanism in ${handshakeResponse.mechanisms}`) + if (!handshakeResponse.mechanisms.find((m) => m === params.mechanism)) { + throw new Error(`Unable to find ${params.mechanism} mechanism in ${handshakeResponse.mechanisms}`) } - this.logger.debug(`Start SASL PLAIN authentication ...`) - const authResponse = await this.sendAndWait( - new SaslAuthenticateRequest({ ...params, mechanism: "PLAIN" }) - ) + this.logger.debug(`Start SASL ${params.mechanism} authentication ...`) + const authResponse = await this.sendAndWait(new SaslAuthenticateRequest(params)) this.logger.debug(`Authentication: ${authResponse.ok} - '${authResponse.data}'`) if (!authResponse.ok) { throw new Error(`Unable Authenticate -> ${authResponse.code}`) diff --git a/src/requests/sasl_authenticate_request.ts b/src/requests/sasl_authenticate_request.ts index 96a0f910..c882d8aa 100644 --- a/src/requests/sasl_authenticate_request.ts +++ b/src/requests/sasl_authenticate_request.ts @@ -2,6 +2,10 @@ import { SaslAuthenticateResponse } from "../responses/sasl_authenticate_respons import { AbstractRequest } from "./abstract_request" import { DataWriter } from "./data_writer" +function assertUnreachable(mechanism: string): never { + throw new Error(`Auth mechanism '${mechanism}' not implemented`) +} + export class SaslAuthenticateRequest extends AbstractRequest { readonly responseKey = SaslAuthenticateResponse.key static readonly Key = 0x0013 @@ -14,10 +18,19 @@ export class SaslAuthenticateRequest extends AbstractRequest { protected writeContent(writer: DataWriter): void { writer.writeString(this.params.mechanism) - writer.writeUInt32(this.params.password.length + this.params.username.length + 2) - writer.writeUInt8(0) - writer.writeData(this.params.username) - writer.writeUInt8(0) - writer.writeData(this.params.password) + switch (this.params.mechanism) { + case "PLAIN": + writer.writeUInt32(this.params.password.length + this.params.username.length + 2) + writer.writeUInt8(0) + writer.writeData(this.params.username) + writer.writeUInt8(0) + writer.writeData(this.params.password) + break + case "EXTERNAL": + writer.writeUInt32(0) + break + default: + assertUnreachable(this.params.mechanism) + } } } diff --git a/test/e2e/connect.test.ts b/test/e2e/connect.test.ts index 4dd55999..14232ef7 100644 --- a/test/e2e/connect.test.ts +++ b/test/e2e/connect.test.ts @@ -1,10 +1,28 @@ import { expect } from "chai" -import { Client } from "../../src" +import { Client, connect } from "../../src" import { createClient } from "../support/fake_data" import { Rabbit } from "../support/rabbit" -import { eventually, username, password } from "../support/util" +import { eventually, username, password, getTestNodesFromEnv } from "../support/util" import { Version } from "../../src/versions" import { randomUUID } from "node:crypto" +import { readFile } from "node:fs/promises" + +async function createTlsClient(): Promise { + const [firstNode] = getTestNodesFromEnv() + return connect({ + hostname: firstNode.host, + port: 5551, + mechanism: "EXTERNAL", + ssl: { + ca: await readFile("./tls-gen/basic/result/ca_certificate.pem", "utf8"), + cert: await readFile(`./tls-gen/basic/result/client_${firstNode.host}_certificate.pem`, "utf8"), + key: await readFile(`./tls-gen/basic/result/client_${firstNode.host}_key.pem`, "utf8"), + }, + username: "", + password: "", + vhost: "/", + }) +} describe("connect", () => { let client: Client @@ -28,6 +46,14 @@ describe("connect", () => { }, 5000) }).timeout(10000) + it("using EXTERNAL auth", async () => { + client = await createTlsClient() + + await eventually(async () => { + expect(await rabbit.getConnections()).lengthOf(1) + }, 5000) + }).timeout(10000) + it("declaring connection name", async () => { const connectionName = `connection-name-${randomUUID()}` client = await createClient(username, password, undefined, undefined, undefined, undefined, connectionName)