mirror of
https://github.com/postgresml/pgcat.git
synced 2026-03-24 17:56:29 +00:00
Compare commits
12 Commits
levkk-asyn
...
levkk-auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ca28a62c4 | ||
|
|
b65c1ddd56 | ||
|
|
ed31053cdb | ||
|
|
4969abf355 | ||
|
|
112c0bdae8 | ||
|
|
fef737ea43 | ||
|
|
345ee88342 | ||
|
|
db3d6c3baa | ||
|
|
197c32b4e8 | ||
|
|
6345c39bd5 | ||
|
|
32b913af94 | ||
|
|
5c673b4333 |
@@ -39,7 +39,7 @@ log_client_connections = false
|
|||||||
log_client_disconnections = false
|
log_client_disconnections = false
|
||||||
|
|
||||||
# Reload config automatically if it changes.
|
# Reload config automatically if it changes.
|
||||||
autoreload = 15000
|
autoreload = true
|
||||||
|
|
||||||
# TLS
|
# TLS
|
||||||
tls_certificate = ".circleci/server.cert"
|
tls_certificate = ".circleci/server.cert"
|
||||||
|
|||||||
@@ -110,10 +110,6 @@ python3 tests/python/tests.py || exit 1
|
|||||||
|
|
||||||
start_pgcat "info"
|
start_pgcat "info"
|
||||||
|
|
||||||
python3 tests/python/async_test.py
|
|
||||||
|
|
||||||
start_pgcat "info"
|
|
||||||
|
|
||||||
# Admin tests
|
# Admin tests
|
||||||
export PGPASSWORD=admin_pass
|
export PGPASSWORD=admin_pass
|
||||||
psql -U admin_user -e -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW STATS' > /dev/null
|
psql -U admin_user -e -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW STATS' > /dev/null
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.rs]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
max_line_length = 120
|
|
||||||
|
|
||||||
[*.toml]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
125
CONFIG.md
125
CONFIG.md
@@ -108,7 +108,7 @@ If we should log client disconnections
|
|||||||
### autoreload
|
### autoreload
|
||||||
```
|
```
|
||||||
path: general.autoreload
|
path: general.autoreload
|
||||||
default: 15000
|
default: false
|
||||||
```
|
```
|
||||||
|
|
||||||
When set to true, PgCat reloads configs if it detects a change in the config file.
|
When set to true, PgCat reloads configs if it detects a change in the config file.
|
||||||
@@ -152,7 +152,7 @@ default: <UNSET>
|
|||||||
example: "server.cert"
|
example: "server.cert"
|
||||||
```
|
```
|
||||||
|
|
||||||
Path to TLS Certificate file to use for TLS connections
|
Path to TLS Certficate file to use for TLS connections
|
||||||
|
|
||||||
### tls_private_key
|
### tls_private_key
|
||||||
```
|
```
|
||||||
@@ -175,11 +175,41 @@ Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DAT
|
|||||||
### admin_password
|
### admin_password
|
||||||
```
|
```
|
||||||
path: general.admin_password
|
path: general.admin_password
|
||||||
default: "admin_pass"
|
default: <UNSET>
|
||||||
```
|
```
|
||||||
|
|
||||||
Password to access the virtual administrative database
|
Password to access the virtual administrative database
|
||||||
|
|
||||||
|
### auth_query (experimental)
|
||||||
|
```
|
||||||
|
path: general.auth_query
|
||||||
|
default: <UNSET>
|
||||||
|
```
|
||||||
|
|
||||||
|
Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be
|
||||||
|
established using the database configured in the pool. This parameter is inherited by every pool
|
||||||
|
and can be redefined in pool configuration.
|
||||||
|
|
||||||
|
### auth_query_user (experimental)
|
||||||
|
```
|
||||||
|
path: general.auth_query_user
|
||||||
|
default: <UNSET>
|
||||||
|
```
|
||||||
|
|
||||||
|
User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
|
||||||
|
specified in `auth_query_user`. The connection will be established using the database configured in the pool.
|
||||||
|
This parameter is inherited by every pool and can be redefined in pool configuration.
|
||||||
|
|
||||||
|
### auth_query_password (experimental)
|
||||||
|
```
|
||||||
|
path: general.auth_query_password
|
||||||
|
default: <UNSET>
|
||||||
|
```
|
||||||
|
|
||||||
|
Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
|
||||||
|
specified in `auth_query_user`. The connection will be established using the database configured in the pool.
|
||||||
|
This parameter is inherited by every pool and can be redefined in pool configuration.
|
||||||
|
|
||||||
## `pools.<pool_name>` Section
|
## `pools.<pool_name>` Section
|
||||||
|
|
||||||
### pool_mode
|
### pool_mode
|
||||||
@@ -213,7 +243,7 @@ If the client doesn't specify, PgCat routes traffic to this role by default.
|
|||||||
`replica` round-robin between replicas only without touching the primary,
|
`replica` round-robin between replicas only without touching the primary,
|
||||||
`primary` all queries go to the primary unless otherwise specified.
|
`primary` all queries go to the primary unless otherwise specified.
|
||||||
|
|
||||||
### query_parser_enabled
|
### query_parser_enabled (experimental)
|
||||||
```
|
```
|
||||||
path: pools.<pool_name>.query_parser_enabled
|
path: pools.<pool_name>.query_parser_enabled
|
||||||
default: true
|
default: true
|
||||||
@@ -234,7 +264,7 @@ If the query parser is enabled and this setting is enabled, the primary will be
|
|||||||
load balancing of read queries. Otherwise, the primary will only be used for write
|
load balancing of read queries. Otherwise, the primary will only be used for write
|
||||||
queries. The primary can always be explicitly selected with our custom protocol.
|
queries. The primary can always be explicitly selected with our custom protocol.
|
||||||
|
|
||||||
### sharding_key_regex
|
### sharding_key_regex (experimental)
|
||||||
```
|
```
|
||||||
path: pools.<pool_name>.sharding_key_regex
|
path: pools.<pool_name>.sharding_key_regex
|
||||||
default: <UNSET>
|
default: <UNSET>
|
||||||
@@ -256,40 +286,7 @@ Current options:
|
|||||||
`pg_bigint_hash`: PARTITION BY HASH (Postgres hashing function)
|
`pg_bigint_hash`: PARTITION BY HASH (Postgres hashing function)
|
||||||
`sha1`: A hashing function based on SHA1
|
`sha1`: A hashing function based on SHA1
|
||||||
|
|
||||||
### auth_query
|
### automatic_sharding_key (experimental)
|
||||||
```
|
|
||||||
path: pools.<pool_name>.auth_query
|
|
||||||
default: <UNSET>
|
|
||||||
example: "SELECT $1"
|
|
||||||
```
|
|
||||||
|
|
||||||
Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be
|
|
||||||
established using the database configured in the pool. This parameter is inherited by every pool
|
|
||||||
and can be redefined in pool configuration.
|
|
||||||
|
|
||||||
### auth_query_user
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.auth_query_user
|
|
||||||
default: <UNSET>
|
|
||||||
example: "sharding_user"
|
|
||||||
```
|
|
||||||
|
|
||||||
User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
|
|
||||||
specified in `auth_query_user`. The connection will be established using the database configured in the pool.
|
|
||||||
This parameter is inherited by every pool and can be redefined in pool configuration.
|
|
||||||
|
|
||||||
### auth_query_password
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.auth_query_password
|
|
||||||
default: <UNSET>
|
|
||||||
example: "sharding_user"
|
|
||||||
```
|
|
||||||
|
|
||||||
Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
|
|
||||||
specified in `auth_query_user`. The connection will be established using the database configured in the pool.
|
|
||||||
This parameter is inherited by every pool and can be redefined in pool configuration.
|
|
||||||
|
|
||||||
### automatic_sharding_key
|
|
||||||
```
|
```
|
||||||
path: pools.<pool_name>.automatic_sharding_key
|
path: pools.<pool_name>.automatic_sharding_key
|
||||||
default: <UNSET>
|
default: <UNSET>
|
||||||
@@ -314,6 +311,30 @@ default: 3000
|
|||||||
|
|
||||||
Connect timeout can be overwritten in the pool
|
Connect timeout can be overwritten in the pool
|
||||||
|
|
||||||
|
### auth_query (experimental)
|
||||||
|
```
|
||||||
|
path: general.auth_query
|
||||||
|
default: <UNSET>
|
||||||
|
```
|
||||||
|
|
||||||
|
Auth query can be overwritten in the pool
|
||||||
|
|
||||||
|
### auth_query_user (experimental)
|
||||||
|
```
|
||||||
|
path: general.auth_query_user
|
||||||
|
default: <UNSET>
|
||||||
|
```
|
||||||
|
|
||||||
|
Auth query user can be overwritten in the pool
|
||||||
|
|
||||||
|
### auth_query_password (experimental)
|
||||||
|
```
|
||||||
|
path: general.auth_query_password
|
||||||
|
default: <UNSET>
|
||||||
|
```
|
||||||
|
|
||||||
|
Auth query password can be overwritten in the pool
|
||||||
|
|
||||||
## `pools.<pool_name>.users.<user_index>` Section
|
## `pools.<pool_name>.users.<user_index>` Section
|
||||||
|
|
||||||
### username
|
### username
|
||||||
@@ -322,8 +343,7 @@ path: pools.<pool_name>.users.<user_index>.username
|
|||||||
default: "sharding_user"
|
default: "sharding_user"
|
||||||
```
|
```
|
||||||
|
|
||||||
PostgreSQL username used to authenticate the user and connect to the server
|
Postgresql username
|
||||||
if `server_username` is not set.
|
|
||||||
|
|
||||||
### password
|
### password
|
||||||
```
|
```
|
||||||
@@ -331,26 +351,7 @@ path: pools.<pool_name>.users.<user_index>.password
|
|||||||
default: "sharding_user"
|
default: "sharding_user"
|
||||||
```
|
```
|
||||||
|
|
||||||
PostgreSQL password used to authenticate the user and connect to the server
|
Postgresql password
|
||||||
if `server_password` is not set.
|
|
||||||
|
|
||||||
### server_username
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.users.<user_index>.server_username
|
|
||||||
default: <UNSET>
|
|
||||||
example: "another_user"
|
|
||||||
```
|
|
||||||
|
|
||||||
PostgreSQL username used to connect to the server.
|
|
||||||
|
|
||||||
### server_password
|
|
||||||
```
|
|
||||||
path: pools.<pool_name>.users.<user_index>.server_password
|
|
||||||
default: <UNSET>
|
|
||||||
example: "another_password"
|
|
||||||
```
|
|
||||||
|
|
||||||
PostgreSQL password used to connect to the server.
|
|
||||||
|
|
||||||
### pool_size
|
### pool_size
|
||||||
```
|
```
|
||||||
@@ -381,7 +382,7 @@ default: [["127.0.0.1", 5432, "primary"], ["localhost", 5432, "replica"]]
|
|||||||
|
|
||||||
Array of servers in the shard, each server entry is an array of `[host, port, role]`
|
Array of servers in the shard, each server entry is an array of `[host, port, role]`
|
||||||
|
|
||||||
### mirrors
|
### mirrors (experimental)
|
||||||
```
|
```
|
||||||
path: pools.<pool_name>.shards.<shard_index>.mirrors
|
path: pools.<pool_name>.shards.<shard_index>.mirrors
|
||||||
default: <UNSET>
|
default: <UNSET>
|
||||||
|
|||||||
119
Cargo.lock
generated
119
Cargo.lock
generated
@@ -4,9 +4,9 @@ version = 3
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.0.1"
|
version = "0.7.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
|
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -54,6 +54,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
@@ -277,9 +283,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.28"
|
version = "0.3.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -292,9 +298,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.28"
|
version = "0.3.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
|
checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@@ -302,15 +308,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.28"
|
version = "0.3.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.28"
|
version = "0.3.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
|
checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
@@ -319,38 +325,38 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.28"
|
version = "0.3.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.28"
|
version = "0.3.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.9",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.28"
|
version = "0.3.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.28"
|
version = "0.3.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.28"
|
version = "0.3.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -387,9 +393,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.17"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f"
|
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -476,9 +482,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.26"
|
version = "0.14.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
|
checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@@ -739,12 +745,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pgcat"
|
name = "pgcat"
|
||||||
version = "1.0.1"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"atomic_enum",
|
"atomic_enum",
|
||||||
"base64",
|
"base64 0.21.0",
|
||||||
"bb8",
|
"bb8",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -834,11 +840,11 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "postgres-protocol"
|
name = "postgres-protocol"
|
||||||
version = "0.6.5"
|
version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d"
|
checksum = "878c6cbf956e03af9aa8204b407b9cbf47c072164800aa918c516cd4b056c50c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.13.1",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
@@ -915,9 +921,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.8.0"
|
version = "1.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac6cf59af1067a3fb53fbe5c88c053764e930f932be1d71d3ffe032cbe147f59"
|
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -926,9 +932,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.7.0"
|
version = "0.6.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6868896879ba532248f33598de5181522d8b3d9d724dfd230911e1a7d4822f5"
|
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
@@ -961,14 +967,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.21.0"
|
version = "0.20.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07180898a28ed6a7f7ba2311594308f595e3dd2e3c3812fa0a80a47b45f17e5d"
|
checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-webpki",
|
|
||||||
"sct",
|
"sct",
|
||||||
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -977,17 +983,7 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
|
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.21.0",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-webpki"
|
|
||||||
version = "0.100.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
|
|
||||||
dependencies = [
|
|
||||||
"ring",
|
|
||||||
"untrusted",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1014,15 +1010,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.160"
|
version = "1.0.159"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
|
checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.160"
|
version = "1.0.159"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df"
|
checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1108,9 +1104,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparser"
|
name = "sqlparser"
|
||||||
version = "0.33.0"
|
version = "0.32.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "355dc4d4b6207ca8a3434fc587db0a8016130a574dbcdbfb93d7f7b5bc5b211a"
|
checksum = "0366f270dbabb5cc2e4c88427dc4c08bba144f81e32fbd459a013f26a4d16aa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
@@ -1227,12 +1223,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.24.0"
|
version = "0.23.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5"
|
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1446,6 +1443,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pgcat"
|
name = "pgcat"
|
||||||
version = "1.0.1"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
@@ -19,7 +19,7 @@ serde_derive = "1"
|
|||||||
regex = "1"
|
regex = "1"
|
||||||
num_cpus = "1"
|
num_cpus = "1"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
sqlparser = "0.33.0"
|
sqlparser = "0.32.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
arc-swap = "1"
|
arc-swap = "1"
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
@@ -28,7 +28,7 @@ hmac = "0.12"
|
|||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
base64 = "0.21"
|
base64 = "0.21"
|
||||||
stringprep = "0.1"
|
stringprep = "0.1"
|
||||||
tokio-rustls = "0.24"
|
tokio-rustls = "0.23"
|
||||||
rustls-pemfile = "1"
|
rustls-pemfile = "1"
|
||||||
hyper = { version = "0.14", features = ["full"] }
|
hyper = { version = "0.14", features = ["full"] }
|
||||||
phf = { version = "0.11.1", features = ["macros"] }
|
phf = { version = "0.11.1", features = ["macros"] }
|
||||||
@@ -37,7 +37,7 @@ futures = "0.3"
|
|||||||
socket2 = { version = "0.4.7", features = ["all"] }
|
socket2 = { version = "0.4.7", features = ["all"] }
|
||||||
nix = "0.26.2"
|
nix = "0.26.2"
|
||||||
atomic_enum = "0.2.0"
|
atomic_enum = "0.2.0"
|
||||||
postgres-protocol = "0.6.5"
|
postgres-protocol = "0.6.4"
|
||||||
fallible-iterator = "0.2"
|
fallible-iterator = "0.2"
|
||||||
|
|
||||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||||
|
|||||||
53
README.md
53
README.md
@@ -21,53 +21,22 @@ PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load bal
|
|||||||
| Client TLS | **Stable** | Clients can connect to the pooler using TLS/SSL. |
|
| Client TLS | **Stable** | Clients can connect to the pooler using TLS/SSL. |
|
||||||
| Client/Server authentication | **Stable** | Clients can connect using MD5 authentication, supported by `libpq` and all Postgres client drivers. PgCat can connect to Postgres using MD5 and SCRAM-SHA-256. |
|
| Client/Server authentication | **Stable** | Clients can connect using MD5 authentication, supported by `libpq` and all Postgres client drivers. PgCat can connect to Postgres using MD5 and SCRAM-SHA-256. |
|
||||||
| Live configuration reloading | **Stable** | Identical to PgBouncer; all settings can be reloaded dynamically (except `host` and `port`). |
|
| Live configuration reloading | **Stable** | Identical to PgBouncer; all settings can be reloaded dynamically (except `host` and `port`). |
|
||||||
| Auth passthrough | **Stable** | MD5 password authentication can be configured to use an `auth_query` so no cleartext passwords are needed in the config file.|
|
|
||||||
| Sharding using extended SQL syntax | **Experimental** | Clients can dynamically configure the pooler to route queries to specific shards. |
|
| Sharding using extended SQL syntax | **Experimental** | Clients can dynamically configure the pooler to route queries to specific shards. |
|
||||||
| Sharding using comments parsing/Regex | **Experimental** | Clients can include shard information (sharding key, shard ID) in the query comments. |
|
| Sharding using comments parsing/Regex | **Experimental** | Clients can include shard information (sharding key, shard ID) in the query comments. |
|
||||||
| Automatic sharding | **Experimental** | PgCat can parse queries, detect sharding keys automatically, and route queries to the correct shard. |
|
| Automatic sharding | **Experimental** | PgCat can parse queries, detect sharding keys automatically, and route queries to the correct shard. |
|
||||||
| Mirroring | **Experimental** | Mirror queries between multiple databases in order to test servers with realistic production traffic. |
|
| Mirroring | **Experimental** | Mirror queries between multiple databases in order to test servers with realistic production traffic. |
|
||||||
|
| Auth passthrough | **Experimental** | MD5 password authentication can be configured to use an `auth_query` so no cleartext passwords are needed in the config file. |
|
||||||
|
| Password rotation | **Experimental** | Allows to rotate passwords without downtime or using third-party tools to manage Postgres authentication. |
|
||||||
|
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
PgCat is stable and used in production to serve hundreds of thousands of queries per second.
|
PgCat is stable and used in production to serve hundreds of thousands of queries per second. Some features remain experimental and are being actively developed. They are optional and can be enabled through configuration.
|
||||||
|
|
||||||
<table>
|
| | |
|
||||||
<tr>
|
|-|-|
|
||||||
<td>
|
|<a href="https://tech.instacart.com/adopting-pgcat-a-nextgen-postgres-proxy-3cf284e68c2f"><img src="./images/instacart.webp" height="70" width="auto"></a>|<a href="https://postgresml.org/blog/scaling-postgresml-to-one-million-requests-per-second"><img src="./images/postgresml.webp" height="70" width="auto"></a>|
|
||||||
<a href="https://tech.instacart.com/adopting-pgcat-a-nextgen-postgres-proxy-3cf284e68c2f">
|
| [Instacart](https://tech.instacart.com/adopting-pgcat-a-nextgen-postgres-proxy-3cf284e68c2f) | [PostgresML](https://postgresml.org/blog/scaling-postgresml-to-one-million-requests-per-second) |
|
||||||
<img src="./images/instacart.webp" height="70" width="auto">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://postgresml.org/blog/scaling-postgresml-to-one-million-requests-per-second">
|
|
||||||
<img src="./images/postgresml.webp" height="70" width="auto">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://onesignal.com">
|
|
||||||
<img src="./images/one_signal.webp" height="70" width="auto">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="https://tech.instacart.com/adopting-pgcat-a-nextgen-postgres-proxy-3cf284e68c2f">
|
|
||||||
Instacart
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://postgresml.org/blog/scaling-postgresml-to-one-million-requests-per-second">
|
|
||||||
PostgresML
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
OneSignal
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
Some features remain experimental and are being actively developed. They are optional and can be enabled through configuration.
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
@@ -131,7 +100,7 @@ You can open a Docker development environment where you can debug tests easier.
|
|||||||
./dev/script/console
|
./dev/script/console
|
||||||
```
|
```
|
||||||
|
|
||||||
This will open a terminal in an environment similar to that used in tests. In there, you can compile the pooler, run tests, do some debugging with the test environment, etc. Objects compiled inside the container (and bundled gems) will be placed in `dev/cache` so they don't interfere with what you have on your machine.
|
This will open a terminal in an environment similar to that used in tests. In there, you can compile the pooler, run tests, do some debugging with the test environment, etc. Objects compiled inside the contaner (and bundled gems) will be placed in `dev/cache` so they don't interfere with what you have on your machine.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -276,6 +245,12 @@ The config can be reloaded by sending a `kill -s SIGHUP` to the process or by qu
|
|||||||
|
|
||||||
Mirroring allows to route queries to multiple databases at the same time. This is useful for prewarning replicas before placing them into the active configuration, or for testing different versions of Postgres with live traffic.
|
Mirroring allows to route queries to multiple databases at the same time. This is useful for prewarning replicas before placing them into the active configuration, or for testing different versions of Postgres with live traffic.
|
||||||
|
|
||||||
|
### Password rotation
|
||||||
|
|
||||||
|
Password rotation allows to specify multiple passwords for a user, so they can connect to PgCat with multiple credentials. This allows distributed applications to change their configuration (connection strings) gradually and for PgCat to monitor their progression in admin statistics. Once the new secret is deployed everywhere, the old one can be removed from PgCat.
|
||||||
|
|
||||||
|
This also decouples server passwords from client passwords, allowing to change one without necessarily changing the other.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
PgCat is free and open source, released under the MIT license.
|
PgCat is free and open source, released under the MIT license.
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ services:
|
|||||||
<<: *common-env-pg
|
<<: *common-env-pg
|
||||||
POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5
|
POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5
|
||||||
PGPORT: 10432
|
PGPORT: 10432
|
||||||
command: ["postgres", "-p", "5432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"]
|
command: ["postgres", "-p", "10432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"]
|
||||||
|
|
||||||
toxiproxy:
|
toxiproxy:
|
||||||
build: .
|
build: .
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ log_client_connections = false
|
|||||||
# If we should log client disconnections
|
# If we should log client disconnections
|
||||||
log_client_disconnections = false
|
log_client_disconnections = false
|
||||||
|
|
||||||
|
# Reload config automatically if it changes.
|
||||||
|
autoreload = false
|
||||||
|
|
||||||
# TLS
|
# TLS
|
||||||
# tls_certificate = "server.cert"
|
# tls_certificate = "server.cert"
|
||||||
# tls_private_key = "server.key"
|
# tls_private_key = "server.key"
|
||||||
@@ -73,7 +76,7 @@ query_parser_enabled = true
|
|||||||
|
|
||||||
# If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for
|
# If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for
|
||||||
# load balancing of read queries. Otherwise, the primary will only be used for write
|
# load balancing of read queries. Otherwise, the primary will only be used for write
|
||||||
# queries. The primary can always be explicitly selected with our custom protocol.
|
# queries. The primary can always be explicitely selected with our custom protocol.
|
||||||
primary_reads_enabled = true
|
primary_reads_enabled = true
|
||||||
|
|
||||||
# So what if you wanted to implement a different hashing function,
|
# So what if you wanted to implement a different hashing function,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
50
pgcat.toml
50
pgcat.toml
@@ -45,7 +45,7 @@ log_client_connections = false
|
|||||||
log_client_disconnections = false
|
log_client_disconnections = false
|
||||||
|
|
||||||
# When set to true, PgCat reloads configs if it detects a change in the config file.
|
# When set to true, PgCat reloads configs if it detects a change in the config file.
|
||||||
autoreload = 15000
|
autoreload = false
|
||||||
|
|
||||||
# Number of worker threads the Runtime will use (4 by default).
|
# Number of worker threads the Runtime will use (4 by default).
|
||||||
worker_threads = 5
|
worker_threads = 5
|
||||||
@@ -57,10 +57,10 @@ tcp_keepalives_count = 5
|
|||||||
# Number of seconds between keepalive packets.
|
# Number of seconds between keepalive packets.
|
||||||
tcp_keepalives_interval = 5
|
tcp_keepalives_interval = 5
|
||||||
|
|
||||||
# Path to TLS Certificate file to use for TLS connections
|
# Path to TLS Certficate file to use for TLS connections
|
||||||
# tls_certificate = "server.cert"
|
# tls_certificate = ".circleci/server.cert"
|
||||||
# Path to TLS private key file to use for TLS connections
|
# Path to TLS private key file to use for TLS connections
|
||||||
# tls_private_key = "server.key"
|
# tls_private_key = ".circleci/server.key"
|
||||||
|
|
||||||
# User name to access the virtual administrative database (pgbouncer or pgcat)
|
# User name to access the virtual administrative database (pgbouncer or pgcat)
|
||||||
# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc..
|
# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc..
|
||||||
@@ -113,21 +113,6 @@ primary_reads_enabled = true
|
|||||||
# `sha1`: A hashing function based on SHA1
|
# `sha1`: A hashing function based on SHA1
|
||||||
sharding_function = "pg_bigint_hash"
|
sharding_function = "pg_bigint_hash"
|
||||||
|
|
||||||
# Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be
|
|
||||||
# established using the database configured in the pool. This parameter is inherited by every pool
|
|
||||||
# and can be redefined in pool configuration.
|
|
||||||
# auth_query = "SELECT $1"
|
|
||||||
|
|
||||||
# User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
|
|
||||||
# specified in `auth_query_user`. The connection will be established using the database configured in the pool.
|
|
||||||
# This parameter is inherited by every pool and can be redefined in pool configuration.
|
|
||||||
# auth_query_user = "sharding_user"
|
|
||||||
|
|
||||||
# Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
|
|
||||||
# specified in `auth_query_user`. The connection will be established using the database configured in the pool.
|
|
||||||
# This parameter is inherited by every pool and can be redefined in pool configuration.
|
|
||||||
# auth_query_password = "sharding_user"
|
|
||||||
|
|
||||||
# Automatically parse this from queries and route queries to the right shard!
|
# Automatically parse this from queries and route queries to the right shard!
|
||||||
# automatic_sharding_key = "data.id"
|
# automatic_sharding_key = "data.id"
|
||||||
|
|
||||||
@@ -137,31 +122,26 @@ idle_timeout = 40000
|
|||||||
# Connect timeout can be overwritten in the pool
|
# Connect timeout can be overwritten in the pool
|
||||||
connect_timeout = 3000
|
connect_timeout = 3000
|
||||||
|
|
||||||
# User configs are structured as pool.<pool_name>.users.<user_index>
|
# auth_query = "SELECT * FROM public.user_lookup('$1')"
|
||||||
# This section holds the credentials for users that may connect to this cluster
|
# auth_query_user = "postgres"
|
||||||
[pools.sharded_db.users.0]
|
# auth_query_password = "postgres"
|
||||||
# PostgreSQL username used to authenticate the user and connect to the server
|
|
||||||
# if `server_username` is not set.
|
|
||||||
username = "sharding_user"
|
|
||||||
|
|
||||||
# PostgreSQL password used to authenticate the user and connect to the server
|
# User configs are structured as pool.<pool_name>.users.<user_index>
|
||||||
# if `server_password` is not set.
|
# This secion holds the credentials for users that may connect to this cluster
|
||||||
|
[pools.sharded_db.users.0]
|
||||||
|
# Postgresql username
|
||||||
|
username = "sharding_user"
|
||||||
|
# Postgresql password
|
||||||
password = "sharding_user"
|
password = "sharding_user"
|
||||||
|
|
||||||
pool_mode = "session"
|
# # Passwords the client can use to connect. Useful for password rotations.
|
||||||
|
# secrets = [ "secret_one", "secret_two" ]
|
||||||
# PostgreSQL username used to connect to the server.
|
|
||||||
# server_username = "another_user"
|
|
||||||
|
|
||||||
# PostgreSQL password used to connect to the server.
|
|
||||||
# server_password = "another_password"
|
|
||||||
|
|
||||||
# Maximum number of server connections that can be established for this user
|
# Maximum number of server connections that can be established for this user
|
||||||
# The maximum number of connection from a single Pgcat process to any database in the cluster
|
# The maximum number of connection from a single Pgcat process to any database in the cluster
|
||||||
# is the sum of pool_size across all users.
|
# is the sum of pool_size across all users.
|
||||||
pool_size = 9
|
pool_size = 9
|
||||||
|
|
||||||
|
|
||||||
# Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way.
|
# Maximum query duration. Dangerous, but protects against DBs that died in a non-obvious way.
|
||||||
# 0 means it is disabled.
|
# 0 means it is disabled.
|
||||||
statement_timeout = 0
|
statement_timeout = 0
|
||||||
|
|||||||
15
src/admin.rs
15
src/admin.rs
@@ -259,6 +259,7 @@ where
|
|||||||
let columns = vec![
|
let columns = vec![
|
||||||
("database", DataType::Text),
|
("database", DataType::Text),
|
||||||
("user", DataType::Text),
|
("user", DataType::Text),
|
||||||
|
("secret", DataType::Text),
|
||||||
("pool_mode", DataType::Text),
|
("pool_mode", DataType::Text),
|
||||||
("cl_idle", DataType::Numeric),
|
("cl_idle", DataType::Numeric),
|
||||||
("cl_active", DataType::Numeric),
|
("cl_active", DataType::Numeric),
|
||||||
@@ -276,10 +277,11 @@ where
|
|||||||
let mut res = BytesMut::new();
|
let mut res = BytesMut::new();
|
||||||
res.put(row_description(&columns));
|
res.put(row_description(&columns));
|
||||||
|
|
||||||
for ((_user_pool, _pool), pool_stats) in all_pool_stats {
|
for (_, pool_stats) in all_pool_stats {
|
||||||
let mut row = vec![
|
let mut row = vec![
|
||||||
pool_stats.database(),
|
pool_stats.database(),
|
||||||
pool_stats.user(),
|
pool_stats.user(),
|
||||||
|
pool_stats.redacted_secret(),
|
||||||
pool_stats.pool_mode().to_string(),
|
pool_stats.pool_mode().to_string(),
|
||||||
];
|
];
|
||||||
pool_stats.populate_row(&mut row);
|
pool_stats.populate_row(&mut row);
|
||||||
@@ -780,7 +782,7 @@ where
|
|||||||
let database = parts[0];
|
let database = parts[0];
|
||||||
let user = parts[1];
|
let user = parts[1];
|
||||||
|
|
||||||
match get_pool(database, user) {
|
match get_pool(database, user, None) {
|
||||||
Some(pool) => {
|
Some(pool) => {
|
||||||
pool.pause();
|
pool.pause();
|
||||||
|
|
||||||
@@ -827,7 +829,7 @@ where
|
|||||||
let database = parts[0];
|
let database = parts[0];
|
||||||
let user = parts[1];
|
let user = parts[1];
|
||||||
|
|
||||||
match get_pool(database, user) {
|
match get_pool(database, user, None) {
|
||||||
Some(pool) => {
|
Some(pool) => {
|
||||||
pool.resume();
|
pool.resume();
|
||||||
|
|
||||||
@@ -895,13 +897,20 @@ where
|
|||||||
res.put(row_description(&vec![
|
res.put(row_description(&vec![
|
||||||
("name", DataType::Text),
|
("name", DataType::Text),
|
||||||
("pool_mode", DataType::Text),
|
("pool_mode", DataType::Text),
|
||||||
|
("secret", DataType::Text),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
for (user_pool, pool) in get_all_pools() {
|
for (user_pool, pool) in get_all_pools() {
|
||||||
let pool_config = &pool.settings;
|
let pool_config = &pool.settings;
|
||||||
|
let redacted_secret = match user_pool.secret {
|
||||||
|
Some(secret) => format!("****{}", &secret[secret.len() - 4..]),
|
||||||
|
None => "<no secret>".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
res.put(data_row(&vec![
|
res.put(data_row(&vec![
|
||||||
user_pool.user.clone(),
|
user_pool.user.clone(),
|
||||||
pool_config.pool_mode.to_string(),
|
pool_config.pool_mode.to_string(),
|
||||||
|
redacted_secret,
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
452
src/auth.rs
Normal file
452
src/auth.rs
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
//! Module implementing various client authentication mechanisms.
|
||||||
|
//!
|
||||||
|
//! Currently supported: plain (via TLS), md5 (via TLS and plain text connection).
|
||||||
|
|
||||||
|
use crate::errors::Error;
|
||||||
|
use crate::tokio::io::AsyncReadExt;
|
||||||
|
use crate::{
|
||||||
|
auth_passthrough::AuthPassthrough,
|
||||||
|
config::get_config,
|
||||||
|
messages::{
|
||||||
|
error_response, md5_hash_password, md5_hash_second_pass, write_all, wrong_password,
|
||||||
|
},
|
||||||
|
pool::{get_pool, ConnectionPool},
|
||||||
|
};
|
||||||
|
use bytes::{BufMut, BytesMut};
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
async fn refetch_auth_hash<S>(
|
||||||
|
pool: &ConnectionPool,
|
||||||
|
stream: &mut S,
|
||||||
|
username: &str,
|
||||||
|
pool_name: &str,
|
||||||
|
) -> Result<String, Error>
|
||||||
|
where
|
||||||
|
S: tokio::io::AsyncWrite + std::marker::Unpin + std::marker::Send,
|
||||||
|
{
|
||||||
|
let config = get_config();
|
||||||
|
|
||||||
|
debug!("Fetching auth hash");
|
||||||
|
|
||||||
|
if config.is_auth_query_configured() {
|
||||||
|
let address = pool.address(0, 0);
|
||||||
|
if let Some(apt) = AuthPassthrough::from_pool_settings(&pool.settings) {
|
||||||
|
let hash = apt.fetch_hash(address).await?;
|
||||||
|
|
||||||
|
debug!("Auth query succeeded");
|
||||||
|
|
||||||
|
return Ok(hash);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Auth query not configured on pool");
|
||||||
|
}
|
||||||
|
|
||||||
|
error_response(
|
||||||
|
stream,
|
||||||
|
&format!(
|
||||||
|
"No password set and auth passthrough failed for database: {}, user: {}",
|
||||||
|
pool_name, username
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Err(Error::ClientError(format!(
|
||||||
|
"Could not obtain hash for {{ username: {:?}, database: {:?} }}. Auth passthrough not enabled.",
|
||||||
|
pool_name, username
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read 'p' message from client.
|
||||||
|
async fn response<R>(stream: &mut R) -> Result<Vec<u8>, Error>
|
||||||
|
where
|
||||||
|
R: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send,
|
||||||
|
{
|
||||||
|
let code = match stream.read_u8().await {
|
||||||
|
Ok(code) => code,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(Error::SocketError(
|
||||||
|
"Error reading password code from client".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if code as char != 'p' {
|
||||||
|
return Err(Error::SocketError(format!("Expected p, got {}", code)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = match stream.read_i32().await {
|
||||||
|
Ok(len) => len,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(Error::SocketError(
|
||||||
|
"Error reading password length from client".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut response = vec![0; (len - 4) as usize];
|
||||||
|
|
||||||
|
// Too short to be a password (null-terminated)
|
||||||
|
if response.len() < 2 {
|
||||||
|
return Err(Error::ClientError(format!("Password response too short")));
|
||||||
|
}
|
||||||
|
|
||||||
|
match stream.read_exact(&mut response).await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => {
|
||||||
|
return Err(Error::SocketError(
|
||||||
|
"Error reading password from client".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(response.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make sure the pool we authenticated to has at least one server connection
|
||||||
|
/// that can serve our request.
|
||||||
|
async fn validate_pool<W>(
|
||||||
|
stream: &mut W,
|
||||||
|
mut pool: ConnectionPool,
|
||||||
|
username: &str,
|
||||||
|
pool_name: &str,
|
||||||
|
) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
W: tokio::io::AsyncWrite + std::marker::Unpin + std::marker::Send,
|
||||||
|
{
|
||||||
|
if !pool.validated() {
|
||||||
|
match pool.validate().await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
error_response(
|
||||||
|
stream,
|
||||||
|
&format!("Pool down for database: {}, user: {}", pool_name, username,),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Err(Error::ClientError(format!("Pool down: {:?}", err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear text authentication.
|
||||||
|
///
|
||||||
|
/// The client will send the password in plain text over the wire.
|
||||||
|
/// To protect against obvious security issues, this is only used over TLS.
|
||||||
|
///
|
||||||
|
/// Clear text authentication is used to support zero-downtime password rotation.
|
||||||
|
/// It allows the client to use multiple passwords when talking to the PgCat
|
||||||
|
/// while the password is being rotated across multiple app instances.
|
||||||
|
pub struct ClearText {
|
||||||
|
username: String,
|
||||||
|
pool_name: String,
|
||||||
|
application_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClearText {
|
||||||
|
/// Create a new ClearText authentication mechanism.
|
||||||
|
pub fn new(username: &str, pool_name: &str, application_name: &str) -> ClearText {
|
||||||
|
ClearText {
|
||||||
|
username: username.to_string(),
|
||||||
|
pool_name: pool_name.to_string(),
|
||||||
|
application_name: application_name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issue 'R' clear text challenge to client.
|
||||||
|
pub async fn challenge<W>(&self, stream: &mut W) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
W: tokio::io::AsyncWrite + std::marker::Unpin + std::marker::Send,
|
||||||
|
{
|
||||||
|
debug!("Sending plain challenge");
|
||||||
|
|
||||||
|
let mut msg = BytesMut::new();
|
||||||
|
msg.put_u8(b'R');
|
||||||
|
msg.put_i32(8);
|
||||||
|
msg.put_i32(3); // Clear text
|
||||||
|
|
||||||
|
write_all(stream, msg).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticate client with server password or secret.
|
||||||
|
pub async fn authenticate<R, W>(
|
||||||
|
&self,
|
||||||
|
read: &mut R,
|
||||||
|
write: &mut W,
|
||||||
|
) -> Result<Option<String>, Error>
|
||||||
|
where
|
||||||
|
R: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send,
|
||||||
|
W: tokio::io::AsyncWrite + std::marker::Unpin + std::marker::Send,
|
||||||
|
{
|
||||||
|
let response = response(read).await?;
|
||||||
|
|
||||||
|
let secret = String::from_utf8_lossy(&response[0..response.len() - 1]).to_string();
|
||||||
|
|
||||||
|
match get_pool(&self.pool_name, &self.username, Some(secret.clone())) {
|
||||||
|
None => match get_pool(&self.pool_name, &self.username, None) {
|
||||||
|
Some(pool) => {
|
||||||
|
match pool.settings.user.password {
|
||||||
|
Some(ref password) => {
|
||||||
|
if password != &secret {
|
||||||
|
wrong_password(write, &self.username).await?;
|
||||||
|
Err(Error::ClientError(format!(
|
||||||
|
"Invalid password {{ username: {}, pool_name: {}, application_name: {} }}",
|
||||||
|
self.username, self.pool_name, self.application_name
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
validate_pool(write, pool, &self.username, &self.pool_name).await?;
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
// Server is storing hashes, we can't query it for the plain text password.
|
||||||
|
error_response(
|
||||||
|
write,
|
||||||
|
&format!(
|
||||||
|
"No server password configured for database: {}, user: {}",
|
||||||
|
self.pool_name, self.username
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Err(Error::ClientError(format!(
|
||||||
|
"No server password configured for {{ username: {}, pool_name: {}, application_name: {} }}",
|
||||||
|
self.username, self.pool_name, self.application_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
error_response(
|
||||||
|
write,
|
||||||
|
&format!(
|
||||||
|
"No pool configured for database: {}, user: {}",
|
||||||
|
self.pool_name, self.username
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Err(Error::ClientError(format!(
|
||||||
|
"Invalid pool name {{ username: {}, pool_name: {}, application_name: {} }}",
|
||||||
|
self.username, self.pool_name, self.application_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(pool) => {
|
||||||
|
validate_pool(write, pool, &self.username, &self.pool_name).await?;
|
||||||
|
Ok(Some(secret))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MD5 hash authentication.
|
||||||
|
///
|
||||||
|
/// Deprecated, but widely used everywhere, and currently required for poolers
|
||||||
|
/// to authencticate clients without involving Postgres.
|
||||||
|
///
|
||||||
|
/// Admin clients are required to use MD5.
|
||||||
|
pub struct Md5 {
|
||||||
|
username: String,
|
||||||
|
pool_name: String,
|
||||||
|
application_name: String,
|
||||||
|
salt: [u8; 4],
|
||||||
|
admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Md5 {
|
||||||
|
pub fn new(username: &str, pool_name: &str, application_name: &str, admin: bool) -> Md5 {
|
||||||
|
let salt: [u8; 4] = [
|
||||||
|
rand::random(),
|
||||||
|
rand::random(),
|
||||||
|
rand::random(),
|
||||||
|
rand::random(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Md5 {
|
||||||
|
username: username.to_string(),
|
||||||
|
pool_name: pool_name.to_string(),
|
||||||
|
application_name: application_name.to_string(),
|
||||||
|
salt,
|
||||||
|
admin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issue a 'R' MD5 challenge to the client.
|
||||||
|
pub async fn challenge<W>(&self, stream: &mut W) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
W: tokio::io::AsyncWrite + std::marker::Unpin + std::marker::Send,
|
||||||
|
{
|
||||||
|
let mut res = BytesMut::new();
|
||||||
|
res.put_u8(b'R');
|
||||||
|
res.put_i32(12);
|
||||||
|
res.put_i32(5); // MD5
|
||||||
|
res.put_slice(&self.salt[..]);
|
||||||
|
|
||||||
|
write_all(stream, res).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticate client with MD5. This is used for both admin and normal users.
|
||||||
|
pub async fn authenticate<R, W>(&self, read: &mut R, write: &mut W) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
R: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send,
|
||||||
|
W: tokio::io::AsyncWrite + std::marker::Unpin + std::marker::Send,
|
||||||
|
{
|
||||||
|
let password_hash = response(read).await?;
|
||||||
|
|
||||||
|
if self.admin {
|
||||||
|
let config = get_config();
|
||||||
|
|
||||||
|
// Compare server and client hashes.
|
||||||
|
let our_hash = md5_hash_password(
|
||||||
|
&config.general.admin_username,
|
||||||
|
&config.general.admin_password,
|
||||||
|
&self.salt,
|
||||||
|
);
|
||||||
|
|
||||||
|
if our_hash != password_hash {
|
||||||
|
wrong_password(write, &self.username).await?;
|
||||||
|
|
||||||
|
Err(Error::ClientError(format!(
|
||||||
|
"Invalid password {{ username: {}, pool_name: {}, application_name: {} }}",
|
||||||
|
self.username, self.pool_name, self.application_name
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match get_pool(&self.pool_name, &self.username, None) {
|
||||||
|
Some(pool) => {
|
||||||
|
match &pool.settings.user.password {
|
||||||
|
Some(ref password) => {
|
||||||
|
let our_hash = md5_hash_password(&self.username, password, &self.salt);
|
||||||
|
|
||||||
|
if our_hash != password_hash {
|
||||||
|
wrong_password(write, &self.username).await?;
|
||||||
|
|
||||||
|
Err(Error::ClientError(format!(
|
||||||
|
"Invalid password {{ username: {}, pool_name: {}, application_name: {} }}",
|
||||||
|
self.username, self.pool_name, self.application_name
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
validate_pool(write, pool, &self.username, &self.pool_name).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
if !get_config().is_auth_query_configured() {
|
||||||
|
error_response(
|
||||||
|
write,
|
||||||
|
&format!(
|
||||||
|
"No password configured and auth_query is not set: {}, user: {}",
|
||||||
|
self.pool_name, self.username
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Err(Error::ClientError(format!(
|
||||||
|
"No password configured and auth_query is not set"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Using auth_query");
|
||||||
|
|
||||||
|
// Fetch hash from server
|
||||||
|
let hash = (*pool.auth_hash.read()).clone();
|
||||||
|
|
||||||
|
let hash = match hash {
|
||||||
|
Some(hash) => {
|
||||||
|
debug!("Using existing hash: {}", hash);
|
||||||
|
hash.clone()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
debug!("Pool has no hash set, fetching new one");
|
||||||
|
|
||||||
|
let hash = refetch_auth_hash(
|
||||||
|
&pool,
|
||||||
|
write,
|
||||||
|
&self.username,
|
||||||
|
&self.pool_name,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
(*pool.auth_hash.write()) = Some(hash.clone());
|
||||||
|
|
||||||
|
hash
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let our_hash = md5_hash_second_pass(&hash, &self.salt);
|
||||||
|
|
||||||
|
// Compare hashes
|
||||||
|
if our_hash != password_hash {
|
||||||
|
debug!("Pool auth query hash did not match, refetching");
|
||||||
|
|
||||||
|
// Server hash maybe changed
|
||||||
|
let hash = refetch_auth_hash(
|
||||||
|
&pool,
|
||||||
|
write,
|
||||||
|
&self.username,
|
||||||
|
&self.pool_name,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let our_hash = md5_hash_second_pass(&hash, &self.salt);
|
||||||
|
|
||||||
|
if our_hash != password_hash {
|
||||||
|
debug!("Auth query failed, passwords don't match");
|
||||||
|
|
||||||
|
wrong_password(write, &self.username).await?;
|
||||||
|
|
||||||
|
Err(Error::ClientError(format!(
|
||||||
|
"Invalid password {{ username: {}, pool_name: {}, application_name: {} }}",
|
||||||
|
self.username, self.pool_name, self.application_name
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
(*pool.auth_hash.write()) = Some(hash);
|
||||||
|
|
||||||
|
validate_pool(
|
||||||
|
write,
|
||||||
|
pool.clone(),
|
||||||
|
&self.username,
|
||||||
|
&self.pool_name,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
validate_pool(write, pool.clone(), &self.username, &self.pool_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
error_response(
|
||||||
|
write,
|
||||||
|
&format!(
|
||||||
|
"No pool configured for database: {}, user: {}",
|
||||||
|
self.pool_name, self.username
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Err(Error::ClientError(format!(
|
||||||
|
"Invalid pool name {{ username: {}, pool_name: {}, application_name: {} }}",
|
||||||
|
self.username, self.pool_name, self.application_name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::pool::ConnectionPool;
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
@@ -72,30 +71,23 @@ impl AuthPassthrough {
|
|||||||
let auth_user = crate::config::User {
|
let auth_user = crate::config::User {
|
||||||
username: self.user.clone(),
|
username: self.user.clone(),
|
||||||
password: Some(self.password.clone()),
|
password: Some(self.password.clone()),
|
||||||
server_username: None,
|
|
||||||
server_password: None,
|
|
||||||
pool_size: 1,
|
pool_size: 1,
|
||||||
statement_timeout: 0,
|
statement_timeout: 0,
|
||||||
pool_mode: None,
|
secrets: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user = &address.username;
|
let user = &address.username;
|
||||||
|
|
||||||
debug!("Connecting to server to obtain auth hashes");
|
debug!("Connecting to server to obtain auth hashes.");
|
||||||
|
|
||||||
let auth_query = self.query.replace("$1", user);
|
let auth_query = self.query.replace("$1", user);
|
||||||
|
|
||||||
match Server::exec_simple_query(address, &auth_user, &auth_query).await {
|
match Server::exec_simple_query(address, &auth_user, &auth_query).await {
|
||||||
Ok(password_data) => {
|
Ok(password_data) => {
|
||||||
if password_data.len() == 2 && password_data.first().unwrap() == user {
|
if password_data.len() == 2 && password_data.first().unwrap() == user {
|
||||||
if let Some(stripped_hash) = password_data
|
if let Some(stripped_hash) = password_data.last().unwrap().to_string().strip_prefix("md5") {
|
||||||
.last()
|
Ok(stripped_hash.to_string())
|
||||||
.unwrap()
|
} else {
|
||||||
.to_string()
|
|
||||||
.strip_prefix("md5") {
|
|
||||||
Ok(stripped_hash.to_string())
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Err(Error::AuthPassthroughError(
|
Err(Error::AuthPassthroughError(
|
||||||
"Obtained hash from auth_query does not seem to be in md5 format.".to_string(),
|
"Obtained hash from auth_query does not seem to be in md5 format.".to_string(),
|
||||||
))
|
))
|
||||||
@@ -107,26 +99,12 @@ impl AuthPassthrough {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
Err(Error::AuthPassthroughError(
|
Err(Error::AuthPassthroughError(
|
||||||
format!("Error trying to obtain password from auth_query, ignoring hash for user '{}'. Error: {:?}",
|
format!("Error trying to obtain password from auth_query, ignoring hash for user '{}'. Error: {:?}",
|
||||||
user, err))
|
user, err)))
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refetch_auth_hash(pool: &ConnectionPool) -> Result<String, Error> {
|
|
||||||
let address = pool.address(0, 0);
|
|
||||||
if let Some(apt) = AuthPassthrough::from_pool_settings(&pool.settings) {
|
|
||||||
let hash = apt.fetch_hash(address).await?;
|
|
||||||
|
|
||||||
return Ok(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(Error::ClientError(format!(
|
|
||||||
"Could not obtain hash for {{ username: {:?}, database: {:?} }}. Auth passthrough not enabled.",
|
|
||||||
address.username, address.database
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|||||||
327
src/client.rs
327
src/client.rs
@@ -1,8 +1,8 @@
|
|||||||
use crate::errors::{ClientIdentifier, Error};
|
use crate::errors::Error;
|
||||||
use crate::pool::BanReason;
|
use crate::pool::BanReason;
|
||||||
/// Handle clients by pretending to be a PostgreSQL server.
|
/// Handle clients by pretending to be a PostgreSQL server.
|
||||||
use bytes::{Buf, BufMut, BytesMut};
|
use bytes::{Buf, BufMut, BytesMut};
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -12,7 +12,6 @@ use tokio::sync::broadcast::Receiver;
|
|||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
use crate::admin::{generate_server_info_for_admin, handle_admin};
|
use crate::admin::{generate_server_info_for_admin, handle_admin};
|
||||||
use crate::auth_passthrough::refetch_auth_hash;
|
|
||||||
use crate::config::{get_config, get_idle_client_in_transaction_timeout, Address, PoolMode};
|
use crate::config::{get_config, get_idle_client_in_transaction_timeout, Address, PoolMode};
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::messages::*;
|
use crate::messages::*;
|
||||||
@@ -90,6 +89,9 @@ pub struct Client<S, T> {
|
|||||||
/// Application name for this client (defaults to pgcat)
|
/// Application name for this client (defaults to pgcat)
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
|
||||||
|
/// Which secret the user is using to connect, if any.
|
||||||
|
secret: Option<String>,
|
||||||
|
|
||||||
/// Used to notify clients about an impending shutdown
|
/// Used to notify clients about an impending shutdown
|
||||||
shutdown: Receiver<()>,
|
shutdown: Receiver<()>,
|
||||||
}
|
}
|
||||||
@@ -202,7 +204,7 @@ pub async fn client_entrypoint(
|
|||||||
// Client probably disconnected rejecting our plain text connection.
|
// Client probably disconnected rejecting our plain text connection.
|
||||||
Ok((ClientConnectionType::Tls, _))
|
Ok((ClientConnectionType::Tls, _))
|
||||||
| Ok((ClientConnectionType::CancelQuery, _)) => Err(Error::ProtocolSyncError(
|
| Ok((ClientConnectionType::CancelQuery, _)) => Err(Error::ProtocolSyncError(
|
||||||
"Bad postgres client (plain)".into(),
|
format!("Bad postgres client (plain)"),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
@@ -290,7 +292,7 @@ pub async fn client_entrypoint(
|
|||||||
/// Handle the first message the client sends.
|
/// Handle the first message the client sends.
|
||||||
async fn get_startup<S>(stream: &mut S) -> Result<(ClientConnectionType, BytesMut), Error>
|
async fn get_startup<S>(stream: &mut S) -> Result<(ClientConnectionType, BytesMut), Error>
|
||||||
where
|
where
|
||||||
S: tokio::io::AsyncRead + std::marker::Unpin + tokio::io::AsyncWrite,
|
S: tokio::io::AsyncRead + std::marker::Unpin + tokio::io::AsyncWrite + std::marker::Send,
|
||||||
{
|
{
|
||||||
// Get startup message length.
|
// Get startup message length.
|
||||||
let len = match stream.read_i32().await {
|
let len = match stream.read_i32().await {
|
||||||
@@ -369,9 +371,9 @@ pub async fn startup_tls(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bad Postgres client.
|
// Bad Postgres client.
|
||||||
Ok((ClientConnectionType::Tls, _)) | Ok((ClientConnectionType::CancelQuery, _)) => {
|
Ok((ClientConnectionType::Tls, _)) | Ok((ClientConnectionType::CancelQuery, _)) => Err(
|
||||||
Err(Error::ProtocolSyncError("Bad postgres client (tls)".into()))
|
Error::ProtocolSyncError(format!("Bad postgres client (tls)")),
|
||||||
}
|
),
|
||||||
|
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
}
|
}
|
||||||
@@ -379,8 +381,8 @@ pub async fn startup_tls(
|
|||||||
|
|
||||||
impl<S, T> Client<S, T>
|
impl<S, T> Client<S, T>
|
||||||
where
|
where
|
||||||
S: tokio::io::AsyncRead + std::marker::Unpin,
|
S: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send,
|
||||||
T: tokio::io::AsyncWrite + std::marker::Unpin,
|
T: tokio::io::AsyncWrite + std::marker::Unpin + std::marker::Send,
|
||||||
{
|
{
|
||||||
pub fn is_admin(&self) -> bool {
|
pub fn is_admin(&self) -> bool {
|
||||||
self.admin
|
self.admin
|
||||||
@@ -404,7 +406,7 @@ where
|
|||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => {
|
None => {
|
||||||
return Err(Error::ClientError(
|
return Err(Error::ClientError(
|
||||||
"Missing user parameter on client startup".into(),
|
"Missing user parameter on client startup".to_string(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -419,8 +421,6 @@ where
|
|||||||
None => "pgcat",
|
None => "pgcat",
|
||||||
};
|
};
|
||||||
|
|
||||||
let client_identifier = ClientIdentifier::new(&application_name, &username, &pool_name);
|
|
||||||
|
|
||||||
let admin = ["pgcat", "pgbouncer"]
|
let admin = ["pgcat", "pgbouncer"]
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|db| *db == pool_name)
|
.filter(|db| *db == pool_name)
|
||||||
@@ -445,196 +445,39 @@ where
|
|||||||
let process_id: i32 = rand::random();
|
let process_id: i32 = rand::random();
|
||||||
let secret_key: i32 = rand::random();
|
let secret_key: i32 = rand::random();
|
||||||
|
|
||||||
// Perform MD5 authentication.
|
let config = get_config();
|
||||||
// TODO: Add SASL support.
|
|
||||||
let salt = md5_challenge(&mut write).await?;
|
|
||||||
|
|
||||||
let code = match read.read_u8().await {
|
let secret = if admin {
|
||||||
Ok(p) => p,
|
debug!("Using md5 auth for admin");
|
||||||
Err(_) => {
|
let auth = crate::auth::Md5::new(&username, &pool_name, &application_name, true);
|
||||||
return Err(Error::ClientSocketError(
|
auth.challenge(&mut write).await?;
|
||||||
"password code".into(),
|
auth.authenticate(&mut read, &mut write).await?;
|
||||||
client_identifier,
|
None
|
||||||
))
|
} else if !config.tls_enabled() {
|
||||||
}
|
debug!("Using md5 auth");
|
||||||
|
let auth = crate::auth::Md5::new(&username, &pool_name, &application_name, false);
|
||||||
|
auth.challenge(&mut write).await?;
|
||||||
|
auth.authenticate(&mut read, &mut write).await?;
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
debug!("Using plain auth");
|
||||||
|
let auth = crate::auth::ClearText::new(&username, &pool_name, &application_name);
|
||||||
|
auth.challenge(&mut write).await?;
|
||||||
|
auth.authenticate(&mut read, &mut write).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
// PasswordMessage
|
// Authenticated admin user.
|
||||||
if code as char != 'p' {
|
|
||||||
return Err(Error::ProtocolSyncError(format!(
|
|
||||||
"Expected p, got {}",
|
|
||||||
code as char
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let len = match read.read_i32().await {
|
|
||||||
Ok(len) => len,
|
|
||||||
Err(_) => {
|
|
||||||
return Err(Error::ClientSocketError(
|
|
||||||
"password message length".into(),
|
|
||||||
client_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut password_response = vec![0u8; (len - 4) as usize];
|
|
||||||
|
|
||||||
match read.read_exact(&mut password_response).await {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(_) => {
|
|
||||||
return Err(Error::ClientSocketError(
|
|
||||||
"password message".into(),
|
|
||||||
client_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Authenticate admin user.
|
|
||||||
let (transaction_mode, server_info) = if admin {
|
let (transaction_mode, server_info) = if admin {
|
||||||
let config = get_config();
|
|
||||||
|
|
||||||
// Compare server and client hashes.
|
|
||||||
let password_hash = md5_hash_password(
|
|
||||||
&config.general.admin_username,
|
|
||||||
&config.general.admin_password,
|
|
||||||
&salt,
|
|
||||||
);
|
|
||||||
|
|
||||||
if password_hash != password_response {
|
|
||||||
let error = Error::ClientGeneralError("Invalid password".into(), client_identifier);
|
|
||||||
|
|
||||||
warn!("{}", error);
|
|
||||||
wrong_password(&mut write, username).await?;
|
|
||||||
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
(false, generate_server_info_for_admin())
|
(false, generate_server_info_for_admin())
|
||||||
}
|
}
|
||||||
// Authenticate normal user.
|
// Authenticated normal user.
|
||||||
else {
|
else {
|
||||||
let mut pool = match get_pool(pool_name, username) {
|
let pool = get_pool(&pool_name, &username, secret.clone()).unwrap();
|
||||||
Some(pool) => pool,
|
|
||||||
None => {
|
|
||||||
error_response(
|
|
||||||
&mut write,
|
|
||||||
&format!(
|
|
||||||
"No pool configured for database: {:?}, user: {:?}",
|
|
||||||
pool_name, username
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Err(Error::ClientGeneralError(
|
|
||||||
"Invalid pool name".into(),
|
|
||||||
client_identifier,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Obtain the hash to compare, we give preference to that written in cleartext in config
|
|
||||||
// if there is nothing set in cleartext and auth passthrough (auth_query) is configured, we use the hash obtained
|
|
||||||
// when the pool was created. If there is no hash there, we try to fetch it one more time.
|
|
||||||
let password_hash = if let Some(password) = &pool.settings.user.password {
|
|
||||||
Some(md5_hash_password(username, password, &salt))
|
|
||||||
} else {
|
|
||||||
if !get_config().is_auth_query_configured() {
|
|
||||||
return Err(Error::ClientAuthImpossible(username.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut hash = (*pool.auth_hash.read()).clone();
|
|
||||||
|
|
||||||
if hash.is_none() {
|
|
||||||
warn!(
|
|
||||||
"Query auth configured \
|
|
||||||
but no hash password found \
|
|
||||||
for pool {}. Will try to refetch it.",
|
|
||||||
pool_name
|
|
||||||
);
|
|
||||||
|
|
||||||
match refetch_auth_hash(&pool).await {
|
|
||||||
Ok(fetched_hash) => {
|
|
||||||
warn!("Password for {}, obtained. Updating.", client_identifier);
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut pool_auth_hash = pool.auth_hash.write();
|
|
||||||
*pool_auth_hash = Some(fetched_hash.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
hash = Some(fetched_hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(err) => {
|
|
||||||
return Err(Error::ClientAuthPassthroughError(
|
|
||||||
err.to_string(),
|
|
||||||
client_identifier,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(md5_hash_second_pass(&hash.unwrap(), &salt))
|
|
||||||
};
|
|
||||||
|
|
||||||
// Once we have the resulting hash, we compare with what the client gave us.
|
|
||||||
// If they do not match and auth query is set up, we try to refetch the hash one more time
|
|
||||||
// to see if the password has changed since the pool was created.
|
|
||||||
//
|
|
||||||
// @TODO: we could end up fetching again the same password twice (see above).
|
|
||||||
if password_hash.unwrap() != password_response {
|
|
||||||
warn!(
|
|
||||||
"Invalid password {}, will try to refetch it.",
|
|
||||||
client_identifier
|
|
||||||
);
|
|
||||||
|
|
||||||
let fetched_hash = refetch_auth_hash(&pool).await?;
|
|
||||||
let new_password_hash = md5_hash_second_pass(&fetched_hash, &salt);
|
|
||||||
|
|
||||||
// Ok password changed in server an auth is possible.
|
|
||||||
if new_password_hash == password_response {
|
|
||||||
warn!(
|
|
||||||
"Password for {}, changed in server. Updating.",
|
|
||||||
client_identifier
|
|
||||||
);
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut pool_auth_hash = pool.auth_hash.write();
|
|
||||||
*pool_auth_hash = Some(fetched_hash);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
wrong_password(&mut write, username).await?;
|
|
||||||
return Err(Error::ClientGeneralError(
|
|
||||||
"Invalid password".into(),
|
|
||||||
client_identifier,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let transaction_mode = pool.settings.pool_mode == PoolMode::Transaction;
|
let transaction_mode = pool.settings.pool_mode == PoolMode::Transaction;
|
||||||
|
|
||||||
// If the pool hasn't been validated yet,
|
|
||||||
// connect to the servers and figure out what's what.
|
|
||||||
if !pool.validated() {
|
|
||||||
match pool.validate().await {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(err) => {
|
|
||||||
error_response(
|
|
||||||
&mut write,
|
|
||||||
&format!(
|
|
||||||
"Pool down for database: {:?}, user: {:?}",
|
|
||||||
pool_name, username
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
return Err(Error::ClientError(format!("Pool down: {:?}", err)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(transaction_mode, pool.server_info())
|
(transaction_mode, pool.server_info())
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Password authentication successful");
|
debug!("Authentication successful");
|
||||||
|
|
||||||
auth_ok(&mut write).await?;
|
auth_ok(&mut write).await?;
|
||||||
write_all(&mut write, server_info).await?;
|
write_all(&mut write, server_info).await?;
|
||||||
@@ -642,7 +485,7 @@ where
|
|||||||
ready_for_query(&mut write).await?;
|
ready_for_query(&mut write).await?;
|
||||||
|
|
||||||
trace!("Startup OK");
|
trace!("Startup OK");
|
||||||
let pool_stats = match get_pool(pool_name, username) {
|
let pool_stats = match get_pool(pool_name, username, secret.clone()) {
|
||||||
Some(pool) => {
|
Some(pool) => {
|
||||||
if !admin {
|
if !admin {
|
||||||
pool.stats
|
pool.stats
|
||||||
@@ -682,6 +525,7 @@ where
|
|||||||
application_name: application_name.to_string(),
|
application_name: application_name.to_string(),
|
||||||
shutdown,
|
shutdown,
|
||||||
connected_to_server: false,
|
connected_to_server: false,
|
||||||
|
secret,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,6 +560,7 @@ where
|
|||||||
application_name: String::from("undefined"),
|
application_name: String::from("undefined"),
|
||||||
shutdown,
|
shutdown,
|
||||||
connected_to_server: false,
|
connected_to_server: false,
|
||||||
|
secret: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,9 +621,9 @@ where
|
|||||||
&mut self.write,
|
&mut self.write,
|
||||||
"terminating connection due to administrator command"
|
"terminating connection due to administrator command"
|
||||||
).await?;
|
).await?;
|
||||||
|
self.stats.disconnect();
|
||||||
|
|
||||||
self.stats.disconnect();
|
return Ok(())
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin clients ignore shutdown.
|
// Admin clients ignore shutdown.
|
||||||
@@ -932,7 +777,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Grab a server from the pool.
|
// Grab a server from the pool.
|
||||||
let mut connection = match pool
|
let connection = match pool
|
||||||
.get(query_router.shard(), query_router.role(), &self.stats)
|
.get(query_router.shard(), query_router.role(), &self.stats)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -951,32 +796,18 @@ where
|
|||||||
error!("Got Sync message but failed to get a connection from the pool");
|
error!("Got Sync message but failed to get a connection from the pool");
|
||||||
self.buffer.clear();
|
self.buffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
error_response(&mut self.write, "could not get connection from the pool")
|
error_response(&mut self.write, "could not get connection from the pool")
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
error!(
|
error!("Could not get connection from pool: {{ pool_name: {:?}, username: {:?}, shard: {:?}, role: \"{:?}\", error: \"{:?}\" }}",
|
||||||
"Could not get connection from pool: \
|
self.pool_name.clone(), self.username.clone(), query_router.shard(), query_router.role(), err);
|
||||||
{{ \
|
|
||||||
pool_name: {:?}, \
|
|
||||||
username: {:?}, \
|
|
||||||
shard: {:?}, \
|
|
||||||
role: \"{:?}\", \
|
|
||||||
error: \"{:?}\" \
|
|
||||||
}}",
|
|
||||||
self.pool_name,
|
|
||||||
self.username,
|
|
||||||
query_router.shard(),
|
|
||||||
query_router.role(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let server = &mut *connection.0;
|
let mut reference = connection.0;
|
||||||
let address = connection.1;
|
let address = connection.1;
|
||||||
|
let server = &mut *reference;
|
||||||
|
|
||||||
// Server is assigned to the client in case the client wants to
|
// Server is assigned to the client in case the client wants to
|
||||||
// cancel a query later.
|
// cancel a query later.
|
||||||
@@ -999,7 +830,6 @@ where
|
|||||||
|
|
||||||
// Set application_name.
|
// Set application_name.
|
||||||
server.set_name(&self.application_name).await?;
|
server.set_name(&self.application_name).await?;
|
||||||
server.switch_async(false);
|
|
||||||
|
|
||||||
let mut initial_message = Some(message);
|
let mut initial_message = Some(message);
|
||||||
|
|
||||||
@@ -1019,37 +849,12 @@ where
|
|||||||
None => {
|
None => {
|
||||||
trace!("Waiting for message inside transaction or in session mode");
|
trace!("Waiting for message inside transaction or in session mode");
|
||||||
|
|
||||||
let message = tokio::select! {
|
match tokio::time::timeout(
|
||||||
message = tokio::time::timeout(
|
idle_client_timeout_duration,
|
||||||
idle_client_timeout_duration,
|
read_message(&mut self.read),
|
||||||
read_message(&mut self.read),
|
)
|
||||||
) => message,
|
.await
|
||||||
|
{
|
||||||
server_message = server.recv() => {
|
|
||||||
debug!("Got async message");
|
|
||||||
|
|
||||||
let server_message = match server_message {
|
|
||||||
Ok(message) => message,
|
|
||||||
Err(err) => {
|
|
||||||
pool.ban(&address, BanReason::MessageReceiveFailed, Some(&self.stats));
|
|
||||||
server.mark_bad();
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match write_all_half(&mut self.write, &server_message).await {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(err) => {
|
|
||||||
server.mark_bad();
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match message {
|
|
||||||
Ok(Ok(message)) => message,
|
Ok(Ok(message)) => message,
|
||||||
Ok(Err(err)) => {
|
Ok(Err(err)) => {
|
||||||
// Client disconnected inside a transaction.
|
// Client disconnected inside a transaction.
|
||||||
@@ -1062,25 +867,11 @@ where
|
|||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Client idle in transaction timeout
|
// Client idle in transaction timeout
|
||||||
error_response(&mut self.write, "idle transaction timeout").await?;
|
error_response(&mut self.write, "idle transaction timeout").await?;
|
||||||
error!(
|
error!("Client idle in transaction timeout: {{ pool_name: {:?}, username: {:?}, shard: {:?}, role: \"{:?}\"}}", self.pool_name.clone(), self.username.clone(), query_router.shard(), query_router.role());
|
||||||
"Client idle in transaction timeout: \
|
|
||||||
{{ \
|
|
||||||
pool_name: {}, \
|
|
||||||
username: {}, \
|
|
||||||
shard: {}, \
|
|
||||||
role: \"{:?}\" \
|
|
||||||
}}",
|
|
||||||
self.pool_name,
|
|
||||||
self.username,
|
|
||||||
query_router.shard(),
|
|
||||||
query_router.role()
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(message) => {
|
Some(message) => {
|
||||||
initial_message = None;
|
initial_message = None;
|
||||||
message
|
message
|
||||||
@@ -1153,11 +944,6 @@ where
|
|||||||
self.buffer.put(&message[..]);
|
self.buffer.put(&message[..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the prepared statement.
|
|
||||||
'C' => {
|
|
||||||
self.buffer.put(&message[..]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
// Execute a prepared statement prepared in `P` and bound in `B`.
|
// Execute a prepared statement prepared in `P` and bound in `B`.
|
||||||
'E' => {
|
'E' => {
|
||||||
@@ -1166,14 +952,9 @@ where
|
|||||||
|
|
||||||
// Sync
|
// Sync
|
||||||
// Frontend (client) is asking for the query result now.
|
// Frontend (client) is asking for the query result now.
|
||||||
'S' | 'H' => {
|
'S' => {
|
||||||
debug!("Sending query to server");
|
debug!("Sending query to server");
|
||||||
|
|
||||||
if code == 'H' {
|
|
||||||
server.switch_async(true);
|
|
||||||
debug!("Client requested flush, going async");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.buffer.put(&message[..]);
|
self.buffer.put(&message[..]);
|
||||||
|
|
||||||
let first_message_code = (*self.buffer.get(0).unwrap_or(&0)) as char;
|
let first_message_code = (*self.buffer.get(0).unwrap_or(&0)) as char;
|
||||||
@@ -1287,7 +1068,7 @@ where
|
|||||||
/// Retrieve connection pool, if it exists.
|
/// Retrieve connection pool, if it exists.
|
||||||
/// Return an error to the client otherwise.
|
/// Return an error to the client otherwise.
|
||||||
async fn get_pool(&mut self) -> Result<ConnectionPool, Error> {
|
async fn get_pool(&mut self) -> Result<ConnectionPool, Error> {
|
||||||
match get_pool(&self.pool_name, &self.username) {
|
match get_pool(&self.pool_name, &self.username, self.secret.clone()) {
|
||||||
Some(pool) => Ok(pool),
|
Some(pool) => Ok(pool),
|
||||||
None => {
|
None => {
|
||||||
error_response(
|
error_response(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// Parse the configuration file.
|
/// Parse the configuration file.
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use log::{error, info};
|
use log::{error, info, warn};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
@@ -178,12 +178,29 @@ impl Address {
|
|||||||
pub struct User {
|
pub struct User {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub server_username: Option<String>,
|
|
||||||
pub server_password: Option<String>,
|
|
||||||
pub pool_size: u32,
|
pub pool_size: u32,
|
||||||
pub pool_mode: Option<PoolMode>,
|
|
||||||
#[serde(default)] // 0
|
#[serde(default)] // 0
|
||||||
pub statement_timeout: u64,
|
pub statement_timeout: u64,
|
||||||
|
pub secrets: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
fn validate(&self) -> Result<(), Error> {
|
||||||
|
match self.secrets {
|
||||||
|
Some(ref secrets) => {
|
||||||
|
for secret in secrets.iter() {
|
||||||
|
if secret.len() < 16 {
|
||||||
|
warn!(
|
||||||
|
"[user: {}] Secret is too short (less than 16 characters)",
|
||||||
|
self.username
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for User {
|
impl Default for User {
|
||||||
@@ -191,11 +208,9 @@ impl Default for User {
|
|||||||
User {
|
User {
|
||||||
username: String::from("postgres"),
|
username: String::from("postgres"),
|
||||||
password: None,
|
password: None,
|
||||||
server_username: None,
|
|
||||||
server_password: None,
|
|
||||||
pool_size: 15,
|
pool_size: 15,
|
||||||
statement_timeout: 0,
|
statement_timeout: 0,
|
||||||
pool_mode: None,
|
secrets: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,7 +222,7 @@ pub struct General {
|
|||||||
pub host: String,
|
pub host: String,
|
||||||
|
|
||||||
#[serde(default = "General::default_port")]
|
#[serde(default = "General::default_port")]
|
||||||
pub port: u16,
|
pub port: i16,
|
||||||
|
|
||||||
pub enable_prometheus_exporter: Option<bool>,
|
pub enable_prometheus_exporter: Option<bool>,
|
||||||
pub prometheus_exporter_port: i16,
|
pub prometheus_exporter_port: i16,
|
||||||
@@ -249,8 +264,8 @@ pub struct General {
|
|||||||
#[serde(default = "General::default_worker_threads")]
|
#[serde(default = "General::default_worker_threads")]
|
||||||
pub worker_threads: usize,
|
pub worker_threads: usize,
|
||||||
|
|
||||||
#[serde(default)] // None
|
#[serde(default)] // False
|
||||||
pub autoreload: Option<u64>,
|
pub autoreload: bool,
|
||||||
|
|
||||||
pub tls_certificate: Option<String>,
|
pub tls_certificate: Option<String>,
|
||||||
pub tls_private_key: Option<String>,
|
pub tls_private_key: Option<String>,
|
||||||
@@ -267,7 +282,7 @@ impl General {
|
|||||||
"0.0.0.0".into()
|
"0.0.0.0".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_port() -> u16 {
|
pub fn default_port() -> i16 {
|
||||||
5432
|
5432
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +292,7 @@ impl General {
|
|||||||
|
|
||||||
// These keepalive defaults should detect a dead connection within 30 seconds.
|
// These keepalive defaults should detect a dead connection within 30 seconds.
|
||||||
// Tokio defaults to disabling keepalives which keeps dead connections around indefinitely.
|
// Tokio defaults to disabling keepalives which keeps dead connections around indefinitely.
|
||||||
// This can lead to permanent server pool exhaustion
|
// This can lead to permenant server pool exhaustion
|
||||||
pub fn default_tcp_keepalives_idle() -> u64 {
|
pub fn default_tcp_keepalives_idle() -> u64 {
|
||||||
5 // 5 seconds
|
5 // 5 seconds
|
||||||
}
|
}
|
||||||
@@ -339,7 +354,7 @@ impl Default for General {
|
|||||||
tcp_keepalives_interval: Self::default_tcp_keepalives_interval(),
|
tcp_keepalives_interval: Self::default_tcp_keepalives_interval(),
|
||||||
log_client_connections: false,
|
log_client_connections: false,
|
||||||
log_client_disconnections: false,
|
log_client_disconnections: false,
|
||||||
autoreload: None,
|
autoreload: false,
|
||||||
tls_certificate: None,
|
tls_certificate: None,
|
||||||
tls_private_key: None,
|
tls_private_key: None,
|
||||||
admin_username: String::from("admin"),
|
admin_username: String::from("admin"),
|
||||||
@@ -362,7 +377,6 @@ pub enum PoolMode {
|
|||||||
#[serde(alias = "session", alias = "Session")]
|
#[serde(alias = "session", alias = "Session")]
|
||||||
Session,
|
Session,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for PoolMode {
|
impl ToString for PoolMode {
|
||||||
fn to_string(&self) -> String {
|
fn to_string(&self) -> String {
|
||||||
match *self {
|
match *self {
|
||||||
@@ -426,7 +440,7 @@ pub struct Pool {
|
|||||||
|
|
||||||
pub shards: BTreeMap<String, Shard>,
|
pub shards: BTreeMap<String, Shard>,
|
||||||
pub users: BTreeMap<String, User>,
|
pub users: BTreeMap<String, User>,
|
||||||
// Note, don't put simple fields below these configs. There's a compatibility issue with TOML that makes it
|
// Note, don't put simple fields below these configs. There's a compatability issue with TOML that makes it
|
||||||
// incompatible to have simple fields in TOML after complex objects. See
|
// incompatible to have simple fields in TOML after complex objects. See
|
||||||
// https://users.rust-lang.org/t/why-toml-to-string-get-error-valueaftertable/85903
|
// https://users.rust-lang.org/t/why-toml-to-string-get-error-valueaftertable/85903
|
||||||
}
|
}
|
||||||
@@ -515,6 +529,10 @@ impl Pool {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for user in self.users.iter() {
|
||||||
|
user.1.validate()?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -664,6 +682,11 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks that we configured TLS.
|
||||||
|
pub fn tls_enabled(&self) -> bool {
|
||||||
|
self.general.tls_certificate.is_some() && self.general.tls_private_key.is_some()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -823,9 +846,8 @@ impl Config {
|
|||||||
.to_string()
|
.to_string()
|
||||||
);
|
);
|
||||||
info!(
|
info!(
|
||||||
"[pool: {}] Default pool mode: {}",
|
"[pool: {}] Pool mode: {:?}",
|
||||||
pool_name,
|
pool_name, pool_config.pool_mode
|
||||||
pool_config.pool_mode.to_string()
|
|
||||||
);
|
);
|
||||||
info!(
|
info!(
|
||||||
"[pool: {}] Load Balancing mode: {:?}",
|
"[pool: {}] Load Balancing mode: {:?}",
|
||||||
@@ -876,16 +898,7 @@ impl Config {
|
|||||||
info!(
|
info!(
|
||||||
"[pool: {}][user: {}] Statement timeout: {}",
|
"[pool: {}][user: {}] Statement timeout: {}",
|
||||||
pool_name, user.1.username, user.1.statement_timeout
|
pool_name, user.1.username, user.1.statement_timeout
|
||||||
);
|
)
|
||||||
info!(
|
|
||||||
"[pool: {}][user: {}] Pool mode: {}",
|
|
||||||
pool_name,
|
|
||||||
user.1.username,
|
|
||||||
match user.1.pool_mode {
|
|
||||||
Some(pool_mode) => pool_mode.to_string(),
|
|
||||||
None => pool_config.pool_mode.to_string(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
src/errors.rs
102
src/errors.rs
@@ -1,19 +1,13 @@
|
|||||||
//! Errors.
|
/// Errors.
|
||||||
|
|
||||||
/// Various errors.
|
/// Various errors.
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
SocketError(String),
|
SocketError(String),
|
||||||
ClientSocketError(String, ClientIdentifier),
|
|
||||||
ClientGeneralError(String, ClientIdentifier),
|
|
||||||
ClientAuthImpossible(String),
|
|
||||||
ClientAuthPassthroughError(String, ClientIdentifier),
|
|
||||||
ClientBadStartup,
|
ClientBadStartup,
|
||||||
ProtocolSyncError(String),
|
ProtocolSyncError(String),
|
||||||
BadQuery(String),
|
BadQuery(String),
|
||||||
ServerError,
|
ServerError,
|
||||||
ServerStartupError(String, ServerIdentifier),
|
|
||||||
ServerAuthError(String, ServerIdentifier),
|
|
||||||
BadConfig,
|
BadConfig,
|
||||||
AllServersDown,
|
AllServersDown,
|
||||||
ClientError(String),
|
ClientError(String),
|
||||||
@@ -24,97 +18,3 @@ pub enum Error {
|
|||||||
AuthError(String),
|
AuthError(String),
|
||||||
AuthPassthroughError(String),
|
AuthPassthroughError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
|
||||||
pub struct ClientIdentifier {
|
|
||||||
pub application_name: String,
|
|
||||||
pub username: String,
|
|
||||||
pub pool_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientIdentifier {
|
|
||||||
pub fn new(application_name: &str, username: &str, pool_name: &str) -> ClientIdentifier {
|
|
||||||
ClientIdentifier {
|
|
||||||
application_name: application_name.into(),
|
|
||||||
username: username.into(),
|
|
||||||
pool_name: pool_name.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ClientIdentifier {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{{ application_name: {}, username: {}, pool_name: {} }}",
|
|
||||||
self.application_name, self.username, self.pool_name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
|
||||||
pub struct ServerIdentifier {
|
|
||||||
pub username: String,
|
|
||||||
pub database: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerIdentifier {
|
|
||||||
pub fn new(username: &str, database: &str) -> ServerIdentifier {
|
|
||||||
ServerIdentifier {
|
|
||||||
username: username.into(),
|
|
||||||
database: database.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ServerIdentifier {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{{ username: {}, database: {} }}",
|
|
||||||
self.username, self.database
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
match &self {
|
|
||||||
&Error::ClientSocketError(error, client_identifier) => write!(
|
|
||||||
f,
|
|
||||||
"Error reading {} from client {}",
|
|
||||||
error, client_identifier
|
|
||||||
),
|
|
||||||
&Error::ClientGeneralError(error, client_identifier) => {
|
|
||||||
write!(f, "{} {}", error, client_identifier)
|
|
||||||
}
|
|
||||||
&Error::ClientAuthImpossible(username) => write!(
|
|
||||||
f,
|
|
||||||
"Client auth not possible, \
|
|
||||||
no cleartext password set for username: {} \
|
|
||||||
in config and auth passthrough (query_auth) \
|
|
||||||
is not set up.",
|
|
||||||
username
|
|
||||||
),
|
|
||||||
&Error::ClientAuthPassthroughError(error, client_identifier) => write!(
|
|
||||||
f,
|
|
||||||
"No cleartext password set, \
|
|
||||||
and no auth passthrough could not \
|
|
||||||
obtain the hash from server for {}, \
|
|
||||||
the error was: {}",
|
|
||||||
client_identifier, error
|
|
||||||
),
|
|
||||||
&Error::ServerStartupError(error, server_identifier) => write!(
|
|
||||||
f,
|
|
||||||
"Error reading {} on server startup {}",
|
|
||||||
error, server_identifier,
|
|
||||||
),
|
|
||||||
&Error::ServerAuthError(error, server_identifier) => {
|
|
||||||
write!(f, "{} for {}", error, server_identifier,)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The rest can use Debug.
|
|
||||||
err => write!(f, "{:?}", err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
35
src/main.rs
35
src/main.rs
@@ -61,6 +61,7 @@ use std::sync::Arc;
|
|||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
mod admin;
|
mod admin;
|
||||||
|
mod auth;
|
||||||
mod auth_passthrough;
|
mod auth_passthrough;
|
||||||
mod client;
|
mod client;
|
||||||
mod config;
|
mod config;
|
||||||
@@ -79,7 +80,6 @@ mod stats;
|
|||||||
mod tls;
|
mod tls;
|
||||||
|
|
||||||
use crate::config::{get_config, reload_config, VERSION};
|
use crate::config::{get_config, reload_config, VERSION};
|
||||||
use crate::messages::configure_socket;
|
|
||||||
use crate::pool::{ClientServerMap, ConnectionPool};
|
use crate::pool::{ClientServerMap, ConnectionPool};
|
||||||
use crate::prometheus::start_metric_server;
|
use crate::prometheus::start_metric_server;
|
||||||
use crate::stats::{Collector, Reporter, REPORTER};
|
use crate::stats::{Collector, Reporter, REPORTER};
|
||||||
@@ -180,19 +180,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
stats_collector.collect().await;
|
stats_collector.collect().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("Config autoreloader: {}", match config.general.autoreload {
|
info!("Config autoreloader: {}", config.general.autoreload);
|
||||||
Some(interval) => format!("{} ms", interval),
|
|
||||||
None => "disabled".into(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(interval) = config.general.autoreload {
|
let mut autoreload_interval = tokio::time::interval(tokio::time::Duration::from_millis(15_000));
|
||||||
let mut autoreload_interval = tokio::time::interval(tokio::time::Duration::from_millis(interval));
|
let autoreload_client_server_map = client_server_map.clone();
|
||||||
let autoreload_client_server_map = client_server_map.clone();
|
|
||||||
|
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
autoreload_interval.tick().await;
|
autoreload_interval.tick().await;
|
||||||
debug!("Automatically reloading config");
|
if config.general.autoreload {
|
||||||
|
info!("Automatically reloading config");
|
||||||
|
|
||||||
if let Ok(changed) = reload_config(autoreload_client_server_map.clone()).await {
|
if let Ok(changed) = reload_config(autoreload_client_server_map.clone()).await {
|
||||||
if changed {
|
if changed {
|
||||||
@@ -200,10 +197,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let mut term_signal = win_signal::ctrl_close().unwrap();
|
let mut term_signal = win_signal::ctrl_close().unwrap();
|
||||||
@@ -288,9 +283,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let drain_tx = drain_tx.clone();
|
let drain_tx = drain_tx.clone();
|
||||||
let client_server_map = client_server_map.clone();
|
let client_server_map = client_server_map.clone();
|
||||||
|
|
||||||
let tls_certificate = get_config().general.tls_certificate.clone();
|
let tls_certificate = config.general.tls_certificate.clone();
|
||||||
|
|
||||||
configure_socket(&socket);
|
|
||||||
|
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let start = chrono::offset::Utc::now().naive_utc();
|
let start = chrono::offset::Utc::now().naive_utc();
|
||||||
@@ -301,7 +294,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
shutdown_rx,
|
shutdown_rx,
|
||||||
drain_tx,
|
drain_tx,
|
||||||
admin_only,
|
admin_only,
|
||||||
tls_certificate,
|
tls_certificate.clone(),
|
||||||
config.general.log_client_connections,
|
config.general.log_client_connections,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -309,7 +302,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let duration = chrono::offset::Utc::now().naive_utc() - start;
|
let duration = chrono::offset::Utc::now().naive_utc() - start;
|
||||||
|
|
||||||
if get_config().general.log_client_disconnections {
|
if config.general.log_client_disconnections {
|
||||||
info!(
|
info!(
|
||||||
"Client {:?} disconnected, session duration: {}",
|
"Client {:?} disconnected, session duration: {}",
|
||||||
addr,
|
addr,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// Helper functions to send one-off protocol messages
|
/// Helper functions to send one-off protocol messages
|
||||||
/// and handle TcpStream (TCP socket).
|
/// and handle TcpStream (TCP socket).
|
||||||
use bytes::{Buf, BufMut, BytesMut};
|
use bytes::{Buf, BufMut, BytesMut};
|
||||||
use log::error;
|
use log::{debug, error};
|
||||||
use md5::{Digest, Md5};
|
use md5::{Digest, Md5};
|
||||||
use socket2::{SockRef, TcpKeepalive};
|
use socket2::{SockRef, TcpKeepalive};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
@@ -46,29 +46,6 @@ where
|
|||||||
write_all(stream, auth_ok).await
|
write_all(stream, auth_ok).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate md5 password challenge.
|
|
||||||
pub async fn md5_challenge<S>(stream: &mut S) -> Result<[u8; 4], Error>
|
|
||||||
where
|
|
||||||
S: tokio::io::AsyncWrite + std::marker::Unpin,
|
|
||||||
{
|
|
||||||
// let mut rng = rand::thread_rng();
|
|
||||||
let salt: [u8; 4] = [
|
|
||||||
rand::random(),
|
|
||||||
rand::random(),
|
|
||||||
rand::random(),
|
|
||||||
rand::random(),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut res = BytesMut::new();
|
|
||||||
res.put_u8(b'R');
|
|
||||||
res.put_i32(12);
|
|
||||||
res.put_i32(5); // MD5
|
|
||||||
res.put_slice(&salt[..]);
|
|
||||||
|
|
||||||
write_all(stream, res).await?;
|
|
||||||
Ok(salt)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Give the client the process_id and secret we generated
|
/// Give the client the process_id and secret we generated
|
||||||
/// used in query cancellation.
|
/// used in query cancellation.
|
||||||
pub async fn backend_key_data<S>(
|
pub async fn backend_key_data<S>(
|
||||||
@@ -257,6 +234,8 @@ pub async fn md5_password_with_hash<S>(stream: &mut S, hash: &str, salt: &[u8])
|
|||||||
where
|
where
|
||||||
S: tokio::io::AsyncWrite + std::marker::Unpin,
|
S: tokio::io::AsyncWrite + std::marker::Unpin,
|
||||||
{
|
{
|
||||||
|
debug!("Sending hash {} to server", hash);
|
||||||
|
|
||||||
let password = md5_hash_second_pass(hash, salt);
|
let password = md5_hash_second_pass(hash, salt);
|
||||||
let mut message = BytesMut::with_capacity(password.len() as usize + 5);
|
let mut message = BytesMut::with_capacity(password.len() as usize + 5);
|
||||||
|
|
||||||
@@ -404,7 +383,7 @@ pub fn row_description(columns: &Vec<(&str, DataType)>) -> BytesMut {
|
|||||||
let mut res = BytesMut::new();
|
let mut res = BytesMut::new();
|
||||||
let mut row_desc = BytesMut::new();
|
let mut row_desc = BytesMut::new();
|
||||||
|
|
||||||
// how many columns we are storing
|
// how many colums we are storing
|
||||||
row_desc.put_i16(columns.len() as i16);
|
row_desc.put_i16(columns.len() as i16);
|
||||||
|
|
||||||
for (name, data_type) in columns {
|
for (name, data_type) in columns {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ impl MirroredClient {
|
|||||||
None => (default, default, crate::config::Pool::default()),
|
None => (default, default, crate::config::Pool::default()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let identifier = PoolIdentifier::new(&self.database, &self.user.username);
|
let identifier = PoolIdentifier::new(&self.database, &self.user.username, None);
|
||||||
|
|
||||||
let manager = ServerPool::new(
|
let manager = ServerPool::new(
|
||||||
self.address.clone(),
|
self.address.clone(),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use log::{Level, Log, Metadata, Record, SetLoggerError};
|
|||||||
//
|
//
|
||||||
// So to summarize, if no `STDOUT_LOG` env var is present, the logger is the default logger. If `STDOUT_LOG` is set, everything
|
// So to summarize, if no `STDOUT_LOG` env var is present, the logger is the default logger. If `STDOUT_LOG` is set, everything
|
||||||
// but errors, that matches the log level set in the `STDOUT_LOG` env var is sent to stdout. You can have also some esoteric configuration
|
// but errors, that matches the log level set in the `STDOUT_LOG` env var is sent to stdout. You can have also some esoteric configuration
|
||||||
// where you set `RUST_LOG=debug` and `STDOUT_LOG=info`, in here, errors will go to stderr, warns and infos to stdout and debugs to stderr.
|
// where you set `RUST_LOG=debug` and `STDOUT_LOG=info`, in here, erros will go to stderr, warns and infos to stdout and debugs to stderr.
|
||||||
//
|
//
|
||||||
pub struct MultiLogger {
|
pub struct MultiLogger {
|
||||||
stderr_logger: env_logger::Logger,
|
stderr_logger: env_logger::Logger,
|
||||||
|
|||||||
476
src/pool.rs
476
src/pool.rs
@@ -59,24 +59,22 @@ pub struct PoolIdentifier {
|
|||||||
|
|
||||||
/// The username the client connects with. Each user gets its own pool.
|
/// The username the client connects with. Each user gets its own pool.
|
||||||
pub user: String,
|
pub user: String,
|
||||||
|
|
||||||
|
/// The client secret (password).
|
||||||
|
pub secret: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PoolIdentifier {
|
impl PoolIdentifier {
|
||||||
/// Create a new user/pool identifier.
|
/// Create a new user/pool identifier.
|
||||||
pub fn new(db: &str, user: &str) -> PoolIdentifier {
|
pub fn new(db: &str, user: &str, secret: Option<String>) -> PoolIdentifier {
|
||||||
PoolIdentifier {
|
PoolIdentifier {
|
||||||
db: db.to_string(),
|
db: db.to_string(),
|
||||||
user: user.to_string(),
|
user: user.to_string(),
|
||||||
|
secret,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Address> for PoolIdentifier {
|
|
||||||
fn from(address: &Address) -> PoolIdentifier {
|
|
||||||
PoolIdentifier::new(&address.database, &address.username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pool settings.
|
/// Pool settings.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PoolSettings {
|
pub struct PoolSettings {
|
||||||
@@ -210,227 +208,241 @@ impl ConnectionPool {
|
|||||||
|
|
||||||
// There is one pool per database/user pair.
|
// There is one pool per database/user pair.
|
||||||
for user in pool_config.users.values() {
|
for user in pool_config.users.values() {
|
||||||
let old_pool_ref = get_pool(pool_name, &user.username);
|
let mut secrets = match &user.secrets {
|
||||||
let identifier = PoolIdentifier::new(pool_name, &user.username);
|
Some(_) => user
|
||||||
|
.secrets
|
||||||
match old_pool_ref {
|
.as_ref()
|
||||||
Some(pool) => {
|
.unwrap()
|
||||||
// If the pool hasn't changed, get existing reference and insert it into the new_pools.
|
.iter()
|
||||||
// We replace all pools at the end, but if the reference is kept, the pool won't get re-created (bb8).
|
.map(|secret| Some(secret.to_string()))
|
||||||
if pool.config_hash == new_pool_hash_value {
|
.collect::<Vec<Option<String>>>(),
|
||||||
info!(
|
None => vec![],
|
||||||
"[pool: {}][user: {}] has not changed",
|
|
||||||
pool_name, user.username
|
|
||||||
);
|
|
||||||
new_pools.insert(identifier.clone(), pool.clone());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"[pool: {}][user: {}] creating new pool",
|
|
||||||
pool_name, user.username
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut shards = Vec::new();
|
|
||||||
let mut addresses = Vec::new();
|
|
||||||
let mut banlist = Vec::new();
|
|
||||||
let mut shard_ids = pool_config
|
|
||||||
.shards
|
|
||||||
.clone()
|
|
||||||
.into_keys()
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
let pool_stats = Arc::new(PoolStats::new(identifier, pool_config.clone()));
|
|
||||||
|
|
||||||
// Allow the pool to be seen in statistics
|
|
||||||
pool_stats.register(pool_stats.clone());
|
|
||||||
|
|
||||||
// Sort by shard number to ensure consistency.
|
|
||||||
shard_ids.sort_by_key(|k| k.parse::<i64>().unwrap());
|
|
||||||
let pool_auth_hash: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
|
||||||
|
|
||||||
for shard_idx in &shard_ids {
|
|
||||||
let shard = &pool_config.shards[shard_idx];
|
|
||||||
let mut pools = Vec::new();
|
|
||||||
let mut servers = Vec::new();
|
|
||||||
let mut replica_number = 0;
|
|
||||||
|
|
||||||
// Load Mirror settings
|
|
||||||
for (address_index, server) in shard.servers.iter().enumerate() {
|
|
||||||
let mut mirror_addresses = vec![];
|
|
||||||
if let Some(mirror_settings_vec) = &shard.mirrors {
|
|
||||||
for (mirror_idx, mirror_settings) in
|
|
||||||
mirror_settings_vec.iter().enumerate()
|
|
||||||
{
|
|
||||||
if mirror_settings.mirroring_target_index != address_index {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
mirror_addresses.push(Address {
|
|
||||||
id: address_id,
|
|
||||||
database: shard.database.clone(),
|
|
||||||
host: mirror_settings.host.clone(),
|
|
||||||
port: mirror_settings.port,
|
|
||||||
role: server.role,
|
|
||||||
address_index: mirror_idx,
|
|
||||||
replica_number,
|
|
||||||
shard: shard_idx.parse::<usize>().unwrap(),
|
|
||||||
username: user.username.clone(),
|
|
||||||
pool_name: pool_name.clone(),
|
|
||||||
mirrors: vec![],
|
|
||||||
stats: Arc::new(AddressStats::default()),
|
|
||||||
});
|
|
||||||
address_id += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let address = Address {
|
|
||||||
id: address_id,
|
|
||||||
database: shard.database.clone(),
|
|
||||||
host: server.host.clone(),
|
|
||||||
port: server.port,
|
|
||||||
role: server.role,
|
|
||||||
address_index,
|
|
||||||
replica_number,
|
|
||||||
shard: shard_idx.parse::<usize>().unwrap(),
|
|
||||||
username: user.username.clone(),
|
|
||||||
pool_name: pool_name.clone(),
|
|
||||||
mirrors: mirror_addresses,
|
|
||||||
stats: Arc::new(AddressStats::default()),
|
|
||||||
};
|
|
||||||
|
|
||||||
address_id += 1;
|
|
||||||
|
|
||||||
if server.role == Role::Replica {
|
|
||||||
replica_number += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We assume every server in the pool share user/passwords
|
|
||||||
let auth_passthrough = AuthPassthrough::from_pool_config(pool_config);
|
|
||||||
|
|
||||||
if let Some(apt) = &auth_passthrough {
|
|
||||||
match apt.fetch_hash(&address).await {
|
|
||||||
Ok(ok) => {
|
|
||||||
if let Some(ref pool_auth_hash_value) = *(pool_auth_hash.read()) {
|
|
||||||
if ok != *pool_auth_hash_value {
|
|
||||||
warn!("Hash is not the same across shards of the same pool, client auth will \
|
|
||||||
be done using last obtained hash. Server: {}:{}, Database: {}", server.host, server.port, shard.database);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug!("Hash obtained for {:?}", address);
|
|
||||||
{
|
|
||||||
let mut pool_auth_hash = pool_auth_hash.write();
|
|
||||||
*pool_auth_hash = Some(ok.clone());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => warn!("Could not obtain password hashes using auth_query config, ignoring. Error: {:?}", err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let manager = ServerPool::new(
|
|
||||||
address.clone(),
|
|
||||||
user.clone(),
|
|
||||||
&shard.database,
|
|
||||||
client_server_map.clone(),
|
|
||||||
pool_stats.clone(),
|
|
||||||
pool_auth_hash.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let connect_timeout = match pool_config.connect_timeout {
|
|
||||||
Some(connect_timeout) => connect_timeout,
|
|
||||||
None => config.general.connect_timeout,
|
|
||||||
};
|
|
||||||
|
|
||||||
let idle_timeout = match pool_config.idle_timeout {
|
|
||||||
Some(idle_timeout) => idle_timeout,
|
|
||||||
None => config.general.idle_timeout,
|
|
||||||
};
|
|
||||||
|
|
||||||
let pool = Pool::builder()
|
|
||||||
.max_size(user.pool_size)
|
|
||||||
.connection_timeout(std::time::Duration::from_millis(connect_timeout))
|
|
||||||
.idle_timeout(Some(std::time::Duration::from_millis(idle_timeout)))
|
|
||||||
.test_on_check_out(false)
|
|
||||||
.build(manager)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
pools.push(pool);
|
|
||||||
servers.push(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
shards.push(pools);
|
|
||||||
addresses.push(servers);
|
|
||||||
banlist.push(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(shards.len(), addresses.len());
|
|
||||||
if let Some(ref _auth_hash) = *(pool_auth_hash.clone().read()) {
|
|
||||||
info!(
|
|
||||||
"Auth hash obtained from query_auth for pool {{ name: {}, user: {} }}",
|
|
||||||
pool_name, user.username
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let pool = ConnectionPool {
|
|
||||||
databases: shards,
|
|
||||||
stats: pool_stats,
|
|
||||||
addresses,
|
|
||||||
banlist: Arc::new(RwLock::new(banlist)),
|
|
||||||
config_hash: new_pool_hash_value,
|
|
||||||
server_info: Arc::new(RwLock::new(BytesMut::new())),
|
|
||||||
auth_hash: pool_auth_hash,
|
|
||||||
settings: PoolSettings {
|
|
||||||
pool_mode: match user.pool_mode {
|
|
||||||
Some(pool_mode) => pool_mode,
|
|
||||||
None => pool_config.pool_mode,
|
|
||||||
},
|
|
||||||
load_balancing_mode: pool_config.load_balancing_mode,
|
|
||||||
// shards: pool_config.shards.clone(),
|
|
||||||
shards: shard_ids.len(),
|
|
||||||
user: user.clone(),
|
|
||||||
default_role: match pool_config.default_role.as_str() {
|
|
||||||
"any" => None,
|
|
||||||
"replica" => Some(Role::Replica),
|
|
||||||
"primary" => Some(Role::Primary),
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
query_parser_enabled: pool_config.query_parser_enabled,
|
|
||||||
primary_reads_enabled: pool_config.primary_reads_enabled,
|
|
||||||
sharding_function: pool_config.sharding_function,
|
|
||||||
automatic_sharding_key: pool_config.automatic_sharding_key.clone(),
|
|
||||||
healthcheck_delay: config.general.healthcheck_delay,
|
|
||||||
healthcheck_timeout: config.general.healthcheck_timeout,
|
|
||||||
ban_time: config.general.ban_time,
|
|
||||||
sharding_key_regex: pool_config
|
|
||||||
.sharding_key_regex
|
|
||||||
.clone()
|
|
||||||
.map(|regex| Regex::new(regex.as_str()).unwrap()),
|
|
||||||
shard_id_regex: pool_config
|
|
||||||
.shard_id_regex
|
|
||||||
.clone()
|
|
||||||
.map(|regex| Regex::new(regex.as_str()).unwrap()),
|
|
||||||
regex_search_limit: pool_config.regex_search_limit.unwrap_or(1000),
|
|
||||||
auth_query: pool_config.auth_query.clone(),
|
|
||||||
auth_query_user: pool_config.auth_query_user.clone(),
|
|
||||||
auth_query_password: pool_config.auth_query_password.clone(),
|
|
||||||
},
|
|
||||||
validated: Arc::new(AtomicBool::new(false)),
|
|
||||||
paused: Arc::new(AtomicBool::new(false)),
|
|
||||||
paused_waiter: Arc::new(Notify::new()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect to the servers to make sure pool configuration is valid
|
secrets.push(None);
|
||||||
// before setting it globally.
|
|
||||||
// Do this async and somewhere else, we don't have to wait here.
|
|
||||||
let mut validate_pool = pool.clone();
|
|
||||||
tokio::task::spawn(async move {
|
|
||||||
let _ = validate_pool.validate().await;
|
|
||||||
});
|
|
||||||
|
|
||||||
// There is one pool per database/user pair.
|
for secret in secrets {
|
||||||
new_pools.insert(PoolIdentifier::new(pool_name, &user.username), pool);
|
let old_pool_ref = get_pool(pool_name, &user.username, secret.clone());
|
||||||
|
let identifier = PoolIdentifier::new(pool_name, &user.username, secret.clone());
|
||||||
|
|
||||||
|
match old_pool_ref {
|
||||||
|
Some(pool) => {
|
||||||
|
// If the pool hasn't changed, get existing reference and insert it into the new_pools.
|
||||||
|
// We replace all pools at the end, but if the reference is kept, the pool won't get re-created (bb8).
|
||||||
|
if pool.config_hash == new_pool_hash_value {
|
||||||
|
info!(
|
||||||
|
"[pool: {}][user: {}] has not changed",
|
||||||
|
pool_name, user.username
|
||||||
|
);
|
||||||
|
new_pools.insert(identifier.clone(), pool.clone());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"[pool: {}][user: {}] creating new pool",
|
||||||
|
pool_name, user.username
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut shards = Vec::new();
|
||||||
|
let mut addresses = Vec::new();
|
||||||
|
let mut banlist = Vec::new();
|
||||||
|
let mut shard_ids = pool_config
|
||||||
|
.shards
|
||||||
|
.clone()
|
||||||
|
.into_keys()
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
let pool_stats = Arc::new(PoolStats::new(identifier, pool_config.clone()));
|
||||||
|
|
||||||
|
// Allow the pool to be seen in statistics
|
||||||
|
pool_stats.register(pool_stats.clone());
|
||||||
|
|
||||||
|
// Sort by shard number to ensure consistency.
|
||||||
|
shard_ids.sort_by_key(|k| k.parse::<i64>().unwrap());
|
||||||
|
let pool_auth_hash: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||||
|
|
||||||
|
for shard_idx in &shard_ids {
|
||||||
|
let shard = &pool_config.shards[shard_idx];
|
||||||
|
let mut pools = Vec::new();
|
||||||
|
let mut servers = Vec::new();
|
||||||
|
let mut replica_number = 0;
|
||||||
|
|
||||||
|
// Load Mirror settings
|
||||||
|
for (address_index, server) in shard.servers.iter().enumerate() {
|
||||||
|
let mut mirror_addresses = vec![];
|
||||||
|
if let Some(mirror_settings_vec) = &shard.mirrors {
|
||||||
|
for (mirror_idx, mirror_settings) in
|
||||||
|
mirror_settings_vec.iter().enumerate()
|
||||||
|
{
|
||||||
|
if mirror_settings.mirroring_target_index != address_index {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mirror_addresses.push(Address {
|
||||||
|
id: address_id,
|
||||||
|
database: shard.database.clone(),
|
||||||
|
host: mirror_settings.host.clone(),
|
||||||
|
port: mirror_settings.port,
|
||||||
|
role: server.role,
|
||||||
|
address_index: mirror_idx,
|
||||||
|
replica_number,
|
||||||
|
shard: shard_idx.parse::<usize>().unwrap(),
|
||||||
|
username: user.username.clone(),
|
||||||
|
pool_name: pool_name.clone(),
|
||||||
|
mirrors: vec![],
|
||||||
|
stats: Arc::new(AddressStats::default()),
|
||||||
|
});
|
||||||
|
address_id += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let address = Address {
|
||||||
|
id: address_id,
|
||||||
|
database: shard.database.clone(),
|
||||||
|
host: server.host.clone(),
|
||||||
|
port: server.port,
|
||||||
|
role: server.role,
|
||||||
|
address_index,
|
||||||
|
replica_number,
|
||||||
|
shard: shard_idx.parse::<usize>().unwrap(),
|
||||||
|
username: user.username.clone(),
|
||||||
|
pool_name: pool_name.clone(),
|
||||||
|
mirrors: mirror_addresses,
|
||||||
|
stats: Arc::new(AddressStats::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
address_id += 1;
|
||||||
|
|
||||||
|
if server.role == Role::Replica {
|
||||||
|
replica_number += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume every server in the pool share user/passwords
|
||||||
|
let auth_passthrough = AuthPassthrough::from_pool_config(pool_config);
|
||||||
|
|
||||||
|
if let Some(apt) = &auth_passthrough {
|
||||||
|
match apt.fetch_hash(&address).await {
|
||||||
|
Ok(ok) => {
|
||||||
|
if let Some(ref pool_auth_hash_value) = *(pool_auth_hash.read()) {
|
||||||
|
if ok != *pool_auth_hash_value {
|
||||||
|
warn!("Hash is not the same across shards of the same pool, client auth will \
|
||||||
|
be done using last obtained hash. Server: {}:{}, Database: {}", server.host, server.port, shard.database);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("Hash obtained for {:?}", address);
|
||||||
|
{
|
||||||
|
let mut pool_auth_hash = pool_auth_hash.write();
|
||||||
|
*pool_auth_hash = Some(ok.clone());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => warn!("Could not obtain password hashes using auth_query config, ignoring. Error: {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let manager = ServerPool::new(
|
||||||
|
address.clone(),
|
||||||
|
user.clone(),
|
||||||
|
&shard.database,
|
||||||
|
client_server_map.clone(),
|
||||||
|
pool_stats.clone(),
|
||||||
|
pool_auth_hash.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let connect_timeout = match pool_config.connect_timeout {
|
||||||
|
Some(connect_timeout) => connect_timeout,
|
||||||
|
None => config.general.connect_timeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
let idle_timeout = match pool_config.idle_timeout {
|
||||||
|
Some(idle_timeout) => idle_timeout,
|
||||||
|
None => config.general.idle_timeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
let pool = Pool::builder()
|
||||||
|
.max_size(user.pool_size)
|
||||||
|
.connection_timeout(std::time::Duration::from_millis(
|
||||||
|
connect_timeout,
|
||||||
|
))
|
||||||
|
.idle_timeout(Some(std::time::Duration::from_millis(idle_timeout)))
|
||||||
|
.test_on_check_out(false)
|
||||||
|
.build(manager)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
pools.push(pool);
|
||||||
|
servers.push(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
shards.push(pools);
|
||||||
|
addresses.push(servers);
|
||||||
|
banlist.push(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(shards.len(), addresses.len());
|
||||||
|
if let Some(ref _auth_hash) = *(pool_auth_hash.clone().read()) {
|
||||||
|
info!(
|
||||||
|
"Auth hash obtained from query_auth for pool {{ name: {}, user: {} }}",
|
||||||
|
pool_name, user.username
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pool = ConnectionPool {
|
||||||
|
databases: shards,
|
||||||
|
stats: pool_stats,
|
||||||
|
addresses,
|
||||||
|
banlist: Arc::new(RwLock::new(banlist)),
|
||||||
|
config_hash: new_pool_hash_value,
|
||||||
|
server_info: Arc::new(RwLock::new(BytesMut::new())),
|
||||||
|
auth_hash: pool_auth_hash,
|
||||||
|
settings: PoolSettings {
|
||||||
|
pool_mode: pool_config.pool_mode,
|
||||||
|
load_balancing_mode: pool_config.load_balancing_mode,
|
||||||
|
// shards: pool_config.shards.clone(),
|
||||||
|
shards: shard_ids.len(),
|
||||||
|
user: user.clone(),
|
||||||
|
default_role: match pool_config.default_role.as_str() {
|
||||||
|
"any" => None,
|
||||||
|
"replica" => Some(Role::Replica),
|
||||||
|
"primary" => Some(Role::Primary),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
query_parser_enabled: pool_config.query_parser_enabled,
|
||||||
|
primary_reads_enabled: pool_config.primary_reads_enabled,
|
||||||
|
sharding_function: pool_config.sharding_function,
|
||||||
|
automatic_sharding_key: pool_config.automatic_sharding_key.clone(),
|
||||||
|
healthcheck_delay: config.general.healthcheck_delay,
|
||||||
|
healthcheck_timeout: config.general.healthcheck_timeout,
|
||||||
|
ban_time: config.general.ban_time,
|
||||||
|
sharding_key_regex: pool_config
|
||||||
|
.sharding_key_regex
|
||||||
|
.clone()
|
||||||
|
.map(|regex| Regex::new(regex.as_str()).unwrap()),
|
||||||
|
shard_id_regex: pool_config
|
||||||
|
.shard_id_regex
|
||||||
|
.clone()
|
||||||
|
.map(|regex| Regex::new(regex.as_str()).unwrap()),
|
||||||
|
regex_search_limit: pool_config.regex_search_limit.unwrap_or(1000),
|
||||||
|
auth_query: pool_config.auth_query.clone(),
|
||||||
|
auth_query_user: pool_config.auth_query_user.clone(),
|
||||||
|
auth_query_password: pool_config.auth_query_password.clone(),
|
||||||
|
},
|
||||||
|
validated: Arc::new(AtomicBool::new(false)),
|
||||||
|
paused: Arc::new(AtomicBool::new(false)),
|
||||||
|
paused_waiter: Arc::new(Notify::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connect to the servers to make sure pool configuration is valid
|
||||||
|
// before setting it globally.
|
||||||
|
// Do this async and somewhere else, we don't have to wait here.
|
||||||
|
let mut validate_pool = pool.clone();
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
let _ = validate_pool.validate().await;
|
||||||
|
});
|
||||||
|
|
||||||
|
// There is one pool per database/user pair.
|
||||||
|
new_pools.insert(PoolIdentifier::new(pool_name, &user.username, secret), pool);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,7 +789,6 @@ impl ConnectionPool {
|
|||||||
self.databases.len()
|
self.databases.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve all bans for all servers.
|
|
||||||
pub fn get_bans(&self) -> Vec<(Address, (BanReason, NaiveDateTime))> {
|
pub fn get_bans(&self) -> Vec<(Address, (BanReason, NaiveDateTime))> {
|
||||||
let mut bans: Vec<(Address, (BanReason, NaiveDateTime))> = Vec::new();
|
let mut bans: Vec<(Address, (BanReason, NaiveDateTime))> = Vec::new();
|
||||||
let guard = self.banlist.read();
|
let guard = self.banlist.read();
|
||||||
@@ -789,7 +800,7 @@ impl ConnectionPool {
|
|||||||
return bans;
|
return bans;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the address from the host url.
|
/// Get the address from the host url
|
||||||
pub fn get_addresses_from_host(&self, host: &str) -> Vec<Address> {
|
pub fn get_addresses_from_host(&self, host: &str) -> Vec<Address> {
|
||||||
let mut addresses = Vec::new();
|
let mut addresses = Vec::new();
|
||||||
for shard in 0..self.shards() {
|
for shard in 0..self.shards() {
|
||||||
@@ -828,13 +839,10 @@ impl ConnectionPool {
|
|||||||
&self.addresses[shard][server]
|
&self.addresses[shard][server]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get server settings retrieved at connection setup.
|
|
||||||
pub fn server_info(&self) -> BytesMut {
|
pub fn server_info(&self) -> BytesMut {
|
||||||
self.server_info.read().clone()
|
self.server_info.read().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate how many used connections in the pool
|
|
||||||
/// for the given server.
|
|
||||||
fn busy_connection_count(&self, address: &Address) -> u32 {
|
fn busy_connection_count(&self, address: &Address) -> u32 {
|
||||||
let state = self.pool_state(address.shard, address.address_index);
|
let state = self.pool_state(address.shard, address.address_index);
|
||||||
let idle = state.idle_connections;
|
let idle = state.idle_connections;
|
||||||
@@ -931,10 +939,10 @@ impl ManageConnection for ServerPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the connection pool
|
/// Get the connection pool
|
||||||
pub fn get_pool(db: &str, user: &str) -> Option<ConnectionPool> {
|
pub fn get_pool(db: &str, user: &str, secret: Option<String>) -> Option<ConnectionPool> {
|
||||||
(*(*POOLS.load()))
|
let identifier = PoolIdentifier::new(db, user, secret);
|
||||||
.get(&PoolIdentifier::new(db, user))
|
|
||||||
.cloned()
|
(*(*POOLS.load())).get(&identifier).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a pointer to all configured pools.
|
/// Get a pointer to all configured pools.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use std::sync::atomic::Ordering;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::config::Address;
|
use crate::config::Address;
|
||||||
use crate::pool::get_all_pools;
|
use crate::pool::{get_all_pools, PoolIdentifier};
|
||||||
use crate::stats::{get_pool_stats, get_server_stats, ServerStats};
|
use crate::stats::{get_pool_stats, get_server_stats, ServerStats};
|
||||||
|
|
||||||
struct MetricHelpType {
|
struct MetricHelpType {
|
||||||
@@ -233,10 +233,10 @@ impl<Value: fmt::Display> PrometheusMetric<Value> {
|
|||||||
Self::from_name(&format!("stats_{}", name), value, labels)
|
Self::from_name(&format!("stats_{}", name), value, labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_pool(pool: &(String, String), name: &str, value: u64) -> Option<PrometheusMetric<u64>> {
|
fn from_pool(pool: &PoolIdentifier, name: &str, value: u64) -> Option<PrometheusMetric<u64>> {
|
||||||
let mut labels = HashMap::new();
|
let mut labels = HashMap::new();
|
||||||
labels.insert("pool", pool.0.clone());
|
labels.insert("pool", pool.db.clone());
|
||||||
labels.insert("user", pool.1.clone());
|
labels.insert("user", pool.user.clone());
|
||||||
|
|
||||||
Self::from_name(&format!("pools_{}", name), value, labels)
|
Self::from_name(&format!("pools_{}", name), value, labels)
|
||||||
}
|
}
|
||||||
@@ -294,7 +294,7 @@ fn push_pool_stats(lines: &mut Vec<String>) {
|
|||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
"Metric {} not implemented for ({},{})",
|
"Metric {} not implemented for ({},{})",
|
||||||
name, pool.0, pool.1
|
name, pool.db, pool.user
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/// Route queries automatically based on explicitly requested
|
/// Route queries automatically based on explicitely requested
|
||||||
/// or implied query characteristics.
|
/// or implied query characteristics.
|
||||||
use bytes::{Buf, BytesMut};
|
use bytes::{Buf, BytesMut};
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
|
|||||||
176
src/server.rs
176
src/server.rs
@@ -17,7 +17,7 @@ use tokio::net::{
|
|||||||
|
|
||||||
use crate::config::{Address, User};
|
use crate::config::{Address, User};
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::errors::{Error, ServerIdentifier};
|
use crate::errors::Error;
|
||||||
use crate::messages::*;
|
use crate::messages::*;
|
||||||
use crate::mirrors::MirroringManager;
|
use crate::mirrors::MirroringManager;
|
||||||
use crate::pool::ClientServerMap;
|
use crate::pool::ClientServerMap;
|
||||||
@@ -38,7 +38,6 @@ pub struct Server {
|
|||||||
|
|
||||||
/// Our server response buffer. We buffer data before we give it to the client.
|
/// Our server response buffer. We buffer data before we give it to the client.
|
||||||
buffer: BytesMut,
|
buffer: BytesMut,
|
||||||
is_async: bool,
|
|
||||||
|
|
||||||
/// Server information the server sent us over on startup.
|
/// Server information the server sent us over on startup.
|
||||||
server_info: BytesMut,
|
server_info: BytesMut,
|
||||||
@@ -104,52 +103,28 @@ impl Server {
|
|||||||
trace!("Sending StartupMessage");
|
trace!("Sending StartupMessage");
|
||||||
|
|
||||||
// StartupMessage
|
// StartupMessage
|
||||||
let username = match user.server_username {
|
startup(&mut stream, &user.username, database).await?;
|
||||||
Some(ref server_username) => server_username,
|
|
||||||
None => &user.username,
|
|
||||||
};
|
|
||||||
|
|
||||||
let password = match user.server_password {
|
|
||||||
Some(ref server_password) => Some(server_password),
|
|
||||||
None => match user.password {
|
|
||||||
Some(ref password) => Some(password),
|
|
||||||
None => None,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
startup(&mut stream, username, database).await?;
|
|
||||||
|
|
||||||
let mut server_info = BytesMut::new();
|
let mut server_info = BytesMut::new();
|
||||||
let mut process_id: i32 = 0;
|
let mut process_id: i32 = 0;
|
||||||
let mut secret_key: i32 = 0;
|
let mut secret_key: i32 = 0;
|
||||||
let server_identifier = ServerIdentifier::new(username, &database);
|
|
||||||
|
|
||||||
// We'll be handling multiple packets, but they will all be structured the same.
|
// We'll be handling multiple packets, but they will all be structured the same.
|
||||||
// We'll loop here until this exchange is complete.
|
// We'll loop here until this exchange is complete.
|
||||||
let mut scram: Option<ScramSha256> = match password {
|
let mut scram: Option<ScramSha256> = None;
|
||||||
Some(password) => Some(ScramSha256::new(password)),
|
if let Some(password) = &user.password.clone() {
|
||||||
None => None,
|
scram = Some(ScramSha256::new(password));
|
||||||
};
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let code = match stream.read_u8().await {
|
let code = match stream.read_u8().await {
|
||||||
Ok(code) => code as char,
|
Ok(code) => code as char,
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading message code on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"message code".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let len = match stream.read_i32().await {
|
let len = match stream.read_i32().await {
|
||||||
Ok(len) => len,
|
Ok(len) => len,
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading message len on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"message len".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("Message: {}", code);
|
trace!("Message: {}", code);
|
||||||
@@ -160,12 +135,7 @@ impl Server {
|
|||||||
// Determine which kind of authentication is required, if any.
|
// Determine which kind of authentication is required, if any.
|
||||||
let auth_code = match stream.read_i32().await {
|
let auth_code = match stream.read_i32().await {
|
||||||
Ok(auth_code) => auth_code,
|
Ok(auth_code) => auth_code,
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading auth code on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"auth code".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("Auth: {}", auth_code);
|
trace!("Auth: {}", auth_code);
|
||||||
@@ -178,18 +148,14 @@ impl Server {
|
|||||||
|
|
||||||
match stream.read_exact(&mut salt).await {
|
match stream.read_exact(&mut salt).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading salt on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"salt".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match password {
|
match &user.password {
|
||||||
// Using plaintext password
|
// Using plaintext password
|
||||||
Some(password) => {
|
Some(password) => {
|
||||||
md5_password(&mut stream, username, password, &salt[..]).await?
|
md5_password(&mut stream, &user.username, password, &salt[..])
|
||||||
|
.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using auth passthrough, in this case we should already have a
|
// Using auth passthrough, in this case we should already have a
|
||||||
@@ -205,12 +171,8 @@ impl Server {
|
|||||||
&salt[..],
|
&salt[..],
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
None => return Err(
|
None =>
|
||||||
Error::ServerAuthError(
|
return Err(Error::AuthError(format!("Auth passthrough (auth_query) failed and no user password is set in cleartext for {{ username: {:?}, database: {:?} }}", user.username, database)))
|
||||||
"Auth passthrough (auth_query) failed and no user password is set in cleartext".into(),
|
|
||||||
server_identifier
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,28 +182,16 @@ impl Server {
|
|||||||
|
|
||||||
SASL => {
|
SASL => {
|
||||||
if scram.is_none() {
|
if scram.is_none() {
|
||||||
return Err(Error::ServerAuthError(
|
return Err(Error::AuthError(format!("SASL auth required and not password specified, auth passthrough (auth_query) method is currently unsupported for SASL auth {{ username: {:?}, database: {:?} }}", user.username, database)));
|
||||||
"SASL auth required and no password specified. \
|
|
||||||
Auth passthrough (auth_query) method is currently \
|
|
||||||
unsupported for SASL auth"
|
|
||||||
.into(),
|
|
||||||
server_identifier,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Starting SASL authentication");
|
debug!("Starting SASL authentication");
|
||||||
|
|
||||||
let sasl_len = (len - 8) as usize;
|
let sasl_len = (len - 8) as usize;
|
||||||
let mut sasl_auth = vec![0u8; sasl_len];
|
let mut sasl_auth = vec![0u8; sasl_len];
|
||||||
|
|
||||||
match stream.read_exact(&mut sasl_auth).await {
|
match stream.read_exact(&mut sasl_auth).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading sasl message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"sasl message".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let sasl_type = String::from_utf8_lossy(&sasl_auth[..sasl_len - 2]);
|
let sasl_type = String::from_utf8_lossy(&sasl_auth[..sasl_len - 2]);
|
||||||
@@ -283,12 +233,7 @@ impl Server {
|
|||||||
|
|
||||||
match stream.read_exact(&mut sasl_data).await {
|
match stream.read_exact(&mut sasl_data).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading sasl cont message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"sasl cont message".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let msg = BytesMut::from(&sasl_data[..]);
|
let msg = BytesMut::from(&sasl_data[..]);
|
||||||
@@ -309,12 +254,7 @@ impl Server {
|
|||||||
let mut sasl_final = vec![0u8; len as usize - 8];
|
let mut sasl_final = vec![0u8; len as usize - 8];
|
||||||
match stream.read_exact(&mut sasl_final).await {
|
match stream.read_exact(&mut sasl_final).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading sasl final message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"sasl final message".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match scram
|
match scram
|
||||||
@@ -344,12 +284,7 @@ impl Server {
|
|||||||
'E' => {
|
'E' => {
|
||||||
let error_code = match stream.read_u8().await {
|
let error_code = match stream.read_u8().await {
|
||||||
Ok(error_code) => error_code,
|
Ok(error_code) => error_code,
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading error code message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"error code message".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("Error: {}", error_code);
|
trace!("Error: {}", error_code);
|
||||||
@@ -365,12 +300,7 @@ impl Server {
|
|||||||
|
|
||||||
match stream.read_exact(&mut error).await {
|
match stream.read_exact(&mut error).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading error message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"error message".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: the error message contains multiple fields; we can decode them and
|
// TODO: the error message contains multiple fields; we can decode them and
|
||||||
@@ -389,12 +319,7 @@ impl Server {
|
|||||||
|
|
||||||
match stream.read_exact(&mut param).await {
|
match stream.read_exact(&mut param).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading parameter status message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"parameter status message".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save the parameter so we can pass it to the client later.
|
// Save the parameter so we can pass it to the client later.
|
||||||
@@ -411,22 +336,12 @@ impl Server {
|
|||||||
// See: <https://www.postgresql.org/docs/12/protocol-message-formats.html>.
|
// See: <https://www.postgresql.org/docs/12/protocol-message-formats.html>.
|
||||||
process_id = match stream.read_i32().await {
|
process_id = match stream.read_i32().await {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading process id message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"process id message".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
secret_key = match stream.read_i32().await {
|
secret_key = match stream.read_i32().await {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading secret key message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"secret key message".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,12 +351,7 @@ impl Server {
|
|||||||
|
|
||||||
match stream.read_exact(&mut idle).await {
|
match stream.read_exact(&mut idle).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => {
|
Err(_) => return Err(Error::SocketError(format!("Error reading transaction status message on server startup {{ username: {:?}, database: {:?} }}", user.username, database))),
|
||||||
return Err(Error::ServerStartupError(
|
|
||||||
"transaction status message".into(),
|
|
||||||
server_identifier,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let (read, write) = stream.into_split();
|
let (read, write) = stream.into_split();
|
||||||
@@ -451,7 +361,6 @@ impl Server {
|
|||||||
read: BufReader::new(read),
|
read: BufReader::new(read),
|
||||||
write,
|
write,
|
||||||
buffer: BytesMut::with_capacity(8196),
|
buffer: BytesMut::with_capacity(8196),
|
||||||
is_async: false,
|
|
||||||
server_info,
|
server_info,
|
||||||
process_id,
|
process_id,
|
||||||
secret_key,
|
secret_key,
|
||||||
@@ -504,7 +413,7 @@ impl Server {
|
|||||||
Ok(stream) => stream,
|
Ok(stream) => stream,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Could not connect to server: {}", err);
|
error!("Could not connect to server: {}", err);
|
||||||
return Err(Error::SocketError("Error reading cancel message".into()));
|
return Err(Error::SocketError(format!("Error reading cancel message")));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
configure_socket(&stream);
|
configure_socket(&stream);
|
||||||
@@ -539,16 +448,6 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch to async mode, flushing messages as soon
|
|
||||||
/// as we receive them without buffering or waiting for "ReadyForQuery".
|
|
||||||
pub fn switch_async(&mut self, on: bool) {
|
|
||||||
if on {
|
|
||||||
self.is_async = true;
|
|
||||||
} else {
|
|
||||||
self.is_async = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Receive data from the server in response to a client request.
|
/// Receive data from the server in response to a client request.
|
||||||
/// This method must be called multiple times while `self.is_data_available()` is true
|
/// This method must be called multiple times while `self.is_data_available()` is true
|
||||||
/// in order to receive all data the server has to offer.
|
/// in order to receive all data the server has to offer.
|
||||||
@@ -644,10 +543,7 @@ impl Server {
|
|||||||
// DataRow
|
// DataRow
|
||||||
'D' => {
|
'D' => {
|
||||||
// More data is available after this message, this is not the end of the reply.
|
// More data is available after this message, this is not the end of the reply.
|
||||||
// If we're async, flush to client now.
|
self.data_available = true;
|
||||||
if !self.is_async {
|
|
||||||
self.data_available = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't flush yet, the more we buffer, the faster this goes...up to a limit.
|
// Don't flush yet, the more we buffer, the faster this goes...up to a limit.
|
||||||
if self.buffer.len() >= 8196 {
|
if self.buffer.len() >= 8196 {
|
||||||
@@ -660,10 +556,7 @@ impl Server {
|
|||||||
|
|
||||||
// CopyOutResponse: copy is starting from the server to the client.
|
// CopyOutResponse: copy is starting from the server to the client.
|
||||||
'H' => {
|
'H' => {
|
||||||
// If we're in async mode, flush now.
|
self.data_available = true;
|
||||||
if !self.is_async {
|
|
||||||
self.data_available = true;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,10 +576,6 @@ impl Server {
|
|||||||
// Keep buffering until ReadyForQuery shows up.
|
// Keep buffering until ReadyForQuery shows up.
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.is_async {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytes = self.buffer.clone();
|
let bytes = self.buffer.clone();
|
||||||
@@ -866,7 +755,9 @@ impl Server {
|
|||||||
Arc::new(RwLock::new(None)),
|
Arc::new(RwLock::new(None)),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
debug!("Connected!, sending query.");
|
|
||||||
|
debug!("Connected!, sending query: {}", query);
|
||||||
|
|
||||||
server.send(&simple_query(query)).await?;
|
server.send(&simple_query(query)).await?;
|
||||||
let mut message = server.recv().await?;
|
let mut message = server.recv().await?;
|
||||||
|
|
||||||
@@ -875,6 +766,8 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn parse_query_message(message: &mut BytesMut) -> Result<Vec<String>, Error> {
|
async fn parse_query_message(message: &mut BytesMut) -> Result<Vec<String>, Error> {
|
||||||
|
debug!("Parsing query message");
|
||||||
|
|
||||||
let mut pair = Vec::<String>::new();
|
let mut pair = Vec::<String>::new();
|
||||||
match message::backend::Message::parse(message) {
|
match message::backend::Message::parse(message) {
|
||||||
Ok(Some(message::backend::Message::RowDescription(_description))) => {}
|
Ok(Some(message::backend::Message::RowDescription(_description))) => {}
|
||||||
@@ -944,6 +837,9 @@ async fn parse_query_message(message: &mut BytesMut) -> Result<Vec<String>, Erro
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("Got auth hash successfully");
|
||||||
|
|
||||||
Ok(pair)
|
Ok(pair)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
src/stats.rs
10
src/stats.rs
@@ -22,7 +22,7 @@ pub use server::{ServerState, ServerStats};
|
|||||||
/// Convenience types for various stats
|
/// Convenience types for various stats
|
||||||
type ClientStatesLookup = HashMap<i32, Arc<ClientStats>>;
|
type ClientStatesLookup = HashMap<i32, Arc<ClientStats>>;
|
||||||
type ServerStatesLookup = HashMap<i32, Arc<ServerStats>>;
|
type ServerStatesLookup = HashMap<i32, Arc<ServerStats>>;
|
||||||
type PoolStatsLookup = HashMap<(String, String), Arc<PoolStats>>;
|
type PoolStatsLookup = HashMap<PoolIdentifier, Arc<PoolStats>>;
|
||||||
|
|
||||||
/// Stats for individual client connections
|
/// Stats for individual client connections
|
||||||
/// Used in SHOW CLIENTS.
|
/// Used in SHOW CLIENTS.
|
||||||
@@ -66,7 +66,7 @@ impl Reporter {
|
|||||||
CLIENT_STATS.write().insert(client_id, stats);
|
CLIENT_STATS.write().insert(client_id, stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reports a client is disconnecting from the pooler.
|
/// Reports a client is disconecting from the pooler.
|
||||||
fn client_disconnecting(&self, client_id: i32) {
|
fn client_disconnecting(&self, client_id: i32) {
|
||||||
CLIENT_STATS.write().remove(&client_id);
|
CLIENT_STATS.write().remove(&client_id);
|
||||||
}
|
}
|
||||||
@@ -76,16 +76,14 @@ impl Reporter {
|
|||||||
fn server_register(&self, server_id: i32, stats: Arc<ServerStats>) {
|
fn server_register(&self, server_id: i32, stats: Arc<ServerStats>) {
|
||||||
SERVER_STATS.write().insert(server_id, stats);
|
SERVER_STATS.write().insert(server_id, stats);
|
||||||
}
|
}
|
||||||
/// Reports a server connection is disconnecting from the pooler.
|
/// Reports a server connection is disconecting from the pooler.
|
||||||
fn server_disconnecting(&self, server_id: i32) {
|
fn server_disconnecting(&self, server_id: i32) {
|
||||||
SERVER_STATS.write().remove(&server_id);
|
SERVER_STATS.write().remove(&server_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a pool with the stats system.
|
/// Register a pool with the stats system.
|
||||||
fn pool_register(&self, identifier: PoolIdentifier, stats: Arc<PoolStats>) {
|
fn pool_register(&self, identifier: PoolIdentifier, stats: Arc<PoolStats>) {
|
||||||
POOL_STATS
|
POOL_STATS.write().insert(identifier, stats);
|
||||||
.write()
|
|
||||||
.insert((identifier.db, identifier.user), stats);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ impl ClientStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reports a client is disconnecting from the pooler and
|
/// Reports a client is disconecting from the pooler and
|
||||||
/// update metrics on the corresponding pool.
|
/// update metrics on the corresponding pool.
|
||||||
pub fn disconnect(&self) {
|
pub fn disconnect(&self) {
|
||||||
self.reporter.client_disconnecting(self.client_id);
|
self.reporter.client_disconnecting(self.client_id);
|
||||||
@@ -140,7 +140,7 @@ impl ClientStats {
|
|||||||
self.error_count.fetch_add(1, Ordering::Relaxed);
|
self.error_count.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reporters the time spent by a client waiting to get a healthy connection from the pool
|
/// Reportes the time spent by a client waiting to get a healthy connection from the pool
|
||||||
pub fn checkout_time(&self, microseconds: u64) {
|
pub fn checkout_time(&self, microseconds: u64) {
|
||||||
self.total_wait_time
|
self.total_wait_time
|
||||||
.fetch_add(microseconds, Ordering::Relaxed);
|
.fetch_add(microseconds, Ordering::Relaxed);
|
||||||
|
|||||||
@@ -102,6 +102,13 @@ impl PoolStats {
|
|||||||
self.identifier.user.clone()
|
self.identifier.user.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn redacted_secret(&self) -> String {
|
||||||
|
match self.identifier.secret {
|
||||||
|
Some(ref s) => format!("****{}", &s[s.len() - 4..]),
|
||||||
|
None => "<no secret>".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pool_mode(&self) -> PoolMode {
|
pub fn pool_mode(&self) -> PoolMode {
|
||||||
self.config.pool_mode
|
self.config.pool_mode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,9 +100,10 @@ impl ServerStats {
|
|||||||
.server_idle(self.state.load(Ordering::Relaxed));
|
.server_idle(self.state.load(Ordering::Relaxed));
|
||||||
|
|
||||||
self.state.store(ServerState::Idle, Ordering::Relaxed);
|
self.state.store(ServerState::Idle, Ordering::Relaxed);
|
||||||
|
self.set_undefined_application();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reports a server connection is disconnecting from the pooler.
|
/// Reports a server connection is disconecting from the pooler.
|
||||||
/// Also updates metrics on the pool regarding server usage.
|
/// Also updates metrics on the pool regarding server usage.
|
||||||
pub fn disconnect(&self) {
|
pub fn disconnect(&self) {
|
||||||
self.reporter.server_disconnecting(self.server_id);
|
self.reporter.server_disconnecting(self.server_id);
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import psycopg2
|
|
||||||
import asyncio
|
|
||||||
import asyncpg
|
|
||||||
|
|
||||||
PGCAT_HOST = "127.0.0.1"
|
|
||||||
PGCAT_PORT = "6432"
|
|
||||||
|
|
||||||
|
|
||||||
def regular_main():
|
|
||||||
# Connect to the PostgreSQL database
|
|
||||||
conn = psycopg2.connect(
|
|
||||||
host=PGCAT_HOST,
|
|
||||||
database="sharded_db",
|
|
||||||
user="sharding_user",
|
|
||||||
password="sharding_user",
|
|
||||||
port=PGCAT_PORT,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Open a cursor to perform database operations
|
|
||||||
cur = conn.cursor()
|
|
||||||
|
|
||||||
# Execute a SQL query
|
|
||||||
cur.execute("SELECT 1")
|
|
||||||
|
|
||||||
# Fetch the results
|
|
||||||
rows = cur.fetchall()
|
|
||||||
|
|
||||||
# Print the results
|
|
||||||
for row in rows:
|
|
||||||
print(row[0])
|
|
||||||
|
|
||||||
# Close the cursor and the database connection
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
# Connect to the PostgreSQL database
|
|
||||||
conn = await asyncpg.connect(
|
|
||||||
host=PGCAT_HOST,
|
|
||||||
database="sharded_db",
|
|
||||||
user="sharding_user",
|
|
||||||
password="sharding_user",
|
|
||||||
port=PGCAT_PORT,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Execute a SQL query
|
|
||||||
for _ in range(25):
|
|
||||||
rows = await conn.fetch("SELECT 1")
|
|
||||||
|
|
||||||
# Print the results
|
|
||||||
for row in rows:
|
|
||||||
print(row[0])
|
|
||||||
|
|
||||||
# Close the database connection
|
|
||||||
await conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
regular_main()
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,11 +1,2 @@
|
|||||||
asyncio==3.4.3
|
|
||||||
asyncpg==0.27.0
|
|
||||||
black==23.3.0
|
|
||||||
click==8.1.3
|
|
||||||
mypy-extensions==1.0.0
|
|
||||||
packaging==23.1
|
|
||||||
pathspec==0.11.1
|
|
||||||
platformdirs==3.2.0
|
|
||||||
psutil==5.9.1
|
|
||||||
psycopg2==2.9.3
|
psycopg2==2.9.3
|
||||||
tomli==2.0.1
|
psutil==5.9.1
|
||||||
@@ -37,9 +37,9 @@ describe "Admin" do
|
|||||||
describe "SHOW POOLS" do
|
describe "SHOW POOLS" do
|
||||||
context "bad credentials" do
|
context "bad credentials" do
|
||||||
it "does not change any stats" do
|
it "does not change any stats" do
|
||||||
bad_password_url = URI(pgcat_conn_str)
|
bad_passsword_url = URI(pgcat_conn_str)
|
||||||
bad_password_url.password = "wrong"
|
bad_passsword_url.password = "wrong"
|
||||||
expect { PG::connect("#{bad_password_url.to_s}?application_name=bad_password") }.to raise_error(PG::ConnectionBad)
|
expect { PG::connect("#{bad_passsword_url.to_s}?application_name=bad_password") }.to raise_error(PG::ConnectionBad)
|
||||||
|
|
||||||
sleep(1)
|
sleep(1)
|
||||||
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
admin_conn = PG::connect(processes.pgcat.admin_connection_string)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe "Auth Query" do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'and with cleartext passwords not set' do
|
context 'and with cleartext passwords not set' do
|
||||||
let(:config_user) { { 'username' => 'sharding_user', 'password' => 'sharding_user' } }
|
let(:config_user) { { 'username' => 'sharding_user' } }
|
||||||
|
|
||||||
it 'it uses obtained passwords' do
|
it 'it uses obtained passwords' do
|
||||||
connection_string = processes.pgcat.connection_string("sharded_db", pg_user['username'], pg_user['password'])
|
connection_string = processes.pgcat.connection_string("sharded_db", pg_user['username'], pg_user['password'])
|
||||||
@@ -76,7 +76,7 @@ describe "Auth Query" do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'allows passwords to be changed without closing existing connections' do
|
it 'allows passwords to be changed without closing existing connections' do
|
||||||
pgconn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username']))
|
pgconn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username'], pg_user['password']))
|
||||||
expect(pgconn.exec("SELECT 1 + 2")).not_to be_nil
|
expect(pgconn.exec("SELECT 1 + 2")).not_to be_nil
|
||||||
Helpers::AuthQuery.exec_in_instances(query: "ALTER USER #{pg_user['username']} WITH ENCRYPTED PASSWORD 'secret2';")
|
Helpers::AuthQuery.exec_in_instances(query: "ALTER USER #{pg_user['username']} WITH ENCRYPTED PASSWORD 'secret2';")
|
||||||
expect(pgconn.exec("SELECT 1 + 4")).not_to be_nil
|
expect(pgconn.exec("SELECT 1 + 4")).not_to be_nil
|
||||||
@@ -84,7 +84,7 @@ describe "Auth Query" do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'allows passwords to be changed and that new password is needed when reconnecting' do
|
it 'allows passwords to be changed and that new password is needed when reconnecting' do
|
||||||
pgconn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username']))
|
pgconn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username'], pg_user['password']))
|
||||||
expect(pgconn.exec("SELECT 1 + 2")).not_to be_nil
|
expect(pgconn.exec("SELECT 1 + 2")).not_to be_nil
|
||||||
Helpers::AuthQuery.exec_in_instances(query: "ALTER USER #{pg_user['username']} WITH ENCRYPTED PASSWORD 'secret2';")
|
Helpers::AuthQuery.exec_in_instances(query: "ALTER USER #{pg_user['username']} WITH ENCRYPTED PASSWORD 'secret2';")
|
||||||
newconn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username'], 'secret2'))
|
newconn = PG.connect(processes.pgcat.connection_string("sharded_db", pg_user['username'], 'secret2'))
|
||||||
|
|||||||
39
tests/ruby/auth_spec.rb
Normal file
39
tests/ruby/auth_spec.rb
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
require_relative 'spec_helper'
|
||||||
|
|
||||||
|
|
||||||
|
describe "Authentication" do
|
||||||
|
describe "multiple secrets configured" do
|
||||||
|
let(:secrets) { ["one_secret", "two_secret"] }
|
||||||
|
let(:processes) { Helpers::Pgcat.three_shard_setup("sharded_db", 5, pool_mode="transaction", lb_mode="random", log_level="info", secrets=["one_secret", "two_secret"]) }
|
||||||
|
|
||||||
|
after do
|
||||||
|
processes.all_databases.map(&:reset)
|
||||||
|
processes.pgcat.shutdown
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can connect using all secrets and postgres password" do
|
||||||
|
secrets.push("sharding_user").each do |secret|
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user", password=secret))
|
||||||
|
conn.exec("SELECT current_user")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "no secrets configured" do
|
||||||
|
let(:secrets) { [] }
|
||||||
|
let(:processes) { Helpers::Pgcat.three_shard_setup("sharded_db", 5, pool_mode="transaction", lb_mode="random", log_level="info") }
|
||||||
|
|
||||||
|
after do
|
||||||
|
processes.all_databases.map(&:reset)
|
||||||
|
processes.pgcat.shutdown
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can connect using only the password" do
|
||||||
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
conn.exec("SELECT current_user")
|
||||||
|
|
||||||
|
expect { PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user", password="secret_one")) }.to raise_error PG::ConnectionBad
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
require 'socket'
|
|
||||||
require 'digest/md5'
|
|
||||||
|
|
||||||
BACKEND_MESSAGE_CODES = {
|
|
||||||
'Z' => "ReadyForQuery",
|
|
||||||
'C' => "CommandComplete",
|
|
||||||
'T' => "RowDescription",
|
|
||||||
'D' => "DataRow",
|
|
||||||
'1' => "ParseComplete",
|
|
||||||
'2' => "BindComplete",
|
|
||||||
'E' => "ErrorResponse",
|
|
||||||
's' => "PortalSuspended",
|
|
||||||
}
|
|
||||||
|
|
||||||
class PostgresSocket
|
|
||||||
def initialize(host, port)
|
|
||||||
@port = port
|
|
||||||
@host = host
|
|
||||||
@socket = TCPSocket.new @host, @port
|
|
||||||
@parameters = {}
|
|
||||||
@verbose = true
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_md5_password_message(username, password, salt)
|
|
||||||
m = Digest::MD5.hexdigest(password + username)
|
|
||||||
m = Digest::MD5.hexdigest(m + salt.map(&:chr).join(""))
|
|
||||||
m = 'md5' + m
|
|
||||||
bytes = (m.split("").map(&:ord) + [0]).flatten
|
|
||||||
message_size = bytes.count + 4
|
|
||||||
|
|
||||||
message = []
|
|
||||||
|
|
||||||
message << 'p'.ord
|
|
||||||
message << [message_size].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << bytes
|
|
||||||
message.flatten!
|
|
||||||
|
|
||||||
|
|
||||||
@socket.write(message.pack('C*'))
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_startup_message(username, database, password)
|
|
||||||
message = []
|
|
||||||
|
|
||||||
message << [196608].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << "user".split('').map(&:ord) # 4, 8
|
|
||||||
message << 0 # 1, 9
|
|
||||||
message << username.split('').map(&:ord) # 2, 11
|
|
||||||
message << 0 # 1, 12
|
|
||||||
message << "database".split('').map(&:ord) # 8, 20
|
|
||||||
message << 0 # 1, 21
|
|
||||||
message << database.split('').map(&:ord) # 2, 23
|
|
||||||
message << 0 # 1, 24
|
|
||||||
message << 0 # 1, 25
|
|
||||||
message.flatten!
|
|
||||||
|
|
||||||
total_message_size = message.size + 4
|
|
||||||
|
|
||||||
message_len = [total_message_size].pack('l>').unpack('CCCC')
|
|
||||||
|
|
||||||
@socket.write([message_len + message].flatten.pack('C*'))
|
|
||||||
|
|
||||||
sleep 0.1
|
|
||||||
|
|
||||||
read_startup_response(username, password)
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_startup_response(username, password)
|
|
||||||
message_code, message_len = @socket.recv(5).unpack("al>")
|
|
||||||
while message_code == 'R'
|
|
||||||
auth_code = @socket.recv(4).unpack('l>').pop
|
|
||||||
case auth_code
|
|
||||||
when 5 # md5
|
|
||||||
salt = @socket.recv(4).unpack('CCCC')
|
|
||||||
send_md5_password_message(username, password, salt)
|
|
||||||
message_code, message_len = @socket.recv(5).unpack("al>")
|
|
||||||
when 0 # trust
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
loop do
|
|
||||||
message_code, message_len = @socket.recv(5).unpack("al>")
|
|
||||||
if message_code == 'Z'
|
|
||||||
@socket.recv(1).unpack("a") # most likely I
|
|
||||||
break # We are good to go
|
|
||||||
end
|
|
||||||
if message_code == 'S'
|
|
||||||
actual_message = @socket.recv(message_len - 4).unpack("C*")
|
|
||||||
k,v = actual_message.pack('U*').split(/\x00/)
|
|
||||||
@parameters[k] = v
|
|
||||||
end
|
|
||||||
if message_code == 'K'
|
|
||||||
process_id, secret_key = @socket.recv(message_len - 4).unpack("l>l>")
|
|
||||||
@parameters["process_id"] = process_id
|
|
||||||
@parameters["secret_key"] = secret_key
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return @parameters
|
|
||||||
end
|
|
||||||
|
|
||||||
def cancel_query
|
|
||||||
socket = TCPSocket.new @host, @port
|
|
||||||
process_key = @parameters["process_id"]
|
|
||||||
secret_key = @parameters["secret_key"]
|
|
||||||
message = []
|
|
||||||
message << [16].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << [80877102].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << [process_key.to_i].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << [secret_key.to_i].pack('l>').unpack('CCCC') # 4
|
|
||||||
message.flatten!
|
|
||||||
socket.write(message.flatten.pack('C*'))
|
|
||||||
socket.close
|
|
||||||
log "[F] Sent CancelRequest message"
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_query_message(query)
|
|
||||||
query_size = query.length
|
|
||||||
message_size = 1 + 4 + query_size
|
|
||||||
message = []
|
|
||||||
message << "Q".ord
|
|
||||||
message << [message_size].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << query.split('').map(&:ord) # 2, 11
|
|
||||||
message << 0 # 1, 12
|
|
||||||
message.flatten!
|
|
||||||
@socket.write(message.flatten.pack('C*'))
|
|
||||||
log "[F] Sent Q message (#{query})"
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_parse_message(query)
|
|
||||||
query_size = query.length
|
|
||||||
message_size = 2 + 2 + 4 + query_size
|
|
||||||
message = []
|
|
||||||
message << "P".ord
|
|
||||||
message << [message_size].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << 0 # unnamed statement
|
|
||||||
message << query.split('').map(&:ord) # 2, 11
|
|
||||||
message << 0 # 1, 12
|
|
||||||
message << [0, 0]
|
|
||||||
message.flatten!
|
|
||||||
@socket.write(message.flatten.pack('C*'))
|
|
||||||
log "[F] Sent P message (#{query})"
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_bind_message
|
|
||||||
message = []
|
|
||||||
message << "B".ord
|
|
||||||
message << [12].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << 0 # unnamed statement
|
|
||||||
message << 0 # unnamed statement
|
|
||||||
message << [0, 0] # 2
|
|
||||||
message << [0, 0] # 2
|
|
||||||
message << [0, 0] # 2
|
|
||||||
message.flatten!
|
|
||||||
@socket.write(message.flatten.pack('C*'))
|
|
||||||
log "[F] Sent B message"
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_describe_message(mode)
|
|
||||||
message = []
|
|
||||||
message << "D".ord
|
|
||||||
message << [6].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << mode.ord
|
|
||||||
message << 0 # unnamed statement
|
|
||||||
message.flatten!
|
|
||||||
@socket.write(message.flatten.pack('C*'))
|
|
||||||
log "[F] Sent D message"
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_execute_message(limit=0)
|
|
||||||
message = []
|
|
||||||
message << "E".ord
|
|
||||||
message << [9].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << 0 # unnamed statement
|
|
||||||
message << [limit].pack('l>').unpack('CCCC') # 4
|
|
||||||
message.flatten!
|
|
||||||
@socket.write(message.flatten.pack('C*'))
|
|
||||||
log "[F] Sent E message"
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_sync_message
|
|
||||||
message = []
|
|
||||||
message << "S".ord
|
|
||||||
message << [4].pack('l>').unpack('CCCC') # 4
|
|
||||||
message.flatten!
|
|
||||||
@socket.write(message.flatten.pack('C*'))
|
|
||||||
log "[F] Sent S message"
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_copydone_message
|
|
||||||
message = []
|
|
||||||
message << "c".ord
|
|
||||||
message << [4].pack('l>').unpack('CCCC') # 4
|
|
||||||
message.flatten!
|
|
||||||
@socket.write(message.flatten.pack('C*'))
|
|
||||||
log "[F] Sent c message"
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_copyfail_message
|
|
||||||
message = []
|
|
||||||
message << "f".ord
|
|
||||||
message << [5].pack('l>').unpack('CCCC') # 4
|
|
||||||
message << 0
|
|
||||||
message.flatten!
|
|
||||||
@socket.write(message.flatten.pack('C*'))
|
|
||||||
log "[F] Sent f message"
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_flush_message
|
|
||||||
message = []
|
|
||||||
message << "H".ord
|
|
||||||
message << [4].pack('l>').unpack('CCCC') # 4
|
|
||||||
message.flatten!
|
|
||||||
@socket.write(message.flatten.pack('C*'))
|
|
||||||
log "[F] Sent H message"
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_from_server()
|
|
||||||
output_messages = []
|
|
||||||
retry_count = 0
|
|
||||||
message_code = nil
|
|
||||||
message_len = 0
|
|
||||||
loop do
|
|
||||||
begin
|
|
||||||
message_code, message_len = @socket.recv_nonblock(5).unpack("al>")
|
|
||||||
rescue IO::WaitReadable
|
|
||||||
return output_messages if retry_count > 50
|
|
||||||
|
|
||||||
retry_count += 1
|
|
||||||
sleep(0.01)
|
|
||||||
next
|
|
||||||
end
|
|
||||||
message = {
|
|
||||||
code: message_code,
|
|
||||||
len: message_len,
|
|
||||||
bytes: []
|
|
||||||
}
|
|
||||||
log "[B] #{BACKEND_MESSAGE_CODES[message_code] || ('UnknownMessage(' + message_code + ')')}"
|
|
||||||
|
|
||||||
actual_message_length = message_len - 4
|
|
||||||
if actual_message_length > 0
|
|
||||||
message[:bytes] = @socket.recv(message_len - 4).unpack("C*")
|
|
||||||
log "\t#{message[:bytes].join(",")}"
|
|
||||||
log "\t#{message[:bytes].map(&:chr).join(" ")}"
|
|
||||||
end
|
|
||||||
output_messages << message
|
|
||||||
return output_messages if message_code == 'Z'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def log(msg)
|
|
||||||
return unless @verbose
|
|
||||||
|
|
||||||
puts msg
|
|
||||||
end
|
|
||||||
|
|
||||||
def close
|
|
||||||
@socket.close
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -2,7 +2,6 @@ require 'json'
|
|||||||
require 'ostruct'
|
require 'ostruct'
|
||||||
require_relative 'pgcat_process'
|
require_relative 'pgcat_process'
|
||||||
require_relative 'pg_instance'
|
require_relative 'pg_instance'
|
||||||
require_relative 'pg_socket'
|
|
||||||
|
|
||||||
class ::Hash
|
class ::Hash
|
||||||
def deep_merge(second)
|
def deep_merge(second)
|
||||||
@@ -13,14 +12,18 @@ end
|
|||||||
|
|
||||||
module Helpers
|
module Helpers
|
||||||
module Pgcat
|
module Pgcat
|
||||||
def self.three_shard_setup(pool_name, pool_size, pool_mode="transaction", lb_mode="random", log_level="info")
|
def self.three_shard_setup(pool_name, pool_size, pool_mode="transaction", lb_mode="random", log_level="info", secrets=nil)
|
||||||
user = {
|
user = {
|
||||||
"password" => "sharding_user",
|
"password" => "sharding_user",
|
||||||
"pool_size" => pool_size,
|
"pool_size" => pool_size,
|
||||||
"statement_timeout" => 0,
|
"statement_timeout" => 0,
|
||||||
"username" => "sharding_user"
|
"username" => "sharding_user",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !secrets.nil?
|
||||||
|
user["secrets"] = secrets
|
||||||
|
end
|
||||||
|
|
||||||
pgcat = PgcatProcess.new(log_level)
|
pgcat = PgcatProcess.new(log_level)
|
||||||
primary0 = PgInstance.new(5432, user["username"], user["password"], "shard0")
|
primary0 = PgInstance.new(5432, user["username"], user["password"], "shard0")
|
||||||
primary1 = PgInstance.new(7432, user["username"], user["password"], "shard1")
|
primary1 = PgInstance.new(7432, user["username"], user["password"], "shard1")
|
||||||
@@ -28,7 +31,7 @@ module Helpers
|
|||||||
|
|
||||||
pgcat_cfg = pgcat.current_config
|
pgcat_cfg = pgcat.current_config
|
||||||
pgcat_cfg["pools"] = {
|
pgcat_cfg["pools"] = {
|
||||||
"#{pool_name}" => {
|
"#{pool_name}" => {
|
||||||
"default_role" => "any",
|
"default_role" => "any",
|
||||||
"pool_mode" => pool_mode,
|
"pool_mode" => pool_mode,
|
||||||
"load_balancing_mode" => lb_mode,
|
"load_balancing_mode" => lb_mode,
|
||||||
@@ -42,8 +45,14 @@ module Helpers
|
|||||||
"2" => { "database" => "shard2", "servers" => [["localhost", primary2.port.to_s, "primary"]] },
|
"2" => { "database" => "shard2", "servers" => [["localhost", primary2.port.to_s, "primary"]] },
|
||||||
},
|
},
|
||||||
"users" => { "0" => user }
|
"users" => { "0" => user }
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !secrets.nil?
|
||||||
|
pgcat_cfg["general"]["tls_certificate"] = "../../.circleci/server.cert"
|
||||||
|
pgcat_cfg["general"]["tls_private_key"] = "../../.circleci/server.key"
|
||||||
|
end
|
||||||
|
|
||||||
pgcat.update_config(pgcat_cfg)
|
pgcat.update_config(pgcat_cfg)
|
||||||
|
|
||||||
pgcat.start
|
pgcat.start
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ class PgcatProcess
|
|||||||
10.times do
|
10.times do
|
||||||
Process.kill 0, @pid
|
Process.kill 0, @pid
|
||||||
PG::connect(connection_string || example_connection_string).close
|
PG::connect(connection_string || example_connection_string).close
|
||||||
|
|
||||||
return self
|
return self
|
||||||
rescue Errno::ESRCH
|
rescue Errno::ESRCH
|
||||||
raise StandardError, "Process #{@pid} died. #{logs}"
|
raise StandardError, "Process #{@pid} died. #{logs}"
|
||||||
@@ -112,10 +111,13 @@ class PgcatProcess
|
|||||||
"postgresql://#{username}:#{password}@0.0.0.0:#{@port}/pgcat"
|
"postgresql://#{username}:#{password}@0.0.0.0:#{@port}/pgcat"
|
||||||
end
|
end
|
||||||
|
|
||||||
def connection_string(pool_name, username, password = nil)
|
def connection_string(pool_name, username, password=nil)
|
||||||
cfg = current_config
|
cfg = current_config
|
||||||
user_idx, user_obj = cfg["pools"][pool_name]["users"].detect { |k, user| user["username"] == username }
|
user_idx, user_obj = cfg["pools"][pool_name]["users"].detect { |k, user| user["username"] == username }
|
||||||
"postgresql://#{username}:#{password || user_obj["password"]}@0.0.0.0:#{@port}/#{pool_name}"
|
|
||||||
|
password = if password.nil? then user_obj["password"] else password end
|
||||||
|
|
||||||
|
"postgresql://#{username}:#{password}@0.0.0.0:#{@port}/#{pool_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def example_connection_string
|
def example_connection_string
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe "Least Outstanding Queries Load Balancing" do
|
|||||||
processes.pgcat.shutdown
|
processes.pgcat.shutdown
|
||||||
end
|
end
|
||||||
|
|
||||||
context "under homogeneous load" do
|
context "under homogenous load" do
|
||||||
it "balances query volume between all instances" do
|
it "balances query volume between all instances" do
|
||||||
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
|
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
require_relative 'spec_helper'
|
|
||||||
|
|
||||||
|
|
||||||
describe "Portocol handling" do
|
|
||||||
let(:processes) { Helpers::Pgcat.single_instance_setup("sharded_db", 1, "session") }
|
|
||||||
let(:sequence) { [] }
|
|
||||||
let(:pgcat_socket) { PostgresSocket.new('localhost', processes.pgcat.port) }
|
|
||||||
let(:pgdb_socket) { PostgresSocket.new('localhost', processes.all_databases.first.port) }
|
|
||||||
|
|
||||||
after do
|
|
||||||
pgdb_socket.close
|
|
||||||
pgcat_socket.close
|
|
||||||
processes.all_databases.map(&:reset)
|
|
||||||
processes.pgcat.shutdown
|
|
||||||
end
|
|
||||||
|
|
||||||
def run_comparison(sequence, socket_a, socket_b)
|
|
||||||
sequence.each do |msg, *args|
|
|
||||||
socket_a.send(msg, *args)
|
|
||||||
socket_b.send(msg, *args)
|
|
||||||
|
|
||||||
compare_messages(
|
|
||||||
socket_a.read_from_server,
|
|
||||||
socket_b.read_from_server
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def compare_messages(msg_arr0, msg_arr1)
|
|
||||||
if msg_arr0.count != msg_arr1.count
|
|
||||||
error_output = []
|
|
||||||
|
|
||||||
error_output << "#{msg_arr0.count} : #{msg_arr1.count}"
|
|
||||||
error_output << "PgCat Messages"
|
|
||||||
error_output += msg_arr0.map { |message| "\t#{message[:code]} - #{message[:bytes].map(&:chr).join(" ")}" }
|
|
||||||
error_output << "PgServer Messages"
|
|
||||||
error_output += msg_arr1.map { |message| "\t#{message[:code]} - #{message[:bytes].map(&:chr).join(" ")}" }
|
|
||||||
error_desc = error_output.join("\n")
|
|
||||||
raise StandardError, "Message count mismatch #{error_desc}"
|
|
||||||
end
|
|
||||||
|
|
||||||
(0..msg_arr0.count - 1).all? do |i|
|
|
||||||
msg0 = msg_arr0[i]
|
|
||||||
msg1 = msg_arr1[i]
|
|
||||||
|
|
||||||
result = [
|
|
||||||
msg0[:code] == msg1[:code],
|
|
||||||
msg0[:len] == msg1[:len],
|
|
||||||
msg0[:bytes] == msg1[:bytes],
|
|
||||||
].all?
|
|
||||||
|
|
||||||
next result if result
|
|
||||||
|
|
||||||
if result == false
|
|
||||||
error_string = []
|
|
||||||
if msg0[:code] != msg1[:code]
|
|
||||||
error_string << "code #{msg0[:code]} != #{msg1[:code]}"
|
|
||||||
end
|
|
||||||
if msg0[:len] != msg1[:len]
|
|
||||||
error_string << "len #{msg0[:len]} != #{msg1[:len]}"
|
|
||||||
end
|
|
||||||
if msg0[:bytes] != msg1[:bytes]
|
|
||||||
error_string << "bytes #{msg0[:bytes]} != #{msg1[:bytes]}"
|
|
||||||
end
|
|
||||||
err = error_string.join("\n")
|
|
||||||
|
|
||||||
raise StandardError, "Message mismatch #{err}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
RSpec.shared_examples "at parity with database" do
|
|
||||||
before do
|
|
||||||
pgcat_socket.send_startup_message("sharding_user", "sharded_db", "sharding_user")
|
|
||||||
pgdb_socket.send_startup_message("sharding_user", "shard0", "sharding_user")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "works" do
|
|
||||||
run_comparison(sequence, pgcat_socket, pgdb_socket)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "Cancel Query" do
|
|
||||||
let(:sequence) {
|
|
||||||
[
|
|
||||||
[:send_query_message, "SELECT pg_sleep(5)"],
|
|
||||||
[:cancel_query]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
it_behaves_like "at parity with database"
|
|
||||||
end
|
|
||||||
|
|
||||||
xcontext "Simple query after parse" do
|
|
||||||
let(:sequence) {
|
|
||||||
[
|
|
||||||
[:send_parse_message, "SELECT 5"],
|
|
||||||
[:send_query_message, "SELECT 1"],
|
|
||||||
[:send_bind_message],
|
|
||||||
[:send_describe_message, "P"],
|
|
||||||
[:send_execute_message],
|
|
||||||
[:send_sync_message],
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Known to fail due to PgCat not supporting flush
|
|
||||||
it_behaves_like "at parity with database"
|
|
||||||
end
|
|
||||||
|
|
||||||
xcontext "Flush message" do
|
|
||||||
let(:sequence) {
|
|
||||||
[
|
|
||||||
[:send_parse_message, "SELECT 1"],
|
|
||||||
[:send_flush_message]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Known to fail due to PgCat not supporting flush
|
|
||||||
it_behaves_like "at parity with database"
|
|
||||||
end
|
|
||||||
|
|
||||||
xcontext "Bind without parse" do
|
|
||||||
let(:sequence) {
|
|
||||||
[
|
|
||||||
[:send_bind_message]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
# This is known to fail.
|
|
||||||
# Server responds immediately, Proxy buffers the message
|
|
||||||
it_behaves_like "at parity with database"
|
|
||||||
end
|
|
||||||
|
|
||||||
context "Simple message" do
|
|
||||||
let(:sequence) {
|
|
||||||
[[:send_query_message, "SELECT 1"]]
|
|
||||||
}
|
|
||||||
|
|
||||||
it_behaves_like "at parity with database"
|
|
||||||
end
|
|
||||||
|
|
||||||
context "Extended protocol" do
|
|
||||||
let(:sequence) {
|
|
||||||
[
|
|
||||||
[:send_parse_message, "SELECT 1"],
|
|
||||||
[:send_bind_message],
|
|
||||||
[:send_describe_message, "P"],
|
|
||||||
[:send_execute_message],
|
|
||||||
[:send_sync_message],
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
it_behaves_like "at parity with database"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -27,7 +27,7 @@ describe "Sharding" do
|
|||||||
processes.pgcat.shutdown
|
processes.pgcat.shutdown
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "automatic routing of extended protocol" do
|
describe "automatic routing of extended procotol" do
|
||||||
it "can do it" do
|
it "can do it" do
|
||||||
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
conn = PG.connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
|
||||||
conn.exec("SET SERVER ROLE TO 'auto'")
|
conn.exec("SET SERVER ROLE TO 'auto'")
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
tomli
|
|
||||||
Reference in New Issue
Block a user