Skip to content

Commit 5f74585

Browse files
committed
Started on md5 auth.
Left to figure out: Whats the right format to store user:pw in userlist? hashmap errors? actually do hash:x comparison
1 parent 1e8fa11 commit 5f74585

File tree

10 files changed

+213
-9
lines changed

10 files changed

+213
-9
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
/target
22
*.deb
3+
.idea/*
4+
tests/ruby/.bundle/*
5+
tests/ruby/vendor/*

Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ sha-1 = "0.10"
1717
toml = "0.5"
1818
serde = "1"
1919
serde_derive = "1"
20+
serde_json = "1"
2021
regex = "1"
2122
num_cpus = "1"
2223
once_cell = "1"

pgcat.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ password = "sharding_user"
4848

4949
# [ host, port, role ]
5050
servers = [
51-
[ "127.0.0.1", 5432, "primary" ],
52-
[ "localhost", 5432, "replica" ],
51+
["127.0.0.1", 5432, "primary"],
52+
["localhost", 5432, "replica"],
5353
# [ "127.0.1.1", 5432, "replica" ],
5454
]
5555
# Database name (e.g. "postgres")
@@ -58,17 +58,17 @@ database = "shard0"
5858
[shards.1]
5959
# [ host, port, role ]
6060
servers = [
61-
[ "127.0.0.1", 5432, "primary" ],
62-
[ "localhost", 5432, "replica" ],
61+
["127.0.0.1", 5432, "primary"],
62+
["localhost", 5432, "replica"],
6363
# [ "127.0.1.1", 5432, "replica" ],
6464
]
6565
database = "shard1"
6666

6767
[shards.2]
6868
# [ host, port, role ]
6969
servers = [
70-
[ "127.0.0.1", 5432, "primary" ],
71-
[ "localhost", 5432, "replica" ],
70+
["127.0.0.1", 5432, "primary"],
71+
["localhost", 5432, "replica"],
7272
# [ "127.0.1.1", 5432, "replica" ],
7373
]
7474
database = "shard2"

src/client.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,18 @@ impl Client {
108108
// Regular startup message.
109109
PROTOCOL_VERSION_NUMBER => {
110110
trace!("Got StartupMessage");
111-
112-
// TODO: perform actual auth.
113111
let parameters = parse_startup(bytes.clone())?;
112+
let mut user_name: String = String::new();
113+
match parameters.get(&"user") {
114+
Some(&user) => user_name = user,
115+
None => return Err(Error::ClientBadStartup),
116+
}
117+
start_auth(&mut stream, &user_name).await?;
114118

115119
// Generate random backend ID and secret key
116120
let process_id: i32 = rand::random();
117121
let secret_key: i32 = rand::random();
118122

119-
auth_ok(&mut stream).await?;
120123
write_all(&mut stream, server_info).await?;
121124
backend_key_data(&mut stream, process_id, secret_key).await?;
122125
ready_for_query(&mut stream).await?;

src/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ pub enum Error {
88
// ServerTimeout,
99
// DirtyServer,
1010
BadConfig,
11+
BadUserList,
1112
AllServersDown,
13+
AuthenticationError
1214
}

src/main.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ use std::sync::Arc;
5050
mod admin;
5151
mod client;
5252
mod config;
53+
mod userlist;
5354
mod constants;
5455
mod errors;
5556
mod messages;
@@ -94,6 +95,15 @@ async fn main() {
9495
}
9596
};
9697

98+
// Prepare user list
99+
match userlist::parse("userlist.json").await {
100+
Ok(_) => (),
101+
Err(err) => {
102+
error!("Userlist parse error: {:?}", err);
103+
return;
104+
}
105+
};
106+
97107
let config = get_config();
98108

99109
let addr = format!("{}:{}", config.general.host, config.general.port);

src/messages.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
/// Helper functions to send one-off protocol messages
22
/// and handle TcpStream (TCP socket).
3+
4+
35
use bytes::{Buf, BufMut, BytesMut};
46
use md5::{Digest, Md5};
57
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
68
use tokio::net::{
79
tcp::{OwnedReadHalf, OwnedWriteHalf},
810
TcpStream,
911
};
12+
use log::{error};
1013

1114
use crate::errors::Error;
1215
use std::collections::HashMap;
1316

17+
use rand::Rng;
18+
19+
use crate::userlist::get_user_list;
20+
21+
1422
/// Postgres data type mappings
1523
/// used in RowDescription ('T') message.
1624
pub enum DataType {
@@ -29,6 +37,98 @@ impl From<&DataType> for i32 {
2937
}
3038
}
3139

40+
/**
41+
1. Generate salt (4 bytes of random data)
42+
md5(concat(md5(concat(password, username)), random-salt)))
43+
2. Send md5 auth request
44+
3. recieve PasswordMessage with salt.
45+
4. refactor md5_password function to be reusable
46+
5. check username hash combo against file
47+
6. AuthenticationOk or ErrorResponse
48+
**/
49+
pub async fn start_auth(stream: &mut TcpStream, user_name: &String) -> Result<(), Error> {
50+
let mut rng = rand::thread_rng();
51+
52+
//Generate random 4 byte salt
53+
let salt = rng.gen::<u32>();
54+
55+
// Send AuthenticationMD5Password request
56+
send_md5_request(stream, salt).await?;
57+
58+
let code = match stream.read_u8().await {
59+
Ok(code) => code as char,
60+
Err(_) => return Err(Error::AuthenticationError),
61+
};
62+
63+
match code {
64+
// Password response
65+
'p' => {
66+
fetch_password_and_authenticate(stream, &user_name, &salt).await?;
67+
Ok(auth_ok(stream).await?)
68+
}
69+
_ => {
70+
error!("Unknown code: {}", code);
71+
return Err(Error::AuthenticationError);
72+
}
73+
}
74+
}
75+
76+
pub async fn send_md5_request(stream: &mut TcpStream, salt: u32) -> Result<(), Error> {
77+
let mut authentication_md5password = BytesMut::with_capacity(12);
78+
authentication_md5password.put_u8(b'R');
79+
authentication_md5password.put_i32(12);
80+
authentication_md5password.put_i32(5);
81+
authentication_md5password.put_u32(salt);
82+
83+
// Send AuthenticationMD5Password request
84+
Ok(write_all(stream, authentication_md5password).await?)
85+
}
86+
87+
pub async fn fetch_password_and_authenticate(stream: &mut TcpStream, user_name: &String, salt: &u32) -> Result<(), Error> {
88+
/**
89+
1. How do I store the lists of users and paswords? clear text or hash?? wtf
90+
2. Add auth to tests
91+
**/
92+
93+
let len = match stream.read_i32().await {
94+
Ok(len) => len,
95+
Err(_) => return Err(Error::AuthenticationError),
96+
};
97+
98+
// Read whatever is left.
99+
let mut password_hash = vec![0u8; len as usize - 4];
100+
101+
match stream.read_exact(&mut password_hash).await {
102+
Ok(_) => (),
103+
Err(_) => return Err(Error::AuthenticationError),
104+
};
105+
106+
let user_list = get_user_list();
107+
let mut password: String = String::new();
108+
match user_list.get(&user_name) {
109+
Some(&p) => password = p,
110+
None => return Err(Error::AuthenticationError),
111+
}
112+
113+
let mut md5 = Md5::new();
114+
115+
// concat('md5', md5(concat(md5(concat(password, username)), random-salt)))
116+
// First pass
117+
md5.update(&password.as_bytes());
118+
md5.update(&user_name.as_bytes());
119+
let output = md5.finalize_reset();
120+
// Second pass
121+
md5.update(format!("{:x}", output));
122+
md5.update(salt.to_be_bytes().to_vec());
123+
124+
125+
let password_string: String = String::from_utf8(password_hash).expect("Could not get password hash");
126+
match format!("md5{:x}", md5.finalize()) == password_string {
127+
true => Ok(()),
128+
_ => Err(Error::AuthenticationError)
129+
}
130+
}
131+
32132
/// Tell the client that authentication handshake completed successfully.
33133
pub async fn auth_ok(stream: &mut TcpStream) -> Result<(), Error> {
34134
let mut auth_ok = BytesMut::with_capacity(9);

src/userlist.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"sven": "clear_text_password",
3+
"sharding_user": "sharding_user"
4+
}

src/userlist.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use arc_swap::{ArcSwap, Guard};
2+
use log::{error};
3+
use once_cell::sync::Lazy;
4+
use tokio::fs::File;
5+
use tokio::io::AsyncReadExt;
6+
7+
use std::collections::{HashMap};
8+
use std::sync::Arc;
9+
10+
use crate::errors::Error;
11+
12+
pub type UserList = HashMap<String, String>;
13+
static USER_LIST: Lazy<ArcSwap<UserList>> = Lazy::new(|| ArcSwap::from_pointee(HashMap::new()));
14+
15+
pub fn get_user_list() -> Guard<Arc<UserList>> {
16+
USER_LIST.load()
17+
}
18+
19+
/// Parse the user list.
20+
pub async fn parse(path: &str) -> Result<(), Error> {
21+
let mut contents = String::new();
22+
let mut file = match File::open(path).await {
23+
Ok(file) => file,
24+
Err(err) => {
25+
error!("Could not open '{}': {}", path, err.to_string());
26+
return Err(Error::BadConfig);
27+
}
28+
};
29+
30+
match file.read_to_string(&mut contents).await {
31+
Ok(_) => (),
32+
Err(err) => {
33+
error!("Could not read config file: {}", err.to_string());
34+
return Err(Error::BadConfig);
35+
}
36+
};
37+
38+
let map: HashMap<String, String> = serde_json::from_str(&contents).expect("JSON was not well-formatted");
39+
40+
41+
42+
USER_LIST.store(Arc::new(map.clone()));
43+
44+
Ok(())
45+
}
46+
47+
#[cfg(test)]
48+
mod test {
49+
use super::*;
50+
51+
#[tokio::test]
52+
async fn test_config() {
53+
parse("userlist.json").await.unwrap();
54+
assert_eq!(get_user_list()["sven"], "clear_text_password");
55+
assert_eq!(get_user_list()["sharding_user"], "sharding_user");
56+
}
57+
}

0 commit comments

Comments
 (0)