Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support TLS #154

Merged
merged 4 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}

shiyuhang0 marked this conversation as resolved.
Show resolved Hide resolved
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;
2 changes: 1 addition & 1 deletion src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
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;
}