Skip to content

Add basic mxd Hotline server skeleton#1

Closed
leynos wants to merge 1 commit intomainfrom
codex/implement-mxd-hotline-server-daemon-in-rust
Closed

Add basic mxd Hotline server skeleton#1
leynos wants to merge 1 commit intomainfrom
codex/implement-mxd-hotline-server-daemon-in-rust

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Jun 4, 2025

Summary

  • scaffold a new Rust project
  • add Diesel with SQLite and Tokio for async server
  • implement DB layer, schema, and migrations
  • implement minimal connection and authentication handling
  • document setup and running instructions
  • add ISC license

Testing

  • cargo check (fails: failed to download crates due to network restrictions)

https://chatgpt.com/codex/tasks/task_e_6840681088c4832284c0ca285c15cea5

Summary by Sourcery

Scaffold a minimal mxd Hotline server in Rust with async TCP handling, a SQLite-based authentication layer, and accompanying project configuration and documentation.

New Features:

  • Scaffold a new Rust project for the mxd Hotline server
  • Implement an asynchronous TCP listener using Tokio to accept client connections
  • Add basic LOGIN command processing with SQLite-backed user authentication via Diesel

Enhancements:

  • Introduce a database layer with R2D2 connection pooling and embedded Diesel migrations

Build:

  • Add Cargo.toml and diesel.toml for project dependencies and database configuration

Documentation:

  • Update README with project overview, setup, and run instructions

Chores:

  • Add ISC license file

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jun 4, 2025

Reviewer's Guide

Introduces a basic Rust-based Hotline server skeleton featuring Tokio-powered async TCP handling, a Diesel/SQLite database layer with embedded migrations, data models, and project scaffolding with dependency configuration and documentation.

File-Level Changes

Change Details Files
Project scaffold, dependency setup, and documentation
  • Initialize Cargo project and configure dependencies (Tokio, Diesel, serde, etc.)
  • Add .gitignore and ISC license
  • Update README with project description and setup/run instructions
README.md
.gitignore
Cargo.toml
LICENSE
Database layer integration with Diesel and migrations
  • Establish r2d2 SQLite connection pool
  • Embed and run Diesel migrations at startup
  • Implement helper functions for querying and creating users
  • Add diesel.toml and SQL migration scripts
  • Generate schema via src/schema.rs
src/db.rs
diesel.toml
migrations/00000000000000_create_users/up.sql
migrations/00000000000000_create_users/down.sql
src/schema.rs
Data models definition
  • Define User and NewUser structs
  • Derive Queryable, Insertable, Serialize, Deserialize traits
src/models.rs
Async TCP server skeleton with minimal authentication
  • Implement Tokio main function and listener loop
  • Spawn per-connection tasks
  • Parse and handle LOGIN commands
  • Run blocking DB lookup to verify credentials
  • Send protocol responses (MXD header, OK, FAIL, ERR)
src/main.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @leynos - I've reviewed your changes and found some issues that need to be addressed.

Blocking issues:

  • Store and compare hashed passwords instead of plaintext (link)

General comments:

  • Consider using a line‐based codec (e.g. Tokio’s BufReader::lines or a length‐prefixed frame) instead of raw 1024-byte reads to simplify command parsing and avoid partial reads.
  • Avoid blocking in the async Tokio runtime for DB calls—either switch to an async‐friendly ORM (diesel_async or sqlx) or centralize spawn_blocking to a dedicated pool.
  • Validate the LOGIN command arguments (username/password) before unwrapping to avoid silently accepting empty values; return a clear ERR code if parsing fails.
Here's what I looked at during the review
  • 🟡 General issues: 6 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/main.rs
}
}

async fn handle_client(mut socket: TcpStream, peer: SocketAddr, pool: DbPool) -> Result<(), Box<dyn Error>> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Handler processes only one command per connection

Currently, the handler closes the connection after processing one message. To support multiple commands per session, implement a loop to handle messages until the client disconnects.

Comment thread src/main.rs
let password = parts.next().unwrap_or("");
let valid = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|e| Box::new(e) as Box<dyn Error>)?;
Ok::<_, Box<dyn Error>>(get_user_by_name(&mut conn, username)?.map(|u| u.password == password))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Store and compare hashed passwords instead of plaintext

Hash passwords using a secure algorithm and use constant-time comparison to protect user credentials.

Comment thread src/main.rs
let pool = establish_pool(&database_url);
{
let mut conn = pool.get().expect("failed to get db connection");
run_migrations(&mut conn);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider handling migration failures gracefully

Refactor run_migrations to return a Result, and propagate errors with the ? operator in main. This allows for proper error logging and clean shutdowns instead of panics.

Suggested implementation:

    {
        let mut conn = pool.get().expect("failed to get db connection");
        run_migrations(&mut conn)?;
    }

You must also update the run_migrations function in your db module to return a Result<(), Box<dyn std::error::Error>> (or a more specific error type if appropriate), and ensure it does not panic internally but instead returns errors.

Comment thread src/main.rs
Comment on lines +39 to +40
let mut buf = vec![0u8; 1024];
let n = socket.read(&mut buf).await?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Avoid allocating a Vec for a fixed-size buffer

Using Vec here increases heap allocations. A stack-allocated array or buffer pool would be more efficient.

Suggested change
let mut buf = vec![0u8; 1024];
let n = socket.read(&mut buf).await?;
let mut buf = [0u8; 1024];
let n = socket.read(&mut buf).await?;

Comment thread src/main.rs
return Ok(());
}
let msg = String::from_utf8_lossy(&buf[..n]);
let mut parts = msg.trim().splitn(3, ' ');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Command parsing fails if password contains spaces

Consider using a more robust parsing method, such as JSON or a unique delimiter, to handle passwords containing spaces.

Suggested implementation:

    let msg = String::from_utf8_lossy(&buf[..n]);
    // Expecting a JSON object like: {"cmd":"LOGIN","username":"the user","password":"the password"}
    let login_req: serde_json::Value = match serde_json::from_str(msg.trim()) {
        Ok(val) => val,
        Err(_) => {
            socket.write_all(b"ERR invalid json\n").await?;
            return Ok(());
        }
    };
    match login_req.get("cmd").and_then(|c| c.as_str()) {
        Some("LOGIN") => {
            let username = login_req.get("username").and_then(|u| u.as_str()).unwrap_or("");
            let password = login_req.get("password").and_then(|p| p.as_str()).unwrap_or("");
            let valid = tokio::task::spawn_blocking(move || {
                let mut conn = pool.get().map_err(|e| Box::new(e) as Box<dyn Error>)?;
                Ok::<_, Box<dyn Error>>(get_user_by_name(&mut conn, username)?.map(|u| u.password == password))
            }).await??;
            if valid.unwrap_or(false) {
                socket.write_all(b"OK\n").await?;
                println!("{} authenticated as {}", peer, username);
            } else {
  1. Add serde_json to your dependencies in Cargo.toml if not already present:
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"
    
  2. Add use serde_json; at the top of your file if not already present.
  3. Update any client code to send JSON-formatted LOGIN commands, e.g.:
    {"cmd":"LOGIN","username":"the user","password":"the password"}
    

Comment thread src/db.rs
Comment on lines +21 to +28
pub struct Connection(pub r2d2::PooledConnection<ConnectionManager<SqliteConnection>>);

impl Deref for Connection {
type Target = SqliteConnection;
fn deref(&self) -> &Self::Target {
&self.0
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Remove unused Connection struct

Removing unused code helps keep the codebase clean and easier to maintain.

Suggested change
pub struct Connection(pub r2d2::PooledConnection<ConnectionManager<SqliteConnection>>);
impl Deref for Connection {
type Target = SqliteConnection;
fn deref(&self) -> &Self::Target {
&self.0
}
}

Comment thread src/main.rs
Comment on lines +50 to +59
let valid = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|e| Box::new(e) as Box<dyn Error>)?;
Ok::<_, Box<dyn Error>>(get_user_by_name(&mut conn, username)?.map(|u| u.password == password))
}).await??;
if valid.unwrap_or(false) {
socket.write_all(b"OK\n").await?;
println!("{} authenticated as {}", peer, username);
} else {
socket.write_all(b"FAIL\n").await?;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Clarify error handling for spawn_blocking

Handle JoinError and your closure's error separately to better distinguish between task panics and application/database errors, which will aid in debugging and monitoring.

Suggested change
let valid = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|e| Box::new(e) as Box<dyn Error>)?;
Ok::<_, Box<dyn Error>>(get_user_by_name(&mut conn, username)?.map(|u| u.password == password))
}).await??;
if valid.unwrap_or(false) {
socket.write_all(b"OK\n").await?;
println!("{} authenticated as {}", peer, username);
} else {
socket.write_all(b"FAIL\n").await?;
}
let join_result = tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|e| Box::new(e) as Box<dyn Error>)?;
Ok::<_, Box<dyn Error>>(get_user_by_name(&mut conn, username)?.map(|u| u.password == password))
}).await;
let valid = match join_result {
Err(join_err) => {
eprintln!("spawn_blocking task panicked or was cancelled: {}", join_err);
socket.write_all(b"ERR Internal error\n").await?;
return Ok(());
}
Ok(inner_result) => match inner_result {
Err(app_err) => {
eprintln!("Error during user lookup: {}", app_err);
socket.write_all(b"ERR Internal error\n").await?;
return Ok(());
}
Ok(valid) => valid,
}
};
if valid.unwrap_or(false) {
socket.write_all(b"OK\n").await?;
println!("{} authenticated as {}", peer, username);
} else {
socket.write_all(b"FAIL\n").await?;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant