diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 877a860..a59db7f 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/.gitignore b/.gitignore index c3cbfb0..520cb2b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules mysql.log docs .DS_Store +.idea diff --git a/README.md b/README.md index 38f9802..6c718ac 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,36 @@ const users = await client.transaction(async (conn) => { console.log(users.length); ``` +### TLS + +TLS configuration: + +- caCerts([]string): A list of root certificates (must be PEM format) that will + be used in addition to the default root certificates to verify the peer's + certificate. +- mode(string): The TLS mode to use. Valid values are "disabled", + "verify_identity". Defaults to "disabled". + +You usually need not specify the caCert, unless the certificate is not included +in the default root certificates. + +```ts +import { Client, TLSConfig, TLSMode } from "https://deno.land/x/mysql/mod.ts"; +const tlsConfig: TLSConfig = { + mode: TLSMode.VERIFY_IDENTITY, + caCerts: [ + await Deno.readTextFile("capath"), + ], +}; +const client = await new Client().connect({ + hostname: "127.0.0.1", + username: "root", + db: "dbname", + password: "password", + tls: tlsConfig, +}); +``` + ### close ```ts diff --git a/mod.ts b/mod.ts index 7e1f8e4..193240d 100644 --- a/mod.ts +++ b/mod.ts @@ -1,5 +1,7 @@ export type { ClientConfig } from "./src/client.ts"; export { Client } from "./src/client.ts"; +export type { TLSConfig } from "./src/client.ts"; +export { TLSMode } from "./src/client.ts"; export type { ExecuteResult } from "./src/connection.ts"; export { Connection } from "./src/connection.ts"; diff --git a/src/client.ts b/src/client.ts index 42afe42..370cb2e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -28,6 +28,23 @@ export interface ClientConfig { idleTimeout?: number; /** charset */ charset?: string; + /** tls config */ + tls?: TLSConfig; +} + +export enum TLSMode { + DISABLED = "disabled", + VERIFY_IDENTITY = "verify_identity", +} +/** + * TLS Config + */ +export interface TLSConfig { + /** mode of tls. only support disabled and verify_identity now*/ + mode?: TLSMode; + /** A list of root certificates (must be PEM format) that will be used in addition to the + * default root certificates to verify the peer's certificate. */ + caCerts?: string[]; } /** Transaction processor */ diff --git a/src/connection.ts b/src/connection.ts index c6d9729..cdeb17c 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1,4 +1,4 @@ -import { ClientConfig } from "./client.ts"; +import { ClientConfig, TLSMode } from "./client.ts"; import { ConnnectionError, ProtocolError, @@ -21,6 +21,7 @@ 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"; +import { buildSSLRequest } from "./packets/builders/tls.ts"; /** * Connection state @@ -62,6 +63,13 @@ export class Connection { private async _connect() { // TODO: implement connect timeout + if ( + this.config.tls?.mode && + this.config.tls.mode !== TLSMode.DISABLED && + this.config.tls.mode !== TLSMode.VERIFY_IDENTITY + ) { + throw new Error("unsupported tls mode"); + } const { hostname, port = 3306, socketPath, username = "", password } = this.config; log.info(`connecting ${this.remoteAddr}`); @@ -79,13 +87,46 @@ export class Connection { try { let receive = await this.nextPacket(); const handshakePacket = parseHandshake(receive.body); + + let handshakeSequenceNumber = receive.header.no; + + // Deno.startTls() only supports VERIFY_IDENTITY now. + let isSSL = false; + if ( + this.config.tls?.mode === TLSMode.VERIFY_IDENTITY + ) { + if ( + (handshakePacket.serverCapabilities & + ServerCapabilities.CLIENT_SSL) === 0 + ) { + throw new Error("Server does not support TLS"); + } + if ( + (handshakePacket.serverCapabilities & + ServerCapabilities.CLIENT_SSL) !== 0 + ) { + const tlsData = buildSSLRequest(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?.caCerts, + }); + } + isSSL = true; + } + const data = buildAuth(handshakePacket, { username, password, db: this.config.db, + ssl: isSSL, }); - 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..a411d79 100644 --- a/src/constant/capabilities.ts +++ b/src/constant/capabilities.ts @@ -1,20 +1,27 @@ enum ServerCapabilities { - CLIENT_PROTOCOL_41 = 0x00000200, - CLIENT_CONNECT_WITH_DB = 0x00000008, - CLIENT_LONG_FLAG = 0x00000004, - CLIENT_DEPRECATE_EOF = 0x01000000, CLIENT_LONG_PASSWORD = 0x00000001, - CLIENT_TRANSACTIONS = 0x00002000, - CLIENT_MULTI_RESULTS = 0x00020000, - CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000, - CLIENT_PLUGIN_AUTH = 0x80000, - CLIENT_SECURE_CONNECTION = 0x8000, CLIENT_FOUND_ROWS = 0x00000002, - CLIENT_CONNECT_ATTRS = 0x00100000, + CLIENT_LONG_FLAG = 0x00000004, + CLIENT_CONNECT_WITH_DB = 0x00000008, + CLIENT_NO_SCHEMA = 0x00000010, + CLIENT_COMPRESS = 0x00000020, + CLIENT_ODBC = 0x00000040, + CLIENT_LOCAL_FILES = 0x00000080, CLIENT_IGNORE_SPACE = 0x00000100, + CLIENT_PROTOCOL_41 = 0x00000200, + CLIENT_INTERACTIVE = 0x00000400, + CLIENT_SSL = 0x00000800, CLIENT_IGNORE_SIGPIPE = 0x00001000, + CLIENT_TRANSACTIONS = 0x00002000, CLIENT_RESERVED = 0x00004000, + CLIENT_SECURE_CONNECTION = 0x00008000, + CLIENT_MULTI_STATEMENTS = 0x00010000, + CLIENT_MULTI_RESULTS = 0x00020000, CLIENT_PS_MULTI_RESULTS = 0x00040000, + CLIENT_PLUGIN_AUTH = 0x00080000, + CLIENT_CONNECT_ATTRS = 0x00100000, + CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000, + CLIENT_DEPRECATE_EOF = 0x01000000, } export default ServerCapabilities; diff --git a/src/logger.ts b/src/logger.ts index 17250d3..dad062a 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -29,7 +29,7 @@ export async function configLogger(config: LoggerConfig) { if (!enable) { logger = new log.Logger("fakeLogger", "NOTSET", {}); - logger.level = 100; + logger.level = 0; } else { if (!config.logger) { await log.setup({ diff --git a/src/packets/builders/auth.ts b/src/packets/builders/auth.ts index abbee55..194c485 100644 --- a/src/packets/builders/auth.ts +++ b/src/packets/builders/auth.ts @@ -3,24 +3,14 @@ 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 }, + params: { username: string; password?: string; db?: string; ssl?: boolean }, ): 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, params); 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..842fdcb --- /dev/null +++ b/src/packets/builders/client_capabilities.ts @@ -0,0 +1,20 @@ +import ServerCapabilities from "../../constant/capabilities.ts"; +import type { HandshakeBody } from "../parsers/handshake.ts"; + +export function clientCapabilities( + packet: HandshakeBody, + params: { db?: string; ssl?: boolean }, +): number { + return (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); +} diff --git a/src/packets/builders/tls.ts b/src/packets/builders/tls.ts new file mode 100644 index 0000000..487301a --- /dev/null +++ b/src/packets/builders/tls.ts @@ -0,0 +1,21 @@ +import { BufferWriter } from "../../buffer.ts"; +import { Charset } from "../../constant/charset.ts"; +import type { HandshakeBody } from "../parsers/handshake.ts"; +import { clientCapabilities } from "./client_capabilities.ts"; + +export function buildSSLRequest( + 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; +}