zeek/src/analyzer/protocol/postgresql/postgresql.spicy
2025-01-09 08:38:45 -05:00

335 lines
10 KiB
Text

# See the file "COPYING" in the main distribution directory for copyright.
#
# A PostgreSQL analyzer.
#
# https://www.postgresql.org/docs/current/protocol.html
#
# Protocol version 3.0
module PostgreSQL;
import spicy;
type SSLFrontendState = enum {
Requested,
NotRequested,
};
type SSLBackendState = enum {
S,
N,
};
# How many chunks to buffer initially when seeing a backend message
# before a frontend or vice versa.
const MAX_BUFFERED = 4;
# When a connection switches to SSL, this consumes all the SSL chunks.
# In zeek_postgres.spicy, SSLSink%init calls zeek::protocol_begin() and
# then zeek::protocol_data_in()
#
# There's a single SSLSink shared between backend and frontend.
type SSLSink = unit {
chunk: bytes &chunked &eod;
};
# Used as context for synchronization between frontend/backend.
type Context = struct {
ssl_frontend_state: SSLFrontendState;
ssl_backend_state: SSLBackendState;
ssl_sink: sink&;
ssl_sink_connected: bool;
};
type ProtocolVersion = unit {
major: uint16;
minor: uint16;
};
type StartupParameter = unit {
name: /[-_\/A-Za-z0-9]+/ &requires=(|$$| > 0);
: uint8 &requires=($$ == 0);
value: /[\x20-\x7e]+/ &requires=(|$$| > 0);
: uint8 &requires=($$ == 0);
};
type StartupMessage = unit {
length: uint32 &requires=(self.length >= 9);
version: ProtocolVersion &requires=($$.major == 3);
parameters: StartupParameter[] &size=self.length - 9;
: skip b"\x00";
};
# Top-level entry for the client.
public type FrontendMessages = unit {
%context = Context;
on %init {
# Until the first FrontendMessages are initialized, ssl_sink in the
# context is a Null reference. Also, we want to use a single sink
# for both, frontend and backend by calling begin_protocol() within
# the SSLSink's %init hook (see postgresql_zeek.spicy).
self.context().ssl_sink = self.s1;
}
var buffered: vector<bytes>;
var s1_connected: bool;
var ssl_requested: bool;
sink s1;
# Peek at the client data.
length: uint32 &requires=(self.length >= 8);
version_or_magic: uint32 {
self.ssl_requested = self.length == 8 && $$ == 80877103;
if (self.ssl_requested) {
self.context().ssl_frontend_state = SSLFrontendState::Requested;
} else {
self.context().ssl_frontend_state = SSLFrontendState::NotRequested;
self.context().ssl_backend_state = SSLBackendState::N;
# Pre-check the supported major version here.
local major = $$ >> 16;
if (major != 3)
throw "unsupported PostgreSQL major version %s" % major;
# Put length and version back into the buffer so PlainFrontendMessages
# can re-parse it.
#
# This explicitly avoids using random access functionality like
# `self.input()` and `self.set_input()` which would disable automatic
# trimming in this unit (which is top-level unit parsing unbounded
# amounts of data).
self.buffered.push_back(pack(self.length, spicy::ByteOrder::Network));
self.buffered.push_back(pack(self.version_or_magic, spicy::ByteOrder::Network));
}
}
# void field for raising an event.
ssl_request: void if(self.ssl_requested == True);
# print "frontend ssl", self.context();
# If the client requested SSL, we do not know how to continue parsing
# until the server confirmed SSL usage via 'S' or 'N' responses. As long
# as it hasn't responded, stall the parsing here and buffer bytes until
# the context() is populated.
#
# In normal operations, Zeek should see the server's response before
# attempting to parse more data, but Robin was concerned that in some
# circumstances (out-of-order packets, reassembly artifacts) we may
# see the client's data before the server's.
#
# In the future, barrier: https://github.com/zeek/spicy/pull/1373
: bytes &chunked &eod {
if (!self.context().ssl_backend_state) {
self.buffered.push_back($$);
if (|self.buffered| > MAX_BUFFERED)
throw "too many frontend messages buffered";
} else {
# print "frontend ssl_state backend set!", self.context();
if (!self.s1_connected) {
if (self.context().ssl_backend_state == SSLBackendState::N) {
self.s1.connect(new PlainFrontendMessages());
} else {
assert (self.context().ssl_sink_connected);
assert (self.context().ssl_backend_state == SSLBackendState::S);
}
self.s1_connected = True;
if (|self.buffered| > 0) {
for (b in self.buffered)
self.s1.write(b);
}
self.buffered.resize(0);
}
self.s1.write($$);
}
}
};
type PlainFrontendMessages = unit {
startup_message: StartupMessage;
: FrontendMessage[];
};
type FrontendMessage = unit {
typ: uint8;
length: uint32 &requires=(self.length >= 4);
switch (self.typ) {
'p' -> : AuthenticationResponse;
'X' -> : Terminate;
'Q' -> : SimpleQuery;
* -> not_implemented: NotImplemented(self.typ);
} &size=self.length - 4;
};
type AuthenticationResponse = unit {
# This is PasswordMessage, SASLInitialMessage, etc. based on context.
# For now, just thread it through.
data: bytes &eod;
};
type Terminate = unit {};
type SimpleQuery = unit {
query: bytes &until=b"\x00";
};
# The client has requested SSL, the server either confirms (S) or
# denies (N). Depending on the result, the ssl_sink in the context
# is connected with a SSLUnit and used, or a sink connected with the
# PlainBackendMessages unit.
#
type MaybeBackendSSL = unit(ctx: Context&) {
# Connected to SSLSink or plaintext messages.
sink s1;
ssl_byte: uint8 &requires=($$ == 'S' || $$ == 'N') {
# print "backend ssl_byte", $$;
if ($$ == 'S') {
ctx.ssl_backend_state = SSLBackendState::S;
ctx.ssl_sink.connect(new SSLSink());
ctx.ssl_sink_connected = True;
# Share the SSL sink with the frontend.
self.s1 = ctx.ssl_sink;
} else {
ctx.ssl_backend_state = SSLBackendState::N;
self.s1.connect(new PlainBackendMessages());
}
}
# Now that s1 is connected, forward the rest of the connection to it.
: bytes &chunked &eod -> self.s1;
};
# Top-level entry for the server.
public type BackendMessages = unit {
%context = Context;
var buffered: vector<bytes>;
var s1_connected: bool;
sink s1;
# Buffer until the SSL frontend state was populated.
: bytes &chunked &eod {
if (!self.context().ssl_frontend_state) {
# print "backend buffering ", |$$|;
self.buffered.push_back($$);
if (|self.buffered| > MAX_BUFFERED)
throw "too many backend messages buffered";
} else {
# The ssl_frontend_state has been set. If The client requested SSL,
# connect to an SSLMaybe instance. If it did not, connect
# directly to PlainBackendMessages.
# print "backend", self.context(), |self.buffered|, self.s1, self.s1_connected;
if (!self.s1_connected) {
if (self.context().ssl_frontend_state == SSLFrontendState::Requested) {
self.s1.connect(new MaybeBackendSSL(self.context()));
} else {
self.s1.connect(new PlainBackendMessages());
}
self.s1_connected = True;
if (|self.buffered| > 0) {
for (b in self.buffered)
self.s1.write(b);
}
self.buffered.resize(0);
}
# print "backend writing to sink", $$, |self.s1|;
self.s1.write($$);
}
}
};
type PlainBackendMessages = unit {
: BackendMessage[];
};
type BackendMessage = unit {
typ: uint8;
length: uint32 &requires=(self.length >= 4);
switch (self.typ) {
'K' -> backend_key_data: BackendKeyData;
'E' -> error: ErrorResponse;
'R' -> auth: AuthenticationRequest(self.length - 4);
'S' -> parameter_status: ParameterStatus;
'D' -> data_row: DataRow;
'Z' -> ready_for_query: ReadyForQuery;
'N' -> notice: NoticeResponse;
* -> not_implemented: NotImplemented(self.typ);
} &size=self.length - 4;
};
type ParameterStatus = unit {
name: /[-_\/A-Za-z0-9]+/ &requires=(|$$| > 0);
: uint8 &requires=($$ == 0);
value: /[\x20-\x7e]+/ &requires=(|$$| > 0);
: uint8 &requires=($$ == 0);
};
# Possible values are 'I' if idle (not in a transaction block);
# 'T' if in a transaction block; or 'E' if in a failed transaction block
# (queries will be rejected until block is ended).
type ReadyForQuery = unit {
transaction_status: uint8 &requires=($$ == 'I' || $$ == 'T' || $$ == 'E');
};
type NoticeIdentifiedField = unit {
code: uint8;
value: bytes &until=b"\x00";
};
type NoticeResponse = unit {
: NoticeIdentifiedField[];
: skip b"\x00";
};
# Just for counting right now.
type DataRow = unit {
column_values: uint16;
: skip bytes &eod;
};
# Fields with a 1 byte field as documented here:
# https://www.postgresql.org/docs/current/protocol-error-fields.html
type ErrorIdentifiedField = unit {
code: uint8;
value: bytes &until=b"\x00";
};
type ErrorResponse = unit {
: ErrorIdentifiedField[];
: skip b"\x00";
};
type AuthenticationRequest = unit(length: uint32) {
identifier: uint32 &requires=($$ <= 12) {
if (self.identifier == 0 && length != 4)
throw "AuthenticationOK with wrong length: %s" % length;
}
# There's more structure (GSS-API, SASL, cleartext), but for now
# just thread through the raw data.
data: bytes &eod;
};
type BackendKeyData = unit {
process_id: uint32;
secret_key: uint32;
};
type NotImplemented = unit(typ: uint8) {
chunk: bytes &eod;
};