Skip to content

Commit e453cf5

Browse files
authored
Fix hard crash when describing "zero" datetime (#64)
* Fix #63: Hard crash in MySQLData.description when a zero-value datetime is received from older MySQL configurations which still do this. Returns a description of the epoch instead. * Use the thread-safe gmtime_r() instead of gmtime() in MySQLTime.init(date:). * Pass a more sensible capacity value to ByteBufferAllocator for SHA digests. * Fix deprecation warnings for TLSConfiguration and declare the explicit dependency on the update NIOSSL version. * Another round of long-overdue CI updates * Don't crash if server capabilities are not available during connection close.
1 parent f5f9378 commit e453cf5

File tree

9 files changed

+96
-45
lines changed

9 files changed

+96
-45
lines changed

.github/workflows/test.yml

+48-26
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
MYSQL_USER: vapor_username
2323
MYSQL_PASSWORD: vapor_password
2424
MYSQL_DATABASE: vapor_database
25-
container: swift:5.2-focal
25+
container: swift:5.4-focal
2626
strategy:
2727
fail-fast: false
2828
matrix:
@@ -53,7 +53,7 @@ jobs:
5353
MYSQL_HOSTNAME: mysql-a
5454
MYSQL_HOSTNAME_A: mysql-a
5555
MYSQL_HOSTNAME_B: mysql-b
56-
LOG_LEVEL: error
56+
LOG_LEVEL: trace
5757
linux:
5858
strategy:
5959
fail-fast: false
@@ -63,36 +63,39 @@ jobs:
6363
- mysql:8.0
6464
- mariadb:latest
6565
runner:
66-
# 5.2 Stable
6766
- swift:5.2-xenial
6867
- swift:5.2-bionic
6968
- swift:5.2-focal
7069
- swift:5.2-centos7
7170
- swift:5.2-centos8
7271
- swift:5.2-amazonlinux2
73-
# 5.2 Unstable
74-
- swiftlang/swift:nightly-5.2-xenial
75-
- swiftlang/swift:nightly-5.2-bionic
76-
# 5.3 Unstable
77-
- swiftlang/swift:nightly-5.3-xenial
78-
- swiftlang/swift:nightly-5.3-bionic
79-
- swiftlang/swift:nightly-5.3-focal
80-
- swiftlang/swift:nightly-5.3-centos7
81-
- swiftlang/swift:nightly-5.3-centos8
82-
- swiftlang/swift:nightly-5.3-amazonlinux2
83-
# Main Unsable
84-
- swiftlang/swift:nightly-master-xenial
85-
- swiftlang/swift:nightly-master-bionic
86-
- swiftlang/swift:nightly-master-focal
87-
- swiftlang/swift:nightly-master-centos7
88-
- swiftlang/swift:nightly-master-centos8
89-
- swiftlang/swift:nightly-master-amazonlinux2
72+
- swift:5.3-xenial
73+
- swift:5.3-bionic
74+
- swift:5.3-focal
75+
- swift:5.3-centos7
76+
- swift:5.3-centos8
77+
- swift:5.3-amazonlinux2
78+
- swift:5.4-bionic
79+
- swift:5.4-focal
80+
- swift:5.4-centos7
81+
- swift:5.4-centos8
82+
- swift:5.4-amazonlinux2
83+
- swiftlang/swift:nightly-5.5-focal
84+
- swiftlang/swift:nightly-5.5-centos8
85+
- swiftlang/swift:nightly-5.5-amazonlinux2
86+
- swiftlang/swift:nightly-main-focal
87+
- swiftlang/swift:nightly-main-centos8
88+
- swiftlang/swift:nightly-main-amazonlinux2
9089
exclude:
9190
- runner: swift:5.2-amazonlinux2
9291
dbimage: mysql:8.0
93-
- runner: swiftlang/swift:nightly-5.3-amazonlinux2
92+
- runner: swift:5.3-amazonlinux2
9493
dbimage: mysql:8.0
95-
- runner: swiftlang/swift:nightly-master-amazonlinux2
94+
- runner: swift:5.4-amazonlinux2
95+
dbimage: mysql:8.0
96+
- runner: swiftlang/swift:nightly-5.5-amazonlinux2
97+
dbimage: mysql:8.0
98+
- runner: swiftlang/swift:nightly-main-amazonlinux2
9699
dbimage: mysql:8.0
97100
container: ${{ matrix.runner }}
98101
runs-on: ubuntu-latest
@@ -117,7 +120,7 @@ jobs:
117120
run: swift test --enable-test-discovery --sanitize=thread
118121
env:
119122
MYSQL_HOSTNAME: mysql
120-
LOG_LEVEL: error
123+
LOG_LEVEL: trace
121124
macOS:
122125
strategy:
123126
fail-fast: false
@@ -126,16 +129,19 @@ jobs:
126129
- mysql@8.0
127130
- mysql@5.7
128131
- mariadb
132+
xcode:
133+
- latest
134+
- latest-stable
129135
include:
130136
- username: root
131137
- formula: mariadb
132138
username: runner
133139
runs-on: macos-latest
134140
steps:
135141
- name: Select latest available Xcode
136-
uses: maxim-lobanov/setup-xcode@1.0
142+
uses: maxim-lobanov/setup-xcode@v1
137143
with:
138-
xcode-version: latest
144+
xcode-version: ${{ matrix.xcode }}
139145
- name: Install MySQL server from Homebrew
140146
run: brew install ${{ matrix.formula }} && brew link --force ${{ matrix.formula }}
141147
- name: Start MySQL server
@@ -157,4 +163,20 @@ jobs:
157163
env:
158164
MYSQL_HOSTNAME: '127.0.0.1'
159165
MYSQL_DATABASE: vapor_database
160-
LOG_LEVEL: error
166+
LOG_LEVEL: trace
167+
# windows:
168+
# strategy:
169+
# fail-fast: false
170+
# matrix:
171+
# swiftver:
172+
# - '5.4.2'
173+
# runs-on: windows-latest
174+
# continue-on-error: true
175+
# steps:
176+
# - name: Check out code
177+
# uses: actions/checkout@v2
178+
# - name: Run tests
179+
# uses: MaxDesiatov/swift-windows-action@v1
180+
# with:
181+
# shell-action: swift test
182+
# swift-version: ${{ matrix.swiftver }}

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ let package = Package(
1313
.package(url: "https://github.com/apple/swift-crypto.git", from: "1.0.0"),
1414
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
1515
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
16-
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"),
16+
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.14.0"),
1717
],
1818
targets: [
1919
.target(name: "MySQLNIO", dependencies: [

Sources/MySQLNIO/MySQLConnection.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public final class MySQLConnection: MySQLDatabase {
66
username: String,
77
database: String,
88
password: String? = nil,
9-
tlsConfiguration: TLSConfiguration? = .forClient(),
9+
tlsConfiguration: TLSConfiguration? = .makeClientConfiguration(),
1010
serverHostname: String? = nil,
1111
logger: Logger = .init(label: "codes.vapor.mysql"),
1212
on eventLoop: EventLoop

Sources/MySQLNIO/MySQLConnectionHandler.swift

+25-6
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ final class MySQLConnectionHandler: ChannelDuplexHandler {
7474
case .commandPhase:
7575
if let current = self.queue.first {
7676
do {
77-
let commandState = try current.handler.handle(packet: &packet, capabilities: self.serverCapabilities!)
77+
guard let capabilities = self.serverCapabilities else {
78+
throw MySQLError.protocolError
79+
}
80+
let commandState = try current.handler.handle(packet: &packet, capabilities: capabilities)
7881
self.handleCommandState(context: context, commandState)
7982
} catch {
8083
self.queue.removeFirst()
@@ -226,7 +229,10 @@ final class MySQLConnectionHandler: ChannelDuplexHandler {
226229
database: state.database,
227230
authPluginName: authPluginName
228231
)
229-
try context.write(self.wrapOutboundOut(.encode(res, capabilities: self.serverCapabilities!)), promise: nil)
232+
guard let capabilities = self.serverCapabilities else {
233+
throw MySQLError.protocolError
234+
}
235+
try context.write(self.wrapOutboundOut(.encode(res, capabilities: capabilities)), promise: nil)
230236
context.flush()
231237
}
232238

@@ -264,7 +270,10 @@ final class MySQLConnectionHandler: ChannelDuplexHandler {
264270
}
265271
guard !packet.isError else {
266272
self.logger.trace("caching_sha2_password replied ERR, decoding")
267-
let err = try packet.decode(MySQLProtocol.ERR_Packet.self, capabilities: self.serverCapabilities!)
273+
guard let capabilities = self.serverCapabilities else {
274+
throw MySQLError.protocolError
275+
}
276+
let err = try packet.decode(MySQLProtocol.ERR_Packet.self, capabilities: capabilities)
268277
throw MySQLError.server(err)
269278
}
270279
guard let status = packet.payload.readInteger(endianness: .little, as: UInt8.self) else {
@@ -327,7 +336,10 @@ final class MySQLConnectionHandler: ChannelDuplexHandler {
327336
case "mysql_native_password":
328337
guard !packet.isError else {
329338
self.logger.trace("mysql_native_password sent ERR, decoding")
330-
let error = try packet.decode(MySQLProtocol.ERR_Packet.self, capabilities: self.serverCapabilities!)
339+
guard let capabilities = self.serverCapabilities else {
340+
throw MySQLError.protocolError
341+
}
342+
let error = try packet.decode(MySQLProtocol.ERR_Packet.self, capabilities: capabilities)
331343
throw MySQLError.server(error)
332344
}
333345
guard !packet.isOK else {
@@ -365,12 +377,16 @@ final class MySQLConnectionHandler: ChannelDuplexHandler {
365377
guard let command = self.queue.first else {
366378
return
367379
}
380+
guard let capabilities = self.serverCapabilities else {
381+
command.promise.fail(MySQLError.protocolError)
382+
return
383+
}
368384
self.commandState = .busy
369385

370386
// send initial
371387
do {
372388
self.sequence.current = nil
373-
let commandState = try command.handler.activate(capabilities: self.serverCapabilities!)
389+
let commandState = try command.handler.activate(capabilities: capabilities)
374390
self.handleCommandState(context: context, commandState)
375391
} catch {
376392
self.queue.removeFirst()
@@ -413,7 +429,10 @@ final class MySQLConnectionHandler: ChannelDuplexHandler {
413429
private func _close(context: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise<Void>?) throws {
414430
self.sequence.reset()
415431
let quit = MySQLProtocol.COM_QUIT()
416-
try context.write(self.wrapOutboundOut(.encode(quit, capabilities: self.serverCapabilities!)), promise: nil)
432+
// N.B.: It is possible to get here without having processed a handshake packet yet, in which case there will
433+
// not be any serverCapabilities. Since COM_QUIT doesn't care about any of those anyway, don't crash if they're
434+
// not there!
435+
try context.write(self.wrapOutboundOut(.encode(quit, capabilities: self.serverCapabilities ?? .init())), promise: nil)
417436
context.flush()
418437

419438
if let promise = promise {

Sources/MySQLNIO/MySQLData.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ public struct MySQLData: CustomStringConvertible, ExpressibleByStringLiteral, Ex
455455
case .bit:
456456
return self.bool!.description
457457
case .datetime, .timestamp:
458-
return self.date!.description
458+
return (self.time!.date ?? Date(timeIntervalSince1970: 0)).description
459459
case .varchar, .varString, .string:
460460
return self.string!.debugDescription
461461
case .double:

Sources/MySQLNIO/MySQLTime.swift

+8-7
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,19 @@ public struct MySQLTime: Equatable, MySQLDataConvertible {
5858
public init(date: Date) {
5959
// let comps = Calendar.current.dateComponents(in: .gmt, from: date)
6060
var rawtime = Int(date.timeIntervalSince1970)
61-
let tm = gmtime(&rawtime)!.pointee
61+
var tms = tm()
62+
gmtime_r(&rawtime, &tms)
6263
var microseconds = date.timeIntervalSince1970.microseconds
6364
if microseconds < 0.0 {
6465
microseconds = 1_000_000 - microseconds
6566
}
6667
self.init(
67-
year: numericCast(1900 + tm.tm_year),
68-
month: numericCast(1 + tm.tm_mon),
69-
day: numericCast(tm.tm_mday),
70-
hour: numericCast(tm.tm_hour),
71-
minute: numericCast(tm.tm_min),
72-
second: numericCast(tm.tm_sec),
68+
year: numericCast(1900 + tms.tm_year),
69+
month: numericCast(1 + tms.tm_mon),
70+
day: numericCast(tms.tm_mday),
71+
hour: numericCast(tms.tm_hour),
72+
minute: numericCast(tms.tm_min),
73+
second: numericCast(tms.tm_sec),
7374
microsecond: UInt32(microseconds)
7475
)
7576
}

Sources/MySQLNIO/Utilities/CryptoUtils.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import Crypto
22

33
func sha256(_ messages: ByteBuffer...) -> ByteBuffer {
44
let digest = SHA256.hash(data: [UInt8](messages.combine().readableBytesView))
5-
var buffer = ByteBufferAllocator().buffer(capacity: 0)
5+
var buffer = ByteBufferAllocator().buffer(capacity: SHA256.Digest.byteCount)
66
buffer.writeBytes(digest)
77
return buffer
88
}
99

1010
func sha1(_ messages: ByteBuffer...) -> ByteBuffer {
1111
let digest = Insecure.SHA1.hash(data: [UInt8](messages.combine().readableBytesView))
12-
var buffer = ByteBufferAllocator().buffer(capacity: 0)
12+
var buffer = ByteBufferAllocator().buffer(capacity: Insecure.SHA1.Digest.byteCount)
1313
buffer.writeBytes(digest)
1414
return buffer
1515
}

Tests/MySQLNIOTests/MySQLNIOTests.swift

+7
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,13 @@ final class MySQLNIOTests: XCTestCase {
264264
XCTAssert(time.microsecond == UInt32(100000))
265265
XCTAssert(time2.microsecond == UInt32(100000))
266266
}
267+
268+
func testDate_zeroIsInvalidButMySQLReturnsIt() throws {
269+
let zeroTime = MySQLTime()
270+
let data = MySQLData(time: zeroTime)
271+
272+
XCTAssertEqual(data.description, "1970-01-01 00:00:00 +0000")
273+
}
267274

268275
func testString_lengthEncoded_uint8() throws {
269276
let conn = try MySQLConnection.test(on: self.eventLoop).wait()

Tests/MySQLNIOTests/Utilities.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ extension MySQLConnection {
1212
} catch {
1313
return eventLoop.makeFailedFuture(error)
1414
}
15+
var tls = TLSConfiguration.makeClientConfiguration()
16+
tls.certificateVerification = .none
1517
return self.connect(
1618
to: addr,
1719
username: env("MYSQL_USERNAME") ?? "vapor_username",
1820
database: env("MYSQL_DATABASE") ?? "vapor_database",
1921
password: env("MYSQL_PASSWORD") ?? "vapor_password",
20-
tlsConfiguration: .forClient(certificateVerification: .none),
22+
tlsConfiguration: tls,
2123
on: eventLoop
2224
)
2325
}

0 commit comments

Comments
 (0)