From 28f9935758a312fbdf842a99df4a273ccdb6eb28 Mon Sep 17 00:00:00 2001 From: Drew McGreevy Date: Thu, 6 Jan 2022 13:11:46 +0000 Subject: [PATCH 1/9] Fix typo for iterator parameter (#124) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b2b8f6..38f9802 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ await client.useConnection(async (conn) => { const { iterator: users } = await conn.execute( `select * from users`, /* params: */ [], - /* iterator: */ false, + /* iterator: */ true, ); for await (const user of users) { console.log(user); From 1622fe5b971fed45c8df0af942d22c801d75446c Mon Sep 17 00:00:00 2001 From: Mikhail Stefantsev Date: Thu, 27 Oct 2022 19:12:44 +0300 Subject: [PATCH 2/9] Migrate RSA to SubtleCrypto (#132) * migrated rsa to subtle crypto * moved to denoland/setup-deno --- .github/workflows/ci.yml | 4 +-- src/auth_plugin/caching_sha2_password.ts | 33 +++++++++++++++--------- src/auth_plugin/crypt.ts | 23 ++++++++++++++--- src/connection.ts | 2 +- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b74a30..4362ed2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Install Deno 1.x - uses: denolib/setup-deno@master + uses: denoland/setup-deno@v1 with: deno-version: v1.x - name: Check fmt @@ -37,7 +37,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Install Deno ${{ matrix.DENO_VERSION }} - uses: denolib/setup-deno@master + uses: denoland/setup-deno@v1 with: deno-version: ${{ matrix.DENO_VERSION }} - name: Show Deno version diff --git a/src/auth_plugin/caching_sha2_password.ts b/src/auth_plugin/caching_sha2_password.ts index c3ed0df..66d0e1c 100644 --- a/src/auth_plugin/caching_sha2_password.ts +++ b/src/auth_plugin/caching_sha2_password.ts @@ -10,12 +10,17 @@ interface handler { } let scramble: Uint8Array, password: string; -function start(scramble_: Uint8Array, password_: string): handler { + +async function start( + scramble_: Uint8Array, + password_: string, +): Promise { scramble = scramble_; password = password_; - return { done: false, next: authMoreResponse }; + return { done: false, next: await authMoreResponse }; } -function authMoreResponse(packet: ReceivePacket): handler { + +async function authMoreResponse(packet: ReceivePacket): Promise { const enum AuthStatusFlags { FullAuth = 0x04, FastPath = 0x03, @@ -26,7 +31,7 @@ function authMoreResponse(packet: ReceivePacket): handler { if (statusFlag === AuthStatusFlags.FullAuth) { authMoreData = new Uint8Array([REQUEST_PUBLIC_KEY]); done = false; - next = encryptWithKey; + next = await encryptWithKey; } if (statusFlag === AuthStatusFlags.FastPath) { done = false; @@ -36,30 +41,34 @@ function authMoreResponse(packet: ReceivePacket): handler { return { done, next, quickRead, data: authMoreData }; } -function encryptWithKey(packet: ReceivePacket): handler { +async function encryptWithKey(packet: ReceivePacket): Promise { const publicKey = parsePublicKey(packet); const len = password.length; - let passwordBuffer: Uint8Array = new Uint8Array(len + 1); + const passwordBuffer: Uint8Array = new Uint8Array(len + 1); for (let n = 0; n < len; n++) { passwordBuffer[n] = password.charCodeAt(n); } passwordBuffer[len] = 0x00; - const encryptedPassword = encrypt(passwordBuffer, scramble, publicKey); - return { done: false, next: terminate, data: encryptedPassword }; + const encryptedPassword = await encrypt(passwordBuffer, scramble, publicKey); + return { + done: false, + next: terminate, + data: new Uint8Array(encryptedPassword), + }; } function parsePublicKey(packet: ReceivePacket): string { return packet.body.skip(1).readNullTerminatedString(); } -function encrypt( + +async function encrypt( password: Uint8Array, scramble: Uint8Array, key: string, -): Uint8Array { +): Promise { const stage1 = xor(password, scramble); - const encrypted = encryptWithPublicKey(key, stage1); - return encrypted; + return await encryptWithPublicKey(key, stage1); } function terminate() { diff --git a/src/auth_plugin/crypt.ts b/src/auth_plugin/crypt.ts index 6e12394..258f1fc 100644 --- a/src/auth_plugin/crypt.ts +++ b/src/auth_plugin/crypt.ts @@ -1,7 +1,22 @@ -import { RSA } from "https://deno.land/x/god_crypto@v0.2.0/mod.ts"; -function encryptWithPublicKey(key: string, data: Uint8Array): Uint8Array { - const publicKey = RSA.parseKey(key); - return RSA.encrypt(data, publicKey); +async function encryptWithPublicKey( + key: string, + data: Uint8Array, +): Promise { + const importedKey = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(key), + { name: "RSA-OAEP", hash: "SHA-256" }, + false, + ["encrypt"], + ); + + return await crypto.subtle.encrypt( + { + name: "RSA-OAEP", + }, + importedKey, + data, + ); } export { encryptWithPublicKey }; diff --git a/src/connection.ts b/src/connection.ts index 015ffdb..3f09c71 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -106,7 +106,7 @@ export class Connection { let result; if (handler) { - result = handler.start(handshakePacket.seed, password!); + result = await handler.start(handshakePacket.seed, password!); while (!result.done) { if (result.data) { const sequenceNumber = receive.header.no + 1; From c3ac5ba2d6a2917f2bed6fb92e7af8aad2810073 Mon Sep 17 00:00:00 2001 From: shi yuhang <52435083+shiyuhang0@users.noreply.github.com> Date: Fri, 25 Nov 2022 22:28:50 +0800 Subject: [PATCH 3/9] Support Authentication Method Mismatch (#137) * Support switch auth method * fmt * ignore latest mariadb --- .github/workflows/ci.yml | 2 +- src/auth.ts | 3 ++- src/buffer.ts | 4 ++++ src/connection.ts | 34 +++++++++++++++++++++++++++++-- src/packets/parsers/authswitch.ts | 21 +++++++++++++++++++ 5 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/packets/parsers/authswitch.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4362ed2..c8eb765 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - mariadb:10.2 - mariadb:10.3 - mariadb:10.4 - - mariadb:latest +# - mariadb:latest steps: - uses: actions/checkout@v1 diff --git a/src/auth.ts b/src/auth.ts index 0342e1d..deafa1d 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -32,7 +32,8 @@ export default function auth( ) { switch (authPluginName) { case "mysql_native_password": - return mysqlNativePassword(password, seed); + // Native password authentication only need and will need 20-byte challenge. + return mysqlNativePassword(password, seed.slice(0, 20)); case "caching_sha2_password": return cachingSha2Password(password, seed); diff --git a/src/buffer.ts b/src/buffer.ts index f978155..5c3e48b 100644 --- a/src/buffer.ts +++ b/src/buffer.ts @@ -63,6 +63,10 @@ export class BufferReader { return decode(buf); } + readRestOfPacketString(): Uint8Array { + return this.buffer.slice(this.pos); + } + readString(len: number): string { const str = decode(this.buffer.slice(this.pos, this.pos + len)); this.pos += len; diff --git a/src/connection.ts b/src/connection.ts index 3f09c71..c0fc50a 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -18,6 +18,8 @@ import { import { FieldInfo, parseField, parseRow } from "./packets/parsers/result.ts"; import { PacketType } from "./constant/packet.ts"; import authPlugin from "./auth_plugin/index.ts"; +import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; +import auth from "./auth.ts"; /** * Connection state @@ -100,8 +102,36 @@ export class Connection { handler = adaptedPlugin; break; case AuthResult.MethodMismatch: - // TODO: Negotiate - throw new Error("Currently cannot support auth method mismatch!"); + const authSwitch = parseAuthSwitch(receive.body); + // If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is + // sent and we have to keep using the cipher sent in the init packet. + if ( + authSwitch.authPluginData === undefined || + authSwitch.authPluginData.length === 0 + ) { + authSwitch.authPluginData = handshakePacket.seed; + } + + let authData; + if (password) { + authData = auth( + authSwitch.authPluginName, + password, + authSwitch.authPluginData, + ); + } else { + authData = Uint8Array.from([]); + } + + await new SendPacket(authData, receive.header.no + 1).send(this.conn); + + receive = await this.nextPacket(); + const authSwitch2 = parseAuthSwitch(receive.body); + if (authSwitch2.authPluginName !== "") { + throw new Error( + "Do not allow to change the auth plugin more than once!", + ); + } } let result; diff --git a/src/packets/parsers/authswitch.ts b/src/packets/parsers/authswitch.ts new file mode 100644 index 0000000..ac8b728 --- /dev/null +++ b/src/packets/parsers/authswitch.ts @@ -0,0 +1,21 @@ +import { BufferReader } from "../../buffer.ts"; + +/** @ignore */ +export interface authSwitchBody { + status: number; + authPluginName: string; + authPluginData: Uint8Array; +} + +/** @ignore */ +export function parseAuthSwitch(reader: BufferReader): authSwitchBody { + const status = reader.readUint8(); + const authPluginName = reader.readNullTerminatedString(); + const authPluginData = reader.readRestOfPacketString(); + + return { + status, + authPluginName, + authPluginData, + }; +} From 4ccecfc4e6b11c78bf6ce03f36e63cef431c04e3 Mon Sep 17 00:00:00 2001 From: lideming Date: Fri, 25 Nov 2022 23:01:43 +0800 Subject: [PATCH 4/9] Handle EOF_Packet and CLIENT_DEPRECATE_EOF correctly (#139) * aware CLIENT_DEPRECATE_EOF * re-enable ci for mariadb:latest --- .github/workflows/ci.yml | 2 +- src/connection.ts | 33 +++------------------------------ 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8eb765..4362ed2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - mariadb:10.2 - mariadb:10.3 - mariadb:10.4 -# - mariadb:latest + - mariadb:latest steps: - uses: actions/checkout@v1 diff --git a/src/connection.ts b/src/connection.ts index c0fc50a..a442b46 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -20,6 +20,7 @@ import { PacketType } from "./constant/packet.ts"; import authPlugin from "./auth_plugin/index.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./auth.ts"; +import ServerCapabilities from "./constant/capabilities.ts"; /** * Connection state @@ -223,34 +224,6 @@ export class Connection { this.close(); }; - /** - * Check if database server version is less than 5.7.0 - * - * MySQL version is "x.y.z" - * eg "5.5.62" - * - * MariaDB version is "5.5.5-x.y.z-MariaDB[-build-infos]" for versions after 5 (10.0 etc) - * eg "5.5.5-10.4.10-MariaDB-1:10.4.10+maria~bionic" - * and "x.y.z-MariaDB-[build-infos]" for 5.x versions - * eg "5.5.64-MariaDB-1~trusty" - */ - private lessThan5_7(): Boolean { - const version = this.serverVersion; - if (!version.includes("MariaDB")) return version < "5.7.0"; - const segments = version.split("-"); - // MariaDB v5.x - if (segments[1] === "MariaDB") return segments[0] < "5.7.0"; - // MariaDB v10+ - return false; - } - - /** Check if the MariaDB version is 10.0 or 10.1 */ - private isMariaDBAndVersion10_0Or10_1(): Boolean { - const version = this.serverVersion; - if (!version.includes("MariaDB")) return false; - return version.includes("5.5.5-10.1") || version.includes("5.5.5-10.0"); - } - /** Close database connection */ close(): void { if (this.state != ConnectionState.CLOSED) { @@ -316,8 +289,8 @@ export class Connection { } const rows = []; - if (this.lessThan5_7() || this.isMariaDBAndVersion10_0Or10_1()) { - // EOF(less than 5.7 or mariadb version is 10.0 or 10.1) + if (!(this.capabilities & ServerCapabilities.CLIENT_DEPRECATE_EOF)) { + // EOF(mysql < 5.7 or mariadb < 10.2) receive = await this.nextPacket(); if (receive.type !== PacketType.EOF_Packet) { throw new ProtocolError(); From e3359d0465b5d59e6c11c5629dcea54456b4b7b8 Mon Sep 17 00:00:00 2001 From: shi yuhang <52435083+shiyuhang0@users.noreply.github.com> Date: Sat, 26 Nov 2022 20:30:18 +0800 Subject: [PATCH 5/9] Enhancement CLIENT_DEPRECATE_EOF (#140) * enhancement eof deprecate * fmt --- src/connection.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/connection.ts b/src/connection.ts index a442b46..c79cf26 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -300,7 +300,11 @@ export class Connection { if (!iterator) { while (true) { receive = await this.nextPacket(); - if (receive.type === PacketType.EOF_Packet) { + // OK_Packet when CLIENT_DEPRECATE_EOF is set. OK_Packet can be 0xfe or 0x00 + if ( + receive.type === PacketType.EOF_Packet || + receive.type === PacketType.OK_Packet + ) { break; } else { const row = parseRow(receive.body, fields); From 14e40364f84d6f73ccf98324eabd0f646b949223 Mon Sep 17 00:00:00 2001 From: lideming Date: Sat, 26 Nov 2022 22:53:47 +0800 Subject: [PATCH 6/9] test auth method mismatch with mysql_native_password (#141) --- test.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test.ts b/test.ts index 71a7189..a41ccb6 100644 --- a/test.ts +++ b/test.ts @@ -306,6 +306,37 @@ testWithClient(async function testExecuteIterator(client) { }); }); +// For MySQL 8, the default auth plugin is `caching_sha2_password`. Create user +// using `mysql_native_password` to test Authentication Method Mismatch. +testWithClient(async function testCreateUserWithMysqlNativePassword(client) { + const { version } = (await client.query(`SELECT VERSION() as version`))[0]; + if (version.startsWith("8.")) { + // MySQL 8 does not have `PASSWORD()` function + await client.execute( + `CREATE USER 'testuser'@'%' IDENTIFIED WITH mysql_native_password BY 'testpassword'`, + ); + } else { + await client.execute( + `CREATE USER 'testuser'@'%' IDENTIFIED WITH mysql_native_password`, + ); + await client.execute( + `SET PASSWORD FOR 'testuser'@'%' = PASSWORD('testpassword')`, + ); + } + await client.execute(`GRANT ALL ON test.* TO 'testuser'@'%'`); +}); + +testWithClient(async function testConnectWithMysqlNativePassword(client) { + assertEquals( + await client.query(`SELECT CURRENT_USER() AS user`), + [{ user: "testuser@%" }], + ); +}, { username: "testuser", password: "testpassword" }); + +testWithClient(async function testDropUserWithMysqlNativePassword(client) { + await client.execute(`DROP USER 'testuser'@'%'`); +}); + registerTests(); Deno.test("configLogger()", async () => { From 8c1fde1726479ab1b788776ed44be9b917db8f24 Mon Sep 17 00:00:00 2001 From: lideming Date: Sat, 26 Nov 2022 22:58:50 +0800 Subject: [PATCH 7/9] Add test and fix caching_sha2_password (#142) * ci run tests with DB password * fix caching_sha2_password encrypt * fix ci with mysql:8 unix socket * ci disallow tcp when testing unix socket --- .github/workflows/ci.yml | 7 ++++--- .github/workflows/wait-for-mysql.sh | 10 +++------- deps.ts | 1 + src/auth_plugin/caching_sha2_password.ts | 4 ++-- src/auth_plugin/crypt.ts | 10 ++++++++-- src/connection.ts | 2 +- test.util.ts | 11 ++++++----- 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4362ed2..877a860 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,10 +46,10 @@ jobs: run: | sudo mkdir -p /var/run/mysqld/tmp sudo chmod -R 777 /var/run/mysqld - docker container run --rm -d -p 3306:3306 \ + docker container run --name mysql --rm -d -p 3306:3306 \ -v /var/run/mysqld:/var/run/mysqld \ -v /var/run/mysqld/tmp:/tmp \ - -e MYSQL_ALLOW_EMPTY_PASSWORD=true \ + -e MYSQL_ROOT_PASSWORD=root \ ${{ matrix.DB_VERSION }} ./.github/workflows/wait-for-mysql.sh - name: Run tests (TCP) @@ -61,7 +61,8 @@ jobs: if [[ "${{ matrix.DB_VERSION }}" == "mysql:5.5" ]]; then SOCKPATH=/var/run/mysqld/tmp/mysql.sock fi + echo "DROP USER 'root'@'localhost';" | docker exec -i mysql mysql -proot DB_SOCKPATH=$SOCKPATH TEST_METHODS=unix \ - deno test --unstable --allow-env --allow-net=127.0.0.1:3306 \ + deno test --unstable --allow-env \ --allow-read=/var/run/mysqld/ --allow-write=/var/run/mysqld/ \ ./test.ts diff --git a/.github/workflows/wait-for-mysql.sh b/.github/workflows/wait-for-mysql.sh index dc4a5da..13302dd 100755 --- a/.github/workflows/wait-for-mysql.sh +++ b/.github/workflows/wait-for-mysql.sh @@ -1,15 +1,11 @@ #!/bin/sh echo "Waiting for MySQL" -for i in `seq 1 10`; +for i in `seq 1 30`; do - result="$(echo '\q' | mysql -h 127.0.0.1 -uroot -P 3306 2>&1 > /dev/null)" - if [ "$result" = "" ]; then - echo "Success waiting for MySQL" - exit 0 - fi + echo '\q' | mysql -h 127.0.0.1 -uroot --password=root -P 3306 && exit 0 >&2 echo "MySQL is waking up" - sleep 10 + sleep 1 done echo "Failed waiting for MySQL" && exit 1 diff --git a/deps.ts b/deps.ts index 745928d..93a948b 100644 --- a/deps.ts +++ b/deps.ts @@ -2,6 +2,7 @@ export type { Deferred } from "https://deno.land/std@0.104.0/async/mod.ts"; export { deferred, delay } from "https://deno.land/std@0.104.0/async/mod.ts"; export { format as byteFormat } from "https://deno.land/x/bytes_formater@v1.4.0/mod.ts"; export { createHash } from "https://deno.land/std@0.104.0/hash/mod.ts"; +export { decode as base64Decode } from "https://deno.land/std@0.104.0/encoding/base64.ts"; export type { SupportedAlgorithm, } from "https://deno.land/std@0.104.0/hash/mod.ts"; diff --git a/src/auth_plugin/caching_sha2_password.ts b/src/auth_plugin/caching_sha2_password.ts index 66d0e1c..1e8cbbe 100644 --- a/src/auth_plugin/caching_sha2_password.ts +++ b/src/auth_plugin/caching_sha2_password.ts @@ -17,7 +17,7 @@ async function start( ): Promise { scramble = scramble_; password = password_; - return { done: false, next: await authMoreResponse }; + return { done: false, next: authMoreResponse }; } async function authMoreResponse(packet: ReceivePacket): Promise { @@ -31,7 +31,7 @@ async function authMoreResponse(packet: ReceivePacket): Promise { if (statusFlag === AuthStatusFlags.FullAuth) { authMoreData = new Uint8Array([REQUEST_PUBLIC_KEY]); done = false; - next = await encryptWithKey; + next = encryptWithKey; } if (statusFlag === AuthStatusFlags.FastPath) { done = false; diff --git a/src/auth_plugin/crypt.ts b/src/auth_plugin/crypt.ts index 258f1fc..8eb2339 100644 --- a/src/auth_plugin/crypt.ts +++ b/src/auth_plugin/crypt.ts @@ -1,10 +1,16 @@ +import { base64Decode } from "../../deps.ts"; + async function encryptWithPublicKey( key: string, data: Uint8Array, ): Promise { + const pemHeader = "-----BEGIN PUBLIC KEY-----\n"; + const pemFooter = "\n-----END PUBLIC KEY-----"; + key = key.trim(); + key = key.substring(pemHeader.length, key.length - pemFooter.length); const importedKey = await crypto.subtle.importKey( - "raw", - new TextEncoder().encode(key), + "spki", + base64Decode(key), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"], diff --git a/src/connection.ts b/src/connection.ts index c79cf26..c6d9729 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -148,7 +148,7 @@ export class Connection { await this.nextPacket(); } if (result.next) { - result = result.next(receive); + result = await result.next(receive); } } } diff --git a/test.util.ts b/test.util.ts index ef737c6..985fb7f 100644 --- a/test.util.ts +++ b/test.util.ts @@ -5,10 +5,13 @@ const { DB_PORT, DB_NAME, DB_PASSWORD, DB_USER, DB_HOST, DB_SOCKPATH } = Deno .env.toObject(); const port = DB_PORT ? parseInt(DB_PORT) : 3306; const db = DB_NAME || "test"; -const password = DB_PASSWORD; +const password = DB_PASSWORD || "root"; const username = DB_USER || "root"; const hostname = DB_HOST || "127.0.0.1"; const sockPath = DB_SOCKPATH || "/var/run/mysqld/mysqld.sock"; +const testMethods = + Deno.env.get("TEST_METHODS")?.split(",") as ("tcp" | "unix")[] || ["tcp"]; +const unixSocketOnly = testMethods.length === 1 && testMethods[0] === "unix"; const config: ClientConfig = { timeout: 10000, @@ -31,10 +34,7 @@ export function testWithClient( tests.push([fn, overrideConfig]); } -export function registerTests(methods?: ("tcp" | "unix")[]) { - if (!methods) { - methods = Deno.env.get("TEST_METHODS")?.split(",") as any || ["tcp"]; - } +export function registerTests(methods: ("tcp" | "unix")[] = testMethods) { if (methods!.includes("tcp")) { tests.forEach(([fn, overrideConfig]) => { Deno.test({ @@ -83,6 +83,7 @@ export async function createTestDB() { ...config, poolSize: 1, db: undefined, + socketPath: unixSocketOnly ? sockPath : undefined, }); await client.execute(`CREATE DATABASE IF NOT EXISTS ${db}`); await client.close(); From aaee92331ad88889b47ebd563e160fa9d7d33790 Mon Sep 17 00:00:00 2001 From: lideming Date: Sat, 26 Nov 2022 23:14:17 +0800 Subject: [PATCH 8/9] Don't block the event loop from finishing (#143) --- src/pool.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pool.ts b/src/pool.ts index 8a0bdcf..f1de757 100644 --- a/src/pool.ts +++ b/src/pool.ts @@ -24,6 +24,12 @@ export class PoolConnection extends Connection { log.warning(`error closing idle connection`, error); } }, this.config.idleTimeout); + try { + // Don't block the event loop from finishing + Deno.unrefTimer(this._idleTimer); + } catch (_error) { + // unrefTimer() is unstable API in older version of Deno + } } } From 30c8c7cd791ab6cc828b15e7fd65dace6a093dce Mon Sep 17 00:00:00 2001 From: Dominic DiGiacomo Date: Thu, 23 Feb 2023 20:21:08 -0500 Subject: [PATCH 9/9] Basic TLS support Accept an array of custom CA certificates --- src/client.ts | 13 ++++++++++++ src/connection.ts | 18 ++++++++++++++++- src/constant/capabilities.ts | 1 + src/packets/builders/auth.ts | 17 +++++----------- src/packets/builders/client_capabilities.ts | 22 +++++++++++++++++++++ src/packets/builders/tls.ts | 22 +++++++++++++++++++++ 6 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 src/packets/builders/client_capabilities.ts create mode 100644 src/packets/builders/tls.ts diff --git a/src/client.ts b/src/client.ts index 42afe42..91a4b80 100644 --- a/src/client.ts +++ b/src/client.ts @@ -28,6 +28,19 @@ export interface ClientConfig { idleTimeout?: number; /** charset */ charset?: string; + /** TLS configuration */ + tls?: TLSConfig; +} + +/** + * TLS Config + */ +export interface TLSConfig { + /** Whether to enable TLS */ + enabled: boolean; + /** A list of root certificates (in PEM format) that will be used in addition to the + * default root certificates to verify the peer's certificate. */ + caCertificates?: string[]; } /** Transaction processor */ diff --git a/src/connection.ts b/src/connection.ts index c6d9729..d266dc6 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -7,6 +7,7 @@ import { } from "./constant/errors.ts"; import { log } from "./logger.ts"; import { buildAuth } from "./packets/builders/auth.ts"; +import { buildTls } from "./packets/builders/tls.ts"; import { buildQuery } from "./packets/builders/query.ts"; import { ReceivePacket, SendPacket } from "./packets/packet.ts"; import { parseError } from "./packets/parsers/err.ts"; @@ -79,13 +80,28 @@ export class Connection { try { let receive = await this.nextPacket(); const handshakePacket = parseHandshake(receive.body); + + let handshakeSequenceNumber = 1 + + if (this.config.tls?.enabled) { + if ((handshakePacket.serverCapabilities & ServerCapabilities.CLIENT_SSL) === 0) { + throw new Error('Server does not support TLS') + } + const tlsData = buildTls(handshakePacket, { db: this.config.db }); + await new SendPacket(tlsData, handshakeSequenceNumber++).send(this.conn); + this.conn = await Deno.startTls(this.conn, { + hostname, + caCerts: this.config.tls.caCertificates, + }); + } + const data = buildAuth(handshakePacket, { username, password, db: this.config.db, }); - await new SendPacket(data, 0x1).send(this.conn); + await new SendPacket(data, handshakeSequenceNumber++).send(this.conn); this.state = ConnectionState.CONNECTING; this.serverVersion = handshakePacket.serverVersion; diff --git a/src/constant/capabilities.ts b/src/constant/capabilities.ts index 6477e1a..8d6d6c7 100644 --- a/src/constant/capabilities.ts +++ b/src/constant/capabilities.ts @@ -15,6 +15,7 @@ enum ServerCapabilities { CLIENT_IGNORE_SIGPIPE = 0x00001000, CLIENT_RESERVED = 0x00004000, CLIENT_PS_MULTI_RESULTS = 0x00040000, + CLIENT_SSL = 0x800, } export default ServerCapabilities; diff --git a/src/packets/builders/auth.ts b/src/packets/builders/auth.ts index abbee55..6a60099 100644 --- a/src/packets/builders/auth.ts +++ b/src/packets/builders/auth.ts @@ -3,24 +3,17 @@ import { BufferWriter } from "../../buffer.ts"; import ServerCapabilities from "../../constant/capabilities.ts"; import { Charset } from "../../constant/charset.ts"; import type { HandshakeBody } from "../parsers/handshake.ts"; +import { clientCapabilities } from './client_capabilities.ts'; /** @ignore */ export function buildAuth( packet: HandshakeBody, params: { username: string; password?: string; db?: string }, ): Uint8Array { - const clientParam: number = - (params.db ? ServerCapabilities.CLIENT_CONNECT_WITH_DB : 0) | - ServerCapabilities.CLIENT_PLUGIN_AUTH | - ServerCapabilities.CLIENT_LONG_PASSWORD | - ServerCapabilities.CLIENT_PROTOCOL_41 | - ServerCapabilities.CLIENT_TRANSACTIONS | - ServerCapabilities.CLIENT_MULTI_RESULTS | - ServerCapabilities.CLIENT_SECURE_CONNECTION | - (ServerCapabilities.CLIENT_LONG_FLAG & packet.serverCapabilities) | - (ServerCapabilities.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA & - packet.serverCapabilities) | - (ServerCapabilities.CLIENT_DEPRECATE_EOF & packet.serverCapabilities); + const clientParam: number = clientCapabilities(packet, { + db: params.db, + ssl: false + }); if (packet.serverCapabilities & ServerCapabilities.CLIENT_PLUGIN_AUTH) { const writer = new BufferWriter(new Uint8Array(1000)); diff --git a/src/packets/builders/client_capabilities.ts b/src/packets/builders/client_capabilities.ts new file mode 100644 index 0000000..e5c4456 --- /dev/null +++ b/src/packets/builders/client_capabilities.ts @@ -0,0 +1,22 @@ +import ServerCapabilities from "../../constant/capabilities.ts"; +import type { HandshakeBody } from "../parsers/handshake.ts"; + +export function clientCapabilities( + packet: HandshakeBody, + params: { db?: string, ssl: boolean } +): number { + const clientParam: number = + (params.db ? ServerCapabilities.CLIENT_CONNECT_WITH_DB : 0) | + ServerCapabilities.CLIENT_PLUGIN_AUTH | + ServerCapabilities.CLIENT_LONG_PASSWORD | + ServerCapabilities.CLIENT_PROTOCOL_41 | + ServerCapabilities.CLIENT_TRANSACTIONS | + ServerCapabilities.CLIENT_MULTI_RESULTS | + ServerCapabilities.CLIENT_SECURE_CONNECTION | + (ServerCapabilities.CLIENT_LONG_FLAG & packet.serverCapabilities) | + (ServerCapabilities.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA & + packet.serverCapabilities) | + (ServerCapabilities.CLIENT_DEPRECATE_EOF & packet.serverCapabilities) | + (params.ssl ? ServerCapabilities.CLIENT_SSL : 0); + return clientParam +} diff --git a/src/packets/builders/tls.ts b/src/packets/builders/tls.ts new file mode 100644 index 0000000..12db325 --- /dev/null +++ b/src/packets/builders/tls.ts @@ -0,0 +1,22 @@ +import { BufferWriter } from "../../buffer.ts"; +import { Charset } from "../../constant/charset.ts"; +import type { HandshakeBody } from "../parsers/handshake.ts"; +import { clientCapabilities } from './client_capabilities.ts'; + +/** @ignore */ +export function buildTls( + packet: HandshakeBody, + params: { db?: string } +): Uint8Array { + const clientParam: number = clientCapabilities(packet, { + db: params.db, + ssl: true + }); + const writer = new BufferWriter(new Uint8Array(32)); + writer + .writeUint32(clientParam) + .writeUint32(2 ** 24 - 1) + .write(Charset.UTF8_GENERAL_CI) + .skip(23); + return writer.wroteData; +}