Monday, August 5, 2013

Simple TCP Server in C - Republish

Security Note

If you need to set up a command server, there are a few issues. The first and foremost is security, though in the contexts of this document we will fore go the security issues. In this document we will discuss the ability to set up and create a simple server using unix and C/C++, leaving the discussion of securing a simple command server to a different document.

First, please be aware that running the resulting server as a root account could cause potential damage to the system (via hackers). So, this is only documented as research, with no liabilities held by the authors, consultants, or companies holding this code specifically as such.

Constructing the Program

First, we need to understand how a TCP/IP server even works. When a message is sent from a client to a server, it is encoded with an ID number, or a port as commonly referred to. Some port numbers are associated with a name, such as www (80), telnet (21), ssh (23), and SMTP/Mail (25). These are referred to as "common" ports. We need to identify the best port (preferrably one that is not used in any circumstances). I like to use the port 666 for software like this as it can be a real devil to secure. You will note that, in unix systems, one must be the root user in order to use privileged ports (ports under 1000), just to ensure the safety and well being of the "common" ports.

On the server itself, a piece of software called a daemon is running, and listening for connections that have the specified ID/port. When one comes in, it can then accept these connections, and recieve and transmit data to/from the other end of the connection (client).

An endpoint for TCP/IP communication is called a socket. The header file is called "sys/socket.h" (except in MS Windows where it is "winsock.h"). We will create a socket handle, using the socket function. Rather than opening the socket (a client will do this), bind to the socket, listen for incoming connections, and then accept them.

socket : This creates an endpoint, or a socket, to communicate with a client or a server on the network. It's use is :

int socket(int domain, int type, int protocol);

The integer returned is the handle to our socket.

bind : bind assigns the local port and server address to the socket. When a socket is created with the above socket command, it is classed only as a family (TCP is the AF_INET family, or internet type of socket). This can be used to request that a specific IP address will be listened to, or any IP address on the machine. The code :

int bind(int s, const struct sockaddr *addr, socklen_t addrlen);

listen : listen tells the program that incoming connections will be used. It also allows you to set things like queue limits for connections that clients have requested.

int listen(int s, int queued_connections);

accept : Once a socket has been created, assigned a port, and is listening for connections, you must call accept. Accept will block (meaning that it will wait until a connection request is made).

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

Creating the Source Code

We'll save the topic for creating a Makefile for a completely different discussion, as this can get extremely complex. In the mean time, use the following :
    # Makefile for a simple TCP Server
    
    PROGRAM =tcp_server
    CC      =gcc
    CCF     =-c
    LINKER  =ld
    LINKERF =-lc /usr/lib/crt1.o
    SRCS    =\
            main
    OBJS    =
    CCSRCS  =
    
    .for SRC in $(SRCS)
      OBJS += $(SRC).o
    .endfor
    
    all: compile link
    
    $(SRCS) :
            $(CC) $(CCF) $@.c -o $@.o
    
    compile: $(SRCS)
    
    link:
            $(LINKER) $(LINKERF) $(OBJS) -o $(PROGRAM)
    
    rmproper: clean
    
    clean:
            @rm *.o
            @rm $(PROGRAM)
    
Now, with this Makefile, all we will ever need to be worried about is adding source modules to the SRCS parameter (which already has a tcp and a main). The backslash is an escape method of saying "we're still adding stuff to the variable using the next line of information".

Okay, we need to have a 'main' function. The purpose of a 'main' function is to provide a uniform method for all C programs to be compiled. So, we create our function, and make it call our server code. If you want to add the ability to use command line options as you'll find on most tcp servers, this is the function you would add the code to. But, since all we need to do is start our TCP server, our function will be short :
    int main(void) {
            start_server(); /* start the tcp server */
            return 0;       /* return "success" to the shell */
    }
    
Nice and simple, eh? In fact, if you commented out that start_server(); line, and replace it with a printf("Hello, World!");, and you've got yourself the infamous "Hello, World!" program that is taught as the first step in C Programming.

But that's not what we're after. We are going to assemble a simple TCP command server. So, we leave the start_server(); line in place, and add a function called start_server(). It should take no parameters, because our example program is purely designed to be as simple as can be.

Prior to performing any socket operations, we will need to make sure we've got the right headers. Of we don't, compiling will fail the program. The headers we'll need are :
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <netdb.h>
    
Once you've got those, the following functions should be okay.

Inside of the function, we will need to create and prepare our socket. First, we create the socket handle (which is an integer) by creating an integer variable and assigning to it the result of calling the socket(AF_INET,SOCK_STREAM,getprotobyname("tcp")). The AF_INET declaration is specifically for the Internet, and the SOCK_STREAM number for the type is required for TCP data. We can typecast the return value of the getprotobyname to int just to keep from getting a lot of warning messages. But, here in our example, we'll just use 0 for that.

Once we have the socket handle created and initialized, we then bind the socket address information to our new socket handle. We first fill out the socket information, setting the address to the IP we are going to bind to (INADDR_ANY if we want to open up to any IP address), and also set the socket's family (the AF_INET is used twice, once for the operating system using the connect function call, and once for the socket information itself). We then push these parameters into the socket information using the bind() function, as in :

bind(our_socket_handle,(struct sockaddr *)&socket_info,sizeof(socket_info)).

Now, before proceding, I MUST hit on a key programming factor. ALWAYS check the status of a function if it returns it. The last thing you need to do is try a function call on an already defunct socket handle. So, PLEASE check the status of the bind function.
    int start_server(void) {
     int                  our_socket_handle,status;
     struct sockaddr_in   socket_info;
    
     our_socket_handle = socket(AF_INET,SOCK_STREAM,0);
     if (our_socket_handle == -1) {
      perror("Creating socket()");
      exit(1);
     }
    
     socket_info.sin_family      = AF_INET;              /* Internet TCP/IP              */
     socket_info.sin_addr.s_addr = htonl(INADDR_ANY);    /* listen on any IP address     */
     socket_info.sin_port        = htons(5000);          /* 5000 is an unprivileged port */
    
     status = bind(our_socket_handle,(struct sockaddr *)&socket_info,sizeof(struct sockaddr));
     if (status) {
      fprintf(stderr,"Cannot bind() to the socket");
      exit(1);
     }
    }
    
Now, we've bound the socket's information to it, so we need to listen() and accept() connections. Here's how we do that.

Listen is a straight forward function. All it is designed to do is tell the operating system that we are waiting for connections on the socket. We also include the number of back logged, or queued connections that we will allow to wait for our program (in this example, we'' listen for 5 connections), e.g. :

status = listen(our_socket_handle,5);.

Then we put a loop function around the accept() function. This allows us to retrieve an unlimited number of connections (though still at one at a time). But, since this is a simple tutorial, we will NOT put the accept in a loop. All we will do is retrieve the information that is sent, send it back, and exit. It's primarily a simple procedure to test with.

This accept() function is designed to recieve a connection. It will block until a connection is recieved, then proceed to the next line of code. It initializes a socket structure with the clients connection, and then releases you to work. Don't forget that the return variable, client_socket_handle, needs to be declared in the variables at the top. We also add the client_socket_info structure, and we're ready to implement it. Also, don't forget to close the sockets :
    status = listen(our_socket_handle,5); if (status) { fprintf(stderr,"Cannot listen() to socket"); exit(1); } client_socket_handle = accept(our_socket_handle,(struct sockaddr *)&client_socket_info,sizeof(struct sockaddr)); write(client_socket_handle,"Hello, World!",strlen("Hello, World!")); close(client_socket_handle); close(our_socket_handle);
Now, with this entire program, if you were to run it, it should not give you the command prompt back. Now, open up another telnet window, and go to the server you are running this on, at port 5000, and you should get an immediate close of the connection, but it should have printed the "Hello, World!" string before closing. Pretty snazzy, eh? Using a combination of the send() and recv() functions, you can build an effective communication tool to do nearly anything you need. Effectively, here is our main.c file :
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netdb.h>
    
    int main(void) {
     start_server(); /* start the tcp server */
     return 0; /* return "success" to the shell */
    }
    
    int start_server(void) {
     int                  our_socket_handle,client_socket_handle,socket_size,status;
     struct sockaddr_in   socket_info;
     struct sockaddr_in   client_socket_info;
    
     our_socket_handle = socket(AF_INET,SOCK_STREAM,0); //(int)getprotobyname("tcp"));
     if (our_socket_handle == -1) {
      perror("Cannot create the socket : ");
      exit(1);
     }
    
     socket_info.sin_family = AF_INET;
     socket_info.sin_addr.s_addr = htonl(INADDR_ANY);
     socket_info.sin_port = htons(5000);
    
     status = bind(our_socket_handle,(struct sockaddr*)&socket_info,sizeof(struct sockaddr));
     if (status == -1) {
      perror("Cannot bind() to socket : ");
      exit(1);
     }
    
     status = listen(our_socket_handle,5);
     if (status == -1) {
      fprintf(stderr,"Cannot listen() to socket");
      exit(1);
     }
    
     socket_size = sizeof(struct sockaddr);
     client_socket_handle = accept(our_socket_handle,(struct sockaddr *)&client_socket_info,&socket_size);
     write(client_socket_handle,"HELLO, WORLD!",strlen("HELLO, WORLD!"));
     close(client_socket_handle);
     close(our_socket_handle);
    };
    

No comments:

Post a Comment