Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ clap = { version = "4", features = ["derive"] }
# checksums
crc32fast = "1"

# constant-time comparison (auth)
subtle = "2"

# float ordering (used for sorted set scores)
ordered-float = "5"

Expand Down
22 changes: 16 additions & 6 deletions crates/ember-persistence/src/aof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,14 @@ impl AofWriter {
let path = path.into();
let exists = path.exists() && fs::metadata(&path).map(|m| m.len() > 0).unwrap_or(false);

let file = OpenOptions::new().create(true).append(true).open(&path)?;
let mut opts = OpenOptions::new();
opts.create(true).append(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let file = opts.open(&path)?;
let mut writer = BufWriter::new(file);

if !exists {
Expand Down Expand Up @@ -488,11 +495,14 @@ impl AofWriter {
self.writer.flush()?;

// reopen the file with truncation, write fresh header
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&self.path)?;
let mut opts = OpenOptions::new();
opts.create(true).write(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let file = opts.open(&self.path)?;
let mut writer = BufWriter::new(file);
format::write_header(&mut writer, format::AOF_MAGIC)?;
writer.flush()?;
Expand Down
13 changes: 11 additions & 2 deletions crates/ember-persistence/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
//! v1 entries (no type tag) are still readable for backward compatibility.

use std::collections::{HashMap, HashSet, VecDeque};
use std::fs::{self, File};
use std::fs::{self, File, OpenOptions};
use std::io::{self, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};

Expand Down Expand Up @@ -85,7 +85,16 @@ impl SnapshotWriter {
let final_path = path.into();
let tmp_path = final_path.with_extension("snap.tmp");

let file = File::create(&tmp_path)?;
let file = {
let mut opts = OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
opts.open(&tmp_path)?
};
let mut writer = BufWriter::new(file);

// write header: magic + version + shard_id + placeholder entry count
Expand Down
1 change: 1 addition & 0 deletions crates/ember-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ metrics-exporter-prometheus = { workspace = true }
thiserror = { workspace = true }
futures = "0.3"
dashmap = "6"
subtle = { workspace = true }

# tls
tokio-rustls = { workspace = true }
Expand Down
3 changes: 2 additions & 1 deletion crates/ember-server/src/concurrent_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::time::{Duration, Instant};
use bytes::BytesMut;
use ember_core::{ConcurrentKeyspace, Engine, TtlResult};
use ember_protocol::{parse_frame, Command, Frame, SetExpire};
use subtle::ConstantTimeEq;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};

use crate::connection_common::{
Expand Down Expand Up @@ -277,7 +278,7 @@ async fn execute_concurrent(
);
}
}
if password == *expected {
if bool::from(password.as_bytes().ct_eq(expected.as_bytes())) {
Frame::Simple("OK".into())
} else {
Frame::Error(
Expand Down
3 changes: 2 additions & 1 deletion crates/ember-server/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use bytes::{Bytes, BytesMut};
use ember_core::{Engine, KeyspaceStats, ShardRequest, ShardResponse, TtlResult, Value};
use ember_protocol::{parse_frame, Command, Frame, SetExpire};
use futures::future::join_all;
use subtle::ConstantTimeEq;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::sync::broadcast;

Expand Down Expand Up @@ -1474,7 +1475,7 @@ async fn execute(
);
}
}
if password == *expected {
if bool::from(password.as_bytes().ct_eq(expected.as_bytes())) {
Frame::Simple("OK".into())
} else {
Frame::Error(
Expand Down
3 changes: 2 additions & 1 deletion crates/ember-server/src/connection_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::time::Duration;

use ember_protocol::types::Frame;
use ember_protocol::Command;
use subtle::ConstantTimeEq;

use crate::server::ServerContext;

Expand Down Expand Up @@ -89,7 +90,7 @@ pub fn try_auth(frame: Frame, ctx: &ServerContext) -> (Frame, bool) {
);
}
}
if password == *expected {
if bool::from(password.as_bytes().ct_eq(expected.as_bytes())) {
(Frame::Simple("OK".into()), true)
} else {
(
Expand Down
33 changes: 32 additions & 1 deletion crates/ember-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ struct Args {
#[arg(long, env = "EMBER_REQUIREPASS")]
requirepass: Option<String>,

/// path to a file containing the password (alternative to --requirepass).
/// avoids exposing the password in /proc/cmdline. the file contents are
/// trimmed of trailing whitespace.
#[arg(long, env = "EMBER_REQUIREPASS_FILE")]
requirepass_file: Option<PathBuf>,

// -- TLS options (matching redis) --
/// port for TLS connections. when set, enables TLS alongside plain TCP
#[arg(long, env = "EMBER_TLS_PORT")]
Expand Down Expand Up @@ -115,7 +121,32 @@ async fn main() {
)
.init();

let args = Args::parse();
let mut args = Args::parse();

// resolve password: --requirepass-file takes the same role as --requirepass
if args.requirepass.is_some() && args.requirepass_file.is_some() {
eprintln!("error: --requirepass and --requirepass-file are mutually exclusive");
std::process::exit(1);
}
if let Some(ref path) = args.requirepass_file {
match std::fs::read_to_string(path) {
Ok(contents) => {
let password = contents.trim_end().to_string();
if password.is_empty() {
eprintln!("error: --requirepass-file is empty: {}", path.display());
std::process::exit(1);
}
args.requirepass = Some(password);
}
Err(e) => {
eprintln!(
"error: failed to read --requirepass-file '{}': {e}",
path.display()
);
std::process::exit(1);
}
}
}

let addr: SocketAddr = format!("{}:{}", args.host, args.port)
.parse()
Expand Down