Skip to content

Instantly share code, notes, and snippets.

@luqmana
Created May 2, 2014 05:16
Show Gist options
  • Save luqmana/2d688d81fed84caba0a6 to your computer and use it in GitHub Desktop.
Save luqmana/2d688d81fed84caba0a6 to your computer and use it in GitHub Desktop.
Ident Server in Rust

Simple Ident Server in Rust

This is a quick example of a simple TCP server written (mostly) in Rust which implements RFC 1413. It has a bit of networking, regex, and FFI.

All code examples have been written against a recent build from git: rustc 0.11-pre (9f484e6 2014-04-30 15:46:47 -0700)

To start off, how about we flesh out the general structure a little:

use std::io::TcpStream;

fn handle_client(mut sock: TcpStream) { }

fn main() { }

We have our main function and handle_client which receives a TcpStream. Neither of them does anything right now so let's have it just start listening on port 113.

use std::io::Listener;
use std::io::{TcpListener, TcpStream};

fn handle_client(mut sock: TcpStream) { }

fn main() {
    let addr = from_str("0.0.0.0:113").unwrap();
    let mut acceptor = match TcpListener::bind(addr).listen() {
        Ok(acceptor) => acceptor,
        Err(e) => fail!("Couldn't listen on {}: {}.", addr, e)
    };
    
    println!("Listening on {}.", addr);
}

Now, let's break this down. We want to listen on 0.0.0.0:113, that is port 113 on all interfaces. But, notice that TcpListener::bind takes a SocketAddr so we use the generic from_str function to take our string and parse a SocketAddr out of it. (Actually from_str gives back an Option<SocketAddr> but since we know we've passed a valid address we just unwrap it directly instead of matching and checking for errors)

Then, once we've binded the address we want, we call listen to get a TcpAcceptor or otherwise fail with an error.

Next let's create a new task for each request we get and have it run handle_client:

use std::io::{Acceptor, Listener};
use std::io::{TcpListener, TcpStream};

fn handle_client(mut sock: TcpStream) {
    println!("Connection from {}.", sock.peer_name());
}

fn main() {
    let addr = from_str("0.0.0.0:113").unwrap();
    let mut acceptor = match TcpListener::bind(addr).listen() {
        Ok(acceptor) => acceptor,
        Err(e) => fail!("Couldn't listen on {}: {}.", addr, e)
    };
    
    println!("Listening on {}.", addr);
    
    for stream in acceptor.incoming() {
        match stream {
            Ok(s) => spawn(proc() handle_client(s)),
            Err(e) => println!("Couldn't accept connection: {}.", e)
        }
    }
}

We simply use the incoming method on the TcpAcceptor to get an iterator that yields incoming connections. For each connection we match to make sure everything's ok and then use spawn to create a new task to handle the connection. spawn takes a proc (a unique closure that may only be invoked once) in which it calls handle_client and thus prints the address/port pair of the incoming connection.

Now, we've got most of the setup out of the way and can get to implementing the protocol. Let's start off by reading in the request:

use std::io::{Acceptor, Listener};
use std::io::{BufferedStream, TcpListener, TcpStream};

fn handle_client(mut sock: TcpStream) {
    println!("Connection from {}.", sock.peer_name());
    
    let ref mut sock = BufferedStream::new(sock);
    let req = match sock.read_line() {
        Ok(buf) => buf,
        Err(e) => {
            println!("Couldn't read from stream: {}.", e);
            return;
        }
    };
}

fn main() {
    let addr = from_str("0.0.0.0:113").unwrap();
    let mut acceptor = match TcpListener::bind(addr).listen() {
        Ok(acceptor) => acceptor,
        Err(e) => fail!("Couldn't listen on {}: {}.", addr, e)
    };
    
    println!("Listening on {}.", addr);
    
    for stream in acceptor.incoming() {
        match stream {
            Ok(s) => spawn(proc() handle_client(s)),
            Err(e) => println!("Couldn't accept connection: {}.", e)
        }
    }
}

We first wrap our sock in a BufferedStream. Now since ident requests are terminated with an EOL (CR-LF according to page 5 of RFC 1413), we use the read_line method.

Now, the request will be a pair of ports separated by a comma. Why not use a regex to make sure that is the kind of requests we get:

#![feature(phase)]

extern crate regex;

#[phase(syntax)]
extern crate regex_macros;

use std::io::{Acceptor, Listener};
use std::io::{BufferedStream, TcpListener, TcpStream};

fn handle_client(mut sock: TcpStream) {
    println!("Connection from {}.", sock.peer_name());
    
    let ref mut sock = BufferedStream::new(sock);
    let req = match sock.read_line() {
        Ok(buf) => buf,
        Err(e) => {
            println!("Couldn't read from stream: {}.", e);
            return;
        }
    };
    
    // Now we match against a regex for the right format
    let r = regex!("(?P<sport>[0-9]+)[ ]*,[ ]*(?P<cport>[0-9]+)");
    
    // Does the input match?
    match r.captures(req) {
        Some(caps) => {
            // Yes it does!
            
        }
        None => println!("Received badly formatted request.")
    }
}

fn main() {
    let addr = from_str("0.0.0.0:113").unwrap();
    let mut acceptor = match TcpListener::bind(addr).listen() {
        Ok(acceptor) => acceptor,
        Err(e) => fail!("Couldn't listen on {}: {}.", addr, e)
    };
    
    println!("Listening on {}.", addr);
    
    for stream in acceptor.incoming() {
        match stream {
            Ok(s) => spawn(proc() handle_client(s)),
            Err(e) => println!("Couldn't accept connection: {}.", e)
        }
    }
}

Ok, so we start off by pulling in the regex library with extern crate regex; as well as another library that let's us use 'native' regex. To do this, we use extern crate regex_macros; as usual but also annotate it with #[phase(syntax)] to indicate it's code that should be run at compile time. (We also add #![feature(phase)] so the compiler won't yell at us about using the phase attribute since it is currently behind a feature gate.)

Now, we can create a regex (that'll be turned into Rust code at compile time) using the regex! macro and use it to match the request we've read in. If the input does indeed match, then we can go ahead and try to process the request.

To do port -> uid and uid -> name steps we'll call out to C:

#![feature(phase)]

extern crate libc;
extern crate regex;

#[phase(syntax)]
extern crate regex_macros;

use libc::{c_int, c_ushort};
use std::io::{Acceptor, Listener};
use std::io::{BufferedStream, TcpListener, TcpStream};

#[link(name = "identd_procfs")]
extern {
    fn lport_to_uid(port: c_ushort) -> c_int;
}

fn handle_client(mut sock: TcpStream) {
    println!("Connection from {}.", sock.peer_name());
    
    let ref mut sock = BufferedStream::new(sock);
    let req = match sock.read_line() {
        Ok(buf) => buf,
        Err(e) => {
            println!("Couldn't read from stream: {}.", e);
            return;
        }
    };
    
    // Now we match against a regex for the right format
    let r = regex!("(?P<sport>[0-9]+)[ ]*,[ ]*(?P<cport>[0-9]+)");
    
    // Does the input match?
    match r.captures(req) {
        Some(caps) => {
            // Yes it does!
            let sport: c_ushort = from_str(caps.name("sport")).unwrap();
            let cport: c_ushort = from_str(caps.name("cport")).unwrap();
            
            let uid = unsafe { lport_to_uid(sport) };
            if uid == -1 {
                println!("Couldn't find user mapped to port: {}.", sport);
                    
                write!(sock, "{}, {} : ERROR : NO-USER", sport, cport);
                return;
            }
        }
        None => println!("Received badly formatted request.")
    }
}

fn main() {
    let addr = from_str("0.0.0.0:113").unwrap();
    let mut acceptor = match TcpListener::bind(addr).listen() {
        Ok(acceptor) => acceptor,
        Err(e) => fail!("Couldn't listen on {}: {}.", addr, e)
    };
    
    println!("Listening on {}.", addr);
    
    for stream in acceptor.incoming() {
        match stream {
            Ok(s) => spawn(proc() handle_client(s)),
            Err(e) => println!("Couldn't accept connection: {}.", e)
        }
    }
}

We've pulled in the libc crate so we can use the right types when dealing with C code. We also added an extern block with one function from a library called identd_procfs. lport_to_uid takes a port and by crawling through /proc finds the uid for the process which currently has that port open.

Now in our handle_client function, once we've matched on the regex we extract the parts we named: sport and cport. Again, we use from_str to convert those strings to the type we want, in this case c_ushort.

So we make our call to lport_to_uid, wrapping it in an unsafe block since it's an FFI call. If we can't an associated uid for the server port we simply write the error to the socket and return.

Since we have a valid uid now, we need the actual username and for that we use getpwuid:

#![feature(phase)]

extern crate libc;
extern crate regex;

#[phase(syntax)]
extern crate regex_macros;

use libc::{c_char, c_int, c_ushort};
use std::io::{Acceptor, Listener};
use std::io::{BufferedStream, TcpListener, TcpStream};
use std::str;

#[link(name = "identd_procfs")]
extern {
    fn lport_to_uid(port: c_ushort) -> c_int;
}

extern {
    fn getpwuid(uid: c_int) -> *passwd;
}

struct passwd {
    pw_name: *c_char,
    pw_passd: *c_char,
    pw_uid: c_int,
    pw_gid: c_int,
    pw_gecox: *c_char,
    pw_dir: *c_char,
    pw_shell: *c_char
}

fn handle_client(mut sock: TcpStream) {
    println!("Connection from {}.", sock.peer_name());
    
    let ref mut sock = BufferedStream::new(sock);
    let req = match sock.read_line() {
        Ok(buf) => buf,
        Err(e) => {
            println!("Couldn't read from stream: {}.", e);
            return;
        }
    };
    
    // Now we match against a regex for the right format
    let r = regex!("(?P<sport>[0-9]+)[ ]*,[ ]*(?P<cport>[0-9]+)");
    
    // Does the input match?
    match r.captures(req) {
        Some(caps) => {
            // Yes it does!
            let sport: c_ushort = from_str(caps.name("sport")).unwrap();
            let cport: c_ushort = from_str(caps.name("cport")).unwrap();
            
            let uid = unsafe { lport_to_uid(sport) };
            if uid == -1 {
                println!("Couldn't find user mapped to port: {}.", sport);
                    
                write!(sock, "{}, {} : ERROR : NO-USER", sport, cport);
                return;
            }
                
            let p = unsafe { getpwuid(uid) };
            if p.is_null() {
                println!("Couldn't map uid ({}) to passwd entry.", uid);
                    
                write!(sock, "{}, {} : ERROR : UNKNOWN-ERROR", sport, cport);
                return;
            }
            
            let name = unsafe {
                str::raw::from_c_str((*p).pw_name)
            };
            
            write!(sock, "{}, {} : USERID : UNIX : {}", sport, cport, name);
            
        }
        None => println!("Received badly formatted request.")
    }
}

fn main() {
    let addr = from_str("0.0.0.0:113").unwrap();
    let mut acceptor = match TcpListener::bind(addr).listen() {
        Ok(acceptor) => acceptor,
        Err(e) => fail!("Couldn't listen on {}: {}.", addr, e)
    };
    
    println!("Listening on {}.", addr);
    
    for stream in acceptor.incoming() {
        match stream {
            Ok(s) => spawn(proc() handle_client(s)),
            Err(e) => println!("Couldn't accept connection: {}.", e)
        }
    }
}

getpwuid takes an int and returns a pointer to a passwd struct. Let's recreate that since Rust structs are laid out the same as C structs. We also add another extern block for getpwuid. We don't need to add the link attribute since the C library is linked in by default.

So we make our call to getpwuid and if all went well we have a pointer to a passwd struct. Then we simply use the from_c_str function which gives us a Rust string which we can use to write out the final response.

And there you have it, a working ident server.

Full code

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