Skip to content
Open
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
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
10 changes: 3 additions & 7 deletions .github/workflows/wait-for-mysql.sh
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
3 changes: 2 additions & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
29 changes: 19 additions & 10 deletions src/auth_plugin/caching_sha2_password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<handler> {
scramble = scramble_;
password = password_;
return { done: false, next: authMoreResponse };
}
function authMoreResponse(packet: ReceivePacket): handler {

async function authMoreResponse(packet: ReceivePacket): Promise<handler> {
const enum AuthStatusFlags {
FullAuth = 0x04,
FastPath = 0x03,
Expand All @@ -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<handler> {
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<ArrayBuffer> {
const stage1 = xor(password, scramble);
const encrypted = encryptWithPublicKey(key, stage1);
return encrypted;
return await encryptWithPublicKey(key, stage1);
}

function terminate() {
Expand Down
29 changes: 25 additions & 4 deletions src/auth_plugin/crypt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
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);
import { base64Decode } from "../../deps.ts";

async function encryptWithPublicKey(
key: string,
data: Uint8Array,
): Promise<ArrayBuffer> {
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(
"spki",
base64Decode(key),
{ name: "RSA-OAEP", hash: "SHA-256" },
false,
["encrypt"],
);

return await crypto.subtle.encrypt(
{
name: "RSA-OAEP",
},
importedKey,
data,
);
}

export { encryptWithPublicKey };
4 changes: 4 additions & 0 deletions src/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
76 changes: 41 additions & 35 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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";
import ServerCapabilities from "./constant/capabilities.ts";

/**
Expand Down Expand Up @@ -117,13 +119,41 @@ 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;
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;
Expand All @@ -134,7 +164,7 @@ export class Connection {
await this.nextPacket();
}
if (result.next) {
result = result.next(receive);
result = await result.next(receive);
}
}
}
Expand Down Expand Up @@ -210,34 +240,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) {
Expand Down Expand Up @@ -303,8 +305,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();
Expand All @@ -314,7 +316,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);
Expand Down
21 changes: 21 additions & 0 deletions src/packets/parsers/authswitch.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
6 changes: 6 additions & 0 deletions src/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}

Expand Down
31 changes: 31 additions & 0 deletions test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading