Due : Friday, January 15, 1999. Midnight.
The purpose of this assignment is to introduce you to the socket programming API as well as a variety of operating system concepts that are widely used in the construction of networked applications.
Please read this entire handout before you begin work.
where "directory" is the location of the files to be accessed and "port" is the TCP port number that clients will use to locate the server. For example,server directory port
After the server has started, it simply spins in an infinite loop waiting for incoming connections until you decide to kill it. Needless to say, the server isn't too terribly exciting to watch (despite the fact that it is the most interesting part of the project to implement).% server /home/beazley/myfiles 10000 File server listening on classes.cs.uchicago.edu port 10000
To connect to the server, you must write a client program that operates as follows:
where "server" is the hostname of the machine running the server and "port" is the port number. For example:client server port
Once the client has connected to the server, it should allow the user to interactively get a directory listing, download and upload files, and close the connection. For example:% client classes.cs.uchicago.edu 10000 Connected to classes.cs.uchicago.edu Welcome to Dave's file server. client>
Although the project is (hopefully) simple to describe, there are a number of issues that you need to worry about. First, the server must be able to handle multiple client connections (all of which might be active at any given time). Second, the server needs to defend itself from attack and abuse. For example, the server should not allow clients to access forbidden files (such as the system password file), overwrite existing files on uploads, or crash if a client starts misbehaving. In addition, the server needs to properly manage system resources (since the server runs forever, it shouldn't leak memory or file descriptors). Finally, the server should limit the number of simultaneous client connections and automatically disconnnect clients that appear to be idle.Welcome to Dave's file server. client> dir total 229 -rw-rw-r-- 1 beazley beazley 134 Dec 19 17:59 Makefile -rw-rw-r-- 1 beazley beazley 75 Dec 19 17:55 Makefile -rwxrwxr-x 1 beazley beazley 5876 Dec 16 22:59 a.out -rwxrwxr-x 1 beazley beazley 9130 Dec 19 21:28 client -rw-rw-r-- 1 beazley beazley 580 Dec 16 22:59 client.c client> get client.c Received 'client.c' 580 bytes. client> get foobar File not found. client> get /etc/passwd File not found. client> put spam Sent spam, 10672 bytes. client> quit Goodbye! %
Most of the work on this project is focused on the implementation of the server. The client should be relatively easy.
The client and server will communicate with each other by sending packets of data back and forth. The format of these packets is as follows:
Some care needs to be taken with the opcode, block number, and block size parameters. If you were to write down a 16 bit integer value on paper, you would probably write it with the most significant bit on the left and the least significant bit on the right (the same way you write numbers in decimal):
When stored in computer memory however, the ordering of the bits varies depends on the machine being used. For example, the most significant part may appear first like this:(most significant) 1101111100110101 (least significant)
or, it may appear last like this:byte 1 byte 2 (most significant) [11011111] [00110101] (least significant)
The first case, in which the most significant bits appear in the first byte is known as "Big endian." The second case in which the least significant bits appear first is known as "Little endian". In the course of normal programming, you almost never have to worry about this. However, when sending data across a network, it is critical to choose a consistent ordering. If you don't do this, things will break in horrible manner when data is sent between machines with a different byte-ordering schemes (between a PC and a Macintosh for instance).byte 1 byte 2 (least significant) [00110101] [11011111] (most significant)
For this project, the opcode, block number, and block size should all be sent in big-endian format in which the most significant byte is given first and the least significant byte is given last. The following C++ code illustrates how you might convert a 32-bit integer into a big-endian encoding:
unsigned int value; // This is the integer you want to convert unsigned char data[4]; // 4 bytes of storage data[0] = (value >> 24) & 0xff; // Most significant part data[1] = (value >> 16) & 0xff; data[2] = (value >> 8) & 0xff; data[3] = value & 0xff; // Least significant part
The client asks to receive a file from the server (download)If the length of the data being sent in each packet is equal to the packet size (512 bytes), it means that more data will follow. If it is less, it means that the packet is the final one. If any other type of packet is received during transmission, it either means that an error has occurred (you should check for an ERR packet) or that there has been a protocol error of some sort. For this assignment, the connection should be closed if any of the following occur:Client (receiver) Server (sender) ------------------ ----------------- RRQ ---> <--- DAT, block #1 ACK,block #1 ---> <--- DAT, block #2 ACK,block #2 ---> <--- DAT, block #3 ACK,block #3 ---> (etc.)The client asks to send a file to the server (upload)
Client (sender) Server (receiver) ------------------ ----------------- WRQ ---> <--- ACK, block #0 DAT, block #1 ---> <--- ACK, block #1 DAT, block #2 ---> <--- ACK, block #2 DAT, block #3 ---> <--- ACK, block #3 (etc.)
The client asks for a directory from the serverIn this case, clients should simply redirect the incoming directory data to standard output instead of writing it to a file.Client (receiver) Server (sender) ------------------ ----------------- RRQ,filename="" ---> <--- DAT, block #1 ACK,block #1 ---> <--- DAT, block #2 ACK,block #2 ---> <--- DAT, block #3 ACK,block #3 ---> (etc.)
Both the client and server should be prepared to handle error cases. For example:
The client asks to receive a non-existent file from the serverIf possible, the client and server should be able to handle ERR packets without terminating the connection. However, if the communication protocol is violated in any other manner, the server should simply print an error message "protocol error" and close the client connection. For example,Client (receiver) Server (sender) ------------------ ----------------- RRQ ---> <--- ERR,"File not found"
A client protocol errorAlthough you might consider ways to make the server recover from such events, this also may be the work of a malacious client trying to attack the server in some manner. Just to be on the safe side, it is better for the server to drop the connection on the first sign of trouble.Client (receiver) Server (sender) ------------------ ----------------- RRQ ---> <--- DAT, block #1 WRQ ---> Protocol error. Connection closed. Lost connection.
When the server is started. It should attempt to open up a socket on the given port. If successful, it should print a message such as the following :server directory port
If the server is unable to start because the given port is in use (or for any other reason), it should print an appropriate error message and exit. For example:% server /home/beazley/myfiles 10000 File server listening on classes.cs.uchicago.edu port 10000
If you get this message, it usually means that the requested port number is already in use. Pick another port number and try again. You should pick a port number that is greater than 1024 and less than 65535.% server /home/beazley/myfiles 10000 Unable to bind to port 10000. Sorry! %
Note : If you kill the server and immediately restart it using the same port number, you may get this error message. Sometimes it takes a while for the operating system to reclaim port numbers that are no longer in use. If this happens to use, just pick a different port number or wait a little while.
Keep in mind that fork() makes a copy of the running server program and returns two different values. In the process that called fork(), fork() returns a non-zero process id of the child. In the child, fork() returns zero.while(1) { int clientfd = accept( ... ) // Wait for a connection if (!fork()) { // Child process handle_client(clientfd); // Go listen to the client close(clientfd); // Close the client exit(0); } else { // Parent process. Do nothing. } close(clientfd); }
Client connecting to the serverIf the client does not respond with ACK, the server should immediately drop the connection. After the initial packet has been received and acknowledged by the client, the server should start waiting for incoming packets.Client (receiver) Server (sender) ------------------ ----------------- connect ---> fork() <--- DAT,block 1, "Hello from Your name's fileserver" ACK,block 1 ---> print message wait for user wait for packets.
Keep in mind that not all packets are valid at any given time. For example, if the server were to receive a DAT packet without first receiving a WRQ packet, it should report a protocol error and immediately close the client connection. Thus, the client handling loop might look roughly like this:
Likewise, functions to send files, receive files, and transmit directories would be programmed to recognize certain packets and report protocol errors in a similar manner.handle_client() { ... while (1) { get_packet() switch(opcode) { case OP_RRQ: if (strlen(filename) == 0) send_directory(); else send_file(); break; case OP_WRQ: receive_file(); break; case OP_DRQ: send_directory(); break; case OP_ERR: case OP_DAT: case OP_ACK: protocol_error() exit(0); default: unknown_opcode_error() exit(0) } } }
You are free to choose the format and content of the messages. The log will help you debug things.% server /home/beazley/myfiles 10000 File server listening on port 10000 [1910] Received a connection from 127.0.0.1, port 4453 [1912] Received a connection from 127.0.0.1, port 4458 [1914] Received a connection from 127.0.0.1, port 4459 [1916] Received a connection from 127.0.0.1, port 4464 [1918] Received a connection from 127.0.0.1, port 4465 [0] Message. Too many people are connected. Go away! [1910] Client timed out. [1912] Client timed out. [1914] Client timed out. [1916] Client timed out. [1918] Client timed out. [1923] Received a connection from 127.0.0.1, port 4533 [1923] Directory [1923] Sent client.cxx 1939 [1923] Directory [1923] client terminated.
When refusing a connection in this manner, the server should just send an ERR packet to the client and close the connection. For example:% client classes.cs.uchicago.edu 10000 Too many people are connected. Go away! %
Client connecting to a busy serverClient (receiver) Server (sender) ------------------ ----------------- connect ---> <--- ERR, "I'm busy, go away" exit close connection
Note : The implementation of this feature is somewhat tricky so make sure you have the rest of the server working first.
Note : do not implement this feature until you have the rest of the server working.
where hostname is the name of the server machine (classes.cs.uchicago.edu for instance) and port is the port number. The client will need to convert the hostname into an IP address (128.135.20.107) and try to establish a connection. If no connection can be established, an error message should be generated:client hostname port
If a bad hostname is given, the following error should be generated:% client classes.cs.uchicago.edu 10000 Unable to connect with the server. %
If successful, the client should print a connection message, followed by a server identification message (sent by the server), and prompt the user for a command.% client spam.cs.uchicago.edu 10000 Unknown host : spam.cs.uchicago.edu %
At the prompt, the user must be able to type the following commands:% client classes.cs.uchicago.edu 10000 Connected to rustler.cs.uchicago.edu (generated by the client) Welcome to Dave's file server (sent by the server) client>
If the server should terminate the connection (or go down for any reason), the client should report a problem and exit. For example:client> spam Unrecognized command.
client> dir Server not responding. Sorry. %
Although protecting the server is clearly more important, one shouldn't completely ignore security on the client. In particular, the client should not respond to incoming RRQ or WRQ packets that would allow a remote server to download or upload files on the user's machine (as protection against servers written by Microsoft perhaps).
If you are new to Unix programming, I recommend consulting the following books :
A copy of this file is available in /usr/local/classes/current/CS219/projects/project1/./* tftp.h : header files for Project 1 */ #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> #include <sys/stat.h> #include <ctype.h> #include <signal.h> #include <sys/wait.h> #include <netdb.h> /* A few constants */ #define TIMEOUT 60 #define PACKET_SIZE 512 #define MAX_CLIENTS 5
You should be able to type 'make server' or 'make client' to build the server and client respectively.# Makefile for Solaris CC = cc LIBS = -lsocket -lnsl server:: $(CC) tftp.c server.c -o server $(LIBS) client:: $(CC) tftp.c client.c -o client $(LIBS)
A copy of this Makefile is available in /usr/local/classes/current/CS219/projects/project1/.
Note : If using the C++, the name of the compiler is 'CC'.
Note : If using Linux, the LIBS line should be empty.
struct sockaddr_in servaddr; /* Create a socket */ int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { printf("Unable to open socket!\n"); exit(1); } /* Bind it to a specific port number (contained in port) */ memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(port); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(fd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) { printf("unable to bind to port %d\n", port); exit(1); } /* Tell the OS to allow no more than 5 pending connections. Note : This is different from allowing 5 active client connections */ listen(fd,5);
int fd; struct sockaddr_in servaddr; struct hostent *host; /* Create the socket */ fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < < 0) { printf("Unable to create socket!\n"); exit(1); } /* Set its address : port = Server port number (e.g., 10000) hostname = Hostname (e.g., "classes.cs.uchicago.edu") */ memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(port); /* Look up the hostname try to turn it into an IP address */ if ((host = gethostbyname(hostname)) == (NULL)) { printf("Unknown hostname : %s\n", hostname); exit(1); } memcpy(&servaddr.sin_addr,host->h_addr,host->h_length); /* Connect with the server */ if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) { printf("Unable to connect with the server.\n"); exit(1); }
while(1) { /* Server runs forever */ /* Accept a client connection */ struct sockaddr_in clientaddr; int len = sizeof(clientaddr); int clientfd = accept(fd, (struct sockaddr *) &clientaddr, &len); if (clientfd < 0) { if (errno != EINTR) { printf("Accept error!\n"); printf("%s\n",strerror(errno)); exit(1); } } else { /* Fork off the client handling process */ if (!fork()) { /* I am the child */ close(fd); /* Say where the connection came from */ printf("[%d] Received a connection from %s, port %d\n", getpid(), inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); /* Go off and talk to the client for awhile */ handle_client(clientfd); /* Done. Close the client connection and exit. */ close(clientfd); exit(0); /* Child process terminated */ } else { /* I am the parent */ /* Do nothing interesting. Just go back to the top of this loop and keeping listening for more connections */ } close(clientfd); } }
Both functions return the actual number of bytes read or written, or a negative number if an error occurred. If both cases, the vptr parameter is a pointer to the actual data to be sent or a pointer to a buffer where received data is to be placed./* Write n bytes of data to the socket fd */ ssize_t writen(int fd, void *vptr, ssize_t n) { size_t nbytes = n; char *ptr = (char *) vptr; while (nbytes) { ssize_t nwritten = write(fd,ptr,nbytes); if (nwritten <= 0) { if (errno == EINTR) nwritten = 0; else return nwritten; } nbytes -= nwritten; ptr += nwritten; } return (n - nbytes); } /* Read, at most maxlen bytes of data from the socket fd */ ssize_t readn(int fd, void *vptr, ssize_t maxlen) { ssize_t nbytes = maxlen; char *ptr = (char *) vptr; while (nbytes) { ssize_t n = read(fd,ptr,nbytes); if (n < 0) { if (errno == EINTR) n = 0; else return n; } else if (n == 0) { break; } ptr += n; nbytes -= n; } return maxlen-nbytes; }
Now, a word about this funny EINTR business. In most cases, network operations can take an unpredictable amount of time to complete. For example, a client might send data to the server very slowly or become stalled due to network traffic. In such cases, the operating system may choose to interrupt any pending system call (like read(), write(), accept(), etc...) to go do something else. A system call may also be interrupted by a signal. In any case, the EINTR check in the readn() and writen() functions is checking to see if the read() or write() system calls have been interrupted. If so, we simply go back to the top of the loop to re-issue the read() or write() call again. As a result, these functions are guaranteed to read or write exactly n nbytes (unless some sort of unrecoverable network error occurs).
You may wish to write similar read and write functions for handling different parts of the TFTP protocol such as reading/writing 16 bit integers, strings, and so forth. In any event, all of your socket communication should utilize the two functions above.
To create a new file for writing, do the following :int fd = open("filename", O_RDONLY,0); if (fd < 0) { printf("Can't open file.\n"); return; }
When a file is opened or created, an integer file descriptor (fd) is returned. This file descriptor can be used in exactly the same way as a socket. In fact, the readn() and writen() functions previously given can also be used to read or write data from files. For example, the following code opens a file, and reads it in 512 byte blocks until the end of the file is reached:int fd = creat("filename", 0644); if (fd < 0) { printf("Can't create file.\n"); return; }
To close a file, use the close() function.char buffer[512]; int fd = open(filename, O_RDONLY, 0); if (fd < 0) return; int nbytes = readn(fd,buffer,512); while (nbytes > 0) { /* Read nbytes of data. Do something with it */ ... /* read the next chunk of data */ nbytes = readn(fd,buffer,512); } /* Done */
close(fd);
int fd = open(filename, O_RDONLY, 0); ... struct stat fileinfo; if (fstat(fd,&fileinfo) < 0) { close(fd); printf("I/O error.\n"); return; } if (!S_ISREG(fileinfo.st_mode)) { close(fd); printf("Not a regular file. Sorry.\n"); return; } ...
FILE *f = popen("ls -l /your/directory","r"); if (f) { int fd = fileno(f); /* fd is now a file descriptor that you can read just like a normal file */ ... fclose(f); } else { printf("Unable to generate directory.\n"); }
Next, in your server, you will need to tell the operating system about your signal handler by putting the following statement somewhere in the code.void sig_alarm(int signo) { /* A timeout occurred. Deal with it */ ... }
Finally, whenever you want to start the 60-second timer, you will need to execute the following statement.signal(SIGALRM,sig_alarm);
If you re-issue the alarm() function, the timer is reset to whatever new value you provide.alarm(60);
When the alarm expires, the system will interrupt your program and go immediately to the sig_alarm() function. In this case, the alarm should be used to terminate idle client connections.
One way to do it is for the server to keep a simple counter of how many clients have been created with fork(). When accepting a new connection, you simply check the counter to see if its value is too high. If so, an error is returned to the client and the connection is closed. Although simple, the tricky part is figuring out how to decrement the counter when a client handler terminates (and how to do it in a reliable manner).
Another option is to created a pre-forked server. With this approach, the server immediately forks five processes to handle clients when it starts. When new connections are received, they are handled by one of the available client handlers. If all of the client handlers are busy, new incoming connections will simply sit in a pending connection queue (this is handled by the operating system and is set by the value of the listen() function). If even more connections arrive (more than can fit in the queue), the operating system will start refusing incoming connections.
Again, it is up to you to figure out how to do this. Keep in mind that having a working server without this feature is better than no server at all.
While no source code is available for this client, it speaks the protocol described in section 3. You should be able to run it against your server to see if you are at least on the right track. Of course, you still have to write your own client to complete the assignment. Additional testing tools will be made available in due course.
DO NOT WAIT UNTIL THE NIGHT BEFORE THE DUE DATE TO BEGIN WORK!
You must handin the following files:
Everything must be handed in electronically on classes.cs.uchicago.edu. Since grading is based on how your program runs, hard-copies are not accepted. Details about the handin procedure will be given in class.
Please do not hesistate about sending e-mail or coming to see me if you run into any problems.