Skip to content

Commit 9d22423

Browse files
committed
Merge branch 'develop' of https://github.com/rustbase/rustbase-cli into release/v0.4.0
2 parents b67e2dc + 6ca3f21 commit 9d22423

5 files changed

Lines changed: 199 additions & 42 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ license = "MIT"
88
[dependencies]
99
tokio = { version = "1.23.1", features = ["macros", "rt-multi-thread", "net", "io-std", "io-util"] }
1010
bson = "2.4.0"
11-
rustyline = "10.0.0"
11+
rustyline = "10.1.1"
1212
colored = "2.0.0"
1313
clap = "3.2.19"
1414
clap_derive = "3.2.18"
@@ -17,4 +17,7 @@ serde = { version = "1.0.147", features = ["derive"] }
1717
tokio-rustls = "0.23.4"
1818
rustls-pemfile = "1.0.1"
1919
rustls = "0.20.7"
20-
webpki = "0.22.0"
20+
webpki = "0.22.0"
21+
scram = "0.6.0"
22+
unicode-width = "0.1.10"
23+
rustyline-derive = "0.7.0"

src/engine/auth.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use scram::ScramClient;
2+
use tokio::io::{AsyncRead, AsyncWrite};
3+
4+
use super::AuthConfig;
5+
use super::Rustbase;
6+
7+
pub async fn auth<IO>(auth_config: AuthConfig, client: &mut IO)
8+
where
9+
IO: AsyncRead + AsyncWrite + Unpin,
10+
{
11+
let scram = ScramClient::new(&auth_config.username, &auth_config.password, None);
12+
let (scram, client_first) = scram.client_first();
13+
14+
let server_first = Rustbase::send_and_receive(client, client_first.as_bytes().to_vec()).await;
15+
let server_first = String::from_utf8(server_first).unwrap();
16+
17+
let scram = scram
18+
.handle_server_first(&server_first)
19+
.expect("Invalid server first message, maybe server is not using scram? or maybe the username is wrong?");
20+
let (scram, client_final) = scram.client_final();
21+
22+
let server_final = Rustbase::send_and_receive(client, client_final.as_bytes().to_vec()).await;
23+
let server_final = String::from_utf8(server_final).unwrap();
24+
25+
scram.handle_server_final(&server_final).unwrap();
26+
}

src/engine/mod.rs

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
1111
use tokio::net::TcpStream;
1212
use tokio_rustls::{client::TlsStream, TlsConnector};
1313

14+
mod auth;
15+
1416
#[derive(Debug)]
1517
pub enum Request {
1618
Query(String),
@@ -39,42 +41,73 @@ pub enum Status {
3941
InvalidAuth,
4042
}
4143

42-
pub struct Rustbase {
44+
#[derive(Clone)]
45+
pub struct RustbaseConfig {
46+
pub tls: Option<TlsConfig>,
4347
pub host: String,
48+
pub port: String,
49+
pub database: String,
50+
pub auth: Option<AuthConfig>,
51+
}
52+
53+
#[derive(Clone)]
54+
pub struct TlsConfig {
55+
pub ca_file: String,
56+
}
57+
58+
#[derive(Clone)]
59+
pub struct AuthConfig {
60+
pub username: String,
61+
pub password: String,
62+
}
63+
64+
pub struct Rustbase {
4465
pub client: Option<TcpStream>,
4566
pub database: String,
4667
pub tls_client: Option<TlsStream<TcpStream>>,
4768
}
4869

4970
impl Rustbase {
50-
pub async fn connect(host: String, port: String, database: String) -> Rustbase {
51-
let client = TcpStream::connect(format!("{}:{}", host, port))
71+
pub async fn connect(config: RustbaseConfig) -> Rustbase {
72+
if config.clone().tls.is_some() {
73+
let mut rb = Rustbase::connect_tls(config.clone()).await;
74+
75+
if let Some(auth) = config.auth {
76+
let client = rb.tls_client.as_mut().unwrap();
77+
78+
auth::auth(auth, client).await;
79+
}
80+
81+
return rb;
82+
}
83+
84+
let mut client = TcpStream::connect(format!("{}:{}", config.host, config.port))
5285
.await
5386
.unwrap();
5487

88+
if let Some(auth) = config.auth {
89+
auth::auth(auth, &mut client).await;
90+
}
91+
5592
Rustbase {
5693
client: Some(client),
57-
database,
94+
database: config.database,
5895
tls_client: None,
59-
host,
6096
}
6197
}
6298

63-
pub async fn connect_tls(
64-
host: String,
65-
port: String,
66-
database: String,
67-
ca_file: String,
68-
) -> Rustbase {
69-
if !Path::new(&ca_file).exists() {
99+
async fn connect_tls(config: RustbaseConfig) -> Rustbase {
100+
let tls = config.tls.unwrap();
101+
102+
if !Path::new(&tls.ca_file).exists() {
70103
println!(
71104
"{} CA file not found: use --ca_file=<path>",
72105
"[Error]".red()
73106
);
74107
}
75108

76109
let mut root_cert_store = rustls::RootCertStore::empty();
77-
let mut pem = BufReader::new(File::open(ca_file).unwrap());
110+
let mut pem = BufReader::new(File::open(tls.ca_file).unwrap());
78111
let certs = rustls_pemfile::certs(&mut pem).unwrap();
79112
let trust_anchors = certs.iter().map(|cert| {
80113
let ta = webpki::TrustAnchor::try_from_cert_der(&cert[..]).unwrap();
@@ -86,28 +119,27 @@ impl Rustbase {
86119
});
87120
root_cert_store.add_server_trust_anchors(trust_anchors);
88121

89-
let config = rustls::ClientConfig::builder()
122+
let client_config = rustls::ClientConfig::builder()
90123
.with_safe_defaults()
91124
.with_root_certificates(root_cert_store)
92125
.with_no_client_auth();
93126

94-
let connector = TlsConnector::from(Arc::new(config));
127+
let connector = TlsConnector::from(Arc::new(client_config));
95128

96-
let domain = rustls::ServerName::try_from(host.as_str())
129+
let domain = rustls::ServerName::try_from(config.host.as_str())
97130
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid dnsname"))
98131
.unwrap();
99132

100-
let client = TcpStream::connect(format!("{}:{}", host, port))
133+
let client = TcpStream::connect(format!("{}:{}", config.host, config.port))
101134
.await
102135
.unwrap();
103136

104137
let tls_client = connector.connect(domain, client).await.unwrap();
105138

106139
Rustbase {
107140
client: None,
108-
database,
141+
database: config.database,
109142
tls_client: Some(tls_client),
110-
host,
111143
}
112144
}
113145

src/main.rs

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,36 @@ mod engine;
44
mod utils;
55

66
use colored::Colorize;
7+
8+
use rustyline::config::Configurer;
79
use rustyline::error::ReadlineError;
8-
use rustyline::{Editor, Result};
10+
use rustyline::highlight::Highlighter;
11+
use rustyline::{ColorMode, Editor, Result};
12+
use rustyline_derive::{Completer, Helper, Hinter, Validator};
13+
14+
use std::borrow::Cow::{self, Borrowed, Owned};
15+
16+
use engine::{AuthConfig, RustbaseConfig, TlsConfig};
17+
18+
#[derive(Completer, Helper, Hinter, Validator)]
19+
struct MaskingHighlighter {
20+
masking: bool,
21+
}
22+
23+
impl Highlighter for MaskingHighlighter {
24+
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
25+
use unicode_width::UnicodeWidthStr;
26+
if self.masking {
27+
Owned("*".repeat(line.width()))
28+
} else {
29+
Borrowed(line)
30+
}
31+
}
32+
33+
fn highlight_char(&self, _line: &str, _pos: usize) -> bool {
34+
self.masking
35+
}
36+
}
937

1038
/// A CLI for Rustbase Database Server
1139
#[derive(clap_derive::Parser)]
@@ -24,14 +52,33 @@ struct Args {
2452
#[clap(long, default_value = "")]
2553
ca_file: String,
2654

55+
/// Use authentication for connection
56+
#[clap(long, short = 'a')]
57+
use_auth: bool,
58+
2759
#[clap(subcommand)]
2860
commands: Option<Commands>,
2961
}
3062

63+
fn prompt<T>(prompt: &str, editor: &mut Editor<T>) -> Result<String>
64+
where
65+
T: rustyline::Helper,
66+
{
67+
let result = loop {
68+
let prompt_result = editor.readline(prompt)?;
69+
70+
if !prompt_result.is_empty() {
71+
break prompt_result;
72+
}
73+
};
74+
75+
Ok(result)
76+
}
77+
3178
#[derive(clap_derive::Subcommand, PartialEq)]
3279
enum Commands {
33-
#[clap(about = "Upgrade Rustbase and Rustbase CLI")]
34-
Upgrade,
80+
#[clap(about = "Update Rustbase CLI")]
81+
Update,
3582
#[clap(about = "Clean repl history")]
3683
Clean,
3784
}
@@ -43,7 +90,7 @@ async fn main() -> Result<()> {
4390
let repl_path = utils::get_current_path().join("repl.history");
4491

4592
match args.commands {
46-
Some(Commands::Upgrade) => {
93+
Some(Commands::Update) => {
4794
println!("Not implemented yet");
4895
return Ok(());
4996
}
@@ -83,28 +130,51 @@ async fn main() -> Result<()> {
83130
);
84131
println!("Press Ctrl+C to exit.");
85132

86-
let mut rl = Editor::<()>::new()?;
133+
let h = MaskingHighlighter { masking: false };
134+
let mut rl = Editor::new()?;
135+
rl.set_helper(Some(h));
87136

88137
rl.load_history(repl_path.to_str().unwrap()).ok();
89138
println!();
90139

91-
let mut database = rl.readline("Database: ")?;
140+
let mut database = prompt("Database: ", &mut rl)?;
92141

93-
loop {
94-
if database.is_empty() {
95-
database = rl.readline("Database: ")?;
96-
continue;
97-
} else {
98-
break;
99-
}
100-
}
142+
let auth_config = if args.use_auth {
143+
let username = prompt("Username: ", &mut rl)?;
144+
145+
rl.helper_mut().expect("No helper").masking = true;
146+
rl.set_color_mode(ColorMode::Forced); // force masking
147+
rl.set_auto_add_history(false); // make sure password is not added to history
148+
149+
let password = prompt("Password: ", &mut rl)?;
101150

102-
let mut client = if args.tls {
103-
engine::Rustbase::connect_tls(args.host, args.port, database.clone(), args.ca_file).await
151+
rl.helper_mut().expect("No helper").masking = false;
152+
rl.set_color_mode(ColorMode::Disabled);
153+
rl.set_auto_add_history(true);
154+
155+
Some(AuthConfig { username, password })
156+
} else {
157+
None
158+
};
159+
160+
let tls_config = if args.tls {
161+
let ca_file = args.ca_file;
162+
163+
Some(TlsConfig { ca_file })
104164
} else {
105-
engine::Rustbase::connect(args.host, args.port, database.clone()).await
165+
None
166+
};
167+
168+
let config = RustbaseConfig {
169+
database: database.clone(),
170+
port: args.port,
171+
host: args.host,
172+
auth: auth_config,
173+
tls: tls_config,
106174
};
107175

176+
let mut client = engine::Rustbase::connect(config).await;
177+
108178
loop {
109179
let readline = rl.readline(format!("{}> ", database).as_str());
110180
match readline {

0 commit comments

Comments
 (0)