A socket is a standard way to perform network communication through the OS. A socket can be thought of as an endpoint to a connection, like a socket on an operator's switchboard. But these sockets are just a programmer's abstraction that takes care of all the nitty-gritty details of the OSI model described above. To the programmer, a socket can be used to send or receive data over a network. This data is transmitted at the session layer (5), above the lower layers (handled by the operating system), which take care of routing. There are several different types of sockets that determine the structure of the transport layer (4). The most common types are stream sockets and datagram sockets.
Stream sockets provide reliable two-way communication similar to when you call someone on the phone. One side initiates the connection to the other, and after the connection is established, either side can communicate to the other. In addition, there is immediate confirmation that what you said actually reached its destination. Stream sockets use a standard communication protocol called Transmission Control Protocol (TCP), which exists on the transport layer (4) of the OSI model. On computer networks, data is usually transmitted in chunks called packets. TCP is designed so that the packets of data will arrive without errors and in sequence, like words arriving at the other end in the order they were spoken when you are talking on the telephone. Webservers, mail servers, and their respective client applications all use TCP and stream sockets to communicate.
Another common type of socket is a datagram socket. Communicating with a datagram socket is more like mailing a letter than making a phone call. The connection is one-way only and unreliable. If you mail several letters, you can't be sure that they arrived in the same order, or even that they reached their destination at all. The postal service is pretty reliable; the Internet, however, is not. Datagram sockets use another standard protocol called UDP instead of TCP on the transport layer (4). UDP stands for User Datagram Protocol, implying that it can be used to create custom protocols. This protocol is very basic and lightweight, with few safeguards built into it. It's not a real connection, just a basic method for sending data from one point to another. With datagram sockets, there is very little overhead in the protocol, but the protocol doesn't do much. If your program needs to confirm that a packet was received by the other side, the other side must be coded to send back an acknowledgment packet. In some cases packet loss is acceptable.
Datagram sockets and UDP are commonly used in networked games and streaming media, since developers can tailor their communications exactly as needed without the built-in overhead of TCP.
In C, sockets behave a lot like files since they use file descriptors to identify themselves. Sockets behave so much like files that you can actually use the read()
and write()
functions to receive and send data using socket file descriptors. However, there are several functions specifically designed for dealing with sockets. These functions have their prototypes defined in /usr/include/sys/sockets.h.
socket(int domain, int type, int protocol)
Used to create a new socket, returns a file descriptor for the socket or -1
on error.
connect(int fd, struct sockaddr *remote_host, socklen_t addr_length)
Connects a socket (described by file descriptor fd
) to a remote host. Returns 0
on success and -1
on error.
bind(int fd, struct sockaddr *local_addr, socklen_t addr_length)
Binds a socket to a local address so it can listen for incoming connections. Returns 0
on success and -1
on error.
listen(int fd, int backlog_queue_size)
Listens for incoming connections and queues connection requests up to backlog_queue_size
. Returns 0
on success and -1
on error.
accept(int fd, sockaddr *remote_host, socklen_t *addr_length)
Accepts an incoming connection on a bound socket. The address information from the remote host is written into the remote_host
structure and the actual size of the address structure is written into *addr_length
. This function returns a new socket file descriptor to identify the connected socket or -1
on error.
send(int fd, void *buffer, size_t n
, int flags)
Sends n
bytes from *buffer
to socket fd
; returns the number of bytes sent or -1
on error.
recv(int fd, void *buffer, size_t n
, int flags)
Receives n
bytes from socket fd
into *buffer
; returns the number of bytes received or -1
on error.
When a socket is created with the socket()
function, the domain, type, and protocol of the socket must be specified. The domain refers to the protocol family of the socket. A socket can be used to communicate using a variety of protocols, from the standard Internet protocol used when you browse the Web to amateur radio protocols such as AX.25 (when you are being a gigantic nerd). These protocol families are defined in bits/socket.h, which is automatically included from sys/socket.h.
/* Protocol families. */ #define PF_UNSPEC 0 /* Unspecified. */ #define PF_LOCAL 1 /* Local to host (pipes and file-domain). */ #define PF_UNIX PF_LOCAL /* Old BSD name for PF_LOCAL. */ #define PF_FILE PF_LOCAL /* Another nonstandard name for PF_LOCAL. */ #define PF_INET 2 /* IP protocol family. */ #define PF_AX25 3 /* Amateur Radio AX.25. */ #define PF_IPX 4 /* Novell Internet Protocol. */ #define PF_APPLETALK 5 /* Appletalk DDP. */ #define PF_NETROM 6 /* Amateur radio NetROM. */ #define PF_BRIDGE 7 /* Multiprotocol bridge. */ #define PF_ATMPVC 8 /* ATM PVCs. */ #define PF_X25 9 /* Reserved for X.25 project. */ #define PF_INET6 10 /* IP version 6. */ ...
As mentioned before, there are several types of sockets, although stream sockets and datagram sockets are the most commonly used. The types of sockets are also defined in bits/socket.h. (The /* comments */
in the code above are just another style that comments out everything between the asterisks.)
/* Types of sockets. */ enum __socket_type { SOCK_STREAM = 1, /* Sequenced, reliable, connection-based byte streams. */ #define SOCK_STREAM SOCK_STREAM SOCK_DGRAM = 2, /* Connectionless, unreliable datagrams of fixed maximum length. */ #define SOCK_DGRAM SOCK_DGRAM ...
The final argument for the socket()
function is the protocol, which should almost always be 0
. The specification allows for multiple protocols within a protocol family, so this argument is used to select one of the protocols from the family. In practice, however, most protocol families only have one protocol, which means this should usually be set for 0
; the first and only protocol in the enumeration of the family. This is the case for everything we will do with sockets in this book, so this argument will always be 0
in our examples.
Many of the socket functions reference a sockaddr
structure to pass address information that defines a host. This structure is also defined in bits/socket.h, as shown on the following page.
/* Get the definition of the macro to define the common sockaddr members. */ #include <bits/sockaddr.h> /* Structure describing a generic socket address. */ struct sockaddr { __SOCKADDR_COMMON (sa_); /* Common data: address family and length. */ char sa_data[14]; /* Address data. */ };
The macro for SOCKADDR_COMMON
is defined in the included bits/sockaddr.h file, which basically translates to an unsigned short int. This value defines the address family of the address, and the rest of the structure is saved for address data. Since sockets can communicate using a variety of protocol families, each with their own way of defining endpoint addresses, the definition of an address must also be variable, depending on the address family. The possible address families are also defined in bits/socket.h; they usually translate directly to the corresponding protocol families.
/* Address families. */ #define AF_UNSPEC PF_UNSPEC #define AF_LOCAL PF_LOCAL #define AF_UNIX PF_UNIX #define AF_FILE PF_FILE #define AF_INET PF_INET #define AF_AX25 PF_AX25 #define AF_IPX PF_IPX #define AF_APPLETALK PF_APPLETALK #define AF_NETROM PF_NETROM #define AF_BRIDGE PF_BRIDGE #define AF_ATMPVC PF_ATMPVC #define AF_X25 PF_X25 #define AF_INET6 PF_INET6 ...
Since an address can contain different types of information, depending on the address family, there are several other address structures that contain, in the address data section, common elements from the sockaddr
structure as well as information specific to the address family. These structures are also the same size, so they can be typecast to and from each other. This means that a socket()
function will simply accept a pointer to a sockaddr
structure, which can in fact point to an address structure for IPv4, IPv6, or X.25. This allows the socket functions to operate on a variety of protocols.
In this book we are going to deal with Internet Protocol version 4, which is the protocol family PF_INET
, using the address family AF_INET
. The parallel socket address structure for AF_INET
is defined in the netinet/in.h file.
/* Structure describing an Internet socket address. */ struct sockaddr_in { __SOCKADDR_COMMON (sin_); in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of 'struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; };
The SOCKADDR_COMMON
part at the top of the structure is simply the unsigned short int mentioned above, which is used to define the address family. Since a socket endpoint address consists of an Internet address and a port number, these are the next two values in the structure. The port number is a 16-bit short, while the in_addr
structure used for the Internet address contains a 32-bit number. The rest of the structure is just 8 bytes of padding to fill out the rest of the sockaddr
structure. This space isn't used for anything, but must be saved so the structures can be interchangeably typecast. In the end, the socket address structures end up looking like this:
The port number and IP address used in the AF_INET
socket address structure are expected to follow the network byte ordering, which is big-endian. This is the opposite of x86's little-endian byte ordering, so these values must be converted. There are several functions specifically for these conversions, whose prototypes are defined in the netinet/in.h and arpa/inet.h include files. Here is a summary of these common byte order conversion functions:
htonl
(long value)
Host-to-Network LongConverts a 32-bit integer from the host's byte order to network byte order
htons
(short value)
Host-to-Network ShortConverts a 16-bit integer from the host's byte order to network byte order
ntohl
(long value)
Network-to-Host LongConverts a 32-bit integer from network byte order to the host's byte order
ntohs
(long value)
Network-to-Host ShortConverts a 16-bit integer from network byte order to the host's byte order
For compatibility with all architectures, these conversion functions should still be used even if the host is using a processor with big-endian byte ordering.
When you see 12.110.110.204, you probably recognize this as an Internet address (IP version 4). This familiar dotted-number notation is a common way to specify Internet addresses, and there are functions to convert this notation to and from a 32-bit integer in network byte order. These functions are defined in the arpa/inet.h include file, and the two most useful conversion functions are:
inet_aton(char *ascii_addr, struct in_addr *network_addr)
This function converts an ASCII string containing an IP address in dottednumber format into an in_addr
structure, which, as you remember, only contains a 32-bit integer representing the IP address in network byte order.
inet_ntoa(struct in_addr *network_addr)
This function converts the other way. It is passed a pointer to an in_addr
structure containing an IP address, and the function returns a character pointer to an ASCII string containing the IP address in dotted-number format. This string is held in a statically allocated memory buffer in the function, so it can be accessed until the next call to inet_ntoa()
, when the string will be overwritten.
The best way to show how these functions are used is by example. The following server code listens for TCP connections on port 7890. When a client connects, it sends the message Hello, world! and then receives data until the connection is closed. This is done using socket functions and structures from the include files mentioned earlier, so these files are included at the beginning of the program. A useful memory dump function has been added to hacking.h, which is shown on the following page.
// Dumps raw memory in hex byte and printable split format void dump(const unsigned char *data_buffer, const unsigned int length) { unsigned char byte; unsigned int i, j; for(i=0; i < length; i++) { byte = data_buffer[i]; printf("%02x ", data_buffer[i]); // Display byte in hex. if(((i%16)==15) || (i==length-1)) { for(j=0; j < 15-(i%16); j++) printf(" "); printf("| "); for(j=(i-(i%16)); j <= i; j++) { // Display printable bytes from line. byte = data_buffer[j]; if((byte > 31) && (byte < 127)) // Outside printable char range printf("%c", byte); else printf("."); } printf("\n"); // End of the dump line (each line is 16 bytes) } // End if } // End for }
This function is used to display packet data by the server program. However, since it is also useful in other places, it has been put into hacking.h, instead. The rest of the server program will be explained as you read the source code.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include "hacking.h" #define PORT 7890 // The port users will be connecting to int main(void) { int sockfd, new_sockfd; // Listen on sock_fd, new connection on new_fd struct sockaddr_in host_addr, client_addr; // My address information socklen_t sin_size; int recv_length=1, yes=1; char buffer[1024]; if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) fatal("in socket"); if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) fatal("setting socket option SO_REUSEADDR");
So far, the program sets up a socket using the socket()
function. We want a TCP/IP socket, so the protocol family is PF_INET
for IPv4 and the socket type is SOCK_STREAM
for a stream socket. The final protocol argument is 0
, since there is only one protocol in the PF_INET
protocol family. This function returns a socket file descriptor which is stored in sockfd
.
The setsockopt()
function is simply used to set socket options. This function call sets the SO_REUSEADDR
socket option to true
, which will allow it to reuse a given address for binding. Without this option set, when the program tries to bind to a given port, it will fail if that port is already in use. If a socket isn't closed properly, it may appear to be in use, so this option lets a socket bind to a port (and take over control of it), even if it seems to be in use.
The first argument to this function is the socket (referenced by a file descriptor), the second specifies the level of the option, and the third specifies the option itself. Since SO_REUSEADDR
is a socket-level option, the level is set to SOL_SOCKET
. There are many different socket options defined in /usr/include/ asm/socket.h. The final two arguments are a pointer to the data that the option should be set to and the length of that data. A pointer to data and the length of that data are two arguments that are often used with socket functions. This allows the functions to handle all sorts of data, from single bytes to large data structures. The SO_REUSEADDR
options uses a 32-bit integer for its value, so to set this option to true
, the final two arguments must be a pointer to the integer value of 1
and the size of an integer (which is 4 bytes).
host_addr.sin_family = AF_INET; // Host byte order host_addr.sin_port = htons(PORT); // Short, network byte order host_addr.sin_addr.s_addr = 0; // Automatically fill with my IP. memset(&(host_addr.sin_zero), '\0', 8); // Zero the rest of the struct. if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1) fatal("binding to socket"); if (listen(sockfd, 5) == -1) fatal("listening on socket");
These next few lines set up the host_addr
structure for use in the bind call. The address family is AF_INET
, since we are using IPv4 and the sockaddr_in
structure. The port is set to PORT
, which is defined as 7890
. This short integer value must be converted into network byte order, so the htons()
function is used. The address is set to 0
, which means it will automatically be filled with the host's current IP address. Since the value 0
is the same regardless of byte order, no conversion is necessary.
The bind()
call passes the socket file descriptor, the address structure, and the length of the address structure. This call will bind the socket to the current IP address on port 7890.
The listen()
call tells the socket to listen for incoming connections, and a subsequent accept()
call actually accepts an incoming connection. The listen()
function places all incoming connections into a backlog queue until an accept()
call accepts the connections. The last argument to the listen()
call sets the maximum size for the backlog queue.
while(1) { // Accept loop. sin_size = sizeof(struct sockaddr_in); new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size); if(new_sockfd == -1) fatal("accepting connection"); printf("server: got connection from %s port %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); send(new_sockfd, "Hello, world!\n", 13, 0); recv_length = recv(new_sockfd, &buffer, 1024, 0); while(recv_length > 0) { printf("RECV: %d bytes\n", recv_length); dump(buffer, recv_length); recv_length = recv(new_sockfd, &buffer, 1024, 0); } close(new_sockfd); } return 0; }
Next is a loop that accepts incoming connections. The accept()
function's first two arguments should make sense immediately; the final argument is a pointer to the size of the address structure. This is because the accept()
function will write the connecting client's address information into the address structure and the size of that structure into sin_size
. For our purposes, the size never changes, but to use the function we must obey the calling convention. The accept()
function returns a new socket file descriptor for the accepted connection. This way, the original socket file descriptor can continue to be used for accepting new connections, while the new socket file descriptor is used for communicating with the connected client.
After getting a connection, the program prints out a connection message, using inet_ntoa()
to convert the sin_addr
address structure to a dotted-number IP string and ntohs()
to convert the byte order of the sin_port
number.
The send()
function sends the 13 bytes of the string Hello, world!\n
to the new socket that describes the new connection. The final argument for the send()
and recv()
functions are flags, that for our purposes, will always be 0
.
Next is a loop that receives data from the connection and prints it out. The recv()
function is given a pointer to a buffer and a maximum length to read from the socket. The function writes the data into the buffer passed to it and returns the number of bytes it actually wrote. The loop will continue as long as the recv()
call continues to receive data.
When compiled and run, the program binds to port 7890 of the host and waits for incoming connections:
reader@hacking:~/booksrc $ gcc simple_server.c reader@hacking:~/booksrc $ ./a.out
A telnet client basically works like a generic TCP connection client, so it can be used to connect to the simple server by specifying the target IP address and port.
matrix@euclid:~ $ telnet 192.168.42.248 7890 Trying 192.168.42.248... Connected to 192.168.42.248. Escape character is '^]'. Hello, world! this is a test fjsghau;ehg;ihskjfhasdkfjhaskjvhfdkjhvbkjgf
Upon connection, the server sends the string Hello, world!
, and the rest is the local character echo of me typing this is a test
and a line of keyboard mashing. Since telnet is line-buffered, each of these two lines is sent back to the server when ENTER
is pressed. Back on the server side, the output shows the connection and the packets of data that are sent back.
reader@hacking:~/booksrc $ ./a.out server: got connection from 192.168.42.1 port 56971 RECV: 16 bytes 74 68 69 73 20 69 73 20 61 20 74 65 73 74 0d 0a | This is a test... RECV: 45 bytes 66 6a 73 67 68 61 75 3b 65 68 67 3b 69 68 73 6b | fjsghau;ehg;ihsk 6a 66 68 61 73 64 6b 66 6a 68 61 73 6b 6a 76 68 | jfhasdkfjhaskjvh 66 64 6b 6a 68 76 62 6b 6a 67 66 0d 0a | fdkjhvbkjgf...
The telnet program works well as a client for our server, so there really isn't much reason to write a specialized client. However, there are thousands of different types of servers that accept standard TCP/IP connections. Every time you use a web browser, it makes a connection to a webserver somewhere. This connection transmits the web page over the connection using HTTP, which defines a certain way to request and send information. By default, webservers run on port 80, which is listed along with many other default ports in /etc/services.
finger 79/tcp # Finger finger 79/udp http 80/tcp www www-http # World Wide Web HTTP
HTTP exists in the application layer—the top layer—of the OSI model. At this layer, all of the networking details have already been taken care of by the lower layers, so HTTP uses plaintext for its structure. Many other application layer protocols also use plaintext, such as POP3, SMTP, IMAP, and FTP's control channel. Since these are standard protocols, they are all well documented and easily researched. Once you know the syntax of these various protocols, you can manually talk to other programs that speak the same language. There's no need to be fluent, but knowing a few important phrases will help you when traveling to foreign servers. In the language of HTTP, requests are made using the command GET
, followed by the resource path and the HTTP protocol version. For example, GET / HTTP/1.0
will request the root document from the webserver using HTTP version 1.0. The request is actually for the root directory of /, but most webservers will automatically search for a default HTML document in that directory of index.html. If the server finds the resource, it will respond using HTTP by sending several headers before sending the content. If the command HEAD
is used instead of GET
, it will only return the HTTP headers without the content. These headers are plaintext and can usually provide information about the server. These headers can be retrieved manually using telnet by connecting to port 80 of a known website, then typing HEAD / HTTP/1.0
and pressing ENTER twice. In the output below, telnet is used to open a TCP-IP connection to the webserver at http://www.internic.net. Then the HTTP application layer is manually spoken to request the headers for the main index page.
reader@hacking:~/booksrc $ telnet www.internic.net 80 Trying 208.77.188.101... Connected to www.internic.net. Escape character is '^]'. HEAD / HTTP/1.0 HTTP/1.1 200 OK Date: Fri, 14 Sep 2007 05:34:14 GMT Server: Apache/2.0.52 (CentOS) Accept-Ranges: bytes Content-Length: 6743 Connection: close Content-Type: text/html; charset=UTF-8 Connection closed by foreign host. reader@hacking:~/booksrc $
This reveals that the webserver is Apache version 2.0.52 and even that the host runs CentOS. This can be useful for profiling, so let's write a program that automates this manual process.
The next few programs will be sending and receiving a lot of data. Since the standard socket functions aren't very friendly, let's write some functions to send and receive data. These functions, called send_string()
and recv_line()
, will be added to a new include file called hacking-network.h.
The normal send()
function returns the number of bytes written, which isn't always equal to the number of bytes you tried to send. The send_string()
function accepts a socket and a string pointer as arguments and makes sure the entire string is sent out over the socket. It uses strlen()
to figure out the total length of the string passed to it.
You may have noticed that every packet the simple server received ended with the bytes 0x0D
and 0x0A
. This is how telnet terminates the lines—it sends a carriage return and a newline character. The HTTP protocol also expects lines to be terminated with these two bytes. A quick look at an ASCII table shows that 0x0D
is a carriage return ('\r'
) and 0x0A
is the newline character ('\n'
).
reader@hacking:~/booksrc $ man ascii | egrep "Hex|0A|0D" Reformatting ascii(7), please wait... Oct Dec Hex Char Oct Dec Hex Char 012 10 0A LF '\n' (new line) 112 74 4A J 015 13 0D CR '\r' (carriage ret) 115 77 4D M reader@hacking:~/booksrc $
The recv_line()
function reads entire lines of data. It reads from the socket passed as the first argument into the a buffer that the second argument points to. It continues receiving from the socket until it encounters the last two linetermination bytes in sequence. Then it terminates the string and exits the function. These new functions ensure that all bytes are sent and receive data as lines terminated by '\r\n'
. They are listed below in a new include file called hacking-network.h.
/* This function accepts a socket FD and a ptr to the null terminated * string to send. The function will make sure all the bytes of the * string are sent. Returns 1 on success and 0 on failure. */ int send_string(int sockfd, unsigned char *buffer) { int sent_bytes, bytes_to_send; bytes_to_send = strlen(buffer); while(bytes_to_send > 0) { sent_bytes = send(sockfd, buffer, bytes_to_send, 0); if(sent_bytes == -1) return 0; // Return 0 on send error. bytes_to_send -= sent_bytes; buffer += sent_bytes; } return 1; // Return 1 on success. } /* This function accepts a socket FD and a ptr to a destination * buffer. It will receive from the socket until the EOL byte * sequence in seen. The EOL bytes are read from the socket, but * the destination buffer is terminated before these bytes. * Returns the size of the read line (without EOL bytes). */ int recv_line(int sockfd, unsigned char *dest_buffer) { #define EOL "\r\n" // End-of-line byte sequence #define EOL_SIZE 2 unsigned char *ptr; int eol_matched = 0; ptr = dest_buffer; while(recv(sockfd, ptr, 1, 0) == 1) { // Read a single byte. if(*ptr == EOL[eol_matched]) { // Does this byte match terminator? eol_matched++; if(eol_matched == EOL_SIZE) { // If all bytes match terminator, *(ptr+1-EOL_SIZE) = '\0'; // terminate the string. return strlen(dest_buffer); // Return bytes received } } else { eol_matched = 0; } ptr++; // Increment the pointer to the next byter. } return 0; // Didn't find the end-of-line characters. }
Making a socket connection to a numerical IP address is pretty simple but named addresses are commonly used for convenience. In the manual HTTP HEAD
request, the telnet program automatically does a DNS (Domain Name Service) lookup to determine that www.internic.net translates to the IP address 192.0.34.161. DNS is a protocol that allows an IP address to be looked up by a named address, similar to how a phone number can be looked up in a phone book if you know the name. Naturally, there are socket-related functions and structures specifically for hostname lookups via DNS. These functions and structures are defined in netdb.h. A function called gethostbyname()
takes a pointer to a string containing a named address and returns a pointer to a hostent
structure, or NULL
pointer on error. The hostent
structure is filled with information from the lookup, including the numerical IP address as a 32-bit integer in network byte order. Similar to the inet_ntoa()
function, the memory for this structure is statically allocated in the function. This structure is shown below, as listed in netdb.h.
/* Description of database entry for a single host. */ struct hostent { char *h_name; /* Official name of host. */ char **h_aliases; /* Alias list. */ int h_addrtype; /* Host address type. */ int h_length; /* Length of address. */ char **h_addr_list; /* List of addresses from name server. */ #define h_addr h_addr_list[0] /* Address, for backward compatibility. */ };
The following code demonstrates the use of the gethostbyname()
function.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include "hacking.h" int main(int argc, char *argv[]) { struct hostent *host_info; struct in_addr *address; if(argc < 2) { printf("Usage: %s <hostname>\n", argv[0]); exit(1); } host_info = gethostbyname(argv[1]); if(host_info == NULL) { printf("Couldn't lookup %s\n", argv[1]); } else { address = (struct in_addr *) (host_info->h_addr); printf("%s has address %s\n", argv[1], inet_ntoa(*address)); } }
This program accepts a hostname as its only argument and prints out the IP address. The gethostbyname()
function returns a pointer to a hostent
structure, which contains the IP address in element h_addr
. A pointer to this element is typecast into an in_addr
pointer, which is later dereferenced for the call to inet_ntoa()
, which expects a in_addr
structure as its argument. Sample program output is shown on the following page.
reader@hacking:~/booksrc $ gcc -o host_lookup host_lookup.c reader@hacking:~/booksrc $ ./host_lookup www.internic.net www.internic.net has address 208.77.188.101 reader@hacking:~/booksrc $ ./host_lookup www.google.com www.google.com has address 74.125.19.103 reader@hacking:~/booksrc $
Using socket functions to build on this, creating a webserver identification program isn't that difficult.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include "hacking.h" #include "hacking-network.h" int main(int argc, char *argv[]) { int sockfd; struct hostent *host_info; struct sockaddr_in target_addr; unsigned char buffer[4096]; if(argc < 2) { printf("Usage: %s <hostname>\n", argv[0]); exit(1); } if((host_info = gethostbyname(argv[1])) == NULL) fatal("looking up hostname"); if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) fatal("in socket"); target_addr.sin_family = AF_INET; target_addr.sin_port = htons(80); target_addr.sin_addr = *((struct in_addr *)host_info->h_addr); memset(&(target_addr.sin_zero), '\0', 8); // Zero the rest of the struct. if (connect(sockfd, (struct sockaddr *)&target_addr, sizeof(struct sockaddr)) == -1) fatal("connecting to target server"); send_string(sockfd, "HEAD / HTTP/1.0\r\n\r\n"); while(recv_line(sockfd, buffer)) { if(strncasecmp(buffer, "Server:", 7) == 0) { printf("The web server for %s is %s\n", argv[1], buffer+8); exit(0); } } printf("Server line not found\n"); exit(1); }
Most of this code should make sense to you now. The target_addr
structure's sin_addr
element is filled using the address from the host_info
structure by typecasting and then dereferencing as before (but this time it's done in a single line). The connect()
function is called to connect to port 80 of the target host, the command string is sent, and the program loops reading each line into buffer. The strncasecmp()
function is a string comparison function from strings.h. This function compares the first n bytes of two strings, ignoring capitalization. The first two arguments are pointers to the strings, and the third argument is n, the number of bytes to compare. The function will return 0
if the strings match, so the if
statement is searching for the line that starts with "Server:"
. When it finds it, it removes the first eight bytes and prints the webserver version information. The following listing shows compilation and execution of the program.
reader@hacking:~/booksrc $ gcc -o webserver_id webserver_id.c reader@hacking:~/booksrc $ ./webserver_id www.internic.net The web server for www.internic.net is Apache/2.0.52 (CentOS) reader@hacking:~/booksrc $ ./webserver_id www.microsoft.com The web server for www.microsoft.com is Microsoft-IIS/7.0 reader@hacking:~/booksrc $
A webserver doesn't have to be much more complex than the simple server we created in the previous section. After accepting a TCP-IP connection, the webserver needs to implement further layers of communication using the HTTP protocol.
The server code listed below is nearly identical to the simple server, except that connection handling code is separated into its own function. This function handles HTTP GET
and HEAD
requests that would come from a web browser. The program will look for the requested resource in the local directory called webroot and send it to the browser. If the file can't be found, the server will respond with a 404 HTTP response. You may already be familiar with this response, which means File Not Found. The complete source code listing follows.
#include <stdio.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include "hacking.h" #include "hacking-network.h" #define PORT 80 // The port users will be connecting to #define WEBROOT "./webroot" // The web server's root directory void handle_connection(int, struct sockaddr_in *); // Handle web requests int get_file_size(int); // Returns the filesize of open file descriptor int main(void) { int sockfd, new_sockfd, yes=1; struct sockaddr_in host_addr, client_addr; // My address information socklen_t sin_size; printf("Accepting web requests on port %d\n", PORT); if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) fatal("in socket"); if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) fatal("setting socket option SO_REUSEADDR"); host_addr.sin_family = AF_INET; // Host byte order host_addr.sin_port = htons(PORT); // Short, network byte order host_addr.sin_addr.s_addr = INADDR_ANY; // Automatically fill with my IP. memset(&(host_addr.sin_zero), '\0', 8); // Zero the rest of the struct. if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1) fatal("binding to socket"); if (listen(sockfd, 20) == -1) fatal("listening on socket"); while(1) { // Accept loop. sin_size = sizeof(struct sockaddr_in); new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size); if(new_sockfd == -1) fatal("accepting connection"); handle_connection(new_sockfd, &client_addr); } return 0; } /* This function handles the connection on the passed socket from the * passed client address. The connection is processed as a web request, * and this function replies over the connected socket. Finally, the * passed socket is closed at the end of the function. */ void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr) { unsigned char *ptr, request[500], resource[500]; int fd, length; length = recv_line(sockfd, request); printf("Got request from %s:%d \"%s\"\n", inet_ntoa(client_addr_ptr->sin_addr), ntohs(client_addr_ptr->sin_port), request); ptr = strstr(request, " HTTP/"); // Search for valid-looking request. if(ptr == NULL) { // Then this isn't valid HTTP. printf(" NOT HTTP!\n"); } else { *ptr = 0; // Terminate the buffer at the end of the URL. ptr = NULL; // Set ptr to NULL (used to flag for an invalid request). if(strncmp(request, "GET ", 4) == 0) // GET request ptr = request+4; // ptr is the URL. if(strncmp(request, "HEAD ", 5) == 0) // HEAD request ptr = request+5; // ptr is the URL. if(ptr == NULL) { // Then this is not a recognized request. printf("\tUNKNOWN REQUEST!\n"); } else { // Valid request, with ptr pointing to the resource name if (ptr[strlen(ptr) - 1] == '/') // For resources ending with '/', strcat(ptr, "index.html"); // add 'index.html' to the end. strcpy(resource, WEBROOT); // Begin resource with web root path strcat(resource, ptr); // and join it with resource path. fd = open(resource, O_RDONLY, 0); // Try to open the file. printf("\tOpening \'%s\'\t", resource); if(fd == -1) { // If file is not found printf(" 404 Not Found\n"); send_string(sockfd, "HTTP/1.0 404 NOT FOUND\r\n"); send_string(sockfd, "Server: Tiny webserver\r\n\r\n"); send_string(sockfd, "<html><head><title>404 Not Found</title></head>"); send_string(sockfd, "<body><h1>URL not found</h1></body></html>\r\n"); } else { // Otherwise, serve up the file. printf(" 200 OK\n"); send_string(sockfd, "HTTP/1.0 200 OK\r\n"); send_string(sockfd, "Server: Tiny webserver\r\n\r\n"); if(ptr == request + 4) { // Then this is a GET request if( (length = get_file_size(fd)) == -1) fatal("getting resource file size"); if( (ptr = (unsigned char *) malloc(length)) == NULL) fatal("allocating memory for reading resource"); read(fd, ptr, length); // Read the file into memory. send(sockfd, ptr, length, 0); // Send it to socket. free(ptr); // Free file memory. } close(fd); // Close the file. } // End if block for file found/not found. } // End if block for valid request. } // End if block for valid HTTP. shutdown(sockfd, SHUT_RDWR); // Close the socket gracefully. } /* This function accepts an open file descriptor and returns * the size of the associated file. Returns -1 on failure. */ int get_file_size(int fd) { struct stat stat_struct; if(fstat(fd, &stat_struct) == -1) return -1; return (int) stat_struct.st_size; }
The handle_connection
function uses the strstr()
function to look for the substring HTTP/
in the request buffer. The strstr()
function returns a pointer to the substring, which will be right at the end of the request. The string is terminated here, and the requests HEAD
and GET
are recognized as processable requests. A HEAD
request will just return the headers, while a GET
request will also return the requested resource (if it can be found).
The files index.html and image.jpg have been put into the directory webroot, as shown in the output below, and then the tinyweb program is compiled. Root privileges are needed to bind to any port below 1024, so the program is setuid root and executed. The server's debugging output shows the results of a web browser's request of http://127.0.0.1:
reader@hacking:~/booksrc $ ls -l webroot/ total 52 -rwxr--r-- 1 reader reader 46794 2007-05-28 23:43 image.jpg -rw-r--r-- 1 reader reader 261 2007-05-28 23:42 index.html reader@hacking:~/booksrc $ cat webroot/index.html <html> <head><title>A sample webpage</title></head> <body bgcolor="#000000" text="#ffffffff"> <center> <h1>This is a sample webpage</h1> ...and here is some sample text<br> <br> ..and even a sample image:<br> <img src="image.jpg"><br> </center> </body> </html> reader@hacking:~/booksrc $ gcc -o tinyweb tinyweb.c reader@hacking:~/booksrc $ sudo chown root ./tinyweb reader@hacking:~/booksrc $ sudo chmod u+s ./tinyweb reader@hacking:~/booksrc $ ./tinyweb Accepting web requests on port 80 Got request from 127.0.0.1:52996 "GET / HTTP/1.1" Opening './webroot/index.html' 200 OK Got request from 127.0.0.1:52997 "GET /image.jpg HTTP/1.1" Opening './webroot/image.jpg' 200 OK Got request from 127.0.0.1:52998 "GET /favicon.ico HTTP/1.1" Opening './webroot/favicon.ico' 404 Not Found
The address 127.0.0.1 is a special loopback address that routes to the local machine. The initial request gets index.html from the webserver, which in turn requests image.jpg. In addition, the browser automatically requests favicon.ico in an attempt to retrieve an icon for the web page. The screenshot below shows the results of this request in a browser.