-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpgsql.h
More file actions
1404 lines (1277 loc) · 49.7 KB
/
pgsql.h
File metadata and controls
1404 lines (1277 loc) · 49.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* @file pgsql.h
* @brief PostgreSQL client for the QB Actor Framework
*
* This file implements an asynchronous PostgreSQL client integrated with the QB Actor
* Framework. It provides a non-blocking interface for database operations such as:
*
* - Connection management to PostgreSQL databases
* - Transaction management (begin, commit, rollback)
* - Support for savepoints within transactions
* - Simple and prepared statement execution with parameter binding
* - Efficient query result retrieval and processing
* - Support for multiple authentication methods (MD5, SCRAM-SHA-256, etc.)
*
* The implementation is designed to work with the actor model, allowing
* database operations to be performed without blocking actor threads. The client
* fully implements the PostgreSQL wire protocol for efficient communication.
*
* Key features:
* - Asynchronous I/O using the QB Actor Framework
* - Support for both plain TCP and SSL/TLS connections
* - Comprehensive transaction management
* - Prepared statement caching for performance
* - Detailed error reporting and handling
*
* @see qb::pg::detail::Database
* @see qb::pg::detail::Transaction
*
* @author qb - C++ Actor Framework
* @copyright Copyright (c) 2011-2025 qb - isndev (cpp.actor)
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <memory>
#include <qb/io/async.h>
#include <qb/io/async/tcp/connector.h>
#include <qb/io/crypto.h>
#include <qb/system/allocator/pipe.h>
// P1-1: Socket includes for keepalive support
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#endif
#include "./src/commands.h"
#include "./src/transaction.h"
/**
* @brief Maximum length for attribute names in PostgreSQL protocol
*
* Defines the maximum length in bytes for attribute names when parsing
* PostgreSQL protocol messages. This limit helps prevent buffer overflow
* attacks and ensures efficient memory usage.
*/
constexpr const uint32_t ATTRIBUTE_NAME_MAX = 1024; // 1 KB
/**
* @brief Maximum length for attribute values in PostgreSQL protocol
*
* Defines the maximum length in bytes for attribute values when parsing
* PostgreSQL protocol messages. This larger limit accommodates typical
* PostgreSQL data values while preventing excessively large allocations.
*/
constexpr const uint32_t ATTRIBUTE_VALUE_MAX = 1024 * 1024; // 1 MB
/**
* @brief Checks if a character is a control character
*
* Used during attribute parsing to validate input and ensure security.
* Control characters are generally not allowed in attribute names or values
* as they may indicate malformed or malicious input.
*
* @param c Character to check
* @return true if the character is a control character (ASCII 0-31 or 127)
*/
inline bool
is_control(int c) {
return ((c >= 0 && c <= 31) || c == 127);
}
/**
* @brief Parses header attributes from a PostgreSQL protocol message
*
* Parses a buffer of header attributes into a case-insensitive map.
* This function is primarily used during SCRAM authentication to process
* challenge-response data between client and server.
*
* Features:
* - Supports both quoted and unquoted attribute values
* - Handles attribute separators (comma and semicolon)
* - Enforces size limits to prevent buffer overflows
* - Validates input to reject control characters
* - Properly handles whitespace according to the PostgreSQL protocol
*
* @param ptr Pointer to the buffer containing attributes
* @param len Length of the buffer
* @return Case-insensitive map of attribute names to values
* @throws std::runtime_error If parsing fails due to control characters or exceeding
* size limits
*/
inline qb::icase_unordered_map<std::string>
parse_header_attributes(const char *ptr, const size_t len) {
qb::icase_unordered_map<std::string> dict;
enum AttributeParseState {
ATTRIBUTE_PARSE_NAME,
ATTRIBUTE_PARSE_VALUE,
ATTRIBUTE_PARSE_IGNORE
} parse_state = ATTRIBUTE_PARSE_NAME;
// misc other variables used for parsing
const char *const end = ptr + len;
std::string attribute_name;
std::string attribute_value;
char value_quote_character = '\0';
// iterate through each character
while (ptr < end) {
switch (parse_state) {
case ATTRIBUTE_PARSE_NAME:
// parsing attribute name
if (*ptr == '=') {
// end of name found (OK if empty)
value_quote_character = '\0';
parse_state = ATTRIBUTE_PARSE_VALUE;
} else if (*ptr == ';' || *ptr == ',') {
// ignore empty attribute names since this may occur naturally
// when quoted values are encountered
if (!attribute_name.empty()) {
// value is empty (OK)
dict.emplace(attribute_name, attribute_value);
attribute_name.erase();
}
} else if (*ptr != ' ') { // ignore whitespace
// check if control character detected, or max sized exceeded
if (is_control(*ptr) || attribute_name.size() >= ATTRIBUTE_NAME_MAX)
throw std::runtime_error(
"ctrl in name found or max attribute name length");
// character is part of the name
attribute_name.push_back(*ptr);
}
break;
case ATTRIBUTE_PARSE_VALUE:
// parsing attribute value
if (value_quote_character == '\0') {
// value is not (yet) quoted
if (*ptr == ';' || *ptr == ',') {
// end of value found (OK if empty)
dict.emplace(attribute_name, attribute_value);
attribute_name.erase();
attribute_value.erase();
parse_state = ATTRIBUTE_PARSE_NAME;
} else if (*ptr == '\'' || *ptr == '"') {
if (attribute_value.empty()) {
// begin quoted value
value_quote_character = *ptr;
} else if (attribute_value.size() >= ATTRIBUTE_VALUE_MAX) {
// max size exceeded
throw std::runtime_error("max attribute size");
} else {
// assume character is part of the (unquoted) value
attribute_value.push_back(*ptr);
}
} else if (*ptr != ' ' ||
!attribute_value
.empty()) { // ignore leading unquoted whitespace
// check if control character detected, or max sized exceeded
if (is_control(*ptr) ||
attribute_value.size() >= ATTRIBUTE_VALUE_MAX)
throw std::runtime_error(
"ctrl in value found or max attribute value length");
// character is part of the (unquoted) value
attribute_value.push_back(*ptr);
}
} else {
// value is quoted
if (*ptr == value_quote_character) {
// end of value found (OK if empty)
dict.emplace(attribute_name, attribute_value);
attribute_name.erase();
attribute_value.erase();
parse_state = ATTRIBUTE_PARSE_IGNORE;
} else if (attribute_value.size() >= ATTRIBUTE_VALUE_MAX) {
// max size exceeded
throw std::runtime_error("max attribute value length");
} else {
// character is part of the (quoted) value
attribute_value.push_back(*ptr);
}
}
break;
case ATTRIBUTE_PARSE_IGNORE:
// ignore everything until we reach a comma "," or semicolon ";"
if (*ptr == ';' || *ptr == ',')
parse_state = ATTRIBUTE_PARSE_NAME;
break;
}
++ptr;
}
// handle last attribute in string
dict.emplace(attribute_name, attribute_value);
return dict;
}
namespace qb::protocol {
/**
* @brief PostgreSQL protocol implementation for the QB actor framework
*
* Handles the message framing and parsing according to the PostgreSQL
* wire protocol specification. This class is responsible for:
*
* - Extracting complete messages from the input stream
* - Managing protocol state between messages
* - Forwarding complete messages to the appropriate handlers
* - Implementing the PostgreSQL message format requirements
*
* The protocol handler processes the incoming byte stream and constructs
* well-formed PostgreSQL protocol messages. It maintains internal state
* to handle partial messages that arrive in multiple network packets.
*
* @tparam IO_ I/O handler type that provides input/output stream access
*/
template <typename IO_>
class pgsql final : public qb::io::async::AProtocol<IO_> {
public:
/**
* @brief PostgreSQL protocol message type
*
* Represents a complete PostgreSQL protocol message including
* message type, length, and payload data.
*/
using message = std::unique_ptr<pg::detail::message>;
private:
message message_; ///< Current message being processed
std::size_t offset_ = 0; ///< Current offset in the input buffer
public:
pgsql() = delete;
/**
* @brief Constructs a PostgreSQL protocol handler
*
* Initializes the protocol handler with a reference to the I/O
* subsystem that provides access to input and output streams.
*
* @param io Reference to the I/O handler
*/
explicit pgsql(IO_ &io) noexcept
: qb::io::async::AProtocol<IO_>(io) {}
/**
* @brief Copy data from input iterator to output iterator
*
* Helper method to copy data between iterators with a maximum limit.
* Used internally for buffer management and message construction.
*
* @tparam InputIter Input iterator type
* @tparam OutputIter Output iterator type
* @param in Start of input range
* @param end End of input range
* @param max Maximum number of items to copy
* @param out Output iterator
* @return InputIter Iterator after the last copied element
*/
template <typename InputIter, typename OutputIter>
InputIter
copy(InputIter in, InputIter end, size_t max, OutputIter out) {
for (size_t i = 0; i < max && in != end; ++i) {
*out++ = *in++;
}
return in;
}
/**
* @brief Calculate the size of a complete PostgreSQL message
*
* Inspects the input buffer to determine if a complete message is available.
* This method implements the PostgreSQL message framing protocol by:
*
* 1. Reading the message type byte and length field
* 2. Creating a new message object if needed
* 3. Reading message payload data up to the expected length
* 4. Determining if the message is complete
*
* If a message is complete, returns its size in bytes. If incomplete,
* returns 0 to indicate more data is needed from the network.
*
* @return std::size_t Size of the complete message, or 0 if incomplete
*/
std::size_t
getMessageSize() noexcept final {
constexpr const size_t header_size =
sizeof(qb::pg::integer) + sizeof(qb::pg::byte);
const auto &in = this->_io.in();
if (in.size() < offset_ + header_size)
return 0; // read more
auto max_bytes = in.size() - offset_;
if (!message_) {
message_ = std::make_unique<pg::detail::message>();
// OPTIMIZED: Use std::copy_n for batch copy instead of byte-by-byte
// This provides ~10x performance improvement for large messages
auto header_begin = in.begin();
auto out = message_->output();
std::copy_n(header_begin, header_size, out);
offset_ += header_size;
max_bytes -= header_size;
}
if (message_->length() > message_->size()) {
// Read the message body
auto out = message_->output();
const std::size_t to_copy =
std::min(message_->length() - message_->size(), max_bytes);
// OPTIMIZED: Use std::copy_n for batch copy instead of byte-by-byte
auto data_begin = in.begin() + offset_;
std::copy_n(data_begin, to_copy, out);
offset_ += to_copy;
}
if (message_->length() == message_->size()) {
return message_->buffer_size();
}
return 0;
}
/**
* @brief Handle a complete PostgreSQL message
*
* Called by the protocol framework when a complete message has been
* received and parsed according to the PostgreSQL protocol rules.
*
* This method:
* 1. Validates the connection state
* 2. Resets the message read pointer
* 3. Forwards the complete message to the I/O handler for processing
* 4. Resets the protocol state to prepare for the next message
*
* @param size Size of the message (unused in this implementation)
*/
void
onMessage(std::size_t) noexcept final {
if (!this->ok())
return;
message_->reset_read();
this->_io.on(std::move(message_));
reset();
}
/**
* @brief Reset the protocol state
*
* Prepares the protocol handler for the next message by resetting
* internal state variables. This ensures that each new message is
* processed from a clean initial state.
*/
void
reset() noexcept final {
offset_ = 0;
}
};
} // namespace qb::protocol
namespace qb::pg {
namespace detail {
using namespace qb::io;
using namespace qb::pg;
/**
* @brief PostgreSQL database client implementation
*
* Core implementation of the PostgreSQL client that handles connection
* establishment, authentication, and query execution. This class provides
* the foundation for asynchronous database operations with PostgreSQL.
*
* Key features:
* - Asynchronous TCP/IP connection management
* - Multiple authentication methods support (Cleartext, MD5, SCRAM-SHA-256)
* - Transaction management (inherited from Transaction class)
* - Query execution and result processing
* - Prepared statement caching and execution
* - Event-driven message handling
*
* The Database class inherits from both the TCP client base class for network
* connectivity and the Transaction class for query and transaction management.
*
* @tparam QB_IO_ I/O handler type that provides networking capabilities
*/
template <typename QB_IO_>
class Database
: public qb::io::async::tcp::client<Database<QB_IO_>, QB_IO_, void>
, public Transaction {
public:
/**
* @brief PostgreSQL protocol handler type
*
* Type alias for the protocol handler used by this database client.
*/
using pg_protocol = qb::protocol::pgsql<Database<QB_IO_>>;
private:
connection_options conn_opts_; ///< Database connection options
client_options_type client_opts_; ///< Client options
integer serverPid_{}; ///< Server process ID
integer serverSecret_{}; ///< Server secret for protocol operations
PreparedQueryStorage storage_; ///< Storage for prepared statements
bool is_connected_ = false; ///< Flag indicating if the connection is established
/**
* @brief Creates a startup message for PostgreSQL connection
*
* Builds the startup message according to the PostgreSQL protocol specification.
* The message includes:
* - Protocol version
* - User authentication information
* - Target database name
* - Client parameters and options
*
* @param m Message object to populate with startup information
*/
void
create_startup_message(message &m) {
m.write(PROTOCOL_VERSION);
// Startup packet: null-terminated name=value pairs (PostgreSQL wire protocol).
// write(std::string) appends the required '\0' terminator; write_sv does not.
m.write(std::string(options::USER));
m.write(conn_opts_.user);
m.write(std::string(options::DATABASE));
m.write(conn_opts_.database);
for (auto &opt : client_opts_) {
m.write(opt.first);
m.write(opt.second);
}
// trailing terminator
m.write('\0');
}
/**
* @brief Sends the startup message to the PostgreSQL server
*/
void
send_startup_message() {
message m(empty_tag);
create_startup_message(m);
*this << m;
}
/**
* @brief Handles new command events in the transaction
*/
void
on_new_command() final {
process_if_query_ready();
}
/**
* @brief Handles sub-command status updates
*
* @param status Status of the sub-command
*/
void
on_sub_command_status(bool) final {}
Transaction *_current_command = this; ///< Current transaction being processed
ISqlQuery *_current_query = nullptr; ///< Current query being executed
bool _ready_for_query = false; ///< Flag indicating if ready for next query
/**
* @brief Finds the next transaction to execute
*
* Recursively traverses the transaction tree to find the
* deepest (leaf) transaction that should be executed next.
*
* @param cmd Current transaction
* @return Transaction* Next transaction to execute
*/
static Transaction *
next_transaction(Transaction *cmd) {
if (!cmd)
return nullptr;
auto sub = cmd->next_transaction();
if (!sub)
return cmd;
else
return next_transaction(sub);
}
/**
* @brief Processes a query in the transaction
*
* Fetches and executes the next query from the given transaction.
* If no more queries are in the current transaction, moves to parent.
*
* @param cmd Transaction containing the query
* @return bool true if a query was processed, false if no queries remain
*/
bool
process_query(Transaction *cmd) {
_ready_for_query = false;
_current_command = next_transaction(cmd);
_current_query = _current_command->next_query();
if (_current_query) {
if (qb::likely(_current_query->is_valid())) {
*this << _current_query->get();
return true;
} else {
LOG_DEBUG("[pgsql] error processing query not valid");
_error = error::client_error{
"query couldn't be processed check logs for more infos"};
on_error_query(error());
return process_query(_current_command) || (_ready_for_query = true);
}
} else if (_current_command->parent()) {
auto next_cmd = _current_command->parent();
do {
next_cmd->pop_transaction();
} while (!next_cmd->result() && (next_cmd = next_cmd->parent()));
return process_query(next_cmd);
}
return false;
}
/**
* @brief Processes queries if the client is ready
*/
void
process_if_query_ready() {
if (_ready_for_query) {
process_query(_current_command);
}
}
/**
* @brief Handles successful query completion
*/
void
on_success_query() {
if (_current_query) {
auto query = _current_command->pop_query();
query->on_success();
_current_query = nullptr;
}
}
/**
* @brief Handles query error
*
* @param err Error information
*/
void
on_error_query(error::db_error const &err) {
_error = err;
if (_current_query) {
_current_command->result(false);
auto query = _current_command->pop_query();
query->on_error(err);
_current_query = nullptr;
}
}
private:
std::string _nonce; ///< Client nonce for SCRAM authentication
std::vector<uint8_t> _password_salt; ///< Salted password for SCRAM authentication
std::string _auth_message; ///< Authentication message for SCRAM protocol
public:
/**
* @brief Handles authentication messages from the server
*
* Processes various authentication methods requested by the PostgreSQL server
* and responds appropriately. Supports multiple authentication mechanisms:
*
* - OK: Authentication already successful
* - Cleartext: Simple plaintext password authentication
* - MD5: MD5 hash-based password authentication
* - SCRAM-SHA-256: Modern challenge-response authentication
*
* For each authentication type, this method constructs and sends the
* appropriate response message according to the PostgreSQL protocol and
* authentication specifications.
*
* @param msg Authentication message from the server
*/
void
on_authentication(message &msg) {
integer auth_state(-1);
msg.read(auth_state);
LOG_DEBUG("[pgsql] Handle auth_event");
switch (auth_state) {
case OK: {
LOG_INFO("[pgsql] Authenticated with server");
is_connected_ = true;
// Apply keepalive settings if configured (P1-1)
apply_keepalive_settings();
} break;
case Cleartext: {
LOG_INFO("[pgsql] Clear text authentication requested");
message pm(password_message_tag);
pm.write(conn_opts_.password);
*this << pm;
} break;
case MD5Password: {
LOG_INFO("[pgsql] MD5 authentication requested");
// Read salt
std::string salt;
msg.read(salt, 4);
// Calculate hash
std::string pwdhash = qb::crypto::to_hex_string(
qb::crypto::md5(conn_opts_.password + conn_opts_.user),
qb::crypto::range_hex_lower);
std::string md5digest =
std::string("md5") +
qb::crypto::to_hex_string(qb::crypto::md5(pwdhash + salt),
qb::crypto::range_hex_lower);
// Construct and send message
message pm(password_message_tag);
pm.write(md5digest);
*this << pm;
} break;
case SCRAM_SHA256: {
LOG_INFO("[pgsql] SCRAM-SHA-256 authentication requested");
message pm(password_message_tag);
// Set new nonce
_nonce =
qb::crypto::generate_random_string(32, qb::crypto::range_hex_lower);
const auto data = "n,,n=" + conn_opts_.user + ",r=" + _nonce;
// Add mechanism
pm.write("SCRAM-SHA-256");
// Add sasl data
pm.write(static_cast<qb::pg::integer>(data.size()));
pm.write_sv(data);
*this << pm;
} break;
case SCRAM_SHA256_CLIENT_PROOF: {
LOG_INFO("[pgsql] SCRAM-SHA-256 authentication client proof check");
std::string data;
msg.read(data);
auto params = parse_header_attributes(data.c_str(), data.size());
// SCRAM inputs
const std::string clientNonce = _nonce; // Nonce generated by client
const std::string username = conn_opts_.user;
const std::string password = conn_opts_.password;
const std::string serverNonce =
std::move(params["r"]); // Combined nonce (client + server)
const std::string salt_base64 = std::move(params["s"]); // Salt (base64)
// Validate and parse iteration count safely
// SECURITY FIX: Added validation and error handling to prevent
// crashes from malicious or malformed SCRAM responses
int iteration = 0;
try {
auto it = params.find("i");
if (it == params.end()) {
throw error::connection_error(
"Missing iteration count in SCRAM response");
}
iteration = std::stoi(it->second);
if (iteration < 1) {
throw error::connection_error(
"Invalid iteration count: must be positive");
}
} catch (const std::exception &e) {
throw error::connection_error(
std::string("Invalid SCRAM iteration count: ") + e.what());
}
// Client-first-message-bare
std::string client_first_message_bare =
"n=" + username + ",r=" + clientNonce;
std::string server_first_message = "r=" + serverNonce +
",s=" + salt_base64 +
",i=" + std::to_string(iteration);
std::string client_final_message_without_proof =
"c=biws,r=" + serverNonce; // "biws" is the base64 encoding of "n,,"
_auth_message = client_first_message_bare + "," + server_first_message +
"," + client_final_message_without_proof;
// Compute SaltedPassword using PBKDF2-HMAC-SHA256
std::vector<unsigned char> salt = qb::crypto::base64_decode(salt_base64);
std::vector<unsigned char> saltedPassword(32); // 32 bytes for SHA256
if (PKCS5_PBKDF2_HMAC(password.c_str(),
static_cast<int>(password.size()), salt.data(),
static_cast<int>(salt.size()), iteration,
EVP_sha256(), 32, saltedPassword.data()) != 1) {
throw std::runtime_error("error during PBKDF2 computing");
}
// Compute clientKey: HMAC(saltedPassword, "Client Key")
std::vector<unsigned char> clientKey =
qb::crypto::hmac_sha256(saltedPassword, "Client Key");
// Compute storedKey: SHA256(clientKey)
std::vector<unsigned char> storedKey = qb::crypto::sha256(clientKey);
// Compute clientSignature: HMAC(storedKey, authMessage)
std::vector<unsigned char> clientSignature =
qb::crypto::hmac_sha256(storedKey, _auth_message);
// Compute clientProof: XOR(clientKey, clientSignature)
std::vector<unsigned char> clientProof =
qb::crypto::xor_bytes(clientKey, clientSignature);
// Encode clientProof in base64
std::string clientProofBase64 =
qb::crypto::base64_encode(clientProof.data(), clientProof.size());
// Construction of final message to send
std::string client_final_message =
"c=biws,r=" + serverNonce + ",p=" + clientProofBase64;
message pm(password_message_tag);
pm.write_sv(client_final_message);
*this << pm;
_password_salt = std::move(saltedPassword);
} break;
case SCRAM_SHA256_SERVER_CHECK: {
try {
std::string serverFinalMessage;
msg.read(serverFinalMessage);
// Extract the server signature from the final message
const std::string prefix = "v=";
size_t pos = serverFinalMessage.find(prefix);
if (pos == std::string::npos) {
throw std::runtime_error(
"server final message does not contain a signature");
}
std::string receivedServerSignatureBase64 =
serverFinalMessage.substr(pos + prefix.size());
// Compute the ServerKey: HMAC(saltedPassword, "Server Key")
std::vector<unsigned char> serverKey =
qb::crypto::hmac_sha256(_password_salt, "Server Key");
// Compute the ServerSignature: HMAC(serverKey, authMessage)
std::vector<unsigned char> computedServerSignature =
qb::crypto::hmac_sha256(serverKey, _auth_message);
// Encode the computed server signature in Base64
std::string computedServerSignatureBase64 =
qb::crypto::base64_encode(computedServerSignature.data(),
computedServerSignature.size());
// Compare the computed server signature with the received one
if (computedServerSignatureBase64 != receivedServerSignatureBase64) {
throw std::runtime_error(
"server signature does not match. Authentication failed");
}
LOG_INFO("[pgsql] SCRAM-SHA-256 Authentication successful: server "
"signature verified");
break;
} catch (std::exception &ex) {
LOG_CRIT("[pgsql] SCRAM-SHA-256 Failed verifying server signature: "
<< ex.what());
}
} break;
default: {
LOG_CRIT("[pgsql] Unsupported authentication scheme "
<< auth_state << "requested by server");
throw std::runtime_error("[pgsql] fatal error: check logs");
}
}
}
/**
* @brief Handles command complete messages
*
* @param msg Command complete message
*/
void
on_command_complete(message &msg) {
command_complete cmpl;
msg.read(cmpl.command_tag);
LOG_DEBUG("[pgsql] Command complete (" << cmpl.command_tag << ")");
if (_current_command)
_current_command->on_command_complete(cmpl.command_tag);
}
/**
* @brief Handles backend key data messages
*
* @param msg Backend key data message
*/
void
on_backend_key_data(message &msg) {
msg.read(serverPid_);
msg.read(serverSecret_);
LOG_DEBUG("[pgsql] Received backend key data");
}
/**
* @brief Handles error response messages
*
* @param msg Error response message
*/
void
on_error_response(message &msg) {
notice_message notice;
msg.read(notice);
LOG_WARN("[pgsql] Error " << notice);
error::query_error err(notice.message, notice.severity, notice.sqlstate,
notice.detail);
on_error_query(err);
}
/**
* @brief Handles parameter status messages
*
* @param msg Parameter status message
*/
void
on_parameter_status(message &msg) {
std::string key;
std::string value;
msg.read(key);
msg.read(value);
LOG_DEBUG("[pgsql] Received parameter " << key << "=" << value);
client_opts_[key] = value;
}
/**
* @brief Handles notice response messages
*
* @param msg Notice response message
*/
void
on_notice_response(message &msg) {
notice_message notice;
msg.read(notice);
LOG_INFO("[pgsql] Received notice" << notice);
}
/**
* @brief Handles ready for query messages
*
* @param msg Ready for query message
*/
void
on_ready_for_query(message &msg) {
on_success_query();
char stat(0);
msg.read(stat);
if (!process_query(_current_command)) {
_ready_for_query = true;
LOG_DEBUG("[pgsql] Database " << conn_opts_.uri << "[" << conn_opts_.database
<< "]"
<< " is ready for query (" << stat << ")");
}
}
/**
* @brief Handles row description messages
*
* @param msg Row description message
*/
void
on_row_description(message &msg) {
row_description_type fields;
smallint col_cnt;
msg.read(col_cnt);
fields.reserve(col_cnt);
for (int i = 0; i < col_cnt; ++i) {
field_description fd;
if (msg.read(fd)) {
fields.push_back(fd);
} else {
LOG_WARN("[pgsql] Failed to read field description " << i);
_current_command->result(false);
break;
}
}
_current_command->on_new_row_description(std::move(fields));
}
/**
* @brief Handles data row messages
*
* @param msg Data row message
*/
void
on_data_row(message &msg) {
row_data row;
if (msg.read(row))
_current_command->on_new_data_row(std::move(row));
else {
LOG_WARN("[pgsql] Failed to read data row");
_current_command->result(false);
}
}
/**
* @brief Handles parse complete messages
*
* @param msg Parse complete message
*/
void
on_parse_complete(message &) {
LOG_DEBUG("[pgsql] Parse complete");
}
/**
* @brief Handles parameter description messages
*
* @param msg Parameter description message
*/
void
on_parameter_description(message &) {
LOG_DEBUG("[pgsql] Parameter descriptions");
}
/**
* @brief Handles bind complete messages
*
* @param msg Bind complete message
*/
void
on_bind_complete(message &) {
LOG_DEBUG("[pgsql] Bind complete");
}
/**
* @brief Handles no data messages
*
* @param msg No data message
*/
void
on_no_data(message &) {
LOG_DEBUG("[pgsql] No data");
}
/**
* @brief Handles portal suspended messages
*
* @param msg Portal suspended message
*/
void
on_portal_suspended(message &) {
LOG_DEBUG("[pgsql] Portal suspended");
}
/**
* @brief Handles empty query response messages
*
* Sent by the server when an empty query string is received.
* Treated as a successful no-op — the server will follow with ReadyForQuery.
*
* @param msg Empty query response message
*/
void
on_empty_query_response(message &) {
LOG_DEBUG("[pgsql] Empty query response");
}
/**
* @brief Handles unrecognized messages
*
* @param msg Unhandled message
*/
void
on_unhandled_message(message &msg) {
LOG_DEBUG("[pgsql] Unhandled message tag " << (char) msg.tag());
}
/**