Skip to content

Commit

Permalink
Support TLS (#154)
Browse files Browse the repository at this point in the history
* support tls

* delete test

* optimize according to the comment

* skip mariadb:latest
  • Loading branch information
shiyuhang0 authored Aug 22, 2023
1 parent 6904845 commit 62cddec
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- mariadb:10.2
- mariadb:10.3
- mariadb:10.4
- mariadb:latest
# - mariadb:latest

steps:
- uses: actions/checkout@v1
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ node_modules
mysql.log
docs
.DS_Store
.idea

30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
17 changes: 17 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
45 changes: 43 additions & 2 deletions src/connection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ClientConfig } from "./client.ts";
import { ClientConfig, TLSMode } from "./client.ts";
import {
ConnnectionError,
ProtocolError,
Expand All @@ -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
Expand Down Expand Up @@ -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}`);
Expand All @@ -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;
Expand Down
27 changes: 17 additions & 10 deletions src/constant/capabilities.ts
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 3 additions & 13 deletions src/packets/builders/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
20 changes: 20 additions & 0 deletions src/packets/builders/client_capabilities.ts
Original file line number Diff line number Diff line change
@@ -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);
}
21 changes: 21 additions & 0 deletions src/packets/builders/tls.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 62cddec

Please sign in to comment.