Tweaks and fixes throughout the project (#27)

This PR contains the work done to do a little bit of housekeeping pass across all packages and the Website service.

To provide further details about the work:
* Refreshed the READMEs and source documentation to match the current code;
* Tagged every test case consistently across the Infrastructure, Localization, Persistence, and Website test targets;
* Removed Website middleware tests now covered by Infrastructure's own suite;
* Conformed the `PrepareDB` method to Sendable;
* Relaxes the production Compose DATABASE_TLS default from require to prefer;
* Added Persistence test verifying the prefer posture falls back to plaintext connections.

Reviewed-on: rock-n-code/loud-amsterdam#27
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
2026-07-30 06:33:57 +00:00
committed by javier
parent 0887135328
commit ea00b94841
71 changed files with 633 additions and 512 deletions
@@ -0,0 +1,162 @@
import NIOCore
import NIOPosix
/// A fake MySQL server speaking just enough of the wire protocol to complete a plaintext handshake.
///
/// Its greeting advertises the `mysql_native_password` plugin but **not** the `CLIENT_SSL` capability, and
/// it answers the client's handshake response with a bare OK packet so a client asking for TLS can only
/// end up connected in plaintext. This is what the `prefer` fallback test connects to, proving the driver
/// downgrades to plaintext rather than refusing the connection.
final class PlaintextMySQLServer {
// MARK: Properties
/// The port the server listens on, assigned by the system at bind time.
let port: Int
/// The listening channel the server accepts connections through.
private let channel: Channel
// MARK: Initializers
private init(
channel: Channel,
port: Int
) {
self.channel = channel
self.port = port
}
// MARK: Functions
/// Starts a server on the loopback interface, on a system-assigned port.
/// - Returns: the running server, ready to be connected to at ``port``.
static func start() async throws -> PlaintextMySQLServer {
let channel = try await ServerBootstrap(group: MultiThreadedEventLoopGroup.singleton)
.childChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(Handler())
}
}
.bind(host: "127.0.0.1", port: 0)
.get()
guard let port = channel.localAddress?.port else {
throw ChannelError.unknownLocalAddress
}
return .init(
channel: channel,
port: port
)
}
/// Stops the server, closing its listening channel.
func stop() async throws {
try await channel.close().get()
}
}
// MARK: - Handlers
private extension PlaintextMySQLServer {
/// Greets a freshly accepted connection, accepts whatever authentication response arrives,
/// and closes on anything after that (e.g. a `COM_QUIT`).
final class Handler: ChannelInboundHandler {
// MARK: Type aliases
typealias InboundIn = ByteBuffer
typealias OutboundOut = ByteBuffer
// MARK: Properties
/// Whether the client's handshake response has already been answered with an OK packet.
private var didAuthenticate = false
// MARK: Functions
func channelActive(context: ChannelHandlerContext) {
context.writeAndFlush(
wrapOutboundOut(Self.greeting(allocator: context.channel.allocator)),
promise: nil
)
}
func channelRead(
context: ChannelHandlerContext,
data: NIOAny
) {
guard didAuthenticate else {
didAuthenticate = true
context.writeAndFlush(
wrapOutboundOut(Self.ok(allocator: context.channel.allocator)),
promise: nil
)
return
}
context.close(promise: nil)
}
// MARK: Helpers
/// The `HandshakeV10` greeting, framed and ready to send as the connection's first packet.
///
/// The advertised capabilities are `CLIENT_LONG_PASSWORD`, `CLIENT_PROTOCOL_41`,
/// `CLIENT_SECURE_CONNECTION`, and `CLIENT_PLUGIN_AUTH` deliberately **not** `CLIENT_SSL`,
/// so the client cannot upgrade the connection to TLS.
private static func greeting(allocator: ByteBufferAllocator) -> ByteBuffer {
var payload = allocator.buffer(capacity: 80)
payload.writeInteger(10, endianness: .little, as: UInt8.self) // protocol version
payload.writeNullTerminatedString("8.0.0") // server version
payload.writeInteger(1, endianness: .little, as: UInt32.self) // connection id
payload.writeBytes([1, 2, 3, 4, 5, 6, 7, 8]) // auth plugin data, part 1
payload.writeInteger(0, endianness: .little, as: UInt8.self) // filler
payload.writeInteger(0x8201, endianness: .little, as: UInt16.self) // capabilities, lower: LONG_PASSWORD | PROTOCOL_41 | SECURE_CONNECTION
payload.writeInteger(0x21, endianness: .little, as: UInt8.self) // character set (utf8)
payload.writeInteger(0x0002, endianness: .little, as: UInt16.self) // status flags (autocommit)
payload.writeInteger(0x0008, endianness: .little, as: UInt16.self) // capabilities, upper: PLUGIN_AUTH
payload.writeInteger(21, endianness: .little, as: UInt8.self) // auth plugin data length
payload.writeBytes([UInt8](repeating: 0, count: 10)) // reserved
payload.writeBytes([9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 0]) // auth plugin data, part 2
payload.writeNullTerminatedString("mysql_native_password") // auth plugin name
return framed(payload, sequence: 0, allocator: allocator)
}
/// A bare OK packet, framed as the reply to the client's handshake response.
private static func ok(allocator: ByteBufferAllocator) -> ByteBuffer {
var payload = allocator.buffer(capacity: 8)
payload.writeBytes([0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00]) // OK, no rows, autocommit, no warnings
return framed(payload, sequence: 2, allocator: allocator)
}
/// Wraps a payload in the MySQL packet frame: a 3-byte little-endian length and a sequence byte.
private static func framed(
_ payload: ByteBuffer,
sequence: UInt8,
allocator: ByteBufferAllocator
) -> ByteBuffer {
var packet = allocator.buffer(capacity: payload.readableBytes + 4)
var payload = payload
packet.writeInteger(UInt8(payload.readableBytes & 0xff))
packet.writeInteger(UInt8((payload.readableBytes >> 8) & 0xff))
packet.writeInteger(UInt8((payload.readableBytes >> 16) & 0xff))
packet.writeInteger(sequence)
packet.writeBuffer(&payload)
return packet
}
}
}