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.