Pure Pascal Firebird wire protocol client: connect without fbclient - #9
Pure Pascal Firebird wire protocol client: connect without fbclient#9mariuz wants to merge 70 commits into
Conversation
Adds the foundations of a Firebird client that speaks the remote (wire)
protocol directly over TCP, with no dependency on fbclient. Verified
against a Firebird 6.0 server: protocol 17 negotiation, Srp256
authentication, ChaCha64 wire encryption, DDL, parameterised DML,
multi-column fetches and blob round trips.
New units under client/wire:
FBWireBigInt arbitrary precision unsigned arithmetic (Knuth algorithm D
division, square and multiply modular exponentiation) as
needed by SRP
FBWireCrypto SHA-1, SHA-256, RC4 and ChaCha20, all verified against the
published test vectors
FBWireSRP the client half of SRP-6a as implemented by the Srp and
Srp256 plugins, including the engine's deviations from the
specification (see the unit header)
FBWireStream buffered TCP transport with per direction stream ciphers,
plus the XDR encoder/decoder
FBWireConst operation codes and protocol constants from protocol.h
FBWireMessage message buffer layout, BLR message descriptions and the
protocol 13+ null bitmap message encoding
FBWireDescribe isc_info_sql describe response parser
FBWireProtocol the connection handshake and the request/response
exchanges for attachments, transactions, DSQL, blobs,
information calls and services
Points worth recording, all of which cost a debugging cycle:
- the wire encryption key type sent in op_crypt is the type the server
advertised in its key clumplets ("Symmetric"), not the name of the
authentication plugin
- text fields must be described with blr_text2/blr_varying2 naming the
column character set; with plain blr_text/blr_varying the engine
assumes the connection character set and divides the byte length by
the maximum character size, silently truncating the column
- XDR sends a boolean as a one byte opaque value padded to four bytes,
not as a big endian integer
- a batched op_fetch must be drained completely before any other request
is sent, so rows are cached as they arrive
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adapts the wire protocol engine to the fbintf object model, so existing
code written against IAttachment, ITransaction, IStatement and IResultSet
runs unchanged against a server with no client library installed:
API := WireFirebirdAPI;
DPB := API.AllocateDPB;
DPB.Add(isc_dpb_user_name).AsString := 'SYSDBA';
DPB.Add(isc_dpb_password).AsString := 'masterkey';
Attachment := API.OpenDatabase('localhost:employee',DPB);
New units:
FBWireClientAPI IFirebirdAPI, the status object and the date and time
conversions (performed arithmetically, since there is
no isc_encode_* to call)
FBWireAttachment attach, create, drop, detach and the database info calls
FBWireTransaction the transaction primitives
FBWireStatement prepare, execute, fetch and the SQLDA equivalent: the
column data lives in a flat message buffer that
TSQLDataItem points into, so the whole fbintf
conversion layer applies unchanged
FBWireBlob blob metadata, segmented read and write
Events, services, arrays, batches and scrollable cursors raise
ibxeNotSupported rather than failing obscurely; client/wire/README.md
records what is and is not implemented.
The plain text password is used only for the SRP exchange and is stripped
from the DPB before the attach, so it never reaches the network.
Bugs found and fixed while bringing the provider up against a live server:
- the null indicator in the message buffer is what the encoder
transmits, so IsNull must read it directly rather than gate on the
column's nullable flag, and writing a value must clear it. Otherwise
every parameter was sent as null.
- a VARCHAR value must be written after its two byte length prefix; the
previous code overwrote the first two characters with the length.
- CHAR values are blank padded to their full width, otherwise a
parameter never compares equal to a blank padded column.
- closing a cursor whose transaction has ended is not an error: the
server has already closed it (isc_dsql_cursor_close_err).
- preparation is lazy in the base class, so anything needing the
statement handle must prepare before checking it.
testsuite/WireTest.pas is a self contained regression test: the
cryptographic primitives against their published vectors, the SRP
exchange against a fixed reference exchange, the message layout, then the
protocol and the provider against a live server. 72 tests, all passing
against Firebird 6.0 over protocol 17 with Srp256 and ChaCha64.
client/wire is added to the FPC search paths and to fbintf.lpk. The
Delphi packages are left alone: the transport is written against the FPC
socket units and a Delphi transport is still to be written.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TFBWireConnection.MaxProtocol caps the highest version offered in op_connect. It defaults to the highest the client implements, and lowering it lets a caller force an older dialect of the protocol - which is how the version matrix in client/wire/README.md was measured, and what the new negotiation test in WireTest exercises. Against Firebird 6.0 the client negotiates 14, 15, 16 and 17 and completes an attach, ping and detach at each. Protocols 14 and 15 fall back to Arc4 because the ChaCha initialisation vector is only advertised from protocol 16. Protocol 13 is refused by this server with isc_miss_wirecrypt, since it predates wire encryption and the server requires it; the version is still offered for Firebird 3 servers and for servers configured with WireCrypt = Enabled. WireTest is now 77 tests, all passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion
Adds .github/workflows/wire-protocol.yml. The server job runs the wire
protocol test against Firebird 3, 4, 5 and 6 containers under both
WireCrypt = Enabled and WireCrypt = Required, seven combinations: Firebird 3
with Required is excluded because protocol 13, the only one it offers
without encryption support, cannot satisfy it. Each job starts the
container, creates a database with isql inside it, builds with the
distribution's FPC and runs the test over TCP; nothing Firebird related is
installed on the runner, which is the point of the client. The negotiated
protocol and the test totals go to the step summary so the matrix view
shows what each server version settled on.
A second job builds and runs the same binary on Linux and Windows with no
server, so a regression in the arithmetic, the hashes, the SRP exchange or
the message layout cannot be hidden by a server problem and the code keeps
compiling on Windows. It points the test at a port with nothing listening;
the live sections then report SKIP and the offline ones still run.
Measured locally while writing this, all with 0 failures: Firebird 6.0.0
with WireCrypt required (81 tests), Firebird 5.0.4 in a container under
both WireCrypt settings (81 tests each), and no server at all (36 tests,
live sections skipped). Firebird 3 and 4 are left to CI because their
images are published for amd64 only and this machine is arm64.
Documentation: doc/WireProtocol.md describes the design, the protocol
details that a plausible reading of the packet layouts gets wrong, the SRP
deviations, the encryption changeover, the message and BLR encoding, the
error reporting gap, the testing layers and the CI matrix.
client/wire/README.md gains the measured results, and the top level README
gains a short section pointing at both.
Also in this commit:
- the test now honours a port in the connect string, which is what lets
the offline job fail fast instead of waiting for a timeout
- it takes an optional scratch database argument and, when given one,
tests create and drop database
- Check flushes, so a log is complete even when the next call raises
- dropping the test table is best effort. An attachment that has read a
table keeps an interest in it that outlives the transaction and
Firebird 5 then refuses to drop it on that attachment; the stock
fbclient provider fails in exactly the same place with exactly the same
error, and a second attachment issuing the drop blocks instead, so this
is the server's behaviour rather than the wire client's. The table is
dropped at the start of the next run.
- TCP_NODELAY is only set where the platform unit declares it, so the
transport compiles on targets whose sockets unit omits it
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
protocol negotiation assertion
Two problems the CI matrix found on its first run.
The character set of a text column was never picked up. The test read
if SQLType in [SQL_TEXT,SQL_VARYING] then
and a Pascal set holds 0..255, while the type codes are 452 and 448, so
the comparison was against truncated values and never matched. Every text
column therefore fell back to character set 0 and the BLR named NONE. The
compiler had been saying so all along:
FBWireDescribe.pas(213,36) Warning: range check error while evaluating
constants (452 must be between 0 and 255)
Rewritten as explicit comparisons. A UTF8 column now reports character set
4 where it previously reported 0, and the BLR names it, so the engine no
longer has to transliterate a column it was told was NONE. This was masked
in testing because the sample database is character set NONE and because
blank padding CHAR parameters made the ASCII comparisons work anyway; it
would have corrupted text in a database that actually uses a multi byte
character set.
The negotiation test asserted that capping the offer at protocol N makes
the server negotiate exactly N. That only holds if the server knows N.
Firebird 3 tops out at 15, so capping at 16 or 17 still yields 15 and the
test failed against it - while the client itself connected perfectly well
at protocol 15 with Srp256 and Arc4, and passed the other 79 checks. The
assertion now requires the negotiated version to be at least 13 and no
higher than the cap, which is the actual contract, and reports the version
it got.
Verified after the change: Firebird 6.0.0 and Firebird 5.0.4 against a
character set UTF8 database, 81 tests and 0 failures each, accented text
included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every supported Firebird version has now been run: 3.0 negotiates protocol 15 with Srp256 and Arc4, and 4.0, 5.0 and 6.0 negotiate protocol 17 with Srp256 and ChaCha64. All of them pass the full suite, 81 tests with 0 failures, under both WireCrypt = Enabled and WireCrypt = Required (Firebird 3 under Enabled only, since protocol 13 cannot satisfy Required). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the short list of missing features with a roadmap that says what each piece actually needs, in the order worth doing, and what already exists to support it. Twelve milestones, from running the existing test suite against this provider through events, services, a Delphi transport, arrays, cancellation, scrollable cursors, batches, inline blobs, protocol 20, compression and engine message text. Running the existing testsuite against the wire provider is first because everything after it benefits: those twenty two programs are written against the interfaces rather than a provider, and exercise far more of the API than WireTest does. A switch in TTestApplication.GetFirebirdAPI is all the plumbing it needs, and that run then becomes the acceptance criterion for the events, services and array milestones. Also records what is deliberately not planned and why: Legacy_Auth (no session key, so it cannot satisfy a server requiring encryption), Win_SSPI, and protocols below 13. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st suite against the wire provider Design and implementation plan for milestone 01 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 02 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 03 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 04 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 05 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and cancellation Design and implementation plan for milestone 06 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 07 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 08 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 09 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 10 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 11 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 12 of the wire protocol client roadmap (doc/WireProtocol.md). Documentation only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TFBWireServiceManager (client/wire/FBWireServices.pas) surfaces the op_service_attach/detach/start/info exchanges, which FBWireProtocol already implemented, as IServiceManager. A service session is its own connection: ConnectTo performs the SRP authentication and starts wire encryption when negotiated exactly as for a database attach, and the password is consumed by SRP and stripped from the SPB, with the proof travelling as isc_spb_specific_auth_data in the op_accept_data flow - the same treatment TFBWireAttachment gives the DPB. HasServiceAPI now answers true and AllocateSPB returns the shared TSPB. Detaching and reattaching a service manager was the first code path to reconnect a TFBWireConnection, and it exposed a latent transport bug: TFBWireTransport.Disconnect left the session ciphers installed, so the reconnect's cleartext handshake was sent through the dead session's cipher and the server dropped the connection. Disconnect now discards both ciphers. The plan's one protocol nuance turned out to be a non-issue: the op_connect packet's p_cnct_operation field is vestigial - the stock client sends op_attach for service connections too - so ConnectTo is unchanged. Verified against Firebird 5 (WireCrypt Required, ChaCha64) and Firebird 6: WireTest's new services section covers the version query, detach and reattach, and header page statistics, 88 tests with 0 failures including create and drop database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every row of the server matrix - Firebird 3, 4, 5 and 6, WireCrypt Enabled and Required - now measures 88 tests with 0 failures, including the new services section. Firebird 3 exercises it over protocol 15 with Arc4; everything else negotiates 17 with ChaCha64. The offline count is unchanged at 36: the services tests need a live server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
testsuite -a wire (--api wire) makes TTestApplication obtain the API from WireFirebirdAPI instead of IB.FirebirdAPI. All twenty two test programs now run over the wire provider and runtest.sh compares the output against the new FBWirereference.log, normalising the run dependent values (transaction ids, page counters, journal timestamps, the database id line) on both sides of the diff. A CI job runs the suite against a Firebird 6 container, restoring the employee example database from the checked in testsuite/employee.gbk, and fails on any difference from the reference log. Harness changes: the two GetFBLibrary dereferences are guarded (the wire provider has no client library), and IAttachment gains HasArraySupport and HasEventSupport alongside the existing capability checks, so tests 2, 7, 8, 10, 13, 17, 18, 20 and 22 skip unsupported features with a fixed message instead of erroring. The suite found seven wire provider defects on its first run, all in paths WireTest never exercised: - SRP account upper casing used AnsiUpperCase, which under fpwidestring (loaded by the suite) returns the string with a trailing #0 included in its length (fpc bug 39746), poisoning the proof hashes; every login from the suite binary failed. ASCII UpperCase is used instead, matching the engine. - ExecuteStatement2 was called with its statement and transaction handles swapped, killing the connection on any execute procedure or insert ... returning. - Execute returned nil for update/insert ... returning: Firebird 5+ describe those as selects (cursor plus one row), older servers as SQLExecProcedure (op_execute2 singleton); both paths are implemented. - The bind after prepare overwrote named parameter names with the describe response's empty names. - Parameter metadata was immutable; type coercion (AsInteger on a SMALLINT, strings to blobs) now updates the message format and relays out the buffer - the client owns the BLR it sends. - DECFLOAT conversions inherited the base class codec, which has no implementation and returned garbage; the provider now carries its own IEEE 754 densely packed decimal codec, verified against the server in both directions. - GetBlobMetaData never looked the column up, so blob subtypes were wrong; it now runs the same system table query as the 3.0 provider. Also: create database from a SQL statement extracts the file spec locally (the stock providers delegate that preparse to fbclient), a nil DPB no longer crashes, the DPB sent with the attach is a verbatim clumplet copy minus the password items rather than a re-encoded copy, the transport discards its session ciphers on disconnect so a connection object can reconnect, and WireIBError takes ownership of a non-wire exception before re-raising it from the caller's handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone 2 builds on milestone 1: the suite's Test 10 is the acceptance criterion for events, and the wire fixes that milestone delivered (cipher reset on reconnect, the capability checks) are needed here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TFBWireEvents (client/wire/FBWireEvents.pas) implements IEvents over the auxiliary connection: op_connect_request on the main connection returns the port of the server's event channel - only the port, because the address in the response is the server's own view of itself, which behind NAT is not reachable, so the client reuses the host it already connected to, as the stock client does. The auxiliary connection needs no handshake, authentication or encryption: the server associates it with the session by the accept, and it only ever delivers op_event packets, drained by a listener thread. One TFBWireEventManager per attachment, created on the first GetEventHandler call, owns the auxiliary transport and the listener and serves all the attachment's IEvents instances, dispatching by event id. All of the event block machinery - the EPB that op_que_events carries, the count diffing and the callback dispatch - is inherited from TFBEvents. Both asynchronous and synchronous waits are supported; the handler is called from the listener thread, as the 2.5 provider calls its handler from an AST thread. The one discovery worth recording: each op_que_events must carry a fresh event id. An interest is one shot, and re-arming under the id of an interest that has already been delivered is accepted by the server but not honoured immediately - counts accumulated while nobody waited were only delivered when the next event fired, one delivery late. The stock client increments its id on every queue, and doing the same made Test 10's output match the stock provider reference line for line. The transport gains an Abort method (socket shutdown without close) so that the listener's blocking read can be released from another thread when the attachment disconnects. Test 10 gains a settling sleep before one output line so that the report order is deterministic; WireTest gains an events section (delivery, deferred counts, cancellation) and now measures 89 tests, and the wire reference log is regenerated with Test 10's full output - byte identical across three consecutive suite runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first CI run of the suite job showed that the reference log is architecture flavoured: FPC's float to text rendering differs in the last digit between x86_64 and ARM, and the implementation id, security class naming and page counts are environment values. The reference is now the normalised output of the CI job itself - the environment that enforces it - captured from the wire-suite-testout artifact, and the documentation records how to regenerate it after an intended output change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first CI run of the events milestone hung: the server opens a random port for the auxiliary event connection, and a container that only publishes 3050 silently blackholes it - the client blocked in connect for minutes and the server logged a select timeout on its side. The containers now pin the port with RemoteAuxPort=3051 and publish it, which is the standard operational answer for events through firewalls and NAT, and the documentation says so. WireTest treats an unreachable auxiliary port as a SKIP with a pointer to RemoteAuxPort instead of dying with an unhandled exception. The same run showed more run dependent values in the suite output than the first normalisation pass caught: per table operation counters, server memory, the database creation date (the employee database is restored afresh in CI, so its creation date is the run date), the total page count, the server version string (the 6-snapshot container updates over time), the implementation codes and the security class name. runtest.sh now normalises all of them on both sides of the comparison and the reference log is stored in that form. The reference log itself is the milestone 1 CI (x86_64) output with Test 10's section replaced by the events output, which contains no architecture sensitive text. Test 10 gains bounded polls with a settling CheckSynchronize where it previously relied on fixed sleeps, so that the report line order no longer depends on the load of the test host - six consecutive runs now reproduce the reference section exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first suite job rerun showed more values that change between CI runs than the first normalisation pass caught: per table operation counters, server memory, the total page count, the employee database creation date (it is restored afresh in CI, so its creation date is the run date), the server version string (the 6-snapshot container updates over time), the implementation codes and the security class name. runtest.sh now normalises all of them on both sides of the comparison and the reference log is stored in that form. Also taken from the events milestone so that the files stay identical across the two branches: the CI containers pin the auxiliary event port with RemoteAuxPort=3051 and publish it, and Test 10 polls with a settling CheckSynchronize where it previously relied on fixed sleeps, so its report line order does not depend on the load of the test host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wire-suite-testout artifact of the batch CI run, unchanged. Three kinds of difference from the previous reference. The batch skip lines of Tests 19 and 20 are replaced by the tests' real output - the milestone showing up in the log. The parameter metadata lines change with the default text type: SQL_TEXT becomes SQL_VARYING with the described subtype and size, matching what the fbclient providers report for the same statements. And the parameter value echoes lose their blank padding - Test 12's INCLEAR hex dumps and CHAR echoes now match the fbclient reference logs byte for byte, where the old wire output had diverged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ap/09-inline-blobs
The connection now offers protocol 19. At 19 op_execute/op_execute2 carry the inline blob size limit the client accepts - taken from the existing IAttachment.GetInlineBlobLimit (default 8KB, zero opts out) - and the server pushes each qualifying blob as an op_inline_blob packet ahead of the row that references it: transaction handle, blob id, the standard blob info response, and the whole blob as one segmented stream, the same two byte length prefixed form op_get_segment returns. ReadOperation intercepts the packets at the same point that already skips op_dummy and op_response_piggyback, and hands them to the attachment through an OnInlineBlob callback, keeping the protocol unit free of provider types. The attachment caches them keyed by transaction handle and blob id, bounded at 16MB - beyond the cap pushes are dropped and the blob opens the classic way, a round trip but never an error. Entries are consumed on open, mirroring the server sending each id once and the engine consuming batch blob registrations the same way, and a transaction's entries are discarded when it ends, including the retaining forms. TFBWireBlob consults the cache before op_open_blob2: on a hit the open, every Read and GetInfo happen with no wire traffic at all - the pushed info buffer carries exactly the four items every fbintf GetInfo caller asks for and is served verbatim. The transport gains a PacketsSent counter (one per socket write, i.e. per round trip) which the new WireTest inline blob section uses to prove the point: a small blob read after the fetch adds zero packets, an over-limit blob and a zero limit both fall back to the classic exchanges, and all three read back intact - 158 tests. The feature is transparent by design and measurement: the full suite diff against the previous reference log is the negotiated version string, 18 to 19, and nothing else, so the reference is regenerated from the CI artifact in a follow up commit. The CI matrix now expects protocol 19 on Firebird 5 and 6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wire-suite-testout artifact of the inline blobs CI run, unchanged. The whole difference from the previous reference is the negotiated version in the getFBVersion output moving from 18 to 19 in its six occurrences: inline blobs change no other observable output, which is the milestone's transparency claim, now measured in the CI environment with the suite's small blobs actually travelling inline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…admap/10-protocol-20
The offer now goes to protocol 20, which Firebird 6 accepts. The field by field audit of protocol.cpp found exactly one unconditional packet change: p_sqlst_flags, a flags word at the end of op_prepare_statement and op_exec_immediate - they share the XDR block - written as zero since fbintf exposes no special prepare flags. Everything else is opt-in. The schema search path travels as an ordinary DPB item: isc_dpb_search_path joins the constants with the rest of the Firebird 6 DPB tags (97..107) and the DPB name table grows to match, so DPB.Add(isc_dpb_search_path).AsString works like any other item. On protocol 20 connections the describe item list gains isc_info_sql_relation_schema, parsed into the column format's new SchemaName - ParsePrepareResponse already skipped unknown items with their length, so the defensive property the design doc asked to verify was already in place. fbintf's IColumnMetaData has no schema member and the 3.0 provider surfaces none, so the name is stored rather than surfaced; tests reach it through TFBWireStatement.ColumnSchemaName and an interface member can follow when fbintf grows one. Named arguments turned out to be a SQL level feature, not a describe change - nothing to do on the wire. WireTest gains a schema section - two schemas with a same named table, an attachment whose search path picks the second, a describe assertion on the per column schema name, and a qualified name bypassing the path - and the negotiation ladder extends to 20: now 164 tests. The suite output is byte identical apart from the negotiated version string, so the reference log is regenerated from the CI artifact in a follow up commit, and the CI matrix expects protocol 20 on Firebird 6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wire-suite-testout artifact of the protocol 20 CI run, unchanged. As with the inline blobs milestone, the whole difference from the previous reference is the negotiated version in the getFBVersion output, 19 to 20 in its six occurrences: the schema aware describe and the prepare flags word change nothing the suite can observe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dmap/11-wire-compression
Requested per attachment with an isc_dpb_config item of WireCompression=true - the same knob the stock client reads, so application code stays provider portable - and effective when the server also enables it: pflag_compress rides on each offered protocol entry's max type field, and the accept's echo is captured before the ptype_mask masking that used to discard it unseen. Compression is a property of the byte stream, not of packets: one zlib stream per direction for the life of the session, using FPC's pure Pascal paszlib so the no-external-dependencies property survives. The streams start immediately after the accept packet has been read in full - the packet itself travels clear, and an op_accept_data payload is part of it - so the remaining handshake, op_crypt included, is compressed. Each packet flush deflates with Z_SYNC_FLUSH: a request/response protocol deadlocks if complete packets do not reach the peer promptly. The pipeline keeps the cipher against the socket - deflate then encrypt on send, decrypt then inflate on receive - so the op_crypt changeover logic is untouched. op_cancel's out of band packet routes through the same deflate state under the existing send lock: the peer inflates one continuous stream and raw bytes must never bypass it. The receive side keeps ReadBytes and the plaintext buffer semantics untouched by staging the not yet inflated stream in a second buffer inside FillRecvBuffer, and HasBufferedData counts those pending bytes, keeping the cipher changeover guard and event polling honest. With compression off the code path is the previous one verbatim: the suite reference log needed no regeneration for the first time since the suite milestone. WireTest gains a compression section - a second attachment negotiates it, then round trips a bulk query and a blob through the compressed (and encrypted) stream - now 167 tests, all passing against the local Firebird 6, which accepts compression out of the box. The CI matrix gains a dedicated WireCompression = true row on Firebird 5, made a new combination rather than an extension by naming the wirecompression axis explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o roadmap/12-message-text
generate_messages.py reads the message headers of a pinned Firebird tree - src/include/firebird/impl/msg, the modern home of what messages2.sql used to hold - and emits the committed FBWireMessages unit: 1682 messages for the facilities a server round trip can produce (JRD, DSQL, DYN, SQLERR, SQLWARN), each with its fb_interpret format string and its SQLCODE, behind a binary search. About 200KB of binary, and the provider still depends on nothing but itself. FormatWireStatus renders a decoded status vector the way fb_interpret does - each isc_arg_gds item's format string with @1..@n arguments substituted, interpreted items verbatim, successive items joined with a "-" continuation line - and becomes the one formatter behind both EIBInterBaseError messages and the protocol level exception, replacing the two numeric fallbacks. SetFromWireStatus now keeps the decoded vector with its argument structure intact instead of flattening the strings away. The SQLERR facility turned out to carry more than the plan knew: the "At line @1, column @2" companion of every DSQL error, and the per SQLCODE interpretation texts isc_sql_interprete looks up at 1000+sqlcode. With those, the wire status object also answers Getsqlcode (the gds__sqlcode rules: an isc_sqlerr item's number argument wins outright, else the first item's mapping decides) and GetSQLMessage client side, so the full fbclient error shape - SQLCODE line, SQL message, engine code and message text - now appears over the wire. The base class's SQLCODE methods became virtual to allow the override. Parity testing against the 3.0 provider on the same server exposed two behavioural gaps beyond message text, both closed: prepare errors now append " When Executing: <sql>" as the other providers do, and the stale reference checks were missing entirely - a prepared statement used across a transaction restart now raises ibxeInterfaceOutofDate exactly as the 3.0 provider does. Test 16's output is now identical to fbclient apart from the invalid-server-name section the test skips for the wire provider by design. WireTest gains offline assertions - the table lookup, a parameterised format, and a two item vector formatted byte for byte - now 171 tests. The suite reference log is regenerated from the CI artifact in a follow up commit: its numeric Engine Code fallbacks give way to the real text everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wire-suite-testout artifact of the message text CI run, unchanged. Every difference from the previous reference is an error message upgrade: numeric Engine Code fallbacks give way to fb_interpret text, exception reports gain their message and stack lines, and the SQLCODE preamble appears where the fbclient providers show one - the wire provider's error output now reads as theirs does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap 01: full test suite over the wire provider (design + implementation)
Roadmap 02: events for the wire protocol provider (design + implementation)
Roadmap 05: Array columns for the wire protocol provider (design + implementation)
Roadmap 06: Statement timeouts and cancellation (design + implementation)
Roadmap 07: Scrollable cursors for the wire protocol provider (design + implementation)
Roadmap 08: The batch API for the wire protocol provider (design + implementation)
Roadmap 09: Inline blobs for the wire protocol provider (design + implementation)
Roadmap 10: Firebird 6 protocol 20 for the wire provider (design + implementation)
Roadmap 11: Wire compression for the wire protocol provider (design + implementation)
Roadmap 12: Engine message text for the wire provider (design + implementation)
…services-land # Conflicts: # client/wire/FBWireClientAPI.pas # client/wire/FBWireStream.pas # client/wire/README.md # doc/WireProtocol.md # fbintf.lpk # fbintf.pas # testsuite/WireTest.pas
…chain Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap 03: Services for the wire protocol provider (design + implementation)
TFBWireTransport now branches per compiler inside the same four methods, per doc/roadmap/04-delphi-transport.md: Winapi.Winsock2 on Windows (one time WSAStartup behind a unit variable) and the Posix.* units elsewhere; getaddrinfo preferring an IPv4 result to match the FPC resolver, TCP_NODELAY, SO_RCVTIMEO/SO_SNDTIMEO from aTimeout, recv/send, and a graceful shutdown in Disconnect. The wire units and FBSDL are listed in fbintf.dpk and fbintf.dproj. Written without a Delphi toolchain: nothing here has been compiled by Delphi, and the docs say so. Wire compression stays FPC only (paszlib); EnableCompression raises a clear error under Delphi. The FPC branch is untouched and WireTest still reports 173 tests, 0 failures locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap 04 design: A Delphi transport
The full suite job now exercises the services API for real instead of reporting it unsupported, so the reference log gains that output. Taken from the CI x86_64 wire-suite-testout artifact of the landing run, as always. The services output includes the raw server version string, which the comparison did not normalise - it would have broken on the next container update - so runtest.sh gains a Server Version rule alongside the existing Implementation one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Update: the roadmap has been implemented — this branch now carries the full feature setSince this PR was opened, the twelve milestones from the roadmap in
What that means for the providerThe gaps listed under "not implemented yet" in the original description are closed. The provider now negotiates protocols 13 through 20 and implements events, services, arrays, statement timeouts and cancellation, scrollable cursors, the batch API, inline blobs, schema support on Firebird 6, optional zlib wire compression beneath the wire encryption, and Testing
Documentation
Two things reviewers should know
As before: offered for consideration, and if the direction is welcome but any of the packaging or the shared-interface additions are not, I am happy to rework them. |
Adds a pure Pascal implementation of the Firebird remote (wire) protocol, so
fbintf can reach a Firebird 3.0, 4.0, 5.0 or 6.0 server over TCP with no
fbclientlibrary installed. It is a third provider alongside the existingtwo, so code written against the fbintf interfaces runs unchanged.
Offered for consideration rather than as a finished proposal: it is a large
addition and it touches shared build files, so if the direction is welcome
but the packaging is not, I am happy to rework it. Everything new lives under
client/wire; the existing providers are untouched.Everything below that line is the ordinary object model:
IAttachment,ITransaction,IStatement,IResultSet,IBlob.Why
client/2.5andclient/3.0both dynamically loadfbclient. That is theright answer on a developer machine and an awkward one for containers,
single file deployments, cross compiled targets, and any machine whose
installed client library is older than the server it has to reach. This
provider implements the protocol those libraries speak, so the dependency
disappears: the compiled program is the whole client.
What works
Srp256andSrpauthentication, in both theop_cond_acceptflow(authentication completes before the attach) and the
op_accept_dataflow (the proof travels in the DPB)
ChaCha64,ChaCha,Arc4op_execute2forsingleton results, fetch, close, drop, set cursor name
INT128,DECFLOAT(16),DECFLOAT(34),BOOLEANand the time zone typesThe password is used for the SRP exchange and stripped from the DPB before
the attach, so it never reaches the network in any form.
What does not, yet
Each raises
ibxeNotSupportedrather than failing obscurely:op_connect_requestand a listener threadFBWireProtocol; theIServiceManagerwrapper does notop_get_slice/op_put_slicewith SDL descriptionsop_batch_*familyop_fetch_scroll, protocol 18FPC only for now. The transport is written against the FPC socket units;
everything above it is compiler neutral, so a Delphi transport is the only
missing piece. The Delphi packages are deliberately left untouched.
Layout
Thirteen units under
client/wire, in three layers:FBWireStream(buffered TCP, per direction ciphers, XDR),FBWireCrypto,FBWireSRP,FBWireBigInt,FBWireConstFBWireProtocol(handshake, one method per packet),FBWireMessage,FBWireDescribeFBWireClientAPI,FBWireAttachment,FBWireTransaction,FBWireStatement,FBWireBlobFBWireProtocolis usable on its own if you want the protocol without thefbintf object model.
The provider reuses the existing machinery rather than duplicating it:
FBParamBlockalready builds exactly the clumplet buffers the protocolcarries,
FBOutputBlockalready parses the information responses, andTSQLDataItemsupplies every data conversion — the provider only points itat the right offset in the message buffer.
Testing
testsuite/WireTest.pasis self contained and needs no fbclient:fpc -Fuclient -Fuclient/2.5 -Fuclient/3.0 -Fuclient/3.0/firebird \ -Fuclient/wire -Ficlient/include -FEbuild testsuite/WireTest.pas ./build/WireTest localhost:employee SYSDBA masterkey localhost:/tmp/scratch.fdbFour offline layers that need no server — big integer arithmetic including a
1024 bit modular exponentiation of the size SRP performs, SHA-1, SHA-256,
RC4 and ChaCha20 against the published FIPS and RFC 8439 vectors, a complete
SRP exchange against a fixed reference exchange, and the message layout and
generated BLR — then the live ones: the handshake, what version is
negotiated when the offer is capped at 14/15/16/17, queries and parameters,
every data type, DML, null handling, accented text, blob round trips,
rollback, and create and drop database.
Every supported server version has been run, all with 0 failures:
ChaCha64ChaCha64ChaCha64ChaCha64ChaCha64Arc4Firebird 3 settles on protocol 15 with Arc4 because that is the newest
protocol it knows and it predates the ChaCha plugins. Everything else in
the suite behaves identically there.
Capping the offer, against a modern server: 14 →
Arc4, 15 →Arc4,16 →
ChaCha64, 17 →ChaCha64. Protocol 13 is refused withisc_miss_wirecryptby a server that requires encryption, which is correct:it predates wire encryption. It is still offered for Firebird 3 and for
servers set to
WireCrypt = Enabled.CI
.github/workflows/wire-protocol.yml, modelled on the node-firebird matrix.The server job runs Firebird 3, 4, 5 and 6 containers against both
WireCrypt = EnabledandWireCrypt = Required— seven combinations.Firebird 3 with
Requiredis excluded because protocol 13 cannot satisfy itby definition. Each job starts the container, creates a database with
isqlinside it, builds with the distribution's FPC and runs the test over TCP.
Nothing Firebird related is installed on the runner, which is the whole
point of the client. The negotiated protocol and the totals go to the step
summary, so the matrix view shows what each version settled on.
The offline job builds and runs the same binary on Linux and Windows
with no server at all, so an arithmetic or cryptography regression cannot be
masked by a server problem and the code keeps compiling on Windows.
All nine jobs are green on this branch.
The matrix earned its keep on the first run. It found two things: the
negotiation test wrongly demanded that capping the offer at protocol N make
the server negotiate exactly N, which is false for a server that does not
know N (Firebird 3 tops out at 15); and, from a compiler warning the matrix
surfaced, that the character set of a text column was never being read.
if SQLType in [SQL_TEXT,SQL_VARYING]compares against a Pascal set, whichholds 0..255, while the type codes are 452 and 448 — so the test matched
truncated values, never fired, and every text column fell back to character
set NONE. That was masked locally because the sample database is character
set NONE. Both are fixed, and a UTF8 column now correctly reports character
set 4.
Roadmap
doc/WireProtocol.mdcarries a twelve point roadmap saying what each missingpiece needs and what already exists to support it. The order it proposes:
TTestApplicationop_connect_request, a second socket, a listener threadIServiceManagerwrapper over exchanges that already existTFBWireTransportover Winsock and Posix socketsop_get_slice,op_put_slice, SDL generationop_cancel, the P16 timeout fieldop_fetch_scrollop_batch_*familyop_inline_blobfirebird.msgreader or a generated tableThe first is deliberately first:
testsuite/already contains twenty twoprograms written against the interfaces rather than against a provider, and
they exercise far more of the API than
WireTestdoes. A switch inTTestApplication.GetFirebirdAPIwould run all of them over the wire, andthat run is then the acceptance criterion for the events, services and array
milestones. I did not add the switch here because it changes shared test
code and seemed better raised than assumed.
Deliberately not planned, with reasons in the document:
Legacy_Auth(itproduces no session key, so it cannot satisfy a server that requires
encryption),
Win_SSPI, and protocols below 13.Protocol details worth recording
These are the places where a plausible reading of the packet layouts
produces a client that does not work. Each cost a debugging cycle and each
is documented in
doc/WireProtocol.mdand commented where it isimplemented.
op_cryptkey type is the type the server advertised, in practiceSymmetric— not the authentication plugin name. Sending the plugin namegets the connection closed with "unknown key".
blr_text2/blr_varying2namingthe column's character set. With plain
blr_text/blr_varyingtheengine assumes the connection character set and reinterprets the byte
length as
length div max bytes per character: aVARCHAR(37)read over aUTF8 connection silently becomes 9 characters and the fetch fails with a
string truncation error.
big endian integer the value lands in the last byte and every boolean reads
back false.
op_fetchmust be drained completely before anything else issent; the server streams one
op_fetch_responseper row and ends the batchwith a message count of zero. Rows are cached as they arrive.
ISC_QUADputs the high word first, which is not the memory layout ofan
Int64on a little endian machine.N,
H(N) xor H(g)is a modular exponentiation, the salt is hashed as itshexadecimal text, and the session key is always SHA-1 even for
Srp256.arch_genericmust be sent in the version entries: matching theserver's own architecture makes it transmit messages as raw memory images
instead of XDR.
Bugs found and fixed while bringing the provider up against a live server,
all of which are now covered by tests: parameters were all being sent as
null (the encoder reads the buffer's null indicator, so
IsNullmust read itdirectly rather than gate on the column's nullable flag); a
VARCHARvaluewas overwriting its own two byte length prefix;
CHARvalues were not blankpadded, so a parameter never compared equal to a padded column; closing a
cursor whose transaction had ended was treated as an error when the server
has already closed it.
One behaviour worth flagging
An attachment that has read a table keeps an interest in it that outlives
the transaction, and Firebird 5 then refuses
drop tableon that sameattachment with
isc_no_meta_update. This is not a property of thisclient: the stock fbclient provider fails in the same place with the same
code, and a second attachment issuing the drop blocks rather than
succeeding.
WireTesttherefore treats dropping its test table as besteffort and drops it at the start of the next run.
Error messages
firebird.msgis a client library resource, so it is not available. Thestatus vector is decoded and rebuilt into a standard ISC vector, so
EIBInterBaseError,GetIBErrorCodeandCheckStatusVectorbehave asusual, and the interpreted text the server supplies is used where it is
present. Where the stock provider says "unsuccessful metadata update" this
one says
Firebird Error Code: 335544351. Closing that gap needs either afirebird.msgreader or a generated table of the common codes.Documentation
doc/WireProtocol.md— design, protocol details, SRP, encryption, messageencoding, error reporting, testing and CI
client/wire/README.md— status and the measured resultsREADME.md— a short section pointing at bothCommits
Notes for review
The reference throughout was the Firebird server's own C++ implementation:
src/remote/protocol.h,protocol.cpp,interface.cppandsrc/auth/SecureRemotePassword. Nothing here is derived from anotherclient; the SRP test vectors were computed independently and cross checked
against a client/server pair rather than taken from an existing
implementation.
Worth a careful look: the message buffer layout in
FBWireMessage(alignment rules and the null bitmap), the lifetime handling in
FBWireStatementwhere the base class prepares lazily, and the encryptionchangeover in
TFBWireConnection.StartWireEncryption, which is asymmetric —the receive cipher is installed before the
op_cryptresponse is readbecause the server encrypts from the moment it receives the request.
Changes outside
client/wire, all additive:fbintf.lpkandMakefile.fpc—client/wireadded to the unit searchpath, and the thirteen units added to the package file list
fbintf.pas— the units added to theusesclauseREADME.md— a short section pointing at the new documentationtestsuite/WireTest.pasand.github/workflows/wire-protocol.yml— newfiles
The Delphi packages (
fbintf.dpk,fbintf.dproj) are deliberatelyuntouched, because the transport is FPC only for now. If you would rather
the wire units were not compiled into the default package at all, dropping
them from
fbintf.pasandfbintf.lpkleaves them available on the searchpath for anyone who wants them and changes nothing for anyone who does not.