CS161 Programming Assignment 2: IMAP Login Server

Chris Erway, Ronald Tse.
{cce, rhtse}@cs.brown.edu
Last updated: Saturday, September 17, 2005

Preliminaries

Questions: cs161tas@cs.brown.edu

Help sessions (held in the fishbowl, 2nd floor):
    Chris - Thursday, 12-2pm
    Ron - Wednesday, 6-8pm

Due date: Monday, October 3rd, 10pm.

1 Introduction

Your second programming task is to implement the start of a simple asynchronous IMAP sever with NMSTL. Your server should provide service to your client that you have already written (which you will want to improve to support LIST).

Before you get hacking away, you'll need to set up your CS account to be able to access the software you'll be using throughout the semester. Please add this line to your shell startup scripts (and don't be shy to ask the TAs if you have problems!):

source /course/cs161/startups/cs161student

The complete protocol description for IMAP version 4, revision 1 is described in RFC 3501 (and a slightly prettier version is provided for your convenience).

For this assignment you'll start the lengthy task of building a full fledged IMAP server by building a "login server". You will implement thes commands from your login client (LOGIN, LOGOUT, CAPABILITY, and NOOP), plus the LIST command which reports the existence of mail folders. The RFC is the best place to learn how these commands (especially LIST) are intended to operate.

Your server will interact with the filesystem in two ways. For LOGIN, you will read a a password file. For LIST, you will read a directory's contents. Your server should avoid blocking. It should use subprocesses, but handle an arbitrary number of clients without starting a process per connection. For now, you should not cache any filesystem interactions. Each LOGIN or LIST command should reexamine the password file or the user's mailbox.

2 Assignment Specification

In the last assignment you wrote the began client side of IMAP, and now you'll begin the server. This time you'll use C++ and the NMSTL library by Jon Salz.

Here are the features we'd like to see in your server:

  1. The main loop of your server should not block on I/O. (We discussed in class why it may be impossible to guarantee this, but you should try. Document any assumptions you make.)
  2. Listen on a specific port with command line switch -p port, so that you can still start them on department machines without root privileges.
  3. Accept logins according to the contents of a password file specified with a command line switch: -f file. The file will be line oriented, with one username and password specified per line, separated by a colon. For example:
  4. jj:pssw0rd
    joe:eoj
    clyde:inky
    
  5. Eventually, you will be implementing maildir for message storage. For now, in order to implement LIST, you need only know that a mailbox will be represented by a directory, and "/" will be the hierarchy separator. You should accept a command line switch -m directory in which user mailboxes will be found. Given -m /var/spool/mail, and user joe, you will consider the subdirectories of /var/spool/mail/joe to be mailboxes. You will not return any name attributes in your LIST responses.
  6. You must not start an individual thread or subprocess for every connection. (You may use a limited number of threads or subprocesses, but the number should not grow with the number of connections.)
  7. Handle load gracefully. Your server should handle resource limits reasonably. File descriptors should be freed correctly. You should limit your server to using a maximum number of file descriptors (an option -n #numfd).
  8. Your server should be able to handle as many simultaneous requests as possible under these constraints, and gracefully denu service to extra connection attempts.
  9. Never hang or crash on bad/malicious input.
  10. All errors (eg. bad input, cannot read file etc.) should be reported to the client, and should provide a reasonable and understandable error message.

3 Testing

You can use your java login client to test your daemon.

Consider the use of ulimit to test that you don't acquire too many file descriptors.

4 NMSTL Basics

For this assignment, you will be using the networking, messaging and threading library NMSTL. This library provides a simple API to be used in order to implement asynchronous I/O. Here are some the highlights:

io_event_loop:

io_event_loop is responsible for making all the callbacks on your asynchronous I/O handling objects. You should construct one of these for use throughout your program, and pass it to all instances of you handler subclasses.

Methods of note:

void io_event_loop::run()

run begins the event loop. Call this once you've done all appropriate initialization.

term_handler:

term_handler creates a terminal on stdin through which you can enter commands. You may find this useful for testing and debugging.

Methods of note:

void on_eof (callback <void>quit_cb)

on_eof registers a callback that will be made when the term receives EOF (i.e. user enters ctrl+d). This can be used for cleanup.

template<class CallbackType>void
add_command (string name, unsigned int min_args, unsigned int max_args, CallbackType cb)

add_command adds a command to the terminal. Pass it the name of the command, the minimum number of arguments, maximum number of arguments, and the callback to be made when the user enters the command in the terminal. Refer to the online documentation for exact callback format.

constbuf / databuf / dynbuf:

These classes are for constant, mutable, and dynamically allocated buffers respectively. These classes will be used throughout NMSTL, refer to online documentation for more details.

io_handler:

this is the io handling superclass on which callbacks are made by the io_event_loop. This class takes an iohandle and io_event_loop, and if appropriate flags are set, it will make callbacks when the iohandle is readable or writable.

Methods of note:

virtual void ravail()
Called when the iohandle is readable. Override this if you want to specify your own behavior when the IO handle becomes readable.
virtual void wavail()
Called when the iohandle is writeable. Override this if you want to specify your own behavior when the IO handle becomes writeable.
void want_read(bool)
This method is called with true if you want the ravail callback to be made and false if you wish to disable this callback.
void want_write(bool)
This method is called with true if you want the wavail callback to be made and false if you wish to disable this callback.

net_handler:

This an io_handler subclass responsible for performing asynchronous IO on a stream based connection. Since you will be writing a logind server that implements a simple stream based protocol, this class should be of extreme interest to you. In addition to the methods inherited from io_handler, there are serveral other methods of note.

Methods of note:

virtual void connected(status stat)

This callback is invoked when the connection either succeeds or fails. However, since you will be using a tcp_acceptor (see below), this method should be less important to you.

virtual int incoming_data (constbuf buf)=0

This callback is invoked when the socket to which the net_handler is bound has incoming data. The return value of this method indicates the number of bytes that have been consumed by you object, and can thus be safely discarded from the passed buffer.

virtual void end_data (constbuf buf)

This callback is made when the connection is closed by the peer. Any data remaining in the stream is contained in the passed constbuf.

bool write (string s) / bool write(constbuf buf)

These methods write the passed string/buf to the connection, and buffer any data that has not been sent. The remaining data is sent as the connection becomes readable again. This method allows you to queue data for an asynchronous send. Unfortunately, for these method to function properly, you cannot overwrite wavail, and (to my knowledge) you do not receive a callback when your write has completed.

tcp_acceptor<class_type arg_type>

This helper class sits on a socket waiting for connections. When one is recieved, it accepts this connection, and passes the socket and the io_event_loop to a new instance of the object specified as a template parameter. However, it is your responsibility to ensure that the socket specified in the constructor is valid, this object will not inform you. You specify the type of object to create, along with the type of any argument you wish passed (though this may be omitted). Upon connect, the tcp_acceptor will create a new instance using the constructor new T(io_loop, socket, arg) or new T(io_loop, socket) if no argument is specified. You will probably use an instance of this class to sit on top of your login port.

Methods of note:

tcp_acceptor (io_event_loop &_loop, in_port_t localport, Arg arg)

The constructor of the object. If you wish an argument to be passed into your object's constructor upon the initiation of a new connection, you specify this argument with arg.

callback<R, T1, T2, etc>

This is class to wrap a function callback. The method signature of the callback is specified in the template parameters where R is the return type, and T1 is the first argument, T2 is the second, etc. For a more detailed treatment of the callback class, refer to the online documentation and sample applications.

ptr <T>

A smart pointer implementation. Basically a reference counting pointer that will delete the underlying object when no references to it remain. For more discussion and a sample application, refer to the online documentation.

Online documentation links

NMSTL main page
Doxygen pages
Sample applications

5 Support Code

We have provided some basic structure to get you started (including a Makefile), and a barebones proc_handler class in proc.h and proc.cc that inherits from net_handler to provide a buffered interface to subprocesses (that you may use like the subprocesses of Flash). You should not be at all shy about modifying and extending this code. It's just something to get started with. It is available in:
    /course/cs161/asgn/logind

6 Submitting your code

Please include a README file explaining briefly which files contain what code, as well as how to run your code. To submit your assignment, place your source files in a directory, and run the following script while in that directory:

/course/cs161/bin/cs161_handin logind

This will cause all the files in the current directory to be submitted. Please contact the TAs if you have problems submitting. For your own sanity, don't leave your submission until the minute before the deadline.

7 Grading scheme

We will be testing your daemon against our custom client that tries to break your deamon. Your code will be graded on the following factors (and their weights).

Documentation and code legibility: 20%
Functionality (non-blocking, error reporting, etc): 50%
Robustness (malicious input, many connections, etc): 30%